commit 081a5fcf1a35930be6aafc101d2905866c1f4e13 Author: Harly Date: Sat Jan 10 11:12:02 2026 -0500 Initial commit for testing/fixing a deprecated godot-colyseus addon diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/addons/godot_colyseus/demo/blur.png b/addons/godot_colyseus/demo/blur.png new file mode 100644 index 0000000..a139e2f Binary files /dev/null and b/addons/godot_colyseus/demo/blur.png differ diff --git a/addons/godot_colyseus/demo/blur.png.import b/addons/godot_colyseus/demo/blur.png.import new file mode 100644 index 0000000..dd4530e --- /dev/null +++ b/addons/godot_colyseus/demo/blur.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdhx6b3h4tkao" +path="res://.godot/imported/blur.png-4563c8475376415b661c69b2b9f1d2d4.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/godot_colyseus/demo/blur.png" +dest_files=["res://.godot/imported/blur.png-4563c8475376415b661c69b2b9f1d2d4.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/addons/godot_colyseus/demo/char.tscn b/addons/godot_colyseus/demo/char.tscn new file mode 100644 index 0000000..5f1fc04 --- /dev/null +++ b/addons/godot_colyseus/demo/char.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=2] + +[ext_resource path="res://addons/godot_colyseus/demo/blur.png" type="Texture2D" id=1] + +[node name="char" type="Sprite2D"] +position = Vector2( 0, -16 ) +scale = Vector2( 0.6, 1 ) +texture = ExtResource( 1 ) diff --git a/addons/godot_colyseus/demo/chat.gd b/addons/godot_colyseus/demo/chat.gd new file mode 100644 index 0000000..42ff7f0 --- /dev/null +++ b/addons/godot_colyseus/demo/chat.gd @@ -0,0 +1,32 @@ +extends Control + +const colyseus = preload("res://addons/godot_colyseus/lib/colyseus.gd") +var room: colyseus.Room + +func _ready(): + var client = colyseus.Client.new("ws://localhost:2567") + var promise = client.join_or_create(colyseus.Schema, "chat") + await promise.completed + if promise.get_state() == promise.State.Failed: + print("Failed") + return + var room: colyseus.Room = promise.get_data() + room.on_message("messages").on(Callable(self, "_on_messages")) + $label.text += "Connected" + self.room = room + + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +#func _process(delta): +# pass + +func _on_messages(data): + $label.text += "\n" + data + + +func _on_send_pressed(): + if $input.text.is_empty(): + return + room.send("message", $input.text) + $input.text = "" diff --git a/addons/godot_colyseus/demo/chat.gd.uid b/addons/godot_colyseus/demo/chat.gd.uid new file mode 100644 index 0000000..3b613fb --- /dev/null +++ b/addons/godot_colyseus/demo/chat.gd.uid @@ -0,0 +1 @@ +uid://cj7jhyoe2eluq diff --git a/addons/godot_colyseus/demo/chat.tscn b/addons/godot_colyseus/demo/chat.tscn new file mode 100644 index 0000000..2437b24 --- /dev/null +++ b/addons/godot_colyseus/demo/chat.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=2 format=3 uid="uid://dpxco2ygtk2bx"] + +[ext_resource type="Script" path="res://addons/godot_colyseus/demo/chat.gd" id="1"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1") + +[node name="input" type="TextEdit" parent="."] +layout_mode = 1 +anchors_preset = -1 +anchor_top = 0.927 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -0.200012 +offset_right = -110.0 + +[node name="send" type="Button" parent="."] +layout_mode = 1 +anchors_preset = 3 +anchor_left = 1.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = -104.0 +offset_top = -42.0 +offset_right = 1.0 +offset_bottom = -4.0 +grow_horizontal = 0 +grow_vertical = 0 +text = "Send" + +[node name="label" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -2.0 +offset_bottom = -54.0 +grow_horizontal = 2 +grow_vertical = 2 + +[connection signal="pressed" from="send" to="." method="_on_send_pressed"] diff --git a/addons/godot_colyseus/demo/control.tscn b/addons/godot_colyseus/demo/control.tscn new file mode 100644 index 0000000..404bf66 --- /dev/null +++ b/addons/godot_colyseus/demo/control.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=2 format=3 uid="uid://cxtq2mh35wwc5"] + +[sub_resource type="GDScript" id="GDScript_uosag"] +script/source = "extends Control + + +# Called when the node enters the scene tree for the first time. +func _ready(): + print(\"test \", await test_await()) + + +# Called every frame. 'delta' is the elapsed time since the previous frame. +func _process(delta): + pass + +func test_await(): + return 2 +" + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = SubResource("GDScript_uosag") diff --git a/addons/godot_colyseus/demo/main.gd b/addons/godot_colyseus/demo/main.gd new file mode 100644 index 0000000..03fef8f --- /dev/null +++ b/addons/godot_colyseus/demo/main.gd @@ -0,0 +1,68 @@ +extends Node2D + +const colyseus = preload("res://addons/godot_colyseus/lib/colyseus.gd") +const Char = preload("./char.tscn") + +class Player extends colyseus.Schema: + static func define_fields(): + return [ + colyseus.Field.new("x", colyseus.NUMBER), + colyseus.Field.new("y", colyseus.NUMBER) + ] + + var node + + func _to_string(): + return str("(",self.x,",",self.y,")") + +class RoomState extends colyseus.Schema: + static func define_fields(): + return [ + colyseus.Field.new("players", colyseus.MAP, Player), + ] + +var room: colyseus.Room + +# Called when the node enters the scene tree for the first time. +func _ready(): + var client = colyseus.Client.new("ws://localhost:2567") + var promise = client.join_or_create(RoomState, "state_handler") + await promise.completed + if promise.get_state() == promise.State.Failed: + print("Failed") + return + var room: colyseus.Room = promise.get_data() + var state: RoomState = room.get_state() + state.listen('players:add').on(Callable(self, "_on_players_add")) + room.on_state_change.on(Callable(self, "_on_state")) + room.on_message("hello").on(Callable(self, "_on_message")) + self.room = room + +func _on_message(data): + print(str("hello:", data)) + +func _on_state(state): + pass + +func _on_players_add(target, value, key): + print("Add:", " key:", key, " ", value) + var ch = Char.instantiate() + ch.position = Vector2(value.x, value.y) + add_child(ch) + value.node = ch + value.listen(":change").on(Callable(self, "_on_player")) + +func _on_player(target): + print("Change ", target) + var ch = target.node + ch.position = Vector2(target.x, target.y) + +func _physics_process(delta): + if Input.is_action_pressed("ui_up"): + room.send("move", { y = -1 }); + elif Input.is_action_pressed("ui_down"): + room.send("move", { y = 1 }); + elif Input.is_action_pressed("ui_left"): + room.send("move", { x = -1 }); + elif Input.is_action_pressed("ui_right"): + room.send("move", { x = 1 }); diff --git a/addons/godot_colyseus/demo/main.gd.uid b/addons/godot_colyseus/demo/main.gd.uid new file mode 100644 index 0000000..9658371 --- /dev/null +++ b/addons/godot_colyseus/demo/main.gd.uid @@ -0,0 +1 @@ +uid://ddjkvt52o2ggh diff --git a/addons/godot_colyseus/demo/main.tscn b/addons/godot_colyseus/demo/main.tscn new file mode 100644 index 0000000..771ce09 --- /dev/null +++ b/addons/godot_colyseus/demo/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://ceqab8i8yqmjj"] + +[ext_resource type="Script" path="res://addons/godot_colyseus/demo/main.gd" id="1"] + +[node name="Node2D" type="Node2D"] +script = ExtResource("1") diff --git a/addons/godot_colyseus/init.gd b/addons/godot_colyseus/init.gd new file mode 100644 index 0000000..c148cbc --- /dev/null +++ b/addons/godot_colyseus/init.gd @@ -0,0 +1,10 @@ +@tool +extends EditorPlugin + + +func _enter_tree(): + pass + + +func _exit_tree(): + pass diff --git a/addons/godot_colyseus/init.gd.uid b/addons/godot_colyseus/init.gd.uid new file mode 100644 index 0000000..ee06b64 --- /dev/null +++ b/addons/godot_colyseus/init.gd.uid @@ -0,0 +1 @@ +uid://b13nsgfmclyf6 diff --git a/addons/godot_colyseus/lib/client.gd b/addons/godot_colyseus/lib/client.gd new file mode 100644 index 0000000..99bcf65 --- /dev/null +++ b/addons/godot_colyseus/lib/client.gd @@ -0,0 +1,132 @@ +extends RefCounted + +const promises = preload("res://addons/godot_colyseus/lib/promises.gd") +const Promise = promises.Promise; +const RunPromise = promises.RunPromise; +const HTTP = preload("res://addons/godot_colyseus/lib/http.gd") +const CRoom = preload("res://addons/godot_colyseus/lib/room.gd") +const RoomInfo = preload("res://addons/godot_colyseus/lib/room_info.gd") + +var endpoint: String + +func _init(endpoint: String): + self.endpoint = endpoint + +func join_or_create(schema_type: GDScript, room_name: String, options: Dictionary = {}) -> Promise: + return RunPromise.new( + _create_match_make_request, + ["joinOrCreate", room_name, options, schema_type]) + +func create(schema_type: GDScript, room_name: String, options: Dictionary = {}) -> Promise: + return RunPromise.new( + _create_match_make_request, + ["create", room_name, options, schema_type]) + +func join(schema_type: GDScript, room_name: String, options: Dictionary = {}) -> Promise: + return RunPromise.new( + _create_match_make_request, + ["join", room_name, options, schema_type]) + +func join_by_id(schema_type: GDScript, room_id: String, options: Dictionary = {}) -> Promise: + return RunPromise.new( + _create_match_make_request, + ["joinById", room_id, options, schema_type]) + +func reconnect(schema_type: GDScript, reconnection_token: String) -> Promise: + var arr = reconnection_token.split(":") + if arr.size() == 2: + var room_id = arr[0] + var token = arr[1] + return RunPromise.new( + _create_match_make_request, + ["reconnect", room_id, {"reconnectionToken": token}, schema_type]) + else: + var fail = Promise.new() + fail.reject("Invalidate `reconnection_token`") + return fail + +func get_available_rooms(room_name:String) -> Promise: + var path = "/matchmake/" + room_name + return RunPromise.new( + _http_get, + [path, {"Accept": "application/json"}] + ).then(_process_rooms) + +func _create_match_make_request( + promise: Promise, + method: String, + room_name: String, + options: Dictionary, + schema_type: GDScript): + var path: String = "/matchmake/" + method + "/" + room_name + var server = endpoint + if server.begins_with("ws"): + server = server.replace("ws", "http") + if options == null: + options = {} + var http = HTTP.new(server) + var req = HTTP.RequestInfo.new("POST", path) + req.add_header("Accept", "application/json") + req.add_header("Content-Type", "application/json") + req.body = options + var resp = http.fetch(req) + + if resp.get_state() == Promise.State.Waiting: + await resp.completed + if resp.get_state() == Promise.State.Failed: + promise.reject(resp.get_error()) + return + var res: HTTP.Response = resp.get_data() + var response = res.json() + if response == null: + promise.reject("Server unreadable!") + return + + if response.get('code') != null: + promise.reject(response['error']) + return + var room = CRoom.new(response["room"]["name"], schema_type) + room.room_id = response["room"]["roomId"] + room.session_id = response["sessionId"] + + room.connect_remote(_build_endpoint(response["room"], { "sessionId": room.session_id })) + + room.on_join.once(Callable(self, "_room_joined"), [promise, room]) + room.on_error.once(Callable(self, "_room_error"), [promise, room]) + +func _room_joined(promise: Promise, room: CRoom): + room.on_error.off(Callable(self, "_room_error")) + promise.resolve(room) + +func _room_error(code: int, message: String, promise: Promise, room: CRoom): + promise.reject(str("[", code, "]", message)) + +func _build_endpoint(room: Dictionary, options: Dictionary = {}): + var params = PackedStringArray() + for name in options.keys(): + params.append(name + "=" + options[name]) + return endpoint + "/" + room["processId"] + "/" + room["roomId"] + "?" + "&".join(params) + +func _http_get(promise: Promise, path: String, headers: Dictionary): + var server = endpoint + if server.begins_with("ws"): + server = server.replace("ws", "http") + var http = HTTP.new(server) + var req = HTTP.RequestInfo.new("GET", path) + for key in headers.keys(): + req.add_header(key, headers[key]) + var resp = http.fetch(req) + + if resp.get_state() == Promise.State.Waiting: + await resp.completed + if resp.get_state() == Promise.State.Failed: + promise.reject(resp.get_error()) + return + var res: HTTP.Response = resp.get_data() + promise.resolve(res.json()) + +func _process_rooms(result, promise: Promise): + var list = [] + for data in result: + list.append(RoomInfo.new(data)) + return list diff --git a/addons/godot_colyseus/lib/client.gd.uid b/addons/godot_colyseus/lib/client.gd.uid new file mode 100644 index 0000000..58cb6aa --- /dev/null +++ b/addons/godot_colyseus/lib/client.gd.uid @@ -0,0 +1 @@ +uid://mutuixu84pvj diff --git a/addons/godot_colyseus/lib/collections.gd b/addons/godot_colyseus/lib/collections.gd new file mode 100644 index 0000000..491a234 --- /dev/null +++ b/addons/godot_colyseus/lib/collections.gd @@ -0,0 +1,180 @@ +extends Object + +const EventListener = preload("res://addons/godot_colyseus/lib/listener.gd") +const SchemaInterface = preload("res://addons/godot_colyseus/lib/schema_interface.gd") + +class Collection extends SchemaInterface: + var sub_type + + func meta_get_subtype(index): + return sub_type + +class ArraySchema extends Collection: + var items = [] + + func clear(decoding: bool = false): + items.clear() + + func meta_get(index): + if items.size() > index: + return items[index] + return null + + func meta_get_key(index): + return str(index) + + func meta_set(index, key, value): + _set_item(index, value) + + func meta_remove(index): + assert(items.size() > index) + items.remove_at(index) + + func _set_item(index, value): + if items.size() > index: + items[index] = value + else: + while items.size() < index - 1: + items.append(null) + items.append(value) + + func meta_set_self(value): + items = value + + func at(index: int): + return items[index] + + func size() -> int: + return items.size() + + func _to_string(): + return JSON.stringify(items) + + func to_object(): + return items + +class MapSchema extends Collection: + var _keys = {} + var items = {} + var _counter = 0 + + func clear(decoding: bool = false): + items.clear() + _keys.clear() + _counter = 0 + + func meta_get(index): + if _keys.has(index): + return items[_keys[index]] + return null + + func meta_get_key(index): + if not _keys.has(index): + return index + return _keys[index] + + func meta_set(index, key, value): + _keys[index] = key + items[key] = value + + func meta_remove(index): + if not _keys.has(index): + return + items.erase(_keys[index]) + _keys.erase(index) + + func at(key: String): + return items.get(key) + + func put(key: String, value): + _keys[_counter] = key + items[key] = value + ++_counter + + func _to_string(): + return JSON.stringify(items) + + func to_object(): + return items + + func keys(): + var list = [] + for k in _keys: + list.append(_keys[k]) + return list + + func size(): + return _keys.size() + + func has(key: String): + return items.has(key) + +class SetSchema extends Collection: + var _counter = 0 + var items = {} + + func clear(decoding: bool = false): + items.clear() + _counter = 0 + + func meta_get(index): + if items.size() > index: + return items[index] + return null + + func meta_get_key(index): + return str(index) + + func meta_set(index, key, value): + _set_item(index, value) + + func meta_remove(index): + items.erase(index) + + func _set_item(index, value): + if items.size() > index: + items[index] = value + else: + while items.size() < index - 1: + items.append(null) + items.append(value) + + func _to_string(): + return JSON.stringify(items) + + func to_object(): + return items + +class CollectionSchema extends Collection: + var items = [] + + func clear(decoding: bool = false): + items.clear() + + func meta_get(index): + if items.size() > index: + return items[index] + return null + + func meta_get_key(index): + return str(index) + + func meta_set(index, key, value): + _set_item(index, value) + + func meta_remove(index): + items.erase(index) + + func _set_item(index, value): + if items.size() > index: + items[index] = value + else: + while items.size() < index - 1: + items.append(null) + items.append(value) + + func _to_string(): + return JSON.stringify(items) + + func to_object(): + return items diff --git a/addons/godot_colyseus/lib/collections.gd.uid b/addons/godot_colyseus/lib/collections.gd.uid new file mode 100644 index 0000000..40e7416 --- /dev/null +++ b/addons/godot_colyseus/lib/collections.gd.uid @@ -0,0 +1 @@ +uid://ciu14viqsjpjg diff --git a/addons/godot_colyseus/lib/colyseus.gd b/addons/godot_colyseus/lib/colyseus.gd new file mode 100644 index 0000000..78c23fa --- /dev/null +++ b/addons/godot_colyseus/lib/colyseus.gd @@ -0,0 +1,32 @@ +extends Object + +const Client = preload("res://addons/godot_colyseus/lib/client.gd") +const Schema = preload("res://addons/godot_colyseus/lib/schema.gd") +const Room = preload("res://addons/godot_colyseus/lib/room.gd") +const types = preload("res://addons/godot_colyseus/lib/types.gd") +const Field = Schema.Field +const REF = types.REF +const MAP = types.MAP +const ARRAY = types.ARRAY +const STRING = types.STRING +const NUMBER = types.NUMBER +const BOOLEAN = types.BOOLEAN +const INT8 = types.INT8 +const UINT8 = types.UINT8 +const INT16 = types.INT16 +const UINT16 = types.UINT16 +const INT32 = types.INT32 +const UINT32 = types.UINT32 +const INT64 = types.INT64 +const UINT64 = types.UINT64 +const FLOAT32 = types.FLOAT32 +const FLOAT64 = types.UINT32 +const collections = preload("res://addons/godot_colyseus/lib/collections.gd") +const ArraySchema = collections.ArraySchema +const MapSchema = collections.MapSchema +const SetSchema = collections.SetSchema +const CollectionSchema = collections.CollectionSchema + +const RoomInfo = preload("res://addons/godot_colyseus/lib/room_info.gd") + +const Promise = preload("res://addons/godot_colyseus/lib/promises.gd").Promise diff --git a/addons/godot_colyseus/lib/colyseus.gd.uid b/addons/godot_colyseus/lib/colyseus.gd.uid new file mode 100644 index 0000000..a03c54b --- /dev/null +++ b/addons/godot_colyseus/lib/colyseus.gd.uid @@ -0,0 +1 @@ +uid://dni1bp1qice02 diff --git a/addons/godot_colyseus/lib/decoder.gd b/addons/godot_colyseus/lib/decoder.gd new file mode 100644 index 0000000..a720f84 --- /dev/null +++ b/addons/godot_colyseus/lib/decoder.gd @@ -0,0 +1,68 @@ +extends RefCounted + +const MsgPack = preload("res://addons/godot_colyseus/lib/msgpack.gd") + +var reader: StreamPeerBuffer + +func _init(reader: StreamPeerBuffer): + reader.big_endian = false + self.reader = reader + +func read_utf8() -> String: + var prefix = reader.get_u8() + var length = -1 + + if prefix < 0xc0: + length = prefix & 0x1f + elif prefix == 0xd9: + length = reader.get_u8() + elif prefix == 0xda: + length = reader.get_u16() + elif prefix == 0xdb: + length = reader.get_u32() + + return reader.get_utf8_string(length) + +func number(): + var prefix = reader.get_u8() + + if prefix < 0x80: + return prefix + elif prefix == 0xca: + return reader.get_float() + elif prefix == 0xcb: + return reader.get_double() + elif prefix == 0xcc: + return reader.get_u8() + elif prefix == 0xcd: + return reader.get_u16() + elif prefix == 0xce: + return reader.get_u32() + elif prefix == 0xcf: + return reader.get_u64() + elif prefix == 0xd0: + return reader.get_8() + elif prefix == 0xd1: + return reader.get_16() + elif prefix == 0xd2: + return reader.get_32() + elif prefix == 0xd3: + return reader.get_64() + elif prefix > 0xdf: + return (0xff - prefix + 1) * -1 + +func has_more() -> bool: + return reader.get_position() < reader.get_size() + +func current_bit() -> int: + return reader.data_array[reader.get_position()] + +func is_number() -> bool: + var prefix = current_bit() + return prefix < 0x80 || (prefix >= 0xca && prefix <= 0xd3) + +func unpack(): + var result = MsgPack.decode(reader) + if result.error == OK: + return result.result + return null diff --git a/addons/godot_colyseus/lib/decoder.gd.uid b/addons/godot_colyseus/lib/decoder.gd.uid new file mode 100644 index 0000000..0944baa --- /dev/null +++ b/addons/godot_colyseus/lib/decoder.gd.uid @@ -0,0 +1 @@ +uid://btjicixj57gl5 diff --git a/addons/godot_colyseus/lib/encoder.gd b/addons/godot_colyseus/lib/encoder.gd new file mode 100644 index 0000000..80a57cd --- /dev/null +++ b/addons/godot_colyseus/lib/encoder.gd @@ -0,0 +1,67 @@ +extends RefCounted + +const MsgPack = preload("res://addons/godot_colyseus/lib/msgpack.gd") + +var writer: StreamPeerBuffer + +func _init(writer: StreamPeerBuffer): + writer.big_endian = false + self.writer = writer + +func string(v: String): + + var bytes = v.to_utf8_buffer() + var length = bytes.size() + + if length < 0x20: + writer.put_u8(length | 0xa0) + elif length < 0x100: + writer.put_u8(0xd9) + writer.put_u8(length) + elif length < 0x10000: + writer.put_u8(0xda) + writer.put_u16(length) + elif length < 0x100000000: + writer.put_u8(0xdb) + writer.put_u32(length) + else: + assert(false) #,"String too long") + + writer.put_data(bytes) + +func number(v): + if v == NAN: + return number(0) + elif v != abs(v): + writer.put_u8(0xcb) + writer.put_double(v) + elif v >= 0: + if v < 0x80: + writer.put_u8(v) + elif v < 0x100: + writer.put_u8(0xcc) + writer.put_u8(v) + elif v < 0x10000: + writer.put_u8(0xcd) + writer.put_u16(v) + elif v < 0x100000000: + writer.put_u8(0xce) + writer.put_u32(v) + else: + writer.put_u8(0xcf) + writer.put_u32(v) + else: + if v >= -0x20: + writer.put_u8(0xe0 | (v + 0x20)) + elif v >= -0x80: + writer.put_u8(0xd0) + writer.put_8(v) + elif v >= -0x8000: + writer.put_u8(0xd1) + writer.put_16(v) + elif v >= -0x80000000: + writer.put_u8(0xd2) + writer.put_32(v) + else: + writer.put_u8(0xd3) + writer.put_64(v) diff --git a/addons/godot_colyseus/lib/encoder.gd.uid b/addons/godot_colyseus/lib/encoder.gd.uid new file mode 100644 index 0000000..bf501ad --- /dev/null +++ b/addons/godot_colyseus/lib/encoder.gd.uid @@ -0,0 +1 @@ +uid://drl5m0b1yqxig diff --git a/addons/godot_colyseus/lib/frame_runner.gd b/addons/godot_colyseus/lib/frame_runner.gd new file mode 100644 index 0000000..e2b5905 --- /dev/null +++ b/addons/godot_colyseus/lib/frame_runner.gd @@ -0,0 +1,23 @@ +extends RefCounted + +var _running = false +var fn: Callable +var argv: Array + +# Called when the node enters the scene tree for the first time. +func _init(fn: Callable,argv: Array = []): + self.fn = fn + self.argv = argv + +func start(): + if not _running: + _running = true + var root: SceneTree = Engine.get_main_loop() + while true: + await root.process_frame + if not _running: + return + fn.callv(argv) + +func stop(): + _running = false diff --git a/addons/godot_colyseus/lib/frame_runner.gd.uid b/addons/godot_colyseus/lib/frame_runner.gd.uid new file mode 100644 index 0000000..905bb5a --- /dev/null +++ b/addons/godot_colyseus/lib/frame_runner.gd.uid @@ -0,0 +1 @@ +uid://b2ng861vtoeih diff --git a/addons/godot_colyseus/lib/http.gd b/addons/godot_colyseus/lib/http.gd new file mode 100644 index 0000000..be37d40 --- /dev/null +++ b/addons/godot_colyseus/lib/http.gd @@ -0,0 +1,229 @@ +extends RefCounted + +const promises = preload("res://addons/godot_colyseus/lib/promises.gd") +const Promise = promises.Promise +const RunPromise = promises.RunPromise + +var _connected = false +var _client_promise: promises.Promise + +class RequestInfo: + var method: String = "GET" + var path: String = "/" + var headers: PackedStringArray = [] + var body + + func _init(method: String = "GET",path: String = "/"): + self.method = method + self.path = path + + func add_header(key: String, value): + headers.append(str(key, ": ", value)) + return self + + func http_method(): + match method.to_upper(): + "GET": + return HTTPClient.METHOD_GET + "POST": + return HTTPClient.METHOD_POST + "PUT": + return HTTPClient.METHOD_PUT + "DELETE": + return HTTPClient.METHOD_DELETE + "HEAD": + return HTTPClient.METHOD_HEAD + "OPTIONS": + return HTTPClient.METHOD_OPTIONS + + func get_body(): + if body == null: + body = "" + if body is Dictionary or body is Array: + body = JSON.stringify(body) + return body + +class Response: + var _response_chunks: Array = [] + var _body + var _has_response = false + + var _status_code: int = 0 + var _content_length: int = 0 + var _headers + + func body() -> PackedByteArray: + if _body == null: + _body = PackedByteArray() + for chunk in _response_chunks: + _body.append_array(chunk) + return _body + + func text() -> String: + return body().get_string_from_utf8() + + func json(): + var test_json_conv = JSON.new() + var err = test_json_conv.parse(text()) + if err == OK: + return test_json_conv.get_data() + print(str(test_json_conv.get_error_message(), ":", test_json_conv.get_error_line())) + return null + + func headers() -> Dictionary: + return _headers + + func status_code() -> int: + return _status_code + + func content_length() -> int: + return _content_length + + func _to_string(): + var lines = PackedStringArray() + lines.append(str("StatusCode: ", status_code())) + lines.append(str("ContentLength: ", content_length())) + lines.append(str("Headers: ")) + var header = headers() + for key in header.keys(): + lines.append(str(" ", key, ": ", header[key])) + lines.append(str("Body: [", body().size(), "]")) + return "\n".join(lines) + +var _old_status + +func _init(server: String): + var url = parseUrl(server) + _client_promise = promises.RunPromise.new(Callable(self, "_setup"), [url.host, url.port, url.ssl]); + +static func parseUrl(url) -> Dictionary: + var regex = RegEx.new() + regex.compile("(\\w+):\\/\\/([^\\/:]+)(:(\\d+))?") + var result = regex.search(url) + var scheme = result.get_string(1) + var ssl = scheme == "https" + var host = result.get_string(2) + var portstr = result.get_string(4) + var port = -1 + if portstr != "": + port = int(portstr) + return { + "host": host, + "ssl": ssl, + "port": port, + } + +var host: String +func _setup(promise: promises.Promise, host, port, ssl): + var client = HTTPClient.new() + self.host = host + var error = client.connect_to_host(host, port, TLSOptions.client() if ssl else null) + if error != OK: + promise.reject(str("ErrorCode: ", error)) + var root = Engine.get_main_loop() + while true: + await root.process_frame + client.poll() + var status = client.get_status() + + match status: + HTTPClient.STATUS_CONNECTED: + promise.resolve(client) + break + HTTPClient.STATUS_DISCONNECTED: + promise.reject("Disconnected from Host") + break + HTTPClient.STATUS_CANT_CONNECT: + promise.reject("Can't Connect to Host") + break + +func _request(promise: Promise, request: RequestInfo): + if _client_promise.get_state() == promises.Promise.State.Waiting: + await _client_promise.completed + if _client_promise.get_state() == Promise.State.Failed: + promise.reject(_client_promise.get_error()) + return + var client: HTTPClient = _client_promise.get_data() + var body = request.get_body() + var error + if body is String: + error = client.request(request.http_method(), request.path, request.headers, body) + elif body is PackedByteArray: + error = client.request_raw(request.http_method(), request.path, request.headers, body) + else: + promise.reject("Unsupport body type") + return + if error != OK: + promise.reject(str("Error code ", error)) + return + var root = Engine.get_main_loop() + var response = Response.new() + while true: + await root.process_frame + error = client.poll() + var status = client.get_status() + match status: + HTTPClient.STATUS_DISCONNECTED: + if response._has_response: + promise.resolve(response) + else: + promise.reject("Disconnected from Host") + return + HTTPClient.STATUS_CANT_CONNECT: + promise.reject("Can't Connect to Host") + return + HTTPClient.STATUS_CONNECTION_ERROR: + promise.reject("Connection Error") + return + HTTPClient.STATUS_BODY: + if not response._has_response: + response._has_response = true + response._status_code = client.get_response_code() + response._content_length = client.get_response_body_length() + response._headers = client.get_response_headers_as_dictionary() + var chunk = client.read_response_body_chunk() + if chunk.is_empty(): + continue + response._response_chunks.append(chunk) + HTTPClient.STATUS_CONNECTED: + promise.resolve(response) + return + pass + +func _resolve(promise: Promise, request: RequestInfo, response: Response): + if response.status_code() == 301: + var new_request = RequestInfo.new(request.method, request.path) + new_request.headers = request.headers + new_request.body = request.body + + var path: String = response.headers()["Location"] + if path.begins_with("http://") or path.begins_with("https://"): + var idx = path.find('/', 8) + var host + if idx >= 0: + host = path.substr(0, idx) + path = path.substr(idx) + else: + host = path + path = '/' + var url = parseUrl(host) + if url.host != self.host: + printerr("Cannot connect to new host ", host, self.host) + promise.resolve(response) + return + else: + new_request.path = path + + promise.resolve(fetch(new_request)) + pass + else: + promise.resolve(response) + +func fetch(request = null) -> Promise: + if request == null: + request = RequestInfo.new() + elif request is String: + var path = request + request = RequestInfo.new() + request.path = path + return RunPromise.new(_request, [request]) diff --git a/addons/godot_colyseus/lib/http.gd.uid b/addons/godot_colyseus/lib/http.gd.uid new file mode 100644 index 0000000..056ec21 --- /dev/null +++ b/addons/godot_colyseus/lib/http.gd.uid @@ -0,0 +1 @@ +uid://co8kfm0yu3vui diff --git a/addons/godot_colyseus/lib/listener.gd b/addons/godot_colyseus/lib/listener.gd new file mode 100644 index 0000000..6c8c1fb --- /dev/null +++ b/addons/godot_colyseus/lib/listener.gd @@ -0,0 +1,62 @@ +extends RefCounted + +const ps = preload("res://addons/godot_colyseus/lib/promises.gd") + + +class Callback: + var fn: Callable + var args + var once = false + + func emit(arg: Array): + var parmas = [] + parmas.append_array(arg) + parmas.append_array(args) + fn.callv(parmas) + +var cbs = [] + +func on(fn: Callable, args: Array = []): + var cb = Callback.new() + cb.fn = fn + cb.args = args + cbs.append(cb) + +func off(fn: Callable): + var willremove = [] + for cb in cbs: + if cb.fn == fn: + willremove.append(cb) + for cb in willremove: + cbs.erase(cb) + +func once(fn: Callable, args: Array = []): + var cb = Callback.new() + cb.fn = fn + cb.args = args + cb.once = true + cbs.append(cb) + +func wait() -> ps.Promise: + var promise = ps.Promise.new() + once(Callable(self, "_on_event"), [promise]) + return promise + +func _on_event(data, promise: ps.Promise): + promise.resolve(data) + +func emit(argv: Array = []): + var willremove = [] + var size = cbs.size() + for i in range(size): + var cb = cbs[i] + if !is_instance_valid(cb): + cbs.remove_at(i) + i -= 1 + size -= 1 + continue + cb.emit(argv) + if cb.once: + willremove.append(cb) + for cb in willremove: + cbs.erase(cb) diff --git a/addons/godot_colyseus/lib/listener.gd.uid b/addons/godot_colyseus/lib/listener.gd.uid new file mode 100644 index 0000000..24ec10b --- /dev/null +++ b/addons/godot_colyseus/lib/listener.gd.uid @@ -0,0 +1 @@ +uid://dtgvuxvgbn08y diff --git a/addons/godot_colyseus/lib/msgpack.gd b/addons/godot_colyseus/lib/msgpack.gd new file mode 100644 index 0000000..78ed8b1 --- /dev/null +++ b/addons/godot_colyseus/lib/msgpack.gd @@ -0,0 +1,487 @@ +# Copyright (C) 2019 Tintin Ho +# +# 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. +# +# +# godot-msgpack +# +# This is a MessagePack serializer written in pure GDSciprt. To install this +# library in your project simply and copy and paste this file inside your +# project (e.g. res://msgpack.gd). +# +# +# class Msgpack +# static Dictionary encode(Variant value) +# Convert a value (number, string, array and dictionary) into their +# counterparts in messagepack. Returns dictionary with three fields: +# `result` which is the packed data (a PackedByteArray); `error` which is the +# error code; and `error_string` which is a human readable error message +# +# static Dictionary decode(PackedByteArray bytes) +# Convert a packed data (a PackedByteArray) into a value, the reverse of the +# encode function. The return value is similar to the one in the encode +# method + +static func encode(value, buffer: StreamPeerBuffer): + var ctx = {error = OK, error_string = ""} + buffer.big_endian = true + + _encode(buffer, value, ctx) + if ctx.error == OK: + return { + result = buffer.data_array, + error = OK, + error_string = "", + } + else: + return { + result = PackedByteArray(), + error = ctx.error, + error_string = ctx.error_string, + } + +static func decode(buffer: StreamPeerBuffer): + buffer.big_endian = true + + var ctx = {error = OK, error_string = ""} + var value = _decode(buffer, ctx) + if ctx.error == OK: + if buffer.get_position() == buffer.get_size(): + return {result = value, error = OK, error_string = ""} + else: + var msg = "excess buffer %s bytes" % [buffer.get_size() - buffer.get_position()] + return {result = null, error = FAILED, error_string = msg} + else: + return {result = null, error = ctx.error, error_string = ctx.error_string} + +static func _encode(buf, value, ctx): + match typeof(value): + TYPE_NIL: + buf.put_u8(0xc0) + + TYPE_BOOL: + if value: + buf.put_u8(0xc3) + else: + buf.put_u8(0xc2) + + TYPE_INT: + if -(1 << 5) <= value and value <= (1 << 7) - 1: + # fixnum (positive and negative) + buf.put_8(value) + elif -(1 << 7) <= value and value <= (1 << 7): + buf.put_u8(0xd0) + buf.put_8(value) + elif -(1 << 15) <= value and value <= (1 << 15): + buf.put_u8(0xd1) + buf.put_16(value) + elif -(1 << 31) <= value and value <= (1 << 31): + buf.put_u8(0xd2) + buf.put_32(value) + else: + buf.put_u8(0xd3) + buf.put_64(value) + + TYPE_FLOAT: + buf.put_u8(0xcb) + buf.put_double(value) + + TYPE_STRING: + var bytes = value.to_utf8_buffer() + + var size = bytes.size() + if size <= (1 << 5) - 1: + # type fixstr [101XXXXX] + buf.put_u8(0xa0 | size) + elif size <= (1 << 8) - 1: + # type str 8 + buf.put_u8(0xd9) + buf.put_u8(size) + elif size <= (1 << 16) - 1: + # type str 16 + buf.put_u8(0xda) + buf.put_u16(size) + elif size <= (1 << 32) - 1: + # type str 32 + buf.put_u8(0xdb) + buf.put_u32(size) + else: + assert(false) + + buf.put_data(bytes) + + TYPE_PACKED_BYTE_ARRAY: + var size = value.size() + if size <= (1 << 8) - 1: + buf.put_u8(0xc4) + buf.put_u8(size) + elif size <= (1 << 16) - 1: + buf.put_u8(0xc5) + buf.put_u16(size) + elif size <= (1 << 32) - 1: + buf.put_u8(0xc6) + buf.put_u32(size) + else: + assert(false) + + buf.put_data(value) + + TYPE_ARRAY: + var size = value.size() + if size <= 15: + # type fixarray [1001XXXX] + buf.put_u8(0x90 | size) + elif size <= (1 << 16) - 1: + # type array 16 + buf.put_u8(0xdc) + buf.put_u16(size) + elif size <= (1 << 32) - 1: + # type array 32 + buf.put_u8(0xdd) + buf.put_u32(size) + else: + assert(false) + + for obj in value: + _encode(buf, obj, ctx) + if ctx.error != OK: + return + + TYPE_DICTIONARY: + var size = value.size() + if size <= 15: + # type fixmap [1000XXXX] + buf.put_u8(0x80 | size) + elif size <= (1 << 16) - 1: + # type map 16 + buf.put_u8(0xde) + buf.put_u16(size) + elif size <= (1 << 32) - 1: + # type map 32 + buf.put_u8(0xdf) + buf.put_u32(size) + else: + assert(false) + + for key in value: + _encode(buf, key, ctx) + if ctx.error != OK: + return + + _encode(buf, value[key], ctx) + if ctx.error != OK: + return + _: + ctx.error = FAILED + ctx.error_string = "unsupported data type %s" % [typeof(value)] + +static func _decode(buffer, ctx): + if buffer.get_position() == buffer.get_size(): + ctx.error = FAILED + ctx.error_string = "unexpected end of input" + return null + + var head = buffer.get_u8() + if head == 0xc0: + return null + elif head == 0xc2: + return false + elif head == 0xc3: + return true + + # Integers + elif head & 0x80 == 0: + # positive fixnum + return head + elif (~head) & 0xe0 == 0: + # negative fixnum + return head - 256 + elif head == 0xcc: + # uint 8 + if buffer.get_size() - buffer.get_position() < 1: + ctx.error = FAILED + ctx.error_string = "not enough buffer for uint8" + return null + + return buffer.get_u8() + elif head == 0xcd: + # uint 16 + if buffer.get_size() - buffer.get_position() < 2: + ctx.error = FAILED + ctx.error_string = "not enough buffer for uint16" + return null + + return buffer.get_u16() + elif head == 0xce: + # uint 32 + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for uint32" + return null + + return buffer.get_u32() + elif head == 0xcf: + # uint 64 + if buffer.get_size() - buffer.get_position() < 8: + ctx.error = FAILED + ctx.error_string = "not enough buffer for uint64" + return null + + return buffer.get_u64() + elif head == 0xd0: + # int 8 + if buffer.get_size() - buffer.get_position() < 1: + ctx.error = FAILED + ctx.error_string = "not enogh buffer for int8" + return null + + return buffer.get_8() + elif head == 0xd1: + # int 16 + if buffer.get_size() - buffer.get_position() < 2: + ctx.error = FAILED + ctx.error_string = "not enogh buffer for int16" + return null + + return buffer.get_16() + elif head == 0xd2: + # int 32 + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for int32" + return null + + return buffer.get_32() + elif head == 0xd3: + # int 64 + if buffer.get_size() - buffer.get_position() < 8: + ctx.error = FAILED + ctx.error_string = "not enough buffer for int64" + return null + + return buffer.get_64() + + # Float + elif head == 0xca: + # float32 + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for float32" + return null + + return buffer.get_float() + elif head == 0xcb: + # float64 + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for float64" + return null + + return buffer.get_double() + + # String + elif (~head) & 0xa0 == 0: + var size = head & 0x1f + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for fixstr required %s bytes" % [size] + return null + + return buffer.get_utf8_string(size) + elif head == 0xd9: + if buffer.get_size() - buffer.get_position() < 1: + ctx.error = FAILED + ctx.error_string = "not enough buffer for str8 size" + return null + + var size = buffer.get_u8() + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for str8 data required %s bytes" % [size] + return null + + return buffer.get_utf8_string(size) + elif head == 0xda: + if buffer.get_size() - buffer.get_position() < 2: + ctx.error = FAILED + ctx.error_string = "not enough buffer for str16 size" + return null + + var size = buffer.get_u16() + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for str16 data required %s bytes" % [size] + return null + + return buffer.get_utf8_string(size) + elif head == 0xdb: + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for str32 size" + return null + + var size = buffer.get_u32() + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for str32 data required %s bytes" % [size] + return null + + return buffer.get_utf8_string(size) + + # Binary + elif head == 0xc4: + if buffer.get_size() - buffer.get_position() < 1: + ctx.error = FAILED + ctx.error_string = "not enough buffer for bin8 size" + return null + + var size = buffer.get_u8() + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for bin8 data required %s bytes" % [size] + return null + + var res = buffer.get_data(size) + assert(res[0] == OK) + return res[1] + elif head == 0xc5: + if buffer.get_size() - buffer.get_position() < 2: + ctx.error = FAILED + ctx.error_string = "not enough buffer for bin16 size" + return null + + var size = buffer.get_u16() + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for bin16 data required %s bytes" % [size] + return null + + var res = buffer.get_data(size) + assert(res[0] == OK) + return res[1] + elif head == 0xc6: + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for bin32 size" + return null + + var size = buffer.get_u32() + if buffer.get_size() - buffer.get_position() < size: + ctx.error = FAILED + ctx.error_string = "not enough buffer for bin32 data required %s bytes" % [size] + return null + + var res = buffer.get_data(size) + assert(res[0] == OK) + return res[1] + + # Array + elif head & 0xf0 == 0x90: + var size = head & 0x0f + var res = [] + for i in range(size): + res.append(_decode(buffer, ctx)) + if ctx.error != OK: + return null + return res + elif head == 0xdc: + if buffer.get_size() - buffer.get_position() < 2: + ctx.error = FAILED + ctx.error_string = "not enough buffer for array16 size" + return null + + var size = buffer.get_u16() + var res = [] + for i in range(size): + res.append(_decode(buffer, ctx)) + if ctx.error != OK: + return null + return res + elif head == 0xdd: + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for array32 size" + return null + + var size = buffer.get_u32() + var res = [] + for i in range(size): + res.append(_decode(buffer, ctx)) + if ctx.error != OK: + return null + return res + + # Map + elif head & 0xf0 == 0x80: + var size = head & 0x0f + var res = {} + for i in range(size): + var k = _decode(buffer, ctx) + if ctx.error != OK: + return null + + var v = _decode(buffer, ctx) + if ctx.error != OK: + return null + + res[k] = v + return res + elif head == 0xde: + if buffer.get_size() - buffer.get_position() < 2: + ctx.error = FAILED + ctx.error_string = "not enough buffer for map16 size" + return null + + var size = buffer.get_u16() + var res = {} + for i in range(size): + var k = _decode(buffer, ctx) + if ctx.error != OK: + return null + + var v = _decode(buffer, ctx) + if ctx.error != OK: + return null + + res[k] = v + return res + elif head == 0xdf: + if buffer.get_size() - buffer.get_position() < 4: + ctx.error = FAILED + ctx.error_string = "not enough buffer for map32 size" + return null + + var size = buffer.get_u32() + var res = {} + for i in range(size): + var k = _decode(buffer, ctx) + if ctx.error != OK: + return null + + var v = _decode(buffer, ctx) + if ctx.error != OK: + return null + + res[k] = v + return res + + else: + ctx.error = FAILED + ctx.error_string = "invalid byte tag %02X at pos %s" % [head, buffer.get_position()] + return null diff --git a/addons/godot_colyseus/lib/msgpack.gd.uid b/addons/godot_colyseus/lib/msgpack.gd.uid new file mode 100644 index 0000000..20d0769 --- /dev/null +++ b/addons/godot_colyseus/lib/msgpack.gd.uid @@ -0,0 +1 @@ +uid://dlclaop7pocqd diff --git a/addons/godot_colyseus/lib/operations.gd b/addons/godot_colyseus/lib/operations.gd new file mode 100644 index 0000000..bb76f3c --- /dev/null +++ b/addons/godot_colyseus/lib/operations.gd @@ -0,0 +1,9 @@ +extends Node + +const ADD = 128 +const REPLACE = 0 +const DELETE = 64 +const DELETE_AND_ADD = 192 +const TOUCH = 1 +const CLEAR = 10 +const SWITCH_TO_STRUCTURE = 255 diff --git a/addons/godot_colyseus/lib/operations.gd.uid b/addons/godot_colyseus/lib/operations.gd.uid new file mode 100644 index 0000000..ff6e7c5 --- /dev/null +++ b/addons/godot_colyseus/lib/operations.gd.uid @@ -0,0 +1 @@ +uid://bucnjhgajvk8x diff --git a/addons/godot_colyseus/lib/promises.gd b/addons/godot_colyseus/lib/promises.gd new file mode 100644 index 0000000..9a848dd --- /dev/null +++ b/addons/godot_colyseus/lib/promises.gd @@ -0,0 +1,107 @@ +extends RefCounted + +class Promise: + enum State { + Waiting, + Success, + Failed + } + var result + + signal completed + + var _state: State = State.Waiting + + func get_state() -> State: + return _state + + func resolve(res = null): + if res is Promise: + await res.completed + result = res.result + _state = res.get_state() + emit_signal("completed") + else: + result = res + _state = State.Success + emit_signal("completed") + + func reject(error = null): + result = error + _state = State.Failed + emit_signal("completed") + + var data:get = get_data + func get_data(): + if _state == State.Success: + return result + return null + + var error:get = get_error + func get_error(): + if _state == State.Failed: + return result + return null + + var is_failed : bool : + get(): + return _state == State.Failed + + func wait(): + if _state == State.Waiting: + await self.completed + return self + + func _to_string(): + match _state: + State.Waiting: + return "[Waiting]" + State.Success: + return str("[Success:",result,"]") + State.Failed: + return str("[Failed:",result,"]") + + func _next(promise, callback: Callable, argv: Array): + await wait() + if _state == State.Success: + var arr = [get_data(), promise] + arr.append_array(argv) + var ret = await callback.callv(arr) + promise.resolve(ret) + + func then(callback: Callable, argv: Array = []) -> Promise: + return RunPromise.new(Callable(self, "_next"), [callback, argv]) + +class FramePromise extends Promise: + var cb: Callable + var argv: Array + + func _init(callback: Callable,argv: Array = []): + cb = callback + self.argv = argv + _run() + + func _run(): + var root = Engine.get_main_loop() + while true: + if root is SceneTree: + await root.process_frame + var arr = [self] + arr.append_array(argv) + cb.callv(arr) + if get_state() != State.Waiting: + break + +class RunPromise extends Promise: + var cb: Callable + var argv: Array + + func _init(callback: Callable,argv: Array = []): + cb = callback + self.argv = argv + _run() + + func _run(): + var arr = [self] + arr.append_array(argv) + await cb.callv(arr) diff --git a/addons/godot_colyseus/lib/promises.gd.uid b/addons/godot_colyseus/lib/promises.gd.uid new file mode 100644 index 0000000..92dbc72 --- /dev/null +++ b/addons/godot_colyseus/lib/promises.gd.uid @@ -0,0 +1 @@ +uid://d4lhtnts1yq1k diff --git a/addons/godot_colyseus/lib/room.gd b/addons/godot_colyseus/lib/room.gd new file mode 100644 index 0000000..d303266 --- /dev/null +++ b/addons/godot_colyseus/lib/room.gd @@ -0,0 +1,198 @@ +extends RefCounted + +const FrameRunner = preload("res://addons/godot_colyseus/lib/frame_runner.gd") +const EventListener = preload("res://addons/godot_colyseus/lib/listener.gd") +const ser = preload("res://addons/godot_colyseus/lib/serializer.gd") +const Decoder = preload("res://addons/godot_colyseus/lib/decoder.gd") +const Encoder = preload("res://addons/godot_colyseus/lib/encoder.gd") +const MsgPack = preload("res://addons/godot_colyseus/lib/msgpack.gd") +const Schema = preload("./schema.gd") + +const CODE_HANDSHAKE = 9 +const CODE_JOIN_ROOM = 10 +const CODE_ERROR = 11 +const CODE_LEAVE_ROOM = 12 +const CODE_ROOM_DATA = 13 +const CODE_ROOM_STATE = 14 +const CODE_ROOM_STATE_PATCH = 15 +const CODE_ROOM_DATA_SCHEMA = 16 + +const ERROR_MATCHMAKE_NO_HANDLER = 4210 +const ERROR_MATCHMAKE_INVALID_CRITERIA = 4211 +const ERROR_MATCHMAKE_INVALID_ROOM_ID = 4212 +const ERROR_MATCHMAKE_UNHANDLED = 4213 +const ERROR_MATCHMAKE_EXPIRED = 4214 + +const ERROR_AUTH_FAILED = 4215 +const ERROR_APPLICATION_ERROR = 4216 + +var room_name: String +var room_id: String +var session_id: String +var serializer: ser.Serializer +var ws: WebSocketPeer +var frame_runner: FrameRunner +var reconnection_token: String + +var schema_type: GDScript + +var _has_joined = false +func has_joined() -> bool: + return _has_joined + +# [code: int, message: String] +var on_error: EventListener = EventListener.new() + +# [] +var on_leave: EventListener = EventListener.new() + +# [] +var on_join: EventListener = EventListener.new() + +# [state: Schema] +var on_state_change: EventListener = EventListener.new() + +# [data] +var _messages = {} +func on_message(event: String, new_listener: bool = true) -> EventListener: + var listener + if not _messages.has(event) or new_listener: + listener = EventListener.new() + _messages[event] = listener + else: + listener = _messages[event] + return listener + +func _init(room_name: String,schema_type: GDScript): + self.room_name = room_name + self.schema_type = schema_type + ws = WebSocketPeer.new() + #ws.connect("connection_established",Callable(self,"_connection_established")) + #ws.connect("connection_error",Callable(self,"_connection_error")) + #ws.connect("connection_closed",Callable(self,"_connection_closed")) + #ws.connect("data_received",Callable(self,"_on_data")) + + frame_runner = FrameRunner.new(_on_frame) + + + +func _connection_established(protocol): + pass + +func _connection_error(): + frame_runner.stop() + +func _connection_closed(was_clean: bool): + frame_runner.stop() + +func _on_data(): + var data = ws.get_packet() + var reader = StreamPeerBuffer.new() + reader.data_array = data + + var decoder = Decoder.new(reader) + var code = reader.get_u8() + match code: + CODE_JOIN_ROOM: + + var token = reader.get_string(reader.get_u8()) + + var serializer_id = reader.get_string(reader.get_u8()) + + if serializer == null: + serializer = ser.getSerializer(serializer_id, schema_type) + + if decoder.has_more(): + if serializer: + serializer.handshake(decoder) + else: + on_error.emit([1, "Can not find serializer"]) + return + + self.reconnection_token = str(room_id, ":", token) + _has_joined = true + on_join.emit() + send_raw([CODE_JOIN_ROOM]) + CODE_ERROR: + var message = decoder.read_utf8() + on_error.emit([0, message]) + CODE_LEAVE_ROOM: + leave() + CODE_ROOM_DATA: + var type + if decoder.is_number(): + type = str('i', decoder.number()) + else: + type = decoder.read_utf8() + + var listener = on_message(type, false) + if listener != null: + var ret = decoder.unpack() + if ret == null: + ret = {} + listener.emit([ret]) + + CODE_ROOM_STATE: + serializer.set_state(decoder) + on_state_change.emit([serializer.get_state()]) + CODE_ROOM_STATE_PATCH: + serializer.patch(decoder) + on_state_change.emit([serializer.get_state()]) + CODE_ROOM_DATA_SCHEMA: + print("Receive message CODE_ROOM_DATA_SCHEMA") + +func connect_remote(url: String): + var _url = url + if url.begins_with("http:"): + _url = url.replace("http:", "ws:") + elif url.begins_with("https:"): + _url = url.replace("https:", "wss:") + #_url = _url.replace("/colyseus", "") + ws.connect_to_url(_url) + frame_runner.start() + +func _on_frame(): + ws.poll() + var state = ws.get_ready_state() + match state: + WebSocketPeer.STATE_OPEN: + while ws.get_available_packet_count() > 0: + _on_data() + WebSocketPeer.STATE_CLOSED: + var code = ws.get_close_code() + var reason = ws.get_close_reason() + _connection_closed(true) + if _has_joined: + leave() + +func send_raw(bytes: PackedByteArray): + ws.send(bytes) + +func send(type: String, message = null): + var buffer = StreamPeerBuffer.new() + buffer.put_u8(CODE_ROOM_DATA) + var encoder = Encoder.new(buffer) + + if typeof(type) == TYPE_STRING: + encoder.string(type) + else: + encoder.number(type) + + if message != null: + var result = MsgPack.encode(message, buffer) + assert(result.error == OK) + + send_raw(buffer.data_array) + +func leave(consented = true): + _has_joined = false + if not room_id.is_empty(): + if consented: + send_raw([CODE_LEAVE_ROOM]) + else: + ws.disconnect_from_host() + on_leave.emit() + +var state : Schema : get = get_state +func get_state() -> Schema: + return serializer.get_state() diff --git a/addons/godot_colyseus/lib/room.gd.uid b/addons/godot_colyseus/lib/room.gd.uid new file mode 100644 index 0000000..912aae2 --- /dev/null +++ b/addons/godot_colyseus/lib/room.gd.uid @@ -0,0 +1 @@ +uid://jpi6ohybugst diff --git a/addons/godot_colyseus/lib/room_info.gd b/addons/godot_colyseus/lib/room_info.gd new file mode 100644 index 0000000..f20f31b --- /dev/null +++ b/addons/godot_colyseus/lib/room_info.gd @@ -0,0 +1,18 @@ +extends RefCounted + +var clients: int +var created_at: String +var max_clients: int +var name: String +var process_id: String +var room_id: String + +func _init(dic): + clients = dic.get('clients') + created_at = dic.get('createdAt') + var num = dic.get('maxClients') + if num != null: + max_clients = num + name = dic.get('name') + process_id = dic.get('processId') + room_id = dic.get('roomId') diff --git a/addons/godot_colyseus/lib/room_info.gd.uid b/addons/godot_colyseus/lib/room_info.gd.uid new file mode 100644 index 0000000..0828c1c --- /dev/null +++ b/addons/godot_colyseus/lib/room_info.gd.uid @@ -0,0 +1 @@ +uid://b8wn1vytiodw3 diff --git a/addons/godot_colyseus/lib/schema.gd b/addons/godot_colyseus/lib/schema.gd new file mode 100644 index 0000000..e12b6ea --- /dev/null +++ b/addons/godot_colyseus/lib/schema.gd @@ -0,0 +1,308 @@ +extends "./schema_interface.gd" + +const col = preload("res://addons/godot_colyseus/lib/collections.gd") +const OP = preload("res://addons/godot_colyseus/lib/operations.gd") +const TypeInfo = preload("res://addons/godot_colyseus/lib/type_info.gd") + +const END_OF_STRUCTURE = 0xc1 +const NIL = 0xc0 +const INDEX_CHANGE = 0xd4 + +const Decoder = preload("res://addons/godot_colyseus/lib/decoder.gd") +const EventListener = preload("res://addons/godot_colyseus/lib/listener.gd") +const SchemaInterface = preload("res://addons/godot_colyseus/lib/schema_interface.gd") + +class Field: + const Types = preload("res://addons/godot_colyseus/lib/types.gd") + var index: int + var name: String + var value + var current_type: TypeInfo + + func _init(name: String,type: String,schema_type = null): + current_type = TypeInfo.new(type) + if schema_type is String: + current_type.sub_type = TypeInfo.new(schema_type) + elif schema_type is GDScript: + if type == Types.REF: + current_type.sub_type = schema_type + else: + current_type.sub_type = TypeInfo.new(Types.REF, schema_type) + elif schema_type is TypeInfo: + current_type.sub_type = schema_type + self.name = name + + func _to_string(): + if current_type: + return current_type.to_string() + else: + return 'null' + +var _fields: Array = [] +var _field_index = {} + +var _refs = {} + +var _change_listeners = {} + +func _get_property_list(): + var result = [] + for field in _fields: + result.append({ + name = field.name, + type = Types.to_gd_type(field.current_type.type), + usage = PROPERTY_USAGE_DEFAULT + }) + return result + +func _get(property): + if _field_index.has(property): + var value = _field_index[property].value + if value is SchemaInterface: + pass + return value + return null + +func _set(property, value): + if _field_index.has(property): + var field = _field_index[property] + var old = field.value + if old is SchemaInterface: + pass + field.value = value + return true + return false + +# [event: String, target, key_or_index] +# path format {path}:{action} +# {action} is one of: +# add Create sub object, paramaters [current, new_value, key] +# remove Delete sub object, paramaters [current, old_value, key] +# replace Replace sub object, paramaters [current, new_value, key] +# delete Current object is deleted, paramaters [current] +# create Current object is created, paramaters [current] +# change Current object's attributes has changed, paramaters [current] +# clear Current Array or Map has cleared, paramaters [current] +func listen(path: String) -> EventListener: + if not _change_listeners.has(path): + _change_listeners[path] = EventListener.new() + return _change_listeners[path] + +static func define_fields() -> Array: + return [] + +func _init(): + _fields = self.get_script().define_fields() + var counter = 0 + for field in _fields: + field.index = counter + _setup_field(field) + counter += 1 + +func _setup_field(field: Field): + _field_index[field.name] = field + var type = field.current_type + match type.type: + Types.MAP: + assert(type.sub_type != null) #,"Schema type is requested") + Types.ARRAY: + assert(type.sub_type != null) #,"Schema type is requested") + field.value = col.Collection.new() + Types.SET: + assert(type.sub_type != null) #,"Schema type is requested") + Types.COLLECTION: + assert(type.sub_type != null) #,"Schema type is requested") + Types.REF: + assert(type.sub_type != null) #,"Schema type is requested") + Types.NUMBER, Types.FLOAT32, Types.FLOAT64: + field.value = 0.0 + Types.INT8, Types.UINT8, Types.INT16, Types.UINT16, Types.INT32, Types.UINT32, Types.INT64, Types.UINT64: + field.value = 0 + Types.STRING: + field.value = "" + +func get_fields(): + return _fields + +func decode(decoder: Decoder) -> int: + + var ref_id = 0 + var ref: Ref = Ref.new(self, TypeInfo.new(Types.REF)) + _refs[ref_id] = ref + var changes = [] + var changed_objects = {} + + while decoder.has_more(): + var byte = decoder.reader.get_u8() + + if byte == OP.SWITCH_TO_STRUCTURE: + ref_id = decoder.number() + + var next_ref = _refs[ref_id] + + assert(next_ref != null) #,str('"refId" not found:', ref_id)) + + ref = next_ref + + continue + + var is_schema = ref.type_info.type == Types.REF + + var operation = byte + if is_schema: + operation = (byte >> 6) << 6 + + + if operation == OP.CLEAR: + ref.value.clear(true) + if ref.value is SchemaInterface: + changes.append({ + target = ref.value, + event = "clear", + argv = [] + }) + continue + + var field_index = byte % _re_replace(operation) + if not is_schema: + field_index = decoder.number() + + var ref_value = ref.value + if ref_value is SchemaInterface: + var old = ref_value.meta_get(field_index) + var new + var key = field_index + if ref.type_info.type != Types.MAP: + key = ref_value.meta_get_key(field_index) + + if operation == OP.DELETE: + if ref.type_info.type == Types.MAP: + key = ref_value.meta_get_key(field_index) + ref_value.meta_remove(field_index) + else: + if ref.type_info.type == Types.MAP: + key = decoder.read_utf8() + var type: TypeInfo = ref_value.meta_get_subtype(field_index) + if type.is_schema_type(): + var new_ref_id = decoder.number() + if _refs.has(new_ref_id): + new = _refs[new_ref_id].value + else: + if operation != OP.REPLACE: + new = type.create() + new.id = new_ref_id + _refs[new_ref_id] = Ref.new(new, type) + else: + new = type.decode(decoder) + + if old != new: + if old == null: + changes.append({ + target = ref_value, + event = "add", + argv = [new, key] + }) + elif new == null: + changes.append({ + target = ref_value, + event = "remove", + argv = [old, key] + }) + else: + changes.append({ + target = ref_value, + event = "replace", + argv = [new, key] + }) + + if old != null: + if old is SchemaInterface && old.id != null: + changes.append({ + target = old, + event = "delete", + argv = [] + }) + _refs.erase(old.id) + + if new != null: + ref_value.meta_set(field_index, key, new) + if new is SchemaInterface: + changes.append({ + target = new, + event = "create", + argv = [] + }) + new.set_parent(ref_value, field_index) + elif old != null: + ref_value.meta_remove(field_index) + + changed_objects[ref_value] = true + + for change in changes: + var target = change.target + target.trigger(change.event, change.argv) + + for target in changed_objects.keys(): + target.trigger("change", []) + + return 0 + + +func _re_replace(operation): + if operation == OP.REPLACE: + return 255 + return operation + +func clear(decoding: bool = false): + pass + +func meta_get(index): + assert(_fields.size() > index) + var field : Field = _fields[index] + return field.value + +func meta_get_key(index): + assert(_fields.size() > index) + var field : Field = _fields[index] + return field.name + +func meta_get_subtype(index): + assert(_fields.size() > index) + var field : Field = _fields[index] + return field.current_type + +func meta_set(index, key, value): + assert(_fields.size() > index) + var field : Field = _fields[index] + field.value = value + +func meta_remove(index): + assert(_fields.size() > index) + var field : Field = _fields[index] + var old = field.value + field.value = null + return old + +func _to_string(): + var obj = to_object() + return JSON.stringify(obj) + +func trigger(event: String, argv: Array = [], path: PackedStringArray = PackedStringArray(), target: Object = self): + var path_copy = PackedStringArray(path) + path_copy.reverse() + var path_str = '/'.join(path_copy) + ":" + event + if _change_listeners.has(path_str): + var ls: EventListener = _change_listeners[path_str] + argv.insert(0, target) + ls.emit(argv) + else: + super.trigger(event, argv, path, target) + +func to_object(): + var dic = {} + for field in _fields: + if field.value is SchemaInterface: + dic[field.name] = field.value.to_object() + else: + dic[field.name] = field.value + return dic diff --git a/addons/godot_colyseus/lib/schema.gd.uid b/addons/godot_colyseus/lib/schema.gd.uid new file mode 100644 index 0000000..2cfed1e --- /dev/null +++ b/addons/godot_colyseus/lib/schema.gd.uid @@ -0,0 +1 @@ +uid://vujiy070i53c diff --git a/addons/godot_colyseus/lib/schema_interface.gd b/addons/godot_colyseus/lib/schema_interface.gd new file mode 100644 index 0000000..f0a5459 --- /dev/null +++ b/addons/godot_colyseus/lib/schema_interface.gd @@ -0,0 +1,54 @@ +extends RefCounted + +const Types = preload("res://addons/godot_colyseus/lib/types.gd") + +class Ref: + var value + var type_info + + func _init(value,type_info): + self.value = value + self.type_info = type_info + +var id +var parent +var parent_index: int +var parent_key + +func clear(decoding: bool = false): + assert(false) + +func meta_get(index): + assert(false) + +func meta_get_key(index) -> String: + assert(false) + return "" + +func meta_get_subtype(index): + assert(false) + +func meta_set(index, key, value): + assert(false) + return null + +func meta_remove(index): + assert(false) + +func set_parent(np, pindex): + if parent == np and parent_index == pindex: + return + if parent != null: + parent.meta_remove(parent_index) + parent = np + parent_index = pindex + parent_key = parent.meta_get_key(parent_index) + +func trigger(event: String, argv: Array = [], path: PackedStringArray = PackedStringArray(), target: Object = self): + if parent == null: + return + path.append(parent_key) + parent.trigger(event, argv, path, target) + +func to_object(): + return "" diff --git a/addons/godot_colyseus/lib/schema_interface.gd.uid b/addons/godot_colyseus/lib/schema_interface.gd.uid new file mode 100644 index 0000000..4fb0255 --- /dev/null +++ b/addons/godot_colyseus/lib/schema_interface.gd.uid @@ -0,0 +1 @@ +uid://7hqsu5vttoy2 diff --git a/addons/godot_colyseus/lib/serializer.gd b/addons/godot_colyseus/lib/serializer.gd new file mode 100644 index 0000000..9157674 --- /dev/null +++ b/addons/godot_colyseus/lib/serializer.gd @@ -0,0 +1,113 @@ + +extends RefCounted + +const Schema = preload("res://addons/godot_colyseus/lib/schema.gd") +const Decoder = preload("res://addons/godot_colyseus/lib/decoder.gd") +const Types = preload("res://addons/godot_colyseus/lib/types.gd") + +class Serializer: + + func set_state(decoder): + pass + + func get_state(): + pass + + func patch(decoder): + pass + + func teardown(): + pass + + func handshake(decoder): + pass + + +class NoneSerializer extends Serializer: + pass + +class ReflectionField extends Schema: + + static func define_fields(): + return [ + Schema.Field.new("name", Schema.Types.STRING), + Schema.Field.new("type", Schema.Types.STRING), + Schema.Field.new("referenced_type", Schema.Types.NUMBER), + ] + + func test(field, reflection: Reflection) -> bool: + if self.type != field.current_type.to_string() or self.name != field.name: + var str1 = str(field.name, '-', self.name) + var str2 = str(field.current_type, '-', self.type) + printerr("Field not match ", str1, " : ", str2) + return false + if self.type == Schema.Types.REF: + var type = reflection.types.at(self.referenced_type) + return type.test(field.schema_type, reflection) + return true + +class ReflectionType extends Schema: + + static func define_fields(): + return [ + Schema.Field.new("id", Schema.Types.NUMBER), + Schema.Field.new("extendsId", Schema.Types.NUMBER), + Schema.Field.new("fields", Schema.Types.ARRAY, ReflectionField), + ] + + func test(schema_type, reflection: Reflection) -> bool: + if not schema_type is GDScript: + printerr("Type schema_type not match ", self.id) + return false + var fields = schema_type.define_fields() + var length = fields.size() + if length != self.fields.size(): + printerr("Type fields count not match ", self.id) + return false + for i in range(length): + var field = self.fields.at(i) + if not field.test(fields[i], reflection): + return false + return true + +class Reflection extends Schema: + + static func define_fields(): + return [ + Schema.Field.new("types", Schema.Types.ARRAY, ReflectionType), + Schema.Field.new("root_type", Schema.Types.NUMBER), + ] + + func test(schema_type: GDScript) -> bool: + return self.types.at(self.root_type).test(schema_type, self) + +class SchemaSerializer extends Serializer: + var state + var schema_type: GDScript + + func _init(schema_type): + self.schema_type = schema_type + self.state = schema_type.new() + + func handshake(decoder): + var reflection = Reflection.new() + reflection.decode(decoder) + assert(reflection.test(schema_type),"Can not detect schema type") + + func set_state(decoder): + state.decode(decoder) + + func get_state(): + return state + + func patch(decoder): + state.decode(decoder) + + +static func getSerializer(id: String, schema_type: GDScript = null) -> Serializer: + match id: + "schema": + return SchemaSerializer.new(schema_type) + "none": + return NoneSerializer.new() + return null diff --git a/addons/godot_colyseus/lib/serializer.gd.uid b/addons/godot_colyseus/lib/serializer.gd.uid new file mode 100644 index 0000000..a1d03a8 --- /dev/null +++ b/addons/godot_colyseus/lib/serializer.gd.uid @@ -0,0 +1 @@ +uid://dsp61m3okjbm diff --git a/addons/godot_colyseus/lib/type_info.gd b/addons/godot_colyseus/lib/type_info.gd new file mode 100644 index 0000000..e11fb76 --- /dev/null +++ b/addons/godot_colyseus/lib/type_info.gd @@ -0,0 +1,97 @@ +extends Object + +const Decoder = preload("res://addons/godot_colyseus/lib/decoder.gd") +const types = preload("res://addons/godot_colyseus/lib/types.gd") +const collections = preload("res://addons/godot_colyseus/lib/collections.gd") + +var type: String +var sub_type + +func _init(type: String,sub_type = null): + self.type = type + self.sub_type = sub_type + +func is_schema_type(): + return type == types.REF or type == types.MAP or type == types.ARRAY or type == types.COLLECTION or type == types.SET + +func _to_string(): + var ret = type + if sub_type and sub_type.type != types.REF: + ret = str(ret, ':', sub_type.type) + return ret + +func create(): + match type: + types.REF: + return sub_type.new() + types.MAP: + var obj = collections.MapSchema.new() + obj.sub_type = sub_type + return obj + types.ARRAY: + var obj = collections.ArraySchema.new() + obj.sub_type = sub_type + return obj + types.SET: + var obj = collections.SetSchema.new() + obj.sub_type = sub_type + return obj + types.COLLECTION: + var obj = collections.CollectionSchema.new() + obj.sub_type = sub_type + return obj + +func decode(decoder: Decoder): + match type: + types.REF: + var obj = sub_type.new() + obj.id = decoder.number() + return obj + types.MAP: + var obj = collections.MapSchema.new() + obj.id = decoder.number() + obj.sub_type = sub_type + return obj + types.ARRAY: + var obj = collections.ArraySchema.new() + obj.id = decoder.number() + obj.sub_type = sub_type + return obj + types.SET: + var obj = collections.SetSchema.new() + obj.id = decoder.number() + obj.sub_type = sub_type + return obj + types.COLLECTION: + var obj = collections.CollectionSchema.new() + obj.id = decoder.number() + obj.sub_type = sub_type + return obj + types.STRING: + return decoder.read_utf8() + types.NUMBER: + return decoder.number() + types.BOOLEAN: + return decoder.reader.get_u8() > 0 + types.INT8: + return decoder.reader.get_8() + types.UINT8: + return decoder.reader.get_u8() + types.INT16: + return decoder.reader.get_16() + types.UINT16: + return decoder.reader.get_u16() + types.INT32: + return decoder.reader.get_32() + types.UINT32: + return decoder.reader.get_u32() + types.INT64: + return decoder.reader.get_64() + types.UINT64: + return decoder.reader.get_u64() + types.FLOAT32: + return decoder.reader.get_float() + types.FLOAT64: + return decoder.reader.get_double() + _: + assert(true) #,str("Unkown support type:", type)) diff --git a/addons/godot_colyseus/lib/type_info.gd.uid b/addons/godot_colyseus/lib/type_info.gd.uid new file mode 100644 index 0000000..b753758 --- /dev/null +++ b/addons/godot_colyseus/lib/type_info.gd.uid @@ -0,0 +1 @@ +uid://bga0qc00bmx6a diff --git a/addons/godot_colyseus/lib/types.gd b/addons/godot_colyseus/lib/types.gd new file mode 100644 index 0000000..92219f5 --- /dev/null +++ b/addons/godot_colyseus/lib/types.gd @@ -0,0 +1,42 @@ +extends Object + +const REF = "ref" +const MAP = "map" +const ARRAY = "array" +const SET = "set" +const COLLECTION = "collection" +const STRING = "string" +const NUMBER = "number" +const BOOLEAN = "boolean" +const INT8 = "int8" +const UINT8 = "uint8" +const INT16 = "int16" +const UINT16 = "uint16" +const INT32 = "int32" +const UINT32 = "uint32" +const INT64 = "int64" +const UINT64 = "uint64" +const FLOAT32 = "float32" +const FLOAT64 = "float64" + +static func to_gd_type(type: String) -> int: + match type: + REF: + return TYPE_OBJECT + MAP: + return TYPE_OBJECT + ARRAY: + return TYPE_OBJECT + SET: + return TYPE_OBJECT + COLLECTION: + return TYPE_OBJECT + STRING: + return TYPE_STRING + NUMBER, FLOAT32, FLOAT64: + return TYPE_FLOAT + BOOLEAN: + return TYPE_BOOL + INT8, UINT8, INT16, UINT16, INT32, UINT32, INT64, UINT64: + return TYPE_INT + return TYPE_NIL diff --git a/addons/godot_colyseus/lib/types.gd.uid b/addons/godot_colyseus/lib/types.gd.uid new file mode 100644 index 0000000..d8e5027 --- /dev/null +++ b/addons/godot_colyseus/lib/types.gd.uid @@ -0,0 +1 @@ +uid://cgnlylx00jd6k diff --git a/addons/godot_colyseus/plugin.cfg b/addons/godot_colyseus/plugin.cfg new file mode 100644 index 0000000..50d1916 --- /dev/null +++ b/addons/godot_colyseus/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="godot-colyseus" +description="" +author="gsioteam" +version="0.1.0" +script="init.gd" diff --git a/default_env.tres b/default_env.tres new file mode 100644 index 0000000..1a5570b --- /dev/null +++ b/default_env.tres @@ -0,0 +1,7 @@ +[gd_resource type="Environment" load_steps=2 format=2] + +[sub_resource type="Sky" id=1] + +[resource] +background_mode = 2 +background_sky = SubResource( 1 ) diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..ec98ad8 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d2437bvwtxh8o" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..4e203be --- /dev/null +++ b/project.godot @@ -0,0 +1,20 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="airplane mode" +run/main_scene="uid://dqpkgbu4i5nhe" +config/features=PackedStringArray("4.4", "Forward Plus") +config/icon="res://icon.svg" + +[autoload] + +Network="*res://scripts/network.gd" diff --git a/scenes/HUD.tscn b/scenes/HUD.tscn new file mode 100644 index 0000000..b7430a5 --- /dev/null +++ b/scenes/HUD.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3 uid="uid://bt0w3qm5krugw"] + +[ext_resource type="Script" uid="uid://csqjx18o6oblo" path="res://scripts/hud.gd" id="1"] + +[node name="HUD" type="CanvasLayer"] +script = ExtResource("1") diff --git a/scenes/HotasMapper.tscn b/scenes/HotasMapper.tscn new file mode 100644 index 0000000..b38427e --- /dev/null +++ b/scenes/HotasMapper.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=1 format=3] + +[ext_resource type="Script" path="res://scripts/hotas_mapper.gd" id="1"] + +[node name="HotasMapper" type="CanvasLayer"] +script = ExtResource("1") diff --git a/scenes/Jet.tscn b/scenes/Jet.tscn new file mode 100644 index 0000000..2fb3a9b --- /dev/null +++ b/scenes/Jet.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=5 format=3 uid="uid://bgns8vvuehatu"] + +[ext_resource type="Script" uid="uid://bs6rfdeujndbp" path="res://scripts/jet_controller.gd" id="1"] +[ext_resource type="Script" uid="uid://3ierf3uelmp8" path="res://scripts/vector_line_3d.gd" id="2"] + +[sub_resource type="BoxMesh" id="1"] +size = Vector3(2, 0.6, 6) + +[sub_resource type="BoxShape3D" id="2"] +size = Vector3(2, 0.6, 6) + +[node name="Jet" type="RigidBody3D"] +mass = 1000.0 +gravity_scale = 0.0 +can_sleep = false +linear_damp = 0.05 +angular_damp = 0.1 +script = ExtResource("1") + +[node name="Mesh" type="MeshInstance3D" parent="."] +mesh = SubResource("1") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] +shape = SubResource("2") + +[node name="DisplacementVector" type="MeshInstance3D" parent="."] +script = ExtResource("2") + +[node name="ChaseCamera" type="Camera3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 0.995004, 0.0998334, 0, -0.0998334, 0.995004, 0, 2, 10) +current = true diff --git a/scenes/Main.tscn b/scenes/Main.tscn new file mode 100644 index 0000000..6cf24f9 --- /dev/null +++ b/scenes/Main.tscn @@ -0,0 +1,44 @@ +[gd_scene load_steps=8 format=3 uid="uid://cck7qvy27rihs"] + +[ext_resource type="PackedScene" uid="uid://bgns8vvuehatu" path="res://scenes/Jet.tscn" id="1"] +[ext_resource type="PackedScene" uid="uid://bt0w3qm5krugw" path="res://scenes/HUD.tscn" id="2"] +[ext_resource type="PackedScene" path="res://scenes/HotasMapper.tscn" id="3"] + +[sub_resource type="StandardMaterial3D" id="2"] +albedo_color = Color(0.1, 0.12, 0.14, 1) + +[sub_resource type="PlaneMesh" id="1"] +size = Vector2(400, 400) + +[sub_resource type="StandardMaterial3D" id="4"] +albedo_color = Color(1, 0.55, 0.1, 1) +roughness = 0.3 + +[sub_resource type="BoxMesh" id="3"] +size = Vector3(12, 12, 1) + +[node name="Main" type="Node3D"] + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(0.696707, -0.462133, 0.548664, 0, 0.764842, 0.644218, -0.717356, -0.448831, 0.532871, 0, 0, 0) +light_energy = 2.5 + +[node name="Ground" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -5, 0) +material_override = SubResource("2") +mesh = SubResource("1") + +[node name="Target" type="MeshInstance3D" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 12, -120) +material_override = SubResource("4") +mesh = SubResource("3") + +[node name="Jet" parent="." instance=ExtResource("1")] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0) + +[node name="HUD" parent="." instance=ExtResource("2")] +player_path = NodePath("../Jet") +camera_path = NodePath("../Jet/ChaseCamera") + +[node name="HotasMapper" parent="." instance=ExtResource("3")] +jet_path = NodePath("../Jet") diff --git a/scenes/MultiplayerMain.tscn b/scenes/MultiplayerMain.tscn new file mode 100644 index 0000000..5f56385 --- /dev/null +++ b/scenes/MultiplayerMain.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3 uid="uid://dqpkgbu4i5nhe"] + +[ext_resource type="Script" uid="uid://df4m8mwaap8kh" path="res://scenes/node.gd" id="1_g1sxi"] + +[node name="MultiplayerMain" type="Node3D"] + +[node name="Node" type="Node" parent="."] +script = ExtResource("1_g1sxi") diff --git a/scenes/MultiplayerMain.tscn13588459406.tmp b/scenes/MultiplayerMain.tscn13588459406.tmp new file mode 100644 index 0000000..5f56385 --- /dev/null +++ b/scenes/MultiplayerMain.tscn13588459406.tmp @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3 uid="uid://dqpkgbu4i5nhe"] + +[ext_resource type="Script" uid="uid://df4m8mwaap8kh" path="res://scenes/node.gd" id="1_g1sxi"] + +[node name="MultiplayerMain" type="Node3D"] + +[node name="Node" type="Node" parent="."] +script = ExtResource("1_g1sxi") diff --git a/scenes/node.gd b/scenes/node.gd new file mode 100644 index 0000000..e092dc3 --- /dev/null +++ b/scenes/node.gd @@ -0,0 +1,5 @@ +extends Node + +func _input(event: InputEvent) -> void: + if event is InputEventKey and event.pressed and event.keycode == KEY_C: + Network.connect_and_join("ws://prospera:2567", "my_room") diff --git a/scenes/node.gd.uid b/scenes/node.gd.uid new file mode 100644 index 0000000..d05fb53 --- /dev/null +++ b/scenes/node.gd.uid @@ -0,0 +1 @@ +uid://df4m8mwaap8kh diff --git a/scripts/hotas_mapper.gd b/scripts/hotas_mapper.gd new file mode 100644 index 0000000..c306599 --- /dev/null +++ b/scripts/hotas_mapper.gd @@ -0,0 +1,315 @@ +extends CanvasLayer + +@export var jet_path: NodePath +@export var config_path: String = "user://hotas_mapping.cfg" +@export var axis_capture_threshold: float = 0.35 +@export var toggle_action: String = "toggle_hotas_mapper" + +@export var panel_position: Vector2 = Vector2(12, 12) +@export var panel_size: Vector2 = Vector2(520, 360) + +const AXIS_CONFIG = [ + { + "name": "Roll", + "axis_prop": "roll_axis", + "invert_prop": "roll_invert", + "device_prop": "roll_device_id", + }, + { + "name": "Pitch", + "axis_prop": "pitch_axis", + "invert_prop": "pitch_invert", + "device_prop": "pitch_device_id", + }, + { + "name": "Yaw", + "axis_prop": "yaw_axis", + "invert_prop": "yaw_invert", + "device_prop": "yaw_device_id", + }, + { + "name": "Throttle", + "axis_prop": "throttle_axis", + "invert_prop": "throttle_invert", + "signed_prop": "throttle_signed", + "device_prop": "throttle_device_id", + }, + { + "name": "Strafe", + "axis_prop": "strafe_axis", + "invert_prop": "strafe_invert", + "device_prop": "strafe_device_id", + }, + { + "name": "Lift", + "axis_prop": "lift_axis", + "invert_prop": "lift_invert", + "device_prop": "lift_device_id", + }, +] + +var _jet: Node +var _panel: PanelContainer +var _status_label: Label +var _device_label: Label +var _rows := {} +var _listening_axis: String = "" + +func _ready() -> void: + _jet = get_node_or_null(jet_path) + _build_ui() + _load_config() + _refresh_labels() + +func _unhandled_input(event: InputEvent) -> void: + if _is_toggle_event(event): + visible = not visible + if not visible: + _listening_axis = "" + _refresh_labels() + get_viewport().set_input_as_handled() + +func _input(event: InputEvent) -> void: + if not visible: + return + if _listening_axis.is_empty(): + return + if event is InputEventJoypadMotion: + if abs(event.axis_value) < axis_capture_threshold: + return + _apply_axis_mapping(_listening_axis, event.axis, event.device) + _listening_axis = "" + _status_label.text = "Axis captured." + _refresh_labels() + +func _build_ui() -> void: + _panel = PanelContainer.new() + _panel.name = "HotasMapper" + _panel.anchor_left = 0.0 + _panel.anchor_top = 0.0 + _panel.anchor_right = 0.0 + _panel.anchor_bottom = 0.0 + _panel.offset_left = panel_position.x + _panel.offset_top = panel_position.y + _panel.offset_right = panel_position.x + panel_size.x + _panel.offset_bottom = panel_position.y + panel_size.y + add_child(_panel) + + var root := VBoxContainer.new() + root.size_flags_vertical = Control.SIZE_EXPAND_FILL + root.size_flags_horizontal = Control.SIZE_EXPAND_FILL + root.add_theme_constant_override("separation", 8) + _panel.add_child(root) + + var header := HBoxContainer.new() + header.size_flags_horizontal = Control.SIZE_EXPAND_FILL + root.add_child(header) + + var title := Label.new() + title.text = "HOTAS Axis Mapper" + header.add_child(title) + + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(spacer) + + var close_button := Button.new() + close_button.text = "Close" + close_button.pressed.connect(_on_close_pressed) + header.add_child(close_button) + + _device_label = Label.new() + _device_label.text = _get_device_text() + root.add_child(_device_label) + + _status_label = Label.new() + _status_label.text = "Click Map, then move the desired axis. Toggle: %s" % _get_toggle_hint() + root.add_child(_status_label) + + for config in AXIS_CONFIG: + root.add_child(_build_axis_row(config)) + +func _build_axis_row(config: Dictionary) -> Control: + var row := HBoxContainer.new() + row.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_theme_constant_override("separation", 8) + + var name_label := Label.new() + name_label.text = config.name + name_label.custom_minimum_size = Vector2(120, 0) + row.add_child(name_label) + + var map_button := Button.new() + map_button.text = "Map" + map_button.pressed.connect(_on_map_pressed.bind(config.axis_prop)) + row.add_child(map_button) + + var axis_label := Label.new() + axis_label.text = "Axis: ?" + axis_label.custom_minimum_size = Vector2(120, 0) + row.add_child(axis_label) + + var device_label := Label.new() + device_label.text = "Dev: ?" + device_label.custom_minimum_size = Vector2(160, 0) + row.add_child(device_label) + + var invert_check := CheckBox.new() + invert_check.text = "Invert" + invert_check.toggled.connect(_on_invert_toggled.bind(config.invert_prop)) + row.add_child(invert_check) + + var signed_check: CheckBox = null + if config.has("signed_prop"): + signed_check = CheckBox.new() + signed_check.text = "Signed" + signed_check.toggled.connect(_on_signed_toggled.bind(config.signed_prop)) + row.add_child(signed_check) + + _rows[config.axis_prop] = { + "axis_label": axis_label, + "device_label": device_label, + "map_button": map_button, + "invert_check": invert_check, + "signed_check": signed_check, + } + return row + +func _on_map_pressed(axis_prop: String) -> void: + if _listening_axis == axis_prop: + _listening_axis = "" + _status_label.text = "Mapping cancelled." + else: + _listening_axis = axis_prop + _status_label.text = "Listening for %s axis..." % axis_prop + _refresh_labels() + +func _on_close_pressed() -> void: + _listening_axis = "" + visible = false + +func _on_invert_toggled(pressed: bool, invert_prop: String) -> void: + if _jet == null: + return + _jet.set(invert_prop, pressed) + _save_config() + +func _on_signed_toggled(pressed: bool, signed_prop: String) -> void: + if _jet == null: + return + _jet.set(signed_prop, pressed) + _save_config() + +func _apply_axis_mapping(axis_prop: String, axis_index: int, device_id: int) -> void: + if _jet == null: + return + _jet.set(axis_prop, axis_index) + if device_id >= 0: + var device_prop = _get_device_prop(axis_prop) + if not device_prop.is_empty(): + _jet.set(device_prop, device_id) + _jet.set("joy_device_id", device_id) + _save_config() + _device_label.text = _get_device_text() + +func _refresh_labels() -> void: + if _jet == null: + return + _device_label.text = _get_device_text() + for config in AXIS_CONFIG: + var axis_prop: String = config.axis_prop + var row = _rows.get(axis_prop, null) + if row == null: + continue + row["axis_label"].text = "Axis: %d" % int(_jet.get(axis_prop)) + var device_prop: String = config.device_prop + var device_id: int = int(_jet.get(device_prop)) + row["device_label"].text = _get_device_label(device_id) + var invert_prop: String = config.invert_prop + row["invert_check"].set_pressed_no_signal(bool(_jet.get(invert_prop))) + if config.has("signed_prop") and row["signed_check"] != null: + row["signed_check"].set_pressed_no_signal(bool(_jet.get(config.signed_prop))) + var map_button: Button = row["map_button"] + if _listening_axis == axis_prop: + map_button.text = "Listening..." + else: + map_button.text = "Map" + +func _load_config() -> void: + if _jet == null: + return + var config := ConfigFile.new() + var err = config.load(config_path) + if err != OK: + return + for entry in AXIS_CONFIG: + var axis_prop: String = entry.axis_prop + if config.has_section_key("axes", axis_prop): + _jet.set(axis_prop, int(config.get_value("axes", axis_prop))) + var invert_prop: String = entry.invert_prop + if config.has_section_key("invert", invert_prop): + _jet.set(invert_prop, bool(config.get_value("invert", invert_prop))) + var device_prop: String = entry.device_prop + if config.has_section_key("devices", device_prop): + _jet.set(device_prop, int(config.get_value("devices", device_prop))) + if entry.has("signed_prop"): + var signed_prop: String = entry.signed_prop + if config.has_section_key("flags", signed_prop): + _jet.set(signed_prop, bool(config.get_value("flags", signed_prop))) + if config.has_section_key("device", "joy_device_id"): + _jet.set("joy_device_id", int(config.get_value("device", "joy_device_id"))) + +func _save_config() -> void: + if _jet == null: + return + var config := ConfigFile.new() + for entry in AXIS_CONFIG: + var axis_prop: String = entry.axis_prop + config.set_value("axes", axis_prop, int(_jet.get(axis_prop))) + var invert_prop: String = entry.invert_prop + config.set_value("invert", invert_prop, bool(_jet.get(invert_prop))) + var device_prop: String = entry.device_prop + config.set_value("devices", device_prop, int(_jet.get(device_prop))) + if entry.has("signed_prop"): + var signed_prop: String = entry.signed_prop + config.set_value("flags", signed_prop, bool(_jet.get(signed_prop))) + config.set_value("device", "joy_device_id", int(_jet.get("joy_device_id"))) + config.save(config_path) + +func _get_device_text() -> String: + if _jet == null: + return "Device: none" + var device_id: int = int(_jet.get("joy_device_id")) + if device_id < 0: + var pads = Input.get_connected_joypads() + if pads.is_empty(): + return "Device: none" + device_id = pads[0] + var name = Input.get_joy_name(device_id) + return "Device: %s (id %d)" % [name, device_id] + +func _get_device_label(device_id: int) -> String: + if device_id < 0: + return "Dev: auto" + var name = Input.get_joy_name(device_id) + if name.is_empty(): + return "Dev: %d" % device_id + return "Dev: %s (%d)" % [name, device_id] + +func _get_device_prop(axis_prop: String) -> String: + for entry in AXIS_CONFIG: + if entry.axis_prop == axis_prop: + return entry.device_prop + return "" + +func _is_toggle_event(event: InputEvent) -> bool: + if InputMap.has_action(toggle_action): + return event.is_action_pressed(toggle_action) + if event is InputEventKey and event.pressed and not event.echo: + return event.keycode == KEY_F1 + return false + +func _get_toggle_hint() -> String: + if InputMap.has_action(toggle_action): + return toggle_action + return "F1" diff --git a/scripts/hotas_mapper.gd.uid b/scripts/hotas_mapper.gd.uid new file mode 100644 index 0000000..8dac851 --- /dev/null +++ b/scripts/hotas_mapper.gd.uid @@ -0,0 +1 @@ +uid://bj8dpyx6m80px diff --git a/scripts/hud.gd b/scripts/hud.gd new file mode 100644 index 0000000..264cdc0 --- /dev/null +++ b/scripts/hud.gd @@ -0,0 +1,130 @@ +extends CanvasLayer + +@export var player_path: NodePath +@export var camera_path: NodePath +@export var vector_scale: float = 2.5 +@export var max_radius: float = 140.0 +@export var gizmo_margin: Vector2 = Vector2(140, 140) +@export var gizmo_axis_length: float = 55.0 +@export var gizmo_axis_width: float = 2.0 +@export var velocity_line_width: float = 3.0 +@export var show_axis_debug: bool = true + +var _velocity_line: Line2D +var _axis_x: Line2D +var _axis_y: Line2D +var _axis_z: Line2D +var _vector_label: Label +var _axis_label: Label + +var _player: RigidBody3D +var _camera: Camera3D + +func _ready() -> void: + _velocity_line = _get_or_create_velocity_line() + _axis_x = _get_or_create_axis_line("AxisX", Color(0.95, 0.2, 0.2, 0.9)) + _axis_y = _get_or_create_axis_line("AxisY", Color(0.2, 0.9, 0.4, 0.9)) + _axis_z = _get_or_create_axis_line("AxisZ", Color(0.2, 0.5, 1.0, 0.9)) + _vector_label = _get_or_create_vector_label() + _axis_label = _get_or_create_axis_label() + _player = get_node_or_null(player_path) + _camera = get_node_or_null(camera_path) + _axis_label.visible = show_axis_debug + +func _process(_delta: float) -> void: + if _player == null or _camera == null: + return + + var velocity = _player.linear_velocity + var cam_basis = _camera.global_transform.basis + var local_velocity = cam_basis.inverse() * velocity + + var arrow = Vector2(local_velocity.x, -local_velocity.y) * vector_scale + if arrow.length() > max_radius: + arrow = arrow.normalized() * max_radius + + var viewport_size = get_viewport().get_visible_rect().size + var center = Vector2( + viewport_size.x - gizmo_margin.x, + viewport_size.y - gizmo_margin.y + ) + _velocity_line.points = PackedVector2Array([center, center + arrow]) + + var player_basis = _player.global_transform.basis + var rel_basis = cam_basis.inverse() * player_basis + _axis_x.points = PackedVector2Array([ + center, + center + Vector2(rel_basis.x.x, -rel_basis.x.y) * gizmo_axis_length + ]) + _axis_y.points = PackedVector2Array([ + center, + center + Vector2(rel_basis.y.x, -rel_basis.y.y) * gizmo_axis_length + ]) + _axis_z.points = PackedVector2Array([ + center, + center + Vector2(rel_basis.z.x, -rel_basis.z.y) * gizmo_axis_length + ]) + _vector_label.text = "Velocity: (%.1f, %.1f, %.1f) m/s | Speed: %.1f" % [ + velocity.x, + velocity.y, + velocity.z, + velocity.length(), + ] + + if show_axis_debug and _axis_label != null: + if _player.has_method("get_axis_debug_text"): + _axis_label.text = _player.get_axis_debug_text() + +func _get_or_create_velocity_line() -> Line2D: + var existing: Line2D = get_node_or_null("VelocityLine") + if existing != null: + return existing + var line := Line2D.new() + line.name = "VelocityLine" + line.width = velocity_line_width + line.default_color = Color(1, 0.7, 0.2, 0.9) + line.antialiased = true + add_child(line) + return line + +func _get_or_create_axis_line(name: String, color: Color) -> Line2D: + var existing: Line2D = get_node_or_null(name) + if existing != null: + return existing + var line := Line2D.new() + line.name = name + line.width = gizmo_axis_width + line.default_color = color + line.antialiased = true + add_child(line) + return line + +func _get_or_create_vector_label() -> Label: + var existing: Label = get_node_or_null("VectorLabel") + if existing != null: + return existing + var label := Label.new() + label.name = "VectorLabel" + label.text = "Velocity:" + label.set_anchors_and_offsets_preset(Control.PRESET_TOP_LEFT) + label.offset_left = 12.0 + label.offset_top = 12.0 + label.offset_right = 520.0 + label.offset_bottom = 40.0 + add_child(label) + return label + +func _get_or_create_axis_label() -> Label: + var existing: Label = get_node_or_null("AxisLabel") + if existing != null: + return existing + var label := Label.new() + label.name = "AxisLabel" + label.text = "Axis:" + label.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT) + label.offset_left = 12.0 + label.offset_top = -60.0 + label.offset_right = 520.0 + label.offset_bottom = -12.0 + add_child(label) + return label diff --git a/scripts/hud.gd.uid b/scripts/hud.gd.uid new file mode 100644 index 0000000..2b3d5c3 --- /dev/null +++ b/scripts/hud.gd.uid @@ -0,0 +1 @@ +uid://csqjx18o6oblo diff --git a/scripts/jet_controller.gd b/scripts/jet_controller.gd new file mode 100644 index 0000000..d8073f1 --- /dev/null +++ b/scripts/jet_controller.gd @@ -0,0 +1,194 @@ +extends RigidBody3D + +@export var joy_device_id: int = -1 + +@export var pitch_axis: int = 1 +@export var pitch_invert: bool = true +@export var pitch_device_id: int = -1 +@export var roll_axis: int = 0 +@export var roll_invert: bool = false +@export var roll_device_id: int = -1 +@export var yaw_axis: int = 2 +@export var yaw_invert: bool = false +@export var yaw_device_id: int = -1 +@export var throttle_axis: int = 3 +@export var throttle_invert: bool = true +@export var throttle_signed: bool = true +@export var throttle_device_id: int = -1 +@export var strafe_axis: int = 4 +@export var strafe_invert: bool = false +@export var strafe_device_id: int = -1 +@export var lift_axis: int = 5 +@export var lift_invert: bool = false +@export var lift_device_id: int = -1 + +@export var stick_deadzone: float = 0.08 +@export var throttle_deadzone: float = 0.02 + +@export var max_thrust: float = 7500.0 +@export var strafe_thrust: float = 3500.0 +@export var lift_thrust: float = 3500.0 +@export var torque_strength: float = 2000.0 + +@export var throttle_response: float = 3.0 +@export var stick_response: float = 6.0 +@export var stick_expo: float = 1.6 +@export var max_speed: float = 180.0 +@export var max_angular_speed: float = 2.6 + +@export var vector_line_path: NodePath = NodePath("DisplacementVector") +@export var vector_scale: float = 0.05 +@export var max_vector_length: float = 20.0 + +var _device_id: int = -1 +var _current_throttle: float = 0.0 +var _vector_line: Node +var _axis_state := { + "roll": 0.0, + "pitch": 0.0, + "yaw": 0.0, + "throttle": 0.0, + "strafe": 0.0, + "lift": 0.0, +} +var _axis_smoothed := { + "roll": 0.0, + "pitch": 0.0, + "yaw": 0.0, + "strafe": 0.0, + "lift": 0.0, +} + +func _ready() -> void: + _vector_line = get_node_or_null(vector_line_path) + _device_id = _resolve_joypad() + if _device_id == -1: + push_warning("No HOTAS detected. Connect the X56 or set joy_device_id.") + +func _physics_process(delta: float) -> void: + _device_id = _resolve_joypad() + if _device_id == -1: + _update_vector_line() + return + + var roll = _shape_axis(_read_axis(_resolve_axis_device(roll_device_id), roll_axis, stick_deadzone, roll_invert)) + var pitch = _shape_axis(_read_axis(_resolve_axis_device(pitch_device_id), pitch_axis, stick_deadzone, pitch_invert)) + var yaw = _shape_axis(_read_axis(_resolve_axis_device(yaw_device_id), yaw_axis, stick_deadzone, yaw_invert)) + var strafe = _shape_axis(_read_axis(_resolve_axis_device(strafe_device_id), strafe_axis, stick_deadzone, strafe_invert)) + var lift = _shape_axis(_read_axis(_resolve_axis_device(lift_device_id), lift_axis, stick_deadzone, lift_invert)) + var raw_throttle = _read_axis(_resolve_axis_device(throttle_device_id), throttle_axis, throttle_deadzone, throttle_invert) + + var target_throttle: float + if throttle_signed: + target_throttle = clamp(raw_throttle, -1.0, 1.0) + else: + target_throttle = clamp((raw_throttle + 1.0) * 0.5, 0.0, 1.0) + + _current_throttle = lerp( + _current_throttle, + target_throttle, + clamp(throttle_response * delta, 0.0, 1.0) + ) + + roll = _smooth_axis("roll", roll, delta) + pitch = _smooth_axis("pitch", pitch, delta) + yaw = _smooth_axis("yaw", yaw, delta) + strafe = _smooth_axis("strafe", strafe, delta) + lift = _smooth_axis("lift", lift, delta) + + _axis_state["roll"] = roll + _axis_state["pitch"] = pitch + _axis_state["yaw"] = yaw + _axis_state["throttle"] = _current_throttle + _axis_state["strafe"] = strafe + _axis_state["lift"] = lift + + var basis = global_transform.basis + var forward = -basis.z + var right = basis.x + var up = basis.y + + var thrust_force = forward * (_current_throttle * max_thrust) + var strafe_force = right * (strafe * strafe_thrust) + var lift_force = up * (lift * lift_thrust) + apply_central_force(thrust_force + strafe_force + lift_force) + + var local_angular = basis.inverse() * angular_velocity + var desired_angular = Vector3(pitch, yaw, -roll) * max_angular_speed + var torque_local = (desired_angular - local_angular) * torque_strength + apply_torque(basis * torque_local) + + if max_speed > 0.0 and linear_velocity.length() > max_speed: + linear_velocity = linear_velocity.limit_length(max_speed) + if max_angular_speed > 0.0 and angular_velocity.length() > max_angular_speed: + angular_velocity = angular_velocity.limit_length(max_angular_speed) + + _update_vector_line() + +func get_axis_debug_text() -> String: + if _device_id == -1: + return "No HOTAS detected" + return "Roll: %.2f (d%d) Pitch: %.2f (d%d) Yaw: %.2f (d%d)\nThrottle: %.2f (d%d) Strafe: %.2f (d%d) Lift: %.2f (d%d)" % [ + _axis_state["roll"], + _resolve_axis_device(roll_device_id), + _axis_state["pitch"], + _resolve_axis_device(pitch_device_id), + _axis_state["yaw"], + _resolve_axis_device(yaw_device_id), + _axis_state["throttle"], + _resolve_axis_device(throttle_device_id), + _axis_state["strafe"], + _resolve_axis_device(strafe_device_id), + _axis_state["lift"], + _resolve_axis_device(lift_device_id), + ] + +func get_joypad_id() -> int: + return _device_id + +func _resolve_joypad() -> int: + if joy_device_id >= 0: + return joy_device_id + var pads = Input.get_connected_joypads() + if pads.is_empty(): + return -1 + return pads[0] + +func _resolve_axis_device(axis_device_id: int) -> int: + if axis_device_id >= 0: + return axis_device_id + return _device_id + +func _read_axis(device_id: int, axis: int, deadzone: float, invert: bool) -> float: + if axis < 0 or device_id < 0: + return 0.0 + var value = Input.get_joy_axis(device_id, axis) + if invert: + value = -value + var magnitude = abs(value) + if magnitude <= deadzone: + return 0.0 + var scaled = (magnitude - deadzone) / (1.0 - deadzone) + return scaled * sign(value) + +func _shape_axis(value: float) -> float: + if stick_expo <= 1.0: + return value + return sign(value) * pow(abs(value), stick_expo) + +func _smooth_axis(name: String, value: float, delta: float) -> float: + if stick_response <= 0.0: + _axis_smoothed[name] = value + return value + var t = clamp(stick_response * delta, 0.0, 1.0) + _axis_smoothed[name] = lerp(_axis_smoothed[name], value, t) + return _axis_smoothed[name] + +func _update_vector_line() -> void: + if _vector_line == null: + return + if not _vector_line.has_method("set_vector"): + return + var local_velocity = global_transform.basis.inverse() * linear_velocity + var clamped = local_velocity.limit_length(max_vector_length) + _vector_line.set_vector(clamped * vector_scale) diff --git a/scripts/jet_controller.gd.uid b/scripts/jet_controller.gd.uid new file mode 100644 index 0000000..7f2267d --- /dev/null +++ b/scripts/jet_controller.gd.uid @@ -0,0 +1 @@ +uid://bs6rfdeujndbp diff --git a/scripts/network.gd b/scripts/network.gd new file mode 100644 index 0000000..6b7ba64 --- /dev/null +++ b/scripts/network.gd @@ -0,0 +1,109 @@ +# res://scripts/network.gd +extends Node + +signal connected(room_id: String) +signal disconnected(reason: String) +signal state_changed() +signal message_received(type: String, payload: Variant) +signal error(msg: String) + +const colyseus := preload("res://addons/godot_colyseus/lib/colyseus.gd") +const RoomState := preload("res://scripts/room_state.gd") +const Room := preload("res://addons/godot_colyseus/lib/room.gd") + +var client: colyseus.Client +var room: Room +var state: RoomState + +var _connecting := false +var _endpoint := "" + +#func is_connected() -> bool: + #return room != null + +func connect_and_join(endpoint: String, room_name: String = "my_room") -> void: + if _connecting: + return + if room != null: + emit_signal("error", "Already connected.") + return + + _connecting = true + _endpoint = endpoint + + client = colyseus.Client.new(endpoint) + + var promise = client.join_or_create(RoomState, room_name) + await promise.completed + _connecting = false + + if promise.get_state() == promise.State.Failed: + var msg := "Join failed: %s" % str(promise.get_error()) + emit_signal("error", msg) + return + + # old addon: result is a property + room = promise.result + if room == null: + emit_signal("error", "Join succeeded but room is null (promise.result).") + return + + state = room.get_state() as RoomState + if state == null: + emit_signal("error", "Joined but could not cast state to RoomState.") + return + + # --- wire listeners --- + room.on_state_change.on(Callable(self, "_on_state_change")) + + # if addon supports on_leave / on_error, hook them too (safe-guarded) + if "on_leave" in room: + room.on_leave.on(Callable(self, "_on_room_left")) + if "on_error" in room: + room.on_error.on(Callable(self, "_on_room_error")) + + emit_signal("connected", room.room_id) + +func leave() -> void: + if room == null: + return + # some addons use room.leave(), some room.disconnect() + if room.has_method("leave"): + room.leave() + elif room.has_method("disconnect"): + pass + #room.disconnect() + _cleanup("left") + +func send(type: String, payload: Variant = null) -> void: + if room == null: + emit_signal("error", "Cannot send, not connected.") + return + room.send(type, payload) + +func listen_message(type: String) -> void: + if room == null: + emit_signal("error", "Cannot listen, not connected.") + return + room.on_message(type).on(Callable(self, "_on_message").bind(type)) + +func reconnect(room_name: String = "my_room") -> void: + _cleanup("reconnect") + await connect_and_join(_endpoint, room_name) + +func _on_state_change(_new_state = null) -> void: + emit_signal("state_changed") + +func _on_message(payload: Variant, type: String) -> void: + emit_signal("message_received", type, payload) + +func _on_room_left(code = null) -> void: + _cleanup("room_left %s" % str(code)) + +func _on_room_error(code = null, message = null) -> void: + _cleanup("room_error %s %s" % [str(code), str(message)]) + +func _cleanup(reason: String) -> void: + room = null + state = null + emit_signal("disconnected", reason) diff --git a/scripts/network.gd.uid b/scripts/network.gd.uid new file mode 100644 index 0000000..d808c17 --- /dev/null +++ b/scripts/network.gd.uid @@ -0,0 +1 @@ +uid://d0amrs41uhwql diff --git a/scripts/room_state.gd b/scripts/room_state.gd new file mode 100644 index 0000000..b4ddd8b --- /dev/null +++ b/scripts/room_state.gd @@ -0,0 +1,10 @@ +# res://scripts/room_state.gd +extends "res://addons/godot_colyseus/lib/schema.gd" +class_name RoomState + +var mySynchronizedProperty: String = "Hello world" + +static func define_fields(): + return [ + Field.new("mySynchronizedProperty", Types.STRING), + ] diff --git a/scripts/room_state.gd.uid b/scripts/room_state.gd.uid new file mode 100644 index 0000000..8c17f8f --- /dev/null +++ b/scripts/room_state.gd.uid @@ -0,0 +1 @@ +uid://dlh5cvfm6xapu diff --git a/scripts/vector_line_3d.gd b/scripts/vector_line_3d.gd new file mode 100644 index 0000000..4220fe9 --- /dev/null +++ b/scripts/vector_line_3d.gd @@ -0,0 +1,22 @@ +extends MeshInstance3D + +@export var color: Color = Color(0.1, 0.8, 1.0, 0.9) + +var _mesh: ImmediateMesh +var _material: StandardMaterial3D + +func _ready() -> void: + _mesh = ImmediateMesh.new() + mesh = _mesh + _material = StandardMaterial3D.new() + _material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + _material.albedo_color = color + +func set_vector(end_point: Vector3) -> void: + if _mesh == null: + return + _mesh.clear_surfaces() + _mesh.surface_begin(Mesh.PRIMITIVE_LINES, _material) + _mesh.surface_add_vertex(Vector3.ZERO) + _mesh.surface_add_vertex(end_point) + _mesh.surface_end() diff --git a/scripts/vector_line_3d.gd.uid b/scripts/vector_line_3d.gd.uid new file mode 100644 index 0000000..477a518 --- /dev/null +++ b/scripts/vector_line_3d.gd.uid @@ -0,0 +1 @@ +uid://3ierf3uelmp8