Per-user campaign storage and public campaign list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
535800c7dc
commit
65677504f6
5 files changed
+527
-12
No files matched your search
+169
-1
@@ -1,5 +1,8 @@
|
||||
// Client for the savegame service (server/): a shared list of savegames on the web server.
|
||||
// Client for the savegame service (server/): a shared list of savegames on the web server,
|
||||
// plus the own campaigns and the campaigns other users published.
|
||||
// All requests are asynchronous; poll() is called once per frame and reports finished requests.
|
||||
// Campaign requests report through fields/take_* functions instead of new events: on success
|
||||
// poll() returns CloudEvent::None, on failure CloudEvent::Error (also kept in campaign_error).
|
||||
// When the service cannot be reached (no server, or the game was opened as a local file),
|
||||
// `online` becomes false and the game falls back to savegame files and the browser autosave.
|
||||
|
||||
@@ -23,6 +26,31 @@ pub struct SaveInfo {
|
||||
pub time: f64,
|
||||
}
|
||||
|
||||
// an own campaign on the server
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub struct CampaignInfo {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub updated: u64,
|
||||
#[serde(default)]
|
||||
pub published: bool,
|
||||
#[serde(default)]
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
// a campaign some user published
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Deserialize, Debug)]
|
||||
pub struct PublicCampaignInfo {
|
||||
pub owner: String,
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub updated: u64,
|
||||
#[serde(default)]
|
||||
pub size: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum Online {
|
||||
Unknown,
|
||||
@@ -30,6 +58,7 @@ pub enum Online {
|
||||
No,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum Req {
|
||||
Health,
|
||||
@@ -44,6 +73,13 @@ enum Req {
|
||||
Users,
|
||||
CreateUser,
|
||||
DeleteUser,
|
||||
CampaignList,
|
||||
CampaignLoad,
|
||||
CampaignSave,
|
||||
CampaignDelete,
|
||||
CampaignPublish,
|
||||
PublicList,
|
||||
PublicLoad,
|
||||
}
|
||||
|
||||
// the logged-in account
|
||||
@@ -82,6 +118,13 @@ pub struct Cloud {
|
||||
pub list: Vec<SaveInfo>,
|
||||
pub listed: bool, // the list has been fetched at least once
|
||||
pub users: Vec<UserInfo>,
|
||||
pub campaigns: Vec<CampaignInfo>, // own campaigns (newest first)
|
||||
pub campaigns_listed: bool,
|
||||
pub public_campaigns: Vec<PublicCampaignInfo>, // published campaigns of all users
|
||||
pub public_listed: bool,
|
||||
pub campaign_error: Option<&'static str>, // language key of the last failed campaign request
|
||||
loaded_campaign: Option<String>,
|
||||
saved_campaign: Option<CampaignInfo>,
|
||||
pending: Option<(u32, Req)>,
|
||||
save_name: String,
|
||||
login_name: String,
|
||||
@@ -127,6 +170,13 @@ impl Cloud {
|
||||
list: Vec::new(),
|
||||
listed: false,
|
||||
users: Vec::new(),
|
||||
campaigns: Vec::new(),
|
||||
campaigns_listed: false,
|
||||
public_campaigns: Vec::new(),
|
||||
public_listed: false,
|
||||
campaign_error: None,
|
||||
loaded_campaign: None,
|
||||
saved_campaign: None,
|
||||
pending: Some((id, Req::Health)),
|
||||
save_name: String::new(),
|
||||
login_name: String::new(),
|
||||
@@ -326,10 +376,120 @@ impl Cloud {
|
||||
self.refresh();
|
||||
CloudEvent::Deleted
|
||||
}
|
||||
Req::CampaignList => {
|
||||
if !ok {
|
||||
return self.campaign_failed(res.status);
|
||||
}
|
||||
match serde_json::from_slice::<Vec<CampaignInfo>>(&res.body) {
|
||||
Ok(l) => {
|
||||
self.campaigns = l;
|
||||
self.campaigns_listed = true;
|
||||
CloudEvent::None
|
||||
}
|
||||
Err(_) => self.campaign_failed(500),
|
||||
}
|
||||
}
|
||||
Req::PublicList => {
|
||||
if !ok {
|
||||
return self.campaign_failed(res.status);
|
||||
}
|
||||
match serde_json::from_slice::<Vec<PublicCampaignInfo>>(&res.body) {
|
||||
Ok(l) => {
|
||||
self.public_campaigns = l;
|
||||
self.public_listed = true;
|
||||
CloudEvent::None
|
||||
}
|
||||
Err(_) => self.campaign_failed(500),
|
||||
}
|
||||
}
|
||||
Req::CampaignLoad | Req::PublicLoad => {
|
||||
if !ok {
|
||||
return self.campaign_failed(res.status);
|
||||
}
|
||||
self.loaded_campaign = Some(String::from_utf8_lossy(&res.body).into_owned());
|
||||
CloudEvent::None
|
||||
}
|
||||
Req::CampaignSave => {
|
||||
if !ok {
|
||||
return self.campaign_failed(res.status);
|
||||
}
|
||||
self.saved_campaign = serde_json::from_slice::<CampaignInfo>(&res.body).ok();
|
||||
self.refresh_campaigns();
|
||||
CloudEvent::None
|
||||
}
|
||||
Req::CampaignDelete | Req::CampaignPublish => {
|
||||
if !ok {
|
||||
return self.campaign_failed(res.status);
|
||||
}
|
||||
self.refresh_campaigns();
|
||||
CloudEvent::None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// campaigns on the server (the campaign editor UI drives these)
|
||||
#[allow(dead_code)]
|
||||
impl Cloud {
|
||||
// ---------------- campaigns. Every call returns false while another request is running.
|
||||
|
||||
pub fn refresh_campaigns(&mut self) -> bool {
|
||||
self.start("GET", "campaigns", &[], Req::CampaignList)
|
||||
}
|
||||
|
||||
// the campaign JSON arrives later through take_loaded_campaign()
|
||||
pub fn load_campaign(&mut self, id: &str) -> bool {
|
||||
self.start("GET", &format!("campaigns/{}", encode(id)), &[], Req::CampaignLoad)
|
||||
}
|
||||
|
||||
// id = None stores a new campaign, Some(id) overwrites an own one;
|
||||
// the stored entry (with its id) arrives through take_saved_campaign()
|
||||
pub fn save_campaign(&mut self, id: Option<&str>, name: &str, json: &str) -> bool {
|
||||
match id {
|
||||
None => self.start("POST", &format!("campaigns?name={}", encode(name)), json.as_bytes(), Req::CampaignSave),
|
||||
Some(id) => self.start("PUT", &format!("campaigns/{}?name={}", encode(id), encode(name)), json.as_bytes(), Req::CampaignSave),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delete_campaign(&mut self, id: &str) -> bool {
|
||||
self.start("DELETE", &format!("campaigns/{}", encode(id)), &[], Req::CampaignDelete)
|
||||
}
|
||||
|
||||
pub fn publish_campaign(&mut self, id: &str, published: bool) -> bool {
|
||||
let body = serde_json::json!({ "published": published }).to_string();
|
||||
self.start("POST", &format!("campaigns/{}/publish", encode(id)), body.as_bytes(), Req::CampaignPublish)
|
||||
}
|
||||
|
||||
pub fn refresh_public_campaigns(&mut self) -> bool {
|
||||
self.start("GET", "public/campaigns", &[], Req::PublicList)
|
||||
}
|
||||
|
||||
// the campaign JSON arrives later through take_loaded_campaign()
|
||||
pub fn load_public_campaign(&mut self, owner: &str, id: &str) -> bool {
|
||||
self.start("GET", &format!("public/campaigns/{}/{}", encode(owner), encode(id)), &[], Req::PublicLoad)
|
||||
}
|
||||
|
||||
// JSON text of a campaign loaded by load_campaign() or load_public_campaign()
|
||||
pub fn take_loaded_campaign(&mut self) -> Option<String> {
|
||||
self.loaded_campaign.take()
|
||||
}
|
||||
|
||||
// the entry of the campaign stored by save_campaign()
|
||||
pub fn take_saved_campaign(&mut self) -> Option<CampaignInfo> {
|
||||
self.saved_campaign.take()
|
||||
}
|
||||
|
||||
pub fn take_campaign_error(&mut self) -> Option<&'static str> {
|
||||
self.campaign_error.take()
|
||||
}
|
||||
|
||||
fn campaign_failed(&mut self, status: i32) -> CloudEvent {
|
||||
let key = error_key(status);
|
||||
self.campaign_error = Some(key);
|
||||
CloudEvent::Error(key)
|
||||
}
|
||||
}
|
||||
|
||||
// (year, month, day, hour, minute) in UTC from unix seconds (Howard Hinnant's civil date algorithm)
|
||||
pub fn civil(secs: i64) -> (i64, i64, i64, i64, i64) {
|
||||
let days = secs.div_euclid(86400);
|
||||
@@ -374,4 +534,12 @@ mod tests {
|
||||
assert_eq!(duration_string(3723.0), "1:02:03");
|
||||
assert_eq!(encode("Wave 3 – ä"), "Wave%203%20%E2%80%93%20%C3%A4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn campaign_lists_parse() {
|
||||
let own: Vec<CampaignInfo> = serde_json::from_str(r#"[{"id":"17-ab","name":"C","updated":5,"published":true,"size":9}]"#).unwrap();
|
||||
assert_eq!((own[0].id.as_str(), own[0].published, own[0].size), ("17-ab", true, 9));
|
||||
let public: Vec<PublicCampaignInfo> = serde_json::from_str(r#"[{"owner":"alice","id":"1-0","name":"P","updated":7}]"#).unwrap();
|
||||
assert_eq!((public[0].owner.as_str(), public[0].updated), ("alice", 7));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user