diff --git a/.gitignore b/.gitignore index b18598c..caa7c54 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,9 @@ tools/**/obj/ *.tar.gz *.7z *.rar + +# --------------------------------------------------------------------------- +# Local build and test output (Makefile targets) +# --------------------------------------------------------------------------- +/.test-results/ +/build/ diff --git a/CLAUDE.md b/CLAUDE.md index 6e02131..936bd96 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,6 +58,36 @@ AI Navigation 2.0.14, ProBuilder 6.1.2, Test Framework 1.7.0. Yarn Spinner is an --- +## 2b. Everyday commands + +A `Makefile` at the repo root wraps everything in this document. Prefer it — the exact +invocations live there instead of being retyped. + +```bash +make # list targets +make check # yarn + voice sheets + EditMode — run this before committing +make test # EditMode suite, with a readable summary +make yarn # compile every Yarn script (seconds, no Unity) +make build # macOS player into build/ +make lock # fail if the Editor has the project open +``` + +`make test` and `make build` refuse to run while the Editor is open, which removes the most +common way these commands fail confusingly. + +### Merge driver — one-time per machine + +`.gitattributes` marks Unity YAML for `unityyamlmerge`. Git needs to be told what that is, or +it falls back to a line-oriented text merge that can produce a scene Unity still loads and that +is quietly wrong: + +```bash +make merge-driver +``` + +That points git at `UnityYAMLMerge` inside the Editor bundle (`Contents/Helpers/`, not +`Contents/Tools/` — the path in most online guides is wrong for Unity 6 on macOS). + ## 3. Unity from the command line ### 3.0 Two different tools, both present diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b1c956c --- /dev/null +++ b/Makefile @@ -0,0 +1,66 @@ +# Nightclub Arcadia — common commands. +# +# Everything here is documented in CLAUDE.md; this file exists so the exact +# invocations live in one place instead of being retyped from memory. + +UNITY_CLI ?= /Users/lennart/.unity/bin/unity +UNITY ?= /Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/MacOS/Unity +PROJECT ?= $(CURDIR)/NightclubArcadia +RESULTS ?= $(CURDIR)/.test-results +SMARTMERGE ?= /Applications/Unity/Hub/Editor/6000.5.8f1/Unity.app/Contents/Helpers/UnityYAMLMerge + +.DEFAULT_GOAL := help + +.PHONY: help check yarn yarn-play voices voices-sync test test-play build lock clean-results merge-driver + +help: ## Show this help + @grep -hE '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) \ + | awk 'BEGIN {FS = ":.*?## "} {printf " \033[36m%-14s\033[0m %s\n", $$1, $$2}' + @echo "" + @echo " Unity must be CLOSED for test/build — run 'make lock' to check." + +check: yarn voices test ## Everything a change should pass before commit + +yarn: ## Compile every Yarn script (seconds, no Unity) + @cd tools/YarnCheck && DOTNET_CLI_TELEMETRY_OPTOUT=1 dotnet run -- ../../NightclubArcadia/Assets/Dialogue + +yarn-play: ## Play a node: make yarn-play NODE=Bartender_Talk PICKS="2 2 0 0" + @cd tools/YarnCheck && DOTNET_CLI_TELEMETRY_OPTOUT=1 \ + dotnet run -- ../../NightclubArcadia/Assets/Dialogue $(NODE) $(PICKS) + +voices: ## Check writing/voices/*.md against skill_bible.json + @python3 tools/writing/sync_voices.py --check + +voices-sync: ## Fold edited voice prose into skill_bible.json + @python3 tools/writing/sync_voices.py --to-json + +test: lock ## Run the EditMode suite + @mkdir -p $(RESULTS) + @$(UNITY_CLI) test $(PROJECT) --no-banner --mode EditMode \ + --output $(RESULTS)/editmode.xml --timeout 900 || true + @python3 tools/ci/report_tests.py $(RESULTS)/editmode.xml + +test-play: lock ## Run the PlayMode suite (none exist yet) + @mkdir -p $(RESULTS) + @$(UNITY_CLI) test $(PROJECT) --no-banner --mode PlayMode \ + --output $(RESULTS)/playmode.xml --timeout 900 || true + @python3 tools/ci/report_tests.py $(RESULTS)/playmode.xml + +build: lock ## Build a macOS player into build/ + @$(UNITY_CLI) build $(PROJECT) --no-banner --target StandaloneOSX -o $(CURDIR)/build/NightclubArcadia.app + +lock: ## Fail if the Unity Editor has the project open + @if pgrep -fl "Unity.app/Contents/MacOS/Unity" | grep -v AssetImportWorker | grep -q .; then \ + echo "The Unity Editor is running — close it first (only one instance per project)."; \ + exit 1; \ + fi + +merge-driver: ## One-time: teach git to merge Unity YAML with UnityYAMLMerge + @test -x "$(SMARTMERGE)" || { echo "UnityYAMLMerge not found at $(SMARTMERGE)"; exit 1; } + @git config merge.unityyamlmerge.name "Unity SmartMerge" + @git config merge.unityyamlmerge.driver "'$(SMARTMERGE)' merge -p --force --fallback none %O %B %A %A" + @git config merge.unityyamlmerge.recursive binary + @echo "configured: $$(git config merge.unityyamlmerge.name)" + +clean-results: ## Remove local test results + @rm -rf $(RESULTS) diff --git a/tools/ci/report_tests.py b/tools/ci/report_tests.py new file mode 100755 index 0000000..fe207e1 --- /dev/null +++ b/tools/ci/report_tests.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +"""Summarise an NUnit results file and exit non-zero if anything failed. + +Unity's own docs state there is no common exit-code definition across the +components under test, so the results XML is the authority, not $?. +""" + +import sys +import xml.etree.ElementTree as ET + + +def main(path): + try: + root = ET.parse(path).getroot() + except FileNotFoundError: + print(f"no results at {path} — the run did not get far enough to write one") + return 1 + except ET.ParseError as exc: + print(f"could not parse {path}: {exc}") + return 1 + + total = int(root.get("total") or 0) + passed = int(root.get("passed") or 0) + failed = int(root.get("failed") or 0) + skipped = int(root.get("skipped") or 0) + + for case in root.iter("test-case"): + if case.get("result") == "Passed": + continue + print(f"FAIL {case.get('fullname')}") + message = case.find("failure/message") + if message is not None and message.text: + for line in message.text.strip().splitlines()[:6]: + print(f" {line}") + + if total == 0: + print("no tests ran") + else: + print(f"{passed}/{total} passed" + (f", {skipped} skipped" if skipped else "")) + return 1 if failed else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else ".test-results/editmode.xml"))