Skip to content

Commit

Permalink
refactoring: splitted mtf2json.py into multiple modules
Browse files Browse the repository at this point in the history
  • Loading branch information
juk0de committed Sep 28, 2024
1 parent 08ec2db commit 0c45477
Show file tree
Hide file tree
Showing 11 changed files with 869 additions and 805 deletions.
3 changes: 2 additions & 1 deletion mtf2json/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# this enables direct import from 'mtf2json' (instead of 'mtf2json.mtf2json')
from .mtf2json import read_mtf, write_json, ConversionError, version, mm_commit # noqa
from .mtf2json import read_mtf, write_json, version, mm_commit # noqa
from .error import ConversionError # noqa
182 changes: 182 additions & 0 deletions mtf2json/armor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from typing import Any


def add_armor_type(value: str, mech_data: dict[str, Any]) -> None:
"""
Add the armor type.
The MTF `Armor:` key in is a bit of a mess: it can contain a value that only describes
the type of armor, e.g:
```
Armor:Standard Armor
```
or the type + tech base (delimited by `(...)`), e.g.:
```
Armor:Standard(Inner Sphere)
```
Furthermore the type description is not consistent. E.g. it can be `Standard` or
`Standard Armor`. We try to clean up the mess a bit by storing all available
information in the JSON 'armor' section, while also choosing a consistent type string:
```
"armor": {
"type": "Standard",
"tech_base: "Inner Sphere"
...
}
```
Note that `tech_base` is optional (only added if there's a string in `(...)`) and
that the term 'Armor' has been removed from the type string.
"""
# create armor section if it doesn't exist
if "armor" not in mech_data:
mech_data["armor"] = {}
armor_type_section: dict[str, str | dict[str, Any]] = mech_data["armor"]

# Extract type and tech base if present
if "(" in value and ")" in value:
type_, tech_base = value.split("(", 1)
tech_base = tech_base.rstrip(")")
else:
type_ = value
tech_base = None

# Clean up type string
type_ = type_.replace(" Armor", "").strip()

# Populate armor section
armor_type_section["type"] = type_
if tech_base:
armor_type_section["tech_base"] = tech_base.strip()


def add_armor_locations(key: str, value: str, mech_data: dict[str, Any]) -> None:
"""
Add individual armor locations to the given `armor_section` dictionary.
The armor pips are stored as individual keys in an MTF file:
```
LA armor:21
RA armor:21
LT armor:30
RT armor:30
CT armor:40
HD armor:9
LL armor:26
RL armor:26
RTL armor:10
RTR armor:10
RTC armor:17
```
We're structuring the values in the JSON 'armor' section like this:
```
"armor": {
...
"left_arm": {
"pips": 21
},
"right_arm": {
"pips": 21
},
"left_torso": {
"front": {
"pips": 30
},
"rear": {
"pips": 10
},
},
"right_torso": {
"front": {
"pips": 30
},
"rear": {
"pips": 10
},
},
"center_torso": {
"front": {
"pips": 40
},
"rear": {
"pips": 17
},
},
"head": {
"pips": 9
}
"left_leg": {
"pips": 26
},
"right_leg": {
"pips": 26
}
}
```
In case of patchwork armor, the location keys MAY contain subkeys
describing the armor type in that location, e.g.:
```
HD Armor:Standard(IS/Clan):9
LL Armor:Reactive(Inner Sphere):34
RTL Armor:8
```
In that case, we add a `type` key to the affected location:
```
"head": {
"pips": 9,
"type": "Standard(IS/Clan)"
}
"left_leg": {
"pips": 26,
"type": "Reactive(Inner Sphere)"
},
```
"""
# create armor section if it doesn't exist
if "armor" not in mech_data:
mech_data["armor"] = {}
armor_section: dict[str, Any] = mech_data["armor"]

# Extract subkeys if present
parts = value.split(":")
if len(parts) == 2:
armor_type = parts[0].strip()
pips_value = int(parts[1].strip())
else:
armor_type = None
pips_value = int(parts[0].strip())

# center torso (front and rear)
if key in ["ct_armor", "rtc_armor"]:
if "center_torso" not in armor_section:
armor_section["center_torso"] = {}
side = "front" if key == "ct_armor" else "rear"
if side not in armor_section["center_torso"]:
armor_section["center_torso"][side] = {}
armor_section["center_torso"][side]["pips"] = pips_value
if armor_type:
armor_section["center_torso"][side]["type"] = armor_type
# right torso (front and rear)
elif key in ["rt_armor", "rtr_armor"]:
if "right_torso" not in armor_section:
armor_section["right_torso"] = {}
side = "front" if key == "rt_armor" else "rear"
if side not in armor_section["right_torso"]:
armor_section["right_torso"][side] = {}
armor_section["right_torso"][side]["pips"] = pips_value
if armor_type:
armor_section["right_torso"][side]["type"] = armor_type
# left torso (front and rear)
elif key in ["lt_armor", "rtl_armor"]:
if "left_torso" not in armor_section:
armor_section["left_torso"] = {}
side = "front" if key == "lt_armor" else "rear"
if side not in armor_section["left_torso"]:
armor_section["left_torso"][side] = {}
armor_section["left_torso"][side]["pips"] = pips_value
if armor_type:
armor_section["left_torso"][side]["type"] = armor_type
else:
if key not in armor_section:
armor_section[key] = {}
armor_section[key]["pips"] = pips_value
if armor_type:
armor_section[key]["type"] = armor_type
60 changes: 60 additions & 0 deletions mtf2json/critical_slots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from typing import Any


def add_crit_slot(key: str, value: str, mech_data: dict[str, Any]) -> None:
"""
Add a critical slot entry.
The MTF contains one critical slot section per location. It start with the location
name and no value. Each following line contains a slot entry with no key. However,
it may contain a `:` if it contains a `:SIZE:` option, but that is filtered by
`__read_line()`.
Here's an example for the left arm:
```
Left Arm:
Shoulder
Upper Arm Actuator
Lower Arm Actuator
Hand Actuator
Heat Sink
Heat Sink
ISERLargeLaser
ISERLargeLaser
ISAntiMissileSystem
-Empty-
-Empty-
-Empty-
```
In JSON, we put all locations into the `critical_slots` section and add a counter for each slot.
Also, we replace `-Empty-` with `None`:
```
"critical_slots": {
"left_arm": {
"1": "Shoulder",
"2": "Upper Arm Actuator",
"3": "Lower Arm Actuator",
"4": "Hand Actuator",
"5": "Heat Sink",
"6": "Heat Sink",
"7": "ISERLargeLaser",
"8": "ISERLargeLaser",
"9": "ISAntiMissileSystem",
"10": None,
"11": None,
"12": None
},
```
"""
# create section if it does not exist
if "critical_slots" not in mech_data:
mech_data["critical_slots"] = {}

# create new subsection if value is empty
if not value:
mech_data["critical_slots"][key] = {}
# otherwise add the given slot entry
else:
crit_slots_section: dict[str, str | None] = mech_data["critical_slots"][key]
slot_number = len(crit_slots_section) + 1
crit_slots_section[str(slot_number)] = value if value != "-Empty-" else None
2 changes: 2 additions & 0 deletions mtf2json/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class ConversionError(Exception):
pass
113 changes: 113 additions & 0 deletions mtf2json/fluff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from typing import Any, cast
from .error import ConversionError


def add_fluff(key: str, value: str, mech_data: dict[str, Any]) -> None:
value = __remove_p_tags(value)
"""
Add the given fluff key and value.
Some of the fluff keys can appear multiple times in an MTF file, e.g.:
```
systemmanufacturer:CHASSIS:Star League
systemmanufacturer:ENGINE:GM
systemmanufacturer:ARMOR:Starshield
systemmanufacturer:TARGETING:Dalban
systemmanufacturer:COMMUNICATIONS:Dalban
systemmode:ENGINE:380
systemmode:CHASSIS:XT
systemmode:TARGETING:HiRez-B
systemmode:COMMUNICATIONS:Commline
```
All fluff keys that appears more than once have "subkeys", i.e. the given
value contains another key / value pair separated by `:`. We create a JSON
subsection for each "primary" key and add the "subkey" entries to it, e.g.:
```
"systemmanufacturer": {
"chassis": "Star League",
"engine": "GM",
"armor": "Starshield"
"targeting": "Dalban",
"communication": "Dalban"
},
"systemmode": {
"engine": "380",
"chassis": "XT",
"targeting": "HiRez-B",
"communications": "Commline"
},
...
The following keys can contain lists as values (separated by `,`):
```
manufacturer
primaryfactory
```
In this case we store the values as JSON lists, e.g.:
```
"manufacturer": [
"Defiance Industries",
"Hegemony Research and Development Department",
"Weapons Division"
],
"primaryfactory": [
"Hesperus II",
"New Earth"
],
...
```
All other keys and values are added verbatim.
"""
# add fluff section if not present
if "fluff" not in mech_data:
mech_data["fluff"] = {}
fluff_section: dict[str, str | list[str] | dict[str, str]] = mech_data["fluff"]

# the key is already in the fluff section
# -> it's a subsection
if key in fluff_section:
try:
subkey, subvalue = value.split(":", 1)
except ValueError:
raise ConversionError(
f"Key '{key}' already exists in the fluff section but value is missing the ':' delimiter!"
)
if isinstance(fluff_section[key], dict):
cast(dict, fluff_section[key])[subkey.lower()] = subvalue.strip()
else:
raise ConversionError(
f"Tried to add '{subkey}:{subvalue}' to fluff section '{key}', but '{key}' is not a dictionary!"
)
# the key is new
else:
# value contains a subkey
# -> create a new subsection
if ":" in value:
subkey, subvalue = value.split(":", 1)
# but ONLY if the subkey is all UPPERCASE, e.g.:
# ```
# systemmanufacturer:CHASSIS:Republic-R
# ```
# Otherwise we could turn some of the longer text strings
# (that sometimes contain `:`) into dicts.
if subkey.isupper():
fluff_section[key] = {subkey.lower(): subvalue.strip()}
else:
fluff_section[key] = value
# value contains a list
elif key in ["manufacturer", "primaryfactory"]:
fluff_section[key] = [item.strip() for item in value.split(",")]
# simple value
else:
fluff_section[key] = value


def __remove_p_tags(text: str) -> str:
"""
Remove <p>, <P>, </p> and </P> tags from the given text.
"""
return (
text.replace("<p>", "")
.replace("</p>", "")
.replace("<P>", "")
.replace("</P>", "")
)
29 changes: 29 additions & 0 deletions mtf2json/heat_sinks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import Any


def add_heat_sinks(value: str, mech_data: dict[str, Any]) -> None:
"""
Add heat sinks section.
Heat sinks are stored as a flat key:value pair in the MTF file, with the value containing
both type and quantity of heat sinks, e.g.:
```
heat sinks:10 IS Double
```
We separate heat sink type and quantity, using the first ` ` as delimiter, and store them
in a JSON section like this:
```
"heat_sinks": {
"quantity": 10,
"type": "IS Double"
}
```
"""
# add heat sinks section if not present
if "heat_sinks" not in mech_data:
mech_data["heat_sinks"] = {}
heat_sinks_section: dict[str, int | str] = mech_data["heat_sinks"]

quantity, type_ = value.split(" ", 1)
heat_sinks_section["quantity"] = int(quantity)
heat_sinks_section["type"] = type_.strip()
Loading

0 comments on commit 0c45477

Please sign in to comment.