LightGame 完成 Windows
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
"""Print per-GUID analysis of the foreground Tilemap to help identify TileIds.
|
||||
|
||||
For each tile asset GUID, prints:
|
||||
- count
|
||||
- bounding box (min/max x, y in Unity cell coords)
|
||||
- 5 sample positions
|
||||
- top neighbor distribution: which GUID most often sits directly ABOVE this one
|
||||
- bottom neighbor distribution: which GUID most often sits directly BELOW
|
||||
|
||||
Run:
|
||||
python tools/analyze_foreground.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from typing import Dict, Tuple
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import unity_to_level as u2l
|
||||
|
||||
|
||||
def short(guid: str) -> str:
|
||||
return guid[:8]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
with open(u2l.SCENE_PATH, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
docs = u2l.split_documents(text)
|
||||
go_names = u2l.parse_gameobjects(docs)
|
||||
tms = u2l.parse_tilemaps(docs, go_names)
|
||||
|
||||
fg = tms.get("foreground")
|
||||
if not fg:
|
||||
print("no foreground tilemap")
|
||||
return 1
|
||||
|
||||
# Build a (x, y) -> guid map; drop duplicates conservatively.
|
||||
cell_guid: Dict[Tuple[int, int], str] = {}
|
||||
for x, y, idx in fg.cells:
|
||||
if 0 <= idx < len(fg.asset_guids):
|
||||
cell_guid[(x, y)] = fg.asset_guids[idx]
|
||||
|
||||
# Group cells by guid.
|
||||
by_guid: Dict[str, list] = defaultdict(list)
|
||||
for (x, y), guid in cell_guid.items():
|
||||
by_guid[guid].append((x, y))
|
||||
|
||||
# Order by count desc.
|
||||
ordered = sorted(by_guid.items(), key=lambda kv: -len(kv[1]))
|
||||
|
||||
print(f"foreground tile assets: {len(fg.asset_guids)}, total cells: {len(cell_guid)}")
|
||||
print()
|
||||
for guid, cells in ordered:
|
||||
xs = [c[0] for c in cells]
|
||||
ys = [c[1] for c in cells]
|
||||
# Neighbors: in Unity Y is up, so "above" = y+1, "below" = y-1.
|
||||
above_counter: Counter = Counter()
|
||||
below_counter: Counter = Counter()
|
||||
left_counter: Counter = Counter()
|
||||
for x, y in cells:
|
||||
up = cell_guid.get((x, y + 1))
|
||||
dn = cell_guid.get((x, y - 1))
|
||||
lf = cell_guid.get((x - 1, y))
|
||||
above_counter[up] += 1 # None means empty/sky
|
||||
below_counter[dn] += 1
|
||||
left_counter[lf] += 1
|
||||
|
||||
# Sample positions: a few spread out cells.
|
||||
sample = sorted(cells)[:: max(1, len(cells) // 5)][:5]
|
||||
|
||||
print(f"{short(guid)} ({guid})")
|
||||
print(f" count : {len(cells)}")
|
||||
print(f" bbox : x [{min(xs)} .. {max(xs)}] y [{min(ys)} .. {max(ys)}]")
|
||||
print(f" samples: {sample}")
|
||||
# top 3 of each direction
|
||||
def fmt(c: Counter) -> str:
|
||||
top = c.most_common(3)
|
||||
return ", ".join(
|
||||
f"{short(g) if g else '<empty>'}={n}" for g, n in top
|
||||
)
|
||||
print(f" above (Unity y+1): {fmt(above_counter)}")
|
||||
print(f" below (Unity y-1): {fmt(below_counter)}")
|
||||
print(f" left : {fmt(left_counter)}")
|
||||
# If almost all rows are the same y, it's probably a horizontal-only tile.
|
||||
y_hist = Counter(ys)
|
||||
top_y = y_hist.most_common(3)
|
||||
print(f" y histogram (top 3): {top_y}")
|
||||
# Single-row or single-col flag
|
||||
if len(set(ys)) <= 2:
|
||||
print(f" >>> appears on only {len(set(ys))} distinct row(s) — likely a 'top' or row-specific tile")
|
||||
if len(set(xs)) <= 2:
|
||||
print(f" >>> appears on only {len(set(xs))} distinct column(s)")
|
||||
print()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Validate default spawn candidates per 3x3 room of LevelTotal.
|
||||
|
||||
Reads tiles[] from src/Apps/LightGame/src/levels/LevelTotalData.h, then for each
|
||||
candidate (col,row,tile_x,tile_y) checks:
|
||||
- tile at (tx,ty) and (tx,ty+1)/(tx,ty-1) per collider (8,0)->(24,32) is non-solid
|
||||
- tile at (tx, ty+1) (the tile directly under feet at world_y=tile_y*32+32) is solid
|
||||
|
||||
Player position used by spawn check (LightGameApp.cpp:181):
|
||||
pos = (tile_x*32, tile_y*32) -- collider min=(8,0), max=(24,32)
|
||||
world_box = pos + collider = (tx*32+8, ty*32) -> (tx*32+24, ty*32+32)
|
||||
is_grounded checks tiles below feet; collider rows tile_top..tile_bottom-1 must be non-solid.
|
||||
|
||||
Solid ids: 5,6,7,8,9,10,11.
|
||||
Deadly: 12, 22.
|
||||
"""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / "src/Apps/LightGame/src/levels/LevelTotalData.h"
|
||||
|
||||
text = SRC.read_text(encoding="utf-8")
|
||||
|
||||
# Parse tiles array
|
||||
m = re.search(r"static const uint16_t tiles\[\]\s*=\s*\{([^}]*)\};", text, re.DOTALL)
|
||||
nums = [int(x) for x in re.findall(r"-?\d+", m.group(1))]
|
||||
W, H = 96, 54
|
||||
assert len(nums) == W * H, len(nums)
|
||||
tiles = [nums[y * W:(y + 1) * W] for y in range(H)]
|
||||
|
||||
SOLID = {5, 6, 7, 8, 9, 10, 11}
|
||||
DEADLY = {12, 22}
|
||||
|
||||
def at(tx, ty):
|
||||
if tx < 0 or tx >= W or ty < 0 or ty >= H:
|
||||
return -1
|
||||
return tiles[ty][tx]
|
||||
|
||||
def collider_overlaps_solid(tx, ty):
|
||||
# world_box = (tx*32+8, ty*32) -> (tx*32+24, ty*32+32)
|
||||
# tile rows: ty .. (ty*32+32-1)//32 = ty (since 32+32-1=63, //32=1, so rows ty..ty+1-1 = ty)
|
||||
# Actually max.y - 1 = ty*32 + 31, //32 = ty. So only row ty.
|
||||
# tile cols: (tx*32+8)//32 = tx, (tx*32+24-1)//32 = tx. Only col tx.
|
||||
return at(tx, ty) in SOLID or at(tx, ty) in DEADLY
|
||||
|
||||
def has_solid_below(tx, ty):
|
||||
# Player feet at world_y = ty*32 + 32. is_grounded probes 1px below = ty*32+33.
|
||||
# Tile row = (ty*32+32)//32 = ty+1.
|
||||
return at(tx, ty + 1) in SOLID
|
||||
|
||||
def safe_above_head(tx, ty):
|
||||
# don't have ceiling spike just touching
|
||||
return at(tx, ty - 1) not in DEADLY
|
||||
|
||||
def candidate_score(tx, ty):
|
||||
if collider_overlaps_solid(tx, ty):
|
||||
return None, "overlap solid/deadly at body row"
|
||||
if not has_solid_below(tx, ty):
|
||||
return None, "not grounded"
|
||||
if not safe_above_head(tx, ty):
|
||||
return None, "deadly above head"
|
||||
return True, "ok"
|
||||
|
||||
# Room grid: cols 0/32/64, rows 0/18/36; size 32x18
|
||||
def find_safe_in_room(rcol, rrow, prefer=None):
|
||||
tx0, ty0 = rcol * 32, rrow * 18
|
||||
tx1, ty1 = tx0 + 32, ty0 + 18
|
||||
candidates = []
|
||||
if prefer:
|
||||
ptx, pty = prefer
|
||||
ok, why = candidate_score(ptx, pty)
|
||||
if ok:
|
||||
return (ptx, pty, "preferred")
|
||||
else:
|
||||
print(f" preferred {(ptx,pty)} rejected: {why}")
|
||||
# Scan center outward
|
||||
cx, cy = (tx0 + tx1) // 2, (ty0 + ty1) // 2
|
||||
for ty in range(ty0, ty1):
|
||||
for tx in range(tx0, tx1):
|
||||
ok, why = candidate_score(tx, ty)
|
||||
if ok:
|
||||
d = (tx - cx) ** 2 + (ty - cy) ** 2
|
||||
candidates.append((d, tx, ty))
|
||||
if not candidates:
|
||||
return None
|
||||
candidates.sort()
|
||||
_, tx, ty = candidates[0]
|
||||
return (tx, ty, "auto")
|
||||
|
||||
# Center room must reuse current spawn (1184, 928) = tile (37, 29)
|
||||
preferences = {
|
||||
(1, 1): (37, 29),
|
||||
}
|
||||
|
||||
print(f"{'Room':<8} {'tile':<10} {'pixel':<14} note")
|
||||
print("-" * 50)
|
||||
for rrow in range(3):
|
||||
for rcol in range(3):
|
||||
prefer = preferences.get((rcol, rrow))
|
||||
result = find_safe_in_room(rcol, rrow, prefer=prefer)
|
||||
if result is None:
|
||||
print(f"({rcol},{rrow}) NO SAFE SPAWN FOUND")
|
||||
continue
|
||||
tx, ty, kind = result
|
||||
# Spawn pos in pixels = (tx*32, ty*32). Verify under feet:
|
||||
below = at(tx, ty + 1)
|
||||
print(f"({rcol},{rrow}) ({tx:3d},{ty:3d}) ({tx*32:5d},{ty*32:4d}) below={below} ({kind})")
|
||||
@@ -0,0 +1,422 @@
|
||||
"""Convert Unity Tilemap data from SampleScene.unity into LevelTotalData.h.
|
||||
|
||||
Workflow:
|
||||
1. Parse the Unity scene YAML (no PyYAML dep — pure text scan,
|
||||
because the file is huge and uses Unity-specific tags).
|
||||
2. For each named Tilemap GameObject (e.g. "foreground", "background"),
|
||||
extract:
|
||||
- m_TileAssetArray : ordered list of tile asset GUIDs
|
||||
- m_Tiles : list of (x, y, m_TileIndex)
|
||||
3. Map each tile asset GUID -> a project TileId (uint16_t).
|
||||
4. Crop to a fixed window (origin + width + height, in Unity cell coords)
|
||||
and emit a C++ uint16_t[] array. Unity Y is up; the C++ array is
|
||||
row-major with row 0 at the TOP, so we flip Y on output.
|
||||
|
||||
Usage:
|
||||
python tools/unity_to_level.py
|
||||
|
||||
Edit FOREGROUND_GUID_TO_TILEID below to fill in the foreground mapping.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SCENE_PATH = os.path.join(REPO_ROOT, "SampleScene.unity")
|
||||
OUTPUT_PATH = os.path.join(
|
||||
REPO_ROOT, "src", "Apps", "LightGame", "src", "levels", "LevelTotalData.h"
|
||||
)
|
||||
|
||||
# Crop window in Unity cell coordinates. Aligned to the `background`
|
||||
# Tilemap's origin (-43, -24). LevelTotal is 96 wide x 54 tall.
|
||||
CROP_ORIGIN_X = -42
|
||||
CROP_ORIGIN_Y = -24
|
||||
CROP_WIDTH = 96
|
||||
CROP_HEIGHT = 54
|
||||
|
||||
# TileId values mirror src/Apps/LightGame/src/engine/TileIds.h
|
||||
class TileId:
|
||||
Empty = 0
|
||||
Background1 = 1
|
||||
Background2 = 2
|
||||
Background3 = 3
|
||||
Background4 = 4
|
||||
GroundTopGreen = 5
|
||||
GroundFillGreen = 6
|
||||
GroundTopGray = 7
|
||||
GroundFillGray = 8
|
||||
GroundTopPurple = 9
|
||||
GroundFillPurple = 10
|
||||
Platform = 11
|
||||
Spike = 12
|
||||
LightPlatformOff = 13
|
||||
LightPlatformOn = 14
|
||||
ShadowPlatformOff = 15
|
||||
ShadowPlatformOn = 16
|
||||
DoorClosed = 17
|
||||
DoorOpen = 18
|
||||
Coin = 19
|
||||
Flag = 20
|
||||
Checkpoint = 21
|
||||
SpikeCeiling = 22
|
||||
DecorVine = 23
|
||||
|
||||
# background Tilemap: m_TileAssetArray order (verified by user).
|
||||
# Indexes 0..3 in the array correspond to TileId 1..4.
|
||||
BACKGROUND_GUID_TO_TILEID: Dict[str, int] = {
|
||||
"7a416b45749852649890b58c40c94382": TileId.Background1,
|
||||
"3e398b33bfe59e8488e6622639e22680": TileId.Background2,
|
||||
"9c0d8493a349d2d4c9394cd4db6c144c": TileId.Background3,
|
||||
"c311fab5d43fea040b20ddf4109be8d4": TileId.Background4,
|
||||
}
|
||||
|
||||
# foreground Tilemap: m_TileAssetArray order (16 entries in scene).
|
||||
# >>> FILL THIS IN <<< — map each GUID to the matching TileId.* constant.
|
||||
# Use TileId.Empty (= 0) to drop a tile, or comment a line out to leave
|
||||
# it as Empty (the default).
|
||||
FOREGROUND_GUID_TO_TILEID: Dict[str, int] = {
|
||||
"fc664ce3f6082ba478b8d50ca2dce27e": 12,
|
||||
"47a0287d160d89e449cfb63ece6bc64d": 10,
|
||||
"9c787be8025ad3e4ea41faf8641b6542": 19,
|
||||
"d317ceed21dc2844cbe35d377a237065": 9,
|
||||
"228f030a961526742ac581cd160291f2": 1,
|
||||
"5276bbe304866324fbd94b343f5d5716": 1,
|
||||
"47822c4ae0c151848be71ecddbe3e363": 20,
|
||||
"9dae7bdac3edf7e4996e186ac7ea8d42": 8,
|
||||
"f3c8dcd4899bb144795bf74e61529d57": 6,
|
||||
"430a6d0aec468d94d929e481d5082741": 5,
|
||||
"dcd1ff73844513940941a7906732f790": 7,
|
||||
"8caba5ecd24bd3143831d4f04096921f": 17,
|
||||
"c0ea1f55905049745a90ea7d4772f95f": 22,
|
||||
"0f7b94bca5b66ea4e8bf37ac8b2c1528": 16,
|
||||
"67e0c92da848ffa49b3792338de741ac": 14,
|
||||
"1289993ae67654b48a59c5743728e507": 20,
|
||||
}
|
||||
|
||||
# Map Unity GameObject name -> tile id mapping table.
|
||||
LAYER_MAPPINGS: Dict[str, Dict[str, int]] = {
|
||||
"foreground": FOREGROUND_GUID_TO_TILEID,
|
||||
"background": BACKGROUND_GUID_TO_TILEID,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parser
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@dataclass
|
||||
class TilemapBlock:
|
||||
name: str
|
||||
asset_guids: List[str] = field(default_factory=list) # by m_TileIndex
|
||||
cells: List[Tuple[int, int, int]] = field(default_factory=list) # x, y, idx
|
||||
|
||||
|
||||
# Scene file blocks are separated by lines that start with "--- !u!".
|
||||
DOC_SEP = re.compile(r"^--- !u!(\d+) &(\d+)", re.MULTILINE)
|
||||
|
||||
|
||||
def split_documents(text: str) -> List[Tuple[int, int, str]]:
|
||||
"""Return a list of (class_id, file_id, body) for each YAML doc."""
|
||||
matches = list(DOC_SEP.finditer(text))
|
||||
docs: List[Tuple[int, int, str]] = []
|
||||
for i, m in enumerate(matches):
|
||||
start = m.end()
|
||||
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
||||
docs.append((int(m.group(1)), int(m.group(2)), text[start:end]))
|
||||
return docs
|
||||
|
||||
|
||||
def parse_gameobjects(docs) -> Dict[int, str]:
|
||||
"""Map GameObject fileID -> name."""
|
||||
name_re = re.compile(r"^\s*m_Name:\s*(.+?)\s*$", re.MULTILINE)
|
||||
out: Dict[int, str] = {}
|
||||
for class_id, file_id, body in docs:
|
||||
if class_id != 1: # GameObject
|
||||
continue
|
||||
m = name_re.search(body)
|
||||
if m:
|
||||
out[file_id] = m.group(1)
|
||||
return out
|
||||
|
||||
|
||||
GAMEOBJECT_REF_RE = re.compile(r"m_GameObject:\s*\{fileID:\s*(\d+)\}")
|
||||
TILE_FIRST_RE = re.compile(
|
||||
r"-\s*first:\s*\{x:\s*(-?\d+),\s*y:\s*(-?\d+),\s*z:\s*-?\d+\}\s*"
|
||||
r"second:\s*\n"
|
||||
r"\s*serializedVersion:\s*\d+\s*\n"
|
||||
r"\s*m_TileIndex:\s*(-?\d+)"
|
||||
)
|
||||
ASSET_ENTRY_RE = re.compile(
|
||||
r"-\s*m_RefCount:\s*(-?\d+)\s*\n\s*m_Data:\s*\{fileID:\s*\d+,\s*guid:\s*([0-9a-f]+),"
|
||||
)
|
||||
|
||||
|
||||
def parse_tilemaps(docs, go_names: Dict[int, str]) -> Dict[str, TilemapBlock]:
|
||||
"""Find Tilemap docs (class 1839735485) and bucket by parent GO name."""
|
||||
out: Dict[str, TilemapBlock] = {}
|
||||
for class_id, _file_id, body in docs:
|
||||
if class_id != 1839735485: # Tilemap
|
||||
continue
|
||||
m = GAMEOBJECT_REF_RE.search(body)
|
||||
if not m:
|
||||
continue
|
||||
go_id = int(m.group(1))
|
||||
name = go_names.get(go_id)
|
||||
if not name:
|
||||
continue
|
||||
|
||||
# Slice out the m_Tiles ... m_AnimatedTiles section so the cell regex
|
||||
# only sees real tile entries.
|
||||
tiles_start = body.find("m_Tiles:")
|
||||
tiles_end = body.find("m_AnimatedTiles", tiles_start if tiles_start >= 0 else 0)
|
||||
tiles_body = body[tiles_start:tiles_end] if tiles_start >= 0 else ""
|
||||
|
||||
# Slice the m_TileAssetArray section.
|
||||
asset_start = body.find("m_TileAssetArray:")
|
||||
asset_end = body.find("m_TileSpriteArray", asset_start if asset_start >= 0 else 0)
|
||||
asset_body = body[asset_start:asset_end] if asset_start >= 0 else ""
|
||||
|
||||
# Collect (refcount, guid) entries in their YAML order.
|
||||
asset_entries: List[Tuple[int, str]] = []
|
||||
for am in ASSET_ENTRY_RE.finditer(asset_body):
|
||||
asset_entries.append((int(am.group(1)), am.group(2)))
|
||||
|
||||
# Collect raw cell entries (x, y, raw_idx).
|
||||
raw_cells: List[Tuple[int, int, int]] = []
|
||||
for tm in TILE_FIRST_RE.finditer(tiles_body):
|
||||
raw_cells.append((int(tm.group(1)), int(tm.group(2)), int(tm.group(3))))
|
||||
|
||||
# Unity does NOT renumber m_TileIndex when entries are removed from
|
||||
# m_TileAssetArray, so the YAML index of an entry is not necessarily
|
||||
# equal to the m_TileIndex referencing it. Reconstruct the mapping
|
||||
# by aligning the YAML-order asset entries with the *sorted distinct*
|
||||
# m_TileIndex values that share the same refcount/usage-count.
|
||||
from collections import Counter as _Counter
|
||||
idx_counts = _Counter(c[2] for c in raw_cells)
|
||||
sorted_idxs = sorted(idx_counts.keys())
|
||||
|
||||
block = TilemapBlock(name=name)
|
||||
if len(sorted_idxs) == len(asset_entries):
|
||||
# Pair entries in array order with idx values in ascending order.
|
||||
# Sanity-check that refcount matches usage count for each pair.
|
||||
for entry, idx in zip(asset_entries, sorted_idxs):
|
||||
refcount, guid = entry
|
||||
actual = idx_counts[idx]
|
||||
if refcount != actual:
|
||||
print(
|
||||
f" [warn] '{name}' refcount mismatch for guid {guid[:8]}: "
|
||||
f"asset refcount={refcount} but idx={idx} appears {actual} times",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# Grow asset_guids so [idx] -> guid works directly.
|
||||
while len(block.asset_guids) <= idx:
|
||||
block.asset_guids.append("")
|
||||
block.asset_guids[idx] = guid
|
||||
else:
|
||||
print(
|
||||
f" [warn] '{name}' has {len(asset_entries)} asset entries but "
|
||||
f"{len(sorted_idxs)} distinct m_TileIndex values; falling back "
|
||||
f"to positional mapping (results may be wrong)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for entry in asset_entries:
|
||||
block.asset_guids.append(entry[1])
|
||||
|
||||
block.cells = raw_cells
|
||||
out[name] = block
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conversion
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def block_to_grid(
|
||||
block: TilemapBlock,
|
||||
guid_to_tile: Dict[str, int],
|
||||
origin_x: int,
|
||||
origin_y: int,
|
||||
width: int,
|
||||
height: int,
|
||||
) -> List[List[int]]:
|
||||
"""Convert a TilemapBlock to a row-major grid (row 0 = top).
|
||||
|
||||
Cells outside the crop window are dropped. Cells whose tile asset has no
|
||||
mapping default to TileId.Empty (and a warning is printed once per GUID).
|
||||
"""
|
||||
grid = [[0 for _ in range(width)] for _ in range(height)]
|
||||
unmapped: Dict[str, int] = {}
|
||||
|
||||
for x, y, idx in block.cells:
|
||||
if idx < 0 or idx >= len(block.asset_guids):
|
||||
continue
|
||||
guid = block.asset_guids[idx]
|
||||
tile_id = guid_to_tile.get(guid)
|
||||
if tile_id is None:
|
||||
unmapped[guid] = unmapped.get(guid, 0) + 1
|
||||
continue
|
||||
if tile_id == 0:
|
||||
continue
|
||||
|
||||
col = x - origin_x
|
||||
# Unity Y is up; flip so row 0 is the top of the array.
|
||||
row = (origin_y + height - 1) - y
|
||||
if 0 <= col < width and 0 <= row < height:
|
||||
grid[row][col] = tile_id
|
||||
|
||||
if unmapped:
|
||||
print(
|
||||
f" [warn] {len(unmapped)} unmapped GUID(s) in '{block.name}' "
|
||||
f"(treated as Empty):",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for guid, count in sorted(unmapped.items(), key=lambda kv: -kv[1]):
|
||||
print(f" {guid} x{count}", file=sys.stderr)
|
||||
|
||||
return grid
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# C++ emitter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HEADER_TEMPLATE = """#pragma once
|
||||
|
||||
#include "LevelData.h"
|
||||
#include "tile_atlas.h"
|
||||
|
||||
namespace LightGame
|
||||
{{
|
||||
namespace LevelTotal
|
||||
{{
|
||||
static const int32_t kMapWidth = {width};
|
||||
static const int32_t kMapHeight = {height};
|
||||
static const int32_t kTileSize = 32;
|
||||
|
||||
static const uint16_t tiles[] = {{
|
||||
{tiles_body}
|
||||
}};
|
||||
|
||||
static const uint16_t background_tiles[] = {{
|
||||
{background_body}
|
||||
}};
|
||||
|
||||
static const ObjectSpawn spawns[] = {{
|
||||
{{ GameObjectType::Player, 69, 29, 0, 0, TriggerAction::None, 0 }},
|
||||
}};
|
||||
|
||||
static const RenderData::Image kAtlas(tile_atlas_pixels, tile_atlas_width, tile_atlas_height);
|
||||
|
||||
static const LevelData data = {{
|
||||
tiles,
|
||||
kMapWidth,
|
||||
kMapHeight,
|
||||
kTileSize,
|
||||
&kAtlas,
|
||||
tile_atlas_columns,
|
||||
spawns,
|
||||
1,
|
||||
nullptr,
|
||||
0,
|
||||
Math::Vector2Int({spawn_px_x}, {spawn_px_y}),
|
||||
0,
|
||||
0,
|
||||
{bounds_max_x},
|
||||
{bounds_max_y},
|
||||
background_tiles
|
||||
}};
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
|
||||
|
||||
def format_grid(grid: List[List[int]], indent: str = " ") -> str:
|
||||
rows = []
|
||||
for row in grid:
|
||||
rows.append(indent + ", ".join(str(v) for v in row) + ",")
|
||||
if rows:
|
||||
rows[-1] = rows[-1].rstrip(",")
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def emit_header(
|
||||
fg_grid: List[List[int]],
|
||||
bg_grid: List[List[int]],
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int = 32,
|
||||
) -> str:
|
||||
spawn_tile_x, spawn_tile_y = 69, 29 # preserved from current LevelTotal
|
||||
return HEADER_TEMPLATE.format(
|
||||
width=width,
|
||||
height=height,
|
||||
tiles_body=format_grid(fg_grid),
|
||||
background_body=format_grid(bg_grid),
|
||||
spawn_px_x=spawn_tile_x * tile_size,
|
||||
spawn_px_y=spawn_tile_y * tile_size,
|
||||
bounds_max_x=width * tile_size,
|
||||
bounds_max_y=height * tile_size,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main() -> int:
|
||||
if not os.path.isfile(SCENE_PATH):
|
||||
print(f"scene file not found: {SCENE_PATH}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
with open(SCENE_PATH, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
print(f"reading {SCENE_PATH} ({len(text)} bytes)")
|
||||
docs = split_documents(text)
|
||||
go_names = parse_gameobjects(docs)
|
||||
tilemaps = parse_tilemaps(docs, go_names)
|
||||
|
||||
print(f"found Tilemaps: {sorted(tilemaps.keys())}")
|
||||
for name, block in tilemaps.items():
|
||||
print(f" {name}: {len(block.cells)} cells, {len(block.asset_guids)} tile assets")
|
||||
|
||||
missing = [n for n in ("foreground", "background") if n not in tilemaps]
|
||||
if missing:
|
||||
print(f"missing Tilemap layers: {missing}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
fg_grid = block_to_grid(
|
||||
tilemaps["foreground"],
|
||||
FOREGROUND_GUID_TO_TILEID,
|
||||
CROP_ORIGIN_X, CROP_ORIGIN_Y, CROP_WIDTH, CROP_HEIGHT,
|
||||
)
|
||||
bg_grid = block_to_grid(
|
||||
tilemaps["background"],
|
||||
BACKGROUND_GUID_TO_TILEID,
|
||||
CROP_ORIGIN_X, CROP_ORIGIN_Y, CROP_WIDTH, CROP_HEIGHT,
|
||||
)
|
||||
|
||||
if not FOREGROUND_GUID_TO_TILEID:
|
||||
print(
|
||||
"\n[note] FOREGROUND_GUID_TO_TILEID is empty — foreground will be all 0.\n"
|
||||
" Edit tools/unity_to_level.py and re-run once you have the mapping.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
out = emit_header(fg_grid, bg_grid, CROP_WIDTH, CROP_HEIGHT)
|
||||
with open(OUTPUT_PATH, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(out)
|
||||
print(f"wrote {OUTPUT_PATH}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user