Expand gameplay systems and interface controls
This commit is contained in:
parent
09b7a78533
commit
2710544070
46 changed files with 2072 additions and 153 deletions
|
|
@ -4,7 +4,10 @@ extends Node
|
|||
const BURST_COUNT: int = 3
|
||||
const WINDOW_COUNT: int = 5
|
||||
const WINDOW_SECONDS: float = 10.0
|
||||
const CALL_COOLDOWN_MILLISECONDS: int = 180
|
||||
const CALL_COOLDOWN_MILLISECONDS: int = 90
|
||||
const DEDICATED_CHAT_WARNING: String = (
|
||||
"Privacy notice: This dedicated server records and retains chat messages."
|
||||
)
|
||||
const CALL_PITCH_VARIANTS: Array[float] = [
|
||||
0.96,
|
||||
1.03,
|
||||
|
|
@ -38,6 +41,32 @@ var _call_variant_indices: Dictionary[int, int] = {}
|
|||
var _sequence: int = 0
|
||||
var _peer_names: Dictionary[int, String] = {}
|
||||
var _relationships: PlayerRelationshipStore
|
||||
var _dedicated_history_enabled: bool = false
|
||||
var _chat_log_path: String = ""
|
||||
|
||||
|
||||
func configure_dedicated_history(enabled: bool, log_path: String) -> bool:
|
||||
_dedicated_history_enabled = false
|
||||
_chat_log_path = ""
|
||||
if not enabled:
|
||||
return true
|
||||
var normalized_path: String = log_path.strip_edges()
|
||||
if normalized_path.is_empty() or not normalized_path.is_absolute_path():
|
||||
return false
|
||||
var directory: String = normalized_path.get_base_dir()
|
||||
if DirAccess.make_dir_recursive_absolute(directory) != OK:
|
||||
return false
|
||||
var file: FileAccess
|
||||
if FileAccess.file_exists(normalized_path):
|
||||
file = FileAccess.open(normalized_path, FileAccess.READ_WRITE)
|
||||
else:
|
||||
file = FileAccess.open(normalized_path, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.close()
|
||||
_dedicated_history_enabled = true
|
||||
_chat_log_path = normalized_path
|
||||
return true
|
||||
|
||||
|
||||
func setup(session: NetworkSession) -> void:
|
||||
|
|
@ -288,7 +317,8 @@ func _handle_request(peer_id: int, data: Dictionary) -> void:
|
|||
var request_id: String = data["request_id"]
|
||||
var ledger: Dictionary = _request_ledgers.get(peer_id, {})
|
||||
if ledger.has(request_id):
|
||||
_send_message(peer_id, ledger[request_id])
|
||||
if typeof(ledger[request_id]) == TYPE_DICTIONARY:
|
||||
_send_message(peer_id, ledger[request_id])
|
||||
return
|
||||
if not _consume_rate(peer_id):
|
||||
_send_rejection(peer_id, "Slow down.")
|
||||
|
|
@ -308,7 +338,11 @@ func _handle_request(peer_id: int, data: Dictionary) -> void:
|
|||
message["request_id"] = request_id
|
||||
message["sender_fingerprint"] = data["sender_fingerprint"]
|
||||
message["sender_signature"] = data["sender_signature"]
|
||||
ledger[request_id] = message.duplicate(true)
|
||||
ledger[request_id] = (
|
||||
message.duplicate(true)
|
||||
if _should_store_host_history()
|
||||
else true
|
||||
)
|
||||
while ledger.size() > 64:
|
||||
ledger.erase(ledger.keys().front())
|
||||
_request_ledgers[peer_id] = ledger
|
||||
|
|
@ -404,9 +438,12 @@ func _apply_message(
|
|||
kind == NetworkChatProtocol.Kind.PLAYER
|
||||
and is_sender_filtered(str(data.get("sender_fingerprint", "")))
|
||||
)
|
||||
_history.append(stored_message)
|
||||
while _history.size() > NetworkChatProtocol.MAX_HISTORY:
|
||||
_history.pop_front()
|
||||
if _should_store_local_history():
|
||||
_history.append(stored_message)
|
||||
while _history.size() > NetworkChatProtocol.MAX_HISTORY:
|
||||
_history.pop_front()
|
||||
if _should_log_message(stored_message):
|
||||
_append_chat_log(stored_message)
|
||||
if emit_live_signals and _message_is_visible(stored_message):
|
||||
message_received.emit(stored_message.duplicate(true))
|
||||
if (
|
||||
|
|
@ -433,8 +470,26 @@ func _on_peer_authenticated(peer_id: int, display_name: String) -> void:
|
|||
if not _session.is_host():
|
||||
return
|
||||
_peer_names[peer_id] = display_name
|
||||
var start := maxi(0, _history.size() - NetworkChatProtocol.LATE_JOIN_HISTORY)
|
||||
receive_chat_history.rpc_id(peer_id, _history.slice(start))
|
||||
var history: Array[Dictionary] = []
|
||||
if _dedicated_history_enabled and _session.is_dedicated_host():
|
||||
var start := maxi(
|
||||
0,
|
||||
_history.size() - NetworkChatProtocol.LATE_JOIN_HISTORY,
|
||||
)
|
||||
history = _history.slice(start)
|
||||
# An explicit empty replacement is intentional for player-hosted rooms and
|
||||
# dedicated servers without logging. It destroys any stale client scrollback
|
||||
# instead of replaying another player's live-session chat on reconnect.
|
||||
receive_chat_history.rpc_id(peer_id, history)
|
||||
if _dedicated_history_enabled and _session.is_dedicated_host():
|
||||
# Deliver the disclosure as a live system message after scrollback. This
|
||||
# raises the normal unread indicator without creating speech or audio.
|
||||
_send_message(peer_id, _make_message(
|
||||
NetworkChatProtocol.Kind.SYSTEM,
|
||||
0,
|
||||
"",
|
||||
DEDICATED_CHAT_WARNING,
|
||||
))
|
||||
_broadcast(_make_message(
|
||||
NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s joined." % display_name
|
||||
))
|
||||
|
|
@ -494,6 +549,44 @@ func _consume_rate(peer_id: int) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func _should_store_host_history() -> bool:
|
||||
return (
|
||||
_session == null
|
||||
or not _session.is_dedicated_host()
|
||||
or _dedicated_history_enabled
|
||||
)
|
||||
|
||||
|
||||
func _should_store_local_history() -> bool:
|
||||
return _should_store_host_history()
|
||||
|
||||
|
||||
func _should_log_message(message: Dictionary) -> bool:
|
||||
return (
|
||||
_dedicated_history_enabled
|
||||
and not _chat_log_path.is_empty()
|
||||
and _session != null
|
||||
and _session.is_dedicated_host()
|
||||
and int(message.get("kind", -1)) == NetworkChatProtocol.Kind.PLAYER
|
||||
)
|
||||
|
||||
|
||||
func _append_chat_log(message: Dictionary) -> void:
|
||||
var file := FileAccess.open(_chat_log_path, FileAccess.READ_WRITE)
|
||||
if file == null:
|
||||
push_error("The configured dedicated-server chat log is unavailable.")
|
||||
return
|
||||
file.seek_end()
|
||||
file.store_line(JSON.stringify({
|
||||
"recorded_at_utc": (
|
||||
Time.get_datetime_string_from_system(true, false) + "Z"
|
||||
),
|
||||
"sender_display_name": str(message.get("sender_display_name", "")),
|
||||
"body": str(message.get("body", "")),
|
||||
}))
|
||||
file.close()
|
||||
|
||||
|
||||
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||
if state in [
|
||||
NetworkSession.State.INACTIVE,
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ func activate_process_root(path: String) -> bool:
|
|||
|
||||
func path_for(store_owner: StringName) -> String:
|
||||
var relative: String = {
|
||||
&"player_save": "player/player_save.json",
|
||||
&"player_save": "player/player_save.nfsave",
|
||||
&"network_profile": "player/network_profile.json",
|
||||
&"player_appearance": "player/player_appearance.json",
|
||||
&"saved_servers": "social/saved_servers.json",
|
||||
|
|
@ -208,6 +208,10 @@ func identity_backup_directory() -> String:
|
|||
return root_path.path_join("identity-backups")
|
||||
|
||||
|
||||
func progression_backup_directory() -> String:
|
||||
return root_path.path_join("progression-backups")
|
||||
|
||||
|
||||
func migration_backup_directory() -> String:
|
||||
return root_path.path_join("backups/migrations")
|
||||
|
||||
|
|
@ -347,7 +351,7 @@ func _test_writable(path: String) -> bool:
|
|||
func _create_layout(path: String, id: String) -> bool:
|
||||
for relative: String in [
|
||||
"player", "social", "backups/saves", "backups/migrations",
|
||||
"backups/conflicts", "identity-backups",
|
||||
"backups/conflicts", "identity-backups", "progression-backups",
|
||||
]:
|
||||
if DirAccess.make_dir_recursive_absolute(path.path_join(relative)) != OK:
|
||||
return false
|
||||
|
|
@ -369,6 +373,7 @@ func _create_layout(path: String, id: String) -> bool:
|
|||
+ "This folder is safe to synchronize with tools such as Syncthing.\n"
|
||||
+ "Active private identity keys remain device-local.\n"
|
||||
+ "Encrypted identity backups require their passphrase.\n"
|
||||
+ "Progression exports do not contain identity keys or social data.\n"
|
||||
+ "Chat and Session Mail are not stored here.\n"
|
||||
+ "Do not play the same profile on two devices at the same time.\n"
|
||||
+ "Conflicting edits are preserved under backups/conflicts; they are not merged.\n"
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ static func migrate_active_to(
|
|||
_remove_tree(staging)
|
||||
return {"ok": false, "message": str(created.get("message", ""))}
|
||||
for relative: String in [
|
||||
"player/player_save.nfsave",
|
||||
"player/player_save.json",
|
||||
"player/network_profile.json",
|
||||
"player/player_appearance.json",
|
||||
|
|
@ -134,7 +135,11 @@ static func migrate_active_to(
|
|||
var source: String = data_root.root_path.path_join(relative)
|
||||
if not FileAccess.file_exists(source):
|
||||
continue
|
||||
if not bool(_copy_verified_json(source, staging.path_join(relative)).get("ok", false)):
|
||||
if not bool(
|
||||
_copy_verified_owned_file(
|
||||
source, staging.path_join(relative)
|
||||
).get("ok", false)
|
||||
):
|
||||
_remove_tree(staging)
|
||||
return {"ok": false, "message": "Migration validation failed for %s." % relative}
|
||||
if DirAccess.dir_exists_absolute(normalized):
|
||||
|
|
@ -248,6 +253,30 @@ static func _copy_verified_json(source: String, destination: String) -> Dictiona
|
|||
}
|
||||
|
||||
|
||||
static func _copy_verified_owned_file(
|
||||
source: String,
|
||||
destination: String,
|
||||
) -> Dictionary:
|
||||
if source.get_file() != "player_save.nfsave":
|
||||
return _copy_verified_json(source, destination)
|
||||
var decoded: Dictionary = ProgressionSaveCodec.read_local_save(source)
|
||||
if not bool(decoded.get("ok", false)):
|
||||
return {"ok": false}
|
||||
var save_data: Dictionary = decoded["data"]
|
||||
var version: int = int(save_data.get("save_version", -1))
|
||||
if version < 1 or version > PlayerSaveManager.SAVE_VERSION:
|
||||
return {"ok": false}
|
||||
var bytes: PackedByteArray = PortableFileGuard.read_bytes(source)
|
||||
if bytes.is_empty() or not _write_bytes(destination, bytes):
|
||||
return {"ok": false}
|
||||
return {
|
||||
"ok": (
|
||||
PortableFileGuard.hash_file(destination)
|
||||
== PortableFileGuard.hash_bytes(bytes)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
static func _valid_owned_data(filename: String, data: Dictionary) -> bool:
|
||||
if filename == "player_save.json":
|
||||
var version: int = int(data.get("save_version", -1))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue