Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions native/companion/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ name = "fic"
path = "src/main.rs"

[dependencies]
form_urlencoded = "1.2.2"
foxhole-inventory-shared = { path = "../shared" }
image = "0.25.9"
serde_json = "1.0.149"
Expand Down
44 changes: 41 additions & 3 deletions native/companion/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use std::fs::File;
use std::io::{self, Read};

use form_urlencoded;
use tiny_http::{Header, Method, Response, Server, StatusCode};

use fis::Catalog;
use fis::Classifier;
use fis::Ocr;
use fis::extract_stockpile;
Expand Down Expand Up @@ -32,6 +34,7 @@ static ICON_CLASS_NAMES_JSON: &str =
include_str!(fir_path!(foxhole / "classifier/class_names.json"));
static QUANTITY_CLASS_NAMES_JSON: &str =
include_str!(fir_path!(includes / "quantities/class_names.json"));
static CATALOG_JSON: &str = include_str!(fir_path!(foxhole / "catalog.json"));

// Helper: read bytes from a filename, where "-" means stdin.
fn read_input_bytes(filename: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -99,34 +102,69 @@ fn command_http_server(args: Vec<String>) -> Result<(), Box<dyn std::error::Erro

let (ocr, mut icon_classifier, mut quantity_classifier) = get_classifiers()?;
let content_type_header = Header::from_bytes("Content-Type", "application/json").unwrap();
let catalog = Catalog::new_from_json(CATALOG_JSON)?;

for mut request in server.incoming_requests() {
let start = std::time::Instant::now();
let method = request.method().to_string();
let method = request.method().clone();
let url = request.url().to_string();
let remote_addr = request
.remote_addr()
.map(|a| a.to_string())
.unwrap_or_default();

if request.method() != &Method::Post || request.url() != "/extract" {
let (path, query) = match url.split_once('?') {
Some((p, q)) => (p, Some(q)),
None => (url.as_str(), None),
};

if method != Method::Post || path != "/extract" {
eprintln!("{remote_addr} {method} {url} 404 {:?}", start.elapsed());
let r = Response::from_string("Not Found").with_status_code(StatusCode(404));
let _ = request.respond(r);
continue;
}

let Ok(includes) = query
.into_iter()
.flat_map(|q| form_urlencoded::parse(q.as_bytes()))
.filter(|(key, _)| key == "include")
.flat_map(|(_, value)| value.split(',').map(String::from).collect::<Vec<_>>())
.map(|value| {
value
.strip_prefix('.')
.ok_or(())
.map(|s| s.split('.').map(String::from).collect())
})
.collect::<Result<Vec<Vec<String>>, _>>()
else {
eprintln!("{remote_addr} {method} {url} 400 {:?}", start.elapsed());
let r = Response::from_string("Bad Request").with_status_code(StatusCode(400));
let _ = request.respond(r);
continue;
};

let result = (|| -> Result<String, Box<dyn std::error::Error>> {
let mut body = Vec::new();
request.as_reader().read_to_end(&mut body)?;
let image = image::load_from_memory(&body)?.into_rgba8();
let stockpile = extract_stockpile(
let mut stockpile = extract_stockpile(
&image,
image.width() as usize,
&ocr,
&mut icon_classifier,
&mut quantity_classifier,
)?;
if !includes.is_empty() {
if let Some(stockpile) = &mut stockpile {
for entry in &mut stockpile.contents {
let mut attributes = entry.attributes.get_or_insert_default();
let code_name = &entry.icon.code_name.as_ref().ok_or("Missing CodeName")?;
let _ = catalog.merge_attributes(code_name, &includes, &mut attributes);
}
}
}

Ok(serde_json::to_string_pretty(&stockpile)?)
})();

Expand Down
53 changes: 53 additions & 0 deletions native/shared/src/catalog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use serde_json::{Value, json};

use crate::types::JsonObject;

pub struct Catalog {
entries: Vec<JsonObject>,
}

impl Catalog {
pub fn new_from_json(json: &str) -> Result<Self, Box<dyn std::error::Error>> {
let entries: Vec<JsonObject> = serde_json::from_str(json)?;
Ok(Self { entries })
}

fn get_path<'a>(entry: &'a JsonObject, path: &[String]) -> Option<&'a Value> {
let (first, rest) = path.split_first()?;
let mut current = entry.get(first)?;
for key in rest {
current = current.get(key)?;
}
Some(current)
}

pub fn merge_attributes(
&self,
code_name: &str,
paths: &[Vec<String>],
attributes: &mut JsonObject,
) -> Result<(), Box<dyn std::error::Error>> {
let entry = self
.entries
.iter()
.find(|entry| entry.get("CodeName").and_then(Value::as_str) == Some(code_name))
.ok_or("No matching catalog item found.")?;

for path in paths {
let (last_key, object_keys) = path.split_last().ok_or("Received empty path")?;
if let Some(attribute_value) = Self::get_path(entry, path) {
let mut parent: &mut JsonObject = attributes;
for key in object_keys.iter() {
parent = parent
.entry(key)
.or_insert(json!({}))
.as_object_mut()
.ok_or(format!("Expected nested value to be an object ({key})"))?;
}
parent.insert(last_key.to_string(), attribute_value.clone());
}
}

Ok(())
}
}
9 changes: 8 additions & 1 deletion native/shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ pub mod types;
mod ocr;
pub use ocr::Ocr;

pub mod catalog;
pub use catalog::Catalog;

mod classifier;
pub use classifier::Classifier;

Expand Down Expand Up @@ -118,7 +121,11 @@ pub fn extract_stockpile(
Ok(q) => q,
Err(_) => return Ok(None),
};
classified_contents.push(Entry { icon, quantity });
classified_contents.push(Entry {
icon,
quantity,
attributes: None,
});
}

stockpile.contents = classified_contents;
Expand Down
1 change: 1 addition & 0 deletions native/shared/src/slicer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub fn slice_stockpile(rgba: &[u8], width: usize) -> Option<Stockpile> {
contents.push(Entry {
icon: Icon::from(icon_bounds.offset(body.x, body.y)),
quantity: Quantity::from(quantity_bounds.offset(body.x, body.y)),
attributes: None,
});
}

Expand Down
3 changes: 3 additions & 0 deletions native/shared/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use serde::Serialize;

pub type JsonObject = serde_json::Map<String, serde_json::Value>;

#[derive(Debug, Default, Serialize)]
pub struct Bounds {
pub x: usize,
Expand Down Expand Up @@ -76,6 +78,7 @@ impl_from_bounds!(Icon, Quantity);
pub struct Entry {
pub icon: Icon,
pub quantity: Quantity,
pub attributes: Option<JsonObject>,
}

#[derive(Debug, Serialize)]
Expand Down