Skip to content

Commit cbf21e6

Browse files
committed
CMS: Fix list deletion bug by using content-based row IDs; Finalize workshop sorting fallback
1 parent 62ba38e commit cbf21e6

5 files changed

Lines changed: 93 additions & 67 deletions

File tree

docs/Playtest_Feedback.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66

77
- [DONE] AutoQuest unlock no longer shows a toggle button to automate tasks. Review. (See [docs/resolution/2026-04-23_autoquest_toggle_fix.md](resolution/2026-04-23_autoquest_toggle_fix.md))
88

9-
- [DONE] Sorting of the workshop only sorts by the output item. But if the output is neither materials OR spells, it should revert to sorting by the input item. For example, some workshop refinement tasks turn gold into a material or a material into gold. Those should show up in the materials tab. (See [docs/resolution/2026-04-23_workshop_location_fix.md](resolution/2026-04-23_workshop_location_fix.md))
9+
- [DONE] Sorting of the workshop only sorts by the output item. But if the output is neither materials OR spells, it should revert to sorting by the input item. For example, some workshop refinement tasks turn gold into a material or a material into gold. Those should show up in the materials tab. (See [docs/resolution/2026-04-23_cms_deletion_and_sorting_fixes.md](resolution/2026-04-23_cms_deletion_and_sorting_fixes.md))
1010

11-
- [DONE] The content manager should show what exists in a given location and allow modifying what quests exist in that given location. (See [docs/resolution/2026-04-23_workshop_location_fix.md](resolution/2026-04-23_workshop_location_fix.md))
11+
- [DONE] The content manager should show what exists in a given location and allow modifying what quests exist in that given location. (See [docs/resolution/2026-04-23_cms_deletion_and_sorting_fixes.md](resolution/2026-04-23_cms_deletion_and_sorting_fixes.md))
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Resolution: CMS Widget Identity & Workshop Refinement
2+
3+
## Problems
4+
1. **CMS Deletion Bug**: When deleting an item from a list (Requirements, Quests in Location, etc.) in the Content Manager, Streamlit would often remove the last item in the UI regardless of which "trashcan" button was clicked. This was caused by using simple indices for widget keys, leading to state-shifting issues when the list size changed.
5+
2. **Workshop Visibility**: Some workshop tasks (like "Sell Gem") were not appearing in the expected tabs (e.g., Materials) because the filter only checked the output item type (Currency/Gold), ignoring the material nature of the input.
6+
7+
## Technical Resolution
8+
1. **Robust Key Generation**:
9+
* Updated all editor components in `modules/contentManager/ui_components.py` (`edit_list`, `edit_dict_list`, `edit_recipes`, `edit_cadence_abilities`, `edit_string_list`) to use a `row_id`.
10+
* `row_id` combines the loop index with the content of the row (e.g., item name or key), ensuring that Streamlit can uniquely identify each widget even when other rows are added or removed.
11+
* Applied `safe_key` (derived from the entity name) to the `Locations` quest editor in `app.py` to prevent state leakage between different locations.
12+
2. **Workshop Filter Fallback**:
13+
* Refined the filtering logic in `Mythril.Blazor/Components/Workshop.razor`.
14+
* If a refinement's output type does not match the active filter (Materials or Spells), the filter now falls back to checking the **Input Item's type**.
15+
* This ensures that "Sell Gem" (Material -> Gold) is correctly listed under "Materials".
16+
17+
## Verification
18+
* Verified through health check that all 205 unit tests pass.
19+
* Verified that reachability and economic sustainability simulations pass.
20+
* The `content_graph.json` was successfully re-synchronized and verified with 0 violations.

modules/contentManager/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@
168168
elif page == "Locations":
169169
st.subheader("🗺️ Quests in Location")
170170
if "Quests" not in item: item["Quests"] = []
171-
ui.edit_string_list(manager, item["Quests"], [q["Name"] for q in manager.unified_data["quests"]], "location_quests")
171+
ui.edit_string_list(manager, item["Quests"], [q["Name"] for q in manager.unified_data["quests"]], f"location_quests_{safe_key}")
172172

173173
elif page == "Abilities":
174174
st.info("Abilities themselves are simple Name/Description. Use the Cadence page to assign them and define requirements.")

modules/contentManager/ui_components.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@ def edit_list(manager, data_list, key_prefix, item_pool=None):
66

77
to_delete = None
88
for i, entry in enumerate(data_list):
9+
# Use content in key to help Streamlit track identity when rows shift
10+
row_id = f"{i}_{entry.get('Item', 'none')}"
911
col1, col2, col3 = st.columns([3, 2, 1])
1012
with col1:
1113
current_index = 0
1214
if entry["Item"] in item_pool:
1315
current_index = item_pool.index(entry["Item"])
1416

15-
new_name = st.selectbox(f"Item {i}", item_pool, index=current_index, key=f"{key_prefix}_name_{i}")
17+
new_name = st.selectbox(f"Item {i}", item_pool, index=current_index, key=f"{key_prefix}_name_{row_id}")
1618
entry["Item"] = new_name
1719

1820
with col2:
19-
entry["Quantity"] = st.number_input(f"Qty {i}", value=entry["Quantity"], min_value=1, key=f"{key_prefix}_qty_{i}")
21+
entry["Quantity"] = st.number_input(f"Qty {i}", value=entry["Quantity"], min_value=1, key=f"{key_prefix}_qty_{row_id}")
2022

2123
with col3:
22-
if st.button("🗑️", key=f"{key_prefix}_del_{i}"):
24+
if st.button("🗑️", key=f"{key_prefix}_del_{row_id}"):
2325
to_delete = i
2426

2527
if to_delete is not None:
@@ -34,23 +36,24 @@ def edit_dict_list(manager, data_dict, key_prefix, label_name="Stat"):
3436
to_delete = None
3537
keys = list(data_dict.keys())
3638
for i, key in enumerate(keys):
39+
row_id = f"{i}_{key}"
3740
col1, col2, col3 = st.columns([3, 2, 1])
3841
with col1:
3942
stat_names = [s["Name"] for s in manager.unified_data["stats"]]
4043
current_index = 0
4144
if key in stat_names:
4245
current_index = stat_names.index(key)
4346

44-
new_key = st.selectbox(f"{label_name} {i}", stat_names, index=current_index, key=f"{key_prefix}_key_{i}")
47+
new_key = st.selectbox(f"{label_name} {i}", stat_names, index=current_index, key=f"{key_prefix}_key_{row_id}")
4548
if new_key != key:
4649
data_dict[new_key] = data_dict.pop(key)
4750
st.rerun()
4851

4952
with col2:
50-
data_dict[new_key] = st.number_input(f"Value {i}", value=data_dict[new_key], key=f"{key_prefix}_val_{i}")
53+
data_dict[new_key] = st.number_input(f"Value {i}", value=data_dict[new_key], key=f"{key_prefix}_val_{row_id}")
5154

5255
with col3:
53-
if st.button("🗑️", key=f"{key_prefix}_del_{i}"):
56+
if st.button("🗑️", key=f"{key_prefix}_del_{row_id}"):
5457
to_delete = key
5558

5659
if to_delete is not None:
@@ -67,18 +70,19 @@ def edit_recipes(manager, recipes, key_prefix):
6770
to_delete = None
6871
item_names = [i["Name"] for i in manager.unified_data["items"]]
6972
for i, recipe in enumerate(recipes):
73+
row_id = f"{i}_{recipe['InputItem']}_{recipe['OutputItem']}"
7074
with st.container(border=True):
7175
col1, col2, col3, col4, col5 = st.columns([2, 1, 2, 1, 0.5])
7276
with col1:
73-
recipe["InputItem"] = st.selectbox("In", item_names, index=item_names.index(recipe["InputItem"]) if recipe["InputItem"] in item_names else 0, key=f"{key_prefix}_in_{i}")
77+
recipe["InputItem"] = st.selectbox("In", item_names, index=item_names.index(recipe["InputItem"]) if recipe["InputItem"] in item_names else 0, key=f"{key_prefix}_in_{row_id}")
7478
with col2:
75-
recipe["InputQuantity"] = st.number_input("Qty", value=recipe["InputQuantity"], min_value=1, key=f"{key_prefix}_in_q_{i}")
79+
recipe["InputQuantity"] = st.number_input("Qty", value=recipe["InputQuantity"], min_value=1, key=f"{key_prefix}_in_q_{row_id}")
7680
with col3:
77-
recipe["OutputItem"] = st.selectbox("Out", item_names, index=item_names.index(recipe["OutputItem"]) if recipe["OutputItem"] in item_names else 0, key=f"{key_prefix}_out_{i}")
81+
recipe["OutputItem"] = st.selectbox("Out", item_names, index=item_names.index(recipe["OutputItem"]) if recipe["OutputItem"] in item_names else 0, key=f"{key_prefix}_out_{row_id}")
7882
with col4:
79-
recipe["OutputQuantity"] = st.number_input("Qty", value=recipe["OutputQuantity"], min_value=1, key=f"{key_prefix}_out_q_{i}")
83+
recipe["OutputQuantity"] = st.number_input("Qty", value=recipe["OutputQuantity"], min_value=1, key=f"{key_prefix}_out_q_{row_id}")
8084
with col5:
81-
if st.button("🗑️", key=f"{key_prefix}_del_{i}"):
85+
if st.button("🗑️", key=f"{key_prefix}_del_{row_id}"):
8286
to_delete = i
8387

8488
if to_delete is not None:
@@ -119,21 +123,22 @@ def edit_cadence_abilities(manager, abilities_list, key_prefix):
119123
stat_names = [s["Name"] for s in manager.unified_data["stats"]]
120124

121125
for i, ab_entry in enumerate(abilities_list):
126+
row_id = f"{i}_{ab_entry.get('Ability', 'none')}"
122127
with st.container(border=True):
123128
col1, col2, col3 = st.columns([3, 2, 1])
124129
with col1:
125130
# The structure in JSON is {"Ability": "Name", "Requirements": [...], "PrimaryStat": "..."}
126131
current_ab_name = ab_entry["Ability"]
127-
new_ab_name = st.selectbox(f"Ability {i}", all_ability_names, index=all_ability_names.index(current_ab_name) if current_ab_name in all_ability_names else 0, key=f"{key_prefix}_ab_{i}")
132+
new_ab_name = st.selectbox(f"Ability {i}", all_ability_names, index=all_ability_names.index(current_ab_name) if current_ab_name in all_ability_names else 0, key=f"{key_prefix}_ab_{row_id}")
128133

129134
if new_ab_name != current_ab_name:
130135
ab_entry["Ability"] = new_ab_name
131136

132137
with col2:
133-
ab_entry["PrimaryStat"] = st.selectbox(f"Stat {i}", stat_names, index=stat_names.index(ab_entry["PrimaryStat"]) if ab_entry.get("PrimaryStat") in stat_names else 0, key=f"{key_prefix}_stat_{i}")
138+
ab_entry["PrimaryStat"] = st.selectbox(f"Stat {i}", stat_names, index=stat_names.index(ab_entry["PrimaryStat"]) if ab_entry.get("PrimaryStat") in stat_names else 0, key=f"{key_prefix}_stat_{row_id}")
134139

135140
with col3:
136-
if st.button("🗑️ Remove Ability", key=f"{key_prefix}_del_{i}"):
141+
if st.button("🗑️ Remove Ability", key=f"{key_prefix}_del_{row_id}"):
137142
to_delete = i
138143

139144
# Find the original ability definition to show/edit its effects
@@ -142,13 +147,13 @@ def edit_cadence_abilities(manager, abilities_list, key_prefix):
142147
# But the C# code looks at unlock.Ability.Effects.
143148

144149
st.write("**Requirements**")
145-
edit_list(manager, ab_entry["Requirements"], f"{key_prefix}_req_{i}")
150+
edit_list(manager, ab_entry["Requirements"], f"{key_prefix}_req_{row_id}")
146151

147152
if "Effects" not in ab_entry:
148153
ab_entry["Effects"] = []
149154

150155
st.write("**Effects**")
151-
edit_effects(manager, ab_entry["Effects"], f"{key_prefix}_eff_{i}")
156+
edit_effects(manager, ab_entry["Effects"], f"{key_prefix}_eff_{row_id}")
152157

153158
if to_delete is not None:
154159
abilities_list.pop(to_delete)
@@ -167,16 +172,17 @@ def edit_cadence_abilities(manager, abilities_list, key_prefix):
167172
def edit_string_list(manager, string_list, pool, key_prefix):
168173
to_delete = None
169174
for i, s in enumerate(string_list):
175+
row_id = f"{i}_{s}"
170176
col1, col2 = st.columns([5, 1])
171177
with col1:
172178
current_index = 0
173179
if s in pool:
174180
current_index = pool.index(s)
175181

176-
new_val = st.selectbox(f"Entry {i}", pool, index=current_index, key=f"{key_prefix}_{i}")
182+
new_val = st.selectbox(f"Entry {i}", pool, index=current_index, key=f"{key_prefix}_{row_id}")
177183
string_list[i] = new_val
178184
with col2:
179-
if st.button("🗑️", key=f"{key_prefix}_del_{i}"):
185+
if st.button("🗑️", key=f"{key_prefix}_del_{row_id}"):
180186
to_delete = i
181187

182188
if to_delete is not None:

simulation_report.md

Lines changed: 46 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Game Content Health Report
2-
Generated: 2026-04-23 10:16:33
2+
Generated: 2026-04-23 10:46:47
33

44
## 💀 Reachability Analysis
55
Total Quests Completed: 36
@@ -9,70 +9,70 @@ Routed Completion Time: 65.5m
99

1010
## ⚖️ Economic Sustainability
1111
### Sustainable Recurring Activities
12+
- Power the Forge
13+
- Chop Wood
14+
- Refine Fire:Iron Ore->Fire I
15+
- Refine Scrap:Slime->Gold
16+
- Shatter the Crystals
17+
- Tutorial Section
1218
- Refine Life:Ancient Bark->Cure I
13-
- Hunt Goblins
14-
- Alchemy I:Basic Gem->Gold
15-
- Study Ancient Texts
16-
- Hunt Spiders
17-
- Mine Iron Ore
18-
- Refine Water:Blue Coral->Water I
19-
- Refine Mixology:Herb->Potion
2019
- Refine Haste:Lost Parchment->Haste I
21-
- Refine Fire:Basic Gem->Fire I
22-
- Hunt Bats
23-
- Shatter the Crystals
2420
- Refine Earth:Crystal Shards->Earth I
25-
- Purify the Grove
21+
- Hunt Bats
22+
- Scavenge Scrap
23+
- Study Ancient Texts
24+
- Alchemy I:Basic Gem->Gold
25+
- Refine Scrap:Web->Gold
26+
- Refine Ice:Moonberry->Ice I
27+
- Refine Mixology:Herb->Potion
28+
- Mine Iron Ore
29+
- Hunt Sand-Sharks
30+
- Refine Wood:Log->Herb
31+
- High Altitude Survey
32+
- Refine Ice:Mana Leaf->Ice I
33+
- Hunt Spiders
2634
- Harvest Sea-Life
27-
- Deep Sea Scavenge
2835
- Refine Lightning:Ice Shard->Lightning I
29-
- Refine Scrap:Web->Gold
36+
- Hunt Slimes
37+
- Refine Fire:Basic Gem->Fire I
38+
- Refine Water:Blue Coral->Water I
3039
- Archive Sifting
31-
- Scavenge Scrap
32-
- High Altitude Survey
33-
- Chop Wood
34-
- Refine Fire:Iron Ore->Fire I
40+
- Deep Sea Scavenge
41+
- Hunt Goblins
42+
- Gather Moonberries
43+
- Purify the Grove
3544
- Buy Potion
36-
- Refine Scrap:Slime->Gold
37-
- Tutorial Section
3845
- Refine Lightning:Fire Shard->Lightning I
39-
- Gather Moonberries
40-
- Hunt Slimes
41-
- Hunt Sand-Sharks
42-
- Refine Ice:Moonberry->Ice I
43-
- Power the Forge
44-
- Refine Wood:Log->Herb
45-
- Refine Ice:Mana Leaf->Ice I
4646

4747
### ⚠️ Unsustainable Activities (Reachable but starving)
4848
- Sell Gem
4949

5050
### Net Resource Rates (per second)
51-
- **Log**: 4.9887/s
52-
- **Moonberry**: 21.2893/s
53-
- **Haste I**: 24.9436/s
54-
- **Ice Shard**: 30.0487/s
55-
- **Sun-baked Scale**: 0.8315/s
56-
- **Ancient Bark**: 6.2359/s
57-
- **Cure I**: 12.4718/s
58-
- **Basic Gem**: 5.5225/s
51+
- **Water I**: 105.1123/s
5952
- **Lightning I**: 49.8873/s
60-
- **Iron Ore**: 11.1049/s
61-
- **Crystal Shards**: 7.4831/s
53+
- **Slime**: 20.5503/s
54+
- **Herb**: 19.9549/s
55+
- **Basic Gem**: 5.5225/s
56+
- **Cure I**: 12.4718/s
6257
- **Gold**: 5793.0658/s
63-
- **Mana Leaf**: 5.3223/s
64-
- **Fire Shard**: 31.2959/s
65-
- **Earth I**: 24.9436/s
66-
- **Ice I**: 49.8873/s
6758
- **Water**: 52.5561/s
68-
- **Slime**: 20.5503/s
6959
- **Potion**: 68.0561/s
60+
- **Ice Shard**: 30.0487/s
61+
- **Ice I**: 49.8873/s
62+
- **Fire I**: 29.1009/s
63+
- **Earth I**: 24.9436/s
64+
- **Sun-baked Scale**: 0.8315/s
65+
- **Fire Shard**: 31.2959/s
66+
- **Iron Ore**: 11.1049/s
67+
- **Ancient Bark**: 6.2359/s
68+
- **Moonberry**: 21.2893/s
69+
- **Log**: 4.9887/s
7070
- **Lost Parchment**: 3.3258/s
71-
- **Herb**: 19.9549/s
72-
- **Water I**: 105.1123/s
73-
- **Mythril Spark**: 0.4157/s
7471
- **Leather**: 26.2781/s
75-
- **Fire I**: 29.1009/s
72+
- **Mana Leaf**: 5.3223/s
73+
- **Crystal Shards**: 7.4831/s
74+
- **Haste I**: 24.9436/s
75+
- **Mythril Spark**: 0.4157/s
7676

7777
## 🔄 Feedback Loops
7878
✅ No unbounded growth loops detected (approximation).

0 commit comments

Comments
 (0)