Files
modbison/scripts/editor/chunk_editor_model.gd
T
daddy 7ecf0d53bd
checkpoint / checkpoint (push) Failing after 3h14m9s
Complete M11 Creator Studio and M12 Trust & Safety for PC5
- Creator Studio: Visual behavior & property inspector, GLB model importer, binary persistence
- Trust & Safety: 18+ TOS confirmation (DOB removed), creator guidelines & briefing card on join, in-game reporting HUD
- Test Harness: Add --filter, --skip-backend, --skip-godot to run_checkpoint_tests.py
2026-09-17 23:13:57 +00:00

195 lines
6.3 KiB
GDScript

class_name ChunkEditorModel
extends RefCounted
## In-memory chunk being edited (docs/architecture/editor.md §7.1's
## `ChunkEditorModel`).
##
## Wraps a `ChunkDataDecoder.ChunkPayload` directly rather than inventing a
## parallel data model -- that payload is already the exact structure that
## round-trips through `ChunkDataDecoder.encode_binary()` /
## `decode_binary()` (world.md §3.6, and the interop bugs fixed in that file
## are proof this contract is exercised for real). Editing this model and
## re-encoding it must always produce a valid, byte-compatible chunk, since
## the sibling save-path task depends on that being true.
##
## Every method here is a "raw" mutator: it changes the payload and returns
## whatever an undo needs to reverse the change, but it does **not** know
## about undo/redo itself. `scripts/editor/editor_core.gd`'s tools
## (scripts/editor/tools/*.gd) are the only things that call these -- they
## wrap each call in an `EditorUndoRedoStack` action. Nothing here should be
## called directly by UI code; that would bypass undo/redo silently.
var payload: ChunkDataDecoder.ChunkPayload
## Monotonic counter seeded from the highest existing "obj_NNNN"-style
## numeric suffix already in the chunk, so freshly generated ids never
## collide with ids that arrived from a decoded chunk.
var _next_object_seq: int = 0
func _init(p_payload: ChunkDataDecoder.ChunkPayload = null) -> void:
payload = p_payload if p_payload != null else ChunkDataDecoder.ChunkPayload.new()
_seed_object_sequence()
func _seed_object_sequence() -> void:
for obj in payload.objects:
var n := _numeric_suffix(obj.id)
if n >= _next_object_seq:
_next_object_seq = n + 1
static func _numeric_suffix(id: String) -> int:
var digits := ""
for i in range(id.length() - 1, -1, -1):
var c: String = id[i]
if c.is_valid_int():
digits = c + digits
else:
break
return int(digits) if digits != "" else -1
## Build a fresh, empty single-layer chunk model -- convenient for tests and
## for a "new chunk" starting point in the in-game editor before a real
## chunk has been loaded from the backend (loading real chunks over HTTP is
## HttpChunkSource's job; this just gives the editor something valid to
## operate on).
static func create_empty(width: int = 32, height: int = 32, world_id: String = "",
room_id: String = "main", coord: Vector3i = Vector3i.ZERO) -> ChunkEditorModel:
var layer := ChunkDataDecoder.TileLayerData.new()
layer.name = "ground"
layer.layer_id = 0
layer.width = width
layer.height = height
layer.depth = 1
layer.palette = [
TileDef.new(0, "", false, 0, 1),
TileDef.new(1, "tiles/default", true, 0, 1),
]
var tiles := PackedInt32Array()
tiles.resize(width * height)
layer.tiles = tiles
var payload := ChunkDataDecoder.ChunkPayload.new()
payload.world_id = world_id
payload.room_id = room_id
payload.coord = coord
payload.bounds = AABB(Vector3(coord.x * width, coord.y * height, coord.z), Vector3(width, height, 1))
payload.tile_layers = [layer]
payload.revision = 0
payload.created_at = Time.get_unix_time_from_system()
payload.updated_at = payload.created_at
return ChunkEditorModel.new(payload)
static func from_binary(data: PackedByteArray) -> ChunkEditorModel:
var decoded := ChunkDataDecoder.decode_binary(data)
if decoded == null:
return null
return ChunkEditorModel.new(decoded)
func encode_binary() -> PackedByteArray:
return ChunkDataDecoder.encode_binary(payload)
# ───────────────────────── tiles ─────────────────────────
func get_layer(layer_idx: int) -> ChunkDataDecoder.TileLayerData:
if layer_idx < 0 or layer_idx >= payload.tile_layers.size():
return null
return payload.tile_layers[layer_idx]
func is_tile_in_bounds(layer_idx: int, x: int, y: int, z: int = 0) -> bool:
var layer := get_layer(layer_idx)
if layer == null:
return false
return x >= 0 and x < layer.width and y >= 0 and y < layer.height and z >= 0 and z < layer.depth
func get_tile_raw(layer_idx: int, x: int, y: int, z: int = 0) -> int:
var layer := get_layer(layer_idx)
if layer == null:
return 0
return layer.get_tile(x, y, z)
## Raw setter -- undo-unaware. Only ever called (in both directions) by
## BrushTool/EraseTool via EditorUndoRedoStack.
func set_tile_raw(layer_idx: int, x: int, y: int, z: int, tile_id: int) -> void:
var layer := get_layer(layer_idx)
if layer == null:
return
layer.set_tile(x, y, z, tile_id)
# ───────────────────────── objects ─────────────────────────
func find_object(object_id: String) -> ChunkDataDecoder.WorldObjectData:
for obj in payload.objects:
if obj.id == object_id:
return obj
return null
func get_object_index(object_id: String) -> int:
for i in range(payload.objects.size()):
if payload.objects[i].id == object_id:
return i
return -1
func generate_object_id() -> String:
var id := "obj_%04d" % _next_object_seq
_next_object_seq += 1
return id
## Raw mutators -- undo-unaware. Only ever called (in both directions) by
## ObjectPlaceTool/EraseTool/TransformTool via EditorUndoRedoStack.
func add_object_raw(obj: ChunkDataDecoder.WorldObjectData) -> void:
payload.objects.append(obj)
func insert_object_raw(obj: ChunkDataDecoder.WorldObjectData, index: int) -> void:
var clamped: int = clampi(index, 0, payload.objects.size())
payload.objects.insert(clamped, obj)
func remove_object_raw(object_id: String) -> ChunkDataDecoder.WorldObjectData:
var idx := get_object_index(object_id)
if idx == -1:
return null
var obj := payload.objects[idx]
payload.objects.remove_at(idx)
return obj
func set_object_position_raw(object_id: String, position: Vector3) -> void:
var obj := find_object(object_id)
if obj != null:
obj.position = position
func set_object_rotation_raw(object_id: String, rotation: Vector4) -> void:
var obj := find_object(object_id)
if obj != null:
obj.rotation = rotation
func set_object_scale_raw(object_id: String, scale: Vector3) -> void:
var obj := find_object(object_id)
if obj != null:
obj.scale = scale
func set_object_property_raw(object_id: String, key: String, value: Variant) -> void:
var obj := find_object(object_id)
if obj != null:
if value == null:
obj.properties.erase(key)
else:
obj.properties[key] = value