From de80abaa80cb522c4547fc0ef80abe1d837392a4 Mon Sep 17 00:00:00 2001 From: halestom15 Date: Wed, 20 May 2026 00:55:20 -0400 Subject: [PATCH] Revert "Revert "Mac projector fallback"" --- mac-patcher/.gitignore | 6 + mac-patcher/README.md | 74 + mac-patcher/SHADER_INVENTORY.md | 59 + mac-patcher/design-refactor.md | 330 +++ mac-patcher/extract_projector_specs.py | 198 ++ mac-patcher/generate_overlay_assets.py | 282 +++ mac-patcher/inspect_cohesion.py | 83 + mac-patcher/inspect_externals.py | 51 + mac-patcher/materials-reference.md | 170 ++ mac-patcher/patch_cohesion_rename.py | 81 + mac-patcher/patch_save_for_mac.py | 2171 +++++++++++++++++ mac-patcher/requirements.txt | 3 + mac-patcher/retro-cohesion.md | 170 ++ mac-patcher/retro-deployment.md | 76 + mac-patcher/retro-movement.md | 122 + mac-patcher/retro-range.md | 86 + mac-patcher/scan_bundles.py | 166 ++ .../mac-fallback-assets/cohesion_halo.png | Bin 0 -> 22585 bytes .../mac-fallback-assets/deployment_blue.png | Bin 0 -> 856 bytes .../mac-fallback-assets/deployment_red.png | Bin 0 -> 856 bytes .../mac-fallback-assets/max_move_cyan.png | Bin 0 -> 4124 bytes .../mac-fallback-assets/range_bombCart.png | Bin 0 -> 12426 bytes .../range_fig_leader_epic.png | Bin 0 -> 19173 bytes .../range_fig_leader_huge.png | Bin 0 -> 18811 bytes .../range_fig_leader_laat.png | Bin 0 -> 19058 bytes .../range_fig_leader_large.png | Bin 0 -> 18753 bytes .../range_fig_leader_long_stadium.png | Bin 0 -> 19025 bytes .../range_fig_leader_medium.png | Bin 0 -> 18599 bytes .../range_fig_leader_small.png | Bin 0 -> 18475 bytes .../range_fig_leader_snail_stadium.png | Bin 0 -> 19127 bytes mod/data/mac-fallback-assets/range_poi.png | Bin 0 -> 10364 bytes .../mac-fallback-assets/range_smokeToken.png | Bin 0 -> 10364 bytes mod/data/mac-fallback-assets/range_token.png | Bin 0 -> 10364 bytes .../range_tokenRangeTwo.png | Bin 0 -> 12344 bytes .../silhouette_mac_fallback.unity3d | Bin 0 -> 10922 bytes mod/src/StarWarsLegion.lua | 1 + mod/src/StarWarsLegion/Order_Token.a57c41.lua | 97 +- .../SETUP_CONTROLLER.1cb552.lua | 32 +- mod/src/includes/Cohesion.ttslua | 28 +- mod/src/includes/Overlays.ttslua | 929 +++++++ mod/src/includes/RangeRulers.ttslua | 30 + src/index.ts | 26 +- 42 files changed, 5218 insertions(+), 53 deletions(-) create mode 100644 mac-patcher/.gitignore create mode 100644 mac-patcher/README.md create mode 100644 mac-patcher/SHADER_INVENTORY.md create mode 100644 mac-patcher/design-refactor.md create mode 100644 mac-patcher/extract_projector_specs.py create mode 100644 mac-patcher/generate_overlay_assets.py create mode 100644 mac-patcher/inspect_cohesion.py create mode 100644 mac-patcher/inspect_externals.py create mode 100644 mac-patcher/materials-reference.md create mode 100644 mac-patcher/patch_cohesion_rename.py create mode 100644 mac-patcher/patch_save_for_mac.py create mode 100644 mac-patcher/requirements.txt create mode 100644 mac-patcher/retro-cohesion.md create mode 100644 mac-patcher/retro-deployment.md create mode 100644 mac-patcher/retro-movement.md create mode 100644 mac-patcher/retro-range.md create mode 100644 mac-patcher/scan_bundles.py create mode 100644 mod/data/mac-fallback-assets/cohesion_halo.png create mode 100644 mod/data/mac-fallback-assets/deployment_blue.png create mode 100644 mod/data/mac-fallback-assets/deployment_red.png create mode 100644 mod/data/mac-fallback-assets/max_move_cyan.png create mode 100644 mod/data/mac-fallback-assets/range_bombCart.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_epic.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_huge.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_laat.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_large.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_long_stadium.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_medium.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_small.png create mode 100644 mod/data/mac-fallback-assets/range_fig_leader_snail_stadium.png create mode 100644 mod/data/mac-fallback-assets/range_poi.png create mode 100644 mod/data/mac-fallback-assets/range_smokeToken.png create mode 100644 mod/data/mac-fallback-assets/range_token.png create mode 100644 mod/data/mac-fallback-assets/range_tokenRangeTwo.png create mode 100644 mod/data/mac-fallback-assets/silhouette_mac_fallback.unity3d create mode 100644 mod/src/includes/Overlays.ttslua diff --git a/mac-patcher/.gitignore b/mac-patcher/.gitignore new file mode 100644 index 00000000..34bac69b --- /dev/null +++ b/mac-patcher/.gitignore @@ -0,0 +1,6 @@ +venv/ +__pycache__/ +*.pyc +*.bak +*.bak-* +scan_bundles_*.out diff --git a/mac-patcher/README.md b/mac-patcher/README.md new file mode 100644 index 00000000..420bd388 --- /dev/null +++ b/mac-patcher/README.md @@ -0,0 +1,74 @@ +# mac-patcher + +Python patcher that injects Mac-compatibility fallbacks into a SWL TTS save +file. Works around the TTS Unity 6 (Berserk) shader bug where vanilla +AssetBundle Projectors (range rulers, cohesion halos, movement templates, +deployment zones, silhouettes) render as a magenta rectangle on macOS +because the custom shader bundled by Allen White (Dicewrench) fails to load +under Unity 6 Metal. + +## What it does + +For each affected object script in a `TS_Save_N.json`: + +- **Cohesion / Range / Maximum Move / Deployment / Silhouette**: routes the + visualization through a Mac-friendly path (vector lines + decal PNGs) + built into the Global script. The vanilla bundle path stays available + for per-seat opt-in (see "Mac TTS U6 Patch" floating panel). + +- **SIL button**: renames `click_function = "toggleSilhouettes"` to + `"macToggleSil"` + injects a forwarder. Empirically the engine silently + swallows the original click_function name in Mac mode (the button plays + its sound/animation but the function is never invoked). The rename + bypasses the issue entirely. + +- **clearSilhouette nil guard**: upstream `clearSilhouette()` derefs + `removeAttachments()[1]` which is nil when `silhouetteState` is true but + the physical silhouette attachment has been lost across save reloads. + Patched to guard the nil case + reset state in onload. + +- **Order Token Speed/Move button overrides**: inlines vanilla bodies of + `changeSpeed1/2/3` and `moveForward/Backwards/Left/Right` to capture + the clicking player's color (`macActivePlayerForMove`) for per-seat + routing. Inlined rather than wrapped because the List Builder copies the + block to Command Token Custom_Models which lack the vanilla + Order_Token function bodies (wrap+call_orig would crash with "attempt + to call a nil value"). + +## Usage + +```bash +# Setup once +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt + +# Patch a save +python3 patch_save_for_mac.py [--reload] +``` + +The patcher also mirrors the patched save into +`~/Library/Tabletop Simulator/Saves/TS_Save_N.json` (next free slot, or the +existing `SWL BETA - MAC PATCH` slot if present) so TTS picks it up in +Games -> Save & Load. + +`--reload` hot-pushes the patched scripts to a running TTS instance via the +External Editor API (no need to restart TTS). + +## Idempotence + +All patches are idempotent: a re-run on an already-patched save strips the +previous Mac patch block from Global (via marker regex) and re-injects +fresh. Per-object wrappers are also stripped before re-injection. + +## Files + +- `patch_save_for_mac.py` - main patcher +- `scan_bundles.py` - UnityPy scanner for the local TTS bundle cache +- `extract_projector_specs.py` - dump Projector material properties from a + bundle (used to discover the `_Arc` shader keyword for firing arc lines) +- `generate_overlay_assets.py` - regenerate the range/cohesion/etc. PNG + decals from authoritative SWL geometry +- `inspect_*.py` - debug helpers +- `retro-*.md` - retrospective notes on each subsystem rewrite +- `SHADER_INVENTORY.md` - list of custom shaders in the vanilla bundles diff --git a/mac-patcher/SHADER_INVENTORY.md b/mac-patcher/SHADER_INVENTORY.md new file mode 100644 index 00000000..926e86db --- /dev/null +++ b/mac-patcher/SHADER_INVENTORY.md @@ -0,0 +1,59 @@ +# SW Legion TTS Mod — Shader Inventory + +Scan effectué le 2026-05-07 sur `~/Library/Tabletop Simulator/Mods/Assetbundles/` (40 bundles `.unity3d`). +Outil : UnityPy 1.25.0, script `scan_bundles.py`. Output brut : `scan_bundles_2026-05-07.out`. + +## TL;DR +**9 shaders custom distincts**, **tous embarqués dans le `.unity3d` qui les utilise**. Donc : +- L'astuce "Always Included Shaders" du player TTS est **non-applicable** : ces shaders ne sont pas chargés depuis le pool du player, ils vivent dans les bundles. +- Le bug magenta vient du fait que le bytecode/code source de ces shaders custom ne fonctionne plus correctement sous Unity 6 + Metal sur Mac. +- Les overlays magenta listés par Martin (cohésion, range, deployment, movement) correspondent **exactement** aux 4 shaders `BucketheadBits/Projector/*`. + +## Inventaire complet (par catégorie) + +### Shaders Projector (overlays affectés par le bug magenta) +| Shader | Materials | Bundles | +|---|---|---| +| `BucketheadBits/Projector/Movement` | `ProjectorMaterial_27mm_speed1_single`, `ProjectorMaterial_27mm_speed2_single`, `ProjectorMaterial_50mm_speed2_single` | 3 | +| `BucketheadBits/Projector/Deployment` | `Projector_Deployment_Blue` (×2), `Projector_Deployment_Red` (×2) | 4 | +| `BucketheadBits/Projector/Range` | `ProjectorMaterial_25mm_token`, `ProjectorMaterial_27mm`, `ProjectorMaterial_50mm` | 3 | +| `BucketheadBits/Projector/Cohesion` | `Cohesion_27mm` | 1 | + +→ 11 materials projector au total, 4 shaders distincts à porter en priorité. + +### Outils / autres custom +| Shader | Materials | Bundles | +|---|---|---| +| `BucketheadBits/MoveTool` | `Speed1`, `Speed2` | 2 | +| `BucketheadBits/Tokens/Lambert Channel Mask` | `unitIDtoken_1` à `unitIDtoken_10` | 10 | +| `BucketheadBits/Silhouette` | `BucketheadBits_Silhouette` | 1 | +| `BucketheadBits/Units/Color Replacer` | `Ewok_SharedMat` | 1 | +| `DWD/LightenSkybox` | `Skybox` | 1 | + +→ Le shader `Tokens/Lambert Channel Mask` correspond aux 10 unitIDtokens loggés en console par TTS lors du bug : "Shader didn't load correctly for AssetBundle material unitIDtoken_X". Donc ce shader est **explicitement confirmé cassé**. + +## Auteurs +- Préfixe **`BucketheadBits/`** sur 8/9 shaders → ancien contributeur "Buckethead". Aucun repo GitHub public trouvé sous ce handle. Possible que ce soit un handle alternatif de Tieren (crédité ailleurs comme "original mod creator"). +- Préfixe **`DWD/`** sur 1 shader (LightenSkybox) → autre auteur, juste un tint skybox. + +## Implications pour la conversation avec Ben + +1. **L'angle "Always Included Shaders" n'est pas la bonne piste** pour ce bug, parce que les shaders custom sont embarqués dans les bundles. Si Ben a essayé cette piste, c'est cohérent que ça n'ait rien donné. + +2. **Le scope minimum réaliste** : porter / remplacer ces 9 shaders sous Unity 6 (compilation Metal-compatible). Soit en partant des sources s'il les a, soit en réécrivant des équivalents stock. + +3. **Materials = 11 projectors + 10 tokens + 4 outils + 1 skybox = 26 materials affectés au total** dans 27 bundles différents. Volumétrie raisonnable si les shaders sont swappable d'un coup. + +4. **Type technique des shaders** : + - Les `Projector/*` sont des shaders Unity Projector (transparent, projection avec bandes alternées) — historiquement tricky à porter Metal. + - Le `Tokens/Lambert Channel Mask` est un shader de coloration par canaux (R/G/B/A) — utilisé pour appliquer la couleur du joueur sur un mesh à canaux. Classique mais codé custom. + - Les autres (MoveTool, Silhouette, Color Replacer, LightenSkybox) sont des shaders d'effets simples. + +## Sources non-trouvées +- Aucun repo public sous `swlegion`, `swlegion-dev`, `matanlurey`, `halestom15`, `Buckethead` ne contient les sources Unity de ces shaders. +- Le projet Unity vit donc en privé sur la machine d'un contributeur (probablement Decaf ou Ben). + +## Prochaine étape +Attendre Ben pour confirmer si les sources sont disponibles. Cet inventaire répond déjà partiellement à ses questions 1 et 2 : +- Q1 ("custom shaders updated or just retargeted ?") : on sait maintenant exactement ce qu'il faut updater. +- Q2 ("which shaders are referenced ?") : liste complète ci-dessus. diff --git a/mac-patcher/design-refactor.md b/mac-patcher/design-refactor.md new file mode 100644 index 00000000..a94b2d03 --- /dev/null +++ b/mac-patcher/design-refactor.md @@ -0,0 +1,330 @@ +# Design refactor — Overlays Projector SWL TTS (Mac/Unity 6) + +**Status** : draft 15 mai 2026. Spec pour le patch event-driven hybride (decals plats + vector lines drapants). Base : 4 retro-*.md + materials-reference.md + shaders Allen White lus. + +## 1. Contexte (résumé exécutif) + +Le passage du player TTS à Unity 6 a strippé les variants Standard receiver des Projectors legacy. Sur Mac, **tous les overlays Projector du mod SWL s'affichent en magenta** : Cohesion Ruler, Range Ruler, Maximum Move, Deployment Boundary. Validation empirique session 14-15 mai : aucun fix côté bundle/shader ne marche (le strip est player-side TTS U6). Fix Berserk = ticket à ouvrir mais sans garantie de calendrier. + +**Solution mod-side validée** : remplacer le pattern Custom_AssetBundle (Projector) par un combo **Decal plat (PNG hébergé) + Vector lines (drapant via raycast)** orchestré par un manager Global event-driven. + +## 1bis. Fallback per-seat avec auto-test au boot + +Le refactor ne **remplace pas** le Projector legacy, il l'**augmente** d'un fallback per-seat. Chaque joueur a un compat flag détecté au boot, et le rendu s'adapte : + +- **Seats non-Mac** (TTS U6 OK) → voient le **Projector legacy** (rendu original préservé) +- **Seats Mac** (bug magenta) → voient le **patch hybride** (decal + vector lines) + +Quand Berserk corrigera le bug, l'auto-test au boot suivant désactivera le compat pour les seats Mac → retour automatique au comportement legacy, zéro action utilisateur. + +### 1bis.1 État par-seat (Global script) + +```lua +compatBySeat = {} -- {Red=true|false, Blue=true|false, ...} true = Mac/needs patch + +function onLoad(script_state) + if script_state ~= "" then + compatBySeat = JSON.decode(script_state) or {} + end + testCompatForAllSeated() +end + +function onSave() + return JSON.encode(compatBySeat) +end + +function getMacSeats() + local out = {} + for color, isMac in pairs(compatBySeat) do + if isMac then table.insert(out, color) end + end + return out +end + +function getNonMacSeats() + local out = {} + for color, isMac in pairs(compatBySeat) do + if not isMac then table.insert(out, color) end + end + return out +end +``` + +### 1bis.2 Test compat au boot + +```lua +function testCompatForAllSeated() + for _, player in pairs(Player.getPlayers()) do + if player.seated then + offerCompatTest(player) + end + end +end + +function offerCompatTest(player) + -- Spawn un canary Projector en zone hors-jeu (sous la table) + local canary = spawnObject({ + type="Custom_AssetBundle", + position={-50, -10, -50}, + scale={0,0,0} + }) + canary.setCustomObject({assetbundle = COHESION_27MM_URL}) + + -- Wait quelques secondes le temps que le bundle charge + Wait.time(function() + -- UI popup pour ce joueur uniquement + player.broadcast("Test rendu : cliquez sur ce que vous voyez sur le canary (Y=-10)") + -- Buttons "Couleurs OK" et "Magenta/rose" + -- onClick stocke compatBySeat[player.color] = true|false + -- destruct canary + end, 3) +end +``` + +### 1bis.3 Spawn d'un overlay (pattern unifié) + +```lua +function spawnOverlay(type, fig, params) + local macSeats = getMacSeats() + local nonMacSeats = getNonMacSeats() + local entry = {type=type, fig=fig, params=params, objects={}} + + -- 1. Projector legacy pour les seats non-Mac + if #nonMacSeats > 0 then + local projector = spawnObject({type="Custom_AssetBundle", ...}) + projector.setCustomObject({assetbundle = bundleURLs[type][fig.baseSize]}) + projector.setLock(true) + projector.setName(type .. "_legacy") + if #macSeats > 0 then projector.setInvisibleTo(macSeats) end + entry.objects.projector = projector + end + + -- 2. Custom_Tile (PNG plat) pour les seats Mac + if #macSeats > 0 then + local tile = spawnObject({type="Custom_Tile", position=fig.pos+Y0.1, scale=...}) + tile.setCustomObject({image = pngURLs[type][fig.baseSize]}) + tile.setLock(true) + tile.setName(type .. "_patch") + if #nonMacSeats > 0 then tile.setInvisibleTo(nonMacSeats) end + entry.objects.tile = tile + end + + -- 3. Vector lines (drape relief) pour les seats Mac + if #macSeats > 0 then + entry.lines = buildVectorLines(type, fig, params, macSeats) + -- ajoutées au batch global + end + + activeOverlays[fig.getGUID() .. ":" .. type] = entry + redrawAll() +end +``` + +### 1bis.4 Bug Berserk fix → reset auto + +Quand Berserk corrigera les variants Standard receiver dans le player TTS U6 : +- Au prochain boot de partie, `testCompatForAllSeated` re-run pour chaque seat +- Les Mac users verront maintenant le canary correctement → cliquent "Couleurs OK" → `compatBySeat[color] = false` +- Le patch ne s'active plus pour eux, comportement legacy restauré +- Aucun changement de code requis côté mod + +**Re-test manuel** : un bouton "Re-test compatibility" dans l'UI Notes du mod permet de forcer un nouveau test à tout moment (utile si le user veut vérifier après une mise à jour TTS). + +### 1bis.5 Incertitudes à valider empiriquement au POC + +| Incertitude | Plan B si KO | +|---|---| +| `Object.setInvisibleTo` cache-t-il le **rendu de la projection** d'un Projector ou seulement le GameObject parent ? | Si KO : si au moins 1 Mac seat présent, on ne spawn PAS le Projector du tout (tout le monde voit le patch — compromis : 1 Mac dans la partie = tout le monde adopte le rendu patch) | +| `Global.setDecals()` supporte-t-il un filtre `players` per entry ? | Si KO : utiliser `Custom_Tile` Objects (déjà ce qu'on a au-dessus, avec `setInvisibleTo`). Plus propre et déjà compatible per-seat | + +À tester en début de phase d'implémentation, avant de pousser tout le code. + +## 2. Architecture event-driven (manager Global) + +### 2.1 État global unique + +```lua +-- mod/src/includes/Overlays.ttslua (nouveau) +local activeOverlays = {} -- clé = "{guid}:{type}" → {type, fig, params} +local hiddenWhilePickedUp = {} -- buffer pour les overlays cachés temporairement +``` + +### 2.2 API publique unifiée + +```lua +-- Spawn / clear par type, scopé à une fig +function spawnOverlay(type, fig, params) +function clearOverlayForFig(type, fig) +function clearAllOverlays() -- reset complet + +-- Adapters pour conserver l'API existante des callers +function spawnCohesionRuler(fig) -- → spawnOverlay("cohesion", fig, {}) +function clearCohesionRuler() -- → clearOverlayForFig("cohesion", self) +function spawnRangeRuler(fig, override) -- → spawnOverlay("range", fig, {override=override}) +function clearRangeRulers() -- → clearOverlayForFig("range", selectedUnitObj) +-- etc. +``` + +### 2.3 Event handlers (Global) + +```lua +function onObjectPickUp(player_color, obj) + -- buffer tous les overlays attachés à obj puis les retirer du draw + hidePickedUp(obj) + redrawAll() +end + +function onObjectDrop(player_color, obj) + -- restaurer les overlays de obj depuis le buffer + restorePickedUp(obj) + redrawAll() +end + +function onObjectDestroy(obj) + -- nettoyage définitif + purgeForFig(obj) + redrawAll() +end +``` + +### 2.4 Rendu unique par tick d'event + +```lua +function redrawAll() + local lines, decals = {}, {} + for _, entry in pairs(activeOverlays) do + local l, d = builders[entry.type](entry.fig, entry.params) + for _, line in ipairs(l) do table.insert(lines, line) end + for _, decal in ipairs(d) do table.insert(decals, decal) end + end + Global.setVectorLines(lines) + Global.setDecals(decals) +end +``` + +**Important** : `setVectorLines` et `setDecals` sont des **singletons** côté TTS — chaque appel remplace toute la collection. D'où le manager global qui agrège. + +## 3. Spec par overlay + +### 3.1 Cohesion + +| Composant | Détail | +|---|---| +| Trigger spawn | Hotkey "Show Cohesion On Hovered Model" / bouton COHESION Order Token / dropCoroutine post-mouvement (`moveState=true`) | +| Trigger clear | Mêmes triggers (toggle) + onPickedUp (auto) + standby/reload globaux | +| Decal PNG | `cohesion_halo.png` — gradient radial blanc fade depuis le centre, fond transparent, 512×512px | +| Decal params | `position = fig.pos + Y0.1`, `rotation = (90, 0, 0)` (face vers le haut), `scale = (baseRadius+1, baseRadius+1, baseRadius+1) × 2` | +| Vector line | 1 cercle filaire blanc à `r = base_radius + 0.5"` du centre, drapé par raycast 32 segments | +| Couleur | #FFFFFF α=0.6 | + +API conservée : `showCohesionOnHoveredModel`, `spawnCohesionRuler`, `clearCohesionRuler`. Compatible avec les 3 sites de require existants (Global, Unit_Leader, Order_Token) car redirigés vers le manager global. + +### 3.2 Range Ruler + +| Composant | Détail | +|---|---| +| Trigger spawn | Hotkey "Show Range On Hovered Model" / bouton RANGE Order Token (targetingMode/attackMode) / bouton R sur POI/tokens | +| Trigger clear | Mêmes triggers (toggle) + clearTemplates (post-drop mouvement) + exit modes | +| Decal PNG | `range_bands.png` (universel) — 4 anneaux concentriques à 6/12/18/24 inches relatifs, couleurs jaune/orange/rouge/magenta foncé, fond transparent, 1024×1024px | +| Decal params | `position = fig.pos + Y0.1`, `scale = (60, 60, 60)` (couvre 24" de rayon + marge) | +| Vector lines | 4 cercles filaires aux rayons absolus **6", 12", 18", 24"** du centre, mêmes couleurs que les bandes du PNG | +| Couleur (par anneau) | R1 #FFC300, R2 #FF7500, R3 #FF1400, R4 #C20042 (alpha 0.6 sur les lignes) | +| Variantes | Tokens (smokeToken/token/tokenRangeTwo/bombCart/POI) → moins d'anneaux (1 ou 2), PNG distincts ou même PNG avec scale ajusté | + +API conservée : `showRangeOnHoveredModel`, `spawnRangeRuler(fig, override)`, `clearRangeRuler/clearRangeRulers`. + +Pour les variantes, le `override` permet de passer un type explicite (`"smokeToken"`, `"poi"`, etc.) au lieu du baseSize de la fig. + +### 3.3 Maximum Move + +| Composant | Détail | +|---|---| +| Trigger spawn | Lors du clic sur bouton speed (1/2/3) sur Order Token (flow mouvement) | +| Trigger clear | clearMovementTemplates (drop final) | +| Decal PNG | `max_move_disk.png` — disque plein bleu ciel #55CCFF α=0.48, fond transparent, 512×512px | +| Decal params | `position = fig.pos + Y0.1`, `scale = ProjectorRadius × 2` selon (baseSize, speed) | +| Vector line | 1 cercle filaire bleu ciel au rayon = ProjectorRadius (drape relief) + 1 anneau base blanc fin à r = baseRadius + 0.5" | +| Couleur | Cercle principal #55CCFF α=0.6, anneau base #FFFFFF α=0.5 | +| Rayons | Lookup table `maxMoveRadius[baseSize][speed]` (ex: `27mm.speed1=4.55", speed2=6.52", speed3=8.48"`) — extraits de `_ProjectorRadius` dans les materials Movement | + +API conservée : flow existant dans `Order_Token.a57c41.lua:552-574`, remplacement du `spawnObject Custom_AssetBundle` par `spawnOverlay("maxmove", fig, {speed=selectedSpeed, baseSize=unitData.baseSize})`. + +### 3.4 Deployment Boundary + +| Composant | Détail | +|---|---| +| Trigger spawn | `spawnDeploymentBoundary(matrix)` au setup (depuis menu UI Deployment) | +| Trigger clear | `clearDeploymentBoundary()` (menu UI Remove Overlay) + standby global | +| Decal PNG | `deployment_red.png` + `deployment_blue.png` — rectangle plein uniforme α=0.5, 256×256px | +| Decal params | Par cellule de la matrix, `position = grid pos + offset`, `scale = (6, 6, 6)` (taille standard d'une cellule SWL), rotation selon `deployRotations[cell]` | +| Vector lines | Contour rectangle drapant (4 côtés × N segments via raycast) par cellule, couleur match decal | +| Couleur | Rouge #FF0000 / Bleu #0000FF α=0.5 | +| Variantes | Half/L/Corner/Round → combinaisons de cellules dans la matrix, le manager gère chaque cellule individuellement (1 decal + 1 contour par cellule) | + +API conservée : `spawnDeploymentBoundary(matrix)`, `clearDeploymentBoundary()`. Internement, chaque cellule est enregistrée comme un overlay distinct avec un GUID synthétique (ex `"deployment:bs:5:3"`). + +## 4. Assets PNG à générer + +| Fichier | Dimensions | Description | Hosting | +|---|---|---|---| +| `cohesion_halo.png` | 512×512 | Gradient radial blanc centre → transparent bord, RGBA | iron-squadron.fr/tts-assets/ | +| `range_bands.png` | 1024×1024 | 4 anneaux concentriques aux rayons relatifs 0.25/0.5/0.75/1.0, gradient discret (pas blend), couleurs jaune/orange/rouge/magenta, alpha 0.6 | iron-squadron.fr/tts-assets/ | +| `range_smoke.png` | 256×256 | 1 anneau à rayon relatif 1.0, blanc opaque | idem | +| `range_token.png` | 256×256 | 1 anneau à rayon relatif 1.0, jaune | idem | +| `range_tokenRangeTwo.png` | 256×256 | 2 anneaux, jaune + orange | idem | +| `range_poi.png` | 256×256 | 1 anneau pour POI (range 0.5 / 3") | idem | +| `range_bombCart.png` | 256×256 | 2 anneaux | idem | +| `max_move_disk.png` | 512×512 | Disque plein bleu ciel #55CCFF α=0.48, RGBA | idem | +| `deployment_red.png` | 256×256 | Carré rouge plein #FF0000 α=0.5 | idem | +| `deployment_blue.png` | 256×256 | Carré bleu plein #0000FF α=0.5 | idem | + +**Génération** : script Python Pillow `generate_overlay_assets.py` qui produit tous les PNG d'un coup. Reproductible, versionnable dans `swlegion-tts/tool/`. + +**Upload** : `rsync` vers le VPS Iron Squadron, dossier `/var/www/iron-squadron/tts-assets/` (ou similaire). URL stable indépendante de Steam. + +## 5. Edge cases et invariants + +| Cas | Gestion | +|---|---| +| Multi-overlays simultanés (plusieurs figs avec cohesion + range) | Cohabitent dans la table `activeOverlays`, un `redrawAll()` au moindre changement | +| Fig détruite (mort en combat) | `onObjectDestroy` → `purgeForFig` → redraw | +| Pickup pendant overlay actif | `onObjectPickUp` → buffer dans `hiddenWhilePickedUp`, retire du rendu | +| Drop après pickup | `onObjectDrop` → restore depuis buffer, redraw (positions recalculées par les builders) | +| Pickup d'un objet sans overlay | Handler skip silencieux (test sur présence dans la table) | +| Reload de partie | `activeOverlays` se reset au boot (variable Lua locale du Global), OK | +| Filtres `standbyTokens` / `removeLockedRulers` (scans `getAllObjects()` par nom) | Deviennent caducs (plus d'Object physique). À supprimer ou laisser comme no-op | +| Test multi-joueurs réseau | Le manager Global est synchronisé par TTS (Lua sync), donc tous les joueurs voient les mêmes overlays | +| Drape impossible sur certains terrains custom | Limitation acceptable (déjà flag par Ben). Le decal plat sert de fallback visuel | + +## 6. Chemin de migration (séquentiel, testable étape par étape) + +1. **Setup Manager Global** : créer `mod/src/includes/Overlays.ttslua`. Aucun caller encore branché. Test : `npm run compile && load mod`, no-op confirmé +2. **Cohesion** (60 lignes, le plus simple) : réécrire `mod/src/includes/Cohesion.ttslua` pour rediriger vers le manager. Test : spawn via hotkey + bouton + dropCoroutine sur Mac +3. **Range Ruler** : réécrire `mod/src/includes/RangeRulers.ttslua` + adapter `POI_Token.lua` + `TokenWithRangeRuler.ttslua`. Test : 4 anneaux + tokens +4. **Maximum Move** : adapter `Order_Token.a57c41.lua:552-574` (spawn inline → call manager) + `clearMovementTemplates`. Test : flow mouvement complet sur Mac +5. **Deployment** : réécrire `SETUP_CONTROLLER.1cb552.lua:243-323`. Test : sélection scenario + spawn de la zone +6. **Cleanup** : supprimer les filtres `standbyTokens` / `removeLockedRulers` pour les types qui ne sont plus des Objects. Garder le filtre pour Movement Template (mesh, toujours Object) +7. **PR sur swlegion/tts** : branche `mac-overlays-refactor`, description du bug + solution + diff. Test communauté + +## 7. Compatibilité conservée + +- **Tous les sites de require existants** continuent à fonctionner sans modif (`require('!/Cohesion')`, `require('!/RangeRulers')`) +- **Toutes les fonctions globales conservent leur signature** (`spawnCohesionRuler(fig)`, `clearRangeRulers()`, etc.) +- **Pas de change d'API publique** → callers (Unit_Leader, Order_Token, POI_Token, etc.) inchangés +- **Variables d'état globales** comme `selectedUnitObj` conservées pour compat avec les filtres et la logique de toggle + +## 8. Risques connus à surveiller + +| Risque | Mitigation | +|---|---| +| Performance multi-overlays (N décals + N×64 vectors) | Tester empiriquement sur table chargée. Si lag, sparse raycast (32 segments) ou skip raycast si table plate détectée | +| Synchronisation decal/vector lines pas pixel-perfect | Vector lines au-dessus du decal (Y légèrement supérieur), tolérance visuelle | +| URL Iron Squadron offline | Hosting sur 2 destinations (VPS + GitHub Pages backup) ou inclure le CDN dans le repo si TTS supporte les data URIs | +| Bug U6 sur les decals aussi (improbable, POC validé le 14 mai) | Garder Path B = ticket Berserk en parallèle | +| Refactor casse un caller obscur | Test exhaustif des 5 sites de spawn + tous les triggers connus | + +## 9. Annexes + +- `materials-reference.md` : tous les params visuels extraits +- `retro-cohesion.md`, `retro-range.md`, `retro-deployment.md`, `retro-movement.md` : déclencheurs et call graphs +- `swlegion-tts/mod/src/includes/` : code source des modules à patcher diff --git a/mac-patcher/extract_projector_specs.py b/mac-patcher/extract_projector_specs.py new file mode 100644 index 00000000..e699f8f1 --- /dev/null +++ b/mac-patcher/extract_projector_specs.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +""" +Scan Unity .mat and .prefab YAML files in UnityProject-U6/Assets/Projectors/ +and extract critical parameters for the SWL TTS mod refactor. + +Output: materials-reference.md with structured tables per overlay. +""" +import os +import re +from pathlib import Path +from collections import defaultdict + +ROOT = Path("/Users/martinpourrat/MARTIN/Star Wars Legion/Mod TTS SWL/UnityProject-U6/Assets/Projectors") +OUT = Path("/Users/martinpourrat/MARTIN/Star Wars Legion/Mod TTS SWL/materials-reference.md") + +OVERLAYS = ["Cohesion", "Range", "Deployment", "Movement"] + + +def read(p: Path) -> str: + return p.read_text(encoding="utf-8", errors="replace") + + +def parse_material(path: Path) -> dict: + """Extract critical fields from a Unity .mat YAML file.""" + text = read(path) + out = {"path": str(path.relative_to(ROOT)), "name": path.stem} + + m = re.search(r"m_ShaderKeywords:\s*(.*)", text) + out["keywords"] = m.group(1).strip() if m else "" + + m = re.search(r"m_Shader:\s*\{fileID:\s*\d+,\s*guid:\s*([0-9a-f]+)", text) + out["shader_guid"] = m.group(1) if m else "" + + # Floats — capture key params + floats = {} + for fname in ["_BaseSize", "_ProjectorRadius", "_BandSize", "_BandContrast", + "_MaxRange", "_Arc", "_RangeSize", "_GradScaler", + "_OneTuner", "_TwoTuner", "_ThreeTuner", "_FourTuner", + "_FiveTuner", "_InfinityTuner"]: + m = re.search(rf"-\s*{re.escape(fname)}:\s*(-?[\d.]+)", text) + if m: + floats[fname] = m.group(1) + out["floats"] = floats + + # Colors — capture all _Color* / _Range* + colors = {} + for m in re.finditer(r"-\s*(_\w+):\s*\{r:\s*([\d.]+),\s*g:\s*([\d.]+),\s*b:\s*([\d.]+),\s*a:\s*([\d.]+)\}", text): + name, r, g, b, a = m.groups() + if name in ("_BumpScale",): # not a color + continue + colors[name] = (float(r), float(g), float(b), float(a)) + out["colors"] = colors + + return out + + +def parse_prefab(path: Path) -> dict: + """Extract critical fields from a Unity .prefab YAML file (Projector components).""" + text = read(path) + out = {"path": str(path.relative_to(ROOT)), "name": path.stem} + + # Count Projector components (Unity type id 119) + projectors = re.findall(r"---\s*!u!119\s*&\d+\s*\nProjector:(.*?)(?=\n---|\Z)", text, re.DOTALL) + out["n_projectors"] = len(projectors) + + proj_data = [] + for body in projectors: + d = {} + for fname in ["m_OrthographicSize", "m_FarClipPlane", "m_NearClipPlane", + "m_FieldOfView", "m_AspectRatio", "m_Orthographic"]: + m = re.search(rf"{fname}:\s*(-?[\d.]+)", body) + if m: + d[fname] = m.group(1) + m = re.search(r"m_Material:\s*\{fileID:\s*\d+,\s*guid:\s*([0-9a-f]+)", body) + if m: + d["material_guid"] = m.group(1) + proj_data.append(d) + out["projectors"] = proj_data + + # Transforms (LocalPosition Y, rotation) + transforms = [] + for body in re.findall(r"---\s*!u!4\s*&\d+\s*\nTransform:(.*?)(?=\n---|\Z)", text, re.DOTALL): + d = {} + m = re.search(r"m_LocalPosition:\s*\{x:\s*(-?[\d.]+),\s*y:\s*(-?[\d.]+),\s*z:\s*(-?[\d.]+)\}", body) + if m: + d["pos"] = (float(m.group(1)), float(m.group(2)), float(m.group(3))) + m = re.search(r"m_LocalEulerAnglesHint:\s*\{x:\s*(-?[\d.]+),\s*y:\s*(-?[\d.]+),\s*z:\s*(-?[\d.]+)\}", body) + if m: + d["rot"] = (float(m.group(1)), float(m.group(2)), float(m.group(3))) + transforms.append(d) + out["transforms"] = transforms + + return out + + +def rgba_to_hex(r, g, b, a): + """Convert 0-1 RGBA to hex + alpha note.""" + rh, gh, bh = int(r * 255), int(g * 255), int(b * 255) + return f"#{rh:02X}{gh:02X}{bh:02X} (α={a:.2f})" + + +def fmt_colors(colors): + """Format dict of colors as compact markdown.""" + if not colors: + return "—" + parts = [] + for name, (r, g, b, a) in colors.items(): + if a == 0 and r == 0 and g == 0 and b == 0: + parts.append(f"`{name}` transparent") + else: + parts.append(f"`{name}` {rgba_to_hex(r, g, b, a)}") + return "
".join(parts) + + +def fmt_floats(floats): + """Format dict of floats.""" + if not floats: + return "—" + return ", ".join(f"`{k}`={v}" for k, v in floats.items()) + + +def main(): + materials_by_overlay = defaultdict(list) + prefabs_by_overlay = defaultdict(list) + + for overlay in OVERLAYS: + for p in sorted((ROOT / overlay).rglob("*.mat")): + if p.suffix == ".meta": + continue + materials_by_overlay[overlay].append(parse_material(p)) + for p in sorted((ROOT / overlay).rglob("*.prefab")): + if p.suffix == ".meta": + continue + prefabs_by_overlay[overlay].append(parse_prefab(p)) + + # Build the master doc + lines = [] + lines.append("# Materials & Prefabs Reference — SWL TTS Projector Overlays") + lines.append("") + lines.append("Extraction directe des `.mat` et `.prefab` Unity (YAML) dans `UnityProject-U6/Assets/Projectors/`. Source de vérité pour reproduire le rendu en vector lines.") + lines.append("") + lines.append("Notation : couleurs en hex + alpha. Champs `Floats` clés du shader projector (paramètres procéduraux des anneaux).") + lines.append("") + + for overlay in OVERLAYS: + lines.append(f"## {overlay}") + lines.append("") + + # Materials table + lines.append("### Materials") + lines.append("") + lines.append("| Material | Shader keywords | Floats clés | Couleurs |") + lines.append("|---|---|---|---|") + for m in materials_by_overlay[overlay]: + kw = m["keywords"] or "—" + lines.append(f"| `{m['name']}` | `{kw}` | {fmt_floats(m['floats'])} | {fmt_colors(m['colors'])} |") + lines.append("") + + # Prefabs table + lines.append("### Prefabs") + lines.append("") + lines.append("| Prefab | Nb Projectors | OrthographicSize / FarClip | Transform pos/rot | Material guid |") + lines.append("|---|---|---|---|---|") + for p in prefabs_by_overlay[overlay]: + n = p["n_projectors"] + proj_sizes = [pr.get("m_OrthographicSize", "?") for pr in p["projectors"]] + proj_far = [pr.get("m_FarClipPlane", "?") for pr in p["projectors"]] + proj_summary = "
".join(f"size={s}, far={f}" for s, f in zip(proj_sizes, proj_far)) if proj_sizes else "—" + + tr_summary = "
".join( + f"pos=({t.get('pos', ('?',)*3)[0]}, {t.get('pos', ('?',)*3)[1]}, {t.get('pos', ('?',)*3)[2]}) rot=({t.get('rot', ('?',)*3)[0]}, {t.get('rot', ('?',)*3)[1]}, {t.get('rot', ('?',)*3)[2]})" + for t in p["transforms"] + ) if p["transforms"] else "—" + + mat_guids = [pr.get("material_guid", "?") for pr in p["projectors"]] + mat_summary = "
".join(g[:12] + "…" for g in mat_guids) if mat_guids else "—" + + lines.append(f"| `{p['name']}` | {n} | {proj_summary} | {tr_summary} | {mat_summary} |") + lines.append("") + + # Final notes + lines.append("## Notes globales") + lines.append("") + lines.append("- Cohesion : double set `Materials/cohesion_*` et `_Revamp/halfCohesion_*`. Probable : `_Revamp/` est la version actuellement utilisée (le bundle live s'appelle `halfcohesion_27mm.unity3d` cf shader-inventory).") + lines.append("- Range : shader procédural avec jusqu'à 5 bandes (`_RangeOne` à `_RangeFive`) + `_RangeInfinity`. Le keyword `_MAXRANGE_RANGEFIVE` ou `_MAXRANGE_RANGEFOUR` détermine la dernière bande visible.") + lines.append("- Movement : un seul Projector par prefab, paramétré par `_BaseSize` + speed. Le `_Arc` doit contrôler la portion d'arc visible.") + lines.append("- Deployment : 6 materials uniques réutilisés par 12 prefabs (rotations différentes pour matérialiser corner/half/L/round/score).") + + OUT.write_text("\n".join(lines), encoding="utf-8") + print(f"Wrote {OUT}") + # Stats + total_mats = sum(len(v) for v in materials_by_overlay.values()) + total_prefabs = sum(len(v) for v in prefabs_by_overlay.values()) + print(f"Scanned {total_mats} materials and {total_prefabs} prefabs across {len(OVERLAYS)} overlays.") + + +if __name__ == "__main__": + main() diff --git a/mac-patcher/generate_overlay_assets.py b/mac-patcher/generate_overlay_assets.py new file mode 100644 index 00000000..d11b1932 --- /dev/null +++ b/mac-patcher/generate_overlay_assets.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python3 +""" +Generate PNG assets for the Mac Cohesion + Range overlay fallback. +""" +from pathlib import Path +from PIL import Image, ImageDraw +import math + +OUT_DIR = Path(__file__).parent / "tts-assets" +OUT_DIR.mkdir(exist_ok=True) + + +# Range band colors (R, G, B in 0..255) — from BB_RangeProjector defaults +RANGE_COLORS = { + "half": (255, 255, 255), # 0.5 / 3" — white + "one": (255, 195, 0), # R1 / 6" — yellow + "two": (255, 117, 0), # R2 / 12" — orange + "three": (255, 20, 0), # R3 / 18" — red + "four": (195, 0, 66), # R4 / 24" — magenta + "five": (134, 0, 112), # R5 / 30" — violet +} + +# Configurations per overlay type (bands = list of (radius_inches, color_key)) +# Edge-based convention: each band's effective world radius is band_r + base_r. +RANGE_BAND_CONFIGS = { + "fig_leader": { + "bands": [(3, "half"), (6, "one"), (12, "two"), + (18, "three"), (24, "four"), (30, "five")], + "max_radius": 30, + }, + # SWL ranges in inches: Range 1 = 6", Range 2 = 12", Range 0.5 (poi) = 3". + "smokeToken": {"bands": [(6, "one")], "max_radius": 6}, + "token": {"bands": [(6, "one")], "max_radius": 6}, + "tokenRangeTwo": {"bands": [(6, "one"), (12, "two")], "max_radius": 12}, + "poi": {"bands": [(3, "one")], "max_radius": 3}, + "bombCart": {"bands": [(6, "one"), (12, "two")], "max_radius": 12}, +} + +# Base radii (inches, half-diameter) per ROUND fig base size. Oblong bases +# (long, snail) live in OBLONG_DIMENSIONS_MM below — they don't use a single +# radius and don't generate round PNGs. +FIG_BASE_RADIUS_IN = { + "small": 27 / 2 / 25.4, + "medium": 50 / 2 / 25.4, + "large": 70 / 2 / 25.4, + "huge": 100 / 2 / 25.4, + "laat": 120 / 2 / 25.4, + "epic": 150 / 2 / 25.4, +} + +# Token base radii (inches), only needed for multi-band tokens. +TOKEN_BASE_RADIUS_IN = { + "tokenRangeTwo": 25.1 / 2 / 25.4, + "bombCart": 50.0 / 2 / 25.4, +} + + +def _with_base_offset(config: dict, base_r: float) -> dict: + """Shift band radii outward by base_r and grow max_radius accordingly so + the PNG band proportions match the world positions of the vector-line + rings (which are drawn at band_r + base_r).""" + return { + "bands": [(r + base_r, c) for r, c in config["bands"]], + "max_radius": config["max_radius"] + base_r, + } + + +def generate_cohesion_halo(size: int = 512) -> Path: + """Halo radial blanc fade transparent — reproduit le rendu Cohesion bundle. + + Le shader BB_CohesionProjector fait `rangeOne = (1 - grad)` puis multiplie + par alpha global. On reproduit ce fade : opaque au centre, transparent au + bord. + """ + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + pixels = img.load() + cx, cy = size / 2, size / 2 + max_r = size / 2 + + # Alpha global du material Cohesion 27mm = 0.37 ; on multiplie par le fade + base_alpha = 0.37 + + for y in range(size): + for x in range(size): + dx, dy = x - cx, y - cy + dist = math.sqrt(dx * dx + dy * dy) + r_norm = dist / max_r # [0, 1+] depuis le centre + if r_norm > 1.0: + continue # transparent au-delà du rayon + + # Fade radial : (1 - r) avec courbe douce + # Le shader fait (1 - grad) linéaire, on garde la même chose + fade = max(0.0, 1.0 - r_norm) + alpha = int(base_alpha * fade * 255) + pixels[x, y] = (255, 255, 255, alpha) + + out = OUT_DIR / "cohesion_halo.png" + img.save(out) + return out + + +def generate_range_band_png(name: str, config: dict, size: int = 1024, + alpha: float = 0.2) -> Path: + """Generate a circular range overlay with concentric bands. + + Drawing from outer-to-inner: each filled ellipse overlays the previous, + producing clean rings. + """ + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + cx = cy = size / 2 + max_r_pix = size / 2 + a = int(alpha * 255) + + for band_r, color_key in reversed(config["bands"]): + r_pix = (band_r / config["max_radius"]) * max_r_pix + r, g, b = RANGE_COLORS[color_key] + draw.ellipse( + [(cx - r_pix, cy - r_pix), (cx + r_pix, cy + r_pix)], + fill=(r, g, b, a), + ) + + out = OUT_DIR / f"range_{name}.png" + img.save(out) + return out + + +# Oblong base dimensions (mm). Used to render stadium-shape range overlays +# whose 6 bands stay concentric to a rectangular footprint with rounded caps. +OBLONG_DIMENSIONS_MM = { + "long": {"width": 100, "length": 175}, + "snail": {"width": 100, "length": 200}, +} + + +def generate_range_band_stadium_png(base_size: str, dims_mm: dict, + config: dict, alpha: float = 0.2, + pixels_per_inch: int = 16) -> Path: + """Generate a stadium-shape PNG with 6 concentric range bands. + + The PNG dimensions encode the *outermost* band's bounding box (so the + decal can be stretched in TTS using scale = {shortSpan, longSpan, 1} and + the bands stay aligned with the matching vector stadium outlines). + + A stadium = rounded rectangle whose corner radius equals half the + short dimension, producing a pill shape with semicircular caps on the + long-axis ends. Each band is one rounded_rectangle of width + 2*(halfWid + band_r) and length 2*(halfLen + band_r), corner radius + halfWid + band_r. Drawn outer-to-inner so each band overpaints the next. + """ + halfWid_in = (dims_mm["width"] / 2) / 25.4 + halfLen_in = (dims_mm["length"] / 2) / 25.4 + max_dist = config["max_radius"] + + shortSpan_in = 2 * (halfWid_in + max_dist) + longSpan_in = 2 * (halfLen_in + max_dist) + + W = int(round(shortSpan_in * pixels_per_inch)) + H = int(round(longSpan_in * pixels_per_inch)) + cx, cy = W / 2, H / 2 + + img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + a = int(alpha * 255) + + for band_r, color_key in reversed(config["bands"]): + half_w_px = (halfWid_in + band_r) * pixels_per_inch + half_h_px = (halfLen_in + band_r) * pixels_per_inch + r, g, b = RANGE_COLORS[color_key] + draw.rounded_rectangle( + [(cx - half_w_px, cy - half_h_px), + (cx + half_w_px, cy + half_h_px)], + radius=half_w_px, # = half of the short side → semicircle caps + fill=(r, g, b, a), + ) + + out = OUT_DIR / f"range_fig_leader_{base_size}_stadium.png" + img.save(out) + return out + + +def generate_filled_disk(name: str, color_rgb: tuple, size: int = 512, + alpha: float = 0.2) -> Path: + """Generate a simple filled disk PNG (used for Maximum Move overlay).""" + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + cx = cy = size / 2 + r = size / 2 + a = int(alpha * 255) + draw.ellipse([(cx - r, cy - r), (cx + r, cy + r)], + fill=(color_rgb[0], color_rgb[1], color_rgb[2], a)) + out = OUT_DIR / f"{name}.png" + img.save(out) + return out + + +def generate_filled_square(name: str, color_rgb: tuple, size: int = 256, + alpha: float = 0.3) -> Path: + """Generate a filled square PNG (used for Deployment Boundary cells).""" + img = Image.new("RGBA", (size, size), + (color_rgb[0], color_rgb[1], color_rgb[2], int(alpha * 255))) + out = OUT_DIR / f"{name}.png" + img.save(out) + return out + + +def generate_silhouette_top_disc(size: int = 512) -> Path: + """Hologram-style top cap for the Mac silhouette cylinder. + + Radial gradient: bright green-cyan center fades to transparent at the + rim. Combined with the vector wireframe cylinder this gives the + Star Wars hologram briefing look (Leia "Help me Obi-Wan" projection). + """ + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + pixels = img.load() + cx, cy = size / 2, size / 2 + max_r = size / 2 + + r_center, g_center, b_center = 102, 255, 178 # bright hologram cyan-green + a_center = 0.85 + + for y in range(size): + for x in range(size): + dx, dy = x - cx, y - cy + dist = math.sqrt(dx * dx + dy * dy) + r_norm = dist / max_r + if r_norm > 1.0: + continue + # Smooth radial falloff (1-r squared) for a soft hologram glow + fade = (1.0 - r_norm) ** 1.5 + alpha = int(a_center * fade * 255) + pixels[x, y] = (r_center, g_center, b_center, alpha) + + out = OUT_DIR / "silhouette_top.png" + img.save(out) + return out + + +def main(): + p = generate_cohesion_halo() + print(f"Generated {p} ({p.stat().st_size} bytes)") + + # Fig leaders (round bases): one PNG per base size, bands shifted by base radius. + fig_cfg = RANGE_BAND_CONFIGS["fig_leader"] + for base_size, br in FIG_BASE_RADIUS_IN.items(): + out = generate_range_band_png(f"fig_leader_{base_size}", + _with_base_offset(fig_cfg, br)) + print(f"Generated {out} ({out.stat().st_size} bytes)") + + # Oblong bases: stadium-shape PNGs (rectangular footprint + rounded caps). + for base_size, dims in OBLONG_DIMENSIONS_MM.items(): + out = generate_range_band_stadium_png(base_size, dims, fig_cfg) + print(f"Generated {out} ({out.stat().st_size} bytes)") + + # Multi-band tokens: bake in their base radius for alignment. + for name, br in TOKEN_BASE_RADIUS_IN.items(): + cfg = RANGE_BAND_CONFIGS[name] + out = generate_range_band_png(name, _with_base_offset(cfg, br)) + print(f"Generated {out} ({out.stat().st_size} bytes)") + + # Single-band tokens: no offset adjustment needed (band sits at outer edge). + for name in ("smokeToken", "token", "poi"): + out = generate_range_band_png(name, RANGE_BAND_CONFIGS[name]) + print(f"Generated {out} ({out.stat().st_size} bytes)") + + # Maximum Move: cyan filled disk (#55CCFF) + p = generate_filled_disk("max_move_cyan", (85, 204, 255), alpha=0.2) + print(f"Generated {p} ({p.stat().st_size} bytes)") + + # Deployment Boundary: red and blue filled squares (alpha 0.5 = match bundle) + p = generate_filled_square("deployment_red", (255, 0, 0), alpha=0.3) + print(f"Generated {p} ({p.stat().st_size} bytes)") + p = generate_filled_square("deployment_blue", (0, 0, 255), alpha=0.3) + print(f"Generated {p} ({p.stat().st_size} bytes)") + + # Silhouette top cap: hologram-style green/cyan radial gradient. + p = generate_silhouette_top_disc() + print(f"Generated {p} ({p.stat().st_size} bytes)") + + +if __name__ == "__main__": + main() diff --git a/mac-patcher/inspect_cohesion.py b/mac-patcher/inspect_cohesion.py new file mode 100644 index 00000000..e26becbe --- /dev/null +++ b/mac-patcher/inspect_cohesion.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Inspect Cohesion bundle structure: list objects, materials, shaders, and the m_Shader PPtr.""" +import os +import sys +import UnityPy + +BUNDLE = os.path.expanduser( + "~/Library/Tabletop Simulator/Mods/Assetbundles/" + "httpssteamusercontentaakamaihdnetugc2482129948496305632EBBE2560D4336E6C96317EDDA787E225CF0E5B48.unity3d" +) + +env = UnityPy.load(BUNDLE) + +print("=" * 80) +print("BUNDLE METADATA") +print("=" * 80) +for f in env.files.values(): + print(f" file: {type(f).__name__}") + if hasattr(f, "unity_version"): + print(f" unity_version: {f.unity_version}") + if hasattr(f, "target_platform"): + print(f" target_platform: {f.target_platform}") + if hasattr(f, "externals"): + print(f" externals (cross-bundle refs):") + for ext in getattr(f, "externals", []): + print(f" - {ext}") + +print() +print("=" * 80) +print("ALL OBJECTS") +print("=" * 80) +for obj in env.objects: + path_id = getattr(obj, "path_id", "?") + type_name = obj.type.name + try: + data = obj.read() + name = getattr(data, "m_Name", "") or "" + except Exception as e: + name = f"" + print(f" PathID={path_id:<6} type={type_name:<15} name={name!r}") + +print() +print("=" * 80) +print("MATERIAL DETAILS (looking for Cohesion_27mm)") +print("=" * 80) +for obj in env.objects: + if obj.type.name != "Material": + continue + data = obj.read() + print(f"\nMaterial PathID={obj.path_id} name={data.m_Name!r}") + sref = data.m_Shader + print(f" m_Shader PPtr:") + print(f" file_id = {getattr(sref, 'file_id', '?')} " + f"(0 = internal to this bundle, !=0 = external file)") + print(f" path_id = {getattr(sref, 'path_id', '?')}") + # Try to resolve + try: + sobj = sref.deref() + if sobj: + sd = sobj.read() + sname = ( + getattr(getattr(sd, "m_ParsedForm", None), "m_Name", None) + or getattr(sd, "m_Name", None) + ) + print(f" resolved -> Shader name = {sname!r}") + else: + print(f" resolved -> None (external or missing)") + except Exception as e: + print(f" resolve error: {e!r}") + +print() +print("=" * 80) +print("SHADER DETAILS") +print("=" * 80) +for obj in env.objects: + if obj.type.name != "Shader": + continue + data = obj.read() + sname = ( + getattr(getattr(data, "m_ParsedForm", None), "m_Name", None) + or getattr(data, "m_Name", None) + ) + print(f" PathID={obj.path_id} Shader name={sname!r}") diff --git a/mac-patcher/inspect_externals.py b/mac-patcher/inspect_externals.py new file mode 100644 index 00000000..c9307a21 --- /dev/null +++ b/mac-patcher/inspect_externals.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""For each bundle: dump its externals (referenced external files) and list materials with external m_Shader PPtr.""" +import os +import sys +import UnityPy + +BUNDLE_DIR = os.path.expanduser("~/Library/Tabletop Simulator/Mods/Assetbundles/") + +bundles = sorted(f for f in os.listdir(BUNDLE_DIR) if f.endswith(".unity3d")) + +# We are most interested in bundles whose materials use external shaders (file_id != 0) +print("=" * 90) +print("BUNDLES WITH EXTERNAL m_Shader REFERENCES") +print("=" * 90) + +for fname in bundles: + path = os.path.join(BUNDLE_DIR, fname) + short_id = fname.replace("httpssteamusercontentaakamaihdnetugc", "")[:20] + env = UnityPy.load(path) + + external_mats = [] + for obj in env.objects: + if obj.type.name != "Material": + continue + try: + data = obj.read() + sref = data.m_Shader + fid = getattr(sref, "file_id", None) + pid = getattr(sref, "path_id", None) + if fid and fid != 0: + external_mats.append((data.m_Name, fid, pid)) + except Exception: + pass + + if not external_mats: + continue + + print(f"\n{short_id}:") + # Dump externals table + for f in env.files.values(): + if hasattr(f, "externals"): + print(f" externals table:") + for i, ext in enumerate(getattr(f, "externals", [])): + # The externals entry usually has a 'path' and 'guid' + ext_path = getattr(ext, "path", "?") + ext_guid = getattr(ext, "guid", "?") + # index i+1 because file_id is 1-based for externals (file_id=0 means self) + print(f" file_id={i+1} path={ext_path!r} guid={ext_guid}") + print(f" materials with external m_Shader:") + for name, fid, pid in external_mats: + print(f" material={name!r} m_Shader=(file_id={fid}, path_id={pid})") diff --git a/mac-patcher/materials-reference.md b/mac-patcher/materials-reference.md new file mode 100644 index 00000000..eabaae36 --- /dev/null +++ b/mac-patcher/materials-reference.md @@ -0,0 +1,170 @@ +# Materials & Prefabs Reference — SWL TTS Projector Overlays + +Extraction directe des `.mat` et `.prefab` Unity (YAML) dans `UnityProject-U6/Assets/Projectors/`. Source de vérité pour reproduire le rendu en vector lines. + +Notation : couleurs en hex + alpha. Champs `Floats` clés du shader projector (paramètres procéduraux des anneaux). + +## Cohesion + +### Materials + +| Material | Shader keywords | Floats clés | Couleurs | +|---|---|---|---| +| `Cohesion_27mm` | `_ARC_OFF _MAXRANGE_RANGEONE _MOVES_SINGLE` | `_BaseSize`=27, `_ProjectorRadius`=4.479488, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.37)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #FFFFFF (α=0.30)
`_MoveTwo` #FF0000 (α=1.00)
`_RangeFive` #FFFF00 (α=1.00)
`_RangeFour` #00FF00 (α=1.00)
`_RangeInfinity` #000000 (α=1.00)
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF6C00 (α=1.00)
`_RangeTwo` #0000FF (α=1.00) | +| `Cohesion_50mm` | `_MAXRANGE_RANGEONE _MOVES_SINGLE` | `_BaseSize`=50, `_ProjectorRadius`=5.470629, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.67)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #FFFFFF (α=0.30)
`_MoveTwo` #FF0000 (α=1.00)
`_RangeFive` #FFFF00 (α=1.00)
`_RangeFour` #00FF00 (α=1.00)
`_RangeInfinity` #7F7F7F (α=1.00)
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF6C00 (α=1.00)
`_RangeTwo` #0000FF (α=1.00) | +| `Cohesion_70mm` | `_MAXRANGE_RANGEONE _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=6.850394, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.67)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #FFFFFF (α=0.30)
`_MoveTwo` #FF0000 (α=1.00)
`_RangeFive` #FFFF00 (α=1.00)
`_RangeFour` #00FF00 (α=1.00)
`_RangeInfinity` #7F7F7F (α=1.00)
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF6C00 (α=1.00)
`_RangeTwo` #0000FF (α=1.00) | +| `Cohesion_27mm` | `_ARC_OFF _MAXRANGE_RANGEONE _MOVES_SINGLE` | `_BaseSize`=27, `_ProjectorRadius`=3.531496, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.37)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #FFFFFF (α=0.30)
`_MoveTwo` #FF0000 (α=1.00)
`_RangeFive` #FFFF00 (α=1.00)
`_RangeFour` #00FF00 (α=1.00)
`_RangeInfinity` #000000 (α=1.00)
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF6C00 (α=1.00)
`_RangeTwo` #0000FF (α=1.00) | +| `Cohesion_50mm` | `_MAXRANGE_RANGEONE _MOVES_SINGLE` | `_BaseSize`=50, `_ProjectorRadius`=3.984252, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.67)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #FFFFFF (α=0.30)
`_MoveTwo` #FF0000 (α=1.00)
`_RangeFive` #FFFF00 (α=1.00)
`_RangeFour` #00FF00 (α=1.00)
`_RangeInfinity` #7F7F7F (α=1.00)
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF6C00 (α=1.00)
`_RangeTwo` #0000FF (α=1.00) | +| `Cohesion_70mm` | `_MAXRANGE_RANGEONE _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=4.3779526, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.67)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #FFFFFF (α=0.30)
`_MoveTwo` #FF0000 (α=1.00)
`_RangeFive` #FFFF00 (α=1.00)
`_RangeFour` #00FF00 (α=1.00)
`_RangeInfinity` #7F7F7F (α=1.00)
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF6C00 (α=1.00)
`_RangeTwo` #0000FF (α=1.00) | + +### Prefabs + +| Prefab | Nb Projectors | OrthographicSize / FarClip | Transform pos/rot | Material guid | +|---|---|---|---|---| +| `cohesion_27mm` | 1 | size=4.547244, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | ebe2e870a59f… | +| `cohesion_50mm` | 1 | size=5.669291, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 406845c49cf4… | +| `cohesion_70mm` | 1 | size=6.850394, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 3a57b0b27e32… | +| `halfCohesion_27mm` | 1 | size=3.531496, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 0e1c90ce1ef6… | +| `halfCohesion_50mm` | 1 | size=3.984252, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 2e05a24ab516… | +| `halfCohesion_70mm` | 1 | size=4.3779526, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 7cee44cc736f… | + +## Range + +### Materials + +| Material | Shader keywords | Floats clés | Couleurs | +|---|---|---|---| +| `ProjectorMaterial_100mm` | `_ARC_ON _MAXRANGE_RANGEFIVE _RANGE_FIVE` | `_BaseSize`=100, `_ProjectorRadius`=30, `_BandSize`=0.278, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_OneTuner`=0.0183, `_TwoTuner`=0.0062, `_ThreeTuner`=0.0245, `_FourTuner`=0.0123, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_100mm_oblong` | `_ARC_ON _MAXRANGE_RANGEFIVE` | `_BaseSize`=47.4, `_ProjectorRadius`=30, `_BandSize`=0.34, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_OneTuner`=-0.022, `_TwoTuner`=-0.0421, `_ThreeTuner`=-0.0324, `_FourTuner`=-0.0525, `_FiveTuner`=0.073, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_100mm_snail` | `_ARC_ON _MAXRANGE_RANGEFIVE` | `_BaseSize`=47.4, `_ProjectorRadius`=30, `_BandSize`=0.34, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_OneTuner`=-0.0216, `_TwoTuner`=-0.0421, `_ThreeTuner`=-0.0324, `_FourTuner`=-0.0525, `_FiveTuner`=0.073, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_120mm` | `_ARC_ON _MAXRANGE_RANGEFIVE` | `_BaseSize`=120, `_ProjectorRadius`=30, `_BandSize`=0.278, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_OneTuner`=0.022, `_TwoTuner`=0.0074, `_ThreeTuner`=0.0292, `_FourTuner`=0.0146, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_150mm` | `_ARC_ON _MAXRANGE_RANGEFIVE` | `_BaseSize`=150, `_ProjectorRadius`=30, `_BandSize`=0.278, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_OneTuner`=0.027, `_TwoTuner`=0.009, `_ThreeTuner`=0.0358, `_FourTuner`=0.018, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_18mm_smokeToken` | `_ARC_OFF _MAXRANGE_RANGEONE` | `_BaseSize`=18.8, `_ProjectorRadius`=6, `_BandSize`=-0.11, `_BandContrast`=0.46, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5, `_OneTuner`=0, `_TwoTuner`=0.0034, `_ThreeTuner`=0.0034, `_FourTuner`=0.0034, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #3A3145 (α=0.59)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFFFFF (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_25mm_token` | `_ARC_OFF _MAXRANGE_RANGEONE` | `_BaseSize`=27, `_ProjectorRadius`=2.97, `_BandSize`=-0.45, `_BandContrast`=3, `_MaxRange`=0, `_Arc`=0, `_RangeSize`=-76.5, `_OneTuner`=0, `_TwoTuner`=0.0034, `_ThreeTuner`=0.0034, `_FourTuner`=0.0034, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_25mm_tokenRangeTwo` | `_ARC_OFF _MAXRANGE_RANGETWO` | `_BaseSize`=28.12, `_ProjectorRadius`=14.9, `_BandSize`=0.04, `_BandContrast`=3, `_MaxRange`=1, `_Arc`=0, `_RangeSize`=-76.5, `_OneTuner`=0, `_TwoTuner`=0.0062, `_ThreeTuner`=0.0034, `_FourTuner`=0.0034, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_27mm` | `_ARC_OFF _MAXRANGE_RANGEFIVE` | `_BaseSize`=27, `_ProjectorRadius`=30, `_BandSize`=0.278, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=0, `_RangeSize`=-76.5, `_GradScaler`=0.0034, `_OneTuner`=0.0053, `_TwoTuner`=0.0019, `_ThreeTuner`=0.0072, `_FourTuner`=0.0037, `_FiveTuner`=0, `_InfinityTuner`=0.0034 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_50mm` | `_ARC_ON _MAXRANGE_RANGEFIVE` | `_BaseSize`=50, `_ProjectorRadius`=30, `_BandSize`=0.278, `_BandContrast`=3, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_GradScaler`=0, `_OneTuner`=0.0098, `_TwoTuner`=0.0034, `_ThreeTuner`=0.0127, `_FourTuner`=0.0063, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_50mm_bombCart` | `_ARC_OFF _MAXRANGE_RANGETWO` | `_BaseSize`=50, `_ProjectorRadius`=12, `_BandSize`=0.01, `_BandContrast`=3, `_MaxRange`=1, `_Arc`=0, `_RangeSize`=-76.5, `_OneTuner`=0, `_TwoTuner`=0.0034, `_ThreeTuner`=0.0034, `_FourTuner`=0.0034, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.60)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `ProjectorMaterial_70mm` | `_ARC_ON _MAXRANGE_RANGEFIVE` | `_BaseSize`=70, `_ProjectorRadius`=30, `_BandSize`=0.278, `_BandContrast`=2.87, `_MaxRange`=4, `_Arc`=1, `_RangeSize`=-76.5, `_OneTuner`=0.0132, `_TwoTuner`=0.0045, `_ThreeTuner`=0.0175, `_FourTuner`=0.009, `_FiveTuner`=0, `_InfinityTuner`=0 | `_Color` #FFFFFF (α=0.35)
`_EmissionColor` #000000 (α=1.00)
`_RangeFive` #860070 (α=1.00)
`_RangeFour` #C20042 (α=1.00)
`_RangeInfinity` transparent
`_RangeOne` #FFC300 (α=1.00)
`_RangeThree` #FF1400 (α=1.00)
`_RangeTwo` #FF7500 (α=1.00) | +| `Projector_BoardGuide` | `m_LightmapFlags: 4` | — | `_CameraFadeParams` transparent
`_Color` #FFFFFF (α=1.00)
`_ColorAddSubDiff` transparent
`_EmissionColor` #000000 (α=1.00)
`_SoftParticleFadeParams` transparent
`_TintColor` #7F7F7F (α=0.50) | + +### Prefabs + +| Prefab | Nb Projectors | OrthographicSize / FarClip | Transform pos/rot | Material guid | +|---|---|---|---|---| +| `projector_100mm` | 1 | size=31.9685, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 4b37ff3d68c5… | +| `projector_100mm_oblong` | 1 | size=33.4, far=45 | pos=(0.0, -1.5, 0.0) rot=(0.0, 0.0, 90.0)
pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 6c0556b105e8… | +| `projector_120mm` | 1 | size=32.3622, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | ab767c454926… | +| `projector_150mm` | 1 | size=32.95276, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 3a33cb678c17… | +| `projector_200mm_oblong` | 1 | size=33.4, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0)
pos=(-0.0, -0.26, -0.0) rot=(0.0, 0.0, 90.0) | cccc683b4b18… | +| `projector_27mm` | 1 | size=30.5314, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | b1c5ef617632… | +| `projector_50mm` | 1 | size=30.984253, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 20ce39a8e0e1… | +| `projector_70mm` | 1 | size=31.37795, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 977480df799a… | +| `projector_POIGuide` | 1 | size=36, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | a0fb1f64d52e… | +| `projector_bomb_cart` | 1 | size=12.98425, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 417ae88dafb1… | +| `projector_smokeToken` | 1 | size=6.3700786, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 3ef1e0f12697… | +| `projector_token` | 1 | size=7.06299, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 94ad9a7a030f… | +| `projector_tokenRangeTwo` | 1 | size=12.6, far=45 | pos=(0.0, 20.0, 0.0) rot=(90.0, 0.0, 0.0) | 0905fd93d0b7… | + +## Deployment + +### Materials + +| Material | Shader keywords | Floats clés | Couleurs | +|---|---|---|---| +| `Projector_Deployment_Blue` | `m_LightmapFlags: 4` | — | `_Color` #0000FF (α=0.50)
`_EmissionColor` #000000 (α=1.00) | +| `Projector_Deployment_Blue_Round` | `m_LightmapFlags: 4` | — | `_Color` #0000FF (α=0.50)
`_EmissionColor` #000000 (α=1.00) | +| `Projector_Deployment_Red` | `m_LightmapFlags: 4` | — | `_Color` #FF0000 (α=0.50)
`_EmissionColor` #000000 (α=1.00) | +| `Projector_Deployment_Red_Round` | `m_LightmapFlags: 4` | — | `_Color` #FF0000 (α=0.50)
`_EmissionColor` #000000 (α=1.00) | +| `Projector_Score_Blue` | `m_LightmapFlags: 4` | — | `_Color` #0000FF (α=0.50)
`_EmissionColor` #000000 (α=1.00) | +| `Projector_Score_Red` | `m_LightmapFlags: 4` | — | `_Color` #FF0000 (α=0.50)
`_EmissionColor` #000000 (α=1.00) | + +### Prefabs + +| Prefab | Nb Projectors | OrthographicSize / FarClip | Transform pos/rot | Material guid | +|---|---|---|---|---| +| `projector_blue_score` | 1 | size=6, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | fe66dad2ac93… | +| `projector_blue_spawn` | 1 | size=3, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | 23c778da5a02… | +| `projector_blue_spawn_L` | 2 | size=1.5, far=25
size=3, far=25 | pos=(10.65, 10.71, -19.76) rot=(90.0, 0.0, 0.0)
pos=(1.5, 1.5, 0.0) rot=(0.0, 0.0, 90.0)
pos=(0.0, -1.5000002, 0.0) rot=(0.0, 0.0, 90.0) | 23c778da5a02…
23c778da5a02… | +| `projector_blue_spawn_corner` | 1 | size=1.5, far=25 | pos=(10.65, 10.71, -19.76) rot=(90.0, 0.0, 0.0)
pos=(-1.5, -1.5, 0.0) rot=(0.0, 0.0, 90.0) | 23c778da5a02… | +| `projector_blue_spawn_half` | 1 | size=3, far=25 | pos=(0.0, -1.5, 0.0) rot=(0.0, 0.0, 90.0)
pos=(10.65, 10.71, -19.76) rot=(90.0, 0.0, 0.0) | 23c778da5a02… | +| `projector_blue_spawn_round2` | 1 | size=12, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | 34786d447192… | +| `projector_red_score` | 1 | size=6, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | 90fdc53ff83c… | +| `projector_red_spawn` | 1 | size=3, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | 93e068805588… | +| `projector_red_spawn_L` | 2 | size=3, far=25
size=1.5, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0)
pos=(0.0, 1.5, 0.0) rot=(0.0, 0.0, 90.0)
pos=(-1.5, -1.5, -0.0) rot=(0.0, 0.0, 90.0) | 93e068805588…
93e068805588… | +| `projector_red_spawn_corner` | 1 | size=1.5, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0)
pos=(1.5, 1.5, -0.0) rot=(0.0, 0.0, 90.0) | 93e068805588… | +| `projector_red_spawn_half` | 1 | size=3, far=25 | pos=(0.0, 1.5, -0.0) rot=(0.0, 0.0, 90.0)
pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | 93e068805588… | +| `projector_red_spawn_round2` | 1 | size=12, far=25 | pos=(0.08, 10.71, -19.42) rot=(90.0, 0.0, 0.0) | 54824e17d05f… | + +## Movement + +### Materials + +| Material | Shader keywords | Floats clés | Couleurs | +|---|---|---|---| +| `ProjectorMaterial_100mm_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=8.622047, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_100mm_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=10.590551, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_100mm_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=12.559055, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_120mm_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=9.803149, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_120mm_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=11.771653, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_120mm_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=13.740157, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_150mm_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=11.574803, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_150mm_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=13.543307, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_150mm_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=15.511811, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_27mm_speed1_double` | `_MAXRANGE_RANGEINFINITY _MOVES_DOUBLE` | `_BaseSize`=27, `_ProjectorRadius`=8.45, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_27mm_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=27, `_ProjectorRadius`=4.547244, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_27mm_speed2_double` | `_MAXRANGE_RANGEINFINITY _MOVES_DOUBLE` | `_BaseSize`=27, `_ProjectorRadius`=12.42748, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_27mm_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=27, `_ProjectorRadius`=6.515748, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_27mm_speed3_double` | `_MAXRANGE_RANGEINFINITY _MOVES_DOUBLE` | `_BaseSize`=27, `_ProjectorRadius`=16.42748, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_27mm_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=27, `_ProjectorRadius`=8.484252, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_50mm_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=50, `_ProjectorRadius`=5.6692915, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_50mm_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=50, `_ProjectorRadius`=7.6377954, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_50mm_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=50, `_ProjectorRadius`=9.606299, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_70mm_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=6.850394, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_70mm_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=8.818897, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_70mm_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=10.787401, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_oblong_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=13.051181, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_oblong_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=15.019685, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_oblong_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=16.98819, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_snail_speed1_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=14.527559, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_snail_speed2_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=16.496063, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | +| `ProjectorMaterial_snail_speed3_single` | `_MAXRANGE_RANGEINFINITY _MOVES_SINGLE` | `_BaseSize`=70, `_ProjectorRadius`=18.464567, `_MaxRange`=5, `_RangeSize`=-76.5 | `_Base` #FFFFFF (α=1.00)
`_Color` #FFFFFF (α=0.48)
`_EmissionColor` #000000 (α=1.00)
`_MoveOne` #55CCFF (α=1.00)
`_MoveTwo` #357A97 (α=1.00)
`_RangeFive` #FFFF6D (α=1.00)
`_RangeFour` #FFD600 (α=1.00)
`_RangeInfinity` #FFFFFF (α=0.47)
`_RangeOne` #6C0000 (α=1.00)
`_RangeThree` #FF7B00 (α=1.00)
`_RangeTwo` #DA0000 (α=1.00) | + +### Prefabs + +| Prefab | Nb Projectors | OrthographicSize / FarClip | Transform pos/rot | Material guid | +|---|---|---|---|---| +| `movement_100mm_speed1_single` | 1 | size=8.622047, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 406c1ddb0995… | +| `movement_100mm_speed2_single` | 1 | size=10.590551, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 0aa471d50563… | +| `movement_100mm_speed3_single` | 1 | size=12.559055, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 8fe5008dea10… | +| `movement_120mm_speed1_single` | 1 | size=9.803149, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 21bc9fa53646… | +| `movement_120mm_speed2_single` | 1 | size=11.771653, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 6d02674694a9… | +| `movement_120mm_speed3_single` | 1 | size=13.740157, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 895a4d7a1f02… | +| `movement_150mm_speed1_single` | 1 | size=11.574803, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | fef40c5dadca… | +| `movement_150mm_speed2_single` | 1 | size=13.543307, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | c9d6b007a730… | +| `movement_150mm_speed3_single` | 1 | size=15.511811, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | d08a6fca8013… | +| `movement_27mm_speed1_double` | 1 | size=8.45, far=25 | pos=(0.08, 13.63, -19.42) rot=(90.0, 0.0, 0.0) | 7037d7245d82… | +| `movement_27mm_speed1_single` | 1 | size=4.547244, far=25 | pos=(0.08, 13.63, -19.42) rot=(90.0, 0.0, 0.0) | 61da0f6e009e… | +| `movement_27mm_speed2_double` | 1 | size=12.42748, far=25 | pos=(0.08, 13.63, -19.42) rot=(90.0, 0.0, 0.0) | 362084914234… | +| `movement_27mm_speed2_single` | 1 | size=6.515748, far=25 | pos=(0.08, 13.63, -19.42) rot=(90.0, 0.0, 0.0) | 6de57d74ffda… | +| `movement_27mm_speed3_double` | 1 | size=16.42748, far=25 | pos=(0.08, 13.63, -19.42) rot=(90.0, 0.0, 0.0) | a0dce2ad51d5… | +| `movement_27mm_speed3_single` | 1 | size=8.484252, far=25 | pos=(0.08, 13.63, -19.42) rot=(90.0, 0.0, 0.0) | b65b690fde07… | +| `movement_50mm_speed1_single` | 1 | size=5.6692915, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 38f1f5f6a60c… | +| `movement_50mm_speed2_single` | 1 | size=7.6377954, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 3d2f279db06b… | +| `movement_50mm_speed3_single` | 1 | size=9.606299, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | ca54633a2d92… | +| `movement_70mm_speed1_single` | 1 | size=6.850394, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 3d2faeaaaaff… | +| `movement_70mm_speed2_single` | 1 | size=8.818897, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 948d0799a0c3… | +| `movement_70mm_speed3_single` | 1 | size=10.787401, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | a986001c0bc6… | +| `movement_oblong_speed1_single` | 1 | size=13.051181, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | b9540fc3dad6… | +| `movement_oblong_speed2_single` | 1 | size=15.019685, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | 672e50dcd47c… | +| `movement_oblong_speed3_single` | 1 | size=16.98819, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | a2468c9fe07d… | +| `movement_snail_speed1_single` | 1 | size=14.527559, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | b7c44d9516fd… | +| `movement_snail_speed2_single` | 1 | size=16.496063, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | d27d5e30c83c… | +| `movement_snail_speed3_single` | 1 | size=18.464567, far=25 | pos=(0.0, 13.63, 0.0) rot=(90.0, 0.0, 0.0) | ecdda5f21c4c… | + +## Notes globales + +- Cohesion : double set `Materials/cohesion_*` et `_Revamp/halfCohesion_*`. Probable : `_Revamp/` est la version actuellement utilisée (le bundle live s'appelle `halfcohesion_27mm.unity3d` cf shader-inventory). +- Range : shader procédural avec jusqu'à 5 bandes (`_RangeOne` à `_RangeFive`) + `_RangeInfinity`. Le keyword `_MAXRANGE_RANGEFIVE` ou `_MAXRANGE_RANGEFOUR` détermine la dernière bande visible. +- Movement : un seul Projector par prefab, paramétré par `_BaseSize` + speed. Le `_Arc` doit contrôler la portion d'arc visible. +- Deployment : 6 materials uniques réutilisés par 12 prefabs (rotations différentes pour matérialiser corner/half/L/round/score). \ No newline at end of file diff --git a/mac-patcher/patch_cohesion_rename.py b/mac-patcher/patch_cohesion_rename.py new file mode 100644 index 00000000..065a14a9 --- /dev/null +++ b/mac-patcher/patch_cohesion_rename.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +PATCH ATTEMPT #1 — Rename embedded Shader m_Name from +'BucketheadBits/Projector/Cohesion' to 'Standard'. + +Theory: at AssetBundle.LoadAsset, Unity may resolve the shader by name via +Shader.Find() before falling back to the embedded bytecode. If so, TTS picks +up its own built-in Standard shader and renders the Cohesion projector with +it, ignoring the broken embedded shader. + +Writes to .patched.unity3d alongside the original. Does NOT touch +the live bundle in ~/Library/Tabletop Simulator/Mods/Assetbundles/ yet. +""" +import os +import sys +import shutil +import UnityPy + +BUNDLE_DIR = os.path.expanduser("~/Library/Tabletop Simulator/Mods/Assetbundles/") +BUNDLE_NAME = "httpssteamusercontentaakamaihdnetugc2482129948496305632EBBE2560D4336E6C96317EDDA787E225CF0E5B48.unity3d" +SRC = os.path.join(BUNDLE_DIR, BUNDLE_NAME) +DST = os.path.join("/tmp", "cohesion.patched.unity3d") + +print(f"Loading: {SRC}") +env = UnityPy.load(SRC) + +modified = False +for obj in env.objects: + if obj.type.name != "Shader": + continue + data = obj.read() + current = getattr(getattr(data, "m_ParsedForm", None), "m_Name", None) or getattr(data, "m_Name", None) + print(f" Found Shader PathID={obj.path_id} current m_Name={current!r}") + if current != "BucketheadBits/Projector/Cohesion": + print(" -> Not our target, skipping.") + continue + + # Modify via typetree (safest cross-version path with UnityPy) + tree = obj.read_typetree() + # m_ParsedForm.m_Name is the canonical name for parsed shaders. + # m_Name may also exist for the asset itself; we try both. + if "m_ParsedForm" in tree and "m_Name" in tree["m_ParsedForm"]: + old = tree["m_ParsedForm"]["m_Name"] + tree["m_ParsedForm"]["m_Name"] = "Standard" + print(f" -> renamed m_ParsedForm.m_Name {old!r} -> 'Standard'") + modified = True + if "m_Name" in tree and tree.get("m_Name"): + old = tree["m_Name"] + tree["m_Name"] = "Standard" + print(f" -> renamed m_Name {old!r} -> 'Standard'") + modified = True + + obj.save_typetree(tree) + +if not modified: + print("ERROR: no shader matched. Aborting.") + sys.exit(1) + +# Write out the patched bundle +print(f"\nSaving patched bundle to: {DST}") +with open(DST, "wb") as f: + f.write(env.file.save(packer="lz4")) + +print(f" size: {os.path.getsize(DST)} bytes (original was {os.path.getsize(SRC)})") + +# Verify roundtrip +print("\nVerifying patched bundle is re-loadable...") +env2 = UnityPy.load(DST) +for obj in env2.objects: + if obj.type.name == "Shader": + d = obj.read() + n = getattr(getattr(d, "m_ParsedForm", None), "m_Name", None) or getattr(d, "m_Name", None) + print(f" After patch: Shader PathID={obj.path_id} m_Name={n!r}") + elif obj.type.name == "Material": + d = obj.read() + sref = d.m_Shader + print(f" Material {d.m_Name!r} m_Shader=(file_id={sref.file_id}, path_id={sref.path_id})") + +print("\nDone. Patched file ready at:", DST) +print("Run this command to replace the live bundle (only when TTS is closed):") +print(f" cp {DST!r} {SRC!r}") diff --git a/mac-patcher/patch_save_for_mac.py b/mac-patcher/patch_save_for_mac.py new file mode 100644 index 00000000..24bd5e22 --- /dev/null +++ b/mac-patcher/patch_save_for_mac.py @@ -0,0 +1,2171 @@ +#!/usr/bin/env python3 +""" +Patch a TTS save JSON (SWL mod beta) to add Mac Cohesion fallback. + +Two-part patch: +1. Append manager + event handlers to the Global LuaScript. +2. Replace the inline Cohesion block in Unit_Leader (99f1c8) and Order_Token + (a57c41) with delegated stubs that call the Global manager via Global.call. + +The native hotkey "Show Cohesion On Hovered Model" and the Order Token +"COHESION" button automatically use the patched code afterwards - they +spawn vector-lines rings instead of legacy Custom_AssetBundle Projectors. + +Usage: + python3 patch_save_for_mac.py +""" +import json +import re +import socket +import sys +from pathlib import Path + +# ------------------------------------------------------------ +# 1) Code appended to Global LuaScript (manager + event handlers) +# ------------------------------------------------------------ +GLOBAL_PATCH_LUA = r""" + +-- ============================================ +-- MAC COHESION FALLBACK (auto-injected for TTS U6 magenta bug) +-- Manager Global + event handlers + vector-lines rendering. +-- Called by Unit_Leader / Order_Token via Global.call from their patched +-- Cohesion functions. +-- ============================================ + +-- Capture the ORIGINAL Global functions loaded by !/RangeRulers and +-- !/Cohesion BEFORE we overwrite them below. We also capture the inner +-- spawn/clear pair because the toggle wrapper resolves them by NAME at +-- call time - without aliasing those too, the original toggle would route +-- back into our Mac versions, defeating Windows mode entirely. +spawnRangeRulerOriginalGlobal = spawnRangeRuler +clearRangeRulersOriginalGlobal = clearRangeRulers +spawnCohesionRulerOriginalGlobal = spawnCohesionRuler +clearCohesionRulerOriginalGlobal = clearCohesionRuler + +activeOverlays = activeOverlays or {} +hiddenWhilePickedUp = hiddenWhilePickedUp or {} + +local ASSETS_BASE = "https://raw.githubusercontent.com/ironsquadronfr-hub/tts/mac-projector-fallback/mod/data/mac-fallback-assets/" +local COHESION_HALO_URL = ASSETS_BASE .. "cohesion_halo.png" +local MAX_MOVE_CYAN_URL = ASSETS_BASE .. "max_move_cyan.png" +local DEPLOYMENT_RED_URL = ASSETS_BASE .. "deployment_red.png" +local DEPLOYMENT_BLUE_URL = ASSETS_BASE .. "deployment_blue.png" +local RANGE_DECAL_URLS = { + smokeToken = ASSETS_BASE .. "range_smokeToken.png", + token = ASSETS_BASE .. "range_token.png", + tokenRangeTwo = ASSETS_BASE .. "range_tokenRangeTwo.png", + poi = ASSETS_BASE .. "range_poi.png", + bombCart = ASSETS_BASE .. "range_bombCart.png", + -- Fig leaders: per-base-size PNGs (bands pre-offset by the base radius so + -- the decal bands line up with the vector-line rings). + fig_leader = { + small = ASSETS_BASE .. "range_fig_leader_small.png", + medium = ASSETS_BASE .. "range_fig_leader_medium.png", + large = ASSETS_BASE .. "range_fig_leader_large.png", + huge = ASSETS_BASE .. "range_fig_leader_huge.png", + laat = ASSETS_BASE .. "range_fig_leader_laat.png", + epic = ASSETS_BASE .. "range_fig_leader_epic.png", + long = ASSETS_BASE .. "range_fig_leader_long_stadium.png", + snail = ASSETS_BASE .. "range_fig_leader_snail_stadium.png", + }, +} +local RANGE_MAX_RADIUS = { + fig_leader = 30, -- R5 max + -- SWL: Range 1 = 6", Range 2 = 12", Range 0.5 = 3". + smokeToken = 6, + token = 6, + tokenRangeTwo = 12, + poi = 3, + bombCart = 12, +} + +-- Range band colors (RGBA) extracted from BB_RangeProjector material defaults +-- + demi-portee blanche pour SWL Range 0.5 (3 inches) +local RANGE_COLORS = { + half = {1.0, 1.0, 1.0, 0.6}, -- 0.5 = 3" white + one = {1.0, 0.764, 0.0, 0.7}, -- R1 yellow + two = {1.0, 0.459, 0.0, 0.7}, -- R2 orange + three = {1.0, 0.079, 0.0, 0.7}, -- R3 red + four = {0.764, 0.0, 0.259, 0.7}, -- R4 magenta + five = {0.528, 0.0, 0.443, 0.7}, -- R5 violet +} + +-- Per-rangeKey configurations: list of {radius_inches, color_name} +local RANGE_CONFIGS = { + -- Fig leaders: half + 4 bands at SWL standard ranges 3/6/12/18/24" + small = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + medium = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + large = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + huge = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + laat = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + epic = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + long = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + snail = {{r=3,c="half"},{r=6,c="one"},{r=12,c="two"},{r=18,c="three"},{r=24,c="four"},{r=30,c="five"}}, + -- Token rangeKeys (1 to 2 bands depending on use). SWL ranges in inches: + -- Range 1 = 6", Range 2 = 12", Range 0.5 (poi) = 3". + smokeToken = {{r=6,c="one"}}, + token = {{r=6,c="one"}}, + tokenRangeTwo = {{r=6,c="one"},{r=12,c="two"}}, + poi = {{r=3,c="one"}}, + bombCart = {{r=6,c="one"},{r=12,c="two"}}, +} + +-- Base footprint dimensions in mm. width <= length; equal means round. +-- Used to drive round vs stadium overlay shape. +local BASE_DIMENSIONS = { + small = {width = 27, length = 27}, + medium = {width = 50, length = 50}, + large = {width = 70, length = 70}, + huge = {width = 100, length = 100}, + laat = {width = 120, length = 120}, + epic = {width = 150, length = 150}, + long = {width = 100, length = 175}, -- oblong + snail = {width = 100, length = 200}, -- oblong +} + +-- Base radius (inches) for round overlays. For oblong bases we use a +-- stadium shape instead (see macBuildStadium), so the radius here is the +-- half-width that drives the cap arcs. +local FIG_BASE_RADIUS_IN = {} +for k, d in pairs(BASE_DIMENSIONS) do + FIG_BASE_RADIUS_IN[k] = (d.width / 2) / 25.4 +end + +local function macIsOblong(baseSize) + local d = BASE_DIMENSIONS[baseSize] + return d ~= nil and d.length > d.width +end + +-- Returns the stadium half-dimensions (inches) and yaw for an oblong base, +-- or nil if the fig's base is round. Callers should branch on the return. +local function macOblongDims(fig, baseSize) + if not macIsOblong(baseSize) then return nil end + local d = BASE_DIMENSIONS[baseSize] + return { + halfLen = (d.length / 2) / 25.4, + halfWid = (d.width / 2) / 25.4, + rotY = fig.getRotation().y, + } +end + +-- Resolve baseSize for a fig. The mod stores it as a script-local Var on the +-- fig itself (set at spawn from the unit's container). unitData.baseSize is +-- only populated on the Unit Leader, not on minis/vehicles - so getVar is +-- the canonical read, with unitData as fallback for safety. +local function macGetBaseSize(fig) + if not fig then return nil end + local ok, bs = pcall(function() return fig.getVar("baseSize") end) + if ok and bs then return bs end + local data = fig.getTable("unitData") + if data and data.baseSize then return data.baseSize end + return nil +end + +local function macFigBaseRadius(fig) + if not fig then return 0.5315 end + local bs = macGetBaseSize(fig) + return (bs and FIG_BASE_RADIUS_IN[bs]) or 0.5315 +end + +-- Token base radii (inches) - measured from token edge for range bands +local TOKEN_BASE_RADIUS = { + smokeToken = 18.8 / 2 / 25.4, -- 0.370" + token = 25.1 / 2 / 25.4, -- 0.494" + tokenRangeTwo = 25.1 / 2 / 25.4, -- 0.494" + poi = 50.8 / 2 / 25.4, -- 1.000" + bombCart = 50.0 / 2 / 25.4, -- 0.984" +} + +-- Hotkey-driven range on tokens renders fig-leader-style bands using the +-- closest equivalent base size (so the visual matches what a unit of that +-- footprint would see). Token R button keeps the single ring (token-spec). +local TOKEN_TO_BASESIZE = { + smokeToken = "small", + token = "small", + tokenRangeTwo = "small", + poi = "medium", + bombCart = "medium", +} + +-- Maximum Move radii (inches), indexed by baseSize then speed 1..3 +-- Source: ProjectorRadius from BB_MovementProjector materials (single variants) +local MAX_MOVE_RADIUS = { + small = { 4.547244, 6.515748, 8.484252}, -- 27mm + medium = { 5.669292, 7.637795, 9.606299}, -- 50mm + large = { 6.850394, 8.818897, 10.787401}, -- 70mm + huge = { 8.622047, 10.590551, 12.559055}, -- 100mm + laat = { 9.803149, 11.771653, 13.740157}, -- 120mm + epic = {11.574803, 13.543307, 15.511811}, -- 150mm + long = {13.051181, 15.019685, 16.988190}, -- oblong + snail = {14.527559, 16.496063, 18.464567}, -- snail +} + +-- Deployment cell dimensions (width X, depth Z, inches). +-- All cells live on a 6"x6" grid; sub-cells are halves/quarters of that cell. +local DEPLOYMENT_CELL_SIZES = { + r = {6, 6}, b = {6, 6}, -- full cell + rl = {6, 6}, bl = {6, 6}, -- large variant (also full cell) + rh = {6, 3}, bh = {6, 3}, -- horizontal half (3" deep) + rs = {3, 6}, bs = {3, 6}, -- vertical half (3" wide), x-offset +1.5 + rss = {3, 6}, bss = {3, 6}, -- vertical half (3" wide), x-offset -1.5 + rc = {3, 3}, bc = {3, 3}, -- corner quarter + rcc = {3, 3}, bcc = {3, 3}, -- opposite corner quarter +} + +local function macRayGroundY(x, z, offset) + local hits = Physics.cast({ + origin = {x, 30, z}, + direction = {0, -1, 0}, + type = 1, + max_distance = 50, + }) + for _, h in ipairs(hits) do + return h.point.y + (offset or 0.05) + end + return offset or 0.05 +end + +local function macBuildRect(cx, cz, hw, hd, color, thickness) + local segPerSide = 8 + local pts = {} + local corners = { + {cx - hw, cz + hd}, {cx + hw, cz + hd}, + {cx + hw, cz - hd}, {cx - hw, cz - hd}, + } + for i = 1, 4 do + local a = corners[i] + local b = corners[i % 4 + 1] + for s = 0, segPerSide - 1 do + local t = s / segPerSide + local x = a[1] + (b[1] - a[1]) * t + local z = a[2] + (b[2] - a[2]) * t + table.insert(pts, {x, macRayGroundY(x, z), z}) + end + end + local c1 = corners[1] + table.insert(pts, {c1[1], macRayGroundY(c1[1], c1[2]), c1[2]}) + return {points = pts, color = color, thickness = thickness or 0.06} +end + +local function macBuildRing(centerPos, radius, color, thickness, ignoreObj) + local nSeg = 64 + local pts = {} + for i = 0, nSeg do + local a = (i / nSeg) * 2 * math.pi + local x = centerPos.x + radius * math.cos(a) + local z = centerPos.z + radius * math.sin(a) + local hits = Physics.cast({ + origin = {x, centerPos.y + 10, z}, + direction = {0, -1, 0}, + type = 1, + max_distance = 20, + }) + local y = centerPos.y + 0.05 + for _, h in ipairs(hits) do + if h.hit_object ~= ignoreObj then + y = h.point.y + 0.05 + break + end + end + table.insert(pts, {x, y, z}) + end + return {points = pts, color = color, thickness = thickness or 0.05} +end + +-- Stadium = rectangle with two semicircular caps on the long-axis ends. +-- halfLen, halfWid in inches (halfLen >= halfWid). dist = expansion outwards +-- from the base edge in inches (so the overlay sits at dist outside the base). +-- rotYdeg rotates the stadium around Y to match the fig's orientation; the +-- long axis is local Z (front-to-back of the model). +local function macBuildStadium(centerPos, halfLen, halfWid, rotYdeg, dist, color, thickness, ignoreObj) + local rotRad = math.rad(rotYdeg or 0) + local cosR, sinR = math.cos(rotRad), math.sin(rotRad) + + local capCenter = halfLen - halfWid -- distance from center to cap arc center, along long axis + local capR = halfWid + dist -- cap arc radius (= straight-side offset from long axis) + + local nSegArc = 32 -- segments per semicircle cap + local pts = {} + + local function addLocal(lx, lz) + local wx = centerPos.x + lx * cosR + lz * sinR + local wz = centerPos.z - lx * sinR + lz * cosR + local hits = Physics.cast({ + origin = {wx, centerPos.y + 10, wz}, + direction = {0, -1, 0}, + type = 1, + max_distance = 20, + }) + local y = centerPos.y + 0.05 + for _, h in ipairs(hits) do + if h.hit_object ~= ignoreObj then + y = h.point.y + 0.05 + break + end + end + table.insert(pts, {wx, y, wz}) + end + + -- Top cap: angle 0 to pi (right edge -> top -> left edge), centered at z=+capCenter + for i = 0, nSegArc do + local a = (i / nSegArc) * math.pi + addLocal(capR * math.cos(a), capCenter + capR * math.sin(a)) + end + -- The straight left side is implicit: the polyline jumps from + -- (-capR, +capCenter) directly to (-capR, -capCenter) - TTS draws a + -- straight segment between consecutive points. + -- Bottom cap: angle pi to 2pi, centered at z=-capCenter + for i = 0, nSegArc do + local a = math.pi + (i / nSegArc) * math.pi + addLocal(capR * math.cos(a), -capCenter + capR * math.sin(a)) + end + -- Close the loop back to the top-cap start point. + addLocal(capR, capCenter) + + return {points = pts, color = color, thickness = thickness or 0.05} +end + +local macBuilders = {} + +macBuilders.range = function(fig, params) + local pos = fig.getPosition() + -- Resolve rangeKey for token-specific single-ring rendering (R button on + -- a token). The hotkey path sets params.forceFigMode = true to bypass + -- this and always render the fig-leader 6-band overlay, using the token's + -- equivalent baseSize for sizing. + local forceFig = params and params.forceFigMode + local rangeKey = (not forceFig) and ((params and params.rangeKey) or fig.getVar("rangeKey")) or nil + local config, decalURL, maxR, br + local oblong, halfLen, halfWid, rotY + local baseSize -- hoisted: used below for firing-arc decision + if rangeKey and RANGE_CONFIGS[rangeKey] then + config = RANGE_CONFIGS[rangeKey] + decalURL = RANGE_DECAL_URLS[rangeKey] + maxR = RANGE_MAX_RADIUS[rangeKey] + br = TOKEN_BASE_RADIUS[rangeKey] or 0 + else + baseSize = macGetBaseSize(fig) + local hasBaseSize = baseSize ~= nil + if not hasBaseSize then + -- Token hit from hotkey: map rangeKey to base size for sizing. + local tk = fig.getVar("rangeKey") + baseSize = (tk and TOKEN_TO_BASESIZE[tk]) or "small" + end + config = RANGE_CONFIGS[baseSize] or RANGE_CONFIGS.small + decalURL = RANGE_DECAL_URLS.fig_leader[baseSize] + or RANGE_DECAL_URLS.fig_leader.small + maxR = RANGE_MAX_RADIUS.fig_leader + if hasBaseSize then + br = macFigBaseRadius(fig) + local od = macOblongDims(fig, baseSize) + if od then + oblong, halfLen, halfWid, rotY = true, od.halfLen, od.halfWid, od.rotY + end + else + -- Token in forceFig mode: use token's measured radius for offset. + local tk = fig.getVar("rangeKey") + br = (tk and TOKEN_BASE_RADIUS[tk]) or FIG_BASE_RADIUS_IN[baseSize] or 0.5315 + end + end + + -- SWL convention: all distances are measured from the base/token edge. + + -- Vector contours: 6 concentric rings for round bases, 6 concentric + -- stadium outlines for oblong bases (each band sits at band.r outwards + -- from the base edge - same rule, just a different shape). + local lines = {} + for _, band in ipairs(config) do + if oblong then + table.insert(lines, macBuildStadium(pos, halfLen, halfWid, rotY, band.r, + RANGE_COLORS[band.c], 0.1, fig)) + else + table.insert(lines, macBuildRing(pos, band.r + br, RANGE_COLORS[band.c], 0.1, fig)) + end + end + + -- Firing arc lines (4 radial lines dividing into Front/R/Rear/L quadrants). + -- Mirrors the vanilla Projector shader: ProjectorMaterial_ sets + -- _Arc=1 / shader keyword _ARC_ON for every non-27mm base (medium, large, + -- huge, laat, epic, long, snail). 27mm sets _Arc=0. + -- Round bases : lines emanate from the base edge, radial from center. + -- Oblong bases: lines emanate from the centers of the two end + -- semicircles (front cap + rear cap), front lines at +/-45 from the + -- long-axis forward, rear lines at +/-135. + local arcColor = {1, 1, 1, 0.6} + if baseSize and baseSize ~= "small" then + if oblong then + local capOffset = (halfLen or 0) - (halfWid or 0) + local rad = math.rad(rotY) + local fwdX, fwdZ = math.sin(rad), math.cos(rad) + local frontX = pos.x + capOffset * fwdX + local frontZ = pos.z + capOffset * fwdZ + local rearX = pos.x - capOffset * fwdX + local rearZ = pos.z - capOffset * fwdZ + local reach = (halfWid or 0) + (maxR or 6) + local arcs = { + {frontX, frontZ, 45}, -- front-right boundary + {frontX, frontZ, -45}, -- front-left boundary + {rearX, rearZ, 135}, -- rear-right boundary + {rearX, rearZ, -135}, -- rear-left boundary + } + for _, e in ipairs(arcs) do + local cx, cz, deg = e[1], e[2], e[3] + local a = math.rad(rotY + deg) + local sinA, cosA = math.sin(a), math.cos(a) + table.insert(lines, { + points = { + {cx, pos.y + 0.1, cz}, + {cx + reach * sinA, pos.y + 0.1, cz + reach * cosA}, + }, + color = arcColor, + thickness = 0.05, + }) + end + else + local figRotY = fig.getRotation().y + local outerR = (maxR or 6) + br + for _, deg in ipairs({45, 135, 225, 315}) do + local a = math.rad(deg + figRotY) + local sinA, cosA = math.sin(a), math.cos(a) + table.insert(lines, { + points = { + {pos.x + br * sinA, pos.y + 0.1, pos.z + br * cosA}, + {pos.x + outerR * sinA, pos.y + 0.1, pos.z + outerR * cosA}, + }, + color = arcColor, + thickness = 0.05, + }) + end + end + end + + -- Decal halo (filled bands) - flat on the ground under the fig + local hits = Physics.cast({ + origin = {pos.x, pos.y + 10, pos.z}, + direction = {0, -1, 0}, + type = 1, + max_distance = 20, + }) + local groundY = pos.y + for _, h in ipairs(hits) do + if h.hit_object ~= fig then groundY = h.point.y; break end + end + + if oblong then + -- Stadium-shape PNGs are authored per oblong base size: the 6 bands + -- inside the PNG already match the stadium vector outlines because + -- the PNG was generated with the same halfWid/halfLen + dist math. + -- Decal scale = footprint of the outermost band (2*(half + maxR)) + -- in each axis, so the PNG stretches to exactly the right size. + local longSpan = 2 * (halfLen + maxR) + local shortSpan = 2 * (halfWid + maxR) + return { + lines = lines, + decals = { + { + name = "range_decal_" .. fig.getGUID(), + url = decalURL, + position = {pos.x, groundY + 0.02, pos.z}, + rotation = {90, rotY, 0}, + scale = {shortSpan, longSpan, 1}, + } + }, + } + end + + local effectiveMaxR = maxR + br + return { + lines = lines, + decals = { + { + name = "range_decal_" .. fig.getGUID(), + url = decalURL, + position = {pos.x, groundY + 0.02, pos.z}, -- below cohesion halo + rotation = {90, 0, 0}, + scale = {effectiveMaxR * 2, effectiveMaxR * 2, effectiveMaxR * 2}, + } + }, + } +end + +macBuilders.maxmove = function(fig, params) + -- Use the anchor position stored at spawn time so the ring stays put + -- when the fig slides toward the destination, instead of tracking it. + local pos = (params and params.anchorPos) or fig.getPosition() + local baseSize = (params and params.baseSize) or "small" + local speed = (params and params.speed) or 1 + local radii = MAX_MOVE_RADIUS[baseSize] or MAX_MOVE_RADIUS.small + local r = radii[speed] or radii[1] + local color = {0.333, 0.800, 1.000, 0.7} + -- Base ring: small white outline matching the figure's base + -- (BB_MovementProjector renders a base ring on top of the cyan max-move). + local br = FIG_BASE_RADIUS_IN[baseSize] or FIG_BASE_RADIUS_IN.small + local baseColor = {1, 1, 1, 0.8} + -- Oblong base dimensions for the stadium base ring. Use the rotation + -- captured at spawn-time so the ring keeps its orientation even after + -- the fig pivots toward its destination. + local od = macOblongDims(fig, baseSize) + local oblong = od ~= nil + local halfLen, halfWid, rotY + if oblong then + halfLen, halfWid = od.halfLen, od.halfWid + rotY = (params and params.anchorRot) or od.rotY + end + + -- Ground raycast for decal position + local hits = Physics.cast({ + origin = {pos.x, pos.y + 10, pos.z}, + direction = {0, -1, 0}, + type = 1, + max_distance = 20, + }) + local groundY = pos.y + for _, h in ipairs(hits) do + if h.hit_object ~= fig then groundY = h.point.y; break end + end + + if oblong then + -- Oblong base: the cyan max-move envelope stays a round ring (isotropic + -- - any rotation reaches the same radius from the pivot), but the + -- inner white base ring follows the rectangular footprint as a + -- stadium hugging the actual base edge. + return { + lines = { + macBuildRing(pos, r, color, 0.1, fig), + macBuildStadium(pos, halfLen, halfWid, rotY, 0, + baseColor, 0.06, fig), + }, + decals = { + { + name = "maxmove_decal_" .. fig.getGUID(), + url = MAX_MOVE_CYAN_URL, + position = {pos.x, groundY + 0.025, pos.z}, + rotation = {90, 0, 0}, + scale = {r * 2, r * 2, r * 2}, + } + }, + } + end + + return { + lines = { + macBuildRing(pos, r, color, 0.1, fig), -- outer cyan max-move + macBuildRing(pos, br, baseColor, 0.06, fig), -- inner white base ring + }, + decals = { + { + name = "maxmove_decal_" .. fig.getGUID(), + url = MAX_MOVE_CYAN_URL, + position = {pos.x, groundY + 0.025, pos.z}, + rotation = {90, 0, 0}, + scale = {r * 2, r * 2, r * 2}, + } + }, + } +end + +macBuilders.deployment = function(_, params) + local pos = params.pos -- {x, y, z} cell center (after offset) + local cell = params.cell -- "r"/"b"/"rh"/... key + local size = DEPLOYMENT_CELL_SIZES[cell] or {6, 6} + local w, d = size[1], size[2] + local cx, cz = pos[1], pos[3] + local hw, hd = w / 2, d / 2 + + local isRed = cell:sub(1, 1) == "r" + local lineColor = isRed and {1, 0.15, 0.15, 0.9} or {0.15, 0.4, 1, 0.9} + local url = isRed and DEPLOYMENT_RED_URL or DEPLOYMENT_BLUE_URL + + local groundY = macRayGroundY(cx, cz, 0) + + return { + lines = { macBuildRect(cx, cz, hw, hd, lineColor, 0.08) }, + decals = { + { + name = "deployment_decal_" .. cell .. "_" .. tostring(cx) .. "_" .. tostring(cz), + url = url, + position = {cx, groundY + 0.03, cz}, + rotation = {90, 0, 0}, + scale = {w, d, 1}, + } + }, + } +end + +macBuilders.cohesion = function(fig, params) + local pos = fig.getPosition() + -- Cohesion overlay = base edge + 3 inches (half Range 1). Round bases get + -- a ring; oblong bases (long/snail) get a stadium-shape concentric to the + -- footprint so the offset stays a constant 3" from the edge. + local rangeKey = fig.getVar("rangeKey") + local oblong, halfLen, halfWid, rotY, r + if rangeKey and TOKEN_BASE_RADIUS[rangeKey] then + r = TOKEN_BASE_RADIUS[rangeKey] + 3.0 + else + local baseSize = macGetBaseSize(fig) or "small" + local od = macOblongDims(fig, baseSize) + if od then + oblong, halfLen, halfWid, rotY = true, od.halfLen, od.halfWid, od.rotY + else + r = (FIG_BASE_RADIUS_IN[baseSize] or FIG_BASE_RADIUS_IN.small) + 3.0 + end + end + if params and params.radius then + oblong, r = false, params.radius + end + + -- Find ground level under the fig so the flat decal sits on the table, + -- not at the fig pivot (which is above the base). + local hits = Physics.cast({ + origin = {pos.x, pos.y + 10, pos.z}, + direction = {0, -1, 0}, + type = 1, + max_distance = 20, + }) + local groundY = pos.y + for _, h in ipairs(hits) do + if h.hit_object ~= fig then + groundY = h.point.y + break + end + end + + if oblong then + -- Halo decal: stretch the round halo PNG into an ellipse aligned with + -- the fig's long axis. Approximates the stadium fade (a true stadium + -- halo would need a custom PNG); good enough visually since the + -- vector stadium outline carries the precise boundary. + local longSpan = 2 * (halfLen + 3.0) + local shortSpan = 2 * (halfWid + 3.0) + return { + lines = { macBuildStadium(pos, halfLen, halfWid, rotY, 3.0, + {1, 1, 1, 0.8}, 0.06, fig) }, + decals = { + { + name = "cohesion_halo_" .. fig.getGUID(), + url = COHESION_HALO_URL, + position = {pos.x, groundY + 0.04, pos.z}, + -- 90 deg pitch lays the decal flat on the table; rotY (yaw) + -- aligns its local Y with the fig's long axis. + rotation = {90, rotY, 0}, + scale = {shortSpan, longSpan, 1}, + } + }, + } + end + + return { + lines = { macBuildRing(pos, r, {1, 1, 1, 0.8}, 0.06, fig) }, + decals = { + { + name = "cohesion_halo_" .. fig.getGUID(), + url = COHESION_HALO_URL, + position = {pos.x, groundY + 0.04, pos.z}, + rotation = {90, 0, 0}, + scale = {r * 2, r * 2, r * 2}, + } + }, + } +end + +function macRedrawAll() + local lines, decals = {}, {} + -- Preload all texture URLs (invisible decals far below the table) so TTS + -- keeps them cached and we never get the white-square flash on respawn. + local preloadURLs = { + COHESION_HALO_URL, + RANGE_DECAL_URLS.smokeToken, + RANGE_DECAL_URLS.token, RANGE_DECAL_URLS.tokenRangeTwo, + RANGE_DECAL_URLS.poi, RANGE_DECAL_URLS.bombCart, + RANGE_DECAL_URLS.fig_leader.small, RANGE_DECAL_URLS.fig_leader.medium, + RANGE_DECAL_URLS.fig_leader.large, RANGE_DECAL_URLS.fig_leader.huge, + RANGE_DECAL_URLS.fig_leader.laat, RANGE_DECAL_URLS.fig_leader.epic, + RANGE_DECAL_URLS.fig_leader.long, RANGE_DECAL_URLS.fig_leader.snail, + MAX_MOVE_CYAN_URL, + DEPLOYMENT_RED_URL, DEPLOYMENT_BLUE_URL, + } + for i, url in ipairs(preloadURLs) do + table.insert(decals, { + name = "preload_" .. i, + url = url, + position = {0, -200 - i * 0.01, 0}, + rotation = {90, 0, 0}, + scale = {0.01, 0.01, 0.01}, + }) + end + local stale = {} + for key, entry in pairs(activeOverlays) do + local b = macBuilders[entry.type] + if b then + -- Deployment entries have no fig (params-driven). Others need a + -- live Object - drop them silently if destroyed/invalid. + local needsFig = (entry.type ~= "deployment") + if needsFig and (not entry.fig + or type(entry.fig.getPosition) ~= "function") then + stale[#stale + 1] = key + else + local ok, out = pcall(b, entry.fig, entry.params) + if ok and out then + for _, l in ipairs(out.lines or {}) do + table.insert(lines, l) + end + for _, d in ipairs(out.decals or {}) do + table.insert(decals, d) + end + else + stale[#stale + 1] = key + end + end + end + end + for _, k in ipairs(stale) do activeOverlays[k] = nil end + Global.setVectorLines(lines) + Global.setDecals(decals) +end + +function gSpawnCohesion(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + activeOverlays[fig.getGUID() .. ":cohesion"] = { + type = "cohesion", fig = fig, params = params or {} + } + macRedrawAll() +end + +function gClearCohesion(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + activeOverlays[fig.getGUID() .. ":cohesion"] = nil + macRedrawAll() +end + +function gToggleCohesion(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + local key = fig.getGUID() .. ":cohesion" + if activeOverlays[key] then + gClearCohesion(params) + else + gSpawnCohesion(params) + end +end + +-- Range polling: a slow timer (5 frames = ~12 fps) that re-draws active +-- range rulers so they follow figures in real-time. Stops automatically when +-- no range overlay is active anymore - avoids the overnight memory leak. +macRangePollingActive = macRangePollingActive or false + +function macRangePoll() + local hasRange = false + for _, e in pairs(activeOverlays) do + if e.type == "range" then hasRange = true; break end + end + if hasRange then + macRedrawAll() + Wait.frames(macRangePoll, 5) + else + macRangePollingActive = false + end +end + +function gSpawnRange(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + activeOverlays[fig.getGUID() .. ":range"] = { + type = "range", fig = fig, params = params or {} + } + macRedrawAll() + if not macRangePollingActive then + macRangePollingActive = true + Wait.frames(macRangePoll, 5) + end +end + +function gClearRange(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + activeOverlays[fig.getGUID() .. ":range"] = nil + macRedrawAll() +end + +function gToggleRange(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + local key = fig.getGUID() .. ":range" + if activeOverlays[key] then + gClearRange(params) + else + gSpawnRange(params) + end +end + +function gSpawnDeployment(params) + -- params: { cell = "r" | "b" | ..., pos = {x, y, z} } + if not params or not params.cell or not params.pos then return end + local key = "deploy:" .. params.cell .. ":" + .. tostring(params.pos[1]) .. ":" .. tostring(params.pos[3]) + activeOverlays[key] = {type = "deployment", fig = nil, params = params} + macRedrawAll() +end + +function gClearAllDeployment() + for k, e in pairs(activeOverlays) do + if e.type == "deployment" then activeOverlays[k] = nil end + end + macRedrawAll() +end + +function gSpawnMaxMove(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + -- Capture the spawn-time position + yaw. MaxMove is anchored to where + -- the move STARTED - it must not follow the fig as it slides toward + -- the destination. The yaw matters for oblong stadium overlays so the + -- white base ring keeps its orientation while the fig pivots. + local p = (params and params.params) or params or {} + p.anchorPos = fig.getPosition() + p.anchorRot = fig.getRotation().y + activeOverlays[fig.getGUID() .. ":maxmove"] = { + type = "maxmove", fig = fig, params = p + } + macRedrawAll() +end + +function gClearMaxMove(params) + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + activeOverlays[fig.getGUID() .. ":maxmove"] = nil + macRedrawAll() +end + +function onObjectPickUp(player_color, obj) + if not obj or not obj.getGUID then return end + local guid = obj.getGUID() + local prefix = guid .. ":" + local plen = #prefix + local changed = false + for key, entry in pairs(activeOverlays) do + if key:sub(1, plen) == prefix then + -- Cohesion: static, hide during pickup, restore on drop. + -- Range: keep visible; the polling timer redraws each tick so + -- the ruler follows the figure in real-time during the drag. + if entry.type == "cohesion" then + hiddenWhilePickedUp[key] = entry + activeOverlays[key] = nil + changed = true + end + end + end + if changed then macRedrawAll() end +end + +function onObjectDrop(player_color, obj) + if not obj or not obj.getGUID then return end + local guid = obj.getGUID() + local prefix = guid .. ":" + local plen = #prefix + + -- Defer the overlay restore until the fig has come to rest. Without this, + -- a flick-drop with residual inertia leaves the overlay anchored at the + -- release position while the fig continues to slide across the table. + local function tryRestore() + local fig = getObjectFromGUID(guid) + if not fig then return end -- destroyed during slide + if fig.held_by_color then return end -- picked up again, hold off + local v = fig.getVelocity and fig.getVelocity() + if v then + local speed = math.sqrt((v.x or 0)^2 + (v.y or 0)^2 + (v.z or 0)^2) + if speed > 0.05 then + Wait.frames(tryRestore, 3) + return + end + end + local changed = false + for key, entry in pairs(hiddenWhilePickedUp) do + if key:sub(1, plen) == prefix then + activeOverlays[key] = entry + hiddenWhilePickedUp[key] = nil + changed = true + end + end + if changed then macRedrawAll() end + end + tryRestore() +end + +function onObjectDestroy(obj) + if not obj or not obj.getGUID then return end + local guid = obj.getGUID() + local prefix = guid .. ":" + local plen = #prefix + local changed = false + for key in pairs(activeOverlays) do + if key:sub(1, plen) == prefix then + activeOverlays[key] = nil + changed = true + end + end + for key in pairs(hiddenWhilePickedUp) do + if key:sub(1, plen) == prefix then + hiddenWhilePickedUp[key] = nil + end + end + if changed then macRedrawAll() end +end + +-- Texture preload is handled by macRedrawAll which always includes all PNG +-- URLs as invisible decals far below the table. No separate preload needed. + +-- ============================================ +-- PER-SEAT MODE TOGGLE (Cohesion + Range) +-- Each seated player picks their renderer: "mac" (this patch) or +-- "windows" (original bundle Projector). When a player triggers an overlay, +-- the router checks THEIR mode and uses that renderer. The rendered overlay +-- is visible to everyone (TTS engine limitation). +-- ============================================ + +playerOverlayMode = playerOverlayMode or {} -- color -> "mac" | "windows" +-- Default = "windows" (majority of TTS users). Mac users toggle their seat +-- via the floating panel. Same for deployment (table-wide setting). +deploymentMode = deploymentMode or "windows" + +function gGetMode(params) + return playerOverlayMode[params.color] or "windows" +end + +function gGetDeploymentMode() + return deploymentMode +end + +function gToggleDeploymentMode() + deploymentMode = (deploymentMode == "mac") and "windows" or "mac" + -- Wipe any active deployment zones so the next setup uses the new mode. + gClearAllDeployment() + macDeferRefresh() +end + +-- Replicates the original showRangeOnHoveredModel toggle, calling the +-- aliased Global originals so our Mac overrides don't recapture the path. +function gWindowsRangeToggle(fig) + if not fig then return end + if rangeRuler ~= nil then + pcall(clearRangeRulersOriginalGlobal) + if selectedUnitObj == fig then + selectedUnitObj = nil + return + end + end + if fig.interactable then + pcall(function() spawnRangeRulerOriginalGlobal(fig) end) + selectedUnitObj = fig + end +end + +-- Replicates the original showCohesionOnHoveredModel toggle. Uses Global +-- aliased originals + Global cohesionRuler state (set by the original). +function gWindowsCohesionToggle(fig) + if not fig then return end + if fig.interactable and selectedUnitObj == fig and cohesionRuler ~= nil then + pcall(clearCohesionRulerOriginalGlobal) + selectedUnitObj = nil + return + end + pcall(clearCohesionRulerOriginalGlobal) + pcall(function() spawnCohesionRulerOriginalGlobal(fig) end) + selectedUnitObj = fig +end + +function gCohesionTrigger(params) + if not params or not params.figGUID then return end + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + local mode = playerOverlayMode[params.playerColor] or "windows" + if mode == "windows" then + gWindowsCohesionToggle(fig) + else + gToggleCohesion({figGUID = params.figGUID}) + end +end + +function gRangeTrigger(params) + if not params or not params.figGUID then return end + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + local mode = playerOverlayMode[params.playerColor] or "windows" + if mode == "windows" then + gWindowsRangeToggle(fig) + else + gToggleRange({ + figGUID = params.figGUID, + forceFigMode = params.forceFigMode, + rangeKey = params.rangeKey, + }) + end +end + +-- UI: floating panel with one toggle button per seated player +function macModeClick(player, _, id) + local color = id:sub(9) -- "macmode_Red" -> "Red" + if player.color ~= color then + broadcastToColor("Only the player at this seat can toggle their mode.", + player.color, {1, 0.5, 0.5}) + return + end + local cur = playerOverlayMode[color] or "windows" + playerOverlayMode[color] = (cur == "mac") and "windows" or "mac" + -- Wipe Mac-side overlays so stale visuals don't linger after the toggle. + -- We don't track per-color ownership, so this clears Cohesion+Range for + -- everyone - acceptable since each player can re-trigger their hotkey. + -- Windows-side overlays (fig-scoped cohesionRuler/rangeRuler) are + -- cleared by iterating objects with active state. + for k, e in pairs(activeOverlays) do + if e.type == "cohesion" or e.type == "range" then + activeOverlays[k] = nil + end + end + macRedrawAll() + for _, obj in ipairs(getAllObjects()) do + if obj.getVar and obj.getVar("cohesionRuler") then + pcall(function() obj.call("clearCohesionRulerOriginal", obj) end) + end + if obj.getVar and obj.getVar("rangeRuler") then + pcall(function() obj.call("clearRangeRulersOriginal", obj) end) + end + end + macDeferRefresh() +end + +local function macFindNodeById(tree, id) + for _, node in ipairs(tree) do + if node.attributes and node.attributes.id == id then return node, tree end + if node.children then + local found, parent = macFindNodeById(node.children, id) + if found then return found, parent end + end + end + return nil, nil +end + +-- Tracks the panel's desired active state across refreshes. Persists so a +-- click on the close X before the initial refresh is honored. Hidden at first +-- load (default = false); the floating "Mac Patch" menu button reveals it. +macModePanelActive = (macModePanelActive == nil) and false or macModePanelActive + +function macModeToggleVisibility() + macModePanelActive = not macModePanelActive + -- Defer the XML rebuild: TTS occasionally throws a UTF-8 byte-buffer + -- encoding error if UI.setXmlTable runs synchronously inside a UI + -- click handler. Deferring one frame + pcall has been reliable. + Wait.frames(function() + pcall(function() + local tree = UI.getXmlTable() or {} + local panel = macFindNodeById(tree, "macModePanel") + if panel then + panel.attributes.active = macModePanelActive and "true" or "false" + UI.setXmlTable(tree) + else + macRefreshModeUI() + end + end) + end, 1) +end + +function macRefreshModeUI() + local seated = Player.getPlayers() + local rows = {{ + tag = "Panel", + attributes = { + color = "transparent", + preferredHeight = "24", + }, + children = { + { + tag = "Text", + attributes = { + text = "Mac TTS U6 Patch", + fontSize = "16", + color = "white", + alignment = "MiddleLeft", + rectAlignment = "MiddleLeft", + }, + }, + { + tag = "Button", + attributes = { + id = "macModePanelClose", + text = "X", + onClick = "macModeToggleVisibility", + color = "#3a1e1e", + textColor = "white", + fontSize = "12", + width = "24", + height = "20", + rectAlignment = "MiddleRight", + }, + }, + }, + }, { + tag = "Text", + attributes = { + text = "Cohesion & Range: pick your renderer", + fontSize = "12", + color = "#bbbbbb", + alignment = "MiddleCenter", + }, + }} + if #seated == 0 then + table.insert(rows, { + tag = "Text", + attributes = { + text = "(no seated players)", + fontSize = "12", + color = "#888888", + alignment = "MiddleCenter", + }, + }) + end + for _, p in ipairs(seated) do + local mode = playerOverlayMode[p.color] or "windows" + local label = p.color .. " - " .. + (mode == "mac" and "MAC FALLBACK" or "WINDOWS ORIGINAL") + local bg = (mode == "mac") and "#1e7a3a" or "#7a3a1e" + table.insert(rows, { + tag = "Button", + attributes = { + id = "macmode_" .. p.color, + text = label, + onClick = "macModeClick", + color = bg, + fontSize = "13", + textColor = "white", + preferredHeight = "28", + }, + }) + end + -- Deployment is table-wide (everyone sees the same zones); use a single + -- global toggle rather than per-seat. + table.insert(rows, { + tag = "Text", + attributes = { + text = "Deployment (table-wide)", + fontSize = "12", + color = "#bbbbbb", + alignment = "MiddleCenter", + }, + }) + table.insert(rows, { + tag = "Button", + attributes = { + id = "macmode_deployment", + text = (deploymentMode == "mac") and "DEPLOYMENT: MAC FALLBACK" + or "DEPLOYMENT: WINDOWS ORIGINAL", + onClick = "macDeploymentClick", + color = (deploymentMode == "mac") and "#1e3a7a" or "#7a3a1e", + fontSize = "13", + textColor = "white", + preferredHeight = "28", + }, + }) + -- Build my panel (will be merged into the existing UI tree below so we + -- don't clobber legionFloatingMenu / Welcome / Chess Clocks etc.). + local panel = { + tag = "Panel", + attributes = { + id = "macModePanel", + active = macModePanelActive and "true" or "false", + rectAlignment = "MiddleRight", + offsetXY = "-10 80", + width = "260", + height = "320", + color = "rgba(0.06,0.06,0.06,0.9)", + padding = "8 8 8 8", + outlineSize = "1 1", + outline = "#303030", + }, + children = {{ + tag = "VerticalLayout", + attributes = { + spacing = "4", + childForceExpandHeight = "false", + childForceExpandWidth = "true", + }, + children = rows, + }}, + } + + local tree = UI.getXmlTable() or {} + -- Remove any stale macModePanel before reinserting the fresh one. + for i = #tree, 1, -1 do + if tree[i].attributes and tree[i].attributes.id == "macModePanel" then + table.remove(tree, i) + end + end + table.insert(tree, panel) + + -- Inject a "Mac Patch" button into the bottom-right legionFloatingMenu + -- (replaces the first interactable=false placeholder button). + local menu = macFindNodeById(tree, "legionFloatingMenu") + if menu and menu.children then + local hasMine = false + for _, c in ipairs(menu.children) do + if c.attributes and c.attributes.id == "macModeMenuButton" then + hasMine = true; break + end + end + if not hasMine then + for i, c in ipairs(menu.children) do + if c.attributes and c.attributes.interactable == "false" then + menu.children[i] = { + tag = "Button", + attributes = { + id = "macModeMenuButton", + fontSize = "10", + onClick = "macModeToggleVisibility", + tooltip = "Toggle Mac TTS U6 Patch panel", + }, + value = "Mac Patch", + } + break + end + end + end + end + + UI.setXmlTable(tree) +end + +function macDeploymentClick(_, _, _) + gToggleDeploymentMode() +end + +-- Defer one frame: TTS throws a UTF-8 byte-buffer encoding error if we +-- rebuild the UI XML synchronously inside a click handler / seat change. +-- pcall guards against any residual race. Promoted to a global function +-- so macModeClick / macModeToggleVisibility / gToggleDeploymentMode (all +-- defined before this point) can call it via name lookup at call time. +function macDeferRefresh() + Wait.frames(function() pcall(macRefreshModeUI) end, 1) +end +function onPlayerChangeColor(_) macDeferRefresh() end +function onPlayerConnect(_) macDeferRefresh() end +function onPlayerDisconnect(_) macDeferRefresh() end + +-- Initial UI build, deferred + pcalled like the rest. +Wait.time(function() pcall(macRefreshModeUI) end, 2) + +-- Override hotkey init functions to capture playerColor and route through +-- the per-seat trigger. Defining initCohesionHotkeys/initRangebandHotkeys +-- here SHADOWS the originals - when the original onLoad runs init*(), our +-- versions register the hotkey instead. + +function initCohesionHotkeys() + addHotkey("Show Cohesion On Hovered Model", + function(playerColor, hoverObject, _) + if not hoverObject or not hoverObject.interactable then return end + gCohesionTrigger({ + figGUID = hoverObject.getGUID(), + playerColor = playerColor, + }) + end) +end + +function initRangebandHotkeys() + addHotkey("Show Range On Hovered Model", + function(playerColor, hoverObject, _) + if not hoverObject or not hoverObject.interactable then return end + gRangeTrigger({ + figGUID = hoverObject.getGUID(), + playerColor = playerColor, + forceFigMode = true, -- hotkey always shows 6-band fig overlay + }) + end) +end + +-- The vanilla showRangeOnHoveredModel calls spawnRangeRuler(fig) which spawns +-- the Range AssetBundle directly. On Mac, the "Oblong Range" shader used by +-- long/snail bundles isn't supported (magenta). Override the global function +-- to route through gToggleRange so our stadium overlay renders instead. +-- Vanilla Range bundle still works for round bases via gWindowsRangeToggle +-- when the player explicitly chose Windows mode. +function showRangeOnHoveredModel(hoverObject) + if not hoverObject or not hoverObject.interactable then return end + gToggleRange({figGUID = hoverObject.getGUID(), forceFigMode = true}) +end + +-- Note: we deliberately do NOT override the global spawnRangeRuler / +-- clearRangeRulers / showRangeOnHoveredModel chain at the Lua scope where +-- the vanilla mod defines it. Overriding spawnRangeRuler from a token +-- script reproducibly SIGSEGV'd TTS on Mac in May 2026 (Mono GC stack +-- corruption). Mac routing happens via the g* functions called from our +-- own hotkey/button wrappers; the originals stay reachable for +-- Windows-mode players via spawnRangeRulerOriginalGlobal aliases above. + +-- END MAC COHESION FALLBACK +""" + + +# ------------------------------------------------------------ +# 2) Wrapper appended AFTER the original Cohesion include (per-seat router) +# ------------------------------------------------------------ +# Strategy: keep the ORIGINAL Cohesion/RangeRulers expansion intact, then +# append wrappers that alias originals + route through the Global router. +# This preserves the original Custom_AssetBundle Projector spawn for +# Windows-mode players (called back as spawnCohesionRulerOriginal). + +COHESION_WRAPPER = r""" +-- MAC PATCH per-seat router (Cohesion) +-- Aliases the original spawn so Windows-mode players still get their bundle. +spawnCohesionRulerOriginal = spawnCohesionRuler +clearCohesionRulerOriginal = clearCohesionRuler + +function spawnCohesionRuler(cohesionSourceObject) + if not cohesionSourceObject then return end + Global.call("gCohesionTrigger", { + figGUID = cohesionSourceObject.getGUID(), + playerColor = nil, + }) +end + +function clearCohesionRuler() + if self and self.getGUID and self.getGUID() ~= "-1" then + Global.call("gClearCohesion", { figGUID = self.getGUID() }) + end + if cohesionRuler ~= nil then + pcall(clearCohesionRulerOriginal) + end +end + +-- Vanilla Order Token's clearTemplates() calls clearCohesionRulers (plural), +-- but the save was built from an older mod version that ships only the +-- singular clearCohesionRuler. Provide the plural as a delegating stub so +-- standby/clearTemplates/etc. don't nil-error on click. +function clearCohesionRulers() + if selectedUnitObj then + pcall(function() selectedUnitObj.setVar("moveState", false) end) + pcall(function() selectedUnitObj.call("clearCohesionRuler") end) + end +end + +-- Same root cause as clearCohesionRulers: newer Order Token code paths in +-- the live save (e.g. moveUnit -> stopAttack) call helpers the older bundled +-- script doesn't ship. Stub them so MOVE/AIM/etc don't nil-error. The bodies +-- mirror the current vanilla source but pcall every dependency so we don't +-- chain-error if anything else is also missing. +function clearAttackLine() + if attackLine then + for k, v in pairs(attackLine) do + pcall(destroyObject, v) + end + attackLine = nil + end +end + +function exitTargetingMode() + enemyHighlighted = false + attackModeOn = false + pcall(clearRangeRulers) + pcall(unhighlightEnemies) + pcall(clearAttackLine) +end + +function exitAttackMode() + enemyHighlighted = false + attackModeOn = false + pcall(clearRangeRulers) + pcall(unhighlightEnemies) +end +-- END MAC PATCH per-seat router (Cohesion) +""" + + +COHESION_BLOCK_RE = re.compile( + r"----#include !/Cohesion\r?\n.*?----#include !/Cohesion\r?\n", + re.DOTALL +) + +RANGE_BLOCK_RE = re.compile( + r"----#include !/RangeRulers\r?\n.*?----#include !/RangeRulers\r?\n", + re.DOTALL +) + +# Maximum Move spawn block in Order_Token (a57c41), inline (not an include). +# Captures from `local maxMoveBundles = getMovementLinks()` up to and +# including the two closing ends of the original `if isDeploy / if ... ~= nil` +# scaffold. The replacement is self-contained (opens 3 ifs, closes all 3), +# so re-applying yields the same canonical block - idempotent. (Earlier +# versions only matched up to setName("Maximum Move") and re-matched their +# own output, accumulating orphan else/end copies on each re-run.) +MAXMOVE_SPAWN_RE = re.compile( + r"local maxMoveBundles = getMovementLinks\(\).*?" + r"\r?\n end\r?\n end\r?\n", + re.DOTALL +) + +MAXMOVE_SPAWN_REPLACEMENT = r"""local maxMoveBundles = getMovementLinks() + local baseSizeMoveBundles = maxMoveBundles[unitData.baseSize] + local maxMoveTemplateBundleToSpawn = baseSizeMoveBundles and baseSizeMoveBundles[unitData.selectedSpeed] + + -- MAC PATCH: permissive condition (covers nil + false) so the overlay + -- also fires when changeSpeed2/3 calls moveUnit() with no isDeploy arg. + if isDeploy ~= true then + if maxMoveTemplateBundleToSpawn ~= nil then + local _macMode = Global.call("gGetMode", {color = macActivePlayerForMove}) + if _macMode == "windows" then + -- WINDOWS ORIGINAL: spawn Custom_AssetBundle Projector + maxMoveTemplate = spawnObject({ + type = "Custom_AssetBundle", + position = {basePos.x, basePos.y + 20, basePos.z}, + rotation = {0, basePos.y, 0}, + scale = {0,0,0} + }) + maxMoveTemplate.setCustomObject({ + type = 0, + assetbundle = maxMoveTemplateBundleToSpawn + }) + maxMoveTemplate.setLock(true) + maxMoveTemplate.use_gravity = false + maxMoveTemplate.setName("Maximum Move") + else + -- MAC FALLBACK: route through the Global Overlays manager + Global.call("gSpawnMaxMove", { + figGUID = selectedUnitObj.getGUID(), + baseSize = unitData.baseSize, + speed = unitData.selectedSpeed, + }) + maxMoveTemplate = nil + end + end + end +""" + +# clearMovementTemplates function: needs to also clear our managed maxmove +# overlay. We replace the whole function body. +# Match up to the first un-indented `end` (= the function's closing end), so +# the regex is idempotent - it matches both the vanilla body and any +# previously patched body shape (which has extra lines past the inner `end`). +CLEAR_MOVEMENT_RE = re.compile( + r"function clearMovementTemplates\(\).*?\r?\nend\b", + re.DOTALL +) + +CLEAR_MOVEMENT_REPLACEMENT = r"""function clearMovementTemplates() + if templateA ~= nil then + destroyObject(templateA) + end + if templateB ~= nil then + destroyObject(templateB) + end + -- MAC PATCH per-seat: destroy bundle Object if present (Windows-mode + -- spawn) AND clear the manager entry (Mac fallback). Either may be set. + if maxMoveTemplate ~= nil then + pcall(destroyObject, maxMoveTemplate) + end + -- Clear only THIS token's MaxMove (keyed by the attached unit), not + -- everyone's - gClearAllMaxMove was killing other tokens' rings when + -- two players had MOVE open at the same time. + if selectedUnitObj then + Global.call("gClearMaxMove", { figGUID = selectedUnitObj.getGUID() }) + end + maxMoveTemplate = nil +end""" + +# Deployment Boundary: replace the Custom_AssetBundle Projector spawn inside +# spawnBoundaryCell (SETUP_CONTROLLER 1cb552) with a Global.call to the manager. +# Idempotent: matches both the vanilla form AND a previously-injected patched +# form (between the MAC PATCH dual-path marker and the closing ` end`). The +# patched alternative is listed first so re-runs prefer it. +DEPLOYMENT_SPAWN_RE = re.compile( + r"(?:" + r"-- MAC PATCH dual-path:.*?\r?\n end" + r"|" + r"local projector = spawnObject\(\{\r?\n" + r" type = \"Custom_AssetBundle\",.*?" + r"projector\.setCustomObject\(\{\r?\n" + r" assetbundle = asset,\r?\n" + r" \}\)" + r")", + re.DOTALL +) + +DEPLOYMENT_SPAWN_REPLACEMENT = r"""-- MAC PATCH dual-path: branch on the table-wide deploymentMode toggle. + local _macDeployMode = Global.call("gGetDeploymentMode") + local projector = nil + if _macDeployMode == "windows" then + projector = spawnObject({ + type = "Custom_AssetBundle", + position = pos, + scale = {0, 0, 0}, + rotation = {0, deployRotations[cell], 0} + }) + projector.setName("Deployment Boundary") + projector.setLock(true) + projector.setCustomObject({ + assetbundle = asset, + }) + else + Global.call("gSpawnDeployment", { cell = cell, pos = pos }) + end""" + +# clearDeploymentBoundary: also clear manager entries. +CLEAR_DEPLOYMENT_RE = re.compile( + r"function clearDeploymentBoundary\(\)\r?\n" + r" local battlefieldObjs = battlefieldZone\.getObjects\(\).*?\r?\n" + r" end\r?\nend", + re.DOTALL +) + +CLEAR_DEPLOYMENT_REPLACEMENT = r"""function clearDeploymentBoundary() + -- MAC PATCH: clear Global Overlays manager entries first. + Global.call("gClearAllDeployment", {}) + local battlefieldObjs = battlefieldZone.getObjects() + for _, obj in pairs(battlefieldObjs) do + if obj.getName() == "Deployment Boundary" then + destroyObject(obj) + end + end +end""" + +# Silhouette URL swap to our Unity-6-compiled bundle. +# +# The upstream BucketheadBits_Silhouette bundle uses Allen White's custom +# Silhouette shader compiled under Unity 2019.4. TTS U6 on Mac fails to +# load that shader at runtime (console: "Shader didn't load correctly for +# AssetBundle material BucketheadBits_Silhouette. Assigning Standard +# shader.") and falls back to Standard, which renders the cylinder opaque. +# +# Our fork-hosted bundle uses the SAME prefab + material + shader source +# (Assets/Sihl/), only the compile target differs (Unity 6 instead of +# Unity 2019.4). Path A from Dicewrench's analysis. +# +# We swap ALL silhouette URLs (default cylinder, snail, long) to the same +# fork URL. Snail/long visual fidelity is acceptable for now (cylinder +# placeholder); per-base meshes are a separate follow-up. +SILHOUETTE_BUNDLE_URL = ( + "https://raw.githubusercontent.com/ironsquadronfr-hub/tts/" + "mac-projector-fallback/mod/data/mac-fallback-assets/" + "silhouette_mac_fallback.unity3d" +) +SILHOUETTE_URL_RE = re.compile( + r'(silhouetteData\s*=\s*)"https?://[^"]*/ugc/[^"]+"' +) +SILHOUETTE_URL_REPLACEMENT = r'\1"' + SILHOUETTE_BUNDLE_URL + '"' + + +# Cleanup of debug prints and corrupted signatures from earlier sessions. +# Safe no-op on a clean save. Matches both the legacy `print("[SIL DEBUG] ...)` +# form AND the chat-visible `printToAll("[SIL] ...", {...})` form. +SIL_DEBUG_STRIP_RE = re.compile( + # Match leading horizontal whitespace only (NOT \s* which would also eat + # the newline before, mashing the function signature against the next + # statement - that was a real bug we hit earlier this session). + r'[ \t]*(?:print\("\[SIL DEBUG\][^\n]*\)|' + r'printToAll\("\[SIL\][^"]*",\s*\{[^}]*\}\))\r?\n' +) +# Repair function signatures that were mashed by the previous \s*-glob bug. +# Idempotent: target the specific mashed patterns; matches are zero on a +# clean save. +SIL_REPAIR_TOGGLE_RE = re.compile( + r'(function toggleSilhouettes\(\))( if silhouetteState then)' +) +SIL_REPAIR_SHOW_RE = re.compile( + r'(function showSilhouette\(\))( for k, guid in pairs\(miniGUIDs)' +) +SIL_REPAIR_SPAWN_RE = re.compile( + r'(function spawnSilhouette\(obj, pos, rot\))( local globals)' +) + + +# Silhouette state-desync fix (upstream bug). +# +# Upstream clearSilhouette assumes removeAttachments()[1] always returns an +# object, but the state variable `silhouetteState` is persisted across saves +# while the physical attachments are not - so a save made with silhouettes +# visible reloads with state=true but no attachments. First SIL click then +# calls clearSilhouette which dereferences nil and crashes. +# +# Two-part patch: +# (1) clearSilhouette: guard the nil case (skip the destruct if no +# attachment), so re-clicks don't crash. +# (2) onload: force silhouetteState=false on every load, since the load +# cannot restore the physical silhouettes anyway. +# +# Also fixes the "two silhouettes stacked" symptom: that happens when +# showSilhouette runs while state is desynced, spawning a second set on top +# of one whose handles have been forgotten. +CLEAR_SILHOUETTE_RE = re.compile( + r"function clearSilhouette\(\).*?silhouetteState = false\r?\nend", + re.DOTALL +) +CLEAR_SILHOUETTE_REPLACEMENT = r"""function clearSilhouette() + -- MAC PATCH: guard nil silToDestroy. silhouetteState can be persisted as + -- true across saves while the physical attachments are not, so a load + -- with state=true + no attachments would otherwise crash here. + for k, guid in pairs(miniGUIDs or {}) do + local obj = getObjectFromGUID(guid) + if obj then + local silToDestroy = obj.removeAttachments()[1] + if silToDestroy then silToDestroy.destruct() end + end + end + silhouetteState = false +end""" + +# Match the existing `function onload()` body (one-line empty or actual +# code). Idempotent: re-runs detect the marker `MAC PATCH: silhouette reset` +# and skip. +SIL_ONLOAD_RESET_LINE = ( + ' -- MAC PATCH: silhouette reset on load (attachments lost across saves)\n' + ' silhouetteState = false\n' +) +ONLOAD_OPEN_RE = re.compile(r"function onload\(\)\r?\n") + + +# SIL button rename (root cause: TTS engine intercepts the click_function +# name "toggleSilhouettes" silently in Mac mode; LCK/R buttons unaffected +# because their click_function names differ. Confirmed empirically 17 mai +# 2026: renaming to "macToggleSil" + forwarder unblocks Mac mode entirely). +# +# We swap the click_function name on the SIL button definition AND inject a +# forwarder function that simply calls toggleSilhouettes(). The original +# toggleSilhouettes is untouched, so Windows mode behavior is identical. +# Idempotent: re-runs detect both markers and skip. +SIL_BUTTON_CLICKFN_OLD = 'click_function = "toggleSilhouettes"' +SIL_BUTTON_CLICKFN_NEW = 'click_function = "macToggleSil"' +SIL_FORWARDER = ''' +-- MAC PATCH: rebuilt SIL button click handler. The original click_function +-- name "toggleSilhouettes" is silently intercepted by TTS in Mac mode (the +-- SIL button plays its click sound/animation but the function is never +-- called). Renaming the click_function on the button definition + adding +-- this forwarder restores the routing. +function macToggleSil() + toggleSilhouettes() +end +-- END MAC PATCH SIL forwarder +''' +SIL_FORWARDER_MARKER = 'function macToggleSil()' + + +RANGE_WRAPPER = r""" +-- MAC PATCH per-seat router (Range) - ALIAS ONLY. +-- +-- HISTORY (do not re-introduce): we previously overrode spawnRangeRuler / +-- clearRangeRulers / clearRangeRuler on the ~20 token scripts. That +-- reproducibly SIGSEGV'd TTS Mac (Mono GC_clear_stack_inner) when a fig +-- hotkey spawned the vanilla Range bundle. Overrides REMOVED - per-seat +-- Range routing for fig hotkey lives in Global (gRangeTrigger via our +-- initRangebandHotkeys shadow). Tokens' R buttons keep vanilla behavior +-- (magenta on Mac, native on Windows) until a per-token Mac fallback +-- ships - see TOKEN_BUTTON_WRAPPER for the per-token override path. +-- +-- The single alias below is REQUIRED: Global macModeClick calls +-- obj.call("clearRangeRulersOriginal", obj) on every object holding a +-- rangeRuler state, to wipe the vanilla ruler when toggling modes. +clearRangeRulersOriginal = clearRangeRulers +-- END MAC PATCH per-seat router (Range) +""" + +# Order_Token-only overrides: intercept the UI button click handlers +# (toggleCohesionRuler, targetingMode, changeSpeedN, moveX) to capture +# playerColor passed by TTS for per-seat routing. +ORDER_TOKEN_BUTTON_OVERRIDES = r""" +-- MAC PATCH per-seat router (Order_Token button click overrides) +function toggleCohesionRuler(_, playerColor) + if not rulerOn then + Global.call("gCohesionTrigger", { + figGUID = selectedUnitObj.getGUID(), + playerColor = playerColor, + }) + rulerOn = true + else + selectedUnitObj.call("clearCohesionRuler") + rulerOn = false + end +end + +function targetingMode(_, playerColor) + if not enemyHighlighted then + exitAttackMode() + highlightEnemies() + Global.call("gRangeTrigger", { + figGUID = selectedUnitObj.getGUID(), + playerColor = playerColor, + }) + enemyHighlighted = true + resetRangeButtons() + else + exitTargetingMode() + end +end + +-- Capture playerColor for Maximum Move per-seat routing. moveUnit() doesn't +-- receive the click color, so each entry-point button stashes it in a +-- script-global var that the spawn block reads. Defaults to nil -> Mac mode. + +-- initMove / initDeploy: redefine the body INLINE rather than wrap, because +-- wrapping (capturing the original via a local + calling it) reproducibly +-- broke every Order Token button click in testing (root cause unknown; +-- suspected Lua chunk-level upvalue interaction with TTS engine click +-- dispatch). The inline body mirrors the vanilla logic + the playerColor +-- capture so the MAXMOVE_SPAWN block can route to Mac/Windows per seat. +function initMove(obj, playerColor) + if not selectedUnitObj then return end + macActivePlayerForMove = playerColor + initPos = selectedUnitObj.getPosition() + initRot = selectedUnitObj.getRotation() + selectedUnitObj.call("setStartPos") + moveUnit(false) +end +function initDeploy(obj, playerColor) + if not selectedUnitObj then return end + macActivePlayerForMove = playerColor + initPos = selectedUnitObj.getPosition() + initRot = selectedUnitObj.getRotation() + selectedUnitObj.call("setStartPos") + moveUnit(true) +end + +-- INLINE the vanilla bodies (mirror Order_Token.a57c41.lua) rather than +-- wrap. The List Builder copies this block to Command Token Custom_Models +-- which DO NOT carry the vanilla Order_Token function bodies - so a +-- wrap-then-call pattern hits `_macOrigChangeSpeed1 = nil` and crashes the +-- moment a Speed/Move button is pressed. Inlining keeps the behavior +-- identical on both Order Tokens (vanilla body re-defined) and Command +-- Tokens (helper fns setTemplateVariables/clearTemplates/moveUnit exist +-- on both because they're part of the include set the List Builder emits). +function changeSpeed1(_, playerColor) + macActivePlayerForMove = playerColor + unitData.selectedSpeed = 1 + setTemplateVariables() + clearTemplates() + moveUnit() +end +function changeSpeed2(_, playerColor) + macActivePlayerForMove = playerColor + unitData.selectedSpeed = 2 + setTemplateVariables() + clearTemplates() + moveUnit() +end +function changeSpeed3(_, playerColor) + macActivePlayerForMove = playerColor + unitData.selectedSpeed = 3 + setTemplateVariables() + clearTemplates() + moveUnit() +end +function moveForward(_, playerColor) + macActivePlayerForMove = playerColor + self.editButton({ + index = 11, click_function = "moveBackwards", + label = "B", tooltip = "Move Backwards" + }) + moveDirection = "forward" + moveUnit() +end +function moveBackwards(_, playerColor) + macActivePlayerForMove = playerColor + self.editButton({ + index = 11, click_function = "moveForward", + label = "F", tooltip = "Move Forward" + }) + moveDirection = "backwards" + moveUnit() +end +function moveLeft(_, playerColor) + macActivePlayerForMove = playerColor + moveDirection = "left" + moveUnit() +end +function moveRight(_, playerColor) + macActivePlayerForMove = playerColor + moveDirection = "right" + moveUnit() +end +-- END MAC PATCH per-seat router (Order_Token button click overrides) +""" + +# Wrapper appended to objects that have `function toggleRangeRuler()` - +# the R button on tokens (POI, Smoke, Bomb Cart, Objective). Per-seat +# routing: Mac players get our vector bands (TOKEN-spec via rangeKey), +# Windows players get the vanilla token bundle (spawnTokenRangeRuler). +# The vanilla function is aliased BEFORE override so Windows-mode clicks +# can fall through to it. +TOKEN_BUTTON_WRAPPER = r""" +-- MAC PATCH per-seat router (token R button) +-- Aliases the vanilla toggleRangeRuler before overriding so Windows-mode +-- players keep spawning the token-specific Range bundle (POI / Smoke / +-- bomb_cart / objective). Mac-mode players get our vector overlay via +-- gRangeTrigger which dispatches to macBuilders.range with the token's +-- rangeKey var. +local _macToggleRangeRulerOriginal = toggleRangeRuler +function toggleRangeRuler(_, playerColor) + local mode = Global.call("gGetMode", { color = playerColor }) + if mode == "windows" then + if _macToggleRangeRulerOriginal then + return _macToggleRangeRulerOriginal() + end + return + end + if rangeOn then + Global.call("gClearRange", { figGUID = self.getGUID() }) + rangeOn = false + else + Global.call("gRangeTrigger", { + figGUID = self.getGUID(), + playerColor = playerColor, + }) + rangeOn = true + end +end +-- END MAC PATCH per-seat router (token R button) +""" + +TOKEN_BUTTON_WRAPPER_RE = re.compile( + r"\r?\n-- MAC PATCH per-seat router \(token R button\).*?" + r"-- END MAC PATCH per-seat router \(token R button\)\r?\n", + re.DOTALL +) + +# List Builder _loadArmyFromJson nil-guard. Stock SWL never nil-checks the +# result of JSON.decode(text) inside importFromText, so any blur of the +# import InputField with empty/invalid text crashes _loadArmyFromJson at +# `data.armyFaction`. The guard is upstream-able. Idempotent via the +# negative lookahead on the marker comment. +LIST_BUILDER_GUARD_RE = re.compile( + r"(function _loadArmyFromJson\(data\)\r?\n)(?! -- MAC PATCH: guard )" +) +LIST_BUILDER_GUARD_REPLACEMENT = ( + r"\1" + " -- MAC PATCH: guard nil/invalid JSON from onEndEdit blur (upstream bug)\r\n" + " if not data then return end\r\n" +) + +# Idempotent marker - used to detect and remove an existing Mac patch in the +# Global LuaScript before applying a fresh one. +GLOBAL_PATCH_MARKER_RE = re.compile( + r"\r?\n-- ={5,}\r?\n-- MAC COHESION FALLBACK.*?-- END MAC COHESION FALLBACK\r?\n", + re.DOTALL +) + +# Idempotent markers for the per-seat wrappers added to object scripts. +# Strip these before re-applying to avoid double-wrapping. +COHESION_WRAPPER_RE = re.compile( + r"\r?\n-- MAC PATCH per-seat router \(Cohesion\).*?" + r"-- END MAC PATCH per-seat router \(Cohesion\)\r?\n", + re.DOTALL +) +RANGE_WRAPPER_RE = re.compile( + r"\r?\n-- MAC PATCH per-seat router \(Range\).*?" + r"-- END MAC PATCH per-seat router \(Range\)\r?\n", + re.DOTALL +) +ORDER_TOKEN_OVERRIDES_RE = re.compile( + r"\r?\n-- MAC PATCH per-seat router \(Order_Token button click overrides\).*?" + r"-- END MAC PATCH per-seat router \(Order_Token button click overrides\)\r?\n", + re.DOTALL +) + + +TARGET_GUIDS = {"99f1c8", "a57c41"} # Objects with the Cohesion block to replace + + +def collect_patched_scripts(data: dict) -> list: + """Return script_states for the TTS External Editor API (Save & Play). + + Includes the Global script and any object script we patched (matching + TARGET_GUIDS). Format: list of dicts with name, guid, script. + """ + states = [ + {"name": "Global", "guid": "-1", "script": data.get("LuaScript", "")} + ] + + def walk(o): + if isinstance(o, dict): + guid = o.get("GUID", "").lower() + if guid in TARGET_GUIDS and "LuaScript" in o: + states.append({ + "name": o.get("Name", guid), + "guid": guid, + "script": o["LuaScript"], + }) + for v in o.values(): + walk(v) + elif isinstance(o, list): + for x in o: + walk(x) + + walk(data) + return states + + +def send_to_tts(script_states: list) -> bool: + """Send a Save & Play (messageID 1) to TTS via the External Editor API. + + Returns True on success, False if TTS isn't listening (likely the API + is disabled in TTS Configuration). + """ + msg = {"messageID": 1, "scriptStates": script_states} + payload = json.dumps(msg).encode("utf-8") + try: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.settimeout(3) + s.connect(("127.0.0.1", 39999)) + s.sendall(payload) + s.close() + return True + except (ConnectionRefusedError, socket.timeout, OSError) as e: + print(f" WARN: could not connect to TTS on port 39999 ({e}).") + print(f" -> Enable Configuration -> Game Tweaks -> 'External Editor API' in TTS.") + return False + + +def patch_object_scripts(data: dict) -> tuple: + """Walk the JSON tree, replace Cohesion and Range blocks in object scripts. + + Cohesion block: only on TARGET_GUIDS (Unit_Leader, Order_Token). + Range block: on every object containing the !/RangeRulers include + (Order_Token, Tokens, POI, bomb_cart, etc. - ~20 objects). + + Returns (n_cohesion_patched, n_range_patched, n_dep_patched). + """ + n_coh, n_rng, n_dep = 0, 0, 0 + + def walk(o): + nonlocal n_coh, n_rng, n_dep + if isinstance(o, dict): + guid = o.get("GUID", "").lower() + if "LuaScript" in o: + ls = o["LuaScript"] + changed = False + + # Strip any previous per-seat wrappers so re-runs are idempotent. + for rx in (COHESION_WRAPPER_RE, RANGE_WRAPPER_RE, + ORDER_TOKEN_OVERRIDES_RE, TOKEN_BUTTON_WRAPPER_RE): + new_ls, n = rx.subn("", ls) + if n > 0: + ls = new_ls + changed = True + + # Cleanup: strip stale debug prints + repair any signatures + # mashed by an earlier (overzealous) strip pass. Both are + # no-ops on a clean save. + new_ls, n = SIL_DEBUG_STRIP_RE.subn("", ls) + if n > 0: + ls = new_ls + changed = True + for rx in (SIL_REPAIR_TOGGLE_RE, SIL_REPAIR_SHOW_RE, SIL_REPAIR_SPAWN_RE): + new_ls, n = rx.subn(r'\1\n\2', ls) + if n > 0: + ls = new_ls + changed = True + + # (Diagnostic SIL prints removed 17 mai 2026 once mute Mac + # mode confirmed. SIL_DEBUG_STRIP_RE above stays as a cleanup + # pass for older saves that still carry the prints.) + + # Silhouette URL swap to our Unity-6-compiled bundle. + # Applies to any object that defines spawnSilhouette + # (Unit_Leader 99f1c8, Bomb Cart b497e1, POI Token 761483). + # Idempotent: the regex only matches Steam UGC URLs (/ugc/), + # not our fork URL, so re-runs are no-ops. + new_ls, n = SILHOUETTE_URL_RE.subn(SILHOUETTE_URL_REPLACEMENT, ls) + if n > 0: + ls = new_ls + changed = True + + # SIL button rename + forwarder. Bypasses the TTS engine + # bug where the click_function name "toggleSilhouettes" is + # silently muted in Mac mode. Idempotent: replace() is no-op + # once swap is done, marker check prevents double-injection. + if SIL_BUTTON_CLICKFN_OLD in ls: + ls = ls.replace(SIL_BUTTON_CLICKFN_OLD, SIL_BUTTON_CLICKFN_NEW) + changed = True + if SIL_BUTTON_CLICKFN_NEW in ls and SIL_FORWARDER_MARKER not in ls: + ls = ls.rstrip() + '\n' + SIL_FORWARDER + changed = True + + # Cohesion: KEEP the original include block, append per-seat + # wrapper after it. Only Unit_Leader / Order_Token have it. + if guid in TARGET_GUIDS: + def coh_repl(m): + return m.group(0) + COHESION_WRAPPER + new_ls, n = COHESION_BLOCK_RE.subn(coh_repl, ls, count=1) + if n > 0: + ls = new_ls + n_coh += 1 + changed = True + + # Silhouette desync fix (any object with clearSilhouette). + # Guard the nil-attachment case and reset silhouetteState in + # onload. Idempotent: the regex matches both the vanilla + # body and our previously-patched body (same shape). + if "function clearSilhouette()" in ls: + new_ls, n = CLEAR_SILHOUETTE_RE.subn(CLEAR_SILHOUETTE_REPLACEMENT, ls, count=1) + if n > 0: + ls = new_ls + changed = True + # Inject the state reset at the top of onload. + if "MAC PATCH: silhouette reset on load" not in ls: + new_ls, n = ONLOAD_OPEN_RE.subn( + "function onload()\n" + SIL_ONLOAD_RESET_LINE, + ls, count=1, + ) + if n > 0: + ls = new_ls + changed = True + + # Range: KEEP block, append wrapper. ~21 objects. + def rng_repl(m): + return m.group(0) + RANGE_WRAPPER + new_ls, n = RANGE_BLOCK_RE.subn(rng_repl, ls, count=1) + if n > 0: + ls = new_ls + n_rng += 1 + changed = True + + # List Builder import guard. Applies to BLUE/RED LIST + # BUILDER (any object that defines _loadArmyFromJson). + # Fixes a pre-existing upstream crash where blurring the + # import InputField with empty/invalid text fires + # onEndEdit -> importFromText -> JSON.decode("") -> nil -> + # data.armyFaction crash. Idempotent. + if "function _loadArmyFromJson(data)" in ls: + new_ls, n = LIST_BUILDER_GUARD_RE.subn( + LIST_BUILDER_GUARD_REPLACEMENT, ls, count=1 + ) + if n > 0: + ls = new_ls + changed = True + + # Maximum Move spawn (only in Order_Token a57c41) - Mac only + if guid == "a57c41": + new_ls, n = MAXMOVE_SPAWN_RE.subn(MAXMOVE_SPAWN_REPLACEMENT, ls, count=1) + if n > 0: + ls = new_ls + changed = True + new_ls, n = CLEAR_MOVEMENT_RE.subn(CLEAR_MOVEMENT_REPLACEMENT, ls, count=1) + if n > 0: + ls = new_ls + changed = True + # Button click overrides (per-seat capture of playerColor) + ls = ls + ORDER_TOKEN_BUTTON_OVERRIDES + changed = True + + # Token R button: append toggleRangeRuler override on any + # object that defines that function (POI, Smoke, BombCart, + # Objective, etc.). Routes the click to the Mac fallback + # Range overlay sized via TOKEN_BASE_RADIUS. (Already-applied + # case is handled by the strip loop above.) + if "function toggleRangeRuler()" in ls: + ls = ls + TOKEN_BUTTON_WRAPPER + changed = True + + # Deployment Boundary (only in SETUP_CONTROLLER 1cb552) - Mac only + if guid == "1cb552": + new_ls, n = DEPLOYMENT_SPAWN_RE.subn(DEPLOYMENT_SPAWN_REPLACEMENT, ls, count=1) + if n > 0: + ls = new_ls + n_dep += 1 + changed = True + new_ls, n = CLEAR_DEPLOYMENT_RE.subn(CLEAR_DEPLOYMENT_REPLACEMENT, ls, count=1) + if n > 0: + ls = new_ls + changed = True + + if changed: + o["LuaScript"] = ls + + for v in o.values(): + walk(v) + elif isinstance(o, list): + for x in o: + walk(x) + + walk(data) + return n_coh, n_rng, n_dep + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("--")] + flags = [a for a in sys.argv[1:] if a.startswith("--")] + if len(args) != 2: + print(f"Usage: {sys.argv[0]} [--reload]") + sys.exit(1) + src = Path(args[0]) + dst = Path(args[1]) + reload_tts = "--reload" in flags + + with src.open("r") as f: + data = json.load(f) + + # Rename so the patched save is recognizable in TTS load list. + data["SaveName"] = "SWL BETA - MAC PATCH" + + # 1. Append manager + handlers to Global LuaScript (idempotent: remove + # any previous Mac patch block first, then append fresh). + original_global_len = len(data.get("LuaScript", "")) + existing = data.get("LuaScript", "") + cleaned, n_removed = GLOBAL_PATCH_MARKER_RE.subn("", existing) + data["LuaScript"] = cleaned + GLOBAL_PATCH_LUA + new_global_len = len(data["LuaScript"]) + note = " (replaced existing patch)" if n_removed else "" + print(f"Global LuaScript: {original_global_len} -> {new_global_len} bytes{note}") + print() + + # 2. Replace Cohesion + Range + Deployment blocks in object scripts. + print("Object script patches:") + n_coh, n_rng, n_dep = patch_object_scripts(data) + print(f" Cohesion blocks replaced: {n_coh}") + print(f" Range blocks replaced: {n_rng}") + print(f" Deployment blocks replaced: {n_dep}") + print() + + dst.parent.mkdir(parents=True, exist_ok=True) + with dst.open("w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + print(f"Written: {dst}") + print(f" Save Name: {data.get('SaveName', '?').strip()}") + print(f" Version: {data.get('VersionNumber', '?')}") + + # Mirror to TTS Saves folder so it appears in Games -> Save & Load. + # Picks the next free TS_Save_N.json slot (TTS scans these on load). + saves_dir = Path.home() / "Library" / "Tabletop Simulator" / "Saves" + if saves_dir.exists(): + existing = sorted( + int(p.stem.split("_")[-1]) + for p in saves_dir.glob("TS_Save_*.json") + if p.stem.split("_")[-1].isdigit() + ) + # Reuse the highest existing slot if its content matches the prefix + # "SWL BETA - MAC PATCH"; else pick next free number. + target = None + for n in reversed(existing): + p = saves_dir / f"TS_Save_{n}.json" + try: + with p.open("r") as f: + if '"SWL BETA - MAC PATCH"' in f.read(512): + target = p + break + except OSError: + continue + if target is None: + next_n = (existing[-1] if existing else 0) + 1 + target = saves_dir / f"TS_Save_{next_n}.json" + with target.open("w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + print(f" Mirrored to: {target}") + + if reload_tts: + print() + print("Hot-reloading into TTS via External Editor API...") + states = collect_patched_scripts(data) + print(f" Sending {len(states)} script(s): " + + ", ".join(f"{s['name']}({s['guid']})" for s in states)) + if send_to_tts(states): + print(" OK Sent. TTS should restart Lua with the new code " + "(table/objects state preserved).") + + +if __name__ == "__main__": + main() diff --git a/mac-patcher/requirements.txt b/mac-patcher/requirements.txt new file mode 100644 index 00000000..fd6ee21e --- /dev/null +++ b/mac-patcher/requirements.txt @@ -0,0 +1,3 @@ +UnityPy>=1.10 +Pillow>=10.0 +requests>=2.31 diff --git a/mac-patcher/retro-cohesion.md b/mac-patcher/retro-cohesion.md new file mode 100644 index 00000000..a67ea58c --- /dev/null +++ b/mac-patcher/retro-cohesion.md @@ -0,0 +1,170 @@ +# Rétro-ingé Cohesion Ruler + +Rédigé en mode prod, base pour le refactor event-driven vector lines (15 mai 2026, post-finding session 14-15 mai). + +## 1. Définition fonctionnelle (rappel règles SWL) + +Le **Cohesion Ruler** matérialise la **distance de cohésion d'unité** : tous les minis d'une même unité doivent rester à une distance maximale du leader d'unité (sinon malus / dispersion). Le ruler aide à vérifier visuellement cette contrainte pendant le tour. + +Distance de cohésion : **0.5 inch** (1,27 cm) entre les bords des socles, règle officielle SWL. + +## 2. Visuel attendu + +3 tailles, indexées par `baseSize` du leader : + +| Clé | Base size physique | Bundle | +|---|---|---| +| `small` | 27 mm | `https://steamusercontent-a.akamaihd.net/ugc/2482129948496305632/...` | +| `medium` | 50 mm | `https://steamusercontent-a.akamaihd.net/ugc/2482129948496305877/...` | +| `large` | 70 mm | `https://steamusercontent-a.akamaihd.net/ugc/2482129948496305957/...` | + +Rendu côté Windows (TTS Unity 2019.4) : cercle semi-transparent vert/cyan projeté au sol via Unity Projector, centré sur la fig. **À extraire empiriquement** via UnityPy + référence Workshop pour la couleur/alpha exacts (en TODO ci-dessous). + +Comportement Projector : drape sur le relief de la table (rochers, élévations) grâce au Projector legacy. + +## 3. Déclencheurs (qui peut spawn/clear le ruler ?) + +### Spawn + +1. **Hotkey global "Show Cohesion On Hovered Model"** — `StarWarsLegion.lua:611-619` + - Bind via `addHotkey()` côté Global + - Appelle `showCohesionOnHoveredModel(hoverObject)` qui : + - Si déjà selected → clear + reset `selectedUnitObj` + - Sinon → `clearCohesionRuler() + spawnCohesionRuler(hoverObject)` + - **État global** `selectedUnitObj` tracké côté Global (variable du script `StarWarsLegion.lua` via `require('!/Cohesion')`) + +2. **Bouton "COHESION" sur Order Token** — `Order_Token.a57c41.lua:347-352, 369-378` + - Bouton créé pendant l'activation d'une unité + - `toggleCohesionRuler()` toggle via flag `rulerOn` local au Order Token + - Appelle `selectedUnitObj.call("spawnCohesionRuler", selectedUnitObj)` → s'exécute dans le script de la **fig sélectionnée** (Unit_Leader.99f1c8) + +3. **Re-spawn automatique après mouvement** — `Unit_Leader.99f1c8.lua:278-286` + - `dropCoroutine()` attend que `getVelocity().y == 0` (fig au sol) + - Si `moveState == true` (= en cours de mesure de mouvement) → `spawnCohesionRuler(self)` + +### Clear + +1. **Hotkey re-press** — toggle via `showCohesionOnHoveredModel` (idem spawn, second appel) + +2. **Bouton COHESION re-press** — toggle via `toggleCohesionRuler` (idem) + +3. **Bouton invisible "unitID" sur Unit_Leader** — `Unit_Leader.99f1c8.lua:45` + - Bouton au centre de la fig avec label = numéro d'unité, couleur alpha 0.01 (quasi-invisible) + - `click_function = "clearCohesionRuler"` + - C'est l'overlay cliquable du numéro affiché sur chaque fig + +4. **onPickedUp sur Unit_Leader** — `Unit_Leader.99f1c8.lua:274-276` + - **Event-driven déjà existant** : pickup la fig → clear automatique + +5. **`standbyTokens()` global** — `StarWarsLegion.lua:562-571` + - Boucle sur `getAllObjects()` et destroy tout objet nommé `"Cohesion Ruler"` (et "Range Ruler", "Movement Template", "Deployment Boundary") + - Déclenché par... à tracer (probablement reset de partie ou hotkey debug) + +6. **`removeLockedRulers()` GAME_CONTROLLER** — `GAME_CONTROLLER.623b03.lua:148-155` + - Boucle sur `getAllObjects()` et destroy "Cohesion Ruler" / "Range Ruler" + - Appelé par la fonction de reload (`reloadObj` voisine) + +7. **`clearCohesionRulers()` (pluriel) Order Token** — `Order_Token.a57c41.lua:788-793` + - Appelé par `clearTemplates()` lors du reset après mouvement + - `selectedUnitObj.setVar("moveState", false)` + `selectedUnitObj.call("clearCohesionRuler")` + +## 4. Lifecycle complet + +``` +[Spawn] + showCohesionOnHoveredModel(fig) [hotkey hover] + toggleCohesionRuler() [via Order Token bouton] + dropCoroutine() [auto si moveState] + ↓ + spawnCohesionRuler(fig): + 1. unitData = fig.getTable("unitData") + 2. bundleURL = getCohesionLinks()[unitData.baseSize] + 3. spawnObject(Custom_AssetBundle, pos = fig.pos + Y+20, scale = 0, rot = (0, fig.rot.y, 0)) + 4. setCustomObject({type=0, assetbundle=bundleURL}) + 5. setLock(true), use_gravity=false, setName("Cohesion Ruler") + ↓ +[Ruler affiché — STATIQUE, ne suit pas la fig] + Note: pas de onFixedUpdate. Si fig bouge sans pickup (impossible normalement + car les figs sont lockables), le ruler reste à l'ancienne position. + +[Clear] + showCohesionOnHoveredModel(same fig) [hotkey re-press toggle] + toggleCohesionRuler() [re-press toggle] + onPickedUp(fig) [pickup auto] + clearCohesionRuler() [click invisible "unitID" button] + standbyTokens() [global reset] + removeLockedRulers() [reload] + clearCohesionRulers() [via Order Token clearTemplates] + ↓ + clearCohesionRuler(): + 1. destroyObject(cohesionRuler) + 2. cohesionRuler = nil + +[Re-spawn auto post-drop] + onDropped → checkVelocity → si moveState → startLuaCoroutine(dropCoroutine) + ↓ + dropCoroutine: + while velocity.y ~= 0: yield + if moveState == true: spawnCohesionRuler(self) +``` + +## 5. État scopé : ATTENTION pattern hétérogène + +`cohesionRuler` est une variable Lua **scopée au script Lua qui fait le require**. Chaque objet qui require `!/Cohesion` a SA propre `cohesionRuler`. Trois scopes coexistent : + +| Site require | Scope `cohesionRuler` | +|---|---| +| `StarWarsLegion.lua:8` (Global) | Script Global | +| `Unit_Leader.99f1c8.lua:1` (chaque fig leader) | Par-fig | +| `Order_Token.a57c41.lua:4` (chaque Order Token) | Par-Order-Token | + +**Conséquence pratique** : +- Hotkey hover → spawn dans le scope Global → `cohesionRuler` du Global mis à jour, **pas celui de la fig** +- Bouton COHESION → `selectedUnitObj.call("spawnCohesionRuler", selectedUnitObj)` → exécute dans le scope de la fig → `cohesionRuler` de la fig mis à jour +- `onPickedUp(fig)` → clear dans le scope de la fig → ne clear PAS un ruler spawn via hotkey hover (mais le bouton invisible "unitID" est sur la fig, donc dans le scope fig...) + +**Bug latent connu (non reproduit)** : si tu spawn via hotkey hover, puis tu pickup une autre fig, le ruler du Global subsiste. À garder en tête pour le refactor (à corriger ou pas selon scope). + +## 6. Filtres de cleanup global + +Deux fonctions scannent `getAllObjects()` et destroy par nom : + +| Fonction | Fichier | Cible | +|---|---|---| +| `standbyTokens()` | StarWarsLegion.lua:562 | "Cohesion Ruler" + "Range Ruler" + "Movement Template" + "Deployment Boundary" | +| `removeLockedRulers()` | GAME_CONTROLLER.623b03.lua:148 | "Cohesion Ruler" + "Range Ruler" | + +**Impact refactor** : si on remplace l'Object Custom_AssetBundle par des vector lines (qui ne sont pas des Objects), ces deux filtres deviennent **caducs**. Il faudra leur substituer un clear de la table d'état globale + `Global.setVectorLines({})`. + +## 7. Paramètres visuels exacts — TODO + +À extraire empiriquement via UnityPy sur les bundles small/medium/large : + +- [ ] Bundle `halfcohesion_27mm.unity3d` (ou équivalent small) : material color, alpha, orthographicSize du Projector, animation éventuelle +- [ ] Bundle medium (50mm) : idem +- [ ] Bundle large (70mm) : idem +- [ ] Screenshot référence côté Workshop (rendu attendu côté Windows TTS 2019.4 / U6 non-magenta) + +Scripts UnityPy disponibles dans `Mod TTS SWL/` (`inspect_cohesion.py`, `inspect_externals.py`). À adapter pour extraire les params materials. + +## 8. Implications pour le refactor + +### Bonnes nouvelles +- API publique petite et conservable : `showCohesionOnHoveredModel`, `spawnCohesionRuler`, `clearCohesionRuler` +- 80% du pattern event-driven déjà en place (`onPickedUp`, `dropCoroutine`) +- Lifecycle bien défini, déclencheurs énumérés exhaustivement + +### Décisions à prendre dans le design doc +1. **Unifier les scopes** : faire que `cohesionRuler` soit une table globale indexée par GUID (Variante A) plutôt que par-script. Résout le bug latent (§5) et permet le pattern multi-figs simultanées. +2. **Remplacer les filtres de cleanup** (§6) par des appels à la fonction de clear globale. +3. **Reproduire le drape sur relief** via `Physics.cast()` par segment, comme le POC validé du 14-15 mai. +4. **Compatibilité Order Token bouton** : conserver la sémantique toggle `rulerOn` ou simplifier. + +(Note : esthétique du rendu vector lines hors specs — on traitera une fois le dossier refactor clos.) + +## 9. Références + +- Code : `swlegion-tts/mod/src/includes/Cohesion.ttslua`, `data/CohesionLinks.ttslua`, `StarWarsLegion.lua`, `StarWarsLegion/Unit_Leader.99f1c8.lua`, `StarWarsLegion/Order_Token.a57c41.lua`, `StarWarsLegion/GAME_CONTROLLER.623b03.lua` +- Bundles : cache TTS local, sources Unity dans `Mod TTS SWL/UnityProject-U6/Assets/` +- Session 14-15 mai validation empirique : `~/.claude/projects/-Users-martinpourrat-MARTIN-Star-Wars-Legion/memory/mod-tts-swl/session-14-mai.md` +- Plan B identifié : `~/.claude/projects/-Users-martinpourrat-MARTIN-Star-Wars-Legion/memory/mod-tts-swl/paths-forward.md` diff --git a/mac-patcher/retro-deployment.md b/mac-patcher/retro-deployment.md new file mode 100644 index 00000000..20d1741e --- /dev/null +++ b/mac-patcher/retro-deployment.md @@ -0,0 +1,76 @@ +# Rétro-ingé Deployment Boundary + +## Visuel attendu + +Zones de déploiement rouge/bleu peintes au sol au début de la partie selon le scenario. Pattern matriciel avec 14 codes de cellule : + +| Code | Sens | +|---|---| +| `r` / `b` | Base red / blue | +| `rh` / `bh` | Home (zone arrière) | +| `rs` / `bs` | Side (zone latérale) | +| `rss` / `bss` | Side stretched | +| `rl` / `bl` | Long (zone allongée, spawn 2 cellules : sX + ccX) | +| `rc` / `bc` | Corner | +| `rcc` / `bcc` | Corner-corner (autre orientation) | + +URLs bundles dans `deployLinks` (`SETUP_CONTROLLER.1cb552.lua:188-202`). + +Plusieurs codes partagent la même URL (e.g. `rh = rs = rss` → même bundle, rotation différente). Distinct rotations : `r=0, rh=0, rs=90, rss=90, ...` (`deployRotations` ligne 204). + +Rendu côté Windows : grand rectangle/zone projeté au sol via Projector. Couleurs rouge/bleu. **À extraire via UnityPy** (task #13). + +## Spawn + +**1 seul déclencheur** : `spawnDeploymentBoundary(matrix)` — `SETUP_CONTROLLER.1cb552.lua:267-314` + +- Reçoit une matrice 12×N (codes par cellule) inversée puis lue ligne par ligne +- Pour chaque cellule non-vide : `spawnBoundaryCell(cell, x, z)` qui spawn 1 Custom_AssetBundle au pos calculé `{xStart + 6*(x-1), yValue, zStart - 6*(z-1)}` + `deployOffset[cell]` +- Cas spécial `bl`/`rl` : spawn 2 cellules (sX + ccX) pour matérialiser le L + +Appelé par `SETUP_CONTROLLER.1cb552.lua:369` (probablement après sélection de la carte Deployment via UI menu). + +## Clear + +**1 seul déclencheur** : `clearDeploymentBoundary()` — ligne 316-323 +- Boucle sur `battlefieldZone.getObjects()` et `destroyObject` chaque objet nommé `"Deployment Boundary"` + +Aussi via : +- **`standbyTokens()`** — `StarWarsLegion.lua:567` — destroy global par nom + +(Pas dans `removeLockedRulers` de GAME_CONTROLLER : Deployment n'est pas dans la liste.) + +## Lifecycle + +``` +[Setup partie] + Menu UI "Mount Deployment" → checkDeployment() → spawnDeploymentBoundary(matrix) + ↓ + Loop sur matrix → pour chaque cell : spawnBoundaryCell + ↓ + Custom_AssetBundle spawn statique (scale 0, locked, no gravity) + +[Clear] + Menu UI "Remove Overlay" → clearDeploymentBoundary() + ↓ + Boucle battlefieldZone → destroyObject par nom + +[Aussi : standbyTokens() global au reset] +``` + +**Pattern : 100% statique.** Pas de suivi, pas d'event. Spawn au setup, clear quand on remove. Plus simple que Cohesion et Range. + +## Différences clés vs Cohesion/Range + +1. **Pas de fig source** — c'est un overlay de zone, pas un overlay attaché à un objet mobile. Pas besoin de drape sur relief si la table est plate (à confirmer : sur certaines tables custom avec relief, est-ce que la zone de déploiement doit draper ? probablement oui pour cohérence visuelle). +2. **Multi-cellules** par scenario — peut-être 10-20 Custom_AssetBundle spawn simultanément (vs 1 pour Cohesion/Range). +3. **Pas de variable d'état scopée** — pas de `deploymentRuler = nil` ; le code itère sur `battlefieldZone.getObjects()` filtré par nom pour le cleanup. + +## Implications refactor + +- API publique à conserver : `spawnDeploymentBoundary(matrix)`, `clearDeploymentBoundary()` +- 14 bundle URLs (mais visuellement ~6 zones distinctes vu les doublons URL) → 6 sets de params visuels à extraire (task #13) +- Pas d'event-driven nécessaire — statique simple +- En vector lines : chaque cellule = un polygone fermé `setVectorLines({{points={p1,p2,p3,p4,p1}, ...}, ...})`. Pas de Physics.cast si table plate, sinon raycast par segment du contour +- Filtre cleanup `standbyTokens` à adapter pour clear la table d'état globale (ou laisser un `Global.setVectorLines({})` de la collection deployment) +- **Décision design** : Deployment partage-t-il la même collection `Global.setVectorLines()` que Cohesion/Range, ou faut-il un canal séparé ? `Global.setVectorLines` est singleton donc tout doit cohabiter dans la même collection. Implication : la table d'état globale doit indexer par type (`cohesion` / `range` / `deployment`) pour gérer les clears partiels. diff --git a/mac-patcher/retro-movement.md b/mac-patcher/retro-movement.md new file mode 100644 index 00000000..579aaca3 --- /dev/null +++ b/mac-patcher/retro-movement.md @@ -0,0 +1,122 @@ +# Rétro-ingé Movement Template + Maximum Move + +**Correction** : Movement implique **2 objets visuels** spawn ensemble lors de l'activation de mouvement. Un seul des deux est cassé magenta. + +## Composant 1 : Movement Template A/B (templates 3D courbes) — NON AFFECTÉ + +Code : `Order_Token.a57c41.lua:480-529` + +```lua +templateA = spawnObject({ type = "Custom_AssetBundle", scale = {1,1,1} ... }) +templateA.setCustomObject({ assetbundle = ..., material = 1 }) +templateA.setColorTint(...) +templateA.setName("Movement Template (A)") +``` + +- **scale = {1,1,1}** → mesh visible (pas un Projector caché) +- **`material = 1`** + **`setColorTint()`** → material standard avec colorTint TTS exposé +- Bundles `longBundle` / `shortBundle` / `sharedBundle` par speed (templateInfo) +- **Pas affecté par bug magenta** : c'est un mesh classique, pas un Projector receiver + +## Composant 2 : Maximum Move (cercle de portée) — CASSÉ MAGENTA ✅ + +Code : `Order_Token.a57c41.lua:552-574` + +```lua +maxMoveTemplate = spawnObject({ + type = "Custom_AssetBundle", + position = {basePos.x, basePos.y + 20, basePos.z}, + scale = {0,0,0} -- ← scale 0, pattern Projector +}) +maxMoveTemplate.setCustomObject({ + type = 0, + assetbundle = maxMoveTemplateBundleToSpawn +}) +maxMoveTemplate.setLock(true) +maxMoveTemplate.use_gravity = false +maxMoveTemplate.setName("Maximum Move") -- ← nom différent de "Movement Template" +``` + +- **scale = {0,0,0}** → Projector legacy (même pattern que Cohesion/Range/Deployment) +- **Pas de setColorTint** sur ce spawn +- **CASSÉ MAGENTA** sur Mac confirmé par Martin + +### Bundles (`includes/data/MovementLinks.ttslua`) + +`getMovementLinks()` retourne table indexée `[baseSize][selectedSpeed]` : + +| baseSize | Taille | Speeds | +|---|---|---| +| `small` | 27mm | 3 bundles (speed 1/2/3) | +| `medium` | 50mm | 3 | +| `large` | 70mm | 3 | +| `huge` | 100mm | 3 | +| `laat` | 120mm | 3 | +| `epic` | 150mm | 3 | +| `long` | 100×175mm oblong | 3 | +| `snail` | 100×200mm oblong | 3 | + +**Total : 24 bundles uniques** pour le cercle de portée max. + +Rendu visuel attendu côté Windows : cercle/anneau projeté au sol matérialisant la portée maximale de mouvement à cette vitesse. **Couleur/alpha à extraire via UnityPy** (task #13). + +## Déclencheurs + +### Spawn +**1 seul déclencheur** : flow d'activation mouvement via Order Token +- Bouton speed (1/2/3) cliqué sur Order Token → set `unitData.selectedSpeed` +- Une fonction de mouvement (autour de `Order_Token.a57c41.lua:480+`) spawn `templateA`, `templateB`, ET `maxMoveTemplate` ensemble +- Conditional : `if isDeploy == false` → pas de Maximum Move pendant la phase de déploiement (ligne 556) + +### Clear +**Fonction `clearMovementTemplates()`** — `Order_Token.a57c41.lua:776-786` +```lua +function clearMovementTemplates() + if templateA ~= nil then destroyObject(templateA) end + if templateB ~= nil then destroyObject(templateB) end + if maxMoveTemplate ~= nil then destroyObject(maxMoveTemplate) end +end +``` + +Appelé par : +- `clearTemplates()` (ligne 770-774) → appelé après le drop final du mouvement +- Aucun cleanup global (Maximum Move PAS dans `standbyTokens()` ni `removeLockedRulers()`) + +## Lifecycle + +``` +[Spawn] + Order Token bouton speed (1/2/3) → spawn flow + ↓ + templateA, templateB (mesh visible) + maxMoveTemplate (Projector) + ↓ + L'utilisateur déplace templateA jusqu'à la position cible + → contrôle visuel "ma fig peut-elle aller là" avec le cercle Maximum Move + +[Clear] + Drop final (fig placée) → clearTemplates() → destruct 3 objets ensemble +``` + +**Pattern : statique au spawn, pas de suivi de fig**. Le cercle Maximum Move est positionné une fois à `basePos + Y20` et reste là pendant que tu manipules les templates de mouvement. + +## État scopé + +`maxMoveTemplate` est une variable par Order Token (`require('!/Cohesion')` ligne 4 mais maxMoveTemplate vient du flow Order_Token directement, pas d'include). Pas de partage Global / Unit_Leader. + +## Implications refactor + +- API publique à conserver : le spawn et `clearMovementTemplates()` (mais cette dernière mélange templates A/B + maxMoveTemplate) +- 24 bundles cercle de portée → 8 sets de params visuels distincts par baseSize (les 3 speeds partagent probablement le même material avec rayon différent ; à extraire task #13) +- Pattern statique simple : spawn une fois, clear ensemble avec templates A/B +- En vector lines : 1 cercle par maxMoveTemplate, dessiné via setVectorLines + Physics.cast par segment pour drape relief +- Cleanup propre : `clearMovementTemplates` doit clear la table d'état globale spécifique au maxMove de cet Order Token +- **Templates A/B (mesh) restent inchangés** — pas dans le scope du refactor + +## Couleur attendue + +Le cercle Maximum Move devrait visuellement matcher la couleur du template de speed correspondant : +- Speed 1 : couleur 1 (vert ?) +- Speed 2 : couleur 2 (jaune ?) +- Speed 3 : couleur 3 (rouge ?) + +Note `templateInfo.moveTemplate[selectedSpeed].colorTint` — c'est le tint des templates A/B, mais le cercle Maximum Move utilise un bundle distinct sans colorTint (le bundle embarque sa propre couleur). À vérifier empiriquement / via UnityPy quelle est la couleur baked-in de chaque bundle Maximum Move par speed. diff --git a/mac-patcher/retro-range.md b/mac-patcher/retro-range.md new file mode 100644 index 00000000..a4569069 --- /dev/null +++ b/mac-patcher/retro-range.md @@ -0,0 +1,86 @@ +# Rétro-ingé Range Ruler + +## Visuel attendu + +12 tailles dans `getRangeRulerLinks()` (`mod/src/includes/data/RangeRulerLinks.ttslua`) : + +| Clé | Cible | Range | +|---|---|---| +| `small` | base 27mm | range mini | +| `medium` | base 50mm | range mini | +| `large` | base 70mm | range mini | +| `huge` | base 100mm | range véhicule | +| `laat` | base 120mm | range LAAT | +| `epic` | base 150mm | range épique | +| `long` | base 100mm oblong | range véhicule long | +| `snail` | base 100x200mm | range escargot | +| `bombCart` | bomb cart | range spécifique | +| `smokeToken` | jeton fumée 18.8mm | range 1 | +| `token` | jeton charge/objectif/condition 25.1mm | range 1 | +| `tokenRangeTwo` | jeton graffiti 25.1mm | range 2 | +| `poi` | POI token 50.8mm | range 0.5 (3") | + +Rendu côté Windows : **4 cercles concentriques** projetés au sol via Unity Projector legacy (1 bundle = N Projectors embarqués pour les bandes de portée 1/2/3/4). Couleurs/alphas/rayons par bande **à extraire via UnityPy** (task #13). + +**Configurations par overlay** : +- Rulers de fig (small/med/large/huge/laat/epic/long/snail) : **4 cercles** (range 1/2/3/4) +- `smokeToken` : 1 cercle (range 1, effet smoke 1 pouce) +- `token` (charge/obj/cond) : 1 cercle (range 1) +- `tokenRangeTwo` (graffiti) : 2 cercles (range 1 + 2) +- `poi` : 1 cercle (range 0.5 = 3") +- `bombCart` : à confirmer empiriquement + +## Déclencheurs + +### Spawn +1. **Hotkey "Show Range On Hovered Model"** — `StarWarsLegion.lua:601-608` → `showRangeOnHoveredModel(hoverObject)` +2. **Bouton "RANGE" sur Order Token** — `Order_Token.a57c41.lua:354-365` → `targetingMode()` ligne 922 → `spawnRangeRuler(selectedUnitObj)` +3. **Bouton "RANGE" via attackMode** — `Order_Token.a57c41.lua:934` → `spawnRangeRuler(selectedUnitObj)` +4. **Bouton "R" sur POI Token** — `POI_Tokens/POI_Token.761483.lua:23,41-46` → `toggleRangeRuler()` → `spawnTokenRangeRuler()` → `spawnRangeRuler(self, tokenRulerBundle)` avec override +5. **Bouton "R" sur autres tokens** — `includes/TokenWithRangeRuler.ttslua:37-43,52-56` (smokeToken, token, tokenRangeTwo via `rangeKey` scopé par-token) + +### Clear +1. **Hotkey re-press** — toggle via `showRangeOnHoveredModel` +2. **`clearRangeRulers()` (pluriel)** — Order Token : appelé par `clearTemplates`, `exitTargetingMode`, `exitAttackMode`, `attackMenu` +3. **`clearRangeRuler()` (singulier)** — POI/Token : appelé par `toggleRangeRuler` et `onDestroy` +4. **`standbyTokens()` global** — `StarWarsLegion.lua:567` — destroy tout objet nommé `"Range Ruler"` +5. **`removeLockedRulers()`** — `GAME_CONTROLLER.623b03.lua:151` — destroy par nom + +## Différence clé vs Cohesion : suivi temps réel + +`RangeRulers.ttslua:78-79` : + +```lua +luaScript = "targetGUID = '"..rangeSourceObject.getGUID().."'\n" + .. "function onFixedUpdate()\n" + .. " if targetGUID ~= nil then\n" + .. " targetObj = getObjectFromGUID(targetGUID)\n" + .. " local targetPosition = targetObj.getPosition()\n" + .. " self.setPosition({targetPosition.x, targetPosition.y + 20, targetPosition.z})\n" + .. " self.setRotation({0,targetObj.getRotation().y,0})\n" + .. " end\n" + .. "end" +rangeRuler.setLuaScript(luaScript) +``` + +→ Le ruler **se repositionne 60 fois/sec** côté objet ruler (pas côté Lua de la fig). Différent de Cohesion qui est statique. + +**Implication refactor** : pour Range, le suivi temps réel est utile gameplay (tu mesures pendant que tu déplaces la fig pour évaluer si tu vas être en range). On ne peut pas simplement passer en pur event-driven sans perte UX. Le drape sur relief recalculé doit suivre — soit redraw à chaque tick visible (avec mémoization de position pour éviter le leak), soit accepter une dégradation UX (redraw au drop seulement). + +## État scopé (même problème que Cohesion §5) + +Variable `rangeRuler` scopée par script-objet (Global, chaque fig leader, chaque POI Token, chaque token générique). Pattern identique à Cohesion : 3+ scopes peuvent désaccorder. + +`selectedUnitObj` global aussi tracké côté Range (`showRangeOnHoveredModel` ligne 7-17 de RangeRulers.ttslua). + +## Override bundle (paramètre `projectorBundleOverride`) + +`spawnRangeRuler(rangeSourceObject, projectorBundleOverride)` — le 2e paramètre permet de fournir une URL bundle au lieu de la lookuper via `unitData.baseSize`. Utilisé par POI Token et TokenWithRangeRuler qui passent `rangeRulerTable[rangeKey]` directement. À conserver dans le refactor. + +## Implications refactor + +- API publique à conserver : `showRangeOnHoveredModel`, `spawnRangeRuler` (avec override), `clearRangeRulers` (Order Token), `clearRangeRuler` (POI/Token) +- 12 bundle URLs → 12 sets de params visuels à extraire (chacun avec **1 à 4 cercles concentriques** selon le type) et reproduire en vector lines (task #13) +- Pattern suivi temps réel → décision design (redraw par tick avec mémoization vs redraw event-driven uniquement) +- Filtres cleanup global (`standbyTokens`, `removeLockedRulers`) à adapter pour la table d'état +- Override `projectorBundleOverride` → s'inscrit naturellement dans une table d'état `{fig=obj, rangeKey="poi"}` ou similaire diff --git a/mac-patcher/scan_bundles.py b/mac-patcher/scan_bundles.py new file mode 100644 index 00000000..eb5edfd2 --- /dev/null +++ b/mac-patcher/scan_bundles.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Scan TTS AssetBundles for material -> shader references.""" +import UnityPy +import os +import sys +from collections import defaultdict, Counter + +BUNDLE_DIR = os.path.expanduser("~/Library/Tabletop Simulator/Mods/Assetbundles/") +STOCK_PREFIXES = ( + "Standard", + "Hidden/", + "Mobile/", + "Particles/", + "UI/", + "Sprites/", + "Skybox/", + "Legacy Shaders/", + "Universal Render Pipeline/", + "URP/", + "Unlit/", + "GUI/", + "Lightmap", + "VertexLit", + "Reflective/", + "Self-Illumin/", + "Transparent/", + "FX/", + "Nature/", + "TextMeshPro/", + "TextMesh Pro/", +) + +def is_stock(name: str) -> bool: + return any(name.startswith(p) for p in STOCK_PREFIXES) + +shader_to_bundles = defaultdict(set) +shader_material_count = Counter() +bundle_to_shaders = defaultdict(set) +embedded_shaders_per_bundle = defaultdict(set) +errors = [] +bundle_summary = [] + +bundles = sorted(f for f in os.listdir(BUNDLE_DIR) if f.endswith(".unity3d")) +print(f"Scanning {len(bundles)} bundles in {BUNDLE_DIR}\n", file=sys.stderr) + +for fname in bundles: + path = os.path.join(BUNDLE_DIR, fname) + short_id = fname.replace("httpssteamusercontentaakamaihdnetugc", "")[:20] + fsize = os.path.getsize(path) + n_mat = 0 + n_shader = 0 + n_tex = 0 + try: + env = UnityPy.load(path) + for obj in env.objects: + t = obj.type.name + if t == "Material": + n_mat += 1 + try: + data = obj.read() + shader_name = None + try: + sref = data.m_Shader + # Try multiple ways to resolve PPtr + try: + shader_obj = sref.deref() + except Exception: + shader_obj = None + if shader_obj is None: + try: + shader_obj_read = sref.read() + if shader_obj_read is not None: + shader_name = ( + getattr(getattr(shader_obj_read, "m_ParsedForm", None), "m_Name", None) + or getattr(shader_obj_read, "m_Name", None) + ) + except Exception: + pass + else: + sd = shader_obj.read() + shader_name = ( + getattr(getattr(sd, "m_ParsedForm", None), "m_Name", None) + or getattr(sd, "m_Name", None) + ) + except Exception as e: + errors.append(f"{short_id} mat shader resolve: {e!r}") + if not shader_name: + # PPtr fields + try: + file_id = getattr(data.m_Shader, "file_id", "?") + path_id = getattr(data.m_Shader, "path_id", "?") + shader_name = f"" + except Exception: + shader_name = "" + # Material name + mat_name = getattr(data, "m_Name", "") or "" + shader_to_bundles[shader_name].add(short_id) + shader_material_count[shader_name] += 1 + bundle_to_shaders[short_id].add((shader_name, mat_name)) + except Exception as e: + errors.append(f"{short_id} mat read err: {e!r}") + elif t == "Shader": + n_shader += 1 + try: + data = obj.read() + sname = ( + getattr(getattr(data, "m_ParsedForm", None), "m_Name", None) + or getattr(data, "m_Name", None) + ) + if sname: + embedded_shaders_per_bundle[short_id].add(sname) + except Exception as e: + errors.append(f"{short_id} shader read: {e!r}") + elif t in ("Texture2D", "Texture"): + n_tex += 1 + except Exception as e: + errors.append(f"{short_id}: load failed: {e!r}") + + bundle_summary.append((short_id, fsize, n_mat, n_shader, n_tex)) + +# === REPORT === +print("=" * 90) +print("BUNDLE SUMMARY (id_prefix, size_bytes, n_materials, n_shaders_embedded, n_textures)") +print("=" * 90) +for short_id, fsize, n_mat, n_shader, n_tex in bundle_summary: + print(f" {short_id} {fsize:>10} mats={n_mat:>3} shaders={n_shader:>2} tex={n_tex:>3}") + +print() +print("=" * 90) +print("SHADER FREQUENCY ACROSS ALL MATERIALS (sorted by usage)") +print("=" * 90) +for sname, count in shader_material_count.most_common(): + flag = " [CUSTOM?]" if not is_stock(sname) and not sname.startswith("4} mats / {n_bundles:>2} bundles {sname}{flag}") + +print() +print("=" * 90) +print("EMBEDDED SHADERS (shaders bundled INSIDE a .unity3d, sorted)") +print("=" * 90) +all_embedded = set() +for shaders in embedded_shaders_per_bundle.values(): + all_embedded.update(shaders) +print(f"\nDistinct embedded shaders: {len(all_embedded)}") +for s in sorted(all_embedded): + bundles_with = [b for b, ss in embedded_shaders_per_bundle.items() if s in ss] + print(f" - {s} ({len(bundles_with)} bundle(s): {','.join(bundles_with[:3])}{'...' if len(bundles_with) > 3 else ''})") + +print() +print("=" * 90) +print("BUNDLES REFERENCING NON-STOCK SHADERS (with material names)") +print("=" * 90) +for short_id, items in sorted(bundle_to_shaders.items()): + custom = [(s, m) for (s, m) in items if not is_stock(s) and not s.startswith("A2>%liFU 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/deployment_red.png b/mod/data/mac-fallback-assets/deployment_red.png new file mode 100644 index 0000000000000000000000000000000000000000..a8e05c5e28d935439f57cf4d2f709049cdbfa337 GIT binary patch literal 856 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJD{c~2L|kcv5PuNX2iDDWKE z&}jGPHlw7BugI5IyNuHtmi)D5_;H*;;mjx+4S^9C0tvh1F7Idd-q+F|3d}JKp00i_ I>zopr0J6>oo&W#< literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/max_move_cyan.png b/mod/data/mac-fallback-assets/max_move_cyan.png new file mode 100644 index 0000000000000000000000000000000000000000..dc017745ade7c63aa858f7608a3ced343b7a245f GIT binary patch literal 4124 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

Bd<4L18 zPhy1_KviTWqb66G4Wh}(ygXz&9|O)f3uwdFCa!4?KlGMbAr;%khN$<(Y752wfi=7~ zWG4G*^5@_P?P@?2+hFLeI(3EmX(Km6l@0{RXC?DJIj|^xzb^^HF3sM z9~PS`6uUoN8GIH{0(P50y^o{>`Mwj7&yB=yv(@-6K(qgn8eWbySkKes&)x$Z9RE3* za02qV^R>uF*@S3(l2A;Yjo6Tj5aKKL4 zu=%7aeAiN__!24|{fT8`0nNkY9a?!V z2v_uGfr~Z0(yKMiEmatc!CA%v8h^#3B;(YCdOu0pdwj|uQwv`&QSUVCoau))5ypqA zv!Qu!vUoYgeZX8+>#Pj9vozsWiEZWlGfHL1JVgkn?@1cm$X4=nb3n3m3_fi@+k@bR zI+`%|Q{6+MP!xim!pt~U7Y=Q%(Yk-1WiTd4I^Gn3Bdu+VLTH}4D-^r-vkRISV5>kH z_so){ZNVQx)jgnCB}StZF&z|CWw3a>bf&5kaM)@1jK#ho#=CL?JWzdsqc%RefUiho z&(!|!c?lYt*PzcFW#g{)7KJFotcVp!ipMD&aeI^AG+})$1IEA4q{DHC!~98D?POGG ziYr^^Nql-nabO~oy#5I`Mb_#;?vq;tp5A1rqFQs-Xh0gm=h5H=FXm`Kh8GlDr#Yb$ zI)?Tu0v8aVDbMqfY_-$m`gwx=l6{5EabF9YCmj`u(w9C9 zQZ#cX>%Uh@9X5^i#>>R5QkyYzu{v%{TK_?U5GF2utp#f)5!r0$iXzi=g}_5*K@Fi zOA0w^<2+1nMESu5Oob^6aVXb9`9zevpj;W{T$H1^h)b1$D|H0EVLCs634DvniQJ$~8psD0W)Fkby;G_juxYBqbDUNaWx{ zaP)=HJLF^!rWA0qJxI816Nu#l~@=dv@RHBjpU2((sL j_or@ySp6?@>S;C418-OBbs(nX|BS)Y)z4*}Q$iB}(dK-T literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_fig_leader_huge.png b/mod/data/mac-fallback-assets/range_fig_leader_huge.png new file mode 100644 index 0000000000000000000000000000000000000000..6ad39ebcba76a691f394fefe08f950cb75aa9228 GIT binary patch literal 18811 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_fig_leader_laat.png b/mod/data/mac-fallback-assets/range_fig_leader_laat.png new file mode 100644 index 0000000000000000000000000000000000000000..3a2ac1d221b1d13e9c357efadb6afdb7fda6c4dd GIT binary patch literal 19058 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_fig_leader_large.png b/mod/data/mac-fallback-assets/range_fig_leader_large.png new file mode 100644 index 0000000000000000000000000000000000000000..c1baafb4cda99568d4f54cd6317be0e17ec5320f GIT binary patch literal 18753 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_fig_leader_snail_stadium.png b/mod/data/mac-fallback-assets/range_fig_leader_snail_stadium.png new file mode 100644 index 0000000000000000000000000000000000000000..81c68a68c4138627192149b6f1f66b0ae59d152a GIT binary patch literal 19127 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_poi.png b/mod/data/mac-fallback-assets/range_poi.png new file mode 100644 index 0000000000000000000000000000000000000000..5a282adb3a6a5f2a524b878032b466cb29a15285 GIT binary patch literal 10364 zcmdUVd0bQ1x9>VhAb?0BIG`efC^%KY36KDB#1T`qYFmdmfYymvkb)u{!Gcx`Xs?A< zl=u@Z*j7-S5X56q(24N&3o;& zhqbmU{Cqo_^)Mr%j*}*in?WQ(Q$!}l_=)T_Fq){}*h%9?&k9X`a%c3;p1w!NUyVAE zdSrISk?WgBjvRkv?x4=>xPX|Pvq8p@B@N=?EA2Zy4WV^mLi+u5)4PW1&xa+Rz1#HZ zyFj9duU=RA2x&~=hAzDYR6CuBb+&%_a*Gk^tQn#v|J%iVEr}(PONA+)iv(n8LbQEx zRhHXYV*5j`TdvR3$JmhCiXrx>`Nq?h#*{XZNG)2D@^BnOv#p5SckPp$MZfztB5u+4 zjinPhlh#B)l;f(eu#O_yASEh2UmANH2z&by>7>ziE38Cx%9ltx)7a;QlL_f|K(r$g zhj9$MN?*JU&*y%|#Wi?$2>+MZnEiCnf^mDr3nr=-zD%Zs$) z*-{a$wOJ!awbf`+4`xVhzuzuNZcb_AVcp+q#7ENO#a@+^#9EVTH`>t^l9EIz={yBL*?Mf6Za_TOKXBup_UOH-2DY5qs4<5>^z^OjF_}#`sW% zrGWiHL`w7hcI^Hw42MZ9*;^suKHW^}VGJ$5tP?A{I8Y5{?K9UmOxgril6%twq`2CP_A8z@E#{@JMfLQmPXfF;*pf;e3DUGFcjkgQjVrfNlFHBRNzCZ z;nf{$iaWx0#jcbYRYoP>As%#ZBDv8RdU0& zxHK{BqvSbiZ#PoA*rk%(ca2a!4RU=9!X>+c%&%1$Q|C&d6(yZtO=!L@mt zM%^Y>5bh!%Q%HXt|6G8?_Fo4{ez|Wfmbh3q}>=H;Vc5Pzl#M-zh{vXf! zO-7Vy2XpwWAubMV*^M3x&qau+bgRUf*d^}E&UuF$(SjIHOD}=IRfok(6i#{eU@CTT`M5j#n=ucpQiajXAL;da5d43 zih+!3cTdt8RoaodTeaJ%X~gyn55-88jX0&g3+XwrF>y9v>jSn=t(%Fm+{@2R7f|En zSRrv!HDS6vMBbf=3WDwm&oXj%JH*GZ#=(?+8|xO6m2k-;VU~b)$X+mX)I+j!BH72q zdJ&bociUMINYZ?vEwP^i!4wElK;Y&icbkN8wl~&;C|VMcUmiCKZd^4P!)cukARkhx(i7qs-AjKO4jGbb|==+ zBXH;@l5emrAWA-${au}iKC71+lXj{mCHO%GmFyG`A^Q1s2PU}En4a*;&eG1Cj8*Y# z=tTi3w$}IGV?xI&D$GbeUXvc|KqXPJ1BiBcWc&fTVYUZ}g0;6NuM^OLuq_BUTk8kz zGNu2xUxB#gt2r{OFDZ7$A|~wdD4ucE|D_Mztyd#*{CIg`VDm3cZk*d1QpE5U7^lge zikDA>;ajGp^5hzFHaWG9Q*9DbCePG<`_bL*hno+MuD!Iw2szP3iW7+@kIxvZ!8U zO!UgbEoeByjt$#_2wa(*;;|Fkmn;|fVVbLmRa&X-X9)Ov>WBMEsdP_l7X&6v_K#;w zsrL7=*u5Jn!>{ZWQAlM4{NQbnCSZtwa&!s-v6b$DSz?Aw46_F1XUREXyNH`2+YHJw zO?mDZQVxS7lY2^#`{$;#>fSyf(PQ`ctgngtQ?`mo=~eyZ-oB*kEa^`0@Yl)9Oh{Ig zZ$$KHtzY%W0+KvaU^_mYd?Rc#ar13qXGKbo=PMHmzcd!Iyir9|55)e9Lxq%1P4dD{ z6_H$-4UJ~n!U!da^|F*sr2jS+6kk-&EA|!8SzQA5UytOcVbe+LV+(t_#0Po%m{4O% zEGQM-&n<>?*urX3F58Z=PmTrkDpgal6N%~{h>4Yj$g#`f6PVDxSw2J;{Uu8+Nf4F= zvlA{O1%ol6mDc%uMZtyrl8GFf0i; z`$smIMK{kS7L3v)a55&LUqgYpfTO$E9Js5saZMujL9QtbnpJ9t$;Bs?)AFesA(kc} z_C;SBet(*n=$FOuFZ$5%sw*(UF9C@!9BFvvG_+?eN_o+n$C4AH?z9eyr4{%+981f8 z+YM3DeqZo#G%e?LgK(N=O*&3s2C?L82k=wz^}89nNs;6xLHpZYGZRViA8w#fvde2G zwgY^Oh>f~76bCNPoP)NE+By(N9HrUYgIyW5G!WCI?BV$nwikRb{P?uRP-#|!Uq^wQ zxoeJga7gib0k`-ryh3jFcG*X5kR8$&qNj)F@?MOx(~$K<_Pe}}#qeZ!jz-r8qi4_j zU~fjdbU_t5%WwKtTasVZ1qi1b==o5FnJ~DQ-@jIXX1Hd-s8~`7h6F&ljGNO6aUcQ( zmQ*fXI>6$t4SyaHvBI!tK$8{5jf&%kc}~%3v(YFkomnU)3+)9MOxGi4MPwWGkb^To zy*5AZRw7PbL;OH}F*|z7^d-sS; zAA{jxH#d~G`K}@w9R)eJTBPW+MM48`0++|E)Hm@qAWEFEnMz~Ty}B*h{$(w^Q5k33SvRR`=rJ*a}7+v<{@z87u$vy2Xpi z3nRaA3#2*o8!KU(kIpG;5egRue1c;pH2?5Sv=ScPQl2~kuQ6DqWZ%E|#&LbbN0{Ii+1BIe zOYC6J6Ty5BH8}8pN8TktH_GLh3!PP=dtDNd?EXk*HwOH z&y#inu4xspw0yyavcFR#gW`f(OA2r1RgTLltox8eL&U8TRh~J0ww;cl{abac4)c%w zfKaU*R#{L4@#fN;To?ZVFm@Wlz2Z%x-1AY+?GK13>(wga^$j5@A0W`mN7jWJ$sT;J zJuCA2>I1fwv}v;5_Rixq03?KE{-^yENQ}sb_(@skb+oLV!Yy;o@+jD4_`fD9H`4R# znH1$`-qrox@+{xm3N}&FYJxZV6@~w-fx#0JY^c$&sA{UO@BNS(Ee`f~S<)FUg&+@` z=Uw*lpB4K%qATgGcnM3&3pYOfC$U?X*x!wm(3QNKzLf)0o$<=E`~$Pq&Yl0xl4nOW ziP64!RhF`{xj+8l?ncyZY`4%_tZL6gesFUBSAxvo@}}aKPfz;HX&wtKwh~uPe690i z&$s_wO8*n)oZXGH{l1ZrKOl%bKPYDt1+1NbkI(hFNAuemA=HEPxDMik`}D7O?KW(J@s$pq|s4mi9EuuBt-a@rpQ{ae{j4F2?3Y%9_f zA^SPcxli_>+vnP?fk6_=>hisJIWiuITJO8^t(5tu2On;xmY?ZsSj$pN8gPxb zbkgasE*mx$t$WKN@7IM6&1*R%d!960ooI|eeqaTJn9i88!_Bvkd@r8cN|0U*@h_$%E@5>j1zG%C3*!Q8}^a1EwdTRKs^j{4s zfX1l5@G3?RP7iOp#pq-PQi5aVbm#D=7<0LNpknaqmb*j(l^i*NzLN3-4{cYs2#vnu zr}@4=54^ulp)cET#VS1Tx@i@EDkTTB^gB-`^h{80F$$A-^^>ao>h7SMhL| z6Ykmyn2d8B9Q{>QjiGO|h`sa@KES@47xT36S(}Of9LPtBjBL^S8}4F(e9RS|U|3?p z8S9oi{-?8OV)`fm${tuAv)XU4Wa>8^Xp-R~T{d{pTbIE+7x}*~Qo`&owDh0cdw&TZ zNqw&Km!!LkUny77#HJy=i0uFB8!Db)2oDoG+0jn^(j?zG<^646PKr%HDaa{peG!mS z(*a|MRkZZ!MrVwWoEUAdPvAbvuAWbw8Le;#G-12f<}5VCc5H_F&6DB1CA;Up@-(Q6 zv$7kXOKW|+$kUF8jY_ul`f4{%52qg1^O_@Q*w&uRXrmFzeghhc-FHG#NM}F93By0u z9xrV1S~-PA9pZ<#?P+jtV=;Y3B8aWXEZ*x#tF+@`@|L$5o&_Z6nLZ7Tm8DC{-qYCh z2sBiKJy&@$+VKm*G2HU}g9emTiU+{wxg)iQZ;;9>6*SJf${P{I6D_+x=25HA* zI#RQ?Y1$@B+39!(yw~_kes!QhRpT+=sYjx(8y1B0o9$n6&}P z&Tn)9eEaBEE81=iiY)_R)2U0UuQ?!J#Cz)0MdjCeQ|BuFsvo@U20w=e-PHY#Bka`t zzBhdm^()#9teS$j^eNO&vm1_A^IL>rDu^Y4T*$v{D)!u&kwPV#-Qe_!U1$D?rjjjg zu#V!}J%7aVGZxHbau%j|7xH%9FcI27(TA}7_3QTGqT6N<9ShE!)p?HY)+qFog- zaUmo!oUMPHSq{%>d8`$NTA|xF%kzyD#pnVspK*%=i|on#-ZXe!?G;VZSRM;vaV~~; z;BIdhcV(3Jfipn#laLEC0jIe83npTA!}+NKPI>nj+F7+*vPEn~ZWz<{^b8VM119Qw zu+Xd0j`gPGkoyBhKd=4TTp*VQt^hShPls5N79hz)`g7hFdz0w76HHSZB@@W|1%|Y} zh=UZSNILbUu=Ua0KH1Hu?JZsFfMF&X51e`*2^Lh+<*<}M5j9orLw~({b$v8(Lu7bI zQ(9DSxNF6~;X~!tH~f_6nb0&Zx%%~X;-(p1`s0ISC)?ahJLM6mzo=-`%n?!Db1B4s zlU$bVOIim*1|hM2RJ@czlVZ(qb*@a->=)7Adlj(mI`@=%bB6sX>>_NlUUOeIg_Kqj zs5!SVEcqi5jjS&Lt$L5xEJKNgT*MTUbCPtWpmX z0Rh7fm33w)>7qyCfWbdEGrAuhmj^~3Zo(0e8)zPR2zk1lGA#d^qOk7~`hI_2f@Hg6 z_1r_2)bC0@a`}GA3)82PS|<6JXq0Nr0p$O>%BCW%SroR*LPWQ83L{eYuAYU2{qhU> zU5K5WJpJJ~QumTzYaO5(d|>zGN0F3!kH1R)+>GPRQrE_Zzjs8kPiNGU>~lgfK&%VV zGF4rwC&PA~XHHzFw<0|QTJS@DOiR)&_>E3HJKKa@V ziQqZ#-$z3mE08JIj`p5~z2r;3XzVyTksUVKG&kHB#6+_!PDwpAC8z_Xa|tvhGcp zKHzKb4aaTEf;+3QaJ1g}cKllba_8M!bLU8;pXU?lhO9ag=^gZwAyS288BEeuw@6H> zLAFu#&~{rkrn!%6iF8)XH$-Iyn7! z9wj(HKh}#c5%5ON^KkMKwUZmEnUh$#7Uq#@$j2?`V*4X(*PC6dHb!a-5m7N8QK;5K6aY?sD;&I}n#u{?)aHg|2(P-B7(5iJnzhP?Vg*E6qP3IpctR3=UvF8T1w;*UN?568?nu&py9CYO?+L4t|!7=fJ@`1 zS11^9LVNwt#!ba0NR)!Z3sik9eCEh<^DV59G%a4KwFUZ5Jo<6#CJr%AX$6$)Kv{;$ zl_{GMvJxvKt1$05P*yuP#z%*QPw6f&e+wXlFU|o#q2E?WYNw&R1=TEEka?uGpMX?m zpp+M&7y4qQ&qRVzhk6&jNNSLSh!Qk9vBrsL3i?xB8#f`Rtrj^D`_Md6IRa%k<4us- z%ePB|%#&fi8h{@{mCPUDbsFF}DXEPB{>VI1JqkrW09OON2H>|Wg*5jPSE$7oA*oOzGu4@B?RjK+T;6O#eSvg} z3DUWG;Z_lmD=HVRN>Z__-|Zxz2PlxqLwk)>NN>=Wdsdu?Ut?XWitpO&y0hcYV-%B@ncaR^Wy<-Old zI~QT#66&&2P@l!uW_8%g$a|xDsw2v}Ok63cs75>&_oRLk&B+OcQY&x{`o>Dg`T_hj z=m&@xwPm!O8mBz9t1Hb#aaPSFBI|Kdic{)@MTqR?eT^vts1JZT#zer)F^^!eHL!{( zS~^V6!1ZVgBSqQRMJT1~|1qr}*1+f|VkJ97(X{}bP!$hPc8a23{B=SdB0hU(i$G<* z#E$g>s?7u`yV^X0;=oBY;A4vLuNCq|Qmo}>=HRdw%v?B*deF!0;q4n~0QSY@zSe59 zv39H<>^-5AxODdp3D;Qw+n;CFxISklf(&<76qRir&cCN?`}ZF0Rp3Rk2?!X`Ta3!O eza-s>oMz^npIP1k8?TQD-=y(=;|`BeM*Syc!aU*t literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_smokeToken.png b/mod/data/mac-fallback-assets/range_smokeToken.png new file mode 100644 index 0000000000000000000000000000000000000000..5a282adb3a6a5f2a524b878032b466cb29a15285 GIT binary patch literal 10364 zcmdUVd0bQ1x9>VhAb?0BIG`efC^%KY36KDB#1T`qYFmdmfYymvkb)u{!Gcx`Xs?A< zl=u@Z*j7-S5X56q(24N&3o;& zhqbmU{Cqo_^)Mr%j*}*in?WQ(Q$!}l_=)T_Fq){}*h%9?&k9X`a%c3;p1w!NUyVAE zdSrISk?WgBjvRkv?x4=>xPX|Pvq8p@B@N=?EA2Zy4WV^mLi+u5)4PW1&xa+Rz1#HZ zyFj9duU=RA2x&~=hAzDYR6CuBb+&%_a*Gk^tQn#v|J%iVEr}(PONA+)iv(n8LbQEx zRhHXYV*5j`TdvR3$JmhCiXrx>`Nq?h#*{XZNG)2D@^BnOv#p5SckPp$MZfztB5u+4 zjinPhlh#B)l;f(eu#O_yASEh2UmANH2z&by>7>ziE38Cx%9ltx)7a;QlL_f|K(r$g zhj9$MN?*JU&*y%|#Wi?$2>+MZnEiCnf^mDr3nr=-zD%Zs$) z*-{a$wOJ!awbf`+4`xVhzuzuNZcb_AVcp+q#7ENO#a@+^#9EVTH`>t^l9EIz={yBL*?Mf6Za_TOKXBup_UOH-2DY5qs4<5>^z^OjF_}#`sW% zrGWiHL`w7hcI^Hw42MZ9*;^suKHW^}VGJ$5tP?A{I8Y5{?K9UmOxgril6%twq`2CP_A8z@E#{@JMfLQmPXfF;*pf;e3DUGFcjkgQjVrfNlFHBRNzCZ z;nf{$iaWx0#jcbYRYoP>As%#ZBDv8RdU0& zxHK{BqvSbiZ#PoA*rk%(ca2a!4RU=9!X>+c%&%1$Q|C&d6(yZtO=!L@mt zM%^Y>5bh!%Q%HXt|6G8?_Fo4{ez|Wfmbh3q}>=H;Vc5Pzl#M-zh{vXf! zO-7Vy2XpwWAubMV*^M3x&qau+bgRUf*d^}E&UuF$(SjIHOD}=IRfok(6i#{eU@CTT`M5j#n=ucpQiajXAL;da5d43 zih+!3cTdt8RoaodTeaJ%X~gyn55-88jX0&g3+XwrF>y9v>jSn=t(%Fm+{@2R7f|En zSRrv!HDS6vMBbf=3WDwm&oXj%JH*GZ#=(?+8|xO6m2k-;VU~b)$X+mX)I+j!BH72q zdJ&bociUMINYZ?vEwP^i!4wElK;Y&icbkN8wl~&;C|VMcUmiCKZd^4P!)cukARkhx(i7qs-AjKO4jGbb|==+ zBXH;@l5emrAWA-${au}iKC71+lXj{mCHO%GmFyG`A^Q1s2PU}En4a*;&eG1Cj8*Y# z=tTi3w$}IGV?xI&D$GbeUXvc|KqXPJ1BiBcWc&fTVYUZ}g0;6NuM^OLuq_BUTk8kz zGNu2xUxB#gt2r{OFDZ7$A|~wdD4ucE|D_Mztyd#*{CIg`VDm3cZk*d1QpE5U7^lge zikDA>;ajGp^5hzFHaWG9Q*9DbCePG<`_bL*hno+MuD!Iw2szP3iW7+@kIxvZ!8U zO!UgbEoeByjt$#_2wa(*;;|Fkmn;|fVVbLmRa&X-X9)Ov>WBMEsdP_l7X&6v_K#;w zsrL7=*u5Jn!>{ZWQAlM4{NQbnCSZtwa&!s-v6b$DSz?Aw46_F1XUREXyNH`2+YHJw zO?mDZQVxS7lY2^#`{$;#>fSyf(PQ`ctgngtQ?`mo=~eyZ-oB*kEa^`0@Yl)9Oh{Ig zZ$$KHtzY%W0+KvaU^_mYd?Rc#ar13qXGKbo=PMHmzcd!Iyir9|55)e9Lxq%1P4dD{ z6_H$-4UJ~n!U!da^|F*sr2jS+6kk-&EA|!8SzQA5UytOcVbe+LV+(t_#0Po%m{4O% zEGQM-&n<>?*urX3F58Z=PmTrkDpgal6N%~{h>4Yj$g#`f6PVDxSw2J;{Uu8+Nf4F= zvlA{O1%ol6mDc%uMZtyrl8GFf0i; z`$smIMK{kS7L3v)a55&LUqgYpfTO$E9Js5saZMujL9QtbnpJ9t$;Bs?)AFesA(kc} z_C;SBet(*n=$FOuFZ$5%sw*(UF9C@!9BFvvG_+?eN_o+n$C4AH?z9eyr4{%+981f8 z+YM3DeqZo#G%e?LgK(N=O*&3s2C?L82k=wz^}89nNs;6xLHpZYGZRViA8w#fvde2G zwgY^Oh>f~76bCNPoP)NE+By(N9HrUYgIyW5G!WCI?BV$nwikRb{P?uRP-#|!Uq^wQ zxoeJga7gib0k`-ryh3jFcG*X5kR8$&qNj)F@?MOx(~$K<_Pe}}#qeZ!jz-r8qi4_j zU~fjdbU_t5%WwKtTasVZ1qi1b==o5FnJ~DQ-@jIXX1Hd-s8~`7h6F&ljGNO6aUcQ( zmQ*fXI>6$t4SyaHvBI!tK$8{5jf&%kc}~%3v(YFkomnU)3+)9MOxGi4MPwWGkb^To zy*5AZRw7PbL;OH}F*|z7^d-sS; zAA{jxH#d~G`K}@w9R)eJTBPW+MM48`0++|E)Hm@qAWEFEnMz~Ty}B*h{$(w^Q5k33SvRR`=rJ*a}7+v<{@z87u$vy2Xpi z3nRaA3#2*o8!KU(kIpG;5egRue1c;pH2?5Sv=ScPQl2~kuQ6DqWZ%E|#&LbbN0{Ii+1BIe zOYC6J6Ty5BH8}8pN8TktH_GLh3!PP=dtDNd?EXk*HwOH z&y#inu4xspw0yyavcFR#gW`f(OA2r1RgTLltox8eL&U8TRh~J0ww;cl{abac4)c%w zfKaU*R#{L4@#fN;To?ZVFm@Wlz2Z%x-1AY+?GK13>(wga^$j5@A0W`mN7jWJ$sT;J zJuCA2>I1fwv}v;5_Rixq03?KE{-^yENQ}sb_(@skb+oLV!Yy;o@+jD4_`fD9H`4R# znH1$`-qrox@+{xm3N}&FYJxZV6@~w-fx#0JY^c$&sA{UO@BNS(Ee`f~S<)FUg&+@` z=Uw*lpB4K%qATgGcnM3&3pYOfC$U?X*x!wm(3QNKzLf)0o$<=E`~$Pq&Yl0xl4nOW ziP64!RhF`{xj+8l?ncyZY`4%_tZL6gesFUBSAxvo@}}aKPfz;HX&wtKwh~uPe690i z&$s_wO8*n)oZXGH{l1ZrKOl%bKPYDt1+1NbkI(hFNAuemA=HEPxDMik`}D7O?KW(J@s$pq|s4mi9EuuBt-a@rpQ{ae{j4F2?3Y%9_f zA^SPcxli_>+vnP?fk6_=>hisJIWiuITJO8^t(5tu2On;xmY?ZsSj$pN8gPxb zbkgasE*mx$t$WKN@7IM6&1*R%d!960ooI|eeqaTJn9i88!_Bvkd@r8cN|0U*@h_$%E@5>j1zG%C3*!Q8}^a1EwdTRKs^j{4s zfX1l5@G3?RP7iOp#pq-PQi5aVbm#D=7<0LNpknaqmb*j(l^i*NzLN3-4{cYs2#vnu zr}@4=54^ulp)cET#VS1Tx@i@EDkTTB^gB-`^h{80F$$A-^^>ao>h7SMhL| z6Ykmyn2d8B9Q{>QjiGO|h`sa@KES@47xT36S(}Of9LPtBjBL^S8}4F(e9RS|U|3?p z8S9oi{-?8OV)`fm${tuAv)XU4Wa>8^Xp-R~T{d{pTbIE+7x}*~Qo`&owDh0cdw&TZ zNqw&Km!!LkUny77#HJy=i0uFB8!Db)2oDoG+0jn^(j?zG<^646PKr%HDaa{peG!mS z(*a|MRkZZ!MrVwWoEUAdPvAbvuAWbw8Le;#G-12f<}5VCc5H_F&6DB1CA;Up@-(Q6 zv$7kXOKW|+$kUF8jY_ul`f4{%52qg1^O_@Q*w&uRXrmFzeghhc-FHG#NM}F93By0u z9xrV1S~-PA9pZ<#?P+jtV=;Y3B8aWXEZ*x#tF+@`@|L$5o&_Z6nLZ7Tm8DC{-qYCh z2sBiKJy&@$+VKm*G2HU}g9emTiU+{wxg)iQZ;;9>6*SJf${P{I6D_+x=25HA* zI#RQ?Y1$@B+39!(yw~_kes!QhRpT+=sYjx(8y1B0o9$n6&}P z&Tn)9eEaBEE81=iiY)_R)2U0UuQ?!J#Cz)0MdjCeQ|BuFsvo@U20w=e-PHY#Bka`t zzBhdm^()#9teS$j^eNO&vm1_A^IL>rDu^Y4T*$v{D)!u&kwPV#-Qe_!U1$D?rjjjg zu#V!}J%7aVGZxHbau%j|7xH%9FcI27(TA}7_3QTGqT6N<9ShE!)p?HY)+qFog- zaUmo!oUMPHSq{%>d8`$NTA|xF%kzyD#pnVspK*%=i|on#-ZXe!?G;VZSRM;vaV~~; z;BIdhcV(3Jfipn#laLEC0jIe83npTA!}+NKPI>nj+F7+*vPEn~ZWz<{^b8VM119Qw zu+Xd0j`gPGkoyBhKd=4TTp*VQt^hShPls5N79hz)`g7hFdz0w76HHSZB@@W|1%|Y} zh=UZSNILbUu=Ua0KH1Hu?JZsFfMF&X51e`*2^Lh+<*<}M5j9orLw~({b$v8(Lu7bI zQ(9DSxNF6~;X~!tH~f_6nb0&Zx%%~X;-(p1`s0ISC)?ahJLM6mzo=-`%n?!Db1B4s zlU$bVOIim*1|hM2RJ@czlVZ(qb*@a->=)7Adlj(mI`@=%bB6sX>>_NlUUOeIg_Kqj zs5!SVEcqi5jjS&Lt$L5xEJKNgT*MTUbCPtWpmX z0Rh7fm33w)>7qyCfWbdEGrAuhmj^~3Zo(0e8)zPR2zk1lGA#d^qOk7~`hI_2f@Hg6 z_1r_2)bC0@a`}GA3)82PS|<6JXq0Nr0p$O>%BCW%SroR*LPWQ83L{eYuAYU2{qhU> zU5K5WJpJJ~QumTzYaO5(d|>zGN0F3!kH1R)+>GPRQrE_Zzjs8kPiNGU>~lgfK&%VV zGF4rwC&PA~XHHzFw<0|QTJS@DOiR)&_>E3HJKKa@V ziQqZ#-$z3mE08JIj`p5~z2r;3XzVyTksUVKG&kHB#6+_!PDwpAC8z_Xa|tvhGcp zKHzKb4aaTEf;+3QaJ1g}cKllba_8M!bLU8;pXU?lhO9ag=^gZwAyS288BEeuw@6H> zLAFu#&~{rkrn!%6iF8)XH$-Iyn7! z9wj(HKh}#c5%5ON^KkMKwUZmEnUh$#7Uq#@$j2?`V*4X(*PC6dHb!a-5m7N8QK;5K6aY?sD;&I}n#u{?)aHg|2(P-B7(5iJnzhP?Vg*E6qP3IpctR3=UvF8T1w;*UN?568?nu&py9CYO?+L4t|!7=fJ@`1 zS11^9LVNwt#!ba0NR)!Z3sik9eCEh<^DV59G%a4KwFUZ5Jo<6#CJr%AX$6$)Kv{;$ zl_{GMvJxvKt1$05P*yuP#z%*QPw6f&e+wXlFU|o#q2E?WYNw&R1=TEEka?uGpMX?m zpp+M&7y4qQ&qRVzhk6&jNNSLSh!Qk9vBrsL3i?xB8#f`Rtrj^D`_Md6IRa%k<4us- z%ePB|%#&fi8h{@{mCPUDbsFF}DXEPB{>VI1JqkrW09OON2H>|Wg*5jPSE$7oA*oOzGu4@B?RjK+T;6O#eSvg} z3DUWG;Z_lmD=HVRN>Z__-|Zxz2PlxqLwk)>NN>=Wdsdu?Ut?XWitpO&y0hcYV-%B@ncaR^Wy<-Old zI~QT#66&&2P@l!uW_8%g$a|xDsw2v}Ok63cs75>&_oRLk&B+OcQY&x{`o>Dg`T_hj z=m&@xwPm!O8mBz9t1Hb#aaPSFBI|Kdic{)@MTqR?eT^vts1JZT#zer)F^^!eHL!{( zS~^V6!1ZVgBSqQRMJT1~|1qr}*1+f|VkJ97(X{}bP!$hPc8a23{B=SdB0hU(i$G<* z#E$g>s?7u`yV^X0;=oBY;A4vLuNCq|Qmo}>=HRdw%v?B*deF!0;q4n~0QSY@zSe59 zv39H<>^-5AxODdp3D;Qw+n;CFxISklf(&<76qRir&cCN?`}ZF0Rp3Rk2?!X`Ta3!O eza-s>oMz^npIP1k8?TQD-=y(=;|`BeM*Syc!aU*t literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_token.png b/mod/data/mac-fallback-assets/range_token.png new file mode 100644 index 0000000000000000000000000000000000000000..5a282adb3a6a5f2a524b878032b466cb29a15285 GIT binary patch literal 10364 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/range_tokenRangeTwo.png b/mod/data/mac-fallback-assets/range_tokenRangeTwo.png new file mode 100644 index 0000000000000000000000000000000000000000..4a8a2f488e72d64ace0344269757f910d16f74ed GIT binary patch literal 12344 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 literal 0 HcmV?d00001 diff --git a/mod/data/mac-fallback-assets/silhouette_mac_fallback.unity3d b/mod/data/mac-fallback-assets/silhouette_mac_fallback.unity3d new file mode 100644 index 0000000000000000000000000000000000000000..15bb9258a1c225572de9b686b6595455abfe184f GIT binary patch literal 10922 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 Mcz6b}|N5b`;!*7d "mac" | "windows" +deploymentMode = deploymentMode or "mac" -- table-wide setting + +function gGetMode(params) + return playerOverlayMode[params.color] or "mac" +end + +function gGetDeploymentMode() + return deploymentMode +end + +function gToggleDeploymentMode() + deploymentMode = (deploymentMode == "mac") and "windows" or "mac" + macRefreshModeUI() +end + +function gCohesionTrigger(params) + if not params or not params.figGUID then return end + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + local mode = playerOverlayMode[params.playerColor] or "mac" + if mode == "windows" then + -- Object may not have the original alias (e.g., hovered non-fig object). + -- pcall guards against "no such function" errors that would otherwise + -- propagate out of the hotkey/button callback. + local ok = pcall(function() fig.call("spawnCohesionRulerOriginal", fig) end) + if not ok then gToggleCohesion({figGUID = params.figGUID}) end + else + gToggleCohesion({figGUID = params.figGUID}) + end +end + +function gRangeTrigger(params) + if not params or not params.figGUID then return end + local fig = getObjectFromGUID(params.figGUID) + if not fig then return end + local mode = playerOverlayMode[params.playerColor] or "mac" + if mode == "windows" then + local ok = pcall(function() fig.call("spawnRangeRulerOriginal", fig) end) + if not ok then gToggleRange({figGUID = params.figGUID}) end + else + gToggleRange({figGUID = params.figGUID}) + end +end + +-- UI: floating panel with one toggle button per seated player +function macModeClick(player, _, id) + local color = id:sub(9) -- "macmode_Red" -> "Red" + if player.color ~= color then + broadcastToColor("Only the player at this seat can toggle their mode.", + player.color, {1, 0.5, 0.5}) + return + end + local cur = playerOverlayMode[color] or "mac" + playerOverlayMode[color] = (cur == "mac") and "windows" or "mac" + -- Wipe any active Cohesion/Range overlays for this player so the next + -- trigger redraws in the chosen mode without stale visuals. + macRefreshModeUI() +end + +local function macFindNodeById(tree, id) + for _, node in ipairs(tree) do + if node.attributes and node.attributes.id == id then return node, tree end + if node.children then + local found, parent = macFindNodeById(node.children, id) + if found then return found, parent end + end + end + return nil, nil +end + +-- Tracks the panel's desired active state across refreshes. Persists so a +-- click on the close X before the initial refresh is honored. +macModePanelActive = (macModePanelActive == nil) and true or macModePanelActive + +function macModeToggleVisibility() + macModePanelActive = not macModePanelActive + local tree = UI.getXmlTable() or {} + local panel = macFindNodeById(tree, "macModePanel") + if panel then + panel.attributes.active = macModePanelActive and "true" or "false" + UI.setXmlTable(tree) + else + macRefreshModeUI() + end +end + +function macRefreshModeUI() + local seated = Player.getPlayers() + local rows = {{ + tag = "Panel", + attributes = { + color = "transparent", + preferredHeight = "24", + }, + children = { + { + tag = "Text", + attributes = { + text = "Mac TTS U6 Patch", + fontSize = "16", + color = "white", + alignment = "MiddleLeft", + rectAlignment = "MiddleLeft", + }, + }, + { + tag = "Button", + attributes = { + id = "macModePanelClose", + text = "X", + onClick = "macModeToggleVisibility", + color = "#3a1e1e", + textColor = "white", + fontSize = "12", + width = "24", + height = "20", + rectAlignment = "MiddleRight", + }, + }, + }, + }, { + tag = "Text", + attributes = { + text = "Cohesion & Range: pick your renderer", + fontSize = "12", + color = "#bbbbbb", + alignment = "MiddleCenter", + }, + }} + if #seated == 0 then + table.insert(rows, { + tag = "Text", + attributes = { + text = "(no seated players)", + fontSize = "12", + color = "#888888", + alignment = "MiddleCenter", + }, + }) + end + for _, p in ipairs(seated) do + local mode = playerOverlayMode[p.color] or "mac" + local label = p.color .. " - " .. + (mode == "mac" and "MAC FALLBACK" or "WINDOWS ORIGINAL") + local bg = (mode == "mac") and "#1e7a3a" or "#7a3a1e" + table.insert(rows, { + tag = "Button", + attributes = { + id = "macmode_" .. p.color, + text = label, + onClick = "macModeClick", + color = bg, + fontSize = "13", + textColor = "white", + preferredHeight = "28", + }, + }) + end + -- Deployment is table-wide (everyone sees the same zones); use a single + -- global toggle rather than per-seat. + table.insert(rows, { + tag = "Text", + attributes = { + text = "Deployment (table-wide)", + fontSize = "12", + color = "#bbbbbb", + alignment = "MiddleCenter", + }, + }) + table.insert(rows, { + tag = "Button", + attributes = { + id = "macmode_deployment", + text = (deploymentMode == "mac") and "DEPLOYMENT: MAC FALLBACK" + or "DEPLOYMENT: WINDOWS ORIGINAL", + onClick = "macDeploymentClick", + color = (deploymentMode == "mac") and "#1e3a7a" or "#7a3a1e", + fontSize = "13", + textColor = "white", + preferredHeight = "28", + }, + }) + -- Build my panel (will be merged into the existing UI tree below so we + -- don't clobber legionFloatingMenu / Welcome / Chess Clocks etc.). + local panel = { + tag = "Panel", + attributes = { + id = "macModePanel", + active = macModePanelActive and "true" or "false", + rectAlignment = "MiddleRight", + offsetXY = "-10 80", + width = "260", + height = "320", + color = "rgba(0.06,0.06,0.06,0.9)", + padding = "8 8 8 8", + outlineSize = "1 1", + outline = "#303030", + }, + children = {{ + tag = "VerticalLayout", + attributes = { + spacing = "4", + childForceExpandHeight = "false", + childForceExpandWidth = "true", + }, + children = rows, + }}, + } + + local tree = UI.getXmlTable() or {} + -- Remove any stale macModePanel before reinserting the fresh one. + for i = #tree, 1, -1 do + if tree[i].attributes and tree[i].attributes.id == "macModePanel" then + table.remove(tree, i) + end + end + table.insert(tree, panel) + + -- Inject a "Mac Patch" button into the bottom-right legionFloatingMenu + -- (replaces the first interactable=false placeholder button). + local menu = macFindNodeById(tree, "legionFloatingMenu") + if menu and menu.children then + local hasMine = false + for _, c in ipairs(menu.children) do + if c.attributes and c.attributes.id == "macModeMenuButton" then + hasMine = true; break + end + end + if not hasMine then + for i, c in ipairs(menu.children) do + if c.attributes and c.attributes.interactable == "false" then + menu.children[i] = { + tag = "Button", + attributes = { + id = "macModeMenuButton", + fontSize = "10", + onClick = "macModeToggleVisibility", + tooltip = "Toggle Mac TTS U6 Patch panel", + }, + value = "Mac Patch", + } + break + end + end + end + end + + UI.setXmlTable(tree) +end + +function macDeploymentClick(_, _, _) + gToggleDeploymentMode() +end + +function onPlayerChangeColor(_) macRefreshModeUI() end +function onPlayerConnect(_) macRefreshModeUI() end +function onPlayerDisconnect(_) macRefreshModeUI() end + +Wait.time(macRefreshModeUI, 2) + +-- Override hotkey init functions to capture playerColor and route through +-- the per-seat trigger. Defining initCohesionHotkeys/initRangebandHotkeys +-- here SHADOWS the originals - when the original onLoad runs init*(), our +-- versions register the hotkey instead. + +function initCohesionHotkeys() + addHotkey("Show Cohesion On Hovered Model", + function(playerColor, hoverObject, _) + if not hoverObject or not hoverObject.interactable then return end + gCohesionTrigger({ + figGUID = hoverObject.getGUID(), + playerColor = playerColor, + }) + end) +end + +function initRangebandHotkeys() + addHotkey("Show Range On Hovered Model", + function(playerColor, hoverObject, _) + if not hoverObject or not hoverObject.interactable then return end + gRangeTrigger({ + figGUID = hoverObject.getGUID(), + playerColor = playerColor, + }) + end) +end + +-- Legacy global-scope wrappers (for any code that still calls these directly). +-- Default to mac mode when no color context is available. +function showCohesionOnHoveredModel(hoverObject) + if not hoverObject or not hoverObject.interactable then return end + gToggleCohesion({ figGUID = hoverObject.getGUID() }) +end + +function spawnCohesionRuler(cohesionSourceObject) + if not cohesionSourceObject then return end + gSpawnCohesion({ figGUID = cohesionSourceObject.getGUID() }) +end + +function clearCohesionRuler() + for key, entry in pairs(activeOverlays) do + if entry.type == "cohesion" then activeOverlays[key] = nil end + end + macRedrawAll() +end + +function showRangeOnHoveredModel(hoverObject) + if not hoverObject or not hoverObject.interactable then return end + gToggleRange({ figGUID = hoverObject.getGUID() }) +end + +function spawnRangeRuler(rangeSourceObject, _) + if not rangeSourceObject then return end + gSpawnRange({ figGUID = rangeSourceObject.getGUID() }) +end + +function clearRangeRulers() + gClearAllRange() +end + diff --git a/mod/src/includes/RangeRulers.ttslua b/mod/src/includes/RangeRulers.ttslua index 6df6e15a..c51676e0 100644 --- a/mod/src/includes/RangeRulers.ttslua +++ b/mod/src/includes/RangeRulers.ttslua @@ -80,3 +80,33 @@ function spawnRangeRuler(rangeSourceObject, projectorBundleOverride) noRulers = false end + +-- ============================================ +-- Mac fallback per-seat router (appended). +-- Mirrors the Cohesion pattern. See !/Overlays for the manager. +-- ============================================ +spawnRangeRulerOriginal = spawnRangeRuler +clearRangeRulersOriginal = clearRangeRulers + +function spawnRangeRuler(rangeSourceObject, _) + if not rangeSourceObject then return end + Global.call("gRangeTrigger", { + figGUID = rangeSourceObject.getGUID(), + playerColor = nil, + }) +end + +function clearRangeRulers() + Global.call("gClearAllRange", {}) + -- Also run the original (destroys the bundle Object spawned in Windows + -- mode). Skip if rangeRuler was never set (pure-Mac session). + if rangeRuler ~= nil then + pcall(clearRangeRulersOriginal) + end +end + +function clearRangeRuler() + if self and self.getGUID and self.getGUID() ~= "-1" then + Global.call("gClearRange", { figGUID = self.getGUID() }) + end +end diff --git a/src/index.ts b/src/index.ts index 45d99efd..ba63a1e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -103,25 +103,31 @@ export async function compileSaveFile( } } +function defaultTTSHomeDir(): string { + const platform = os.platform(); + if (platform === 'win32') { + return steam.homeDir.win32(process.env); + } + if (platform === 'darwin') { + return path.join(os.homedir(), 'Library', 'Tabletop Simulator'); + } + if (platform === 'linux') { + return path.join(os.homedir(), '.local', 'share', 'Tabletop Simulator'); + } + throw new Error(`Unsupported platform: ${platform}`); +} + export async function destroySymlink(homeDir?: string): Promise { - // TODO: Add non-win32 support. if (!homeDir) { - if (os.platform() !== 'win32') { - throw new Error(`Unsupported platform: ${os.platform()}`); - } - homeDir = steam.homeDir.win32(process.env); + homeDir = defaultTTSHomeDir(); } const from = path.join(homeDir, 'Saves', 'TTSDevLink'); return fs.remove(from); } export async function createSymlink(homeDir?: string): Promise { - // TODO: Add non-win32 support. if (!homeDir) { - if (os.platform() !== 'win32') { - throw new Error(`Unsupported platform: ${os.platform()}`); - } - homeDir = steam.homeDir.win32(process.env); + homeDir = defaultTTSHomeDir(); } await destroySymlink(homeDir); const from = path.join(homeDir, 'Saves', 'TTSDevLink');