Skip to content
Closed
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
55 changes: 55 additions & 0 deletions docs/native-script-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Native numeric script state and session schemas

The source-facing session now exposes numeric `flags` and `vars` maps. These
are typed views over the existing native save, not a second script store.
Reading a missing value does not allocate native state. Writes run the native
flag commands and variable writer, preserving flag events, live visibility,
edition-specific league aliases and derived object-graphics slots. Context
variables stay outside persistent session maps and snapshots; the special
range ends at 0x8014 in Kanto and 0x8015 in Hoenn.

Whole-map replacements validate every key/value and conflicting numeric/string
alias before any command runs. They preserve native-only named flags and
context variables. Session enumeration includes its existing scalar schema,
numeric state, detached playtime and presentation fields. Recursive copy code
can obtain an isolated flags/vars snapshot without borrowing native tables.
The session retires its maps when its game data, maps, save, edition or mod
loader changes, and on explicit release.

LuaJIT's Lua-5.1 `pairs` and `next` do not consult `__pairs`. `TableIteration`
installs dispatchers before the mod mutation journals open. Only registered
views opt in; ordinary inputs go to the original host functions, including
the host's own treatment of metatables. `pairs(view)` uses an owned snapshot.
Numeric maps implement `next(view, key)` through a typed native cursor, without
quadratic snapshot reconstruction or lending the native table. Deleting the
current variable while iterating keeps the underlying native cursor semantics.
Registry callbacks do not capture their own weak key.

Mutation-journal iteration is captured independently of later global wrappers.
Failed entry wrappers unwind before rollback iterates registries or calls
optional resource services. Native mods may wrap the iteration functions in
the usual forwarding style; rollback and teardown retain the infrastructure
and take back the mod's wrapper.

## Validation

```
luajit tools/script-stores/check.lua
luajit tools/script-stores/session-check.lua
```

The backend suite exercises all five editions through the real native command
implementations, compiler and runner. The session suite verifies live reads,
numeric writes, replacements, aliases, special-variable exclusion, ordinary
table semantics, weak-key lifetime, recursive copies, direct cursor mutation
and real SDK entry rollback with a native iteration-wrapper peer.

## Remaining scripting integration

This adds session schemas used by the forthcoming scripting-space bridge.
It does not supply `src.core.game3.scripting.space`, a replacement VM, symbol
catalogues, complete party/bag/storage schemas, detached preview construction
or all source event/field/battle/UI/audio services. The unchanged original
package still stops at the scripting-space import. Full renderer, five-edition
gameplay, native-mod coexistence and performance acceptance remain open.
QA uses isolated identities, preserves player data and keeps 8 GiB free.
101 changes: 101 additions & 0 deletions docs/native-session-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Native game/session and frame boundaries

`src.core.Game3` is a distinct class with per-native-owner facade instances.
It does not alias the central `Game`, instantiate a second engine or bootstrap
Game when imported. Standalone construction without an explicit native owner
declines instead of silently sharing another game; full source construction
remains separate. Explicit owner resolution rejects retired handles. A source
session is scoped to its native save, dataset, map table and edition; replacing
one retires its observations and queued callbacks.

## Native ownership

The native Game retains its frame, fixed-step, input, script, battle, audio and
save owners. Captured base methods run with that native Game, while source
wrappers receive their stable facade. A wrapper may hold an entry. Repeated base
calls in one entry retain the first native result, including nil-bearing return
lists, instead of advancing again. Native failures cannot be hidden or retried.
The native frame's existing collector step also exposes the source `stepGC`
seam; there is no additional collector running beside it.

The runtime services one deferred batch at `Game:_step`, after input promotion
and stack recovery. New callbacks wait for the next native phase. Nested drains,
save/dataset/loader changes, stop, teardown and replacement batches cannot revive
an old cohort. A source runtime wrapper may hold that field phase. Callback
failures preserve diagnostics and owner attribution without replaying the batch.
`Runtime.update` cannot start a separate simulation outside that phase.

Native playtime remains seconds. The source view projects hours/minutes/seconds
and fractional seconds without changing the save representation or integrating
another accumulator. Repeated RTC/VBlank observations do not tick another clock.
The VBlank property here observes serviced native phases; complete source RTC
and time-event interfaces are separate from this lifecycle boundary.

## Callback and input interoperability

Requiring the Game3/runtime namespace identifies the mod's expected game-handle
contract through the existing mod seam journal. This does not require changing a
manifest or recognizing a particular mod ID. Its game-bearing events and direct
game hook arguments receive the facade. Its `next` calls unwrap those handles
before native participants or the native body run. Other participants receive
the original game/payload, and custom event payloads keep their own contract.
Rollback and teardown remove that namespace selection. Record/context families
other than these explicit game/save boundaries still require their own adapters.

Desktop mouse wrappers run before the native mouse path; native synthesized touch
is not also delivered as another source touch. Physical touch, editors, importers,
boot reports and the native pointer owners retain their entry paths. Pointer and
frame failures restore their scopes. Other generations bypass the source facade.

Native title callbacks lend single-use actions to a source intro. A delayed
action still invokes the original native new-game/continue callback once and
declines if its save, dataset, loader or boot scope was replaced. Native boot UI,
naming, RNG, map entry and persistence are not copied into this facade.
Source reset callbacks run before mod wrappers unwind; cleanup is idempotent.

## Session surface and remaining work

The initial session view covers edition/generation, canonical current map and
position, player header, money/coins, bike/surf observations, native options/mod
state and the playtime projection. Supported scalar writes are typed and change
only the corresponding in-memory native fields. Coordinates, map changes and
clock replacement remain native commands. Temporary presentation extensions and
preview replacements never replace the underlying native save/stack. Persistence
continues through the existing native services and `mod.save`.

Numeric flags and variables now use typed native command-backed views, with
session/map iteration and detached recursive-copy support; see
`native-script-state.md`. Party, bag, storage, dex, complete session schemas,
preview isolation, custom
boot UI and further field/actor/battle/audio services still need their typed
adapters. A differently shaped native collection is not exposed under a source
name. The data facade supplies the prior owned map views; other data schemas need
their own contracts. Unchanged-mod startup, full renderer/gameplay, GPU and
performance acceptance remain incomplete.

## Checks

Run with LuaJIT from the repository root:

```
luajit tools/session-runtime/check.lua
luajit tools/session-runtime/dispatch-check.lua
luajit tools/session-runtime/callback-check.lua
luajit tests/run_engine.lua
luajit tests/run_modkit.lua
```

These ROM-free checks exercise the actual native Game/StateStack/player phases,
frame clock/audio/GC ownership, title callbacks with a synthetic disk reader,
failure/replacement paths, all five editions, other-generation isolation, and
unchanged SDK consumers sharing callbacks with a native peer. They do not open
player profiles. Native QA must use the shared 8 GiB free-space guard.

The same callback consumer runs on API 2 and API 3 where supported, selected by
`SESSION_RUNTIME_API`; its default is the host's actual advertised API level.
Integration prerequisites are the early native owner/player boundary (#254),
source map identities (#239), and owned map read views (#251 with dependencies)
when the data facade is used. The five-edition checks also need the separate
cartridge registrations (#183, #186, #187). Loading an original API-3 mod on
upstream additionally requires the existing API-3 stack; this boundary does not
raise or bypass the advertised mod API version.
34 changes: 31 additions & 3 deletions main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,7 @@ function love.wheelmoved(x, y)
Game:wheelmoved(x, y)
end

function love.mousepressed(x, y, button, istouch)
local function nativeMousePressed(x, y, button, istouch)
if bootReport then return dismissBootReport() end
if TouchEditor then
-- Android primary touch already arrived via love.touchpressed; a second
Expand All @@ -958,7 +958,7 @@ function love.mousepressed(x, y, button, istouch)
end
end

function love.mousereleased(x, y, button, istouch)
local function nativeMouseReleased(x, y, button, istouch)
if TouchEditor then
if love.system.getOS() == "Android" then return end
return TouchEditor.mousereleased(x, y, button)
Expand All @@ -985,7 +985,7 @@ function love.mousereleased(x, y, button, istouch)
end
end

function love.mousemoved(x, y, dx, dy, istouch)
local function nativeMouseMoved(x, y, dx, dy, istouch)
-- FREE-CAMERA LOOK FIRST, and only when one is up: `Game:cameraLook`
-- answers false on every other map and camera, so the touch path below
-- keeps every case it had. Warp back to centre after a real look so the
Expand Down Expand Up @@ -1016,6 +1016,34 @@ function love.mousemoved(x, y, dx, dy, istouch)
end
end

-- Source mouse wrappers see one desktop event. The native pointer path keeps
-- its touch synthesis, without also reporting that synthesis as a second
-- source touch event. Editors/importers and physical touch keep their owners.
function love.mousepressed(x, y, button, istouch)
local facade=package.loaded['src.core.Game3']
if Game and not(bootReport or editorMode or Importer or TouchEditor or istouch)
and facade and facade._pointer then
return facade._pointer('mousepressed',Game,nativeMousePressed,x,y,button,istouch)
end
return nativeMousePressed(x,y,button,istouch)
end
function love.mousereleased(x, y, button, istouch)
local facade=package.loaded['src.core.Game3']
if Game and not(bootReport or editorMode or Importer or TouchEditor or istouch)
and facade and facade._pointer then
return facade._pointer('mousereleased',Game,nativeMouseReleased,x,y,button,istouch)
end
return nativeMouseReleased(x,y,button,istouch)
end
function love.mousemoved(x, y, dx, dy, istouch)
local facade=package.loaded['src.core.Game3']
if Game and not(bootReport or editorMode or Importer or TouchEditor or istouch or Game.lookWarping)
and facade and facade._pointer then
return facade._pointer('mousemoved',Game,nativeMouseMoved,x,y,dx,dy,istouch)
end
return nativeMouseMoved(x,y,dx,dy,istouch)
end

function love.textinput(text)
if TouchEditor then return end
if Importer then return Importer:textinput(text) end
Expand Down
57 changes: 57 additions & 0 deletions src/compat/gen1/ScriptMap.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
-- Numeric source-facing tables, with synchronous native command writes.
-- Iteration uses owned values; neither the save nor a script context is lent.
local Iteration=require('src.compat.gen1.TableIteration')
local M={}
local function key(value)
value=tonumber(value)
if type(value)=='number'and value==value and value%1==0 and value>=0 and value<=65535 then return value end
end
function M.new(backend,kind)
assert(kind=='flags'or kind=='vars','invalid script map kind')
local state={backend=backend,kind=kind};local view={}
local function readable(k)
return k and(kind=='flags'or k>=0x4000 and not backend:isSpecial(k))
end
setmetatable(view,{
__index=function(_,k)
k=key(k);if not readable(k)then return nil end
if kind=='flags'then return backend:flagValue(k)end
return backend:varValue(k)
end,
__newindex=function(_,k,value)
k=key(k);assert(readable(k),'invalid persistent script map key')
if kind=='flags'then if value==nil then value=false end;backend:setFlag(k,value)
elseif value==nil then backend:clearVar(k)
else backend:setVar(k,value)end
end,
})
return Iteration.register(view,function(s)return s.backend:snapshot()[s.kind]end,state,function(s,k)
if s.kind=='flags'then return s.backend:nextFlag(k)end
return s.backend:nextVar(k)
end)
end
function M.replace(backend,kind,value)
assert(kind=='flags'or kind=='vars','invalid script map kind')
assert(type(value)=='table','invalid persistent script map')
-- Validate and normalize the complete replacement before any command can
-- mutate native state. No special variables belong to a persistent map.
local normalized={}
for k,v in pairs(value)do
local n=key(k)
assert(n and(kind=='flags'or n>=0x4000 and not backend:isSpecial(n)),'invalid persistent script map key')
if kind=='flags'then assert(type(v)=='boolean','invalid script flag value')
else assert(type(v)=='number'and v==v and v%1==0 and math.abs(v)<=9007199254740991,'invalid script variable value')end
assert(normalized[n]==nil or normalized[n]==v,'conflicting script map aliases')
normalized[n]=v
end
local current=backend:snapshot()[kind]
for k in pairs(current)do
if normalized[k]==nil then
if kind=='flags'then backend:setFlag(k,false)else backend:clearVar(k)end
end
end
for k,v in pairs(normalized)do
if kind=='flags'then backend:setFlag(k,v)else backend:setVar(k,v)end
end
end
return M
Loading