Claim protection from a local index: revision-polled panel endpoint, no per-block network checks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
10 files changed
+442
-127
No files matched your search
@@ -570,6 +570,20 @@ const MIGRATIONS: &[&str] = &[
|
||||
);
|
||||
CREATE INDEX quest_assignments_user ON quest_assignments(user_uuid, period_key, position);
|
||||
"#,
|
||||
// Bumped by any change to claims, guilds or membership so game servers can
|
||||
// poll a single number instead of asking about every block.
|
||||
r#"
|
||||
INSERT OR IGNORE INTO kv (key, value) VALUES ('guild_rev', '1');
|
||||
CREATE TRIGGER guild_claims_rev_i AFTER INSERT ON guild_claims BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guild_claims_rev_d AFTER DELETE ON guild_claims BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guild_claims_rev_u AFTER UPDATE ON guild_claims BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guild_members_rev_i AFTER INSERT ON guild_members BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guild_members_rev_d AFTER DELETE ON guild_members BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guild_members_rev_u AFTER UPDATE ON guild_members BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guilds_rev_i AFTER INSERT ON guilds BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guilds_rev_d AFTER DELETE ON guilds BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
CREATE TRIGGER guilds_rev_u AFTER UPDATE OF name, tag, leader_uuid ON guilds BEGIN UPDATE kv SET value = CAST(value AS INTEGER) + 1 WHERE key = 'guild_rev'; END;
|
||||
"#,
|
||||
];
|
||||
|
||||
pub async fn connect(data_dir: &Path) -> Result<SqlitePool> {
|
||||
|
||||
@@ -8,7 +8,7 @@ use axum::extract::{Path, Query, State};
|
||||
use axum::Json;
|
||||
use scopenet_shared::{Guild, GuildClaim, GuildMember, GuildPost};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct InstanceQuery {
|
||||
@@ -908,6 +908,58 @@ pub async fn server_claim_snapshot(
|
||||
})).collect::<Vec<_>>() })))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ClaimIndexPayload {
|
||||
/// The revision the game server already holds.
|
||||
pub revision: Option<String>,
|
||||
}
|
||||
|
||||
/// Everything a game server needs to decide claim protection locally:
|
||||
/// every claimed chunk on this server plus who belongs to each claiming guild.
|
||||
/// Answers `{"unchanged": true}` when `revision` is still current, so polling
|
||||
/// is one cheap query.
|
||||
pub async fn server_claim_index(
|
||||
GameServer(server): GameServer,
|
||||
State(state): State<AppState>,
|
||||
Json(payload): Json<ClaimIndexPayload>,
|
||||
) -> AppResult<Json<Value>> {
|
||||
let revision: String = sqlx::query_scalar("SELECT value FROM kv WHERE key = 'guild_rev'").fetch_optional(&state.db).await?.unwrap_or_else(|| "0".into());
|
||||
if payload.revision.as_deref() == Some(revision.as_str()) {
|
||||
return Ok(Json(json!({ "revision": revision, "unchanged": true })));
|
||||
}
|
||||
let rows: Vec<(String, i32, i32, String, String, String)> = sqlx::query_as(
|
||||
"SELECT gc.dimension, gc.chunk_x, gc.chunk_z, gc.guild_id, g.name, g.tag
|
||||
FROM guild_claims gc JOIN guilds g ON g.id = gc.guild_id WHERE gc.server_id = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
let members: Vec<(String, String)> = sqlx::query_as(
|
||||
"SELECT gm.guild_id, gm.uuid FROM guild_members gm
|
||||
WHERE gm.guild_id IN (SELECT DISTINCT guild_id FROM guild_claims WHERE server_id = ?)",
|
||||
)
|
||||
.bind(server.id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
// Guilds are sent once; each claim refers to its guild by index.
|
||||
let mut guilds: Vec<Value> = Vec::new();
|
||||
let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
|
||||
let mut claims: Vec<Value> = Vec::with_capacity(rows.len());
|
||||
for (dimension, x, z, guild_id, name, tag) in rows {
|
||||
let i = *index.entry(guild_id.clone()).or_insert_with(|| {
|
||||
guilds.push(json!({ "id": guild_id, "name": name, "tag": tag }));
|
||||
guilds.len() - 1
|
||||
});
|
||||
claims.push(json!([dimension, x, z, i]));
|
||||
}
|
||||
let mut roster: std::collections::BTreeMap<String, Vec<String>> = Default::default();
|
||||
for (guild, uuid) in members {
|
||||
roster.entry(guild).or_default().push(uuid);
|
||||
}
|
||||
Ok(Json(json!({ "revision": revision, "unchanged": false, "guilds": guilds, "claims": claims, "members": roster })))
|
||||
}
|
||||
|
||||
/// Token-authenticated check called by the Minecraft server plugin/mod to
|
||||
/// verify if a player can build/break in a chunk.
|
||||
pub async fn server_check_chunk(
|
||||
|
||||
@@ -175,6 +175,7 @@ pub fn api(state: &AppState) -> Router<AppState> {
|
||||
.route("/sync", post(servers::sync))
|
||||
.route("/guilds/check-chunk", post(guilds::server_check_chunk))
|
||||
.route("/guilds/claim-snapshot", post(guilds::server_claim_snapshot))
|
||||
.route("/guilds/claim-index", post(guilds::server_claim_index))
|
||||
.route("/guilds/claim", post(guilds::server_claim_chunk))
|
||||
.route("/guilds/unclaim", post(guilds::server_unclaim_chunk))
|
||||
.route("/guilds/player", post(guilds::server_get_player_guild))
|
||||
|
||||
Reference in new issue
Block a user