From d206cf1415006f36ddea7fcacbd77c5c8f5e5dc9 Mon Sep 17 00:00:00 2001
From: DarkGod <darkgod@net-core.org>
Date: Tue, 18 Apr 2017 19:12:20 +0200
Subject: [PATCH] MapScript generator modularizing stuff and making Tilemap the
 basis for mapscripts

---
 .../map/{FromCustom.lua => MapScript.lua}     |  18 +-
 .../Tilemap.lua}                              | 239 ++++++++++--------
 .../engine/tilemaps/WaveFunctionCollapse.lua  |  94 +++++++
 game/modules/tome/class/Game.lua              |   6 +-
 game/modules/tome/data/zones/test/grids.lua   |   1 +
 .../zones/test/mapscripts/inner_outer.lua     |  72 ++++++
 .../tome/data/zones/test/samples/wfctest.tmx  |  14 +-
 .../tome/data/zones/test/samples/wfctest2.tmx |   2 +-
 .../tome/data/zones/test/samples/wfctest4.tmx |  13 +-
 game/modules/tome/data/zones/test/zone.lua    |  34 +--
 10 files changed, 353 insertions(+), 140 deletions(-)
 rename game/engines/default/engine/generator/map/{FromCustom.lua => MapScript.lua} (86%)
 rename game/engines/default/engine/{WaveFunctionCollapse.lua => tilemaps/Tilemap.lua} (59%)
 create mode 100644 game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua
 create mode 100644 game/modules/tome/data/zones/test/mapscripts/inner_outer.lua

diff --git a/game/engines/default/engine/generator/map/FromCustom.lua b/game/engines/default/engine/generator/map/MapScript.lua
similarity index 86%
rename from game/engines/default/engine/generator/map/FromCustom.lua
rename to game/engines/default/engine/generator/map/MapScript.lua
index 4ab6f89fcc..5dbb2e1a0a 100644
--- a/game/engines/default/engine/generator/map/FromCustom.lua
+++ b/game/engines/default/engine/generator/map/MapScript.lua
@@ -30,7 +30,7 @@ function _M:init(zone, map, level, data)
 	self.data = data
 	self.grid_list = zone.grid_list
 	self.spots = {}
-	self.mapsize = {self.map.w, self.map.h}
+	self.mapsize = {self.map.w, self.map.h, w=self.map.w, h=self.map.h}
 
 	RoomsLoader.init(self, data)
 end
@@ -55,7 +55,21 @@ function _M:regenerate()
 end
 
 function _M:custom(lev, old_lev)
-	if self.data.custom then return self.data.custom(self, lev, old_lev) end
+	if self.data.mapscript then
+		local file = self:getFile(self.data.mapscript..".lua", "mapscripts")
+		local f, err = loadfile(file)
+		if not f and err then error(err) end
+		setfenv(f, setmetatable(env or {
+			self = self,
+			lev = lev,
+			old_lev = old_lev,
+			Tilemap = require "engine.tilemaps.Tilemap",
+		}, {__index=_G}))
+		return f()
+	end
+	if self.data.custom then
+		return self.data.custom(self, lev, old_lev)
+	end
 	error("Generator FromCustom called without customization!")
 end
 
diff --git a/game/engines/default/engine/WaveFunctionCollapse.lua b/game/engines/default/engine/tilemaps/Tilemap.lua
similarity index 59%
rename from game/engines/default/engine/WaveFunctionCollapse.lua
rename to game/engines/default/engine/tilemaps/Tilemap.lua
index 72da351aed..b9432f076f 100644
--- a/game/engines/default/engine/WaveFunctionCollapse.lua
+++ b/game/engines/default/engine/tilemaps/Tilemap.lua
@@ -21,28 +21,41 @@ require "engine.class"
 local lom = require "lxp.lom"
 local mime = require "mime"
 
---- Generate map-like data from samples using the WaveFunctionCollapse algorithm (in C++)
--- @classmod engine.WaveFunctionCollapse
+--- Base class to generate map-like
+-- @classmod engine.tilemaps.Tilemap
 module(..., package.seeall, class.make)
 
---- Run the algorithm
--- It will produce internal results which this class can then manipulate
--- If async is true in the parameters the generator will run in an asynchronous thread and you must call :waitCompute()
--- before using the results, this allows you to run multiple WFCs for later merging wihtout taking more time (if the user have enoguh CPUs obviously)
-function _M:init(t)
-	assert(t.mode == "overlapping", "bad WaveFunctionCollapse mode")
-	assert(t.size, "WaveFunctionCollapse has no size")
-	self.data_w = t.size[1]
-	self.data_h = t.size[2]
-	if t.mode == "overlapping" then
-		if type(t.sample) == "string" then
-			t.sample = self:tmxLoad(t.sample)
+function _M:init(size, fill_with)
+	if size then
+		self.data_w = size[1]
+		self.data_h = size[2]
+		if self.data_w and self.data_h then
+			self.data = {}
+			for y = 1, self.data_h do
+				self.data[y] = {}
+				for x = 1, self.data_w do
+					self.data[y][x] = fill_with or ' '
+				end
+			end
 		end
-		self:run(t)
 	end
 end
 
---- Used internally to load a sample from a tmx file
+--- Find all empty spaces (defaults to ' ') and fill them with a give char
+function _M:fillAll(fill_with, empty_char)
+	if not self.data then return end
+	empty_char = empty_char or ' '
+	fill_with = fill_with or '#'
+	for y = 1, self.data_h do
+		for x = 1, self.data_w do
+			if self.data[y][x] == empty_char then
+				self.data[y][x] = fill_with
+			end
+		end
+	end
+end
+
+--- Used internally to load a tilemap from a tmx file
 function _M:tmxLoad(file)
 	local f = fs.open(file, "r") local data = f:read(10485760) f:close()
 	local map = lom.parse(data)
@@ -118,36 +131,14 @@ function _M:tmxLoad(file)
 			end
 		end
 	end
-	for y = 1, h do	data[y] = table.concat(data[y]) end
-	return data
-end
-
---- Used internally to parse the results
-function _M:parseResult(data)
-	if not data then return end
-	self.data = {}
-	for y = 1, #data do
-		local x = 1
-		self.data[y] = {}
-		for c in data[y]:gmatch('.') do
-			self.data[y][x] = c
-			x = x + 1
-		end
-	end
+	return data, w, h
 end
 
---- Called by the constructor to actaully start doing stuff
-function _M:run(t)
-	print("[WaveFunctionCollapse] running with parameter table:")
-	table.print(t)
-	if not t.async then
-		local data = core.generator.wfc.overlapping(t.sample, t.size[1], t.size[2], t.n, t.symmetry, t.periodic_out, t.periodic_in, t.has_foundation)
-		self:parseResult(data)
-		return false
-	else
-		self.async_data = core.generator.wfc.asyncOverlapping(t.sample, t.size[1], t.size[2], t.n, t.symmetry, t.periodic_out, t.periodic_in, t.has_foundation)
-		return true
-	end
+function _M:collapseToLineFormat(data)
+	if not data then return nil end
+	local ndata = {}
+	for y = 1, #data do ndata[y] = table.concat(data[y]) end
+	return ndata
 end
 
 --- Do we have results, or did we fail?
@@ -155,31 +146,9 @@ function _M:hasResult()
 	return self.data and true or false
 end
 
---- Wait for computation to finish if in async mode, if not it just returns immediately
-function _M:waitCompute()
-	if not self.async_data then return end
-	local data = self.async_data:wait()
-	self.async_data = nil
-
-	self:parseResult(data)
-	return self:hasResult()
-end
-
---- Wait for multiple WaveFunctionCollapse at once
--- Static
-function _M:waitAll(...)
-	local all_have_data = true
-	for _, wfcasync in ipairs{...} do
-		wfcasync:waitCompute()
-		if not wfcasync:hasResult() then all_have_data = false end -- We cant break, we need to wait all the threads to not leave them dangling in the wind
-	end
-	return all_have_data
-end
-
---- Return a list of groups of tiles representing each of the connected areas
-function _M:findFloodfillGroups(wall)
-	if type(wall) == "table" then local first = wall[1] wall = table.reverse(wall) wall.__first = first
-	else wall = {[wall] = true, __first=wall} end
+--- Return a list of groups of tiles that matches the given cond function
+function _M:findGroups(cond)
+	if not self.data then return {} end
 
 	local fills = {}
 	local opens = {}
@@ -187,7 +156,7 @@ function _M:findFloodfillGroups(wall)
 	for i = 1, self.data_w do
 		opens[i] = {}
 		for j = 1, self.data_h do
-			if not wall[self.data[j][i]] then
+			if cond(self.data[j][i]) then
 				opens[i][j] = #list+1
 				list[#list+1] = {x=i, y=j}
 			end
@@ -226,28 +195,40 @@ function _M:findFloodfillGroups(wall)
 		local i, l = next(list)
 		local closed = floodFill(l.x, l.y)
 		groups[#groups+1] = {id=id, list=closed}
-		print("[WaveFunctionCollapse] Floodfill group", i, #closed)
+		print("[Tilemap] Floodfill group", i, #closed)
 	end
 
-	return groups, wall
+	return groups
+end
+
+--- Return a list of groups of tiles representing each of the connected areas
+function _M:findGroupsNotOf(wall)
+	wall = table.reverse(wall)
+	return self:findGroups(function(c) return not wall[c] end)
 end
 
---- Find groups by floodfill and apply a custom function over them
+--- Return a list of groups of tiles representing each of the connected areas
+function _M:findGroupsOf(floor)
+	floor = table.reverse(floor)
+	return self:findGroups(function(c) return floor[c] end)
+end
+
+--- Apply a custom method over the given groups, sorting them from bigger to smaller
 -- It gives the groups in order of bigger to smaller
-function _M:applyOnFloodfillGroups(wall, fct)
+function _M:applyOnGroups(groups, fct)
 	if not self.data then return end
-	local groups = self:findFloodfillGroups(wall)
 	table.sort(groups, function(a,b) return #a.list > #b.list end)
-	for _, group in ipairs(groups) do
-		fct(self.data_w, self.data_h, self.data, group)
+	for id, group in ipairs(groups) do
+		fct(self.data_w, self.data_h, self.data, group, id)
 	end
 end
 
 --- Given a list of groups, eliminate them all
 function _M:eliminateGroups(wall, groups)
-	print("[WaveFunctionCollapse] Eleminating groups", #groups)
+	if not self.data then return end
+	print("[Tilemap] Eleminating groups", #groups)
 	for i = 1, #groups do
-		print("[WaveFunctionCollapse] Eleminating group "..i.." of", #groups[i].list)
+		print("[Tilemap] Eleminating group "..i.." of", #groups[i].list)
 		for j = 1, #groups[i].list do
 			local jn = groups[i].list[j]
 			self.data[jn.y][jn.x] = wall
@@ -256,12 +237,13 @@ function _M:eliminateGroups(wall, groups)
 end
 
 --- Simply destroy all connected groups except the biggest one
-function _M:eliminateByFloodfill(wall)
-	local groups, wall = self:findFloodfillGroups(wall)
+function _M:eliminateByFloodfill(walls)
+	if not self.data then return 0 end
+	local groups = self:findGroupsNotOf(walls)
 
 	-- If nothing exists, regen
 	if #groups == 0 then
-		print("[WaveFunctionCollapse] Floodfill found nothing")
+		print("[Tilemap] Floodfill found nothing")
 		return 0
 	end
 
@@ -269,20 +251,64 @@ function _M:eliminateByFloodfill(wall)
 	table.sort(groups, function(a,b) return #a.list < #b.list end)
 	local g = table.remove(groups)
 	if g and #g.list > 0 then
-		print("[WaveFunctionCollapse] Ok floodfill with main group size", #g.list)
-		self:eliminateGroups(wall.__first, groups)
+		print("[Tilemap] Ok floodfill with main group size", #g.list)
+		self:eliminateGroups(walls[1], groups)
 		return #g.list
 	else
-		print("[WaveFunctionCollapse] Floodfill left nothing")
+		print("[Tilemap] Floodfill left nothing")
 		return 0
 	end
 end
 
+function _M:isInGroup(group, x, y)
+	if not group.reverse then
+		group.reverse = {}
+		for j = 1, #group.list do
+			local jn = group.list[j]
+			group.reverse[jn.x] = group.reverse[jn.x] or {}
+			group.reverse[jn.x][jn.y] = true
+		end
+	end
+	return group.reverse[x] and group.reverse[x][y]
+end
+
+--- Find the biggest rectangle that can fit fully in the given group
+function _M:groupInnerRectangle(group)
+	
+end
+
+--- Find the smallest rectangle that can fit around in the given group
+function _M:groupOuterRectangle(group)
+	local n = group.list[1]
+	if not n then return end -- wtf?
+	local x1, x2 = n.x, n.x
+	local y1, y2 = n.y, n.y
+
+	for j = 1, #group.list do
+		local jn = group.list[j]
+		if jn.x < x1 then x1 = jn.x end
+		if jn.x > x2 then x2 = jn.x end
+		if jn.y < y1 then y1 = jn.y end
+		if jn.y > y2 then y2 = jn.y end
+	end
+
+	-- Debug
+	-- for i = x1, x2 do for j = y1, y2 do
+	-- 	if not self:isInGroup(group, i, j) then
+	-- 		if self.data[j][i] == '#' then
+	-- 			self.data[j][i] = 'T'
+	-- 		end
+	-- 	end
+	-- end end
+
+	return {x1=x1, y1=y1, x2=x2, y2=y2}
+end
+
 --- Get the results
 -- @param is_array if true returns a table[][] of characters, if false a table[] of string lines
 function _M:getResult(is_array)
-	if is_array then return self.data end
 	if not self.data then return nil end
+	if is_array then return self.data end
 	local data = {}
 	for y = 1, self.data_h do data[y] = table.concat(self.data[y]) end
 	return data
@@ -290,8 +316,13 @@ end
 
 --- Debug function to print the result to the log
 function _M:printResult()
+	if not self.data then
+		print("-------------")
+		print("------------- Tilemap result")		
+		return
+	end
 	print("-------------")
-	print("------------- WaveFunctionCollapse result")
+	print("------------- Tilemap result")
 	print("-----------[[")
 	for _, line in ipairs(self:getResult()) do
 		print(line)
@@ -301,22 +332,28 @@ function _M:printResult()
 	print("-------------")
 end
 
---- Merge and other WaveFunctionCollapse's data
-function _M:merge(wfc, empty_char, char_order)
+--- Merge and other Tilemap's data
+function _M:merge(x, y, tm, char_order, empty_char)
+	if not self.data or not tm.data then return end
+	x = math.floor(x)
+	y = math.floor(y)
 	char_order = table.reverse(char_order or {})
 	empty_char = empty_char or ' '
-	if not wfc.data then return end
-
-	for i = 1, math.min(self.data_w, wfc.data_w) do
-		for j = 1, math.min(self.data_h, wfc.data_h) do
-			local c = wfc.data[j][i]
-			if c ~= empty_char then
-				local sc = self.data[j][i]
-				local sc_o = char_order[sc] or 0
-				local c_o = char_order[c] or 0
-
-				if c_o >= sc_o then
-					self.data[j][i] = wfc.data[j][i]
+	if not tm.data then return end
+
+	for i = 1, tm.data_w do
+		for j = 1, tm.data_h do
+			local si, sj = i + x - 1, j + y - 1
+			if si >= 1 and si <= self.data_w and sj >= 1 and sj <= self.data_h then
+				local c = tm.data[j][i]
+				if c ~= empty_char then
+					local sc = self.data[sj][si]
+					local sc_o = char_order[sc] or 0
+					local c_o = char_order[c] or 0
+
+					if c_o >= sc_o then
+						self.data[sj][si] = tm.data[j][i]
+					end
 				end
 			end
 		end
diff --git a/game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua b/game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua
new file mode 100644
index 0000000000..0b65a151fd
--- /dev/null
+++ b/game/engines/default/engine/tilemaps/WaveFunctionCollapse.lua
@@ -0,0 +1,94 @@
+-- TE4 - T-Engine 4
+-- Copyright (C) 2009 - 2017 Nicolas Casalini
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
+--
+-- Nicolas Casalini "DarkGod"
+-- darkgod@te4.org
+
+require "engine.class"
+local lom = require "lxp.lom"
+local mime = require "mime"
+local Tilemap = require "engine.tilemaps.Tilemap"
+
+--- Generate map-like data from samples using the WaveFunctionCollapse algorithm (in C++)
+-- @classmod engine.tilemaps.WaveFunctionCollapse
+module(..., package.seeall, class.inherit(Tilemap))
+
+--- Run the algorithm
+-- It will produce internal results which this class can then manipulate
+-- If async is true in the parameters the generator will run in an asynchronous thread and you must call :waitCompute()
+-- before using the results, this allows you to run multiple WFCs for later merging wihtout taking more time (if the user have enoguh CPUs obviously)
+function _M:init(t)
+	assert(t.mode == "overlapping", "bad WaveFunctionCollapse mode")
+	assert(t.size, "WaveFunctionCollapse has no size")
+	self.data_w = t.size[1]
+	self.data_h = t.size[2]
+	if t.mode == "overlapping" then
+		if type(t.sample) == "string" then
+			t.sample = self:collapseToLineFormat(self:tmxLoad(t.sample))
+		end
+		self:run(t)
+	end
+end
+
+--- Used internally to parse the results
+function _M:parseResult(data)
+	print("[WaveFunctionCollapse] compute done", data)
+	if not data then return end
+	self.data = {}
+	for y = 1, #data do
+		local x = 1
+		self.data[y] = {}
+		for c in data[y]:gmatch('.') do
+			self.data[y][x] = c
+			x = x + 1
+		end
+	end
+end
+
+--- Called by the constructor to actaully start doing stuff
+function _M:run(t)
+	print("[WaveFunctionCollapse] running with parameter table:")
+	table.print(t)
+	if not t.async then
+		local data = core.generator.wfc.overlapping(t.sample, t.size[1], t.size[2], t.n, t.symmetry, t.periodic_out, t.periodic_in, t.has_foundation)
+		self:parseResult(data)
+		return false
+	else
+		self.async_data = core.generator.wfc.asyncOverlapping(t.sample, t.size[1], t.size[2], t.n, t.symmetry, t.periodic_out, t.periodic_in, t.has_foundation)
+		return true
+	end
+end
+
+--- Wait for computation to finish if in async mode, if not it just returns immediately
+function _M:waitCompute()
+	if not self.async_data then return end
+	local data = self.async_data:wait()
+	self.async_data = nil
+
+	self:parseResult(data)	
+	return self:hasResult()
+end
+
+--- Wait for multiple WaveFunctionCollapse at once
+-- Static
+function _M:waitAll(...)
+	local all_have_data = true
+	for _, wfcasync in ipairs{...} do
+		wfcasync:waitCompute()
+		if not wfcasync:hasResult() then all_have_data = false end -- We cant break, we need to wait all the threads to not leave them dangling in the wind
+	end
+	return all_have_data
+end
diff --git a/game/modules/tome/class/Game.lua b/game/modules/tome/class/Game.lua
index 76840edbd6..6640730f5e 100644
--- a/game/modules/tome/class/Game.lua
+++ b/game/modules/tome/class/Game.lua
@@ -1845,7 +1845,11 @@ function _M:setupCommands()
 			print("===============")
 		end end,
 		[{"_g","ctrl"}] = function() if config.settings.cheat then
-			self:changeLevel(game.level.level + 1)
+			if self.zone.short_name ~= "test" then
+				self:changeLevel(1, "test")
+			else
+				self:changeLevel(game.level.level + 1)
+			end
 do return end
 			local m = game.zone:makeEntity(game.level, "actor", {name="elven mage"}, nil, true)
 			local x, y = util.findFreeGrid(game.player.x, game.player.y, 20, true, {[Map.ACTOR]=true})
diff --git a/game/modules/tome/data/zones/test/grids.lua b/game/modules/tome/data/zones/test/grids.lua
index e1d255e1f3..f332848dbb 100644
--- a/game/modules/tome/data/zones/test/grids.lua
+++ b/game/modules/tome/data/zones/test/grids.lua
@@ -20,3 +20,4 @@
 load("/data/general/grids/basic.lua")
 load("/data/general/grids/forest.lua")
 load("/data/general/grids/water.lua")
+load("/data/general/grids/jungle_hut.lua")
diff --git a/game/modules/tome/data/zones/test/mapscripts/inner_outer.lua b/game/modules/tome/data/zones/test/mapscripts/inner_outer.lua
new file mode 100644
index 0000000000..09963d311f
--- /dev/null
+++ b/game/modules/tome/data/zones/test/mapscripts/inner_outer.lua
@@ -0,0 +1,72 @@
+-- ToME - Tales of Maj'Eyal
+-- Copyright (C) 2009 - 2017 Nicolas Casalini
+--
+-- This program is free software: you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation, either version 3 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License
+-- along with this program.  If not, see <http://www.gnu.org/licenses/>.
+--
+-- Nicolas Casalini "DarkGod"
+-- darkgod@te4.org
+
+local Tilemap = require "engine.tilemaps.Tilemap"
+local WaveFunctionCollapse = require "engine.tilemaps.WaveFunctionCollapse"
+local merge_order = {'.', '_', 'r', '+', '#', 'O', '=', ';', 'T'}
+
+-- Water & trees layer
+local wfcwater = WaveFunctionCollapse.new{
+	mode="overlapping", async=true,
+	sample=self:getFile("!wfctest2.tmx", "samples"),
+	size={self.mapsize.w/3, self.mapsize.h},
+	n=3, symmetry=8, periodic_out=true, periodic_in=true, has_foundation=false
+}
+-- Outer buildings
+local wfcouter = WaveFunctionCollapse.new{
+	mode="overlapping", async=true,
+	sample=self:getFile("!wfctest.tmx", "samples"),
+	size={self.mapsize.w/3, self.mapsize.h},
+	n=3, symmetry=8, periodic_out=true, periodic_in=true, has_foundation=false
+}
+-- Inner buildings
+local wfcinner = WaveFunctionCollapse.new{
+	mode="overlapping", async=true,
+	sample=self:getFile("!wfctest4.tmx", "samples"),
+	size={self.mapsize.w*2/3, self.mapsize.h},
+	n=3, symmetry=8, periodic_out=false, periodic_in=false, has_foundation=false
+}
+
+-- Wait for all generators to finish
+if not WaveFunctionCollapse:waitAll(wfcinner, wfcwater, wfcouter) then print("[inner_outer] a WFC failed") return self:regenerate() end
+
+-- Merge them all
+local tm = Tilemap.new(self.mapsize)
+wfcouter:merge(1, 1, wfcwater, self.data.merge_order)
+if wfcouter:eliminateByFloodfill{'#', 'T'} < 400 then print("[inner_outer] outer is too small") return self:regenerate() end
+if wfcinner:eliminateByFloodfill{'#', 'T'} < 400 then print("[inner_outer] inner is too small") return self:regenerate() end
+tm:merge(1, 1, wfcouter, self.data.merge_order)
+tm:merge(self.mapsize.w - wfcinner.data_w, 1, wfcinner, self.data.merge_order)
+
+-- Find rooms
+local rooms = tm:findGroupsOf{'r'}
+tm:applyOnGroups(rooms, function(w, h, data, room, idx)
+	print("ROOM", idx, "::" , unpack(tm:groupOuterRectangle(room)))
+	for j = 1, #room.list do
+		local jn = room.list[j]
+		-- data[jn.y][jn.x] = tostring(idx)
+	end
+end)
+tm:fillAll('.', 'r')
+tm:fillAll()
+
+-- Elimitate the rest
+if tm:eliminateByFloodfill{'#', 'T'} < 400 then return self:regenerate() end
+tm:printResult()
+return tm:getResult(true)
diff --git a/game/modules/tome/data/zones/test/samples/wfctest.tmx b/game/modules/tome/data/zones/test/samples/wfctest.tmx
index 45b1429d26..74dd827bb4 100644
--- a/game/modules/tome/data/zones/test/samples/wfctest.tmx
+++ b/game/modules/tome/data/zones/test/samples/wfctest.tmx
@@ -17,7 +17,7 @@
   </tile>
   <tile id="51">
    <properties>
-    <property name="id" value="."/>
+    <property name="id" value="_"/>
    </properties>
   </tile>
   <tile id="54">
@@ -25,16 +25,26 @@
     <property name="id" value="*"/>
    </properties>
   </tile>
+  <tile id="63">
+   <properties>
+    <property name="id" value="O"/>
+   </properties>
+  </tile>
  </tileset>
  <tileset firstgid="91" name="tome-terrains" tilewidth="32" tileheight="32" tilecount="144">
   <image source="../../../../../../../tiled-maps/gfx/tome-terrains.png" width="256" height="576"/>
+  <tile id="22">
+   <properties>
+    <property name="id" value=";"/>
+   </properties>
+  </tile>
  </tileset>
  <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36">
   <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/>
  </tileset>
  <layer name="Terrain" width="16" height="16">
   <data encoding="base64" compression="zlib">
-   eJxjZGBgYARiFiiNjLGJYRM3QcPYxHCJk6KfWDNZaGQ/LrWEwoucsCInLMjRPxTkSU1/g818fPK40iq94hcA4S4l9A==
+   eJxzYGBgcADiQiiNjLGJYRMvRMPYxHCJk6KfWDNJUUsN9xMKL3LCihz/kaN/KMhjC0N8YTrYzKck/mgdvwBZdmTy
   </data>
  </layer>
 </map>
diff --git a/game/modules/tome/data/zones/test/samples/wfctest2.tmx b/game/modules/tome/data/zones/test/samples/wfctest2.tmx
index db0047375b..a5cb3f469c 100644
--- a/game/modules/tome/data/zones/test/samples/wfctest2.tmx
+++ b/game/modules/tome/data/zones/test/samples/wfctest2.tmx
@@ -49,7 +49,7 @@
  </tileset>
  <layer name="Terrain" width="16" height="16">
   <data encoding="base64" compression="zlib">
-   eJxjYBgFwwEUomFy9NZDMTb9+MxE1otPPzb3odtdj0dNPxQjy2PTj+4GZL3IZuByPyn6kf1Eqf5CBvz+JxSG+NIALnEAH5QrWQ==
+   eJxjYKAPKKSCfmRMjt5+KCbVDGS9yGYMJv34wgddDl0NjF8Pxdjchi/skfXi0o8LoNtdz0Ba/GDTT44byNWPHC6U6oeZQU76xBe/+AAAWvQrWQ==
   </data>
  </layer>
 </map>
diff --git a/game/modules/tome/data/zones/test/samples/wfctest4.tmx b/game/modules/tome/data/zones/test/samples/wfctest4.tmx
index e18428543a..f9102f03c8 100644
--- a/game/modules/tome/data/zones/test/samples/wfctest4.tmx
+++ b/game/modules/tome/data/zones/test/samples/wfctest4.tmx
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<map version="1.0" orientation="orthogonal" renderorder="right-down" width="16" height="16" tilewidth="32" tileheight="32" nextobjectid="31">
+<map version="1.0" orientation="orthogonal" renderorder="right-down" width="20" height="20" tilewidth="32" tileheight="32" nextobjectid="31">
  <properties>
   <property name="status_all" value="{no_teleport=true}"/>
  </properties>
@@ -22,7 +22,12 @@
   </tile>
   <tile id="54">
    <properties>
-    <property name="id" value="*"/>
+    <property name="id" value="_"/>
+   </properties>
+  </tile>
+  <tile id="58">
+   <properties>
+    <property name="id" value="r"/>
    </properties>
   </tile>
  </tileset>
@@ -32,9 +37,9 @@
  <tileset firstgid="235" name="numbers" tilewidth="32" tileheight="32" tilecount="36">
   <image source="../../../../../../../tiled-maps/gfx/numbers.png" width="192" height="192"/>
  </tileset>
- <layer name="Terrain" width="16" height="16">
+ <layer name="Terrain" width="20" height="20">
   <data encoding="base64" compression="zlib">
-   eJxjZGBgYMSBTfDIoatDxoTEidVPrFvw6UeXI0YNvfXj8yc5+onBtLafmHRDaboj1e/DXT85GABHnxWG
+   eJytkzEOwDAIAxOJJ3Xj///qVCmywLiEwVPQQbDZa62d6CFvTH4Ieac6PGR2ZmU8pdcNr8OseCoz2tME75MRnhHfWS6q+bAeGX+zptTaIC/7Wzfn2b4iXuU3Y3lRF3lyw3NgKfNZ0TfKmnoTyr1M87pZmMw+6gVVviXl
   </data>
  </layer>
 </map>
diff --git a/game/modules/tome/data/zones/test/zone.lua b/game/modules/tome/data/zones/test/zone.lua
index a293566aa4..3f8ff466e7 100644
--- a/game/modules/tome/data/zones/test/zone.lua
+++ b/game/modules/tome/data/zones/test/zone.lua
@@ -24,8 +24,8 @@ return {
 	max_level = 4,
 	decay = {300, 800},
 	actor_adjust_level = function(zone, level, e) return zone.base_level + e:getRankLevelAdjust() + level.level-1 + rng.range(-1,2) end,
-	width = 50, height = 50,
-	-- all_remembered = true,
+	width = 80, height = 50,
+	all_remembered = true,
 	all_lited = true,
 	no_level_connectivity = true,
 	
@@ -35,37 +35,13 @@ return {
 	generator =  {
 		map = {
 -- [[
-			class = "engine.generator.map.FromCustom",
+			class = "engine.generator.map.MapScript",
 			['<'] = "UP", ['>'] = "DOWN",
 			['.'] = "FLOOR", ['+'] = "DOOR", ['#'] = "WALL",
+			['_'] = "OLD_FLOOR", ['O'] = "OLD_WALL", 
 			[';'] = "GRASS", ['T'] = "TREE",
 			['='] = "DEEP_WATER",
-			custom = function(self, lev, old_lev)
-				local WaveFunctionCollapse = require "engine.WaveFunctionCollapse"
-				-- Water & trees layer
-				local wfcwater = WaveFunctionCollapse.new{
-					mode="overlapping", async=true,
-					sample=self:getFile("!wfctest2.tmx", "samples"),
-					size=self.mapsize,
-					n=3, symmetry=8, periodic_out=true, periodic_in=true, has_foundation=false
-				}
-
-				-- Base buildings
-				local wfc = WaveFunctionCollapse.new{
-					mode="overlapping", async=true,
-					sample=self:getFile("!wfctest4.tmx", "samples"),
-					size=self.mapsize,
-					n=3, symmetry=8, periodic_out=false, periodic_in=false, has_foundation=false
-				}
-				-- Wait for all generators to finish
-				if not WaveFunctionCollapse:waitAll(wfc, wfcwater) then return self:regenerate() end
-				-- Merge water in
-				wfc:merge(wfcwater, ' ', {'.', '+', '#', '=', ';', 'T'})
-				-- Elimitate the rest
-				if wfc:eliminateByFloodfill{'#', 'T'} < 800 then return self:regenerate() end
-				wfc:printResult()
-				return wfc:getResult(true)
-			end,
+			mapscript = "!inner_outer",
 --]]
 --[[
 			class = "engine.generator.map.Hexacle",
-- 
GitLab