Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -11,6 +11,7 @@ import pandas as pd
|
|
11 |
import uuid
|
12 |
import math
|
13 |
import time
|
|
|
14 |
|
15 |
# Import our GameState class from gamestate.py
|
16 |
from gamestate import GameState
|
@@ -26,6 +27,63 @@ CSV_COLUMNS = ['obj_id', 'type', 'pos_x', 'pos_y', 'pos_z', 'rot_x', 'rot_y', 'r
|
|
26 |
os.makedirs(SAVE_DIR, exist_ok=True)
|
27 |
os.makedirs(GLOBAL_SAVES_DIR, exist_ok=True)
|
28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
@st.cache_data(ttl=3600)
|
30 |
def load_plot_metadata():
|
31 |
"""Scans SAVE_DIR for plot files and returns metadata."""
|
@@ -42,9 +100,9 @@ def load_plot_metadata():
|
|
42 |
parsed_plots = []
|
43 |
for filename in plot_files:
|
44 |
try:
|
45 |
-
parts = filename[:-4].split('_')
|
46 |
-
grid_x = int(parts[1][1:])
|
47 |
-
grid_z = int(parts[2][1:])
|
48 |
plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
|
49 |
parsed_plots.append({
|
50 |
'id': filename[:-4],
|
@@ -110,14 +168,14 @@ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
|
|
110 |
continue
|
111 |
|
112 |
relative_obj = {
|
113 |
-
'obj_id': obj_id,
|
114 |
'type': obj_type,
|
115 |
'pos_x': pos.get('x', 0.0) - plot_x_offset,
|
116 |
'pos_y': pos.get('y', 0.0),
|
117 |
'pos_z': pos.get('z', 0.0) - plot_z_offset,
|
118 |
-
'rot_x': rot.get('_x', 0.0),
|
119 |
'rot_y': rot.get('_y', 0.0),
|
120 |
-
'rot_z': rot.get('_z', 0.0),
|
121 |
'rot_order': rot.get('_order', 'XYZ')
|
122 |
}
|
123 |
relative_objects.append(relative_obj)
|
@@ -134,7 +192,6 @@ def save_plot_data(filename, objects_data_list, plot_x_offset, plot_z_offset):
|
|
134 |
# --- Initialize GameState Singleton ---
|
135 |
@st.cache_resource
|
136 |
def get_game_state():
|
137 |
-
# This instance is shared across all sessions and reruns.
|
138 |
return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
|
139 |
|
140 |
game_state = get_game_state()
|
@@ -147,17 +204,24 @@ if 'new_plot_name' not in st.session_state:
|
|
147 |
if 'js_save_data_result' not in st.session_state:
|
148 |
st.session_state.js_save_data_result = None
|
149 |
if 'player_position' not in st.session_state:
|
150 |
-
# This can be updated from JS if needed
|
151 |
st.session_state.player_position = {"x": 0, "y": 0, "z": 0}
|
152 |
if 'loaded_global_state' not in st.session_state:
|
153 |
st.session_state.loaded_global_state = None
|
154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
155 |
plots_metadata = load_plot_metadata()
|
156 |
all_initial_objects = []
|
157 |
for plot in plots_metadata:
|
158 |
all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
|
159 |
|
160 |
-
#
|
161 |
if not game_state.get_state():
|
162 |
game_state.update_state(all_initial_objects)
|
163 |
|
@@ -214,10 +278,12 @@ with st.sidebar:
|
|
214 |
with open(save_file_path, "w", encoding="utf-8") as f:
|
215 |
json.dump(global_save_data, f, indent=2)
|
216 |
st.success(f"Global state saved to {default_save_name}")
|
217 |
-
# Also store it in session state for download purposes.
|
218 |
st.session_state.loaded_global_state = global_save_data
|
|
|
|
|
|
|
|
|
219 |
|
220 |
-
# List global save files available in the folder.
|
221 |
st.subheader("📂 Global Saves")
|
222 |
save_files = sorted([f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")])
|
223 |
for file in save_files:
|
@@ -225,7 +291,6 @@ with st.sidebar:
|
|
225 |
file_path = os.path.join(GLOBAL_SAVES_DIR, file)
|
226 |
with open(file_path, "r", encoding="utf-8") as f:
|
227 |
loaded_save = json.load(f)
|
228 |
-
# Update the shared game state with loaded data.
|
229 |
with game_state.lock:
|
230 |
game_state.world_state = loaded_save.get("game_state", [])
|
231 |
st.session_state.player_position = loaded_save.get("player_position", {"x": 0, "y": 0, "z": 0})
|
@@ -234,7 +299,6 @@ with st.sidebar:
|
|
234 |
|
235 |
st.markdown("---")
|
236 |
st.header("Download Global Save as Markdown")
|
237 |
-
# Use the loaded global state if available; otherwise use a default dictionary.
|
238 |
current_save = st.session_state.get("loaded_global_state")
|
239 |
if current_save is None:
|
240 |
current_save = {"timestamp": "N/A", "game_state": [], "player_position": {"x": 0, "y": 0, "z": 0}}
|
@@ -284,7 +348,6 @@ if save_data_from_js is not None:
|
|
284 |
st.success(f"New plot created and saved: {target_filename}")
|
285 |
else:
|
286 |
st.success(f"Updated existing plot: {target_filename}")
|
287 |
-
# Update shared game state with new objects from this session
|
288 |
game_state.update_state(objects_to_save)
|
289 |
save_processed_successfully = True
|
290 |
else:
|
@@ -321,8 +384,6 @@ html_content_with_state = None
|
|
321 |
try:
|
322 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
323 |
html_template = f.read()
|
324 |
-
|
325 |
-
# Use doubled curly braces for literal braces in the injected JS code.
|
326 |
js_injection_script = f"""
|
327 |
<script>
|
328 |
window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
|
|
|
11 |
import uuid
|
12 |
import math
|
13 |
import time
|
14 |
+
from datetime import datetime
|
15 |
|
16 |
# Import our GameState class from gamestate.py
|
17 |
from gamestate import GameState
|
|
|
27 |
os.makedirs(SAVE_DIR, exist_ok=True)
|
28 |
os.makedirs(GLOBAL_SAVES_DIR, exist_ok=True)
|
29 |
|
30 |
+
# --- Helper Functions for Global Save Management ---
|
31 |
+
|
32 |
+
def load_latest_global_save():
|
33 |
+
"""
|
34 |
+
Load the JSON file with the most recent timestamp from GLOBAL_SAVES_DIR.
|
35 |
+
Returns None if no global save exists.
|
36 |
+
"""
|
37 |
+
json_files = [f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")]
|
38 |
+
if not json_files:
|
39 |
+
return None
|
40 |
+
# Assuming filenames contain timestamp info, sort them in reverse order.
|
41 |
+
json_files.sort(reverse=True)
|
42 |
+
latest_file = json_files[0]
|
43 |
+
with open(os.path.join(GLOBAL_SAVES_DIR, latest_file), "r", encoding="utf-8") as f:
|
44 |
+
return json.load(f)
|
45 |
+
|
46 |
+
def consolidate_global_saves():
|
47 |
+
"""
|
48 |
+
Merge all JSON files in GLOBAL_SAVES_DIR into one consolidated file and delete older ones.
|
49 |
+
The union of game objects (using unique 'obj_id's) is computed.
|
50 |
+
Returns the consolidated state.
|
51 |
+
"""
|
52 |
+
json_files = [f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")]
|
53 |
+
if not json_files:
|
54 |
+
return None
|
55 |
+
merged_state = {"timestamp": "", "game_state": [], "player_position": {"x": 0, "y": 0, "z": 0}}
|
56 |
+
# Use a dictionary keyed by obj_id to union objects.
|
57 |
+
obj_dict = {}
|
58 |
+
latest_timestamp = None
|
59 |
+
latest_player_position = {"x": 0, "y": 0, "z": 0}
|
60 |
+
for f in json_files:
|
61 |
+
with open(os.path.join(GLOBAL_SAVES_DIR, f), "r", encoding="utf-8") as file:
|
62 |
+
data = json.load(file)
|
63 |
+
for obj in data.get("game_state", []):
|
64 |
+
obj_id = obj.get("obj_id")
|
65 |
+
if obj_id:
|
66 |
+
obj_dict[obj_id] = obj
|
67 |
+
file_timestamp = data.get("timestamp")
|
68 |
+
if not latest_timestamp or file_timestamp > latest_timestamp:
|
69 |
+
latest_timestamp = file_timestamp
|
70 |
+
latest_player_position = data.get("player_position", {"x": 0, "y": 0, "z": 0})
|
71 |
+
merged_state["timestamp"] = latest_timestamp if latest_timestamp else time.strftime("%Y-%m-%d %H:%M:%S")
|
72 |
+
merged_state["game_state"] = list(obj_dict.values())
|
73 |
+
merged_state["player_position"] = latest_player_position
|
74 |
+
# Write the consolidated state with current timestamp.
|
75 |
+
consolidated_filename = f"consolidated_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
76 |
+
consolidated_path = os.path.join(GLOBAL_SAVES_DIR, consolidated_filename)
|
77 |
+
with open(consolidated_path, "w", encoding="utf-8") as f:
|
78 |
+
json.dump(merged_state, f, indent=2)
|
79 |
+
# Delete all old JSON files except the newly consolidated one.
|
80 |
+
for f in json_files:
|
81 |
+
try:
|
82 |
+
os.remove(os.path.join(GLOBAL_SAVES_DIR, f))
|
83 |
+
except Exception as e:
|
84 |
+
st.error(f"Error deleting old global save {f}: {e}")
|
85 |
+
return merged_state
|
86 |
+
|
87 |
@st.cache_data(ttl=3600)
|
88 |
def load_plot_metadata():
|
89 |
"""Scans SAVE_DIR for plot files and returns metadata."""
|
|
|
100 |
parsed_plots = []
|
101 |
for filename in plot_files:
|
102 |
try:
|
103 |
+
parts = filename[:-4].split('_')
|
104 |
+
grid_x = int(parts[1][1:])
|
105 |
+
grid_z = int(parts[2][1:])
|
106 |
plot_name = " ".join(parts[3:]) if len(parts) > 3 else f"Plot ({grid_x},{grid_z})"
|
107 |
parsed_plots.append({
|
108 |
'id': filename[:-4],
|
|
|
168 |
continue
|
169 |
|
170 |
relative_obj = {
|
171 |
+
'obj_id': obj_id,
|
172 |
'type': obj_type,
|
173 |
'pos_x': pos.get('x', 0.0) - plot_x_offset,
|
174 |
'pos_y': pos.get('y', 0.0),
|
175 |
'pos_z': pos.get('z', 0.0) - plot_z_offset,
|
176 |
+
'rot_x': rot.get('_x', 0.0),
|
177 |
'rot_y': rot.get('_y', 0.0),
|
178 |
+
'rot_z': rot.get('_z', 0.0),
|
179 |
'rot_order': rot.get('_order', 'XYZ')
|
180 |
}
|
181 |
relative_objects.append(relative_obj)
|
|
|
192 |
# --- Initialize GameState Singleton ---
|
193 |
@st.cache_resource
|
194 |
def get_game_state():
|
|
|
195 |
return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv")
|
196 |
|
197 |
game_state = get_game_state()
|
|
|
204 |
if 'js_save_data_result' not in st.session_state:
|
205 |
st.session_state.js_save_data_result = None
|
206 |
if 'player_position' not in st.session_state:
|
|
|
207 |
st.session_state.player_position = {"x": 0, "y": 0, "z": 0}
|
208 |
if 'loaded_global_state' not in st.session_state:
|
209 |
st.session_state.loaded_global_state = None
|
210 |
|
211 |
+
# --- On Client Start: Load Latest Global Save if Available ---
|
212 |
+
latest_global_save = load_latest_global_save()
|
213 |
+
if latest_global_save is not None:
|
214 |
+
with game_state.lock:
|
215 |
+
game_state.world_state = latest_global_save.get("game_state", [])
|
216 |
+
st.session_state.player_position = latest_global_save.get("player_position", {"x": 0, "y": 0, "z": 0})
|
217 |
+
st.session_state.loaded_global_state = latest_global_save
|
218 |
+
|
219 |
plots_metadata = load_plot_metadata()
|
220 |
all_initial_objects = []
|
221 |
for plot in plots_metadata:
|
222 |
all_initial_objects.extend(load_plot_objects(plot['filename'], plot['x_offset'], plot['z_offset']))
|
223 |
|
224 |
+
# If GameState is still empty, update it with initial objects.
|
225 |
if not game_state.get_state():
|
226 |
game_state.update_state(all_initial_objects)
|
227 |
|
|
|
278 |
with open(save_file_path, "w", encoding="utf-8") as f:
|
279 |
json.dump(global_save_data, f, indent=2)
|
280 |
st.success(f"Global state saved to {default_save_name}")
|
|
|
281 |
st.session_state.loaded_global_state = global_save_data
|
282 |
+
# Consolidate all global saves into one file and delete older ones.
|
283 |
+
consolidated_state = consolidate_global_saves()
|
284 |
+
if consolidated_state is not None:
|
285 |
+
st.session_state.loaded_global_state = consolidated_state
|
286 |
|
|
|
287 |
st.subheader("📂 Global Saves")
|
288 |
save_files = sorted([f for f in os.listdir(GLOBAL_SAVES_DIR) if f.endswith(".json")])
|
289 |
for file in save_files:
|
|
|
291 |
file_path = os.path.join(GLOBAL_SAVES_DIR, file)
|
292 |
with open(file_path, "r", encoding="utf-8") as f:
|
293 |
loaded_save = json.load(f)
|
|
|
294 |
with game_state.lock:
|
295 |
game_state.world_state = loaded_save.get("game_state", [])
|
296 |
st.session_state.player_position = loaded_save.get("player_position", {"x": 0, "y": 0, "z": 0})
|
|
|
299 |
|
300 |
st.markdown("---")
|
301 |
st.header("Download Global Save as Markdown")
|
|
|
302 |
current_save = st.session_state.get("loaded_global_state")
|
303 |
if current_save is None:
|
304 |
current_save = {"timestamp": "N/A", "game_state": [], "player_position": {"x": 0, "y": 0, "z": 0}}
|
|
|
348 |
st.success(f"New plot created and saved: {target_filename}")
|
349 |
else:
|
350 |
st.success(f"Updated existing plot: {target_filename}")
|
|
|
351 |
game_state.update_state(objects_to_save)
|
352 |
save_processed_successfully = True
|
353 |
else:
|
|
|
384 |
try:
|
385 |
with open(html_file_path, 'r', encoding='utf-8') as f:
|
386 |
html_template = f.read()
|
|
|
|
|
387 |
js_injection_script = f"""
|
388 |
<script>
|
389 |
window.ALL_INITIAL_OBJECTS = {json.dumps(injected_state["ALL_INITIAL_OBJECTS"])};
|