Compare commits
13 Commits
v2.0.0-rc1
...
fbe2b7a35e
28
.gitea/workflows/cargo-check.yml
Normal file
28
.gitea/workflows/cargo-check.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
name: Cargo Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- ".rustfmt.toml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- ".rustfmt.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Run cargo check
|
||||||
|
run: cargo check --all-targets
|
||||||
30
.gitea/workflows/cargo-fmt.yml
Normal file
30
.gitea/workflows/cargo-fmt.yml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Cargo Format Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- ".rustfmt.toml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "src/**"
|
||||||
|
- "Cargo.toml"
|
||||||
|
- "Cargo.lock"
|
||||||
|
- ".rustfmt.toml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-fmt:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust toolchain with rustfmt
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- name: Run cargo fmt check
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
39
.gitea/workflows/docker-build.yml
Normal file
39
.gitea/workflows/docker-build.yml
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Docker Image CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.GITEA_DOMAIN }}
|
||||||
|
username: ${{ env.GITEA_REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata for Docker
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -31,3 +31,7 @@ target
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
|
||||||
bracket_pairings.txt
|
bracket_pairings.txt
|
||||||
|
|
||||||
|
.cursor/
|
||||||
|
.claude/
|
||||||
|
.codex/
|
||||||
21
Dockerfile
21
Dockerfile
@@ -1,14 +1,19 @@
|
|||||||
FROM rust:1.92 AS build
|
FROM rust:1.92 AS builder
|
||||||
|
|
||||||
RUN rustup target add x86_64-unknown-linux-musl && \
|
RUN rustup target add x86_64-unknown-linux-musl && \
|
||||||
apt update && \
|
apt-get update && \
|
||||||
apt install -y musl-tools musl-dev && \
|
apt-get install -y --no-install-recommends musl-tools musl-dev ca-certificates && \
|
||||||
update-ca-certificates
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY ./src ./src
|
WORKDIR /app
|
||||||
COPY ./Cargo.lock .
|
COPY Cargo.toml Cargo.lock ./
|
||||||
COPY ./Cargo.toml .
|
COPY src ./src
|
||||||
|
|
||||||
RUN cargo build --target x86_64-unknown-linux-musl --release
|
RUN cargo build --target x86_64-unknown-linux-musl --release
|
||||||
|
|
||||||
ENTRYPOINT ["./target/x86_64-unknown-linux-musl/release/connect4-moderator-server"]
|
FROM scratch AS runtime
|
||||||
|
|
||||||
|
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||||
|
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/connect4-moderator-server /connect4-moderator-server
|
||||||
|
|
||||||
|
ENTRYPOINT ["/connect4-moderator-server"]
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ async def gameloop(socket):
|
|||||||
match message[0]:
|
match message[0]:
|
||||||
case "CONNECT":
|
case "CONNECT":
|
||||||
await socket.send("READY")
|
await socket.send("READY")
|
||||||
|
case "RECONNECT":
|
||||||
|
await socket.send("READY")
|
||||||
|
|
||||||
case "GAME":
|
case "GAME":
|
||||||
if message[1] == "START":
|
if message[1] == "START":
|
||||||
|
|||||||
44
src/main.rs
44
src/main.rs
@@ -305,33 +305,47 @@ async fn handle_connection(
|
|||||||
|
|
||||||
// Remove and terminate any matches
|
// Remove and terminate any matches
|
||||||
// We may not be a client disconnecting, do this check
|
// We may not be a client disconnecting, do this check
|
||||||
let clients_guard = sd.clients.read().await;
|
if let Some(username) = sd.usernames.read().await.get(&addr).cloned() {
|
||||||
for client in clients_guard.values() {
|
|
||||||
let client = client.read().await;
|
|
||||||
if client.addr == addr {
|
|
||||||
let username = client.username.clone();
|
|
||||||
let tournament_guard = sd.tournament.read().await;
|
let tournament_guard = sd.tournament.read().await;
|
||||||
|
let client = sd.clients.read().await.get(&username).cloned().unwrap();
|
||||||
|
let mut client = client.write().await;
|
||||||
|
client.ready = false;
|
||||||
|
|
||||||
if client.current_match.is_some() {
|
if client.current_match.is_some() {
|
||||||
|
let current_match =
|
||||||
|
sd.matches.read().await.get(&client.current_match.unwrap()).cloned().unwrap();
|
||||||
|
let current_match = current_match.read().await;
|
||||||
|
if let Some(wait_thread) = ¤t_match.wait_thread {
|
||||||
|
wait_thread.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(timeout_thread) = ¤t_match.timeout_thread {
|
||||||
|
timeout_thread.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
if current_match.demo_mode {
|
||||||
|
sd.matches.write().await.remove(¤t_match.id);
|
||||||
|
sd.broadcast(&format!("GAME:{}:TERMINATED", current_match.id)).await;
|
||||||
|
sd.clients.write().await.remove(&username);
|
||||||
|
sd.broadcast(&format!("DISCONNECT:{}", username)).await;
|
||||||
|
} else {
|
||||||
sd.disconnected_clients.write().await.push(username.clone());
|
sd.disconnected_clients.write().await.push(username.clone());
|
||||||
|
}
|
||||||
} else if tournament_guard.is_some() {
|
} else if tournament_guard.is_some() {
|
||||||
let tourney = tournament_guard.clone().unwrap();
|
let tourney = tournament_guard.clone().unwrap();
|
||||||
if tourney.read().await.contains_player(username.clone()) {
|
if tourney.read().await.contains_player(username.clone()) {
|
||||||
sd.disconnected_clients.write().await.push(username.clone());
|
sd.disconnected_clients.write().await.push(username.clone());
|
||||||
}
|
} else {
|
||||||
}
|
sd.clients.write().await.remove(&username);
|
||||||
|
sd.broadcast(&format!("DISCONNECT:{}", username)).await;
|
||||||
drop(client);
|
}
|
||||||
drop(clients_guard);
|
} else {
|
||||||
|
|
||||||
sd.clients.write().await.remove(&username);
|
sd.clients.write().await.remove(&username);
|
||||||
sd.usernames.write().await.remove(&addr);
|
|
||||||
|
|
||||||
sd.broadcast(&format!("DISCONNECT:{}", username)).await;
|
sd.broadcast(&format!("DISCONNECT:{}", username)).await;
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sd.usernames.write().await.remove(&addr);
|
||||||
sd.observers.write().await.remove(&addr);
|
sd.observers.write().await.remove(&addr);
|
||||||
|
|
||||||
let mut admin_guard = sd.admin.write().await;
|
let mut admin_guard = sd.admin.write().await;
|
||||||
|
|||||||
@@ -63,11 +63,7 @@ impl Server {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut reconnecting = false;
|
let reconnecting = self.disconnected_clients.read().await.contains(&requested_username);
|
||||||
let disconnected_guard = self.disconnected_clients.read().await;
|
|
||||||
if disconnected_guard.contains(&requested_username) {
|
|
||||||
reconnecting = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let clients_guard = self.clients.read().await;
|
let clients_guard = self.clients.read().await;
|
||||||
let existing_client = clients_guard.get(&requested_username).cloned();
|
let existing_client = clients_guard.get(&requested_username).cloned();
|
||||||
@@ -105,42 +101,24 @@ impl Server {
|
|||||||
let mut client = client_guard.write().await;
|
let mut client = client_guard.write().await;
|
||||||
client.addr = addr;
|
client.addr = addr;
|
||||||
client.connection = tx.clone();
|
client.connection = tx.clone();
|
||||||
// I don't think this will fail
|
|
||||||
let match_id = client.current_match.unwrap();
|
|
||||||
let client_color = client.color;
|
|
||||||
|
|
||||||
drop(client);
|
if let Some(current_match_id) = client.current_match {
|
||||||
|
let current_match = self.matches.read().await.get(¤t_match_id).cloned().unwrap();
|
||||||
let matches_guard = self.matches.read().await;
|
let mut current_match = current_match.write().await;
|
||||||
let mut the_match = matches_guard.get(&match_id).unwrap().write().await;
|
current_match.ledger.clear();
|
||||||
if the_match.demo_mode {
|
current_match.board = vec![vec![Color::None; 6]; 7];
|
||||||
drop(the_match);
|
let opponent_username = if current_match.player1 == requested_username {
|
||||||
drop(matches_guard);
|
current_match.player2.clone()
|
||||||
self.terminate_match(match_id).await;
|
|
||||||
return Ok(());
|
|
||||||
} else {
|
} else {
|
||||||
the_match.ledger.clear();
|
current_match.player1.clone()
|
||||||
the_match.board = vec![vec![Color::None; 6]; 7];
|
|
||||||
let opponent_username = if the_match.player1 == requested_username {
|
|
||||||
the_match.player2.clone()
|
|
||||||
} else {
|
|
||||||
the_match.player1.clone()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if the_match.wait_thread.is_some() {
|
|
||||||
the_match.wait_thread.as_ref().unwrap().abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if the_match.timeout_thread.is_some() {
|
|
||||||
the_match.timeout_thread.as_ref().unwrap().abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
let clients_guard = self.clients.read().await;
|
let clients_guard = self.clients.read().await;
|
||||||
let opponent = clients_guard.get(&opponent_username).unwrap().read().await;
|
let opponent = clients_guard.get(&opponent_username).unwrap().read().await;
|
||||||
let _ = send(&opponent.connection, "GAME:TERMINATED");
|
let _ = send(&opponent.connection, "GAME:TERMINATED");
|
||||||
let _ = send(
|
let _ = send(
|
||||||
&tx,
|
&tx,
|
||||||
&format!("GAME:START:{}", bool::from(client_color) as u8),
|
&format!("GAME:START:{}", bool::from(client.color) as u8),
|
||||||
);
|
);
|
||||||
let _ = send(
|
let _ = send(
|
||||||
&opponent.connection,
|
&opponent.connection,
|
||||||
@@ -170,14 +148,14 @@ impl Server {
|
|||||||
|
|
||||||
if let Some(client_guard) = found_client {
|
if let Some(client_guard) = found_client {
|
||||||
self.disconnected_clients.write().await.retain(|name| name != &requested_username);
|
self.disconnected_clients.write().await.retain(|name| name != &requested_username);
|
||||||
|
self.usernames.write().await.insert(addr, requested_username.clone());
|
||||||
let mut client = client_guard.write().await;
|
let mut client = client_guard.write().await;
|
||||||
client.addr = addr;
|
client.addr = addr;
|
||||||
client.connection = tx.clone();
|
client.connection = tx.clone();
|
||||||
|
|
||||||
let _ = send(&tx, "RECONNECT:ACK");
|
if let Some(current_match_id) = client.current_match {
|
||||||
|
|
||||||
let matches_guard = self.matches.read().await;
|
let matches_guard = self.matches.read().await;
|
||||||
let the_match = matches_guard.get(&client.current_match.unwrap()).unwrap().read().await;
|
let the_match = matches_guard.get(¤t_match_id).unwrap().read().await;
|
||||||
|
|
||||||
let last = the_match.ledger.last();
|
let last = the_match.ledger.last();
|
||||||
if last.is_some() && last.unwrap().0 != client.color {
|
if last.is_some() && last.unwrap().0 != client.color {
|
||||||
@@ -185,12 +163,23 @@ impl Server {
|
|||||||
&tx,
|
&tx,
|
||||||
&format!("OPPONENT:{}", the_match.ledger.last().unwrap().1),
|
&format!("OPPONENT:{}", the_match.ledger.last().unwrap().1),
|
||||||
);
|
);
|
||||||
|
} else if last.is_none() && client.color == Color::Red {
|
||||||
|
let _ = send(&tx, "GAME:START:1");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow::anyhow!(format!(
|
// Clear they're state just in case, even if it's not terminated
|
||||||
"ERROR:INVALID:RECONNECT:{}",
|
let _ = send(&tx, "GAME:TERMINATED");
|
||||||
requested_username
|
}
|
||||||
)));
|
|
||||||
|
let tournament = self.tournament.read().await.as_ref().cloned();
|
||||||
|
if let Some(tourney) = tournament {
|
||||||
|
let tourney = tourney.read().await;
|
||||||
|
if !tourney.contains_player(requested_username) {
|
||||||
|
let _ = send(&tx, "RECONNECT:ACK");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return self.handle_connect_cmd(addr, tx, requested_username).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -266,7 +255,8 @@ impl Server {
|
|||||||
let username = username.unwrap();
|
let username = username.unwrap();
|
||||||
|
|
||||||
if clients_guard.get(&username).unwrap().read().await.ready {
|
if clients_guard.get(&username).unwrap().read().await.ready {
|
||||||
return Err(anyhow::anyhow!("ERROR:INVALID:READY"));
|
let _ = send(&tx, "READY:ACK");
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client = clients_guard.get(&username).unwrap().write().await;
|
let mut client = clients_guard.get(&username).unwrap().write().await;
|
||||||
@@ -281,6 +271,7 @@ impl Server {
|
|||||||
let mut client = clients_guard.get(&username).unwrap().write().await;
|
let mut client = clients_guard.get(&username).unwrap().write().await;
|
||||||
let mut opponent = clients_guard.get(&opponent_username).unwrap().write().await;
|
let mut opponent = clients_guard.get(&opponent_username).unwrap().write().await;
|
||||||
|
|
||||||
|
if opponent.ready {
|
||||||
let match_id: u32 = gen_match_id(&self.matches).await;
|
let match_id: u32 = gen_match_id(&self.matches).await;
|
||||||
let new_match = Arc::new(RwLock::new(Match::new(
|
let new_match = Arc::new(RwLock::new(Match::new(
|
||||||
match_id,
|
match_id,
|
||||||
@@ -329,6 +320,7 @@ impl Server {
|
|||||||
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let clients_guard = self.clients.read().await;
|
let clients_guard = self.clients.read().await;
|
||||||
let mut client = clients_guard.get(&username).unwrap().write().await;
|
let mut client = clients_guard.get(&username).unwrap().write().await;
|
||||||
@@ -1384,19 +1376,26 @@ impl Server {
|
|||||||
self.broadcast(&format!("GAME:{}:TERMINATED", the_match.id)).await;
|
self.broadcast(&format!("GAME:{}:TERMINATED", the_match.id)).await;
|
||||||
|
|
||||||
let clients_guard = self.clients.read().await;
|
let clients_guard = self.clients.read().await;
|
||||||
|
|
||||||
if the_match.player1 != SERVER_PLAYER_USERNAME.to_string() {
|
if the_match.player1 != SERVER_PLAYER_USERNAME.to_string() {
|
||||||
let mut player1 = clients_guard.get(&the_match.player1).unwrap().write().await;
|
let player1 = clients_guard.get(&the_match.player1).cloned();
|
||||||
|
if let Some(player1) = player1 {
|
||||||
|
let mut player1 = player1.write().await;
|
||||||
let _ = send(&player1.connection, "GAME:TERMINATED");
|
let _ = send(&player1.connection, "GAME:TERMINATED");
|
||||||
player1.current_match = None;
|
player1.current_match = None;
|
||||||
player1.color = Color::None;
|
player1.color = Color::None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if the_match.player2 != SERVER_PLAYER_USERNAME.to_string() {
|
if the_match.player2 != SERVER_PLAYER_USERNAME.to_string() {
|
||||||
let mut player2 = clients_guard.get(&the_match.player2).unwrap().write().await;
|
let player2 = clients_guard.get(&the_match.player2).cloned();
|
||||||
|
if let Some(player2) = player2 {
|
||||||
|
let mut player2 = player2.write().await;
|
||||||
let _ = send(&player2.connection, "GAME:TERMINATED");
|
let _ = send(&player2.connection, "GAME:TERMINATED");
|
||||||
player2.current_match = None;
|
player2.current_match = None;
|
||||||
player2.color = Color::None;
|
player2.color = Color::None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
drop(clients_guard);
|
drop(clients_guard);
|
||||||
|
|
||||||
drop(the_match);
|
drop(the_match);
|
||||||
|
|||||||
Reference in New Issue
Block a user