From cdddab898b9059b5413e692683a40fed82b2ee74 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Thu, 29 Sep 2022 12:03:31 -0400 Subject: [PATCH 01/11] Replays snapshotdata.gd: Now has a separate "lifetime history" of stored snapshots and EncDecBuffer. New function, _add_to_lifetime_history, that accepts a snapshot, encodes it and appends it to to the end of the lifetime history before clearing the lifetime history buffer's buffer New function, _convert_replay_to_snapshots grabs a replay of encoded snapshot data and decodes it through the lifetime history buffer, replacing each index of the replay with a snapshot before clearing the lifetime history buffer and returning the replay itself. New function, _convert_replay_to_lifetime_history, which is the same as the previous function, but instead of modifying the replay array and returning it, the snapshots are decoded and deposited directly in the _lifetime_history array. replay.gd: stores gzip compressed "replays" of serialized snapshot data to files. If the game is running out of the editor (even if project is not opened specifically), replays are saved to res://replays/ . Otherwise, replays are saved to a replay directory in Godot's userdata folder. gitignore updated to not include .REPLAY files or the replays/ folder. --- .gitignore | 4 ++ addons/keh_network/replay.gd | 69 ++++++++++++++++++++++++++++++ addons/keh_network/snapshotdata.gd | 42 +++++++++++++++++- project.godot | 20 ++++++--- 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 addons/keh_network/replay.gd diff --git a/.gitignore b/.gitignore index 4d18b76..005709d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,7 @@ export_presets.cfg # Mono-specific ignores .mono/ data_*/ + +# Replay-specific ignores +Replays/* +*.REPLAY \ No newline at end of file diff --git a/addons/keh_network/replay.gd b/addons/keh_network/replay.gd new file mode 100644 index 0000000..f633269 --- /dev/null +++ b/addons/keh_network/replay.gd @@ -0,0 +1,69 @@ +extends Reference +class_name Replay + +const save_path: String = "user://replays/" +const debug_save_path: String = "res://replays/" + +static func replay_to_compressed_buffer(replay: Array) -> PoolByteArray: + return var2bytes(replay).compress(File.COMPRESSION_GZIP) + +static func decompress_data(file: File, end: int) -> PoolByteArray: + return file.get_buffer(end).decompress_dynamic(-1,File.COMPRESSION_GZIP) + +static func read_compressed_replay_file(filepath: String) -> Array: + var file := File.new() + print("reading compressed replay file...") + if file.open(filepath, File.READ) == OK: + return open_compressed(file) + else: + print("error reading compressed replay file!") + return [] + +static func open_compressed(file: File) -> Array: + print("opening compressed replay file") + file.seek_end() + var end := file.get_position() + assert(end == file.get_len()) + file.seek(0) + print("decompressing replay file...") + var replay = bytes2var(decompress_data(file,end)) + print("data successfully decompressed!") + assert(replay is Array) + return replay + +static func get_file_name(title: String) -> String: + return str("%s %s %s.REPLAY"%[title, get_datetime_string(), OS.get_unique_id()]) + +static func get_datetime_string() -> String: + return Time.get_datetime_string_from_system(false, true).replace(":", "-") + +static func file_path_debug(name: String) -> String: + return debug_save_path + name + +static func file_path_normal(name: String) -> String: + return save_path + name + +static func save_compressed(file: File, replay: Array, title: String) -> void: + # diverges behavior based on if the game is exported or not + if !OS.has_feature("standalone"): + make_dir_if_doesnt_exist(debug_save_path) + file.open(file_path_debug(get_file_name(title)), File.WRITE) + else: + make_dir_if_doesnt_exist(save_path) + file.open(file_path_normal(get_file_name(title)), File.WRITE) + print("storing compressed replay file...") + file.store_buffer(replay_to_compressed_buffer(replay)) + + print("closing compressed replay file...") + print(get_file_name(title)) + file.close() + +static func make_dir_if_doesnt_exist(path: String) -> void: + var dir:= Directory.new() + if !dir.dir_exists(path): + if dir.make_dir(path) != OK: + # this is mad barebones + printerr("make_dir failed!") + +static func save(replay: Array) -> void: + save_compressed(File.new(),replay,"replay") diff --git a/addons/keh_network/snapshotdata.gd b/addons/keh_network/snapshotdata.gd index 20fb35a..e2881e8 100644 --- a/addons/keh_network/snapshotdata.gd +++ b/addons/keh_network/snapshotdata.gd @@ -40,6 +40,8 @@ var _entity_name: Dictionary = {} # Holds the history of snapshots var _history: Array = [] +var _lifetime_history: Array = [] +var _lifetime_history_buffer := EncDecBuffer.new() # These two dictionaries are used to perform easier queries to locate specific # snapshots. The first one is from snapshot signature into snapshot and the @@ -182,9 +184,36 @@ func reset() -> void: _server_state = null _history.clear() + if !_lifetime_history.empty(): + Replay.save(_lifetime_history) + # maybe just clear the buffer? + _lifetime_history_buffer = EncDecBuffer.new() + _lifetime_history.clear() _ssig_to_snap.clear() _isig_to_snap.clear() +# this might be OD trash for memory... maybe set up streaming func? +func _convert_replay_to_snapshots(replay: Array) -> Array: + for idx in replay.size(): + _lifetime_history_buffer.buffer = replay[idx] + # Maybe make this dependent on if _lifetime_history is empty or not instead? + if idx == 0: + replay[idx] = decode_full(_lifetime_history_buffer) + else: + replay[idx] = decode_delta(_lifetime_history_buffer) + _lifetime_history_buffer.buffer = PoolByteArray() + return replay + +# this might be OD trash for memory... maybe set up streaming func? +func convert_replay_to_lifetime_history(replay: Array) -> void: + for idx in replay.size(): + _lifetime_history_buffer.buffer = replay[idx] + # Maybe make this dependent on if _lifetime_history is empty or not instead? + if idx == 0: + _lifetime_history.push_back(decode_full(_lifetime_history_buffer)) + else: + _lifetime_history.push_back(decode_delta(_lifetime_history_buffer)) + _lifetime_history_buffer.buffer = PoolByteArray() func _instantiate_snap_entity(eclass: Script, uid: int, chash: int) -> SnapEntityBase: var ret: SnapEntityBase = null @@ -582,7 +611,18 @@ func _add_to_history(snap: NetSnapshot) -> void: _history.push_back(snap) _ssig_to_snap[snap.signature] = snap _isig_to_snap[snap.input_sig] = snap - + # Only save full history if the client is a server. Not so sure about this. +# if network.has_authority(): + _add_to_lifetime_history(snap) + +func _add_to_lifetime_history(snap: NetSnapshot) -> void: + # Maybe make this dependent on if _lifetime_history is empty or not instead? + if snap.signature == 0: + encode_full(snap,_lifetime_history_buffer,snap.input_sig) + else: + encode_delta(snap,_history[-1],_lifetime_history_buffer,snap.input_sig) + _lifetime_history.push_back(_lifetime_history_buffer.buffer) + _lifetime_history_buffer.buffer = PoolByteArray() func _check_history_size(max_size: int, has_authority: bool) -> void: var popped: int = 0 diff --git a/project.godot b/project.godot index 6e4f89f..3e53ee1 100644 --- a/project.godot +++ b/project.godot @@ -214,6 +214,11 @@ _global_script_classes=[ { "language": "GDScript", "path": "res://demos/network/scripts/rectdrawer.gd" }, { +"base": "Reference", +"class": "Replay", +"language": "GDScript", +"path": "res://addons/keh_network/replay.gd" +}, { "base": "Control", "class": "SFXHelper", "language": "GDScript", @@ -331,6 +336,7 @@ _global_script_class_icons={ "Network": "", "Quantize": "", "RectangleDrawer": "", +"Replay": "", "SFXHelper": "", "SampleDataAsset": "", "SampleScriptedResource": "", @@ -385,32 +391,32 @@ unused=true move_forward={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":87,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } move_backward={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":83,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } move_left={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":65,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } move_right={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":68,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } jump={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":32,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } sprint={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777237,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777237,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } shoot={ @@ -430,7 +436,7 @@ command_unit={ } multiselect={ "deadzone": 0.5, -"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777238,"unicode":0,"echo":false,"script":null) +"events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":16777238,"physical_scancode":0,"unicode":0,"echo":false,"script":null) ] } From d06723d554224adcb5be25081f7d807203567f8d Mon Sep 17 00:00:00 2001 From: ExplodingImplosion Date: Fri, 30 Sep 2022 00:24:06 -0400 Subject: [PATCH 02/11] small update Cleaned some code that didn't do anything Capitalized print strings Reorganized/changed some of the print strings get_file_name renamed to make_file_name for clarity organized similar file opening functionality into a simple function call removed redundant file_path_normal and file_path_debug functions Saving a replay now has an optional "name" argument (default is just 'Replay') --- addons/keh_network/replay.gd | 41 ++++++++++++------------------ addons/keh_network/snapshotdata.gd | 5 +++- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/addons/keh_network/replay.gd b/addons/keh_network/replay.gd index f633269..ac1ef6b 100644 --- a/addons/keh_network/replay.gd +++ b/addons/keh_network/replay.gd @@ -12,51 +12,42 @@ static func decompress_data(file: File, end: int) -> PoolByteArray: static func read_compressed_replay_file(filepath: String) -> Array: var file := File.new() - print("reading compressed replay file...") + print("Reading compressed replay file...") if file.open(filepath, File.READ) == OK: return open_compressed(file) else: - print("error reading compressed replay file!") + print("Error reading compressed replay file!") return [] static func open_compressed(file: File) -> Array: - print("opening compressed replay file") - file.seek_end() - var end := file.get_position() - assert(end == file.get_len()) - file.seek(0) - print("decompressing replay file...") - var replay = bytes2var(decompress_data(file,end)) - print("data successfully decompressed!") + print("Decompressing replay file...") + var replay = bytes2var(decompress_data(file,file.get_len())) + print("Data successfully decompressed!") assert(replay is Array) return replay -static func get_file_name(title: String) -> String: +static func make_file_name(title: String) -> String: return str("%s %s %s.REPLAY"%[title, get_datetime_string(), OS.get_unique_id()]) static func get_datetime_string() -> String: return Time.get_datetime_string_from_system(false, true).replace(":", "-") -static func file_path_debug(name: String) -> String: - return debug_save_path + name - -static func file_path_normal(name: String) -> String: - return save_path + name +static func open_file_at_directory(file: File, file_name: String, directory: String) -> void: + make_dir_if_doesnt_exist(directory) + file.open(directory + file_name, File.WRITE) static func save_compressed(file: File, replay: Array, title: String) -> void: # diverges behavior based on if the game is exported or not if !OS.has_feature("standalone"): - make_dir_if_doesnt_exist(debug_save_path) - file.open(file_path_debug(get_file_name(title)), File.WRITE) + open_file_at_directory(file,make_file_name(title),debug_save_path) else: - make_dir_if_doesnt_exist(save_path) - file.open(file_path_normal(get_file_name(title)), File.WRITE) - print("storing compressed replay file...") + open_file_at_directory(file,make_file_name(title),save_path) + print("Storing compressed replay file...") file.store_buffer(replay_to_compressed_buffer(replay)) - print("closing compressed replay file...") - print(get_file_name(title)) + prints("Closing compressed replay file",make_file_name(title),"...") file.close() + prints("File closed.") static func make_dir_if_doesnt_exist(path: String) -> void: var dir:= Directory.new() @@ -65,5 +56,5 @@ static func make_dir_if_doesnt_exist(path: String) -> void: # this is mad barebones printerr("make_dir failed!") -static func save(replay: Array) -> void: - save_compressed(File.new(),replay,"replay") +static func save(replay: Array,name: String = "Replay") -> void: + save_compressed(File.new(),replay,name) diff --git a/addons/keh_network/snapshotdata.gd b/addons/keh_network/snapshotdata.gd index e2881e8..f44330d 100644 --- a/addons/keh_network/snapshotdata.gd +++ b/addons/keh_network/snapshotdata.gd @@ -185,7 +185,10 @@ func reset() -> void: _server_state = null _history.clear() if !_lifetime_history.empty(): - Replay.save(_lifetime_history) + if network.has_authority(): + Replay.save(_lifetime_history,"Server Replay") + else: + Replay.save(_lifetime_history,"Client Replay") # maybe just clear the buffer? _lifetime_history_buffer = EncDecBuffer.new() _lifetime_history.clear() From 202337a6f60fc1a254fc1e66a64f05ccfef57f95 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Sun, 2 Oct 2022 10:32:49 -0400 Subject: [PATCH 03/11] Decoupled replay from network addon replay.gd: File moved to new folder Added copyright notice header Replays can now be initialized as their own objects snapshotdata.gd: Removed previous coupling with replay.gd, now 'reverted' to previous. TODO: implement a variable rate at which replays store full snapshots to make playback easier. --- addons/exploding_replays/replay.gd | 183 +++++++++++++++++++++++++++++ addons/keh_network/replay.gd | 60 ---------- addons/keh_network/snapshotdata.gd | 45 ------- project.godot | 2 +- 4 files changed, 184 insertions(+), 106 deletions(-) create mode 100644 addons/exploding_replays/replay.gd delete mode 100644 addons/keh_network/replay.gd diff --git a/addons/exploding_replays/replay.gd b/addons/exploding_replays/replay.gd new file mode 100644 index 0000000..5417f7c --- /dev/null +++ b/addons/exploding_replays/replay.gd @@ -0,0 +1,183 @@ +############################################################################### +# Copyright (c) 2022 Miles Mazzotta +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +############################################################################### + +extends Reference +class_name Replay + +var _history: Array +var _tickrate: int +var _full_snapshot_tickrate: int +var edec: EncDecBuffer = get_net_edec() + +func _init(tickrate: int,full_snapshot_tickrate: int) -> void: + setup(tickrate,full_snapshot_tickrate) + +func setup(tickrate: int,full_snapshot_tickrate: int) -> void: + _tickrate = tickrate + _full_snapshot_tickrate = full_snapshot_tickrate + +func add_snapshot(snapshot: NetSnapshot) -> void: + # There is no practical reason to cache this as a local variable. + # This is simply here to make some of the lower lines of code shorter. + var sd: NetSnapshotData = network.snapshot_data + if snapshot.signature == 0: + sd.encode_full(snapshot,edec,snapshot.input_sig) + else: + sd.encode_delta(snapshot,sd._history[-1],edec,snapshot.input_sig) + add_buffer() + +# Only for use when it is safe to assume that the buffer is full with proper +# snapshot info +func add_buffer() -> void: + _history.append(edec.buffer) + +func reset() -> void: + _history.clear() + # Every time a new replay is loaded, the tickrate and full_snapshot_tickrate + # SHOULD be set to something new. No point in setting them to 0, because + # if a replay is loaded and these are incorrect, something has definitely + # gone wrong. +# tickrate = 0 +# full_snapshot_tickrate = 0 + +func save_self(name: String, directory: String) -> void: + if !_history.empty(): + save(self,name,directory) + else: + print("Attempted to save Replay %s at %s but couldn't. Replay history is empty!"%[name,directory]) + +func save_and_reset(name: String, directory: String) -> void: + save_self(name,directory) + reset() + +func load_replay(filepath: String) -> void: + deserialize(read_compressed_replay_file(filepath)) + # Could totally just be this instead: +# _history = convert_to_snapshots(read_compressed_replay_file(filepath),buffer) + +# Theoretically this is faster than reading a replay file, converting that +# array from an array of poolbytearrays to an array of snapshots, and then +# assigning the history var to the array of snapshots. Why do I say theoretically? +# Because I have ZERO clue if that's actually true or not. +func deserialize(serialized: Array) -> void: + assert_enumerated_array_correct(serialized) + assert(_history.empty()) + setup(serialized[TICKRATE],serialized[FULL_SNAPSHOT_TICKRATE]) + for s_snap in serialized[HISTORY]: + assert(s_snap is PoolByteArray) + edec.buffer = s_snap + if _history.empty(): + _history.append(network.snapshot_data.decode_full(edec)) + else: + _history.append(network.snapshot_data.decode_delta(edec)) + + + + +# STATIC AND HELPER FUNCS ////////////////////////////////////////////////////// + +static func get_net_edec() -> EncDecBuffer: + return network._update_control.edec + +static func get_net_buffer() -> PoolByteArray: + return network._update_control.edec.buffer + +const default_save_path: String = "user://replays/" + +static func convert_to_snapshots(replay: Array, buffer: EncDecBuffer) -> Array: + assert_enumerated_array_correct(replay) + var history: Array = replay[HISTORY] + for idx in history.size(): + assert(history[idx] is PoolByteArray) + buffer.buffer = history[idx] + if idx == 0: + history[idx] = network.snapshot_data.decode_full(buffer) + else: + history[idx] = network.snapshot_data.decode_delta(buffer) + # doesn't need to return necessarily, this func operates over the actual array itself + return replay + +enum {TICKRATE,FULL_SNAPSHOT_TICKRATE,HISTORY,REPLAY_MAX} +static func to_enumerated_array(replay: Replay) -> Array: + return [replay._tickrate,replay._full_snapshot_tickrate,replay._history] + +static func assert_enumerated_array_correct(array: Array) -> void: + assert(array.size() == REPLAY_MAX) + assert(array[TICKRATE] is int and array[FULL_SNAPSHOT_TICKRATE] is int and array[HISTORY] is Array) + +static func replay_to_compressed_buffer(replay: Replay) -> PoolByteArray: + return var2bytes(to_enumerated_array(replay)).compress(File.COMPRESSION_GZIP) + +static func decompress_data(file: File, end: int) -> PoolByteArray: + return file.get_buffer(end).decompress_dynamic(-1,File.COMPRESSION_GZIP) + +static func read_compressed_replay_file(filepath: String) -> Array: + var file := File.new() + print("Reading compressed replay file...") + if file.open(filepath, File.READ) == OK: + return open_compressed(file) + else: + print("Error reading compressed replay file!") + return [] + +static func open_compressed(file: File) -> Array: + print("Decompressing replay file...") + var replay = bytes2var(decompress_data(file,file.get_len())) + print("Data successfully decompressed!") + assert(replay is Array) + return replay + +static func default_file(title: String) -> String: + return title + get_datetime_string() + OS.get_unique_id() + +static func as_replay_file(filename: String) -> String: + return str("%s.REPLAY"%[filename]) + +static func get_datetime_string() -> String: + return Time.get_datetime_string_from_system(false, true).replace(":", "-") + +static func open_file_at_directory(file: File, file_name: String, directory: String) -> void: + make_dir_if_doesnt_exist(directory) + file.open(directory + file_name, File.WRITE) + +static func save_compressed(file: File, replay: Replay, title: String, directory: String) -> void: + # diverges behavior based on if the game is exported or not + if !OS.has_feature("standalone"): + open_file_at_directory(file,as_replay_file(title),directory) + else: + open_file_at_directory(file,as_replay_file(title),directory) + print("Storing compressed replay file...") + file.store_buffer(replay_to_compressed_buffer(replay)) + + prints("Closing compressed replay file",as_replay_file(title),"...") + file.close() + prints("File closed.") + +static func make_dir_if_doesnt_exist(path: String) -> void: + var dir:= Directory.new() + if !dir.dir_exists(path): + if dir.make_dir(path) != OK: + # this is mad barebones + printerr("make_dir failed!") + +static func save(replay: Replay,name: String, path: String) -> void: + save_compressed(File.new(),replay,name,path) diff --git a/addons/keh_network/replay.gd b/addons/keh_network/replay.gd deleted file mode 100644 index ac1ef6b..0000000 --- a/addons/keh_network/replay.gd +++ /dev/null @@ -1,60 +0,0 @@ -extends Reference -class_name Replay - -const save_path: String = "user://replays/" -const debug_save_path: String = "res://replays/" - -static func replay_to_compressed_buffer(replay: Array) -> PoolByteArray: - return var2bytes(replay).compress(File.COMPRESSION_GZIP) - -static func decompress_data(file: File, end: int) -> PoolByteArray: - return file.get_buffer(end).decompress_dynamic(-1,File.COMPRESSION_GZIP) - -static func read_compressed_replay_file(filepath: String) -> Array: - var file := File.new() - print("Reading compressed replay file...") - if file.open(filepath, File.READ) == OK: - return open_compressed(file) - else: - print("Error reading compressed replay file!") - return [] - -static func open_compressed(file: File) -> Array: - print("Decompressing replay file...") - var replay = bytes2var(decompress_data(file,file.get_len())) - print("Data successfully decompressed!") - assert(replay is Array) - return replay - -static func make_file_name(title: String) -> String: - return str("%s %s %s.REPLAY"%[title, get_datetime_string(), OS.get_unique_id()]) - -static func get_datetime_string() -> String: - return Time.get_datetime_string_from_system(false, true).replace(":", "-") - -static func open_file_at_directory(file: File, file_name: String, directory: String) -> void: - make_dir_if_doesnt_exist(directory) - file.open(directory + file_name, File.WRITE) - -static func save_compressed(file: File, replay: Array, title: String) -> void: - # diverges behavior based on if the game is exported or not - if !OS.has_feature("standalone"): - open_file_at_directory(file,make_file_name(title),debug_save_path) - else: - open_file_at_directory(file,make_file_name(title),save_path) - print("Storing compressed replay file...") - file.store_buffer(replay_to_compressed_buffer(replay)) - - prints("Closing compressed replay file",make_file_name(title),"...") - file.close() - prints("File closed.") - -static func make_dir_if_doesnt_exist(path: String) -> void: - var dir:= Directory.new() - if !dir.dir_exists(path): - if dir.make_dir(path) != OK: - # this is mad barebones - printerr("make_dir failed!") - -static func save(replay: Array,name: String = "Replay") -> void: - save_compressed(File.new(),replay,name) diff --git a/addons/keh_network/snapshotdata.gd b/addons/keh_network/snapshotdata.gd index f44330d..19e6c31 100644 --- a/addons/keh_network/snapshotdata.gd +++ b/addons/keh_network/snapshotdata.gd @@ -40,8 +40,6 @@ var _entity_name: Dictionary = {} # Holds the history of snapshots var _history: Array = [] -var _lifetime_history: Array = [] -var _lifetime_history_buffer := EncDecBuffer.new() # These two dictionaries are used to perform easier queries to locate specific # snapshots. The first one is from snapshot signature into snapshot and the @@ -184,40 +182,9 @@ func reset() -> void: _server_state = null _history.clear() - if !_lifetime_history.empty(): - if network.has_authority(): - Replay.save(_lifetime_history,"Server Replay") - else: - Replay.save(_lifetime_history,"Client Replay") - # maybe just clear the buffer? - _lifetime_history_buffer = EncDecBuffer.new() - _lifetime_history.clear() _ssig_to_snap.clear() _isig_to_snap.clear() -# this might be OD trash for memory... maybe set up streaming func? -func _convert_replay_to_snapshots(replay: Array) -> Array: - for idx in replay.size(): - _lifetime_history_buffer.buffer = replay[idx] - # Maybe make this dependent on if _lifetime_history is empty or not instead? - if idx == 0: - replay[idx] = decode_full(_lifetime_history_buffer) - else: - replay[idx] = decode_delta(_lifetime_history_buffer) - _lifetime_history_buffer.buffer = PoolByteArray() - return replay - -# this might be OD trash for memory... maybe set up streaming func? -func convert_replay_to_lifetime_history(replay: Array) -> void: - for idx in replay.size(): - _lifetime_history_buffer.buffer = replay[idx] - # Maybe make this dependent on if _lifetime_history is empty or not instead? - if idx == 0: - _lifetime_history.push_back(decode_full(_lifetime_history_buffer)) - else: - _lifetime_history.push_back(decode_delta(_lifetime_history_buffer)) - _lifetime_history_buffer.buffer = PoolByteArray() - func _instantiate_snap_entity(eclass: Script, uid: int, chash: int) -> SnapEntityBase: var ret: SnapEntityBase = null @@ -614,18 +581,6 @@ func _add_to_history(snap: NetSnapshot) -> void: _history.push_back(snap) _ssig_to_snap[snap.signature] = snap _isig_to_snap[snap.input_sig] = snap - # Only save full history if the client is a server. Not so sure about this. -# if network.has_authority(): - _add_to_lifetime_history(snap) - -func _add_to_lifetime_history(snap: NetSnapshot) -> void: - # Maybe make this dependent on if _lifetime_history is empty or not instead? - if snap.signature == 0: - encode_full(snap,_lifetime_history_buffer,snap.input_sig) - else: - encode_delta(snap,_history[-1],_lifetime_history_buffer,snap.input_sig) - _lifetime_history.push_back(_lifetime_history_buffer.buffer) - _lifetime_history_buffer.buffer = PoolByteArray() func _check_history_size(max_size: int, has_authority: bool) -> void: var popped: int = 0 diff --git a/project.godot b/project.godot index 3e53ee1..40b67d6 100644 --- a/project.godot +++ b/project.godot @@ -217,7 +217,7 @@ _global_script_classes=[ { "base": "Reference", "class": "Replay", "language": "GDScript", -"path": "res://addons/keh_network/replay.gd" +"path": "res://addons/exploding_replays/replay.gd" }, { "base": "Control", "class": "SFXHelper", From cfa85ddad74243b42070105e7444d86ca372bf98 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Sun, 2 Oct 2022 10:37:48 -0400 Subject: [PATCH 04/11] cleaning .gitignore reverted snapshotdata whitespace restored --- .gitignore | 6 +----- addons/keh_network/snapshotdata.gd | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 005709d..96d1197 100644 --- a/.gitignore +++ b/.gitignore @@ -11,8 +11,4 @@ export_presets.cfg # Mono-specific ignores .mono/ -data_*/ - -# Replay-specific ignores -Replays/* -*.REPLAY \ No newline at end of file +data_*/ \ No newline at end of file diff --git a/addons/keh_network/snapshotdata.gd b/addons/keh_network/snapshotdata.gd index 19e6c31..20fb35a 100644 --- a/addons/keh_network/snapshotdata.gd +++ b/addons/keh_network/snapshotdata.gd @@ -185,6 +185,7 @@ func reset() -> void: _ssig_to_snap.clear() _isig_to_snap.clear() + func _instantiate_snap_entity(eclass: Script, uid: int, chash: int) -> SnapEntityBase: var ret: SnapEntityBase = null @@ -582,6 +583,7 @@ func _add_to_history(snap: NetSnapshot) -> void: _ssig_to_snap[snap.signature] = snap _isig_to_snap[snap.input_sig] = snap + func _check_history_size(max_size: int, has_authority: bool) -> void: var popped: int = 0 From 0505d7892e0f5a05d43ed4c2961a7185cb3d9b92 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Sun, 2 Oct 2022 11:00:48 -0400 Subject: [PATCH 05/11] begin setting up replay demo --- demos/replays/Replay Viewer.gd | 144 +++++++++++++++++++++++++++++++ demos/replays/Replay Viewer.tscn | 25 ++++++ main.gd | 10 +-- main.tscn | 51 +++++++++-- 4 files changed, 219 insertions(+), 11 deletions(-) create mode 100644 demos/replays/Replay Viewer.gd create mode 100644 demos/replays/Replay Viewer.tscn diff --git a/demos/replays/Replay Viewer.gd b/demos/replays/Replay Viewer.gd new file mode 100644 index 0000000..002bb45 --- /dev/null +++ b/demos/replays/Replay Viewer.gd @@ -0,0 +1,144 @@ +extends Control + +var is_playing: bool +var tenseconds: int +var last_snap: NetSnapshot +var replay: Replay +onready var c: RichTextLabel = $Panel/C +onready var files: FileDialog = $FileDialog +onready var replayinfo: Label = $"Replay Info" +onready var replaychanger: Button = $newreplay +onready var timeline: HScrollBar = $Timeline +onready var playbackspeed: SpinBox = $HBoxContainer2/SpinBox +onready var viewport: Viewport = $ViewportContainer/Viewport +onready var timereadout: Label = $VBoxContainer/time +onready var tickreadout: Label = $VBoxContainer/ticknum + +func _ready() -> void: + files.call_deferred("invalidate") + change_replay() + +func _physics_process(delta: float) -> void: + pass + +func _process(delta: float) -> void: + pass + +func _input(event: InputEvent) -> void: + pass + +#static func get_time_from_ticknum(ticknum: int, tickrate: int) -> String: +# return History.get_replay_length_from_tick_count(ticknum, tickrate) + +func tick_feed(value: int) -> void: + pass + +func simulate_up_to_current_snapshot(this_snap: NetSnapshot) -> void: + pass + +func change_replay() -> void: + files.show() + # setup subwindow size (godot 4 quack func) + +func clear_replay() -> void: + pass + +func read_replay() -> void: + pass + +func setup_timeline() -> void: + pass + +func on_timeline_ticked(value: String) -> void: + pass + +func set_replay_info() -> void: + pass +# replayinfo.set_text("tickrate: %s | SS tickrate: %s | size: %s | length %s\nmap: %s | mode: %s"%[replay.tickrate, +# replay.snapshot_tickrate, +# replay.history.size(), +# get_time_from_ticknum(replay.history.size(),replay.tickrate), +# Map.map_name_from_path(replay.map_file_path), +# Gamemodes.get_gamemode_name(replay.gamemode)]) + +func go_to_first_tick() -> void: + timeline.set_value(0) + +func go_back_1_tick() -> void: + move_by_amnt(-1) + +func pause_if_playing() -> void: + if is_playing: + pause() + +func move_by_amnt(amnt: int) -> void: + timeline.set_value(timeline.get_value() + amnt) + +func go_forward_1_tick() -> void: + move_by_amnt(1) +# timeline.set_value(timeline.get_value() + 1) + +func go_to_last_tick() -> void: + timeline.set_value(timeline.get_max()) + pause_if_playing() + +func pause_unpause() -> void: + is_playing = !is_playing + +func play() -> void: + is_playing = true + +func pause() -> void: + is_playing = false + +func on_left_pressed() -> void: + go_back_1_tick() + pause_if_playing() + +func get_playback_speed() -> int: + return int(replay.tickrate * playbackspeed.get_value()) + +func on_right_pressed() -> void: + go_forward_1_tick() + pause_if_playing() + +func change_playback_speed(value: float) -> void: + pass +# Quack.set_tickrate(replay.tickrate * value) + +func goto_main_menu() -> void: + pass +# Quack.change_scene("res://Interface/Menus/Main Menu.tscn") + +func go_forward() -> void: + move_by_amnt(tenseconds) + +func go_back() -> void: + move_by_amnt(-tenseconds) + + +func clear_gameplay_if_loaded() -> void: + pass +# if map_is_loaded(): +# Network.get_entities().clear() +# Network.clear_map() + +func map_is_loaded() -> bool: + pass + return Network.map != null + +func setup_map() -> void: + pass +# Network.map = load(replay.map_file_path).instantiate() +# viewport.add_child(Network.map) +# Network.setup_entity_groups() +# last_tick = [] + + +func on_tree_exiting() -> void: + pass +# clear_gameplay_if_loaded() + +func on_timeline_scrolled() -> void: + pass + pause_if_playing() diff --git a/demos/replays/Replay Viewer.tscn b/demos/replays/Replay Viewer.tscn new file mode 100644 index 0000000..109f2d0 --- /dev/null +++ b/demos/replays/Replay Viewer.tscn @@ -0,0 +1,25 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://demos/replays/Replay Viewer.gd" type="Script" id=1] + +[node name="Replay Viewer" type="Control"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 1 ) + +[node name="Panel" type="Panel" parent="."] +anchor_right = 1.0 +anchor_bottom = 1.0 +__meta__ = { +"_editor_description_": "" +} + +[node name="ViewportContainer" type="ViewportContainer" parent="Panel"] +margin_left = 256.0 +margin_right = 1024.0 +margin_bottom = 450.0 + +[node name="Viewport" type="Viewport" parent="Panel/ViewportContainer"] +size = Vector2( 768, 450 ) +handle_input_locally = false +render_target_update_mode = 3 diff --git a/main.gd b/main.gd index 6da414d..a31fc8d 100644 --- a/main.gd +++ b/main.gd @@ -33,6 +33,7 @@ func _ready() -> void: set_tab_button("bt_megademo", "megademo") set_tab_button("bt_dbghelper", "dbghelper") set_tab_button("bt_audiomaster", "audiomaster") + set_tab_button("bt_replaydemo","replaydemo") # Connect the networking signals. Those are necessary in order to transition # into the game scene only on success and give the chance to show a message @@ -186,11 +187,10 @@ func _on_bt_dbgload_pressed() -> void: func _on_bt_amasterload_pressed(): open_scene("res://demos/audiomaster/amaster.tscn") +### Replay demo +func _on_bt_replaydemo_pressed() -> void: + open_scene("res://demos/replaydemo/Replay Viewer.tscn") + func _on_bt_quit_pressed() -> void: get_tree().quit() - - - - - diff --git a/main.tscn b/main.tscn index 7c258d5..d508037 100644 --- a/main.tscn +++ b/main.tscn @@ -10,9 +10,6 @@ anchor_bottom = 1.0 margin_top = 1.0 margin_bottom = 1.0 script = ExtResource( 1 ) -__meta__ = { -"_edit_use_anchors_": false -} [node name="mpnl" type="Panel" parent="."] anchor_right = 1.0 @@ -140,6 +137,14 @@ toggle_mode = true group = ExtResource( 2 ) text = "Complete" +[node name="bt_replaydemo" type="Button" parent="mpnl/demo_list/pnl/vbox"] +margin_top = 314.0 +margin_right = 206.0 +margin_bottom = 334.0 +toggle_mode = true +group = ExtResource( 2 ) +text = "Replay" + [node name="bt_quit" type="Button" parent="mpnl/demo_list/pnl"] anchor_top = 1.0 anchor_bottom = 1.0 @@ -148,9 +153,6 @@ margin_top = -34.3674 margin_right = 231.0 margin_bottom = -14.3674 text = "Quit" -__meta__ = { -"_edit_use_anchors_": false -} [node name="stabs" type="TabContainer" parent="mpnl"] anchor_right = 1.0 @@ -666,7 +668,42 @@ __meta__ = { "_edit_use_anchors_": false } +[node name="replaydemo" type="Panel" parent="mpnl/stabs"] +visible = false +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 4.0 +margin_top = 8.0 +margin_right = -4.0 +margin_bottom = -4.0 + +[node name="bt_replaydemo" type="Button" parent="mpnl/stabs/replaydemo"] +margin_left = 14.0 +margin_top = 11.0 +margin_right = 122.0 +margin_bottom = 31.0 +text = "Load" +__meta__ = { +"_edit_use_anchors_": false +} + +[node name="description" type="Label" parent="mpnl/stabs/replaydemo"] +anchor_right = 1.0 +anchor_bottom = 1.0 +margin_left = 14.0 +margin_top = 46.0 +margin_right = -9.99994 +margin_bottom = -20.0 +text = "Interdependency: Network addon, EncDecBuffer + +DESCRIPTION GOES HERE" +autowrap = true +__meta__ = { +"_edit_use_anchors_": false +} + [node name="audiomaster" type="Panel" parent="mpnl/stabs"] +visible = false anchor_right = 1.0 anchor_bottom = 1.0 margin_left = 4.0 @@ -711,6 +748,7 @@ autowrap = true __meta__ = { "_edit_use_anchors_": false } + [connection signal="pressed" from="mpnl/demo_list/pnl/bt_quit" to="." method="_on_bt_quit_pressed"] [connection signal="pressed" from="mpnl/stabs/encdecbuffer/bt_encdecload" to="." method="_on_bt_encdecload_pressed"] [connection signal="pressed" from="mpnl/stabs/quantize/bt_utilsload" to="." method="_on_bt_utilsload_pressed"] @@ -725,4 +763,5 @@ __meta__ = { [connection signal="pressed" from="mpnl/stabs/fancyle/bt_fleload" to="." method="_on_bt_fleload_pressed"] [connection signal="pressed" from="mpnl/stabs/dbghelper/bt_dbgload" to="." method="_on_bt_dbgload_pressed"] [connection signal="pressed" from="mpnl/stabs/inventory/bt_invdemoload" to="." method="_on_bt_invdemoload_pressed"] +[connection signal="pressed" from="mpnl/stabs/replaydemo/bt_replaydemo" to="." method="_on_bt_replaydemo_pressed"] [connection signal="pressed" from="mpnl/stabs/audiomaster/bt_amasterload" to="." method="_on_bt_amasterload_pressed"] From f75604fe1f72a3da6e27582a2e349bd88b327916 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Sun, 2 Oct 2022 17:16:16 -0400 Subject: [PATCH 06/11] Update main.tscn Implement description draft --- main.tscn | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/main.tscn b/main.tscn index d508037..8ff6af3 100644 --- a/main.tscn +++ b/main.tscn @@ -696,7 +696,15 @@ margin_right = -9.99994 margin_bottom = -20.0 text = "Interdependency: Network addon, EncDecBuffer -DESCRIPTION GOES HERE" +This demo provides a way to view replays of gameplay recorded in the 'mega' demo. If enabled, whenever a player manually disconnects from the mega demo, a .REPLAY file is saved to the specified file location. The demo itself is a dedicated replay viewing client, with traditional multimedia controls: + +- Pausing and playing | SPACEBAR +- Skip forward/back | CTRL + RIGHT/LEFT ARROW KEYS +- Adjust playback speed | UP/DOWN ARROW KEYS +- Adjustable timeline playhead | RIGHT/LEFT ARROW KEYS +- Restarting/skipping to end | CTRL + SHIFT + RIGHT/LEFT ARROW KEYS + +F4 returns to the main menu." autowrap = true __meta__ = { "_edit_use_anchors_": false From fb75cf7623c8e1f28512c89a1953f28447a8277e Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Mon, 3 Oct 2022 12:08:58 -0400 Subject: [PATCH 07/11] gaming removed an extra space typo from the dbghelper debug helper addon section replayviewer updates, unfinished replays now save snapshots to their arrays and encode upon being saved save func no longer static, moved to what was previously save_self --- addons/exploding_replays/replay.gd | 33 ++- demos/replays/Replay Viewer.tscn | 25 -- .../{Replay Viewer.gd => replayviewer.gd} | 105 ++++++-- demos/replays/replayviewer.tscn | 227 ++++++++++++++++++ main.gd | 2 +- main.tscn | 3 +- 6 files changed, 337 insertions(+), 58 deletions(-) delete mode 100644 demos/replays/Replay Viewer.tscn rename demos/replays/{Replay Viewer.gd => replayviewer.gd} (52%) create mode 100644 demos/replays/replayviewer.tscn diff --git a/addons/exploding_replays/replay.gd b/addons/exploding_replays/replay.gd index 5417f7c..b6f1a2e 100644 --- a/addons/exploding_replays/replay.gd +++ b/addons/exploding_replays/replay.gd @@ -36,19 +36,18 @@ func setup(tickrate: int,full_snapshot_tickrate: int) -> void: _full_snapshot_tickrate = full_snapshot_tickrate func add_snapshot(snapshot: NetSnapshot) -> void: + _history.append(snapshot) + +func encode_snapshot(snapshot: NetSnapshot) -> PoolByteArray: # There is no practical reason to cache this as a local variable. # This is simply here to make some of the lower lines of code shorter. var sd: NetSnapshotData = network.snapshot_data + edec.buffer = PoolByteArray() if snapshot.signature == 0: sd.encode_full(snapshot,edec,snapshot.input_sig) else: sd.encode_delta(snapshot,sd._history[-1],edec,snapshot.input_sig) - add_buffer() - -# Only for use when it is safe to assume that the buffer is full with proper -# snapshot info -func add_buffer() -> void: - _history.append(edec.buffer) + return edec.buffer func reset() -> void: _history.clear() @@ -59,14 +58,21 @@ func reset() -> void: # tickrate = 0 # full_snapshot_tickrate = 0 -func save_self(name: String, directory: String) -> void: +func save(name: String, directory: String) -> void: if !_history.empty(): - save(self,name,directory) + var newarray: Array + var temp: Array = _history + newarray.resize(_history.size()) + for i in newarray.size(): + newarray[i] = encode_snapshot(_history[i]) + _history = newarray + save_compressed(File.new(),self,name,directory) + _history = temp else: print("Attempted to save Replay %s at %s but couldn't. Replay history is empty!"%[name,directory]) func save_and_reset(name: String, directory: String) -> void: - save_self(name,directory) + save(name,directory) reset() func load_replay(filepath: String) -> void: @@ -160,11 +166,7 @@ static func open_file_at_directory(file: File, file_name: String, directory: Str file.open(directory + file_name, File.WRITE) static func save_compressed(file: File, replay: Replay, title: String, directory: String) -> void: - # diverges behavior based on if the game is exported or not - if !OS.has_feature("standalone"): - open_file_at_directory(file,as_replay_file(title),directory) - else: - open_file_at_directory(file,as_replay_file(title),directory) + open_file_at_directory(file,as_replay_file(title),directory) print("Storing compressed replay file...") file.store_buffer(replay_to_compressed_buffer(replay)) @@ -178,6 +180,3 @@ static func make_dir_if_doesnt_exist(path: String) -> void: if dir.make_dir(path) != OK: # this is mad barebones printerr("make_dir failed!") - -static func save(replay: Replay,name: String, path: String) -> void: - save_compressed(File.new(),replay,name,path) diff --git a/demos/replays/Replay Viewer.tscn b/demos/replays/Replay Viewer.tscn deleted file mode 100644 index 109f2d0..0000000 --- a/demos/replays/Replay Viewer.tscn +++ /dev/null @@ -1,25 +0,0 @@ -[gd_scene load_steps=2 format=2] - -[ext_resource path="res://demos/replays/Replay Viewer.gd" type="Script" id=1] - -[node name="Replay Viewer" type="Control"] -anchor_right = 1.0 -anchor_bottom = 1.0 -script = ExtResource( 1 ) - -[node name="Panel" type="Panel" parent="."] -anchor_right = 1.0 -anchor_bottom = 1.0 -__meta__ = { -"_editor_description_": "" -} - -[node name="ViewportContainer" type="ViewportContainer" parent="Panel"] -margin_left = 256.0 -margin_right = 1024.0 -margin_bottom = 450.0 - -[node name="Viewport" type="Viewport" parent="Panel/ViewportContainer"] -size = Vector2( 768, 450 ) -handle_input_locally = false -render_target_update_mode = 3 diff --git a/demos/replays/Replay Viewer.gd b/demos/replays/replayviewer.gd similarity index 52% rename from demos/replays/Replay Viewer.gd rename to demos/replays/replayviewer.gd index 002bb45..61b7a6d 100644 --- a/demos/replays/Replay Viewer.gd +++ b/demos/replays/replayviewer.gd @@ -1,4 +1,4 @@ -extends Control +extends Panel var is_playing: bool var tenseconds: int @@ -8,11 +8,22 @@ onready var c: RichTextLabel = $Panel/C onready var files: FileDialog = $FileDialog onready var replayinfo: Label = $"Replay Info" onready var replaychanger: Button = $newreplay -onready var timeline: HScrollBar = $Timeline -onready var playbackspeed: SpinBox = $HBoxContainer2/SpinBox -onready var viewport: Viewport = $ViewportContainer/Viewport -onready var timereadout: Label = $VBoxContainer/time -onready var tickreadout: Label = $VBoxContainer/ticknum +onready var timeline: HScrollBar = $VBoxContainer/Timeline +onready var playbackspeed: SpinBox = $TimeScale +onready var viewport: Viewport = $CenterContainer/ViewportContainer/Viewport +onready var timereadout: Label = $TimecodeInfo/TimeReadout/Readout +onready var tickreadout: Label = $TimecodeInfo/FrameReadout/Readout +onready var fpsreadout: Label = $FPS/Readout +onready var playpauseicon: Button = $VBoxContainer/MediaControls/PauseAndPlayMediaControls/PauseAndPlay + +var playicon: Texture = preload("res://addons/keh_gddb/editor/btplay_16x16.png") +var pauseicon: Texture = preload("res://addons/keh_gddb/editor/btpause_16x16.png") + +func assign_icon() -> void: + playpauseicon.set_button_icon(get_icon_from_is_playing()) + +func get_icon_from_is_playing() -> Texture: + return playicon if is_playing else pauseicon func _ready() -> void: files.call_deferred("invalidate") @@ -22,10 +33,69 @@ func _physics_process(delta: float) -> void: pass func _process(delta: float) -> void: - pass + fpsreadout.set_text(str(Engine.get_frames_per_second())) func _input(event: InputEvent) -> void: - pass + # Forward 1 frame + if Input.is_action_pressed("ui_right"): + # Forward 10 seconds + if Input.is_action_pressed("sprint"): + # To end + if Input.is_action_pressed("multiselect"): + go_to_end() + else: + go_forward() + else: + go_forward_1() + + # Back 1 frame + if Input.is_action_pressed("ui_left"): + # Back 10 seconds + if Input.is_action_pressed("sprint"): + # Restart + if Input.is_action_pressed("multiselect"): + restart() + else: + go_back() + else: + go_back_1() + + # Timescale up/down + if Input.is_action_pressed("ui_up"): + pass + if Input.is_action_pressed("ui_down"): + pass + + # Play/pause + if Input.is_action_pressed("ui_select"): + pause_unpause() + + if (event is InputEventKey): + if (event.pressed): + match event.scancode: + KEY_F1: + OS.vsync_enabled = !OS.vsync_enabled + + KEY_F4: + # Restore mouse visibility + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + # Go back to the main menu + # warning-ignore:return_value_discarded + get_tree().change_scene("res://main.tscn") + + KEY_F10: + OverlayDebugInfo.toggle_visibility() + + + KEY_ESCAPE: + # TODO: toggle visibility of a menu - set mouse mode based on that + # For now just toggle mouse mode + if (Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE): + # It's already visible, so capture it TODO: only if freecam enabled + Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED) + else: + # It's captured, so show it + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) #static func get_time_from_ticknum(ticknum: int, tickrate: int) -> String: # return History.get_replay_length_from_tick_count(ticknum, tickrate) @@ -61,10 +131,10 @@ func set_replay_info() -> void: # Map.map_name_from_path(replay.map_file_path), # Gamemodes.get_gamemode_name(replay.gamemode)]) -func go_to_first_tick() -> void: +func restart() -> void: timeline.set_value(0) -func go_back_1_tick() -> void: +func go_back_1() -> void: move_by_amnt(-1) func pause_if_playing() -> void: @@ -74,32 +144,35 @@ func pause_if_playing() -> void: func move_by_amnt(amnt: int) -> void: timeline.set_value(timeline.get_value() + amnt) -func go_forward_1_tick() -> void: +func go_forward_1() -> void: move_by_amnt(1) # timeline.set_value(timeline.get_value() + 1) -func go_to_last_tick() -> void: +func go_to_end() -> void: timeline.set_value(timeline.get_max()) pause_if_playing() func pause_unpause() -> void: is_playing = !is_playing + assign_icon() func play() -> void: is_playing = true + assign_icon() func pause() -> void: is_playing = false + assign_icon() func on_left_pressed() -> void: - go_back_1_tick() + go_back_1() pause_if_playing() func get_playback_speed() -> int: return int(replay.tickrate * playbackspeed.get_value()) func on_right_pressed() -> void: - go_forward_1_tick() + go_forward_1() pause_if_playing() func change_playback_speed(value: float) -> void: @@ -142,3 +215,7 @@ func on_tree_exiting() -> void: func on_timeline_scrolled() -> void: pass pause_if_playing() + + +func Pause() -> void: + pass # Replace with function body. diff --git a/demos/replays/replayviewer.tscn b/demos/replays/replayviewer.tscn new file mode 100644 index 0000000..9926923 --- /dev/null +++ b/demos/replays/replayviewer.tscn @@ -0,0 +1,227 @@ +[gd_scene load_steps=7 format=2] + +[ext_resource path="res://addons/keh_gddb/editor/btplay_16x16.png" type="Texture" id=1] +[ext_resource path="res://shared/fonts/Aileron-Bold.otf" type="DynamicFontData" id=2] +[ext_resource path="res://shared/fonts/aileron-bold.tres" type="DynamicFont" id=3] +[ext_resource path="res://demos/replays/replayviewer.gd" type="Script" id=4] + +[sub_resource type="DynamicFont" id=1] +size = 14 +font_data = ExtResource( 2 ) + +[sub_resource type="DynamicFont" id=2] +size = 10 +font_data = ExtResource( 2 ) + +[node name="Replay Viewer" type="Panel"] +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource( 4 ) +__meta__ = { +"_editor_description_": "" +} + +[node name="CenterContainer" type="CenterContainer" parent="."] +margin_top = 64.0 +margin_right = 1024.0 +margin_bottom = 512.0 + +[node name="ViewportContainer" type="ViewportContainer" parent="CenterContainer"] +margin_left = 149.0 +margin_top = 20.0 +margin_right = 875.0 +margin_bottom = 428.0 + +[node name="Viewport" type="Viewport" parent="CenterContainer/ViewportContainer"] +size = Vector2( 726, 408 ) +handle_input_locally = false +render_target_update_mode = 3 + +[node name="FPS" type="HBoxContainer" parent="."] +margin_left = 904.0 +margin_top = 48.0 +margin_right = 968.0 +margin_bottom = 68.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="FPS"] +margin_right = 36.0 +margin_bottom = 20.0 +custom_fonts/font = ExtResource( 3 ) +text = "FPS: " + +[node name="Readout" type="Label" parent="FPS"] +margin_left = 36.0 +margin_right = 36.0 +margin_bottom = 20.0 +custom_fonts/font = ExtResource( 3 ) + +[node name="TimeScaleLabel" type="Label" parent="."] +margin_left = 904.0 +margin_top = 520.0 +margin_right = 1028.0 +margin_bottom = 538.0 +custom_fonts/font = SubResource( 1 ) +text = "Playback Speed:" + +[node name="TimeScale" type="SpinBox" parent="."] +margin_left = 904.0 +margin_top = 538.0 +margin_right = 1024.0 +margin_bottom = 562.0 +min_value = 0.1 +max_value = 5.0 +step = 0.1 +value = 1.0 + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +margin_left = 128.0 +margin_top = 498.0 +margin_right = 896.0 +margin_bottom = 560.0 +custom_constants/separation = 0 +alignment = 2 +__meta__ = { +"_editor_description_": "" +} + +[node name="Timeline" type="HSlider" parent="VBoxContainer"] +margin_top = 6.0 +margin_right = 768.0 +margin_bottom = 22.0 +rounded = true + +[node name="MediaControls" type="HBoxContainer" parent="VBoxContainer"] +margin_top = 22.0 +margin_right = 768.0 +margin_bottom = 62.0 +rect_min_size = Vector2( 0, 40 ) +custom_constants/separation = 0 +alignment = 1 + +[node name="Restart" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 48.0 +margin_right = 144.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +text = "[<-" +icon_align = 1 + +[node name="10SecondsBack" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 144.0 +margin_right = 240.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +text = "<<" +icon_align = 1 + +[node name="1FrameBack" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 240.0 +margin_right = 336.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +text = "<" +icon_align = 1 + +[node name="PauseAndPlay" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 336.0 +margin_right = 432.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +icon = ExtResource( 1 ) +icon_align = 1 + +[node name="1FrameForward" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 432.0 +margin_right = 528.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +text = ">" +icon_align = 1 + +[node name="10SecondsForward" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 528.0 +margin_right = 624.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +text = ">>" +icon_align = 1 + +[node name="ToEnd" type="Button" parent="VBoxContainer/MediaControls"] +margin_left = 624.0 +margin_right = 720.0 +margin_bottom = 40.0 +rect_min_size = Vector2( 96, 0 ) +custom_fonts/font = ExtResource( 3 ) +text = "->]" +icon_align = 1 + +[node name="TimecodeInfo" type="VBoxContainer" parent="."] +margin_top = 320.0 +margin_right = 146.0 +margin_bottom = 350.0 + +[node name="TimeReadout" type="HBoxContainer" parent="TimecodeInfo"] +margin_right = 146.0 +margin_bottom = 13.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="TimecodeInfo/TimeReadout"] +margin_right = 72.0 +margin_bottom = 13.0 +custom_fonts/font = SubResource( 2 ) +text = "Timecode: " + +[node name="Readout" type="Label" parent="TimecodeInfo/TimeReadout"] +margin_left = 72.0 +margin_right = 98.0 +margin_bottom = 13.0 +custom_fonts/font = SubResource( 2 ) +text = "00:00" + +[node name="Max" type="Label" parent="TimecodeInfo/TimeReadout"] +margin_left = 98.0 +margin_right = 127.0 +margin_bottom = 13.0 +custom_fonts/font = SubResource( 2 ) +text = "/00:00" + +[node name="FrameReadout" type="HBoxContainer" parent="TimecodeInfo"] +margin_top = 17.0 +margin_right = 146.0 +margin_bottom = 30.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="TimecodeInfo/FrameReadout"] +margin_right = 71.0 +margin_bottom = 13.0 +custom_fonts/font = SubResource( 2 ) +text = "Current frame: " + +[node name="Readout" type="Label" parent="TimecodeInfo/FrameReadout"] +margin_left = 71.0 +margin_right = 107.0 +margin_bottom = 13.0 +custom_fonts/font = SubResource( 2 ) +text = "000000" + +[node name="Max" type="Label" parent="TimecodeInfo/FrameReadout"] +margin_left = 107.0 +margin_right = 146.0 +margin_bottom = 13.0 +custom_fonts/font = SubResource( 2 ) +text = "/000000" + +[connection signal="pressed" from="VBoxContainer/MediaControls/Restart" to="." method="restart"] +[connection signal="pressed" from="VBoxContainer/MediaControls/10SecondsBack" to="." method="go_back"] +[connection signal="pressed" from="VBoxContainer/MediaControls/1FrameBack" to="." method="go_back_1"] +[connection signal="pressed" from="VBoxContainer/MediaControls/PauseAndPlay" to="." method="pause_unpause"] +[connection signal="pressed" from="VBoxContainer/MediaControls/1FrameForward" to="." method="go_forward_1"] +[connection signal="pressed" from="VBoxContainer/MediaControls/10SecondsForward" to="." method="go_forward"] +[connection signal="pressed" from="VBoxContainer/MediaControls/ToEnd" to="." method="go_to_end"] diff --git a/main.gd b/main.gd index a31fc8d..5cbb791 100644 --- a/main.gd +++ b/main.gd @@ -189,7 +189,7 @@ func _on_bt_amasterload_pressed(): ### Replay demo func _on_bt_replaydemo_pressed() -> void: - open_scene("res://demos/replaydemo/Replay Viewer.tscn") + open_scene("res://demos/replaydemo/replayviewer.tscn") func _on_bt_quit_pressed() -> void: diff --git a/main.tscn b/main.tscn index 8ff6af3..02a7252 100644 --- a/main.tscn +++ b/main.tscn @@ -593,7 +593,7 @@ margin_top = 46.0 margin_right = -9.99994 margin_bottom = -20.0 text = "Interdependency: none -This addon is meant to provide a few scripts to help the debugging process. +This addon is meant to provide a few scripts to help the debugging process. Brief feature overview: - overlayinfo.gd: Quickly add text labels on screen without the need to create temporary UI controls all over the place. It also allows timed labels to be shown for the specified amount of seconds. Labels are added/removed into/from a container box that expands/shrinks according to the contents. @@ -695,6 +695,7 @@ margin_top = 46.0 margin_right = -9.99994 margin_bottom = -20.0 text = "Interdependency: Network addon, EncDecBuffer +This addon is meant to provide an easy way of recording and viewing gameplay at a later time for users of the Network addon. This demo provides a way to view replays of gameplay recorded in the 'mega' demo. If enabled, whenever a player manually disconnects from the mega demo, a .REPLAY file is saved to the specified file location. The demo itself is a dedicated replay viewing client, with traditional multimedia controls: From 289d4583bb9dbc4c797db42554f976446fb5fb59 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Mon, 3 Oct 2022 16:18:27 -0400 Subject: [PATCH 08/11] gaming main.gd: replayviewer filepath fixed replayviewer.gd and .tscn: general updates megamain.gd: replay instance variable, records replays if project settings say so replay.gd: has scene_path instance variable (serializes it when saved), added functions to check if a snapshot is a full snapshot based on its relative signature, and to check if a specific snapshot from a replay is a snapshot. also added some helper funcs for accessing the "status" of a replay. (How 'long' a replay is, how far through the replay a given frame index is). made the replay an actual plugin --- addons/exploding_replays/plugin.cfg | 7 + addons/exploding_replays/pluginloader.gd | 81 +++++++++ addons/exploding_replays/replay.gd | 57 ++++++- demos/mega/megamain.gd | 25 ++- demos/replays/replayviewer.gd | 168 ++++++++++-------- demos/replays/replayviewer.tscn | 207 ++++++++++++++++++++--- main.gd | 2 +- project.godot | 2 +- 8 files changed, 442 insertions(+), 107 deletions(-) create mode 100644 addons/exploding_replays/plugin.cfg create mode 100644 addons/exploding_replays/pluginloader.gd diff --git a/addons/exploding_replays/plugin.cfg b/addons/exploding_replays/plugin.cfg new file mode 100644 index 0000000..ba63c46 --- /dev/null +++ b/addons/exploding_replays/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="Network Replays" +description="idk" +author="Miles Mazzotta" +version="0.1" +script="pluginloader.gd" diff --git a/addons/exploding_replays/pluginloader.gd b/addons/exploding_replays/pluginloader.gd new file mode 100644 index 0000000..3fe17ac --- /dev/null +++ b/addons/exploding_replays/pluginloader.gd @@ -0,0 +1,81 @@ +############################################################################### +# Copyright (c) 2022 Miles Mazzotta +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +############################################################################### + +tool +extends EditorPlugin + +const base_path: String = Replay.explodingreplays + +var _extra_settings: Array = [] + + +# This will be called by the engine whenever the plugin is activated +func enable_plugin() -> void: + pass + +# This will be called by the engine whenever the plugin is deactivated +func disable_plugin() -> void: + pass + + # Remove the additional project settings - those will remain on the ProjectSettings window until + # the editor is restarted + for es in _extra_settings: + ProjectSettings.clear(es) + + _extra_settings.clear() + + +func _enter_tree(): + _reg_setting(Replay.recsetting, TYPE_BOOL, false) + _reg_setting(Replay.capratesetting, TYPE_INT, 30) + _reg_setting(Replay.fullratesetting, TYPE_INT, 30) + _reg_setting(Replay.defaultdiresetting, TYPE_STRING, Replay.default_save_path) + + +func _exit_tree() -> void: + pass + + + +# def_val is relying on the variant, thus no static typing +func _reg_setting(sname: String, type: int, def_val, info: Dictionary = {}) -> void: + var fpath: String = base_path + sname + if (!ProjectSettings.has_setting(fpath)): + ProjectSettings.set(fpath, def_val) + + _extra_settings.append(fpath) + + # Those must be done regardless if the setting existed before or not, otherwise the ProjectSettings window + # will not work correctly (yeah, the default value as well as the hints must be provided) + ProjectSettings.set_initial_value(fpath, def_val) + + var propinfo: Dictionary = { + "name": fpath, + "type": type + } + if (info.has("hint")): + propinfo["hint"] = info.hint + if (info.has("hint_string")): + propinfo["hint_string"] = info.hint_string + + ProjectSettings.add_property_info(propinfo) + diff --git a/addons/exploding_replays/replay.gd b/addons/exploding_replays/replay.gd index b6f1a2e..0d1eb18 100644 --- a/addons/exploding_replays/replay.gd +++ b/addons/exploding_replays/replay.gd @@ -27,23 +27,34 @@ var _history: Array var _tickrate: int var _full_snapshot_tickrate: int var edec: EncDecBuffer = get_net_edec() +var _scene_path: String -func _init(tickrate: int,full_snapshot_tickrate: int) -> void: - setup(tickrate,full_snapshot_tickrate) +func _init(tickrate: int,full_snapshot_tickrate: int,scene_path: String) -> void: + setup(tickrate,full_snapshot_tickrate, scene_path) -func setup(tickrate: int,full_snapshot_tickrate: int) -> void: +func setup(tickrate: int,full_snapshot_tickrate: int, scene_path: String) -> void: _tickrate = tickrate _full_snapshot_tickrate = full_snapshot_tickrate + _scene_path = scene_path func add_snapshot(snapshot: NetSnapshot) -> void: _history.append(snapshot) +func is_full_snapshot(snapshot: NetSnapshot) -> bool: + return snapshot.signature%_full_snapshot_tickrate == 0 + +func get_snapshot_is_full(idx: int) -> bool: + return is_full_snapshot(get_snapshot(idx)) + +func get_snapshot(idx: int) -> NetSnapshot: + return _history[idx] + func encode_snapshot(snapshot: NetSnapshot) -> PoolByteArray: # There is no practical reason to cache this as a local variable. # This is simply here to make some of the lower lines of code shorter. var sd: NetSnapshotData = network.snapshot_data edec.buffer = PoolByteArray() - if snapshot.signature == 0: + if is_full_snapshot(snapshot): sd.encode_full(snapshot,edec,snapshot.input_sig) else: sd.encode_delta(snapshot,sd._history[-1],edec,snapshot.input_sig) @@ -75,6 +86,7 @@ func save_and_reset(name: String, directory: String) -> void: save(name,directory) reset() +# Maybe rename to denote that it's related to files func load_replay(filepath: String) -> void: deserialize(read_compressed_replay_file(filepath)) # Could totally just be this instead: @@ -87,8 +99,11 @@ func load_replay(filepath: String) -> void: func deserialize(serialized: Array) -> void: assert_enumerated_array_correct(serialized) assert(_history.empty()) - setup(serialized[TICKRATE],serialized[FULL_SNAPSHOT_TICKRATE]) - for s_snap in serialized[HISTORY]: + setup(serialized[TICKRATE],serialized[FULL_SNAPSHOT_TICKRATE],serialized[SCENE_PATH]) + deserialize_history(serialized[HISTORY]) + +func deserialize_history(serialized_history: Array) -> void: + for s_snap in serialized_history: assert(s_snap is PoolByteArray) edec.buffer = s_snap if _history.empty(): @@ -96,6 +111,17 @@ func deserialize(serialized: Array) -> void: else: _history.append(network.snapshot_data.decode_delta(edec)) +func get_current_time_unix(idx: int) -> int: + return idx/_tickrate + +func get_current_time_as_string(idx: int) -> String: + return Time.get_time_string_from_unix_time(get_current_time_unix(idx)) + +func get_total_time_unix() -> int: + return (_history.size()-1)/_tickrate + +func get_total_time_as_string() -> String: + return Time.get_time_string_from_unix_time(get_total_time_unix()) @@ -108,6 +134,19 @@ static func get_net_buffer() -> PoolByteArray: return network._update_control.edec.buffer const default_save_path: String = "user://replays/" +# over-engineering stuff for fun +const explodingreplays = "exploding_addons/replays/" +const recsetting = "record_replays" +const capratesetting = "capture_rate" +const fullratesetting = "full_snapshot_capture_rate" +const defaultdiresetting = "default_replay_directory" + +static func get_default_directory() -> String: + if ProjectSettings.has_setting(explodingreplays+defaultdiresetting): + return ProjectSettings.get_setting(explodingreplays+defaultdiresetting) + else: + return default_save_path + static func convert_to_snapshots(replay: Array, buffer: EncDecBuffer) -> Array: assert_enumerated_array_correct(replay) @@ -115,16 +154,16 @@ static func convert_to_snapshots(replay: Array, buffer: EncDecBuffer) -> Array: for idx in history.size(): assert(history[idx] is PoolByteArray) buffer.buffer = history[idx] - if idx == 0: + if idx%replay[FULL_SNAPSHOT_TICKRATE] == 0: history[idx] = network.snapshot_data.decode_full(buffer) else: history[idx] = network.snapshot_data.decode_delta(buffer) # doesn't need to return necessarily, this func operates over the actual array itself return replay -enum {TICKRATE,FULL_SNAPSHOT_TICKRATE,HISTORY,REPLAY_MAX} +enum {TICKRATE,FULL_SNAPSHOT_TICKRATE,SCENE_PATH,HISTORY,REPLAY_MAX} static func to_enumerated_array(replay: Replay) -> Array: - return [replay._tickrate,replay._full_snapshot_tickrate,replay._history] + return [replay._tickrate,replay._full_snapshot_tickrate,replay._scene_path,replay._history] static func assert_enumerated_array_correct(array: Array) -> void: assert(array.size() == REPLAY_MAX) diff --git a/demos/mega/megamain.gd b/demos/mega/megamain.gd index 7b23b07..893daf5 100644 --- a/demos/mega/megamain.gd +++ b/demos/mega/megamain.gd @@ -46,6 +46,7 @@ var _ui_player: Dictionary = {} # then this property will be changed. var _disconnected_message: String +var _replay: Replay func _ready() -> void: @@ -78,8 +79,24 @@ func _ready() -> void: # this way if (!network.has_authority()): network.notify_ready() - - + var record_replays: bool + var replay_capture_rate: int + var replay_full_capture_rate: int + if ProjectSettings.has_setting(Replay.explodingreplays + Replay.recsetting): + record_replays = ProjectSettings.get_setting(Replay.recsetting) + else: + return + if ProjectSettings.has_setting(Replay.explodingreplays + Replay.capratesetting): + replay_capture_rate = ProjectSettings.get_setting(Replay.capratesetting) + else: + record_replays = false + return + if ProjectSettings.has_setting(Replay.explodingreplays + Replay.fullratesetting): + replay_full_capture_rate = ProjectSettings.get_setting(Replay.fullratesetting) + else: + record_replays = false + return + _replay = Replay.new(replay_capture_rate,replay_full_capture_rate,"res://demos/mega/megamain.tscn") func _exit_tree() -> void: # Hide the OverlayDebugInfo @@ -121,6 +138,8 @@ func _physics_process(_dt: float) -> void: # Then each of the connected players - in this case, clients for pid in network.player_data.remote_player: create_player_character(network.player_data.remote_player[pid]) + if _replay: + _replay.add_snapshot(network.snapshot_data._history[-1]) # Owned custom property and own network ID var owned_cprop: float = network.player_data.local_player.get_custom_property("testing_broadcast") @@ -147,6 +166,8 @@ func _input(evt: InputEvent) -> void: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) # Go back to the main menu # warning-ignore:return_value_discarded + if _replay: + _replay.save(Replay.default_file("Replay"),Replay.get_default_directory()) get_tree().change_scene("res://main.tscn") KEY_F10: diff --git a/demos/replays/replayviewer.gd b/demos/replays/replayviewer.gd index 61b7a6d..73d723a 100644 --- a/demos/replays/replayviewer.gd +++ b/demos/replays/replayviewer.gd @@ -4,28 +4,36 @@ var is_playing: bool var tenseconds: int var last_snap: NetSnapshot var replay: Replay +var gameworld: Spatial onready var c: RichTextLabel = $Panel/C onready var files: FileDialog = $FileDialog onready var replayinfo: Label = $"Replay Info" onready var replaychanger: Button = $newreplay -onready var timeline: HScrollBar = $VBoxContainer/Timeline +onready var timeline: HSlider = $VBoxContainer/Timeline onready var playbackspeed: SpinBox = $TimeScale onready var viewport: Viewport = $CenterContainer/ViewportContainer/Viewport onready var timereadout: Label = $TimecodeInfo/TimeReadout/Readout +onready var maxtime: Label = $TimecodeInfo/TimeReadout/Max onready var tickreadout: Label = $TimecodeInfo/FrameReadout/Readout +onready var maxticks: Label = $TimecodeInfo/FrameReadout/Max onready var fpsreadout: Label = $FPS/Readout -onready var playpauseicon: Button = $VBoxContainer/MediaControls/PauseAndPlayMediaControls/PauseAndPlay +onready var playpause: Button = $VBoxContainer/MediaControls/PauseAndPlay +onready var tickrate: Label = $ReplayInfo/Tickrate/Readout +onready var fullrate: Label = $ReplayInfo/FullSnapshotRate/Readout var playicon: Texture = preload("res://addons/keh_gddb/editor/btplay_16x16.png") var pauseicon: Texture = preload("res://addons/keh_gddb/editor/btpause_16x16.png") func assign_icon() -> void: - playpauseicon.set_button_icon(get_icon_from_is_playing()) + playpause.set_button_icon(get_icon_from_is_playing()) func get_icon_from_is_playing() -> Texture: return playicon if is_playing else pauseicon +const defaultreplayfolder: String = "user://replays" func _ready() -> void: + Replay.make_dir_if_doesnt_exist(defaultreplayfolder) + files.set_current_dir(defaultreplayfolder) files.call_deferred("invalidate") change_replay() @@ -77,11 +85,7 @@ func _input(event: InputEvent) -> void: OS.vsync_enabled = !OS.vsync_enabled KEY_F4: - # Restore mouse visibility - Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) - # Go back to the main menu - # warning-ignore:return_value_discarded - get_tree().change_scene("res://main.tscn") + goto_main_menu() KEY_F10: OverlayDebugInfo.toggle_visibility() @@ -97,39 +101,77 @@ func _input(event: InputEvent) -> void: # It's captured, so show it Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) -#static func get_time_from_ticknum(ticknum: int, tickrate: int) -> String: -# return History.get_replay_length_from_tick_count(ticknum, tickrate) - -func tick_feed(value: int) -> void: - pass - func simulate_up_to_current_snapshot(this_snap: NetSnapshot) -> void: - pass + var remainder: int = this_snap.signature%replay._tickrate+1 + var last_full_snap_idx: int = this_snap.signature + for i in remainder: + assert(last_snap.signature + i != this_snap.signature) + update_entities(replay.get_snapshot(last_full_snap_idx+i)) func change_replay() -> void: files.show() # setup subwindow size (godot 4 quack func) func clear_replay() -> void: - pass - -func read_replay() -> void: - pass + pause_if_playing() + if gameworld: + gameworld.queue_free() + gameworld = null + last_snap = null + +# Maybe rename to denote that it's related to files +static func load_new_replay(filepath: String) -> Replay: + var serialized: Array = Replay.read_compressed_replay_file(filepath) + Replay.assert_enumerated_array_correct(serialized) + var ret = Replay.new(serialized[Replay.TICKRATE],serialized[Replay.FULL_SNAPSHOT_TICKRATE],serialized[Replay.SCENE_PATH]) + ret.deserialize_history(serialized[Replay.HISTORY]) + return ret + +func read_replay(filepath: String) -> void: + clear_replay() + if replay: + replay = load_new_replay(filepath) + else: + replay.load_replay(filepath) + change_playback_speed(playbackspeed.value) + setup_timeline() + tenseconds = replay._tickratre * 10 + setup_game_scene() + +func setup_game_scene() -> void: + var scene: Resource = load(replay._scene_path) + assert(scene is PackedScene) + gameworld = scene.instance() + assert(gameworld is Spatial) + viewport.add_child(gameworld) + call_deferred("set_physics_process_recursive",gameworld,false) func setup_timeline() -> void: - pass - -func on_timeline_ticked(value: String) -> void: - pass + timeline.set_min(0) + timeline.set_max(replay._history.size()-1) + +func on_timeline_ticked(value: int) -> void: + var snapshot: NetSnapshot = replay.get_snapshot(value) + if !replay.is_full_snapshot(snapshot) and (!last_snap or last_snap.signature != snapshot.signature - 1): + simulate_up_to_current_snapshot(snapshot) + update_entities(snapshot) + last_snap = snapshot + timereadout.set_text(replay.get_current_time_as_string(value)) + tickreadout.set_text(str(value)) + +func update_entities(snapshot: NetSnapshot) -> void: + var entity_data: Dictionary = snapshot._entity_data + for entity_type in entity_data.values(): + assert(entity_type is Dictionary) + for entity in entity_type.values(): + assert(entity is SnapEntityBase) + entity.apply_state(network.snapshot_data._get_entity_info(entity.get_script()).get_game_node(entity.id)) func set_replay_info() -> void: - pass -# replayinfo.set_text("tickrate: %s | SS tickrate: %s | size: %s | length %s\nmap: %s | mode: %s"%[replay.tickrate, -# replay.snapshot_tickrate, -# replay.history.size(), -# get_time_from_ticknum(replay.history.size(),replay.tickrate), -# Map.map_name_from_path(replay.map_file_path), -# Gamemodes.get_gamemode_name(replay.gamemode)]) + tickrate.set_text(str(replay._tickrate)) + fullrate.set_text(str(replay._full_snapshot_tickrate)) + maxtime.set_text(replay.get_total_time_as_string()) + maxticks.set_text(str(replay._history.size()-1)) func restart() -> void: timeline.set_value(0) @@ -153,21 +195,30 @@ func go_to_end() -> void: pause_if_playing() func pause_unpause() -> void: - is_playing = !is_playing - assign_icon() + if replay: + pause_or_unpause(!is_playing) func play() -> void: - is_playing = true - assign_icon() + pause_or_unpause(true) func pause() -> void: - is_playing = false - assign_icon() + pause_or_unpause(false) func on_left_pressed() -> void: go_back_1() pause_if_playing() +func pause_or_unpause(playing: bool) -> void: + is_playing = playing + apply_pause_unpaused_differences() + +func apply_pause_unpaused_differences() -> void: +# apply_physics_processing_if_playing() + assign_icon() + +func apply_physics_processing_if_playing() -> void: + set_physics_process_recursive(gameworld,is_playing) + func get_playback_speed() -> int: return int(replay.tickrate * playbackspeed.get_value()) @@ -176,46 +227,31 @@ func on_right_pressed() -> void: pause_if_playing() func change_playback_speed(value: float) -> void: - pass -# Quack.set_tickrate(replay.tickrate * value) + Engine.set_iterations_per_second(replay._tickrate * value) func goto_main_menu() -> void: - pass -# Quack.change_scene("res://Interface/Menus/Main Menu.tscn") + # Restore mouse visibility + Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) + # Go back to the main menu + # warning-ignore:return_value_discarded + get_tree().change_scene("res://main.tscn") func go_forward() -> void: move_by_amnt(tenseconds) func go_back() -> void: move_by_amnt(-tenseconds) - - -func clear_gameplay_if_loaded() -> void: - pass -# if map_is_loaded(): -# Network.get_entities().clear() -# Network.clear_map() - -func map_is_loaded() -> bool: - pass - return Network.map != null - -func setup_map() -> void: - pass -# Network.map = load(replay.map_file_path).instantiate() -# viewport.add_child(Network.map) -# Network.setup_entity_groups() -# last_tick = [] - - -func on_tree_exiting() -> void: - pass -# clear_gameplay_if_loaded() func on_timeline_scrolled() -> void: pass pause_if_playing() - -func Pause() -> void: - pass # Replace with function body. +static func set_physics_process_recursive(node: Node, enabled: bool) -> void: + if !node.get_script(): + if enabled == false: + assert(!node.is_physics_processing()) + else: + node.set_physics_process(enabled) + if node.get_child_count() > 0: + for child in node.get_children(): + set_physics_process_recursive(child,enabled) diff --git a/demos/replays/replayviewer.tscn b/demos/replays/replayviewer.tscn index 9926923..62fafa6 100644 --- a/demos/replays/replayviewer.tscn +++ b/demos/replays/replayviewer.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=7 format=2] +[gd_scene load_steps=8 format=2] [ext_resource path="res://addons/keh_gddb/editor/btplay_16x16.png" type="Texture" id=1] [ext_resource path="res://shared/fonts/Aileron-Bold.otf" type="DynamicFontData" id=2] @@ -10,7 +10,11 @@ size = 14 font_data = ExtResource( 2 ) [sub_resource type="DynamicFont" id=2] -size = 10 +size = 8 +font_data = ExtResource( 2 ) + +[sub_resource type="DynamicFont" id=3] +size = 24 font_data = ExtResource( 2 ) [node name="Replay Viewer" type="Panel"] @@ -85,8 +89,8 @@ __meta__ = { "_editor_description_": "" } -[node name="Timeline" type="HSlider" parent="VBoxContainer"] -margin_top = 6.0 +[node name="Timeline" type="HScrollBar" parent="VBoxContainer"] +margin_top = 10.0 margin_right = 768.0 margin_bottom = 22.0 rounded = true @@ -163,61 +167,205 @@ text = "->]" icon_align = 1 [node name="TimecodeInfo" type="VBoxContainer" parent="."] -margin_top = 320.0 -margin_right = 146.0 -margin_bottom = 350.0 +margin_left = 8.0 +margin_top = 520.0 +margin_right = 130.0 +margin_bottom = 550.0 +__meta__ = { +"_edit_group_": true +} [node name="TimeReadout" type="HBoxContainer" parent="TimecodeInfo"] -margin_right = 146.0 -margin_bottom = 13.0 +margin_right = 122.0 +margin_bottom = 10.0 custom_constants/separation = 0 [node name="Label" type="Label" parent="TimecodeInfo/TimeReadout"] -margin_right = 72.0 -margin_bottom = 13.0 +margin_right = 62.0 +margin_bottom = 10.0 custom_fonts/font = SubResource( 2 ) text = "Timecode: " [node name="Readout" type="Label" parent="TimecodeInfo/TimeReadout"] -margin_left = 72.0 -margin_right = 98.0 -margin_bottom = 13.0 +margin_left = 62.0 +margin_right = 84.0 +margin_bottom = 10.0 custom_fonts/font = SubResource( 2 ) text = "00:00" [node name="Max" type="Label" parent="TimecodeInfo/TimeReadout"] -margin_left = 98.0 -margin_right = 127.0 -margin_bottom = 13.0 +margin_left = 84.0 +margin_right = 109.0 +margin_bottom = 10.0 custom_fonts/font = SubResource( 2 ) text = "/00:00" [node name="FrameReadout" type="HBoxContainer" parent="TimecodeInfo"] -margin_top = 17.0 -margin_right = 146.0 -margin_bottom = 30.0 +margin_top = 14.0 +margin_right = 122.0 +margin_bottom = 24.0 custom_constants/separation = 0 [node name="Label" type="Label" parent="TimecodeInfo/FrameReadout"] -margin_right = 71.0 -margin_bottom = 13.0 +margin_right = 59.0 +margin_bottom = 10.0 custom_fonts/font = SubResource( 2 ) text = "Current frame: " [node name="Readout" type="Label" parent="TimecodeInfo/FrameReadout"] -margin_left = 71.0 -margin_right = 107.0 -margin_bottom = 13.0 +margin_left = 59.0 +margin_right = 89.0 +margin_bottom = 10.0 custom_fonts/font = SubResource( 2 ) text = "000000" [node name="Max" type="Label" parent="TimecodeInfo/FrameReadout"] -margin_left = 107.0 -margin_right = 146.0 -margin_bottom = 13.0 +margin_left = 89.0 +margin_right = 122.0 +margin_bottom = 10.0 custom_fonts/font = SubResource( 2 ) text = "/000000" +[node name="ReplayInfo" type="VBoxContainer" parent="."] +margin_left = 8.0 +margin_top = 88.0 +margin_right = 130.0 +margin_bottom = 118.0 +__meta__ = { +"_edit_group_": true +} + +[node name="Tickrate" type="HBoxContainer" parent="ReplayInfo"] +margin_right = 128.0 +margin_bottom = 10.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="ReplayInfo/Tickrate"] +margin_right = 36.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "Tickrate: " + +[node name="Readout" type="Label" parent="ReplayInfo/Tickrate"] +margin_left = 36.0 +margin_right = 46.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "30" + +[node name="FullSnapshotRate" type="HBoxContainer" parent="ReplayInfo"] +margin_top = 14.0 +margin_right = 128.0 +margin_bottom = 24.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="ReplayInfo/FullSnapshotRate"] +margin_right = 75.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "Full Snapshot rate: " + +[node name="Readout" type="Label" parent="ReplayInfo/FullSnapshotRate"] +margin_left = 75.0 +margin_right = 128.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "30 (1 second)" + +[node name="Scene" type="HBoxContainer" parent="ReplayInfo"] +margin_top = 28.0 +margin_right = 128.0 +margin_bottom = 38.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="ReplayInfo/Scene"] +margin_right = 28.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "Scene: " + +[node name="Readout" type="Label" parent="ReplayInfo/Scene"] +margin_left = 28.0 +margin_right = 68.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "mega.tscn" + +[node name="GameInfo" type="VBoxContainer" parent="."] +visible = false +margin_left = 8.0 +margin_top = 120.0 +margin_right = 136.0 +margin_bottom = 150.0 +__meta__ = { +"_edit_group_": true +} + +[node name="Tickrate" type="HBoxContainer" parent="GameInfo"] +margin_right = 128.0 +margin_bottom = 10.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="GameInfo/Tickrate"] +margin_right = 36.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "Tickrate: " + +[node name="Readout" type="Label" parent="GameInfo/Tickrate"] +margin_left = 36.0 +margin_right = 46.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "30" + +[node name="FullSnapshotRate" type="HBoxContainer" parent="GameInfo"] +margin_top = 14.0 +margin_right = 128.0 +margin_bottom = 24.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="GameInfo/FullSnapshotRate"] +margin_right = 75.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "Full Snapshot rate: " + +[node name="Readout" type="Label" parent="GameInfo/FullSnapshotRate"] +margin_left = 75.0 +margin_right = 128.0 +margin_bottom = 10.0 +custom_fonts/font = SubResource( 2 ) +text = "30 (1 second)" + +[node name="ChangeReplay" type="Button" parent="."] +margin_left = 185.0 +margin_right = 370.0 +margin_bottom = 36.0 +custom_fonts/font = SubResource( 3 ) +text = "Change Replay" + +[node name="MainMenu" type="Button" parent="."] +margin_right = 185.0 +margin_bottom = 36.0 +custom_fonts/font = SubResource( 3 ) +text = "Main Menu" + +[node name="FileDialog" type="FileDialog" parent="."] +visible = true +margin_left = 152.0 +margin_top = 96.0 +margin_right = 728.0 +margin_bottom = 488.0 +popup_exclusive = true +window_title = "Select Replay" +resizable = true +mode = 0 +access = 2 +filters = PoolStringArray( ".REPLAY" ) + +[connection signal="scrolling" from="VBoxContainer/Timeline" to="." method="on_timeline_scrolled"] +[connection signal="value_changed" from="VBoxContainer/Timeline" to="." method="on_timeline_ticked"] [connection signal="pressed" from="VBoxContainer/MediaControls/Restart" to="." method="restart"] [connection signal="pressed" from="VBoxContainer/MediaControls/10SecondsBack" to="." method="go_back"] [connection signal="pressed" from="VBoxContainer/MediaControls/1FrameBack" to="." method="go_back_1"] @@ -225,3 +373,6 @@ text = "/000000" [connection signal="pressed" from="VBoxContainer/MediaControls/1FrameForward" to="." method="go_forward_1"] [connection signal="pressed" from="VBoxContainer/MediaControls/10SecondsForward" to="." method="go_forward"] [connection signal="pressed" from="VBoxContainer/MediaControls/ToEnd" to="." method="go_to_end"] +[connection signal="pressed" from="ChangeReplay" to="." method="change_replay"] +[connection signal="pressed" from="MainMenu" to="." method="goto_main_menu"] +[connection signal="file_selected" from="FileDialog" to="." method="read_replay"] diff --git a/main.gd b/main.gd index 5cbb791..ecb2371 100644 --- a/main.gd +++ b/main.gd @@ -189,7 +189,7 @@ func _on_bt_amasterload_pressed(): ### Replay demo func _on_bt_replaydemo_pressed() -> void: - open_scene("res://demos/replaydemo/replayviewer.tscn") + open_scene("res://demos/replays/replayviewer.tscn") func _on_bt_quit_pressed() -> void: diff --git a/project.godot b/project.godot index 40b67d6..37e26b5 100644 --- a/project.godot +++ b/project.godot @@ -380,7 +380,7 @@ gdscript/warnings/unused_class_variable=true [editor_plugins] -enabled=PoolStringArray( "keh_audiomaster", "keh_dataasset", "keh_dbghelper", "keh_gddb", "keh_network", "keh_smooth", "keh_ui" ) +enabled=PoolStringArray( "res://addons/exploding_replays/plugin.cfg", "res://addons/keh_audiomaster/plugin.cfg", "res://addons/keh_dataasset/plugin.cfg", "res://addons/keh_dbghelper/plugin.cfg", "res://addons/keh_gddb/plugin.cfg", "res://addons/keh_network/plugin.cfg", "res://addons/keh_smooth/plugin.cfg", "res://addons/keh_ui/plugin.cfg" ) [global] From a6ee4ea6412cf0960d7c089e637e0f28fe258383 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Mon, 3 Oct 2022 16:35:52 -0400 Subject: [PATCH 09/11] updoots --- addons/exploding_replays/replay.gd | 4 ++++ demos/mega/megamain.gd | 17 ++++++++++++----- project.godot | 4 ++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/addons/exploding_replays/replay.gd b/addons/exploding_replays/replay.gd index 0d1eb18..4ab9b8c 100644 --- a/addons/exploding_replays/replay.gd +++ b/addons/exploding_replays/replay.gd @@ -70,13 +70,17 @@ func reset() -> void: # full_snapshot_tickrate = 0 func save(name: String, directory: String) -> void: + print("Attempting to save replay...") if !_history.empty(): + print("Generating serialized history...") var newarray: Array var temp: Array = _history newarray.resize(_history.size()) for i in newarray.size(): newarray[i] = encode_snapshot(_history[i]) + print("Serialized history encoded and stored.") _history = newarray + print("Attempting to write replay to disk...") save_compressed(File.new(),self,name,directory) _history = temp else: diff --git a/demos/mega/megamain.gd b/demos/mega/megamain.gd index 893daf5..9d4e6fb 100644 --- a/demos/mega/megamain.gd +++ b/demos/mega/megamain.gd @@ -83,22 +83,31 @@ func _ready() -> void: var replay_capture_rate: int var replay_full_capture_rate: int if ProjectSettings.has_setting(Replay.explodingreplays + Replay.recsetting): - record_replays = ProjectSettings.get_setting(Replay.recsetting) + record_replays = ProjectSettings.get_setting(Replay.explodingreplays + Replay.recsetting) else: + printsettingmsg(Replay.recsetting) return if ProjectSettings.has_setting(Replay.explodingreplays + Replay.capratesetting): - replay_capture_rate = ProjectSettings.get_setting(Replay.capratesetting) + replay_capture_rate = ProjectSettings.get_setting(Replay.explodingreplays + Replay.capratesetting) else: + printsettingmsg(Replay.capratesetting) record_replays = false return if ProjectSettings.has_setting(Replay.explodingreplays + Replay.fullratesetting): - replay_full_capture_rate = ProjectSettings.get_setting(Replay.fullratesetting) + replay_full_capture_rate = ProjectSettings.get_setting(Replay.explodingreplays + Replay.fullratesetting) else: + printsettingmsg(Replay.fullratesetting) record_replays = false return + print("Recording replay of this session.") _replay = Replay.new(replay_capture_rate,replay_full_capture_rate,"res://demos/mega/megamain.tscn") +static func printsettingmsg(msg: String) -> void: + print('Cannot record replay! Cannot access %s setting'%[msg]) + func _exit_tree() -> void: + if _replay: + _replay.save(Replay.default_file("Replay"),Replay.get_default_directory()) # Hide the OverlayDebugInfo OverlayDebugInfo.set_visibility(false) @@ -166,8 +175,6 @@ func _input(evt: InputEvent) -> void: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) # Go back to the main menu # warning-ignore:return_value_discarded - if _replay: - _replay.save(Replay.default_file("Replay"),Replay.get_default_directory()) get_tree().change_scene("res://main.tscn") KEY_F10: diff --git a/project.godot b/project.godot index 37e26b5..eb7da96 100644 --- a/project.godot +++ b/project.godot @@ -382,6 +382,10 @@ gdscript/warnings/unused_class_variable=true enabled=PoolStringArray( "res://addons/exploding_replays/plugin.cfg", "res://addons/keh_audiomaster/plugin.cfg", "res://addons/keh_dataasset/plugin.cfg", "res://addons/keh_dbghelper/plugin.cfg", "res://addons/keh_gddb/plugin.cfg", "res://addons/keh_network/plugin.cfg", "res://addons/keh_smooth/plugin.cfg", "res://addons/keh_ui/plugin.cfg" ) +[exploding_addons] + +replays/record_replays=true + [global] warning=true From 15ec5173e8555e1886fab7df0f818325f9acc363 Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Mon, 3 Oct 2022 16:49:12 -0400 Subject: [PATCH 10/11] more stuff reading a serialized replay gets up to build_tracker after replayviewer.gd calls read_replay --> load_new_replay --> deserialize_history --> decode_delta --- demos/mega/megamain.gd | 7 +++++-- demos/replays/replayviewer.gd | 6 +++--- demos/replays/replayviewer.tscn | 3 ++- project.godot | 2 ++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/demos/mega/megamain.gd b/demos/mega/megamain.gd index 9d4e6fb..282f527 100644 --- a/demos/mega/megamain.gd +++ b/demos/mega/megamain.gd @@ -101,6 +101,8 @@ func _ready() -> void: return print("Recording replay of this session.") _replay = Replay.new(replay_capture_rate,replay_full_capture_rate,"res://demos/mega/megamain.tscn") +# if !get_tree().has_network_peer(): +# pass static func printsettingmsg(msg: String) -> void: print('Cannot record replay! Cannot access %s setting'%[msg]) @@ -148,7 +150,7 @@ func _physics_process(_dt: float) -> void: for pid in network.player_data.remote_player: create_player_character(network.player_data.remote_player[pid]) if _replay: - _replay.add_snapshot(network.snapshot_data._history[-1]) + call_deferred("add_most_recent_snapshot_to_replay") # Owned custom property and own network ID var owned_cprop: float = network.player_data.local_player.get_custom_property("testing_broadcast") @@ -160,7 +162,8 @@ func _physics_process(_dt: float) -> void: var cprop: float = network.player_data.remote_player[pid].get_custom_property("testing_broadcast") OverlayDebugInfo.set_label("test_broad%s" % pid, "Custom Value (%s): %s" % [pid, cprop]) - +func add_most_recent_snapshot_to_replay() -> void: + _replay.add_snapshot(network.snapshot_data._history[-1]) # Provide means to get back to the main menu func _input(evt: InputEvent) -> void: diff --git a/demos/replays/replayviewer.gd b/demos/replays/replayviewer.gd index 73d723a..d27a02a 100644 --- a/demos/replays/replayviewer.gd +++ b/demos/replays/replayviewer.gd @@ -9,7 +9,7 @@ onready var c: RichTextLabel = $Panel/C onready var files: FileDialog = $FileDialog onready var replayinfo: Label = $"Replay Info" onready var replaychanger: Button = $newreplay -onready var timeline: HSlider = $VBoxContainer/Timeline +onready var timeline: HScrollBar = $VBoxContainer/Timeline onready var playbackspeed: SpinBox = $TimeScale onready var viewport: Viewport = $CenterContainer/ViewportContainer/Viewport onready var timereadout: Label = $TimecodeInfo/TimeReadout/Readout @@ -130,9 +130,9 @@ static func load_new_replay(filepath: String) -> Replay: func read_replay(filepath: String) -> void: clear_replay() if replay: - replay = load_new_replay(filepath) - else: replay.load_replay(filepath) + else: + replay = load_new_replay(filepath) change_playback_speed(playbackspeed.value) setup_timeline() tenseconds = replay._tickratre * 10 diff --git a/demos/replays/replayviewer.tscn b/demos/replays/replayviewer.tscn index 62fafa6..b600fbd 100644 --- a/demos/replays/replayviewer.tscn +++ b/demos/replays/replayviewer.tscn @@ -93,6 +93,7 @@ __meta__ = { margin_top = 10.0 margin_right = 768.0 margin_bottom = 22.0 +max_value = 0.0 rounded = true [node name="MediaControls" type="HBoxContainer" parent="VBoxContainer"] @@ -358,7 +359,7 @@ margin_top = 96.0 margin_right = 728.0 margin_bottom = 488.0 popup_exclusive = true -window_title = "Select Replay" +window_title = "Open a File" resizable = true mode = 0 access = 2 diff --git a/project.godot b/project.godot index eb7da96..7a296b6 100644 --- a/project.godot +++ b/project.godot @@ -385,6 +385,8 @@ enabled=PoolStringArray( "res://addons/exploding_replays/plugin.cfg", "res://add [exploding_addons] replays/record_replays=true +replays/capture_rate=60 +replays/full_snapshot_capture_rate=60 [global] From ed3510b324124087105393805f969b4c243a6a3b Mon Sep 17 00:00:00 2001 From: 21maz <60752903+ExplodingImplosion@users.noreply.github.com> Date: Tue, 4 Oct 2022 15:59:27 -0400 Subject: [PATCH 11/11] stuff --- addons/exploding_replays/replay.gd | 2 +- demos/replays/replayviewer.gd | 10 ++++++++-- demos/replays/replayviewer.tscn | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/addons/exploding_replays/replay.gd b/addons/exploding_replays/replay.gd index 4ab9b8c..52841c0 100644 --- a/addons/exploding_replays/replay.gd +++ b/addons/exploding_replays/replay.gd @@ -104,7 +104,7 @@ func deserialize(serialized: Array) -> void: assert_enumerated_array_correct(serialized) assert(_history.empty()) setup(serialized[TICKRATE],serialized[FULL_SNAPSHOT_TICKRATE],serialized[SCENE_PATH]) - deserialize_history(serialized[HISTORY]) + call_deferred("deserialize_history",serialized[HISTORY]) func deserialize_history(serialized_history: Array) -> void: for s_snap in serialized_history: diff --git a/demos/replays/replayviewer.gd b/demos/replays/replayviewer.gd index d27a02a..9b96284 100644 --- a/demos/replays/replayviewer.gd +++ b/demos/replays/replayviewer.gd @@ -118,13 +118,14 @@ func clear_replay() -> void: gameworld.queue_free() gameworld = null last_snap = null + network.reset_system() # Maybe rename to denote that it's related to files static func load_new_replay(filepath: String) -> Replay: var serialized: Array = Replay.read_compressed_replay_file(filepath) Replay.assert_enumerated_array_correct(serialized) var ret = Replay.new(serialized[Replay.TICKRATE],serialized[Replay.FULL_SNAPSHOT_TICKRATE],serialized[Replay.SCENE_PATH]) - ret.deserialize_history(serialized[Replay.HISTORY]) + ret.call_deferred("deserialize_history",serialized[Replay.HISTORY]) return ret func read_replay(filepath: String) -> void: @@ -135,8 +136,13 @@ func read_replay(filepath: String) -> void: replay = load_new_replay(filepath) change_playback_speed(playbackspeed.value) setup_timeline() - tenseconds = replay._tickratre * 10 + tenseconds = replay._tickrate * 10 setup_game_scene() + network._update_control.sig += 1 + network._update_control.snap = NetSnapshot.new(network._update_control.sig) + for k in network.snapshot_data._entity_info.keys(): + network._update_control.snap.add_type(k) + network.snapshot_data.client_check_snapshot(network._update_control.snap) func setup_game_scene() -> void: var scene: Resource = load(replay._scene_path) diff --git a/demos/replays/replayviewer.tscn b/demos/replays/replayviewer.tscn index b600fbd..f205d07 100644 --- a/demos/replays/replayviewer.tscn +++ b/demos/replays/replayviewer.tscn @@ -363,7 +363,7 @@ window_title = "Open a File" resizable = true mode = 0 access = 2 -filters = PoolStringArray( ".REPLAY" ) +filters = PoolStringArray( "*.REPLAY" ) [connection signal="scrolling" from="VBoxContainer/Timeline" to="." method="on_timeline_scrolled"] [connection signal="value_changed" from="VBoxContainer/Timeline" to="." method="on_timeline_ticked"]