Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions LICENSE-ROXAS2712.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Roxas2712 — Fork Contributions

Version 1.0 · Effective 25 October 2026

Copyright © 2026 Roxas2712, to the extent of rights held.
All rights reserved except as expressly granted below or already granted.

## 1. Scope and ownership

This notice applies only to original contributions made by Roxas2712
(GitHub: Roxas2712) to `Roxas2712/Gen2Recomped`, including original additions
within modified files, insofar as he owns the relevant rights. Git history,
pull-request diffs and source provenance identify contributions; a commit
author or a whole-file change alone does not establish ownership of every
part of that file.

It does not claim ownership of Cedric's work, Gen1Recomp / BOIS CLUB GAMES
work, other contributors' work, third-party dependencies, or ROM-derived
content. Their applicable licences and notices remain in force. It does not
grant permission to distribute the complete combined fork where another
rights holder's permission is required.

## 2. General permission and reserved rights

For contributions covered by this notice and not already licensed more
broadly, you may read and study the published source and make private,
non-commercial modifications and builds for your own use. This grants no
general permission to redistribute, sublicense, sell, or offer those
contributions as a hosted service. Other uses require Roxas2712's
written permission, except for the specific permission below and rights
available independently under applicable law.

Keep the relevant copyright, licence and attribution notices. Do not
represent Roxas2712's contributions as exclusively authored by someone
else. Public source availability alone does not put them in the public
domain or grant an open-source licence.

## 3. Specific permission for Cedric / UNDERdecoded

Roxas2712 expressly grants Cedric (Ced; GitHub: UNDERdecoded /
UNDERdecodedHD) a non-exclusive, worldwide, perpetual, royalty-free
permission to use, reproduce, integrate, modify and distribute the covered
contributions that Roxas2712 submits or expressly offers through pull
requests to `UNDERdecoded/Gen2Recomped`, for Cedric's Gen2Recomped /
RetroRecompiled project. This includes its renamed continuation maintained
by Cedric, its official builds, and its commercial operation.

Cedric may license the incorporated contributions to recipients as part of
that project, subject to preserving this permission's applicable credit and
notice requirements. This permission is not a copyright assignment, is not
exclusive, and does not cover unpublished work, separate repositories,
ModCompass service operation, credentials, or content not offered in those
pull requests. Roxas2712 retains his own rights to his contributions.

For uses relying on this permission:

- retain the relevant source copyright and licence notices;
- give visible credit in the project's README or equivalent public project
page, and in the application's existing About / Credits view, if present;
- use at least the following credit, or a substantially equivalent one:

> Contributions by Roxas2712 —
> https://github.com/Roxas2712

A maintained consolidated contributor credit is sufficient; a separate
credit on every screen or for every change is not required. Adapted
contributions must not be attributed exclusively to another author. Other
contributors' credits must also remain intact.

## 4. Earlier licences and permissions remain effective

This notice does not revoke, narrow, or retrospectively add conditions to
any permission already validly granted, including MIT / GPL grants,
separate agreements, or permissions arising from previously submitted
pull requests. Where an earlier grant permits a use independently of this
notice, its existing terms govern that use. For such earlier uses, the
visible credit above is requested to the extent it is not already required.

For subsequent contributions, this is the default statement of Roxas2712's own reserved rights and the specific PR permission, subject to any
applicable existing licence obligations or separately agreed terms. It
does not change another rights holder's licence or guarantee compatibility
of a particular combination of code.

## 5. No warranty

To the extent permitted by applicable law, these contributions are provided
as is, without warranty. No support obligation, endorsement, or rights to
third-party code, game assets, names, or trademarks are granted.
12 changes: 12 additions & 0 deletions src/world/MapLoader.lua
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,18 @@ function MapLoader.cached(mapId)
return m
end

-- A source renderer may attach a presentation-only Map to a neighbor record.
-- Native drawing still belongs to the loader's resident map and renderer.
function MapLoader.forNeighbor(neighbor, data)
if not neighbor then return nil end
local id = neighbor.id
local map = id and MapLoader.cached(id)
if not map and data and data.maps and data.maps[id] then
map = MapLoader.load(data, id)
end
return map or neighbor.map
end

-- evict one resident map, releasing its renderer's GPU objects. Callers
-- must ensure the map is not the current map and not drawn as a connected
-- strip (nothing live may hold its renderer) -- MapLoader.trim guarantees
Expand Down
19 changes: 12 additions & 7 deletions src/world/OverworldController.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,10 @@ function OverworldState:neighborReach()
end

function OverworldState:rebuildNeighbors()
-- Source renderers inspect the same live map/tileset registries as the
-- native loader. Missing fields made an already resident connected world
-- appear empty to those readers. Rebind on every rebuild after data edits.
self.maps, self.tilesets = Game.data.maps, Game.data.tilesets
local mapId = self.map.id
self.neighbors = {}
local hops = FieldDefaults.world(Game.data, "neighborHops") or NEIGHBOR_HOPS
Expand All @@ -2202,7 +2206,7 @@ function OverworldState:rebuildNeighbors()
hops, reachW, reachH)) do
keep[n.id] = true
local m = MapLoader.load(Game.data, n.id)
table.insert(self.neighbors, { map = m, ox = n.ox, oy = n.oy })
table.insert(self.neighbors, { id = m.id, map = m, ox = n.ox, oy = n.oy, cx = n.ox / 16, cy = n.oy / 16 })
end
-- bound resident memory: drop maps behind us that are neither current nor
-- a drawn neighbor, releasing their window batch / border image / atlas
Expand Down Expand Up @@ -17289,7 +17293,7 @@ function OverworldState:drawWorld()
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
self.map.renderer:draw(cam.x, bgY, vw, vh)
for _, nb in ipairs(self.neighbors) do
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
MapLoader.forNeighbor(nb, Game.data).renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
end
-- the Mirage Tower on BG0, under the sprites (src/world/Gen3MirageTower)
if self.g3MirageTower then
Expand Down Expand Up @@ -18095,7 +18099,7 @@ function OverworldState:drawWorld()
self.map.renderer:drawBorderFill(cam.x, bgY, vw, vh)
self.map.renderer:draw(cam.x, bgY, vw, vh)
for _, nb in ipairs(self.neighbors) do
nb.map.renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
MapLoader.forNeighbor(nb, Game.data).renderer:drawMapOnly(cam.x - nb.ox, bgY - nb.oy, vw, vh)
end
end
end
Expand Down Expand Up @@ -18585,10 +18589,11 @@ function OverworldState:drawWorld()
self.map.renderer:drawAbove(cam.x, bgY, vw, vh)
aim(self.map.renderer)
for _, nb in ipairs(self.neighbors) do
if nb.map.renderer.drawAbove then
aim(nb.map.renderer, nb.ox, nb.oy)
nb.map.renderer:drawAbove(cam.x - nb.ox, bgY - nb.oy, vw, vh)
aim(nb.map.renderer)
local renderer = MapLoader.forNeighbor(nb, Game.data).renderer
if renderer.drawAbove then
aim(renderer, nb.ox, nb.oy)
renderer:drawAbove(cam.x - nb.ox, bgY - nb.oy, vw, vh)
aim(renderer)
end
end
for _, e in ipairs(onTop or {}) do drawEntity(e) end
Expand Down
69 changes: 69 additions & 0 deletions tests/engine/connected_map_publication.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package.path='./?.lua;./?/init.lua;'..package.path
local T=require('tests.modkit')
local Version=require('src.core.GameVersion')
local World=require('src.world.OverworldController')
local Map=require('src.world.Map')
local Loader=require('src.world.MapLoader')
local previous=Version.get()
local gameSlot,oldGame
for i=1,100 do
local name,value=debug.getupvalue(World.rebuildNeighbors,i)
if not name then break end
if name=='Game'then gameSlot,oldGame=i,value;break end
end
assert(gameSlot,'actual native rebuild must have its game owner')
local oldLoad,oldTrim=Loader.load,Loader.trim
for _,edition in ipairs({'red','yellow','gold','crystal','emerald','firered'})do
Version.set(edition)
local pixel=Version.isGen3()and 16 or 32
local data=T.fixtures.fresh();data.maps={}
local ts={id='FIX_OUT',blockCells=pixel/16,blockTiles=pixel/8,blocks={},walkable={}}
data.tilesets={FIX_OUT=ts}
local maps={}
for _,id in ipairs({'ROOT','EAST','NORTH'})do
local def={id=id,width=2,height=2,blockPx=pixel,tileset='FIX_OUT',blocks={0,0,0,0},objects={},connections={}}
data.maps[id]=def;maps[id]=Map.new(def,ts)
end
data.maps.ROOT.connections.east={map='EAST',offset=-1}
data.maps.EAST.connections.north={map='NORTH',offset=-3}
local game={data=data,save={flags={},party={}},renderer={worldViewSize=function()return 160,144 end}}
debug.setupvalue(World.rebuildNeighbors,gameSlot,game)
local loads=0
Loader.load=function(actual,id)
T.eq(actual,data,'loader receives the actual native dataset')
loads=loads+1;return assert(maps[id])
end
Loader.trim=function(keep)
T.check(keep.ROOT and keep.EAST and keep.NORTH,'resident set protects active and connected terrain')
end
local world=setmetatable({map=maps.ROOT,npcs={},entities={},npcPool={},neighbors={},ghosts={}}, {__index=World})
world:rebuildNeighbors()
T.eq(world.maps,data.maps,edition..' exposes the actual native map registry')
T.eq(world.tilesets,data.tilesets,edition..' exposes the actual native tileset registry')
T.eq(#world.neighbors,2,'native BFS admits direct and farther connected terrain')
T.eq(loads,2,'source observation adds no parallel map loads')
local native={}
-- The source renderer's native-neighbor index, over actual loaded Maps.
for _,neighbor in ipairs(world.neighbors)do
native[neighbor.id]=neighbor
T.eq(neighbor.id,neighbor.map.id,'source ID is the loaded map identity')
T.eq(neighbor.ox,neighbor.cx*16,'pixel/cell X offsets agree')
T.eq(neighbor.oy,neighbor.cy*16,'pixel/cell Y offsets agree')
T.eq(world.maps[neighbor.id],neighbor.map.def,'renderer and loader see the same definition')
T.eq(world.tilesets[neighbor.map.def.tileset],ts,'neighbor tileset remains native')
end
T.eq(native.EAST.map,world.neighbors[1].map,'source neighbor index finds actual east map')
T.eq(native.EAST.ox,2*pixel,'Gen1/2 blocks and Gen3 metatiles preserve their own width')
T.eq(native.EAST.oy,-pixel,'negative connection offset preserves native units')
local defs,tilesets={},{}
for key,value in pairs(data.maps)do defs[key]=value end
for key,value in pairs(data.tilesets)do tilesets[key]=value end
data.maps,data.tilesets=defs,tilesets
world:rebuildNeighbors()
T.eq(world.maps,defs,'registry replacement retires the prior map table')
T.eq(world.tilesets,tilesets,'registry replacement retires the prior tileset table')
end
Loader.load,Loader.trim=oldLoad,oldTrim
debug.setupvalue(World.rebuildNeighbors,gameSlot,oldGame)
Version.set(previous)
T.finish('native connected-map publication for Gen1 through Gen3 source renderers')
55 changes: 55 additions & 0 deletions tests/engine/neighbor_renderer_ownership.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- Copyright (c) 2026 Roxas2712. See LICENSE-ROXAS2712.md.
local T=require('tests.harness')
love=require('tests.love_stub')
local Version=require('src.core.GameVersion')
local Loader=require('src.world.MapLoader')
local Pipelines=require('src.render.Pipelines')
local Fixture=require('tests.world_draw.fixture')
local old=Version.get()
local cacheSlot,originalCache
for i=1,100 do
local name,value=debug.getupvalue(Loader.cached,i)
if name=='cache'then cacheSlot,originalCache=i,value;break end
if not name then break end
end
assert(cacheSlot)
local pipeline,draw,load=Pipelines.worldPipeline,Pipelines.drawWorld,Loader.load
for _,edition in ipairs({'red','gold','crystal','emerald','firered','platinum'})do
Version.set(edition)
for _,declines in ipairs({false,true})do
local f=Fixture.new(2);local draws,above=0,0
local native={id='OWNED_NEIGHBOR',renderer={
drawMapOnly=function(_,x,y,w,h)
T.eq(x,f.world.camera.x-32,'native strip keeps neighbor X origin')
T.eq(y,f.world.camera.y+16,'native strip keeps neighbor Y origin')
T.eq(w,160);T.eq(h,144);draws=draws+1
end,
drawAbove=function()above=above+1 end,
}}
local view={id=native.id,renderer={image='source-owned-presentation'}}
local neighbor={id=native.id,map=view,ox=32,oy=-16}
f.world.neighbors={neighbor}
debug.setupvalue(Loader.cached,cacheSlot,{[native.id]=native})
Pipelines.worldPipeline=function()if declines then return 'source_draw',{ }end end
Pipelines.drawWorld=function()return nil end
T.eq(Loader.forNeighbor(neighbor,f.game.data),native,'loader retains native renderer ownership')
local ok,why=pcall(f.world.drawWorld,f.world)
T.check(ok,edition..' native draw survives '..(declines and'pipeline decline'or'flat draw')..': '..tostring(why))
T.eq(draws,1,'actual native ground pass draws the resident neighbor once')
T.eq(above,1,'actual upper layer uses the same native renderer')
T.eq(neighbor.map,view,'source presentation view is not overwritten')
T.eq(view.renderer.drawMapOnly,nil,'native draw does not inject methods into a borrowed view')
debug.setupvalue(Loader.cached,cacheSlot,{})
f.game.data.maps={[native.id]={id=native.id}}
local reloads=0
Loader.load=function(data,id)T.eq(data,f.game.data);T.eq(id,native.id);reloads=reloads+1;return native end
T.eq(Loader.forNeighbor(neighbor,f.game.data),native,'invalidated native renderer is resolved through its actual loader')
T.eq(reloads,1,'missing native resource is rebuilt once through loader authority')
T.eq(Loader.forNeighbor({id='CUSTOM',map=view},f.game.data),view,'custom non-native neighbor keeps its own renderer')
Loader.load=load
end
end
debug.setupvalue(Loader.cached,cacheSlot,originalCache)
Pipelines.worldPipeline,Pipelines.drawWorld,Loader.load=pipeline,draw,load
Version.set(old)
T.finish('native neighbor renderers survive borrowed source views and pipeline decline')
Loading