diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..3d8ae64 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,139 @@ +# Keeps a release pull request open with the next version and its changelog, worked out by +# release-please from the Conventional Commits pushed to main. Merging that pull request tags the +# version and publishes a GitHub release with its changelog; the builds for each system are then +# attached to it, with a file of their SHA-256 checksums, and the changes contributors made are +# credited to them in its notes. Each archive, and each executable in it, gets a build provenance +# attestation, which `gh attestation verify` checks. The builds hold no game files: the editor +# needs OpenReliant and your own copy of StarLancer. +# +# Run by hand from the Actions tab, it builds everything and keeps the archives as the run's +# artifacts, but publishes nothing and makes no attestations. +# +# The builds are attached after release-please has published the release, so the repository's +# immutable releases must stay off: GitHub refuses files for an immutable release once it is +# published, as OpenReliant found. +name: Release + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: write + issues: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + outputs: + created: ${{ steps.release.outputs.release_created }} + tag: ${{ steps.release.outputs.tag_name }} + steps: + - uses: googleapis/release-please-action@v5 + id: release + with: + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + target-branch: ${{ github.ref_name }} + + build: + needs: release-please + if: needs.release-please.outputs.created == 'true' || github.event_name == 'workflow_dispatch' + strategy: + fail-fast: false + matrix: + include: + - { name: linux-x86_64, os: ubuntu-latest, target: x86_64-linux-gnu } + - { name: linux-aarch64, os: ubuntu-latest, target: aarch64-linux-gnu } + - { name: windows-x86_64, os: windows-latest, target: x86_64-windows-gnu } + - { name: windows-aarch64, os: windows-latest, target: aarch64-windows-gnu } + - { name: macos-x86_64, os: macos-latest, target: x86_64-macos } + - { name: macos-aarch64, os: macos-latest, target: aarch64-macos } + runs-on: ${{ matrix.os }} + # The build only reads the repository. The attestations need a token from GitHub's OIDC provider, + # to get a signing certificate from Sigstore, and the rights `actions/attest` asks for to store + # them. + permissions: + contents: read + id-token: write + attestations: write + artifact-metadata: write + steps: + # The whole history with its tags, so that the build can name the commit a build by hand is + # past the last release. + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - uses: mlugg/setup-zig@v2 + with: + version: 0.17.0 + # An explicit target builds for the architecture's baseline processor, so the binary runs on + # any machine of that kind, not just ones like the runner. + - run: zig build --release=fast -Dstrip -Dtarget=${{ matrix.target }} + - name: Package + shell: bash + env: + VERSION: ${{ needs.release-please.outputs.tag || github.ref_name }} + run: | + name="openreliant-editor-$VERSION-${{ matrix.name }}" + mkdir -p "$name" dist + for file in README.md LICENSE CHANGELOG.md; do + if [ -f "$file" ]; then cp "$file" "$name"/; fi + done + # The licences of the libraries and fonts whose text has to go with every copy. + cp zig-out/LICENSE-*.txt "$name"/ + case "${{ matrix.target }}" in + *windows*) + cp zig-out/bin/openreliant-editor.exe "$name"/ + 7z a -tzip "dist/$name.zip" "$name" > /dev/null + ;; + *) + cp zig-out/bin/openreliant-editor "$name"/ + tar -czf "dist/$name.tar.gz" "$name" + ;; + esac + # For a release, the archive and the executable in it get a build provenance attestation, + # signed through Sigstore, which names the workflow run that built them and its commit. + - if: needs.release-please.outputs.created == 'true' + uses: actions/attest@v4 + with: + subject-path: | + dist/* + zig-out/bin/* + - uses: actions/upload-artifact@v7 + with: + name: ${{ matrix.name }} + path: dist/* + + attach: + needs: [release-please, build] + if: needs.release-please.outputs.created == 'true' + runs-on: ubuntu-latest + steps: + # For the script that credits the contributors. + - uses: actions/checkout@v7 + - uses: actions/download-artifact@v8 + with: + path: dist + merge-multiple: true + # The SHA-256 of each archive, in the format `sha256sum --check` reads. + - name: Write the checksums of the archives + working-directory: dist + env: + TAG: ${{ needs.release-please.outputs.tag }} + run: sha256sum -- *.tar.gz *.zip > "openreliant-editor-$TAG-sha256sums.txt" + # Each change a contributor made gets their handle in the notes (`release-credits.sh`). + - name: Attach the builds to the release, and credit the contributors + shell: bash + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ needs.release-please.outputs.tag }} + run: | + gh release upload "$TAG" dist/* --repo "$GITHUB_REPOSITORY" --clobber + changelog="$(gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json body --jq .body | scripts/release-credits.sh)" + note="OpenReliant Editor needs OpenReliant and your own copy of StarLancer: see the README. The macOS builds are not signed, so macOS blocks them at first; run \`xattr -d com.apple.quarantine openreliant-editor\` once to allow them. To check a download, compare it with \`openreliant-editor-$TAG-sha256sums.txt\`, or run \`gh attestation verify --repo $GITHUB_REPOSITORY\`." + gh release edit "$TAG" --repo "$GITHUB_REPOSITORY" --notes "$note + + $changelog" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..1fa291d --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +# Builds the editor and runs the tests on Linux, macOS and Windows for every pull request and every +# push to main, and checks the Zig code's formatting and the repository's files. +name: Tests + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: mlugg/setup-zig@v2 + with: + version: 0.17.0 + # The formatting and the files are the same on every system, so they're checked once. + - if: runner.os == 'Linux' + run: zig fmt --check src build.zig + - if: runner.os == 'Linux' + run: scripts/check-files.sh + - run: zig build --summary all + - run: zig build test --summary all + # Releases will be built ReleaseFast, without the safety checks, so the tests run in that mode + # too. + - run: zig build test --release=fast --summary all diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c524e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +# Zig's build output, its cache, and the packages it fetches into the project. +/zig-out/ +/.zig-cache/ +/zig-pkg/ + +# The game's files and what's made from them: never commit these. `scripts/check-files.sh`, run by +# the pre-commit hook and by CI, refuses them too. +/game/ +*.[hH][oO][gG] +*.[dD][tT][eE] +*.[sS][hH][pP] +*.[fF][mM]8 +*.[sS][pP][rR] +*.[bB][iI][kK] +*.[eE][xX][eE] +*.[dD][lL][lL] + +# The pictures the editor takes of its screens with --screenshot. +/shots/ diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1 @@ +{} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1a3572f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,23 @@ +# Notes for coding agents + +[CONTRIBUTING.md](CONTRIBUTING.md) holds the project's conventions: follow it in full, and +OpenReliant's own [CONTRIBUTING.md](https://github.com/OpenReliant/openreliant/blob/main/CONTRIBUTING.md) +where this one points to it. These notes add how the maintainer works with an agent. + +- **Starting an issue.** Begin with `gh issue view N --comments`, and read the comments on related + closed issues too, in this repository and in OpenReliant's. +- **Trying a change.** When the maintainer tries a change, build and launch the editor right away: + `make run ARGS=''`. Once they are happy with it, run the checks and commit. +- **Checking a screen.** `openreliant-editor --screenshot ` draws without a + window and saves the frame, for checking a screen against `design/` without showing a window on + the maintainer's screen. `--show`, `--line` and `--game` set up what it shows. +- **Pull requests.** Push the feature branch and open the pull request when the change is ready. + The maintainer merges it. See one pull request merged before starting the next feature. +- **Attribution.** Commits and pull requests carry the maintainer's git identity alone. End commit + messages and pull request bodies with their content, leaving out trailers such as + `Co-Authored-By` and "Generated with" footers. +- **Gaps.** File an issue for each gap you leave, and name the issues in your reply. +- **Plain English.** Write docs, comments, messages, issues, pull requests and replies in plain, + simple technical English, the way you would explain the code to a colleague: normal word order, + common words, short sentences. +- **Replies.** Use the docs' punctuation in replies too: no em or en dashes. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..af12470 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,2 @@ +@AGENTS.md +@CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5ab3a43 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing to OpenReliant Editor + +OpenReliant Editor is a mission editor, script debugger and mod editor for +[OpenReliant](https://github.com/OpenReliant/openreliant). It follows OpenReliant's conventions for +its Zig, its documentation and its history, as OpenReliant's +[CONTRIBUTING.md](https://github.com/OpenReliant/openreliant/blob/main/CONTRIBUTING.md) gives them. +This guide gives what is the editor's own, and is written for people and coding agents alike: +`AGENTS.md` and `CLAUDE.md` point agents here. + +## Getting started + +- Install [Zig 0.17](https://ziglang.org). `zig build` builds the editor, `zig build test` runs the + tests, and `zig build check -fincremental --watch` reports compile errors moments after a save. + The first build fetches OpenReliant and the libraries `build.zig.zon` names, and builds the C and + C++ ones. +- Run `make hooks` once. It installs a pre-commit hook that keeps the game's files out of the + repository. `make help` lists the other workflows. +- `make run ARGS=''` opens a mod, `make run ARGS='.dte'` a mission file. The editor + needs a game folder set up by `openreliant install` for the mods list and the game's own + missions ([installation guide](https://github.com/OpenReliant/openreliant/blob/main/docs/guide/installation.md)). + +## How the editor is put together + +[docs/architecture.md](docs/architecture.md) describes the modules: the platform, the interface +kit, the commands, the game profiles, the mission documents and the screens. + +### The design is the reference + +The screens in [`design/`](design/README.md) are what the editor looks like. A screen follows its +page in `design/screens`: its colours, sizes, text and order. Changing how a screen looks starts +with its page, whose picture is taken again in the same pull request (`make pictures`). Check a +screen against its page with `--screenshot`, which draws without a window: + +```bash +zig-out/bin/openreliant-editor --screenshot script.png +``` + +Text goes where a browser puts the design's: the fonts draw at the design's exact sizes +(`src/ui/fonts.zig`), and `gui.textTop` places a line in the middle of its box as the stylesheet +does, its baseline on a whole pixel. + +### Games come from their profile + +OpenReliant will play other games than StarLancer, so the editor never names a game's commands, +conditions, records or files outside `src/game/`. The interface asks the game's profile +(`src/game.zig`), and a profile reads them from OpenReliant's own tables and builds missions with +OpenReliant's own compiler (`src/game/starlancer.zig`). A profile keeps its data, its names, +extensions, words and records, apart from what it asks of OpenReliant's library, so that profiles +can become files that a Game mode edits. + +### Every action is a command + +Each action is declared once in [`src/commands.zig`](src/commands.zig): its label, its icon, its +menu, and its default keys on Windows and Linux and on macOS. The menus, the toolbar, the command +palette and the Keys settings read that table, and the keys run the action through it, so an +action has the same name, icon and keys wherever it shows. An action that doesn't work yet still +shows, greyed out, with the issue that plans it (`commands.plans`). + +### Text on screen + +Labels and short fragments rather than sentences: `Held · line 46`, `1 problem`, `Use enemies`. +Technical terms are fine where they're the clearest word. The text on screen uses American spelling, +as OpenReliant's screens do; docs and comments use British spelling. + +## Writing Zig + +Write idiomatic Zig 0.17, as OpenReliant's guide describes: name every number, give an id space an +`enum` and switch over it exhaustively, use tagged unions for variants and optionals for "none", +check invariants with `comptime`, and share code rather than copy it. + +C and C++ come in through two shims and one header: the text editor's +([`src/ui/text_editor`](src/ui/text_editor)), Dear ImGui's internal calls +([`src/ui/imgui_extras`](src/ui/imgui_extras)), and [`src/platform/c.h`](src/platform/c.h), which +the build translates into the `c` module with SDL3 and Dear Bindings' C API for Dear ImGui. + +Add `test` blocks next to the code in the same change, with inputs built by hand so that they run +without the game's files. `--check-missions` opens each of a game's own missions as source and +builds it again, for checking a change against the stock missions: + +```bash +zig-out/bin/openreliant-editor --check-missions --game +``` + +## Documentation + +Document what the editor does under [`docs/`](docs), in plain, present-tense reference style. Call +the engine OpenReliant and the original game StarLancer. The project's text uses colons, commas, +parentheses or a second sentence in place of em and en dashes, which `make check-files` refuses. + +## Checks + +Before you open a pull request, run: + +```bash +make check +``` + +It checks the formatting, the repository's files and the tests. CI runs the same on Linux, macOS +and Windows, and the tests in ReleaseFast too. + +## Commits and pull requests + +- Branch from `main` (`feat/script-view`, `fix/...`, `docs/...`) and open a pull request. `main` + changes only through squash-merged pull requests. +- Title the pull request in [Conventional Commits](https://www.conventionalcommits.org) form: + `feat: the Script view builds missions as they're typed`. Keep the body short, and end it with + `Closes #N` for each issue it finishes. +- File an issue for each gap you leave, and link it from the code where the gap is. +- Releases come from [release-please](https://github.com/googleapis/release-please), as + OpenReliant's do ([`release.yml`](.github/workflows/release.yml), + [`release-please-config.json`](release-please-config.json)): it keeps a release pull request open + with the next version and its changelog, and merging it tags the version and attaches the builds + for Windows, macOS and Linux, on x86-64 and Arm, with their checksums. `build.zig.zon` holds the + version being worked towards, which `--version` reports; the first release is 0.0.1. +- The repository holds the editor and its design. The game's files, and everything extracted or + derived from them, stay out: the pre-commit hook, `make check-files` and CI see to it. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7989474 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +# The editor's workflows. `make help` lists them. + +.PHONY: help build run test check fmt check-files hooks pictures icon + +help: + @echo "make build build the editor into zig-out/bin" + @echo "make run build and run the editor (ARGS='' to open one)" + @echo "make test run the tests" + @echo "make check run the checks CI runs: formatting, files, tests" + @echo "make fmt format the Zig code" + @echo "make hooks install the pre-commit hook that keeps the game's files out" + @echo "make pictures take the design's pictures again (design/pictures.sh)" + @echo "make icon make the icon's PNGs, .ico and .icns from its SVG (scripts/icon.sh)" + +build: + zig build --release=safe + +run: + zig build run --release=safe -- $(ARGS) + +test: + zig build test --summary all + +check: check-files + zig fmt --check src build.zig + zig build test --summary all + +fmt: + zig fmt src build.zig + +check-files: + scripts/check-files.sh + +hooks: + printf '#!/bin/sh\nexec scripts/check-files.sh --staged\n' > .git/hooks/pre-commit + chmod +x .git/hooks/pre-commit + +pictures: + design/pictures.sh + +icon: + scripts/icon.sh diff --git a/README.md b/README.md index db0d0dd..cb23a2e 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,12 @@ A mission editor, script debugger and mod editor for StarLancer's engine. It links to the running game, so that a change to a mission or a mod shows in the game as it's made. -**Status:** design, before any code. The plan is OpenReliant's -[#1126](https://github.com/OpenReliant/openreliant/issues/1126), and the [design](design/README.md) -shows every screen. +**Status:** Mission mode's Script view works. It opens a mod's missions, a mission file or one of +the game's own missions as source, colours it with the game's commands, checks it as it's typed, +offers the game's commands and conditions as you type, explains them on hover and in the Reference +panel, builds the mission, and plays it in OpenReliant, from its start or from a part. The rest of +the [design](design/README.md) is in place and greyed out, each part with the issue that plans it. +The plan is OpenReliant's [#1126](https://github.com/OpenReliant/openreliant/issues/1126). ![The Script view of a mission, held at a breakpoint](design/images/mission-script.png) @@ -23,6 +26,26 @@ shows every screen. It needs OpenReliant and your own copy of StarLancer. +## Building it + +With [Zig 0.17](https://ziglang.org): + +```bash +zig build --release=safe +zig-out/bin/openreliant-editor +``` + +Open a mod's folder, a mission's source or a mission file from the start screen, with Open…, or on +the command line: + +```bash +zig-out/bin/openreliant-editor StarLancer/mods/first-mission +zig-out/bin/openreliant-editor StarLancer/mods/first-mission/mission90.dte +``` + +`--help` lists the options. [CONTRIBUTING.md](CONTRIBUTING.md) describes how to work on the editor, +and [docs/architecture.md](docs/architecture.md) how it's put together. + ## Licence [Mozilla Public License 2.0](LICENSE), as OpenReliant. diff --git a/assets/icon/README.md b/assets/icon/README.md new file mode 100644 index 0000000..f1efd59 --- /dev/null +++ b/assets/icon/README.md @@ -0,0 +1,30 @@ +# The editor's icon + +OpenReliant's icon with EDITOR under it: the letters OR in Newtown, the O in the website's +off-white (`#e8eaf0`) and the R in its orange (`#f08a2c`), raised, and EDITOR in IBM Plex Sans +SemiBold, the interface's font, in its grey (`#8a93a6`) and spaced 0.14 em as in the menu bar, as +wide as OR. They stand on OpenReliant's dark sky: `#07080c`, with a warm glow from the top right +(`#3a1d0c`), a blue one from the bottom left (`#101a3a`) and a few faint stars, which fade out at +the smaller sizes. + +| File | What it is | +|---|---| +| `openreliant-editor.svg` | The source, 1024 by 1024. The letters are Newtown's and Plex's outlines, so it needs no font. | +| `openreliant-editor-16.png` to `openreliant-editor-1024.png` | The icon at 16, 32, 48, 64, 128, 256, 512 and 1024 pixels. | +| `openreliant-editor.ico` | For Windows: 16, 20, 24, 32, 40, 48 and 64 pixels as bitmaps, and 256 as a PNG. | +| `openreliant-editor.icns` | For macOS: 16, 32, 128, 256 and 512 points, each at 1x and 2x. | + +`openreliant-editor.exe` carries the `.ico` (`build.zig`), and Windows shows it for the file, the +editor's window and its taskbar button. On macOS and Linux, the editor gives its window the 256 and +512 pixel PNGs, which macOS shows in the Dock ([`icon.zig`](../../src/platform/icon.zig)). + +`make icon` makes the PNGs, the `.ico` and the `.icns` from the SVG again +([`icon.sh`](../../scripts/icon.sh)). It needs `rsvg-convert` (librsvg), `icotool` (icoutils) and +macOS's `iconutil`. + +## Credits and licence + +Newtown is by Roger White, from Roger's Fonts, and is in the public domain +([fonts](../../design/fonts/README.md)). IBM Plex Sans is under the SIL Open Font License. The icon +is the editor's own work, under the [Mozilla Public License 2.0](../../LICENSE) like the rest of the +repository. diff --git a/assets/icon/openreliant-editor-1024.png b/assets/icon/openreliant-editor-1024.png new file mode 100644 index 0000000..ea8005a Binary files /dev/null and b/assets/icon/openreliant-editor-1024.png differ diff --git a/assets/icon/openreliant-editor-128.png b/assets/icon/openreliant-editor-128.png new file mode 100644 index 0000000..048f7f8 Binary files /dev/null and b/assets/icon/openreliant-editor-128.png differ diff --git a/assets/icon/openreliant-editor-16.png b/assets/icon/openreliant-editor-16.png new file mode 100644 index 0000000..150f483 Binary files /dev/null and b/assets/icon/openreliant-editor-16.png differ diff --git a/assets/icon/openreliant-editor-256.png b/assets/icon/openreliant-editor-256.png new file mode 100644 index 0000000..aa5e593 Binary files /dev/null and b/assets/icon/openreliant-editor-256.png differ diff --git a/assets/icon/openreliant-editor-32.png b/assets/icon/openreliant-editor-32.png new file mode 100644 index 0000000..1867c61 Binary files /dev/null and b/assets/icon/openreliant-editor-32.png differ diff --git a/assets/icon/openreliant-editor-48.png b/assets/icon/openreliant-editor-48.png new file mode 100644 index 0000000..0b359a2 Binary files /dev/null and b/assets/icon/openreliant-editor-48.png differ diff --git a/assets/icon/openreliant-editor-512.png b/assets/icon/openreliant-editor-512.png new file mode 100644 index 0000000..0fbc6aa Binary files /dev/null and b/assets/icon/openreliant-editor-512.png differ diff --git a/assets/icon/openreliant-editor-64.png b/assets/icon/openreliant-editor-64.png new file mode 100644 index 0000000..bec907d Binary files /dev/null and b/assets/icon/openreliant-editor-64.png differ diff --git a/assets/icon/openreliant-editor.icns b/assets/icon/openreliant-editor.icns new file mode 100644 index 0000000..278eb52 Binary files /dev/null and b/assets/icon/openreliant-editor.icns differ diff --git a/assets/icon/openreliant-editor.ico b/assets/icon/openreliant-editor.ico new file mode 100644 index 0000000..39f2bb7 Binary files /dev/null and b/assets/icon/openreliant-editor.ico differ diff --git a/assets/icon/openreliant-editor.svg b/assets/icon/openreliant-editor.svg new file mode 100644 index 0000000..1dc1c97 --- /dev/null +++ b/assets/icon/openreliant-editor.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..4946127 --- /dev/null +++ b/build.zig @@ -0,0 +1,240 @@ +const std = @import("std"); +const Translator = @import("translate_c").Translator; +const rc = @import("rc"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + // The releases leave the debug information out of the editor. + const strip = b.option(bool, "strip", "Leave the debug information out of the editor") orelse false; + // The C and C++ libraries' headers become a Zig module through the translate-c package, as in + // OpenReliant. + const translate_c = b.dependency("translate_c", .{}); + + // OpenReliant's library, which reads and writes missions exactly as the game does, and its + // mission source parser, which builds missions from their source. + const openreliant = b.dependency("openreliant", .{ .target = target, .optimize = optimize }); + + // SDL3 for the window, the input and the GPU, the same package and version as OpenReliant's. + // Building for a Mac other than the host, SDL and the editor need the SDK's paths: from xcrun. + const macos_sdk: ?[]const u8 = if (target.result.os.tag == .macos and !target.query.isNative()) + std.mem.trimEnd(u8, b.run(&.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" }), "\n") + else + null; + const sdl_dependency = if (macos_sdk) |sdk| b.dependency("sdl", .{ + .target = target, + .optimize = optimize, + .system_include_path = b.graph.cwdRelativePath(b.pathJoin(&.{ sdk, "usr/include" })), + .system_framework_path = b.graph.cwdRelativePath(b.pathJoin(&.{ sdk, "System/Library/Frameworks" })), + .library_path = b.graph.cwdRelativePath(b.pathJoin(&.{ sdk, "usr/lib" })), + }) else b.dependency("sdl", .{ .target = target, .optimize = optimize }); + const sdl_library = sdl_dependency.artifact("SDL3"); + + // Dear ImGui's docking branch draws the whole interface. It is C++: Dear Bindings' C API wraps + // it, its own backends drive it with SDL3 and SDL_GPU, and the text editor and the few + // internal calls the editor makes join it through small C shims (`src/ui/text_editor`, + // `src/ui/imgui_extras`). + const interface = interfaceLibrary(b, target, sdl_library); + if (macos_sdk) |sdk| addMacosSdk(b, interface.root_module, sdk); + + // The one translated header holds SDL, Dear ImGui and the text editor's shim, so that the types + // they share, such as `SDL_GPUDevice`, are the same Zig types. + const translator: Translator = .init(translate_c, .{ + .c_source_file = b.path("src/platform/c.h"), + .target = target, + .optimize = optimize, + .default_init = true, + }); + translator.linkLibrary(interface); + translator.linkLibrary(sdl_library); + for (interfaceIncludes(b)) |include| translator.addIncludePath(include); + + // The version Release Please keeps in build.zig.zon, and where the checkout is past its last + // release, for `--version` (`src/version.zig`). + const build_options = b.addOptions(); + build_options.addOption([]const u8, "version", @import("build.zig.zon").version); + build_options.addOption([]const u8, "describe", describe(b)); + + const editor = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + .strip = strip, + .imports = &.{ + .{ .name = "openreliant", .module = openreliant.module("openreliant") }, + .{ .name = "source_parser", .module = openreliant.module("source_parser") }, + .{ .name = "c", .module = translator.mod }, + }, + }); + editor.addOptions("build_options", build_options); + if (macos_sdk) |sdk| addMacosSdk(b, editor, sdk); + addFonts(b, editor, openreliant); + // Luau's licence goes with the program, whose mission source parser it is. + b.getInstallStep().dependOn(&b.addInstallFile(openreliant.path("deps/luau/LICENSE.txt"), "LICENSE-luau.txt").step); + // The editor's icon, which the window shows on macOS, also in the Dock, and on Linux + // (`src/platform/icon.zig`). On Windows, the executable carries it. + for ([_][]const u8{ "openreliant-editor-256.png", "openreliant-editor-512.png" }) |icon| { + editor.addAnonymousImport(icon, .{ .root_source_file = b.path(b.fmt("assets/icon/{s}", .{icon})) }); + } + + const exe = b.addExecutable(.{ .name = "openreliant-editor", .root_module = editor }); + if (target.result.os.tag == .windows) addWindowsResources(b, exe, target, optimize); + b.installArtifact(exe); + + const run = b.addRunArtifact(exe); + run.step.dependOn(b.getInstallStep()); + run.addPassthruArgs(); + b.step("run", "Run the editor").dependOn(&run.step); + + // The tests, and `zig build check`, which compiles the program and the tests without linking + // them: `zig build check -fincremental --watch` reports errors within moments of a save. + const tests = b.addTest(.{ .root_module = editor }); + b.step("test", "Run tests").dependOn(&b.addRunArtifact(tests).step); + const check = b.step("check", "Check that the program and the tests compile, without writing binaries"); + check.dependOn(&b.addTest(.{ .root_module = editor }).step); + check.dependOn(&b.addExecutable(.{ .name = "openreliant-editor", .root_module = editor }).step); +} + +/// Gives `module` the macOS SDK's headers, frameworks and libraries at `sdk`, which a build for a +/// Mac other than the host does not find by itself. +fn addMacosSdk(b: *std.Build, module: *std.Build.Module, sdk: []const u8) void { + module.addSystemIncludePath(b.graph.cwdRelativePath(b.pathJoin(&.{ sdk, "usr/include" }))); + module.addSystemFrameworkPath(b.graph.cwdRelativePath(b.pathJoin(&.{ sdk, "System/Library/Frameworks" }))); + module.addLibraryPath(b.graph.cwdRelativePath(b.pathJoin(&.{ sdk, "usr/lib" }))); +} + +/// `git describe` of the checkout against the release tags, such as `v0.0.1-12-gabc1234-dirty`, or +/// nothing where there is no git or no tag, as in a source archive. The build can't track what +/// the answer depends on, so asking makes the build run build.zig again each time, as OpenReliant's +/// does. +fn describe(b: *std.Build) []const u8 { + b.graph.poisonCache(); + const root = b.root.toString(b.allocator) catch @panic("out of memory"); + return switch (b.runFallible(&.{ "git", "-C", root, "describe", "--tags", "--match", "v*", "--long", "--dirty", "--abbrev=7" }, .{ .stderr_behavior = .ignore })) { + .success => |out| std.mem.trimEnd(u8, out, "\n"), + else => "", + }; +} + +/// Gives the Windows executable the editor's icon (`assets/icon`), its name and version for the +/// file's properties and the Task Manager, and its manifest (`src/platform/windows.manifest`). The +/// rc package compiles them without the system's headers, so every system builds the same bytes. +/// Its compiler is built in Debug, which compiles fastest, as it reads only these few files. +fn addWindowsResources(b: *std.Build, exe: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, optimize: std.lang.Optimize) void { + const version = @import("build.zig.zon").version; + const numbers = std.SemanticVersion.parse(version) catch @panic("build.zig.zon's version isn't a semantic version"); + const script = b.addWriteFiles().add("editor.rc", b.fmt( + \\1 ICON "openreliant-editor.ico" + \\ + \\1 VERSIONINFO + \\FILEVERSION {0d},{1d},{2d},0 + \\PRODUCTVERSION {0d},{1d},{2d},0 + \\BEGIN + \\ BLOCK "StringFileInfo" + \\ BEGIN + \\ BLOCK "040904B0" + \\ BEGIN + \\ VALUE "ProductName", "OpenReliant Editor" + \\ VALUE "FileDescription", "OpenReliant Editor" + \\ VALUE "ProductVersion", "{3s}" + \\ VALUE "FileVersion", "{3s}" + \\ VALUE "OriginalFilename", "openreliant-editor.exe" + \\ END + \\ END + \\ BLOCK "VarFileInfo" + \\ BEGIN + \\ VALUE "Translation", 0x0409, 1200 + \\ END + \\END + \\ + , .{ numbers.major, numbers.minor, numbers.patch, version })); + const resources: rc.Compiler = .init(b.dependency("rc", .{ .optimize = .debug }), .{ + .rc_files = &.{script}, + .manifest = .{ .file = b.path("src/platform/windows.manifest"), .for_dll = false }, + .target = target, + .optimize = optimize, + .rc_options = .{ .auto_includes = .none, .include_paths = &.{b.path("assets/icon")} }, + }); + exe.root_module.addObjectFile(resources.getObjectFile()); +} + +/// Dear ImGui with its docking branch, Dear Bindings' C API over it, its SDL3 and SDL_GPU backends, +/// and the text editor with its C shim, as one static library. It is built optimized whatever the +/// editor's own mode, so that typing in a long mission stays smooth in a debug build too. +fn interfaceLibrary(b: *std.Build, target: std.Build.ResolvedTarget, sdl_library: *std.Build.Step.Compile) *std.Build.Step.Compile { + const imgui = b.dependency("imgui", .{}); + const bindings = b.dependency("dear_bindings", .{}); + const text_editor = b.dependency("text_editor", .{}); + const library = b.addLibrary(.{ + .name = "interface", + .linkage = .static, + .root_module = b.createModule(.{ + .target = target, + .optimize = .fast, + .link_libcpp = true, + }), + }); + library.root_module.linkLibrary(sdl_library); + for (interfaceIncludes(b)) |include| library.root_module.addIncludePath(include); + const flags: []const []const u8 = &.{ "-std=c++17", "-fno-sanitize=undefined" }; + library.root_module.addCSourceFiles(.{ + .root = imgui.path("."), + .files = &.{ "imgui.cpp", "imgui_draw.cpp", "imgui_tables.cpp", "imgui_widgets.cpp", "imgui_demo.cpp", "backends/imgui_impl_sdl3.cpp", "backends/imgui_impl_sdlgpu3.cpp" }, + .flags = flags, + }); + library.root_module.addCSourceFiles(.{ + .root = bindings.path("."), + .files = &.{ "dcimgui.cpp", "backends/dcimgui_impl_sdl3.cpp", "backends/dcimgui_impl_sdlgpu3.cpp" }, + .flags = flags, + }); + library.root_module.addCSourceFile(.{ .file = text_editor.path("TextEditor.cpp"), .flags = flags }); + library.root_module.addCSourceFile(.{ .file = b.path("src/ui/text_editor/shim.cpp"), .flags = flags }); + library.root_module.addCSourceFile(.{ .file = b.path("src/ui/imgui_extras/extras.cpp"), .flags = flags }); + // The libraries' licences go with the program. + for ([_]struct { std.Build.LazyPath, []const u8 }{ + .{ imgui.path("LICENSE.txt"), "LICENSE-imgui.txt" }, + .{ bindings.path("LICENSE.txt"), "LICENSE-dear-bindings.txt" }, + .{ text_editor.path("LICENSE"), "LICENSE-text-editor.txt" }, + }) |notice| b.getInstallStep().dependOn(&b.addInstallFile(notice[0], notice[1]).step); + return library; +} + +/// The folders the interface's headers are in. +fn interfaceIncludes(b: *std.Build) [7]std.Build.LazyPath { + const imgui = b.dependency("imgui", .{}); + const bindings = b.dependency("dear_bindings", .{}); + return .{ + imgui.path("."), + imgui.path("backends"), + bindings.path("."), + bindings.path("backends"), + b.dependency("text_editor", .{}).path("."), + b.path("src/ui/text_editor"), + b.path("src/ui/imgui_extras"), + }; +} + +/// The fonts the editor carries, which `src/platform/fonts.zig` embeds: IBM Plex Sans for the +/// interface, JetBrains Mono for code and values, Newtown for its name, from OpenReliant, and +/// Lucide's icons, with the codepoints its icons have. +fn addFonts(b: *std.Build, module: *std.Build.Module, openreliant: *std.Build.Dependency) void { + const plex = b.dependency("plex_sans", .{}); + const mono = b.dependency("jetbrains_mono", .{}); + const lucide = b.dependency("lucide", .{}); + for ([_]struct { []const u8, std.Build.LazyPath }{ + .{ "IBMPlexSans-Regular.ttf", plex.path("fonts/complete/ttf/IBMPlexSans-Regular.ttf") }, + .{ "IBMPlexSans-Medium.ttf", plex.path("fonts/complete/ttf/IBMPlexSans-Medium.ttf") }, + .{ "IBMPlexSans-SemiBold.ttf", plex.path("fonts/complete/ttf/IBMPlexSans-SemiBold.ttf") }, + .{ "JetBrainsMono-Regular.ttf", mono.path("fonts/ttf/JetBrainsMono-Regular.ttf") }, + .{ "JetBrainsMono-Medium.ttf", mono.path("fonts/ttf/JetBrainsMono-Medium.ttf") }, + .{ "JetBrainsMono-SemiBold.ttf", mono.path("fonts/ttf/JetBrainsMono-SemiBold.ttf") }, + .{ "Newtown.ttf", openreliant.path("deps/newtown/Newtown.ttf") }, + .{ "lucide.ttf", lucide.path("font/lucide.ttf") }, + .{ "lucide-codepoints.json", lucide.path("font/codepoints.json") }, + }) |font| module.addAnonymousImport(font[0], .{ .root_source_file = font[1] }); + for ([_]struct { std.Build.LazyPath, []const u8 }{ + .{ plex.path("LICENSE.txt"), "LICENSE-ibm-plex.txt" }, + .{ mono.path("OFL.txt"), "LICENSE-jetbrains-mono.txt" }, + .{ lucide.path("LICENSE"), "LICENSE-lucide.txt" }, + }) |notice| b.getInstallStep().dependOn(&b.addInstallFile(notice[0], notice[1]).step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..234d218 --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,55 @@ +.{ + .name = .openreliant_editor, + .version = "0.0.1", // x-release-please-version + .fingerprint = 0xca6294c595ab78da, + .minimum_zig_version = "0.17.0", + .dependencies = .{ + .openreliant = .{ + .url = "git+https://github.com/OpenReliant/openreliant.git#fd5d7dec75b2d9a3748c39f203699d82918b254e", + .hash = "openreliant-0.14.0-0BXqcn9DEAGMfVU3sRjik-iid3jrPhuvcGUVgqUSJcTc", + }, + .sdl = .{ + .url = "git+https://github.com/castholm/SDL.git?ref=v0.5.4+3.4.16#b72b3671502a70e109350ecac90afdc480dcfcc6", + .hash = "sdl-0.5.4+3.4.16-SDL--sQ4qAH3kwC19Zs1kxdHplaSDnWl2MUptZD4AItI", + }, + .translate_c = .{ + .url = "git+https://codeberg.org/ziglang/translate-c?ref=2.0.0#0da7a16c3235b935b82421646076e0657cda21f6", + .hash = "translate_c-2.0.0-Q_BUWltOBwA0229IE8wwSxxGzOxyRso3hvcb1nN8oJX_", + }, + .dear_bindings = .{ + .url = "https://github.com/dearimgui/dear_bindings/releases/download/DearBindings_v0.24_ImGui_v1.92.9b-docking/DearBindings_v0.24_ImGui_v1.92.9b-docking.zip", + .hash = "N-V-__8AACU1ZQR8hBZ0rIHIpCXU-lyD74drmNZtaXlch9Tn", + }, + .text_editor = .{ + .url = "git+https://github.com/goossens/ImGuiColorTextEdit.git#b12d713e77e8edddff1e69973f67085ac9cfaf74", + .hash = "N-V-__8AANDcfQCwWgHQB--1nns3rygYVA-j1syez9jGDaJA", + }, + .plex_sans = .{ + .url = "https://github.com/IBM/plex/releases/download/%40ibm%2Fplex-sans%401.1.0/ibm-plex-sans.zip", + .hash = "N-V-__8AAHx9wwCXDYaI-Bqj3h8Q9ISr9xIuGRGIhT34zt2L", + }, + .jetbrains_mono = .{ + .url = "https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip", + .hash = "N-V-__8AADWVlwASf7XCdCqpjVW5Jv_7oogANJ_H4_dIoEp6", + }, + .lucide = .{ + .url = "https://registry.npmjs.org/lucide-static/-/lucide-static-1.57.0.tgz", + .hash = "N-V-__8AAPRkFwMXDdr1ybO31hZPOkzp3zHTztiZPpU1GEoF", + }, + .imgui = .{ + .url = "git+https://github.com/ocornut/imgui.git?ref=v1.92.9b-docking#b48d1afbe8ee8b238e2961dc363a949dd7304e23", + .hash = "N-V-__8AAMiwhADTFf225fEZAyMZ0m0uy3MKKpZbqgX5260R", + }, + .rc = .{ + .url = "git+https://codeberg.org/ziglang/rc?ref=zig-0.17.x#a8a3b3cebbe6e414fa12d6ad028c56a3a349c75e", + .hash = "rc-0.0.0-rMftL71EAAAxhe4swpM5rrfcmC9k_j7ApwJJPRssBiDL", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "assets", + "LICENSE", + }, +} diff --git a/design/README.md b/design/README.md index 026cdf2..a762e3a 100644 --- a/design/README.md +++ b/design/README.md @@ -197,7 +197,6 @@ Cmd for Ctrl. Changed keys are saved for each system on its own. - The live scene: [#1059](https://github.com/OpenReliant/openreliant/issues/1059) - The editor's live changes to the mission: [#1057](https://github.com/OpenReliant/openreliant/issues/1057) - Playing a mission from a part: [#577](https://github.com/OpenReliant/openreliant/issues/577) -- The mission format as a package: [#1060](https://github.com/OpenReliant/openreliant/issues/1060) - The game's console and reloading mods' scripts: [#1201](https://github.com/OpenReliant/openreliant/issues/1201) - Starting with a chosen set of mods: [#1202](https://github.com/OpenReliant/openreliant/issues/1202) - Exporting a mod for the catalogue: [#1203](https://github.com/OpenReliant/openreliant/issues/1203) diff --git a/design/fonts/Newtown.ttf b/design/fonts/Newtown.ttf index 6501db6..678d791 100644 Binary files a/design/fonts/Newtown.ttf and b/design/fonts/Newtown.ttf differ diff --git a/design/fonts/README.md b/design/fonts/README.md index b4f87ed..306b336 100644 --- a/design/fonts/README.md +++ b/design/fonts/README.md @@ -6,5 +6,9 @@ is in the public domain: Roger White's site, Roger's Fonts, says that every font domain, for personal and commercial use alike, and asks only that the source be acknowledged where a font is used commercially. +Browsers refuse the original file, so this copy has two fixes: the last range of its character map +maps U+FFFF to the missing glyph, as the format asks, and its `maxp` table gives 2 zones rather than +3. + The pages load IBM Plex Sans and JetBrains Mono from Google Fonts. Both are under the SIL Open Font License, so the editor can carry them too. diff --git a/design/images/command-palette.png b/design/images/command-palette.png index 8467883..6aebfae 100644 Binary files a/design/images/command-palette.png and b/design/images/command-palette.png differ diff --git a/design/images/keys-macos.png b/design/images/keys-macos.png index 1a482e0..9eed34e 100644 Binary files a/design/images/keys-macos.png and b/design/images/keys-macos.png differ diff --git a/design/images/keys.png b/design/images/keys.png index 3920a29..0b2711e 100644 Binary files a/design/images/keys.png and b/design/images/keys.png differ diff --git a/design/images/mission-blocks-flow.png b/design/images/mission-blocks-flow.png index 074903d..2af37b4 100644 Binary files a/design/images/mission-blocks-flow.png and b/design/images/mission-blocks-flow.png differ diff --git a/design/images/mission-blocks.png b/design/images/mission-blocks.png index c47691e..b8e45b3 100644 Binary files a/design/images/mission-blocks.png and b/design/images/mission-blocks.png differ diff --git a/design/images/mission-debugging.png b/design/images/mission-debugging.png index 2b2bfdf..400d9d6 100644 Binary files a/design/images/mission-debugging.png and b/design/images/mission-debugging.png differ diff --git a/design/images/mission-guided.png b/design/images/mission-guided.png index 1b6092b..14794af 100644 Binary files a/design/images/mission-guided.png and b/design/images/mission-guided.png differ diff --git a/design/images/mission-scene.png b/design/images/mission-scene.png index b8c2671..6dbd9a0 100644 Binary files a/design/images/mission-scene.png and b/design/images/mission-scene.png differ diff --git a/design/images/mission-script.png b/design/images/mission-script.png index 9bfe379..a928e9e 100644 Binary files a/design/images/mission-script.png and b/design/images/mission-script.png differ diff --git a/design/images/mission-writing.png b/design/images/mission-writing.png index 9a14b1f..825c651 100644 Binary files a/design/images/mission-writing.png and b/design/images/mission-writing.png differ diff --git a/design/images/mod-code.png b/design/images/mod-code.png index f799d92..f6c48c6 100644 Binary files a/design/images/mod-code.png and b/design/images/mod-code.png differ diff --git a/design/images/mod-game-files.png b/design/images/mod-game-files.png index b78aab0..c459ac9 100644 Binary files a/design/images/mod-game-files.png and b/design/images/mod-game-files.png differ diff --git a/design/images/mod-manifest-ini.png b/design/images/mod-manifest-ini.png index e985bc8..a7eb77e 100644 Binary files a/design/images/mod-manifest-ini.png and b/design/images/mod-manifest-ini.png differ diff --git a/design/images/mod-manifest.png b/design/images/mod-manifest.png index e9d24ab..9746338 100644 Binary files a/design/images/mod-manifest.png and b/design/images/mod-manifest.png differ diff --git a/design/images/mod-share.png b/design/images/mod-share.png index 4fe6192..bedae01 100644 Binary files a/design/images/mod-share.png and b/design/images/mod-share.png differ diff --git a/design/images/start.png b/design/images/start.png index ee1334d..59bce8e 100644 Binary files a/design/images/start.png and b/design/images/start.png differ diff --git a/design/pictures.sh b/design/pictures.sh index 283bfb3..4547319 100755 --- a/design/pictures.sh +++ b/design/pictures.sh @@ -45,8 +45,11 @@ for page in screens/*.html; do out="images/$name.png" rm -f "$out" # The browser stays open after it writes the picture, so it's stopped once the file is there. + # The pages load Newtown from fonts/, which a page opened from a file may only do with + # --allow-file-access-from-files. "$chrome" --headless=new --disable-gpu --hide-scrollbars --force-device-scale-factor=1 \ - --window-size=1440,900 --virtual-time-budget=5000 --user-data-dir="$(native "$profile")" \ + --window-size=1440,900 --virtual-time-budget=5000 --allow-file-access-from-files \ + --user-data-dir="$(native "$profile")" \ --screenshot="$(native "$PWD/$out")" "file:///$(native "$PWD/$page" | sed 's#^/##')" >/dev/null 2>&1 & pid=$! for _ in $(seq 1 60); do diff --git a/design/screens/start.html b/design/screens/start.html index 58d1d14..0d8af92 100644 --- a/design/screens/start.html +++ b/design/screens/start.html @@ -35,8 +35,10 @@
-OpenReliant +
+OpenReliant Editor +