├── .gitignore ├── LICENSE ├── README.md ├── client ├── .gitattributes ├── .gitignore ├── addons │ └── protobuf │ │ ├── parser.gd │ │ ├── parser.gd.uid │ │ ├── plugin.cfg │ │ ├── protobuf_cmdln.gd │ │ ├── protobuf_cmdln.gd.uid │ │ ├── protobuf_core.gd │ │ ├── protobuf_core.gd.uid │ │ ├── protobuf_ui.gd │ │ ├── protobuf_ui.gd.uid │ │ ├── protobuf_ui_dock.gd │ │ ├── protobuf_ui_dock.gd.uid │ │ ├── protobuf_ui_dock.tscn │ │ ├── protobuf_util.gd │ │ └── protobuf_util.gd.uid ├── assets │ ├── SmileySans-Oblique.ttf │ ├── SmileySans-Oblique.ttf.import │ ├── background.svg │ ├── background.svg.import │ ├── background_effect.gdshader │ ├── background_effect.gdshader.uid │ ├── blob.png │ ├── blob.png.import │ ├── icon.png │ ├── icon.png.import │ ├── rainbow.gdshader │ └── rainbow.gdshader.uid ├── component │ ├── actor │ │ ├── actor.gd │ │ ├── actor.gd.uid │ │ └── actor.tscn │ ├── leaderboard │ │ ├── leaderboard.gd │ │ ├── leaderboard.gd.uid │ │ └── leaderboard.tscn │ ├── logger │ │ ├── logger.gd │ │ ├── logger.gd.uid │ │ └── logger.tscn │ ├── ping │ │ ├── ping.gd │ │ ├── ping.gd.uid │ │ └── ping.tscn │ ├── rush_particles │ │ └── rush_particles.tscn │ └── spore │ │ ├── spore.gd │ │ ├── spore.gd.uid │ │ └── spore.tscn ├── export_presets.cfg ├── global │ ├── global.gd │ ├── global.gd.uid │ ├── vfx_pre_compile.tscn │ ├── ws_client.gd │ └── ws_client.gd.uid ├── project.godot ├── proto.gd ├── proto.gd.uid └── view │ ├── connecting │ ├── connecting.gd │ ├── connecting.gd.uid │ └── connecting.tscn │ ├── game │ ├── game.gd │ ├── game.gd.uid │ └── game.tscn │ ├── leaderboard_view │ ├── leaderboard_view.gd │ ├── leaderboard_view.gd.uid │ └── leaderboard_view.tscn │ ├── login │ ├── login.gd │ ├── login.gd.uid │ └── login.tscn │ └── register │ ├── register.gd │ ├── register.gd.uid │ └── register.tscn ├── proto └── packet.proto └── server ├── .env ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── build.rs ├── cross_build_linux.bat ├── migrations └── 20250113000000_create_table.sql ├── sqlx_migrate.bat └── src ├── client_agent.rs ├── command.rs ├── db.rs ├── hub.rs ├── lib.rs ├── main.rs ├── player.rs ├── proto.rs ├── proto_util.rs ├── spore.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /dist-web 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025-present Jerry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Agarust 2 | 3 | _Agarust_ is a server-authoritative real-time multiplayer online game powered by Godot 4 and Rust 🤖🦀 inspired by agar.io 4 | 5 | Play now on itch.io: [jerryshell.itch.io/agarust](https://jerryshell.itch.io/agarust) 6 | 7 | - Use the mouse to control the direction of movement 8 | - Press the left mouse button to sprint 9 | - Sprinting drops 20% of your mass 10 | - Players with too little mass can't sprint 11 | - You can only eat another player if the difference in mass is greater than 1.2 times 12 | - The player's mass will slowly drop over time, the higher the mass, the higher the chance of dropping 13 | - The formula for converting mass to radius: `Mass = PI * Radius * Radius` 14 | 15 | ## Tech stack 16 | 17 | - Godot 4 18 | - Rust 19 | - Protocol Buffers 20 | - WebScoket 21 | - SQLite 22 | 23 | ## Setup server 24 | 25 | ```bash 26 | cd server 27 | ``` 28 | 29 | ### Init database 30 | 31 | **Note**: You **MUST** initialise the database before you can compile the source code, for more details see: [sqlx - Compile-time verification](https://github.com/launchbadge/sqlx?tab=readme-ov-file#compile-time-verification) 32 | 33 | ```bash 34 | cargo install sqlx-cli 35 | ``` 36 | 37 | ```bash 38 | sqlx migrate run --database-url "sqlite:agarust_db.sqlite?mode=rwc" 39 | ``` 40 | 41 | ### Run 42 | 43 | ```bash 44 | cargo run 45 | ``` 46 | 47 | ## Setup client 48 | 49 | Import the `client` folder using [Godot 4](https://godotengine.org) 50 | 51 | ### Change server URL 52 | 53 | Change `debug_server_url` and `release_server_url` in [client/global/global.gd](client/global/global.gd) 54 | 55 | ## Credits 56 | 57 | - [Godot 4 + Golang MMO Tutorial Series by Tristan Batchler](https://www.tbat.me/projects/godot-golang-mmo-tutorial-series) 58 | - [Actors with Tokio by Alice Ryhl](https://draft.ryhl.io/blog/actors-with-tokio) 59 | -------------------------------------------------------------------------------- /client/.gitattributes: -------------------------------------------------------------------------------- 1 | # Normalize EOL for all files that Git considers text files. 2 | * text=auto eol=lf 3 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Godot 4+ specific ignores 2 | .godot/ 3 | /android/ 4 | *.tmp 5 | -------------------------------------------------------------------------------- /client/addons/protobuf/parser.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cutvq5ht484rt 2 | -------------------------------------------------------------------------------- /client/addons/protobuf/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | name="Godobuf" 3 | description="Google Protobuf implementation for Godot/GDScript" 4 | author="oniksan, bschug, jerry" 5 | version="0.6.0" 6 | script="protobuf_ui.gd" 7 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_cmdln.gd: -------------------------------------------------------------------------------- 1 | # 2 | # BSD 3-Clause License 3 | # 4 | # Copyright (c) 2018, Oleg Malyavkin 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # * Neither the name of the copyright holder nor the names of its 18 | # contributors may be used to endorse or promote products derived from 19 | # this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | extends SceneTree 33 | 34 | var Parser = preload("res://addons/protobuf/parser.gd") 35 | var Util = preload("res://addons/protobuf/protobuf_util.gd") 36 | 37 | 38 | func error(msg: String): 39 | push_error(msg) 40 | quit() 41 | 42 | 43 | func _init(): 44 | var arguments = {} 45 | for argument in OS.get_cmdline_args(): 46 | if argument.find("=") > -1: 47 | var key_value = argument.split("=") 48 | arguments[key_value[0].lstrip("--")] = key_value[1] 49 | 50 | if !arguments.has("input") || !arguments.has("output"): 51 | error("Expected 2 Parameters: input and output") 52 | 53 | var input_file_name = arguments["input"] 54 | var output_file_name = arguments["output"] 55 | 56 | var file = FileAccess.open(input_file_name, FileAccess.READ) 57 | if file == null: 58 | error("File: '" + input_file_name + "' not found.") 59 | 60 | var parser = Parser.new() 61 | 62 | if parser.work( 63 | Util.extract_dir(input_file_name), 64 | Util.extract_filename(input_file_name), 65 | output_file_name, 66 | "res://addons/protobuf/protobuf_core.gd" 67 | ): 68 | print("Compiled '", input_file_name, "' to '", output_file_name, "'.") 69 | else: 70 | error("Compilation failed.") 71 | 72 | quit() 73 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_cmdln.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c3xhdk0sdw17b 2 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_core.gd: -------------------------------------------------------------------------------- 1 | # 2 | # BSD 3-Clause License 3 | # 4 | # Copyright (c) 2018 - 2023, Oleg Malyavkin 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # * Neither the name of the copyright holder nor the names of its 18 | # contributors may be used to endorse or promote products derived from 19 | # this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | # DEBUG_TAB redefine this " " if you need, example: const DEBUG_TAB = "\t" 33 | 34 | const PROTO_VERSION = 0 35 | 36 | const DEBUG_TAB: String = " " 37 | 38 | enum PB_ERR { 39 | NO_ERRORS = 0, 40 | VARINT_NOT_FOUND = -1, 41 | REPEATED_COUNT_NOT_FOUND = -2, 42 | REPEATED_COUNT_MISMATCH = -3, 43 | LENGTHDEL_SIZE_NOT_FOUND = -4, 44 | LENGTHDEL_SIZE_MISMATCH = -5, 45 | PACKAGE_SIZE_MISMATCH = -6, 46 | UNDEFINED_STATE = -7, 47 | PARSE_INCOMPLETE = -8, 48 | REQUIRED_FIELDS = -9 49 | } 50 | 51 | enum PB_DATA_TYPE { 52 | INT32 = 0, 53 | SINT32 = 1, 54 | UINT32 = 2, 55 | INT64 = 3, 56 | SINT64 = 4, 57 | UINT64 = 5, 58 | BOOL = 6, 59 | ENUM = 7, 60 | FIXED32 = 8, 61 | SFIXED32 = 9, 62 | FLOAT = 10, 63 | FIXED64 = 11, 64 | SFIXED64 = 12, 65 | DOUBLE = 13, 66 | STRING = 14, 67 | BYTES = 15, 68 | MESSAGE = 16, 69 | MAP = 17 70 | } 71 | 72 | const DEFAULT_VALUES_2 = { 73 | PB_DATA_TYPE.INT32: null, 74 | PB_DATA_TYPE.SINT32: null, 75 | PB_DATA_TYPE.UINT32: null, 76 | PB_DATA_TYPE.INT64: null, 77 | PB_DATA_TYPE.SINT64: null, 78 | PB_DATA_TYPE.UINT64: null, 79 | PB_DATA_TYPE.BOOL: null, 80 | PB_DATA_TYPE.ENUM: null, 81 | PB_DATA_TYPE.FIXED32: null, 82 | PB_DATA_TYPE.SFIXED32: null, 83 | PB_DATA_TYPE.FLOAT: null, 84 | PB_DATA_TYPE.FIXED64: null, 85 | PB_DATA_TYPE.SFIXED64: null, 86 | PB_DATA_TYPE.DOUBLE: null, 87 | PB_DATA_TYPE.STRING: null, 88 | PB_DATA_TYPE.BYTES: null, 89 | PB_DATA_TYPE.MESSAGE: null, 90 | PB_DATA_TYPE.MAP: null 91 | } 92 | 93 | const DEFAULT_VALUES_3 = { 94 | PB_DATA_TYPE.INT32: 0, 95 | PB_DATA_TYPE.SINT32: 0, 96 | PB_DATA_TYPE.UINT32: 0, 97 | PB_DATA_TYPE.INT64: 0, 98 | PB_DATA_TYPE.SINT64: 0, 99 | PB_DATA_TYPE.UINT64: 0, 100 | PB_DATA_TYPE.BOOL: false, 101 | PB_DATA_TYPE.ENUM: 0, 102 | PB_DATA_TYPE.FIXED32: 0, 103 | PB_DATA_TYPE.SFIXED32: 0, 104 | PB_DATA_TYPE.FLOAT: 0.0, 105 | PB_DATA_TYPE.FIXED64: 0, 106 | PB_DATA_TYPE.SFIXED64: 0, 107 | PB_DATA_TYPE.DOUBLE: 0.0, 108 | PB_DATA_TYPE.STRING: "", 109 | PB_DATA_TYPE.BYTES: [], 110 | PB_DATA_TYPE.MESSAGE: null, 111 | PB_DATA_TYPE.MAP: [] 112 | } 113 | 114 | enum PB_TYPE { 115 | VARINT = 0, FIX64 = 1, LENGTHDEL = 2, STARTGROUP = 3, ENDGROUP = 4, FIX32 = 5, UNDEFINED = 8 116 | } 117 | 118 | enum PB_RULE { OPTIONAL = 0, REQUIRED = 1, REPEATED = 2, RESERVED = 3 } 119 | 120 | enum PB_SERVICE_STATE { FILLED = 0, UNFILLED = 1 } 121 | 122 | 123 | class PBField: 124 | func _init(a_name: String, a_type: int, a_rule: int, a_tag: int, packed: bool, a_value = null): 125 | name = a_name 126 | type = a_type 127 | rule = a_rule 128 | tag = a_tag 129 | option_packed = packed 130 | value = a_value 131 | 132 | var name: String 133 | var type: int 134 | var rule: int 135 | var tag: int 136 | var option_packed: bool 137 | var value 138 | var is_map_field: bool = false 139 | var option_default: bool = false 140 | 141 | 142 | class PBTypeTag: 143 | var ok: bool = false 144 | var type: int 145 | var tag: int 146 | var offset: int 147 | 148 | 149 | class PBServiceField: 150 | var field: PBField 151 | var func_ref = null 152 | var state: int = PB_SERVICE_STATE.UNFILLED 153 | 154 | 155 | class PBPacker: 156 | static func convert_signed(n: int) -> int: 157 | if n < -2147483648: 158 | return (n << 1) ^ (n >> 63) 159 | else: 160 | return (n << 1) ^ (n >> 31) 161 | 162 | static func deconvert_signed(n: int) -> int: 163 | if n & 0x01: 164 | return ~(n >> 1) 165 | else: 166 | return n >> 1 167 | 168 | static func pack_varint(value) -> PackedByteArray: 169 | var varint: PackedByteArray = PackedByteArray() 170 | if typeof(value) == TYPE_BOOL: 171 | if value: 172 | value = 1 173 | else: 174 | value = 0 175 | for _i in range(9): 176 | var b = value & 0x7F 177 | value >>= 7 178 | if value: 179 | varint.append(b | 0x80) 180 | else: 181 | varint.append(b) 182 | break 183 | if varint.size() == 9 && (varint[8] & 0x80 != 0): 184 | varint.append(0x01) 185 | return varint 186 | 187 | static func pack_bytes(value, count: int, data_type: int) -> PackedByteArray: 188 | var bytes: PackedByteArray = PackedByteArray() 189 | if data_type == PB_DATA_TYPE.FLOAT: 190 | var spb: StreamPeerBuffer = StreamPeerBuffer.new() 191 | spb.put_float(value) 192 | bytes = spb.get_data_array() 193 | elif data_type == PB_DATA_TYPE.DOUBLE: 194 | var spb: StreamPeerBuffer = StreamPeerBuffer.new() 195 | spb.put_double(value) 196 | bytes = spb.get_data_array() 197 | else: 198 | for _i in range(count): 199 | bytes.append(value & 0xFF) 200 | value >>= 8 201 | return bytes 202 | 203 | static func unpack_bytes(bytes: PackedByteArray, index: int, count: int, data_type: int): 204 | var value = 0 205 | if data_type == PB_DATA_TYPE.FLOAT: 206 | var spb: StreamPeerBuffer = StreamPeerBuffer.new() 207 | for i in range(index, count + index): 208 | spb.put_u8(bytes[i]) 209 | spb.seek(0) 210 | value = spb.get_float() 211 | elif data_type == PB_DATA_TYPE.DOUBLE: 212 | var spb: StreamPeerBuffer = StreamPeerBuffer.new() 213 | for i in range(index, count + index): 214 | spb.put_u8(bytes[i]) 215 | spb.seek(0) 216 | value = spb.get_double() 217 | else: 218 | for i in range(index + count - 1, index - 1, -1): 219 | value |= (bytes[i] & 0xFF) 220 | if i != index: 221 | value <<= 8 222 | return value 223 | 224 | static func unpack_varint(varint_bytes) -> int: 225 | var value: int = 0 226 | for i in range(varint_bytes.size() - 1, -1, -1): 227 | value |= varint_bytes[i] & 0x7F 228 | if i != 0: 229 | value <<= 7 230 | return value 231 | 232 | static func pack_type_tag(type: int, tag: int) -> PackedByteArray: 233 | return pack_varint((tag << 3) | type) 234 | 235 | static func isolate_varint(bytes: PackedByteArray, index: int) -> PackedByteArray: 236 | var result: PackedByteArray = PackedByteArray() 237 | for i in range(index, bytes.size()): 238 | result.append(bytes[i]) 239 | if !(bytes[i] & 0x80): 240 | break 241 | return result 242 | 243 | static func unpack_type_tag(bytes: PackedByteArray, index: int) -> PBTypeTag: 244 | var varint_bytes: PackedByteArray = isolate_varint(bytes, index) 245 | var result: PBTypeTag = PBTypeTag.new() 246 | if varint_bytes.size() != 0: 247 | result.ok = true 248 | result.offset = varint_bytes.size() 249 | var unpacked: int = unpack_varint(varint_bytes) 250 | result.type = unpacked & 0x07 251 | result.tag = unpacked >> 3 252 | return result 253 | 254 | static func pack_length_delimeted( 255 | type: int, tag: int, bytes: PackedByteArray 256 | ) -> PackedByteArray: 257 | var result: PackedByteArray = pack_type_tag(type, tag) 258 | result.append_array(pack_varint(bytes.size())) 259 | result.append_array(bytes) 260 | return result 261 | 262 | static func pb_type_from_data_type(data_type: int) -> int: 263 | if ( 264 | data_type == PB_DATA_TYPE.INT32 265 | || data_type == PB_DATA_TYPE.SINT32 266 | || data_type == PB_DATA_TYPE.UINT32 267 | || data_type == PB_DATA_TYPE.INT64 268 | || data_type == PB_DATA_TYPE.SINT64 269 | || data_type == PB_DATA_TYPE.UINT64 270 | || data_type == PB_DATA_TYPE.BOOL 271 | || data_type == PB_DATA_TYPE.ENUM 272 | ): 273 | return PB_TYPE.VARINT 274 | elif ( 275 | data_type == PB_DATA_TYPE.FIXED32 276 | || data_type == PB_DATA_TYPE.SFIXED32 277 | || data_type == PB_DATA_TYPE.FLOAT 278 | ): 279 | return PB_TYPE.FIX32 280 | elif ( 281 | data_type == PB_DATA_TYPE.FIXED64 282 | || data_type == PB_DATA_TYPE.SFIXED64 283 | || data_type == PB_DATA_TYPE.DOUBLE 284 | ): 285 | return PB_TYPE.FIX64 286 | elif ( 287 | data_type == PB_DATA_TYPE.STRING 288 | || data_type == PB_DATA_TYPE.BYTES 289 | || data_type == PB_DATA_TYPE.MESSAGE 290 | || data_type == PB_DATA_TYPE.MAP 291 | ): 292 | return PB_TYPE.LENGTHDEL 293 | else: 294 | return PB_TYPE.UNDEFINED 295 | 296 | static func pack_field(field: PBField) -> PackedByteArray: 297 | var type: int = pb_type_from_data_type(field.type) 298 | var type_copy: int = type 299 | if field.rule == PB_RULE.REPEATED && field.option_packed: 300 | type = PB_TYPE.LENGTHDEL 301 | var head: PackedByteArray = pack_type_tag(type, field.tag) 302 | var data: PackedByteArray = PackedByteArray() 303 | if type == PB_TYPE.VARINT: 304 | var value 305 | if field.rule == PB_RULE.REPEATED: 306 | for v in field.value: 307 | data.append_array(head) 308 | if field.type == PB_DATA_TYPE.SINT32 || field.type == PB_DATA_TYPE.SINT64: 309 | value = convert_signed(v) 310 | else: 311 | value = v 312 | data.append_array(pack_varint(value)) 313 | return data 314 | else: 315 | if field.type == PB_DATA_TYPE.SINT32 || field.type == PB_DATA_TYPE.SINT64: 316 | value = convert_signed(field.value) 317 | else: 318 | value = field.value 319 | data = pack_varint(value) 320 | elif type == PB_TYPE.FIX32: 321 | if field.rule == PB_RULE.REPEATED: 322 | for v in field.value: 323 | data.append_array(head) 324 | data.append_array(pack_bytes(v, 4, field.type)) 325 | return data 326 | else: 327 | data.append_array(pack_bytes(field.value, 4, field.type)) 328 | elif type == PB_TYPE.FIX64: 329 | if field.rule == PB_RULE.REPEATED: 330 | for v in field.value: 331 | data.append_array(head) 332 | data.append_array(pack_bytes(v, 8, field.type)) 333 | return data 334 | else: 335 | data.append_array(pack_bytes(field.value, 8, field.type)) 336 | elif type == PB_TYPE.LENGTHDEL: 337 | if field.rule == PB_RULE.REPEATED: 338 | if type_copy == PB_TYPE.VARINT: 339 | if field.type == PB_DATA_TYPE.SINT32 || field.type == PB_DATA_TYPE.SINT64: 340 | var signed_value: int 341 | for v in field.value: 342 | signed_value = convert_signed(v) 343 | data.append_array(pack_varint(signed_value)) 344 | else: 345 | for v in field.value: 346 | data.append_array(pack_varint(v)) 347 | return pack_length_delimeted(type, field.tag, data) 348 | elif type_copy == PB_TYPE.FIX32: 349 | for v in field.value: 350 | data.append_array(pack_bytes(v, 4, field.type)) 351 | return pack_length_delimeted(type, field.tag, data) 352 | elif type_copy == PB_TYPE.FIX64: 353 | for v in field.value: 354 | data.append_array(pack_bytes(v, 8, field.type)) 355 | return pack_length_delimeted(type, field.tag, data) 356 | elif field.type == PB_DATA_TYPE.STRING: 357 | for v in field.value: 358 | var obj = v.to_utf8_buffer() 359 | data.append_array(pack_length_delimeted(type, field.tag, obj)) 360 | return data 361 | elif field.type == PB_DATA_TYPE.BYTES: 362 | for v in field.value: 363 | data.append_array(pack_length_delimeted(type, field.tag, v)) 364 | return data 365 | elif typeof(field.value[0]) == TYPE_OBJECT: 366 | for v in field.value: 367 | var obj: PackedByteArray = v.to_bytes() 368 | data.append_array(pack_length_delimeted(type, field.tag, obj)) 369 | return data 370 | else: 371 | if field.type == PB_DATA_TYPE.STRING: 372 | var str_bytes: PackedByteArray = field.value.to_utf8_buffer() 373 | if PROTO_VERSION == 2 || (PROTO_VERSION == 3 && str_bytes.size() > 0): 374 | data.append_array(str_bytes) 375 | return pack_length_delimeted(type, field.tag, data) 376 | if field.type == PB_DATA_TYPE.BYTES: 377 | if PROTO_VERSION == 2 || (PROTO_VERSION == 3 && field.value.size() > 0): 378 | data.append_array(field.value) 379 | return pack_length_delimeted(type, field.tag, data) 380 | elif typeof(field.value) == TYPE_OBJECT: 381 | var obj: PackedByteArray = field.value.to_bytes() 382 | if obj.size() > 0: 383 | data.append_array(obj) 384 | return pack_length_delimeted(type, field.tag, data) 385 | else: 386 | pass 387 | if data.size() > 0: 388 | head.append_array(data) 389 | return head 390 | else: 391 | return data 392 | 393 | static func skip_unknown_field(bytes: PackedByteArray, offset: int, type: int) -> int: 394 | if type == PB_TYPE.VARINT: 395 | return offset + isolate_varint(bytes, offset).size() 396 | if type == PB_TYPE.FIX64: 397 | return offset + 8 398 | if type == PB_TYPE.LENGTHDEL: 399 | var length_bytes: PackedByteArray = isolate_varint(bytes, offset) 400 | var length: int = unpack_varint(length_bytes) 401 | return offset + length_bytes.size() + length 402 | if type == PB_TYPE.FIX32: 403 | return offset + 4 404 | return PB_ERR.UNDEFINED_STATE 405 | 406 | static func unpack_field( 407 | bytes: PackedByteArray, offset: int, field: PBField, type: int, message_func_ref 408 | ) -> int: 409 | if field.rule == PB_RULE.REPEATED && type != PB_TYPE.LENGTHDEL && field.option_packed: 410 | var count = isolate_varint(bytes, offset) 411 | if count.size() > 0: 412 | offset += count.size() 413 | count = unpack_varint(count) 414 | if type == PB_TYPE.VARINT: 415 | var val 416 | var counter = offset + count 417 | while offset < counter: 418 | val = isolate_varint(bytes, offset) 419 | if val.size() > 0: 420 | offset += val.size() 421 | val = unpack_varint(val) 422 | if ( 423 | field.type == PB_DATA_TYPE.SINT32 424 | || field.type == PB_DATA_TYPE.SINT64 425 | ): 426 | val = deconvert_signed(val) 427 | elif field.type == PB_DATA_TYPE.BOOL: 428 | if val: 429 | val = true 430 | else: 431 | val = false 432 | field.value.append(val) 433 | else: 434 | return PB_ERR.REPEATED_COUNT_MISMATCH 435 | return offset 436 | elif type == PB_TYPE.FIX32 || type == PB_TYPE.FIX64: 437 | var type_size 438 | if type == PB_TYPE.FIX32: 439 | type_size = 4 440 | else: 441 | type_size = 8 442 | var val 443 | var counter = offset + count 444 | while offset < counter: 445 | if (offset + type_size) > bytes.size(): 446 | return PB_ERR.REPEATED_COUNT_MISMATCH 447 | val = unpack_bytes(bytes, offset, type_size, field.type) 448 | offset += type_size 449 | field.value.append(val) 450 | return offset 451 | else: 452 | return PB_ERR.REPEATED_COUNT_NOT_FOUND 453 | else: 454 | if type == PB_TYPE.VARINT: 455 | var val = isolate_varint(bytes, offset) 456 | if val.size() > 0: 457 | offset += val.size() 458 | val = unpack_varint(val) 459 | if field.type == PB_DATA_TYPE.SINT32 || field.type == PB_DATA_TYPE.SINT64: 460 | val = deconvert_signed(val) 461 | elif field.type == PB_DATA_TYPE.BOOL: 462 | if val: 463 | val = true 464 | else: 465 | val = false 466 | if field.rule == PB_RULE.REPEATED: 467 | field.value.append(val) 468 | else: 469 | field.value = val 470 | else: 471 | return PB_ERR.VARINT_NOT_FOUND 472 | return offset 473 | elif type == PB_TYPE.FIX32 || type == PB_TYPE.FIX64: 474 | var type_size 475 | if type == PB_TYPE.FIX32: 476 | type_size = 4 477 | else: 478 | type_size = 8 479 | var val 480 | if (offset + type_size) > bytes.size(): 481 | return PB_ERR.REPEATED_COUNT_MISMATCH 482 | val = unpack_bytes(bytes, offset, type_size, field.type) 483 | offset += type_size 484 | if field.rule == PB_RULE.REPEATED: 485 | field.value.append(val) 486 | else: 487 | field.value = val 488 | return offset 489 | elif type == PB_TYPE.LENGTHDEL: 490 | var inner_size = isolate_varint(bytes, offset) 491 | if inner_size.size() > 0: 492 | offset += inner_size.size() 493 | inner_size = unpack_varint(inner_size) 494 | if inner_size >= 0: 495 | if inner_size + offset > bytes.size(): 496 | return PB_ERR.LENGTHDEL_SIZE_MISMATCH 497 | if message_func_ref != null: 498 | var message = message_func_ref.call() 499 | if inner_size > 0: 500 | var sub_offset = message.from_bytes( 501 | bytes, offset, inner_size + offset 502 | ) 503 | if sub_offset > 0: 504 | if sub_offset - offset >= inner_size: 505 | offset = sub_offset 506 | return offset 507 | else: 508 | return PB_ERR.LENGTHDEL_SIZE_MISMATCH 509 | return sub_offset 510 | else: 511 | return offset 512 | elif field.type == PB_DATA_TYPE.STRING: 513 | var str_bytes: PackedByteArray = PackedByteArray() 514 | for i in range(offset, inner_size + offset): 515 | str_bytes.append(bytes[i]) 516 | if field.rule == PB_RULE.REPEATED: 517 | field.value.append(str_bytes.get_string_from_utf8()) 518 | else: 519 | field.value = str_bytes.get_string_from_utf8() 520 | return offset + inner_size 521 | elif field.type == PB_DATA_TYPE.BYTES: 522 | var val_bytes: PackedByteArray = PackedByteArray() 523 | for i in range(offset, inner_size + offset): 524 | val_bytes.append(bytes[i]) 525 | if field.rule == PB_RULE.REPEATED: 526 | field.value.append(val_bytes) 527 | else: 528 | field.value = val_bytes 529 | return offset + inner_size 530 | else: 531 | return PB_ERR.LENGTHDEL_SIZE_NOT_FOUND 532 | else: 533 | return PB_ERR.LENGTHDEL_SIZE_NOT_FOUND 534 | return PB_ERR.UNDEFINED_STATE 535 | 536 | static func unpack_message(data, bytes: PackedByteArray, offset: int, limit: int) -> int: 537 | while true: 538 | var tt: PBTypeTag = unpack_type_tag(bytes, offset) 539 | if tt.ok: 540 | offset += tt.offset 541 | if data.has(tt.tag): 542 | var service: PBServiceField = data[tt.tag] 543 | var type: int = pb_type_from_data_type(service.field.type) 544 | if ( 545 | type == tt.type 546 | || ( 547 | tt.type == PB_TYPE.LENGTHDEL 548 | && service.field.rule == PB_RULE.REPEATED 549 | && service.field.option_packed 550 | ) 551 | ): 552 | var res: int = unpack_field( 553 | bytes, offset, service.field, type, service.func_ref 554 | ) 555 | if res > 0: 556 | service.state = PB_SERVICE_STATE.FILLED 557 | offset = res 558 | if offset == limit: 559 | return offset 560 | elif offset > limit: 561 | return PB_ERR.PACKAGE_SIZE_MISMATCH 562 | elif res < 0: 563 | return res 564 | else: 565 | break 566 | else: 567 | var res: int = skip_unknown_field(bytes, offset, tt.type) 568 | if res > 0: 569 | offset = res 570 | if offset == limit: 571 | return offset 572 | elif offset > limit: 573 | return PB_ERR.PACKAGE_SIZE_MISMATCH 574 | elif res < 0: 575 | return res 576 | else: 577 | break 578 | else: 579 | return offset 580 | return PB_ERR.UNDEFINED_STATE 581 | 582 | static func pack_message(data) -> PackedByteArray: 583 | var DEFAULT_VALUES 584 | if PROTO_VERSION == 2: 585 | DEFAULT_VALUES = DEFAULT_VALUES_2 586 | elif PROTO_VERSION == 3: 587 | DEFAULT_VALUES = DEFAULT_VALUES_3 588 | var result: PackedByteArray = PackedByteArray() 589 | var keys: Array = data.keys() 590 | keys.sort() 591 | for i in keys: 592 | if data[i].field.value != null: 593 | if ( 594 | data[i].state == PB_SERVICE_STATE.UNFILLED 595 | && !data[i].field.is_map_field 596 | && typeof(data[i].field.value) == typeof(DEFAULT_VALUES[data[i].field.type]) 597 | && data[i].field.value == DEFAULT_VALUES[data[i].field.type] 598 | ): 599 | continue 600 | elif data[i].field.rule == PB_RULE.REPEATED && data[i].field.value.size() == 0: 601 | continue 602 | result.append_array(pack_field(data[i].field)) 603 | elif data[i].field.rule == PB_RULE.REQUIRED: 604 | print("Error: required field is not filled: Tag:", data[i].field.tag) 605 | return PackedByteArray() 606 | return result 607 | 608 | static func check_required(data) -> bool: 609 | var keys: Array = data.keys() 610 | for i in keys: 611 | if data[i].field.rule == PB_RULE.REQUIRED && data[i].state == PB_SERVICE_STATE.UNFILLED: 612 | return false 613 | return true 614 | 615 | static func construct_map(key_values): 616 | var result = {} 617 | for kv in key_values: 618 | result[kv.get_key()] = kv.get_value() 619 | return result 620 | 621 | static func tabulate(text: String, nesting: int) -> String: 622 | var tab: String = "" 623 | for _i in range(nesting): 624 | tab += DEBUG_TAB 625 | return tab + text 626 | 627 | static func value_to_string(value, field: PBField, nesting: int) -> String: 628 | var result: String = "" 629 | var text: String 630 | if field.type == PB_DATA_TYPE.MESSAGE: 631 | result += "{" 632 | nesting += 1 633 | text = message_to_string(value.data, nesting) 634 | if text != "": 635 | result += "\n" + text 636 | nesting -= 1 637 | result += tabulate("}", nesting) 638 | else: 639 | nesting -= 1 640 | result += "}" 641 | elif field.type == PB_DATA_TYPE.BYTES: 642 | result += "<" 643 | for i in range(value.size()): 644 | result += str(value[i]) 645 | if i != (value.size() - 1): 646 | result += ", " 647 | result += ">" 648 | elif field.type == PB_DATA_TYPE.STRING: 649 | result += '"' + value + '"' 650 | elif field.type == PB_DATA_TYPE.ENUM: 651 | result += "ENUM::" + str(value) 652 | else: 653 | result += str(value) 654 | return result 655 | 656 | static func field_to_string(field: PBField, nesting: int) -> String: 657 | var result: String = tabulate(field.name + ": ", nesting) 658 | if field.type == PB_DATA_TYPE.MAP: 659 | if field.value.size() > 0: 660 | result += "(\n" 661 | nesting += 1 662 | for i in range(field.value.size()): 663 | var local_key_value = field.value[i].data[1].field 664 | result += ( 665 | tabulate( 666 | value_to_string(local_key_value.value, local_key_value, nesting), 667 | nesting 668 | ) 669 | + ": " 670 | ) 671 | local_key_value = field.value[i].data[2].field 672 | result += value_to_string(local_key_value.value, local_key_value, nesting) 673 | if i != (field.value.size() - 1): 674 | result += "," 675 | result += "\n" 676 | nesting -= 1 677 | result += tabulate(")", nesting) 678 | else: 679 | result += "()" 680 | elif field.rule == PB_RULE.REPEATED: 681 | if field.value.size() > 0: 682 | result += "[\n" 683 | nesting += 1 684 | for i in range(field.value.size()): 685 | result += tabulate(str(i) + ": ", nesting) 686 | result += value_to_string(field.value[i], field, nesting) 687 | if i != (field.value.size() - 1): 688 | result += "," 689 | result += "\n" 690 | nesting -= 1 691 | result += tabulate("]", nesting) 692 | else: 693 | result += "[]" 694 | else: 695 | result += value_to_string(field.value, field, nesting) 696 | result += ";\n" 697 | return result 698 | 699 | static func message_to_string(data, nesting: int = 0) -> String: 700 | var DEFAULT_VALUES 701 | if PROTO_VERSION == 2: 702 | DEFAULT_VALUES = DEFAULT_VALUES_2 703 | elif PROTO_VERSION == 3: 704 | DEFAULT_VALUES = DEFAULT_VALUES_3 705 | var result: String = "" 706 | var keys: Array = data.keys() 707 | keys.sort() 708 | for i in keys: 709 | if data[i].field.value != null: 710 | if ( 711 | data[i].state == PB_SERVICE_STATE.UNFILLED 712 | && !data[i].field.is_map_field 713 | && typeof(data[i].field.value) == typeof(DEFAULT_VALUES[data[i].field.type]) 714 | && data[i].field.value == DEFAULT_VALUES[data[i].field.type] 715 | ): 716 | continue 717 | elif data[i].field.rule == PB_RULE.REPEATED && data[i].field.value.size() == 0: 718 | continue 719 | result += field_to_string(data[i].field, nesting) 720 | elif data[i].field.rule == PB_RULE.REQUIRED: 721 | result += data[i].field.name + ": " + "error" 722 | return result 723 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_core.gd.uid: -------------------------------------------------------------------------------- 1 | uid://dy5xl2y6fg246 2 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_ui.gd: -------------------------------------------------------------------------------- 1 | # 2 | # BSD 3-Clause License 3 | # 4 | # Copyright (c) 2018, Oleg Malyavkin 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # * Neither the name of the copyright holder nor the names of its 18 | # contributors may be used to endorse or promote products derived from 19 | # this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | @tool 33 | extends EditorPlugin 34 | 35 | var dock 36 | 37 | 38 | func _enter_tree(): 39 | # Initialization of the plugin goes here 40 | # First load the dock scene and instance it: 41 | dock = preload("res://addons/protobuf/protobuf_ui_dock.tscn").instantiate() 42 | 43 | # Add the loaded scene to the docks: 44 | add_control_to_dock(DOCK_SLOT_LEFT_BR, dock) 45 | # Note that LEFT_UL means the left of the editor, upper-left dock 46 | 47 | 48 | func _exit_tree(): 49 | # Clean-up of the plugin goes here 50 | # Remove the scene from the docks: 51 | remove_control_from_docks(dock) # Remove the dock 52 | dock.free() # Erase the control from the memory 53 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_ui.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bsg2hj74p8po1 2 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_ui_dock.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | # 3 | # BSD 3-Clause License 4 | # 5 | # Copyright (c) 2018 - 2023, Oleg Malyavkin 6 | # All rights reserved. 7 | # 8 | # Redistribution and use in source and binary forms, with or without 9 | # modification, are permitted provided that the following conditions are met: 10 | # 11 | # * Redistributions of source code must retain the above copyright notice, this 12 | # list of conditions and the following disclaimer. 13 | # 14 | # * Redistributions in binary form must reproduce the above copyright notice, 15 | # this list of conditions and the following disclaimer in the documentation 16 | # and/or other materials provided with the distribution. 17 | # 18 | # * Neither the name of the copyright holder nor the names of its 19 | # contributors may be used to endorse or promote products derived from 20 | # this software without specific prior written permission. 21 | # 22 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | 33 | extends VBoxContainer 34 | 35 | var Parser = preload("res://addons/protobuf/parser.gd") 36 | var Util = preload("res://addons/protobuf/protobuf_util.gd") 37 | 38 | var input_file_path = null 39 | var output_file_path = null 40 | 41 | 42 | func _on_InputFileButton_pressed(): 43 | show_dialog($InputFileDialog) 44 | $InputFileDialog.invalidate() 45 | 46 | 47 | func _on_OutputFileButton_pressed(): 48 | show_dialog($OutputFileDialog) 49 | $OutputFileDialog.invalidate() 50 | 51 | 52 | func _on_InputFileDialog_file_selected(path): 53 | input_file_path = path 54 | $HBoxContainer/InputFileEdit.text = path 55 | 56 | 57 | func _on_OutputFileDialog_file_selected(path): 58 | output_file_path = path 59 | $HBoxContainer2/OutputFileEdit.text = path 60 | 61 | 62 | func show_dialog(dialog): 63 | dialog.popup_centered() 64 | 65 | 66 | func _on_CompileButton_pressed(): 67 | if input_file_path == null || output_file_path == null: 68 | show_dialog($FilesErrorAcceptDialog) 69 | return 70 | 71 | var file = FileAccess.open(input_file_path, FileAccess.READ) 72 | if file == null: 73 | print("File: '", input_file_path, "' not found.") 74 | show_dialog($FailAcceptDialog) 75 | return 76 | 77 | var parser = Parser.new() 78 | 79 | if parser.work( 80 | Util.extract_dir(input_file_path), 81 | Util.extract_filename(input_file_path), 82 | output_file_path, 83 | "res://addons/protobuf/protobuf_core.gd" 84 | ): 85 | show_dialog($SuccessAcceptDialog) 86 | else: 87 | show_dialog($FailAcceptDialog) 88 | 89 | file.close() 90 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_ui_dock.gd.uid: -------------------------------------------------------------------------------- 1 | uid://jcjaykakp8pa 2 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_ui_dock.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cinebpo0mb8ri"] 2 | 3 | [ext_resource type="Script" uid="uid://jcjaykakp8pa" path="res://addons/protobuf/protobuf_ui_dock.gd" id="1"] 4 | 5 | [node name="Godobuf" type="VBoxContainer"] 6 | offset_right = 176.0 7 | offset_bottom = 124.0 8 | size_flags_horizontal = 3 9 | size_flags_vertical = 3 10 | script = ExtResource("1") 11 | 12 | [node name="InputFileLabel" type="Label" parent="."] 13 | layout_mode = 2 14 | text = "Input Protobuf file:" 15 | 16 | [node name="HBoxContainer" type="HBoxContainer" parent="."] 17 | layout_mode = 2 18 | 19 | [node name="InputFileEdit" type="LineEdit" parent="HBoxContainer"] 20 | custom_minimum_size = Vector2(150, 0) 21 | layout_mode = 2 22 | size_flags_horizontal = 3 23 | editable = false 24 | 25 | [node name="InputFileButton" type="Button" parent="HBoxContainer"] 26 | layout_mode = 2 27 | text = "..." 28 | 29 | [node name="OutputFileButton" type="Label" parent="."] 30 | layout_mode = 2 31 | text = "Output GDScript file:" 32 | 33 | [node name="HBoxContainer2" type="HBoxContainer" parent="."] 34 | layout_mode = 2 35 | 36 | [node name="OutputFileEdit" type="LineEdit" parent="HBoxContainer2"] 37 | custom_minimum_size = Vector2(150, 0) 38 | layout_mode = 2 39 | size_flags_horizontal = 3 40 | editable = false 41 | 42 | [node name="OutputFileButton" type="Button" parent="HBoxContainer2"] 43 | layout_mode = 2 44 | text = "..." 45 | 46 | [node name="CompileButton" type="Button" parent="."] 47 | layout_mode = 2 48 | text = "Compile" 49 | 50 | [node name="InputFileDialog" type="FileDialog" parent="."] 51 | title = "Open a File" 52 | size = Vector2i(600, 600) 53 | ok_button_text = "Open" 54 | file_mode = 0 55 | access = 2 56 | filters = PackedStringArray("*.proto; Google Protobuf File") 57 | 58 | [node name="OutputFileDialog" type="FileDialog" parent="."] 59 | size = Vector2i(600, 600) 60 | access = 2 61 | filters = PackedStringArray("*.gd; GDScript") 62 | 63 | [node name="FilesErrorAcceptDialog" type="AcceptDialog" parent="."] 64 | size = Vector2i(350, 100) 65 | dialog_text = "Need select both output & input files!" 66 | 67 | [node name="SuccessAcceptDialog" type="AcceptDialog" parent="."] 68 | size = Vector2i(200, 100) 69 | dialog_text = "Compile success done." 70 | 71 | [node name="FailAcceptDialog" type="AcceptDialog" parent="."] 72 | size = Vector2i(350, 100) 73 | dialog_text = "Compile fail. See details in console output." 74 | 75 | [node name="SuccessTestDialog" type="AcceptDialog" parent="."] 76 | size = Vector2i(350, 120) 77 | dialog_text = "All tests were completed successfully. 78 | See console for details." 79 | 80 | [node name="FailTestDialog" type="AcceptDialog" parent="."] 81 | size = Vector2i(300, 120) 82 | dialog_text = "Errors occurred while running tests! 83 | See console for details." 84 | 85 | [connection signal="pressed" from="HBoxContainer/InputFileButton" to="." method="_on_InputFileButton_pressed"] 86 | [connection signal="pressed" from="HBoxContainer2/OutputFileButton" to="." method="_on_OutputFileButton_pressed"] 87 | [connection signal="pressed" from="CompileButton" to="." method="_on_CompileButton_pressed"] 88 | [connection signal="file_selected" from="InputFileDialog" to="." method="_on_InputFileDialog_file_selected"] 89 | [connection signal="file_selected" from="OutputFileDialog" to="." method="_on_OutputFileDialog_file_selected"] 90 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_util.gd: -------------------------------------------------------------------------------- 1 | # 2 | # BSD 3-Clause License 3 | # 4 | # Copyright (c) 2018, Oleg Malyavkin 5 | # All rights reserved. 6 | # 7 | # Redistribution and use in source and binary forms, with or without 8 | # modification, are permitted provided that the following conditions are met: 9 | # 10 | # * Redistributions of source code must retain the above copyright notice, this 11 | # list of conditions and the following disclaimer. 12 | # 13 | # * Redistributions in binary form must reproduce the above copyright notice, 14 | # this list of conditions and the following disclaimer in the documentation 15 | # and/or other materials provided with the distribution. 16 | # 17 | # * Neither the name of the copyright holder nor the names of its 18 | # contributors may be used to endorse or promote products derived from 19 | # this software without specific prior written permission. 20 | # 21 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | 32 | static func extract_dir(file_path): 33 | var parts = file_path.split("/", false) 34 | parts.remove_at(parts.size() - 1) 35 | var path 36 | if file_path.begins_with("/"): 37 | path = "/" 38 | else: 39 | path = "" 40 | for part in parts: 41 | path += part + "/" 42 | return path 43 | 44 | 45 | static func extract_filename(file_path): 46 | var parts = file_path.split("/", false) 47 | return parts[parts.size() - 1] 48 | -------------------------------------------------------------------------------- /client/addons/protobuf/protobuf_util.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bvoslu1lqiv58 2 | -------------------------------------------------------------------------------- /client/assets/SmileySans-Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryshell/agarust/175c3330d3aa7f545d46ef0670f0f95d4b6a4cf1/client/assets/SmileySans-Oblique.ttf -------------------------------------------------------------------------------- /client/assets/SmileySans-Oblique.ttf.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="font_data_dynamic" 4 | type="FontFile" 5 | uid="uid://0gqv8bre2w8q" 6 | path="res://.godot/imported/SmileySans-Oblique.ttf-9acd062d3c2416566a4a852adb8376bb.fontdata" 7 | 8 | [deps] 9 | 10 | source_file="res://assets/SmileySans-Oblique.ttf" 11 | dest_files=["res://.godot/imported/SmileySans-Oblique.ttf-9acd062d3c2416566a4a852adb8376bb.fontdata"] 12 | 13 | [params] 14 | 15 | Rendering=null 16 | antialiasing=1 17 | generate_mipmaps=false 18 | disable_embedded_bitmaps=true 19 | multichannel_signed_distance_field=false 20 | msdf_pixel_range=8 21 | msdf_size=48 22 | allow_system_fallback=true 23 | force_autohinter=false 24 | hinting=1 25 | subpixel_positioning=1 26 | keep_rounding_remainders=true 27 | oversampling=0.0 28 | Fallbacks=null 29 | fallbacks=[] 30 | Compress=null 31 | compress=true 32 | preload=[] 33 | language_support={} 34 | script_support={} 35 | opentype_features={} 36 | -------------------------------------------------------------------------------- /client/assets/background.svg: -------------------------------------------------------------------------------- 1 |  -------------------------------------------------------------------------------- /client/assets/background.svg.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://dyo2iothubrem" 6 | path="res://.godot/imported/background.svg-ef40ca42a4262420673651b4c6a8a059.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/background.svg" 14 | dest_files=["res://.godot/imported/background.svg-ef40ca42a4262420673651b4c6a8a059.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | svg/scale=1.0 36 | editor/scale_with_editor_scale=false 37 | editor/convert_colors_with_editor_theme=false 38 | -------------------------------------------------------------------------------- /client/assets/background_effect.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform vec2 amplitutde = vec2(1.0, 0.0); 4 | uniform vec2 speed = vec2(1.0, 0.0); 5 | 6 | void fragment() { 7 | vec2 pos = mod((UV - amplitutde * sin(TIME + vec2(UV.y, UV.x) * speed)) / TEXTURE_PIXEL_SIZE, 1.0 / TEXTURE_PIXEL_SIZE) * TEXTURE_PIXEL_SIZE; 8 | COLOR = texture(TEXTURE, pos); 9 | } 10 | -------------------------------------------------------------------------------- /client/assets/background_effect.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://cuybon2vubibm 2 | -------------------------------------------------------------------------------- /client/assets/blob.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryshell/agarust/175c3330d3aa7f545d46ef0670f0f95d4b6a4cf1/client/assets/blob.png -------------------------------------------------------------------------------- /client/assets/blob.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://drda0yvn27i6h" 6 | path="res://.godot/imported/blob.png-fa0fa46bc6d8b1788699212647907671.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/blob.png" 14 | dest_files=["res://.godot/imported/blob.png-fa0fa46bc6d8b1788699212647907671.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /client/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jerryshell/agarust/175c3330d3aa7f545d46ef0670f0f95d4b6a4cf1/client/assets/icon.png -------------------------------------------------------------------------------- /client/assets/icon.png.import: -------------------------------------------------------------------------------- 1 | [remap] 2 | 3 | importer="texture" 4 | type="CompressedTexture2D" 5 | uid="uid://bw217t4x3yfx3" 6 | path="res://.godot/imported/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.ctex" 7 | metadata={ 8 | "vram_texture": false 9 | } 10 | 11 | [deps] 12 | 13 | source_file="res://assets/icon.png" 14 | dest_files=["res://.godot/imported/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.ctex"] 15 | 16 | [params] 17 | 18 | compress/mode=0 19 | compress/high_quality=false 20 | compress/lossy_quality=0.7 21 | compress/hdr_compression=1 22 | compress/normal_map=0 23 | compress/channel_pack=0 24 | mipmaps/generate=false 25 | mipmaps/limit=-1 26 | roughness/mode=0 27 | roughness/src_normal="" 28 | process/fix_alpha_border=true 29 | process/premult_alpha=false 30 | process/normal_map_invert_y=false 31 | process/hdr_as_srgb=false 32 | process/hdr_clamp_exposure=false 33 | process/size_limit=0 34 | detect_3d/compress_to=1 35 | -------------------------------------------------------------------------------- /client/assets/rainbow.gdshader: -------------------------------------------------------------------------------- 1 | shader_type canvas_item; 2 | 3 | uniform float strength: hint_range(0., 1.) = 0.3; 4 | uniform float speed: hint_range(0., 10.) = 0.5; 5 | uniform float angle: hint_range(0., 360.) = 0.; 6 | 7 | void fragment() { 8 | float hue = UV.x * cos(radians(angle)) - UV.y * sin(radians(angle)); 9 | hue = fract(hue + fract(TIME * speed)); 10 | float x = 1. - abs(mod(hue / (1./ 6.), 2.) - 1.); 11 | vec3 rainbow; 12 | if(hue < 1./6.){ 13 | rainbow = vec3(1., x, 0.); 14 | } else if (hue < 1./3.) { 15 | rainbow = vec3(x, 1., 0); 16 | } else if (hue < 0.5) { 17 | rainbow = vec3(0, 1., x); 18 | } else if (hue < 2./3.) { 19 | rainbow = vec3(0., x, 1.); 20 | } else if (hue < 5./6.) { 21 | rainbow = vec3(x, 0., 1.); 22 | } else { 23 | rainbow = vec3(1., 0., x); 24 | } 25 | vec4 color = texture(TEXTURE, UV); 26 | COLOR = mix(color, color * vec4(rainbow, color.a), strength); 27 | } 28 | -------------------------------------------------------------------------------- /client/assets/rainbow.gdshader.uid: -------------------------------------------------------------------------------- 1 | uid://3yy1k1mdmahd 2 | -------------------------------------------------------------------------------- /client/component/actor/actor.gd: -------------------------------------------------------------------------------- 1 | class_name Actor 2 | extends Area2D 3 | 4 | const ACTOR = preload("res://component/actor/actor.tscn") 5 | const zoom_speed := 0.1 6 | 7 | @export var rush_shader: Shader 8 | 9 | @onready var collision_shape: CollisionShape2D = %CollisionShape 10 | @onready var nameplate: Label = %Nameplate 11 | @onready var camera: Camera2D = %Camera 12 | @onready var rush_particles: GPUParticles2D = %RushParticles 13 | 14 | var connection_id: String 15 | var actor_nickname: String 16 | var start_x: float 17 | var start_y: float 18 | var start_radius: float 19 | var speed: float 20 | var color: Color 21 | var is_rushing: bool: 22 | set(new_value): 23 | is_rushing = new_value 24 | if is_rushing: 25 | material.shader = rush_shader 26 | else: 27 | material.shader = null 28 | var is_player: bool 29 | 30 | var direction: Vector2 31 | var radius: float: 32 | set(new_radius): 33 | radius = new_radius 34 | collision_shape.shape.set_radius(radius) 35 | rush_particles.process_material.emission_ring_radius = radius 36 | rush_particles.process_material.emission_ring_inner_radius = radius 37 | _update_zoom() 38 | queue_redraw() 39 | 40 | var target_zoom := 2.0 41 | var furthest_zoom_allowed := target_zoom 42 | 43 | var server_position: Vector2 44 | var server_radius: float 45 | 46 | 47 | static func instantiate( 48 | p_connection_id: String, 49 | p_actor_nickname: String, 50 | p_start_x: float, 51 | p_start_y: float, 52 | p_start_radius: float, 53 | p_speed: float, 54 | p_color: Color, 55 | p_is_rushing: bool, 56 | p_is_player: bool 57 | ) -> Actor: 58 | var actor := ACTOR.instantiate() 59 | actor.connection_id = p_connection_id 60 | actor.actor_nickname = p_actor_nickname 61 | actor.start_x = p_start_x 62 | actor.start_y = p_start_y 63 | actor.start_radius = p_start_radius 64 | actor.speed = p_speed 65 | actor.color = p_color 66 | actor.is_rushing = p_is_rushing 67 | actor.is_player = p_is_player 68 | return actor 69 | 70 | 71 | func _ready(): 72 | position.x = start_x 73 | position.y = start_y 74 | server_position = position 75 | direction = Vector2.RIGHT 76 | radius = start_radius 77 | server_radius = start_radius 78 | collision_shape.shape.radius = radius 79 | nameplate.text = actor_nickname 80 | camera.enabled = is_player 81 | 82 | 83 | func _physics_process(delta) -> void: 84 | position += direction * speed * delta 85 | server_position += direction * speed * delta 86 | position = lerp(position, server_position, 0.05) 87 | 88 | if not is_player: 89 | return 90 | 91 | var mouse_position := get_global_mouse_position() 92 | 93 | var distance_squared_to_mouse := position.distance_squared_to(mouse_position) 94 | if distance_squared_to_mouse < pow(radius, 2): 95 | return 96 | 97 | rush_particles.emitting = is_rushing 98 | if not is_equal_approx(camera.zoom.x, target_zoom): 99 | camera.zoom = lerp(camera.zoom, Vector2.ONE * target_zoom, 0.05) 100 | if not is_equal_approx(radius, server_radius): 101 | radius = lerp(radius, server_radius, 0.05) 102 | if is_player and Input.is_action_pressed("rush") and not is_rushing: 103 | var mouse_screen_position = get_viewport().get_mouse_position() 104 | if mouse_screen_position.y > 128: 105 | _rush() 106 | 107 | var direction_to_mouse := position.direction_to(mouse_position).normalized() 108 | 109 | var angle_diff = abs(direction.angle_to(direction_to_mouse)) 110 | if angle_diff > TAU / 32: 111 | direction = direction_to_mouse 112 | _send_direction_angle() 113 | 114 | 115 | func _draw() -> void: 116 | draw_circle(Vector2.ZERO, radius, color) 117 | if Global.show_server_position: 118 | draw_circle(server_position - position, radius, Color.WHEAT, false, 2.0) 119 | 120 | 121 | func _input(event): 122 | if not is_player: 123 | return 124 | 125 | if event.is_action_pressed("zoom_in"): 126 | target_zoom = min(4, target_zoom + zoom_speed) 127 | elif event.is_action_pressed("zoom_out"): 128 | target_zoom = max(furthest_zoom_allowed, target_zoom - zoom_speed) 129 | camera.zoom.y = camera.zoom.x 130 | 131 | 132 | func _send_direction_angle(): 133 | var packet := Global.proto.Packet.new() 134 | var update_player_direction_angle := packet.new_update_player_direction_angle() 135 | update_player_direction_angle.set_direction_angle(direction.angle()) 136 | WsClient.send(packet) 137 | 138 | 139 | func _update_zoom() -> void: 140 | if is_node_ready(): 141 | _update_nameplate_font_size() 142 | 143 | if not is_player: 144 | return 145 | 146 | var new_furthest_zoom_allowed := 2 * start_radius / server_radius 147 | if is_equal_approx(target_zoom, furthest_zoom_allowed): 148 | target_zoom = new_furthest_zoom_allowed 149 | furthest_zoom_allowed = new_furthest_zoom_allowed 150 | 151 | 152 | func _update_nameplate_font_size(): 153 | nameplate.add_theme_font_size_override("font_size", max(16, radius / 2)) 154 | 155 | 156 | func _rush(): 157 | var packet = Global.proto.Packet.new() 158 | packet.new_rush() 159 | WsClient.send(packet) 160 | -------------------------------------------------------------------------------- /client/component/actor/actor.gd.uid: -------------------------------------------------------------------------------- 1 | uid://c3yg0cl3t7bht 2 | -------------------------------------------------------------------------------- /client/component/actor/actor.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://b7p42ft05kib6"] 2 | 3 | [ext_resource type="Shader" uid="uid://3yy1k1mdmahd" path="res://assets/rainbow.gdshader" id="1_j04ga"] 4 | [ext_resource type="Script" uid="uid://c3yg0cl3t7bht" path="res://component/actor/actor.gd" id="1_wnuxu"] 5 | [ext_resource type="PackedScene" uid="uid://bmqxeihcu3h80" path="res://component/rush_particles/rush_particles.tscn" id="3_1tdkq"] 6 | 7 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_mxwvi"] 8 | resource_local_to_scene = true 9 | 10 | [sub_resource type="CircleShape2D" id="CircleShape2D_fakvn"] 11 | resource_local_to_scene = true 12 | 13 | [node name="Actor" type="Area2D"] 14 | material = SubResource("ShaderMaterial_mxwvi") 15 | script = ExtResource("1_wnuxu") 16 | rush_shader = ExtResource("1_j04ga") 17 | 18 | [node name="RushParticles" parent="." instance=ExtResource("3_1tdkq")] 19 | unique_name_in_owner = true 20 | 21 | [node name="CollisionShape" type="CollisionShape2D" parent="."] 22 | unique_name_in_owner = true 23 | shape = SubResource("CircleShape2D_fakvn") 24 | 25 | [node name="Nameplate" type="Label" parent="."] 26 | unique_name_in_owner = true 27 | anchors_preset = 8 28 | anchor_left = 0.5 29 | anchor_top = 0.5 30 | anchor_right = 0.5 31 | anchor_bottom = 0.5 32 | offset_left = -16.5 33 | offset_top = -11.5 34 | offset_right = 16.5 35 | offset_bottom = 11.5 36 | grow_horizontal = 2 37 | grow_vertical = 2 38 | text = "Test" 39 | horizontal_alignment = 1 40 | vertical_alignment = 1 41 | 42 | [node name="Camera" type="Camera2D" parent="."] 43 | unique_name_in_owner = true 44 | position_smoothing_enabled = true 45 | -------------------------------------------------------------------------------- /client/component/leaderboard/leaderboard.gd: -------------------------------------------------------------------------------- 1 | class_name Leaderboard 2 | extends ScrollContainer 3 | 4 | var _score_list: Array[int] 5 | 6 | @onready var list_container: VBoxContainer = %ListContainer 7 | @onready var entry_teamplate: HBoxContainer = %EntryTeamplate 8 | 9 | 10 | func _ready() -> void: 11 | entry_teamplate.hide() 12 | 13 | 14 | func _add_entry(player_nickname: String, score: int, highlight: bool) -> void: 15 | _score_list.append(score) 16 | _score_list.sort() 17 | var pos := len(_score_list) - _score_list.find(score) - 1 18 | 19 | var entry: HBoxContainer = entry_teamplate.duplicate() 20 | var name_label: Label = entry.get_child(0) 21 | var score_label: Label = entry.get_child(1) 22 | 23 | list_container.add_child(entry) 24 | 25 | list_container.move_child(entry, pos) 26 | 27 | name_label.text = player_nickname 28 | score_label.text = str(score) 29 | if highlight: 30 | name_label.add_theme_color_override("font_color", Color.YELLOW) 31 | 32 | entry.show() 33 | 34 | 35 | func remove(player_nickname: String) -> void: 36 | for i in range(len(_score_list)): 37 | var entry: HBoxContainer = list_container.get_child(i) 38 | var name_label: Label = entry.get_child(0) 39 | if name_label.text == player_nickname: 40 | _score_list.remove_at(len(_score_list) - i - 1) 41 | entry.free() 42 | return 43 | 44 | 45 | func set_score(player_nickname: String, score: int, highlight: bool = false) -> void: 46 | remove(player_nickname) 47 | _add_entry(player_nickname, score, highlight) 48 | 49 | 50 | func clear() -> void: 51 | _score_list.clear() 52 | for entry in list_container.get_children(): 53 | if entry != entry_teamplate: 54 | entry.free() 55 | -------------------------------------------------------------------------------- /client/component/leaderboard/leaderboard.gd.uid: -------------------------------------------------------------------------------- 1 | uid://csq77lku4qjkr 2 | -------------------------------------------------------------------------------- /client/component/leaderboard/leaderboard.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://dis1cx3bi13xh"] 2 | 3 | [ext_resource type="Script" uid="uid://csq77lku4qjkr" path="res://component/leaderboard/leaderboard.gd" id="1_10xqa"] 4 | 5 | [node name="Leaderboard" type="ScrollContainer"] 6 | anchors_preset = 15 7 | anchor_right = 1.0 8 | anchor_bottom = 1.0 9 | grow_horizontal = 2 10 | grow_vertical = 2 11 | script = ExtResource("1_10xqa") 12 | 13 | [node name="VBoxContainer" type="VBoxContainer" parent="."] 14 | layout_mode = 2 15 | size_flags_horizontal = 3 16 | size_flags_vertical = 3 17 | 18 | [node name="Title" type="Label" parent="VBoxContainer"] 19 | layout_mode = 2 20 | text = "Leaderboard" 21 | 22 | [node name="ListContainer" type="VBoxContainer" parent="VBoxContainer"] 23 | unique_name_in_owner = true 24 | layout_mode = 2 25 | size_flags_horizontal = 3 26 | size_flags_vertical = 3 27 | 28 | [node name="EntryTeamplate" type="HBoxContainer" parent="VBoxContainer/ListContainer"] 29 | unique_name_in_owner = true 30 | layout_mode = 2 31 | 32 | [node name="NameLabel" type="Label" parent="VBoxContainer/ListContainer/EntryTeamplate"] 33 | layout_mode = 2 34 | size_flags_horizontal = 3 35 | text = "Test" 36 | 37 | [node name="ScoreLabel" type="Label" parent="VBoxContainer/ListContainer/EntryTeamplate"] 38 | layout_mode = 2 39 | text = "123" 40 | -------------------------------------------------------------------------------- /client/component/logger/logger.gd: -------------------------------------------------------------------------------- 1 | class_name Logger 2 | extends RichTextLabel 3 | 4 | 5 | func _message(message: String, color: Color = Color.WHITE) -> void: 6 | append_text("[color=#%s]%s[/color]\n" % [color.to_html(false), str(message)]) 7 | 8 | 9 | func info(message: String) -> void: 10 | _message(message, Color.WHITE) 11 | 12 | 13 | func warning(message: String) -> void: 14 | _message(message, Color.YELLOW) 15 | 16 | 17 | func error(message: String) -> void: 18 | _message(message, Color.ORANGE_RED) 19 | 20 | 21 | func success(message: String) -> void: 22 | _message(message, Color.LAWN_GREEN) 23 | 24 | 25 | func chat(sender_name: String, message: String) -> void: 26 | _message( 27 | ( 28 | "[color=#%s]%s:[/color] [i]%s[/i]" 29 | % [Color.CORNFLOWER_BLUE.to_html(false), sender_name, message] 30 | ) 31 | ) 32 | -------------------------------------------------------------------------------- /client/component/logger/logger.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cvpl8fum0bwrg 2 | -------------------------------------------------------------------------------- /client/component/logger/logger.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cpmsnc8fuugqe"] 2 | 3 | [ext_resource type="Script" uid="uid://cvpl8fum0bwrg" path="res://component/logger/logger.gd" id="1_bql4o"] 4 | 5 | [node name="Logger" type="RichTextLabel"] 6 | anchors_preset = 15 7 | anchor_right = 1.0 8 | anchor_bottom = 1.0 9 | grow_horizontal = 2 10 | grow_vertical = 2 11 | bbcode_enabled = true 12 | scroll_following = true 13 | script = ExtResource("1_bql4o") 14 | -------------------------------------------------------------------------------- /client/component/ping/ping.gd: -------------------------------------------------------------------------------- 1 | extends Label 2 | 3 | @onready var timer: Timer = %Timer 4 | 5 | 6 | func _ready() -> void: 7 | WsClient.packet_received.connect(_on_ws_packet_received) 8 | timer.timeout.connect(_on_timer_timeout) 9 | _send_ping() 10 | 11 | 12 | func _on_ws_packet_received(packet: Global.proto.Packet) -> void: 13 | if packet.has_ping(): 14 | var ping := packet.get_ping() 15 | var client_timestamp := ping.get_client_timestamp() 16 | var now := int(Time.get_unix_time_from_system() * 1000) 17 | var diff := now - client_timestamp 18 | text = "Ping: %s ms" % diff 19 | 20 | 21 | func _on_timer_timeout() -> void: 22 | _send_ping() 23 | 24 | 25 | func _send_ping() -> void: 26 | var packet := Global.proto.Packet.new() 27 | var ping := packet.new_ping() 28 | var now := int(Time.get_unix_time_from_system() * 1000) 29 | ping.set_client_timestamp(now) 30 | WsClient.send(packet) 31 | -------------------------------------------------------------------------------- /client/component/ping/ping.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cws8dlh5671q3 2 | -------------------------------------------------------------------------------- /client/component/ping/ping.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=2 format=3 uid="uid://cs885p6pg785h"] 2 | 3 | [ext_resource type="Script" uid="uid://cws8dlh5671q3" path="res://component/ping/ping.gd" id="1_fc5bw"] 4 | 5 | [node name="Ping" type="Label"] 6 | offset_right = 80.0 7 | offset_bottom = 24.0 8 | text = "Ping: 0 ms" 9 | vertical_alignment = 1 10 | script = ExtResource("1_fc5bw") 11 | 12 | [node name="Timer" type="Timer" parent="."] 13 | unique_name_in_owner = true 14 | autostart = true 15 | -------------------------------------------------------------------------------- /client/component/rush_particles/rush_particles.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://bmqxeihcu3h80"] 2 | 3 | [ext_resource type="Texture2D" uid="uid://drda0yvn27i6h" path="res://assets/blob.png" id="1_si7hh"] 4 | 5 | [sub_resource type="Curve" id="Curve_nu47k"] 6 | _data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.25, 1), 0.0, 0.0, 0, 0] 7 | point_count = 2 8 | 9 | [sub_resource type="CurveTexture" id="CurveTexture_bdijl"] 10 | curve = SubResource("Curve_nu47k") 11 | 12 | [sub_resource type="Curve" id="Curve_r0eof"] 13 | _limits = [-200.0, 200.0, 0.0, 1.0] 14 | _data = [Vector2(0, -100), 0.0, 0.0, 0, 0, Vector2(1, 100), 0.0, 0.0, 0, 0] 15 | point_count = 2 16 | 17 | [sub_resource type="CurveTexture" id="CurveTexture_arkch"] 18 | curve = SubResource("Curve_r0eof") 19 | 20 | [sub_resource type="Curve" id="Curve_36utw"] 21 | _data = [Vector2(0, 0), 0.0, 0.0, 0, 0, Vector2(0.25, 1), 0.0, 0.0, 0, 0, Vector2(0.5, 0.2), 0.0, 0.0, 0, 0, Vector2(0.75, 0.5), 0.0, 0.0, 0, 0, Vector2(1, 0), 0.0, 0.0, 0, 0] 22 | point_count = 5 23 | 24 | [sub_resource type="CurveTexture" id="CurveTexture_jrmyl"] 25 | curve = SubResource("Curve_36utw") 26 | 27 | [sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_dbtlh"] 28 | particle_flag_disable_z = true 29 | emission_shape = 6 30 | emission_ring_axis = Vector3(0, 0, 1) 31 | emission_ring_height = 0.0 32 | emission_ring_radius = 100.0 33 | emission_ring_inner_radius = 100.0 34 | emission_ring_cone_angle = 90.0 35 | gravity = Vector3(0, 0, 0) 36 | radial_accel_min = 10.0 37 | radial_accel_max = 10.0 38 | radial_accel_curve = SubResource("CurveTexture_arkch") 39 | scale_min = 0.2 40 | scale_curve = SubResource("CurveTexture_jrmyl") 41 | alpha_curve = SubResource("CurveTexture_bdijl") 42 | turbulence_enabled = true 43 | 44 | [node name="RushParticles" type="GPUParticles2D"] 45 | z_index = -1 46 | emitting = false 47 | amount = 128 48 | texture = ExtResource("1_si7hh") 49 | randomness = 0.2 50 | process_material = SubResource("ParticleProcessMaterial_dbtlh") 51 | -------------------------------------------------------------------------------- /client/component/spore/spore.gd: -------------------------------------------------------------------------------- 1 | class_name Spore 2 | extends Area2D 3 | 4 | const SPORE = preload("res://component/spore/spore.tscn") 5 | 6 | var spore_id: String 7 | var x: float 8 | var y: float 9 | var radius: float 10 | var color: Color 11 | var underneath_player: bool 12 | 13 | @onready var collision_shape: CollisionShape2D = %CollisionShape 14 | 15 | 16 | static func instantiate( 17 | p_spore_id: String, p_x: float, p_y: float, p_radius: float, p_underneath_player: bool 18 | ) -> Spore: 19 | var spore := SPORE.instantiate() 20 | spore.spore_id = p_spore_id 21 | spore.x = p_x 22 | spore.y = p_y 23 | spore.radius = p_radius 24 | spore.underneath_player = p_underneath_player 25 | return spore 26 | 27 | 28 | func _ready() -> void: 29 | if underneath_player: 30 | area_exited.connect(_on_area_exited) 31 | position.x = x 32 | position.y = y 33 | collision_shape.shape.radius = radius 34 | color = Color.from_hsv(randf(), 1, 1, 1) 35 | 36 | 37 | func _draw() -> void: 38 | draw_circle(Vector2.ZERO, radius, color) 39 | 40 | 41 | func _on_area_exited(area: Area2D) -> void: 42 | if area is Actor: 43 | underneath_player = false 44 | -------------------------------------------------------------------------------- /client/component/spore/spore.gd.uid: -------------------------------------------------------------------------------- 1 | uid://b3oy58hfccmku 2 | -------------------------------------------------------------------------------- /client/component/spore/spore.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=3 format=3 uid="uid://bctoa48vbcjqg"] 2 | 3 | [ext_resource type="Script" uid="uid://b3oy58hfccmku" path="res://component/spore/spore.gd" id="1_7vnp8"] 4 | 5 | [sub_resource type="CircleShape2D" id="CircleShape2D_7luv8"] 6 | resource_local_to_scene = true 7 | 8 | [node name="Spore" type="Area2D"] 9 | script = ExtResource("1_7vnp8") 10 | 11 | [node name="CollisionShape" type="CollisionShape2D" parent="."] 12 | unique_name_in_owner = true 13 | shape = SubResource("CircleShape2D_7luv8") 14 | -------------------------------------------------------------------------------- /client/export_presets.cfg: -------------------------------------------------------------------------------- 1 | [preset.0] 2 | 3 | name="Web" 4 | platform="Web" 5 | runnable=true 6 | advanced_options=true 7 | dedicated_server=false 8 | custom_features="" 9 | export_filter="all_resources" 10 | include_filter="" 11 | exclude_filter="" 12 | export_path="../../agarust-dist/web/index.html" 13 | patches=PackedStringArray() 14 | encryption_include_filters="" 15 | encryption_exclude_filters="" 16 | seed=0 17 | encrypt_pck=false 18 | encrypt_directory=false 19 | script_export_mode=2 20 | 21 | [preset.0.options] 22 | 23 | custom_template/debug="" 24 | custom_template/release="" 25 | variant/extensions_support=false 26 | variant/thread_support=false 27 | vram_texture_compression/for_desktop=true 28 | vram_texture_compression/for_mobile=false 29 | html/export_icon=true 30 | html/custom_html_shell="" 31 | html/head_include="" 32 | html/canvas_resize_policy=2 33 | html/focus_canvas_on_start=true 34 | html/experimental_virtual_keyboard=true 35 | progressive_web_app/enabled=false 36 | progressive_web_app/ensure_cross_origin_isolation_headers=true 37 | progressive_web_app/offline_page="" 38 | progressive_web_app/display=1 39 | progressive_web_app/orientation=1 40 | progressive_web_app/icon_144x144="" 41 | progressive_web_app/icon_180x180="" 42 | progressive_web_app/icon_512x512="" 43 | progressive_web_app/background_color=Color(0, 0, 0, 1) 44 | -------------------------------------------------------------------------------- /client/global/global.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | const proto := preload("res://proto.gd") 4 | const debug_server_url := "ws://127.0.0.1:8080" 5 | const release_server_url := "wss://agarust-server.d8s.fun" 6 | 7 | var server_url := debug_server_url 8 | var connection_id := "" 9 | var show_server_position := false 10 | 11 | 12 | func _ready() -> void: 13 | if not OS.is_debug_build(): 14 | server_url = release_server_url 15 | WsClient.closed.connect(_on_ws_closed) 16 | 17 | 18 | func _on_ws_closed() -> void: 19 | get_tree().change_scene_to_file("res://view/connecting/connecting.tscn") 20 | -------------------------------------------------------------------------------- /client/global/global.gd.uid: -------------------------------------------------------------------------------- 1 | uid://boiy8oqa8ohn1 2 | -------------------------------------------------------------------------------- /client/global/vfx_pre_compile.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=4 format=3 uid="uid://4wvs4kpky4st"] 2 | 3 | [ext_resource type="PackedScene" uid="uid://bmqxeihcu3h80" path="res://component/rush_particles/rush_particles.tscn" id="2_1vkly"] 4 | [ext_resource type="Shader" uid="uid://3yy1k1mdmahd" path="res://assets/rainbow.gdshader" id="3_vtlmn"] 5 | 6 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_82fu5"] 7 | shader = ExtResource("3_vtlmn") 8 | shader_parameter/strength = 0.3 9 | shader_parameter/speed = 0.5 10 | shader_parameter/angle = 0.0 11 | 12 | [node name="VfxPreCompile" type="Node2D"] 13 | 14 | [node name="RushParticles" parent="." instance=ExtResource("2_1vkly")] 15 | material = SubResource("ShaderMaterial_82fu5") 16 | position = Vector2(104, 104) 17 | emitting = true 18 | -------------------------------------------------------------------------------- /client/global/ws_client.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | var socket: WebSocketPeer 4 | var last_state := WebSocketPeer.STATE_CLOSED 5 | 6 | signal connected 7 | signal closed 8 | signal packet_received(packet: Global.proto.Packet) 9 | 10 | 11 | func _physics_process(_delta: float) -> void: 12 | _poll() 13 | _update_state() 14 | _read_data() 15 | 16 | 17 | func _poll() -> void: 18 | if socket.get_ready_state() != socket.STATE_CLOSED: 19 | socket.poll() 20 | 21 | 22 | func _update_state() -> void: 23 | var state := socket.get_ready_state() 24 | if last_state != state: 25 | last_state = state 26 | if state == socket.STATE_OPEN: 27 | connected.emit() 28 | elif state == socket.STATE_CLOSED: 29 | closed.emit() 30 | 31 | 32 | func _read_data() -> void: 33 | var packet := _get_packet() 34 | if packet: 35 | packet_received.emit(packet) 36 | 37 | 38 | func _get_packet() -> Global.proto.Packet: 39 | if socket.get_available_packet_count() < 1: 40 | return null 41 | 42 | var data := socket.get_packet() 43 | 44 | var packet := Global.proto.Packet.new() 45 | var result := packet.from_bytes(data) 46 | if result != OK: 47 | printerr("Error build packet from data: %s" % data) 48 | return null 49 | 50 | return packet 51 | 52 | 53 | func connect_to_server(url: String, tls_options: TLSOptions = null) -> int: 54 | socket = WebSocketPeer.new() 55 | var err := socket.connect_to_url(url, tls_options) 56 | if err != OK: 57 | return err 58 | 59 | last_state = socket.get_ready_state() 60 | 61 | return OK 62 | 63 | 64 | func send(packet: Global.proto.Packet) -> int: 65 | var data := packet.to_bytes() 66 | return socket.send(data) 67 | 68 | 69 | func close(code: int = 1000, reason: String = "") -> void: 70 | socket.close(code, reason) 71 | last_state = socket.get_ready_state() 72 | -------------------------------------------------------------------------------- /client/global/ws_client.gd.uid: -------------------------------------------------------------------------------- 1 | uid://du8gviwiudsjy 2 | -------------------------------------------------------------------------------- /client/project.godot: -------------------------------------------------------------------------------- 1 | ; Engine configuration file. 2 | ; It's best edited using the editor UI and not directly, 3 | ; since the parameters that go here are not all obvious. 4 | ; 5 | ; Format: 6 | ; [section] ; section goes between [] 7 | ; param=value ; assign values to parameters 8 | 9 | config_version=5 10 | 11 | [application] 12 | 13 | config/name="Agarust" 14 | run/main_scene="res://view/connecting/connecting.tscn" 15 | config/features=PackedStringArray("4.4", "Forward Plus") 16 | boot_splash/fullsize=false 17 | boot_splash/image="res://assets/icon.png" 18 | config/icon="res://assets/icon.png" 19 | 20 | [autoload] 21 | 22 | Global="*res://global/global.gd" 23 | WsClient="*res://global/ws_client.gd" 24 | VfxPreCompile="*res://global/vfx_pre_compile.tscn" 25 | 26 | [display] 27 | 28 | window/size/viewport_width=1920 29 | window/size/viewport_height=1080 30 | window/size/window_width_override=1280 31 | window/size/window_height_override=720 32 | window/stretch/mode="canvas_items" 33 | window/stretch/aspect="expand" 34 | 35 | [editor_plugins] 36 | 37 | enabled=PackedStringArray("res://addons/protobuf/plugin.cfg") 38 | 39 | [gui] 40 | 41 | theme/custom_font="res://assets/SmileySans-Oblique.ttf" 42 | 43 | [input] 44 | 45 | zoom_in={ 46 | "deadzone": 0.5, 47 | "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(161, 34),"global_position":Vector2(175, 104),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null) 48 | ] 49 | } 50 | zoom_out={ 51 | "deadzone": 0.5, 52 | "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(395, 22),"global_position":Vector2(409, 92),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null) 53 | ] 54 | } 55 | rush={ 56 | "deadzone": 0.5, 57 | "events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(95, 27),"global_position":Vector2(109, 97),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null) 58 | ] 59 | } 60 | 61 | [rendering] 62 | 63 | textures/vram_compression/import_etc2_astc=true 64 | -------------------------------------------------------------------------------- /client/proto.gd.uid: -------------------------------------------------------------------------------- 1 | uid://cjunx2r7hdnp0 2 | -------------------------------------------------------------------------------- /client/view/connecting/connecting.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | 4 | func _ready() -> void: 5 | WsClient.connected.connect(_on_ws_connected) 6 | WsClient.packet_received.connect(_on_ws_packet_received) 7 | WsClient.connect_to_server(Global.server_url) 8 | 9 | 10 | func _on_ws_connected() -> void: 11 | print_debug("server connected") 12 | 13 | 14 | func _on_ws_packet_received(packet: Global.proto.Packet) -> void: 15 | if packet.has_hello(): 16 | _handle_hello_msg(packet.get_hello()) 17 | 18 | 19 | func _handle_hello_msg(hello_msg: Global.proto.Hello) -> void: 20 | Global.connection_id = hello_msg.get_connection_id() 21 | get_tree().change_scene_to_file("res://view/login/login.tscn") 22 | -------------------------------------------------------------------------------- /client/view/connecting/connecting.gd.uid: -------------------------------------------------------------------------------- 1 | uid://ceh4nrkrkjpin 2 | -------------------------------------------------------------------------------- /client/view/connecting/connecting.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://i8esfsijgi4"] 2 | 3 | [ext_resource type="Script" uid="uid://ceh4nrkrkjpin" path="res://view/connecting/connecting.gd" id="1_36lmk"] 4 | [ext_resource type="Shader" uid="uid://cuybon2vubibm" path="res://assets/background_effect.gdshader" id="3_08j1f"] 5 | [ext_resource type="Texture2D" uid="uid://dyo2iothubrem" path="res://assets/background.svg" id="4_5br80"] 6 | 7 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_4xac1"] 8 | shader = ExtResource("3_08j1f") 9 | shader_parameter/amplitutde = Vector2(1, 0) 10 | shader_parameter/speed = Vector2(1, 0) 11 | 12 | [sub_resource type="LabelSettings" id="LabelSettings_dxj85"] 13 | font_size = 64 14 | 15 | [node name="Connection" type="Node2D"] 16 | script = ExtResource("1_36lmk") 17 | 18 | [node name="ParallaxBackground" type="Parallax2D" parent="."] 19 | repeat_size = Vector2(12000, 12000) 20 | repeat_times = 2 21 | 22 | [node name="Background" type="Sprite2D" parent="ParallaxBackground"] 23 | texture_repeat = 2 24 | material = SubResource("ShaderMaterial_4xac1") 25 | texture = ExtResource("4_5br80") 26 | centered = false 27 | region_enabled = true 28 | region_rect = Rect2(0, 0, 12000, 12000) 29 | 30 | [node name="Gui" type="CanvasLayer" parent="."] 31 | 32 | [node name="Label" type="Label" parent="Gui"] 33 | anchors_preset = 8 34 | anchor_left = 0.5 35 | anchor_top = 0.5 36 | anchor_right = 0.5 37 | anchor_bottom = 0.5 38 | offset_left = -20.0 39 | offset_top = -11.5 40 | offset_right = 20.0 41 | offset_bottom = 11.5 42 | grow_horizontal = 2 43 | grow_vertical = 2 44 | text = "Connecting to server..." 45 | label_settings = SubResource("LabelSettings_dxj85") 46 | -------------------------------------------------------------------------------- /client/view/game/game.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var world: Node2D = %World 4 | @onready var logout_button: Button = %LogoutButton 5 | @onready var chat_edit: LineEdit = %ChatEdit 6 | @onready var send_chat_button: Button = %SendChatButton 7 | @onready var leaderboard: Leaderboard = %Leaderboard 8 | @onready var logger: Logger = %Logger 9 | @onready var show_server_position_check: CheckButton = %ShowServerPositionCheck 10 | 11 | var player_map: Dictionary = {} 12 | var spore_map: Dictionary = {} 13 | 14 | 15 | func _ready() -> void: 16 | WsClient.packet_received.connect(_on_ws_packet_received) 17 | logout_button.pressed.connect(_on_logout_button_pressed) 18 | chat_edit.text_submitted.connect(_on_chat_edit_text_submited) 19 | send_chat_button.pressed.connect(_on_send_chat_button_pressed) 20 | show_server_position_check.button_pressed = Global.show_server_position 21 | show_server_position_check.toggled.connect(_on_show_server_position_check_toggled) 22 | _send_join() 23 | 24 | 25 | func _send_join() -> void: 26 | var packet := Global.proto.Packet.new() 27 | packet.new_join() 28 | WsClient.send(packet) 29 | 30 | 31 | func _on_ws_packet_received(packet: Global.proto.Packet) -> void: 32 | if packet.has_chat(): 33 | print_debug(packet) 34 | _handle_chat_msg(packet.get_chat()) 35 | elif packet.has_update_player(): 36 | print_debug(packet) 37 | _handle_update_player_msg(packet.get_update_player()) 38 | elif packet.has_update_player_batch(): 39 | _handle_update_player_batch_msg(packet.get_update_player_batch()) 40 | elif packet.has_update_spore(): 41 | _handle_update_spore_msg(packet.get_update_spore()) 42 | elif packet.has_update_spore_batch(): 43 | _handle_update_spore_batch_msg(packet.get_update_spore_batch()) 44 | elif packet.has_consume_spore(): 45 | _handle_consume_spore_msg(packet.get_consume_spore()) 46 | elif packet.has_disconnect(): 47 | _handle_disconnect_msg(packet.get_disconnect()) 48 | 49 | 50 | func _on_logout_button_pressed() -> void: 51 | get_tree().change_scene_to_file("res://view/connecting/connecting.tscn") 52 | 53 | 54 | func _on_chat_edit_text_submited(new_text: String) -> void: 55 | if new_text.is_empty(): 56 | return 57 | var packet := Global.proto.Packet.new() 58 | var chat := packet.new_chat() 59 | chat.set_msg(new_text) 60 | WsClient.send(packet) 61 | chat_edit.text = "" 62 | 63 | 64 | func _on_send_chat_button_pressed() -> void: 65 | _on_chat_edit_text_submited(chat_edit.text) 66 | 67 | 68 | func _on_show_server_position_check_toggled(toggled_on: bool) -> void: 69 | Global.show_server_position = toggled_on 70 | 71 | 72 | func _handle_chat_msg(chat_msg: Global.proto.Chat) -> void: 73 | var connection_id = chat_msg.get_connection_id() 74 | if connection_id in player_map: 75 | var player = player_map[connection_id] 76 | logger.chat(player.actor_nickname, chat_msg.get_msg()) 77 | 78 | 79 | func _handle_update_player_batch_msg( 80 | update_player_batch_msg: Global.proto.UpdatePlayerBatch 81 | ) -> void: 82 | for update_player_msg: Global.proto.UpdatePlayer in ( 83 | update_player_batch_msg.get_update_player_batch() 84 | ): 85 | _handle_update_player_msg(update_player_msg) 86 | 87 | 88 | func _handle_update_player_msg(update_player_msg: Global.proto.UpdatePlayer) -> void: 89 | var actor_connection_id := update_player_msg.get_connection_id() 90 | var actor_nickname := update_player_msg.get_nickname() 91 | var x := update_player_msg.get_x() 92 | var y := update_player_msg.get_y() 93 | var radius := update_player_msg.get_radius() 94 | var speed := update_player_msg.get_speed() 95 | var color_hex := update_player_msg.get_color() 96 | var is_rushing := update_player_msg.get_is_rushing() 97 | 98 | var color := Color.hex64(color_hex) 99 | var is_player := actor_connection_id == Global.connection_id 100 | 101 | if actor_connection_id not in player_map: 102 | _add_actor( 103 | actor_connection_id, actor_nickname, x, y, radius, speed, color, is_rushing, is_player 104 | ) 105 | else: 106 | var direction := update_player_msg.get_direction_angle() 107 | _update_actor(actor_connection_id, x, y, direction, speed, radius, is_rushing, is_player) 108 | 109 | 110 | func _handle_update_spore_batch_msg(update_spore_batch_msg: Global.proto.UpdateSporeBatch) -> void: 111 | for update_spore_msg: Global.proto.UpdateSpore in ( 112 | update_spore_batch_msg.get_update_spore_batch() 113 | ): 114 | _handle_update_spore_msg(update_spore_msg) 115 | 116 | 117 | func _handle_update_spore_msg(update_spore_msg: Global.proto.UpdateSpore) -> void: 118 | var spore_id := update_spore_msg.get_id() 119 | var x := update_spore_msg.get_x() 120 | var y := update_spore_msg.get_y() 121 | var radius := update_spore_msg.get_radius() 122 | var underneath_player := false 123 | 124 | if Global.connection_id in player_map: 125 | var player = player_map[Global.connection_id] 126 | var player_pos := Vector2(player.position.x, player.position.y) 127 | var spore_pos := Vector2(x, y) 128 | underneath_player = ( 129 | player_pos.distance_squared_to(spore_pos) < player.radius * player.radius 130 | ) 131 | 132 | if spore_id not in spore_map: 133 | var spore := Spore.instantiate(spore_id, x, y, radius, underneath_player) 134 | world.add_child(spore) 135 | spore_map[spore_id] = spore 136 | 137 | 138 | func _handle_consume_spore_msg(consume_spore_msg: Global.proto.ConsumeSpore) -> void: 139 | var connection_id := consume_spore_msg.get_connection_id() 140 | var spore_id := consume_spore_msg.get_spore_id() 141 | if connection_id in player_map and spore_id in spore_map: 142 | var actor = player_map[connection_id] 143 | var actor_mass := _radius_to_mass(actor.radius) 144 | 145 | var spore = spore_map[spore_id] 146 | var spore_mass := _radius_to_mass(spore.radius) 147 | 148 | _set_actor_mass(actor, actor_mass + spore_mass) 149 | _remove_spore(spore) 150 | 151 | 152 | func _handle_disconnect_msg(disconnect_msg: Global.proto.Disconnect) -> void: 153 | var connection_id = disconnect_msg.get_connection_id() 154 | if connection_id in player_map: 155 | var player = player_map[connection_id] 156 | var reason := disconnect_msg.get_reason() 157 | logger.info("%s disconnected because %s" % [player.actor_nickname, reason]) 158 | _remove_actor(player) 159 | 160 | 161 | func _add_actor( 162 | connection_id: String, 163 | actor_nickname: String, 164 | x: float, 165 | y: float, 166 | radius: float, 167 | speed: float, 168 | color: Color, 169 | is_rushing: bool, 170 | is_player: bool 171 | ) -> void: 172 | var actor := Actor.instantiate( 173 | connection_id, actor_nickname, x, y, radius, speed, color, is_rushing, is_player 174 | ) 175 | actor.z_index = 1 176 | world.add_child(actor) 177 | var mass := _radius_to_mass(radius) 178 | _set_actor_mass(actor, mass) 179 | player_map[connection_id] = actor 180 | 181 | if is_player: 182 | actor.area_entered.connect(_on_player_area_entered) 183 | 184 | 185 | func _radius_to_mass(radius: float) -> float: 186 | return radius * radius * PI 187 | 188 | 189 | func _set_actor_mass(actor: Actor, mass: float) -> void: 190 | actor.radius = sqrt(mass / PI) 191 | leaderboard.set_score(actor.actor_nickname, roundi(mass)) 192 | 193 | 194 | func _on_player_area_entered(area: Area2D) -> void: 195 | if area is Spore: 196 | _consume_spore(area as Spore) 197 | elif area is Actor: 198 | _collide_actor(area as Actor) 199 | 200 | 201 | func _consume_spore(spore: Spore) -> void: 202 | if spore.underneath_player: 203 | return 204 | 205 | var packet := Global.proto.Packet.new() 206 | var consume_spore_msg := packet.new_consume_spore() 207 | consume_spore_msg.set_spore_id(spore.spore_id) 208 | WsClient.send(packet) 209 | _remove_spore(spore) 210 | 211 | 212 | func _remove_spore(spore: Spore) -> void: 213 | spore_map.erase(spore.spore_id) 214 | spore.queue_free() 215 | 216 | 217 | func _collide_actor(actor: Actor) -> void: 218 | var player = player_map[Global.connection_id] 219 | var player_mass := _radius_to_mass(player.radius) 220 | var actor_mass := _radius_to_mass(actor.radius) 221 | 222 | if player_mass >= actor_mass * 1.2: 223 | _consume_actor(actor) 224 | 225 | 226 | func _consume_actor(actor: Actor) -> void: 227 | var packet := Global.proto.Packet.new() 228 | var consume_player_msg := packet.new_consume_player() 229 | consume_player_msg.set_victim_connection_id(actor.connection_id) 230 | WsClient.send(packet) 231 | _remove_actor(actor) 232 | 233 | 234 | func _remove_actor(actor: Actor) -> void: 235 | player_map.erase(actor.connection_id) 236 | actor.queue_free() 237 | leaderboard.remove(actor.actor_nickname) 238 | 239 | 240 | func _update_actor( 241 | connection_id: String, 242 | x: float, 243 | y: float, 244 | direction: float, 245 | speed: float, 246 | radius: float, 247 | is_rushing: bool, 248 | is_player: bool 249 | ) -> void: 250 | var actor: Actor = player_map[connection_id] 251 | 252 | _set_actor_mass(actor, _radius_to_mass(radius)) 253 | actor.server_radius = radius 254 | 255 | actor.speed = speed 256 | actor.is_rushing = is_rushing 257 | actor.is_player = is_player 258 | 259 | var server_position := Vector2(x, y) 260 | if actor.position.distance_squared_to(server_position) > 20: 261 | actor.server_position = server_position 262 | 263 | if not is_player: 264 | actor.direction = Vector2.from_angle(direction) 265 | -------------------------------------------------------------------------------- /client/view/game/game.gd.uid: -------------------------------------------------------------------------------- 1 | uid://betq3365mnd81 2 | -------------------------------------------------------------------------------- /client/view/game/game.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://b6eavbvcua5hk"] 2 | 3 | [ext_resource type="Script" uid="uid://betq3365mnd81" path="res://view/game/game.gd" id="1_dhpwo"] 4 | [ext_resource type="PackedScene" uid="uid://dis1cx3bi13xh" path="res://component/leaderboard/leaderboard.tscn" id="3_vpl7a"] 5 | [ext_resource type="PackedScene" uid="uid://cpmsnc8fuugqe" path="res://component/logger/logger.tscn" id="4_8v2iv"] 6 | [ext_resource type="Texture2D" uid="uid://dyo2iothubrem" path="res://assets/background.svg" id="4_nyaor"] 7 | [ext_resource type="PackedScene" uid="uid://cs885p6pg785h" path="res://component/ping/ping.tscn" id="5_fhova"] 8 | 9 | [node name="Game" type="Node2D"] 10 | script = ExtResource("1_dhpwo") 11 | 12 | [node name="ParallaxBackground" type="Parallax2D" parent="."] 13 | repeat_size = Vector2(12000, 12000) 14 | repeat_times = 2 15 | 16 | [node name="Background" type="Sprite2D" parent="ParallaxBackground"] 17 | texture_repeat = 2 18 | texture = ExtResource("4_nyaor") 19 | centered = false 20 | region_enabled = true 21 | region_rect = Rect2(0, 0, 12000, 12000) 22 | 23 | [node name="World" type="Node2D" parent="."] 24 | unique_name_in_owner = true 25 | 26 | [node name="Gui" type="CanvasLayer" parent="."] 27 | 28 | [node name="GuiInputEventReferenceRect" type="ReferenceRect" parent="Gui"] 29 | anchors_preset = 10 30 | anchor_right = 1.0 31 | offset_bottom = 128.0 32 | grow_horizontal = 2 33 | 34 | [node name="MarginContainer" type="MarginContainer" parent="Gui"] 35 | anchors_preset = 15 36 | anchor_right = 1.0 37 | anchor_bottom = 1.0 38 | grow_horizontal = 2 39 | grow_vertical = 2 40 | theme_override_constants/margin_left = 20 41 | theme_override_constants/margin_top = 20 42 | theme_override_constants/margin_right = 20 43 | theme_override_constants/margin_bottom = 20 44 | 45 | [node name="VBoxContainer" type="VBoxContainer" parent="Gui/MarginContainer"] 46 | layout_mode = 2 47 | 48 | [node name="HBoxContainer" type="HBoxContainer" parent="Gui/MarginContainer/VBoxContainer"] 49 | layout_mode = 2 50 | 51 | [node name="LogoutButton" type="Button" parent="Gui/MarginContainer/VBoxContainer/HBoxContainer"] 52 | unique_name_in_owner = true 53 | layout_mode = 2 54 | text = "Logout" 55 | 56 | [node name="ChatEdit" type="LineEdit" parent="Gui/MarginContainer/VBoxContainer/HBoxContainer"] 57 | unique_name_in_owner = true 58 | layout_mode = 2 59 | size_flags_horizontal = 3 60 | placeholder_text = "Chat..." 61 | 62 | [node name="SendChatButton" type="Button" parent="Gui/MarginContainer/VBoxContainer/HBoxContainer"] 63 | unique_name_in_owner = true 64 | layout_mode = 2 65 | text = "Send" 66 | 67 | [node name="Leaderboard" parent="Gui/MarginContainer/VBoxContainer" instance=ExtResource("3_vpl7a")] 68 | unique_name_in_owner = true 69 | custom_minimum_size = Vector2(300, 400) 70 | layout_mode = 2 71 | size_flags_horizontal = 8 72 | 73 | [node name="Logger" parent="Gui/MarginContainer/VBoxContainer" instance=ExtResource("4_8v2iv")] 74 | unique_name_in_owner = true 75 | custom_minimum_size = Vector2(500, 400) 76 | layout_mode = 2 77 | size_flags_horizontal = 0 78 | size_flags_vertical = 10 79 | 80 | [node name="ShowServerPositionCheck" type="CheckButton" parent="Gui"] 81 | unique_name_in_owner = true 82 | offset_left = 16.0 83 | offset_top = 64.0 84 | offset_right = 226.0 85 | offset_bottom = 95.0 86 | text = "Show server position" 87 | 88 | [node name="Ping" parent="Gui" instance=ExtResource("5_fhova")] 89 | offset_left = 24.0 90 | offset_top = 96.0 91 | offset_right = 104.0 92 | offset_bottom = 120.0 93 | -------------------------------------------------------------------------------- /client/view/leaderboard_view/leaderboard_view.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var back_button: Button = %BackButton 4 | @onready var leaderboard: Leaderboard = %Leaderboard 5 | 6 | 7 | func _ready() -> void: 8 | WsClient.packet_received.connect(_on_ws_packet_received) 9 | back_button.pressed.connect(_on_back_button_pressed) 10 | _fetch_leaderboard() 11 | 12 | 13 | func _on_ws_packet_received(packet: Global.proto.Packet) -> void: 14 | if packet.has_leaderboard_response(): 15 | var leaderboard_response = packet.get_leaderboard_response() 16 | for entry: Global.proto.LeaderboardEntry in ( 17 | leaderboard_response.get_leaderboard_entry_list() 18 | ): 19 | var player_nickname := entry.get_player_nickname() 20 | var rank_and_name := "%d. %s" % [entry.get_rank(), player_nickname] 21 | var score: int = entry.get_score() 22 | leaderboard.set_score(rank_and_name, score) 23 | 24 | 25 | func _fetch_leaderboard() -> void: 26 | var packet := Global.proto.Packet.new() 27 | packet.new_leaderboard_request() 28 | WsClient.send(packet) 29 | 30 | 31 | func _on_back_button_pressed() -> void: 32 | get_tree().change_scene_to_file("res://view/login/login.tscn") 33 | -------------------------------------------------------------------------------- /client/view/leaderboard_view/leaderboard_view.gd.uid: -------------------------------------------------------------------------------- 1 | uid://qf7ywdiisj6v 2 | -------------------------------------------------------------------------------- /client/view/leaderboard_view/leaderboard_view.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=6 format=3 uid="uid://b0o7nr0wlupp"] 2 | 3 | [ext_resource type="Script" uid="uid://qf7ywdiisj6v" path="res://view/leaderboard_view/leaderboard_view.gd" id="1_qr5vi"] 4 | [ext_resource type="PackedScene" uid="uid://dis1cx3bi13xh" path="res://component/leaderboard/leaderboard.tscn" id="2_rkw7s"] 5 | [ext_resource type="Texture2D" uid="uid://dyo2iothubrem" path="res://assets/background.svg" id="3_aemde"] 6 | [ext_resource type="Shader" uid="uid://cuybon2vubibm" path="res://assets/background_effect.gdshader" id="3_cbesx"] 7 | 8 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_yrhip"] 9 | shader = ExtResource("3_cbesx") 10 | shader_parameter/amplitutde = Vector2(1, 0) 11 | shader_parameter/speed = Vector2(1, 0) 12 | 13 | [node name="LeaderboardView" type="Node2D"] 14 | script = ExtResource("1_qr5vi") 15 | 16 | [node name="ParallaxBackground" type="Parallax2D" parent="."] 17 | repeat_size = Vector2(12000, 12000) 18 | repeat_times = 2 19 | 20 | [node name="Background" type="Sprite2D" parent="ParallaxBackground"] 21 | texture_repeat = 2 22 | material = SubResource("ShaderMaterial_yrhip") 23 | texture = ExtResource("3_aemde") 24 | centered = false 25 | region_enabled = true 26 | region_rect = Rect2(0, 0, 12000, 12000) 27 | 28 | [node name="Gui" type="CanvasLayer" parent="."] 29 | 30 | [node name="BackButton" type="Button" parent="Gui"] 31 | unique_name_in_owner = true 32 | offset_left = 16.0 33 | offset_top = 16.0 34 | offset_right = 61.0 35 | offset_bottom = 47.0 36 | text = "Back" 37 | 38 | [node name="Leaderboard" parent="Gui" instance=ExtResource("2_rkw7s")] 39 | unique_name_in_owner = true 40 | offset_left = 448.0 41 | offset_top = 64.0 42 | offset_right = -448.0 43 | offset_bottom = -16.0 44 | -------------------------------------------------------------------------------- /client/view/login/login.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @onready var username_edit: LineEdit = %UsernameEdit 4 | @onready var password_edit: LineEdit = %PasswordEdit 5 | @onready var login_button: Button = %LoginButton 6 | @onready var register_button: Button = %RegisterButton 7 | @onready var leaderboard_button: Button = %LeaderboardButton 8 | @onready var logger: Logger = %Logger 9 | @onready var connection_id_label: Label = %ConnectionIdLabel 10 | @onready var message_panel: Panel = %MessagePanel 11 | @onready var message_label: Label = %MessageLabel 12 | 13 | 14 | func _ready() -> void: 15 | WsClient.packet_received.connect(_on_ws_packet_received) 16 | login_button.pressed.connect(_on_login_button_pressed) 17 | register_button.pressed.connect(_on_register_button_pressed) 18 | leaderboard_button.pressed.connect(_on_leaderboard_button_pressed) 19 | connection_id_label.text = "Connection ID: %s" % [Global.connection_id] 20 | 21 | 22 | func _on_ws_packet_received(packet: Global.proto.Packet) -> void: 23 | if packet.has_login_ok(): 24 | message_panel.hide() 25 | get_tree().change_scene_to_file("res://view/game/game.tscn") 26 | elif packet.has_login_err(): 27 | message_panel.hide() 28 | logger.error(packet.get_login_err().get_reason()) 29 | 30 | 31 | func _on_login_button_pressed() -> void: 32 | var username := username_edit.text.strip_edges() 33 | var password := password_edit.text.strip_edges() 34 | if username.is_empty() or password.is_empty(): 35 | return 36 | 37 | message_panel.show() 38 | message_label.text = "Loading..." 39 | 40 | var packet := Global.proto.Packet.new() 41 | var login_message := packet.new_login() 42 | login_message.set_username(username) 43 | login_message.set_password(password) 44 | WsClient.send(packet) 45 | 46 | 47 | func _on_register_button_pressed() -> void: 48 | get_tree().change_scene_to_file("res://view/register/register.tscn") 49 | 50 | 51 | func _on_leaderboard_button_pressed() -> void: 52 | get_tree().change_scene_to_file("res://view/leaderboard_view/leaderboard_view.tscn") 53 | -------------------------------------------------------------------------------- /client/view/login/login.gd.uid: -------------------------------------------------------------------------------- 1 | uid://bti66t0cs6p0s 2 | -------------------------------------------------------------------------------- /client/view/login/login.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=9 format=3 uid="uid://brcr5qfnvbrao"] 2 | 3 | [ext_resource type="Script" uid="uid://bti66t0cs6p0s" path="res://view/login/login.gd" id="1_cvf3h"] 4 | [ext_resource type="PackedScene" uid="uid://cpmsnc8fuugqe" path="res://component/logger/logger.tscn" id="3_m0dil"] 5 | [ext_resource type="Texture2D" uid="uid://dyo2iothubrem" path="res://assets/background.svg" id="4_47oqa"] 6 | [ext_resource type="PackedScene" uid="uid://cs885p6pg785h" path="res://component/ping/ping.tscn" id="4_g2reg"] 7 | [ext_resource type="Shader" uid="uid://cuybon2vubibm" path="res://assets/background_effect.gdshader" id="4_v3yw7"] 8 | 9 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_vqait"] 10 | shader = ExtResource("4_v3yw7") 11 | shader_parameter/amplitutde = Vector2(1, 0) 12 | shader_parameter/speed = Vector2(1, 0) 13 | 14 | [sub_resource type="LabelSettings" id="LabelSettings_yei6d"] 15 | font_size = 36 16 | 17 | [sub_resource type="LabelSettings" id="LabelSettings_g1chx"] 18 | font_size = 40 19 | 20 | [node name="Login" type="Node2D"] 21 | script = ExtResource("1_cvf3h") 22 | 23 | [node name="ParallaxBackground" type="Parallax2D" parent="."] 24 | repeat_size = Vector2(12000, 12000) 25 | repeat_times = 2 26 | 27 | [node name="Background" type="Sprite2D" parent="ParallaxBackground"] 28 | texture_repeat = 2 29 | material = SubResource("ShaderMaterial_vqait") 30 | texture = ExtResource("4_47oqa") 31 | centered = false 32 | region_enabled = true 33 | region_rect = Rect2(0, 0, 12000, 12000) 34 | 35 | [node name="Gui" type="CanvasLayer" parent="."] 36 | 37 | [node name="VBoxContainer" type="VBoxContainer" parent="Gui"] 38 | custom_minimum_size = Vector2(500, 0) 39 | anchors_preset = 8 40 | anchor_left = 0.5 41 | anchor_top = 0.5 42 | anchor_right = 0.5 43 | anchor_bottom = 0.5 44 | offset_left = -33.5 45 | offset_top = -33.0 46 | offset_right = 33.5 47 | offset_bottom = 33.0 48 | grow_horizontal = 2 49 | grow_vertical = 2 50 | 51 | [node name="TitleLabel" type="Label" parent="Gui/VBoxContainer"] 52 | layout_mode = 2 53 | text = "Agarust" 54 | label_settings = SubResource("LabelSettings_yei6d") 55 | horizontal_alignment = 1 56 | vertical_alignment = 1 57 | 58 | [node name="UsernameEdit" type="LineEdit" parent="Gui/VBoxContainer"] 59 | unique_name_in_owner = true 60 | layout_mode = 2 61 | placeholder_text = "Username" 62 | max_length = 16 63 | clear_button_enabled = true 64 | select_all_on_focus = true 65 | 66 | [node name="PasswordEdit" type="LineEdit" parent="Gui/VBoxContainer"] 67 | unique_name_in_owner = true 68 | layout_mode = 2 69 | placeholder_text = "Password" 70 | max_length = 64 71 | clear_button_enabled = true 72 | select_all_on_focus = true 73 | secret = true 74 | 75 | [node name="HBoxContainer" type="HBoxContainer" parent="Gui/VBoxContainer"] 76 | layout_mode = 2 77 | 78 | [node name="LoginButton" type="Button" parent="Gui/VBoxContainer/HBoxContainer"] 79 | unique_name_in_owner = true 80 | custom_minimum_size = Vector2(100, 0) 81 | layout_mode = 2 82 | text = "Login" 83 | 84 | [node name="RegisterButton" type="Button" parent="Gui/VBoxContainer/HBoxContainer"] 85 | unique_name_in_owner = true 86 | custom_minimum_size = Vector2(100, 0) 87 | layout_mode = 2 88 | text = "Register" 89 | 90 | [node name="LeaderboardButton" type="Button" parent="Gui/VBoxContainer/HBoxContainer"] 91 | unique_name_in_owner = true 92 | layout_mode = 2 93 | size_flags_horizontal = 10 94 | text = "Leaderboard" 95 | 96 | [node name="Logger" parent="Gui" instance=ExtResource("3_m0dil")] 97 | unique_name_in_owner = true 98 | anchors_preset = 7 99 | anchor_left = 0.5 100 | anchor_top = 1.0 101 | anchor_right = 0.5 102 | offset_left = -256.0 103 | offset_top = -440.0 104 | offset_right = 256.0 105 | offset_bottom = -16.0 106 | grow_vertical = 0 107 | 108 | [node name="ConnectionIdLabel" type="Label" parent="Gui"] 109 | unique_name_in_owner = true 110 | offset_left = 16.0 111 | offset_top = 16.0 112 | offset_right = 165.0 113 | offset_bottom = 39.0 114 | text = "Connection ID: ABC" 115 | 116 | [node name="Ping" parent="Gui" instance=ExtResource("4_g2reg")] 117 | offset_left = 16.0 118 | offset_top = 48.0 119 | offset_right = 96.0 120 | offset_bottom = 72.0 121 | 122 | [node name="MessagePanel" type="Panel" parent="Gui"] 123 | unique_name_in_owner = true 124 | visible = false 125 | anchors_preset = 8 126 | anchor_left = 0.5 127 | anchor_top = 0.5 128 | anchor_right = 0.5 129 | anchor_bottom = 0.5 130 | offset_left = -300.0 131 | offset_top = -200.0 132 | offset_right = 300.0 133 | offset_bottom = 200.0 134 | grow_horizontal = 2 135 | grow_vertical = 2 136 | 137 | [node name="MessageLabel" type="Label" parent="Gui/MessagePanel"] 138 | unique_name_in_owner = true 139 | layout_mode = 1 140 | anchors_preset = 14 141 | anchor_top = 0.5 142 | anchor_right = 1.0 143 | anchor_bottom = 0.5 144 | offset_top = -11.5 145 | offset_bottom = 11.5 146 | grow_horizontal = 2 147 | grow_vertical = 2 148 | text = "VFX Pre Compile ..." 149 | label_settings = SubResource("LabelSettings_g1chx") 150 | horizontal_alignment = 1 151 | vertical_alignment = 1 152 | -------------------------------------------------------------------------------- /client/view/register/register.gd: -------------------------------------------------------------------------------- 1 | extends Node2D 2 | 3 | @export var game_scene: PackedScene 4 | 5 | @onready var username_edit: LineEdit = %UsernameEdit 6 | @onready var password_edit: LineEdit = %PasswordEdit 7 | @onready var password_edit_2: LineEdit = %PasswordEdit2 8 | @onready var color_picker: ColorPicker = %ColorPicker 9 | @onready var register_button: Button = %RegisterButton 10 | @onready var back_button: Button = %BackButton 11 | @onready var logger: Logger = %Logger 12 | @onready var connection_id_label: Label = %ConnectionIdLabel 13 | @onready var message_panel: Panel = %MessagePanel 14 | @onready var message_label: Label = %MessageLabel 15 | 16 | 17 | func _ready() -> void: 18 | WsClient.packet_received.connect(_on_ws_packet_received) 19 | register_button.pressed.connect(_on_register_button_pressed) 20 | back_button.pressed.connect(_on_back_button_pressed) 21 | connection_id_label.text = "Connection ID: %s" % [Global.connection_id] 22 | 23 | 24 | func _on_ws_packet_received(packet: Global.proto.Packet) -> void: 25 | if packet.has_register_ok(): 26 | message_panel.hide() 27 | logger.success("register success") 28 | elif packet.has_register_err(): 29 | message_panel.hide() 30 | logger.error(packet.get_register_err().get_reason()) 31 | 32 | 33 | func _on_register_button_pressed() -> void: 34 | var username := username_edit.text.strip_edges() 35 | var password := password_edit.text.strip_edges() 36 | var password2 := password_edit_2.text.strip_edges() 37 | if username.is_empty() or password.is_empty(): 38 | return 39 | if password != password2: 40 | logger.error("passwords do not match") 41 | return 42 | 43 | message_panel.show() 44 | message_label.text = "Loading..." 45 | 46 | var packet := Global.proto.Packet.new() 47 | var register_message := packet.new_register() 48 | register_message.set_username(username) 49 | register_message.set_password(password) 50 | register_message.set_color(color_picker.color.to_rgba64()) 51 | WsClient.send(packet) 52 | 53 | 54 | func _on_back_button_pressed() -> void: 55 | get_tree().change_scene_to_file("res://view/login/login.tscn") 56 | -------------------------------------------------------------------------------- /client/view/register/register.gd.uid: -------------------------------------------------------------------------------- 1 | uid://1ms5shec5bs4 2 | -------------------------------------------------------------------------------- /client/view/register/register.tscn: -------------------------------------------------------------------------------- 1 | [gd_scene load_steps=8 format=3 uid="uid://dcmx5j0nynrpq"] 2 | 3 | [ext_resource type="Script" uid="uid://1ms5shec5bs4" path="res://view/register/register.gd" id="1_kfcug"] 4 | [ext_resource type="Shader" uid="uid://cuybon2vubibm" path="res://assets/background_effect.gdshader" id="3_64uqn"] 5 | [ext_resource type="Texture2D" uid="uid://dyo2iothubrem" path="res://assets/background.svg" id="4_nt46l"] 6 | [ext_resource type="PackedScene" uid="uid://cpmsnc8fuugqe" path="res://component/logger/logger.tscn" id="5_o2ql1"] 7 | 8 | [sub_resource type="ShaderMaterial" id="ShaderMaterial_vqait"] 9 | shader = ExtResource("3_64uqn") 10 | shader_parameter/amplitutde = Vector2(1, 0) 11 | shader_parameter/speed = Vector2(1, 0) 12 | 13 | [sub_resource type="LabelSettings" id="LabelSettings_yei6d"] 14 | font_size = 36 15 | 16 | [sub_resource type="LabelSettings" id="LabelSettings_g1chx"] 17 | font_size = 40 18 | 19 | [node name="Register" type="Node2D"] 20 | script = ExtResource("1_kfcug") 21 | 22 | [node name="ParallaxBackground" type="Parallax2D" parent="."] 23 | repeat_size = Vector2(12000, 12000) 24 | repeat_times = 2 25 | 26 | [node name="Background" type="Sprite2D" parent="ParallaxBackground"] 27 | texture_repeat = 2 28 | material = SubResource("ShaderMaterial_vqait") 29 | texture = ExtResource("4_nt46l") 30 | centered = false 31 | region_enabled = true 32 | region_rect = Rect2(0, 0, 12000, 12000) 33 | 34 | [node name="Gui" type="CanvasLayer" parent="."] 35 | 36 | [node name="VBoxContainer" type="VBoxContainer" parent="Gui"] 37 | custom_minimum_size = Vector2(500, 0) 38 | anchors_preset = 8 39 | anchor_left = 0.5 40 | anchor_top = 0.5 41 | anchor_right = 0.5 42 | anchor_bottom = 0.5 43 | offset_left = -33.5 44 | offset_top = -33.0 45 | offset_right = 33.5 46 | offset_bottom = 33.0 47 | grow_horizontal = 2 48 | grow_vertical = 2 49 | 50 | [node name="TitleLabel" type="Label" parent="Gui/VBoxContainer"] 51 | layout_mode = 2 52 | text = "Agarust" 53 | label_settings = SubResource("LabelSettings_yei6d") 54 | horizontal_alignment = 1 55 | vertical_alignment = 1 56 | 57 | [node name="UsernameEdit" type="LineEdit" parent="Gui/VBoxContainer"] 58 | unique_name_in_owner = true 59 | layout_mode = 2 60 | placeholder_text = "Username" 61 | max_length = 16 62 | clear_button_enabled = true 63 | select_all_on_focus = true 64 | 65 | [node name="PasswordEdit" type="LineEdit" parent="Gui/VBoxContainer"] 66 | unique_name_in_owner = true 67 | layout_mode = 2 68 | placeholder_text = "Password" 69 | max_length = 64 70 | clear_button_enabled = true 71 | select_all_on_focus = true 72 | secret = true 73 | 74 | [node name="PasswordEdit2" type="LineEdit" parent="Gui/VBoxContainer"] 75 | unique_name_in_owner = true 76 | layout_mode = 2 77 | placeholder_text = "Confirm password" 78 | max_length = 64 79 | clear_button_enabled = true 80 | select_all_on_focus = true 81 | secret = true 82 | 83 | [node name="ColorPicker" type="ColorPicker" parent="Gui/VBoxContainer"] 84 | unique_name_in_owner = true 85 | layout_mode = 2 86 | color = Color(0.0392157, 0.156863, 0.835294, 1) 87 | edit_alpha = false 88 | sampler_visible = false 89 | color_modes_visible = false 90 | sliders_visible = false 91 | hex_visible = false 92 | presets_visible = false 93 | 94 | [node name="HBoxContainer" type="HBoxContainer" parent="Gui/VBoxContainer"] 95 | layout_mode = 2 96 | 97 | [node name="RegisterButton" type="Button" parent="Gui/VBoxContainer/HBoxContainer"] 98 | unique_name_in_owner = true 99 | custom_minimum_size = Vector2(100, 0) 100 | layout_mode = 2 101 | text = "Register" 102 | 103 | [node name="BackButton" type="Button" parent="Gui/VBoxContainer/HBoxContainer"] 104 | unique_name_in_owner = true 105 | layout_mode = 2 106 | size_flags_horizontal = 10 107 | text = "Back" 108 | 109 | [node name="Logger" parent="Gui" instance=ExtResource("5_o2ql1")] 110 | unique_name_in_owner = true 111 | anchors_preset = 7 112 | anchor_left = 0.5 113 | anchor_top = 1.0 114 | anchor_right = 0.5 115 | offset_left = -256.0 116 | offset_top = -296.0 117 | offset_right = 256.0 118 | offset_bottom = -16.0 119 | grow_vertical = 0 120 | 121 | [node name="ConnectionIdLabel" type="Label" parent="Gui"] 122 | unique_name_in_owner = true 123 | offset_left = 16.0 124 | offset_top = 16.0 125 | offset_right = 165.0 126 | offset_bottom = 39.0 127 | text = "Connection ID: ABC" 128 | 129 | [node name="MessagePanel" type="Panel" parent="Gui"] 130 | unique_name_in_owner = true 131 | visible = false 132 | anchors_preset = 8 133 | anchor_left = 0.5 134 | anchor_top = 0.5 135 | anchor_right = 0.5 136 | anchor_bottom = 0.5 137 | offset_left = -300.0 138 | offset_top = -200.0 139 | offset_right = 300.0 140 | offset_bottom = 200.0 141 | grow_horizontal = 2 142 | grow_vertical = 2 143 | 144 | [node name="MessageLabel" type="Label" parent="Gui/MessagePanel"] 145 | unique_name_in_owner = true 146 | layout_mode = 1 147 | anchors_preset = 14 148 | anchor_top = 0.5 149 | anchor_right = 1.0 150 | anchor_bottom = 0.5 151 | offset_top = -11.5 152 | offset_bottom = 11.5 153 | grow_horizontal = 2 154 | grow_vertical = 2 155 | text = "Message Label" 156 | label_settings = SubResource("LabelSettings_g1chx") 157 | horizontal_alignment = 1 158 | vertical_alignment = 1 159 | -------------------------------------------------------------------------------- /proto/packet.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package proto; 4 | 5 | message Packet { 6 | oneof data { 7 | Ping ping = 1; 8 | Hello hello = 2; 9 | Login login = 3; 10 | LoginOk login_ok = 4; 11 | LoginErr login_err = 5; 12 | Register register = 6; 13 | RegisterOk register_ok = 7; 14 | RegisterErr register_err = 8; 15 | Join join = 9; 16 | Disconnect disconnect = 10; 17 | Chat chat = 11; 18 | UpdatePlayer update_player = 12; 19 | UpdatePlayerBatch update_player_batch = 13; 20 | UpdatePlayerDirectionAngle update_player_direction_angle = 14; 21 | UpdateSpore update_spore = 15; 22 | UpdateSporeBatch update_spore_batch = 16; 23 | ConsumeSpore consume_spore = 17; 24 | ConsumePlayer consume_player = 18; 25 | Rush rush = 19; 26 | LeaderboardRequest leaderboard_request = 20; 27 | LeaderboardResponse leaderboard_response = 21; 28 | } 29 | } 30 | 31 | message Ping { int64 client_timestamp = 1; } 32 | 33 | message Hello { string connection_id = 1; } 34 | 35 | message Login { 36 | string username = 1; 37 | string password = 2; 38 | } 39 | 40 | message LoginOk {} 41 | 42 | message LoginErr { string reason = 1; } 43 | 44 | message Register { 45 | string username = 1; 46 | string password = 2; 47 | int64 color = 3; 48 | } 49 | 50 | message RegisterOk {} 51 | 52 | message RegisterErr { string reason = 1; } 53 | 54 | message Join {} 55 | 56 | message Disconnect { 57 | string connection_id = 1; 58 | string reason = 2; 59 | } 60 | 61 | message Chat { 62 | string connection_id = 1; 63 | string msg = 2; 64 | } 65 | 66 | message UpdatePlayer { 67 | string connection_id = 1; 68 | string nickname = 2; 69 | double x = 3; 70 | double y = 4; 71 | double radius = 5; 72 | double direction_angle = 6; 73 | double speed = 7; 74 | int64 color = 8; 75 | bool is_rushing = 9; 76 | } 77 | 78 | message UpdatePlayerBatch { repeated UpdatePlayer update_player_batch = 1; } 79 | 80 | message UpdatePlayerDirectionAngle { double direction_angle = 1; } 81 | 82 | message UpdateSpore { 83 | string id = 1; 84 | double x = 2; 85 | double y = 3; 86 | double radius = 4; 87 | } 88 | 89 | message UpdateSporeBatch { repeated UpdateSpore update_spore_batch = 1; } 90 | 91 | message ConsumeSpore { 92 | string connection_id = 1; 93 | string spore_id = 2; 94 | } 95 | 96 | message ConsumePlayer { 97 | string connection_id = 1; 98 | string victim_connection_id = 2; 99 | } 100 | 101 | message Rush {} 102 | 103 | message LeaderboardRequest {} 104 | 105 | message LeaderboardEntry { 106 | uint64 rank = 1; 107 | string player_nickname = 2; 108 | uint64 score = 3; 109 | } 110 | 111 | message LeaderboardResponse { 112 | repeated LeaderboardEntry leaderboard_entry_list = 1; 113 | } 114 | -------------------------------------------------------------------------------- /server/.env: -------------------------------------------------------------------------------- 1 | BIND_ADDR=127.0.0.1:8080 2 | DATABASE_URL=sqlite:agarust_db.sqlite 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.log.* 3 | agarust_db.sqlite 4 | upload.bat 5 | -------------------------------------------------------------------------------- /server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2024" 3 | name = "agarust-server" 4 | version = "0.1.0" 5 | 6 | [dependencies] 7 | anyhow = "*" 8 | bcrypt = "*" 9 | bytes = "*" 10 | dotenv = "*" 11 | futures-util = "*" 12 | hashbrown = "*" 13 | nanoid = "*" 14 | prost = "*" 15 | rand = "*" 16 | sqlx = {version = "*", features = ["runtime-tokio", "sqlite"]} 17 | tokio = {version = "*", features = ["full"]} 18 | tokio-tungstenite = "*" 19 | tracing = "*" 20 | tracing-appender = "*" 21 | tracing-subscriber = "*" 22 | 23 | [build-dependencies] 24 | prost-build = "*" 25 | 26 | [profile.release] 27 | codegen-units = 1 28 | lto = true 29 | panic = "abort" 30 | strip = true 31 | -------------------------------------------------------------------------------- /server/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | unsafe { std::env::set_var("OUT_DIR", "src/") }; 3 | if let Err(e) = prost_build::compile_protos(&["../proto/packet.proto"], &["../proto/"]) { 4 | eprintln!("compile_protos error: {:?}", e); 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /server/cross_build_linux.bat: -------------------------------------------------------------------------------- 1 | cross b -r --target x86_64-unknown-linux-musl 2 | -------------------------------------------------------------------------------- /server/migrations/20250113000000_create_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS auth ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 3 | username TEXT NOT NULL UNIQUE, 4 | password TEXT NOT NULL 5 | ); 6 | 7 | CREATE TABLE IF NOT EXISTS player ( 8 | id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, 9 | auth_id INTEGER NOT NULL, 10 | nickname TEXT NOT NULL UNIQUE, 11 | color INTEGER NOT NULL, 12 | best_score INTEGER NOT NULL DEFAULT 0 13 | ); 14 | -------------------------------------------------------------------------------- /server/sqlx_migrate.bat: -------------------------------------------------------------------------------- 1 | sqlx migrate run --database-url sqlite:agarust_db.sqlite?mode=rwc 2 | -------------------------------------------------------------------------------- /server/src/client_agent.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use anyhow::Result; 3 | use bytes::Bytes; 4 | use futures_util::{SinkExt, StreamExt}; 5 | use prost::Message as _; 6 | use sqlx::query_as; 7 | use std::{io::Cursor, net::SocketAddr, sync::Arc, time::Duration}; 8 | use tokio::{ 9 | net::TcpStream, 10 | sync::{ 11 | mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, 12 | oneshot, 13 | }, 14 | time::interval, 15 | }; 16 | use tokio_tungstenite::{WebSocketStream, tungstenite::Message}; 17 | use tracing::{error, info, warn}; 18 | 19 | #[derive(Debug)] 20 | pub struct ClientAgent { 21 | pub ws_stream: WebSocketStream, 22 | pub socket_addr: SocketAddr, 23 | pub connection_id: Arc, 24 | pub db: db::Db, 25 | pub hub_command_sender: UnboundedSender, 26 | pub client_agent_command_sender: UnboundedSender, 27 | pub client_agent_command_receiver: UnboundedReceiver, 28 | pub db_player: Option, 29 | } 30 | 31 | impl ClientAgent { 32 | pub async fn new( 33 | ws_stream: WebSocketStream, 34 | socket_addr: SocketAddr, 35 | db: db::Db, 36 | hub_command_sender: UnboundedSender, 37 | ) -> Result { 38 | let (client_agent_command_sender, client_agent_command_receiver) = 39 | unbounded_channel::(); 40 | 41 | let connection_id = { 42 | let client_agent_command_sender = client_agent_command_sender.clone(); 43 | let (response_sender, response_receiver) = oneshot::channel(); 44 | hub_command_sender.send(command::Command::RegisterClientAgent { 45 | socket_addr, 46 | client_agent_command_sender, 47 | response_sender, 48 | })?; 49 | response_receiver.await? 50 | }; 51 | 52 | let client_agent = Self { 53 | ws_stream, 54 | socket_addr, 55 | connection_id, 56 | db, 57 | hub_command_sender, 58 | client_agent_command_sender, 59 | client_agent_command_receiver, 60 | db_player: None, 61 | }; 62 | 63 | Ok(client_agent) 64 | } 65 | 66 | pub async fn run(mut self) { 67 | let hello_packet = proto_util::hello_packet(self.connection_id.clone()); 68 | self.send_packet(&hello_packet).await; 69 | 70 | loop { 71 | tokio::select! { 72 | ws_stream_next = self.ws_stream.next() => { 73 | match ws_stream_next { 74 | Some(ws_stream_next_result) => { 75 | match ws_stream_next_result { 76 | Ok(ws_stream_message) => { 77 | self.handle_ws_stream_message(ws_stream_message).await; 78 | }, 79 | Err(e) => { 80 | warn!("ws_stream_next_result error, disconnect {:?}: {:?}", self.socket_addr, e); 81 | break; 82 | }, 83 | } 84 | }, 85 | None => { 86 | warn!("ws_stream_next None, disconnect {:?}", self.socket_addr); 87 | break; 88 | }, 89 | } 90 | }, 91 | command_recv = self.client_agent_command_receiver.recv() => { 92 | match command_recv { 93 | Some(command) => { 94 | self.handle_command(command).await; 95 | }, 96 | None => { 97 | warn!("command_recv None, disconnect {:?}", self.socket_addr); 98 | break; 99 | }, 100 | } 101 | }, 102 | }; 103 | } 104 | 105 | let _ = self 106 | .hub_command_sender 107 | .send(command::Command::UnregisterClientAgent { 108 | connection_id: self.connection_id, 109 | }); 110 | } 111 | 112 | async fn handle_ws_stream_message(&mut self, ws_stream_message: Message) { 113 | match ws_stream_message { 114 | Message::Binary(bytes) => match proto::Packet::decode(Cursor::new(bytes)) { 115 | Ok(packet) => { 116 | self.handle_packet(&packet).await; 117 | } 118 | Err(e) => { 119 | warn!("proto decode error {:?}: {:?}", self, e); 120 | } 121 | }, 122 | Message::Close(close_frame) => { 123 | info!("client close_frame: {:?}", close_frame); 124 | let _ = self.ws_stream.close(None).await; 125 | } 126 | _ => { 127 | warn!("unkonwn message: {:?}", ws_stream_message); 128 | } 129 | } 130 | } 131 | 132 | async fn handle_packet(&mut self, packet: &proto::Packet) { 133 | let data = match &packet.data { 134 | Some(data) => data, 135 | None => { 136 | warn!("packet has no data"); 137 | return; 138 | } 139 | }; 140 | match data { 141 | proto::packet::Data::Ping(_) => { 142 | self.send_packet(packet).await; 143 | } 144 | proto::packet::Data::Login(login) => { 145 | let auth = match self.db.auth_get_one_by_username(&login.username).await { 146 | Ok(auth) => auth, 147 | Err(e) => { 148 | warn!("auth query error: {:?}", e); 149 | let packet = 150 | proto_util::login_err_packet("incorrect username or password".into()); 151 | self.send_packet(&packet).await; 152 | return; 153 | } 154 | }; 155 | 156 | match bcrypt::verify(&login.password, &auth.password) { 157 | Ok(valid) => { 158 | if !valid { 159 | warn!("bcrypt valid false"); 160 | let packet = proto_util::login_err_packet( 161 | "incorrect username or password".into(), 162 | ); 163 | self.send_packet(&packet).await; 164 | return; 165 | } 166 | } 167 | Err(e) => { 168 | warn!("bcrypt verify error: {:?}", e); 169 | let packet = 170 | proto_util::login_err_packet("incorrect username or password".into()); 171 | self.send_packet(&packet).await; 172 | return; 173 | } 174 | } 175 | 176 | let player = match self.db.player_get_one_by_auth_id(auth.id).await { 177 | Ok(player) => player, 178 | Err(e) => { 179 | warn!("player query error: {:?}", e); 180 | let packet = 181 | proto_util::login_err_packet("incorrect username or password".into()); 182 | self.send_packet(&packet).await; 183 | return; 184 | } 185 | }; 186 | 187 | self.db_player = Some(player); 188 | 189 | let packet = proto_util::login_ok_packet(); 190 | self.send_packet(&packet).await; 191 | } 192 | proto::packet::Data::Register(register) => { 193 | let mut transaction = match self.db.db_pool.begin().await { 194 | Ok(transaction) => transaction, 195 | Err(e) => { 196 | warn!("transaction begin error: {:?}", e); 197 | let packet = 198 | proto_util::register_err_packet("transaction begin error".into()); 199 | self.send_packet(&packet).await; 200 | return; 201 | } 202 | }; 203 | 204 | if register.username.is_empty() { 205 | let packet = proto_util::register_err_packet("username is empty".into()); 206 | self.send_packet(&packet).await; 207 | return; 208 | } 209 | if register.username.len() > 16 { 210 | let packet = proto_util::register_err_packet("username too long".into()); 211 | self.send_packet(&packet).await; 212 | return; 213 | } 214 | 215 | let query_result = query_as!( 216 | db::Auth, 217 | r#"SELECT * FROM auth WHERE username = ? LIMIT 1"#, 218 | register.username 219 | ) 220 | .fetch_one(&mut *transaction) 221 | .await; 222 | 223 | if query_result.is_ok() { 224 | let packet = proto_util::register_err_packet("username already exists".into()); 225 | self.send_packet(&packet).await; 226 | return; 227 | } 228 | 229 | let password = match bcrypt::hash(®ister.password, bcrypt::DEFAULT_COST) { 230 | Ok(password) => password, 231 | Err(_) => { 232 | let packet = proto_util::register_err_packet("password hash error".into()); 233 | self.send_packet(&packet).await; 234 | return; 235 | } 236 | }; 237 | 238 | let query_result = query_as!( 239 | db::Auth, 240 | r#"INSERT INTO auth ( username, password ) VALUES ( ?, ? )"#, 241 | register.username, 242 | password, 243 | ) 244 | .execute(&mut *transaction) 245 | .await; 246 | 247 | let auth_id = match query_result { 248 | Ok(query_result) => query_result.last_insert_rowid(), 249 | Err(e) => { 250 | warn!("auth insert error: {:?}", e); 251 | let packet = proto_util::register_err_packet("auth insert error".into()); 252 | self.send_packet(&packet).await; 253 | return; 254 | } 255 | }; 256 | 257 | // force alpha 0xFFFF 258 | let color = register.color | 0xFFFF; 259 | let query_result = query_as!( 260 | db::Player, 261 | r#"INSERT INTO player ( auth_id, nickname, color ) VALUES ( ?, ?, ? )"#, 262 | auth_id, 263 | register.username, 264 | color, 265 | ) 266 | .execute(&mut *transaction) 267 | .await; 268 | 269 | if let Err(e) = query_result { 270 | warn!("player insert error: {:?}", e); 271 | let packet = proto_util::register_err_packet("player insert error".into()); 272 | self.send_packet(&packet).await; 273 | return; 274 | } 275 | 276 | if let Err(e) = transaction.commit().await { 277 | warn!("transaction commit error: {:?}", e); 278 | let packet = proto_util::register_err_packet("transaction commit error".into()); 279 | self.send_packet(&packet).await; 280 | return; 281 | } 282 | 283 | let packet = proto_util::register_ok_packet(); 284 | self.send_packet(&packet).await; 285 | } 286 | proto::packet::Data::Join(_) => { 287 | let db_player = match self.db_player.as_ref() { 288 | Some(db_player) => db_player, 289 | None => { 290 | warn!("join without login"); 291 | return; 292 | } 293 | }; 294 | let _ = self.hub_command_sender.send(command::Command::Join { 295 | connection_id: self.connection_id.clone(), 296 | player_db_id: db_player.id, 297 | nickname: db_player.nickname.clone(), 298 | color: db_player.color, 299 | }); 300 | } 301 | proto::packet::Data::Chat(chat) => { 302 | let _ = self.hub_command_sender.send(command::Command::Chat { 303 | connection_id: self.connection_id.clone(), 304 | msg: chat.msg.as_str().into(), 305 | }); 306 | } 307 | proto::packet::Data::UpdatePlayerDirectionAngle(update_player_direction_angle) => { 308 | let _ = 309 | self.hub_command_sender 310 | .send(command::Command::UpdatePlayerDirectionAngle { 311 | connection_id: self.connection_id.clone(), 312 | direction_angle: update_player_direction_angle.direction_angle, 313 | }); 314 | } 315 | proto::packet::Data::ConsumeSpore(consume_spore) => { 316 | let _ = self 317 | .hub_command_sender 318 | .send(command::Command::ConsumeSpore { 319 | connection_id: self.connection_id.clone(), 320 | spore_id: consume_spore.spore_id.as_str().into(), 321 | }); 322 | } 323 | proto::packet::Data::ConsumePlayer(consume_player) => { 324 | let _ = self 325 | .hub_command_sender 326 | .send(command::Command::ConsumePlayer { 327 | connection_id: self.connection_id.clone(), 328 | victim_connection_id: consume_player.victim_connection_id.as_str().into(), 329 | }); 330 | } 331 | proto::packet::Data::Rush(_) => { 332 | let _ = self.hub_command_sender.send(command::Command::Rush { 333 | connection_id: self.connection_id.clone(), 334 | }); 335 | } 336 | proto::packet::Data::Disconnect(_) => { 337 | let _ = self 338 | .client_agent_command_sender 339 | .send(command::Command::DisconnectClinet); 340 | } 341 | proto::packet::Data::LeaderboardRequest(_) => { 342 | let leaderboard_entry_list = match self.db.player_get_list(100).await { 343 | Ok(player_list) => player_list 344 | .iter() 345 | .enumerate() 346 | .map(|(index, player)| command::LeaderboardEntry { 347 | rank: (index + 1) as u64, 348 | player_nickname: player.nickname.clone(), 349 | score: player.best_score as u64, 350 | }) 351 | .collect::>(), 352 | Err(e) => { 353 | error!("fetch leaderboard error: {:?}", e); 354 | return; 355 | } 356 | }; 357 | 358 | let packet = proto_util::leaderboard_response(&leaderboard_entry_list); 359 | self.send_packet(&packet).await; 360 | } 361 | _ => { 362 | warn!("unknown packet data: {:?}", data); 363 | } 364 | } 365 | } 366 | 367 | async fn handle_command(&mut self, command: command::Command) { 368 | match command { 369 | command::Command::SendPacket { packet } => { 370 | self.send_packet(&packet).await; 371 | } 372 | command::Command::SendBytes { bytes } => { 373 | self.send_bytes(bytes).await; 374 | } 375 | command::Command::UpdateSporeBatch { spore_batch } => { 376 | const SEND_INTERNAL_DURATION: Duration = Duration::from_millis(20); 377 | const SPORE_CHUNKS: usize = 20; 378 | let client_agent_command_sender = self.client_agent_command_sender.clone(); 379 | tokio::spawn(async move { 380 | let mut send_interval = interval(SEND_INTERNAL_DURATION); 381 | for spore_chunk in spore_batch.chunks(SPORE_CHUNKS) { 382 | let packet = proto_util::update_spore_batch_packet(spore_chunk); 383 | let bytes = packet.encode_to_vec().into(); 384 | let _ = 385 | client_agent_command_sender.send(command::Command::SendBytes { bytes }); 386 | send_interval.tick().await; 387 | } 388 | }); 389 | } 390 | command::Command::SyncPlayerBestScore { current_score } => { 391 | let db_player_id = { 392 | let db_player = match self.db_player.as_mut() { 393 | Some(db_player) => db_player, 394 | None => { 395 | warn!("sync player best score without login"); 396 | return; 397 | } 398 | }; 399 | if db_player.best_score > current_score { 400 | return; 401 | } 402 | 403 | db_player.best_score = current_score; 404 | 405 | db_player.id 406 | }; 407 | 408 | if let Err(e) = self 409 | .db 410 | .player_update_best_score_by_id(current_score, db_player_id) 411 | .await 412 | { 413 | error!("UPDATE player SET best_score error: {:?}", e); 414 | } 415 | } 416 | command::Command::DisconnectClinet => { 417 | warn!("Command::DisconnectClinet"); 418 | let _ = self.ws_stream.close(None).await; 419 | } 420 | _ => { 421 | warn!("unknown command: {:?}", command); 422 | } 423 | } 424 | } 425 | 426 | async fn send_packet(&mut self, packet: &proto::Packet) { 427 | let bytes = packet.encode_to_vec().into(); 428 | self.send_bytes(bytes).await; 429 | } 430 | 431 | async fn send_bytes(&mut self, bytes: Bytes) { 432 | let _ = self.ws_stream.send(Message::binary(bytes)).await; 433 | } 434 | } 435 | -------------------------------------------------------------------------------- /server/src/command.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug)] 4 | pub enum Command { 5 | RegisterClientAgent { 6 | socket_addr: SocketAddr, 7 | client_agent_command_sender: UnboundedSender, 8 | response_sender: tokio::sync::oneshot::Sender>, 9 | }, 10 | UnregisterClientAgent { 11 | connection_id: Arc, 12 | }, 13 | Join { 14 | connection_id: Arc, 15 | player_db_id: i64, 16 | nickname: Arc, 17 | color: i64, 18 | }, 19 | DisconnectClinet, 20 | SendPacket { 21 | packet: proto::Packet, 22 | }, 23 | SendBytes { 24 | bytes: bytes::Bytes, 25 | }, 26 | SyncPlayerBestScore { 27 | current_score: i64, 28 | }, 29 | Chat { 30 | connection_id: Arc, 31 | msg: Arc, 32 | }, 33 | UpdatePlayerDirectionAngle { 34 | connection_id: Arc, 35 | direction_angle: f64, 36 | }, 37 | UpdateSporeBatch { 38 | spore_batch: Vec, 39 | }, 40 | ConsumeSpore { 41 | connection_id: Arc, 42 | spore_id: Arc, 43 | }, 44 | ConsumePlayer { 45 | connection_id: Arc, 46 | victim_connection_id: Arc, 47 | }, 48 | Rush { 49 | connection_id: Arc, 50 | }, 51 | LeaderboardRequest, 52 | LeaderboardResponse { 53 | entry_list: Vec, 54 | }, 55 | } 56 | 57 | #[derive(Debug, Clone)] 58 | pub struct LeaderboardEntry { 59 | pub rank: u64, 60 | pub player_nickname: Arc, 61 | pub score: u64, 62 | } 63 | -------------------------------------------------------------------------------- /server/src/db.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use sqlx::{Pool, Sqlite, query_as, sqlite::SqliteQueryResult}; 3 | use std::sync::Arc; 4 | 5 | #[derive(Debug)] 6 | pub struct Auth { 7 | pub id: i64, 8 | pub username: Arc, 9 | pub password: Arc, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub struct Player { 14 | pub id: i64, 15 | pub auth_id: i64, 16 | pub nickname: Arc, 17 | pub color: i64, 18 | pub best_score: i64, 19 | } 20 | 21 | #[derive(Debug, Clone)] 22 | pub struct Db { 23 | pub db_pool: Pool, 24 | } 25 | 26 | impl Db { 27 | pub async fn new(database_url: &str) -> Result { 28 | let db_pool = sqlx::sqlite::SqlitePool::connect(database_url).await?; 29 | Ok(Self { db_pool }) 30 | } 31 | 32 | pub async fn auth_get_one_by_username(&self, username: &str) -> Result { 33 | query_as!( 34 | Auth, 35 | r#"SELECT * FROM auth WHERE username = ? LIMIT 1"#, 36 | username 37 | ) 38 | .fetch_one(&self.db_pool) 39 | .await 40 | .map_err(|e| e.into()) 41 | } 42 | 43 | pub async fn player_get_one_by_auth_id(&self, auth_id: i64) -> Result { 44 | query_as!( 45 | Player, 46 | r#"SELECT * FROM player WHERE auth_id = ? LIMIT 1"#, 47 | auth_id 48 | ) 49 | .fetch_one(&self.db_pool) 50 | .await 51 | .map_err(|e| e.into()) 52 | } 53 | 54 | pub async fn player_get_list(&self, limit: i64) -> Result> { 55 | query_as!( 56 | Player, 57 | r#"SELECT * FROM player ORDER BY best_score DESC LIMIT ?"#, 58 | limit, 59 | ) 60 | .fetch_all(&self.db_pool) 61 | .await 62 | .map_err(|e| e.into()) 63 | } 64 | 65 | pub async fn player_update_best_score_by_id( 66 | &self, 67 | best_score: i64, 68 | id: i64, 69 | ) -> Result { 70 | query_as!( 71 | db::Player, 72 | r#"UPDATE player SET best_score = ? WHERE id = ?"#, 73 | best_score, 74 | id, 75 | ) 76 | .execute(&self.db_pool) 77 | .await 78 | .map_err(|e| e.into()) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /server/src/hub.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use bytes::Bytes; 3 | use hashbrown::HashMap; 4 | use nanoid::nanoid; 5 | use prost::Message; 6 | use std::time::Duration; 7 | use tokio::{ 8 | select, 9 | sync::mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel}, 10 | time::{Instant, interval}, 11 | }; 12 | use tracing::{error, info, warn}; 13 | 14 | const TICK_DURATION: Duration = Duration::from_millis(50); 15 | const SPAWN_SPORE_DURATION: Duration = Duration::from_millis(2000); 16 | const MAX_SPORE_COUNT: usize = 1000; 17 | 18 | #[derive(Debug)] 19 | pub struct Client { 20 | pub socket_addr: SocketAddr, 21 | pub connection_id: Arc, 22 | pub client_agent_command_sender: UnboundedSender, 23 | pub player: Option, 24 | } 25 | 26 | #[derive(Debug)] 27 | pub struct Hub { 28 | pub client_map: HashMap, Client>, 29 | pub spore_map: HashMap, spore::Spore>, 30 | pub command_sender: UnboundedSender, 31 | pub command_receiver: UnboundedReceiver, 32 | pub db: db::Db, 33 | } 34 | 35 | impl Hub { 36 | pub fn new(db: db::Db) -> Self { 37 | let (command_sender, command_receiver) = unbounded_channel::(); 38 | Self { 39 | client_map: HashMap::new(), 40 | spore_map: HashMap::new(), 41 | command_sender, 42 | command_receiver, 43 | db, 44 | } 45 | } 46 | 47 | pub async fn run(mut self) { 48 | for _ in 0..MAX_SPORE_COUNT { 49 | self.spawn_spore(); 50 | } 51 | 52 | let mut tick_interval = interval(TICK_DURATION); 53 | let mut last_tick = Instant::now(); 54 | 55 | let mut spawn_spore_interval = interval(SPAWN_SPORE_DURATION); 56 | 57 | loop { 58 | select! { 59 | _ = tick_interval.tick() => { 60 | let delta = last_tick.elapsed(); 61 | self.tick_player(delta); 62 | last_tick = Instant::now(); 63 | } 64 | _ = spawn_spore_interval.tick() => { 65 | if self.spore_map.len() < MAX_SPORE_COUNT { 66 | self.spawn_spore(); 67 | } 68 | } 69 | Some(command) = self.command_receiver.recv() => { 70 | self.handle_command(command).await; 71 | } 72 | } 73 | } 74 | } 75 | 76 | async fn handle_command(&mut self, command: command::Command) { 77 | match command { 78 | command::Command::RegisterClientAgent { 79 | socket_addr, 80 | client_agent_command_sender, 81 | response_sender, 82 | } => { 83 | info!("RegisterClientAgent: {:?}", socket_addr); 84 | 85 | let connection_id: Arc = nanoid!().into(); 86 | info!("connection_id: {:?}", connection_id); 87 | 88 | let client = Client { 89 | socket_addr, 90 | connection_id: connection_id.clone(), 91 | client_agent_command_sender, 92 | player: None, 93 | }; 94 | self.client_map.insert(connection_id.clone(), client); 95 | 96 | let _ = response_sender.send(connection_id); 97 | } 98 | command::Command::UnregisterClientAgent { connection_id } => { 99 | info!("UnregisterClient: {:?}", connection_id); 100 | 101 | self.client_map.remove(&connection_id); 102 | 103 | let packet = proto_util::disconnect_packet(connection_id, "unregister".into()); 104 | self.broadcast_packet(&packet); 105 | } 106 | command::Command::Join { 107 | connection_id, 108 | player_db_id, 109 | nickname, 110 | color, 111 | } => { 112 | info!( 113 | "PlayerJoin: {:?} {:?} {:?} {:#x?}", 114 | connection_id, player_db_id, nickname, color 115 | ); 116 | 117 | self.client_map.values().for_each(|client| { 118 | let player = match &client.player { 119 | Some(player) => player, 120 | None => return, 121 | }; 122 | if player.db_id == player_db_id { 123 | let _ = client 124 | .client_agent_command_sender 125 | .send(command::Command::DisconnectClinet); 126 | } 127 | }); 128 | 129 | let client = match self.client_map.get_mut(&connection_id) { 130 | Some(client) => client, 131 | None => { 132 | error!("client not found: {:?}", connection_id); 133 | return; 134 | } 135 | }; 136 | 137 | let player = player::Player::random(player_db_id, connection_id, nickname, color); 138 | 139 | let player_x = player.x; 140 | let player_y = player.y; 141 | 142 | client.player = Some(player); 143 | 144 | let mut spore_batch = self.spore_map.values().cloned().collect::>(); 145 | spore_batch.sort_by_cached_key(|spore| { 146 | ((player_x - spore.x).powi(2) + (player_y - spore.y).powi(2)) as i64 147 | }); 148 | 149 | let _ = client 150 | .client_agent_command_sender 151 | .send(command::Command::UpdateSporeBatch { spore_batch }); 152 | } 153 | command::Command::Chat { connection_id, msg } => { 154 | let packet = proto_util::chat_packet(connection_id, msg); 155 | self.broadcast_packet(&packet); 156 | } 157 | command::Command::UpdatePlayerDirectionAngle { 158 | connection_id, 159 | direction_angle, 160 | } => { 161 | if let Some(client) = self.client_map.get_mut(&connection_id) { 162 | if let Some(player) = client.player.as_mut() { 163 | player.direction_angle = direction_angle; 164 | } 165 | } 166 | } 167 | command::Command::ConsumeSpore { 168 | connection_id, 169 | spore_id, 170 | } => { 171 | if let (Some(client), Some(spore)) = ( 172 | self.client_map.get_mut(&connection_id), 173 | self.spore_map.get_mut(&spore_id), 174 | ) { 175 | if let Some(player) = client.player.as_mut() { 176 | let is_close = util::check_distance_is_close( 177 | player.x, 178 | player.y, 179 | player.radius, 180 | spore.x, 181 | spore.y, 182 | spore.radius, 183 | ); 184 | 185 | if !is_close { 186 | warn!("consume spore error, distance too far"); 187 | return; 188 | } 189 | 190 | let spore_mass = util::radius_to_mass(spore.radius); 191 | player.increase_mass(spore_mass); 192 | 193 | self.spore_map.remove(&spore_id); 194 | 195 | let current_score = util::radius_to_mass(player.radius) as i64; 196 | 197 | let client_agent_command_sender = 198 | client.client_agent_command_sender.clone(); 199 | 200 | self.broadcast_packet(&proto_util::consume_spore_packet( 201 | connection_id, 202 | spore_id, 203 | )); 204 | 205 | let _ = client_agent_command_sender 206 | .send(command::Command::SyncPlayerBestScore { current_score }); 207 | } 208 | } 209 | } 210 | command::Command::ConsumePlayer { 211 | connection_id, 212 | victim_connection_id, 213 | } => { 214 | if let [Some(player_client), Some(victim_client)] = self 215 | .client_map 216 | .get_many_mut([&connection_id, &victim_connection_id]) 217 | { 218 | if let (Some(player), Some(victim)) = 219 | (&mut player_client.player, &mut victim_client.player) 220 | { 221 | let player_mass = util::radius_to_mass(player.radius); 222 | let victim_mass = util::radius_to_mass(victim.radius); 223 | 224 | if player_mass < victim_mass * 1.2 { 225 | warn!("consume player error, too small"); 226 | return; 227 | } 228 | 229 | let is_close = util::check_distance_is_close( 230 | player.x, 231 | player.y, 232 | player.radius, 233 | victim.x, 234 | victim.y, 235 | victim.radius, 236 | ); 237 | 238 | if !is_close { 239 | warn!("consume player error, distance too far"); 240 | return; 241 | } 242 | 243 | player.increase_mass(victim_mass); 244 | 245 | victim.respawn(); 246 | } 247 | } 248 | } 249 | command::Command::Rush { connection_id } => { 250 | if let Some(client) = self.client_map.get_mut(&connection_id) { 251 | if let Some(player) = client.player.as_mut() { 252 | if player.radius < 20.0 { 253 | return; 254 | } 255 | if player.rush_instant.is_some() { 256 | return; 257 | } 258 | let player_mass = util::radius_to_mass(player.radius); 259 | let drop_mass = player_mass * 0.2; 260 | if let Some(mass) = player.try_drop_mass(drop_mass) { 261 | player.rush(); 262 | 263 | let mut spore = spore::Spore::random(); 264 | spore.x = player.x; 265 | spore.y = player.y; 266 | spore.radius = util::mass_to_radius(mass); 267 | 268 | let packet = proto_util::update_spore_pack(&spore); 269 | 270 | self.spore_map.insert(spore.id.clone(), spore); 271 | 272 | self.broadcast_packet(&packet); 273 | } 274 | } 275 | } 276 | } 277 | _ => { 278 | warn!("unknown command: {:?}", command); 279 | } 280 | } 281 | } 282 | 283 | fn broadcast_packet(&self, packet: &proto::Packet) { 284 | let bytes = packet.encode_to_vec().into(); 285 | self.broadcast_bytes(bytes); 286 | } 287 | 288 | fn broadcast_bytes(&self, bytes: Bytes) { 289 | self.client_map 290 | .values() 291 | .filter(|client| client.player.is_some()) 292 | .for_each(|client| { 293 | let bytes = bytes.clone(); 294 | let _ = client 295 | .client_agent_command_sender 296 | .send(command::Command::SendBytes { bytes }); 297 | }); 298 | } 299 | 300 | fn spawn_spore(&mut self) { 301 | let spore = spore::Spore::random(); 302 | 303 | let packet = proto_util::update_spore_pack(&spore); 304 | 305 | self.spore_map.insert(spore.id.clone(), spore); 306 | 307 | self.broadcast_packet(&packet); 308 | } 309 | 310 | fn tick_player(&mut self, delta: Duration) { 311 | let mut spore_packet_list = vec![]; 312 | 313 | let player_list = self 314 | .client_map 315 | .values_mut() 316 | .flat_map(|client| client.player.as_mut()); 317 | 318 | for player in player_list { 319 | player.tick(delta); 320 | 321 | let drop_mass_probability = player.radius / (MAX_SPORE_COUNT as f64 * 4.0); 322 | if rand::random::() < drop_mass_probability { 323 | let drop_mass = util::radius_to_mass((5.0 + player.radius / 50.0).min(15.0)); 324 | if let Some(mass) = player.try_drop_mass(drop_mass) { 325 | let mut spore = spore::Spore::random(); 326 | spore.x = player.x; 327 | spore.y = player.y; 328 | spore.radius = util::mass_to_radius(mass); 329 | 330 | let packet = proto_util::update_spore_pack(&spore); 331 | spore_packet_list.push(packet); 332 | 333 | self.spore_map.insert(spore.id.clone(), spore); 334 | } 335 | } 336 | } 337 | 338 | self.sync_player(); 339 | 340 | for spore_packet in spore_packet_list { 341 | self.broadcast_packet(&spore_packet); 342 | } 343 | } 344 | 345 | fn sync_player(&self) { 346 | let player_list = self 347 | .client_map 348 | .values() 349 | .filter_map(|client| client.player.as_ref()) 350 | .collect::>(); 351 | 352 | let packet = proto_util::update_player_batch_packet(&player_list); 353 | self.broadcast_packet(&packet); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /server/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod client_agent; 2 | pub mod command; 3 | pub mod db; 4 | pub mod hub; 5 | pub mod player; 6 | pub mod proto; 7 | pub mod proto_util; 8 | pub mod spore; 9 | pub mod util; 10 | 11 | use anyhow::Result; 12 | use std::{net::SocketAddr, sync::Arc}; 13 | use tokio::{net::TcpStream, sync::mpsc::UnboundedSender}; 14 | 15 | pub async fn handle_tcp_stream( 16 | tcp_stream: TcpStream, 17 | socket_addr: SocketAddr, 18 | db: db::Db, 19 | hub_command_sender: UnboundedSender, 20 | ) -> Result<()> { 21 | let ws_stream = tokio_tungstenite::accept_async(tcp_stream).await?; 22 | 23 | let client_agent = 24 | client_agent::ClientAgent::new(ws_stream, socket_addr, db, hub_command_sender).await?; 25 | 26 | client_agent.run().await; 27 | 28 | Ok(()) 29 | } 30 | -------------------------------------------------------------------------------- /server/src/main.rs: -------------------------------------------------------------------------------- 1 | const DEFAULT_LOG_DIRECTORY: &str = "./"; 2 | const DEFAULT_LOG_FILE_NAME_PREFIX: &str = "agarust_server.log"; 3 | const DEFAULT_BIND_ADDR: &str = "127.0.0.1:8080"; 4 | const DEFAULT_DATABASE_URL: &str = "sqlite:agarust_db.sqlite"; 5 | 6 | #[tokio::main] 7 | async fn main() -> anyhow::Result<()> { 8 | dotenv::dotenv().ok(); 9 | 10 | let _tracing_guard = init_tracing(); 11 | 12 | let bind_addr = std::env::var("BIND_ADDR").unwrap_or(DEFAULT_BIND_ADDR.to_string()); 13 | tracing::info!("bind_addr: {:?}", bind_addr); 14 | 15 | let tcp_listener = tokio::net::TcpListener::bind(&bind_addr).await?; 16 | 17 | let database_url = std::env::var("DATABASE_URL").unwrap_or(DEFAULT_DATABASE_URL.to_string()); 18 | tracing::info!("database_url: {:?}", database_url); 19 | 20 | let db = agarust_server::db::Db::new(&database_url).await?; 21 | 22 | let hub = agarust_server::hub::Hub::new(db.clone()); 23 | let hub_command_sender = hub.command_sender.clone(); 24 | 25 | let hub_run_future = hub.run(); 26 | tokio::spawn(hub_run_future); 27 | 28 | while let Ok((tcp_stream, socket_addr)) = tcp_listener.accept().await { 29 | tracing::info!("tcp_listener accept: {:?}", socket_addr); 30 | let tcp_stream_future = agarust_server::handle_tcp_stream( 31 | tcp_stream, 32 | socket_addr, 33 | db.clone(), 34 | hub_command_sender.clone(), 35 | ); 36 | tokio::spawn(async move { 37 | let tcp_stream_result = tcp_stream_future.await; 38 | tracing::info!( 39 | "{:?} tcp_stream_result: {:?}", 40 | socket_addr, 41 | tcp_stream_result 42 | ) 43 | }); 44 | } 45 | 46 | Ok(()) 47 | } 48 | 49 | fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard { 50 | let log_directory = std::env::var("LOG_DIRECTORY").unwrap_or(DEFAULT_LOG_DIRECTORY.to_string()); 51 | println!("log_directory: {:?}", log_directory); 52 | 53 | let log_file_name_prefix = 54 | std::env::var("LOG_FILE_NAME_PREFIX").unwrap_or(DEFAULT_LOG_FILE_NAME_PREFIX.to_string()); 55 | println!("log_file_name_prefix: {:?}", log_file_name_prefix); 56 | 57 | let file_appender = tracing_appender::rolling::daily(log_directory, log_file_name_prefix); 58 | let (non_blocking_writer, worker_guard) = tracing_appender::non_blocking(file_appender); 59 | tracing_subscriber::fmt() 60 | .compact() 61 | .with_file(true) 62 | .with_line_number(true) 63 | .with_thread_ids(true) 64 | .with_target(false) 65 | .with_writer(non_blocking_writer) 66 | .with_ansi(false) 67 | .init(); 68 | 69 | worker_guard 70 | } 71 | -------------------------------------------------------------------------------- /server/src/player.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use std::time::Duration; 3 | use tokio::time::Instant; 4 | 5 | const PLAYER_BOUND: f64 = 3000.0; 6 | const INIT_RADIUS: f64 = 20.0; 7 | const INIT_DIRECTION_ANGLE: f64 = 0.0; 8 | const INIT_SPEED: f64 = 150.0; 9 | const RUSH_SPEED: f64 = 300.0; 10 | const RUSH_DURATION: Duration = Duration::from_secs(2); 11 | 12 | fn random_xy() -> f64 { 13 | (rand::random::() * 2.0 - 1.0) * PLAYER_BOUND 14 | } 15 | 16 | #[derive(Debug, Clone)] 17 | pub struct Player { 18 | pub db_id: i64, 19 | pub connection_id: Arc, 20 | pub nickname: Arc, 21 | pub x: f64, 22 | pub y: f64, 23 | pub radius: f64, 24 | pub direction_angle: f64, 25 | pub speed: f64, 26 | pub color: i64, 27 | pub rush_instant: Option, 28 | } 29 | 30 | impl Player { 31 | pub fn random(db_id: i64, connection_id: Arc, nickname: Arc, color: i64) -> Self { 32 | Self { 33 | db_id, 34 | connection_id, 35 | nickname, 36 | x: random_xy(), 37 | y: random_xy(), 38 | radius: INIT_RADIUS, 39 | direction_angle: INIT_DIRECTION_ANGLE, 40 | speed: INIT_SPEED, 41 | color, 42 | rush_instant: None, 43 | } 44 | } 45 | 46 | pub fn tick(&mut self, delta: Duration) { 47 | let delta_secs = delta.as_secs_f64(); 48 | 49 | let new_x = self.x + self.speed * self.direction_angle.cos() * delta_secs; 50 | let new_y = self.y + self.speed * self.direction_angle.sin() * delta_secs; 51 | 52 | self.x = new_x; 53 | self.y = new_y; 54 | 55 | if let Some(rush_instant) = self.rush_instant { 56 | if rush_instant.elapsed() > RUSH_DURATION { 57 | self.speed = INIT_SPEED; 58 | self.rush_instant = None; 59 | } 60 | } 61 | } 62 | 63 | pub fn rush(&mut self) { 64 | self.speed = RUSH_SPEED; 65 | self.rush_instant = Some(Instant::now()); 66 | } 67 | 68 | pub fn respawn(&mut self) { 69 | self.x = random_xy(); 70 | self.y = random_xy(); 71 | self.radius = INIT_RADIUS; 72 | self.speed = INIT_SPEED; 73 | } 74 | 75 | pub fn increase_mass(&mut self, mass: f64) { 76 | let mut player_mass = util::radius_to_mass(self.radius); 77 | player_mass += mass; 78 | 79 | self.radius = util::mass_to_radius(player_mass); 80 | } 81 | 82 | pub fn try_decrease_mass(&mut self, mass: f64) -> bool { 83 | if self.radius <= 10.0 { 84 | return false; 85 | } 86 | 87 | let mut player_mass = util::radius_to_mass(self.radius); 88 | player_mass -= mass; 89 | if player_mass <= 0.0 { 90 | return false; 91 | } 92 | 93 | self.radius = util::mass_to_radius(player_mass); 94 | 95 | true 96 | } 97 | 98 | pub fn try_drop_mass(&mut self, mass: f64) -> Option { 99 | if self.try_decrease_mass(mass) { 100 | return Some(mass); 101 | } 102 | None 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /server/src/proto.rs: -------------------------------------------------------------------------------- 1 | // This file is @generated by prost-build. 2 | #[derive(Clone, PartialEq, ::prost::Message)] 3 | pub struct Packet { 4 | #[prost( 5 | oneof = "packet::Data", 6 | tags = "1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21" 7 | )] 8 | pub data: ::core::option::Option, 9 | } 10 | /// Nested message and enum types in `Packet`. 11 | pub mod packet { 12 | #[derive(Clone, PartialEq, ::prost::Oneof)] 13 | pub enum Data { 14 | #[prost(message, tag = "1")] 15 | Ping(super::Ping), 16 | #[prost(message, tag = "2")] 17 | Hello(super::Hello), 18 | #[prost(message, tag = "3")] 19 | Login(super::Login), 20 | #[prost(message, tag = "4")] 21 | LoginOk(super::LoginOk), 22 | #[prost(message, tag = "5")] 23 | LoginErr(super::LoginErr), 24 | #[prost(message, tag = "6")] 25 | Register(super::Register), 26 | #[prost(message, tag = "7")] 27 | RegisterOk(super::RegisterOk), 28 | #[prost(message, tag = "8")] 29 | RegisterErr(super::RegisterErr), 30 | #[prost(message, tag = "9")] 31 | Join(super::Join), 32 | #[prost(message, tag = "10")] 33 | Disconnect(super::Disconnect), 34 | #[prost(message, tag = "11")] 35 | Chat(super::Chat), 36 | #[prost(message, tag = "12")] 37 | UpdatePlayer(super::UpdatePlayer), 38 | #[prost(message, tag = "13")] 39 | UpdatePlayerBatch(super::UpdatePlayerBatch), 40 | #[prost(message, tag = "14")] 41 | UpdatePlayerDirectionAngle(super::UpdatePlayerDirectionAngle), 42 | #[prost(message, tag = "15")] 43 | UpdateSpore(super::UpdateSpore), 44 | #[prost(message, tag = "16")] 45 | UpdateSporeBatch(super::UpdateSporeBatch), 46 | #[prost(message, tag = "17")] 47 | ConsumeSpore(super::ConsumeSpore), 48 | #[prost(message, tag = "18")] 49 | ConsumePlayer(super::ConsumePlayer), 50 | #[prost(message, tag = "19")] 51 | Rush(super::Rush), 52 | #[prost(message, tag = "20")] 53 | LeaderboardRequest(super::LeaderboardRequest), 54 | #[prost(message, tag = "21")] 55 | LeaderboardResponse(super::LeaderboardResponse), 56 | } 57 | } 58 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 59 | pub struct Ping { 60 | #[prost(int64, tag = "1")] 61 | pub client_timestamp: i64, 62 | } 63 | #[derive(Clone, PartialEq, ::prost::Message)] 64 | pub struct Hello { 65 | #[prost(string, tag = "1")] 66 | pub connection_id: ::prost::alloc::string::String, 67 | } 68 | #[derive(Clone, PartialEq, ::prost::Message)] 69 | pub struct Login { 70 | #[prost(string, tag = "1")] 71 | pub username: ::prost::alloc::string::String, 72 | #[prost(string, tag = "2")] 73 | pub password: ::prost::alloc::string::String, 74 | } 75 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 76 | pub struct LoginOk {} 77 | #[derive(Clone, PartialEq, ::prost::Message)] 78 | pub struct LoginErr { 79 | #[prost(string, tag = "1")] 80 | pub reason: ::prost::alloc::string::String, 81 | } 82 | #[derive(Clone, PartialEq, ::prost::Message)] 83 | pub struct Register { 84 | #[prost(string, tag = "1")] 85 | pub username: ::prost::alloc::string::String, 86 | #[prost(string, tag = "2")] 87 | pub password: ::prost::alloc::string::String, 88 | #[prost(int64, tag = "3")] 89 | pub color: i64, 90 | } 91 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 92 | pub struct RegisterOk {} 93 | #[derive(Clone, PartialEq, ::prost::Message)] 94 | pub struct RegisterErr { 95 | #[prost(string, tag = "1")] 96 | pub reason: ::prost::alloc::string::String, 97 | } 98 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 99 | pub struct Join {} 100 | #[derive(Clone, PartialEq, ::prost::Message)] 101 | pub struct Disconnect { 102 | #[prost(string, tag = "1")] 103 | pub connection_id: ::prost::alloc::string::String, 104 | #[prost(string, tag = "2")] 105 | pub reason: ::prost::alloc::string::String, 106 | } 107 | #[derive(Clone, PartialEq, ::prost::Message)] 108 | pub struct Chat { 109 | #[prost(string, tag = "1")] 110 | pub connection_id: ::prost::alloc::string::String, 111 | #[prost(string, tag = "2")] 112 | pub msg: ::prost::alloc::string::String, 113 | } 114 | #[derive(Clone, PartialEq, ::prost::Message)] 115 | pub struct UpdatePlayer { 116 | #[prost(string, tag = "1")] 117 | pub connection_id: ::prost::alloc::string::String, 118 | #[prost(string, tag = "2")] 119 | pub nickname: ::prost::alloc::string::String, 120 | #[prost(double, tag = "3")] 121 | pub x: f64, 122 | #[prost(double, tag = "4")] 123 | pub y: f64, 124 | #[prost(double, tag = "5")] 125 | pub radius: f64, 126 | #[prost(double, tag = "6")] 127 | pub direction_angle: f64, 128 | #[prost(double, tag = "7")] 129 | pub speed: f64, 130 | #[prost(int64, tag = "8")] 131 | pub color: i64, 132 | #[prost(bool, tag = "9")] 133 | pub is_rushing: bool, 134 | } 135 | #[derive(Clone, PartialEq, ::prost::Message)] 136 | pub struct UpdatePlayerBatch { 137 | #[prost(message, repeated, tag = "1")] 138 | pub update_player_batch: ::prost::alloc::vec::Vec, 139 | } 140 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 141 | pub struct UpdatePlayerDirectionAngle { 142 | #[prost(double, tag = "1")] 143 | pub direction_angle: f64, 144 | } 145 | #[derive(Clone, PartialEq, ::prost::Message)] 146 | pub struct UpdateSpore { 147 | #[prost(string, tag = "1")] 148 | pub id: ::prost::alloc::string::String, 149 | #[prost(double, tag = "2")] 150 | pub x: f64, 151 | #[prost(double, tag = "3")] 152 | pub y: f64, 153 | #[prost(double, tag = "4")] 154 | pub radius: f64, 155 | } 156 | #[derive(Clone, PartialEq, ::prost::Message)] 157 | pub struct UpdateSporeBatch { 158 | #[prost(message, repeated, tag = "1")] 159 | pub update_spore_batch: ::prost::alloc::vec::Vec, 160 | } 161 | #[derive(Clone, PartialEq, ::prost::Message)] 162 | pub struct ConsumeSpore { 163 | #[prost(string, tag = "1")] 164 | pub connection_id: ::prost::alloc::string::String, 165 | #[prost(string, tag = "2")] 166 | pub spore_id: ::prost::alloc::string::String, 167 | } 168 | #[derive(Clone, PartialEq, ::prost::Message)] 169 | pub struct ConsumePlayer { 170 | #[prost(string, tag = "1")] 171 | pub connection_id: ::prost::alloc::string::String, 172 | #[prost(string, tag = "2")] 173 | pub victim_connection_id: ::prost::alloc::string::String, 174 | } 175 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 176 | pub struct Rush {} 177 | #[derive(Clone, Copy, PartialEq, ::prost::Message)] 178 | pub struct LeaderboardRequest {} 179 | #[derive(Clone, PartialEq, ::prost::Message)] 180 | pub struct LeaderboardEntry { 181 | #[prost(uint64, tag = "1")] 182 | pub rank: u64, 183 | #[prost(string, tag = "2")] 184 | pub player_nickname: ::prost::alloc::string::String, 185 | #[prost(uint64, tag = "3")] 186 | pub score: u64, 187 | } 188 | #[derive(Clone, PartialEq, ::prost::Message)] 189 | pub struct LeaderboardResponse { 190 | #[prost(message, repeated, tag = "1")] 191 | pub leaderboard_entry_list: ::prost::alloc::vec::Vec, 192 | } 193 | -------------------------------------------------------------------------------- /server/src/proto_util.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub fn hello_packet(connection_id: Arc) -> proto::Packet { 4 | proto::Packet { 5 | data: Some(proto::packet::Data::Hello(proto::Hello { 6 | connection_id: connection_id.to_string(), 7 | })), 8 | } 9 | } 10 | 11 | pub fn login_ok_packet() -> proto::Packet { 12 | proto::Packet { 13 | data: Some(proto::packet::Data::LoginOk(proto::LoginOk {})), 14 | } 15 | } 16 | 17 | pub fn login_err_packet(reason: Arc) -> proto::Packet { 18 | proto::Packet { 19 | data: Some(proto::packet::Data::LoginErr(proto::LoginErr { 20 | reason: reason.to_string(), 21 | })), 22 | } 23 | } 24 | 25 | pub fn register_ok_packet() -> proto::Packet { 26 | proto::Packet { 27 | data: Some(proto::packet::Data::RegisterOk(proto::RegisterOk {})), 28 | } 29 | } 30 | 31 | pub fn register_err_packet(reason: Arc) -> proto::Packet { 32 | proto::Packet { 33 | data: Some(proto::packet::Data::RegisterErr(proto::RegisterErr { 34 | reason: reason.to_string(), 35 | })), 36 | } 37 | } 38 | 39 | pub fn chat_packet(connection_id: Arc, msg: Arc) -> proto::Packet { 40 | proto::Packet { 41 | data: Some(proto::packet::Data::Chat(proto::Chat { 42 | connection_id: connection_id.to_string(), 43 | msg: msg.to_string(), 44 | })), 45 | } 46 | } 47 | 48 | pub fn update_player(player: &player::Player) -> proto::UpdatePlayer { 49 | proto::UpdatePlayer { 50 | connection_id: player.connection_id.to_string(), 51 | nickname: player.nickname.to_string(), 52 | x: player.x, 53 | y: player.y, 54 | radius: player.radius, 55 | direction_angle: player.direction_angle, 56 | speed: player.speed, 57 | color: player.color, 58 | is_rushing: player.rush_instant.is_some(), 59 | } 60 | } 61 | 62 | pub fn update_player_packet(player: &player::Player) -> proto::Packet { 63 | proto::Packet { 64 | data: Some(proto::packet::Data::UpdatePlayer(update_player(player))), 65 | } 66 | } 67 | 68 | pub fn update_player_batch_packet(player_list: &[&player::Player]) -> proto::Packet { 69 | let update_player_batch = player_list 70 | .iter() 71 | .map(|player| update_player(player)) 72 | .collect::>(); 73 | proto::Packet { 74 | data: Some(proto::packet::Data::UpdatePlayerBatch( 75 | proto::UpdatePlayerBatch { 76 | update_player_batch, 77 | }, 78 | )), 79 | } 80 | } 81 | 82 | pub fn update_spore(spore: &spore::Spore) -> proto::UpdateSpore { 83 | proto::UpdateSpore { 84 | id: spore.id.to_string(), 85 | x: spore.x, 86 | y: spore.y, 87 | radius: spore.radius, 88 | } 89 | } 90 | 91 | pub fn update_spore_pack(spore: &spore::Spore) -> proto::Packet { 92 | proto::Packet { 93 | data: Some(proto::packet::Data::UpdateSpore(update_spore(spore))), 94 | } 95 | } 96 | 97 | pub fn update_spore_batch_packet(spore_list: &[spore::Spore]) -> proto::Packet { 98 | let update_spore_batch = spore_list 99 | .iter() 100 | .map(update_spore) 101 | .collect::>(); 102 | proto::Packet { 103 | data: Some(proto::packet::Data::UpdateSporeBatch( 104 | proto::UpdateSporeBatch { update_spore_batch }, 105 | )), 106 | } 107 | } 108 | 109 | pub fn consume_spore_packet(connection_id: Arc, spore_id: Arc) -> proto::Packet { 110 | proto::Packet { 111 | data: Some(proto::packet::Data::ConsumeSpore(proto::ConsumeSpore { 112 | connection_id: connection_id.to_string(), 113 | spore_id: spore_id.to_string(), 114 | })), 115 | } 116 | } 117 | 118 | pub fn disconnect_packet(connection_id: Arc, reason: Arc) -> proto::Packet { 119 | proto::Packet { 120 | data: Some(proto::packet::Data::Disconnect(proto::Disconnect { 121 | connection_id: connection_id.to_string(), 122 | reason: reason.to_string(), 123 | })), 124 | } 125 | } 126 | 127 | pub fn leaderboard_response(leaderboard_entry_list: &[command::LeaderboardEntry]) -> proto::Packet { 128 | let leaderboard_entry_list = leaderboard_entry_list 129 | .iter() 130 | .map(|entry| proto::LeaderboardEntry { 131 | rank: entry.rank, 132 | player_nickname: entry.player_nickname.to_string(), 133 | score: entry.score, 134 | }) 135 | .collect::>(); 136 | proto::Packet { 137 | data: Some(proto::packet::Data::LeaderboardResponse( 138 | proto::LeaderboardResponse { 139 | leaderboard_entry_list, 140 | }, 141 | )), 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /server/src/spore.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use nanoid::nanoid; 3 | 4 | const SPORE_BOUND: f64 = 3000.0; 5 | 6 | fn random_xy() -> f64 { 7 | (rand::random::() * 2.0 - 1.0) * SPORE_BOUND 8 | } 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct Spore { 12 | pub id: Arc, 13 | pub x: f64, 14 | pub y: f64, 15 | pub radius: f64, 16 | } 17 | 18 | impl Spore { 19 | pub fn random() -> Self { 20 | let radius = (rand::random::() * 3.0 + 10.0).max(5.0); 21 | Self { 22 | id: nanoid!().into(), 23 | x: random_xy(), 24 | y: random_xy(), 25 | radius, 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/src/util.rs: -------------------------------------------------------------------------------- 1 | use std::f64::consts::PI; 2 | 3 | pub fn radius_to_mass(radius: f64) -> f64 { 4 | PI * radius * radius 5 | } 6 | 7 | pub fn mass_to_radius(mass: f64) -> f64 { 8 | (mass / PI).sqrt() 9 | } 10 | 11 | pub fn check_distance_is_close( 12 | x1: f64, 13 | y1: f64, 14 | radius1: f64, 15 | x2: f64, 16 | y2: f64, 17 | radius2: f64, 18 | ) -> bool { 19 | let distance_sq = (x1 - x2).powi(2) + (y1 - y2).powi(2); 20 | 21 | let threshold = radius1 + radius2 + 10.0; 22 | let threshold_sq = threshold.powi(2); 23 | 24 | distance_sq < threshold_sq 25 | } 26 | --------------------------------------------------------------------------------