← Back to Home
Scripting Deep Dive

Three Languages.
Client & Server.
Infinite Possibilities.

Talx gives you three distinct scripting layers: IptScrae - the original 1995 Forth-like stack language, Lua (WebAssembly in the browser, or gopher-lua on the server), and Python via Pyodide. Each has a distinct performance profile and sweet spot. Use what fits.

⚡ IptScrae - Zero-overhead, 1995 compatible 🌙 Lua - Client WASM & Server plugins 🐍 Python - Full stdlib via Pyodide 🤖 Cyborg scripts - Intercept your chat 🔌 Server plugins - Hot-reloadable

Performance & Tradeoffs

All three client languages run in your browser - no server involvement. The server-side Lua plugin layer is separate and lives inside the Go binary. Here's the full picture.

Language Runtime Startup Cost Handler Speed Timeout Numbers Async? Best For
IptScrae JavaScript VM (in-browser) Instant Fastest 500 ms 32-bit int only No Classic room/hotspot scripts, legacy compatibility
Lua (client) Wasmoon WASM (in-browser) ~80 ms first load, cached Fast 1500 ms Lua 5.4 floats No (coroutines ok) Modern room scripts, cyborg logic, complex hotspot flows
Python (client) Pyodide WASM (in-browser) ~4–8 s first load, cached Medium 2000 ms Full Python numerics Yes (asyncio) Complex data processing, numpy/pandas, rapid prototyping
Lua (server plugin) gopher-lua sandboxed VM Per-plugin, <5 ms Fast 5000 ms (configurable) Lua 5.1 doubles Timers, HTTP fetch Server-wide automation, bots, chat moderation, HTTP integrations

🏃 Zero-Overhead Scripting

IptScrae executes in the same JS thread that renders the Palace canvas. It was designed for constrained 1990s hardware - opcode dispatch is essentially a switch statement. If you need a script that fires on every keystroke or every frame, IptScrae is the right choice. No WASM startup, no JIT warm-up, no GC pauses.

⚖️ The WASM Tradeoff

Both Lua (Wasmoon) and Python (Pyodide) run inside WebAssembly sandboxes. The first load in a fresh browser session pays a compilation cost - Pyodide is much heavier (~6 MB WASM + Python stdlib). After the first load, both are cached by the browser's service worker and subsequent loads are near-instant. If your users often clear cache or visit only occasionally, IptScrae's zero-download profile wins.

🔢 Integer vs Float

A classic IptScrae gotcha: all numbers are 32-bit signed integers. There is no floating point. Division truncates. Colors, room coordinates, and timer values all work fine, but if you need sub-pixel math or trig, switch to Lua (native floats) or Python (full IEEE 754 + numpy). This limitation doesn't affect 99% of room scripts.

🧵 Async & Timers

IptScrae and client Lua are synchronous. Handlers must complete before the next event fires. Server-side Lua plugins have access to palace.timer.after and palace.timer.every, plus palace.http.fetch for external API calls. Python (Pyodide) supports asyncio for deferred work - the most powerful option for client-side coroutines and delayed effects.

🔄 Hot Reload

Server Lua plugins support live hot-reload without restarting the server. The admin panel lets you disable, enable, and reload individual plugin bundles. The VM is discarded and re-created cleanly - all phantom bots owned by the plugin are automatically destroyed and the plugin's main.lua runs fresh. No reconnect required for connected users.

🏰 Palace Compatibility

IptScrae is 100% backward-compatible with scripts written for the original Palace (1996–2005). If you have an old .ipt file, paste it in and it works. Lua client scripts can call all the same host effects via the t.* API, but there is no mechanical translator - new scripts only. Server Lua maps 1:1 to the classic s-pluginapi.h C ABI.

The Original - Born 1995

IptScrae (pig Latin for "script") is a Forth-like stack-based language built into every Palace client since 1996. It's deterministic, fast, and proven - over 297 opcodes, all tested, all supported.

Stack-Based Postfix Notation

IptScrae uses Reverse Polish Notation (RPN): operands come before operators. Values are pushed onto a stack; operators pop and push results.

IptScrae · Basic Concepts
-- Postfix: push 3, push 4, add → 7 on stack
3 4 + -- stack: [7]

-- Push a string then SAY it
"Hello, world!" SAY

-- Variables: => assigns, GLOBAL name retrieves
42 => myNum
myNum ITOA SAY -- says "42"

-- ON events trigger blocks
ON ENTER {
  "Welcome to my room!" SAY
}

ON HOTSPOT CLICK {
  USERID GETUSERNAME => who
  who " touched the orb!" CAT SAY
  7 GOTOROOM
}

Classic vs Extended

Classic IptScrae matches the 1996–2005 Palace spec. Extended IptScrae (enabled in Preferences → "Enable extended Iptscrae") adds 256-color palettes, GETROOMOPTIONS, and modern Palace additions.

⚡ When to use IptScrae

  • You have existing scripts from old Palace worlds
  • The script fires very frequently (every chat, every move)
  • You want zero install cost for visitors - no WASM to download
  • You're doing simple string manipulation and room navigation
  • You need maximum compatibility with third-party Palace clients

⚠ Limitations

  • Integers only - no floating point
  • No closures, no first-class functions
  • No async - 500 ms hard timeout per handler
  • Stack overflows are silent no-ops, not exceptions

Full Reference

The complete searchable command reference (all 297+ opcodes, classic and extended) lives in the client help system and at:

📖 IptScrae Command Reference → Classic Iptscrae Guide (1990s docs) →

Modern Scripting in the Browser

Client Lua runs Lua 5.4 via Wasmoon WebAssembly - the same VM as the desktop engine, fully sandboxed in your browser tab. It mirrors every IptScrae host effect through a modern t.* API, plus adds closures, proper error messages, coroutines, and floating-point math.

Lua (client) · Event API · Lua 5.4 / Wasmoon WASM
-- Register events with t.on()
t.on("ENTER", function(ctx)
  t.chat.say("Welcome! You arrived from room " .. ctx.fromRoom)
end)

-- Intercept outgoing chat
t.on("OUTCHAT", function(ctx)
  if ctx.chatstr:sub(1, 1) == "!" then
    -- prefix ! means shout in caps
    ctx.chatstr = ctx.chatstr:upper()
  end
end)

-- Register a /command
t.command("spin", function(ctx)
  for i = 0, 7 do
    t.avatar.face(i)
  end
end)

-- Wrap a hotspot for OOP-style access
local door = t.wrapSpot(3)
t.on("HOTSPOT_CLICK", function(ctx)
  if ctx.id == 3 then
    door.lock(not door.locked())
  end
end)

🌙 When to use client Lua

  • You need real numbers, trig, or proper string handling
  • You're building reusable room scripts with closures and local state
  • You want readable error messages and stack traces (vs silent IptScrae failures)
  • You need to intercept and mutate events (rewrite chat, redirect doors)
  • Your handler logic exceeds IptScrae's 500 ms comfort zone
EventTriggered When
ENTERYou enter a room
LEAVEYou leave a room
OUTCHATYou send a chat message (mutable)
INCHATSomeone else sends chat
HOTSPOT_CLICKA hotspot is clicked
ALARMA timer fires
PROPCHANGEAvatar/prop changes
USERMOVEA user moves in the room
📖 Palace Lua API Reference →

Python in the Browser via Pyodide

Write Palace scripts in pure Python - running entirely in your browser via Pyodide WebAssembly, zero server involvement. The full CPython 3.12 stdlib is available. Import random, json, math, even numpy or pandas (loaded on demand).

Python (client) · Pyodide WASM · CPython 3.12
from palace import room, user, chat, avatar

# Decorate event handlers with @room.on_*
@room.on_enter
def greet(evt):
  me = user.me()
  chat.say(f"Hello from Python, {me.name}! You came from room {evt.from_room}.")

# asyncio works - deferred effects
import asyncio

@room.on_hotspot("mystery_box")
async def open_box(evt):
  chat.say("Opening...")
  await asyncio.sleep(1.5)
  import random
  prizes = ["🎁 a rare prop!", "🐸 a frog!", "💎 a gem!"]
  chat.say(f"You found {random.choice(prizes)}")

# Full stdlib available
@room.on_enter
def stamp(evt):
  from datetime import datetime
  now = datetime.utcnow().strftime("%H:%M UTC")
  chat.say(f"Clock says: {now}")

🐍 When to use Python

  • You know Python and want to script without learning a new language
  • You need the full stdlib: json, re, datetime, math
  • You want asyncio for timed / multi-step sequences
  • You're doing statistical or data-heavy processing
  • Startup cost is acceptable (users stay in the room, not first-second impressions)

⚠ Pyodide Startup Warning

The first time a user visits a room with a Python script, Pyodide downloads ~6 MB of WebAssembly and Python stdlib. On a fast connection this takes 2–4 seconds; on slow mobile it can take 10+ seconds. Subsequent visits use the browser cache - effectively free. If instant load is required, use IptScrae or client Lua instead.

📖 Palace Python API Reference →

Cyborg Scripts

A cyborg script is personal - it runs on your client, intercepting your outgoing chat before it reaches the server. Use it to create chat macros, auto-formatters, personal bots, and shortcut commands. Your cyborg script can be written in IptScrae, Lua, or Python.

🤖 What is a Cyborg?

The original Palace called personal automation scripts "cyborgs" - a human and a bot at the same time. Your cyborg script sees every message you type before it leaves your machine. It can block the message, rewrite it, or fire side effects (change your avatar face, navigate to a room, etc.). Other users see the result, not the raw input.

To edit your cyborg script:

  1. Open the client menu and click Edit Cyborg
  2. Select your preferred language (IptScrae, Lua, or Python) from the editor header
  3. Write your script - it activates immediately on save
IptScrae
Lua
Python
IptScrae · Cyborg Example - Marketplace Offer Formatter
-- Cyborgs Marketplace helper: type ".offer propname" to broadcast a
-- nicely formatted offer message to the room.
-- (Marketplace server integration: dead link - coming soon)

ON OUTCHAT {
  -- CHATSTR is your outgoing message
  CHATSTR => msg
  msg 7 LEFT ".offer " == IF {
    -- Strip the ".offer " prefix, keep the prop name
    msg LEN => len
    msg 8 len MID => propName
    -- Build offer message
    "✦ Offering: " propName CAT
    " - whisper me to trade!" CAT
    => CHATSTR -- rewrite outgoing chat
  }
}

🏪 Cyborgs Marketplace

The Cyborgs Marketplace is a community repository of shareable cyborg scripts. Find scripts for prop trading, auto-greeting, moderation helpers, games, and more.

Browse Marketplace
Submit a Cyborg Script
Top-rated Cyborgs
Cyborg Script Guidelines

Server Plugins in Lua

Server plugins run inside the Go binary in a sandboxed gopher-lua VM - one per plugin, hot-reloadable without a server restart. They have full access to the server event stream, all connected users and rooms, the chat pipeline, and an outbound HTTP allowlist.

🔌 Server plugins vs Client scripts

Client scripts run in your browser and affect only you. Server plugins run on the server and affect every connected user. A server plugin can listen to every chat message from every user, send god-messages, move users between rooms, create NPC phantom bots, and call external HTTP APIs. With great power comes the usual caveats - only server admins can install plugins.

Plugin Bundle Format

A plugin is a zip file (or directory) containing a plugin.json manifest and a main.lua entry point:

plugin.json · Manifest
{
  "id": "marketplace-greeter",
  "name": "Marketplace Greeter",
  "version": "1.0.0",
  "apiVersion": 1,
  "entry": "main.lua",
  "description": "Welcome users and announce marketplace",
  "permissions": ["chat", "user", "command"]
}
main.lua · Server Plugin · gopher-lua
-- Greet users on login, announce marketplace
-- Cyborgs Marketplace link: dead - coming soon

palace.on("LOGON", function(msg)
  local u = palace.user.get(msg.userId)
  if not u then return end
  palace.chat.god(msg.userId,
    "Welcome, " .. u.name .. "! Type /market to browse.")
end)

-- /market command broadcasts marketplace info
palace.command.register("market", 0, function(ctx)
  palace.chat.user(ctx.userId,
    "🏪 Marketplace: https://talx.fyi/market (coming soon)")
end)

-- Repeating timer: hourly server announcement
palace.timer.every(3600, function()
  palace.chat.global("✦ Marketplace is open! Type /market for details.")
end)

-- Cancel/rewrite bad words before they reach chat
palace.on("OUTCHAT", function(msg)
  if msg.text:find("spam") then
    return false -- cancel the message
  end
end)

Full Host API

The server plugin API maps 1:1 to the classic Palace 4.5 s-pluginapi.h C ABI. If you wrote a C plugin in the 2000s, all the same capabilities are here.

NamespaceKey Functions
palace.user.*get, list, rank, moveToRoom, gag, logoff
palace.room.*get, list, users, message
palace.chat.*god, user, room, global, whisper, talk
palace.command.*register, unregister
palace.timer.*after, every, cancel
palace.store.*global.get/set/del, user.*, room.*
palace.http.*fetch (allowlist), route (serve endpoints)
palace.user (bots)createPhantom, walkTo, destroyPhantom
palace.loose.*add, move, delete loose props
palace.spot.*setState, move, lockDoor
palace.util.*sha256, jsonEncode, randomPoint, throttle
📖 Server Lua Plugin SDK Reference → 🌙 gopher-lua on GitHub (official Lua 5.1 VM) → 📚 Lua 5.1 Reference Manual (lua.org) →

Building NPC Bots

Phantom users are server-created bot accounts with no underlying client connection. They appear in rooms, can walk, talk, change avatars, and react to events - entirely server-driven. A wandering NPC, a shopkeeper, an auto-moderator - all implemented as phantom users.

NPC Bot Example · main.lua
-- Create a shopkeeper NPC at room entrance
local botId = palace.user.createPhantom("Shopkeeper", 0, {
  face = 5, color = 2
})

-- Respond when users enter the room
palace.on("USERENTER", function(msg)
  if msg.roomId == 0 then
    local u = palace.user.get(msg.userId)
    palace.chat.talk(botId, msg.userId,
      "Greetings " .. u.name .. "! Browse our wares.")
  end
end)

-- Wander around the room slowly
local function wander()
  local x, y = palace.util.randomPoint(0)
  palace.user.walkTo(botId, x, y, {
    speed = 40,
    onArrive = function() palace.timer.after(5, wander) end
  })
end
palace.timer.after(3, wander)

🤖 Phantom User Capabilities

  • Appear as real users in any room
  • Walk smoothly with configurable speed via palace.user.walkTo
  • Send any chat type (talk, whisper, god, room, global)
  • Change their own avatar, face, color, and props
  • Be moved between rooms
  • React to any server event
  • Automatically destroyed on plugin reload

Which Language Should I Use?

⚡ Choose IptScrae if…

You have an existing .ipt script from an old Palace world, you need the absolute lowest latency (fires on every frame), you want zero-download compatibility for users on slow connections, or you're doing simple room greetings and navigation. The 297-opcode spec is stable and won't change.

🌙 Choose client Lua if…

You want to write new scripts with a modern language. Closures, pattern matching, t.wrapSpot, proper error messages, 1500 ms timeout, and real floating-point numbers. About 80 ms first-load overhead, then cached. Ideal for most new room and cyborg scripts.

🐍 Choose Python if…

You're more comfortable with Python than Lua, you need asyncio for timed sequences, or you need the full stdlib or third-party packages. The 4–8 second first-load cost is fine for rooms where users will stay a while.

🔌 Choose a server plugin if…

You need something that affects all users - moderation, server-wide announcements, NPC bots, external webhook integrations, or command extensions. Requires server admin access. Hot-reloadable, zero user impact on reload.

💡 You can mix all four

There's no restriction on using multiple languages in the same world. A room might have an IptScrae hotspot script for instant legacy feel, a Lua cyborg for personal chat macros, and a server plugin running an NPC bot. Each layer is fully independent - they don't interfere with each other.