# Astral Chain native runtime. Self-contained: the Horizon host (runtime/), the rewriter (third_party/d2r), the NVN-on-Metal
# layer (third_party/d2r_gfx), the shader translator (prebuilt/) and the scripts are this folder's own copies, forked from
# games/totk; nothing here reads games/totk. Everything generated goes to the work folder.
# Inputs come from scripts/extract_exefs.py, scripts/merge_romfs.py and the rewrite (see README.md), then:
#   make prepare   # scripts/prepare_runtime.py: tables, RomFS index, fonts config
#   make runtime   # AC_build/build/alc-native
#   make boot      # run it from the work folder (cwd: extracted/ and rewritten/ are read relative to it)

# opus and libvpx come from the repository's tools/deps: made once per Mac from pinned sources (no Homebrew).
# make runs here (games/<game>); set a *_PREFIX to use another build.
OPUS_PREFIX ?= $(or $(shell python3 ../../tools/deps prefix opus),$(error tools/deps could not make opus))
VPX_PREFIX ?= $(or $(shell python3 ../../tools/deps prefix libvpx),$(error tools/deps could not make libvpx))

WORK ?= AC_build
OUT := $(WORK)/build
PY ?= python3
NATIVE_OPT ?= -O2

GFX_SRC := $(wildcard third_party/d2r_gfx/gfx/nvn/*.c third_party/d2r_gfx/gfx/nvn/*.m third_party/d2r_gfx/gfx/nvn/*.S third_party/d2r_gfx/gfx/tex/*.c third_party/d2r_gfx/gfx/tex/*.m)
GFX_OBJECTS := $(patsubst %,$(OUT)/obj/%.o,$(GFX_SRC))
GFX_HEADERS := $(wildcard third_party/d2r_gfx/gfx/nvn/*.h third_party/d2r_gfx/gfx/nvn/include/*.h third_party/d2r_gfx/host/*.h)
GFX_FLAGS := -std=gnu11 -Wno-error -Wno-unused-function -Wno-unused-parameter -fobjc-arc -DGAME_ALC -Ithird_party/d2r_gfx/gfx/nvn/include

RUNTIME_SRC := $(filter-out runtime/frontend_ios.m,$(wildcard runtime/*.c runtime/*.m runtime/*.S))
RUNTIME_HEADERS := $(wildcard runtime/*.h)
GENERATED := $(OUT)/runtime_tables.h $(OUT)/romfs_config.h $(OUT)/shared_fonts_config.h

.PHONY: prepare runtime boot clean
prepare:
	"$(PY)" scripts/prepare_runtime.py --work "$(WORK)"

$(GENERATED):
	@echo "missing $@: run make prepare" && false

$(OUT)/obj/%.o: % $(GFX_HEADERS) Makefile
	@mkdir -p "$(dir $@)"
	xcrun clang $(NATIVE_OPT) -g -Wall -Wextra $(GFX_FLAGS) -c "$<" -o "$@"

$(OUT)/alc-native: $(RUNTIME_SRC) $(RUNTIME_HEADERS) $(GENERATED) $(GFX_OBJECTS) Makefile
	xcrun clang -std=c11 $(NATIVE_OPT) -g -Wall -Wextra -Werror -Wno-deprecated-declarations \
	  -Ithird_party/d2r_gfx/gfx/nvn/include -I$(OUT) -I$(OPUS_PREFIX)/include/opus -I$(VPX_PREFIX)/include \
	  $(RUNTIME_SRC) $(GFX_OBJECTS) $(OPUS_PREFIX)/lib/libopus.a $(VPX_PREFIX)/lib/libvpx.a \
	  -framework Metal -framework MetalFX -framework Foundation -framework QuartzCore -framework ImageIO -framework CoreGraphics \
	  -framework CoreText -framework UniformTypeIdentifiers -fobjc-arc -framework Cocoa -framework CoreVideo -framework CoreFoundation \
	  -framework AudioToolbox -framework GameController -framework CoreHaptics -framework IOKit -o "$@"

runtime: $(OUT)/alc-native

# The runtime shader translation the graphics layer starts on first use: <work>/build/shader-service/totk_xlate (the
# repository's prebuilt NativeAOT translator, checked against its SHA-256) and <work>/build/spirv-cross/spirv-cross,
# built like build_app.py does (same pinned commit, sources and flags as SPIRV-Cross's CMake Release build).
SPIRV_CROSS_COMMIT := aa217aeb6c9f0ace7a0ab233b28807edf45eb165
SPIRV_CROSS_SOURCES := main.cpp spirv_cfg.cpp spirv_cpp.cpp spirv_cross.cpp spirv_cross_parsed_ir.cpp spirv_cross_util.cpp \
  spirv_glsl.cpp spirv_hlsl.cpp spirv_msl.cpp spirv_parser.cpp spirv_reflect.cpp
.PHONY: shader-tools
shader-tools: $(OUT)/shader-service/totk_xlate $(OUT)/spirv-cross/spirv-cross
$(OUT)/shader-service/totk_xlate: prebuilt/macos-arm64/totk_xlate prebuilt/macos-arm64/totk_xlate.sha256
	@mkdir -p "$(dir $@)"
	test "$$(shasum -a 256 "$<" | cut -d' ' -f1)" = "$$(cut -d' ' -f1 "prebuilt/macos-arm64/totk_xlate.sha256")"
	cp "$<" "$@" && chmod +x "$@"
$(OUT)/spirv-cross/spirv-cross:
	@mkdir -p "$(WORK)/src" "$(dir $@)obj"
	test -d "$(WORK)/src/SPIRV-Cross" || git clone -q https://github.com/KhronosGroup/SPIRV-Cross.git "$(WORK)/src/SPIRV-Cross"
	git -C "$(WORK)/src/SPIRV-Cross" checkout -q $(SPIRV_CROSS_COMMIT)
	for f in $(SPIRV_CROSS_SOURCES); do xcrun clang++ -O3 -DNDEBUG -std=c++11 -arch arm64 -Wno-deprecated-declarations \
	  -c "$(WORK)/src/SPIRV-Cross/$$f" -o "$(dir $@)obj/$$f.o" & done; wait
	xcrun clang++ -arch arm64 $(addprefix $(dir $@)obj/,$(addsuffix .o,$(SPIRV_CROSS_SOURCES))) -o "$@"

# ALC_LOG: where the boot's stderr goes (the host is loud on purpose; every abort names what is missing).
# The launcher's three switches, all recorded in CHANGELOG (stage 3, the first frame):
#   TOTK_FREE_CORES=1     the task manager waits on its fiber with a user-space spin that never enters an SVC,
#                         so the virtual-core model's SVC-boundary preemption never fires and the job's worker
#                         starves; free cores match the hardware's three real cores (stage 3, the park).
#   TOTK_GPU_ALLOW_SUBMIT the TotK-era kernel-panic guard; Astral submits from frame 1 on this machine.
#   TOTK_GPU_MIDCLEAR=1   Astral clears colours mid-pass / masked / scissored on its normal boot path; the
#                         TotK-era isolation guard aborts without it.
# Windowed by default (runtime_front_run). Headless runs set it explicitly: TOTK_HEADLESS=1 make boot —
# the mode test is presence-based, so an empty value would silently drop the window (the old target set
# TOTK_HEADLESS= empty and every make boot ran without one).
ALC_LOG ?= $(WORK)/logs/boot.log
boot: runtime
	@mkdir -p "$(dir $(ALC_LOG))"
	cd "$(WORK)" && TOTK_FREE_CORES=1 TOTK_GPU_ALLOW_SUBMIT=1 TOTK_GPU_MIDCLEAR=1 ./build/alc-native rewritten/v101 boot 2> "$(abspath $(ALC_LOG))"; echo "exit $$?; log: $(ALC_LOG)"

# The shader seed the app carries (the TotK tree's mechanism, TOTK_SHADER_SEED_DIR): the translations this machine's
# cache holds, copied into the bundle's Resources/shader-seed. A first run then translates nothing for what the seed
# already has; new shaders still land in the work folder's cache. Re-run after playing to refresh it.
.PHONY: shader-seed
SEED_CACHE ?= $(OUT)/shader-cache
shader-seed:
	@{ ls -d "$(SEED_CACHE)"/*/ 2>/dev/null | head -1; } | while read d; do \
	  n=$$(find "$$d" -name '*.metal' | wc -l | tr -d ' '); \
	  echo "shader seed: $$n translations from $$d"; \
	  rm -rf "$(OUT)/shader-seed"; mkdir -p "$(OUT)/shader-seed"; \
	  cp "$$d"/*.metal "$$d"/*.json "$(OUT)/shader-seed"/ 2>/dev/null || true; \
	done; [ -d "$(OUT)/shader-seed" ] || { echo "no shader cache yet (play once first)"; exit 1; }

# The player's mods, made into checked tables: exefs patches (mods/1.0.1/<name>/exefs/*.pchtxt) -> <work>/patches,
# RomFS mods (mods/<name>/romfs/...) -> their manifest.tsv (sizes and SHA-256 of the replacements and the originals).
.PHONY: patches mods app
patches:
	"$(PY)" scripts/prepare_patches.py --work "$(WORK)"
mods:
	for d in mods/*/romfs; do [ -d "$$d" ] && "$(PY)" scripts/make_mod_manifest.py "$$d" --romfs "$(WORK)/extracted/v101/romfs"; done; true

# The app: a small launcher bundle (settings window with presets, then the game) that runs the game from the work
# folder, which it names in Info.plist (ALCWorkFolder): nothing large is copied. Its icon comes from the game's own
# control data (GAME=<base or update NSP> KEYS=<prod.keys>), or the app goes without one.
APP ?= Astral Chain.app
GAME ?=
KEYS ?=
$(OUT)/launcher: app/launcher.m Makefile
	xcrun clang -std=gnu11 -O2 -Wall -Wextra -Werror -fobjc-arc app/launcher.m -framework Cocoa -o "$@"
$(OUT)/AppIcon.icns:
	@if [ -n "$(GAME)" ] && [ -n "$(KEYS)" ]; then cd scripts && "$(PY)" extract_app_icon.py "$(GAME)" --keys "$(KEYS)" --title 01007300020fa000 --output "$(abspath $@)"; \
	 else echo "no GAME= / KEYS=: the app gets no icon"; fi
# SELF_CONTAINED=1 (the default): the bundle carries the runtime as well — the rewritten modules, the merged RomFS,
# the shared fonts, the patch tables and the shader tools live in Contents/Resources/work, so the app runs from anywhere
# (another Mac, a copy, a USB disk) without this repository. The copies use APFS clonefile where the volume allows it
# (instant, no extra space until a file changes). SELF_CONTAINED=0 keeps the old thin app that points at $(WORK).
SELF_CONTAINED ?= 1
app: runtime shader-tools $(OUT)/launcher $(OUT)/AppIcon.icns
	rm -rf "$(APP)" && mkdir -p "$(APP)/Contents/MacOS" "$(APP)/Contents/Resources"
	@if [ -d "$(OUT)/shader-seed" ]; then rm -rf "$(APP)/Contents/Resources/shader-seed"; cp -R "$(OUT)/shader-seed" "$(APP)/Contents/Resources/shader-seed"; \
	   echo "shader seed: $$(find "$(APP)/Contents/Resources/shader-seed" -name '*.metal' | wc -l | tr -d ' ') translations carried"; \
	 else echo "no $(OUT)/shader-seed (run make shader-seed after playing to ship one)"; fi
	@if [ "$(SELF_CONTAINED)" = "1" ]; then \
	   W="$(APP)/Contents/Resources/work"; mkdir -p "$$W/rewritten" "$$W/extracted/v101" "$$W/extracted/firmware" "$$W/patches" "$$W/build"; \
	   echo "self-contained: copying the runtime into the bundle (clonefile where possible)"; \
	   cp -Rc "$(WORK)/rewritten/v101" "$$W/rewritten/" 2>/dev/null || cp -R "$(WORK)/rewritten/v101" "$$W/rewritten/"; \
	   cp -Rc "$(WORK)/extracted/v101/romfs" "$$W/extracted/v101/" 2>/dev/null || cp -R "$(WORK)/extracted/v101/romfs" "$$W/extracted/v101/"; \
	   cp -Rc "$(WORK)/extracted/firmware/shared-fonts" "$$W/extracted/firmware/" 2>/dev/null || cp -R "$(WORK)/extracted/firmware/shared-fonts" "$$W/extracted/firmware/"; \
	   cp -R "$(WORK)/patches" "$$W/"; \
	   mkdir -p "$$W/build"; cp "$(OUT)/alc-native" "$$W/build/"; \
	   cp -Rc "$(OUT)/shader-service" "$(OUT)/spirv-cross" "$$W/build/" 2>/dev/null || cp -R "$(OUT)/shader-service" "$(OUT)/spirv-cross" "$$W/build/"; \
	   du -sh "$$W" | sed 's/^/self-contained work folder: /'; \
	 fi
	cp "$(OUT)/launcher" "$(APP)/Contents/MacOS/Astral Chain"
	[ -f "$(OUT)/AppIcon.icns" ] && cp "$(OUT)/AppIcon.icns" "$(APP)/Contents/Resources/" || true
	printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>' \
	  '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' \
	  '<plist version="1.0"><dict>' \
	  '<key>CFBundleExecutable</key><string>Astral Chain</string>' \
	  '<key>CFBundleIdentifier</key><string>org.macnativegames.alc</string>' \
	  '<key>CFBundleName</key><string>Astral Chain</string>' \
	  '<key>CFBundlePackageType</key><string>APPL</string>' \
	  '<key>CFBundleShortVersionString</key><string>1.0.1</string>' \
	  '<key>CFBundleVersion</key><string>1</string>' \
	  '<key>CFBundleIconFile</key><string>AppIcon</string>' \
	  '<key>LSMinimumSystemVersion</key><string>26.0</string>' \
	  '<key>LSApplicationCategoryType</key><string>public.app-category.action-games</string>' \
	  '<key>NSHighResolutionCapable</key><true/>' \
	  '<key>ALCWorkFolder</key><string>$(abspath $(WORK))</string>' \
	  '</dict></plist>' > "$(APP)/Contents/Info.plist"
	codesign --force --sign - "$(APP)"
	@echo "built $(APP) (game data stay in $(abspath $(WORK)))"

# Install the (small) app where Launchpad and Spotlight look: /Applications, or ~/Applications when that is not writable.
INSTALL_DIR ?= $(shell [ -w /Applications ] && echo /Applications || echo $(HOME)/Applications)
.PHONY: install
install: app
	mkdir -p "$(INSTALL_DIR)" && rm -rf "$(INSTALL_DIR)/$(APP)" && cp -R "$(APP)" "$(INSTALL_DIR)/"
	/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -f "$(INSTALL_DIR)/$(APP)"
	@echo "installed $(INSTALL_DIR)/$(APP)"

clean:
	rm -rf "$(OUT)/obj" "$(OUT)/alc-native"
