import os import json import atexit from flask import Flask, Response, request from flask_socketio import SocketIO, emit, join_room, leave_room app = Flask(__name__) app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'your_default_secret_key') socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading') CHUNK_SIZE = 16 WORLD_FILE = 'world_data.json' server_world = {} server_players = {} server_player_count = 0 LAND_HEIGHT = 4 SEA_LEVEL = 2 def get_chunk_key(x, z): return f"{int(x // CHUNK_SIZE)},{int(z // CHUNK_SIZE)}" def get_block_server(wx, wy, wz): chunk_key = get_chunk_key(wx, wz) chunk = server_world.get(chunk_key) if not chunk: return None lx = int((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE lz = int((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE return chunk['blocks'].get(str(lx), {}).get(str(wy), {}).get(str(lz), None) def set_block_server(wx, wy, wz, block_type): chunk_key = get_chunk_key(wx, wz) if chunk_key not in server_world: server_world[chunk_key] = { 'blocks': {}, 'maxHeight': 0 } chunk = server_world[chunk_key] lx = int((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE lz = int((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE lx, wy, lz = str(lx), str(wy), str(lz) if lx not in chunk['blocks']: chunk['blocks'][lx] = {} if wy not in chunk['blocks'][lx]: chunk['blocks'][lx][wy] = {} old_type = chunk['blocks'][lx][wy].get(lz) if block_type is None: if lz in chunk['blocks'][lx][wy]: del chunk['blocks'][lx][wy][lz] if not chunk['blocks'][lx][wy]: del chunk['blocks'][lx][wy] if not chunk['blocks'][lx]: del chunk['blocks'][lx] else: chunk['blocks'][lx][wy][lz] = block_type chunk['maxHeight'] = max(chunk['maxHeight'], int(wy)) return old_type != block_type def generate_chunk_server(chunkX, chunkZ): chunk_key = f"{chunkX},{chunkZ}" if chunk_key in server_world: return chunk = { 'blocks': {}, 'maxHeight': 0 } for x in range(CHUNK_SIZE): x_str = str(x) chunk['blocks'][x_str] = {} for z in range(CHUNK_SIZE): z_str = str(z) for y in range(LAND_HEIGHT): y_str = str(y) if y_str not in chunk['blocks'][x_str]: chunk['blocks'][x_str][y_str] = {} block_type = 'earth' if y == LAND_HEIGHT - 1: block_type = 'grass' chunk['blocks'][x_str][y_str][z_str] = block_type for y in range(SEA_LEVEL): y_str = str(y) world_x = chunkX * CHUNK_SIZE + x world_z = chunkZ * CHUNK_SIZE + z ck = get_chunk_key(world_x, world_z) c = server_world.get(ck, chunk) lx = int((world_x % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE lz = int((world_z % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE block_exists = c.get('blocks', {}).get(str(lx), {}).get(str(y), {}).get(str(lz)) if block_exists is None: if y_str not in chunk['blocks'][x_str]: chunk['blocks'][x_str][y_str] = {} chunk['blocks'][x_str][y_str][z_str] = 'sea' chunk['maxHeight'] = LAND_HEIGHT - 1 server_world[chunk_key] = chunk def load_world(): global server_world if os.path.exists(WORLD_FILE): try: with open(WORLD_FILE, 'r') as f: server_world = json.load(f) except (json.JSONDecodeError, FileNotFoundError): server_world = {} def save_world(): with open(WORLD_FILE, 'w') as f: json.dump(server_world, f) @socketio.on('connect') def handle_connect(): global server_player_count server_player_count += 1 @socketio.on('disconnect') def handle_disconnect(): global server_player_count server_player_count -= 1 if request.sid in server_players: emit('player_left', {'sid': request.sid}, broadcast=True, include_self=False) del server_players[request.sid] @socketio.on('join') def handle_join(data): sid = request.sid nickname = data.get('nickname', f'Player_{sid[:4]}') initial_pos = {'x': 0, 'y': LAND_HEIGHT * 0.5 + 2.0, 'z': 0} server_players[sid] = { 'nickname': nickname, 'position': initial_pos, 'rotationY': 0, 'blockType': 'grass' } emit('player_joined', { 'sid': sid, 'nickname': nickname, 'position': initial_pos, 'rotationY': 0 }, broadcast=True) other_players = {s: player for s, player in server_players.items() if s != sid} emit('current_players', other_players) initial_chunk_x = int(initial_pos['x'] // CHUNK_SIZE) initial_chunk_z = int(initial_pos['z'] // CHUNK_SIZE) chunk_data_to_send = {} for cx in range(initial_chunk_x - 1, initial_chunk_x + 2): for cz in range(initial_chunk_z - 1, initial_chunk_z + 2): key = f"{cx},{cz}" generate_chunk_server(cx, cz) if key in server_world: chunk_data_to_send[key] = server_world[key] emit('initial_world_data', chunk_data_to_send) @socketio.on('request_chunk') def handle_request_chunk(data): cx = data['chunkX'] cz = data['chunkZ'] key = f"{cx},{cz}" generate_chunk_server(cx, cz) if key in server_world: emit('chunk_data', { 'chunkKey': key, 'chunkData': server_world[key] }) @socketio.on('send_player_state') def handle_player_state(data): sid = request.sid if sid in server_players: player_data = server_players[sid] player_data['position'] = data['position'] player_data['rotationY'] = data['rotationY'] emit('player_state_update', { 'sid': sid, 'position': data['position'], 'rotationY': data['rotationY'] }, broadcast=True, include_self=False) @socketio.on('send_block_change') def handle_block_change(data): sid = request.sid wx = data['wx'] wy = data['wy'] wz = data['wz'] block_type = data['type'] player_pos = server_players[sid]['position'] dist_sq = (player_pos['x'] - wx*1)**2 + (player_pos['y'] - wy*0.5)**2 + (player_pos['z'] - wz*1)**2 if dist_sq > 25*25: return effective_wy = wy if block_type is not None: chunk_key_at_wx_wz = get_chunk_key(wx, wz) chunk_at_wx_wz = server_world.get(chunk_key_at_wx_wz) current_stack_height = 0 if chunk_at_wx_wz: lx = int((wx % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE lz = int((wz % CHUNK_SIZE) + CHUNK_SIZE) % CHUNK_SIZE for h in range(chunk_at_wx_wz['maxHeight'] + 2, -1, -1): if str(h) in chunk_at_wx_wz['blocks'].get(str(lx),{}) and str(lz) in chunk_at_wx_wz['blocks'].get(str(lx),{}).get(str(h),{}): block = chunk_at_wx_wz['blocks'][str(lx)][str(h)][str(lz)] if block is not None and block != 'sea': current_stack_height = h + 1 break effective_wy = max(current_stack_height, SEA_LEVEL if block_type == 'sea' else 0) if set_block_server(wx, effective_wy, wz, block_type): chunk_key_changed = get_chunk_key(wx, wz) emit('block_changed', { 'chunkKey': chunk_key_changed, 'wx': wx, 'wy': effective_wy, 'wz': wz, 'type': block_type }, broadcast=True) html_content = """ Battime - Voxel World Builder

Выберите никнейм

+
Battime Engine v4.3
Управление: WASD/Мышь
На мобильных: Джойстик/Тап

Создать новый блок

""" @app.route('/') def index(): return Response(html_content, mimetype='text/html') if __name__ == '__main__': load_world() atexit.register(save_world) if not server_world: for x in range(-1, 2): for z in range(-1, 2): generate_chunk_server(x, z) port = int(os.environ.get('PORT', 7860)) socketio.run(app, host='0.0.0.0', port=port, debug=False, allow_unsafe_werkzeug=True)