mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 15:51:17 +02:00
big ass modding update
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
-- mod.world: the supported way for mod code to act on the running
|
||||
-- overworld. Every method resolves the live OverworldState by scanning
|
||||
-- the state stack for the isOverworld marker and returns nil, "no
|
||||
-- overworld" when none is up -- called from the title screen this is a
|
||||
-- quiet no-op, never a crash. Reaching into OverworldState internals
|
||||
-- stays unsupported; anything a mod legitimately needs belongs here.
|
||||
|
||||
local Logger = require("src.core.Logger")
|
||||
local MapLoader = require("src.world.MapLoader")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
|
||||
local WorldAPI = {}
|
||||
WorldAPI.__index = WorldAPI
|
||||
|
||||
local NO_OVERWORLD = "no overworld"
|
||||
|
||||
function WorldAPI.new(game, modId)
|
||||
return setmetatable({ game = game, modId = modId }, WorldAPI)
|
||||
end
|
||||
|
||||
-- the live overworld, or nil. Game.overworld is the fast path; the stack
|
||||
-- scan is the authority, so a state pushed over the world (a battle, a
|
||||
-- menu) still resolves to the world underneath it.
|
||||
function WorldAPI:overworld()
|
||||
local game = self.game
|
||||
local stack = game and game.stack
|
||||
local states = stack and stack.states
|
||||
if states then
|
||||
for i = #states, 1, -1 do
|
||||
if states[i].isOverworld then return states[i] end
|
||||
end
|
||||
end
|
||||
local ow = game and game.overworld
|
||||
if ow and ow.isOverworld and ow.map then return ow end
|
||||
return nil
|
||||
end
|
||||
|
||||
function WorldAPI:current()
|
||||
local ow = self:overworld()
|
||||
if not ow or not ow.map then return nil, NO_OVERWORLD end
|
||||
local p = ow.player
|
||||
return { mapId = ow.map.id, x = p and p.cellX, y = p and p.cellY,
|
||||
facing = p and p.facing }
|
||||
end
|
||||
|
||||
-- opts.arrive = "fly" | "teleport" picks the arrival FX; anything else
|
||||
-- lands the player without one, like a scripted warp.
|
||||
function WorldAPI:warpTo(mapId, x, y, facing, opts)
|
||||
local ow = self:overworld()
|
||||
if not ow then return nil, NO_OVERWORLD end
|
||||
if not self.game.data.maps[mapId] then
|
||||
return nil, "unknown map: " .. tostring(mapId)
|
||||
end
|
||||
if opts and (opts.arrive == "fly" or opts.arrive == "teleport") then
|
||||
ow.arriveWarp = opts.arrive
|
||||
end
|
||||
ow:startWarpTo(mapId, x, y, facing or "down", opts and opts.onDone,
|
||||
{ via = "warp", keepMusic = opts and opts.keepMusic })
|
||||
return true
|
||||
end
|
||||
|
||||
-- save.objectToggles is the same store the spawn filter reads, so a toggle
|
||||
-- on an inactive map takes effect the next time it is entered.
|
||||
function WorldAPI:toggleObject(mapId, objName, visible)
|
||||
local save = self.game and self.game.save
|
||||
if not save then return nil, "no save" end
|
||||
save.objectToggles = save.objectToggles or {}
|
||||
save.objectToggles[mapId] = save.objectToggles[mapId] or {}
|
||||
save.objectToggles[mapId][objName] = visible and true or false
|
||||
Runtime.emit("world.object_toggled",
|
||||
{ mapId = mapId, objName = objName, visible = visible and true or false })
|
||||
local ow = self:overworld()
|
||||
if ow and ow.map and ow.map.id == mapId then
|
||||
ow:setMap(mapId, ow.player.cellX, ow.player.cellY, ow.player.facing,
|
||||
{ seamless = true, via = "reload", keepMusic = true })
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function WorldAPI:setFlag(name, value)
|
||||
local save = self.game and self.game.save
|
||||
if not save or not save.flags then return nil, "no save" end
|
||||
save.flags[name] = value
|
||||
return true
|
||||
end
|
||||
|
||||
function WorldAPI:getFlag(name)
|
||||
local save = self.game and self.game.save
|
||||
return save and save.flags and save.flags[name]
|
||||
end
|
||||
|
||||
-- active map only: this mutates the runtime Map and rebuilds the renderer.
|
||||
-- A layout change that must survive a reload belongs in a maps patch.
|
||||
function WorldAPI:replaceBlock(bx, by, block)
|
||||
local ow = self:overworld()
|
||||
if not ow or not ow.map then return nil, NO_OVERWORLD end
|
||||
ow:replaceBlock(bx, by, block)
|
||||
return true
|
||||
end
|
||||
|
||||
-- objDef uses the same shape as maps[].objects. Runtime objects are not
|
||||
-- serialized: a permanent NPC belongs in a maps patch, this is for
|
||||
-- scripted and dynamic actors the mod re-spawns on map.entered.
|
||||
function WorldAPI:spawnNpc(mapId, objDef)
|
||||
local ow = self:overworld()
|
||||
if not ow then return nil, NO_OVERWORLD end
|
||||
if type(objDef) ~= "table" then return nil, "objDef must be a table" end
|
||||
local copy = {}
|
||||
for k, v in pairs(objDef) do copy[k] = v end
|
||||
return ow:addRuntimeObject(mapId, copy, self.modId)
|
||||
end
|
||||
|
||||
function WorldAPI:removeNpc(npcId)
|
||||
local ow = self:overworld()
|
||||
if not ow then return nil, NO_OVERWORLD end
|
||||
return ow:removeRuntimeObject(npcId, self.modId)
|
||||
end
|
||||
|
||||
-- a handle onto a live NPC: scriptMove / marchInPlace / face, which is
|
||||
-- everything the scripted-movement queue exposes
|
||||
local Handle = {}
|
||||
Handle.__index = Handle
|
||||
|
||||
function Handle:scriptMove(dir, tiles, onDone)
|
||||
self.ow:scriptMove(self.npc, dir, tiles or 1, onDone)
|
||||
return true
|
||||
end
|
||||
|
||||
function Handle:marchInPlace(onDone)
|
||||
self.ow:marchInPlace(self.npc, onDone)
|
||||
return true
|
||||
end
|
||||
|
||||
function Handle:face(dir)
|
||||
self.npc.facing = dir
|
||||
return true
|
||||
end
|
||||
|
||||
function Handle:position()
|
||||
return self.npc.cellX, self.npc.cellY
|
||||
end
|
||||
|
||||
function WorldAPI:npc(mapId, indexOrName)
|
||||
local ow = self:overworld()
|
||||
if not ow then return nil, NO_OVERWORLD end
|
||||
if ow.map and ow.map.id ~= mapId then return nil, "map is not active" end
|
||||
for _, npc in ipairs(ow.npcs or {}) do
|
||||
if npc.def.index == indexOrName or npc.def.name == indexOrName
|
||||
or npc.id == indexOrName then
|
||||
return setmetatable({ ow = ow, npc = npc, id = npc.id }, Handle)
|
||||
end
|
||||
end
|
||||
return nil, "no such object: " .. tostring(indexOrName)
|
||||
end
|
||||
|
||||
-- FIFO queueing is owned by the script runner; until it lands this runs
|
||||
-- the rows when nothing else is running and refuses otherwise, so a mod
|
||||
-- never silently loses a script.
|
||||
function WorldAPI:queueScript(rows, extra)
|
||||
local ow = self:overworld()
|
||||
if not ow or not ow.runner then return nil, NO_OVERWORLD end
|
||||
if ow.runner:isRunning() then return nil, "a script is already running" end
|
||||
ow.runner:run(rows, extra)
|
||||
return true
|
||||
end
|
||||
|
||||
-- drop a map's cached instance so the next load re-reads its record; when
|
||||
-- it is the active map the world reloads around the player in place
|
||||
function WorldAPI:invalidateMap(mapId)
|
||||
local ow = self:overworld()
|
||||
if not ow then
|
||||
local had = MapLoader.invalidate(mapId)
|
||||
Runtime.emit("map.reloaded", { mapId = mapId, reason = "invalidate" })
|
||||
return had
|
||||
end
|
||||
local ok, err = pcall(ow.reloadMap, ow, mapId, "invalidate")
|
||||
if not ok then
|
||||
Logger.warn("[%s] invalidateMap %s failed: %s", tostring(self.modId),
|
||||
tostring(mapId), tostring(err))
|
||||
return nil, tostring(err)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return WorldAPI
|
||||
Reference in New Issue
Block a user