diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 4e18113..33745bc 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -46,14 +46,6 @@ derives trusted context from registered peers, authoritative regions, and server-owned state before mutating inventory, wallet, progression, or shared world state. See [`decisions/0001-host-authority.md`](decisions/0001-host-authority.md). -Moderation follows the same boundary. Player hosts may grant session-scoped -operator status to an authenticated identity. Dedicated servers derive -operators from their configured fingerprint allowlist. Operator status is -replicated for presentation, but kick, ban, unban, and artwork-reset requests -are always reauthorized against the authenticated sender by the host. Only a -player host can grant or revoke operators; operators cannot moderate the host -or another operator. - Protocol compatibility is defined in `network/network_protocol.gd`. A visible release version is not a reason to change the protocol number. diff --git a/fish/fish_quality.gd b/fish/fish_quality.gd index 61534e0..6ae4031 100644 --- a/fish/fish_quality.gd +++ b/fish/fish_quality.gd @@ -23,11 +23,16 @@ const MINNOW_ROLL_WEIGHTS: Array[float] = [34.0, 27.0, 20.0, 13.0, 6.0] const SARDINE_ROLL_WEIGHTS: Array[float] = [25.0, 28.0, 23.0, 16.0, 8.0] const LUMINOUS_ROE_ROLL_WEIGHTS: Array[float] = [16.0, 26.0, 26.0, 20.0, 12.0] const SALE_MULTIPLIERS: Array[float] = [1.0, 1.1, 1.25, 1.5, 2.0] -# Legacy multiplier helpers remain available for compatibility. New encounters -# roll directly within the authored quality bands below. +# Legacy profile multiplier retained for serialized/profile compatibility. +# New encounters use the weighted quality/rarity/weight bands below. const BARRIER_HEALTH_MULTIPLIERS: Array[float] = [1.0, 1.25, 1.6, 2.2, 3.25] -# Inclusive per-barrier health bands authored for each quality tier. Encounter -# generation rolls uniformly across the selected tier's complete band. +# Barrier health is weighted 70% by quality, 20% by rarity, and 10% by the +# catch's position in its authored weight range. The upper bounds define the +# intended per-barrier challenge bands; a small seeded variance keeps barriers +# from feeling identical without crossing those bands. +const BARRIER_QUALITY_WEIGHT: float = 0.70 +const BARRIER_RARITY_WEIGHT: float = 0.20 +const BARRIER_WEIGHT_WEIGHT: float = 0.10 const BARRIER_HEALTH_MINIMUMS: Array[int] = [1, 10, 50, 100, 200] const BARRIER_HEALTH_MAXIMUMS: Array[int] = [9, 50, 100, 200, 400] const DISPLAY_NAMES: PackedStringArray = [ @@ -84,26 +89,32 @@ static func apply_barrier_health(base_health: int, quality: int) -> int: ) -static func barrier_health_range(quality: int) -> Vector2i: +static func barrier_health_for_catch( + quality: int, + rarity: int, + weight_percentile: float, + variance: float = 1.0, +) -> int: var safe_quality: int = ( quality if is_valid(quality) else Tier.BORING ) - return Vector2i( + var safe_rarity: float = clampf(float(rarity) / 4.0, 0.0, 1.0) + var safe_weight: float = clampf(weight_percentile, 0.0, 1.0) + var weighted_health: float = float(BARRIER_HEALTH_MAXIMUMS[safe_quality]) * ( + BARRIER_QUALITY_WEIGHT + + BARRIER_RARITY_WEIGHT * safe_rarity + + BARRIER_WEIGHT_WEIGHT * safe_weight + ) + var varied_health: int = roundi( + weighted_health * clampf(variance, 0.8, 1.2) + ) + return clampi( + varied_health, BARRIER_HEALTH_MINIMUMS[safe_quality], BARRIER_HEALTH_MAXIMUMS[safe_quality], ) -static func roll_barrier_health( - rng: RandomNumberGenerator, - quality: int, -) -> int: - var health_range: Vector2i = barrier_health_range(quality) - if rng == null: - return health_range.x - return rng.randi_range(health_range.x, health_range.y) - - static func roll( rng: RandomNumberGenerator, weight_multipliers: Array[float] = [], diff --git a/fishing/bass_catch_profile.tres b/fishing/bass_catch_profile.tres index 24024a7..6385eec 100644 --- a/fishing/bass_catch_profile.tres +++ b/fishing/bass_catch_profile.tres @@ -6,6 +6,8 @@ script = ExtResource("1_profile") barrier_count_min = 3 barrier_count_max = 4 +barrier_health_min = 4 +barrier_health_max = 7 first_barrier_margin = 0.14 final_barrier_margin = 0.1 minimum_barrier_spacing = 0.14 diff --git a/fishing/carp_catch_profile.tres b/fishing/carp_catch_profile.tres index e1e0efe..94a8470 100644 --- a/fishing/carp_catch_profile.tres +++ b/fishing/carp_catch_profile.tres @@ -6,6 +6,8 @@ script = ExtResource("1_profile") barrier_count_min = 3 barrier_count_max = 4 +barrier_health_min = 5 +barrier_health_max = 8 first_barrier_margin = 0.13 final_barrier_margin = 0.09 minimum_barrier_spacing = 0.14 diff --git a/fishing/catch_controller.gd b/fishing/catch_controller.gd index 7e75370..62769aa 100644 --- a/fishing/catch_controller.gd +++ b/fishing/catch_controller.gd @@ -69,6 +69,8 @@ var _reel_input_held: bool = false var _reel_speed: float = 0.0 var _click_power: int = 1 var _fish_quality: int = FishQualityType.Tier.BORING +var _fish_rarity: int = 0 +var _fish_weight_percentile: float = 0.0 var _chase_delay_remaining: float = 0.0 var _failure_epsilon: float = 0.0001 var _auto_click_accumulator: float = 0.0 @@ -111,6 +113,8 @@ func start_encounter( reel_speed: float, click_power: int, fish_quality: int = FishQualityType.Tier.BORING, + fish_rarity: int = 0, + fish_weight_percentile: float = 0.0, ) -> void: reset() if profile == null: @@ -124,6 +128,8 @@ func start_encounter( if FishQualityType.is_valid(fish_quality) else FishQualityType.Tier.BORING ) + _fish_rarity = clampi(fish_rarity, 0, 4) + _fish_weight_percentile = clampf(fish_weight_percentile, 0.0, 1.0) _chase_delay_remaining = CHASE_START_DELAY chase_progress = -CHASE_START_OFFSET _seed_encounter_rng() @@ -139,6 +145,8 @@ func start_authoritative_encounter( click_power: int, encounter_seed_value: int, fish_quality: int = FishQualityType.Tier.BORING, + fish_rarity: int = 0, + fish_weight_percentile: float = 0.0, ) -> void: var previous_test_mode: bool = use_deterministic_test_seed var previous_seed: int = deterministic_test_seed @@ -149,6 +157,8 @@ func start_authoritative_encounter( reel_speed, click_power, fish_quality, + fish_rarity, + fish_weight_percentile, ) use_deterministic_test_seed = previous_test_mode deterministic_test_seed = previous_seed @@ -317,8 +327,11 @@ func _generate_barriers(profile: CatchDifficultyProfileType) -> void: + spacing * float(barrier_index) + random_offsets[barrier_index] * random_slack ) - var health: int = FishQualityType.roll_barrier_health( - _rng, _fish_quality + var health: int = FishQualityType.barrier_health_for_catch( + _fish_quality, + _fish_rarity, + _fish_weight_percentile, + _rng.randf_range(0.9, 1.1), ) _barriers.append(Barrier.new(position, health)) diff --git a/fishing/catch_difficulty_profile.gd b/fishing/catch_difficulty_profile.gd index 60aa190..bd2508c 100644 --- a/fishing/catch_difficulty_profile.gd +++ b/fishing/catch_difficulty_profile.gd @@ -3,6 +3,8 @@ extends Resource @export_range(0, 12, 1) var barrier_count_min: int = 2 @export_range(0, 12, 1) var barrier_count_max: int = 4 +@export_range(1, 100, 1) var barrier_health_min: int = 3 +@export_range(1, 100, 1) var barrier_health_max: int = 7 @export_range(0.0, 0.45, 0.01) var first_barrier_margin: float = 0.15 @export_range(0.0, 0.45, 0.01) var final_barrier_margin: float = 0.10 @export_range(0.01, 1.0, 0.01) var minimum_barrier_spacing: float = 0.15 @@ -13,6 +15,11 @@ func get_barrier_count_range() -> Vector2i: return Vector2i(minimum, maxi(minimum, barrier_count_max)) +func get_barrier_health_range() -> Vector2i: + var minimum: int = maxi(1, barrier_health_min) + return Vector2i(minimum, maxi(minimum, barrier_health_max)) + + func get_generation_interval() -> Vector2: var start: float = clampf(first_barrier_margin, 0.0, 0.95) var finish: float = clampf(1.0 - final_barrier_margin, start, 1.0) diff --git a/fishing/common_catch_profile.tres b/fishing/common_catch_profile.tres index 86c2b74..4af3284 100644 --- a/fishing/common_catch_profile.tres +++ b/fishing/common_catch_profile.tres @@ -6,6 +6,8 @@ script = ExtResource("1_profile") barrier_count_min = 2 barrier_count_max = 4 +barrier_health_min = 3 +barrier_health_max = 7 first_barrier_margin = 0.15 final_barrier_margin = 0.1 minimum_barrier_spacing = 0.15 diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 243a069..e0db860 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -1045,6 +1045,8 @@ func _activate_bite(confirmation_override: bool = false) -> void: _get_effective_reel_speed(), _get_effective_barrier_damage(), _pending_catch.quality, + int(_pending_catch.fish.rarity), + _pending_catch.fish.get_weight_percentile(_pending_catch.weight_lb), ) _catch_controller.set_reel_input( Input.is_action_pressed("fish_primary") diff --git a/main/main.gd b/main/main.gd index 29cf997..4647005 100644 --- a/main/main.gd +++ b/main/main.gd @@ -308,15 +308,7 @@ func _start_dedicated_server() -> void: if not _discovery.set_base_url_override(config.discovery_url): _fail_dedicated_server("The discovery URL is invalid.") return - if not _network_session.configure_dedicated_operators( - config.operator_fingerprints - ): - _fail_dedicated_server("The server operator list is invalid.") - return _configure_portable_stores() - if not _discovery.configure_dedicated_runtime(config.server_name): - _fail_dedicated_server("The server room name is invalid.") - return _initialize_application(true) _player.set_local_control(false) _player.visible = false @@ -331,6 +323,7 @@ func _start_dedicated_server() -> void: _fail_dedicated_server("Could not start the dedicated server.") return _network_session.set_session_display_name(config.server_name) + _discovery.set_room_name(config.server_name) if not _network_session.set_host_open(true): _fail_dedicated_server("Could not open the dedicated server.") return diff --git a/network/discovery_client.gd b/network/discovery_client.gd index 476cff4..bdb7134 100644 --- a/network/discovery_client.gd +++ b/network/discovery_client.gd @@ -58,7 +58,6 @@ var _session: NetworkSession var _base_url: String = "" var _room_name: String = DEFAULT_ROOM_NAME var _room_name_uses_default: bool = true -var _host_settings_persistence_enabled: bool = true var _discoverable: bool = false var _host_status_message: String = "" var _host_status_is_error: bool = false @@ -219,11 +218,6 @@ func get_public_join_state() -> PublicJoinState: return _public_join_state -func configure_dedicated_runtime(room_name: String) -> bool: - _host_settings_persistence_enabled = false - return set_room_name(room_name) - - func set_room_name(value: String) -> bool: var cleaned: String = _sanitize_room_name(value) if cleaned.is_empty(): @@ -1053,8 +1047,6 @@ func _load_settings() -> void: func _save_settings() -> void: - if not _host_settings_persistence_enabled: - return var config := ConfigFile.new() config.set_value("host", "room_name", _room_name) config.set_value( diff --git a/network/network_fishing_service.gd b/network/network_fishing_service.gd index a5a2154..7303f12 100644 --- a/network/network_fishing_service.gd +++ b/network/network_fishing_service.gd @@ -548,6 +548,8 @@ func _start_bite(attempt: NetworkFishingAttempt) -> void: attempt.barrier_damage, attempt.encounter_seed, fish_catch.quality, + int(fish.rarity), + fish.get_weight_percentile(fish_catch.weight_lb), ) var data: Dictionary = { "attempt_id": attempt.attempt_id, diff --git a/network/network_player_list_service.gd b/network/network_player_list_service.gd index 4742d14..c4307f1 100644 --- a/network/network_player_list_service.gd +++ b/network/network_player_list_service.gd @@ -15,9 +15,6 @@ var _surface_drawing: NetworkSurfaceDrawingService var _revision := 0 var _host_block_pairs: Dictionary[String, bool] = {} var _peer_fingerprints: Dictionary[int, String] = {} -var _remote_bans: Array[Dictionary] = [] -var _ban_snapshot_requested: bool = false -var _ban_snapshot_loaded: bool = false func setup( @@ -43,7 +40,6 @@ func setup( func(_count: int, _maximum: int) -> void: _sync_authenticated_peers() ) _session.state_changed.connect(_on_session_state_changed) - _session.operator_status_changed.connect(_on_operator_status_changed) _relationships.relationship_changed.connect(_on_relationship_changed) _bans.bans_changed.connect(_changed) _chat.set_relationship_store(_relationships) @@ -68,24 +64,13 @@ func get_entries() -> Array[PlayerListEntry]: entry.compact_fingerprint = NetworkIdentityCrypto.compact_suffix(record.identity_fingerprint) entry.display_name = record.display_name entry.is_host = peer_id == 1 - entry.is_operator = _session.is_peer_operator(peer_id) entry.is_local_player = peer_id == local_id entry.continuity_state = _known.identity_status(record.identity_fingerprint, record.display_name) entry.ping_to_host_ms = _session.get_peer_rtt_ms(peer_id) entry.muted = _relationships.is_muted(record.identity_fingerprint) entry.blocked = blocked - entry.can_kick = ( - _session.can_local_moderate() - and peer_id != local_id - and peer_id != 1 - and (_session.is_host() or not entry.is_operator) - ) + entry.can_kick = _session.is_host() and peer_id != local_id and peer_id != 1 entry.can_ban = entry.can_kick - entry.can_manage_operator = ( - _session.can_manage_operators() - and peer_id != local_id - and peer_id != 1 - ) entry.revision = _revision result.append(entry) result.sort_custom(_entry_before) @@ -104,14 +89,6 @@ func is_local_host() -> bool: return _session.is_host() -func is_local_moderator() -> bool: - return _session.can_local_moderate() - - -func can_manage_operators() -> bool: - return _session.can_manage_operators() - - func is_open_host() -> bool: return _session.is_open_host() @@ -125,12 +102,7 @@ func get_relationships() -> Array[Dictionary]: func get_bans() -> Array[Dictionary]: - if _session.is_host(): - return _bans.get_bans(_session.get_host_identity_fingerprint()) - if not _session.is_local_operator(): - return [] - _request_ban_snapshot() - return _remote_bans.duplicate(true) + return _bans.get_bans(_session.get_host_identity_fingerprint()) if _session.is_host() else [] func set_surface_drawing_service( @@ -159,12 +131,14 @@ func get_session_artwork_counts() -> Vector2i: func reset_session_artwork() -> bool: - if _session.is_host(): - return _reset_session_artwork_on_host() - if not _session.is_local_operator(): + if not _session.is_host() or _surface_drawing == null: return false - request_reset_session_artwork.rpc_id(1) - return true + var ok: bool = _surface_drawing.clear_session_artwork() + moderation_finished.emit( + ok, + "Session artwork cleared." if ok else "Session artwork could not be cleared.", + ) + return ok func set_muted(fingerprint: String, display_name: String, value: bool) -> bool: @@ -180,13 +154,12 @@ func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool func kick(peer_id: int, fingerprint: String, revision: int) -> bool: - if _session.is_host(): - return _kick_on_host(peer_id, fingerprint, revision, true) - if not _session.is_local_operator(): - moderation_finished.emit(false, "Only the host or an operator can remove players.") + if not _valid_moderation_target(peer_id, fingerprint, revision): + moderation_finished.emit(false, "That player is no longer connected.") return false - request_kick.rpc_id(1, peer_id, fingerprint) - return true + var ok := _session.kick_authenticated_peer(peer_id, fingerprint) + moderation_finished.emit(ok, "Player removed." if ok else "Player could not be removed.") + return ok func ban( @@ -195,158 +168,22 @@ func ban( display_name: String, revision: int, ) -> bool: - if _session.is_host(): - return _ban_on_host( - peer_id, fingerprint, display_name, revision, true - ) - if not _session.is_local_operator(): - moderation_finished.emit(false, "Only the host or an operator can ban players.") + if not _valid_moderation_target(peer_id, fingerprint, revision): + moderation_finished.emit(false, "That player is no longer connected.") return false - request_ban.rpc_id(1, peer_id, fingerprint) + var host_fingerprint := _session.get_host_identity_fingerprint() + if not _bans.ban(host_fingerprint, fingerprint, display_name): + moderation_finished.emit(false, "Ban could not be saved.") + return false + var ok := _session.kick_authenticated_peer(peer_id, fingerprint, true) + moderation_finished.emit(ok, "Player banned." if ok else "Ban saved.") return true func unban(fingerprint: String) -> bool: - if _session.is_host(): - var ok: bool = _bans.unban( - _session.get_host_identity_fingerprint(), fingerprint - ) - moderation_finished.emit( - ok, "Player unbanned." if ok else "Ban could not be removed." - ) - return ok - if not _session.is_local_operator(): - moderation_finished.emit(false, "Only the host or an operator can remove bans.") + if not _session.is_host(): return false - request_unban.rpc_id(1, fingerprint) - return true - - -func set_operator( - peer_id: int, - fingerprint: String, - enabled: bool, - revision: int, -) -> bool: - if ( - not _session.can_manage_operators() - or not _valid_moderation_target( - peer_id, fingerprint, revision, true, true - ) - ): - moderation_finished.emit(false, "That player is no longer connected.") - return false - var ok: bool = _session.set_peer_operator(peer_id, fingerprint, enabled) - moderation_finished.emit( - ok, - ("Player is now an operator." if enabled else "Operator access removed.") - if ok - else "Operator access could not be changed.", - ) - return ok - - -@rpc("any_peer", "call_remote", "reliable", 0) -func request_kick(peer_id: int, fingerprint: String) -> void: - var sender_id: int = multiplayer.get_remote_sender_id() - if not _valid_operator_sender(sender_id): - return - var ok: bool = _kick_on_host(peer_id, fingerprint, -1, false) - _send_moderation_result( - sender_id, ok, "Player removed." if ok else "Player could not be removed." - ) - - -@rpc("any_peer", "call_remote", "reliable", 0) -func request_ban(peer_id: int, fingerprint: String) -> void: - var sender_id: int = multiplayer.get_remote_sender_id() - if not _valid_operator_sender(sender_id): - return - var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) - var display_name: String = record.display_name if record != null else "Player" - var ok: bool = _ban_on_host( - peer_id, fingerprint, display_name, -1, false - ) - _send_moderation_result( - sender_id, ok, "Player banned." if ok else "Player could not be banned." - ) - _send_ban_snapshot(sender_id) - - -@rpc("any_peer", "call_remote", "reliable", 0) -func request_unban(fingerprint: String) -> void: - var sender_id: int = multiplayer.get_remote_sender_id() - if ( - not _valid_operator_sender(sender_id) - or not NetworkIdentityCrypto.valid_fingerprint(fingerprint) - ): - return - var ok: bool = _bans.unban( - _session.get_host_identity_fingerprint(), fingerprint - ) - _send_moderation_result( - sender_id, ok, "Player unbanned." if ok else "Ban could not be removed." - ) - _send_ban_snapshot(sender_id) - - -@rpc("any_peer", "call_remote", "reliable", 0) -func request_reset_session_artwork() -> void: - var sender_id: int = multiplayer.get_remote_sender_id() - if not _valid_operator_sender(sender_id): - return - var ok: bool = _reset_session_artwork_on_host(false) - _send_moderation_result( - sender_id, - ok, - "Session artwork cleared." - if ok - else "Session artwork could not be cleared.", - ) - - -@rpc("any_peer", "call_remote", "reliable", 0) -func request_ban_snapshot() -> void: - var sender_id: int = multiplayer.get_remote_sender_id() - if _valid_operator_sender(sender_id): - _send_ban_snapshot(sender_id) - - -@rpc("authority", "call_remote", "reliable", 0) -func receive_moderation_result(success: bool, message: String) -> void: - if not _session.is_joined_client(): - return - moderation_finished.emit(success, message.left(120)) - - -@rpc("authority", "call_remote", "reliable", 0) -func receive_ban_snapshot(records: Array) -> void: - if not _session.is_local_operator() or records.size() > HostBanStore.MAX_BANS: - return - var sanitized: Array[Dictionary] = [] - for value: Variant in records: - if typeof(value) != TYPE_DICTIONARY: - return - var record: Dictionary = value - var fingerprint: String = str(record.get("target_fingerprint", "")) - var display_name: String = str( - record.get("last_known_display_name", "Player") - ).strip_edges().left(NetworkProtocol.MAX_DISPLAY_NAME_LENGTH) - if ( - not NetworkIdentityCrypto.valid_fingerprint(fingerprint) - or display_name.is_empty() - or typeof(record.get("banned_unix")) != TYPE_INT - ): - return - sanitized.append({ - "target_fingerprint": fingerprint, - "last_known_display_name": display_name, - "banned_unix": int(record["banned_unix"]), - }) - _remote_bans = sanitized - _ban_snapshot_requested = false - _ban_snapshot_loaded = true - _changed() + return _bans.unban(_session.get_host_identity_fingerprint(), fingerprint) func is_locally_blocked(fingerprint: String) -> bool: @@ -434,22 +271,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: ]: _host_block_pairs.clear() _peer_fingerprints.clear() - _remote_bans.clear() - _ban_snapshot_requested = false - _ban_snapshot_loaded = false - elif state == NetworkSession.State.JOINED_CLIENT: - _request_ban_snapshot() - _changed() - - -func _on_operator_status_changed(peer_id: int, enabled: bool) -> void: - if peer_id == _session.get_local_peer_id(): - if enabled: - _request_ban_snapshot() - else: - _remote_bans.clear() - _ban_snapshot_requested = false - _ban_snapshot_loaded = false _changed() @@ -474,137 +295,16 @@ func _peer_for_fingerprint(fingerprint: String) -> int: return 0 -func _valid_moderation_target( - peer_id: int, - fingerprint: String, - revision: int, - require_revision: bool, - can_target_operator: bool, -) -> bool: - if ( - not _session.is_host() - or (require_revision and revision != _revision) - or peer_id == 1 - or (not can_target_operator and _session.is_peer_operator(peer_id)) - ): +func _valid_moderation_target(peer_id: int, fingerprint: String, revision: int) -> bool: + if not _session.is_host() or revision != _revision or peer_id == 1: return false var record := _session.get_peer_record(peer_id) return record != null and record.identity_fingerprint == fingerprint -func _valid_operator_sender(peer_id: int) -> bool: - return ( - _session.is_host() - and peer_id > 1 - and _session.is_authenticated_peer(peer_id) - and _session.is_peer_operator(peer_id) - ) - - -func _kick_on_host( - peer_id: int, - fingerprint: String, - revision: int, - local_host_request: bool, -) -> bool: - if not _valid_moderation_target( - peer_id, - fingerprint, - revision, - local_host_request, - local_host_request, - ): - if local_host_request: - moderation_finished.emit(false, "That player is no longer connected.") - return false - var ok: bool = _session.kick_authenticated_peer(peer_id, fingerprint) - if local_host_request: - moderation_finished.emit( - ok, "Player removed." if ok else "Player could not be removed." - ) - return ok - - -func _ban_on_host( - peer_id: int, - fingerprint: String, - display_name: String, - revision: int, - local_host_request: bool, -) -> bool: - if not _valid_moderation_target( - peer_id, - fingerprint, - revision, - local_host_request, - local_host_request, - ): - if local_host_request: - moderation_finished.emit(false, "That player is no longer connected.") - return false - var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) - var trusted_display_name: String = ( - record.display_name if record != null else display_name - ) - var host_fingerprint: String = _session.get_host_identity_fingerprint() - if not _bans.ban(host_fingerprint, fingerprint, trusted_display_name): - if local_host_request: - moderation_finished.emit(false, "Ban could not be saved.") - return false - var ok: bool = _session.kick_authenticated_peer(peer_id, fingerprint, true) - if local_host_request: - moderation_finished.emit(ok, "Player banned." if ok else "Ban saved.") - return true - - -func _reset_session_artwork_on_host( - emit_local_result: bool = true, -) -> bool: - if not _session.is_host() or _surface_drawing == null: - return false - var ok: bool = _surface_drawing.clear_session_artwork() - if emit_local_result: - moderation_finished.emit( - ok, - "Session artwork cleared." - if ok - else "Session artwork could not be cleared.", - ) - return ok - - -func _send_moderation_result( - peer_id: int, - success: bool, - message: String, -) -> void: - receive_moderation_result.rpc_id(peer_id, success, message.left(120)) - - -func _send_ban_snapshot(peer_id: int) -> void: - if not _valid_operator_sender(peer_id): - return - receive_ban_snapshot.rpc_id( - peer_id, - _bans.get_bans(_session.get_host_identity_fingerprint()), - ) - - -func _request_ban_snapshot() -> void: - if ( - _session.is_local_operator() - and not _ban_snapshot_requested - and not _ban_snapshot_loaded - ): - _ban_snapshot_requested = true - request_ban_snapshot.rpc_id(1) - - func _entry_before(a: PlayerListEntry, b: PlayerListEntry) -> bool: if a.is_host != b.is_host: return a.is_host - if a.is_operator != b.is_operator: - return a.is_operator if a.is_local_player != b.is_local_player: return a.is_local_player var compared := a.display_name.naturalnocasecmp_to(b.display_name) diff --git a/network/network_session.gd b/network/network_session.gd index 2043072..1d332da 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -32,7 +32,6 @@ signal server_trust_required( is_changed: bool, ) signal peer_identity_observed(peer_id: int, status: String) -signal operator_status_changed(peer_id: int, is_operator: bool) signal server_lost signal remote_recovery_requested(peer_id: int, entry_position: Vector3) signal remote_recovery_presentation_changed( @@ -103,10 +102,6 @@ var _moderation_disconnect_message := "" var _host_port: int = 0 var _session_display_name: String = "NETfishing Room" var _dedicated_host: bool = false -var _configured_operator_fingerprints: Dictionary[String, bool] = {} -var _session_operator_fingerprints: Dictionary[String, bool] = {} -var _operator_peer_ids: Dictionary[int, bool] = {} -var _local_operator: bool = false func _ready() -> void: @@ -142,20 +137,6 @@ func setup( _profile_ready = _profile_ready and _player_identity.load_or_create() -func configure_dedicated_operators( - fingerprints: PackedStringArray, -) -> bool: - if state != State.INACTIVE: - return false - var configured: Dictionary[String, bool] = {} - for fingerprint: String in fingerprints: - if not NetworkIdentityCrypto.valid_fingerprint(fingerprint): - return false - configured[fingerprint] = true - _configured_operator_fingerprints = configured - return true - - func start_private_host( port: int = DEFAULT_PORT, port_attempts: int = 1, @@ -234,9 +215,6 @@ func _start_host( multiplayer.multiplayer_peer = peer _session_id = Crypto.new().generate_random_bytes(16).hex_encode() _registry.clear() - _session_operator_fingerprints.clear() - _operator_peer_ids.clear() - _local_operator = false _spawn_service.clear_remote_players() if not dedicated: _register_player_host() @@ -323,44 +301,6 @@ func is_dedicated_host() -> bool: return is_host() and _dedicated_host -func is_local_operator() -> bool: - return state == State.JOINED_CLIENT and _local_operator - - -func can_local_moderate() -> bool: - return is_host() or is_local_operator() - - -func can_manage_operators() -> bool: - return is_host() and not _dedicated_host - - -func is_peer_operator(peer_id: int) -> bool: - return bool(_operator_peer_ids.get(peer_id, false)) - - -func set_peer_operator( - peer_id: int, - fingerprint: String, - enabled: bool, -) -> bool: - if not can_manage_operators() or peer_id <= 1: - return false - var record: PeerRegistry.PeerRecord = _registry.get_peer(peer_id) - if ( - record == null - or not record.identity_authenticated - or record.identity_fingerprint != fingerprint - ): - return false - if enabled: - _session_operator_fingerprints[fingerprint] = true - else: - _session_operator_fingerprints.erase(fingerprint) - _set_operator_status(peer_id, enabled) - return true - - func set_session_display_name(value: String) -> void: var cleaned: String = value.strip_edges().left(48) if cleaned.is_empty(): @@ -882,7 +822,6 @@ func _on_peer_disconnected(peer_id: int) -> void: _pending_authentication.erase(peer_id) _pending_identity_challenges.erase(peer_id) _authenticated_identity_cache.erase(peer_id) - _operator_peer_ids.erase(peer_id) var recovery_attempt: String = _recovery_attempts.get(peer_id, "") if not recovery_attempt.is_empty(): _recovery_attempts.erase(peer_id) @@ -1197,11 +1136,6 @@ func submit_client_hello(data: Dictionary) -> void: NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE ) return - var operator_enabled: bool = _operator_for_fingerprint( - str(identity["fingerprint"]) - ) - if operator_enabled: - _operator_peer_ids[sender_id] = true var submitted_appearance := CharacterCustomizationCatalog.sanitized_snapshot( data["cosmetic_snapshot"] ) @@ -1246,12 +1180,9 @@ func submit_client_hello(data: Dictionary) -> void: ) ) receive_spawn_list.rpc_id(sender_id, _build_spawn_list()) - receive_operator_snapshot.rpc_id(sender_id, _operator_peer_id_snapshot()) receive_peer_spawn.rpc( _make_spawn_entry(sender_id, display_name, spawn_transform) ) - if operator_enabled: - receive_operator_status.rpc(sender_id, true) peer_authenticated.emit(sender_id, display_name) _emit_peer_count() @@ -1403,41 +1334,12 @@ func receive_peer_spawn(entry: Dictionary) -> void: func receive_peer_despawn(peer_id: int) -> void: if state != State.JOINED_CLIENT: return - _operator_peer_ids.erase(peer_id) _registry.remove_peer(peer_id) _spawn_service.remove_peer(peer_id) peer_removed.emit(peer_id) _emit_peer_count() -@rpc("authority", "call_remote", "reliable", 0) -func receive_operator_snapshot(peer_ids: PackedInt32Array) -> void: - if ( - state != State.JOINED_CLIENT - or peer_ids.size() > get_session_max_players() - ): - return - _operator_peer_ids.clear() - for peer_id: int in peer_ids: - if peer_id > 1: - _operator_peer_ids[peer_id] = true - _update_local_operator() - for peer_id: int in _operator_peer_ids: - operator_status_changed.emit(peer_id, true) - - -@rpc("authority", "call_remote", "reliable", 0) -func receive_operator_status(peer_id: int, enabled: bool) -> void: - if state != State.JOINED_CLIENT or peer_id <= 1: - return - if enabled: - _operator_peer_ids[peer_id] = true - else: - _operator_peer_ids.erase(peer_id) - _update_local_operator() - operator_status_changed.emit(peer_id, enabled) - - func _apply_spawn_entry(entry: Dictionary) -> void: if ( typeof(entry.get("peer_id")) != TYPE_INT @@ -1568,36 +1470,6 @@ func _sanitized_capabilities(value: Variant) -> PackedStringArray: return result -func _operator_for_fingerprint(fingerprint: String) -> bool: - return bool(( - _configured_operator_fingerprints - if _dedicated_host - else _session_operator_fingerprints - ).get(fingerprint, false)) - - -func _operator_peer_id_snapshot() -> PackedInt32Array: - var result: PackedInt32Array = PackedInt32Array() - for peer_id: int in _operator_peer_ids: - result.append(peer_id) - result.sort() - return result - - -func _set_operator_status(peer_id: int, enabled: bool) -> void: - if enabled: - _operator_peer_ids[peer_id] = true - else: - _operator_peer_ids.erase(peer_id) - operator_status_changed.emit(peer_id, enabled) - receive_operator_status.rpc(peer_id, enabled) - - -func _update_local_operator() -> void: - var local_peer_id: int = multiplayer.get_unique_id() - _local_operator = bool(_operator_peer_ids.get(local_peer_id, false)) - - func _verify_spawn_identity(entry: Dictionary) -> bool: var fingerprint := str(entry.get("identity_fingerprint", "")) var public_pem := NetworkIdentityCrypto.normalize_public_pem( @@ -2057,8 +1929,5 @@ func _teardown_peer() -> void: _server_identity_fingerprint = "" _server_identity_public_key = "" _session_identity_keys.clear() - _session_operator_fingerprints.clear() - _operator_peer_ids.clear() - _local_operator = false _host_port = 0 _dedicated_host = false diff --git a/network/player_list_entry.gd b/network/player_list_entry.gd index f401da6..fbe0d8f 100644 --- a/network/player_list_entry.gd +++ b/network/player_list_entry.gd @@ -6,7 +6,6 @@ var full_fingerprint := "" var compact_fingerprint := "" var display_name := "" var is_host := false -var is_operator := false var is_local_player := false var continuity_state := "" var ping_to_host_ms := -1 @@ -14,5 +13,4 @@ var muted := false var blocked := false var can_kick := false var can_ban := false -var can_manage_operator := false var revision := 0 diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 17252ba..8279bac 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -10,7 +10,6 @@ readonly RUN_ROOT="$(mktemp -d -t netfishing-validations.XXXXXX)" readonly -a QUICK_TESTS=( "tests/android_readiness_validation.gd" - "tests/dedicated_server_config_validation.gd" "tests/fish_catalog_content_validation.gd" "tests/fish_quality_validation.gd" "tests/fishing_audio_validation.gd" @@ -33,7 +32,6 @@ readonly -a RUNTIME_TESTS=( readonly -a HOST_TESTS=( "tests/art_tools_validation.gd" - "tests/dedicated_host_session_validation.gd" "tests/economy_regression_validation.gd" "tests/fish_hotbar_showcase_validation.gd" "tests/fishing_authority_validation.gd" @@ -46,7 +44,6 @@ readonly -a NETWORK_TESTS=( "tests/fish_showcase_multiplayer_validation.gd" "tests/fishing_multiplayer_validation.gd" "tests/job_multiplayer_validation.gd" - "tests/operator_multiplayer_validation.gd" "tests/surface_drawing_multiplayer_validation.gd" "tests/world_time_multiplayer_validation.gd" ) diff --git a/server/dedicated_server_config.gd b/server/dedicated_server_config.gd index 0618466..457c382 100644 --- a/server/dedicated_server_config.gd +++ b/server/dedicated_server_config.gd @@ -5,7 +5,6 @@ const DEFAULT_NAME: String = "NETfishing Dedicated Server" const DEFAULT_BIND_ADDRESS: String = "*" const DEFAULT_PORT: int = 7777 const DEFAULT_MAX_PLAYERS: int = 8 -const MAX_OPERATORS: int = 64 var server_name: String = DEFAULT_NAME var bind_address: String = DEFAULT_BIND_ADDRESS @@ -14,7 +13,6 @@ var max_players: int = DEFAULT_MAX_PLAYERS var public_listing: bool = false var discovery_url: String = "" var data_directory: String = "" -var operator_fingerprints: PackedStringArray = PackedStringArray() var error_message: String = "" @@ -67,9 +65,6 @@ func _load_file(path: String) -> bool: discovery_url = str(file.get_value( "discovery", "url", discovery_url )) - operator_fingerprints = _parse_fingerprint_list(file.get_value( - "moderation", "operators", operator_fingerprints - )) return true @@ -93,10 +88,6 @@ func _apply_environment() -> void: data_directory = _environment_string( "NETFISHING_DATA_DIR", data_directory ) - if OS.has_environment("NETFISHING_SERVER_OPERATORS"): - operator_fingerprints = _parse_fingerprint_list( - OS.get_environment("NETFISHING_SERVER_OPERATORS") - ) func _apply_arguments(arguments: PackedStringArray) -> void: @@ -115,10 +106,6 @@ func _apply_arguments(arguments: PackedStringArray) -> void: data_directory = argument.trim_prefix("--data-dir=") elif argument.begins_with("--discovery-url="): discovery_url = argument.trim_prefix("--discovery-url=") - elif argument.begins_with("--operators="): - operator_fingerprints = _parse_fingerprint_list( - argument.trim_prefix("--operators=") - ) elif argument == "--public": public_listing = true elif argument == "--private": @@ -130,7 +117,6 @@ func _validate() -> void: bind_address = bind_address.strip_edges() discovery_url = discovery_url.strip_edges().trim_suffix("/") data_directory = data_directory.strip_edges() - operator_fingerprints = _normalized_fingerprints(operator_fingerprints) if server_name.is_empty(): error_message = "Server name cannot be empty." elif not _safe_text(server_name): @@ -148,17 +134,6 @@ func _validate() -> void: or discovery_url.begins_with("http://") ): error_message = "A public server requires a discovery URL." - elif operator_fingerprints.size() > MAX_OPERATORS: - error_message = "A server may configure at most %d operators." % ( - MAX_OPERATORS - ) - else: - for fingerprint: String in operator_fingerprints: - if not NetworkIdentityCrypto.valid_fingerprint(fingerprint): - error_message = ( - "Server operator fingerprints must be 64 lowercase hex characters." - ) - break static func _safe_text(value: String) -> bool: @@ -189,29 +164,3 @@ static func _environment_bool(name: String, fallback: bool) -> bool: static func _parse_int(value: String, fallback: int) -> int: return int(value) if value.strip_edges().is_valid_int() else fallback - - -static func _parse_fingerprint_list(value: Variant) -> PackedStringArray: - var values: PackedStringArray = PackedStringArray() - if typeof(value) == TYPE_STRING: - var text: String = str(value) - for separator: String in [";", "\n", "\r", "\t", " "]: - text = text.replace(separator, ",") - values = text.split(",", false) - elif typeof(value) in [TYPE_ARRAY, TYPE_PACKED_STRING_ARRAY]: - for entry: Variant in value: - if typeof(entry) in [TYPE_STRING, TYPE_STRING_NAME]: - values.append(str(entry)) - return _normalized_fingerprints(values) - - -static func _normalized_fingerprints( - values: PackedStringArray, -) -> PackedStringArray: - var result: PackedStringArray = PackedStringArray() - for value: String in values: - var fingerprint: String = value.strip_edges().to_lower() - if not fingerprint.is_empty() and fingerprint not in result: - result.append(fingerprint) - result.sort() - return result diff --git a/tests/dedicated_host_session_validation.gd b/tests/dedicated_host_session_validation.gd index 2d098bd..6f1de7d 100644 --- a/tests/dedicated_host_session_validation.gd +++ b/tests/dedicated_host_session_validation.gd @@ -1,9 +1,6 @@ extends SceneTree const TEST_PORT: int = 35777 -const OPERATOR_FINGERPRINT: String = ( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" -) func _init() -> void: @@ -38,18 +35,12 @@ func _run() -> void: host_bans, true, ) - assert(session.configure_dedicated_operators( - PackedStringArray([OPERATOR_FINGERPRINT]) - )) assert(session.start_dedicated_host(TEST_PORT, 5, "127.0.0.1")) assert(session.is_dedicated_host()) assert(session.get_local_peer_id() == 0) assert(session.get_player_count() == 0) assert(session.get_session_max_players() == 5) assert(session.get_host_port() == TEST_PORT) - assert(bool(session.call( - "_operator_for_fingerprint", OPERATOR_FINGERPRINT - ))) assert(session.set_host_open(true)) assert(session.is_open_host()) session.disconnect_session("Dedicated host validation complete.") diff --git a/tests/dedicated_server_config_validation.gd b/tests/dedicated_server_config_validation.gd index 2da24a3..ecc721c 100644 --- a/tests/dedicated_server_config_validation.gd +++ b/tests/dedicated_server_config_validation.gd @@ -1,12 +1,6 @@ extends SceneTree const ConfigType = preload("res://server/dedicated_server_config.gd") -const OPERATOR_A: String = ( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" -) -const OPERATOR_B: String = ( - "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210" -) func _initialize() -> void: @@ -31,9 +25,6 @@ func _run() -> void: file.set_value("server", "public", true) file.set_value("server", "data_directory", "/tmp/configured-server") file.set_value("discovery", "url", "https://discovery.netfishing.org/") - file.set_value( - "moderation", "operators", PackedStringArray([OPERATOR_B, OPERATOR_A]) - ) assert(file.save(path) == OK) var configured := ConfigType.new() @@ -46,35 +37,8 @@ func _run() -> void: assert(int(configured.get("max_players")) == 12) assert(bool(configured.get("public_listing"))) assert(str(configured.get("discovery_url")) == "https://discovery.netfishing.org") - assert( - configured.get("operator_fingerprints") - == PackedStringArray([OPERATOR_A, OPERATOR_B]) - ) assert(DirAccess.remove_absolute(path) == OK) - var parsed := ConfigType.new() - parsed.set( - "operator_fingerprints", - ConfigType._parse_fingerprint_list( - "%s, %s;%s" % [OPERATOR_B.to_upper(), OPERATOR_A, OPERATOR_A] - ), - ) - parsed.set("data_directory", "/tmp/parsed-server") - parsed.call("_validate") - assert(parsed.is_valid()) - assert( - parsed.get("operator_fingerprints") - == PackedStringArray([OPERATOR_A, OPERATOR_B]) - ) - - var invalid_operator := ConfigType.new() - invalid_operator.set( - "operator_fingerprints", PackedStringArray(["not-a-fingerprint"]) - ) - invalid_operator.set("data_directory", "/tmp/invalid-operator-server") - invalid_operator.call("_validate") - assert(not invalid_operator.is_valid()) - var invalid := ConfigType.new() invalid.set("public_listing", true) invalid.set("data_directory", "/tmp/invalid-server") @@ -94,27 +58,6 @@ func _run() -> void: DiscoveryClient.UPNP_RETRY_INTERVAL_SECONDS < DiscoveryClient.UPNP_RENEW_INTERVAL_SECONDS ) - var discovery_settings_path: String = ProjectSettings.globalize_path( - DiscoveryClient.SETTINGS_PATH - ) - assert(not FileAccess.file_exists(discovery_settings_path)) - assert(discovery.set_room_name("Client Room")) - var client_settings: PackedByteArray = FileAccess.get_file_as_bytes( - discovery_settings_path - ) - assert(not client_settings.is_empty()) - var dedicated_discovery := DiscoveryClient.new() - dedicated_discovery.call("_load_settings") - assert( - dedicated_discovery.configure_dedicated_runtime("Headless Room") - ) - assert(dedicated_discovery.get_room_name() == "Headless Room") - assert( - FileAccess.get_file_as_bytes(discovery_settings_path) - == client_settings - ) - dedicated_discovery.free() - assert(DirAccess.remove_absolute(discovery_settings_path) == OK) assert( discovery.get_host_state() == DiscoveryClient.HostState.CLOSED diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index 72ebecc..366128f 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -83,68 +83,40 @@ func _validate_barrier_challenge_curve() -> void: var expected_maximums: Array[int] = [9, 50, 100, 200, 400] var previous_health: int = 0 for quality: int in FishQualityType.TIER_COUNT: - var expected_range := Vector2i( - expected_minimums[quality], expected_maximums[quality] - ) - assert( - FishQualityType.barrier_health_range(quality) - == expected_range - ) - assert( - FishQualityType.roll_barrier_health(null, quality) - == expected_range.x - ) - var rng := RandomNumberGenerator.new() - rng.seed = 73013 + quality - var observed_counts: Dictionary[int, int] = {} - var observed_minimum: int = expected_range.y - var observed_maximum: int = expected_range.x - var observed_total: int = 0 - var band_size: int = expected_range.y - expected_range.x + 1 - var sample_count: int = maxi(10000, band_size * 250) - for _sample: int in sample_count: - var rolled_health: int = FishQualityType.roll_barrier_health( - rng, quality - ) - assert( - rolled_health >= expected_range.x - and rolled_health <= expected_range.y - ) - observed_counts[rolled_health] = ( - int(observed_counts.get(rolled_health, 0)) + 1 - ) - observed_minimum = mini(observed_minimum, rolled_health) - observed_maximum = maxi(observed_maximum, rolled_health) - observed_total += rolled_health - assert(observed_minimum == expected_range.x) - assert(observed_maximum == expected_range.y) - for authored_health: int in range( - expected_range.x, expected_range.y + 1 - ): - assert(observed_counts.has(authored_health)) - var observed_average: float = ( - float(observed_total) / float(sample_count) - ) - var authored_midpoint: float = ( - float(expected_range.x + expected_range.y) / 2.0 - ) - assert( - absf(observed_average - authored_midpoint) - < maxf(0.15, float(band_size) * 0.01) - ) var health: int = FishQualityType.apply_barrier_health(8, quality) assert(health > previous_health) previous_health = health assert(FishQualityType.apply_barrier_health(8, -1) == 8) assert(FishQualityType.apply_barrier_health(0, FishQualityType.Tier.SHINY) == 4) + var impressive_low_input: int = FishQualityType.barrier_health_for_catch( + FishQualityType.Tier.IMPRESSIVE, + 0, + 0.0, + 1.0, + ) + var impressive_high_input: int = FishQualityType.barrier_health_for_catch( + FishQualityType.Tier.IMPRESSIVE, + 4, + 1.0, + 1.0, + ) + assert(impressive_low_input >= 50 and impressive_low_input <= 100) + assert(impressive_high_input > impressive_low_input) + assert(impressive_high_input <= 100) assert( - FishQualityType.barrier_health_range(-1) - == Vector2i(1, 9) + FishQualityType.barrier_health_for_catch( + FishQualityType.Tier.BORING, + 4, + 1.0, + 1.0, + ) <= 9 ) var profile := CatchDifficultyProfile.new() profile.barrier_count_min = 1 profile.barrier_count_max = 1 + profile.barrier_health_min = 8 + profile.barrier_health_max = 8 profile.first_barrier_margin = 0.2 profile.final_barrier_margin = 0.2 profile.minimum_barrier_spacing = 0.1 @@ -169,9 +141,12 @@ func _validate_barrier_challenge_curve() -> void: assert(health <= expected_maximums[quality]) controller.queue_free() - var shiny_health: int = FishQualityType.barrier_health_range( - FishQualityType.Tier.SHINY - ).y + var shiny_health: int = FishQualityType.barrier_health_for_catch( + FishQualityType.Tier.SHINY, + 4, + 1.0, + 1.0, + ) var base_power_clicks: int = ceili(float(shiny_health) / 1.0) var max_power_clicks: int = ceili( float(shiny_health) diff --git a/tests/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd index 20f02e1..52ab878 100644 --- a/tests/fishing_authority_validation.gd +++ b/tests/fishing_authority_validation.gd @@ -104,7 +104,7 @@ func _run() -> void: assert(not bobber.visible) # A private host rolls and retains the authoritative catch before the fight - # so its quality selects the barrier band clients receive in snapshots. + # so its quality can scale the same barriers clients receive in snapshots. fishing_spot.call("_begin_aiming", player) fishing_spot.set("_cast_charge", 0.32) fishing_spot.call("_update_cast_charge", 0.0) @@ -138,6 +138,8 @@ func _run() -> void: attempt.barrier_damage, attempt.encounter_seed, fish_catch.quality, + int(fish.rarity), + fish.get_weight_percentile(fish_catch.weight_lb), ) var quality_barriers: Array = attempt.controller.get("_barriers") var reference_barriers: Array = reference_controller.get("_barriers") diff --git a/tests/operator_multiplayer_validation.gd b/tests/operator_multiplayer_validation.gd deleted file mode 100644 index 5847aa3..0000000 --- a/tests/operator_multiplayer_validation.gd +++ /dev/null @@ -1,232 +0,0 @@ -extends SceneTree - -const MainScene = preload("res://main/main.tscn") -const TEST_PORT: int = 18140 -const FIRST_BANNED_FINGERPRINT: String = ( - "1111111111111111111111111111111111111111111111111111111111111111" -) -const SECOND_BANNED_FINGERPRINT: String = ( - "2222222222222222222222222222222222222222222222222222222222222222" -) - -var _moderation_results: Array[Dictionary] = [] - - -func _initialize() -> void: - call_deferred("_run") - - -func _run() -> void: - var arguments: PackedStringArray = OS.get_cmdline_user_args() - if arguments.has("host"): - await _run_host() - return - if arguments.has("client"): - await _run_client() - return - push_error("Operator multiplayer validation needs host or client mode.") - quit(1) - - -func _run_host() -> void: - var main: Node = await _create_initialized_main() - var session := main.get_node("%NetworkSession") as NetworkSession - var service := ( - main.get_node("%NetworkPlayerListService") - as NetworkPlayerListService - ) - var bans := main.get_node("%HostBanStore") as HostBanStore - assert(session.start_private_host(TEST_PORT)) - assert(session.set_host_open(true)) - - var remote_peer_id: int = await _wait_for_remote_peer(session) - assert(remote_peer_id > 1) - var remote_record: PeerRegistry.PeerRecord = session.get_peer_record( - remote_peer_id - ) - assert(remote_record != null and remote_record.identity_authenticated) - var host_fingerprint: String = session.get_host_identity_fingerprint() - assert(bans.ban( - host_fingerprint, FIRST_BANNED_FINGERPRINT, "First Banned Player" - )) - - var entry: PlayerListEntry = _entry_for_peer(service, remote_peer_id) - assert(entry != null and entry.can_manage_operator) - assert(service.set_operator( - remote_peer_id, - remote_record.identity_fingerprint, - true, - entry.revision, - )) - assert(session.is_peer_operator(remote_peer_id)) - var players_page := main.find_child( - "PlayersPage", true, false - ) as PlayersPage - assert(players_page != null) - players_page.call("_refresh") - assert(_has_button_text(players_page, "deop")) - - var unban_deadline: int = Time.get_ticks_msec() + 12000 - while ( - Time.get_ticks_msec() < unban_deadline - and bans.is_banned(host_fingerprint, FIRST_BANNED_FINGERPRINT) - ): - await process_frame - assert(not bans.is_banned( - host_fingerprint, FIRST_BANNED_FINGERPRINT - )) - assert(bans.ban( - host_fingerprint, SECOND_BANNED_FINGERPRINT, "Second Banned Player" - )) - - entry = _entry_for_peer(service, remote_peer_id) - assert(entry != null and entry.is_operator) - assert(service.set_operator( - remote_peer_id, - remote_record.identity_fingerprint, - false, - entry.revision, - )) - assert(not session.is_peer_operator(remote_peer_id)) - - var disconnect_deadline: int = Time.get_ticks_msec() + 8000 - while ( - Time.get_ticks_msec() < disconnect_deadline - and session.is_authenticated_peer(remote_peer_id) - ): - await process_frame - assert(not session.is_authenticated_peer(remote_peer_id)) - assert(bans.is_banned(host_fingerprint, SECOND_BANNED_FINGERPRINT)) - print("Operator multiplayer host validation: PASS") - session.disconnect_session("") - main.queue_free() - for _frame: int in 4: - await process_frame - await create_timer(0.1).timeout - quit() - - -func _run_client() -> void: - var main: Node = await _create_initialized_main() - main.call( - "_on_title_join_game_requested", "127.0.0.1:%d" % TEST_PORT - ) - var session := main.get_node("%NetworkSession") as NetworkSession - var service := ( - main.get_node("%NetworkPlayerListService") - as NetworkPlayerListService - ) - service.moderation_finished.connect( - func(success: bool, message: String) -> void: - _moderation_results.append({ - "success": success, - "message": message, - }) - ) - var join_deadline: int = Time.get_ticks_msec() + 20000 - while Time.get_ticks_msec() < join_deadline: - await process_frame - if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY: - main.call("_confirm_server_trust") - if session.is_joined_client(): - break - assert(session.is_joined_client()) - - var operator_deadline: int = Time.get_ticks_msec() + 10000 - while Time.get_ticks_msec() < operator_deadline and not session.is_local_operator(): - await process_frame - assert(session.is_local_operator()) - assert(service.is_local_moderator()) - var players_page := main.find_child( - "PlayersPage", true, false - ) as PlayersPage - assert(players_page != null) - players_page.call("_refresh") - var tabs := players_page.get("_tabs") as HBoxContainer - assert((tabs.get_child(2) as Button).visible) - players_page.call("_select_tab", 2) - assert(int(players_page.get("_current_tab")) == 2) - var local_entry: PlayerListEntry = _entry_for_peer( - service, session.get_local_peer_id() - ) - assert(local_entry != null and local_entry.is_operator) - - var ban_deadline: int = Time.get_ticks_msec() + 10000 - while ( - Time.get_ticks_msec() < ban_deadline - and not _has_ban(service.get_bans(), FIRST_BANNED_FINGERPRINT) - ): - await process_frame - assert(_has_ban(service.get_bans(), FIRST_BANNED_FINGERPRINT)) - assert(service.unban(FIRST_BANNED_FINGERPRINT)) - var result_deadline: int = Time.get_ticks_msec() + 8000 - while Time.get_ticks_msec() < result_deadline and _moderation_results.is_empty(): - await process_frame - assert(not _moderation_results.is_empty()) - assert(bool(_moderation_results.back().get("success", false))) - - var deop_deadline: int = Time.get_ticks_msec() + 10000 - while Time.get_ticks_msec() < deop_deadline and session.is_local_operator(): - await process_frame - assert(not session.is_local_operator()) - assert(not service.is_local_moderator()) - assert(not (tabs.get_child(2) as Button).visible) - assert(int(players_page.get("_current_tab")) == 0) - service.request_unban.rpc_id(1, SECOND_BANNED_FINGERPRINT) - await create_timer(1.0).timeout - print("Operator multiplayer client validation: PASS") - session.disconnect_session("") - main.queue_free() - for _frame: int in 4: - await process_frame - await create_timer(0.1).timeout - quit() - - -func _create_initialized_main() -> Node: - root.size = Vector2i(1280, 720) - var main: Node = MainScene.instantiate() - root.add_child(main) - for _frame: int in 4: - await process_frame - if not bool(main.get("_application_initialized")): - main.call("_activate_selected_data_path", "", true) - for _frame: int in 8: - await process_frame - assert(bool(main.get("_application_initialized"))) - return main - - -func _wait_for_remote_peer(session: NetworkSession) -> int: - var deadline: int = Time.get_ticks_msec() + 20000 - while Time.get_ticks_msec() < deadline: - await process_frame - for peer_id: int in session.get_authenticated_peer_ids(): - if peer_id != session.get_local_peer_id(): - return peer_id - return 0 - - -func _entry_for_peer( - service: NetworkPlayerListService, - peer_id: int, -) -> PlayerListEntry: - for entry: PlayerListEntry in service.get_entries(): - if entry.peer_id == peer_id: - return entry - return null - - -func _has_ban(records: Array[Dictionary], fingerprint: String) -> bool: - for record: Dictionary in records: - if str(record.get("target_fingerprint", "")) == fingerprint: - return true - return false - - -func _has_button_text(root_node: Node, text: String) -> bool: - for node: Node in root_node.find_children("*", "Button", true, false): - var button := node as Button - if button != null and button.text == text: - return true - return false diff --git a/tests/operator_multiplayer_validation.gd.uid b/tests/operator_multiplayer_validation.gd.uid deleted file mode 100644 index 2951ea6..0000000 --- a/tests/operator_multiplayer_validation.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://cv17xsf20ue5t diff --git a/ui/players_page.gd b/ui/players_page.gd index 380299e..8c93247 100644 --- a/ui/players_page.gd +++ b/ui/players_page.gd @@ -194,7 +194,7 @@ func _set_host_toggle_state( func _select_tab(index: int) -> void: - if index == 2 and (_service == null or not _service.is_local_moderator()): + if index == 2 and (_service == null or not _service.is_local_host()): return _current_tab = index _refresh() @@ -207,12 +207,10 @@ func _refresh() -> void: _count_label.text = "%d / %d connected" % [ _service.get_connected_count(), _service.get_max_players(), ] - if _current_tab == 2 and not _service.is_local_moderator(): - _current_tab = 0 for index: int in _tabs.get_child_count(): var button := _tabs.get_child(index) as Button button.button_pressed = index == _current_tab - button.visible = index != 2 or _service.is_local_moderator() + button.visible = index != 2 or _service.is_local_host() _refresh_host_settings() for child: Node in _list.get_children(): child.queue_free() @@ -332,7 +330,7 @@ func _on_host_status_changed(message: String, is_error: bool) -> void: func _build_active_rows() -> void: - if _service.is_local_moderator(): + if _service.is_local_host(): _build_session_artwork_controls() var entries := _service.get_entries() if entries.is_empty(): @@ -341,12 +339,10 @@ func _build_active_rows() -> void: for entry: PlayerListEntry in entries: var row := _make_row() var identity := Label.new() - identity.custom_minimum_size.x = 430 + identity.custom_minimum_size.x = 515 var markers: Array[String] = [] if entry.is_host: markers.append("host") - if entry.is_operator: - markers.append("operator") if entry.is_local_player: markers.append("You") identity.text = "%s%s · %s\n%s" % [ @@ -355,7 +351,7 @@ func _build_active_rows() -> void: entry.compact_fingerprint, entry.continuity_state, ] - identity.tooltip_text = "Full identity fingerprint:\n%s" % ( + identity.tooltip_text = NetworkIdentityCrypto.format_fingerprint( entry.full_fingerprint ) identity.add_theme_color_override( @@ -385,12 +381,6 @@ func _build_active_rows() -> void: block.pressed.connect(_confirm_block.bind(entry)) UtilityPageStyle.apply_ocean_button(block) row.add_child(block) - if entry.can_manage_operator: - var operator := Button.new() - operator.text = "deop" if entry.is_operator else "op" - operator.pressed.connect(_confirm_operator.bind(entry)) - UtilityPageStyle.apply_ocean_button(operator) - row.add_child(operator) var kick := Button.new() kick.text = "kick" kick.disabled = not entry.can_kick @@ -549,24 +539,6 @@ func _confirm_ban(entry: PlayerListEntry) -> void: ) -func _confirm_operator(entry: PlayerListEntry) -> void: - var enabled: bool = not entry.is_operator - _confirm( - ( - "Grant operator access to %s for this room session?" - if enabled - else "Remove operator access from %s?" - ) % entry.display_name, - func() -> void: - _service.set_operator( - entry.peer_id, - entry.full_fingerprint, - enabled, - entry.revision, - ) - ) - - func _confirm_unban(fingerprint: String) -> void: _confirm("Unban %s?" % NetworkIdentityCrypto.compact_suffix(fingerprint), func() -> void: _service.unban(fingerprint) diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 4a7ec9b..1b10bfd 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -156,7 +156,6 @@ func _ready() -> void: %ChangeDataFolder.pressed.connect(_choose_data_folder) %ExportPlayerIdentity.pressed.connect(_choose_identity_export.bind("player")) %ImportPlayerIdentity.pressed.connect(_choose_identity_import.bind("player")) - %CopyPlayerFingerprint.pressed.connect(_copy_player_fingerprint) %ExportHostIdentity.pressed.connect(_choose_identity_export.bind("host")) %ImportHostIdentity.pressed.connect(_choose_identity_import.bind("host")) for index: int in _world_options.size(): @@ -486,16 +485,6 @@ func _refresh_data_page() -> void: _data_root.override_active or (_network_session != null and _network_session.is_session_active()) ) - var fingerprint: String = ( - _player_identity.fingerprint if _player_identity != null else "" - ) - %CopyPlayerFingerprint.disabled = not NetworkIdentityCrypto.valid_fingerprint( - fingerprint - ) - %CopyPlayerFingerprint.text = ( - "Copy Player Fingerprint · %s" - % NetworkIdentityCrypto.compact_suffix(fingerprint) - ) func _open_data_folder() -> void: @@ -503,17 +492,6 @@ func _open_data_folder() -> void: _feedback.text = "Could not open the data folder." -func _copy_player_fingerprint() -> void: - var fingerprint: String = ( - _player_identity.fingerprint if _player_identity != null else "" - ) - if not NetworkIdentityCrypto.valid_fingerprint(fingerprint): - _feedback.text = "Player identity is unavailable." - return - DisplayServer.clipboard_set(fingerprint) - _feedback.text = "Full player fingerprint copied for server operator setup." - - func _choose_data_folder() -> void: if _data_root == null or _data_root.override_active: _feedback.text = "The data folder is externally managed." diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index df2e261..4cf1681 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -913,11 +913,6 @@ layout_mode = 2 text = "Active identity keys stay on this device.\nEncrypted backups can be stored in your synced data folder.\nDo not play the same profile on two devices at the same time." autowrap_mode = 2 -[node name="CopyPlayerFingerprint" type="Button" parent="DataPage/Paper/Content"] -unique_name_in_owner = true -layout_mode = 2 -text = "Copy Player Fingerprint" - [node name="IdentityGrid" type="GridContainer" parent="DataPage/Paper/Content"] layout_mode = 2 columns = 2