diff --git a/.gitignore b/.gitignore index 4d18b76..96d1197 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,4 @@ export_presets.cfg # Mono-specific ignores .mono/ -data_*/ +data_*/ \ No newline at end of file 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 new file mode 100644 index 0000000..52841c0 --- /dev/null +++ b/addons/exploding_replays/replay.gd @@ -0,0 +1,225 @@ +############################################################################### +# 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() +var _scene_path: String + +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, 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 is_full_snapshot(snapshot): + sd.encode_full(snapshot,edec,snapshot.input_sig) + else: + sd.encode_delta(snapshot,sd._history[-1],edec,snapshot.input_sig) + return 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(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: + 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(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: +# _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],serialized[SCENE_PATH]) + call_deferred("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(): + _history.append(network.snapshot_data.decode_full(edec)) + 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()) + + + +# 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/" +# 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) + var history: Array = replay[HISTORY] + for idx in history.size(): + assert(history[idx] is PoolByteArray) + buffer.buffer = history[idx] + 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,SCENE_PATH,HISTORY,REPLAY_MAX} +static func to_enumerated_array(replay: Replay) -> Array: + 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) + 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: + 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!") diff --git a/demos/mega/megamain.gd b/demos/mega/megamain.gd index 7b23b07..282f527 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,10 +79,37 @@ 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.explodingreplays + Replay.recsetting) + else: + printsettingmsg(Replay.recsetting) + return + if ProjectSettings.has_setting(Replay.explodingreplays + 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.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") +# if !get_tree().has_network_peer(): +# pass - +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) @@ -121,6 +149,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: + 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") @@ -132,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 new file mode 100644 index 0000000..9b96284 --- /dev/null +++ b/demos/replays/replayviewer.gd @@ -0,0 +1,263 @@ +extends Panel + +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 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 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: + 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() + +func _physics_process(delta: float) -> void: + pass + +func _process(delta: float) -> void: + fpsreadout.set_text(str(Engine.get_frames_per_second())) + +func _input(event: InputEvent) -> void: + # 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: + goto_main_menu() + + 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) + +func simulate_up_to_current_snapshot(this_snap: NetSnapshot) -> void: + 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: + pause_if_playing() + if gameworld: + 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.call_deferred("deserialize_history",serialized[Replay.HISTORY]) + return ret + +func read_replay(filepath: String) -> void: + clear_replay() + if replay: + replay.load_replay(filepath) + else: + replay = load_new_replay(filepath) + change_playback_speed(playbackspeed.value) + setup_timeline() + 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) + 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: + 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: + 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) + +func go_back_1() -> 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() -> void: + move_by_amnt(1) +# timeline.set_value(timeline.get_value() + 1) + +func go_to_end() -> void: + timeline.set_value(timeline.get_max()) + pause_if_playing() + +func pause_unpause() -> void: + if replay: + pause_or_unpause(!is_playing) + +func play() -> void: + pause_or_unpause(true) + +func pause() -> void: + 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()) + +func on_right_pressed() -> void: + go_forward_1() + pause_if_playing() + +func change_playback_speed(value: float) -> void: + Engine.set_iterations_per_second(replay._tickrate * value) + +func goto_main_menu() -> void: + # 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 on_timeline_scrolled() -> void: + pass + pause_if_playing() + +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 new file mode 100644 index 0000000..f205d07 --- /dev/null +++ b/demos/replays/replayviewer.tscn @@ -0,0 +1,379 @@ +[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] +[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 = 8 +font_data = ExtResource( 2 ) + +[sub_resource type="DynamicFont" id=3] +size = 24 +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="HScrollBar" parent="VBoxContainer"] +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"] +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_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 = 122.0 +margin_bottom = 10.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="TimecodeInfo/TimeReadout"] +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 = 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 = 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 = 14.0 +margin_right = 122.0 +margin_bottom = 24.0 +custom_constants/separation = 0 + +[node name="Label" type="Label" parent="TimecodeInfo/FrameReadout"] +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 = 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 = 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 = "Open a File" +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"] +[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"] +[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 6da414d..ecb2371 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/replays/replayviewer.tscn") + func _on_bt_quit_pressed() -> void: get_tree().quit() - - - - - diff --git a/main.tscn b/main.tscn index 46b41ef..02a7252 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 @@ -591,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. @@ -666,6 +668,49 @@ __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 +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: + +- 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 +} + [node name="audiomaster" type="Panel" parent="mpnl/stabs"] visible = false anchor_right = 1.0 @@ -712,6 +757,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"] @@ -726,4 +772,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"] diff --git a/project.godot b/project.godot index 6e4f89f..7a296b6 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/exploding_replays/replay.gd" +}, { "base": "Control", "class": "SFXHelper", "language": "GDScript", @@ -331,6 +336,7 @@ _global_script_class_icons={ "Network": "", "Quantize": "", "RectangleDrawer": "", +"Replay": "", "SFXHelper": "", "SampleDataAsset": "", "SampleScriptedResource": "", @@ -374,7 +380,13 @@ 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" ) + +[exploding_addons] + +replays/record_replays=true +replays/capture_rate=60 +replays/full_snapshot_capture_rate=60 [global] @@ -385,32 +397,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 +442,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) ] }