Skip to content

Commit 673f35e

Browse files
committed
Add Streamlit-based Content Manager GUI and runner script
1 parent fc1c7f8 commit 673f35e

3 files changed

Lines changed: 301 additions & 0 deletions

File tree

modules/contentManager/app.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import streamlit as st
2+
import pandas as pd
3+
from data_io import ContentManager
4+
import subprocess
5+
import os
6+
7+
st.set_page_config(page_title="Mythril Content Manager", layout="wide")
8+
9+
if 'manager' not in st.session_state:
10+
st.session_state.manager = ContentManager()
11+
12+
manager = st.session_state.manager
13+
14+
st.sidebar.title("💎 Mythril CMS")
15+
page = st.sidebar.selectbox("Navigate", ["Quests", "Items", "Cadences", "Locations", "Refinements", "Stats"])
16+
17+
if st.sidebar.button("💾 Save All Changes"):
18+
manager.save_all()
19+
st.sidebar.success("Saved to JSON files (Backup created)")
20+
21+
if st.sidebar.button("🔨 Compile & Verify"):
22+
with st.spinner("Compiling content graph..."):
23+
res1 = subprocess.run(["python", "scripts/migrate_to_graph.py"], capture_output=True, text=True)
24+
res2 = subprocess.run(["python", "scripts/verify_graph.py"], capture_output=True, text=True)
25+
26+
if res1.returncode == 0:
27+
st.sidebar.success("Compiled successfully")
28+
else:
29+
st.sidebar.error(f"Migration Error: {res1.stderr}")
30+
31+
if res2.returncode == 0:
32+
st.sidebar.success("Graph verified")
33+
else:
34+
st.sidebar.error(f"Verification Failed:\n{res2.stdout}")
35+
36+
def edit_list(data_list, key_prefix):
37+
new_list = []
38+
for i, item in enumerate(data_list):
39+
col1, col2, col3 = st.columns([3, 2, 1])
40+
with col1:
41+
name = st.selectbox(f"Item {i}", [i["Name"] for i in manager.unified_data["items"]],
42+
index=[i["Name"] for i in manager.unified_data["items"]].index(item["Item"]) if item["Item"] in [i["Name"] for i in manager.unified_data["items"]] else 0,
43+
key=f"{key_prefix}_name_{i}")
44+
with col2:
45+
qty = st.number_input(f"Qty {i}", value=item["Quantity"], min_value=1, key=f"{key_prefix}_qty_{i}")
46+
with col3:
47+
if st.button("🗑️", key=f"{key_prefix}_del_{i}"):
48+
continue
49+
new_list.append({"Item": name, "Quantity": qty})
50+
51+
if st.button("➕ Add Entry", key=f"{key_prefix}_add"):
52+
new_list.append({"Item": manager.unified_data["items"][0]["Name"], "Quantity": 1})
53+
st.rerun()
54+
return new_list
55+
56+
# --- Page Rendering ---
57+
58+
st.title(f"Manage {page}")
59+
60+
data = manager.unified_data[page.lower()]
61+
df = pd.DataFrame(data)
62+
63+
search = st.text_input(f"Search {page}...", "")
64+
if search:
65+
df = df[df['Name'].str.contains(search, case=False)]
66+
67+
selected_name = st.selectbox(f"Select {page} to Edit", ["-- New --"] + df['Name'].tolist())
68+
69+
if selected_name == "-- New --":
70+
if st.button(f"Create New {page}"):
71+
new_item = {"Name": "New " + page, "Description": ""}
72+
if page == "Quests":
73+
new_item.update({"DurationSeconds": 10, "Type": "Single", "Requirements": [], "Rewards": [], "PrimaryStat": "Vitality", "RequiredStats": {}, "StatRewards": {}, "Requires": [], "UnlocksCadences": []})
74+
elif page == "Items":
75+
new_item.update({"ItemType": "Material", "Augments": []})
76+
manager.unified_data[page.lower()].append(new_item)
77+
st.rerun()
78+
79+
else:
80+
item = next(i for i in data if i["Name"] == selected_name)
81+
82+
with st.form(f"edit_{page}_{selected_name}"):
83+
st.subheader(f"Editing: {selected_name}")
84+
85+
name = st.text_input("Name", item["Name"])
86+
description = st.text_area("Description", item.get("Description", ""))
87+
88+
if page == "Quests":
89+
col1, col2, col3 = st.columns(3)
90+
with col1:
91+
duration = st.number_input("Duration (s)", value=item["DurationSeconds"])
92+
with col2:
93+
q_type = st.selectbox("Type", ["Single", "Recurring", "Unlock"], index=["Single", "Recurring", "Unlock"].index(item["Type"]))
94+
with col3:
95+
stat = st.selectbox("Primary Stat", [s["Name"] for s in manager.unified_data["stats"]], index=[s["Name"] for s in manager.unified_data["stats"]].index(item["PrimaryStat"]))
96+
97+
st.write("---")
98+
st.subheader("Unlocks & Dependencies")
99+
requires = st.multiselect("Prerequisite Quests", [q["Name"] for q in manager.unified_data["quests"] if q["Name"] != name], default=item["Requires"])
100+
unlocks_c = st.multiselect("Unlocks Cadences", [c["Name"] for c in manager.unified_data["cadences"]], default=item["UnlocksCadences"])
101+
102+
# Complex nested data (Requirements/Rewards) are hard in standard Streamlit forms
103+
# so we'll just show them as JSON for now or implement outside form
104+
st.info("Requirements and Rewards are managed below the form for technical reasons.")
105+
106+
if page == "Items":
107+
i_type = st.selectbox("Type", ["Material", "Currency", "Consumable", "Spell", "KeyItem"], index=["Material", "Currency", "Consumable", "Spell", "KeyItem"].index(item["ItemType"]))
108+
109+
if st.form_submit_button("Update Basic Info"):
110+
item["Name"] = name
111+
item["Description"] = description
112+
if page == "Quests":
113+
item["DurationSeconds"] = duration
114+
item["Type"] = q_type
115+
item["PrimaryStat"] = stat
116+
item["Requires"] = requires
117+
item["UnlocksCadences"] = unlocks_c
118+
if page == "Items":
119+
item["ItemType"] = i_type
120+
st.success("Updated basic info in memory.")
121+
st.rerun()
122+
123+
# Nested data management outside the form
124+
if page == "Quests":
125+
st.subheader("📦 Requirements")
126+
item["Requirements"] = edit_list(item["Requirements"], "req")
127+
st.subheader("💎 Rewards")
128+
item["Rewards"] = edit_list(item["Rewards"], "rew")
129+
130+
if st.button("🔥 Delete Entity", type="secondary"):
131+
manager.unified_data[page.lower()].remove(item)
132+
st.warning(f"Deleted {selected_name} from memory.")
133+
st.rerun()

modules/contentManager/data_io.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import json
2+
import os
3+
import shutil
4+
from datetime import datetime
5+
6+
DATA_DIR = "Mythril.Blazor/wwwroot/data"
7+
8+
class ContentManager:
9+
def __init__(self, data_dir=DATA_DIR):
10+
self.data_dir = data_dir
11+
self.raw_data = {}
12+
self.unified_data = {
13+
"items": [],
14+
"quests": [],
15+
"cadences": [],
16+
"locations": [],
17+
"refinements": [],
18+
"stats": []
19+
}
20+
self.load_all()
21+
22+
def _load_json(self, filename):
23+
path = os.path.join(self.data_dir, filename)
24+
if not os.path.exists(path):
25+
return []
26+
with open(path, 'r', encoding='utf-8') as f:
27+
return json.load(f)
28+
29+
def load_all(self):
30+
# Load raw files
31+
files = [
32+
"items.json", "stat_augments.json", "quests.json",
33+
"quest_details.json", "quest_unlocks.json",
34+
"quest_cadence_unlocks.json", "cadences.json",
35+
"locations.json", "refinements.json", "stats.json"
36+
]
37+
for f in files:
38+
self.raw_data[f] = self._load_json(f)
39+
40+
self._unify_items()
41+
self._unify_quests()
42+
self._unify_cadences()
43+
self._unify_locations()
44+
self._unify_refinements()
45+
self._unify_stats()
46+
47+
def _unify_items(self):
48+
augments = { a["Item"]: a["Augments"] for a in self.raw_data["stat_augments.json"] }
49+
self.unified_data["items"] = []
50+
for item in self.raw_data["items.json"]:
51+
item_copy = item.copy()
52+
item_copy["Augments"] = augments.get(item["Name"], [])
53+
self.unified_data["items"].append(item_copy)
54+
55+
def _unify_quests(self):
56+
details = { d["Quest"]: d for d in self.raw_data["quest_details.json"] }
57+
unlocks = { u["Quest"]: u["Requires"] for u in self.raw_data["quest_unlocks.json"] }
58+
cadence_unlocks = { q["Quest"]: q["Cadences"] for q in self.raw_data["quest_cadence_unlocks.json"] }
59+
60+
self.unified_data["quests"] = []
61+
for quest in self.raw_data["quests.json"]:
62+
q_name = quest["Name"]
63+
q_detail = details.get(q_name, {})
64+
65+
unified_q = {
66+
"Name": q_name,
67+
"Description": quest.get("Description", ""),
68+
"DurationSeconds": q_detail.get("DurationSeconds", 10),
69+
"Type": q_detail.get("Type", "Single"),
70+
"Requirements": q_detail.get("Requirements", []),
71+
"Rewards": q_detail.get("Rewards", []),
72+
"PrimaryStat": q_detail.get("PrimaryStat", "Vitality"),
73+
"RequiredStats": q_detail.get("RequiredStats", {}),
74+
"StatRewards": q_detail.get("StatRewards", {}),
75+
"Requires": unlocks.get(q_name, []),
76+
"UnlocksCadences": cadence_unlocks.get(q_name, [])
77+
}
78+
self.unified_data["quests"].append(unified_q)
79+
80+
def _unify_cadences(self):
81+
self.unified_data["cadences"] = [c.copy() for c in self.raw_data["cadences.json"]]
82+
83+
def _unify_locations(self):
84+
self.unified_data["locations"] = [l.copy() for l in self.raw_data["locations.json"]]
85+
86+
def _unify_refinements(self):
87+
self.unified_data["refinements"] = [r.copy() for r in self.raw_data["refinements.json"]]
88+
89+
def _unify_stats(self):
90+
self.unified_data["stats"] = [s.copy() for s in self.raw_data["stats.json"]]
91+
92+
def save_all(self):
93+
# Create backup
94+
backup_dir = os.path.join(self.data_dir, "backups", datetime.now().strftime("%Y%m%d_%H%M%S"))
95+
os.makedirs(backup_dir, exist_ok=True)
96+
for f in self.raw_data.keys():
97+
src = os.path.join(self.data_dir, f)
98+
if os.path.exists(src):
99+
shutil.copy(src, os.path.join(backup_dir, f))
100+
101+
# Split unified data back into files
102+
new_items = []
103+
new_augments = []
104+
for item in self.unified_data["items"]:
105+
new_items.append({ "Name": item["Name"], "Description": item["Description"], "ItemType": item["ItemType"] })
106+
if item.get("Augments"):
107+
new_augments.append({ "Item": item["Name"], "Augments": item["Augments"] })
108+
109+
new_quests = []
110+
new_details = []
111+
new_unlocks = []
112+
new_cadence_unlocks = []
113+
for q in self.unified_data["quests"]:
114+
new_quests.append({ "Name": q["Name"], "Description": q["Description"] })
115+
new_details.append({
116+
"Quest": q["Name"],
117+
"DurationSeconds": q["DurationSeconds"],
118+
"Type": q["Type"],
119+
"Requirements": q["Requirements"],
120+
"Rewards": q["Rewards"],
121+
"PrimaryStat": q["PrimaryStat"],
122+
"RequiredStats": q["RequiredStats"],
123+
"StatRewards": q["StatRewards"]
124+
})
125+
if q.get("Requires"):
126+
new_unlocks.append({ "Quest": q["Name"], "Requires": q["Requires"] })
127+
if q.get("UnlocksCadences"):
128+
new_cadence_unlocks.append({ "Quest": q["Name"], "Cadences": q["UnlocksCadences"] })
129+
130+
self._save_json("items.json", new_items)
131+
self._save_json("stat_augments.json", new_augments)
132+
self._save_json("quests.json", new_quests)
133+
self._save_json("quest_details.json", new_details)
134+
self._save_json("quest_unlocks.json", new_unlocks)
135+
self._save_json("quest_cadence_unlocks.json", new_cadence_unlocks)
136+
self._save_json("cadences.json", self.unified_data["cadences"])
137+
self._save_json("locations.json", self.unified_data["locations"])
138+
self._save_json("refinements.json", self.unified_data["refinements"])
139+
self._save_json("stats.json", self.unified_data["stats"])
140+
141+
def _save_json(self, filename, data):
142+
path = os.path.join(self.data_dir, filename)
143+
with open(path, 'w', encoding='utf-8') as f:
144+
json.dump(data, f, indent=2)

scripts/run_content_manager.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import subprocess
2+
import sys
3+
import os
4+
5+
def run():
6+
print("Checking dependencies...")
7+
try:
8+
import streamlit
9+
import pandas
10+
except ImportError:
11+
print("Missing dependencies. Installing streamlit and pandas...")
12+
subprocess.check_call([sys.executable, "-m", "pip", "install", "streamlit", "pandas"])
13+
14+
print("Launching Mythril Content Manager...")
15+
# Change to the module directory to ensure relative imports in app.py work correctly
16+
os.chdir(os.path.join(os.getcwd(), "modules", "contentManager"))
17+
18+
try:
19+
subprocess.run(["streamlit", "run", "app.py"])
20+
except KeyboardInterrupt:
21+
print("\nShutting down Content Manager.")
22+
23+
if __name__ == "__main__":
24+
run()

0 commit comments

Comments
 (0)