Repository navigation
Script runtime hardening, isolated threadpool states, new event model, dev tools - #24
Merged
Merged
Conversation
…ckage
- Move async.fetch request/response handling into LuaHttp and JSON<->Lua
conversion into api/util/LuaJson; AsyncLib keeps only the yield/resume glue.
- Sparse tables like {[2] = x} now encode as a JSON object instead of []
(previously silently dropped their contents). Empty response bodies decode
to nil instead of throwing an NPE.
- Fix the typo'd package ru.pyxiion.ignis.api.wrappertoLuaValue -> api.wrapper.
- Add LuaJsonTest and LuaHttpTest (the HTTP/JSON code had no coverage).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
Both interpreters now decrement a per-state countdown per instruction and call LuaState.checkpoint() every checkpointInterval instructions (default 100) instead of reading the interrupt flag on each one. checkpoint() services interrupt() requests and polls an optional Builder.checkpointHandler, which hosts can use for time limits. After the handler throws it is polled again on the next instruction, so code that catches the error with pcall is rejected too until it unwinds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
- ScriptWatchdog: Lua entered from the server thread (events, commands, scheduler, async resumes, chest GUI and mob AI callbacks, script loading) is stopped after 5 s (-Dpxignis.scriptTimeoutMs, 0 disables). It hooks into PxLuaNova's checkpoint handler, so it costs nothing per instruction. - reload() compiles every script before tearing anything down. A syntax error cancels the reload and the old scripts keep running; a script that fails at runtime is reported and the rest still load (previously one error stopped later scripts and command registration). /ignis reload now reports per-file errors. - Server start no longer fires "init" twice (reload() already fires it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
mc.sleep/mc.fetch were removed in 0.17.0 but README, MODRINTH, the guides, references and lua-types still used them. Also document that event handlers can await async calls directly and the new script time limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
Coroutines: - pcall/xpcall inside a coroutine run as protected frames of the frame interpreter, so code inside them can yield and resume correctly. Before, the callee ran in the recursive interpreter: a yield did not suspend it, the code after it ran immediately, and resume values were lost. - Yielding from Lua entered from Java (metamethods, table.sort comparators, load readers) now raises "attempt to yield across a C-call boundary" instead of silently corrupting the coroutine. - Resume values were copied one register too far after coroutine.yield, overwriting the next local. - Java functions can be coroutine bodies; yields from generic-for Java iterators work; the resumer's status goes back to "running". - Remove the unused virtual-thread coroutine model (lua_resume/lua_yield, thread factories, OrphanedThread). Other fixes found by the previously failing tests: - Arithmetic metamethods from the right operand got swapped arguments (5 - v called __sub(v, 5)); __le is looked up on both operands. - Unary minus on a non-numeric string returned 0 instead of an error. - Weak tables lost integer keys set while the array part grew. - load() with a reader did not stop at an empty string. - An enclosing xpcall handler ran for errors caught by an inner pcall. - print() re-encoded non-UTF-8 bytes. Tests: 698 pass. The remaining reference-output differences are excluded one by one, with reasons, in pxluanova-test/build.gradle, and CI now runs the PxLuaNova suite. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
nova.sync functions are compiled to JVM bytecode and never went through the interpreters' instruction countdown, so `nova.sync(function() while true do end end)()` could not be stopped by a checkpoint handler (PxIgnis' ScriptWatchdog). LuaJC now emits a call to LuaState.compiledBackwardJump() before every backward branch, and TailcallVarargs.eval calls it per tail call; it polls checkpoint() on the current state every 100 calls, with the same pcall-proof re-check after a rejection. No measurable slowdown on a 50M-iteration numeric loop. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
snapshot() turns values into a state-independent form on the source state's thread; materialize() rebuilds them in the target on its own thread. Tables are deep-copied (cycles, sharing and metatables kept), Lua functions are rebuilt from their prototype with copied upvalues (source globals become target globals, shared upvalues stay shared), library functions and tables are relinked by path, userdata goes through a pluggable policy, and anything else is an error naming the value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
Lua states are not thread-safe, but async.task("threadpool", ...) ran the
task's closure in the scripts' state on a pool thread, next to the server
thread: any global or shared table access could race.
- Tasks on an isolated executor (threadpool) now run in a fresh worker
state: the same sandbox and pure libraries plus its own `async`, but no
mc, register or script globals. The function, its upvalues and the
arguments are copied in, results are copied back (LuaTransfer); work a
worker sends to "main" runs in the scripts' state the same way.
- StateTransferPolicy: vectors are copied; players/worlds/entities and
tasks/promises/mutexes are refused with an error naming the value.
- One AsyncLib per state, created in ScriptEnvironment.rebuild. LuaMcApi
used to build it eagerly with stateProvider(), i.e. before the first
state existed, and would have kept using the first state after
/ignis reload.
- task:try() no longer reads results on the resolving thread.
Main-to-main tasks are unchanged (same state, no copies).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
Events: - Built-in events get new names (player.join, block.break, entity.hurt, ...) and pass one event table e; e:cancel(reason?) / e.cancelled. - Handlers run by numeric priority (higher first, aliases lowest..highest), cancelled events skip handlers without receiveCancelled, and a throwing handler cancels a built-in cancellable event. - Options: priority, receiveCancelled, throttle on mc.on and region:on, also accepted before the handler (Nova trailing blocks). - Old names keep working as LegacyEvent views (positional args, return false) with a deprecation warning; unknown names warn with a suggestion. - Region events enter/leave/move/death with e.region; region death now fires; throttle uses a tick clock instead of tick-handler countdowns. - Minecraft callbacks go through events/GameEvents. Dev tools: - ScriptErrors: errors and warnings go to the log and, rate-limited, to players with px.ignis.prompt_errors. - LuaLS types bundled in the jar and unpacked to config/ignis/.types, plus a .luarc.json created once. - /ignis watch on|off (auto reload on save), /ignis eval, /ignis status. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 288 |
| Duplication | 104 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
This was referenced Sep 28, 2026
PyXiion
added this pull request to stack #27
September 28, 2026 12:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Runtime safety
nova.synccode polls on backward jumps and tail-call loops.ScriptWatchdog: Lua running on the server thread longer than 5 s (-Dpxignis.scriptTimeoutMs) is stopped, andpcallcannot swallow the error./ignis reload: all scripts are compiled first; a syntax error cancels the reload and the old scripts keep running.PxLuaNova coroutines
pcall/xpcallrun as protected frames, so yielding insidepcallworks.pxluanovajob; release depends on it).Isolated threadpool tasks
asynctasks onthreadpoolrun in their ownLuaState(sandbox +async, nomc).LuaTransfer; userdata followsStateTransferPolicy.New event model
player.join,block.break,entity.hurt, …) and pass one event tablee; handlers cancel withe:cancel(reason?).lowest…highestare aliases for -200…200.receiveCancelled = true.priority,receiveCancelled,throttle) work onmc.onandregion:on, and may also come before the handler for Nova trailing blocks.return false, and a deprecation warning.enter/leave/move/deathwithe.region.throttleonly counted down while atickhandler existed.Dev tools
px.ignis.prompt_errors, rate-limited.config/ignis/.types, plus a.luarc.jsoncreated once.eis typed per event./ignis watch on|off: auto-reload when a script is saved./ignis eval <lua>: run Lua in the scripts' state;meis the calling player./ignis status: loaded scripts, handlers, commands, scheduled tasks and regions.Refactoring and docs
AsyncLib.demo.lua/items.lua, README and MODRINTH updated for the new API.docs/events.md.Breaking changes
mc.emitrefuses built-in event names; it now returnsfalsewhen a handler cancelled the event.Testing
maven.fabricmc.netfrom the dev environment): 151 pass. New suites:EventModelTest,DevToolsTest,AsyncIsolationTest,ScriptWatchdogTest,LuaTransferTest,LuaStateCheckpointTest.GameEvents,IgnisCommand,IgnisRuntime,PxIgnis.kt,Region.kt,ScriptEnvironment,LuaMcApi, mixins). CI needs to build it, and it should be checked on a server:/ignis watch,/ignis eval,/ignis status.typesextractionNotes
🤖 Generated with Claude Code
https://claude.ai/code/session_01X7gxWF7FdViJa9dKAZJiac
Generated by Claude Code