1 use mio; |
|
2 |
|
3 use super::common::rnd_reply; |
|
4 use crate::utils::to_engine_msg; |
|
5 use crate::{ |
|
6 protocol::messages::{ |
|
7 add_flags, remove_flags, server_chat, HWProtocolMessage, HWServerMessage::*, |
|
8 ProtocolFlags as Flags, |
|
9 }, |
|
10 server::{ |
|
11 core::HWServer, |
|
12 coretypes, |
|
13 coretypes::{ClientId, GameCfg, RoomId, VoteType, Voting, MAX_HEDGEHOGS_PER_TEAM}, |
|
14 room::{HWRoom, RoomFlags, MAX_TEAMS_IN_ROOM}, |
|
15 }, |
|
16 utils::is_name_illegal, |
|
17 }; |
|
18 use base64::{decode, encode}; |
|
19 use log::*; |
|
20 use std::{cmp::min, iter::once, mem::swap}; |
|
21 |
|
22 #[derive(Clone)] |
|
23 struct ByMsg<'a> { |
|
24 messages: &'a [u8], |
|
25 } |
|
26 |
|
27 impl<'a> Iterator for ByMsg<'a> { |
|
28 type Item = &'a [u8]; |
|
29 |
|
30 fn next(&mut self) -> Option<<Self as Iterator>::Item> { |
|
31 if let Some(size) = self.messages.get(0) { |
|
32 let (msg, next) = self.messages.split_at(*size as usize + 1); |
|
33 self.messages = next; |
|
34 Some(msg) |
|
35 } else { |
|
36 None |
|
37 } |
|
38 } |
|
39 } |
|
40 |
|
41 fn by_msg(source: &[u8]) -> ByMsg { |
|
42 ByMsg { messages: source } |
|
43 } |
|
44 |
|
45 const VALID_MESSAGES: &[u8] = |
|
46 b"M#+LlRrUuDdZzAaSjJ,NpPwtgfhbc12345\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8A"; |
|
47 const NON_TIMED_MESSAGES: &[u8] = b"M#hb"; |
|
48 |
|
49 #[cfg(canhazslicepatterns)] |
|
50 fn is_msg_valid(msg: &[u8], team_indices: &[u8]) -> bool { |
|
51 match msg { |
|
52 [size, typ, body..] => { |
|
53 VALID_MESSAGES.contains(typ) |
|
54 && match body { |
|
55 [1...MAX_HEDGEHOGS_PER_TEAM, team, ..] if *typ == b'h' => { |
|
56 team_indices.contains(team) |
|
57 } |
|
58 _ => *typ != b'h', |
|
59 } |
|
60 } |
|
61 _ => false, |
|
62 } |
|
63 } |
|
64 |
|
65 fn is_msg_valid(msg: &[u8], _team_indices: &[u8]) -> bool { |
|
66 if let Some(typ) = msg.get(1) { |
|
67 VALID_MESSAGES.contains(typ) |
|
68 } else { |
|
69 false |
|
70 } |
|
71 } |
|
72 |
|
73 fn is_msg_empty(msg: &[u8]) -> bool { |
|
74 msg.get(1).filter(|t| **t == b'+').is_some() |
|
75 } |
|
76 |
|
77 fn is_msg_timed(msg: &[u8]) -> bool { |
|
78 msg.get(1) |
|
79 .filter(|t| !NON_TIMED_MESSAGES.contains(t)) |
|
80 .is_some() |
|
81 } |
|
82 |
|
83 fn voting_description(kind: &VoteType) -> String { |
|
84 format!( |
|
85 "New voting started: {}", |
|
86 match kind { |
|
87 VoteType::Kick(nick) => format!("kick {}", nick), |
|
88 VoteType::Map(name) => format!("map {}", name.as_ref().unwrap()), |
|
89 VoteType::Pause => "pause".to_string(), |
|
90 VoteType::NewSeed => "new seed".to_string(), |
|
91 VoteType::HedgehogsPerTeam(number) => format!("hedgehogs per team: {}", number), |
|
92 } |
|
93 ) |
|
94 } |
|
95 |
|
96 fn room_message_flag(msg: &HWProtocolMessage) -> RoomFlags { |
|
97 use crate::protocol::messages::HWProtocolMessage::*; |
|
98 match msg { |
|
99 ToggleRestrictJoin => RoomFlags::RESTRICTED_JOIN, |
|
100 ToggleRestrictTeams => RoomFlags::RESTRICTED_TEAM_ADD, |
|
101 ToggleRegisteredOnly => RoomFlags::RESTRICTED_UNREGISTERED_PLAYERS, |
|
102 _ => RoomFlags::empty(), |
|
103 } |
|
104 } |
|
105 |
|
106 pub fn handle( |
|
107 server: &mut HWServer, |
|
108 client_id: ClientId, |
|
109 response: &mut super::Response, |
|
110 room_id: RoomId, |
|
111 message: HWProtocolMessage, |
|
112 ) { |
|
113 let client = &mut server.clients[client_id]; |
|
114 let room = &mut server.rooms[room_id]; |
|
115 |
|
116 use crate::protocol::messages::HWProtocolMessage::*; |
|
117 match message { |
|
118 Part(msg) => { |
|
119 let msg = match msg { |
|
120 Some(s) => format!("part: {}", s), |
|
121 None => "part".to_string(), |
|
122 }; |
|
123 super::common::exit_room(server, client_id, response, &msg); |
|
124 } |
|
125 Chat(msg) => { |
|
126 response.add( |
|
127 ChatMsg { |
|
128 nick: client.nick.clone(), |
|
129 msg, |
|
130 } |
|
131 .send_all() |
|
132 .in_room(room_id), |
|
133 ); |
|
134 } |
|
135 TeamChat(msg) => { |
|
136 let room = &server.rooms[room_id]; |
|
137 if let Some(ref info) = room.game_info { |
|
138 if let Some(clan_color) = room.find_team_color(client_id) { |
|
139 let client = &server.clients[client_id]; |
|
140 let engine_msg = |
|
141 to_engine_msg(format!("b{}]{}\x20\x20", client.nick, msg).bytes()); |
|
142 let team = room.clan_team_owners(clan_color).collect(); |
|
143 response.add(ForwardEngineMessage(vec![engine_msg]).send_many(team)) |
|
144 } |
|
145 } |
|
146 } |
|
147 Fix => { |
|
148 if client.is_admin() { |
|
149 room.set_is_fixed(true); |
|
150 room.set_join_restriction(false); |
|
151 room.set_team_add_restriction(false); |
|
152 room.set_unregistered_players_restriction(true); |
|
153 } |
|
154 } |
|
155 Unfix => { |
|
156 if client.is_admin() { |
|
157 room.set_is_fixed(false); |
|
158 } |
|
159 } |
|
160 Greeting(text) => { |
|
161 if client.is_admin() || client.is_master() && !room.is_fixed() { |
|
162 room.greeting = text; |
|
163 } |
|
164 } |
|
165 MaxTeams(count) => { |
|
166 if !client.is_master() { |
|
167 response.add(Warning("You're not the room master!".to_string()).send_self()); |
|
168 } else if !(2..=MAX_TEAMS_IN_ROOM).contains(&count) { |
|
169 response |
|
170 .add(Warning("/maxteams: specify number from 2 to 8".to_string()).send_self()); |
|
171 } else { |
|
172 server.rooms[room_id].max_teams = count; |
|
173 } |
|
174 } |
|
175 RoomName(new_name) => { |
|
176 if is_name_illegal(&new_name) { |
|
177 response.add(Warning("Illegal room name! A room name must be between 1-40 characters long, must not have a trailing or leading space and must not have any of these characters: $()*+?[]^{|}".to_string()).send_self()); |
|
178 } else if server.has_room(&new_name) { |
|
179 response.add( |
|
180 Warning("A room with the same name already exists.".to_string()).send_self(), |
|
181 ); |
|
182 } else { |
|
183 let room = &mut server.rooms[room_id]; |
|
184 if room.is_fixed() || room.master_id != Some(client_id) { |
|
185 response.add(Warning("Access denied.".to_string()).send_self()); |
|
186 } else { |
|
187 let mut old_name = new_name.clone(); |
|
188 let client = &server.clients[client_id]; |
|
189 swap(&mut room.name, &mut old_name); |
|
190 super::common::get_room_update(Some(old_name), room, Some(&client), response); |
|
191 } |
|
192 } |
|
193 } |
|
194 ToggleReady => { |
|
195 let flags = if client.is_ready() { |
|
196 room.ready_players_number -= 1; |
|
197 remove_flags(&[Flags::Ready]) |
|
198 } else { |
|
199 room.ready_players_number += 1; |
|
200 add_flags(&[Flags::Ready]) |
|
201 }; |
|
202 |
|
203 let msg = if client.protocol_number < 38 { |
|
204 LegacyReady(client.is_ready(), vec![client.nick.clone()]) |
|
205 } else { |
|
206 ClientFlags(flags, vec![client.nick.clone()]) |
|
207 }; |
|
208 response.add(msg.send_all().in_room(room.id)); |
|
209 client.set_is_ready(!client.is_ready()); |
|
210 |
|
211 if room.is_fixed() && room.ready_players_number == room.players_number { |
|
212 super::common::start_game(server, room_id, response); |
|
213 } |
|
214 } |
|
215 AddTeam(mut info) => { |
|
216 if room.teams.len() >= room.max_teams as usize { |
|
217 response.add(Warning("Too many teams!".to_string()).send_self()); |
|
218 } else if room.addable_hedgehogs() == 0 { |
|
219 response.add(Warning("Too many hedgehogs!".to_string()).send_self()); |
|
220 } else if room.find_team(|t| t.name == info.name) != None { |
|
221 response.add( |
|
222 Warning("There's already a team with same name in the list.".to_string()) |
|
223 .send_self(), |
|
224 ); |
|
225 } else if room.game_info.is_some() { |
|
226 response.add( |
|
227 Warning("Joining not possible: Round is in progress.".to_string()).send_self(), |
|
228 ); |
|
229 } else if room.is_team_add_restricted() { |
|
230 response.add( |
|
231 Warning("This room currently does not allow adding new teams.".to_string()) |
|
232 .send_self(), |
|
233 ); |
|
234 } else { |
|
235 info.owner = client.nick.clone(); |
|
236 let team = room.add_team(client.id, *info, client.protocol_number < 42); |
|
237 client.teams_in_game += 1; |
|
238 client.clan = Some(team.color); |
|
239 response.add(TeamAccepted(team.name.clone()).send_self()); |
|
240 response.add( |
|
241 TeamAdd(team.to_protocol()) |
|
242 .send_all() |
|
243 .in_room(room_id) |
|
244 .but_self(), |
|
245 ); |
|
246 response.add( |
|
247 TeamColor(team.name.clone(), team.color) |
|
248 .send_all() |
|
249 .in_room(room_id), |
|
250 ); |
|
251 response.add( |
|
252 HedgehogsNumber(team.name.clone(), team.hedgehogs_number) |
|
253 .send_all() |
|
254 .in_room(room_id), |
|
255 ); |
|
256 |
|
257 let room_master = if let Some(id) = room.master_id { |
|
258 Some(&server.clients[id]) |
|
259 } else { |
|
260 None |
|
261 }; |
|
262 super::common::get_room_update(None, room, room_master, response); |
|
263 } |
|
264 } |
|
265 RemoveTeam(name) => match room.find_team_owner(&name) { |
|
266 None => response.add( |
|
267 Warning("Error: The team you tried to remove does not exist.".to_string()) |
|
268 .send_self(), |
|
269 ), |
|
270 Some((id, _)) if id != client_id => response |
|
271 .add(Warning("You can't remove a team you don't own.".to_string()).send_self()), |
|
272 Some((_, name)) => { |
|
273 client.teams_in_game -= 1; |
|
274 client.clan = room.find_team_color(client.id); |
|
275 super::common::remove_teams( |
|
276 room, |
|
277 vec![name.to_string()], |
|
278 client.is_in_game(), |
|
279 response, |
|
280 ); |
|
281 |
|
282 match room.game_info { |
|
283 Some(ref info) if info.teams_in_game == 0 => { |
|
284 super::common::end_game(server, room_id, response) |
|
285 } |
|
286 _ => (), |
|
287 } |
|
288 } |
|
289 }, |
|
290 SetHedgehogsNumber(team_name, number) => { |
|
291 let addable_hedgehogs = room.addable_hedgehogs(); |
|
292 if let Some((_, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) { |
|
293 let max_hedgehogs = min( |
|
294 MAX_HEDGEHOGS_PER_TEAM, |
|
295 addable_hedgehogs + team.hedgehogs_number, |
|
296 ); |
|
297 if !client.is_master() { |
|
298 response.add(Error("You're not the room master!".to_string()).send_self()); |
|
299 } else if !(1..=max_hedgehogs).contains(&number) { |
|
300 response |
|
301 .add(HedgehogsNumber(team.name.clone(), team.hedgehogs_number).send_self()); |
|
302 } else { |
|
303 team.hedgehogs_number = number; |
|
304 response.add( |
|
305 HedgehogsNumber(team.name.clone(), number) |
|
306 .send_all() |
|
307 .in_room(room_id) |
|
308 .but_self(), |
|
309 ); |
|
310 } |
|
311 } else { |
|
312 response.add(Warning("No such team.".to_string()).send_self()); |
|
313 } |
|
314 } |
|
315 SetTeamColor(team_name, color) => { |
|
316 if let Some((owner, team)) = room.find_team_and_owner_mut(|t| t.name == team_name) { |
|
317 if !client.is_master() { |
|
318 response.add(Error("You're not the room master!".to_string()).send_self()); |
|
319 } else { |
|
320 team.color = color; |
|
321 response.add( |
|
322 TeamColor(team.name.clone(), color) |
|
323 .send_all() |
|
324 .in_room(room_id) |
|
325 .but_self(), |
|
326 ); |
|
327 server.clients[owner].clan = Some(color); |
|
328 } |
|
329 } else { |
|
330 response.add(Warning("No such team.".to_string()).send_self()); |
|
331 } |
|
332 } |
|
333 Cfg(cfg) => { |
|
334 if room.is_fixed() { |
|
335 response.add(Warning("Access denied.".to_string()).send_self()); |
|
336 } else if !client.is_master() { |
|
337 response.add(Error("You're not the room master!".to_string()).send_self()); |
|
338 } else { |
|
339 let cfg = match cfg { |
|
340 GameCfg::Scheme(name, mut values) => { |
|
341 if client.protocol_number == 49 && values.len() >= 2 { |
|
342 let mut s = "X".repeat(50); |
|
343 s.push_str(&values.pop().unwrap()); |
|
344 values.push(s); |
|
345 } |
|
346 GameCfg::Scheme(name, values) |
|
347 } |
|
348 cfg => cfg, |
|
349 }; |
|
350 |
|
351 response.add(cfg.to_server_msg().send_all().in_room(room.id).but_self()); |
|
352 room.set_config(cfg); |
|
353 } |
|
354 } |
|
355 Save(name, location) => { |
|
356 response.add( |
|
357 server_chat(format!("Room config saved as {}", name)) |
|
358 .send_all() |
|
359 .in_room(room_id), |
|
360 ); |
|
361 room.save_config(name, location); |
|
362 } |
|
363 #[cfg(feature = "official-server")] |
|
364 SaveRoom(filename) => { |
|
365 if client.is_admin() { |
|
366 match room.get_saves() { |
|
367 Ok(contents) => response.request_io(super::IoTask::SaveRoom { |
|
368 room_id, |
|
369 filename, |
|
370 contents, |
|
371 }), |
|
372 Err(e) => { |
|
373 warn!("Error while serializing the room configs: {}", e); |
|
374 response.add( |
|
375 Warning("Unable to serialize the room configs.".to_string()) |
|
376 .send_self(), |
|
377 ) |
|
378 } |
|
379 } |
|
380 } |
|
381 } |
|
382 #[cfg(feature = "official-server")] |
|
383 LoadRoom(filename) => { |
|
384 if client.is_admin() { |
|
385 response.request_io(super::IoTask::LoadRoom { room_id, filename }); |
|
386 } |
|
387 } |
|
388 Delete(name) => { |
|
389 if !room.delete_config(&name) { |
|
390 response.add(Warning(format!("Save doesn't exist: {}", name)).send_self()); |
|
391 } else { |
|
392 response.add( |
|
393 server_chat(format!("Room config {} has been deleted", name)) |
|
394 .send_all() |
|
395 .in_room(room_id), |
|
396 ); |
|
397 } |
|
398 } |
|
399 CallVote(None) => { |
|
400 response.add(server_chat("Available callvote commands: kick <nickname>, map <name>, pause, newseed, hedgehogs <number>".to_string()) |
|
401 .send_self()); |
|
402 } |
|
403 CallVote(Some(kind)) => { |
|
404 let is_in_game = room.game_info.is_some(); |
|
405 let error = match &kind { |
|
406 VoteType::Kick(nick) => { |
|
407 if server |
|
408 .find_client(&nick) |
|
409 .filter(|c| c.room_id == Some(room_id)) |
|
410 .is_some() |
|
411 { |
|
412 None |
|
413 } else { |
|
414 Some("/callvote kick: No such user!".to_string()) |
|
415 } |
|
416 } |
|
417 VoteType::Map(None) => { |
|
418 let names: Vec<_> = server.rooms[room_id].saves.keys().cloned().collect(); |
|
419 if names.is_empty() { |
|
420 Some("/callvote map: No maps saved in this room!".to_string()) |
|
421 } else { |
|
422 Some(format!("Available maps: {}", names.join(", "))) |
|
423 } |
|
424 } |
|
425 VoteType::Map(Some(name)) => { |
|
426 if room.saves.get(&name[..]).is_some() { |
|
427 None |
|
428 } else { |
|
429 Some("/callvote map: No such map!".to_string()) |
|
430 } |
|
431 } |
|
432 VoteType::Pause => { |
|
433 if is_in_game { |
|
434 None |
|
435 } else { |
|
436 Some("/callvote pause: No game in progress!".to_string()) |
|
437 } |
|
438 } |
|
439 VoteType::NewSeed => None, |
|
440 VoteType::HedgehogsPerTeam(number) => match number { |
|
441 1...MAX_HEDGEHOGS_PER_TEAM => None, |
|
442 _ => Some("/callvote hedgehogs: Specify number from 1 to 8.".to_string()), |
|
443 }, |
|
444 }; |
|
445 |
|
446 match error { |
|
447 None => { |
|
448 let msg = voting_description(&kind); |
|
449 let voting = Voting::new(kind, server.room_clients(client_id).collect()); |
|
450 let room = &mut server.rooms[room_id]; |
|
451 room.voting = Some(voting); |
|
452 response.add(server_chat(msg).send_all().in_room(room_id)); |
|
453 super::common::submit_vote( |
|
454 server, |
|
455 coretypes::Vote { |
|
456 is_pro: true, |
|
457 is_forced: false, |
|
458 }, |
|
459 response, |
|
460 ); |
|
461 } |
|
462 Some(msg) => { |
|
463 response.add(server_chat(msg).send_self()); |
|
464 } |
|
465 } |
|
466 } |
|
467 Vote(vote) => { |
|
468 super::common::submit_vote( |
|
469 server, |
|
470 coretypes::Vote { |
|
471 is_pro: vote, |
|
472 is_forced: false, |
|
473 }, |
|
474 response, |
|
475 ); |
|
476 } |
|
477 ForceVote(vote) => { |
|
478 let is_forced = client.is_admin(); |
|
479 super::common::submit_vote( |
|
480 server, |
|
481 coretypes::Vote { |
|
482 is_pro: vote, |
|
483 is_forced, |
|
484 }, |
|
485 response, |
|
486 ); |
|
487 } |
|
488 ToggleRestrictJoin | ToggleRestrictTeams | ToggleRegisteredOnly => { |
|
489 if client.is_master() { |
|
490 room.flags.toggle(room_message_flag(&message)); |
|
491 super::common::get_room_update(None, room, Some(&client), response); |
|
492 } |
|
493 } |
|
494 StartGame => { |
|
495 super::common::start_game(server, room_id, response); |
|
496 } |
|
497 EngineMessage(em) => { |
|
498 if client.teams_in_game > 0 { |
|
499 let decoding = decode(&em[..]).unwrap(); |
|
500 let messages = by_msg(&decoding); |
|
501 let valid = messages.filter(|m| is_msg_valid(m, &client.team_indices)); |
|
502 let non_empty = valid.clone().filter(|m| !is_msg_empty(m)); |
|
503 let sync_msg = valid.clone().filter(|m| is_msg_timed(m)).last().map(|m| { |
|
504 if is_msg_empty(m) { |
|
505 Some(encode(m)) |
|
506 } else { |
|
507 None |
|
508 } |
|
509 }); |
|
510 |
|
511 let em_response = encode(&valid.flat_map(|msg| msg).cloned().collect::<Vec<_>>()); |
|
512 if !em_response.is_empty() { |
|
513 response.add( |
|
514 ForwardEngineMessage(vec![em_response]) |
|
515 .send_all() |
|
516 .in_room(room.id) |
|
517 .but_self(), |
|
518 ); |
|
519 } |
|
520 let em_log = encode(&non_empty.flat_map(|msg| msg).cloned().collect::<Vec<_>>()); |
|
521 if let Some(ref mut info) = room.game_info { |
|
522 if !em_log.is_empty() { |
|
523 info.msg_log.push(em_log); |
|
524 } |
|
525 if let Some(msg) = sync_msg { |
|
526 info.sync_msg = msg; |
|
527 } |
|
528 } |
|
529 } |
|
530 } |
|
531 RoundFinished => { |
|
532 let mut game_ended = false; |
|
533 if client.is_in_game() { |
|
534 client.set_is_in_game(false); |
|
535 response.add( |
|
536 ClientFlags(remove_flags(&[Flags::InGame]), vec![client.nick.clone()]) |
|
537 .send_all() |
|
538 .in_room(room.id), |
|
539 ); |
|
540 let team_names: Vec<_> = room |
|
541 .client_teams(client_id) |
|
542 .map(|t| t.name.clone()) |
|
543 .collect(); |
|
544 |
|
545 if let Some(ref mut info) = room.game_info { |
|
546 info.teams_in_game -= team_names.len() as u8; |
|
547 if info.teams_in_game == 0 { |
|
548 game_ended = true; |
|
549 } |
|
550 |
|
551 for team_name in team_names { |
|
552 let msg = once(b'F').chain(team_name.bytes()); |
|
553 response.add( |
|
554 ForwardEngineMessage(vec![to_engine_msg(msg)]) |
|
555 .send_all() |
|
556 .in_room(room_id) |
|
557 .but_self(), |
|
558 ); |
|
559 |
|
560 let remove_msg = to_engine_msg(once(b'F').chain(team_name.bytes())); |
|
561 if let Some(m) = &info.sync_msg { |
|
562 info.msg_log.push(m.clone()); |
|
563 } |
|
564 if info.sync_msg.is_some() { |
|
565 info.sync_msg = None |
|
566 } |
|
567 info.msg_log.push(remove_msg.clone()); |
|
568 response.add( |
|
569 ForwardEngineMessage(vec![remove_msg]) |
|
570 .send_all() |
|
571 .in_room(room_id) |
|
572 .but_self(), |
|
573 ); |
|
574 } |
|
575 } |
|
576 } |
|
577 if game_ended { |
|
578 super::common::end_game(server, room_id, response) |
|
579 } |
|
580 } |
|
581 Rnd(v) => { |
|
582 let result = rnd_reply(&v); |
|
583 let mut echo = vec!["/rnd".to_string()]; |
|
584 echo.extend(v.into_iter()); |
|
585 let chat_msg = ChatMsg { |
|
586 nick: server.clients[client_id].nick.clone(), |
|
587 msg: echo.join(" "), |
|
588 }; |
|
589 response.add(chat_msg.send_all().in_room(room_id)); |
|
590 response.add(result.send_all().in_room(room_id)); |
|
591 } |
|
592 Delegate(nick) => { |
|
593 let delegate_id = server.find_client(&nick).map(|c| (c.id, c.room_id)); |
|
594 let client = &server.clients[client_id]; |
|
595 if !(client.is_admin() || client.is_master()) { |
|
596 response.add( |
|
597 Warning("You're not the room master or a server admin!".to_string()) |
|
598 .send_self(), |
|
599 ) |
|
600 } else { |
|
601 match delegate_id { |
|
602 None => response.add(Warning("Player is not online.".to_string()).send_self()), |
|
603 Some((id, _)) if id == client_id => response |
|
604 .add(Warning("You're already the room master.".to_string()).send_self()), |
|
605 Some((_, id)) if id != Some(room_id) => response |
|
606 .add(Warning("The player is not in your room.".to_string()).send_self()), |
|
607 Some((id, _)) => { |
|
608 super::common::change_master(server, room_id, id, response); |
|
609 } |
|
610 } |
|
611 } |
|
612 } |
|
613 _ => warn!("Unimplemented!"), |
|
614 } |
|
615 } |
|