Synchronize world state across editor and room saves

This commit is contained in:
Your Name 2026-08-11 21:49:32 -04:00
parent 1965e548b4
commit ee7d36e72b
3 changed files with 75 additions and 13 deletions

View file

@ -492,6 +492,7 @@ local function saveRoomFromEditor()
gameWorld:invalidatePreview(name) gameWorld:invalidatePreview(name)
if editorView == "world" then rebuildWorldRoomButtons() end if editorView == "world" then rebuildWorldRoomButtons() end
currentRoom.name = name currentRoom.name = name
gameWorld:setLevel(currentRoom)
editorRoomName = name editorRoomName = name
roomNameField:editText(name) roomNameField:editText(name)
activeField = nil activeField = nil
@ -520,6 +521,7 @@ local function clearRuntimeSaves()
clearSaveConfirmationScope = nil clearSaveConfirmationScope = nil
if roomName then if roomName then
currentRoom = Room:new{ name = roomName } currentRoom = Room:new{ name = roomName }
gameWorld:setLevel(currentRoom)
setEditorRoomName(currentRoom.name) setEditorRoomName(currentRoom.name)
editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'" editorStatus = "Cleared " .. removed .. " runtime save item(s) and reloaded '" .. roomName .. ".'"
else else
@ -905,6 +907,7 @@ local function openWorldRoom(cellX, cellY)
currentRoom = Room:new{ name = name } currentRoom = Room:new{ name = name }
editorStatus = "Editing '" .. name .. ".'" editorStatus = "Editing '" .. name .. ".'"
end end
gameWorld:setLevel(currentRoom)
gameWorld:setLastRoom(currentRoom.name) gameWorld:setLastRoom(currentRoom.name)
setEditorRoomName(currentRoom.name) setEditorRoomName(currentRoom.name)
assetsView() assetsView()

View file

@ -66,8 +66,12 @@ function love.quit()
-- An explicit editor save/clear removes the runtime copy intentionally; -- An explicit editor save/clear removes the runtime copy intentionally;
-- don't immediately recreate it while shutting down. -- don't immediately recreate it while shutting down.
if currentRoom and not suppressRuntimeAutosave then if currentRoom and not suppressRuntimeAutosave then
if gameWorld then
gameWorld:markDirty(currentRoom)
gameWorld:flushRuntime()
else
currentRoom:save() currentRoom:save()
if gameWorld then gameWorld:setLastRoom(currentRoom.name) end end
end end
end end

View file

@ -12,15 +12,54 @@ function World:initialize()
self.previewCache = {} self.previewCache = {}
self.lastRoom = nil self.lastRoom = nil
self:load() self:load()
self:loadRuntimeState()
self.levels = {} self.levels = {}
end end
function World:loadLevels() function World:loadLevels()
for _, placed in ipairs(self.rooms) do self.levels[placed.name] = Room:new{name = placed.name} end -- Gameplay saves go to LÖVE's runtime directory. Seed every world slice
-- through the same runtime-first loader, not just the initially visible one.
for _, placed in ipairs(self.rooms) do self.levels[placed.name] = Room:new{name = placed.name, runtime = true} end
self:rebuildCollision() self:rebuildCollision()
end end
-- The editor can replace a level with a freshly loaded authored Room. Keep the
-- global simulation pointing at that exact instance, not a stale preload.
function World:setLevel(room)
if not room or not room.name or not self:roomLocation(room.name) then return false end
self.levels[room.name] = room
self:rebuildCollision()
return true
end
-- Runtime state is one logical world snapshot: each level slice is written to
-- LÖVE's save directory, and this companion file says which slice was active.
-- The authored world layout remains source-only for the editor.
function World:loadRuntimeState()
local raw = love.filesystem.read("rooms/world.sav")
if not raw then return end
local ok, state = pcall(TSerial.unpack, raw)
if ok and type(state) == "table" and state.lastRoom and self:roomLocation(state.lastRoom) then
self.lastRoom = state.lastRoom
end
end
function World:saveRuntime(dirtyRooms)
for room in pairs(dirtyRooms or {}) do room:save() end
return love.filesystem.write("rooms/world.sav", TSerial.pack({lastRoom = currentRoom and currentRoom.name}, nil, true))
end
function World:markDirty(room)
self.dirtyRooms = self.dirtyRooms or {}
self.dirtyRooms[room] = true
end
function World:flushRuntime()
self:saveRuntime(self.dirtyRooms)
self.dirtyRooms = {}
end
function World:listRoomNames() function World:listRoomNames()
local names = {} local names = {}
for _, file in ipairs(love.filesystem.getDirectoryItems("rooms")) do for _, file in ipairs(love.filesystem.getDirectoryItems("rooms")) do
@ -120,7 +159,6 @@ end
function World:setLastRoom(name) function World:setLastRoom(name)
if name and self.lastRoom ~= name then if name and self.lastRoom ~= name then
self.lastRoom = name self.lastRoom = name
return self:save()
end end
return true return true
end end
@ -310,8 +348,10 @@ function World:movePlayer(room, key)
current[color] = player:getGridPos() current[color] = player:getGridPos()
end end
if moved then if moved then
self:markDirty(room)
self:playerJoinCheck(room, previous, current) self:playerJoinCheck(room, previous, current)
self:tick(room) self:tick(room)
self:flushRuntime()
end end
end end
@ -326,6 +366,7 @@ function World:switchPlayerColor(room, color)
end end
end end
player:playSwitchSound(canSwitch) player:playSwitchSound(canSwitch)
if canSwitch then self:markDirty(room); self:flushRuntime() end
return canSwitch return canSwitch
end end
@ -353,19 +394,33 @@ function World:nextRoom(room, pos)
elseif pos.y < 1 then exit, entry.y = "top", gridHeight end elseif pos.y < 1 then exit, entry.y = "top", gridHeight end
local nextName = self:getNeighbor(room.name, exit) or room.name local nextName = self:getNeighbor(room.name, exit) or room.name
local activeColors = room:getActiveColors() local activeColors = room:getActiveColors()
room:save() self:markDirty(room)
room.activeColors = {red = false, green = false, blue = false} local destination = self.levels[nextName]
currentRoom = self.levels[nextName]
for _, color in ipairs(activeColors) do for _, color in ipairs(activeColors) do
if currentRoom.player[color] then local player = room.player[color]
currentRoom.player[color].x, currentRoom.player[color].y = entry.x, entry.y -- Active copies travel with the player. Inactive copies are deliberately
else -- left in their room so World:playerJoinCheck can merge them later.
currentRoom:registerPlayer(Player:new(entry.x, entry.y, color), color) room.player[color] = nil
for i = #room.colorsPlayerHas, 1, -1 do
if room.colorsPlayerHas[i] == color then table.remove(room.colorsPlayerHas, i) end
end end
if destination.player[color] then
for i = #destination.colorsPlayerHas, 1, -1 do
if destination.colorsPlayerHas[i] == color then table.remove(destination.colorsPlayerHas, i) end
end
end
destination.player[color] = player
table.insert(destination.colorsPlayerHas, color)
end
room.activeColors = {red = false, green = false, blue = false}
currentRoom = destination
self:markDirty(currentRoom)
for _, color in ipairs(activeColors) do
currentRoom.player[color].x, currentRoom.player[color].y = entry.x, entry.y
currentRoom:setActiveColor(color, true) currentRoom:setActiveColor(color, true)
end end
self:playerJoinCheck(currentRoom) self:playerJoinCheck(currentRoom)
self:setLastRoom(currentRoom.name) self.lastRoom = currentRoom.name
setEditorRoomName(currentRoom.name) setEditorRoomName(currentRoom.name)
end end
@ -397,7 +452,7 @@ function World:moveGroup(group, direction)
self:rebuildCollision() self:rebuildCollision()
for room in pairs(changed) do room:resolveSwitchesAndDoors() end for room in pairs(changed) do room:resolveSwitchesAndDoors() end
self:rebuildCollision() self:rebuildCollision()
for room in pairs(changed) do if room ~= currentRoom then room:save() end end for room in pairs(changed) do self:markDirty(room) end
end end
function World:drawOverlaps(room, color) function World:drawOverlaps(room, color)