549 lines
19 KiB
Python
549 lines
19 KiB
Python
#!/bin/python3
|
|
|
|
"""
|
|
________ ______ ______ __
|
|
| \ / \ / \ | \
|
|
\$$$$$$$$______ ______ ______ ______ | $$$$$$\ ______ ______ | $$$$$$\ _| $$_
|
|
| $$ / \ / \ / \ | \ | $$ \$$ / \ | \ | $$_ \$$| $$ \
|
|
| $$ | $$$$$$\| $$$$$$\| $$$$$$\ \$$$$$$\| $$ | $$$$$$\ \$$$$$$\| $$ \ \$$$$$$
|
|
| $$ | $$ $$| $$ \$$| $$ \$$/ $$| $$ __ | $$ \$$/ $$| $$$$ | $$ __
|
|
| $$ | $$$$$$$$| $$ | $$ | $$$$$$$| $$__/ \| $$ | $$$$$$$| $$ | $$| \
|
|
| $$ \$$ \| $$ | $$ \$$ $$ \$$ $$| $$ \$$ $$| $$ \$$ $$
|
|
\$$ \$$$$$$$ \$$ \$$ \$$$$$$$ \$$$$$$ \$$ \$$$$$$$ \$$ \$$$$
|
|
|
|
|
|
Copyright (C) 2013 Michael Fogleman
|
|
Copyright (C) 2018/2019 Stefano Peris <xenonlab.develop@gmail.com>
|
|
|
|
Github repository: <https://github.com/XenonLab-Studio/TerraCraft>
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import time
|
|
|
|
from collections import deque
|
|
|
|
from pyglet.gl import *
|
|
|
|
from .blocks import *
|
|
from .utilities import *
|
|
|
|
|
|
def iter_neighbors(position):
|
|
"""Iterate all the positions neighboring this position"""
|
|
x, y, z = position
|
|
for face in FACES:
|
|
dx, dy, dz = face
|
|
neighbor = x + dx, y + dy, z + dz
|
|
yield neighbor, face
|
|
|
|
|
|
class Sector:
|
|
"""A sector is a chunk of the world of the size SECTOR_SIZE in each directions.
|
|
|
|
It contains the block description of a sector. As it is initially generated.
|
|
"""
|
|
|
|
def __init__(self, position):
|
|
self.blocks = {}
|
|
"""Location and kind of the blocks in this sector."""
|
|
|
|
self.visible = set({})
|
|
"""Set of visible blocks if we look at this sector alone"""
|
|
|
|
self.outline = set({})
|
|
"""Blocks on the outline of the section"""
|
|
|
|
self.face_full_cache = set({})
|
|
|
|
self.position = position
|
|
"""Location of this sector."""
|
|
|
|
self.min_block = [i * SECTOR_SIZE for i in position]
|
|
"""Minimum location (included) of block in this section."""
|
|
|
|
self.max_block = [(i + 1) * SECTOR_SIZE for i in position]
|
|
"""Maximum location (excluded) of block in this section."""
|
|
|
|
def is_face_full(self, direction):
|
|
"""Check if one of the face of this section is full of blocks.
|
|
|
|
The direction is a normalized vector from `FACES`."""
|
|
return direction in self.face_full_cache
|
|
|
|
def contains(self, pos):
|
|
"""True if the position `pos` is inside this sector."""
|
|
return (self.min_block[0] <= pos[0] < self.max_block[0]
|
|
and self.min_block[1] <= pos[1] < self.max_block[1]
|
|
and self.min_block[2] <= pos[2] < self.max_block[2])
|
|
|
|
def contains_y(self, y):
|
|
"""True if the horizontal plan `y` is inside this sector."""
|
|
return self.min_block[1] <= y < self.max_block[1]
|
|
|
|
def contains_y_range(self, ymin, ymax):
|
|
"""True if the horizontal plan between `ymin` and `ymax` is inside this
|
|
sector."""
|
|
return self.min_block[1] <= ymax and ymin <= self.max_block[1]
|
|
|
|
def blocks_from_face(self, face):
|
|
"""Iterate all blocks from a face"""
|
|
axis = 0 if face[0] != 0 else (1 if face[1] != 0 else 2)
|
|
if face[axis] == -1:
|
|
pos = self.min_block[axis]
|
|
else:
|
|
pos = self.max_block[axis] - 1
|
|
for block in self.outline:
|
|
if block[axis] == pos:
|
|
yield block
|
|
|
|
def empty(self, pos):
|
|
"""Return false if there is no block at this position in this chunk"""
|
|
return pos not in self.blocks
|
|
|
|
def get_block(self, position):
|
|
"""Return the block stored at this position of this sector. Else None."""
|
|
return self.blocks[position]
|
|
|
|
def add_block(self, position, block):
|
|
"""Add a block to this chunk only if the `position` is part of this chunk."""
|
|
if not self.contains(position):
|
|
return
|
|
|
|
self.blocks[position] = block
|
|
if self.exposed(position):
|
|
self.visible.add(position)
|
|
self.check_neighbors(position)
|
|
|
|
for axis in range(3):
|
|
if position[axis] == self.min_block[axis]:
|
|
self.outline.add(position)
|
|
face = [0] * 3
|
|
face[axis] = -1
|
|
face = tuple(face)
|
|
if self.check_face_full(face):
|
|
self.face_full_cache.add(face)
|
|
elif position[axis] == self.max_block[axis] - 1:
|
|
self.outline.add(position)
|
|
face = [0] * 3
|
|
face[axis] = 1
|
|
face = tuple(face)
|
|
if self.check_face_full(face):
|
|
self.face_full_cache.add(face)
|
|
|
|
def check_face_full(self, face):
|
|
axis = (face[1] != 0) * 1 + (face[2] != 0) * 2
|
|
if face[axis] == -1:
|
|
fixed_pos = self.min_block[axis]
|
|
else:
|
|
fixed_pos = self.max_block[axis] - 1
|
|
axis2 = (axis + 1) % 3
|
|
axis3 = (axis + 2) % 3
|
|
|
|
pos = [None] * 3
|
|
pos[axis] = fixed_pos
|
|
for a2 in range(self.min_block[axis2], self.max_block[axis2]):
|
|
for a3 in range(self.min_block[axis3], self.max_block[axis3]):
|
|
pos[axis2] = a2
|
|
pos[axis3] = a3
|
|
block_pos = tuple(pos)
|
|
if block_pos not in self.blocks:
|
|
return False
|
|
return True
|
|
|
|
def remove_block(self, position):
|
|
"""Remove a block from this sector at the `position`.
|
|
|
|
Returns discarded full faces in case.
|
|
"""
|
|
del self.blocks[position]
|
|
self.check_neighbors(position)
|
|
self.visible.discard(position)
|
|
self.outline.discard(position)
|
|
|
|
discarded = set({})
|
|
# Update the full faces
|
|
for face in list(self.face_full_cache):
|
|
axis = (face[1] != 0) * 1 + (face[2] != 0) * 2
|
|
if face[axis] == -1:
|
|
border = self.min_block
|
|
else:
|
|
x, y, z = self.max_block
|
|
border = x - 1, y - 1, z - 1
|
|
if position[axis] == border[axis]:
|
|
self.face_full_cache.discard(face)
|
|
discarded.add(face)
|
|
return discarded
|
|
|
|
def exposed(self, position):
|
|
""" Returns False if given `position` is surrounded on all 6 sides by
|
|
blocks, True otherwise.
|
|
"""
|
|
for neighbor, _face in iter_neighbors(position):
|
|
if self.empty(neighbor):
|
|
return True
|
|
return False
|
|
|
|
def check_neighbors(self, position):
|
|
""" Check all blocks surrounding `position` and ensure their visual
|
|
state is current. This means hiding blocks that are not exposed and
|
|
ensuring that all exposed blocks are shown. Usually used after a block
|
|
is added or removed.
|
|
"""
|
|
for neighbor, _face in iter_neighbors(position):
|
|
if self.empty(neighbor):
|
|
continue
|
|
if self.exposed(neighbor):
|
|
if neighbor not in self.visible:
|
|
self.visible.add(neighbor)
|
|
else:
|
|
if neighbor in self.visible:
|
|
self.visible.remove(neighbor)
|
|
|
|
|
|
class Model(object):
|
|
def __init__(self, batch, group):
|
|
self.batch = batch
|
|
|
|
self.group = group
|
|
|
|
# Procedural generator
|
|
self._generator = None
|
|
|
|
# Same mapping as `world` but only contains blocks that are shown.
|
|
self.shown = {}
|
|
|
|
# Mapping from position to a pyglet `VertextList` for all shown sections.
|
|
self._shown = {}
|
|
|
|
# Mapping from sector index a list of positions inside that sector.
|
|
self.sectors = {}
|
|
|
|
# Actual set of shown sectors
|
|
self.shown_sectors = set({})
|
|
|
|
# List of sectors requested but not yet received
|
|
self.requested = set({})
|
|
|
|
# Simple function queue implementation. The queue is populated with
|
|
# _show_block() and _hide_block() calls
|
|
self.queue = deque()
|
|
|
|
def count_blocks(self):
|
|
"""Return the number of blocks in this model"""
|
|
return sum([len(s.blocks) for s in self.sectors.values()])
|
|
|
|
@property
|
|
def generator(self):
|
|
return self._generator
|
|
|
|
@generator.setter
|
|
def generator(self, generator):
|
|
assert self._generator is None
|
|
generator.set_callback(self.on_sector_received)
|
|
self._generator = generator
|
|
|
|
def on_sector_received(self, chunk):
|
|
"""Called when a part of the world is returned.
|
|
|
|
This is not executed by the main thread. So the result have to be passed
|
|
to the main thread.
|
|
"""
|
|
self._enqueue(self.register_sector, chunk)
|
|
# This sleep looks to be needed to reduce the load of the main thread.
|
|
# Maybe it also release the GIL and reduce the coupling with the main thread.
|
|
time.sleep(0.01)
|
|
|
|
def hit_test(self, position, vector, max_distance=NODE_SELECTOR):
|
|
""" Line of sight search from current position. If a block is
|
|
intersected it is returned, along with the block previously in the line
|
|
of sight. If no block is found, return None, None.
|
|
|
|
Parameters
|
|
----------
|
|
position : tuple of len 3
|
|
The (x, y, z) position to check visibility from.
|
|
vector : tuple of len 3
|
|
The line of sight vector.
|
|
max_distance : int
|
|
How many blocks away to search for a hit.
|
|
|
|
"""
|
|
m = 8
|
|
x, y, z = position
|
|
dx, dy, dz = vector
|
|
previous = None
|
|
for _ in range(max_distance * m):
|
|
checked_position = normalize((x, y, z))
|
|
if checked_position != previous and not self.empty(checked_position):
|
|
return checked_position, previous
|
|
previous = checked_position
|
|
x, y, z = x + dx / m, y + dy / m, z + dz / m
|
|
return None, None
|
|
|
|
def empty(self, position, must_be_loaded=False):
|
|
""" Returns True if given `position` does not contain block.
|
|
|
|
If `must_be_loaded` is True, this returns False if the block is not yet loaded.
|
|
"""
|
|
sector_pos = sectorize(position)
|
|
sector = self.sectors.get(sector_pos, None)
|
|
if sector is None:
|
|
return not must_be_loaded
|
|
return sector.empty(position)
|
|
|
|
def exposed(self, position):
|
|
""" Returns False if given `position` is surrounded on all 6 sides by
|
|
blocks, True otherwise.
|
|
|
|
"""
|
|
x, y, z = position
|
|
for dx, dy, dz in FACES:
|
|
pos = (x + dx, y + dy, z + dz)
|
|
if self.empty(pos, must_be_loaded=True):
|
|
return True
|
|
return False
|
|
|
|
def add_block(self, position, block, immediate=True):
|
|
""" Add a block with the given `texture` and `position` to the world.
|
|
|
|
Parameters
|
|
----------
|
|
position : tuple of len 3
|
|
The (x, y, z) position of the block to add.
|
|
block : Block object
|
|
An instance of the Block class.
|
|
immediate : bool
|
|
Whether or not to draw the block immediately.
|
|
|
|
"""
|
|
sector_pos = sectorize(position)
|
|
sector = self.sectors.get(sector_pos, None)
|
|
if sector is None:
|
|
# Sector not yet loaded
|
|
# It would be better to create it
|
|
# and then to merge it when the sector is loaded
|
|
return
|
|
|
|
if position in sector.blocks:
|
|
self.remove_block(position, immediate)
|
|
sector.add_block(position, block)
|
|
self._enqueue(self.update_batch_sector, sector)
|
|
|
|
def remove_block(self, position, immediate=True):
|
|
""" Remove the block at the given `position`.
|
|
|
|
Parameters
|
|
----------
|
|
position : tuple of len 3
|
|
The (x, y, z) position of the block to remove.
|
|
immediate : bool
|
|
Whether or not to immediately remove block from canvas.
|
|
|
|
"""
|
|
sector_pos = sectorize(position)
|
|
sector = self.sectors.get(sector_pos)
|
|
if sector is None:
|
|
# Nothing to do
|
|
return
|
|
|
|
if position not in sector.blocks:
|
|
# Nothing to do
|
|
return
|
|
|
|
|
|
discarded = sector.remove_block(position)
|
|
|
|
# Removing a block can make a neighbor section visible
|
|
if discarded:
|
|
x, y, z = sector.position
|
|
for dx, dy, dz in discarded:
|
|
neighbor_pos = x + dx, y + dy, z + dz
|
|
if neighbor_pos in self.sectors:
|
|
continue
|
|
if neighbor_pos in self.requested:
|
|
continue
|
|
if neighbor_pos not in self.shown_sectors:
|
|
continue
|
|
neighbor = self.generator.generate(neighbor_pos)
|
|
self.register_sector(neighbor)
|
|
|
|
self._enqueue(self.update_batch_sector, sector)
|
|
|
|
def get_block(self, position):
|
|
"""Return a block from this position.
|
|
|
|
If no blocks, None is returned.
|
|
"""
|
|
sector_pos = sectorize(position)
|
|
sector = self.sectors.get(sector_pos)
|
|
if sector is None:
|
|
return None
|
|
return sector.blocks.get(position, None)
|
|
|
|
def update_batch_sector(self, sector):
|
|
visible = sector.position in self.shown_sectors
|
|
|
|
# Clean up previous description
|
|
block = self._shown.pop(sector.position, None)
|
|
if block:
|
|
block.delete()
|
|
|
|
if visible:
|
|
points = len(sector.visible) * 24
|
|
vertex_data = []
|
|
tex_coords = []
|
|
|
|
# Merge all the blocks together
|
|
for position in sector.visible:
|
|
x, y, z = position
|
|
vertex_data.extend(cube_vertices(x, y, z, 0.5))
|
|
block = sector.get_block(position)
|
|
tex_coords.extend(block.tex_coords)
|
|
|
|
# create vertex list
|
|
# FIXME Maybe `add_indexed()` should be used instead
|
|
vertex_list = self.batch.add(points, GL_QUADS, self.group,
|
|
('v3f/static', vertex_data),
|
|
('t2f/static', tex_coords))
|
|
self._shown[sector.position] = vertex_list
|
|
|
|
def register_sector(self, sector):
|
|
"""Add a new sector to this world definition.
|
|
"""
|
|
# Assert if the sector is already there.
|
|
# It also could be skipped, or merged together.
|
|
assert sector.position not in self.sectors
|
|
self.requested.discard(sector.position)
|
|
self.sectors[sector.position] = sector
|
|
if sector.position not in self.shown_sectors:
|
|
return
|
|
|
|
# Update the displayed blocks
|
|
self._enqueue(self.update_batch_sector, sector)
|
|
|
|
# Is sector around have to be loaded too?
|
|
x, y, z = sector.position
|
|
for face in FACES:
|
|
# The sector have to be accessible
|
|
if sector.is_face_full(face):
|
|
continue
|
|
pos = x + face[0], y + face[1], z + face[2]
|
|
# Must not be already loaded
|
|
if pos in self.sectors:
|
|
continue
|
|
# Must be shown actually
|
|
if pos not in self.shown_sectors:
|
|
continue
|
|
# Must not be already requested
|
|
if pos in self.requested:
|
|
continue
|
|
# Then request the sector
|
|
if self.generator is not None:
|
|
self.requested.add(pos)
|
|
self.generator.request_sector(pos)
|
|
|
|
def show_sector(self, sector_pos):
|
|
""" Ensure all blocks in the given sector that should be shown are
|
|
drawn to the canvas.
|
|
"""
|
|
self.shown_sectors.add(sector_pos)
|
|
sector = self.sectors.get(sector_pos, None)
|
|
if sector is None:
|
|
if sector_pos in self.requested:
|
|
# Already requested
|
|
return
|
|
# If sectors around not yet loaded
|
|
if not self.is_sector_visible(sector_pos):
|
|
return
|
|
if self.generator is not None:
|
|
# This sector is about to be loaded
|
|
self.requested.add(sector_pos)
|
|
self.generator.request_sector(sector_pos)
|
|
return
|
|
|
|
self._enqueue(self.update_batch_sector, sector)
|
|
|
|
def is_sector_visible(self, sector_pos):
|
|
"""Check if a sector is visible.
|
|
|
|
For now only check if no from a sector position.
|
|
"""
|
|
x, y, z = sector_pos
|
|
for dx, dy, dz in FACES:
|
|
pos = (x + dx, y + dy, z + dz)
|
|
neighbor = self.sectors.get(pos, None)
|
|
if neighbor is not None:
|
|
neighbor_face = (-dx, -dy, -dz)
|
|
if not neighbor.is_face_full(neighbor_face):
|
|
return True
|
|
return False
|
|
|
|
def hide_sector(self, sector_pos):
|
|
""" Ensure all blocks in the given sector that should be hidden are
|
|
removed from the canvas.
|
|
|
|
"""
|
|
self.shown_sectors.discard(sector_pos)
|
|
sector = self.sectors.get(sector_pos, None)
|
|
if sector is not None:
|
|
self._enqueue(self.update_batch_sector, sector)
|
|
|
|
def show_only_sectors(self, sector_positions):
|
|
""" Update the shown sectors.
|
|
|
|
Show the ones which are not part of the list, and hide the others.
|
|
"""
|
|
after_set = set(sector_positions)
|
|
before_set = self.shown_sectors
|
|
hide = before_set - after_set
|
|
# Use a list to respect the order of the sectors
|
|
show = [s for s in sector_positions if s not in before_set]
|
|
for sector_pos in show:
|
|
self.show_sector(sector_pos)
|
|
for sector_pos in hide:
|
|
self.hide_sector(sector_pos)
|
|
|
|
def _enqueue(self, func, *args):
|
|
""" Add `func` to the internal queue.
|
|
|
|
"""
|
|
self.queue.append((func, args))
|
|
|
|
def _dequeue(self):
|
|
""" Pop the top function from the internal queue and call it.
|
|
|
|
"""
|
|
func, args = self.queue.popleft()
|
|
func(*args)
|
|
|
|
def process_queue(self):
|
|
""" Process the entire queue while taking periodic breaks. This allows
|
|
the game loop to run smoothly. The queue contains calls to
|
|
_show_block() and _hide_block() so this method should be called if
|
|
add_block() or remove_block() was called with immediate=False
|
|
|
|
"""
|
|
start = time.perf_counter()
|
|
while self.queue and time.perf_counter() - start < 1.0 / TICKS_PER_SEC:
|
|
self._dequeue()
|
|
|
|
def process_entire_queue(self):
|
|
""" Process the entire queue with no breaks.
|
|
|
|
"""
|
|
while self.queue:
|
|
self._dequeue()
|