ci: gate .NET workflow on server paths; add GDScript lint/format CI

- Run .NET build/test only when the solution, server/, or the workflow changes.
- Add gdlint and gdformat --check for client/ on relevant path changes.
- Align client GDScript with gdtoolkit (formatting, enum order, line length).
pull/10/head
VinPropane 2026-03-30 21:16:16 -04:00
parent a8cbcb5ff7
commit 15b0ee0921
6 changed files with 64 additions and 16 deletions

View File

@ -4,8 +4,16 @@ name: .NET
on: on:
push: push:
branches: [main] branches: [main]
paths:
- "NeonSprawl.sln"
- "server/**"
- ".github/workflows/dotnet.yml"
pull_request: pull_request:
branches: [main] branches: [main]
paths:
- "NeonSprawl.sln"
- "server/**"
- ".github/workflows/dotnet.yml"
jobs: jobs:
build: build:

33
.github/workflows/gdscript.yml vendored 100644
View File

@ -0,0 +1,33 @@
# Lint and format-check GDScript (Godot 4). Uses gdtoolkit: https://github.com/Scony/godot-gdscript-toolkit
name: GDScript
on:
push:
branches: [main]
paths:
- "client/**/*.gd"
- ".github/workflows/gdscript.yml"
pull_request:
branches: [main]
paths:
- "client/**/*.gd"
- ".github/workflows/gdscript.yml"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install gdtoolkit
run: pip install "gdtoolkit==4.5.0"
- name: gdlint
run: gdlint client/
- name: gdformat (check only)
run: gdformat --check client/

View File

@ -5,7 +5,8 @@ extends Node3D
signal target_chosen(world: Vector3) signal target_chosen(world: Vector3)
## Fallback when `Viewport.get_camera_3d()` is null (e.g. some editor setups). Set from `main.gd` in `_ready`. ## Fallback when `Viewport.get_camera_3d()` is null (e.g. some editor setups). Set from `main.gd`
## in `_ready`.
var fallback_camera: Camera3D var fallback_camera: Camera3D

View File

@ -1,6 +1,7 @@
extends Node3D extends Node3D
## NS-16: composes ground pick + server authority; see `ground_pick.gd` / `position_authority_client.gd`. ## NS-16: composes ground pick + server authority; see `ground_pick.gd` /
## `position_authority_client.gd`.
@onready var _camera: Camera3D = $World/Camera3D @onready var _camera: Camera3D = $World/Camera3D
@onready var _player: CharacterBody3D = $Player @onready var _player: CharacterBody3D = $Player
@ -11,7 +12,9 @@ extends Node3D
func _ready() -> void: func _ready() -> void:
_ground_pick.set("fallback_camera", _camera) _ground_pick.set("fallback_camera", _camera)
_ground_pick.connect("target_chosen", Callable(self, "_on_target_chosen")) _ground_pick.connect("target_chosen", Callable(self, "_on_target_chosen"))
_authority.connect("authoritative_position_received", Callable(self, "_on_authoritative_position")) _authority.connect(
"authoritative_position_received", Callable(self, "_on_authoritative_position")
)
_authority.call("sync_from_server") _authority.call("sync_from_server")

View File

@ -13,13 +13,13 @@ func _ready() -> void:
_goal = global_position _goal = global_position
func set_move_goal(world_pos: Vector3) -> void: func set_move_goal(world_pos: Vector3) -> void:
_goal = world_pos _goal = world_pos
_goal.y = global_position.y _goal.y = global_position.y
## NS-16: snap to authoritative server position; clears velocity so `move_and_slide` does not fight the sync. ## NS-16: snap to authoritative server position; clears velocity so `move_and_slide` does not fight
## the sync.
func snap_to_server(world_pos: Vector3) -> void: func snap_to_server(world_pos: Vector3) -> void:
global_position = world_pos global_position = world_pos
velocity = Vector3.ZERO velocity = Vector3.ZERO

View File

@ -1,18 +1,19 @@
extends Node extends Node
## NS-16: POST MoveCommand then GET PositionState; emits authoritative world position for the avatar. ## NS-16: POST MoveCommand then GET PositionState; emits authoritative world position for the
## avatar.
## (No `class_name` so headless/CI can load the project before `.godot` import exists.) ## (No `class_name` so headless/CI can load the project before `.godot` import exists.)
signal authoritative_position_received(world: Vector3) signal authoritative_position_received(world: Vector3)
enum Phase { BOOT_GET, POST_MOVE, VERIFY_GET }
@export var base_url: String = "http://127.0.0.1:5253" @export var base_url: String = "http://127.0.0.1:5253"
@export var dev_player_id: String = "dev-local-1" @export var dev_player_id: String = "dev-local-1"
var _http: HTTPRequest var _http: HTTPRequest
var _busy: bool = false var _busy: bool = false
var _phase: Phase = Phase.BOOT_GET
enum _Phase { BOOT_GET, POST_MOVE, VERIFY_GET }
var _phase: _Phase = _Phase.BOOT_GET
func _ready() -> void: func _ready() -> void:
@ -25,7 +26,7 @@ func sync_from_server() -> void:
if _busy: if _busy:
return return
_busy = true _busy = true
_phase = _Phase.BOOT_GET _phase = Phase.BOOT_GET
_request_get() _request_get()
@ -33,7 +34,7 @@ func submit_move_target(world: Vector3) -> void:
if _busy: if _busy:
return return
_busy = true _busy = true
_phase = _Phase.POST_MOVE _phase = Phase.POST_MOVE
var url := "%s/game/players/%s/move" % [_base_root(), _player_path_segment()] var url := "%s/game/players/%s/move" % [_base_root(), _player_path_segment()]
var payload := { var payload := {
"schemaVersion": 1, "schemaVersion": 1,
@ -64,7 +65,9 @@ func _request_get() -> void:
_busy = false _busy = false
func _on_request_completed(_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: func _on_request_completed(
_result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray
) -> void:
if _result != HTTPRequest.RESULT_SUCCESS: if _result != HTTPRequest.RESULT_SUCCESS:
_busy = false _busy = false
return return
@ -72,17 +75,17 @@ func _on_request_completed(_result: int, response_code: int, _headers: PackedStr
var text := body.get_string_from_utf8() var text := body.get_string_from_utf8()
match _phase: match _phase:
_Phase.BOOT_GET: Phase.BOOT_GET:
_busy = false _busy = false
if response_code == 200: if response_code == 200:
_emit_position_from_response(text) _emit_position_from_response(text)
_Phase.POST_MOVE: Phase.POST_MOVE:
if response_code != 200: if response_code != 200:
_busy = false _busy = false
return return
_phase = _Phase.VERIFY_GET _phase = Phase.VERIFY_GET
_request_get() _request_get()
_Phase.VERIFY_GET: Phase.VERIFY_GET:
_busy = false _busy = false
if response_code == 200: if response_code == 200:
_emit_position_from_response(text) _emit_position_from_response(text)