Created
January 14, 2023 15:30
-
-
Save HeatXD/d28462034351a50f8412478c5ff40f60 to your computer and use it in GitHub Desktop.
godot 4 -> ggrs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use ggrs::{Config, GGRSError, P2PSession, PlayerType, SessionBuilder, UdpNonBlockingSocket, GameStateCell}; | |
use godot::prelude::*; | |
use std::{ | |
collections::{HashMap, VecDeque}, | |
net::SocketAddr, | |
}; | |
pub struct GGRSConfig; | |
impl Config for GGRSConfig { | |
type Input = i32; | |
type State = Variant; | |
type Address = SocketAddr; | |
} | |
#[derive(GodotClass)] | |
#[class(base=RefCounted)] | |
pub struct GodotGGRSInput{ | |
/* | |
Input Status | |
1000 = Confirmed Input | |
2000 = Predicted Input | |
3000 = Disconnected Input | |
*/ | |
status: i32, | |
input: i32, | |
#[base] | |
base: Base<RefCounted>, | |
} | |
#[godot_api] | |
impl GodotGGRSInput { | |
#[func] | |
fn status(&mut self) -> i32 { | |
self.status | |
} | |
#[func] | |
fn input(&mut self) -> i32 { | |
self.input | |
} | |
fn set(&mut self, input: i32, status: i32) { | |
self.input = input; | |
self.status = status; | |
} | |
} | |
#[derive(GodotClass)] | |
#[class(base=RefCounted)] | |
pub struct GodotGGRSRequest { | |
/* | |
Request Codes | |
1000 = GGRSRequest::SaveGameState | |
2000 = GGRSRequest::LoadGameState | |
3000 = GGRSRequest::AdvanceFrame | |
*/ | |
code: i64, | |
frame: Option<i32>, | |
inputs: VecDeque<Gd<GodotGGRSInput>>, | |
ggrs_cell: Option<GameStateCell<Variant>>, | |
#[base] | |
base: Base<RefCounted>, | |
} | |
#[godot_api] | |
impl GodotGGRSRequest { | |
#[func] | |
fn frame(&mut self) -> i32 { | |
if let Some(frame) = self.frame { | |
return frame; | |
} | |
return -1; | |
} | |
#[func] | |
fn code(&mut self) -> i64 { | |
self.code | |
} | |
#[func] | |
fn state_save(&mut self, frame: i32, savestate: Variant) { | |
if let Some(cell) = &self.ggrs_cell{ | |
cell.save(frame, Some(savestate), Some(0)); | |
} | |
} | |
#[func] | |
fn state_load(&mut self) -> Variant { | |
if let Some(cell) = &self.ggrs_cell{ | |
if let Some(savestate) = cell.load(){ | |
return savestate; | |
} | |
} | |
// if no state is found just give back null | |
return Variant::default(); | |
} | |
fn set(&mut self,code: i64, frame: Option<i32>, cell: Option<GameStateCell<Variant>>) { | |
self.code = code; | |
self.frame = frame; | |
self.ggrs_cell = cell; | |
} | |
fn add_input(&mut self, input: Gd<GodotGGRSInput>) { | |
self.inputs.push_back(input); | |
} | |
#[func] | |
fn has_input(&mut self) -> bool { | |
!self.inputs.is_empty() | |
} | |
#[func] | |
fn get_input(&mut self) -> Gd<GodotGGRSInput> { | |
if let Some(input) = self.inputs.pop_front() { | |
return input; | |
}; | |
// its empty return dummy input | |
return Gd::<GodotGGRSInput>::new_default(); | |
} | |
} | |
#[derive(GodotClass)] | |
#[class(base=RefCounted)] | |
pub struct GodotGGRSRequestCollection { | |
/* | |
1000 = OK | |
2000 = SESSION ERROR | |
3000 = GGRSError::PredictionThreshold | |
4000 = GGRSError::InvalidRequest | |
5000 = GGRSError::MismatchedChecksum | |
6000 = GGRSError::NotSynchronized | |
7000 = GGRSError::SpectatorTooFarBehind | |
8000 = Unkown Error | |
*/ | |
code: i64, | |
frame: Option<i32>, | |
info: Option<GodotString>, | |
requests: VecDeque<Gd<GodotGGRSRequest>>, | |
#[base] | |
base: Base<RefCounted>, | |
} | |
#[godot_api] | |
impl GodotGGRSRequestCollection { | |
fn set(&mut self, code: i64, frame: Option<i32>, info: Option<GodotString>){ | |
self.code = code; | |
self.frame = frame; | |
self.info = info; | |
} | |
#[func] | |
fn code(&mut self) -> i64 { | |
self.code | |
} | |
fn add_request(&mut self, event: Gd<GodotGGRSRequest>) { | |
self.requests.push_back(event); | |
} | |
#[func] | |
fn info(&mut self) -> GodotString { | |
if let Some(info) = &self.info { | |
return info.clone(); | |
} | |
return GodotString::from(""); | |
} | |
#[func] | |
fn has_requests(&mut self) -> bool { | |
!self.requests.is_empty() | |
} | |
#[func] | |
fn get_request(&mut self) -> Gd<GodotGGRSRequest> { | |
if let Some(req) = self.requests.pop_front() { | |
return req; | |
}; | |
// its empty return dummy event | |
return Gd::<GodotGGRSRequest>::new_default(); | |
} | |
} | |
#[derive(GodotClass)] | |
#[class(base=RefCounted)] | |
pub struct GodotGGRSEvent { | |
/* | |
Event Codes | |
1000 = GGRSEvent::Synchronizing | |
2000 = GGRSEvent::Synchronized | |
3000 = GGRSEvent::NetworkInterrupted | |
4000 = GGRSEvent::NetworkResumed | |
5000 = GGRSEvent::WaitRecommendation | |
6000 = GGRSEvent::Disconnected | |
*/ | |
code: i64, | |
addr: Option<GodotString>, | |
total: Option<u32>, | |
count: Option<u32>, | |
disconnect_timeout: Option<u32>, | |
skip_frames: Option<u32>, | |
#[base] | |
base: Base<RefCounted>, | |
} | |
#[godot_api] | |
impl GodotGGRSEvent { | |
#[func] | |
fn code(&mut self) -> i64 { | |
self.code | |
} | |
#[func] | |
fn skip_frames(&mut self) -> u32 { | |
if let Some(skip) = self.skip_frames { | |
return skip; | |
} | |
return 0; | |
} | |
#[func] | |
fn addr(&mut self) -> GodotString { | |
if let Some(addr) = &self.addr { | |
return addr.clone(); | |
} | |
return GodotString::from(""); | |
} | |
#[func] | |
fn total(&mut self) -> u32 { | |
if let Some(total) = self.total { | |
return total; | |
} | |
return 0; | |
} | |
#[func] | |
fn count(&mut self) -> u32 { | |
if let Some(count) = self.count { | |
return count; | |
} | |
return 0; | |
} | |
#[func] | |
fn disconnect_timeout(&mut self) -> u32 { | |
if let Some(timeout) = self.disconnect_timeout { | |
return timeout; | |
} | |
return 0; | |
} | |
fn set( | |
&mut self, | |
code: i64, | |
addr: Option<GodotString>, | |
total: Option<u32>, | |
count: Option<u32>, | |
disconnect_timeout: Option<u32>, | |
skip_frames: Option<u32>, | |
) { | |
self.code = code; | |
self.addr = addr; | |
self.total = total; | |
self.count = count; | |
self.disconnect_timeout = disconnect_timeout; | |
self.skip_frames = skip_frames; | |
} | |
} | |
#[derive(GodotClass)] | |
#[class(base=RefCounted)] | |
pub struct GodotGGRSEventCollection { | |
events: VecDeque<Gd<GodotGGRSEvent>>, | |
#[base] | |
base: Base<RefCounted>, | |
} | |
#[godot_api] | |
impl GodotGGRSEventCollection { | |
fn add_event(&mut self, event: Gd<GodotGGRSEvent>) { | |
self.events.push_back(event); | |
} | |
#[func] | |
fn has_events(&mut self) -> bool { | |
!self.events.is_empty() | |
} | |
#[func] | |
fn get_event(&mut self) -> Gd<GodotGGRSEvent> { | |
if let Some(ev) = self.events.pop_front() { | |
return ev; | |
}; | |
// its empty return dummy event | |
return Gd::<GodotGGRSEvent>::new_default(); | |
} | |
} | |
#[derive(GodotClass)] | |
#[class(base=Node)] | |
pub struct GodotGGRS { | |
show_debug: bool, | |
players: HashMap<i64, PlayerType<SocketAddr>>, | |
p2p_session: Option<P2PSession<GGRSConfig>>, | |
#[base] | |
base: Base<Node>, | |
} | |
#[godot_api] | |
impl GodotGGRS { | |
fn has_player(&mut self, player_handle: i64) -> bool { | |
self.players.contains_key(&player_handle) | |
} | |
#[func] | |
fn show_debug_messages(&mut self, show: bool) { | |
self.show_debug = show; | |
} | |
#[func] | |
fn add_player_local(&mut self, player_handle: i64) -> bool { | |
if self.has_player(player_handle) { | |
return false; | |
} | |
self.players.insert(player_handle, PlayerType::Local); | |
return true; | |
} | |
#[func] | |
fn add_player_remote(&mut self, player_handle: i64, addr: GodotString) -> bool { | |
if self.has_player(player_handle) { | |
return false; | |
} | |
let result = addr.to_string().parse::<SocketAddr>(); | |
if result.is_err() { | |
return false; | |
} | |
self.players | |
.insert(player_handle, PlayerType::Remote(result.unwrap())); | |
return true; | |
} | |
#[func] | |
fn create_p2p_session( | |
&mut self, | |
num_players: i32, | |
fps: i32, | |
delay: i32, | |
local_port: u16, | |
sparse_saving: bool, | |
) -> bool { | |
let mut create_sess = || -> Result<(), GGRSError> { | |
// create session | |
let mut sess_build = SessionBuilder::<GGRSConfig>::new() | |
.with_num_players(num_players as usize) | |
.with_fps(fps as usize)? | |
.with_input_delay(delay as usize) | |
.with_sparse_saving_mode(sparse_saving); | |
// add players | |
for (handle, player_type) in self.players.drain() { | |
sess_build = sess_build.add_player(player_type, handle as usize)?; | |
} | |
// add spectators | |
// todo | |
// start ggrs session | |
let result = UdpNonBlockingSocket::bind_to_port(local_port); | |
if result.is_err() { | |
return Err(GGRSError::InvalidRequest { | |
info: "couldn't bind to port".to_string(), | |
}); | |
} | |
self.p2p_session = Some(sess_build.start_p2p_session(result.unwrap())?); | |
Ok(()) | |
}; | |
if let Err(err) = create_sess() { | |
if self.show_debug { | |
godot_print!("ERROR FROM RUST! {}", err); | |
} | |
return false; | |
} | |
return true; | |
} | |
#[func] | |
fn events(&mut self) -> Gd<GodotGGRSEventCollection> { | |
let mut result = Gd::<GodotGGRSEventCollection>::new_default(); | |
let Some(session) = &mut self.p2p_session else { return result; }; | |
for ev in session.events() { | |
match ev { | |
ggrs::GGRSEvent::Synchronizing { addr, total, count } => { | |
let mut event = Gd::<GodotGGRSEvent>::new_default(); | |
event.bind_mut().set( | |
1000, | |
Some(GodotString::from(addr.to_string())), | |
Some(total), | |
Some(count), | |
None, | |
None, | |
); | |
result.bind_mut().add_event(event); | |
} | |
ggrs::GGRSEvent::Synchronized { addr } => { | |
let mut event = Gd::<GodotGGRSEvent>::new_default(); | |
event.bind_mut().set( | |
2000, | |
Some(GodotString::from(addr.to_string())), | |
None, | |
None, | |
None, | |
None, | |
); | |
result.bind_mut().add_event(event); | |
} | |
ggrs::GGRSEvent::NetworkInterrupted { | |
addr, | |
disconnect_timeout, | |
} => { | |
let mut event = Gd::<GodotGGRSEvent>::new_default(); | |
event.bind_mut().set( | |
3000, | |
Some(GodotString::from(addr.to_string())), | |
None, | |
None, | |
Some(disconnect_timeout as u32), | |
None, | |
); | |
result.bind_mut().add_event(event); | |
} | |
ggrs::GGRSEvent::NetworkResumed { addr } => { | |
let mut event = Gd::<GodotGGRSEvent>::new_default(); | |
event.bind_mut().set( | |
4000, | |
Some(GodotString::from(addr.to_string())), | |
None, | |
None, | |
None, | |
None, | |
); | |
result.bind_mut().add_event(event); | |
} | |
ggrs::GGRSEvent::WaitRecommendation { skip_frames } => { | |
let mut event = Gd::<GodotGGRSEvent>::new_default(); | |
event | |
.bind_mut() | |
.set(5000, None, None, None, None, Some(skip_frames)); | |
result.bind_mut().add_event(event); | |
} | |
ggrs::GGRSEvent::Disconnected { addr } => { | |
let mut event = Gd::<GodotGGRSEvent>::new_default(); | |
event.bind_mut().set( | |
6000, | |
Some(GodotString::from(addr.to_string())), | |
None, | |
None, | |
None, | |
None, | |
); | |
result.bind_mut().add_event(event); | |
} | |
}; | |
} | |
return result; | |
} | |
#[func] | |
fn poll_remote_clients(&mut self) { | |
let Some(session) = &mut self.p2p_session else { return; }; | |
session.poll_remote_clients(); | |
} | |
// -1000 = SESSION ERROR | |
#[func] | |
fn frames_ahead(&self) -> i32 { | |
let Some(session) = &self.p2p_session else { return -1000 }; | |
session.frames_ahead() | |
} | |
/* | |
1000 = Error | |
2000 = SessionState::Synchronizing | |
3000 = SessionState::Running | |
*/ | |
#[func] | |
fn current_state(&self) -> u32 { | |
let Some(session) = &self.p2p_session else { return 1000 }; | |
match session.current_state() { | |
ggrs::SessionState::Synchronizing => return 2000, | |
ggrs::SessionState::Running => return 3000, | |
} | |
} | |
#[func] | |
fn add_local_input(&mut self, handle: i64, input: i32) { | |
let Some(session) = &mut self.p2p_session else { return }; | |
if session.add_local_input(handle as usize, input).is_err() { | |
if self.show_debug { | |
godot_print!("Could not add local input!"); | |
} | |
} | |
} | |
#[func] | |
fn advance_frame(&mut self) -> Gd<GodotGGRSRequestCollection> { | |
let mut result = Gd::<GodotGGRSRequestCollection>::new_default(); | |
// handle errors | |
let Some(session) = &mut self.p2p_session else { | |
result.bind_mut().set(1000, None, None); | |
return result; | |
}; | |
let progress = session.advance_frame(); | |
if let Err(err) = progress { | |
match err { | |
GGRSError::PredictionThreshold => { | |
result.bind_mut().set(2000, None, None); | |
} | |
GGRSError::InvalidRequest { info } => { | |
result.bind_mut().set(3000, None, Some(GodotString::from(info.to_string()))); | |
} | |
GGRSError::MismatchedChecksum { frame } => { | |
result.bind_mut().set(4000, Some(frame), None); | |
} | |
GGRSError::NotSynchronized => { | |
result.bind_mut().set(5000, None, None); | |
} | |
GGRSError::SpectatorTooFarBehind => { | |
result.bind_mut().set(6000, None, None); | |
} | |
_ => result.bind_mut().set(7000, None, None), | |
} | |
return result; | |
} | |
// no errors? handle incoming requests | |
result.bind_mut().set(1000, None, None); | |
let requests = progress.unwrap(); | |
for req in requests { | |
let mut request = Gd::<GodotGGRSRequest>::new_default(); | |
match req { | |
ggrs::GGRSRequest::SaveGameState { cell, frame } => { | |
request.bind_mut().set(1000, Some(frame), Some(cell)); | |
} | |
ggrs::GGRSRequest::LoadGameState { cell , frame } => { | |
request.bind_mut().set(2000, Some(frame), Some(cell)); | |
} | |
ggrs::GGRSRequest::AdvanceFrame { inputs } => { | |
request.bind_mut().set(3000, None, None); | |
for (input, status) in inputs { | |
let mut new_input = Gd::<GodotGGRSInput>::new_default(); | |
let input_status = match status { | |
ggrs::InputStatus::Confirmed => 1000, | |
ggrs::InputStatus::Predicted => 2000, | |
ggrs::InputStatus::Disconnected => 3000, | |
}; | |
new_input.bind_mut().set(input, input_status); | |
request.bind_mut().add_input(new_input); | |
} | |
} | |
} | |
result.bind_mut().add_request(request); | |
}; | |
return result; | |
} | |
} | |
#[godot_api] | |
impl GodotExt for GodotGGRS { | |
fn init(base: Base<Node>) -> Self { | |
GodotGGRS { | |
show_debug: false, | |
players: HashMap::new(), | |
p2p_session: None, | |
base, | |
} | |
} | |
} | |
#[godot_api] | |
impl GodotExt for GodotGGRSEventCollection { | |
fn init(base: Base<RefCounted>) -> Self { | |
GodotGGRSEventCollection { | |
events: VecDeque::new(), | |
base, | |
} | |
} | |
} | |
#[godot_api] | |
impl GodotExt for GodotGGRSEvent { | |
fn init(base: Base<RefCounted>) -> Self { | |
GodotGGRSEvent { | |
code: 0, | |
addr: None, | |
total: None, | |
count: None, | |
disconnect_timeout: None, | |
skip_frames: None, | |
base, | |
} | |
} | |
} | |
#[godot_api] | |
impl GodotExt for GodotGGRSRequestCollection { | |
fn init(base: Base<RefCounted>) -> Self { | |
GodotGGRSRequestCollection { | |
code: 0, | |
frame: None, | |
info: None, | |
requests: VecDeque::new(), | |
base, | |
} | |
} | |
} | |
#[godot_api] | |
impl GodotExt for GodotGGRSRequest { | |
fn init(base: Base<RefCounted>) -> Self { | |
GodotGGRSRequest { | |
code: 0, | |
frame: None, | |
ggrs_cell: None, | |
inputs: VecDeque::new(), | |
base, | |
} | |
} | |
} | |
#[godot_api] | |
impl GodotExt for GodotGGRSInput { | |
fn init(base: Base<RefCounted>) -> Self { | |
GodotGGRSInput { | |
input: 0, | |
status: 0, | |
base: base, | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment