Added Glitch Room Source code to Gitty
This commit is contained in:
commit
1ac9eb5644
505 files changed
+28504
No files matched your search
@@ -0,0 +1,4 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Normalize EOL for all files that Git considers text files.
|
||||||
|
* text=auto eol=lf
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Godot 4+ specific ignores
|
||||||
|
.godot/
|
||||||
|
/android/
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
@tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
|
||||||
|
const WORKSPACE_SCENE := preload(
|
||||||
|
"res://editor/workspace/EscapeRoomWorkspace.tscn"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DATABASE_SETTING := (
|
||||||
|
"arcade_escape/project_database"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ROOM_SETTING := (
|
||||||
|
"arcade_escape/default_room"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var workspace: EscapeRoomWorkspace
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
_ensure_project_settings()
|
||||||
|
_create_workspace()
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
_destroy_workspace()
|
||||||
|
|
||||||
|
|
||||||
|
func _has_main_screen() -> bool:
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _make_visible(visible: bool) -> void:
|
||||||
|
if workspace != null:
|
||||||
|
workspace.visible = visible
|
||||||
|
|
||||||
|
|
||||||
|
func _get_plugin_name() -> String:
|
||||||
|
return "Escape Room"
|
||||||
|
|
||||||
|
|
||||||
|
func _get_plugin_icon() -> Texture2D:
|
||||||
|
return EditorInterface.get_editor_theme().get_icon(
|
||||||
|
"Node2D",
|
||||||
|
"EditorIcons"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_workspace() -> void:
|
||||||
|
|
||||||
|
if workspace != null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var database := _load_project_database()
|
||||||
|
var room := _load_default_room()
|
||||||
|
|
||||||
|
if database == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
workspace = WORKSPACE_SCENE.instantiate()
|
||||||
|
|
||||||
|
workspace.set_undo_redo_manager(
|
||||||
|
get_undo_redo()
|
||||||
|
)
|
||||||
|
|
||||||
|
EditorInterface.get_editor_main_screen().add_child(
|
||||||
|
workspace
|
||||||
|
)
|
||||||
|
|
||||||
|
workspace.set_anchors_and_offsets_preset(
|
||||||
|
Control.PRESET_FULL_RECT
|
||||||
|
)
|
||||||
|
|
||||||
|
if not workspace.configure(
|
||||||
|
database,
|
||||||
|
room
|
||||||
|
):
|
||||||
|
push_error(
|
||||||
|
"Arcade Escape editor workspace failed to configure."
|
||||||
|
)
|
||||||
|
|
||||||
|
_make_visible(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _destroy_workspace() -> void:
|
||||||
|
if workspace == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
if is_instance_valid(workspace):
|
||||||
|
workspace.queue_free()
|
||||||
|
|
||||||
|
workspace = null
|
||||||
|
|
||||||
|
func _ensure_project_settings() -> void:
|
||||||
|
|
||||||
|
if not ProjectSettings.has_setting(
|
||||||
|
DATABASE_SETTING
|
||||||
|
):
|
||||||
|
ProjectSettings.set_setting(
|
||||||
|
DATABASE_SETTING,
|
||||||
|
"res://data/project_database.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectSettings.set_initial_value(
|
||||||
|
DATABASE_SETTING,
|
||||||
|
"res://data/project_database.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ProjectSettings.has_setting(
|
||||||
|
ROOM_SETTING
|
||||||
|
):
|
||||||
|
ProjectSettings.set_setting(
|
||||||
|
ROOM_SETTING,
|
||||||
|
"res://data/rooms/arcade_room.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectSettings.set_initial_value(
|
||||||
|
ROOM_SETTING,
|
||||||
|
"res://data/rooms/arcade_room.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
ProjectSettings.save()
|
||||||
|
|
||||||
|
func _load_project_database() -> ProjectDatabase:
|
||||||
|
|
||||||
|
var path := String(
|
||||||
|
ProjectSettings.get_setting(
|
||||||
|
DATABASE_SETTING,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if path.is_empty():
|
||||||
|
push_error(
|
||||||
|
"Arcade Escape project database path is empty."
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var resource := ResourceLoader.load(
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
if not resource is ProjectDatabase:
|
||||||
|
push_error(
|
||||||
|
"Arcade Escape expected ProjectDatabase at '%s'."
|
||||||
|
% path
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return resource as ProjectDatabase
|
||||||
|
|
||||||
|
func _load_default_room() -> RoomDefinition:
|
||||||
|
|
||||||
|
var path := String(
|
||||||
|
ProjectSettings.get_setting(
|
||||||
|
ROOM_SETTING,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if path.is_empty():
|
||||||
|
push_error(
|
||||||
|
"Arcade Escape default room path is empty."
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var resource := ResourceLoader.load(
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
if not resource is RoomDefinition:
|
||||||
|
push_error(
|
||||||
|
"Arcade Escape expected RoomDefinition at '%s'."
|
||||||
|
% path
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
return resource as RoomDefinition
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dey5xa25bteww
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="Escape Room"
|
||||||
|
description="Escape room authoring tools."
|
||||||
|
author="Escape Gallery"
|
||||||
|
version="0.1.0"
|
||||||
|
script="escape_room_plugin.gd"
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 536 B |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://cistuspr5bf8h"
|
||||||
|
path="res://.godot/imported/tex_cabinet.png-f6576ddd6a1e314af47e0494fc6c164e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://art/devices/tex_cabinet.png"
|
||||||
|
dest_files=["res://.godot/imported/tex_cabinet.png-f6576ddd6a1e314af47e0494fc6c164e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 437 B |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b7i278fi27mbh"
|
||||||
|
path="res://.godot/imported/tex_coin_release.png-3fe2091a6961abd0e69cf82228b39c9e.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://art/devices/tex_coin_release.png"
|
||||||
|
dest_files=["res://.godot/imported/tex_coin_release.png-3fe2091a6961abd0e69cf82228b39c9e.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 446 B |
@@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://b7piqhhrvttw8"
|
||||||
|
path="res://.godot/imported/tex_light.png-80cad21869b9d5cc56aa8fcfdd96c808.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://art/devices/tex_light.png"
|
||||||
|
dest_files=["res://.godot/imported/tex_light.png-80cad21869b9d5cc56aa8fcfdd96c808.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
extends Node
|
||||||
|
signal changed
|
||||||
|
## Machine-local bindings; joypads are resolved by identity, never "All Devices".
|
||||||
|
const FILE := "user://arcade_controls.cfg"
|
||||||
|
const STEPS := [
|
||||||
|
["arcade_start", "Press PLAYER ONE START"],
|
||||||
|
["arcade_menu", "Press PLAYER ONE SELECT / RETURN TO MENU"],
|
||||||
|
["arcade_up", "Move PLAYER ONE joystick UP"],
|
||||||
|
["arcade_down", "Move PLAYER ONE joystick DOWN"],
|
||||||
|
["arcade_left", "Move PLAYER ONE joystick LEFT"],
|
||||||
|
["arcade_right", "Move PLAYER ONE joystick RIGHT"],
|
||||||
|
["arcade_action_1", "Press FIRE / LEFT MISSILE BATTERY"],
|
||||||
|
["arcade_action_2", "Press SUPERZAPPER / CENTER MISSILE BATTERY"],
|
||||||
|
["arcade_action_3", "Press RIGHT MISSILE BATTERY"],
|
||||||
|
["robotron_fire_up", "Choose ROBOTRON FIRE UP — button or second joystick"],
|
||||||
|
["robotron_fire_down", "Choose ROBOTRON FIRE DOWN — button or second joystick"],
|
||||||
|
["robotron_fire_left", "Choose ROBOTRON FIRE LEFT — button or second joystick"],
|
||||||
|
["robotron_fire_right", "Choose ROBOTRON FIRE RIGHT — button or second joystick"],
|
||||||
|
]
|
||||||
|
var bindings: Dictionary = {}
|
||||||
|
var problem := ""
|
||||||
|
var path := FILE
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
load_profile()
|
||||||
|
Input.joy_connection_changed.connect(_devices_changed)
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
handle_quit(event)
|
||||||
|
|
||||||
|
func handle_quit(event: InputEvent) -> bool:
|
||||||
|
if event is InputEventKey and event.pressed and not event.echo and (event.physical_keycode == KEY_ESCAPE or event.keycode == KEY_ESCAPE):
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
get_tree().quit()
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func devices() -> Array:
|
||||||
|
var found: Array = []
|
||||||
|
for id in Input.get_connected_joypads():
|
||||||
|
found.append({"id": id, "guid": Input.get_joy_guid(id), "name": Input.get_joy_name(id)})
|
||||||
|
return found
|
||||||
|
|
||||||
|
func matches(identity: Dictionary, connected: Array) -> Array:
|
||||||
|
return connected.filter(func(d): return d.guid == identity.guid and d.name == identity.name)
|
||||||
|
|
||||||
|
func encode(event: InputEvent, connected: Array) -> Dictionary:
|
||||||
|
var result := {}
|
||||||
|
if event is InputEventKey:
|
||||||
|
if event.physical_keycode in [KEY_ESCAPE, KEY_F1]: return {}
|
||||||
|
result = {"kind": "key", "code": event.physical_keycode, "logical": event.keycode if event.physical_keycode == 0 else 0}
|
||||||
|
elif event is InputEventJoypadButton:
|
||||||
|
result = {"kind": "button", "code": event.button_index}
|
||||||
|
elif event is InputEventJoypadMotion:
|
||||||
|
if absf(event.axis_value) < .65: return {}
|
||||||
|
result = {"kind": "axis", "code": event.axis, "sign": -1 if event.axis_value < 0 else 1}
|
||||||
|
elif event is InputEventMouseButton:
|
||||||
|
if event.button_index > MOUSE_BUTTON_MIDDLE: return {}
|
||||||
|
return {"kind": "mouse", "code": event.button_index}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||||
|
var current := connected.filter(func(d): return d.id == event.device)
|
||||||
|
if current.is_empty(): return {}
|
||||||
|
var identity: Dictionary = current[0].duplicate()
|
||||||
|
var same := matches(identity, connected)
|
||||||
|
identity["slot"] = same.find(current[0])
|
||||||
|
identity["count"] = same.size()
|
||||||
|
identity.erase("id")
|
||||||
|
result["controller"] = identity
|
||||||
|
return result
|
||||||
|
|
||||||
|
func resolve(binding: Dictionary, connected: Array) -> int:
|
||||||
|
var identity: Dictionary = binding.controller
|
||||||
|
var same := matches(identity, connected)
|
||||||
|
# An absent twin must not silently hand its controls to the remaining twin.
|
||||||
|
if same.size() != int(identity.count): return -1
|
||||||
|
return int(same[int(identity.slot)].id) if int(identity.slot) < same.size() else -1
|
||||||
|
|
||||||
|
func decode(binding: Dictionary, connected: Array) -> InputEvent:
|
||||||
|
var event: InputEvent
|
||||||
|
match binding.kind:
|
||||||
|
"key":
|
||||||
|
event = InputEventKey.new()
|
||||||
|
event.physical_keycode = int(binding.code)
|
||||||
|
event.keycode = int(binding.get("logical", 0))
|
||||||
|
"mouse":
|
||||||
|
event = InputEventMouseButton.new()
|
||||||
|
event.button_index = int(binding.code)
|
||||||
|
"button":
|
||||||
|
event = InputEventJoypadButton.new()
|
||||||
|
event.button_index = int(binding.code)
|
||||||
|
"axis":
|
||||||
|
event = InputEventJoypadMotion.new()
|
||||||
|
event.axis = int(binding.code)
|
||||||
|
event.axis_value = float(binding.sign)
|
||||||
|
_:
|
||||||
|
return null
|
||||||
|
if binding.has("controller"):
|
||||||
|
var id := resolve(binding, connected)
|
||||||
|
if id < 0: return null
|
||||||
|
event.device = id
|
||||||
|
return event
|
||||||
|
|
||||||
|
func valid(data: Dictionary) -> bool:
|
||||||
|
for step in STEPS:
|
||||||
|
if not data.get(step[0]) is Dictionary: return false
|
||||||
|
var b: Dictionary = data[step[0]]
|
||||||
|
if b.get("kind", "") not in ["key", "mouse", "button", "axis"]: return false
|
||||||
|
if not b.get("code") is int or b.code < 0: return false
|
||||||
|
if b.kind == "axis" and b.get("sign", 0) not in [-1, 1]: return false
|
||||||
|
if b.kind in ["axis", "button"]:
|
||||||
|
if not b.get("controller") is Dictionary: return false
|
||||||
|
var c: Dictionary = b.controller
|
||||||
|
if not c.get("guid") is String or not c.get("name") is String: return false
|
||||||
|
if not c.get("slot") is int or not c.get("count") is int: return false
|
||||||
|
if c.slot < 0 or c.count <= c.slot: return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
func load_profile() -> bool:
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
if config.load(path) != OK: return false
|
||||||
|
var data = config.get_value("controls", "bindings", {})
|
||||||
|
if config.get_value("controls", "version", 0) != 1 or not data is Dictionary or not valid(data):
|
||||||
|
problem = "Saved controls could not be read. Please run setup again."
|
||||||
|
return false
|
||||||
|
bindings = data
|
||||||
|
apply_profile()
|
||||||
|
return true
|
||||||
|
|
||||||
|
func save_profile(data: Dictionary) -> Error:
|
||||||
|
if not valid(data): return ERR_INVALID_DATA
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
config.set_value("controls", "version", 1)
|
||||||
|
config.set_value("controls", "bindings", data)
|
||||||
|
var error := config.save(path + ".tmp")
|
||||||
|
if error != OK: return error
|
||||||
|
error = DirAccess.rename_absolute(ProjectSettings.globalize_path(path + ".tmp"), ProjectSettings.globalize_path(path))
|
||||||
|
if error != OK: return error
|
||||||
|
bindings = data.duplicate(true)
|
||||||
|
apply_profile()
|
||||||
|
return OK
|
||||||
|
|
||||||
|
func apply_profile() -> void:
|
||||||
|
if bindings.is_empty(): return
|
||||||
|
problem = ""
|
||||||
|
var connected := devices()
|
||||||
|
for action in bindings:
|
||||||
|
var event := decode(bindings[action], connected)
|
||||||
|
replace_action(action, event)
|
||||||
|
if action in ["arcade_up", "arcade_down", "arcade_left", "arcade_right"]:
|
||||||
|
replace_action(action.replace("arcade_", "robotron_move_"), event)
|
||||||
|
if event == null:
|
||||||
|
problem = "A saved controller is missing or the controller order changed. Reconnect it or run Control Setup."
|
||||||
|
elif bindings[action].has("controller") and int(bindings[action].controller.count) > 1 and problem.is_empty():
|
||||||
|
problem = "Identical controllers detected: check PLAYER ONE after reconnecting USB devices. Run setup if they swap."
|
||||||
|
changed.emit()
|
||||||
|
|
||||||
|
func replace_action(action: String, event: InputEvent) -> void:
|
||||||
|
Input.action_release(action)
|
||||||
|
InputMap.action_erase_events(action)
|
||||||
|
InputMap.action_set_deadzone(action, .35)
|
||||||
|
if event != null: InputMap.action_add_event(action, event)
|
||||||
|
|
||||||
|
func _devices_changed(_device: int, _connected: bool) -> void:
|
||||||
|
apply_profile.call_deferred()
|
||||||
|
|
||||||
|
func describe(binding: Dictionary) -> String:
|
||||||
|
var event := decode(binding, devices())
|
||||||
|
var text := event.as_text() if event != null else "%s %s (not connected)" % [binding.kind, binding.code]
|
||||||
|
if binding.has("controller"):
|
||||||
|
text += " — %s #%d" % [binding.controller.name, int(binding.controller.slot) + 1]
|
||||||
|
return text
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b58xhfwt4fvby
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
class_name ArcadeGame
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
signal game_started
|
||||||
|
signal game_won
|
||||||
|
signal game_lost
|
||||||
|
signal game_reset
|
||||||
|
|
||||||
|
|
||||||
|
var is_running: bool = false
|
||||||
|
var run_config: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func configure(config: Dictionary = {}) -> void:
|
||||||
|
run_config = config
|
||||||
|
|
||||||
|
|
||||||
|
func start_game() -> bool:
|
||||||
|
if is_running:
|
||||||
|
return false
|
||||||
|
|
||||||
|
is_running = true
|
||||||
|
game_started.emit()
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func win_game() -> void:
|
||||||
|
if not is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_running = false
|
||||||
|
game_won.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func lose_game() -> void:
|
||||||
|
if not is_running:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_running = false
|
||||||
|
game_lost.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func reset_game() -> void:
|
||||||
|
is_running = false
|
||||||
|
game_reset.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func restart_game() -> void:
|
||||||
|
reset_game()
|
||||||
|
start_game()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bv62i5q5hvkqd
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
@tool
|
||||||
|
class_name GameDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var game_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
|
||||||
|
@export var scene: PackedScene
|
||||||
|
|
||||||
|
@export var required_capabilities: Array[StringName] = []
|
||||||
|
@export var optional_capabilities: Array[StringName] = []
|
||||||
|
## Require every common capability, plus every capability in at least one alternative.
|
||||||
|
@export var control_alternatives: Array[PackedStringArray] = []
|
||||||
|
|
||||||
|
@export_group("Events")
|
||||||
|
@export var events: Array[GameEventDefinition] = []
|
||||||
|
|
||||||
|
|
||||||
|
func can_run_on(
|
||||||
|
cabinet: CabinetProfile
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
for capability in required_capabilities:
|
||||||
|
if not cabinet.has_capability(capability):
|
||||||
|
return false
|
||||||
|
|
||||||
|
if control_alternatives.is_empty(): return true
|
||||||
|
for alternative in control_alternatives:
|
||||||
|
var supported := true
|
||||||
|
for capability in alternative:
|
||||||
|
if not cabinet.has_capability(StringName(capability)): supported = false
|
||||||
|
if supported: return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func get_event(
|
||||||
|
event_id: StringName
|
||||||
|
) -> GameEventDefinition:
|
||||||
|
|
||||||
|
for event in events:
|
||||||
|
if event == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if event.event_id == event_id:
|
||||||
|
return event
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func has_event(
|
||||||
|
event_id: StringName
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
return get_event(event_id) != null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://o538v0kdvs0j
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@tool
|
||||||
|
class_name GameEventDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var event_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
@export_multiline var description: String = ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://byegr76ey4613
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
@tool
|
||||||
|
class_name CabinetProfile
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var cabinet_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
|
||||||
|
@export var capabilities: Array[StringName] = []
|
||||||
|
|
||||||
|
@export var hardware_config: HardwareConfig
|
||||||
|
|
||||||
|
const FEET_TO_METERS: float = 0.3048
|
||||||
|
const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Stored internally in meters for compatibility with the
|
||||||
|
# existing room/layout system.
|
||||||
|
#
|
||||||
|
@export_storage
|
||||||
|
var footprint_width_m: float = 0.80
|
||||||
|
|
||||||
|
@export_storage
|
||||||
|
var footprint_depth_m: float = 0.75
|
||||||
|
|
||||||
|
@export_storage
|
||||||
|
var height_m: float = 1.80
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Designer-facing dimensions.
|
||||||
|
#
|
||||||
|
@export_group("Physical Dimensions")
|
||||||
|
|
||||||
|
@export_range(0.25, 12.0, 0.01, "suffix: ft")
|
||||||
|
var footprint_width_ft: float:
|
||||||
|
get:
|
||||||
|
return (
|
||||||
|
footprint_width_m
|
||||||
|
* METERS_TO_FEET
|
||||||
|
)
|
||||||
|
set(value):
|
||||||
|
footprint_width_m = (
|
||||||
|
value
|
||||||
|
* FEET_TO_METERS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@export_range(0.25, 12.0, 0.01, "suffix: ft")
|
||||||
|
var footprint_depth_ft: float:
|
||||||
|
get:
|
||||||
|
return (
|
||||||
|
footprint_depth_m
|
||||||
|
* METERS_TO_FEET
|
||||||
|
)
|
||||||
|
set(value):
|
||||||
|
footprint_depth_m = (
|
||||||
|
value
|
||||||
|
* FEET_TO_METERS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@export_range(0.25, 20.0, 0.01, "suffix: ft")
|
||||||
|
var height_ft: float:
|
||||||
|
get:
|
||||||
|
return (
|
||||||
|
height_m
|
||||||
|
* METERS_TO_FEET
|
||||||
|
)
|
||||||
|
set(value):
|
||||||
|
height_m = (
|
||||||
|
value
|
||||||
|
* FEET_TO_METERS
|
||||||
|
)
|
||||||
|
|
||||||
|
@export_group("Physical Appearance")
|
||||||
|
|
||||||
|
@export var editor_texture: Texture2D = null
|
||||||
|
|
||||||
|
@export_range(0.1, 4.0, 0.05)
|
||||||
|
var editor_texture_scale: float = 1.0
|
||||||
|
|
||||||
|
func has_capability(capability: StringName) -> bool:
|
||||||
|
return capabilities.has(capability)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c0jj5j5loqsma
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@tool
|
||||||
|
class_name HardwareAction
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
enum ActionType {
|
||||||
|
DIGITAL_OUTPUT,
|
||||||
|
PULSE_OUTPUT,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@export var action_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
|
||||||
|
@export var action_type: ActionType = ActionType.PULSE_OUTPUT
|
||||||
|
|
||||||
|
@export_range(0, 1000, 1)
|
||||||
|
var gpio_pin: int = 17
|
||||||
|
|
||||||
|
@export_range(1, 10000, 1)
|
||||||
|
var pulse_duration_ms: int = 250
|
||||||
|
|
||||||
|
@export var active_high: bool = true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c7eu1n5yey1x4
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
@tool
|
||||||
|
class_name HardwareConfig
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var controller_id: StringName = &""
|
||||||
|
@export var actions: Array[HardwareAction] = []
|
||||||
|
|
||||||
|
|
||||||
|
func get_action(action_id: StringName) -> HardwareAction:
|
||||||
|
for action in actions:
|
||||||
|
if action.action_id == action_id:
|
||||||
|
return action
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
func has_action(action_id: StringName) -> bool:
|
||||||
|
return get_action(action_id) != null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://60whm57itpjb
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
extends Node
|
||||||
|
const VERSION := "arcade-cabinet-2"
|
||||||
|
var cabinet_id := "cabinet_01"
|
||||||
|
var database: ProjectDatabase = preload("res://data/project_database.tres")
|
||||||
|
var cabinet: CabinetProfile
|
||||||
|
var connection: Node
|
||||||
|
var game: ArcadeGame
|
||||||
|
var session := ""
|
||||||
|
var phase := "idle"
|
||||||
|
var boot := ""
|
||||||
|
var reports: Array[Dictionary] = []
|
||||||
|
var outcomes: Dictionary = {}
|
||||||
|
var busy := false
|
||||||
|
var executing := false
|
||||||
|
var event_number := 0
|
||||||
|
var notice: Label
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
config.load("user://arcade_network.cfg")
|
||||||
|
cabinet_id = str(config.get_value("network", "cabinet_id", cabinet_id))
|
||||||
|
for arg in OS.get_cmdline_user_args():
|
||||||
|
if arg.begins_with("--cabinet="): cabinet_id = arg.trim_prefix("--cabinet=")
|
||||||
|
for definition in database.cabinets:
|
||||||
|
if str(definition.cabinet_id) == cabinet_id: cabinet = definition
|
||||||
|
boot = Crypto.new().generate_random_bytes(16).hex_encode()
|
||||||
|
# Remote mode must never also run the launcher's local reward mapping.
|
||||||
|
RoomRuntime.active_assignment = null
|
||||||
|
HardwareManager.active_cabinet = null
|
||||||
|
connection = preload("res://core/network/room_connection.gd").new()
|
||||||
|
add_child(connection)
|
||||||
|
connection.configure("cabinet")
|
||||||
|
var layer := CanvasLayer.new()
|
||||||
|
add_child(layer)
|
||||||
|
notice = Label.new()
|
||||||
|
notice.text = "Waiting for the room operator"
|
||||||
|
notice.position = Vector2(30, 1000)
|
||||||
|
notice.add_theme_font_size_override("font_size", 22)
|
||||||
|
layer.add_child(notice)
|
||||||
|
var timer := Timer.new()
|
||||||
|
timer.wait_time = .5
|
||||||
|
timer.timeout.connect(poll)
|
||||||
|
add_child(timer)
|
||||||
|
timer.start()
|
||||||
|
poll()
|
||||||
|
|
||||||
|
func poll() -> void:
|
||||||
|
if busy or executing: return
|
||||||
|
busy = true
|
||||||
|
var sent := reports.duplicate(true)
|
||||||
|
var result: Dictionary = await connection.post("/cabinet/poll", {
|
||||||
|
"cabinet": cabinet_id, "protocol": 2, "boot": boot, "version": VERSION,
|
||||||
|
"gpio": "real" if HardwareManager.real_gpio else "simulation", "reports": sent,
|
||||||
|
})
|
||||||
|
busy = false
|
||||||
|
if result.has("error"):
|
||||||
|
notice.text = "Room connection unavailable — waiting to reconnect"
|
||||||
|
return
|
||||||
|
for report in sent: reports.erase(report)
|
||||||
|
if str(result.get("session", "")) != session:
|
||||||
|
session = str(result.get("session", ""))
|
||||||
|
outcomes.clear()
|
||||||
|
reports.clear()
|
||||||
|
unload()
|
||||||
|
phase = "idle"
|
||||||
|
for command in result.get("commands", []):
|
||||||
|
executing = true
|
||||||
|
await execute(command)
|
||||||
|
executing = false
|
||||||
|
notice.text = "Waiting for the room operator" if game == null else ""
|
||||||
|
|
||||||
|
func unload() -> void:
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
if is_instance_valid(game):
|
||||||
|
remove_child(game)
|
||||||
|
game.queue_free()
|
||||||
|
game = null
|
||||||
|
|
||||||
|
func event(name: String) -> void:
|
||||||
|
event_number += 1
|
||||||
|
reports.append({"kind": "event", "id": boot + ":" + str(event_number), "session": session, "event": name})
|
||||||
|
phase = {"started": "playing", "won": "won", "lost": "lost"}.get(name, phase)
|
||||||
|
if name in ["won", "lost"] and game != null:
|
||||||
|
game.set_process_unhandled_input(false)
|
||||||
|
game.set_process_input(false)
|
||||||
|
|
||||||
|
func acknowledge(command: Dictionary, ok: bool, detail: String) -> void:
|
||||||
|
var report := {"kind": "ack", "id": command.id, "session": session, "ok": ok, "detail": detail}
|
||||||
|
outcomes[command.id] = report
|
||||||
|
reports.append(report)
|
||||||
|
|
||||||
|
func execute(command: Dictionary) -> void:
|
||||||
|
if command.get("session") != session: return
|
||||||
|
if outcomes.has(command.id):
|
||||||
|
reports.append(outcomes[command.id])
|
||||||
|
return
|
||||||
|
match command.get("type"):
|
||||||
|
"prepare":
|
||||||
|
var definition: GameDefinition
|
||||||
|
for entry in database.games:
|
||||||
|
if str(entry.game_id) == str(command.get("game", "pacman")): definition = entry
|
||||||
|
if definition == null or cabinet == null or not definition.can_run_on(cabinet):
|
||||||
|
acknowledge(command, false, "Game is missing or incompatible with this cabinet")
|
||||||
|
return
|
||||||
|
if command.get("variant", "standard") != "standard":
|
||||||
|
acknowledge(command, false, "Unsupported game variant")
|
||||||
|
return
|
||||||
|
unload()
|
||||||
|
game = definition.scene.instantiate()
|
||||||
|
if str(definition.game_id) == "missile_command": game.set("relative_mouse_aim", true)
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if str(definition.game_id) in ["missile_command", "tempest"] else Input.MOUSE_MODE_VISIBLE
|
||||||
|
add_child(game)
|
||||||
|
game.configure({})
|
||||||
|
game.game_started.connect(func(): event("started"))
|
||||||
|
game.game_won.connect(func(): event("won"))
|
||||||
|
game.game_lost.connect(func(): event("lost"))
|
||||||
|
phase = "ready"
|
||||||
|
acknowledge(command, true, definition.display_name + " loaded and ready")
|
||||||
|
"start":
|
||||||
|
var ok: bool = game != null and phase == "ready" and game.start_game()
|
||||||
|
acknowledge(command, ok, "Game started" if ok else "Cabinet was not ready")
|
||||||
|
"reset":
|
||||||
|
unload()
|
||||||
|
phase = "idle"
|
||||||
|
acknowledge(command, true, "Game unloaded; cabinet idle")
|
||||||
|
"reward", "output":
|
||||||
|
if (command.type == "reward" and phase != "won") or not HardwareManager.real_gpio or cabinet == null or cabinet.hardware_config == null:
|
||||||
|
acknowledge(command, false, "Reward requires a win and real GPIO mode")
|
||||||
|
return
|
||||||
|
var action := cabinet.hardware_config.get_action(StringName(command.get("action_id", "coin_release")))
|
||||||
|
if action == null:
|
||||||
|
acknowledge(command, false, "Hardware action is not defined on this cabinet")
|
||||||
|
return
|
||||||
|
var ok: bool = await HardwareManager.client.send({
|
||||||
|
"controller": str(cabinet.hardware_config.controller_id), "pin": action.gpio_pin,
|
||||||
|
"mode": "pulse" if action.action_type == HardwareAction.ActionType.PULSE_OUTPUT else "set", "active_high": action.active_high, "duration_ms": action.pulse_duration_ms,
|
||||||
|
"request_id": command.id,
|
||||||
|
})
|
||||||
|
acknowledge(command, ok, "GPIO bridge accepted pulse; delivery unconfirmed" if ok else "GPIO failed or acknowledgement lost; inspect cabinet")
|
||||||
|
_:
|
||||||
|
acknowledge(command, false, "Unsupported command")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dfoc8gpbuicc4
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
extends Node
|
||||||
|
## Transport boundary: gameplay and operator code only exchange command/state dictionaries.
|
||||||
|
var url := "http://127.0.0.1:8766"
|
||||||
|
var token := ""
|
||||||
|
|
||||||
|
func configure(role: String) -> void:
|
||||||
|
var config := ConfigFile.new()
|
||||||
|
config.load("user://arcade_network.cfg")
|
||||||
|
url = str(config.get_value("network", "url", url)).trim_suffix("/")
|
||||||
|
token = str(config.get_value("network", role + "_token", ""))
|
||||||
|
if OS.has_environment("ARCADE_ROOM_URL"): url = OS.get_environment("ARCADE_ROOM_URL").trim_suffix("/")
|
||||||
|
var name := "ARCADE_%s_TOKEN" % role.to_upper()
|
||||||
|
if OS.has_environment(name): token = OS.get_environment(name)
|
||||||
|
|
||||||
|
func post(path: String, payload: Dictionary = {}) -> Dictionary:
|
||||||
|
if token.is_empty(): return {"error": "Room connection token is not configured"}
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
request.timeout = 2.0
|
||||||
|
add_child(request)
|
||||||
|
var error := request.request(url + path, ["Content-Type: application/json", "Authorization: Bearer " + token], HTTPClient.METHOD_POST, JSON.stringify(payload))
|
||||||
|
var result: Dictionary = {"error": "Room controller is unreachable"}
|
||||||
|
if error == OK:
|
||||||
|
var response: Array = await request.request_completed
|
||||||
|
var body = JSON.parse_string(response[3].get_string_from_utf8())
|
||||||
|
if response[0] == HTTPRequest.RESULT_SUCCESS and body is Dictionary:
|
||||||
|
result = body
|
||||||
|
request.queue_free()
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bhsk7cucg2sbx
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
class_name RoomLogicEngine
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
signal event_received(
|
||||||
|
source_id: StringName,
|
||||||
|
event_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal rule_triggered(
|
||||||
|
rule_id: StringName,
|
||||||
|
source_id: StringName,
|
||||||
|
event_id: StringName,
|
||||||
|
target_id: StringName,
|
||||||
|
action_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal action_requested(
|
||||||
|
target_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
rule_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal trace_entry_created(
|
||||||
|
entry: Dictionary
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var room: RoomDefinition
|
||||||
|
|
||||||
|
var _pending_actions: Array[Dictionary] = []
|
||||||
|
var _simulation_time: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
room_definition: RoomDefinition
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
room = room_definition
|
||||||
|
reset()
|
||||||
|
|
||||||
|
|
||||||
|
func reset() -> void:
|
||||||
|
|
||||||
|
_pending_actions.clear()
|
||||||
|
_simulation_time = 0.0
|
||||||
|
|
||||||
|
|
||||||
|
func get_simulation_time() -> float:
|
||||||
|
|
||||||
|
return _simulation_time
|
||||||
|
|
||||||
|
|
||||||
|
func emit_event(
|
||||||
|
source_id: StringName,
|
||||||
|
event_id: StringName
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
event_received.emit(
|
||||||
|
source_id,
|
||||||
|
event_id
|
||||||
|
)
|
||||||
|
|
||||||
|
_create_trace_entry(
|
||||||
|
&"event",
|
||||||
|
{
|
||||||
|
"source_id": source_id,
|
||||||
|
"event_id": event_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for rule in room.logic_rules:
|
||||||
|
|
||||||
|
if rule == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rule.source_id != source_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rule.event_id != event_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
_trigger_rule(
|
||||||
|
rule
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func advance(
|
||||||
|
delta_seconds: float
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if delta_seconds <= 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
_simulation_time += delta_seconds
|
||||||
|
|
||||||
|
_process_pending_actions()
|
||||||
|
|
||||||
|
|
||||||
|
func _trigger_rule(
|
||||||
|
rule: RoomLogicRule
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
rule_triggered.emit(
|
||||||
|
rule.rule_id,
|
||||||
|
rule.source_id,
|
||||||
|
rule.event_id,
|
||||||
|
rule.target_id,
|
||||||
|
rule.action_id
|
||||||
|
)
|
||||||
|
|
||||||
|
_create_trace_entry(
|
||||||
|
&"rule",
|
||||||
|
{
|
||||||
|
"rule_id": rule.rule_id,
|
||||||
|
"source_id": rule.source_id,
|
||||||
|
"event_id": rule.event_id,
|
||||||
|
"target_id": rule.target_id,
|
||||||
|
"action_id": rule.action_id,
|
||||||
|
"delay_seconds": rule.delay_seconds,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if rule.delay_seconds <= 0.0:
|
||||||
|
|
||||||
|
_request_action(
|
||||||
|
rule.target_id,
|
||||||
|
rule.action_id,
|
||||||
|
rule.rule_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
_pending_actions.append(
|
||||||
|
{
|
||||||
|
"execute_at": (
|
||||||
|
_simulation_time
|
||||||
|
+ rule.delay_seconds
|
||||||
|
),
|
||||||
|
|
||||||
|
"target_id": (
|
||||||
|
rule.target_id
|
||||||
|
),
|
||||||
|
|
||||||
|
"action_id": (
|
||||||
|
rule.action_id
|
||||||
|
),
|
||||||
|
|
||||||
|
"rule_id": (
|
||||||
|
rule.rule_id
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _process_pending_actions() -> void:
|
||||||
|
|
||||||
|
if _pending_actions.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var ready_actions: Array[Dictionary] = []
|
||||||
|
var remaining_actions: Array[Dictionary] = []
|
||||||
|
|
||||||
|
for pending in _pending_actions:
|
||||||
|
|
||||||
|
var execute_at: float = (
|
||||||
|
pending.get(
|
||||||
|
"execute_at",
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if execute_at <= _simulation_time:
|
||||||
|
ready_actions.append(
|
||||||
|
pending
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
remaining_actions.append(
|
||||||
|
pending
|
||||||
|
)
|
||||||
|
|
||||||
|
_pending_actions = remaining_actions
|
||||||
|
|
||||||
|
for pending in ready_actions:
|
||||||
|
|
||||||
|
_request_action(
|
||||||
|
pending.get(
|
||||||
|
"target_id",
|
||||||
|
&""
|
||||||
|
),
|
||||||
|
pending.get(
|
||||||
|
"action_id",
|
||||||
|
&""
|
||||||
|
),
|
||||||
|
pending.get(
|
||||||
|
"rule_id",
|
||||||
|
&""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _request_action(
|
||||||
|
target_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
rule_id: StringName
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
_create_trace_entry(
|
||||||
|
&"action",
|
||||||
|
{
|
||||||
|
"target_id": target_id,
|
||||||
|
"action_id": action_id,
|
||||||
|
"rule_id": rule_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
action_requested.emit(
|
||||||
|
target_id,
|
||||||
|
action_id,
|
||||||
|
rule_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_trace_entry(
|
||||||
|
entry_type: StringName,
|
||||||
|
data: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var entry := data.duplicate()
|
||||||
|
|
||||||
|
entry["type"] = entry_type
|
||||||
|
entry["time"] = _simulation_time
|
||||||
|
|
||||||
|
trace_entry_created.emit(
|
||||||
|
entry
|
||||||
|
)
|
||||||
|
|
||||||
|
func has_pending_actions() -> bool:
|
||||||
|
|
||||||
|
return not _pending_actions.is_empty()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cxw5i220nmobr
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
@tool
|
||||||
|
class_name RoomLogicRule
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var rule_id: StringName = &""
|
||||||
|
|
||||||
|
@export_group("Source")
|
||||||
|
|
||||||
|
@export var source_id: StringName = &""
|
||||||
|
@export var event_id: StringName = &""
|
||||||
|
|
||||||
|
@export_group("Target")
|
||||||
|
|
||||||
|
@export var target_id: StringName = &""
|
||||||
|
@export var action_id: StringName = &""
|
||||||
|
|
||||||
|
@export_group("Timing")
|
||||||
|
|
||||||
|
@export_range(
|
||||||
|
0.0,
|
||||||
|
3600.0,
|
||||||
|
0.1
|
||||||
|
)
|
||||||
|
var delay_seconds: float = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cp40t70asl4p0
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@tool
|
||||||
|
class_name RoomWallDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
const FEET_TO_METERS: float = 0.3048
|
||||||
|
const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS
|
||||||
|
|
||||||
|
|
||||||
|
@export var wall_id: StringName = &""
|
||||||
|
@export var start: Vector2 = Vector2.ZERO
|
||||||
|
@export var end: Vector2 = Vector2(
|
||||||
|
1.0,
|
||||||
|
0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Stored internally in meters so existing room/layout
|
||||||
|
# rendering code keeps working without modification.
|
||||||
|
#
|
||||||
|
@export_storage
|
||||||
|
var thickness: float = 0.10
|
||||||
|
|
||||||
|
|
||||||
|
@export_group("Physical Dimensions")
|
||||||
|
|
||||||
|
@export_range(0.01, 4.0, 0.01, "suffix: ft")
|
||||||
|
var thickness_ft: float:
|
||||||
|
get:
|
||||||
|
return (
|
||||||
|
thickness
|
||||||
|
* METERS_TO_FEET
|
||||||
|
)
|
||||||
|
set(value):
|
||||||
|
thickness = (
|
||||||
|
value
|
||||||
|
* FEET_TO_METERS
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dl7ydh8g6877n
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
class_name GameEvents
|
||||||
|
|
||||||
|
|
||||||
|
const STARTED: StringName = &"game_started"
|
||||||
|
const WON: StringName = &"game_won"
|
||||||
|
const LOST: StringName = &"game_lost"
|
||||||
|
const RESET: StringName = &"game_reset"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cs0xs0amg3lrv
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
class_name GameHost
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
signal game_loaded(game: ArcadeGame)
|
||||||
|
signal game_unloaded
|
||||||
|
|
||||||
|
|
||||||
|
var current_game: ArcadeGame
|
||||||
|
|
||||||
|
|
||||||
|
func load_game(game_definition: GameDefinition) -> ArcadeGame:
|
||||||
|
unload_game()
|
||||||
|
|
||||||
|
if game_definition == null:
|
||||||
|
push_error("GameHost received a null GameDefinition.")
|
||||||
|
return null
|
||||||
|
|
||||||
|
if game_definition.scene == null:
|
||||||
|
push_error(
|
||||||
|
"Game '%s' has no scene assigned."
|
||||||
|
% game_definition.game_id
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var instance := game_definition.scene.instantiate()
|
||||||
|
|
||||||
|
if not instance is ArcadeGame:
|
||||||
|
push_error(
|
||||||
|
"Game scene '%s' does not inherit from ArcadeGame."
|
||||||
|
% game_definition.game_id
|
||||||
|
)
|
||||||
|
|
||||||
|
instance.queue_free()
|
||||||
|
return null
|
||||||
|
|
||||||
|
current_game = instance as ArcadeGame
|
||||||
|
|
||||||
|
add_child(current_game)
|
||||||
|
|
||||||
|
current_game.game_started.connect(_on_game_started)
|
||||||
|
current_game.game_won.connect(_on_game_won)
|
||||||
|
current_game.game_lost.connect(_on_game_lost)
|
||||||
|
current_game.game_reset.connect(_on_game_reset)
|
||||||
|
|
||||||
|
game_loaded.emit(current_game)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[GAME HOST] Loaded '%s'"
|
||||||
|
% game_definition.display_name
|
||||||
|
)
|
||||||
|
|
||||||
|
return current_game
|
||||||
|
|
||||||
|
|
||||||
|
func unload_game() -> void:
|
||||||
|
if current_game == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
print("[GAME HOST] Unloading current game.")
|
||||||
|
|
||||||
|
current_game.queue_free()
|
||||||
|
current_game = null
|
||||||
|
|
||||||
|
game_unloaded.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_game_started() -> void:
|
||||||
|
RoomRuntime.fire_game_event(GameEvents.STARTED)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_game_won() -> void:
|
||||||
|
RoomRuntime.fire_game_event(GameEvents.WON)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_game_lost() -> void:
|
||||||
|
RoomRuntime.fire_game_event(GameEvents.LOST)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_game_reset() -> void:
|
||||||
|
RoomRuntime.fire_game_event(GameEvents.RESET)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cp6novpbgl1uk
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
extends Node
|
||||||
|
signal action_activated(cabinet_id: StringName, action_id: StringName, context: Dictionary)
|
||||||
|
signal action_deactivated(cabinet_id: StringName, action_id: StringName, context: Dictionary)
|
||||||
|
signal status_changed(message: String)
|
||||||
|
var active_cabinet: CabinetProfile
|
||||||
|
var real_gpio := false
|
||||||
|
var status := "GPIO simulation (use --gpio=real on the Pi)"
|
||||||
|
var client: Node
|
||||||
|
var generations: Dictionary = {}
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
real_gpio = "--gpio=real" in OS.get_cmdline_user_args()
|
||||||
|
if real_gpio:
|
||||||
|
client = preload("res://core/runtime/pi_gpio_client.gd").new()
|
||||||
|
client.status_changed.connect(_status)
|
||||||
|
add_child(client)
|
||||||
|
_status("GPIO real mode — local Pi bridge required")
|
||||||
|
if "--gpio-test" in OS.get_cmdline_user_args():
|
||||||
|
test_led.call_deferred()
|
||||||
|
|
||||||
|
func _status(message: String) -> void:
|
||||||
|
status = message
|
||||||
|
print("[GPIO] ", message)
|
||||||
|
status_changed.emit(message)
|
||||||
|
|
||||||
|
func configure(cabinet: CabinetProfile) -> void:
|
||||||
|
active_cabinet = cabinet
|
||||||
|
|
||||||
|
func trigger(action_id: StringName, context: Dictionary = {}) -> void:
|
||||||
|
if active_cabinet == null:
|
||||||
|
_status("No active cabinet for GPIO action")
|
||||||
|
return
|
||||||
|
trigger_for_cabinet(active_cabinet, action_id, context)
|
||||||
|
|
||||||
|
func trigger_for_cabinet(cabinet: CabinetProfile, action_id: StringName, context: Dictionary = {}) -> void:
|
||||||
|
if cabinet == null or cabinet.hardware_config == null:
|
||||||
|
_status("Missing cabinet hardware configuration")
|
||||||
|
return
|
||||||
|
var action := cabinet.hardware_config.get_action(action_id)
|
||||||
|
if action == null:
|
||||||
|
_status("Unknown hardware action: %s" % action_id)
|
||||||
|
return
|
||||||
|
_send_action(cabinet, action, context)
|
||||||
|
|
||||||
|
func _send_action(cabinet: CabinetProfile, action: HardwareAction, context: Dictionary) -> void:
|
||||||
|
var key := "%s:%s" % [cabinet.hardware_config.controller_id, action.gpio_pin]
|
||||||
|
var generation := int(generations.get(key, 0)) + 1
|
||||||
|
generations[key] = generation
|
||||||
|
var pulse := action.action_type == HardwareAction.ActionType.PULSE_OUTPUT
|
||||||
|
var active := true if pulse else bool(context.get("active", true))
|
||||||
|
if real_gpio:
|
||||||
|
var ok: bool = await client.send({
|
||||||
|
"controller": str(cabinet.hardware_config.controller_id),
|
||||||
|
"pin": action.gpio_pin,
|
||||||
|
"active_high": action.active_high,
|
||||||
|
"mode": "pulse" if pulse else "set",
|
||||||
|
"duration_ms": action.pulse_duration_ms,
|
||||||
|
"active": active,
|
||||||
|
})
|
||||||
|
if not ok: return
|
||||||
|
else:
|
||||||
|
print("[MOCK GPIO] %s cabinet=%s action=%s BCM=%d" % ["PULSE" if pulse else "SET", cabinet.cabinet_id, action.action_id, action.gpio_pin])
|
||||||
|
if active:
|
||||||
|
action_activated.emit(cabinet.cabinet_id, action.action_id, context)
|
||||||
|
else:
|
||||||
|
action_deactivated.emit(cabinet.cabinet_id, action.action_id, context)
|
||||||
|
if pulse:
|
||||||
|
await get_tree().create_timer(action.pulse_duration_ms / 1000.0).timeout
|
||||||
|
if int(generations.get(key, 0)) == generation:
|
||||||
|
# Physical timing belongs to the bridge, even if the game exits mid-pulse.
|
||||||
|
action_deactivated.emit(cabinet.cabinet_id, action.action_id, context)
|
||||||
|
|
||||||
|
func test_led() -> void:
|
||||||
|
var cabinet = load("res://data/cabinets/cabinet_01.tres").duplicate(true)
|
||||||
|
var action: HardwareAction = cabinet.hardware_config.get_action(&"coin_release")
|
||||||
|
action.pulse_duration_ms = 2000
|
||||||
|
_send_action(cabinet, action, {"source": "gpio_led_test"})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://5d4k46jwlo4y
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
class_name LaunchConfig
|
||||||
|
|
||||||
|
|
||||||
|
var cabinet_id: StringName
|
||||||
|
var scenario_id: StringName
|
||||||
|
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
default_cabinet: StringName = &"",
|
||||||
|
default_scenario: StringName = &""
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
cabinet_id = default_cabinet
|
||||||
|
scenario_id = default_scenario
|
||||||
|
|
||||||
|
|
||||||
|
static func from_command_line(
|
||||||
|
default_cabinet: StringName = &"",
|
||||||
|
default_scenario: StringName = &""
|
||||||
|
) -> LaunchConfig:
|
||||||
|
|
||||||
|
var config := LaunchConfig.new(
|
||||||
|
default_cabinet,
|
||||||
|
default_scenario
|
||||||
|
)
|
||||||
|
|
||||||
|
for arg in OS.get_cmdline_user_args():
|
||||||
|
|
||||||
|
if arg.begins_with("--cabinet="):
|
||||||
|
config.cabinet_id = StringName(
|
||||||
|
arg.trim_prefix("--cabinet=")
|
||||||
|
)
|
||||||
|
|
||||||
|
elif arg.begins_with("--scenario="):
|
||||||
|
config.scenario_id = StringName(
|
||||||
|
arg.trim_prefix("--scenario=")
|
||||||
|
)
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
func print_debug() -> void:
|
||||||
|
print("")
|
||||||
|
print("========= LAUNCH CONFIG =========")
|
||||||
|
print("Cabinet: ", cabinet_id)
|
||||||
|
print("Scenario: ", scenario_id)
|
||||||
|
print("=================================")
|
||||||
|
print("")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://brd536grsx38f
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
class_name LogicPathId
|
||||||
|
|
||||||
|
|
||||||
|
static func make(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
event_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
device_id: StringName
|
||||||
|
) -> StringName:
|
||||||
|
|
||||||
|
return StringName(
|
||||||
|
"%s::%s::%s::%s"
|
||||||
|
% [
|
||||||
|
cabinet_id,
|
||||||
|
event_id,
|
||||||
|
action_id,
|
||||||
|
device_id
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cy746kl1sgsrn
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
extends Node
|
||||||
|
signal completed(id: int, ok: bool)
|
||||||
|
signal status_changed(message: String)
|
||||||
|
const URL := "http://127.0.0.1:8765"
|
||||||
|
var pending: Array[Dictionary] = []
|
||||||
|
var serial := 0
|
||||||
|
var busy := false
|
||||||
|
var heartbeat: HTTPRequest
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
process_mode = Node.PROCESS_MODE_ALWAYS
|
||||||
|
heartbeat = HTTPRequest.new()
|
||||||
|
heartbeat.timeout = 1.0
|
||||||
|
add_child(heartbeat)
|
||||||
|
var timer := Timer.new()
|
||||||
|
timer.wait_time = 1.0
|
||||||
|
timer.timeout.connect(_heartbeat)
|
||||||
|
add_child(timer)
|
||||||
|
timer.start()
|
||||||
|
|
||||||
|
func _heartbeat() -> void:
|
||||||
|
if heartbeat.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED:
|
||||||
|
heartbeat.request(URL + "/heartbeat", [], HTTPClient.METHOD_POST, "{}")
|
||||||
|
|
||||||
|
func send(payload: Dictionary) -> bool:
|
||||||
|
if pending.size() >= 32:
|
||||||
|
status_changed.emit("GPIO output queue is full")
|
||||||
|
return false
|
||||||
|
serial += 1
|
||||||
|
var id := serial
|
||||||
|
pending.append({"id": id, "payload": payload})
|
||||||
|
_pump.call_deferred()
|
||||||
|
while true:
|
||||||
|
var result: Array = await completed
|
||||||
|
if result[0] == id: return result[1]
|
||||||
|
return false
|
||||||
|
|
||||||
|
func _pump() -> void:
|
||||||
|
if busy or pending.is_empty(): return
|
||||||
|
busy = true
|
||||||
|
var item: Dictionary = pending[0]
|
||||||
|
var request := HTTPRequest.new()
|
||||||
|
request.timeout = 2.0
|
||||||
|
add_child(request)
|
||||||
|
var ok := false
|
||||||
|
var message := "GPIO bridge unavailable — start pi_gpio_bridge.py on this Pi"
|
||||||
|
var error := request.request(URL + "/output", ["Content-Type: application/json"], HTTPClient.METHOD_POST, JSON.stringify(item.payload))
|
||||||
|
if error == OK:
|
||||||
|
var response: Array = await request.request_completed
|
||||||
|
var body = JSON.parse_string(response[3].get_string_from_utf8())
|
||||||
|
ok = response[0] == HTTPRequest.RESULT_SUCCESS and response[1] == 200 and body is Dictionary and body.get("ok", false) == true
|
||||||
|
if body is Dictionary and body.has("error"): message = str(body.error)
|
||||||
|
request.queue_free()
|
||||||
|
pending.pop_front()
|
||||||
|
busy = false
|
||||||
|
status_changed.emit("GPIO %s accepted by Pi bridge" % item.payload.pin if ok else message)
|
||||||
|
completed.emit(item.id, ok)
|
||||||
|
_pump.call_deferred()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://74k3b8polfi2
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
@tool
|
||||||
|
class_name ProjectDatabase
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var scenarios: Array[ScenarioDefinition] = []
|
||||||
|
@export var cabinets: Array[CabinetProfile] = []
|
||||||
|
@export var games: Array[GameDefinition] = []
|
||||||
|
|
||||||
|
|
||||||
|
func get_scenario(
|
||||||
|
scenario_id: StringName
|
||||||
|
) -> ScenarioDefinition:
|
||||||
|
|
||||||
|
for scenario in scenarios:
|
||||||
|
if scenario == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if scenario.scenario_id == scenario_id:
|
||||||
|
return scenario
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_cabinet(
|
||||||
|
cabinet_id: StringName
|
||||||
|
) -> CabinetProfile:
|
||||||
|
|
||||||
|
for cabinet in cabinets:
|
||||||
|
if cabinet == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cabinet.cabinet_id == cabinet_id:
|
||||||
|
return cabinet
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_game(
|
||||||
|
game_id: StringName
|
||||||
|
) -> GameDefinition:
|
||||||
|
|
||||||
|
for game in games:
|
||||||
|
if game == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if game.game_id == game_id:
|
||||||
|
return game
|
||||||
|
|
||||||
|
return null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://diqnrxrhwx2ug
|
||||||
@@ -0,0 +1,315 @@
|
|||||||
|
class_name RoomController
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
signal scenario_loaded(scenario: ScenarioDefinition)
|
||||||
|
|
||||||
|
signal validation_completed(
|
||||||
|
result: ValidationResult
|
||||||
|
)
|
||||||
|
|
||||||
|
signal cabinet_status_changed(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
status: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal room_status_changed(
|
||||||
|
status: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var database: ProjectDatabase
|
||||||
|
var active_scenario: ScenarioDefinition
|
||||||
|
|
||||||
|
var cabinet_statuses: Dictionary = {}
|
||||||
|
var room: RoomDefinition
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
project_database: ProjectDatabase,
|
||||||
|
room_definition: RoomDefinition = null
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
database = project_database
|
||||||
|
room = room_definition
|
||||||
|
|
||||||
|
|
||||||
|
func load_scenario(
|
||||||
|
scenario_id: StringName
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
if database == null:
|
||||||
|
push_error(
|
||||||
|
"RoomController has no ProjectDatabase."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var scenario := database.get_scenario(
|
||||||
|
scenario_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if scenario == null:
|
||||||
|
push_error(
|
||||||
|
"Scenario '%s' not found."
|
||||||
|
% scenario_id
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
active_scenario = scenario
|
||||||
|
|
||||||
|
_initialize_cabinet_statuses()
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("======= ROOM CONTROLLER =======")
|
||||||
|
print(
|
||||||
|
"Loaded scenario: %s v%d"
|
||||||
|
% [
|
||||||
|
scenario.display_name,
|
||||||
|
scenario.version
|
||||||
|
]
|
||||||
|
)
|
||||||
|
print("===============================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
scenario_loaded.emit(
|
||||||
|
active_scenario
|
||||||
|
)
|
||||||
|
|
||||||
|
room_status_changed.emit(
|
||||||
|
&"LOADED"
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func validate_active_scenario() -> ValidationResult:
|
||||||
|
var result := ValidationResult.new()
|
||||||
|
|
||||||
|
result.merge(
|
||||||
|
ScenarioValidator.validate(
|
||||||
|
active_scenario
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if room != null:
|
||||||
|
result.merge(
|
||||||
|
RoomValidator.validate(
|
||||||
|
room,
|
||||||
|
database
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
validation_completed.emit(
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.is_valid():
|
||||||
|
room_status_changed.emit(
|
||||||
|
&"VALID"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
room_status_changed.emit(
|
||||||
|
&"INVALID"
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func prep_room() -> bool:
|
||||||
|
if active_scenario == null:
|
||||||
|
push_error(
|
||||||
|
"Cannot prep room: no scenario loaded."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var validation := validate_active_scenario()
|
||||||
|
|
||||||
|
if not validation.is_valid():
|
||||||
|
print(
|
||||||
|
"[ROOM CONTROLLER] Prep aborted: "
|
||||||
|
+ validation.get_summary()
|
||||||
|
)
|
||||||
|
|
||||||
|
room_status_changed.emit(
|
||||||
|
&"PREP_FAILED"
|
||||||
|
)
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
room_status_changed.emit(
|
||||||
|
&"PREPARING"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("=========== PREP ROOM ===========")
|
||||||
|
|
||||||
|
for assignment in active_scenario.assignments:
|
||||||
|
_prep_assignment(assignment)
|
||||||
|
|
||||||
|
print("=================================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
room_status_changed.emit(
|
||||||
|
&"READY"
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _prep_assignment(
|
||||||
|
assignment: ScenarioAssignment
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var cabinet := assignment.cabinet
|
||||||
|
var game := assignment.game
|
||||||
|
|
||||||
|
_set_cabinet_status(
|
||||||
|
cabinet.cabinet_id,
|
||||||
|
&"PREPARING"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[ROOM CONTROLLER] Cabinet '%s'"
|
||||||
|
% cabinet.cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
" Game: %s"
|
||||||
|
% game.game_id
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
" Variant: %s"
|
||||||
|
% assignment.variant
|
||||||
|
)
|
||||||
|
|
||||||
|
print(
|
||||||
|
" Hardware: %s"
|
||||||
|
% cabinet.hardware_config.controller_id
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Networking will eventually happen here.
|
||||||
|
#
|
||||||
|
# send_to_cabinet(
|
||||||
|
# cabinet.cabinet_id,
|
||||||
|
# scenario,
|
||||||
|
# assignment
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
|
||||||
|
print(
|
||||||
|
" [SIMULATION] Cabinet ready."
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_cabinet_status(
|
||||||
|
cabinet.cabinet_id,
|
||||||
|
&"READY"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_cabinet_statuses() -> void:
|
||||||
|
cabinet_statuses.clear()
|
||||||
|
|
||||||
|
if active_scenario == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
for assignment in active_scenario.assignments:
|
||||||
|
|
||||||
|
if assignment == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if assignment.cabinet == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
_set_cabinet_status(
|
||||||
|
assignment.cabinet.cabinet_id,
|
||||||
|
&"NOT_PREPARED"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_cabinet_status(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
status: StringName
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
cabinet_statuses[cabinet_id] = status
|
||||||
|
|
||||||
|
cabinet_status_changed.emit(
|
||||||
|
cabinet_id,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_cabinet_status(
|
||||||
|
cabinet_id: StringName
|
||||||
|
) -> StringName:
|
||||||
|
|
||||||
|
return cabinet_statuses.get(
|
||||||
|
cabinet_id,
|
||||||
|
&"UNKNOWN"
|
||||||
|
)
|
||||||
|
|
||||||
|
func simulate_game_event(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
event_name: StringName
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if active_scenario == null:
|
||||||
|
push_error(
|
||||||
|
"Cannot simulate event: no active scenario."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var assignment := (
|
||||||
|
active_scenario
|
||||||
|
.get_assignment_for_cabinet(
|
||||||
|
cabinet_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if assignment == null:
|
||||||
|
push_error(
|
||||||
|
"No assignment exists for cabinet '%s'."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(
|
||||||
|
"[ROOM SIMULATION] %s -> %s"
|
||||||
|
% [
|
||||||
|
cabinet_id,
|
||||||
|
event_name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
var actions := (
|
||||||
|
assignment
|
||||||
|
.get_actions_for_event(
|
||||||
|
event_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if actions.is_empty():
|
||||||
|
print(
|
||||||
|
"[ROOM SIMULATION] No actions mapped."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for action_id in actions:
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[ROOM SIMULATION] %s -> %s"
|
||||||
|
% [
|
||||||
|
event_name,
|
||||||
|
action_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
HardwareManager.trigger_for_cabinet(
|
||||||
|
assignment.cabinet,
|
||||||
|
action_id,
|
||||||
|
{
|
||||||
|
"game_event": event_name
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbe24sbmbh6tt
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
signal game_event_fired(
|
||||||
|
event_name: StringName,
|
||||||
|
cabinet_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var active_assignment: ScenarioAssignment
|
||||||
|
|
||||||
|
|
||||||
|
func configure(assignment: ScenarioAssignment) -> void:
|
||||||
|
if assignment == null:
|
||||||
|
push_error("Cannot configure RoomRuntime with null assignment.")
|
||||||
|
return
|
||||||
|
|
||||||
|
active_assignment = assignment
|
||||||
|
|
||||||
|
HardwareManager.configure(assignment.cabinet)
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("======= ROOM RUNTIME =======")
|
||||||
|
print("Cabinet: ", assignment.cabinet.display_name)
|
||||||
|
print("Game: ", assignment.game.display_name)
|
||||||
|
print("Variant: ", assignment.variant)
|
||||||
|
print("============================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
func fire_game_event(event_name: StringName) -> void:
|
||||||
|
if active_assignment == null:
|
||||||
|
push_error("RoomRuntime has no active assignment.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print(
|
||||||
|
"[GAME EVENT] %s -> %s"
|
||||||
|
% [
|
||||||
|
active_assignment.game.game_id,
|
||||||
|
event_name
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
game_event_fired.emit(
|
||||||
|
event_name,
|
||||||
|
active_assignment.cabinet.cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
var actions := active_assignment.get_actions_for_event(
|
||||||
|
event_name
|
||||||
|
)
|
||||||
|
|
||||||
|
if actions.is_empty():
|
||||||
|
print("[SCENARIO] No hardware actions mapped.")
|
||||||
|
return
|
||||||
|
|
||||||
|
for action_id in actions:
|
||||||
|
print(
|
||||||
|
"[SCENARIO] %s -> %s"
|
||||||
|
% [event_name, action_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
HardwareManager.trigger(
|
||||||
|
action_id,
|
||||||
|
{
|
||||||
|
"game_event": event_name
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://eelj57byt0su
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
class_name RoomValidator
|
||||||
|
|
||||||
|
|
||||||
|
static func validate(
|
||||||
|
room: RoomDefinition,
|
||||||
|
database: ProjectDatabase
|
||||||
|
) -> ValidationResult:
|
||||||
|
|
||||||
|
var result := ValidationResult.new()
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
result.add_error(
|
||||||
|
"RoomDefinition is null."
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
_validate_cabinet_placements(
|
||||||
|
room,
|
||||||
|
database,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
_validate_devices(
|
||||||
|
room,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
_validate_bindings(
|
||||||
|
room,
|
||||||
|
database,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
static func _validate_cabinet_placements(
|
||||||
|
room: RoomDefinition,
|
||||||
|
database: ProjectDatabase,
|
||||||
|
result: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var seen_ids: Dictionary = {}
|
||||||
|
|
||||||
|
for placement in room.cabinet_placements:
|
||||||
|
|
||||||
|
if placement == null:
|
||||||
|
result.add_warning(
|
||||||
|
"Room contains a null cabinet placement."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var cabinet_id := placement.cabinet_id
|
||||||
|
|
||||||
|
if cabinet_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"Room contains a cabinet placement with no cabinet_id."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if seen_ids.has(cabinet_id):
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s' is placed in the room more than once."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen_ids[cabinet_id] = true
|
||||||
|
|
||||||
|
if database != null:
|
||||||
|
if database.get_cabinet(
|
||||||
|
cabinet_id
|
||||||
|
) == null:
|
||||||
|
|
||||||
|
result.add_error(
|
||||||
|
"Room references unknown cabinet '%s'."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
static func _validate_devices(
|
||||||
|
room: RoomDefinition,
|
||||||
|
result: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var seen_ids: Dictionary = {}
|
||||||
|
|
||||||
|
for device in room.devices:
|
||||||
|
|
||||||
|
if device == null:
|
||||||
|
result.add_warning(
|
||||||
|
"Room contains a null device."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if device.device_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"Room contains a device with no device_id."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if seen_ids.has(
|
||||||
|
device.device_id
|
||||||
|
):
|
||||||
|
result.add_error(
|
||||||
|
"Room device '%s' appears more than once."
|
||||||
|
% device.device_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen_ids[
|
||||||
|
device.device_id
|
||||||
|
] = true
|
||||||
|
|
||||||
|
static func _validate_bindings(
|
||||||
|
room: RoomDefinition,
|
||||||
|
database: ProjectDatabase,
|
||||||
|
result: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var seen_bindings: Dictionary = {}
|
||||||
|
|
||||||
|
for binding in room.bindings:
|
||||||
|
|
||||||
|
if binding == null:
|
||||||
|
result.add_warning(
|
||||||
|
"Room contains a null device binding."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var cabinet_id := binding.cabinet_id
|
||||||
|
var action_id := binding.hardware_action_id
|
||||||
|
var device_id := binding.device_id
|
||||||
|
|
||||||
|
if cabinet_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"A room binding has no cabinet_id."
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if action_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"A room binding for cabinet '%s' has no hardware action."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if device_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"A room binding for cabinet '%s' has no device_id."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if room.get_cabinet_placement(
|
||||||
|
cabinet_id
|
||||||
|
) == null:
|
||||||
|
|
||||||
|
result.add_error(
|
||||||
|
(
|
||||||
|
"Binding references cabinet '%s', "
|
||||||
|
+ "but that cabinet is not placed in the room."
|
||||||
|
)
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if room.get_device(
|
||||||
|
device_id
|
||||||
|
) == null:
|
||||||
|
|
||||||
|
result.add_error(
|
||||||
|
"Binding references unknown room device '%s'."
|
||||||
|
% device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
var cabinet: CabinetProfile = null
|
||||||
|
|
||||||
|
if database != null:
|
||||||
|
cabinet = database.get_cabinet(
|
||||||
|
cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if cabinet == null:
|
||||||
|
result.add_error(
|
||||||
|
"Binding references unknown cabinet '%s'."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if cabinet.hardware_config == null:
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s' has no HardwareConfig."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
elif not cabinet.hardware_config.has_action(
|
||||||
|
action_id
|
||||||
|
):
|
||||||
|
result.add_error(
|
||||||
|
(
|
||||||
|
"Room binding references unknown hardware action '%s' "
|
||||||
|
+ "on cabinet '%s'."
|
||||||
|
)
|
||||||
|
% [
|
||||||
|
action_id,
|
||||||
|
cabinet_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
var binding_key := (
|
||||||
|
"%s::%s::%s"
|
||||||
|
% [
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
device_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if seen_bindings.has(
|
||||||
|
binding_key
|
||||||
|
):
|
||||||
|
result.add_warning(
|
||||||
|
"Duplicate room binding '%s'."
|
||||||
|
% binding_key
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen_bindings[
|
||||||
|
binding_key
|
||||||
|
] = true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bbu1ejey4q6vx
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
class_name ScenarioValidator
|
||||||
|
|
||||||
|
|
||||||
|
static func validate(
|
||||||
|
scenario: ScenarioDefinition
|
||||||
|
) -> ValidationResult:
|
||||||
|
|
||||||
|
var result := ValidationResult.new()
|
||||||
|
|
||||||
|
if scenario == null:
|
||||||
|
result.add_error("Scenario is null.")
|
||||||
|
return result
|
||||||
|
|
||||||
|
if scenario.assignments.is_empty():
|
||||||
|
result.add_error(
|
||||||
|
"Scenario has no cabinet assignments."
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
var seen_cabinets: Dictionary = {}
|
||||||
|
|
||||||
|
for assignment in scenario.assignments:
|
||||||
|
_validate_assignment(
|
||||||
|
assignment,
|
||||||
|
seen_cabinets,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_assignment(
|
||||||
|
assignment: ScenarioAssignment,
|
||||||
|
seen_cabinets: Dictionary,
|
||||||
|
result: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if assignment == null:
|
||||||
|
result.add_error(
|
||||||
|
"Scenario contains a null assignment."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if assignment.cabinet == null:
|
||||||
|
result.add_error(
|
||||||
|
"An assignment has no cabinet."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var cabinet := assignment.cabinet
|
||||||
|
var cabinet_id := cabinet.cabinet_id
|
||||||
|
|
||||||
|
if cabinet_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"A cabinet has no cabinet_id."
|
||||||
|
)
|
||||||
|
|
||||||
|
if seen_cabinets.has(cabinet_id):
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s' appears more than once."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen_cabinets[cabinet_id] = true
|
||||||
|
|
||||||
|
if assignment.game == null:
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s' has no game assigned."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var game := assignment.game
|
||||||
|
|
||||||
|
if game.game_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s' has a game with no game_id."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if game.scene == null:
|
||||||
|
result.add_error(
|
||||||
|
"Game '%s' has no scene assigned."
|
||||||
|
% game.game_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if not game.can_run_on(cabinet):
|
||||||
|
result.add_error(
|
||||||
|
"Game '%s' cannot run on cabinet '%s'."
|
||||||
|
% [
|
||||||
|
game.game_id,
|
||||||
|
cabinet_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if cabinet.hardware_config == null:
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s' has no HardwareConfig."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
_validate_event_mappings(
|
||||||
|
assignment,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_event_mappings(
|
||||||
|
assignment: ScenarioAssignment,
|
||||||
|
result: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var cabinet := assignment.cabinet
|
||||||
|
var cabinet_id := cabinet.cabinet_id
|
||||||
|
var hardware := cabinet.hardware_config
|
||||||
|
var game:= assignment.game
|
||||||
|
|
||||||
|
_validate_game_events(
|
||||||
|
game,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
|
||||||
|
for mapping in assignment.event_mappings:
|
||||||
|
|
||||||
|
if mapping == null:
|
||||||
|
result.add_warning(
|
||||||
|
"Cabinet '%s' contains a null event mapping."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if mapping.game_event == &"":
|
||||||
|
result.add_warning(
|
||||||
|
"Cabinet '%s' has a mapping with no game event."
|
||||||
|
% cabinet_id
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if not game.has_event(
|
||||||
|
mapping.game_event
|
||||||
|
):
|
||||||
|
result.add_error(
|
||||||
|
(
|
||||||
|
"Game '%s' does not define event '%s' "
|
||||||
|
+ "used by cabinet '%s'."
|
||||||
|
)
|
||||||
|
% [
|
||||||
|
game.game_id,
|
||||||
|
mapping.game_event,
|
||||||
|
cabinet_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if mapping.hardware_actions.is_empty():
|
||||||
|
result.add_warning(
|
||||||
|
"Event '%s' on cabinet '%s' has no actions."
|
||||||
|
% [
|
||||||
|
mapping.game_event,
|
||||||
|
cabinet_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
for action_id in mapping.hardware_actions:
|
||||||
|
|
||||||
|
if not hardware.has_action(action_id):
|
||||||
|
result.add_error(
|
||||||
|
"Cabinet '%s': event '%s' references unknown hardware action '%s'."
|
||||||
|
% [
|
||||||
|
cabinet_id,
|
||||||
|
mapping.game_event,
|
||||||
|
action_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
static func _validate_game_events(
|
||||||
|
game: GameDefinition,
|
||||||
|
result: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var seen_events: Dictionary = {}
|
||||||
|
|
||||||
|
for event in game.events:
|
||||||
|
if event == null:
|
||||||
|
result.add_warning(
|
||||||
|
"Game '%s' contains a null event definition."
|
||||||
|
% game.game_id
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if event.event_id == &"":
|
||||||
|
result.add_error(
|
||||||
|
"Game '%s' contains an event with no event_id."
|
||||||
|
% game.game_id
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if seen_events.has(
|
||||||
|
event.event_id
|
||||||
|
):
|
||||||
|
result.add_error(
|
||||||
|
"Game '%s' defines event '%s' more than once."
|
||||||
|
% [
|
||||||
|
game.game_id,
|
||||||
|
event.event_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
seen_events[
|
||||||
|
event.event_id
|
||||||
|
] = true
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dhcgaiviprvg8
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
@tool
|
||||||
|
class_name ValidationResult
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
var errors: Array[String] = []
|
||||||
|
var warnings: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func add_error(message: String) -> void:
|
||||||
|
errors.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func add_warning(message: String) -> void:
|
||||||
|
warnings.append(message)
|
||||||
|
|
||||||
|
|
||||||
|
func is_valid() -> bool:
|
||||||
|
return errors.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func has_warnings() -> bool:
|
||||||
|
return not warnings.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func get_summary() -> String:
|
||||||
|
if not is_valid():
|
||||||
|
return "%d error(s), %d warning(s)" % [
|
||||||
|
errors.size(),
|
||||||
|
warnings.size()
|
||||||
|
]
|
||||||
|
|
||||||
|
if has_warnings():
|
||||||
|
return "Valid with %d warning(s)" % warnings.size()
|
||||||
|
|
||||||
|
return "Valid"
|
||||||
|
|
||||||
|
func merge(
|
||||||
|
other: ValidationResult
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if other == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
errors.append_array(
|
||||||
|
other.errors
|
||||||
|
)
|
||||||
|
|
||||||
|
warnings.append_array(
|
||||||
|
other.warnings
|
||||||
|
)
|
||||||
|
|
||||||
|
var result := ValidationResult.new()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bey4rq671pjlp
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@tool
|
||||||
|
class_name ScenarioAssignment
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var cabinet: CabinetProfile
|
||||||
|
@export var game: GameDefinition
|
||||||
|
|
||||||
|
@export var variant: StringName = &"standard"
|
||||||
|
|
||||||
|
@export var event_mappings: Array[ScenarioEventMapping] = []
|
||||||
|
|
||||||
|
|
||||||
|
func get_actions_for_event(event_name: StringName) -> Array[StringName]:
|
||||||
|
var result: Array[StringName] = []
|
||||||
|
|
||||||
|
for mapping in event_mappings:
|
||||||
|
if mapping.game_event == event_name:
|
||||||
|
result.append_array(mapping.hardware_actions)
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ibujr0ervhhq
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
@tool
|
||||||
|
class_name ScenarioDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var scenario_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
@export var version: int = 1
|
||||||
|
|
||||||
|
@export var assignments: Array[ScenarioAssignment] = []
|
||||||
|
|
||||||
|
|
||||||
|
func get_assignment_for_cabinet(
|
||||||
|
cabinet_id: StringName
|
||||||
|
) -> ScenarioAssignment:
|
||||||
|
|
||||||
|
for assignment in assignments:
|
||||||
|
if assignment.cabinet == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if assignment.cabinet.cabinet_id == cabinet_id:
|
||||||
|
return assignment
|
||||||
|
|
||||||
|
return null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d3pt6qa05kcnl
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
class_name ScenarioEditSession
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
signal scenario_changed(
|
||||||
|
scenario: ScenarioDefinition
|
||||||
|
)
|
||||||
|
|
||||||
|
signal dirty_changed(
|
||||||
|
is_dirty: bool
|
||||||
|
)
|
||||||
|
|
||||||
|
signal saved
|
||||||
|
signal reverted
|
||||||
|
|
||||||
|
|
||||||
|
var source_scenario: ScenarioDefinition
|
||||||
|
var working_scenario: ScenarioDefinition
|
||||||
|
|
||||||
|
var is_dirty: bool = false
|
||||||
|
|
||||||
|
var saved_state: Dictionary = {}
|
||||||
|
|
||||||
|
func begin(
|
||||||
|
scenario: ScenarioDefinition
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
if scenario == null:
|
||||||
|
push_error(
|
||||||
|
"ScenarioEditSession cannot begin with a null scenario."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
source_scenario = scenario
|
||||||
|
|
||||||
|
_create_working_copy()
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func mark_dirty() -> void:
|
||||||
|
|
||||||
|
if working_scenario == null:
|
||||||
|
_set_dirty(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var current_state := (
|
||||||
|
_capture_scenario_state(
|
||||||
|
working_scenario
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_dirty(
|
||||||
|
current_state != saved_state
|
||||||
|
)
|
||||||
|
|
||||||
|
func save() -> bool:
|
||||||
|
if source_scenario == null:
|
||||||
|
push_error(
|
||||||
|
"ScenarioEditSession has no source scenario."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if working_scenario == null:
|
||||||
|
push_error(
|
||||||
|
"ScenarioEditSession has no working scenario."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
_apply_working_copy_to_source()
|
||||||
|
|
||||||
|
var path := source_scenario.resource_path
|
||||||
|
|
||||||
|
if path.is_empty():
|
||||||
|
push_error(
|
||||||
|
"Source ScenarioDefinition has no resource path."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var error := ResourceSaver.save(
|
||||||
|
source_scenario,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
if error != OK:
|
||||||
|
push_error(
|
||||||
|
"Failed to save ScenarioDefinition. Error: %s"
|
||||||
|
% error
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
saved_state = (
|
||||||
|
_capture_scenario_state(
|
||||||
|
working_scenario
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_dirty(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
saved.emit()
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func revert() -> void:
|
||||||
|
|
||||||
|
if source_scenario == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
_create_working_copy()
|
||||||
|
|
||||||
|
reverted.emit()
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[SCENARIO EDITOR] Reverted unsaved changes."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _create_working_copy() -> void:
|
||||||
|
|
||||||
|
var copy := ScenarioDefinition.new()
|
||||||
|
|
||||||
|
copy.scenario_id = source_scenario.scenario_id
|
||||||
|
copy.display_name = source_scenario.display_name
|
||||||
|
copy.version = source_scenario.version
|
||||||
|
|
||||||
|
for source_assignment in source_scenario.assignments:
|
||||||
|
|
||||||
|
if source_assignment == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var assignment := (
|
||||||
|
ScenarioAssignment.new()
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Preserve references to the actual
|
||||||
|
# game/cabinet definitions.
|
||||||
|
#
|
||||||
|
assignment.cabinet = (
|
||||||
|
source_assignment.cabinet
|
||||||
|
)
|
||||||
|
|
||||||
|
assignment.game = (
|
||||||
|
source_assignment.game
|
||||||
|
)
|
||||||
|
|
||||||
|
assignment.variant = (
|
||||||
|
source_assignment.variant
|
||||||
|
)
|
||||||
|
|
||||||
|
assignment.event_mappings = (
|
||||||
|
_duplicate_mappings(
|
||||||
|
source_assignment.event_mappings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
copy.assignments.append(
|
||||||
|
assignment
|
||||||
|
)
|
||||||
|
|
||||||
|
working_scenario = copy
|
||||||
|
|
||||||
|
#
|
||||||
|
# This working copy is now our clean,
|
||||||
|
# saved baseline.
|
||||||
|
#
|
||||||
|
saved_state = (
|
||||||
|
_capture_scenario_state(
|
||||||
|
working_scenario
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_dirty(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
scenario_changed.emit(
|
||||||
|
working_scenario
|
||||||
|
)
|
||||||
|
|
||||||
|
func _duplicate_mappings(
|
||||||
|
source_mappings: Array[ScenarioEventMapping]
|
||||||
|
) -> Array[ScenarioEventMapping]:
|
||||||
|
|
||||||
|
var result: Array[ScenarioEventMapping] = []
|
||||||
|
|
||||||
|
for source_mapping in source_mappings:
|
||||||
|
if source_mapping == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var mapping := ScenarioEventMapping.new()
|
||||||
|
|
||||||
|
mapping.game_event = (
|
||||||
|
source_mapping.game_event
|
||||||
|
)
|
||||||
|
|
||||||
|
mapping.hardware_actions = (
|
||||||
|
source_mapping.hardware_actions.duplicate()
|
||||||
|
)
|
||||||
|
|
||||||
|
result.append(mapping)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_working_copy_to_source() -> void:
|
||||||
|
for working_assignment in working_scenario.assignments:
|
||||||
|
if working_assignment == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if working_assignment.cabinet == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var source_assignment := (
|
||||||
|
source_scenario.get_assignment_for_cabinet(
|
||||||
|
working_assignment.cabinet.cabinet_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if source_assignment == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_assignment.event_mappings = (
|
||||||
|
_duplicate_mappings(
|
||||||
|
working_assignment.event_mappings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
func _capture_scenario_state(
|
||||||
|
scenario: ScenarioDefinition
|
||||||
|
) -> Dictionary:
|
||||||
|
|
||||||
|
if scenario == null:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var state := {
|
||||||
|
"scenario_id": scenario.scenario_id,
|
||||||
|
"display_name": scenario.display_name,
|
||||||
|
"version": scenario.version,
|
||||||
|
"assignments": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for assignment in scenario.assignments:
|
||||||
|
|
||||||
|
if assignment == null:
|
||||||
|
state["assignments"].append(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var assignment_state := {
|
||||||
|
"cabinet_id": StringName(),
|
||||||
|
"game_id": StringName(),
|
||||||
|
"variant": assignment.variant,
|
||||||
|
"event_mappings": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if assignment.cabinet != null:
|
||||||
|
assignment_state["cabinet_id"] = (
|
||||||
|
assignment.cabinet.cabinet_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if assignment.game != null:
|
||||||
|
assignment_state["game_id"] = (
|
||||||
|
assignment.game.game_id
|
||||||
|
)
|
||||||
|
|
||||||
|
for mapping in assignment.event_mappings:
|
||||||
|
|
||||||
|
if mapping == null:
|
||||||
|
assignment_state[
|
||||||
|
"event_mappings"
|
||||||
|
].append(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
var mapping_state := {
|
||||||
|
"game_event": mapping.game_event,
|
||||||
|
"hardware_actions": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for action_id in mapping.hardware_actions:
|
||||||
|
mapping_state[
|
||||||
|
"hardware_actions"
|
||||||
|
].append(
|
||||||
|
action_id
|
||||||
|
)
|
||||||
|
|
||||||
|
assignment_state[
|
||||||
|
"event_mappings"
|
||||||
|
].append(
|
||||||
|
mapping_state
|
||||||
|
)
|
||||||
|
|
||||||
|
state["assignments"].append(
|
||||||
|
assignment_state
|
||||||
|
)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
func _set_dirty(
|
||||||
|
value: bool
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if is_dirty == value:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_dirty = value
|
||||||
|
|
||||||
|
dirty_changed.emit(
|
||||||
|
is_dirty
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dc74xmy3c5hy2
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@tool
|
||||||
|
class_name ScenarioEventMapping
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var game_event: StringName = &""
|
||||||
|
@export var hardware_actions: Array[StringName] = []
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://lixrugm1v2d2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
@tool
|
||||||
|
class_name CabinetPlacement
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var cabinet_id: StringName = &""
|
||||||
|
@export var position: Vector2 = Vector2.ZERO
|
||||||
|
@export var rotation_degrees: float = 0.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cwblyetugbr4d
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@tool
|
||||||
|
class_name RoomDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var room_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
|
||||||
|
@export var cabinet_placements: Array[CabinetPlacement] = []
|
||||||
|
@export var devices: Array[RoomDeviceDefinition] = []
|
||||||
|
@export var bindings: Array[RoomDeviceBinding] = []
|
||||||
|
|
||||||
|
@export_group("Geometry")
|
||||||
|
@export var boundary_points: PackedVector2Array = PackedVector2Array()
|
||||||
|
@export var walls: Array[RoomWallDefinition] = []
|
||||||
|
|
||||||
|
@export_group("Logic")
|
||||||
|
|
||||||
|
@export var logic_rules: Array[RoomLogicRule] = []
|
||||||
|
|
||||||
|
func get_device(
|
||||||
|
device_id: StringName
|
||||||
|
) -> RoomDeviceDefinition:
|
||||||
|
|
||||||
|
for device in devices:
|
||||||
|
if device == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if device.device_id == device_id:
|
||||||
|
return device
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_cabinet_placement(
|
||||||
|
cabinet_id: StringName
|
||||||
|
) -> CabinetPlacement:
|
||||||
|
|
||||||
|
for placement in cabinet_placements:
|
||||||
|
if placement == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if placement.cabinet_id == cabinet_id:
|
||||||
|
return placement
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_device_for_action(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName
|
||||||
|
) -> RoomDeviceDefinition:
|
||||||
|
|
||||||
|
for binding in bindings:
|
||||||
|
if binding == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
binding.cabinet_id == cabinet_id
|
||||||
|
and
|
||||||
|
binding.hardware_action_id == action_id
|
||||||
|
):
|
||||||
|
return get_device(binding.device_id)
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
func get_wall(
|
||||||
|
wall_id: StringName
|
||||||
|
) -> RoomWallDefinition:
|
||||||
|
|
||||||
|
for wall in walls:
|
||||||
|
if wall == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if wall.wall_id == wall_id:
|
||||||
|
return wall
|
||||||
|
|
||||||
|
return null
|
||||||
|
|
||||||
|
func has_wall(
|
||||||
|
wall_id: StringName
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
return get_wall(
|
||||||
|
wall_id
|
||||||
|
) != null
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dgm2dkw5rar8i
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
@tool
|
||||||
|
class_name RoomDeviceBinding
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var cabinet_id: StringName = &""
|
||||||
|
@export var hardware_action_id: StringName = &""
|
||||||
|
|
||||||
|
@export var device_id: StringName = &""
|
||||||
|
|
||||||
|
@export_group("Physical Cable Route")
|
||||||
|
|
||||||
|
@export var route_points: PackedVector2Array = (
|
||||||
|
PackedVector2Array()
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://r2rpbasss511
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
@tool
|
||||||
|
class_name RoomDeviceDefinition
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
enum DeviceType {
|
||||||
|
GENERIC,
|
||||||
|
COIN_RELEASE,
|
||||||
|
LIGHT,
|
||||||
|
DOOR_LOCK,
|
||||||
|
DRAWER_LOCK,
|
||||||
|
BUZZER
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
enum Behavior {
|
||||||
|
MOMENTARY,
|
||||||
|
LATCHED
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@export var device_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
|
||||||
|
@export var device_type: DeviceType = DeviceType.GENERIC
|
||||||
|
@export var behavior: Behavior = Behavior.MOMENTARY
|
||||||
|
|
||||||
|
@export var initial_active: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
@export_group("Layout")
|
||||||
|
@export var position: Vector2 = Vector2.ZERO
|
||||||
|
|
||||||
|
const FEET_TO_METERS: float = 0.3048
|
||||||
|
const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Internal/storage representation.
|
||||||
|
#
|
||||||
|
@export_storage
|
||||||
|
var physical_size_m: Vector2 = Vector2(
|
||||||
|
0.25,
|
||||||
|
0.25
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Designer-facing representation.
|
||||||
|
#
|
||||||
|
@export_group("Physical Dimensions")
|
||||||
|
|
||||||
|
@export_custom(
|
||||||
|
PROPERTY_HINT_NONE,
|
||||||
|
"suffix: ft"
|
||||||
|
)
|
||||||
|
var physical_size_ft: Vector2:
|
||||||
|
get:
|
||||||
|
return (
|
||||||
|
physical_size_m
|
||||||
|
* METERS_TO_FEET
|
||||||
|
)
|
||||||
|
set(value):
|
||||||
|
physical_size_m = (
|
||||||
|
value
|
||||||
|
* FEET_TO_METERS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@export_group("Physical Appearance")
|
||||||
|
|
||||||
|
@export var editor_texture: Texture2D = null
|
||||||
|
|
||||||
|
@export_range(0.1, 4.0, 0.05)
|
||||||
|
var editor_texture_scale: float = 1.0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ejum18yr7xeo
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
class_name RoomEditSession
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
signal room_changed(room: RoomDefinition)
|
||||||
|
signal dirty_changed(is_dirty: bool)
|
||||||
|
signal saved
|
||||||
|
signal reverted
|
||||||
|
|
||||||
|
|
||||||
|
var source_room: RoomDefinition
|
||||||
|
var working_room: RoomDefinition
|
||||||
|
|
||||||
|
var is_dirty: bool = false
|
||||||
|
|
||||||
|
var saved_state: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func begin(room: RoomDefinition) -> bool:
|
||||||
|
if room == null:
|
||||||
|
push_error(
|
||||||
|
"RoomEditSession cannot begin with a null room."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
source_room = room
|
||||||
|
_create_working_copy()
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func mark_dirty() -> void:
|
||||||
|
|
||||||
|
if working_room == null:
|
||||||
|
_set_dirty(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var current_state := (
|
||||||
|
_capture_room_state(
|
||||||
|
working_room
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_dirty(
|
||||||
|
current_state != saved_state
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func save() -> bool:
|
||||||
|
if source_room == null:
|
||||||
|
push_error(
|
||||||
|
"RoomEditSession has no source room."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
if working_room == null:
|
||||||
|
push_error(
|
||||||
|
"RoomEditSession has no working room."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
_apply_working_copy_to_source()
|
||||||
|
|
||||||
|
var path := source_room.resource_path
|
||||||
|
|
||||||
|
if path.is_empty():
|
||||||
|
push_error(
|
||||||
|
"Source RoomDefinition has no resource path."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var error := ResourceSaver.save(
|
||||||
|
source_room,
|
||||||
|
path
|
||||||
|
)
|
||||||
|
|
||||||
|
if error != OK:
|
||||||
|
push_error(
|
||||||
|
"Failed to save RoomDefinition. Error: %s"
|
||||||
|
% error
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[ROOM EDITOR] Saved room to '%s'."
|
||||||
|
% path
|
||||||
|
)
|
||||||
|
|
||||||
|
saved_state = (
|
||||||
|
_capture_room_state(
|
||||||
|
working_room
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_dirty(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
saved.emit()
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func revert() -> void:
|
||||||
|
|
||||||
|
if source_room == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
_create_working_copy()
|
||||||
|
|
||||||
|
reverted.emit()
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[ROOM EDITOR] Reverted unsaved changes."
|
||||||
|
)
|
||||||
|
|
||||||
|
func reload_from_disk() -> bool:
|
||||||
|
|
||||||
|
if source_room == null:
|
||||||
|
return false
|
||||||
|
|
||||||
|
var path := (
|
||||||
|
source_room.resource_path
|
||||||
|
)
|
||||||
|
|
||||||
|
if path.is_empty():
|
||||||
|
push_error(
|
||||||
|
"Source RoomDefinition has no resource path."
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
var reloaded := (
|
||||||
|
ResourceLoader.load(
|
||||||
|
path,
|
||||||
|
"",
|
||||||
|
ResourceLoader.CACHE_MODE_REPLACE
|
||||||
|
)
|
||||||
|
as RoomDefinition
|
||||||
|
)
|
||||||
|
|
||||||
|
if reloaded == null:
|
||||||
|
push_error(
|
||||||
|
"Failed to reload RoomDefinition from '%s'."
|
||||||
|
% path
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
|
||||||
|
source_room = reloaded
|
||||||
|
|
||||||
|
_create_working_copy()
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[ROOM EDITOR] Reloaded room from disk: '%s'."
|
||||||
|
% path
|
||||||
|
)
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
func _create_working_copy() -> void:
|
||||||
|
|
||||||
|
working_room = (
|
||||||
|
_duplicate_room(
|
||||||
|
source_room
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
saved_state = (
|
||||||
|
_capture_room_state(
|
||||||
|
working_room
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_dirty(
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
room_changed.emit(
|
||||||
|
working_room
|
||||||
|
)
|
||||||
|
|
||||||
|
func _apply_working_copy_to_source() -> void:
|
||||||
|
|
||||||
|
for working_placement in working_room.cabinet_placements:
|
||||||
|
if working_placement == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var source_placement := (
|
||||||
|
source_room.get_cabinet_placement(
|
||||||
|
working_placement.cabinet_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if source_placement == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_placement.position = (
|
||||||
|
working_placement.position
|
||||||
|
)
|
||||||
|
|
||||||
|
source_placement.rotation_degrees = (
|
||||||
|
working_placement.rotation_degrees
|
||||||
|
)
|
||||||
|
|
||||||
|
for working_device in working_room.devices:
|
||||||
|
if working_device == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var source_device := (
|
||||||
|
source_room.get_device(
|
||||||
|
working_device.device_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if source_device == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_device.position = (
|
||||||
|
working_device.position
|
||||||
|
)
|
||||||
|
|
||||||
|
for working_binding in working_room.bindings:
|
||||||
|
|
||||||
|
if working_binding == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var source_binding: RoomDeviceBinding = null
|
||||||
|
|
||||||
|
for candidate in source_room.bindings:
|
||||||
|
|
||||||
|
if candidate == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if (
|
||||||
|
candidate.cabinet_id
|
||||||
|
== working_binding.cabinet_id
|
||||||
|
and candidate.hardware_action_id
|
||||||
|
== working_binding.hardware_action_id
|
||||||
|
and candidate.device_id
|
||||||
|
== working_binding.device_id
|
||||||
|
):
|
||||||
|
source_binding = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if source_binding == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
source_binding.route_points = (
|
||||||
|
working_binding.route_points.duplicate()
|
||||||
|
)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copy authored room geometry.
|
||||||
|
#
|
||||||
|
source_room.boundary_points = (
|
||||||
|
working_room.boundary_points.duplicate()
|
||||||
|
)
|
||||||
|
|
||||||
|
source_room.walls = _duplicate_walls(
|
||||||
|
working_room.walls
|
||||||
|
)
|
||||||
|
|
||||||
|
func _duplicate_room(
|
||||||
|
source: RoomDefinition
|
||||||
|
) -> RoomDefinition:
|
||||||
|
|
||||||
|
var copy := (
|
||||||
|
source.duplicate(true)
|
||||||
|
as RoomDefinition
|
||||||
|
)
|
||||||
|
|
||||||
|
if copy == null:
|
||||||
|
return null
|
||||||
|
|
||||||
|
#
|
||||||
|
# Explicitly duplicate authored geometry so
|
||||||
|
# wall Resources are never shared with source_room.
|
||||||
|
#
|
||||||
|
copy.boundary_points = (
|
||||||
|
source.boundary_points.duplicate()
|
||||||
|
)
|
||||||
|
|
||||||
|
copy.walls = _duplicate_walls(
|
||||||
|
source.walls
|
||||||
|
)
|
||||||
|
|
||||||
|
return copy
|
||||||
|
|
||||||
|
|
||||||
|
func _duplicate_walls(
|
||||||
|
source: Array[RoomWallDefinition]
|
||||||
|
) -> Array[RoomWallDefinition]:
|
||||||
|
|
||||||
|
var result: Array[RoomWallDefinition] = []
|
||||||
|
|
||||||
|
for wall in source:
|
||||||
|
if wall == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var copy := (
|
||||||
|
wall.duplicate(true)
|
||||||
|
as RoomWallDefinition
|
||||||
|
)
|
||||||
|
|
||||||
|
if copy != null:
|
||||||
|
result.append(
|
||||||
|
copy
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
func _capture_room_state(
|
||||||
|
room: RoomDefinition
|
||||||
|
) -> Dictionary:
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var state := {
|
||||||
|
"room_id": room.room_id,
|
||||||
|
"display_name": room.display_name,
|
||||||
|
"boundary_points": room.boundary_points.duplicate(),
|
||||||
|
"cabinet_placements": [],
|
||||||
|
"devices": [],
|
||||||
|
"bindings": [],
|
||||||
|
"walls": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for placement in room.cabinet_placements:
|
||||||
|
|
||||||
|
if placement == null:
|
||||||
|
state["cabinet_placements"].append(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
state["cabinet_placements"].append(
|
||||||
|
{
|
||||||
|
"cabinet_id": placement.cabinet_id,
|
||||||
|
"position": placement.position,
|
||||||
|
"rotation_degrees": (
|
||||||
|
placement.rotation_degrees
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for device in room.devices:
|
||||||
|
|
||||||
|
if device == null:
|
||||||
|
state["devices"].append(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
state["devices"].append(
|
||||||
|
{
|
||||||
|
"device_id": device.device_id,
|
||||||
|
"display_name": device.display_name,
|
||||||
|
"device_type": device.device_type,
|
||||||
|
"behavior": device.behavior,
|
||||||
|
"initial_active": (
|
||||||
|
device.initial_active
|
||||||
|
),
|
||||||
|
"position": device.position,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for binding in room.bindings:
|
||||||
|
|
||||||
|
if binding == null:
|
||||||
|
state["bindings"].append(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
state["bindings"].append(
|
||||||
|
{
|
||||||
|
"cabinet_id": binding.cabinet_id,
|
||||||
|
"hardware_action_id": (
|
||||||
|
binding.hardware_action_id
|
||||||
|
),
|
||||||
|
"device_id": binding.device_id,
|
||||||
|
"route_points": (
|
||||||
|
binding.route_points.duplicate()
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
for wall in room.walls:
|
||||||
|
|
||||||
|
if wall == null:
|
||||||
|
state["walls"].append(
|
||||||
|
null
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
state["walls"].append(
|
||||||
|
{
|
||||||
|
"wall_id": wall.wall_id,
|
||||||
|
"start": wall.start,
|
||||||
|
"end": wall.end,
|
||||||
|
"thickness": wall.thickness,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
|
func _set_dirty(
|
||||||
|
value: bool
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if is_dirty == value:
|
||||||
|
return
|
||||||
|
|
||||||
|
is_dirty = value
|
||||||
|
|
||||||
|
dirty_changed.emit(
|
||||||
|
is_dirty
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://w0en0xus0g5j
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
@tool
|
||||||
|
class_name RoomSimulator
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
signal device_state_changed(
|
||||||
|
device_id: StringName,
|
||||||
|
active: bool
|
||||||
|
)
|
||||||
|
|
||||||
|
signal binding_activated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
device_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal binding_deactivated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
device_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal path_activated(
|
||||||
|
path_id: StringName,
|
||||||
|
cabinet_id: StringName,
|
||||||
|
event_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
device_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
signal path_deactivated(
|
||||||
|
path_id: StringName,
|
||||||
|
cabinet_id: StringName,
|
||||||
|
event_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
device_id: StringName
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var room: RoomDefinition
|
||||||
|
var device_states: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
room_definition: RoomDefinition
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
room = room_definition
|
||||||
|
device_states.clear()
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
push_error(
|
||||||
|
"RoomSimulator received a null RoomDefinition."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
for device in room.devices:
|
||||||
|
if device == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
device_states[
|
||||||
|
device.device_id
|
||||||
|
] = device.initial_active
|
||||||
|
|
||||||
|
if not Engine.is_editor_hint():
|
||||||
|
_connect_hardware_manager()
|
||||||
|
|
||||||
|
print("")
|
||||||
|
print("======= ROOM SIMULATOR =======")
|
||||||
|
print("Room: ", room.display_name)
|
||||||
|
print("Devices: ", room.devices.size())
|
||||||
|
print("==============================")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
|
||||||
|
func _connect_hardware_manager() -> void:
|
||||||
|
|
||||||
|
if not HardwareManager.action_activated.is_connected(
|
||||||
|
_on_hardware_action_activated
|
||||||
|
):
|
||||||
|
HardwareManager.action_activated.connect(
|
||||||
|
_on_hardware_action_activated
|
||||||
|
)
|
||||||
|
|
||||||
|
if not HardwareManager.action_deactivated.is_connected(
|
||||||
|
_on_hardware_action_deactivated
|
||||||
|
):
|
||||||
|
HardwareManager.action_deactivated.connect(
|
||||||
|
_on_hardware_action_deactivated
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func reset() -> void:
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
for device in room.devices:
|
||||||
|
if device == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
_set_device_state(
|
||||||
|
device.device_id,
|
||||||
|
device.initial_active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_device_state(
|
||||||
|
device_id: StringName
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
return device_states.get(
|
||||||
|
device_id,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Runtime HardwareManager entry points
|
||||||
|
#
|
||||||
|
func _on_hardware_action_activated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
_process_action_activated(
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hardware_action_deactivated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
_process_action_deactivated(
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Editor simulation entry points
|
||||||
|
#
|
||||||
|
func simulate_action_activated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary = {}
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
_process_action_activated(
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func simulate_action_deactivated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary = {}
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
_process_action_deactivated(
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Shared processing
|
||||||
|
#
|
||||||
|
func _process_action_activated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var device := room.get_device_for_action(
|
||||||
|
cabinet_id,
|
||||||
|
action_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if device == null:
|
||||||
|
print(
|
||||||
|
"[SIMULATION] No room device bound to %s:%s"
|
||||||
|
% [
|
||||||
|
cabinet_id,
|
||||||
|
action_id
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
var event_id := StringName(
|
||||||
|
context.get(
|
||||||
|
"game_event",
|
||||||
|
"unknown_event"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var path_id := LogicPathId.make(
|
||||||
|
cabinet_id,
|
||||||
|
event_id,
|
||||||
|
action_id,
|
||||||
|
device.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
path_activated.emit(
|
||||||
|
path_id,
|
||||||
|
cabinet_id,
|
||||||
|
event_id,
|
||||||
|
action_id,
|
||||||
|
device.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
binding_activated.emit(
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
device.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
_set_device_state(
|
||||||
|
device.device_id,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _process_action_deactivated(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if room == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var device := room.get_device_for_action(
|
||||||
|
cabinet_id,
|
||||||
|
action_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if device == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var event_id := StringName(
|
||||||
|
context.get(
|
||||||
|
"game_event",
|
||||||
|
"unknown_event"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var path_id := LogicPathId.make(
|
||||||
|
cabinet_id,
|
||||||
|
event_id,
|
||||||
|
action_id,
|
||||||
|
device.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
path_deactivated.emit(
|
||||||
|
path_id,
|
||||||
|
cabinet_id,
|
||||||
|
event_id,
|
||||||
|
action_id,
|
||||||
|
device.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
binding_deactivated.emit(
|
||||||
|
cabinet_id,
|
||||||
|
action_id,
|
||||||
|
device.device_id
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
device.behavior
|
||||||
|
== RoomDeviceDefinition.Behavior.MOMENTARY
|
||||||
|
):
|
||||||
|
_set_device_state(
|
||||||
|
device.device_id,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_device_state(
|
||||||
|
device_id: StringName,
|
||||||
|
active: bool
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
device_states[
|
||||||
|
device_id
|
||||||
|
] = active
|
||||||
|
|
||||||
|
print(
|
||||||
|
"[SIMULATION] Device '%s' -> %s"
|
||||||
|
% [
|
||||||
|
device_id,
|
||||||
|
"ACTIVE" if active else "INACTIVE"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
device_state_changed.emit(
|
||||||
|
device_id,
|
||||||
|
active
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bfb7sad2mjnlf
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
class_name SimulatedHardwareBackend
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
|
||||||
|
signal pin_state_changed(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
gpio_pin: int,
|
||||||
|
active: bool,
|
||||||
|
action_id: StringName,
|
||||||
|
context: Dictionary
|
||||||
|
)
|
||||||
|
|
||||||
|
signal trace_entry_created(
|
||||||
|
entry: Dictionary
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
var pin_states: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func reset() -> void:
|
||||||
|
|
||||||
|
pin_states.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func activate_action(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action: HardwareAction,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if action == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var gpio_active := action.active_high
|
||||||
|
|
||||||
|
_set_pin_state(
|
||||||
|
cabinet_id,
|
||||||
|
action,
|
||||||
|
gpio_active,
|
||||||
|
true,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func deactivate_action(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action: HardwareAction,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
if action == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var gpio_active := not action.active_high
|
||||||
|
|
||||||
|
_set_pin_state(
|
||||||
|
cabinet_id,
|
||||||
|
action,
|
||||||
|
gpio_active,
|
||||||
|
false,
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_pin_state(
|
||||||
|
cabinet_id: StringName,
|
||||||
|
action: HardwareAction,
|
||||||
|
electrical_state: bool,
|
||||||
|
logical_active: bool,
|
||||||
|
context: Dictionary
|
||||||
|
) -> void:
|
||||||
|
|
||||||
|
var key := "%s::%d" % [
|
||||||
|
cabinet_id,
|
||||||
|
action.gpio_pin
|
||||||
|
]
|
||||||
|
|
||||||
|
pin_states[key] = electrical_state
|
||||||
|
|
||||||
|
var entry := {
|
||||||
|
"type": &"gpio",
|
||||||
|
"cabinet_id": cabinet_id,
|
||||||
|
"action_id": action.action_id,
|
||||||
|
"gpio_pin": action.gpio_pin,
|
||||||
|
"electrical_state": electrical_state,
|
||||||
|
"logical_active": logical_active,
|
||||||
|
"active_high": action.active_high,
|
||||||
|
"context": context.duplicate(),
|
||||||
|
}
|
||||||
|
|
||||||
|
trace_entry_created.emit(
|
||||||
|
entry
|
||||||
|
)
|
||||||
|
|
||||||
|
pin_state_changed.emit(
|
||||||
|
cabinet_id,
|
||||||
|
action.gpio_pin,
|
||||||
|
logical_active,
|
||||||
|
action.action_id,
|
||||||
|
context
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b3xyoxcebkhoc
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
@tool
|
||||||
|
class_name TestSequence
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export var sequence_id: StringName = &""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
@export_group("Compatibility")
|
||||||
|
@export var scenario_id: StringName = &""
|
||||||
|
|
||||||
|
@export_multiline
|
||||||
|
var description: String = ""
|
||||||
|
|
||||||
|
@export var steps: Array[TestSequenceStep] = []
|
||||||
|
|
||||||
|
|
||||||
|
func get_sorted_steps() -> Array[TestSequenceStep]:
|
||||||
|
|
||||||
|
var result: Array[TestSequenceStep] = []
|
||||||
|
|
||||||
|
for step in steps:
|
||||||
|
|
||||||
|
if step == null:
|
||||||
|
continue
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
step
|
||||||
|
)
|
||||||
|
|
||||||
|
result.sort_custom(
|
||||||
|
func(
|
||||||
|
a: TestSequenceStep,
|
||||||
|
b: TestSequenceStep
|
||||||
|
) -> bool:
|
||||||
|
|
||||||
|
return (
|
||||||
|
a.time_offset_seconds
|
||||||
|
< b.time_offset_seconds
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://clbm4alwyibrd
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
@tool
|
||||||
|
class_name TestSequenceStep
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
|
||||||
|
@export_range(0.0, 36000.0, 0.1)
|
||||||
|
var time_offset_seconds: float = 0.0
|
||||||
|
|
||||||
|
@export var source_id: StringName = &""
|
||||||
|
@export var event_id: StringName = &""
|
||||||
|
|
||||||
|
@export_multiline
|
||||||
|
var note: String = ""
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c5022gsswr487
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CabinetProfile" format=3 uid="uid://cl4j1vtv388pt"]
|
||||||
|
|
||||||
|
[ext_resource type="Resource" uid="uid://cxna0oimf8mop" path="res://data/cabinets/cabinet_01_hardware.tres" id="1_b4cnm"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cistuspr5bf8h" path="res://art/devices/tex_cabinet.png" id="1_hv02u"]
|
||||||
|
[ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="1_v1x8f"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_v1x8f")
|
||||||
|
cabinet_id = &"cabinet_01"
|
||||||
|
display_name = "Cabinet 01"
|
||||||
|
capabilities = Array[StringName]([&"joystick_4way", &"button_1", &"start_button", &"joystick_horizontal"])
|
||||||
|
hardware_config = ExtResource("1_b4cnm")
|
||||||
|
footprint_width_m = 0.9144000000000001
|
||||||
|
footprint_depth_m = 0.9144000000000001
|
||||||
|
height_m = 1.8288000000000002
|
||||||
|
footprint_width_ft = 3.0
|
||||||
|
footprint_depth_ft = 3.0
|
||||||
|
height_ft = 6.0
|
||||||
|
editor_texture = ExtResource("1_hv02u")
|
||||||
|
metadata/_custom_type_script = "uid://c0jj5j5loqsma"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
[gd_resource type="Resource" script_class="HardwareConfig" format=3 uid="uid://cxna0oimf8mop"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c7eu1n5yey1x4" path="res://core/hardware/hardware_action.gd" id="1_w3p60"]
|
||||||
|
[ext_resource type="Script" uid="uid://60whm57itpjb" path="res://core/hardware/hardware_config.gd" id="2_s5has"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ogdj5"]
|
||||||
|
script = ExtResource("1_w3p60")
|
||||||
|
action_id = &"coin_release"
|
||||||
|
display_name = "Coin Release"
|
||||||
|
pulse_duration_ms = 600
|
||||||
|
metadata/_custom_type_script = "uid://c7eu1n5yey1x4"
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_gu74o"]
|
||||||
|
script = ExtResource("1_w3p60")
|
||||||
|
action_id = &"start_light"
|
||||||
|
display_name = "Start Light"
|
||||||
|
gpio_pin = 20
|
||||||
|
metadata/_custom_type_script = "uid://c7eu1n5yey1x4"
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("2_s5has")
|
||||||
|
controller_id = &"cabinet_01_gpio"
|
||||||
|
actions = Array[ExtResource("1_w3p60")]([SubResource("Resource_ogdj5"), SubResource("Resource_gu74o")])
|
||||||
|
metadata/_custom_type_script = "uid://60whm57itpjb"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CabinetProfile" format=3 uid="uid://d1gyyfaaclas8"]
|
||||||
|
|
||||||
|
[ext_resource type="Resource" uid="uid://ddv64yg3h5vsi" path="res://data/cabinets/cabinet_02_hardware.tres" id="1_bfw68"]
|
||||||
|
[ext_resource type="Texture2D" uid="uid://cistuspr5bf8h" path="res://art/devices/tex_cabinet.png" id="1_lrfm7"]
|
||||||
|
[ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="2_lrfm7"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("2_lrfm7")
|
||||||
|
cabinet_id = &"cabinet_02"
|
||||||
|
display_name = "Cabinet 02"
|
||||||
|
capabilities = Array[StringName]([&"joystick_4way", &"button_1", &"start_button", &"joystick_horizontal"])
|
||||||
|
hardware_config = ExtResource("1_bfw68")
|
||||||
|
footprint_width_m = 0.9144000000000001
|
||||||
|
footprint_depth_m = 0.9144000000000001
|
||||||
|
height_m = 1.8288000000000002
|
||||||
|
footprint_width_ft = 3.0
|
||||||
|
footprint_depth_ft = 3.0
|
||||||
|
height_ft = 6.0
|
||||||
|
editor_texture = ExtResource("1_lrfm7")
|
||||||
|
metadata/_custom_type_script = "uid://c0jj5j5loqsma"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
[gd_resource type="Resource" script_class="HardwareConfig" format=3 uid="uid://ddv64yg3h5vsi"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c7eu1n5yey1x4" path="res://core/hardware/hardware_action.gd" id="1_dasqc"]
|
||||||
|
[ext_resource type="Script" uid="uid://60whm57itpjb" path="res://core/hardware/hardware_config.gd" id="2_ye2fg"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ogdj5"]
|
||||||
|
script = ExtResource("1_dasqc")
|
||||||
|
action_id = &"coin_release"
|
||||||
|
display_name = "Coin Release"
|
||||||
|
gpio_pin = 27
|
||||||
|
pulse_duration_ms = 400
|
||||||
|
metadata/_custom_type_script = "uid://c7eu1n5yey1x4"
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_dasqc"]
|
||||||
|
script = ExtResource("1_dasqc")
|
||||||
|
action_id = &"start_light"
|
||||||
|
display_name = "Start Light"
|
||||||
|
gpio_pin = 20
|
||||||
|
metadata/_custom_type_script = "uid://c7eu1n5yey1x4"
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_t81ly"]
|
||||||
|
script = ExtResource("1_dasqc")
|
||||||
|
action_id = &"glitch_code"
|
||||||
|
display_name = "Glitch Code"
|
||||||
|
gpio_pin = 21
|
||||||
|
pulse_duration_ms = 1200
|
||||||
|
metadata/_custom_type_script = "uid://c7eu1n5yey1x4"
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("2_ye2fg")
|
||||||
|
controller_id = &"cabinet_02_gpio"
|
||||||
|
actions = Array[ExtResource("1_dasqc")]([SubResource("Resource_ogdj5"), SubResource("Resource_dasqc"), SubResource("Resource_t81ly")])
|
||||||
|
metadata/_custom_type_script = "uid://60whm57itpjb"
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[gd_resource type="Resource" script_class="CabinetProfile" format=3 uid="uid://d4ns05ptr4ybw"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="1_0s4iw"]
|
||||||
|
[ext_resource type="Resource" uid="uid://4t6n8gvu717o" path="res://data/cabinets/cabinet_03_hardware.tres" id="1_ojmvu"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_0s4iw")
|
||||||
|
cabinet_id = &"cabinet_03"
|
||||||
|
display_name = "Cabinet 03"
|
||||||
|
capabilities = Array[StringName]([&"spinner", &"button_1", &"start_button"])
|
||||||
|
hardware_config = ExtResource("1_ojmvu")
|
||||||
|
footprint_width_m = 1.0
|
||||||
|
footprint_depth_m = 1.0
|
||||||
|
height_m = 2.0
|
||||||
|
metadata/_custom_type_script = "uid://c0jj5j5loqsma"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[gd_resource type="Resource" script_class="HardwareConfig" format=3 uid="uid://4t6n8gvu717o"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://c7eu1n5yey1x4" path="res://core/hardware/hardware_action.gd" id="1_2vvyl"]
|
||||||
|
[ext_resource type="Script" uid="uid://60whm57itpjb" path="res://core/hardware/hardware_config.gd" id="2_hx0bc"]
|
||||||
|
|
||||||
|
[sub_resource type="Resource" id="Resource_ogdj5"]
|
||||||
|
script = ExtResource("1_2vvyl")
|
||||||
|
action_id = &"coin_release"
|
||||||
|
display_name = "Coin Release"
|
||||||
|
gpio_pin = 27
|
||||||
|
pulse_duration_ms = 400
|
||||||
|
metadata/_custom_type_script = "uid://c7eu1n5yey1x4"
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("2_hx0bc")
|
||||||
|
controller_id = &"cabinet_02_gpio"
|
||||||
|
actions = Array[ExtResource("1_2vvyl")]([SubResource("Resource_ogdj5")])
|
||||||
|
metadata/_custom_type_script = "uid://60whm57itpjb"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[gd_resource type="Resource" script_class="RoomDeviceDefinition" format=3 uid="uid://b5f2nynxh0ul2"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://ejum18yr7xeo" path="res://core/simulation/room_device_definition.gd" id="1_mqpli"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_mqpli")
|
||||||
|
metadata/_custom_type_script = "uid://ejum18yr7xeo"
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[gd_resource type="ButtonGroup" format=3 uid="uid://cly57dx12c8dg"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="GameEventDefinition" format=3 uid="uid://bfr1jcppwfl3y"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://byegr76ey4613" path="res://core/games/game_event_definition.gd" id="1_c8820"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_c8820")
|
||||||
|
event_id = &"game_lost"
|
||||||
|
display_name = "Game Lost"
|
||||||
|
metadata/_custom_type_script = "uid://byegr76ey4613"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="GameEventDefinition" format=3 uid="uid://c16gsr3mhf8kt"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://byegr76ey4613" path="res://core/games/game_event_definition.gd" id="1_ud73p"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_ud73p")
|
||||||
|
event_id = &"game_reset"
|
||||||
|
display_name = "Game Reset"
|
||||||
|
metadata/_custom_type_script = "uid://byegr76ey4613"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="GameEventDefinition" format=3 uid="uid://dnpg3vgcuutmb"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://byegr76ey4613" path="res://core/games/game_event_definition.gd" id="1_yxh1o"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_yxh1o")
|
||||||
|
event_id = &"game_started"
|
||||||
|
display_name = "Game Started"
|
||||||
|
metadata/_custom_type_script = "uid://byegr76ey4613"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[gd_resource type="Resource" script_class="GameEventDefinition" format=3 uid="uid://djimuypcf66vn"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://byegr76ey4613" path="res://core/games/game_event_definition.gd" id="1_jg0s2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_jg0s2")
|
||||||
|
event_id = &"game_won"
|
||||||
|
display_name = "Game Won"
|
||||||
|
metadata/_custom_type_script = "uid://byegr76ey4613"
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[gd_resource type="Resource" script_class="GameDefinition" format=3]
|
||||||
|
|
||||||
|
[ext_resource type="PackedScene" path="res://games/frogger/Frogger.tscn" id="1_bn3jg"]
|
||||||
|
[ext_resource type="Script" path="res://core/games/game_event_definition.gd" id="1_kd6b3"]
|
||||||
|
[ext_resource type="Script" path="res://core/games/game_definition.gd" id="1_mpx30"]
|
||||||
|
[ext_resource type="Resource" path="res://data/game_events/game_started.tres" id="2_q2a4n"]
|
||||||
|
[ext_resource type="Resource" path="res://data/game_events/game_won.tres" id="3_otabs"]
|
||||||
|
[ext_resource type="Resource" path="res://data/game_events/game_lost.tres" id="4_x4djy"]
|
||||||
|
[ext_resource type="Resource" path="res://data/game_events/game_reset.tres" id="5_madm2"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_mpx30")
|
||||||
|
game_id = &"frogger"
|
||||||
|
display_name = "Frogger"
|
||||||
|
scene = ExtResource("1_bn3jg")
|
||||||
|
required_capabilities = Array[StringName]([&"joystick_4way", &"start_button"])
|
||||||
|
events = Array[ExtResource("1_kd6b3")]([ExtResource("2_q2a4n"), ExtResource("3_otabs"), ExtResource("4_x4djy"), ExtResource("5_madm2")])
|
||||||
|
|
||||||
|
|
||||||
Loaded 100 of 505 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user