about summary refs log tree commit diff
path: root/dropcalc
diff options
context:
space:
mode:
authorStarfall <us@starfall.systems>2024-02-07 17:54:47 -0600
committerStarfall <us@starfall.systems>2024-02-07 17:56:03 -0600
commit482794473972a1265308fcb89069066483bbdda0 (patch)
tree5dc3ea5306a5a2648a4815442643ddf2ebf1571f /dropcalc
parentf06167b79397b425272fd4843d599d5f5c372fb6 (diff)
dropcalc: Median XL .txt parser
Was intended to eventually become a drop calculator. Might work for
other Diablo II mods without changes, I don't think there are any
MXL-specific fields involved in this.

Abandoned due to lack of organizational structure on the project and ...
creative differences with one of the lead devs.
Diffstat (limited to 'dropcalc')
-rw-r--r--dropcalc/.gitignore6
-rw-r--r--dropcalc/LICENSE13
-rw-r--r--dropcalc/Pipfile12
-rw-r--r--dropcalc/Pipfile.lock147
-rw-r--r--dropcalc/README.md20
-rwxr-xr-xdropcalc/create_db.py69
-rw-r--r--dropcalc/dropcalc-notes61
-rw-r--r--dropcalc/mxl_types/__init__.py15
-rw-r--r--dropcalc/mxl_types/base.py34
-rw-r--r--dropcalc/mxl_types/dao.py13
-rw-r--r--dropcalc/mxl_types/item_ratio.py39
-rw-r--r--dropcalc/mxl_types/item_type.py32
-rw-r--r--dropcalc/mxl_types/set.py15
-rw-r--r--dropcalc/mxl_types/treasure_class.py46
-rw-r--r--dropcalc/mxl_types/unique.py18
-rw-r--r--dropcalc/util.py4
16 files changed, 544 insertions, 0 deletions
diff --git a/dropcalc/.gitignore b/dropcalc/.gitignore
new file mode 100644
index 0000000..cfff1c9
--- /dev/null
+++ b/dropcalc/.gitignore
@@ -0,0 +1,6 @@
+# Intellij IDEA
+*.iml
+.idea/
+
+# Python
+__pycache__
diff --git a/dropcalc/LICENSE b/dropcalc/LICENSE
new file mode 100644
index 0000000..eca96fd
--- /dev/null
+++ b/dropcalc/LICENSE
@@ -0,0 +1,13 @@
+           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+                   Version 2, December 2004
+ 
+Copyright (C) 2023 Starfall <us@starfall.systems>
+
+Everyone is permitted to copy and distribute verbatim or modified
+copies of this license document, and changing it is allowed as long
+as the name is changed.
+ 
+           DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
+  TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. You just DO WHAT THE FUCK YOU WANT TO.
diff --git a/dropcalc/Pipfile b/dropcalc/Pipfile
new file mode 100644
index 0000000..99eb835
--- /dev/null
+++ b/dropcalc/Pipfile
@@ -0,0 +1,12 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+sqlalchemy = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.11"
diff --git a/dropcalc/Pipfile.lock b/dropcalc/Pipfile.lock
new file mode 100644
index 0000000..26425ac
--- /dev/null
+++ b/dropcalc/Pipfile.lock
@@ -0,0 +1,147 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "47bb17e54bf2d6f4da261a2bd6e74be1c1ec26a8e00cb40956889302e10045cd"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.11"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "greenlet": {
+            "hashes": [
+                "sha256:0a02d259510b3630f330c86557331a3b0e0c79dac3d166e449a39363beaae174",
+                "sha256:0b6f9f8ca7093fd4433472fd99b5650f8a26dcd8ba410e14094c1e44cd3ceddd",
+                "sha256:100f78a29707ca1525ea47388cec8a049405147719f47ebf3895e7509c6446aa",
+                "sha256:1757936efea16e3f03db20efd0cd50a1c86b06734f9f7338a90c4ba85ec2ad5a",
+                "sha256:19075157a10055759066854a973b3d1325d964d498a805bb68a1f9af4aaef8ec",
+                "sha256:19bbdf1cce0346ef7341705d71e2ecf6f41a35c311137f29b8a2dc2341374565",
+                "sha256:20107edf7c2c3644c67c12205dc60b1bb11d26b2610b276f97d666110d1b511d",
+                "sha256:22f79120a24aeeae2b4471c711dcf4f8c736a2bb2fabad2a67ac9a55ea72523c",
+                "sha256:2847e5d7beedb8d614186962c3d774d40d3374d580d2cbdab7f184580a39d234",
+                "sha256:28e89e232c7593d33cac35425b58950789962011cc274aa43ef8865f2e11f46d",
+                "sha256:329c5a2e5a0ee942f2992c5e3ff40be03e75f745f48847f118a3cfece7a28546",
+                "sha256:337322096d92808f76ad26061a8f5fccb22b0809bea39212cd6c406f6a7060d2",
+                "sha256:3fcc780ae8edbb1d050d920ab44790201f027d59fdbd21362340a85c79066a74",
+                "sha256:41bdeeb552d814bcd7fb52172b304898a35818107cc8778b5101423c9017b3de",
+                "sha256:4eddd98afc726f8aee1948858aed9e6feeb1758889dfd869072d4465973f6bfd",
+                "sha256:52e93b28db27ae7d208748f45d2db8a7b6a380e0d703f099c949d0f0d80b70e9",
+                "sha256:55d62807f1c5a1682075c62436702aaba941daa316e9161e4b6ccebbbf38bda3",
+                "sha256:5805e71e5b570d490938d55552f5a9e10f477c19400c38bf1d5190d760691846",
+                "sha256:599daf06ea59bfedbec564b1692b0166a0045f32b6f0933b0dd4df59a854caf2",
+                "sha256:60d5772e8195f4e9ebf74046a9121bbb90090f6550f81d8956a05387ba139353",
+                "sha256:696d8e7d82398e810f2b3622b24e87906763b6ebfd90e361e88eb85b0e554dc8",
+                "sha256:6e6061bf1e9565c29002e3c601cf68569c450be7fc3f7336671af7ddb4657166",
+                "sha256:80ac992f25d10aaebe1ee15df45ca0d7571d0f70b645c08ec68733fb7a020206",
+                "sha256:816bd9488a94cba78d93e1abb58000e8266fa9cc2aa9ccdd6eb0696acb24005b",
+                "sha256:85d2b77e7c9382f004b41d9c72c85537fac834fb141b0296942d52bf03fe4a3d",
+                "sha256:87c8ceb0cf8a5a51b8008b643844b7f4a8264a2c13fcbcd8a8316161725383fe",
+                "sha256:89ee2e967bd7ff85d84a2de09df10e021c9b38c7d91dead95b406ed6350c6997",
+                "sha256:8bef097455dea90ffe855286926ae02d8faa335ed8e4067326257cb571fc1445",
+                "sha256:8d11ebbd679e927593978aa44c10fc2092bc454b7d13fdc958d3e9d508aba7d0",
+                "sha256:91e6c7db42638dc45cf2e13c73be16bf83179f7859b07cfc139518941320be96",
+                "sha256:97e7ac860d64e2dcba5c5944cfc8fa9ea185cd84061c623536154d5a89237884",
+                "sha256:990066bff27c4fcf3b69382b86f4c99b3652bab2a7e685d968cd4d0cfc6f67c6",
+                "sha256:9fbc5b8f3dfe24784cee8ce0be3da2d8a79e46a276593db6868382d9c50d97b1",
+                "sha256:ac4a39d1abae48184d420aa8e5e63efd1b75c8444dd95daa3e03f6c6310e9619",
+                "sha256:b2c02d2ad98116e914d4f3155ffc905fd0c025d901ead3f6ed07385e19122c94",
+                "sha256:b2d3337dcfaa99698aa2377c81c9ca72fcd89c07e7eb62ece3f23a3fe89b2ce4",
+                "sha256:b489c36d1327868d207002391f662a1d163bdc8daf10ab2e5f6e41b9b96de3b1",
+                "sha256:b641161c302efbb860ae6b081f406839a8b7d5573f20a455539823802c655f63",
+                "sha256:b8ba29306c5de7717b5761b9ea74f9c72b9e2b834e24aa984da99cbfc70157fd",
+                "sha256:b9934adbd0f6e476f0ecff3c94626529f344f57b38c9a541f87098710b18af0a",
+                "sha256:ce85c43ae54845272f6f9cd8320d034d7a946e9773c693b27d620edec825e376",
+                "sha256:cf868e08690cb89360eebc73ba4be7fb461cfbc6168dd88e2fbbe6f31812cd57",
+                "sha256:d2905ce1df400360463c772b55d8e2518d0e488a87cdea13dd2c71dcb2a1fa16",
+                "sha256:d57e20ba591727da0c230ab2c3f200ac9d6d333860d85348816e1dca4cc4792e",
+                "sha256:d6a8c9d4f8692917a3dc7eb25a6fb337bff86909febe2f793ec1928cd97bedfc",
+                "sha256:d923ff276f1c1f9680d32832f8d6c040fe9306cbfb5d161b0911e9634be9ef0a",
+                "sha256:daa7197b43c707462f06d2c693ffdbb5991cbb8b80b5b984007de431493a319c",
+                "sha256:dbd4c177afb8a8d9ba348d925b0b67246147af806f0b104af4d24f144d461cd5",
+                "sha256:dc4d815b794fd8868c4d67602692c21bf5293a75e4b607bb92a11e821e2b859a",
+                "sha256:e9d21aaa84557d64209af04ff48e0ad5e28c5cca67ce43444e939579d085da72",
+                "sha256:ea6b8aa9e08eea388c5f7a276fabb1d4b6b9d6e4ceb12cc477c3d352001768a9",
+                "sha256:eabe7090db68c981fca689299c2d116400b553f4b713266b130cfc9e2aa9c5a9",
+                "sha256:f2f6d303f3dee132b322a14cd8765287b8f86cdc10d2cb6a6fae234ea488888e",
+                "sha256:f33f3258aae89da191c6ebaa3bc517c6c4cbc9b9f689e5d8452f7aedbb913fa8",
+                "sha256:f7bfb769f7efa0eefcd039dd19d843a4fbfbac52f1878b1da2ed5793ec9b1a65",
+                "sha256:f89e21afe925fcfa655965ca8ea10f24773a1791400989ff32f467badfe4a064",
+                "sha256:fa24255ae3c0ab67e613556375a4341af04a084bd58764731972bcbc8baeba36"
+            ],
+            "markers": "platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
+            "version": "==3.0.1"
+        },
+        "sqlalchemy": {
+            "hashes": [
+                "sha256:0666031df46b9badba9bed00092a1ffa3aa063a5e68fa244acd9f08070e936d3",
+                "sha256:0a8c6aa506893e25a04233bc721c6b6cf844bafd7250535abb56cb6cc1368884",
+                "sha256:0e680527245895aba86afbd5bef6c316831c02aa988d1aad83c47ffe92655e74",
+                "sha256:14aebfe28b99f24f8a4c1346c48bc3d63705b1f919a24c27471136d2f219f02d",
+                "sha256:1e018aba8363adb0599e745af245306cb8c46b9ad0a6fc0a86745b6ff7d940fc",
+                "sha256:227135ef1e48165f37590b8bfc44ed7ff4c074bf04dc8d6f8e7f1c14a94aa6ca",
+                "sha256:31952bbc527d633b9479f5f81e8b9dfada00b91d6baba021a869095f1a97006d",
+                "sha256:3e983fa42164577d073778d06d2cc5d020322425a509a08119bdcee70ad856bf",
+                "sha256:42d0b0290a8fb0165ea2c2781ae66e95cca6e27a2fbe1016ff8db3112ac1e846",
+                "sha256:42ede90148b73fe4ab4a089f3126b2cfae8cfefc955c8174d697bb46210c8306",
+                "sha256:4895a63e2c271ffc7a81ea424b94060f7b3b03b4ea0cd58ab5bb676ed02f4221",
+                "sha256:4af79c06825e2836de21439cb2a6ce22b2ca129bad74f359bddd173f39582bf5",
+                "sha256:5f94aeb99f43729960638e7468d4688f6efccb837a858b34574e01143cf11f89",
+                "sha256:616fe7bcff0a05098f64b4478b78ec2dfa03225c23734d83d6c169eb41a93e55",
+                "sha256:62d9e964870ea5ade4bc870ac4004c456efe75fb50404c03c5fd61f8bc669a72",
+                "sha256:638c2c0b6b4661a4fd264f6fb804eccd392745c5887f9317feb64bb7cb03b3ea",
+                "sha256:63bfc3acc970776036f6d1d0e65faa7473be9f3135d37a463c5eba5efcdb24c8",
+                "sha256:6463aa765cf02b9247e38b35853923edbf2f6fd1963df88706bc1d02410a5577",
+                "sha256:64ac935a90bc479fee77f9463f298943b0e60005fe5de2aa654d9cdef46c54df",
+                "sha256:683ef58ca8eea4747737a1c35c11372ffeb84578d3aab8f3e10b1d13d66f2bc4",
+                "sha256:75eefe09e98043cff2fb8af9796e20747ae870c903dc61d41b0c2e55128f958d",
+                "sha256:787af80107fb691934a01889ca8f82a44adedbf5ef3d6ad7d0f0b9ac557e0c34",
+                "sha256:7c424983ab447dab126c39d3ce3be5bee95700783204a72549c3dceffe0fc8f4",
+                "sha256:7e0dc9031baa46ad0dd5a269cb7a92a73284d1309228be1d5935dac8fb3cae24",
+                "sha256:87a3d6b53c39cd173990de2f5f4b83431d534a74f0e2f88bd16eabb5667e65c6",
+                "sha256:89a01238fcb9a8af118eaad3ffcc5dedaacbd429dc6fdc43fe430d3a941ff965",
+                "sha256:9585b646ffb048c0250acc7dad92536591ffe35dba624bb8fd9b471e25212a35",
+                "sha256:964971b52daab357d2c0875825e36584d58f536e920f2968df8d581054eada4b",
+                "sha256:967c0b71156f793e6662dd839da54f884631755275ed71f1539c95bbada9aaab",
+                "sha256:9ca922f305d67605668e93991aaf2c12239c78207bca3b891cd51a4515c72e22",
+                "sha256:a86cb7063e2c9fb8e774f77fbf8475516d270a3e989da55fa05d08089d77f8c4",
+                "sha256:aeb397de65a0a62f14c257f36a726945a7f7bb60253462e8602d9b97b5cbe204",
+                "sha256:b41f5d65b54cdf4934ecede2f41b9c60c9f785620416e8e6c48349ab18643855",
+                "sha256:bd45a5b6c68357578263d74daab6ff9439517f87da63442d244f9f23df56138d",
+                "sha256:c14eba45983d2f48f7546bb32b47937ee2cafae353646295f0e99f35b14286ab",
+                "sha256:c1bda93cbbe4aa2aa0aa8655c5aeda505cd219ff3e8da91d1d329e143e4aff69",
+                "sha256:c4722f3bc3c1c2fcc3702dbe0016ba31148dd6efcd2a2fd33c1b4897c6a19693",
+                "sha256:c80c38bd2ea35b97cbf7c21aeb129dcbebbf344ee01a7141016ab7b851464f8e",
+                "sha256:cabafc7837b6cec61c0e1e5c6d14ef250b675fa9c3060ed8a7e38653bd732ff8",
+                "sha256:cc1d21576f958c42d9aec68eba5c1a7d715e5fc07825a629015fe8e3b0657fb0",
+                "sha256:d0f7fb0c7527c41fa6fcae2be537ac137f636a41b4c5a4c58914541e2f436b45",
+                "sha256:d4041ad05b35f1f4da481f6b811b4af2f29e83af253bf37c3c4582b2c68934ab",
+                "sha256:d5578e6863eeb998980c212a39106ea139bdc0b3f73291b96e27c929c90cd8e1",
+                "sha256:e3b5036aa326dc2df50cba3c958e29b291a80f604b1afa4c8ce73e78e1c9f01d",
+                "sha256:e599a51acf3cc4d31d1a0cf248d8f8d863b6386d2b6782c5074427ebb7803bda",
+                "sha256:f3420d00d2cb42432c1d0e44540ae83185ccbbc67a6054dcc8ab5387add6620b",
+                "sha256:f48ed89dd11c3c586f45e9eec1e437b355b3b6f6884ea4a4c3111a3358fd0c18",
+                "sha256:f508ba8f89e0a5ecdfd3761f82dda2a3d7b678a626967608f4273e0dba8f07ac",
+                "sha256:fd54601ef9cc455a0c61e5245f690c8a3ad67ddb03d3b91c361d076def0b4c60"
+            ],
+            "index": "pypi",
+            "version": "==2.0.23"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
+                "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.8.0"
+        }
+    },
+    "develop": {}
+}
diff --git a/dropcalc/README.md b/dropcalc/README.md
new file mode 100644
index 0000000..265581d
--- /dev/null
+++ b/dropcalc/README.md
@@ -0,0 +1,20 @@
+# Future home of Median XL Dropcalc
+
+and maybe a proper MXLDB eventually, too
+
+all it does right now is read some of the txts and dump them into an in-memory SQLite database, then do nothing with them
+
+## Dependencies
+
+Install python3, pip, and pipenv; I leave it up to the reader to decide how to do it because for me it's `paru -S python python-pip python-pipenv`.
+
+Run `pipenv shell` to create the dev environment which will install everything from the Pipfile (just sqlalchemy for now).
+
+If you don't like pipenv you still need python3 and pip, then just `pip install --user sqlalchemy`.
+
+## Usage
+```
+create_db.py /path/to/txts
+```
+
+If you just give it the MOD repo or your d2 install folder (if you have the txts in it) that'll work too.
diff --git a/dropcalc/create_db.py b/dropcalc/create_db.py
new file mode 100755
index 0000000..be60fb6
--- /dev/null
+++ b/dropcalc/create_db.py
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+
+import csv
+import mxl_types
+from argparse import ArgumentParser
+from pathlib import Path
+from sqlalchemy import create_engine, text
+from sqlalchemy.orm import Session
+
+
+def generate_tcs():
+	# TODO
+	# grab weapons, armor, misc? with spawnable=1 *and* who have itemtype with TreasureClass=1 (any itemtype? just direct?)
+	# qlvl 1-3 in weap3, 4-6 in weap6, 7-9 in weap9 ... 91-93 in weap93
+	# armo3 ... armo99
+	# vanilla has bow3 ... bow87 as well, unknown but probably based on the itemtype having Shoots and Quiver
+	# don't know if misc TCs are generated, neither median nor vanilla actually uses them if they are
+	pass
+
+
+def create_db(folder):
+	engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
+	mxl_types.BaseDAO.metadata.create_all(engine)
+
+	try1 = (Path(folder) / "Data" / "global" / "excel")
+	if try1.exists():
+		data_folder = try1
+	else:
+		data_folder = folder
+
+	types = [
+		mxl_types.ItemRatio,
+		mxl_types.ItemType,
+		mxl_types.Weapon,
+		mxl_types.Armor,
+		mxl_types.Misc,
+		mxl_types.UniqueItem,
+		mxl_types.SetItem,
+		mxl_types.TreasureClass,
+	]
+	for mytype in types:
+		file = data_folder / mytype.filename
+		with file.open(encoding='windows-1252') as csvfile, Session(engine) as session:
+			reader = csv.DictReader(csvfile, dialect='excel-tab')
+			for csv_row in reader:
+				db_row = mytype(csv_row)
+				if not db_row.is_valid():
+					continue
+				session.add(db_row)
+			session.commit()
+			count = session.scalar(text(f'select count(*) from {mytype.__tablename__}'))
+			print(f'Counted {count} {mytype.__tablename__}')
+
+
+def handle_args():
+	parser = ArgumentParser()
+	parser.add_argument('folder', help='Location of Median XL .txt files')
+	return parser.parse_args()
+
+
+def main():
+	args = handle_args()
+	create_db(args.folder)
+
+
+if __name__ == '__main__':
+	import sys
+
+	sys.exit(main())
diff --git a/dropcalc/dropcalc-notes b/dropcalc/dropcalc-notes
new file mode 100644
index 0000000..b334e43
--- /dev/null
+++ b/dropcalc/dropcalc-notes
@@ -0,0 +1,61 @@
+TreasureClassEx.txt
+tab-separated
+Picks
+Unique, Set, Rare, Magic
+NoDrop
+Item{1-10} : can be a TC name, Weapons/Armor/Misc/Gems code, UniqueItems index, maybe even SetItems index
+Prob{1-10}
+
+group/level can upgrade tc but that's skippable for now until we need to deal with monsters
+
+
+
+for generic TCs:
+read Weapons.txt, Armor.txt code, rarity
+use name for now, deal with namestr & string tbls later
+
+
+
+NoDrop adjustment:
+fundamentally it just retries once per (close, partied) player until an item is hit.
+far and/or unpartied players count half
+
+
+item quality:
+ItemRatio.txt
+Unique, UniqueDivisor, UniqueMin... etc
+
+chance = 128 * (Unique - (ilvl-qlvl)/Divisor)
+eMF = (MF * factor) / (MF + Factor) like basically all diminishing returns
+chance = chance * eMF
+cap to minChance
+chance = chance - (chance * tcUnique / 1024)
+
+roll 0 to chance-1, if less than 128 then success
+which means lower "chance" is better
+
+unique is picked with rarity / total rarity of uniques for base that have the right ilvl as expected
+
+game rolls unique - set - rare - magic - superior - normal in order
+
+
+
+affixes the fuckening:
+rares have 2+d4 affixes, 50/50 for each to be prefix or suffix until 3
+crafts have 4 affixes (or 0.4/0.2/0.2/0.2 for 1-4 at ilvl 1), otherwise the same
+generate in order, exclude by group
+
+alvl of item is:
+tmplvl = min(ilvl, 99)
+tmplvl = max(qlvl, tmplvl)
+if magic_level: 
+	alvl = tmplvl + magic_level	
+else if tmplvl < (99 - qlvl/2):
+	alvl = tmplvl - qlvl/2
+else:
+	alvl = 2 * tmplvl - 99
+return min(alvl, 99)
+
+should likely monte carlo simulate crafts rather than try and calculate them
+
+need to fuck around with Itemtypes, MagicPrefix, MagicSuffix to get all the possible affixes
diff --git a/dropcalc/mxl_types/__init__.py b/dropcalc/mxl_types/__init__.py
new file mode 100644
index 0000000..09b00bc
--- /dev/null
+++ b/dropcalc/mxl_types/__init__.py
@@ -0,0 +1,15 @@
+__all__ = ["ItemRatio", "ItemType", "BaseItem", "Weapon", "Armor", "Misc", "UniqueItem", "SetItem", "TreasureClass"]
+
+from .dao import BaseDAO
+
+from .item_ratio import ItemRatio
+from .item_type import ItemType
+from .base import BaseItem, Weapon, Armor, Misc
+from .unique import UniqueItem
+from .set import SetItem
+from .treasure_class import TreasureClass
+
+# TODO
+# MonStats grab Id, hcIdx, enabled, TreasureClass1(H), maybe remaining TC2/3/4(H)
+# SuperUniques Superunique, TC(H)
+# Levels MonLvl3 and nmon1-nmon20
diff --git a/dropcalc/mxl_types/base.py b/dropcalc/mxl_types/base.py
new file mode 100644
index 0000000..cd7e8be
--- /dev/null
+++ b/dropcalc/mxl_types/base.py
@@ -0,0 +1,34 @@
+from sqlalchemy import ForeignKey, String
+from sqlalchemy.orm import Mapped, mapped_column
+
+from .dao import BaseDAO
+
+
+class BaseItem(BaseDAO):
+	__tablename__ = "base"
+
+	code: Mapped[str] = mapped_column(String(4), primary_key=True)
+	name: Mapped[str]                                    # internal name only, game actually looks up `code` in strings
+	type = mapped_column(ForeignKey("item_type.Code"))   # can have two item_type... and all their parents
+	type2 = mapped_column(ForeignKey("item_type.Code"))  # should really figure out a better way to store them
+	rarity: Mapped[int]
+	level: Mapped[int]                                   # variously referred to as ilvl or qlvl
+	spawnable: Mapped[int]                               # items with spawnable=0 *can* be dropped directly from TCs, but shouldn't randomly drop
+	category: Mapped[str]
+
+	__mapper_args__ = {'polymorphic_on': 'category'}
+
+
+class Weapon(BaseItem):
+	filename = "Weapons.txt"
+	__mapper_args__ = {'polymorphic_identity': 'weap'}
+
+
+class Armor(BaseItem):
+	filename = "Armor.txt"
+	__mapper_args__ = {'polymorphic_identity': 'armo'}
+
+
+class Misc(BaseItem):
+	filename = "Misc.txt"
+	__mapper_args__ = {'polymorphic_identity': 'misc'}
diff --git a/dropcalc/mxl_types/dao.py b/dropcalc/mxl_types/dao.py
new file mode 100644
index 0000000..db459e7
--- /dev/null
+++ b/dropcalc/mxl_types/dao.py
@@ -0,0 +1,13 @@
+from sqlalchemy.orm import DeclarativeBase
+
+
+class BaseDAO(DeclarativeBase):
+	__csv_to_db__ = dict()
+
+	def is_valid(self):
+		return True
+
+	def __init__(self, iterable=(), **kwargs):
+		self.__dict__.update(iterable, **kwargs)
+		for this, that in self.__csv_to_db__.items():
+			self.__dict__[this] = self.__dict__[that]
diff --git a/dropcalc/mxl_types/item_ratio.py b/dropcalc/mxl_types/item_ratio.py
new file mode 100644
index 0000000..7a783c3
--- /dev/null
+++ b/dropcalc/mxl_types/item_ratio.py
@@ -0,0 +1,39 @@
+from sqlalchemy.orm import Mapped, mapped_column
+
+from .dao import BaseDAO
+
+
+class ItemRatio(BaseDAO):
+	filename = "ItemRatio_mp.txt"
+	__csv_to_db__ = {"Class_Specific": "Class Specific"}
+	__tablename__ = "item_ratio"
+
+	id: Mapped[int] = mapped_column(primary_key=True)
+
+	# D2Fileguide goes into more detail about how these are used
+	# the gist is 1 in (Quality - (mlvl-ilvl) / Divisor) * 128 of rolling each of these
+	# in the order unique, set, rare, magic, hiq, normal (and falling back on loq)
+	# further modified by MF after diminishing returns value (250 uni, 500 set, 600 rar) - which I believe is hardcoded
+	# and then the TC values
+	Unique: Mapped[int]
+	UniqueDivisor: Mapped[int]
+	UniqueMin: Mapped[int]
+	Rare: Mapped[int]
+	RareDivisor: Mapped[int]
+	RareMin: Mapped[int]
+	Set: Mapped[int]
+	SetDivisor: Mapped[int]
+	SetMin: Mapped[int]
+	Magic: Mapped[int]
+	MagicDivisor: Mapped[int]
+	MagicMin: Mapped[int]
+	HiQuality: Mapped[int]
+	HiQualityDivisor: Mapped[int]
+	Normal: Mapped[int]
+	NormalDivisor: Mapped[int]
+
+	Uber: Mapped[int]  # exceptional/elite as decided by normcode, ubercode, ultracode in Weapons.txt / Armor.txt
+	Class_Specific: Mapped[int]  # class specifics as decided by Class in Itemtypes.txt
+
+	def is_valid(self):
+		return self.Version == "1"
diff --git a/dropcalc/mxl_types/item_type.py b/dropcalc/mxl_types/item_type.py
new file mode 100644
index 0000000..8dd9b37
--- /dev/null
+++ b/dropcalc/mxl_types/item_type.py
@@ -0,0 +1,32 @@
+from typing import Optional
+
+from sqlalchemy import ForeignKey, String
+from sqlalchemy.orm import Mapped, mapped_column
+
+from .dao import BaseDAO
+
+
+class ItemType(BaseDAO):
+	filename = "ItemTypes.txt"
+	__tablename__ = "item_type"
+
+	Id: Mapped[int] = mapped_column(primary_key=True)
+	Code: Mapped[str] = mapped_column(String(4))  # referred to by other tables
+	ItemType: Mapped[Optional[str]]               # internal human readable name
+
+	Equiv1 = mapped_column(ForeignKey("item_type.Code"))
+	Equiv2 = mapped_column(ForeignKey("item_type.Code"))
+
+	# what does Rarity do here? fileguide description is unclear
+
+	def is_valid(self):
+		return is_int(self.Id) and self.Code not in (None, '')
+
+
+def is_int(obj):
+	try:
+		int(obj)
+	except ValueError:
+		return False
+	else:
+		return True
diff --git a/dropcalc/mxl_types/set.py b/dropcalc/mxl_types/set.py
new file mode 100644
index 0000000..498cdcf
--- /dev/null
+++ b/dropcalc/mxl_types/set.py
@@ -0,0 +1,15 @@
+from sqlalchemy import ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column
+
+from . import BaseDAO
+
+
+class SetItem(BaseDAO):
+	filename = "SetItems.txt"
+	__tablename__ = "set_item"
+
+	id: Mapped[int] = mapped_column(primary_key=True)
+	index: Mapped[str]   # string reference, often also human-readable
+	item = mapped_column(ForeignKey("base.code"))  # note inconsistency with everything else...
+	rarity: Mapped[int]  # drop frequency, relative to other sets on this base
+	lvl: Mapped[int]     # minimum drop level
diff --git a/dropcalc/mxl_types/treasure_class.py b/dropcalc/mxl_types/treasure_class.py
new file mode 100644
index 0000000..d00da6f
--- /dev/null
+++ b/dropcalc/mxl_types/treasure_class.py
@@ -0,0 +1,46 @@
+from typing import Optional
+
+from sqlalchemy.orm import Mapped, mapped_column
+
+from . import BaseDAO
+
+
+class TreasureClass(BaseDAO):
+	filename = "TreasureClassEx.txt"
+	__csv_to_db__ = {"name": "Treasure Class"}
+	__tablename__ = "treasure_class"
+
+	name: Mapped[str] = mapped_column(primary_key=True)
+
+	group: Mapped[Optional[int]]
+	level: Mapped[Optional[int]]
+
+	Picks: Mapped[int]
+
+	Unique: Mapped[Optional[str]]
+	Set: Mapped[Optional[int]]
+	Rare: Mapped[Optional[int]]
+	Magic: Mapped[Optional[int]]
+
+	NoDrop: Mapped[Optional[int]]
+
+	Item1: Mapped[str]
+	Prob1: Mapped[int]
+	Item2: Mapped[Optional[str]]
+	Prob2: Mapped[Optional[int]]
+	Item3: Mapped[Optional[str]]
+	Prob3: Mapped[Optional[int]]
+	Item4: Mapped[Optional[str]]
+	Prob4: Mapped[Optional[int]]
+	Item5: Mapped[Optional[str]]
+	Prob5: Mapped[Optional[int]]
+	Item6: Mapped[Optional[str]]
+	Prob6: Mapped[Optional[int]]
+	Item7: Mapped[Optional[str]]
+	Prob7: Mapped[Optional[int]]
+	Item8: Mapped[Optional[str]]
+	Prob8: Mapped[Optional[int]]
+	Item9: Mapped[Optional[str]]
+	Prob9: Mapped[Optional[int]]
+	Item10: Mapped[Optional[str]]
+	Prob10: Mapped[Optional[int]]
diff --git a/dropcalc/mxl_types/unique.py b/dropcalc/mxl_types/unique.py
new file mode 100644
index 0000000..49c4e39
--- /dev/null
+++ b/dropcalc/mxl_types/unique.py
@@ -0,0 +1,18 @@
+from sqlalchemy import ForeignKey
+from sqlalchemy.orm import Mapped, mapped_column
+
+from . import BaseDAO
+
+
+class UniqueItem(BaseDAO):
+	filename = "UniqueItems.txt"
+	__tablename__ = "unique_item"
+
+	id: Mapped[int] = mapped_column(primary_key=True)
+	index: Mapped[str]   # string reference, often also human-readable
+	code = mapped_column(ForeignKey("base.code"))
+	rarity: Mapped[int]  # drop frequency, relative to other uniques on this base
+	lvl: Mapped[int]     # minimum drop level
+
+	def is_valid(self):
+		return self.enabled == "1"  # TODO: can we create fixed drop but not randomly available uniques by setting this to 0?
diff --git a/dropcalc/util.py b/dropcalc/util.py
new file mode 100644
index 0000000..923765b
--- /dev/null
+++ b/dropcalc/util.py
@@ -0,0 +1,4 @@
+def is_int(x):
+    try: int(x)
+    except ValueError: return False
+    else: return True