Starfield Animation Framework (SAF) is a framework for playing custom animations in Starfield, designed as a modern counterpart to NAF (Native Animation Framework) that works with current versions of the game.
SAF was created for two main reasons:
- the original CCF (Custom Command Framework) used by NAF is no longer maintained and no longer works correctly with the latest Starfield,
- I wanted a tool that behaves like NAF but is built on the current ecosystem (SFSE + CommonLibSF) and on a new Modern Command Framework (MCF).
The framework is an SFSE plugin. It registers its own console command saf, which allows you to:
- play animations on the player or on a chosen actor,
- load animations from external files (e.g. GLTF, GLB),
- control transition time, speed, playback mode, etc. (depending on the exact command and parameters).
SAF’s internal logic is modeled after NAF:
- the animation engine works directly on actor skeletons,
- data from animation files are mapped onto character bones and layered on top of the current state,
- the framework tries to minimize interference with the rest of the game – once an animation finishes, the actor returns to normal behavior,
- character skeleton same as NAF (HumanRace.json).
At the moment, due to changes between game versions and the lack of a stable way to access the “selected reference” from the console, the behavior is:
saf play <animation>– by default plays the animation on the player,- to play an animation on an NPC you must provide its FormID:
saf play <animation> <NPC_FormID>(the FormID can be read from the console after clicking on the NPC). - 'saf stop' - Stops the animation from playing - for Player
- 'saf stop <NPC_FormID>`- Stops the animation from playing - for NPC
This fork adds an xmake build that cross-compiles the SFSE
plugin from Linux to a Windows x64 DLL using the real Microsoft toolchain
— cl.exe, link.exe and rc.exe — run under Wine via
msvc-wine. Earlier revisions used
clang-cl + lld-link against an xwin SDK; that is gone.
wine(64-bit) andxmakeonPATH. Verified against wine-11.7 (Staging) and xmake v3.0.9.- An msvc-wine installation. On this machine msvc-wine itself lives in
/mnt/sata/msvc-wine/and the MSVC + Windows SDK tree it produced lives in/mnt/sata/msvc/, whose wrappers are/mnt/sata/msvc/bin/x64/{cl,link,lib,rc}. To reproduce it elsewhere:
git clone https://github.com/mstorsjo/msvc-wine.git /mnt/sata/msvc-wine
cd /mnt/sata/msvc-wine
./vsdownload.py --accept-license --dest /mnt/sata/msvc # downloads MSVC + Windows SDK
./install.sh /mnt/sata/msvc # writes the bin/x64 wrappersVerify with /mnt/sata/msvc/bin/x64/cl — it should print the cl.exe banner.
At the time of writing that is Microsoft (R) C/C++ Optimizing Compiler Version 19.51.36248 for x64 (MSVC toolset 14.51.36231, Windows SDK 10.0.26100.0).
The build does not call msvc-wine's wrappers directly. build-tools/msvc-wine/{cl,link,lib,rc,ml64,dumpbin}
are symlinks to a single msvc-wrapper.sh that sits in front of them, picks the
tool from the name it was invoked by, and finds the installation through
--msvc-wine-bin (default /mnt/sata/msvc/bin/x64) or $MSVC_WINE_BIN. It
exists to handle two things that otherwise break the build:
-
Response files. xmake passes the full argument list on the command line. Wine hands that to the Windows process as a single
UNICODE_STRING, capped at 32767 characters, and the link line here (~700 objects withbuild/.objs/...paths) is several times that —link.exewould silently receive a truncated command line. Anything past the threshold is written to an@responsefile instead, which the MSVC tools expand themselves. -
Closing inherited file descriptors. Wine's daemons (
wineserver,mspdbsrv.exe,services.exe, …) outlive the compiler invocation that starts them and get reparented to init, keeping every file descriptor they inherited. xmake does not setCLOEXECon itsflockfor.xmake/<plat>/<arch>/project.lock, so a build killed mid-compile leaves that lock held by daemons whose owning process is gone — and since anflockreleases only when every descriptor on the open file description closes, no laterxmakecan ever acquire it. The wrapper closes everything above stderr before exec'ing the real tool.
One more Linux-specific piece, in modules/detect/tools/find_rc.lua: xmake
resolves the mrc toolset by importing detect.tools.find_rc, and the builtin
returns nil on the first line when the host is not Windows. version.rc then
fails with cannot get program for mrc. The project-local override keeps the
builtin's probe logic minus that host check, and xmake.lua registers it with
add_moduledirs("modules").
git clone --recurse-submodules https://github.com/0xra0/StarfieldAnimationFramework.git
# or, in an existing clone:
git submodule update --init --recursivexmake f -p windows -a x64 -m release --toolchain=msvc-wine -y
xmake build -j4-j4 caps parallelism at 4 jobs. xmake otherwise uses every core, and each job
is a separate Wine process — pick a number your cooling is comfortable with, and
pass the same -j on every rebuild.
If MSVC lives somewhere else, point the config at it:
xmake f ... --toolchain=msvc-wine --msvc-wine-bin=/path/to/msvc/bin/x64 -y.
The build uses the repository's top-level source tree (Animation/,
Commands/, Papyrus/, …), which is the tree upstream maintains. Plugin/src/
is an older snapshot of the same code, kept for reference and not built.
Built as separate static-library targets and linked in:
| Target | Source | Version |
|---|---|---|
fmt_lib |
extern/fmt/ |
11.0.2 |
spdlog_lib |
extern/spdlog/ |
master (f355b3d) |
simdjson_lib |
extern/simdjson/ |
master (e341c8b4) |
ozz_lib |
extern/ozz-animation/ |
0.16.0 — base + animation runtime + animation offline |
zlib_lib |
extern/zlib/ |
1.3.1 |
Compiled directly into the plugin target rather than into a library of their own:
extern/commonlibsf/— CommonLibSF (libxse fork, branchstarfield-crosshairref), targeting game runtime 1.16.244.0. Only the parts SAF needs are built: all ofREL/,REX/andSFSE/, plus a hand-picked set ofRE/translation units for Papyrus marshalling — pulling in the wholeRE/layer is not necessary.extern/fastgltf/(0.8.0) — itssrc/*.cppminus the bundled simdjson, which would collide withsimdjson_lib.extern/zstr/— header-only gzip streams layered on zlib's headers, whichzlib_libexports publicly.
ozz-animation, zlib and zstr used to come from xrepo; they are vendored now
because xrepo would rebuild them with cmake, which has no path to MSVC-under-Wine.
build/windows/x64/release/StarfieldAnimationFramework.dll, alongside a
StarfieldAnimationFramework.pdb. The DLL is roughly 3.3 MB and
exports exactly seven symbols: SFSEPlugin_Version, SFSEPlugin_Query,
SFSEPlugin_Load and the four SAFAPI_* entry points. Releases up to v1.0.2 were
~9.7 MB because the build applied xmake's utils.symbols.export_all rule, which
added ~23,500 mangled symbols to the export table; everything SAF actually needs
already carries __declspec(dllexport), so the rule is not used.
xmake build prints nothing, uses no CPU, and never finishes. A previous
build was interrupted and its orphaned Wine daemons are still holding the flock on
.xmake/<plat>/<arch>/project.lock (see "What the wrappers do" above). Confirm it:
ps -o pid,stat,wchan:24 -p "$(pgrep -f 'xmake build')" # WCHAN = locks_lock_inode_wait
fuser -v .xmake/linux/x86_64/project.lock # lists the wine processes holding itKill the listed wineserver / *.exe processes and the lock drops immediately.
The fd-closing in msvc-wrapper.sh prevents new builds from creating this state,
but a lock left behind by an older build still has to be cleared once by hand.
fatal error LNK1136: invalid or corrupt file. A build killed mid-compile can
leave zero-byte object files and cache them, so every subsequent build faithfully
restores the empty objects from build/.build_cache and the link keeps failing.
Purge just the bad entries rather than the whole cache:
find build/.build_cache -type f -size 0 -print | while read -r f; do rm -f "$f" "$f.txt"; done
find build/.objs -name '*.obj' -size 0 -delete
xmake build -j4error: cannot get program for mrc. modules/detect/tools/find_rc.lua is
missing, or add_moduledirs("modules") is absent from xmake.lua. See the end of
"What the wrappers do".
Original mod by mielu91m — https://github.com/mielu91m/StarfieldAnimationFramework-. This repository is a modified fork that adds the Linux cross-compile build system above; all mod functionality and design are the original author's work.
Distributed under the MIT License — see LICENSE. Note: the upstream repository declares no license; MIT is applied here for consistency with the sibling Modern Command Framework by the same author.