2 Commits
Author SHA1 Message Date
Sansan eda2258fde Ignore and untrack dSYM debug bundles
Build and Test / build (push) Successful in 23s
Build and Test / windows (push) Successful in 23s
2026-10-05 18:49:16 +02:00
SansanandClaude Sonnet 5 ad7d19b85a Embed fonts into the executable, ship release artifacts as bare binaries
Build and Test / build (push) Successful in 22s
Build and Test / windows (push) Successful in 1m8s
Fonts are now baked in with #load and loaded via LoadFontFromMemory
instead of LoadFontEx reading assets/*.ttf from disk, so a build no
longer needs an assets/ folder next to it at runtime - the chdir to
the executable's own directory in main.odin is dead code, removed.

just package / just package-windows now produce a single executable
(build/<name>_<version>_<os>_<arch>[.exe]) instead of a tar.gz/zip,
and the release workflow's upload/release-asset globs and the lld/zip
toolchain package list are updated to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-03 20:11:47 +02:00
10 changed files with 37 additions and 1772 deletions

No files matched your search

+1 -1
View File
@@ -49,7 +49,7 @@ jobs:
- name: Setup Toolchain - name: Setup Toolchain
uses: ./.gitea/actions/setup uses: ./.gitea/actions/setup
with: with:
packages: lld zip packages: lld
- name: Cache Windows SDK - name: Cache Windows SDK
uses: actions/cache@v4 uses: actions/cache@v4
+2 -3
View File
@@ -24,7 +24,7 @@ jobs:
- name: Setup Toolchain - name: Setup Toolchain
uses: ./.gitea/actions/setup uses: ./.gitea/actions/setup
with: with:
packages: lld zip packages: lld
- name: Set Version - name: Set Version
run: echo "GAME_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV" run: echo "GAME_VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
@@ -60,5 +60,4 @@ jobs:
NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18 NODE_OPTIONS: '--experimental-fetch' # if nodejs < 18
with: with:
files: |- files: |-
build/${{ env.PROJECT_NAME }}_*.tar.gz build/${{ env.PROJECT_NAME }}_*
build/${{ env.PROJECT_NAME }}_*.zip
+1
View File
@@ -19,3 +19,4 @@ HanjaKanaMun
# Build output # Build output
build/ build/
*.dSYM/
+8 -7
View File
@@ -10,7 +10,7 @@ Commands (see `justfile`): `just run`, `just check`, `just test`, `just package
### Completed ### Completed
- [x] Code split into modules under `src/` (themes, assets, audio, ui, card, decks, memory, profile, screens) - [x] Code split into modules under `src/` (themes, assets, audio, ui, card, decks, memory, profile, screens)
- [x] `.odin` files tracked in git (`.gitignore` used to ignore `*.odin`) - [x] `.odin` files tracked in git (`.gitignore` used to ignore `*.odin`)
- [x] Fonts: M PLUS Rounded 1c (UI + kana), Jua (Hangul), Mali (Thai), all OFL, loaded via `LoadFontEx` with explicit codepoints - [x] Fonts: M PLUS Rounded 1c (UI + kana), Jua (Hangul), Mali (Thai), all OFL, embedded via `#load` and loaded with `LoadFontFromMemory` + explicit codepoints - release builds are a single self-contained executable
- [x] Per-language themes (`themes.Theme`) with flag-based card backs: Japan (Hinomaru), Korea (Taegukgi), Thailand (Trairong) - [x] Per-language themes (`themes.Theme`) with flag-based card backs: Japan (Hinomaru), Korea (Taegukgi), Thailand (Trairong)
- [x] Card flip animation (art rendered to textures and squashed), lift, shadow, lighting; matched cards pulse; cards are dealt in at game start - [x] Card flip animation (art rendered to textures and squashed), lift, shadow, lighting; matched cards pulse; cards are dealt in at game start
- [x] Sound effects synthesized in code (flip, match, mismatch, win, button click), no audio files needed - [x] Sound effects synthesized in code (flip, match, mismatch, win, button click), no audio files needed
@@ -30,15 +30,16 @@ Commands (see `justfile`): `just run`, `just check`, `just test`, `just package
- [ ] macOS release build (would need a macOS runner or osxcross) - [ ] macOS release build (would need a macOS runner or osxcross)
## Known Issues / Gotchas ## Known Issues / Gotchas
1. **Font glyphs must be listed**: raylib only bakes the codepoints passed to `LoadFontEx`. Any new non-ASCII text needs its characters added in `assets.codepoints_for` (or the `EXTRA_*` strings), otherwise it renders as `?`. The `fonts_cover_every_deck_character` test checks deck text. 1. **Font glyphs must be listed**: raylib only bakes the codepoints passed to `LoadFontFromMemory`. Any new non-ASCII text needs its characters added in `assets.codepoints_for` (or the `EXTRA_*` strings), otherwise it renders as `?`. The `fonts_cover_every_deck_character` test checks deck text.
2. **Old font failure root cause**: `DroidSansJapanese.ttf` was a GitHub HTML page saved from a blob URL, not a font. Always download fonts from raw URLs and check them with `file`. 2. **Old font failure root cause**: `DroidSansJapanese.ttf` was a GitHub HTML page saved from a blob URL, not a font. Always download fonts from raw URLs and check them with `file`.
3. **Thai shaping**: raylib does no text shaping, so Thai combining vowels/tone marks would be misplaced. Thai cards use standalone consonants; Thai UI text avoids marks (win message is English). 3. **Thai shaping**: raylib does no text shaping, so Thai combining vowels/tone marks would be misplaced. Thai cards use standalone consonants; Thai UI text avoids marks (win message is English).
4. Tests share the memory game's global state, so they must run single-threaded (`just test` sets `ODIN_TEST_THREADS=1`). 4. Tests share the memory game's global state, so they must run single-threaded (`just test` sets `ODIN_TEST_THREADS=1`).
5. The game changes directory to the executable's folder at startup so `assets/` is found from anywhere. 5. **Windows builds are cross-compiled**: Odin can't link Windows binaries on Linux, so `just windows` builds objects with `-build-mode:obj` and links with `lld-link` against an xwin SDK. The VC runtime is linked statically; only Windows system DLLs are needed. New Odin core usage can need new import libs (e.g. `ole32.lib` for `os.user_data_dir`); the CI `windows` job catches this. Not yet tested on a real Windows machine.
6. **Windows builds are cross-compiled**: Odin can't link Windows binaries on Linux, so `just windows` builds objects with `-build-mode:obj` and links with `lld-link` against an xwin SDK. The VC runtime is linked statically; only Windows system DLLs are needed. New Odin core usage can need new import libs (e.g. `ole32.lib` for `os.user_data_dir`); the CI `windows` job catches this. Not yet tested on a real Windows machine. 6. **Odin constant arrays can't be indexed at runtime**: lookup tables (`DECKS`, `LAYOUTS`, …) are `@(rodata)` globals instead of `::` constants.
7. **Odin constant arrays can't be indexed at runtime**: lookup tables (`DECKS`, `LAYOUTS`, …) are `@(rodata)` globals instead of `::` constants. 7. **Profile memory**: `profile` always allocates from the heap allocator, because the test runner frees each test's allocator afterwards.
8. **Profile memory**: `profile` always allocates from the heap allocator, because the test runner frees each test's allocator afterwards. 8. **`odin test` segfaults if the stack rlimit is "unlimited"**: the CI runner's container sets `ulimit -s unlimited`, which makes `getrlimit(RLIMIT_STACK).rlim_cur` come back as `RLIM_INFINITY` (a huge unsigned value). Odin's `core:thread` passes that straight to `pthread_attr_setstacksize`, `pthread_create` then fails, and `thread.create()` returns `nil` - which `core:testing`'s `pool_init` dereferences unconditionally, segfaulting the whole test binary (exit 139) before it prints anything. `just test` works around it with `ulimit -s 8192` on the same line as `odin test` (`just` runs each recipe line in its own shell, so a separate line wouldn't stick). Upstream Odin bug (core:thread/core:testing don't handle a failed thread creation); not our code.
9. **`odin test` segfaults if the stack rlimit is "unlimited"**: the CI runner's container sets `ulimit -s unlimited`, which makes `getrlimit(RLIMIT_STACK).rlim_cur` come back as `RLIM_INFINITY` (a huge unsigned value). Odin's `core:thread` passes that straight to `pthread_attr_setstacksize`, `pthread_create` then fails, and `thread.create()` returns `nil` - which `core:testing`'s `pool_init` dereferences unconditionally, segfaulting the whole test binary (exit 139) before it prints anything. `just test` works around it with `ulimit -s 8192` before running `odin test`. Upstream Odin bug (core:thread/core:testing don't handle a failed thread creation); not our code. 9. **CI jobs share one self-hosted runner**: `build` and `windows` in `gitea-build.yaml` both clone `actions/checkout`/`actions/cache` into the same cache directory on the runner. Starting them at the same time can race on that clone and fail one job outright, so `windows` runs with `needs: build`.
10. **Release artifacts are bare executables**, not archives: fonts are embedded in the binary (see gotcha 1), so there's nothing else to ship alongside it.
## File Structure ## File Structure
``` ```
-20
View File
@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleIdentifier</key>
<string>com.apple.xcode.dsym.HanjaKanaMun</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>dSYM</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -28,9 +28,9 @@ Requires [Odin](https://odin-lang.org) (dev-2026-09) and [just](https://just.sys
just run # build and play (debug) just run # build and play (debug)
just check # type-check with -vet just check # type-check with -vet
just test # run tests just test # run tests
just package 1.0 # native release build -> build/HanjaKanaMun_1.0_<os>_<arch>.tar.gz just package 1.0 # native release build -> build/HanjaKanaMun_1.0_<os>_<arch> (self-contained executable)
just windows-deps # Linux only: fetch Windows SDK/CRT (xwin) and raylib.lib just windows-deps # Linux only: fetch Windows SDK/CRT (xwin) and raylib.lib
just package-windows 1.0 # cross-compiled Windows build -> build/HanjaKanaMun_1.0_windows_x86_64.zip just package-windows 1.0 # cross-compiled Windows build -> build/HanjaKanaMun_1.0_windows_x86_64.exe
``` ```
The Windows build needs `lld-link`. On macOS, `brew install lld` and set `LLD_LINK`, and point `XWIN_SDK` and The Windows build needs `lld-link`. On macOS, `brew install lld` and set `LLD_LINK`, and point `XWIN_SDK` and
+11 -13
View File
@@ -36,16 +36,15 @@ test:
# `ulimit` line wouldn't carry over to the `odin test` line.) # `ulimit` line wouldn't carry over to the `odin test` line.)
ulimit -s 8192 && odin test tests -vet -define:ODIN_TEST_THREADS=1 -out:{{build_dir}}/tests ulimit -s 8192 && odin test tests -vet -define:ODIN_TEST_THREADS=1 -out:{{build_dir}}/tests
# Optimized native build into build/<os>/, with the assets it needs at runtime # Optimized native build into build/<os>/<name> (self-contained: fonts are embedded)
release: release:
mkdir -p {{build_dir}}/{{os()}}/{{name}} mkdir -p {{build_dir}}/{{os()}}
odin build . -vet -o:speed -out:{{build_dir}}/{{os()}}/{{name}}/{{name}} odin build . -vet -o:speed -out:{{build_dir}}/{{os()}}/{{name}}
cp -R assets {{build_dir}}/{{os()}}/{{name}}/
# Native release build packed as build/<name>_<version>_<os>_<arch>.tar.gz # Native release build as build/<name>_<version>_<os>_<arch>
package version="dev": release package version="dev": release
tar -czf {{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz -C {{build_dir}}/{{os()}} {{name}} cp {{build_dir}}/{{os()}}/{{name}} {{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}
@echo "{{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}.tar.gz" @echo "{{build_dir}}/{{name}}_{{version}}_{{os()}}_{{arch()}}"
# Download the Windows SDK + CRT (via xwin, accepting Microsoft's license) and raylib.lib. Linux x86_64 only. # Download the Windows SDK + CRT (via xwin, accepting Microsoft's license) and raylib.lib. Linux x86_64 only.
windows-deps: windows-deps:
@@ -67,12 +66,12 @@ windows-deps:
unzip -o -j {{build_dir}}/odin-windows.zip 'dist/vendor/raylib/windows/raylib.lib' -d "{{raylib_windows}}" unzip -o -j {{build_dir}}/odin-windows.zip 'dist/vendor/raylib/windows/raylib.lib' -d "{{raylib_windows}}"
fi fi
# Cross-compile a Windows x86_64 build into build/windows/ (run `just windows-deps` first) # Cross-compile a self-contained Windows x86_64 build into build/windows/ (run `just windows-deps` first)
windows: windows:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
obj_dir={{build_dir}}/obj-windows obj_dir={{build_dir}}/obj-windows
out_dir={{build_dir}}/windows/{{name}} out_dir={{build_dir}}/windows
mkdir -p "$obj_dir" "$out_dir" mkdir -p "$obj_dir" "$out_dir"
rm -f "$obj_dir"/*.obj rm -f "$obj_dir"/*.obj
odin build . -vet -o:speed -target:windows_amd64 -build-mode:obj -out:"$obj_dir/{{name}}.obj" odin build . -vet -o:speed -target:windows_amd64 -build-mode:obj -out:"$obj_dir/{{name}}.obj"
@@ -88,12 +87,11 @@ windows:
libcmt.lib libvcruntime.lib ucrt.lib \ libcmt.lib libvcruntime.lib ucrt.lib \
kernel32.lib user32.lib gdi32.lib winmm.lib shell32.lib opengl32.lib \ kernel32.lib user32.lib gdi32.lib winmm.lib shell32.lib opengl32.lib \
advapi32.lib bcrypt.lib ntdll.lib synchronization.lib ole32.lib advapi32.lib bcrypt.lib ntdll.lib synchronization.lib ole32.lib
cp -R assets "$out_dir/"
# Windows build packed as build/<name>_<version>_windows_x86_64.zip # Windows build as build/<name>_<version>_windows_x86_64.exe
package-windows version="dev": windows package-windows version="dev": windows
cd {{build_dir}}/windows && zip -qr ../{{name}}_{{version}}_windows_x86_64.zip {{name}} cp {{build_dir}}/windows/{{name}}.exe {{build_dir}}/{{name}}_{{version}}_windows_x86_64.exe
@echo "{{build_dir}}/{{name}}_{{version}}_windows_x86_64.zip" @echo "{{build_dir}}/{{name}}_{{version}}_windows_x86_64.exe"
# Format all Odin files with odinfmt (settings in odinfmt.json) # Format all Odin files with odinfmt (settings in odinfmt.json)
fmt: fmt:
-2
View File
@@ -30,8 +30,6 @@ main :: proc() {
raylib.InitWindow(themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, "HanjaKanaMun") raylib.InitWindow(themes.SCREEN_WIDTH, themes.SCREEN_HEIGHT, "HanjaKanaMun")
defer raylib.CloseWindow() defer raylib.CloseWindow()
raylib.SetExitKey(.KEY_NULL) raylib.SetExitKey(.KEY_NULL)
// Assets are loaded relative to the executable, so it can be started from anywhere
raylib.ChangeDirectory(raylib.GetApplicationDirectory())
raylib.SetTargetFPS(60) raylib.SetTargetFPS(60)
raylib.InitAudioDevice() raylib.InitAudioDevice()
+12 -8
View File
@@ -12,12 +12,14 @@ Font_Style :: enum {
THAI, // Thai script (Mali SemiBold) THAI, // Thai script (Mali SemiBold)
} }
// Fonts are baked into the executable so a release is a single self-contained
// binary: no assets/ folder has to ship (or be found) alongside it at runtime.
@(rodata) @(rodata)
FONT_PATHS := [Font_Style]cstring{ FONT_DATA := [Font_Style][]u8{
.REGULAR = "assets/MPLUSRounded1c-Medium.ttf", .REGULAR = #load("../../assets/MPLUSRounded1c-Medium.ttf"),
.BOLD = "assets/MPLUSRounded1c-ExtraBold.ttf", .BOLD = #load("../../assets/MPLUSRounded1c-ExtraBold.ttf"),
.KOREAN = "assets/Jua-Regular.ttf", .KOREAN = #load("../../assets/Jua-Regular.ttf"),
.THAI = "assets/Mali-SemiBold.ttf", .THAI = #load("../../assets/Mali-SemiBold.ttf"),
} }
fonts: [Font_Style]raylib.Font fonts: [Font_Style]raylib.Font
@@ -56,10 +58,12 @@ codepoints_for :: proc(style: Font_Style, allocator := context.allocator) -> [dy
load_font :: proc(style: Font_Style) -> raylib.Font { load_font :: proc(style: Font_Style) -> raylib.Font {
codepoints := codepoints_for(style) codepoints := codepoints_for(style)
defer delete(codepoints) defer delete(codepoints)
path := FONT_PATHS[style] data := FONT_DATA[style]
font := raylib.LoadFontEx(path, FONT_BASE_SIZE, raw_data(codepoints), i32(len(codepoints))) font := raylib.LoadFontFromMemory(
".ttf", raw_data(data), i32(len(data)), FONT_BASE_SIZE, raw_data(codepoints), i32(len(codepoints)),
)
if !raylib.IsFontValid(font) || font.texture.id == 0 { if !raylib.IsFontValid(font) || font.texture.id == 0 {
raylib.TraceLog(.WARNING, "ASSETS: Falling back to default font, could not load %s", path) raylib.TraceLog(.WARNING, "ASSETS: Falling back to default font, could not load style %v", style)
return raylib.GetFontDefault() return raylib.GetFontDefault()
} }
// Mipmaps keep small UI text smooth when the large atlas is scaled down // Mipmaps keep small UI text smooth when the large atlas is scaled down