├── .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 |
--------------------------------------------------------------------------------