Skip to content

proposal: client-side Lua scripts and package runtime #20

Description

@PyXiion

Summary

Allow script packages to include client-side Lua scripts and client assets.

The server sends the current package revision to clients when they join and distributes new revisions during hot reload without requiring a server restart.

Client scripts run in isolated, package-scoped runtimes and expose a dedicated client API. This proposal provides the foundation for future client-side UI, input, rendering, and game-client APIs.

Related proposals:

Package Layout

example_package/
  package.json
  server/
    main.lua
  client/
    main.lua
    shop.lua
  assets/
    shop.rml
    shop.rcss

The existing package manifest proposal should be extended rather than replaced:

{
  "id": "example_package",
  "version": "1.0.0",
  "entry": "server/main.lua",
  "dependencies": {},
  "permissions": [
    "server.events.register",
    "client",
    "client.ui",
    "network.channel.send",
    "network.channel.receive"
  ],
  "client": {
    "entry": "client/main.lua",
    "resources": [
      "client/**",
      "assets/**"
    ],
    "required": false
  }
}

Existing top-level server package fields remain valid. The client section is additive.

Client Runtime

A client entrypoint receives:

local client = require "client"
local async = require "async"
local ref = require "ref"
local net = require "net"

The client runtime does not receive the server-side mc API by default.

The initial client API should provide:

client.on("start", function()
end)

client.on("stop", function(reason)
end)

client.on("tick", function(delta)
end)

client.log("Package started")
client.warn("Missing item")
client.resource("assets/shop.rml")

Client event behavior should follow the event proposal, including priorities, one-shot handlers, filters where applicable, and automatic event-group cleanup.

The client runtime should reuse the scheduler and async semantics from the existing proposals rather than create a separate incompatible scheduling system.

Package Revisions

Each client revision is identified by:

package ID + version + content hash

The content hash changes whenever a client script or declared client resource changes.

The client caches complete package revisions by content hash. A package must not activate from a mixture of files belonging to different revisions.

Join Loading

When a player joins:

  1. The server sends the current package catalog.
  2. The client compares package revision hashes with its cache.
  3. Missing revisions are downloaded.
  4. The complete bundle is verified.
  5. Dependencies are checked.
  6. A fresh isolated runtime is created.
  7. The client entrypoint is loaded.
  8. The package receives start.
  9. The runtime becomes active.

The server resolves external package imports. The client does not fetch arbitrary GitHub URLs or other remote resources independently.

Optional package failure disables only that package. Required package failure must follow an explicit server policy: reject activation or prevent joining.

Hot Reload

Hot reload uses staged activation:

active revision A
  -> download revision B
  -> verify revision B
  -> initialize revision B
  -> activate revision B
  -> stop revision A

The current revision remains active until the replacement has initialized successfully.

If initialization fails, the old revision remains active.

If revisions A, B, and C arrive while B is downloading, B is stale and must not activate. The client should converge on the newest revision, C.

A new revision gets a fresh runtime. This avoids stale require caches, old closures, old event handlers, and partially reset global state.

Package-local state is not preserved across reload by default. Persistent state must use an explicit system such as net.shared, net.channel, or a future client storage API.

Lifecycle and Cleanup

Stopping a package closes:

  • Event subscriptions.
  • Timers and scheduled tasks.
  • Coroutines and async operations.
  • Reactive watchers.
  • Network handles.
  • UI contexts introduced by dependent proposals.
  • Resource references.

Every client package should have an implicit task and event group associated with its package revision. Reloading or stopping the package clears that group.

Shutdown must be bounded. A package must not be able to block a replacement revision indefinitely.

Networking

Networking remains defined by #13.

Client scripts may use:

local net = require "net"

local actions = net.channel("example_package:actions")

actions:send({
  kind = "select",
  value = "apple",
})

The client runtime supplies the package capability context, but does not add a second RPC system.

Old network handles close when the old runtime stops. The new runtime receives fresh handles and follows the normal pending -> ready -> closed lifecycle.

Transient channel messages sent to the old runtime are not replayed automatically. Persistent or recoverable state should use net.shared or an explicit channel snapshot protocol.

Capabilities

Capability permissions follow #12.

Suggested target-qualified capabilities:

client
client.ui
client.input
client.render
client.world.read
client.player.read
network.channel.send
network.channel.receive

Runtime checks must apply to calls made from:

  • Event handlers.
  • Timers.
  • Watchers.
  • Coroutines.
  • Async continuations.
  • UI callbacks.

A package with only client cannot access UI, input, world data, or networking unless those permissions are also granted.

This is capability control, not a perfect sandbox. A client cannot be protected from a malicious server that intentionally sends malicious code.

Dependencies

Client dependencies use the package dependency mechanism from #5. Dependencies must:

  • Be present before activation.
  • Load before dependents.
  • Use compatible revisions.
  • Reject dependency cycles.

Cross-package script, resource, and API access is not implicit and requires a future explicit capability.

Errors and Diagnostics

Client errors should be isolated to the package and reported with:

package ID
package revision
client/server side
script path
callback or event name
load state
reload state
stack trace

Useful package states include:

downloaded
verified
loading
active
stopping
failed
disabled
stale

Repeated or fatal errors may disable the package without affecting unrelated packages.

Configuration and Secrets

Client scripts may receive explicitly public client configuration, but server configuration and secrets must never be sent to the client.

Client scripts must not receive a client equivalent of the server secret API.

Non-Goals

  • Defining the UI API.
  • Defining client rendering APIs.
  • Defining client world or player wrappers.
  • Providing arbitrary filesystem access.
  • Loading native libraries.
  • Exposing Java or Minecraft client objects directly.
  • Guaranteeing protection from a malicious server.
  • Reusing the server API unchanged.
  • Allowing server code to directly manipulate client UI.

Acceptance Criteria

Examples

Client Entrypoint

local client = require "client"
local net = require "net"

local actions

client.on("start", function()
  client.log("Shop client started")
  actions = net.channel("shops:actions")
end)

client.on("stop", function(reason)
  client.log("Shop client stopped: " .. reason)
end)

client.on("tick", function(delta)
  -- Optional per-tick client work.
end)

Package Resource

local client = require "client"

local document = client.resource("assets/shop.rml")
client.log("Using resource: " .. document)

Client resources are resolved within the package and cannot escape its resource namespace.

Hot Reload

A package revision can keep durable state on the server while client-local runtime state is rebuilt:

local net = require "net"

local state = net.shared("shops:state")
local actions = net.channel("shops:actions")

state:ready(function(shared)
  -- Runs again when a new client revision reacquires the handle.
  print(shared:get().title)
end)

actions.handler = function(message)
  -- The handler belongs to this package revision and is removed on reload.
  print(message.kind)
end

Optional Package Failure

A failure in an optional client package disables that package only. Other packages and the client remain active.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requesthelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions