Spaces:
Sleeping
Sleeping
| 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}") | |