Legacy code, circa 2012. I wrote this as part of a MUGEN clone I was building back then. The MUGEN engine drives characters with a scripting format called CNS (Character States) plus CMD (Command/Input) files. Sourcy parses those files and emits either Lua (for a scripted runtime) or C++ (for a compiled runtime), selected at the command line.
This is being revived from a Visual Studio 2008 project. The original code has been upgraded to build on modern compilers, the grammars, and the architecture are unchanged. The build system is new.
compile krono.cns krono2.cns krono3.cns krono.cmd --output LUA --entityname krono
compile krono.cns krono2.cns krono3.cns krono.cmd --output CPP --entityname krono
Given one or more .cns / .cmd files describing a MUGEN-style character, Sourcy emits a set of files in the target language. With --output LUA (the default), you get a per-file Lua module plus a master file that wires them together (See tests/lua/Cloudi_master.lua or tests/lua/Cloudi_master.cpp):
crono.lua ← state blocks from krono.cns
crono2.lua ← state blocks from krono2.cns
crono3.lua ← state blocks from krono3.cns
crono_cmd.lua ← command definitions from krono.cmd
crono_master.lua ← entry point: require()s the other files
With --output CPP, the same conceptual structure is emitted as C++ — one source/header pair per input file plus a master entry point. The result is meant to be compiled into a runtime that doesn't embed a Lua VM, trading flexibility (no hot-reloading) for raw speed.
In both cases each character state in CNS becomes a function on the character object (e.g. crono:state100() in Lua, crono::state100() in C++), and the host runtime ticks the state machine on every frame.
A worked example of the input → output translation lives in compilerinfo.txt.
| Tool | Why |
|---|---|
| CMake ≥ 3.16 | Build system |
| A C/C++ compiler | MSVC, Clang, or GCC. C99 + C++11 |
| Java (JRE/JDK) | To run the ANTLR 3.4 code generator at build time |
The ANTLR 3.4 jar and the libantlr3c C runtime source are vendored under third_party/antlr3/, so you don't need to install ANTLR or its C runtime separately.
cmake -S . -B build
cmake --build buildOverride the deps location if needed:
cmake -S . -B build \
-DDEPS_DIR=/path/to/deps/antlr3 \
-DANTLR3_JAR=/path/to/antlr-3.4-complete.jarThe executable lands at build/compile (or build/Debug/compile.exe on multi-config generators).
Sourcy is structured as a pipeline of stages that pass an AST between them. Every stage implements a PipelineStage interface and is composed into a Pipeline. There is a fixed front-end and one of several interchangeable back-ends, picked at runtime via --output:
┌────────────────────────── FRONT-END (fixed) ──────────────────────────┐
input files ──► │ parser (ANTLR3 lexer+parser) ──► AST ──► semantic check ──► AST │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────── BACK-END (selected by --output) ───────────┐
│ │
│ AST ──► Lua code gen ──► code blocks ──► .lua files │
│ AST ──► C++ code gen ──► code blocks ──► .cpp / .h │
│ │
└───────────────────────────────────────────────────────┘
Conceptually that maps to the source tree like this:
| Stage | Class / File |
|---|---|
| File → AST | CNSFileToAST |
| AST semantic & error check | CNSCheckStage (CNSCheckSemanticActions.cpp) |
| AST → Lua intermediate code blocks | CNSASTToLUAStage (LUAGenSemanticActions.cpp) |
| AST → C++ intermediate code blocks | CNSASTToCPPStage (CPPGenSemanticActions.cpp) |
| Lua file emission | LUAPostGenerator |
| C++ file emission | CPPPostGenerator |
| Pipeline plumbing | Pipeline, PipelineStage, CNSFrontEndPipeline, LUABackEndPipeline, CPPBackEndPipeline |
The lexers, parsers, and tree walkers are written in ANTLR 3. At build time, CMake invokes java -cp antlr-3.4-complete.jar org.antlr.Tool over five grammar files (under Grammars/) which emit C code into build/generated/:
| Grammar | Kind | Produces | Purpose |
|---|---|---|---|
def.g |
Combined lexer+parser | defLexer.{c,h}, defParser.{c,h} |
Tokens shared across CNS — pulled in via tokenVocab |
cns.g |
Combined lexer+parser | cnsLexer.{c,h}, cnsParser.{c,h} |
CNS source → AST |
cnscheck.g |
Tree parser | cnscheck.{c,h} |
Walks the AST to validate semantics, emit errors |
luagen.g |
Tree parser | luagen.{c,h} |
Walks the AST and emits Lua |
cppgen.g |
Tree parser | cppgen.{c,h} |
Walks the AST and emits C++ |
The dependency chain matters: cns.g imports tokens from def.g, and the three tree parsers (cnscheck, luagen, cppgen) all import tokens from cns.g. The CMake build models that explicitly via cns.tokens / def.tokens as build artifacts.
A CNS file is essentially a list of state blocks. Each block has a numeric ID and a body containing controllers (PlaySnd, ChangeState, HitDef, …) gated by triggers (triggerall, trigger1, trigger2, …). The compiler emits one Lua function per state block, with controllers translated to function calls and triggers to chained boolean expressions:
[Statedef 1013]
type = S
movetype= A
physics = N
ctrl = 0
[State 0, PosAdd]
type = PosAdd
trigger1 =time = 0
x= -20
y = 10
[State 203, HitDef]
type = HitDef
trigger1 = time = 0 ;Activate at time = 0 (start of state)
attr = S, NA ;Attributes of the HitDef (explained later)
damage = 50 ;Damage points to deal
guardflag = MA
pausetime = 0,10
hitsound = S21,ifelse(random>499,1,0)
guardsound = 6,0
sparkxy = -10,-20
animtype = hard
ground.type = High
ground.slidetime = 5
ground.hittime = 12
ground.velocity = 0
air.velocity = 0,-4function Cloudi:state_1013()
-- State initialization
self:setStateParams
{
stateType = "S",
moveType = "A",
physics = "N",
ctrl = 0,
}
-- State controller
if ((self:time()) == 0) then
self:posAdd {
x = -20,
y = 10
}
end
-- State controller
if ((self:time()) == 0) then
self:hitDef {
attr = {"S", "NA"},
damage = 50,
guardFlag = "MA",
pauseTime = {0, 10},
hitSound = {{"S", 21}, (self:ifElse(((self:random()) > 499), 1, 0))},
guardSound = {6, 0},
sparkXY = {-10, -20},
animType = "hard",
ground_type = "High",
ground_slideTime = 5,
ground_hitTime = 12,
ground_velocity = 0,
air_velocity = {0, -4}
}
end
endvoid Cloudi::state_1013()
{
// State initialization
if(time().GetInt32() == 0) {
setStateParams_data params;
params.stateType = 'S';
params.moveType = 'A';
params.physics = 'N';
params.ctrl = 0;
setStateParams(params);
}
// State controller
if (((time()) == 0) ) {
posAdd_data d;
d.x = -20;
d.y = 10;
posAdd(d);
}
// State controller
if (((time()) == 0) ) {
hitDef_data d;
d.attr = {"S", "NA"};
d.damage = 50;
d.guardFlag = "MA";
d.pauseTime = {0, 10};
d.hitSound = {{"S", 21}, (ifElse(((random()) > 499), 1, 0))};
d.guardSound = {6, 0};
d.sparkXY = {-10, -20};
d.animType = "hard";
d.ground_type = "High";
d.ground_slideTime = 5;
d.ground_hitTime = 12;
d.ground_velocity = 0;
d.air_velocity = {0, -4};
hitDef(d);
}
}The runtime (not in this repo) provides the FightActor superclass, the per-frame onTick / onRender loop, input handling, collision/hit boxes, and the controller / trigger primitives that the generated code calls into. Two runtimes exist conceptually:
- Lua runtime — loads the
*.luafiles viarequire, dispatches state functions per tick. Cheap to iterate on, supports hot reload. - C++ runtime — links against the generated
*.cpp/*.hdirectly, eliminating the script VM at the cost of needing a full rebuild on every character change.
The compiler is the same up to the back-end; only the emission stage differs.
The semantic checker enforces things like no duplicate state IDs across input files — that's why multi-file inputs are passed in a single invocation rather than compiled independently.
These are real ASTs produced by the front-end during development (rendered to PNG via ANTLR's debug output). They give you a feel for what the parser is doing before code generation kicks in.
A complete [StateDef N] block with [State N] controllers underneath.
[STATEDEF 130]
TYPE = S
PHYSICS = S
[STATE 1]
TYPE = CHANGEANIM
TRIGGER1 = ANIM=130
VALUE = 130
[STATE 2]
TYPE = CHANGESTATE
TRIGGER1 = COMMAND="HOLDDOWN"
...
How MUGEN trigger expressions get parsed — operator precedence, function calls, comparisons.
X = IFELSE(SYSVAR(1) = 0, CONST(VELOCITY.JUMP.NEU.X), IFELSE(SYSVAR(1) = 1, CONST(VELOCITY.JUMP.FWD.X), CONST(VELOCITY.JUMP.BACK.X)))
Nested control structures and how triggers chain.
Interval / range trigger expressions (e.g. time = [0, 10]).
A larger CNS corpus used during development is in tests/cns.
The corresponding output files for the above source CNS/CMD files can be found in tests/lua, and tests/cpp.
compile/
├── CMakeLists.txt ← cross-platform build
├── main.cpp ← entry point, arg parsing, pipeline wiring
├── Pipeline*.{cpp,h} ← pipeline / stage abstractions
├── CNS*.{cpp,h} ← front-end stages (parse, check)
├── LUA*.{cpp,h} ← Lua back-end stages (codegen, post-gen)
├── CPP*.{cpp,h} ← C++ back-end stages (codegen, post-gen)
├── *SemanticActions.{cpp,h} ← semantic-action helpers used by the ANTLR grammars
├── antlr3customtokenstream.* ← custom token stream used by the front-end
├── Grammars/
│ ├── def.g
│ ├── cns.g
│ ├── cnscheck.g
│ ├── luagen.g ← Lua back-end tree walker
│ └── cppgen.g ← C++ back-end tree walker
├── third_party/
│ └── antlr3/ ← vendored ANTLR 3.4 jar + libantlr3c source (BSD-3)
├── tests/ ← sample inputs and AST screenshots
└── compilerinfo.txt ← original design notes (2012)
- Code from 2012, originally targeting Visual Studio 2008 + Win32. Expect warnings on modern Clang/GCC — the CMakeLists silences the worst offenders (
-Wno-pointer-sign,-Wno-incompatible-pointer-types,-fpermissive) without "fixing" anything. - The bundled
antlr3config.hshipped with libantlr3c-3.4 is regenerated at configure time from CMake's owncheck_include_fileprobes, because the vendored copy assumed Linux (malloc.his not a thing on macOS). - The runtimes that consume the generated Lua or C++ are not in this directory — this repo is only the compiler.
The C code generated by ANTLR 3 (last released in 2014) contains constructs — large switches over imaginary <UP> / <DOWN> tokens, implicit returns in long functions, casts between tree adaptor subtypes — that modern Clang and GCC mis-optimize at -O2 and above. The symptom is a bogus runtime error during AST walking:
... error 3 : cannot match to any predicted input
... error 9 : org.antlr.runtime.UnwantedTokenException, near (...) : Extraneous input - expected <UP>
ERROR: Incomplete state controller. Missing attributes for the controller <NAME>.
The bug only appears in Release builds and only on inputs that exercise certain controller patterns (the cloudi.cns / cloudi.cmd pair under tests/cns/ reproduces it). AddressSanitizer and UndefinedBehaviorSanitizer both come back clean, so it's neither a memory error nor a UB class either tool covers; it's a real optimization-vs-code-generator incompatibility.
The fix lives in CMakeLists.txt: the three ANTLR3-generated tree parsers (cnscheck.c, luagen.c, cppgen.c) are pinned to -O1 via set_source_files_properties(... COMPILE_OPTIONS "-O1"). The combined lexer+parser files (cnsLexer.c, cnsParser.c, defLexer.c, defParser.c) are left at the project's default optimization level (they're not tree parsers and don't exhibit the issue. Performance impact is negligible because parsing/walking is not the hot path).
The workaround is currently scoped to non-MSVC compilers (GCC / Clang / AppleClang). MSVC has not been observed to exhibit the bug; if it ever is, the same set_source_files_properties call needs a different flag (/Od or /O1 (note that MSVC's /O1 has different semantics from GCC's)).
If you ever migrate the project to ANTLR 4, this workaround can be removed: ANTLR 4's runtime is C++, has continued maintenance, and the code-generator does not produce the patterns that trigger the bug.
This is preserved 2012 C++, not a continuously maintained codebase. The goal of bringing it back was to make it build and run on modern toolchains, not to modernize the source. Treat the contents as a historical snapshot, with a few specific notes for any reader who wants to understand the trade-offs:
- Global state in semantic actions. Files like
CNSSemanticActions.cpp,CNSCheckSemanticActions.cppandLUAGenSemanticActions.cpphold per-walk state in file-scope variables (pCurController*,pCurCodeBlock, …). This is inherent to ANTLR 3 with C actions: grammar callbacks are free functions with no context pointer, so per-walk state has to live somewhere accessible. The compiler runs single-threaded, so this is correct — just not thread-safe. Modern ANTLR 4 grammars don't need this pattern. - Raw
new/deletein the pipeline.Pipelineowns its stages via raw pointers and frees them in its destructor. Pre-unique_ptrstyle; functionally fine, idiomatically out of date. CNSSemantics.cppis large (≈8.4k lines). It is almost entirely a table of controller / trigger signatures, not behavior. Conceptually it's a data file disguised as a translation unit. Splitting it would be cosmetic.- Tree parser interfaces via
dynamic_cast.Pipelineusesdynamic_castbetweenPipelineStageand theInputXxx/OutputXxxinterfaces. Functionally correct multi-interface composition; today it would be designed with templates orstd::variantover a state-type tag. - Argument parser is permissive. Bad numeric input falls through
atoiwithout error reporting. Tolerable for a tool you invoke yourself; not how you'd write a library.
If you're looking for a model of modern C++ design, this is not it. If you're looking for a working ANTLR-3 compiler front-end with a clean pipeline architecture, it's a reasonable starting point.
Released under the MIT License. Copyright (c) 2012-2026 Miguel Angel Exposito Sanchez (radexx).
You can use, modify, and redistribute this code (including in closed-source derivative products) as long as the copyright notice and license text are preserved in the distribution.
"MUGEN" is a trademark of Elecbyte. This project is unaffiliated with and not endorsed by Elecbyte. It only consumes the CNS/CMD text formats, which are interoperability descriptions and not copyrighted artifacts.
This project links against libantlr3c 3.4 and uses the ANTLR 3.4 code generator, both distributed under the BSD 3-Clause license. Their copyright notices apply to the generated and runtime code.



