Improved Docs, AnimationStepApplyMesh+Materials, some other minor tweaks
This commit is contained in:
114
CLAUDE.md
Normal file
114
CLAUDE.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Integration Project
|
||||
|
||||
Unreal Engine 5.5.4 game project running on Linux (PopOS). This is **Luprex**, a game engine built on top of Unreal that uses Lua as its scripting language. The engine handles multiplayer networking automatically so scripters don't write networking code.
|
||||
|
||||
## Build System
|
||||
|
||||
- Use `build.py` for all builds. Do NOT follow Epic's standard build instructions.
|
||||
- `build.py all` — full rebuild (engine, game, intellisense, project files)
|
||||
- `build.py c++` — lightweight rebuild (only if you've only edited C++ files in this repo)
|
||||
- Lua and Blueprint edits don't require a rebuild.
|
||||
- Do not edit `Integration.uproject` directly — edit `Integration.uproject.tpl.json` instead, then run `build.py`.
|
||||
- Do not run builds yourself. The IDE builds automatically when the user runs the code.
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `Source/Integration/` — Game module C++ source (Unreal-side driver code)
|
||||
- `Content/` — Unreal assets
|
||||
- `luprex/` — The Luprex DLL (driven portion of the engine)
|
||||
- `Docs/` — Documentation. When trying to understand this system, start with the markdown files in the Docs directory.
|
||||
- `Config/` — Unreal config files
|
||||
- `EnginePatches/` — Custom engine modifications
|
||||
|
||||
## Architecture: Driver / Driven Separation
|
||||
|
||||
The engine is split into two halves:
|
||||
|
||||
- **Driven portion (Luprex DLL)** — All game logic. Deterministic, OS-independent, no I/O. Pure standard-compliant C++. Organized as a library with class `DrivenEngine` as the top-level API.
|
||||
- **Driver (EXE or Unreal integration)** — Handles all I/O (sockets, files). Feeds events into the driven portion via `struct EngineWrapper`, a pure C interface of function pointers. There's a command-line driver and an Unreal driver.
|
||||
|
||||
The driven portion never calls into the driver. Output goes through polled buffers. This separation enables deterministic replay for debugging: the driver logs all events, and can replay them to reproduce crashes.
|
||||
|
||||
## Architecture: World Models and Predictive Reexecution
|
||||
|
||||
There are four types of world models, each a separate `World` instance with its own Lua interpreter:
|
||||
|
||||
- **Master** (server, one) — authoritative state, executes commands immediately when they arrive.
|
||||
- **Server-synchronous** (server, one per client) — executes commands when the acknowledgement is issued.
|
||||
- **Client-synchronous** (client, one) — executes commands when the same acknowledgement arrives; determinism keeps it in perfect sync with its server-synchronous counterpart.
|
||||
- **Asynchronous** (client, one) — snapshot of client-synchronous with predictions applied for responsive rendering; rolled back when server confirms.
|
||||
|
||||
The synchronous models lag behind the master but stay in lockstep with each other. The asynchronous model fills the latency gap for the player.
|
||||
|
||||
Two update channels flow into the synchronous models:
|
||||
1. **Command acknowledgements** — for the client's own actions, keeping the two synchronous models in lockstep.
|
||||
2. **Difference transmission** — for everything else (other players' actions, server-side events, tangibles entering/leaving visibility).
|
||||
|
||||
See `Docs/Predictive Reexecution.md` for the full explanation.
|
||||
|
||||
## Architecture: Lua / Unreal Separation
|
||||
|
||||
Lua scripts have no access to the Unreal API whatsoever. The scripter works with plain Lua tables, animation queues of key-value tuples, and coroutines. There are no "unreal bindings." The Luprex DLL is engine-agnostic — Unreal (or any other front end) interprets the animation queues and renders accordingly.
|
||||
|
||||
Small concessions to reality exist in animation step tuples:
|
||||
- `bp` — names which Tangible Actor blueprint to use for rendering
|
||||
- `mat_color` — a persistent value that hints at material color
|
||||
These are still just key-value data; the script never touches the Unreal API directly.
|
||||
|
||||
## Architecture: Tangibles
|
||||
|
||||
Tangibles are game objects. Each has:
|
||||
- A **Lua table** — the scripter stores arbitrary game data here.
|
||||
- An **animation queue** — a fixed-length sequence of key-value animation steps.
|
||||
- A **C++ tangible** — holds the ID, animation queue, positional tracker, etc.
|
||||
- A **metatable** — engine-reserved; contains __id, __index (class), __threads.
|
||||
|
||||
Animation steps contain **transient** values (like `action`) that don't propagate, and **persistent** values (like `xyz`, `facing`, `plane`) that carry forward automatically.
|
||||
|
||||
On the Unreal side, **Tangible Actor blueprints** (TangibleStaticMesh, TangibleSkeletalMesh, TangibleCharacter) monitor the animation queue and perform the visual animations. Custom blueprints can interpret the queue in any way they want.
|
||||
|
||||
## Architecture: Lua Environment
|
||||
|
||||
- **Patched Lua runtime** — deterministic table iteration, deterministic table length, flag bits on tables, generalized less-than, C++ exceptions instead of longjmp, and more. See `Docs/A Summary of our Lua Patches.md`.
|
||||
- **LuaStack API** — custom C++ API replacing the standard Lua C API. Uses `LuaDefStack`/`LuaExtStack` with `LuaArg`/`LuaVar`/`LuaRet` slots mapped to stack positions. See `Docs/Our In-House Lua API.md`.
|
||||
- **LuaDefine macro** — declares Lua-callable C++ functions and auto-registers them in a global registry for automatic insertion into the Lua environment.
|
||||
- **eng::malloc heap** — custom deterministic memory allocator for the driven portion, ensuring reproducible addresses during replay.
|
||||
|
||||
## Architecture: Determinism
|
||||
|
||||
The driven portion must be fully deterministic so that synchronous models stay in lockstep and event replay works. Rules: no true random numbers, no iterating unordered maps, no real-time clocks, no threads (with carefully sandboxed exceptions). See `Docs/The Event-Driven Structure of the Engine.md`.
|
||||
|
||||
## Architecture: GUI System
|
||||
|
||||
Blueprints call into Lua via two mechanisms:
|
||||
- **Invokes** — change world state, forwarded to server, executed in order per predictive reexecution rules.
|
||||
- **Probes** — read-only, return data to blueprints, run locally on client.
|
||||
|
||||
Look-at widgets, hotkeys, and menus are built on top of this. The menu system is implemented entirely in "user space" Lua and blueprint code. See `Docs/Displaying Widget Blueprints.md`.
|
||||
|
||||
## Key Documentation
|
||||
|
||||
- `Docs/Predictive Reexecution.md` — how the four world models stay in sync
|
||||
- `Docs/The Event-Driven Structure of the Engine.md` — driver/driven separation, determinism, replay
|
||||
- `Docs/Multipass Difference Transmission.md` — the algorithm for syncing Lua table graphs
|
||||
- `Docs/Animation Queues and Tangible Actors.md` — how blueprints interpret animation queues
|
||||
- `Docs/Our In-House Lua API.md` — the LuaStack API (LuaDefStack, LuaExtStack)
|
||||
- `Docs/A Summary of our Lua Patches.md` — all modifications to the Lua runtime
|
||||
- `Docs/Major Data Structures.md` — World, tangibles, threads, classes, source database
|
||||
- `Docs/Displaying Widget Blueprints.md` — GUI system (invokes, probes, look-at widgets, menus)
|
||||
- `Docs/Global Variables.md` — different types of global data and their transmission rules
|
||||
- `Docs/Correct Implementation of Blocking Operations and NoPredict.md` — how to handle blocking ops
|
||||
- `Docs/Difference Transmission with Threads.md` — why concurrent diff transmission is hard
|
||||
|
||||
## Session Startup
|
||||
|
||||
At the beginning of every session, do a directory listing of these three directories so you know what files are available:
|
||||
- `Docs/` — documentation
|
||||
- `Source/Integration/` — Unreal-side C++ code
|
||||
- `luprex/cpp/core/` — Luprex DLL core C++ code
|
||||
|
||||
These two code directories contain 99% of the code we'll be working on together.
|
||||
|
||||
## Current Status
|
||||
|
||||
(Use this section to track what we're working on across sessions.)
|
||||
@@ -88,3 +88,6 @@ UIScaleCurve=(EditorCurveData=(Keys=((Time=480.000000,Value=0.444000),(Time=720.
|
||||
+CollisionChannelRedirects=(OldName="PawnMovement",NewName="Pawn")
|
||||
+CollisionChannelRedirects=(OldName="Clickable",NewName="LookAtDetection")
|
||||
|
||||
[/Script/GameplayDebugger.GameplayDebuggerConfig]
|
||||
ActivationKey=None
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
58
Docs/About Determinism.md
Normal file
58
Docs/About Determinism.md
Normal file
@@ -0,0 +1,58 @@
|
||||
### About Determinism
|
||||
|
||||
The driven portion of the Luprex engine is deterministic. This document explains what that means and why it matters. For the specific rules you must follow to maintain determinism, see "The Event-Driven Structure of the Engine."
|
||||
|
||||
## Two Degrees of Determinism
|
||||
|
||||
There are two distinct degrees of determinism in the engine, each serving a different purpose.
|
||||
|
||||
**Value-level determinism** is the property that the server-synchronous and client-synchronous models stay in the same logical state. These two models run on different machines and receive the same command acknowledgements in the same order. Value-level determinism guarantees that they end up containing the same Lua tables with the same keys and values. If you go into both models and print things out, everything looks the same. However, a Lua table in one model is not necessarily at the same memory address as the corresponding table in the other, because they are running on different machines with different memory layouts. The two models are *equal* in the Lisp sense of `equal`, but not in the sense of `eq`.
|
||||
|
||||
**Bit-exact determinism** is the property that a recorded event log, when replayed into a fresh DrivenEngine, reproduces the original execution right down to the memory level: every data structure is at the same address, every byte of memory is the same. This is the `eq` level of equivalence.
|
||||
|
||||
The engine currently aims for both, but they serve different purposes and impose different costs.
|
||||
|
||||
## Value-Level Determinism: Synchronous Model Pairing
|
||||
|
||||
Value-level determinism is what makes the multiplayer architecture work. It is non-negotiable.
|
||||
|
||||
Luprex uses four types of world models to handle multiplayer networking (see "Predictive Reexecution" for the full explanation). Two of these models are critical to understand here:
|
||||
|
||||
- The **server-synchronous** model runs on the server.
|
||||
- The **client-synchronous** model runs on the client.
|
||||
|
||||
These two models receive the same command acknowledgements in the same order. Because the driven portion is deterministic at the value level, the two models always end up in the same logical state: the same Lua tables, the same values, the same game state. They never need to exchange full state to stay in sync.
|
||||
|
||||
The two models are running on different machines, so naturally they have different memory layouts and different pointer addresses. That's fine. All that matters is that the values match. This is why value-level determinism is sufficient for synchronous model pairing.
|
||||
|
||||
The constraints that maintain value-level determinism are:
|
||||
|
||||
- **Deterministic Lua table iteration.** We patch the Lua runtime so that iterating over a table always produces keys in the same order, regardless of memory layout. Without this, two engines processing the same commands could iterate tables in different orders and produce different results.
|
||||
- **No iterating over unordered maps.** Unordered maps produce elements in an order that depends on memory addresses. Since addresses differ between the two models, iteration order would differ, breaking value-level determinism. (An exception: iterating an unordered map and then immediately sorting the results into a predictable order is allowed, because the randomness is sandboxed.)
|
||||
- **No genuinely random numbers.** Pseudorandom numbers are fine as long as the state is privately owned by the driven portion and seeded deterministically.
|
||||
- **Controlled use of real-time clocks.** The driven portion (the Luprex DLL) cannot call system functions to obtain the current time, because the result would differ between runs and between machines. However, the driver can feed the current time into the driven portion as an event. Since events are the same during paired execution and during replay, the time value is deterministic from the driven portion's perspective.
|
||||
|
||||
## Bit-Exact Determinism: Replay Debugging
|
||||
|
||||
Bit-exact determinism enables replay debugging. It is valuable but expensive, and its cost-benefit tradeoff is an open question.
|
||||
|
||||
As the server runs, the driver can write a log of every event it feeds into the driven portion. Later, a new DrivenEngine can be created and fed those same events from the log file. The goal of bit-exact determinism is that during this replay, the DrivenEngine does the *exact* same thing it did during the live run, right down to every data structure being at the same memory address.
|
||||
|
||||
Why does this matter? If the server crashed during the live run, the replay will crash in exactly the same way. You can run the replay inside a debugger, single-step right up to the crash, and examine the exact same pointers and memory layout that existed during the original crash.
|
||||
|
||||
Value-level determinism alone is not sufficient for this. If the replay produces the same logical state but at different memory addresses, then pointer-related bugs (buffer overruns, use-after-free, etc.) might not reproduce. Bit-exact determinism ensures they do.
|
||||
|
||||
The additional constraints that maintain bit-exact determinism, beyond those needed for value-level determinism, are:
|
||||
|
||||
- **The eng::malloc heap.** A custom memory allocator positioned at a fixed address, used exclusively by the driven portion. Because the driven portion is deterministic, the sequence of allocations and frees is identical between the live run and the replay, so every data structure ends up at the same address. See "The Event-Driven Structure of the Engine" for details.
|
||||
- **No threads in the driven portion.** Thread scheduling is nondeterministic at the OS level. Even if two threaded programs produce the same final values, the interleaving of operations differs between runs, which would cause memory allocations to occur in different orders and at different addresses.
|
||||
|
||||
Note that the constraints for value-level determinism (deterministic table iteration, no unordered maps, etc.) also contribute to bit-exact determinism. But they are *required* for value-level determinism regardless. The eng::malloc heap and the no-threads rule are the additional cost imposed specifically by the bit-exact guarantee.
|
||||
|
||||
## The Practical Distinction
|
||||
|
||||
If the engine ever relaxed its determinism requirements, the value-level constraints would remain because they are essential to the multiplayer architecture. The bit-exact constraints (eng::malloc, no threads) could theoretically be dropped if replay debugging were deemed not worth the cost. Dropping the no-threads rule in particular would be a significant performance benefit.
|
||||
|
||||
## Lua Scripters Don't Need to Worry
|
||||
|
||||
The Lua environment is carefully sandboxed to be deterministic at both levels without any effort from the scripter. Lua's random number generators are seeded pseudorandom generators owned by the driven portion. Table iteration is patched to be deterministic. Lua "threads" (coroutines) are not real OS threads and don't run concurrently. The scripter writes ordinary Lua code and gets determinism for free.
|
||||
@@ -30,7 +30,7 @@ This document contains several things:
|
||||
Suppose you want a character to jump up and down in-place. Typically, you would use the lua function *tangible.animate* to achieve this:
|
||||
|
||||
```lua
|
||||
tangible.animate(actor, nil, {action='jump', height=3.0}))"
|
||||
tangible.animate{tan=actor, anim={action='jump', height=3.0}}
|
||||
```
|
||||
|
||||
This function adds an *animation step* to the character's *animation queue*. An animation queue is just a sequence of animation steps. Its length is set to a fixed value, perhaps 10 steps. So if you add a step 11, then the oldest step is automatically discarded.
|
||||
@@ -277,7 +277,7 @@ The routine FinishAnimation takes an *lxAnimationStep* as a parameter. This is s
|
||||
*FinishAnimation* also takes three boolean parameters: Auto Update XYZ, Auto Update Plane, and Auto Update Facing. These require some explanation. Suppose that the lua programmer pushes an animation step that looks like this:
|
||||
|
||||
```lua
|
||||
tangible.animate(actor, nil, {action="emote", animation="dance", xyz={1,2,3}})
|
||||
tangible.animate{tan=actor, anim={action="emote", animation="dance", xyz={1,2,3}}}
|
||||
```
|
||||
|
||||
Since "play an emote" isn't a travel command like "moveto" or "warpto", the lua scripter shouldn't have specified a destination XYZ, but he did so anyway, and now we have to recover from the mistake. Specifically, the tangible actor really does need to move to the new XYZ coordinate, otherwise bad things happen.
|
||||
@@ -293,9 +293,9 @@ Suppose that in your game, players can teleport from one place to another by cas
|
||||
Here is the tricky scenario: player T, the teleporter, is standing in front of player V, the viewer. Player V can see player T. Then, player T casts the teleport spell, and executes this lua code:
|
||||
|
||||
```lua
|
||||
tangible.animate(actor, nil, {action="sparkle", particles=999})
|
||||
tangible.animate{tan=actor, anim={action="sparkle", particles=999}}
|
||||
|
||||
tangible.animate(actor, nil, {action="warpto", xyz={1000000, 0, 0}})
|
||||
tangible.animate{tan=actor, anim={action="warpto", xyz={1000000, 0, 0}}}
|
||||
```
|
||||
|
||||
Luprex has no idea how long these animations take to execute. As far as Luprex is concerned, they are instantaneous. As soon as player T executes that code, Luprex thinks that player T has moved to {1000000, 0, 0}, a million miles away from player V.
|
||||
@@ -313,9 +313,9 @@ In order for Unreal to actually discard a Tangible Actor, two conditions must be
|
||||
Now, let's consider a small variation of the lua commands:
|
||||
|
||||
```lua
|
||||
tangible.animate(actor, nil, {action="sparkle", particles=999})
|
||||
tangible.animate{tan=actor, anim={action="sparkle", particles=999}}
|
||||
|
||||
tangible.animate(actor, nil, {action="warpto", plane="somewhere-else"})
|
||||
tangible.animate{tan=actor, anim={action="warpto", plane="somewhere-else"}}
|
||||
```
|
||||
|
||||
Here, instead of changing the XYZ of the player, the warpto changes the plane of the player. Unreal engine doesn't have the concept of "planes." What is the blueprint supposed to do? The answer is that we've added the concept of "plane" to Unreal at the C++ level. We have provided an API function *SetTangiblePlane*, that takes a string. When the tangible actor executes the "warpto" animation, that's all it has to do. The C++ code will make sure to only render tangibles that are on the same plane as the player.
|
||||
@@ -382,17 +382,17 @@ In TangibleStaticMesh, animations are played one at a time. But you can imagine
|
||||
For example, imagine a "fireworks launcher" object that launches rockets, and which can launch many rockets simultaneously. The rockets are owned by the launcher: it's actually just one Tangible Actor, the launcher and its rockets. Now imagine that the fireworks launcher is controlled using animate commands like this:
|
||||
|
||||
```lua
|
||||
tangible.animate(actor, nil, {action="launch", type="burst", color="blue"})
|
||||
tangible.animate{tan=actor, anim={action="launch", type="burst", color="blue"}}
|
||||
|
||||
tangible.animate(actor, nil, {action="launch", type="burst", color="green"})
|
||||
tangible.animate{tan=actor, anim={action="launch", type="burst", color="green"}}
|
||||
|
||||
tangible.animate(actor, nil, {action="wait"})
|
||||
tangible.animate{tan=actor, anim={action="wait"}}
|
||||
|
||||
tangible.animate(actor, nil, {action="launch", type="sparkle", color="blue"})
|
||||
tangible.animate{tan=actor, anim={action="launch", type="sparkle", color="blue"}}
|
||||
|
||||
tangible.animate(actor, nil, {action="launch", type="sparkle", color="green"})
|
||||
tangible.animate{tan=actor, anim={action="launch", type="sparkle", color="green"}}
|
||||
|
||||
tangible.animate(actor, nil, {action="wait"})
|
||||
tangible.animate{tan=actor, anim={action="wait"}}
|
||||
```
|
||||
|
||||
Our rocket launcher has the following unusual behavior: when it sees a sequence of animation steps with action="launch", it launches them all at the same time. So in response to the *tangible.animate* commands above, our launcher would launch two "burst" rockets simultaneously. Then, it would wait for both of those rockets to finish, however long that takes. Then, it launches two "sparkle" rockets simultaneously. Then it waits for the two sparkles to finish.
|
||||
|
||||
@@ -32,7 +32,7 @@ To call a Lua function, use the blueprint node 'Invoke Lua Function' or 'Probe L
|
||||
|
||||
By default, luprex puts a crosshair on the screen. When you aim the crosshair at an object, you are said to be "looking at" that object. Sometimes, looking at an object causes a "look-at widget" to pop up. Here's a typical example of a look-at widget, which displays the hotkeys for that object:
|
||||
|
||||

|
||||

|
||||
|
||||
Luprex contains all the following mechanisms to support the idea of look-at widgets:
|
||||
|
||||
@@ -213,7 +213,7 @@ Currently, there is no way to pop up any widget other than a look-at widget. The
|
||||
|
||||
The configuration for a widget blueprint could be complicated. Consider this menu:
|
||||
|
||||

|
||||

|
||||
|
||||
This menu could be a single widget blueprint, *MenuWidgetBP*. In its *BeginPlay* event, it could probe *gui.getmenu(actor, place),* which might return { "Resume", nil, "Save Game", "Load Game", nil, "Session", "Difficulty", "Options", nil, "Main Menu", "Quit Game" }. This design would make it possible for one widget blueprint to represent all the menus in the game.
|
||||
|
||||
@@ -221,7 +221,7 @@ We will provide common blueprints like *HotkeyWidgetBP* and *MenuWidgetBP* in ou
|
||||
|
||||
Here's an interesting window from a role-playing game:
|
||||
|
||||

|
||||

|
||||
|
||||
Since there are hundreds of spells in this game, it would not be good to have a separate widget blueprint for each one. Instead, it's probably best to have a *SpellWidgetBP* that can render any spell. This might probe *spell.getinfo(actor, place, spell),* which might return a vector like this:
|
||||
|
||||
|
||||
45
Docs/Eris and Snapshot-Rollback.md
Normal file
45
Docs/Eris and Snapshot-Rollback.md
Normal file
@@ -0,0 +1,45 @@
|
||||
### Eris and Snapshot/Rollback
|
||||
|
||||
## What Eris Is
|
||||
|
||||
Eris is a third-party Lua serialization library. It can serialize almost anything in a Lua VM — tables, closures, yielded coroutines — into a binary string, and deserialize it later, even in a different VM. It is essentially a rewrite of Pluto (a Lua 5.1 serialization library) targeting Lua 5.2. Eris ships as a custom distribution of Lua: the Lua runtime in our `luprex/ext/eris-master/` directory *is* Eris, which includes Lua plus the serialization code.
|
||||
|
||||
The two core operations are:
|
||||
|
||||
- `eris_dump` — serialize a Lua value (and everything it references) into a binary stream.
|
||||
- `eris_undump` — deserialize a binary stream back into a Lua value.
|
||||
|
||||
## The Permanent Object Table
|
||||
|
||||
C functions (like those registered via `LuaDefine`) cannot be serialized — they are function pointers that may differ between runs. Eris handles this through a "permanent object table": a lookup table that maps C functions to string keys. During serialization, when Eris encounters a C function, it writes the string key instead. During deserialization, it looks up the string key and substitutes the C function.
|
||||
|
||||
The `LuaSnap` class maintains both a `persist` table (value-to-key, for serialization) and an `unpersist` table (key-to-value, for deserialization) in the Lua registry. Whenever the source module registers a C function into the Lua environment, it also registers it in these tables.
|
||||
|
||||
## How Snapshot/Rollback Works
|
||||
|
||||
The class `LuaSnap` (in luasnap.hpp/cpp) wraps Eris to provide snapshot and rollback for the Lua interpreter. It owns a `lua_State` and exposes two operations:
|
||||
|
||||
- `serialize(StreamBuffer*)` — uses `eris_dump` to serialize the contents of the Lua registry into a binary buffer. Certain registry entries that don't belong in the snapshot (the main thread, the persist/unpersist tables, the world pointer, diff transmission maps) are excluded.
|
||||
- `deserialize(StreamBuffer*)` — uses `eris_undump` to restore the Lua registry from a previously serialized buffer.
|
||||
|
||||
## Role in Predictive Reexecution
|
||||
|
||||
Snapshot/rollback is used to implement the asynchronous model on the client (see "Predictive Reexecution"). There is only one client model, but it alternates between two forms:
|
||||
|
||||
- **Synchronous form** — the confirmed state, in lockstep with the server-synchronous model.
|
||||
- **Asynchronous form** — the synchronous state plus unacknowledged user commands applied on top. This is what the player sees.
|
||||
|
||||
To get from synchronous to asynchronous form, apply any unacknowledged user commands. To get from asynchronous back to synchronous form, roll back to the snapshotted state. The model switches between these forms on an as-needed basis. The typical cycle is:
|
||||
|
||||
1. The model is in synchronous form. Serialize it (take a snapshot).
|
||||
2. Apply unacknowledged user commands to produce the asynchronous form.
|
||||
3. Render from the asynchronous state.
|
||||
4. When new acknowledgements or difference transmissions arrive, deserialize (roll back) to restore the synchronous form.
|
||||
5. Apply the updates to the synchronous state.
|
||||
6. Repeat.
|
||||
|
||||
Because this uses Eris serialization rather than raw memory copying, snapshot/rollback only requires value-level determinism, not bit-exact determinism. The deserialized state has the same values but not necessarily the same memory addresses.
|
||||
|
||||
## An Alternative Design
|
||||
|
||||
A comment in luasnap.cpp describes an alternative approach that was prototyped and found to work: instead of using Eris serialization, use a custom memory allocator that tracks all memory blocks used by Lua, then snapshot by copying those blocks and rollback by restoring them. This would likely be faster than Eris serialization/deserialization, but would require maintaining additional code. Since Eris serialize/deserialize is also needed for save-game functionality, the current design avoids duplicating effort.
|
||||
@@ -63,7 +63,7 @@ When you're typing commands into a lua interpreter interactively, you inevitably
|
||||
```lua
|
||||
a = tangible.actor() — get a pointer to my actor
|
||||
|
||||
tangible.animate(a, nil, {action="warpto", xyz={100,100,100}})
|
||||
tangible.animate{tan=a, anim={action="warpto", xyz={100,100,100}}}
|
||||
```
|
||||
|
||||
You just created a temporary variable, *a*. In a normal lua interactive shell, these temporary variables go into the global environment table.
|
||||
|
||||
89
Docs/Predictive Reexecution.md
Normal file
89
Docs/Predictive Reexecution.md
Normal file
@@ -0,0 +1,89 @@
|
||||
### Predictive Reexecution
|
||||
|
||||
This document explains the general principles of predictive reexecution, the mechanism by which Luprex keeps all connected clients in sync with the server while hiding network latency from the player.
|
||||
|
||||
## The Problem: Network Latency
|
||||
|
||||
In a multiplayer game, the server is the authority on the state of the world. When a player clicks a button or types a command, the command must travel to the server, the server must process it, and the result must travel back. This round trip takes time. If we made the player wait for the round trip before showing anything on screen, the game would feel sluggish and unresponsive.
|
||||
|
||||
Predictive reexecution solves this problem. The client predicts the outcome of the player's action and shows it immediately, without waiting for the server. Later, when the server's authoritative result arrives, the client corrects itself if the prediction was wrong.
|
||||
|
||||
## The Four World Models
|
||||
|
||||
The system uses four types of world models. Each is an instance of class World, with its own Lua interpreter.
|
||||
|
||||
**Master** (one, on the server). This is the authoritative state of the entire game. When a command arrives from any client, the master executes it immediately. The master contains all tangibles, all globals, and all source code. It is the single source of truth.
|
||||
|
||||
**Server-synchronous** (one per connected client, on the server). This is the server's record of what a particular client knows. It does not execute commands when they first arrive at the server. Instead, it executes them later, when the server issues an acknowledgement. The server needs this model in order to compute difference transmissions: it compares the master to the server-synchronous model to figure out what updates to send.
|
||||
|
||||
**Client-synchronous** (one, on the client). This is the client's confirmed view of the world. It receives the exact same command acknowledgements as its corresponding server-synchronous model, and because the driven portion of the engine is fully deterministic, the two remain in perfect sync at all times.
|
||||
|
||||
**Asynchronous** (one, on the client). This is the client-synchronous model with predictions applied on top. It is what the player actually sees on screen. It is created by taking a snapshot of the client-synchronous model and then applying predicted actions to the snapshot. It is transient: when corrections arrive, the client rolls back to the client-synchronous state and reapplies predictions as needed.
|
||||
|
||||
## What Happens When the Player Issues a Command
|
||||
|
||||
Suppose the player clicks a button that invokes a Lua function. Here is the sequence of events:
|
||||
|
||||
1. The client sends the command to the server.
|
||||
|
||||
2. The client immediately applies the command to the asynchronous model, so the player sees an instant response. This is the prediction.
|
||||
|
||||
3. The server receives the command and executes it on the master model.
|
||||
|
||||
4. The server issues an acknowledgement for the command.
|
||||
|
||||
5. The server-synchronous model for that client executes the command upon receiving the acknowledgement.
|
||||
|
||||
6. The acknowledgement travels to the client. The client-synchronous model executes the command upon receiving the acknowledgement.
|
||||
|
||||
7. The client can now roll back the asynchronous model to the (updated) client-synchronous state and reapply any predictions that haven't been acknowledged yet.
|
||||
|
||||
Because the server-synchronous and client-synchronous models always receive the same acknowledgements in the same order, and because the driven portion is deterministic, they always end up in the exact same state. They never need to exchange full state to stay in sync.
|
||||
|
||||
## Two Update Channels
|
||||
|
||||
The synchronous models receive updates through two channels:
|
||||
|
||||
**Command acknowledgements.** These carry the client's own actions. When an acknowledgement arrives, both the server-synchronous and client-synchronous models execute the command. This is how a client's own actions propagate into the synchronous models.
|
||||
|
||||
**Difference transmission.** This carries everything else: other players' actions, server-side events, tangibles entering or leaving the visibility radius, and any other changes to the master that aren't caused by this client's commands. The server compares the master model to the server-synchronous model and sends corrections. The client applies those corrections to the client-synchronous model.
|
||||
|
||||
Between these two channels, the synchronous models are kept up to date with the master.
|
||||
|
||||
## Snapshot and Rollback
|
||||
|
||||
The asynchronous model is not a separate, persistent world model. It is implemented using the snapshot-and-rollback mechanism built into class World.
|
||||
|
||||
The client-synchronous model is the persistent model on the client. When the client needs to show the player a predicted state, it takes a snapshot of the client-synchronous model's memory, applies predictions to it, and renders from the resulting state. When it is time to incorporate new acknowledgements or difference transmissions, it rolls back to the snapshot, applies the updates to the client-synchronous model, and then reapplies any predictions that are still pending.
|
||||
|
||||
The snapshot mechanism works at the memory level: it saves and restores the raw memory used by the Lua interpreter, without needing to understand the contents. This makes it fast.
|
||||
|
||||
## NoPredict and Blocking Operations
|
||||
|
||||
Not all operations can be predicted. Consider an HTTP request: the client cannot predict what the server will return. For operations like these, the system uses a mechanism called NoPredict.
|
||||
|
||||
When a Lua thread attempts a blocking operation in a nonauthoritative (predicted) context, the thread is killed rather than allowed to proceed. This prevents the asynchronous model from going down a code path whose outcome cannot be predicted. The thread will execute for real when the command reaches the master model, and the results will propagate to the client through the normal acknowledgement and difference transmission channels.
|
||||
|
||||
The rules for handling this are described in the document "Correct Implementation of Blocking Operations and NoPredict." In summary:
|
||||
|
||||
- In an authoritative context (the master model), blocking operations proceed normally.
|
||||
- In a nonauthoritative context (a predicted model), blocking operations trigger a NoPredict, which kills the thread via lua_yield.
|
||||
- In a probe context (not yieldable), blocking operations throw an error, because probes are not allowed to change the world state at all.
|
||||
|
||||
## The Scripter's View
|
||||
|
||||
One of the most important consequences of this design is that the Lua scripter does not write any networking code. There are no "send message" or "receive message" calls. The scripter writes Lua code as if the game were running in a single Lua state with coroutines, which is normal for Lua anyway.
|
||||
|
||||
The networking is handled entirely by the engine. Command acknowledgements and difference transmission keep all the models in sync automatically. The scripter occasionally needs to provide hints to the system, such as:
|
||||
|
||||
- Using `permit_invoke` to mark which functions may be called by blueprints, for security.
|
||||
- Being aware that blocking operations will trigger NoPredict in predicted contexts.
|
||||
- Using `global.set` and `global.get` for data that needs to be broadcast to all clients.
|
||||
|
||||
But the core game logic is written without any awareness of networking. This is a clean separation that makes scripting much simpler.
|
||||
|
||||
## Why Determinism Matters
|
||||
|
||||
The entire scheme depends on the determinism of the driven portion of the engine. If the server-synchronous and client-synchronous models processed the same acknowledgements but arrived at different states, the system would break down.
|
||||
|
||||
This is why the engine goes to great lengths to ensure determinism: patching Lua for deterministic table iteration and table length, using the eng::malloc heap for reproducible memory addresses, avoiding threads, avoiding real-time clocks, and avoiding unordered maps. These constraints are described in detail in the document "The Event-Driven Structure of the Engine."
|
||||
@@ -6,7 +6,7 @@ Most of the Luprex engine is 'event-driven'. The event-driven design makes certa
|
||||
|
||||
To be clear about what I mean by "event driven," think of a traditional Finite State Machine (FSM). Here's an example FSM, for a coin-operated turnstile:
|
||||
|
||||

|
||||

|
||||
|
||||
So this example FSM has two *events* that it can process: a person can insert a coin, and a person can push through the turnstile.
|
||||
|
||||
|
||||
5
Docs/Things to document.md
Normal file
5
Docs/Things to document.md
Normal file
@@ -0,0 +1,5 @@
|
||||
### Things to Document
|
||||
|
||||
1. **The Lua-facing API calling convention.** Lua-facing C++ functions use either positional parameters or a `{keyword=value}` single-table pattern, but never a mix of the two. The codebase is still being migrated toward this convention — some older functions don't follow it yet. Need a doc (or at least a section) explaining the convention and when each style is used.
|
||||
|
||||
5. **The Unreal-side integration layer.** How the Unreal driver ticks, fetches animation queues, manages tangible actors — the C++ code in `Source/Integration/`. The docs cover the driver/driven separation conceptually and the blueprint side, but not this middle layer.
|
||||
71
Docs/Tokens - A New Lua Type.md
Normal file
71
Docs/Tokens - A New Lua Type.md
Normal file
@@ -0,0 +1,71 @@
|
||||
### A New Lua Type: Tokens
|
||||
|
||||
Tokens are a custom Lua data type built on top of Lua's lightuserdata. They are mainly intended for use as sentinels and special reserved values.
|
||||
|
||||
## Motivation
|
||||
|
||||
Tokens were invented when we were developing a JSON-to-LUA converter. Such a converter is mostly straightforward: json tables and lua tables are very similar. However, we did encounter a stumbling block. Consider this JSON:
|
||||
|
||||
```json
|
||||
{ "foo": null }
|
||||
```
|
||||
|
||||
In Lua, setting a table key to nil deletes the key. There is no way to represent "foo is present with value null" in a Lua table. You might try `{foo = 0}` or `{foo = "null"}`, but both are lossy: you can no longer distinguish JSON null from the number 0 or the string "null". Any sentinel value drawn from an existing Lua type collides with legitimate values of that type.
|
||||
|
||||
The solution is to use lightuserdata. A lightuserdata is a distinct Lua type — it cannot be confused with a string, number, boolean, or nil, and unlike nil, it can be stored in a table. The Luprex engine does not use lightuserdata for any other purpose, so all lightuserdata values are available for use as tokens.
|
||||
|
||||
## What a Token Is
|
||||
|
||||
A token is a short string encoded as a base36 number and stored in the 8-byte lightuserdata value. The lightuserdata is not actually a pointer to anything — it holds the base36-encoded integer directly. Tokens may only contain the characters a-z and 0-9. Since 36^12 fits in 64 bits but 36^13 does not, the maximum token length is 12 characters. That is sufficient for most natural identifiers.
|
||||
|
||||
Since lightuserdata is not used for anything else, it is safe to assume that any lightuserdata in our engine represents a token.
|
||||
|
||||
## The C++ Side: struct LuaToken
|
||||
|
||||
On the C++ side, tokens are represented by `struct LuaToken` (in luastack.hpp). You can construct one from a string or from the raw integer:
|
||||
|
||||
```cpp
|
||||
LuaToken("null") // parsed at compile time via consteval — becomes 0x10FAA9
|
||||
LuaToken(0x10FAA9) // equivalent raw value
|
||||
```
|
||||
|
||||
The string form is preferred — it is readable, and because the constructor is `consteval`, it compiles down to the same constant as the raw integer. There is zero runtime cost. If the string contains invalid characters (anything outside a-z, 0-9) or is too long, the error is caught at compile time.
|
||||
|
||||
There is also a runtime constructor that accepts `std::string_view`, for cases where the token string is not known at compile time.
|
||||
|
||||
The LuaStack API provides the usual accessors for tokens:
|
||||
|
||||
```cpp
|
||||
LS.set(slot, LuaToken("null")) // store a token in a LuaSlot
|
||||
LuaToken t = LS.cktoken(slot) // extract a token (error if not lightuserdata)
|
||||
auto t = LS.trytoken(slot) // extract a token (returns empty optional on mismatch)
|
||||
```
|
||||
|
||||
Named token constants can be auto-registered into the Lua environment using the `LuaTokenConstant` macro, which works the same way `LuaDefine` auto-registers functions:
|
||||
|
||||
```cpp
|
||||
LuaTokenConstant(null, "null", "Represents JSON null")
|
||||
```
|
||||
|
||||
## Properties
|
||||
|
||||
- **Distinct type.** Tokens are lightuserdata, a separate Lua type. They cannot collide with strings, numbers, booleans, tables, or nil.
|
||||
- **Storable in tables.** Unlike nil, tokens can be used as both table keys and table values.
|
||||
- **No allocation.** Tokens are 8 bytes inline. There is no heap allocation and no string interning.
|
||||
- **Fast comparison.** Comparing two tokens is just an integer comparison.
|
||||
|
||||
## Limitation: No Token Literals in Lua
|
||||
|
||||
Lua's parser has no syntax for token literals. In C++, you can write `LuaToken("null")` and it's clean and compile-time. In Lua, there is no equivalent — you cannot write a token literal the way you write `"hello"` or `42`.
|
||||
|
||||
Currently, the way tokens are made available to Lua is that C++ code uses `LuaTokenConstant` to insert specific token values into global tables. Lua scripts can then reference these pre-registered constants by name.
|
||||
|
||||
Modifying the Lua parser to add token literal syntax has been considered but is unappealing — it would be a significant and invasive patch. Adding a Lua function like `token("null")` to construct tokens at runtime is also possible and not off the table, but there hasn't been a need for it yet.
|
||||
|
||||
## Passing Tokens to Unreal
|
||||
|
||||
Tokens can get passed to Unreal in a variety of ways. For example, in animation step key-value pairs, the value can be a token. When animation queues are passed to Unreal, tokens are converted to FNames. Since both tokens and FNames are short identifier-like strings with fast comparison, the mapping is natural.
|
||||
|
||||
## Usage
|
||||
|
||||
Tokens are mainly intended as sentinels and special reserved values. The JSON null example above is the motivating case, but tokens can represent any short reserved constant the engine needs.
|
||||
@@ -2,6 +2,12 @@
|
||||
#include "AnimQueue.h"
|
||||
#include "UtilityLibrary.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Components/MeshComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Components/SkeletalMeshComponent.h"
|
||||
#include "AssetLookup.h"
|
||||
#include "LuprexGameModeBase.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include <iostream>
|
||||
|
||||
FlxAnimationStep::FlxAnimationStep(int64 hash, std::string_view body) {
|
||||
@@ -535,3 +541,132 @@ FlxAnimationStep FlxAnimTracker::GetCurrentAnimation() {
|
||||
return result;
|
||||
}
|
||||
|
||||
void UlxAnimationStepLibrary::AnimationStepApplyMaterials(const FlxAnimationStep& step, AActor* actor) {
|
||||
if (actor == nullptr) return;
|
||||
|
||||
// Step 1: Build tables of mat_ parameters (vectors and scalars).
|
||||
//
|
||||
TMap<FName, FVector> VectorParams;
|
||||
TMap<FName, float> ScalarParams;
|
||||
{
|
||||
std::string_view body((const char*)(step.Body.GetData()), step.Body.Num());
|
||||
FlxAnimationStepDecoder decoder(body);
|
||||
while (!decoder.AtEOF()) {
|
||||
FlxAnimationField field = decoder.ReadField();
|
||||
if (field.Name.size() <= 4) continue;
|
||||
if (field.Name.substr(0, 4) != "mat_") continue;
|
||||
std::string_view suffix = field.Name.substr(4);
|
||||
FName paramName(suffix.size(), (const UTF8CHAR*)suffix.data());
|
||||
if (field.Type == SimpleDynamicTag::VECTOR) {
|
||||
VectorParams.Add(paramName, FVector(field.X, field.Y, field.Z));
|
||||
} else if (field.Type == SimpleDynamicTag::NUMBER) {
|
||||
ScalarParams.Add(paramName, (float)field.X);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Early exit if no mat_ parameters found.
|
||||
//
|
||||
if (VectorParams.IsEmpty() && ScalarParams.IsEmpty()) return;
|
||||
|
||||
// Step 3: Loop over all mesh components and apply material parameters.
|
||||
//
|
||||
TInlineComponentArray<UMeshComponent*> MeshComponents;
|
||||
actor->GetComponents<UMeshComponent>(MeshComponents);
|
||||
|
||||
for (UMeshComponent* MeshComp : MeshComponents) {
|
||||
int32 NumMaterials = MeshComp->GetNumMaterials();
|
||||
for (int32 SlotIndex = 0; SlotIndex < NumMaterials; SlotIndex++) {
|
||||
UMaterialInterface* CurrentMat = MeshComp->GetMaterial(SlotIndex);
|
||||
if (CurrentMat == nullptr) continue;
|
||||
|
||||
// Check if the material has any parameter that doesn't
|
||||
// match what was specified.
|
||||
//
|
||||
bool AnyMismatch = false;
|
||||
for (auto& Pair : VectorParams) {
|
||||
FLinearColor Compare;
|
||||
if (CurrentMat->GetVectorParameterValue(Pair.Key, Compare)) {
|
||||
FLinearColor Desired(Pair.Value.X, Pair.Value.Y, Pair.Value.Z);
|
||||
if (Compare != Desired) {
|
||||
AnyMismatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (auto& Pair : ScalarParams) {
|
||||
float Compare;
|
||||
if (CurrentMat->GetScalarParameterValue(Pair.Key, Compare)) {
|
||||
if (Compare != Pair.Value) {
|
||||
AnyMismatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!AnyMismatch) continue;
|
||||
|
||||
// Strip away any existing dynamic material before creating a new one.
|
||||
//
|
||||
UMaterialInterface* BaseMat = CurrentMat;
|
||||
while (UMaterialInstanceDynamic* MID = Cast<UMaterialInstanceDynamic>(BaseMat)) {
|
||||
BaseMat = MID->Parent;
|
||||
}
|
||||
if (BaseMat != CurrentMat) {
|
||||
MeshComp->SetMaterial(SlotIndex, BaseMat);
|
||||
}
|
||||
|
||||
// Create the new dynamic material.
|
||||
//
|
||||
UMaterialInstanceDynamic* MID = MeshComp->CreateDynamicMaterialInstance(SlotIndex);
|
||||
for (auto& Pair : VectorParams) {
|
||||
FVector& Vec = Pair.Value;
|
||||
MID->SetVectorParameterValue(Pair.Key, FLinearColor(Vec.X, Vec.Y, Vec.Z));
|
||||
}
|
||||
for (auto& Pair : ScalarParams) {
|
||||
MID->SetScalarParameterValue(Pair.Key, Pair.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UlxAnimationStepLibrary::AnimationStepApplyMesh(const FlxAnimationStep& step, bool FallbackToBP, AActor* actor) {
|
||||
if (actor == nullptr) return;
|
||||
|
||||
// Step 1: Look for a "mesh" or "bp" string field in the animation step.
|
||||
//
|
||||
FString MeshName = AnimationStepGetString(step, TEXT("mesh"));
|
||||
if (MeshName.IsEmpty() && FallbackToBP) {
|
||||
MeshName = AnimationStepGetString(step, TEXT("bp"));
|
||||
}
|
||||
if (MeshName.IsEmpty()) return;
|
||||
|
||||
// Step 2: Find the actor's mesh component. There must be exactly one.
|
||||
//
|
||||
TInlineComponentArray<UMeshComponent*> MeshComponents;
|
||||
actor->GetComponents<UMeshComponent>(MeshComponents);
|
||||
if (MeshComponents.Num() != 1) {
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("AnimationStepApplyMesh: Actor %s has %d mesh components, expected exactly 1"), *actor->GetName(), MeshComponents.Num());
|
||||
return;
|
||||
}
|
||||
UMeshComponent* MeshComp = MeshComponents[0];
|
||||
|
||||
// Step 3: Apply the mesh based on the component type.
|
||||
//
|
||||
if (UStaticMeshComponent* StaticComp = Cast<UStaticMeshComponent>(MeshComp)) {
|
||||
UStaticMesh* NewMesh = nullptr;
|
||||
UlxAssetLookup::LoadStaticMeshAsset(NewMesh, actor, MeshName, false);
|
||||
if (NewMesh == nullptr) return;
|
||||
if (StaticComp->GetStaticMesh() != NewMesh) {
|
||||
StaticComp->SetStaticMesh(NewMesh);
|
||||
}
|
||||
} else if (USkeletalMeshComponent* SkelComp = Cast<USkeletalMeshComponent>(MeshComp)) {
|
||||
// TODO: Skeletal mesh support
|
||||
// USkeletalMesh* NewMesh = nullptr;
|
||||
// UlxAssetLookup::LoadSkeletalMeshAsset(NewMesh, actor, MeshName, true);
|
||||
// if (NewMesh == nullptr) return;
|
||||
// if (SkelComp->GetSkeletalMeshAsset() != NewMesh) {
|
||||
// SkelComp->SetSkeletalMeshAsset(NewMesh);
|
||||
// }
|
||||
} else {
|
||||
UE_LOG(LogLuprexIntegration, Error, TEXT("AnimationStepApplyMesh: Actor %s has unsupported mesh component type"), *actor->GetName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,22 @@ public:
|
||||
|
||||
UFUNCTION(BlueprintPure, meta = (BlueprintAutocast), Category = "Luprex|Animation Step")
|
||||
static int64 AnimationStepID(const FlxAnimationStep& step) { return step.Hash; }
|
||||
|
||||
// Scan an animation step for key-value pairs of the form mat_XXXX={x,y,z}.
|
||||
// For each match, create a dynamic material instance on the actor's mesh
|
||||
// components and set the vector parameter XXXX. Materials are restored to
|
||||
// their base (non-dynamic) state before applying, so parameters from
|
||||
// previous calls do not persist.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, Meta = (DefaultToSelf = "actor"), Category = "Luprex|Animation Step")
|
||||
static void AnimationStepApplyMaterials(const FlxAnimationStep& step, AActor* actor);
|
||||
|
||||
// Look for a mesh=name key-value pair in the animation step.
|
||||
// If found, load the named mesh and apply it to the actor's
|
||||
// mesh component. The actor must have exactly one mesh component.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable, Meta = (DefaultToSelf = "actor"), Category = "Luprex|Animation Step")
|
||||
static void AnimationStepApplyMesh(const FlxAnimationStep& step, bool FallbackToBP, AActor* actor);
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
@@ -85,11 +85,11 @@ static AnimValue parse_anim_value(LuaCoreStack &LS, LuaSlot val) {
|
||||
return result;
|
||||
}
|
||||
|
||||
void AnimState::set_persistent(const eng::string &name) {
|
||||
void AnimStepEditor::set_persistent(const eng::string &name) {
|
||||
map_[name].persistent = true;
|
||||
}
|
||||
|
||||
void AnimState::print_debug_string(eng::ostringstream &oss) {
|
||||
void AnimStepEditor::print_debug_string(eng::ostringstream &oss) {
|
||||
bool first = true;
|
||||
if (map_.empty()) {
|
||||
oss << "[empty]";
|
||||
@@ -117,13 +117,13 @@ void AnimState::print_debug_string(eng::ostringstream &oss) {
|
||||
}
|
||||
}
|
||||
|
||||
eng::string AnimState::debug_string() {
|
||||
eng::string AnimStepEditor::debug_string() {
|
||||
eng::ostringstream oss;
|
||||
print_debug_string(oss);
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
eng::string AnimState::encode() const {
|
||||
eng::string AnimStepEditor::encode() const {
|
||||
StreamBuffer sb;
|
||||
for (const auto &pair : map_) {
|
||||
const eng::string &name = pair.first;
|
||||
@@ -135,7 +135,7 @@ eng::string AnimState::encode() const {
|
||||
return eng::string(sb.view());
|
||||
}
|
||||
|
||||
void AnimState::decode(std::string_view s) {
|
||||
void AnimStepEditor::decode(std::string_view s) {
|
||||
map_.clear();
|
||||
StreamBuffer sb(s);
|
||||
while (!sb.empty()) {
|
||||
@@ -146,7 +146,7 @@ void AnimState::decode(std::string_view s) {
|
||||
}
|
||||
}
|
||||
|
||||
void AnimState::decode_persistent(std::string_view s) {
|
||||
void AnimStepEditor::decode_persistent(std::string_view s) {
|
||||
map_.clear();
|
||||
StreamBuffer sb(s);
|
||||
AnimValue dummy;
|
||||
@@ -163,7 +163,7 @@ void AnimState::decode_persistent(std::string_view s) {
|
||||
}
|
||||
}
|
||||
|
||||
eng::string AnimState::add_default(const eng::string &name, const AnimValue &def, const AnimState *other) {
|
||||
eng::string AnimStepEditor::add_default(const eng::string &name, const AnimValue &def, const AnimStepEditor *other) {
|
||||
AnimValue &value = map_[name];
|
||||
value.persistent = true;
|
||||
if (value.type == SimpleDynamicTag::UNINITIALIZED) {
|
||||
@@ -185,7 +185,7 @@ eng::string AnimState::add_default(const eng::string &name, const AnimValue &def
|
||||
return "";
|
||||
}
|
||||
|
||||
eng::string AnimState::add_defaults(const AnimState *other) {
|
||||
eng::string AnimStepEditor::add_defaults(const AnimStepEditor *other) {
|
||||
eng::string err;
|
||||
AnimValue defval;
|
||||
|
||||
@@ -209,7 +209,7 @@ eng::string AnimState::add_defaults(const AnimState *other) {
|
||||
}
|
||||
|
||||
|
||||
eng::string AnimState::from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent, bool allowauto) {
|
||||
eng::string AnimStepEditor::from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent, bool allowauto) {
|
||||
LuaVar key, val;
|
||||
LuaExtStack LS(LS0.state(), key, val);
|
||||
util::DXYZ xyz;
|
||||
@@ -241,7 +241,7 @@ eng::string AnimState::from_lua(LuaCoreStack &LS0, LuaSlot tab, bool persistent,
|
||||
return "";
|
||||
}
|
||||
|
||||
eng::string AnimState::merge(const AnimState &previous, const AnimState &update) {
|
||||
eng::string AnimStepEditor::merge(const AnimStepEditor &previous, const AnimStepEditor &update) {
|
||||
// Copy everything over from the previous entry.
|
||||
map_ = previous.map_;
|
||||
|
||||
@@ -286,7 +286,7 @@ eng::string AnimState::merge(const AnimState &previous, const AnimState &update)
|
||||
}
|
||||
|
||||
|
||||
void AnimState::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool transient, bool persistent) {
|
||||
void AnimStepEditor::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool transient, bool persistent) {
|
||||
LuaVar name, val;
|
||||
LuaExtStack LS(LS0.state(), name, val);
|
||||
LS.newtable(tab);
|
||||
@@ -319,7 +319,7 @@ void AnimState::to_lua(LuaCoreStack &LS0, LuaSlot tab, bool transient, bool pers
|
||||
|
||||
// The syntax used by this parser is not general enough to represent all
|
||||
// possible strings. That's OK, though, since it's just for unit testing.
|
||||
void AnimState::parse(std::string_view config) {
|
||||
void AnimStepEditor::parse(std::string_view config) {
|
||||
while (true) {
|
||||
config = sv::ltrim(config);
|
||||
if (config.empty()) break;
|
||||
@@ -336,7 +336,7 @@ void AnimState::parse(std::string_view config) {
|
||||
}
|
||||
}
|
||||
|
||||
void AnimState::clear_and_parse(std::string_view config) {
|
||||
void AnimStepEditor::clear_and_parse(std::string_view config) {
|
||||
map_.clear();
|
||||
parse(config);
|
||||
}
|
||||
@@ -442,15 +442,15 @@ void AnimQueue::update_encqueue(int limit, bool add, std::string_view add_enc, i
|
||||
}
|
||||
|
||||
AnimQueue::AnimQueue() {
|
||||
update_encqueue(10, true, AnimState().encode(), 0, 0);
|
||||
update_encqueue(10, true, AnimStepEditor().encode(), 0, 0);
|
||||
}
|
||||
|
||||
void AnimQueue::clear(const AnimState &state) {
|
||||
void AnimQueue::clear(const AnimStepEditor &state) {
|
||||
update_encqueue(get_size_limit(), true, state.encode(), 0, 0);
|
||||
}
|
||||
|
||||
void AnimQueue::clear() {
|
||||
update_encqueue(get_size_limit(), true, AnimState().encode(), 0, 0);
|
||||
update_encqueue(get_size_limit(), true, AnimStepEditor().encode(), 0, 0);
|
||||
}
|
||||
|
||||
void AnimQueue::set_limit(int limit) {
|
||||
@@ -458,12 +458,12 @@ void AnimQueue::set_limit(int limit) {
|
||||
update_encqueue(limit, false, std::string_view(), 0, limit);
|
||||
}
|
||||
|
||||
void AnimQueue::add(const AnimState &state) {
|
||||
void AnimQueue::add(const AnimStepEditor &state) {
|
||||
int limit = get_size_limit();
|
||||
update_encqueue(limit, true, state.encode(), 0, limit - 1);
|
||||
}
|
||||
|
||||
void AnimQueue::replace(const AnimState &state) {
|
||||
void AnimQueue::replace(const AnimStepEditor &state) {
|
||||
int limit = get_size_limit();
|
||||
update_encqueue(limit, true, state.encode(), 1, limit);
|
||||
}
|
||||
@@ -528,7 +528,7 @@ void AnimQueue::print_debug_string(eng::ostringstream &oss, bool full) const {
|
||||
assert(sb.empty());
|
||||
for (int i = encsteps.size() - 1; i >= 0; i --) {
|
||||
if (!first) oss << "; ";
|
||||
AnimState state(encsteps[i]);
|
||||
AnimStepEditor state(encsteps[i]);
|
||||
state.print_debug_string(oss);
|
||||
first = false;
|
||||
}
|
||||
@@ -553,16 +553,16 @@ AnimCoreState AnimQueue::get_final_core_state() const {
|
||||
return result;
|
||||
}
|
||||
|
||||
AnimState AnimQueue::get_final_persistent() const {
|
||||
AnimStepEditor AnimQueue::get_final_persistent() const {
|
||||
std::string_view encstep = get_final_encstep();
|
||||
AnimState result;
|
||||
AnimStepEditor result;
|
||||
result.decode_persistent(encstep);
|
||||
return result;
|
||||
}
|
||||
|
||||
AnimState AnimQueue::get_final_everything() const {
|
||||
AnimStepEditor AnimQueue::get_final_everything() const {
|
||||
std::string_view encstep = get_final_encstep();
|
||||
AnimState result;
|
||||
AnimStepEditor result;
|
||||
result.decode(encstep);
|
||||
return result;
|
||||
}
|
||||
@@ -571,44 +571,44 @@ LuaDefine(unittests_animqueue, "", "some unit tests") {
|
||||
// Useful objects.
|
||||
AnimQueue aq, aqs;
|
||||
StreamBuffer sb;
|
||||
AnimState astate;
|
||||
AnimStepEditor astate;
|
||||
eng::string enc;
|
||||
AnimCoreState core;
|
||||
|
||||
// Debug string of a newly initialized queue
|
||||
LuaAssertStrEq(L, aq.full_debug_string(), "limit=10; [empty]");
|
||||
|
||||
// Test AnimState simple setters.
|
||||
// Test AnimStepEditor simple setters.
|
||||
astate.set_string("color", "blue");
|
||||
astate.set_dxyz("xyz", util::DXYZ(1,2,3));
|
||||
astate.set_number("half", 0.5);
|
||||
astate.set_boolean("nice", true);
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color:blue half:0.5 nice:true xyz:1,2,3");
|
||||
|
||||
// // Test AnimState simple getters.
|
||||
// // Test AnimStepEditor simple getters.
|
||||
// LuaAssert(L, astate.get_string("color") == "blue");
|
||||
// LuaAssert(L, astate.get_xyz("xyz") == util::DXYZ(1,2,3));
|
||||
// LuaAssert(L, astate.get_number("half") == 0.5);
|
||||
// LuaAssert(L, astate.get_boolean("nice") == true);
|
||||
|
||||
// // Test AnimState simple getters on nonexistent data.
|
||||
// // Test AnimStepEditor simple getters on nonexistent data.
|
||||
// LuaAssert(L, astate.get_string("q") == "");
|
||||
// LuaAssert(L, astate.get_xyz("q") == util::DXYZ(0,0,0));
|
||||
// LuaAssert(L, astate.get_number("q") == 0.0);
|
||||
// LuaAssert(L, astate.get_boolean("q") == false);
|
||||
|
||||
// // Test AnimState simple getters on wrong-type data.
|
||||
// // Test AnimStepEditor simple getters on wrong-type data.
|
||||
// LuaAssert(L, astate.get_string("half") == "");
|
||||
// LuaAssert(L, astate.get_xyz("half") == util::DXYZ(0,0,0));
|
||||
// LuaAssert(L, astate.get_number("color") == 0.0);
|
||||
// LuaAssert(L, astate.get_boolean("color") == false);
|
||||
|
||||
// Test AnimState persistence manipulation.
|
||||
// Test AnimStepEditor persistence manipulation.
|
||||
astate.set_persistent("color");
|
||||
astate.set_persistent("nice");
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color=blue half:0.5 nice=true xyz:1,2,3");
|
||||
|
||||
// Test AnimState parser.
|
||||
// Test AnimStepEditor parser.
|
||||
astate.clear_and_parse("color:green mean=true pos=3,4,5 ok:false");
|
||||
LuaAssertStrEq(L, astate.debug_string(), "color:green mean=true ok:false pos=3,4,5");
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// ANIMATION QUEUES
|
||||
// ANIMATION QUEUES AND ANIMATION STEPS
|
||||
//
|
||||
// See "Animation Queues and Tangible Actors.md" for an overview.
|
||||
//
|
||||
// An animation queue is a fifo queue of animation steps. New animations are
|
||||
// pushed on the back, and old ones are popped from the front.
|
||||
//
|
||||
// An animation step is a set of key-value pairs, where each key is an
|
||||
// identifier, and each value is either a number, a boolean, an XYZ coordinate,
|
||||
// or a string. A key-value pair can be either persistent or nonpersistent.
|
||||
// identifier, and each value is either a number, boolean, vector,
|
||||
// token, or string. A key-value pair can be either persistent or nonpersistent.
|
||||
// So a typical animation step might be:
|
||||
//
|
||||
// action=walk [nonpersistent]
|
||||
@@ -22,9 +24,34 @@
|
||||
// by mixing the hash value of the previous step with the hash value
|
||||
// of the encoded string of key-value pairs.
|
||||
//
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// SERIALIZED STORAGE
|
||||
// Class AnimStepEditor is used to read+write animation steps.
|
||||
//
|
||||
// Note: this class is not used for storage of animation steps.
|
||||
// Animation steps are stored in class AnimQueue. AnimStepEditor
|
||||
// is only used when you want to extract animation steps from
|
||||
// an AnimQueue, or insert new animation steps into an AnimQueue.
|
||||
//
|
||||
// Class AnimStepEditor is quite simple: it's a map from Key to
|
||||
// AnimValue. AnimValue is a container that can hold a number,
|
||||
// string, vector, token, or boolean. Class AnimStepEditor provides
|
||||
// a variety of accessors to set key-value pairs.
|
||||
//
|
||||
// For example, you can populate an animation step by setting
|
||||
// key-value pairs directly. You can import key-value pairs
|
||||
// from a lua table. You can also merge key-value pairs from
|
||||
// a different AnimStepEditor.
|
||||
//
|
||||
// When importing from a lua_table or a from another AnimStepEditor,
|
||||
// there are rules for resolving conflicts between any key-value
|
||||
// pairs that are already in the builder with those being imported.
|
||||
// See the documentation for those functions for more information.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Class AnimQueue stores animation queues.
|
||||
//
|
||||
// The entired animation queue is stored in a serialized format,
|
||||
// as a shared string. This means that the animation queue can be
|
||||
@@ -118,7 +145,7 @@ struct AnimValue : public SimpleDynamicValue {
|
||||
};
|
||||
|
||||
|
||||
class AnimState
|
||||
class AnimStepEditor
|
||||
{
|
||||
private:
|
||||
using Map = eng::map<eng::string, AnimValue>;
|
||||
@@ -126,7 +153,7 @@ private:
|
||||
|
||||
// Set the default value, internal
|
||||
//
|
||||
eng::string add_default(const eng::string &name, const AnimValue &v, const AnimState *other);
|
||||
eng::string add_default(const eng::string &name, const AnimValue &v, const AnimStepEditor *other);
|
||||
|
||||
public:
|
||||
// Clear everything
|
||||
@@ -160,7 +187,7 @@ public:
|
||||
|
||||
// Constructs an empty state.
|
||||
//
|
||||
AnimState() {}
|
||||
AnimStepEditor() {}
|
||||
|
||||
// Convert to an encoded string.
|
||||
//
|
||||
@@ -188,7 +215,7 @@ public:
|
||||
// - If no default value can be found in 'other', then a hardwired
|
||||
// default value is provided.
|
||||
//
|
||||
eng::string add_defaults(const AnimState *other);
|
||||
eng::string add_defaults(const AnimStepEditor *other);
|
||||
|
||||
// Parse an animstate from a Lua Table.
|
||||
//
|
||||
@@ -199,7 +226,7 @@ public:
|
||||
//
|
||||
// If 'allowauto' is true, then the lua table may contain a key-value
|
||||
// pair of the form (key, math.auto). These keys will be stored in the
|
||||
// AnimState map with mapentry.type == SimpleDynamicTag::AUTO.
|
||||
// AnimStepEditor map with mapentry.type == SimpleDynamicTag::AUTO.
|
||||
// This is done to express an intent that the value should be
|
||||
// automatically computer later.
|
||||
//
|
||||
@@ -207,7 +234,7 @@ public:
|
||||
|
||||
// Generate a merged animstate using a previous state and an update.
|
||||
//
|
||||
// Keys from both previous and update are combined to create this AnimState.
|
||||
// Keys from both previous and update are combined to create this AnimStepEditor.
|
||||
// Values from 'update' override values from 'previous'. Persistent flags
|
||||
// are taken from 'previous'. If a key exists in both previous and update,
|
||||
// and the key is persistent in 'previous', then the types must match,
|
||||
@@ -218,7 +245,7 @@ public:
|
||||
// a rule to compute that value automatically. Failure to find a rule
|
||||
// results in an error.
|
||||
//
|
||||
eng::string merge(const AnimState &previous, const AnimState &update);
|
||||
eng::string merge(const AnimStepEditor &previous, const AnimStepEditor &update);
|
||||
|
||||
// Convert an animstate to a lua table.
|
||||
//
|
||||
@@ -250,7 +277,7 @@ public:
|
||||
|
||||
// Constructor from an encoded string.
|
||||
//
|
||||
AnimState(std::string_view s) { decode(s); }
|
||||
AnimStepEditor(std::string_view s) { decode(s); }
|
||||
};
|
||||
|
||||
struct AnimCoreState
|
||||
@@ -271,7 +298,7 @@ public:
|
||||
// Clear the steps to an initial state.
|
||||
//
|
||||
void clear();
|
||||
void clear(const AnimState &initial);
|
||||
void clear(const AnimStepEditor &initial);
|
||||
|
||||
// Change the size limit.
|
||||
//
|
||||
@@ -282,14 +309,14 @@ public:
|
||||
// Note: add does not automatically compose the step with the previous
|
||||
// step, you have to do that yourself.
|
||||
//
|
||||
void add(const AnimState &state);
|
||||
void add(const AnimStepEditor &state);
|
||||
|
||||
// Replace the most recent animation step.
|
||||
//
|
||||
// Note: replace does not automatically compose the step with the previous
|
||||
// step, you have to do that yourself.
|
||||
//
|
||||
void replace(const AnimState &state);
|
||||
void replace(const AnimStepEditor &state);
|
||||
|
||||
// Serialize or deserialize to a StreamBuffer
|
||||
//
|
||||
@@ -324,11 +351,11 @@ public:
|
||||
|
||||
// Get the final entry, all persistent variables.
|
||||
//
|
||||
AnimState get_final_persistent() const;
|
||||
AnimStepEditor get_final_persistent() const;
|
||||
|
||||
// Get the final entry, everything persistent and non-persistent.
|
||||
//
|
||||
AnimState get_final_everything() const;
|
||||
AnimStepEditor get_final_everything() const;
|
||||
|
||||
// Get a serialized representation of the animation queue.
|
||||
//
|
||||
|
||||
@@ -256,22 +256,22 @@
|
||||
// either.
|
||||
//
|
||||
// Lua has a datatype called 'lightuserdata'. A lightuserdata holds an
|
||||
// int64. That gives me an option: I can store json null as
|
||||
// lightuserdata(0x6E756C6C00000000). When we see this lightuserdata
|
||||
// value, we would know we have a json null. Why 0x6E756C6C00000000?
|
||||
// Because if you interpret those 8 bytes as 8 ascii characters, it's the
|
||||
// string "null".
|
||||
// int64. That gives me an option: I can store json null as a
|
||||
// lightuserdata. When we see this lightuserdata value, we would know
|
||||
// we have a json null.
|
||||
//
|
||||
// So that finally brings me to what a "token" is. A token is a lightuserdata
|
||||
// containing up to 8 ascii characters. So in effect, it's a short string,
|
||||
// but it's a string that's distinguishable from a normal lua string. It
|
||||
// doesn't have the same type as a lua string (it shows up as a lightuserdata).
|
||||
// containing a short string encoded as a base36 number. Tokens may only
|
||||
// contain the characters a-z and 0-9, and can be up to 12 characters long
|
||||
// (since 36^12 fits in 64 bits). In effect, it's a short string, but it's
|
||||
// a string that's distinguishable from a normal lua string. It doesn't have
|
||||
// the same type as a lua string (it shows up as a lightuserdata).
|
||||
// The purpose of tokens is to represent special unique values, like json null.
|
||||
//
|
||||
// To make working with tokens easy, I've created a C++ class 'LuaToken'.
|
||||
// To make working with tokens easy, I've created a C++ struct 'LuaToken'.
|
||||
// It stores an int64. You can construct a LuaToken in two different ways:
|
||||
//
|
||||
// LuaToken(0x6E756C6C00000000)
|
||||
// LuaToken(0x10FAA9)
|
||||
// LuaToken("null")
|
||||
//
|
||||
// Those are equivalent. The second form is just as fast as the first,
|
||||
|
||||
@@ -74,14 +74,19 @@ LuaDefine(tangible_animfinal, "tan",
|
||||
LuaDefStack LS(L, tanobj, result);
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
Tangible *tan = w->tangible_get(LS, tanobj, false);
|
||||
AnimState state = tan->anim_queue_.get_final_everything();
|
||||
state.to_lua(LS, result, true, true);
|
||||
AnimStepEditor step = tan->anim_queue_.get_final_everything();
|
||||
step.to_lua(LS, result, true, true);
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(tangible_animinit, "tan,config",
|
||||
LuaDefine(tangible_animinit, "config",
|
||||
"|Reinitialize the animation queue and specify persistent state."
|
||||
"|"
|
||||
"|The configuration table must contain the following keywords:"
|
||||
"|"
|
||||
"| tan: the tangible to reinitialize."
|
||||
"| anim: a table of key-value pairs for the initial animation state."
|
||||
"|"
|
||||
"|Every tangible has an animation queue. The queue consists of a"
|
||||
"|sequence of animation steps. Each step consists of a list of"
|
||||
"|key-value pairs. For example, if you want a human person to jump"
|
||||
@@ -102,7 +107,7 @@ LuaDefine(tangible_animinit, "tan,config",
|
||||
"|When you add an animation step to the animation queue, you do not have"
|
||||
"|to always specify xyz and plane. For example, you can legally say:"
|
||||
"|"
|
||||
"| tangible.animate(a, nil, {action='jump', height=3.0}))"
|
||||
"| tangible.animate{tan=a, anim={action='jump', height=3.0}}"
|
||||
"|"
|
||||
"|This adds a step to the animation queue. That step contains"
|
||||
"|xyz and plane, even though we didn't specify xyz and plane in"
|
||||
@@ -125,39 +130,51 @@ LuaDefine(tangible_animinit, "tan,config",
|
||||
"|"
|
||||
"|This function, tangible.animinit, is used to reconfigure the set of"
|
||||
"|persistent state variables that are retained by the tangible's"
|
||||
"|animation queue. You must provide a table containing all the"
|
||||
"|animation queue. You must provide an anim table containing all the"
|
||||
"|persistent values you want, and their initial values."
|
||||
"|") {
|
||||
LuaArg tanobj, config;
|
||||
LuaDefStack LS(L, tanobj, config);
|
||||
LuaArg config;
|
||||
LuaVar tanobj, animtab;
|
||||
LuaDefStack LS(L, config, tanobj, animtab);
|
||||
|
||||
LuaKeywordParser kp(LS, config);
|
||||
kp.required(tanobj, "tan");
|
||||
kp.required(animtab, "anim");
|
||||
kp.check_throw();
|
||||
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
Tangible *tan = w->tangible_get(LS, tanobj, false);
|
||||
AnimState state;
|
||||
eng::string error = state.from_lua(LS, config, true, false);
|
||||
AnimStepEditor step;
|
||||
eng::string error = step.from_lua(LS, animtab, true, false);
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
}
|
||||
AnimState defsource = tan->anim_queue_.get_final_persistent();
|
||||
error = state.add_defaults(&defsource);
|
||||
AnimStepEditor defsource = tan->anim_queue_.get_final_persistent();
|
||||
error = step.add_defaults(&defsource);
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
}
|
||||
tan->anim_queue_.clear(state);
|
||||
tan->anim_queue_.clear(step);
|
||||
tan->update_plane_item();
|
||||
return LS.result();
|
||||
}
|
||||
|
||||
LuaDefine(tangible_animate, "tan,options,config",
|
||||
LuaDefine(tangible_animate, "config",
|
||||
"|Add an animation step to the tangible."
|
||||
"|"
|
||||
"|The animation queue stores animation steps. This function, "
|
||||
"|tangible.animate, adds one new animation step to the queue."
|
||||
"|The configuration table must contain the following keywords:"
|
||||
"|"
|
||||
"| tan: the tangible to animate."
|
||||
"| anim: a table of key-value pairs for the animation step."
|
||||
"| replace: (optional) if true, the last step in the queue is"
|
||||
"| removed, and the new animation replaces it. Persistent"
|
||||
"| state is carried over from the step that was replaced."
|
||||
"|"
|
||||
"|It might be useful to read doc(tangible.animinit) before reading"
|
||||
"|more."
|
||||
"|"
|
||||
"|An animation step is just a list of key-value pairs. Therefore,"
|
||||
"|the config table must be key-value pairs. Keys must be simple"
|
||||
"|the anim table must be key-value pairs. Keys must be simple"
|
||||
"|identifiers. Values can be numbers, strings, booleans, or"
|
||||
"|coordinates."
|
||||
"|"
|
||||
@@ -171,34 +188,29 @@ LuaDefine(tangible_animate, "tan,options,config",
|
||||
"|animation step, but nothing is propagated forward to future animation"
|
||||
"|steps."
|
||||
"|"
|
||||
"|The options can be nil, or options can be a table containing"
|
||||
"|the following flags:"
|
||||
"|"
|
||||
"| replace: if true, then the last step in the queue is removed,"
|
||||
"| and the new animation replaces it. Persistent state is carried"
|
||||
"| over from the step that was replaced."
|
||||
"|"
|
||||
"|") {
|
||||
LuaArg tanobj, options, steptab;
|
||||
LuaVar option;
|
||||
LuaDefStack LS(L, option, tanobj, options, steptab);
|
||||
LuaArg config;
|
||||
LuaVar tanobj, animtab, option;
|
||||
LuaDefStack LS(L, config, tanobj, animtab, option);
|
||||
|
||||
LuaKeywordParser kp(LS, config);
|
||||
kp.required(tanobj, "tan");
|
||||
kp.required(animtab, "anim");
|
||||
bool replace = false;
|
||||
if (!LS.isnil(options)) {
|
||||
LuaKeywordParser kp(LS, options);
|
||||
if (kp.optional(option, "replace")) {
|
||||
replace = LS.ckboolean(option);
|
||||
}
|
||||
kp.final_check_throw();
|
||||
if (kp.optional(option, "replace")) {
|
||||
replace = LS.ckboolean(option);
|
||||
}
|
||||
kp.check_throw();
|
||||
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
Tangible *tan = w->tangible_get(LS, tanobj, false);
|
||||
AnimState previous = tan->anim_queue_.get_final_persistent();
|
||||
AnimState update;
|
||||
eng::string error = update.from_lua(LS, steptab, false, true);
|
||||
AnimStepEditor previous = tan->anim_queue_.get_final_persistent();
|
||||
AnimStepEditor update;
|
||||
eng::string error = update.from_lua(LS, animtab, false, true);
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
}
|
||||
AnimState merge;
|
||||
AnimStepEditor merge;
|
||||
error = merge.merge(previous, update);
|
||||
if (!error.empty()) {
|
||||
luaL_error(L, "%s", error.c_str());
|
||||
@@ -291,16 +303,13 @@ LuaDefine(tangible_build, "config",
|
||||
"|Build a new tangible object."
|
||||
"|"
|
||||
"|The configuration table must contain the keyword 'class', which"
|
||||
"|must be the name of a class created with 'makeclass'. It may also"
|
||||
"|contain the following values:"
|
||||
"|"
|
||||
"| bp - the unreal blueprint, defaults to class name."
|
||||
"| plane - the plane, defaults to actor.plane"
|
||||
"| xyz - the xyz coordinate, defaults to actor.xyz"
|
||||
"| facing - the rotation, defaults to actor.facing"
|
||||
"|"
|
||||
"|Tangible.build will create an initial animstate containing only"
|
||||
"|bp, plane, xyz, and facing."
|
||||
"|must be the name of a class created with 'makeclass'. It may"
|
||||
"|optionally contain the keyword 'anim', which is a table of"
|
||||
"|key-value pairs for the initial animation step. The anim table"
|
||||
"|may contain any key-value pairs that tangible.animate accepts."
|
||||
"|The mandatory keys (bp, plane, xyz, facing) will be defaulted"
|
||||
"|from the actor if not specified. If bp is not specified, it"
|
||||
"|defaults to the class name."
|
||||
"|"
|
||||
"|After creating the tangible and setting up the initial animation"
|
||||
"|state, build will call the constructor for the tangible."
|
||||
@@ -315,17 +324,14 @@ LuaDefine(tangible_build, "config",
|
||||
"|The constructor is not allowed to block."
|
||||
){
|
||||
LuaArg config;
|
||||
LuaVar classname, classtab, bp, plane, xyz, facing, mt, func;
|
||||
LuaVar classname, classtab, animtab, mt, func;
|
||||
LuaRet database;
|
||||
LuaDefStack LS(L, config, classname, classtab, bp, plane, xyz, facing, mt, func, database);
|
||||
LuaDefStack LS(L, config, classname, classtab, animtab, mt, func, database);
|
||||
World *w = World::fetch_global_pointer(L);
|
||||
|
||||
LuaKeywordParser kp(LS, config);
|
||||
kp.required(classname, "class");
|
||||
kp.optional(bp, "bp");
|
||||
kp.optional(plane, "plane");
|
||||
kp.optional(xyz, "xyz");
|
||||
kp.optional(facing, "facing");
|
||||
kp.optional(animtab, "anim");
|
||||
kp.check_throw();
|
||||
|
||||
// Verify the class.
|
||||
@@ -335,41 +341,38 @@ LuaDefine(tangible_build, "config",
|
||||
}
|
||||
|
||||
// Calculate the initial animation state.
|
||||
AnimState state;
|
||||
if (!LS.isnil(bp)) {
|
||||
state.set_string("bp", LS.ckstring(bp));
|
||||
} else {
|
||||
state.set_string("bp", LS.classname(classtab));
|
||||
AnimStepEditor step;
|
||||
if (!LS.isnil(animtab)) {
|
||||
err = step.from_lua(LS, animtab, true, false);
|
||||
if (err != "") {
|
||||
luaL_error(L, "%s", err.c_str());
|
||||
}
|
||||
}
|
||||
if (!LS.isnil(plane)) {
|
||||
state.set_string("plane", LS.ckstring(plane));
|
||||
}
|
||||
if (!LS.isnil(xyz)) {
|
||||
state.set_dxyz("xyz", LS.ckxyz(xyz));
|
||||
}
|
||||
if (!LS.isnil(facing)) {
|
||||
state.set_number("facing", LS.cknumber(facing));
|
||||
|
||||
// Default bp to the class name if not specified.
|
||||
if (!step.contains("bp")) {
|
||||
step.set_string("bp", LS.classname(classtab));
|
||||
}
|
||||
|
||||
// Add default values from the actor. Set persistent flags.
|
||||
Tangible *actor = w->tangible_get(w->lthread_actor_id_);
|
||||
AnimState actorstate = actor->anim_queue_.get_final_persistent();
|
||||
err = state.add_defaults(&actorstate);
|
||||
AnimStepEditor actorstate = actor->anim_queue_.get_final_persistent();
|
||||
err = step.add_defaults(&actorstate);
|
||||
if (err != "") {
|
||||
luaL_error(L, "%s", err.c_str());
|
||||
}
|
||||
|
||||
|
||||
int64_t new_id = w->alloc_id_predictable();
|
||||
Tangible *tan = w->tangible_make(LS, database, new_id);
|
||||
|
||||
// Update the class of the new tangible.
|
||||
LS.getmetatable(mt, database);
|
||||
LS.rawset(mt, "__index", classtab);
|
||||
|
||||
|
||||
// Initialize the animstate of the new tangible.
|
||||
tan->anim_queue_.clear(state);
|
||||
tan->anim_queue_.clear(step);
|
||||
tan->update_plane_item();
|
||||
|
||||
|
||||
// Call the constructor and finish.
|
||||
LS.rawget(func, classtab, "init");
|
||||
if (!LS.isfunction(func)) {
|
||||
|
||||
@@ -227,7 +227,7 @@ Tangible *World::tangible_make(const LuaCoreStack &LS0, LuaSlot database, int64_
|
||||
t->delete_on_disconnect_ = false;
|
||||
|
||||
// AnimQueue initializes itself to a valid default state.
|
||||
AnimState state;
|
||||
AnimStepEditor state;
|
||||
state.add_defaults(nullptr);
|
||||
t->anim_queue_.clear(state);
|
||||
t->update_plane_item();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
void World::tangible_clear_anim_queue_to_empty(int64_t id) {
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
AnimState state;
|
||||
AnimStepEditor state;
|
||||
t->anim_queue_.clear(state);
|
||||
t->update_plane_item();
|
||||
}
|
||||
@@ -16,7 +16,7 @@ void World::tangible_clear_anim_queue_to_empty(int64_t id) {
|
||||
void World::tangible_clear_plane_and_xyz(int64_t id, const eng::string &plane, const util::DXYZ &xyz) {
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
AnimState state;
|
||||
AnimStepEditor state;
|
||||
state.set_string("plane", plane);
|
||||
state.set_dxyz("xyz", xyz);
|
||||
state.set_persistent("plane");
|
||||
@@ -28,7 +28,7 @@ void World::tangible_clear_plane_and_xyz(int64_t id, const eng::string &plane, c
|
||||
void World::tangible_walkto(int64_t id, float x, float y) {
|
||||
Tangible *t = tangible_get(id);
|
||||
assert(t != nullptr);
|
||||
AnimState state = t->anim_queue_.get_final_persistent();
|
||||
AnimStepEditor state = t->anim_queue_.get_final_persistent();
|
||||
state.set_string("action", "walkto");
|
||||
state.set_dxyz("xyz", util::DXYZ(x,y,0.0));
|
||||
t->anim_queue_.add(state);
|
||||
@@ -71,7 +71,7 @@ eng::string World::tangibles_near_debug_string(int64_t actor, int64_t distance)
|
||||
LS.rawget(tanobj, tangibles, id);
|
||||
LS.tangetclass(classtab, tanobj);
|
||||
eng::string cname = LS.classname(classtab);
|
||||
AnimState state = tan->anim_queue_.get_final_persistent();
|
||||
AnimStepEditor state = tan->anim_queue_.get_final_persistent();
|
||||
result << id << " (" << cname << "): " << state.debug_string() << std::endl;
|
||||
}
|
||||
return result.str();
|
||||
|
||||
@@ -10,22 +10,22 @@ function login.init()
|
||||
global.set("nextplayer", player + 1)
|
||||
dprint("login.init initializing player ", player)
|
||||
actor.player = player
|
||||
tangible.animinit(actor, {bp="character", plane="earth", xyz={player * 100, 0, 90}})
|
||||
tangible.animinit{tan=actor, anim={bp="character", plane="earth", xyz={player * 100, 0, 90}}}
|
||||
end
|
||||
|
||||
-- This gets called on the admin user. You can call login.init in here if you want.
|
||||
function world.init()
|
||||
dprint("world.init")
|
||||
global.set("nextplayer", 0)
|
||||
tangible.build{class=cube, plane="earth", xyz={500,-100,0}}
|
||||
tangible.build{class=sphere, plane="earth", xyz={500,100,0}}
|
||||
tangible.build{class=cube, anim={plane="earth", xyz={500,-100,0}, mat_color={1,0,0}}}
|
||||
tangible.build{class=sphere, anim={plane="earth", xyz={500,100,0}, mat_color={0,0,1}}}
|
||||
login.init()
|
||||
end
|
||||
|
||||
function engio.move(action, xyz, facing)
|
||||
-- todo: sanity check the parameters.
|
||||
dprint("engio.move ", action, " ", xyz[1], " ", xyz[2], " ", xyz[3])
|
||||
tangible.animate(actor, nil, {action=action, interactive=true, xyz=xyz, facing=facing})
|
||||
tangible.animate{tan=actor, anim={action=action, interactive=true, xyz=xyz, facing=facing}}
|
||||
end
|
||||
|
||||
function cube.lookhotkeys(keys)
|
||||
@@ -85,8 +85,8 @@ function engio.presshotkey(action)
|
||||
end
|
||||
|
||||
function jp3()
|
||||
tangible.animate(actor, nil, {action="play", seq="jump"})
|
||||
tangible.animate(actor, nil, {action="play", seq="jump"})
|
||||
tangible.animate(actor, nil, {action="play", seq="jump"})
|
||||
tangible.animate{tan=actor, anim={action="play", seq="jump"}}
|
||||
tangible.animate{tan=actor, anim={action="play", seq="jump"}}
|
||||
tangible.animate{tan=actor, anim={action="play", seq="jump"}}
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user