Skip to content

Commit

Permalink
Add support for enabling and disabling DLC #8
Browse files Browse the repository at this point in the history
  • Loading branch information
circlesabound committed Oct 20, 2024
1 parent cb75ffc commit 7187146
Show file tree
Hide file tree
Showing 17 changed files with 483 additions and 24 deletions.
32 changes: 32 additions & 0 deletions openapi/mgmt-server-rest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,27 @@ paths:
responses:
'200':
description: Ok
/server/mods/dlc:
get:
summary: Gets status of official DLC mods
responses:
'200':
description: A JSON dictionary with entries indicating whether each official DLC is enabled or disabled
content:
application/json:
schema:
$ref: '#/components/schemas/ServerModDlcList'
put:
summary: Pushes status of official DLC mods to enable on the server
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ServerModDlcList'
responses:
'200':
description: Ok
/server/mods/list:
get:
summary: Gets a list of mods installed on the Factorio server.
Expand Down Expand Up @@ -727,6 +748,17 @@ components:
type: integer
maximum_segment_size_peer_count:
type: integer
ServerModDlcList:
type: array
items:
$ref: '#/components/schemas/DlcName'
DlcName:
type: string
enum:
- "base"
- "space-age"
- "elevated-rails"
- "quality"
ServerModList:
type: array
items:
Expand Down
1 change: 1 addition & 0 deletions src/agent/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub enum Error {
ProcessSignalError(nix::Error),

// Mods
MalformedModList,
ModNotFound {
mod_name: String,
mod_version: String,
Expand Down
86 changes: 82 additions & 4 deletions src/agent/main.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#![feature(trait_alias)]

use std::{
convert::{TryFrom, TryInto},
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
time::Duration,
collections::HashSet, convert::{TryFrom, TryInto}, net::{IpAddr, Ipv4Addr, SocketAddr}, sync::Arc, time::Duration
};

use crate::{
Expand Down Expand Up @@ -342,6 +339,14 @@ impl AgentController {
// **************
// Mod management
// **************
AgentRequest::ModDlcsGet => {
self.mod_dlcs_get(operation_id).await;
}

AgentRequest::ModDlcsSet(dlcs) => {
self.mod_dlcs_set(dlcs.into_iter().collect(), operation_id).await;
}

AgentRequest::ModListGet => {
self.mod_list_get(operation_id).await;
}
Expand Down Expand Up @@ -1146,6 +1151,79 @@ impl AgentController {
}
}

async fn mod_dlcs_get(&self, operation_id: OperationId) {
match ModManager::read_or_apply_default().await {
Ok(m) => {
self.reply_success(AgentOutMessage::DlcList(m.dlcs.into_iter().collect()), operation_id)
.await;
}
Err(e) => {
self.reply_failed(
AgentOutMessage::Error(format!("Failed to get DLC: {:?}", e)),
operation_id,
)
.await;
}
}
}

async fn mod_dlcs_set(&self, dlcs: HashSet<Dlc>, operation_id: OperationId) {
// validate that base is included
if !dlcs.contains(&Dlc::Base) {
self.reply_failed(AgentOutMessage::Error("Failed to set DLC: list must include base".to_owned()), operation_id).await;
return;
}

if let Ok(vm) =
tokio::time::timeout(Duration::from_millis(250), self.version_manager.read()).await
{
match vm.versions.values().next() {
None => {
self.reply_failed(AgentOutMessage::NotInstalled, operation_id)
.await;
}
Some(v) => {
// validate if non-base DLC, then version > 1
if dlcs.len() > 1 && v.version.starts_with("1") {
self.reply_failed(
AgentOutMessage::Error(format!("Failed to set DLC: list includes non-base DLC which installed game version {} does not support", v.version))
, operation_id
)
.await;
} else {
match ModManager::read_or_apply_default().await {
Ok(mut m) => {
m.dlcs = dlcs;
if let Err(e) = m.apply_metadata_only().await {
self.reply_failed(
AgentOutMessage::Error(format!(
"Unable to write mod list when setting DLC: {:?}",
e
)),
operation_id,
)
.await;
} else {
self.reply_success(AgentOutMessage::Ok, operation_id).await;
}
},
Err(e) => {
self.reply_failed(
AgentOutMessage::Error(format!("Failed to initialise mod manager: {:?}", e)),
operation_id,
)
.await;
},
}
}
}
}
} else {
self.reply_failed(AgentOutMessage::ConflictingOperation, operation_id)
.await;
}
}

async fn mod_list_get(&self, operation_id: OperationId) {
match ModManager::read_or_apply_default().await {
Ok(m) => {
Expand Down
97 changes: 84 additions & 13 deletions src/agent/server/mods.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use std::{
collections::HashSet,
convert::{TryFrom, TryInto},
path::{Path, PathBuf},
borrow::Borrow, collections::HashSet, convert::{TryFrom, TryInto}, path::{Path, PathBuf}, str::FromStr
};

use factorio_file_parser::ModSettings;
Expand All @@ -27,6 +25,7 @@ lazy_static! {
}

pub struct ModManager {
pub dlcs: HashSet<Dlc>,
pub mods: Vec<Mod>,
pub settings: Option<ModSettings>,
pub path: PathBuf,
Expand All @@ -37,8 +36,21 @@ impl ModManager {
if !MOD_DIR.is_dir() {
Ok(None)
} else {
// Don't bother with mod list, directly parse the mod zips
// Read DLC state from mod-list.json
let mod_list_json = fs::read_to_string(&*MOD_LIST_PATH).await?;
let mod_list: ModList = serde_json::from_str(&mod_list_json)?;
let dlcs = match ModManager::read_dlcs_from_mod_list(&mod_list) {
Ok(dlcs) => dlcs,
Err(e) => {
error!("Error reading mod-list file: {:?}. Assuming base mod enabled with no other DLC", e);
HashSet::from([Dlc::Base])
},
};

// For actual mods, don't bother with mod list, directly parse the mod zips
// No support for "installed but disabled" mods
// TODO we're reading the mod-list.json now anyway, use that instead of parsing
// TODO also mod-list.json supports versioning now
let mut mod_zip_names = vec![];
let mut entries = fs::read_dir(&*MOD_DIR).await?;
while let Some(entry) = entries.next_entry().await? {
Expand Down Expand Up @@ -77,6 +89,7 @@ impl ModManager {
}

Ok(Some(ModManager {
dlcs,
mods,
settings,
path: MOD_DIR.clone(),
Expand All @@ -91,6 +104,7 @@ impl ModManager {
info!("Generating mod dir and contents using defaults");

let ret = ModManager {
dlcs: HashSet::from([Dlc::Base]),
mods: vec![],
settings: None,
path: MOD_DIR.clone(),
Expand Down Expand Up @@ -173,8 +187,8 @@ impl ModManager {
pub async fn apply_metadata_only(&self) -> Result<()> {
fs::create_dir_all(&*MOD_DIR).await?;

// ModList impl automatically adds the base mod to its internal structure
let mod_list_json = serde_json::to_string(&ModList::from(self.mods.clone()))?;
// ModList impl of From incorporates DLC into its list
let mod_list_json = serde_json::to_string(&ModList::from(self))?;
fs::write(&*MOD_LIST_PATH, mod_list_json).await?;

if let Some(settings) = self.settings.clone() {
Expand Down Expand Up @@ -255,6 +269,25 @@ impl ModManager {
delete: mods_to_delete,
}
}

fn read_dlcs_from_mod_list(mod_list: impl Borrow<ModList>) -> Result<HashSet<Dlc>> {
let mut dlcs = HashSet::new();
for mod_list_elem in mod_list.borrow().mods.iter() {
if let Ok(dlc_name) = Dlc::from_str(&mod_list_elem.name) {
if mod_list_elem.enabled {
dlcs.insert(dlc_name);
}
}
}

// sanity check
if !dlcs.contains(&Dlc::Base) {
error!("Error reading DLCs from mod-list.json - no base mod found");
Err(Error::MalformedModList)
} else {
Ok(dlcs)
}
}
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
Expand Down Expand Up @@ -298,21 +331,24 @@ impl Default for ModList {
mods: vec![ModListElem {
name: "base".to_owned(),
enabled: true,
version: None,
}],
}
}
}

impl From<Vec<Mod>> for ModList {
fn from(v: Vec<Mod>) -> Self {
impl<T: Borrow<ModManager>> From<T> for ModList {
fn from(m: T) -> Self {
// Assume base is always enabled
let mut elems = vec![ModListElem {
name: "base".to_owned(),
let mut elems: Vec<_> = m.borrow().dlcs.iter().map(|d| ModListElem {
name: (*d).to_string(),
enabled: true,
}];
elems.extend(v.into_iter().map(|m| ModListElem {
name: m.name,
version: None,
}).collect();
elems.extend(m.borrow().mods.iter().map(|m| ModListElem {
name: m.name.clone(),
enabled: true,
version: Some(m.version.clone()),
}));
ModList { mods: elems }
}
Expand All @@ -322,6 +358,8 @@ impl From<Vec<Mod>> for ModList {
struct ModListElem {
name: String,
enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
}

#[cfg(test)]
Expand All @@ -331,6 +369,39 @@ mod tests {
use std::collections::HashMap;

use fctrl::util;
use serde_json::json;

#[test]
fn can_parse_valid_dlc() -> std::result::Result<(), Box<dyn std::error::Error>> {
util::testing::logger_init();

let mod_list: ModList = serde_json::from_value(json!({
"mods": [
{
"name": "base",
"enabled": true
},
{
"name": "space-age",
"enabled": true
},
{
"name": "elevated-rails",
"enabled": true
},
{
"name": "quality",
"enabled": true
}
]
}))?;
let dlcs = ModManager::read_dlcs_from_mod_list(mod_list)?;
assert!(dlcs.contains(&Dlc::Base));
assert!(dlcs.contains(&Dlc::SpaceAge));
assert!(dlcs.contains(&Dlc::ElevatedRails));
assert!(dlcs.contains(&Dlc::Quality));
Ok(())
}

#[test]
fn can_parse_valid_mod_filenames() -> std::result::Result<(), Box<dyn std::error::Error>> {
Expand Down
25 changes: 24 additions & 1 deletion src/mgmt-server/clients.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::{
collections::HashMap, pin::Pin, str::FromStr, sync::{
collections::{HashMap, HashSet}, pin::Pin, str::FromStr, sync::{
atomic::{AtomicBool, AtomicU8, Ordering},
Arc,
}, time::Duration
Expand Down Expand Up @@ -201,6 +201,28 @@ impl AgentApiClient {
.await
}

pub async fn mod_dlcs_get(&self) -> Result<HashSet<Dlc>> {
let request = AgentRequest::ModDlcsGet;
let (_id, sub) = self.send_request_and_subscribe(request).await?;

response_or_timeout(sub, Duration::from_millis(500), |r| match r.content {
AgentOutMessage::DlcList(mods) => Ok(mods.into_iter().collect()),
m => Err(default_message_handler(m)),
})
.await
}

pub async fn mod_dlcs_set(&self, dlcs: HashSet<Dlc>) -> Result<()> {
let request = AgentRequest::ModDlcsSet(dlcs.into_iter().collect());
let (_id, sub) = self.send_request_and_subscribe(request).await?;

response_or_timeout(sub, Duration::from_millis(500), |r| match r.content {
AgentOutMessage::Ok => Ok(()),
m => Err(default_message_handler(m)),
})
.await
}

pub async fn mod_list_get(&self) -> Result<Vec<ModObject>> {
let request = AgentRequest::ModListGet;
let (_id, sub) = self.send_request_and_subscribe(request).await?;
Expand Down Expand Up @@ -463,6 +485,7 @@ fn default_message_handler(agent_message: AgentOutMessage) -> Error {
| AgentOutMessage::ConfigSecrets(_)
| AgentOutMessage::ConfigServerSettings(_)
| AgentOutMessage::ConfigWhiteList(_)
| AgentOutMessage::DlcList(_)
| AgentOutMessage::FactorioVersion(_)
| AgentOutMessage::Message(_)
| AgentOutMessage::ModsList(_)
Expand Down
2 changes: 2 additions & 0 deletions src/mgmt-server/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
routes::server::put_secrets,
routes::server::get_server_settings,
routes::server::put_server_settings,
routes::server::get_dlcs,
routes::server::set_dlcs,
routes::server::get_mods_list,
routes::server::apply_mods_list,
routes::server::get_mod_settings,
Expand Down
Loading

0 comments on commit 7187146

Please sign in to comment.