Unknown mess
This commit is contained in:
@@ -61,6 +61,11 @@ This patch adds the lua function *genlt* and the C function *lua_genlt*. This is
|
||||
|
||||
This patch is live and functioning. The generalized less-than operator is quite useful as the second parameter to lua's builtin *table.sort* function. We have also provided an iterator *table.sortedpairs* that is similar to the lua builtin *table.pairs* that iterates over a table in sorted order. This implicitly uses the *genlt* comparison operator.
|
||||
|
||||
We originally designed this patch to help with determinism.
|
||||
But we eventually realized it was the wrong design, and we
|
||||
ended up not needing it for determinism. It's still a
|
||||
very useful function, though.
|
||||
|
||||
## The Table Iterator Patch
|
||||
|
||||
This patch is designed to address the nondeterminism of the lua 'next' iterator. In the original Lua design, table iteration was nondeterministic. By that, I mean that in the original lua, I can create two empty tables, I can then perform the same sequence of insertions and deletions on those two tables. The two tables are identical: they have the same keys, and they've had the same same sequence of operations applied to them. But I can iterate over them using "table.pairs", and they produce their keys in two different orders. That's nondeterminism.
|
||||
@@ -81,23 +86,63 @@ This patch is live and is used implicitly whenever you iterate over a lua table.
|
||||
|
||||
## The Table Length Patch
|
||||
|
||||
The builtin lua function lua_len is nondeterministic. By that, I mean that two tables with the exact same keys might return different values for lua_len. We can't allow nondeterministic anything in our version of Lua. We have altered the implementation of lua_len so that it is deterministic. Two tables with the same keys will always return the same lua_len, that is now guaranteed.
|
||||
I've changed the lua length operator so that when it is
|
||||
applied to a table, it returns the number of keys in the
|
||||
table. It does this in constant time. This change affects
|
||||
lua_len, lua_rawlen, and the lua # operator.
|
||||
|
||||
Our new implementation of lua_len conforms to the specification in the documentation. I'm not sure that's the right thing to do.
|
||||
You might be wondering what the lua length operator
|
||||
used to do? The lua documentation says this:
|
||||
|
||||
It's obvious how this specification got written: they implemented an algorithm to find the length of a vector as efficiently as possible. By "vector," I mean a table whose keys are 1,2,3,4,5 and so forth. After they wrote this vector-length algorithm, somebody asked, "what happens if you apply that algorithm to a table that's not a vector?" The implementor replied, "it wasn't meant for non-vectors." "Ok, but what *does* it do if you apply it to a non-vector?" They puzzled it out, and they wrote down what it does as "the specification." But that specification when applied to non-vectors isn't *useful*. It's just what their vector-length algorithm happens to spit out when you feed it an input that it wasn't designed to handle.
|
||||
> The length operator applied on a table returns a border in
|
||||
> that table. A border in a table t is any non-negative
|
||||
> integer that satisfies the following condition:
|
||||
>
|
||||
> (border == 0 or t[border] ~= nil) and
|
||||
> (t[border + 1] == nil or border == math.maxinteger)
|
||||
|
||||
So why did they use an algorithm that only works on vectors? Why not use a better algorithm, one that can return the number of keys in the table regardless of whether the table is a vector? The answer is that given the lua table internal representation, returning the number of keys in the table is O(N), whereas the vector-only implementation is usually O(1).
|
||||
Those are *terrible* semantics:
|
||||
|
||||
However, I had to change the table internal representation for the table iterator patch (above). With the modified table representation, returning the number of keys in the table can be done in constant time, whether it's a vector or not.
|
||||
- They're not useful for anything.
|
||||
- It's not deterministic.
|
||||
- In no sense of the word "length" is this the
|
||||
length of the table.
|
||||
|
||||
Let me explain how that mess happened. They obviously
|
||||
wanted the length operator to return the number of keys in
|
||||
the table. Unfortunately, to count the number of keys in a
|
||||
lua table actually takes O(N) time. So they came up with a
|
||||
hack to make it faster: O(1). Unfortunately, the hack relies
|
||||
on the table being a vector. That is, the table must have
|
||||
numbered keys starting with 1. As long as you apply their
|
||||
hack to a vector, it works perfectly and returns the
|
||||
number of keys.
|
||||
|
||||
Now, I'm seriously tempted to have lua_len just return the number of keys in the table. That would be so straightforward and self-explanatory, and faster than the current algorithm. The only reason I haven't done this is that it wouldn't conform to the specification! My new lua_len algorithm is similar to the original algorithm, in that it fails in exactly the same way on non-vectors, in order to be compliant with the specification.
|
||||
Unfortunately, if you apply the hacked length algorithm to a
|
||||
table that isn't a vector, it doesn't work at all.
|
||||
|
||||
Since this feels insane, I have also provided a totally new API function: lua_nkeys. This returns the number of keys in the table, full stop. It's constant-time.
|
||||
But I think the lua documentation didn't want to admit, "it
|
||||
doesn't work at all." So instead, they invented this
|
||||
concept of "a border" and pretended that was in some way a
|
||||
helpful result. They should have just said, "the result is
|
||||
undefined."
|
||||
|
||||
This patch also includes a function lua_nthkey, to get the Nth item in the table iteration, random-access style. I am not certain that this is a good idea, and I have deliberately avoided the use of this function for now, until I am convinced that it's wise.
|
||||
I had to change the table internal representation
|
||||
for the table iterator patch (above). With the modified
|
||||
table representation, returning the number of keys in the
|
||||
table can be done in constant time, whether it's a vector or
|
||||
not. So I changed the length operator to just return
|
||||
the number of keys, full stop.
|
||||
|
||||
This patch is live, and is necessary to the determinism of the system.
|
||||
I've also added another function, lua_nkeys. This also
|
||||
returns the number of keys in the table. It doesn't add any
|
||||
functionality - I could use lua_rawlen and that would work
|
||||
just as well. However, using lua_nkeys emphasizes the fact
|
||||
that my code needs the *real* table length, not the "border"
|
||||
bullshit that lua used to provide.
|
||||
|
||||
This patch is live, and is necessary to the determinism of
|
||||
the system.
|
||||
|
||||
## The Table Flag Bits Patch
|
||||
|
||||
@@ -170,7 +215,7 @@ That's not bad, but it puts both values and methods into the same namespace:
|
||||
|
||||
- By putting both values and methods into the same namespace, we create the possibility of unintended mistakes.
|
||||
|
||||
I am thinking about implementing a new metatable entry: __METHODS = true. If this flag is present, then the colon operator *obj:method* looks for the method in the metatable, instead of looking for it in the object. With this new metamethod, the way to create a class would be to make a table full of methods, and then in the class table, put __METHODS = true. Then you would do this:
|
||||
I am thinking about implementing a new metatable entry: __METHODS = true. If this flag is present, then the colon operator *obj:method* looks for the method in the metatable, instead of looking for it in the object. With this new metamethod, the way to create a class would be to make a table full of methods, and then in that table, also put __METHODS = true. Then you would do this:
|
||||
|
||||
```lua
|
||||
setmetatable(obj, class)
|
||||
@@ -200,6 +245,6 @@ There's no obvious approach to fixing this, so I haven't patched it yet.
|
||||
|
||||
GC Finalizers and weak tables both introduce nondeterminism into Lua execution. We can't allow that. It may be necessary to patch the lua interpreter to simply disable these functions. Alternately, we could simply ask the scripters not to use these features, and declare "undefined behavior" if they do.
|
||||
|
||||
Update 1: I'm using GC finalizers in some cases to clean up userdata objects. I think it's safe as long as the only thing the finalizer does is free memory.
|
||||
Update 1: I'm using GC finalizers in some cases to clean up userdata objects. I think it's safe as long as the only thing the finalizer does is free memory. (NOTE: WHERE?)
|
||||
|
||||
Update 2: I don't remember using userdata objects at all. I am not sure that Update 1 is the truth any more.
|
||||
|
||||
@@ -1,58 +1,138 @@
|
||||
### 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."
|
||||
Luprex uses two different kinds of determinism.
|
||||
|
||||
## Two Degrees of Determinism
|
||||
**Synchronous Model Determinism** Predictive reexecution
|
||||
uses four world models, including a server-synchronous and
|
||||
client-synchronous model. These two models are fed the same
|
||||
events, and must remain in the same state after executing
|
||||
the same events. See the document "Predictive Reexecution"
|
||||
for an explanation of why these models exist. I you were to
|
||||
do a comparison of the two models, they would be equal in
|
||||
the lisp sense of `equal`, but not in the sense of `eq`,
|
||||
because corresponding data structures are not at the
|
||||
same memory address.
|
||||
|
||||
There are two distinct degrees of determinism in the engine, each serving a different purpose.
|
||||
**Replay Log Determinism** The server stores a log of all
|
||||
events it feeds into the Luprex DLL. It can replay a log by
|
||||
feeding the same events into a new copy of the Luprex DLL.
|
||||
When replaying a log, the new copy of the Luprex DLL
|
||||
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.
|
||||
|
||||
**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`.
|
||||
These two forms of determinism serve different purposes and
|
||||
impose different costs.
|
||||
|
||||
**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.
|
||||
## Implementing Synchronous Model Determinism
|
||||
|
||||
The engine currently aims for both, but they serve different purposes and impose different costs.
|
||||
To get the two synchronous models to be deterministic
|
||||
enough, we had to take several steps:
|
||||
|
||||
## Value-Level Determinism: Synchronous Model Pairing
|
||||
- **Deterministic Lua table iteration.** We patch the Lua
|
||||
runtime so that iterating over a table always produces
|
||||
keys in the same order. The order depends only on
|
||||
the order in which the keys were inserted, but not on the
|
||||
memory layout.
|
||||
- **No iterating over C++ 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.** We do not use random
|
||||
numbers in the world model. We do use pseudorandom
|
||||
numbers, we store the generator's state as part of the
|
||||
world model and maintain it using difference transmission.
|
||||
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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.
|
||||
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:
|
||||
To implement replay determinism, we took several
|
||||
difficult steps:
|
||||
|
||||
- **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.
|
||||
- **The Driver/Driven Partition**. The luprex engine is
|
||||
event-driven portion, and an event-driver. The driven
|
||||
portion contains all the game logic. The driver is mainly
|
||||
for I/O. The driven portion cannot contain any I/O. That
|
||||
includes:
|
||||
|
||||
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.
|
||||
- **Clocks only in the Driver.** The driven portion cannot
|
||||
call system functions to obtain the current time.
|
||||
However, the driver can feed the current time into the
|
||||
driven portion as an event.
|
||||
- **Lua Source files only in the Driver** The driven
|
||||
portion cannot read lua source files. It can however
|
||||
enter a state that indicates to the driver that it
|
||||
wants a lua source file. Then, the driver can feed
|
||||
the lua source file in as an event.
|
||||
- **Sockets only in the Driver** The driven portion
|
||||
cannot open TCP/IP sockets. However, it can enter
|
||||
a state that indicates its desire to make a TCP/IP
|
||||
connection, and then the driver can do it and feed
|
||||
the data into the driven portion.
|
||||
|
||||
## The Practical Distinction
|
||||
- **The eng::malloc heap.** A custom memory allocator
|
||||
positioned at a fixed address, used exclusively by the
|
||||
driven portion. The memory allocator, if asked to
|
||||
perform the same sequence of malloc/free operations,
|
||||
will return the same addresses.
|
||||
|
||||
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.
|
||||
- **No threads in the driven portion.** Thread scheduling is
|
||||
nondeterministic at the OS level. We cannot use it in the
|
||||
driven portion.
|
||||
|
||||
## Should we Ditch Replay Determinism?
|
||||
|
||||
Implementing synchronous model determinism is necessary
|
||||
for predictive reexecution. It is non-negotiable.
|
||||
|
||||
On the other hand, replay log determinism is not necessarily
|
||||
required for us to have a usable engine. We could ditch it.
|
||||
It certainly does impose a lot of difficult constraints on
|
||||
the engine.
|
||||
|
||||
The driver/driven distinction certainly required us to tie
|
||||
ourselves into knots in some part of the engine design.
|
||||
But, that's pretty baked in at this point, we're probably
|
||||
never going to change that.
|
||||
|
||||
However, it also imposes a no-threads requirement. That
|
||||
is certainly a bummer from a performance perspective.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
@@ -272,9 +272,9 @@ It is perfectly safe to call FinishedAnimation from anywhere. You can call it fr
|
||||
|
||||
Some animations are instantaneous, such as "warpto." They take zero frames to complete. The following sequence of events occurs: the lua code pushes the "warpto" animation into the queue. The Unreal C++ code sees this and calls *AnimationQueueChanged*. This, in turn, calls *Init Action: Warpto*. That routine actually performs the warpto, and then calls *FinishedAnimation* immediately. So it's even fine to call *FinishedAnimation* directly from the routine that initiates an animation, if you want the animation to be finished as soon as it starts.
|
||||
|
||||
The routine FinishAnimation takes an *lxAnimationStep* as a parameter. This is so that it knows which animation step to mark as finished. TangibleStaticMesh always passes in the variable *CurrentAnimation*.
|
||||
The routine FinishedAnimation takes an *lxAnimationStep* as a parameter. This is so that it knows which animation step to mark as finished. TangibleStaticMesh always passes in the variable *CurrentAnimation*.
|
||||
|
||||
*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:
|
||||
*FinishedAnimation* 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{tan=actor, anim={action="emote", animation="dance", xyz={1,2,3}}}
|
||||
@@ -284,7 +284,7 @@ Since "play an emote" isn't a travel command like "moveto" or "warpto", the lua
|
||||
|
||||
The convention that we have adopted is that to recover from this type of mistake, it is considered acceptable to just play the emote in-place (ie, without moving the actor), then, when the emote is fully finished, the blueprint warps the player to the specified XYZ coordinate. In other words, every animation step is treated as if it has an *implicit* "warpto" at the end of it. This rule guarantees that if the lua programmer sets the xyz, facing, or plane in an animation step, the character will end up at the desired xyz, facing, and plane, no matter what the animation step is.
|
||||
|
||||
That's why *FinishAnimation* has those three boolean flags: Auto Update XYZ, Auto Update Plane, Auto Update Facing. If those are all true – and they almost always should be – then *FinishAnimation* will implement the implicit "warpto" for you. I cannot think of a situation where you would want these flags to be false, but I have left the option, in case somebody wants to do something odd in a Tangible Actor.
|
||||
That's why *FinishedAnimation* has those three boolean flags: Auto Update XYZ, Auto Update Plane, Auto Update Facing. If those are all true – and they almost always should be – then *FinishedAnimation* will implement the implicit "warpto" for you. I cannot think of a situation where you would want these flags to be false, but I have left the option, in case somebody wants to do something odd in a Tangible Actor.
|
||||
|
||||
## How Tangible Actors handle Warping Away
|
||||
|
||||
|
||||
49
Docs/Blueprint Text Export.md
Normal file
49
Docs/Blueprint Text Export.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Blueprint Text Export
|
||||
|
||||
Blueprints are stored as binary `.uasset` files that Claude Code cannot read directly. To work around this, a text exporter automatically converts blueprint graphs to readable text files whenever a blueprint is saved in the Unreal editor.
|
||||
|
||||
## How It Works
|
||||
|
||||
The game module (`FlxIntegrationModuleImpl` in `Source/Integration/Integration.cpp`) hooks into `UPackage::PackageSavedWithContextEvent`. When a blueprint is saved, it iterates each `UEdGraph` in the blueprint and runs `FlxBlueprintExporter` on it. The output is written to `Saved/BlueprintExports/<BlueprintName>/<GraphName>.txt`.
|
||||
|
||||
The exporter class (`Source/Integration/BlueprintExporter.h/.cpp`) processes one graph at a time. The constructor runs all passes and the result is available via `GetOutput()`.
|
||||
|
||||
## Output Format
|
||||
|
||||
The graph file is written to `Saved/BlueprintExports/<BlueprintName>/<GraphName>.txt`. A details file with node-name-to-GUID mappings is written to `Saved/BlueprintExports/<BlueprintName>/DETAILS/<GraphName>.txt`.
|
||||
|
||||
Every line in the graph file starts with a keyword, making it easy to parse. The format is:
|
||||
|
||||
```
|
||||
node Event_Tick
|
||||
return Output_Delegate, Delta_Seconds
|
||||
goto Set_Tick_Delta_Seconds
|
||||
|
||||
node Set_Tick_Delta_Seconds
|
||||
input Real Tick_Delta_Seconds = Event_Tick.Delta_Seconds
|
||||
return Output_Get
|
||||
goto CallFunctionByName
|
||||
```
|
||||
|
||||
### Line Keywords
|
||||
|
||||
- `node Name` — starts a new node block.
|
||||
- `input Type Name = Source` — an input data pin. Source is a `Node.Pin` reference, a literal value, `<self>`, or `<default>`.
|
||||
- `return Pin1, Pin2` — output data pins.
|
||||
- `goto Target` — exec flow (single output).
|
||||
- `goto-if PinName Target` — exec flow (multiple outputs, e.g. branch true/false).
|
||||
- `// comment text` — comment node.
|
||||
|
||||
### Special Handling
|
||||
|
||||
- String defaults are shown in quotes.
|
||||
- Variable get nodes are inlined (the variable name appears directly at the point of use rather than as a separate node).
|
||||
- Knot (reroute) nodes are followed through transparently.
|
||||
|
||||
## Node Ordering
|
||||
|
||||
Nodes are sorted by a traversal algorithm: find starter nodes (exec output but no exec input), sort them by Y position, then traverse each. The traversal visits a node's inputs first (so data sources appear before their consumers), then emits the node itself, then follows exec outputs. This produces a readable top-down flow. Any unvisited nodes are appended at the end.
|
||||
|
||||
## Node Naming
|
||||
|
||||
Names are derived from `ENodeTitleType::ListView` titles, sanitized to alphanumeric plus underscores. Duplicates get `_2`, `_3`, etc.
|
||||
57
Docs/Comment-Formatting-Standards.md
Normal file
57
Docs/Comment-Formatting-Standards.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Comment Formatting Standards
|
||||
|
||||
## Line Width
|
||||
|
||||
Wrap comments at approximately 60 columns. Code and function signatures are not wrapped.
|
||||
|
||||
## Comment Style
|
||||
|
||||
Use C++ style (`//`) for all comments. Use `/** */` only for UENUM/UPROPERTY values that should appear as tooltips in the blueprint editor.
|
||||
|
||||
## Section Banners
|
||||
|
||||
Each class, struct, or enum gets a banner block. The banner line is 60 characters of slashes. The banner names the type and gives a brief description.
|
||||
|
||||
```cpp
|
||||
////////////////////////////////////////////////////////////
|
||||
//
|
||||
// FlxExampleClass
|
||||
//
|
||||
// Brief description of what this class does.
|
||||
//
|
||||
////////////////////////////////////////////////////////////
|
||||
```
|
||||
|
||||
## Header File Banners
|
||||
|
||||
Ideally, we'd like to have documentation about what a header
|
||||
file is for. This would be a banner block at the top of the
|
||||
header file, before the pragma once. If you're claude code,
|
||||
and there is no documentation yet, generate a little, but
|
||||
mark it "DOCUMENTATION GENERATED BY CLAUDE."
|
||||
|
||||
## Member Comments
|
||||
|
||||
Comments on member variables and functions use `//` followed by a blank `//` line:
|
||||
|
||||
```cpp
|
||||
// Brief description of what this does.
|
||||
//
|
||||
void DoSomething();
|
||||
```
|
||||
|
||||
## UFUNCTION Spacing
|
||||
|
||||
UFUNCTION-decorated members should be followed by a single blank line to visually separate them from the next member:
|
||||
|
||||
```cpp
|
||||
// Get the value.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable)
|
||||
int32 GetValue() const;
|
||||
|
||||
// Set the value.
|
||||
//
|
||||
UFUNCTION(BlueprintCallable)
|
||||
void SetValue(int32 Val);
|
||||
```
|
||||
Reference in New Issue
Block a user