diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 0000000..d5e9799 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,35 @@ +name: Testing + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + testing: + name: Testing + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install Rust toolchain + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + + - name: Install wasm-pack + run: | + cargo install wasm-pack + + - name: Build library + run: | + wasm-pack build --release --target web + + - name: Check coding style + run: | + cargo fmt --check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e8ac1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target/ +__pycache__/ +Cargo.lock +/pkg/ diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e9a59a5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "dogma-engine" +version = "0.0.0-git" +authors = ["Patric Stout "] +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +console_error_panic_hook = "0.1" +serde = { version = "1.0", features = ["derive"] } +serde_repr = "0.1" +serde-wasm-bindgen = "0.4" +strum = "0.25" +strum_macros = "0.25" +wasm-bindgen = "0.2" + +[profile.release] +opt-level = "s" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..68b1edc --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2023 TrueBrain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a907169 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# EVEShip.fit's Dogma Engine + +This library calculates accurately statistics of an EVE Online ship fit. + +The input are several data-files provided by EVE Online, together with a ship fit. +The output are all the Dogma attributes, containing all the details of the ship. + +## Implementation + +This Dogma engine implements a multi-pass approach. + +- [pass 1](./src/calculate/pass_1.rs): collect all the Dogma attributes of the hull and modules. +- [pass 2](./src/calculate/pass_2.rs): collect all the Dogma effects of the hull and modules. +- [pass 3](./src/calculate/pass_3.rs): apply all the Dogma effects to the hull/modules, calculating the actual Dogma attribute values. +- [pass 4](./src/calculate/pass_4.rs): augment the Dogma attributes with EVEShip.fit specific attributes. + +## EVEShip.fit's specific attributes + +`Pass 4` create Dogma attributes that do not exist in-game, but are calculated based on other Dogma attributes. +To make rendering a fit easier, these are calculated by this library, and presented as new Dogma attributes. + +Their identifier is always a negative value, to visually separate them. + +- `-1`: align-time +- `-2`: scan strength +- `-3`: CPU usage +- `-4`: PG usage + +## Integration + +### Javascript (WebAssembly) + +The primary goal of this library is to build a WebAssembly variant that can easily be used in the browser. +This means that there is no need for a server-component, and everything can be calculated in the browser. + +This is done with [wasm-pack](https://rustwasm.github.io/wasm-pack/). + +To make sure that EVEShip.fit is as fast as possible, all data-files are read by Javascript, and made available to this library by callbacks. +Transferring all data-files from Javascript to Rust is simply too expensive. + +In result, Javascript needs to have the following functions defined: + +- `get_dogma_attributes(type_id)` - To get a list of all Dogma attributes for a given item. +- `get_dogma_attribute(attribute_id)` - To get all the details of a single Dogma attribute. +- `get_dogma_effects(type_id)` - To get a list of all Dogma effects for a given item. +- `get_dogma_effect(effect_id)` - To get all the details of a single Dogma effect. +- `get_type_id(type_id)` - To get all the details of a single item. + +The returning value should be a Javascript object. +The fields are defined in in [data_types.rs](./src/data_types.rs). diff --git a/src/calculate.rs b/src/calculate.rs new file mode 100644 index 0000000..fc9df68 --- /dev/null +++ b/src/calculate.rs @@ -0,0 +1,53 @@ +use serde::Serialize; +use std::collections::BTreeMap; + +mod info; +mod item; +mod pass_1; +mod pass_2; +mod pass_3; +mod pass_4; + +use info::Info; +use item::Item; + +#[derive(Serialize, Debug)] +pub struct Ship { + pub hull: Item, + pub items: Vec, + + #[serde(skip_serializing)] + pub skills: Vec, + #[serde(skip_serializing)] + pub char: Item, + #[serde(skip_serializing)] + pub structure: Item, +} + +impl Ship { + pub fn new(ship_id: i32) -> Ship { + Ship { + hull: Item::new(ship_id), + items: Vec::new(), + skills: Vec::new(), + char: Item::new(0), + structure: Item::new(0), + } + } +} + +trait Pass { + fn pass(info: &Info, ship: &mut Ship); +} + +pub fn calculate(ship_layout: &super::data_types::ShipLayout, skills: &BTreeMap) -> Ship { + let info = Info::new(ship_layout, skills); + let mut ship = Ship::new(info.ship_layout.ship_id); + + pass_1::PassOne::pass(&info, &mut ship); + pass_2::PassTwo::pass(&info, &mut ship); + pass_3::PassThree::pass(&info, &mut ship); + pass_4::PassFour::pass(&info, &mut ship); + + ship +} diff --git a/src/calculate/info.rs b/src/calculate/info.rs new file mode 100644 index 0000000..6f68c5a --- /dev/null +++ b/src/calculate/info.rs @@ -0,0 +1,65 @@ +use serde_wasm_bindgen; +use std::collections::BTreeMap; +use wasm_bindgen::prelude::*; + +use super::super::data_types; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = window)] + fn get_dogma_attributes(type_id: i32) -> JsValue; + + #[wasm_bindgen(js_namespace = window)] + fn get_dogma_attribute(attribute_id: i32) -> JsValue; + + #[wasm_bindgen(js_namespace = window)] + fn get_dogma_effects(type_id: i32) -> JsValue; + + #[wasm_bindgen(js_namespace = window)] + fn get_dogma_effect(effect_id: i32) -> JsValue; + + #[wasm_bindgen(js_namespace = window)] + fn get_type_id(type_id: i32) -> JsValue; +} + +pub struct Info<'a> { + pub ship_layout: &'a data_types::ShipLayout, + pub skills: &'a BTreeMap, +} + +impl Info<'_> { + pub fn new<'a>( + ship_layout: &'a data_types::ShipLayout, + skills: &'a BTreeMap, + ) -> Info<'a> { + Info { + ship_layout, + skills, + } + } + + pub fn get_dogma_attributes(&self, type_id: i32) -> Vec { + let js = get_dogma_attributes(type_id); + serde_wasm_bindgen::from_value(js).unwrap() + } + + pub fn get_dogma_attribute(&self, attribute_id: i32) -> data_types::DogmaAttribute { + let js = get_dogma_attribute(attribute_id); + serde_wasm_bindgen::from_value(js).unwrap() + } + + pub fn get_dogma_effects(&self, type_id: i32) -> Vec { + let js = get_dogma_effects(type_id); + serde_wasm_bindgen::from_value(js).unwrap() + } + + pub fn get_dogma_effect(&self, effect_id: i32) -> data_types::DogmaEffect { + let js = get_dogma_effect(effect_id); + serde_wasm_bindgen::from_value(js).unwrap() + } + + pub fn get_type_id(&self, type_id: i32) -> data_types::TypeId { + let js = get_type_id(type_id); + serde_wasm_bindgen::from_value(js).unwrap() + } +} diff --git a/src/calculate/item.rs b/src/calculate/item.rs new file mode 100644 index 0000000..22c160e --- /dev/null +++ b/src/calculate/item.rs @@ -0,0 +1,80 @@ +use serde::Serialize; +use std::collections::BTreeMap; +use strum_macros::EnumIter; + +#[derive(Serialize, Debug, Copy, Clone, PartialEq, Eq)] +pub enum EffectCategory { + Passive, + Active, + Target, + Area, + Online, + Overload, + Dungeon, + System, +} + +#[derive(Serialize, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, EnumIter)] +pub enum EffectOperator { + PreAssign, + PreMul, + PreDiv, + ModAdd, + ModSub, + PostMul, + PostDiv, + PostPercent, + PostAssignment, +} + +#[derive(Serialize, Debug, Copy, Clone)] +pub enum Object { + Ship, + Item(usize), + Skill(usize), + Char, + Structure, +} + +#[derive(Serialize, Debug)] +pub struct Effect { + pub operator: EffectOperator, + pub penalty: bool, + pub source: Object, + pub source_category: EffectCategory, + pub source_attribute_id: i32, +} + +#[derive(Serialize, Debug)] +pub struct Attribute { + pub base_value: f32, + pub value: Option, + pub effects: Vec, +} + +#[derive(Serialize, Debug)] +pub struct Item { + pub type_id: i32, + pub attributes: BTreeMap, + pub effects: Vec, +} + +impl Attribute { + pub fn new(value: f32) -> Attribute { + Attribute { + base_value: value, + value: None, + effects: Vec::new(), + } + } +} + +impl Item { + pub fn new(type_id: i32) -> Item { + Item { + type_id, + attributes: BTreeMap::new(), + effects: Vec::new(), + } + } +} diff --git a/src/calculate/pass_1.rs b/src/calculate/pass_1.rs new file mode 100644 index 0000000..5b0adfe --- /dev/null +++ b/src/calculate/pass_1.rs @@ -0,0 +1,56 @@ +use super::item::{Attribute, Item}; +use super::{Info, Pass, Ship}; + +const ATTRIBUTE_MASS_ID: i32 = 4; +const ATTRIBUTE_CAPACITY_ID: i32 = 38; +const ATTRIBUTE_VOLUME_ID: i32 = 161; +const ATTRIBUTE_RADIUS_ID: i32 = 162; +const ATTRIBUTE_SKILL_LEVEL_ID: i32 = 280; + +pub struct PassOne {} + +impl Item { + pub fn set_attribute(&mut self, attribute_id: i32, value: f32) { + self.attributes.insert(attribute_id, Attribute::new(value)); + } + + fn set_attributes(&mut self, info: &Info) { + for dogma_attribute in info.get_dogma_attributes(self.type_id) { + self.set_attribute(dogma_attribute.attributeID, dogma_attribute.value); + } + } +} + +impl Pass for PassOne { + fn pass(info: &Info, ship: &mut Ship) { + ship.hull.set_attributes(info); + + /* Some attributes of ships come from the TypeID information. */ + let type_id = info.get_type_id(info.ship_layout.ship_id); + ship.hull + .set_attribute(ATTRIBUTE_MASS_ID, type_id.mass.unwrap()); + ship.hull + .set_attribute(ATTRIBUTE_CAPACITY_ID, type_id.capacity.unwrap()); + ship.hull + .set_attribute(ATTRIBUTE_VOLUME_ID, type_id.volume.unwrap()); + ship.hull + .set_attribute(ATTRIBUTE_RADIUS_ID, type_id.radius.unwrap()); + + for (skill_id, skill_level) in info.skills { + let mut skill = Item::new(*skill_id); + + skill.set_attributes(info); + skill.set_attribute(ATTRIBUTE_SKILL_LEVEL_ID, *skill_level as f32); + + ship.skills.push(skill); + } + + for item_id in &info.ship_layout.items { + let mut item = Item::new(*item_id); + + item.set_attributes(info); + + ship.items.push(item); + } + } +} diff --git a/src/calculate/pass_2.rs b/src/calculate/pass_2.rs new file mode 100644 index 0000000..cb27f02 --- /dev/null +++ b/src/calculate/pass_2.rs @@ -0,0 +1,248 @@ +use crate::data_types::{DogmaEffectModifierInfoDomain, DogmaEffectModifierInfoFunc}; + +use super::item::{Effect, EffectCategory, EffectOperator, Item, Object}; +use super::{Info, Pass, Ship}; + +/** AttributeIDs for requiredSkill1, requiredSkill2, .. */ +const ATTRIBUTE_SKILLS: [i32; 6] = [182, 183, 184, 1285, 1289, 1290]; +/** Categories of the effect source which are exempt of stacking penalty. + * Ship (6), Charge (8), Skill (16), Implant (20) and Subsystem (32) */ +const EXEMPT_PENALTY_CATEGORY_IDS: [i32; 5] = [6, 8, 16, 20, 32]; + +pub struct PassTwo {} + +#[derive(Debug)] +enum Modifier { + LocationRequiredSkillModifier(i32), + LocationGroupModifier(i32), + LocationModifier(), + OwnerRequiredSkillModifier(i32), + ItemModifier(), +} + +#[derive(Debug)] +struct Pass2Effect { + modifier: Modifier, + operator: EffectOperator, + source: Object, + source_category: EffectCategory, + source_attribute_id: i32, + target: Object, + target_attribute_id: i32, +} + +fn get_modifier_func( + func: DogmaEffectModifierInfoFunc, + skill_type_id: Option, + group_id: Option, +) -> Modifier { + match func { + DogmaEffectModifierInfoFunc::LocationRequiredSkillModifier => { + Modifier::LocationRequiredSkillModifier(skill_type_id.unwrap()) + } + DogmaEffectModifierInfoFunc::LocationGroupModifier => { + Modifier::LocationGroupModifier(group_id.unwrap()) + } + DogmaEffectModifierInfoFunc::LocationModifier => Modifier::LocationModifier(), + DogmaEffectModifierInfoFunc::ItemModifier => Modifier::ItemModifier(), + DogmaEffectModifierInfoFunc::OwnerRequiredSkillModifier => { + Modifier::OwnerRequiredSkillModifier(skill_type_id.unwrap()) + } + _ => panic!("Unknown modifier function: {:?}", func), + } +} + +fn get_target_object(domain: DogmaEffectModifierInfoDomain, origin: Object) -> Object { + match domain { + DogmaEffectModifierInfoDomain::ShipID => Object::Ship, + DogmaEffectModifierInfoDomain::CharID => Object::Char, + DogmaEffectModifierInfoDomain::OtherID => Object::Char, // TODO -- This is incorrect + DogmaEffectModifierInfoDomain::StructureID => Object::Structure, + DogmaEffectModifierInfoDomain::ItemID => origin, + _ => panic!("Unknown domain: {:?}", domain), + } +} + +fn get_effect_category(category: i32) -> EffectCategory { + match category { + 0 => EffectCategory::Passive, + 1 => EffectCategory::Active, + 2 => EffectCategory::Target, + 3 => EffectCategory::Area, + 4 => EffectCategory::Online, + 5 => EffectCategory::Overload, + 6 => EffectCategory::Dungeon, + 7 => EffectCategory::System, + _ => panic!("Unknown effect category: {}", category), + } +} + +fn get_effect_operator(operation: i32) -> EffectOperator { + match operation { + -1 => EffectOperator::PreAssign, + 0 => EffectOperator::PreMul, + 1 => EffectOperator::PreDiv, + 2 => EffectOperator::ModAdd, + 3 => EffectOperator::ModSub, + 4 => EffectOperator::PostMul, + 5 => EffectOperator::PostDiv, + 6 => EffectOperator::PostPercent, + 7 => EffectOperator::PostAssignment, + _ => panic!("Unknown effect operation: {}", operation), + } +} + +impl Item { + fn add_effect( + &mut self, + info: &Info, + attribute_id: i32, + source_category_id: i32, + effect: &Pass2Effect, + ) { + let attr = info.get_dogma_attribute(attribute_id); + + if !self.attributes.contains_key(&attribute_id) { + self.set_attribute(attribute_id, attr.defaultValue); + } + + /* Penalties are only count when an attribute is not stackable and when the item is not in the exempt category. */ + let penalty = !attr.stackable && !EXEMPT_PENALTY_CATEGORY_IDS.contains(&source_category_id); + + let attribute = self.attributes.get_mut(&attribute_id).unwrap(); + attribute.effects.push(Effect { + operator: effect.operator, + penalty, + source: effect.source, + source_category: effect.source_category, + source_attribute_id: effect.source_attribute_id, + }); + } + + fn collect_effects(&mut self, info: &Info, origin: Object, effects: &mut Vec) { + for dogma_effect in info.get_dogma_effects(self.type_id) { + let type_dogma_effect = info.get_dogma_effect(dogma_effect.effectID); + + if !type_dogma_effect.modifierInfo.is_empty() { + for modifier in type_dogma_effect.modifierInfo { + /* We ignore operator 9 (calculates Skill Level based on Skill Points; irrelevant for fits). */ + if modifier.operation.unwrap() == 9 { + continue; + } + + let effect_modifier = + get_modifier_func(modifier.func, modifier.skillTypeID, modifier.groupID); + let target = get_target_object(modifier.domain, origin); + let category = get_effect_category(type_dogma_effect.effectCategory); + let operator = get_effect_operator(modifier.operation.unwrap()); + + effects.push(Pass2Effect { + modifier: effect_modifier, + operator, + source: origin, + source_category: category, + source_attribute_id: modifier.modifyingAttributeID.unwrap(), + target, + target_attribute_id: modifier.modifiedAttributeID.unwrap(), + }); + } + } else { + self.effects.push(dogma_effect.effectID); + } + } + } +} + +impl Pass for PassTwo { + fn pass(info: &Info, ship: &mut Ship) { + let mut effects = Vec::new(); + + /* Collect all the effects in a single list. */ + ship.hull.collect_effects(info, Object::Ship, &mut effects); + for (index, item) in ship.items.iter_mut().enumerate() { + item.collect_effects(info, Object::Item(index), &mut effects); + } + for (index, skill) in ship.skills.iter_mut().enumerate() { + skill.collect_effects(info, Object::Skill(index), &mut effects); + } + + /* Depending on the modifier, move the effects to the correct attribute. */ + for effect in effects { + let source_type_id = match effect.source { + Object::Ship => info.ship_layout.ship_id, + Object::Item(index) => ship.items[index].type_id, + Object::Skill(index) => ship.skills[index].type_id, + _ => panic!("Unknown source object"), + }; + let category_id = info.get_type_id(source_type_id).categoryID; + + match effect.modifier { + Modifier::ItemModifier() => { + let target = match effect.target { + Object::Ship => &mut ship.hull, + Object::Char => &mut ship.char, + Object::Structure => &mut ship.structure, + Object::Item(index) => &mut ship.items[index], + Object::Skill(index) => &mut ship.skills[index], + }; + + target.add_effect(info, effect.target_attribute_id, category_id, &effect); + } + Modifier::LocationModifier() => { + // TODO + } + Modifier::LocationGroupModifier(group_id) => { + let type_id = info.get_type_id(ship.hull.type_id); + if type_id.groupID == group_id { + ship.hull.add_effect( + info, + effect.target_attribute_id, + category_id, + &effect, + ); + } + + for item in &mut ship.items { + let type_id = info.get_type_id(item.type_id); + + if type_id.groupID == group_id { + item.add_effect(info, effect.target_attribute_id, category_id, &effect); + } + } + } + Modifier::LocationRequiredSkillModifier(skill_type_id) => { + for attribute_skill_id in &ATTRIBUTE_SKILLS { + if ship.hull.attributes.contains_key(attribute_skill_id) + && ship.hull.attributes[attribute_skill_id].base_value + == skill_type_id as f32 + { + ship.hull.add_effect( + info, + effect.target_attribute_id, + category_id, + &effect, + ); + } + + for item in &mut ship.items { + if item.attributes.contains_key(attribute_skill_id) + && item.attributes[attribute_skill_id].base_value + == skill_type_id as f32 + { + item.add_effect( + info, + effect.target_attribute_id, + category_id, + &effect, + ); + } + } + } + } + Modifier::OwnerRequiredSkillModifier(_skill_type_id) => { + // TODO + } + } + } + } +} diff --git a/src/calculate/pass_3.rs b/src/calculate/pass_3.rs new file mode 100644 index 0000000..f9746c0 --- /dev/null +++ b/src/calculate/pass_3.rs @@ -0,0 +1,270 @@ +use std::collections::BTreeMap; +use strum::IntoEnumIterator; + +use super::item::{Attribute, EffectCategory, EffectOperator, Item, Object}; +use super::{Info, Pass, Ship}; + +/* Penalty factor: 1 / math.exp((1 / 2.67) ** 2) */ +const PENALTY_FACTOR: f32 = 0.8691199808003974; + +const OPERATOR_HAS_PENALTY: [EffectOperator; 5] = [ + EffectOperator::PreMul, + EffectOperator::PostMul, + EffectOperator::PostPercent, + EffectOperator::PreDiv, + EffectOperator::PostDiv, +]; + +pub struct PassThree {} + +struct Cache { + hull: BTreeMap, + items: BTreeMap>, + skills: BTreeMap>, +} + +impl Attribute { + fn calculate_value( + &self, + info: &Info, + ship: &Ship, + categories: &Vec, + cache: &mut Cache, + item: Object, + attribute_id: i32, + ) -> f32 { + if self.value.is_some() { + return self.value.unwrap(); + } + let cache_value = match item { + Object::Ship => cache.hull.get(&attribute_id), + Object::Item(index) => cache.items.get(&index).and_then(|x| x.get(&attribute_id)), + Object::Skill(index) => cache.skills.get(&index).and_then(|x| x.get(&attribute_id)), + _ => None, + }; + if cache_value.is_some() { + return *cache_value.unwrap(); + } + + let mut current_value = self.base_value; + + for operator in EffectOperator::iter() { + let mut values = (Vec::new(), Vec::new(), Vec::new()); + + /* Collect all the values for this operator. */ + for effect in &self.effects { + if effect.operator != operator { + continue; + } + if !categories.contains(&effect.source_category) { + continue; + } + + let source = match effect.source { + Object::Ship => &ship.hull, + Object::Item(index) => &ship.items[index], + Object::Skill(index) => &ship.skills[index], + _ => panic!("Unknown source object"), + }; + + let source_value = match source.attributes.get(&effect.source_attribute_id) { + Some(attribute) => attribute.calculate_value( + info, + ship, + categories, + cache, + effect.source, + effect.source_attribute_id, + ), + None => { + let dogma_attribute = info.get_dogma_attribute(effect.source_attribute_id); + dogma_attribute.defaultValue + } + }; + + /* Simplify the values so we can do the math easier later on. */ + let source_value = match operator { + EffectOperator::PreAssign => source_value, + EffectOperator::PreMul => source_value - 1.0, + EffectOperator::PreDiv => 1.0 / source_value - 1.0, + EffectOperator::ModAdd => source_value, + EffectOperator::ModSub => -source_value, + EffectOperator::PostMul => source_value - 1.0, + EffectOperator::PostDiv => 1.0 / source_value - 1.0, + EffectOperator::PostPercent => source_value / 100.0, + EffectOperator::PostAssignment => source_value, + }; + + /* Check whether stacking penalty counts; negative and positive values have their own penalty. */ + if effect.penalty && OPERATOR_HAS_PENALTY.contains(&effect.operator) { + if source_value < 0.0 { + values.2.push(source_value); + } else { + values.1.push(source_value); + } + } else { + values.0.push(source_value); + } + } + + if values.0.is_empty() && values.1.is_empty() && values.2.is_empty() { + continue; + } + + /* Apply the operator on the values. */ + match operator { + EffectOperator::PreAssign | EffectOperator::PostAssignment => { + let dogma_attribute = info.get_dogma_attribute(attribute_id); + + current_value = if dogma_attribute.highIsGood { + *values + .0 + .iter() + .max_by(|x, y| x.abs().partial_cmp(&y.abs()).unwrap()) + .unwrap() + } else { + *values + .0 + .iter() + .min_by(|x, y| x.abs().partial_cmp(&y.abs()).unwrap()) + .unwrap() + }; + + assert!(values.1.is_empty()); + assert!(values.2.is_empty()); + } + + EffectOperator::PreMul + | EffectOperator::PreDiv + | EffectOperator::PostMul + | EffectOperator::PostDiv + | EffectOperator::PostPercent => { + for value in values.0 { + current_value *= 1.0 + value; + } + + /* Sort values.1 (positive values) based on value; highest value gets lowest penalty. */ + values + .1 + .sort_by(|x, y| x.abs().partial_cmp(&y.abs()).unwrap()); + /* Sort values.2 (negative values) based on value; lowest value gets lowest penalty. */ + values + .2 + .sort_by(|x, y| y.abs().partial_cmp(&x.abs()).unwrap()); + + /* Apply positive stacking penalty. */ + for (index, value) in values.1.iter().enumerate() { + current_value *= 1.0 + value * PENALTY_FACTOR.powi(index.pow(2) as i32); + } + /* Apply negative stacking penalty. */ + for (index, value) in values.2.iter().enumerate() { + current_value *= 1.0 + value * PENALTY_FACTOR.powi(index.pow(2) as i32); + } + } + + EffectOperator::ModAdd | EffectOperator::ModSub => { + for value in values.0 { + current_value += value; + } + + assert!(values.1.is_empty()); + assert!(values.2.is_empty()); + } + } + } + + match item { + Object::Ship => { + cache.hull.insert(attribute_id, current_value); + } + Object::Item(index) => { + if !cache.items.contains_key(&index) { + cache.items.insert(index, BTreeMap::new()); + } + cache + .items + .get_mut(&index) + .unwrap() + .insert(attribute_id, current_value); + } + Object::Skill(index) => { + if !cache.skills.contains_key(&index) { + cache.skills.insert(index, BTreeMap::new()); + } + cache + .skills + .get_mut(&index) + .unwrap() + .insert(attribute_id, current_value); + } + _ => {} + } + + current_value + } +} + +impl Item { + fn calculate_values( + &self, + info: &Info, + ship: &Ship, + categories: &Vec, + cache: &mut Cache, + item: Object, + ) { + for attribute_id in self.attributes.keys() { + self.attributes[&attribute_id].calculate_value( + info, + ship, + &categories, + cache, + item, + *attribute_id, + ); + } + } + + fn store_cached_values(&mut self, info: &Info, cache: &BTreeMap) { + for (attribute_id, value) in cache { + if let Some(attribute) = self.attributes.get_mut(&attribute_id) { + attribute.value = Some(*value); + } else { + let dogma_attribute = info.get_dogma_attribute(*attribute_id); + + let mut attribute = Attribute::new(dogma_attribute.defaultValue); + attribute.value = Some(*value); + + self.attributes.insert(*attribute_id, attribute); + } + } + } +} + +impl Pass for PassThree { + fn pass(info: &Info, ship: &mut Ship) { + let categories = vec![ + EffectCategory::Passive, + EffectCategory::Active, + EffectCategory::Online, + ]; + + let mut cache = Cache { + hull: BTreeMap::new(), + items: BTreeMap::new(), + skills: BTreeMap::new(), + }; + + ship.hull + .calculate_values(info, ship, &categories, &mut cache, Object::Ship); + for (index, item) in ship.items.iter().enumerate() { + item.calculate_values(info, ship, &categories, &mut cache, Object::Item(index)); + } + /* No need to calculate skills; recursively they will resolve what is needed. */ + + ship.hull.store_cached_values(info, &cache.hull); + for (index, item) in ship.items.iter_mut().enumerate() { + item.store_cached_values(info, &cache.items[&index]); + } + } +} diff --git a/src/calculate/pass_4.rs b/src/calculate/pass_4.rs new file mode 100644 index 0000000..2289039 --- /dev/null +++ b/src/calculate/pass_4.rs @@ -0,0 +1,88 @@ +use super::item::Attribute; +use super::{Info, Pass, Ship}; + +pub struct PassFour {} + +fn add_attribute(ship: &mut Ship, attribute_id: i32, base_value: f32, value: f32) { + let mut attribute = Attribute::new(base_value); + attribute.value = Some(value); + ship.hull.attributes.insert(attribute_id, attribute); +} + +fn calculate_align_time(ship: &mut Ship) -> (f32, f32) { + /* Align-time is based on agility (70) and mass (4). */ + + let base_agility = ship.hull.attributes.get(&70).unwrap().base_value; + let base_mass = ship.hull.attributes.get(&4).unwrap().base_value; + let base_align_time = -(0.25 as f32).ln() * base_agility * base_mass / 1000000.0; + + let agility = ship.hull.attributes.get(&70).unwrap().value.unwrap(); + let mass = ship.hull.attributes.get(&4).unwrap().value.unwrap(); + let align_time = -(0.25 as f32).ln() * agility * mass / 1000000.0; + + (base_align_time, align_time) +} + +fn add_scan_strength(ship: &mut Ship) -> (f32, f32) { + /* Scan Strength can be one of 4 values. */ + + let mut base_scan_strength = 0.0; + let mut scan_strength = 0.0; + for attribute_id in vec![208, 209, 210, 211].iter() { + if ship.hull.attributes.contains_key(attribute_id) { + let attribute = ship.hull.attributes.get(attribute_id).unwrap(); + + if attribute.base_value > base_scan_strength { + base_scan_strength = attribute.base_value; + } + if attribute.value.unwrap() > scan_strength { + scan_strength = attribute.value.unwrap(); + } + } + } + + (base_scan_strength, scan_strength) +} + +fn add_cpu_usage(ship: &mut Ship) -> (f32, f32) { + /* How much CPU is being used, which is adding up cpuOuput (50) from all items. */ + + let mut cpu_usage = 0.0; + for item in &ship.items { + if item.attributes.contains_key(&50) { + cpu_usage += item.attributes.get(&50).unwrap().value.unwrap(); + } + } + + (0.0, cpu_usage) +} + +fn add_pg_usage(ship: &mut Ship) -> (f32, f32) { + /* How much PG is being used, which is adding up powerOutput (30) from all items. */ + + let mut pg_usage = 0.0; + for item in &ship.items { + if item.attributes.contains_key(&30) { + pg_usage += item.attributes.get(&30).unwrap().value.unwrap(); + } + } + + (0.0, pg_usage) +} + +/* Attributes don't contain all information displayed, so we calculate some fake attributes with those values. */ +impl Pass for PassFour { + fn pass(_info: &Info, ship: &mut Ship) { + let align_time = calculate_align_time(ship); + add_attribute(ship, -1, align_time.0, align_time.1); + + let scan_strength = add_scan_strength(ship); + add_attribute(ship, -2, scan_strength.0, scan_strength.1); + + let cpu_usage = add_cpu_usage(ship); + add_attribute(ship, -3, cpu_usage.0, cpu_usage.1); + + let pg_usage = add_pg_usage(ship); + add_attribute(ship, -4, pg_usage.0, pg_usage.1); + } +} diff --git a/src/console.rs b/src/console.rs new file mode 100644 index 0000000..290e79e --- /dev/null +++ b/src/console.rs @@ -0,0 +1,15 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = log)] + pub fn js_log(s: &str); +} + +#[allow(unused_macros)] +macro_rules! log { + ($($t:tt)*) => (crate::console::js_log(&format_args!($($t)*).to_string())) +} + +#[allow(unused_imports)] +pub(crate) use log; diff --git a/src/data_types.rs b/src/data_types.rs new file mode 100644 index 0000000..2e6b283 --- /dev/null +++ b/src/data_types.rs @@ -0,0 +1,108 @@ +use serde::Deserialize; +use serde_repr::*; + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct TypeId { + pub name: String, + pub groupID: i32, + pub categoryID: i32, + pub marketGroupID: Option, + pub capacity: Option, + pub mass: Option, + pub radius: Option, + pub volume: Option, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct TypeDogmaAttribute { + pub attributeID: i32, + pub value: f32, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct TypeDogmaEffect { + pub effectID: i32, + pub isDefault: bool, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct TypeDogma { + pub dogmaAttributes: Vec, + pub dogmaEffects: Vec, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct DogmaAttribute { + pub defaultValue: f32, + pub highIsGood: bool, + pub stackable: bool, +} + +#[allow(non_snake_case)] +#[derive(Deserialize_repr, Debug)] +#[repr(i32)] +pub enum DogmaEffectModifierInfoDomain { + ItemID = 0, + ShipID = 1, + CharID = 2, + OtherID = 3, + StructureID = 4, + Target = 5, + TargetID = 6, +} + +#[allow(non_snake_case)] +#[derive(Deserialize_repr, Debug)] +#[repr(i32)] +pub enum DogmaEffectModifierInfoFunc { + ItemModifier = 0, + LocationGroupModifier = 1, + LocationModifier = 2, + LocationRequiredSkillModifier = 3, + OwnerRequiredSkillModifier = 4, + EffectStopper = 5, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct DogmaEffectModifierInfo { + pub domain: DogmaEffectModifierInfoDomain, + pub func: DogmaEffectModifierInfoFunc, + pub modifiedAttributeID: Option, + pub modifyingAttributeID: Option, + pub operation: Option, + pub groupID: Option, + pub skillTypeID: Option, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct DogmaEffect { + pub dischargeAttributeID: Option, + pub durationAttributeID: Option, + pub effectCategory: i32, + pub electronicChance: bool, + pub isAssistance: bool, + pub isOffensive: bool, + pub isWarpSafe: bool, + pub propulsionChance: bool, + pub rangeChance: bool, + pub rangeAttributeID: Option, + pub falloffAttributeID: Option, + pub trackingSpeedAttributeID: Option, + pub fittingUsageChanceAttributeID: Option, + pub resistanceAttributeID: Option, + pub modifierInfo: Vec, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct ShipLayout { + pub ship_id: i32, + pub items: Vec, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0ef5ddb --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,26 @@ +use serde_wasm_bindgen; +use std::collections::BTreeMap; +use wasm_bindgen::prelude::*; + +mod calculate; +mod console; +mod data_types; + +#[wasm_bindgen] +pub fn init() { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); +} + +#[wasm_bindgen] +pub fn calculate(js_ship_layout: JsValue, js_skills: JsValue) -> JsValue { + let ship_layout: data_types::ShipLayout = + serde_wasm_bindgen::from_value(js_ship_layout).unwrap(); + let skills: BTreeMap = serde_wasm_bindgen::from_value(js_skills).unwrap(); + let skills = skills + .into_iter() + .map(|(k, v)| (k.parse::().unwrap(), v)) + .collect(); + + let statistics = calculate::calculate(&ship_layout, &skills); + serde_wasm_bindgen::to_value(&statistics).unwrap() +}