Spaces:
Running
Running
import streamlit as st # π Streamlit magic | |
import streamlit.components.v1 as components # πΌοΈ Embed custom HTML/JS | |
import os # π File operations | |
import json # π JSON encoding/decoding | |
import pandas as pd # π DataFrame handling | |
import uuid # π Unique IDs | |
import math # β Math utils | |
import time # β³ Time utilities | |
from gamestate import GameState # πΌ Shared game-state singleton | |
# π Page setup | |
st.set_page_config(page_title="Infinite World Builder", layout="wide") | |
# π Constants for world dimensions & CSV schema | |
SAVE_DIR = "saved_worlds" | |
PLOT_WIDTH = 50.0 # βοΈ Plot width in world units | |
PLOT_DEPTH = 50.0 # βοΈ Plot depth in world units | |
CSV_COLUMNS = [ | |
'obj_id', 'type', | |
'pos_x', 'pos_y', 'pos_z', | |
'rot_x', 'rot_y', 'rot_z', 'rot_order' | |
] | |
# ποΈ Ensure directory for plots exists | |
os.makedirs(SAVE_DIR, exist_ok=True) | |
# π Cache for 1h | |
def load_plot_metadata(): | |
# π Scan SAVE_DIR for plot CSVs | |
try: | |
plot_files = [f for f in os.listdir(SAVE_DIR) | |
if f.endswith(".csv") and f.startswith("plot_X")] | |
except FileNotFoundError: | |
st.error(f"Folder '{SAVE_DIR}' missing! π¨") | |
return [] | |
except Exception as e: | |
st.error(f"Error reading '{SAVE_DIR}': {e}") | |
return [] | |
parsed = [] | |
for fn in plot_files: | |
try: | |
parts = fn[:-4].split('_') # strip .csv | |
gx = int(parts[1][1:]) # X index | |
gz = int(parts[2][1:]) # Z index | |
name = " ".join(parts[3:]) if len(parts)>3 else f"Plot({gx},{gz})" | |
parsed.append({ | |
'id': fn[:-4], | |
'filename': fn, | |
'grid_x': gx, | |
'grid_z': gz, | |
'name': name, | |
'x_offset': gx * PLOT_WIDTH, | |
'z_offset': gz * PLOT_DEPTH | |
}) | |
except Exception: | |
st.warning(f"Skip invalid file: {fn}") | |
parsed.sort(key=lambda p: (p['grid_x'], p['grid_z'])) | |
return parsed | |
def load_plot_objects(filename, x_offset, z_offset): | |
# π₯ Load objects from a plot CSV and shift by offsets | |
path = os.path.join(SAVE_DIR, filename) | |
try: | |
df = pd.read_csv(path) | |
# π‘οΈ Ensure essentials exist | |
if not all(c in df.columns for c in ['type','pos_x','pos_y','pos_z']): | |
st.warning(f"Missing cols in {filename} π§") | |
return [] | |
# π Guarantee obj_id | |
df['obj_id'] = df.get('obj_id', pd.Series([str(uuid.uuid4()) for _ in df.index])) | |
# π Fill missing rotation | |
for col, default in [('rot_x',0.0),('rot_y',0.0),('rot_z',0.0),('rot_order','XYZ')]: | |
if col not in df.columns: | |
df[col] = default | |
objs = [] | |
for _, row in df.iterrows(): | |
o = row.to_dict() | |
o['pos_x'] += x_offset | |
o['pos_z'] += z_offset | |
objs.append(o) | |
return objs | |
except FileNotFoundError: | |
st.error(f"CSV not found: {filename}") | |
return [] | |
except pd.errors.EmptyDataError: | |
return [] | |
except Exception as e: | |
st.error(f"Load error {filename}: {e}") | |
return [] | |
def save_plot_data(filename, objects_list, px, pz): | |
# πΎ Save list of new objects relative to plot origin | |
path = os.path.join(SAVE_DIR, filename) | |
if not isinstance(objects_list, list): | |
st.error("π Invalid data format for save") | |
return False | |
rel = [] | |
for o in objects_list: | |
pos = o.get('position', {}) | |
rot = o.get('rotation', {}) | |
typ = o.get('type','Unknown') | |
oid = o.get('obj_id', str(uuid.uuid4())) | |
# π Skip bad objects | |
if not all(k in pos for k in ['x','y','z']) or typ=='Unknown': | |
continue | |
rel.append({ | |
'obj_id': oid, 'type': typ, | |
'pos_x': pos['x']-px, 'pos_y': pos['y'], 'pos_z': pos['z']-pz, | |
'rot_x': rot.get('_x',0.0), 'rot_y': rot.get('_y',0.0), | |
'rot_z': rot.get('_z',0.0), 'rot_order': rot.get('_order','XYZ') | |
}) | |
try: | |
pd.DataFrame(rel, columns=CSV_COLUMNS).to_csv(path, index=False) | |
st.success(f"π Saved {len(rel)} to {filename}") | |
return True | |
except Exception as e: | |
st.error(f"Save failed: {e}") | |
return False | |
# π Singleton for global world state | |
def get_game_state(): | |
return GameState(save_dir=SAVE_DIR, csv_filename="world_state.csv") | |
game_state = get_game_state() | |
# π§ Session state defaults | |
st.session_state.setdefault('selected_object','None') | |
st.session_state.setdefault('new_plot_name','') | |
st.session_state.setdefault('js_save_data_result',None) | |
# π Load everything | |
plots_metadata = load_plot_metadata() | |
all_initial_objects = [] | |
for p in plots_metadata: | |
all_initial_objects += load_plot_objects(p['filename'], p['x_offset'], p['z_offset']) | |
# π₯οΈ Sidebar UI | |
with st.sidebar: | |
st.title("ποΈ World Controls") | |
st.header("π Navigate Plots") | |
cols = st.columns(2) | |
i = 0 | |
for p in sorted(plots_metadata, key=lambda x:(x['grid_x'],x['grid_z'])): | |
label = f"β‘οΈ {p['name']} ({p['grid_x']},{p['grid_z']})" | |
if cols[i].button(label, key=f"nav_{p['id']}"): | |
try: | |
from streamlit_js_eval import streamlit_js_eval | |
js = f"teleportPlayer({p['x_offset']+PLOT_WIDTH/2},{p['z_offset']+PLOT_DEPTH/2});" | |
streamlit_js_eval(js_code=js, key=f"tp_{p['id']}") | |
except Exception as e: | |
st.error(f"TP fail: {e}") | |
i = (i+1)%2 | |
st.markdown("---") | |
st.header("π² Place Objects") | |
opts = ["None","Simple House","Tree","Rock","Fence Post"] | |
idx = opts.index(st.session_state.selected_object) if st.session_state.selected_object in opts else 0 | |
sel = st.selectbox("Select:", opts, index=idx, key="selected_object_widget") | |
if sel != st.session_state.selected_object: | |
st.session_state.selected_object = sel | |
st.markdown("---") | |
st.header("πΎ Save Work") | |
if st.button("πΎ Save Current Work", key="save_button"): | |
from streamlit_js_eval import streamlit_js_eval | |
streamlit_js_eval(js_code="getSaveDataAndPosition();", key="js_save_processor") | |
st.rerun() | |
# π¨ Handle incoming save data | |
raw = st.session_state.get("js_save_processor") | |
if raw: | |
st.info("π¬ Got save data!") | |
ok=False | |
try: | |
pay = json.loads(raw) if isinstance(raw,str) else raw | |
pos, objs = pay.get('playerPosition'), pay.get('objectsToSave') | |
if isinstance(objs,list) and pos: | |
gx, gz = math.floor(pos['x']/PLOT_WIDTH), math.floor(pos['z']/PLOT_DEPTH) | |
fn = f"plot_X{gx}_Z{gz}.csv" | |
if save_plot_data(fn, objs, gx*PLOT_WIDTH, gz*PLOT_DEPTH): | |
load_plot_metadata.clear() | |
try: | |
from streamlit_js_eval import streamlit_js_eval | |
streamlit_js_eval(js_code="resetNewlyPlacedObjects();", key="reset_js") | |
except: | |
pass | |
game_state.update_state(objs) | |
ok=True | |
if not ok: | |
st.error("β Save error") | |
except Exception as e: | |
st.error(f"Err: {e}") | |
st.session_state.js_save_processor=None | |
if ok: st.rerun() | |
# π Main view | |
st.header("π Infinite Shared 3D World") | |
st.caption("β‘οΈ Explore, click to build, πΎ to save!") | |
# π Inject state into JS | |
state = { | |
"ALL_INITIAL_OBJECTS": all_initial_objects, | |
"PLOTS_METADATA": plots_metadata, | |
"SELECTED_OBJECT_TYPE": st.session_state.selected_object, | |
"PLOT_WIDTH": PLOT_WIDTH, | |
"PLOT_DEPTH": PLOT_DEPTH, | |
"GAME_STATE": game_state.get_state() | |
} | |
try: | |
with open('index.html','r',encoding='utf-8') as f: | |
html = f.read() | |
script = f""" | |
<script> | |
window.ALL_INITIAL_OBJECTS = {json.dumps(state['ALL_INITIAL_OBJECTS'])}; | |
window.PLOTS_METADATA = {json.dumps(state['PLOTS_METADATA'])}; | |
window.SELECTED_OBJECT_TYPE = {json.dumps(state['SELECTED_OBJECT_TYPE'])}; | |
window.PLOT_WIDTH = {json.dumps(state['PLOT_WIDTH'])}; | |
window.PLOT_DEPTH = {json.dumps(state['PLOT_DEPTH'])}; | |
window.GAME_STATE = {json.dumps(state['GAME_STATE'])}; | |
console.log('π State injected!', {{ | |
objs: window.ALL_INITIAL_OBJECTS.length, | |
plots: window.PLOTS_METADATA.length, | |
gs: window.GAME_STATE.length | |
}}); | |
</script> | |
""" | |
html = html.replace('</head>', script + '\n</head>', 1) | |
components.html(html, height=750, scrolling=False) | |
except FileNotFoundError: | |
st.error("β index.html missing!") | |
except Exception as e: | |
st.error(f"π± HTML inject failed: {e}") | |