Skip to content

Commit

Permalink
Merge pull request #131 from Amulet-Team/player_location
Browse files Browse the repository at this point in the history
Implemented support for player location
  • Loading branch information
gentlegiantJGC committed May 29, 2021
2 parents 200c452 + d0107b9 commit 26be4ff
Show file tree
Hide file tree
Showing 156 changed files with 1,483 additions and 240 deletions.
16 changes: 16 additions & 0 deletions amulet/api/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ class EntryDoesNotExist(EntryLoadError):
pass


class PlayerLoadError(EntryLoadError):
"""
An error thrown if a player failed to load for some reason.
"""

pass


class PlayerDoesNotExist(EntryDoesNotExist, PlayerLoadError):
"""
An error thrown if a player does not exist.
"""

pass


class ChunkLoadError(EntryLoadError):
"""
An error thrown if a chunk failed to load for some reason.
Expand Down
116 changes: 99 additions & 17 deletions amulet/api/history/history_manager/database.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
"""
The DatabaseHistoryManager is like a dictionary that can cache historical versions of the values.
The class consists of the temporary database in RAM, the cache on disk and a way to pull from the original data source.
The temporary database:
This is a normal dictionary in RAM.
When accessing and modifying data this is the data you are modifying.
The cache on disk:
This is a database on disk of the data serialised using pickle.
This is populated with the original version of the data and each revision of the data.
The temporary database can be cleared using the unload methods and successive get calls will re-populate the temporary database from this cache.
As previously stated, the cache also stores historical versions of the data which enables undoing and redoing changes.
The original data source (raw form)
This is the original data from the world/structure.
If the data does not exist in the temporary or cache databases it will be loaded from here.
"""

from abc import abstractmethod
from typing import Tuple, Any, Dict, Generator
from typing import Tuple, Any, Dict, Generator, Iterable, Set
import threading

from amulet.api.history.data_types import EntryKeyType, EntryType
Expand All @@ -15,6 +31,9 @@
class DatabaseHistoryManager(ContainerHistoryManager):
"""Manage the history of a number of items in a database."""

_temporary_database: Dict[EntryKeyType, EntryType]
_history_database: Dict[EntryKeyType, RevisionManager]

DoesNotExistError = EntryDoesNotExist
LoadError = EntryLoadError

Expand Down Expand Up @@ -64,34 +83,79 @@ def changed_entries(self) -> Generator[EntryKeyType, None, None]:
if history_entry.changed and key not in changed:
yield key

def _all_entries(self, *args, **kwargs) -> Set[EntryKeyType]:
with self._lock:
keys = set()
deleted_keys = set()
for key in self._temporary_database.keys():
if self._temporary_database[key] is None:
deleted_keys.add(key)
else:
keys.add(key)

for key in self._history_database.keys():
if key not in self._temporary_database:
if self._history_database[key].is_deleted:
deleted_keys.add(key)
else:
keys.add(key)

for key in self._raw_all_entries(*args, **kwargs):
if key not in keys and key not in deleted_keys:
keys.add(key)

return keys

@abstractmethod
def _raw_all_entries(self, *args, **kwargs) -> Iterable[EntryKeyType]:
"""
The keys for all entries in the raw database.
"""
raise NotImplementedError

def __contains__(self, item: EntryKeyType) -> bool:
return self._has_entry(item)

def _has_entry(self, key: EntryKeyType):
"""
Does the entry exist in one of the databases.
Subclasses should implement a proper method calling this.
"""
return key in self._temporary_database or key in self._history_database
with self._lock:
if key in self._temporary_database:
return self._temporary_database[key] is not None
elif key in self._history_database:
return not self._history_database[key].is_deleted
else:
return self._raw_has_entry(key)

@abstractmethod
def _raw_has_entry(self, key: EntryKeyType) -> bool:
"""
Does the raw database have this entry.
Will be called if the key is not present in the loaded database.
"""
raise NotImplementedError

def _get_entry(self, key: EntryKeyType) -> Changeable:
"""
Get a key from the database.
Subclasses should implement a proper method calling this.
"""
with self._lock:
if key in self._temporary_database:
# if the entry is loaded in RAM, just return it.
entry = self._temporary_database[key]
elif key in self._history_database:
# if it is present in the cache, load it and return it.
entry = self._temporary_database[key] = self._history_database[
key
].get_current_entry()
else:
if key in self._temporary_database:
entry = self._temporary_database[key]
elif key in self._history_database:
entry = self._temporary_database[key] = self._history_database[
key
].get_current_entry()
else:
entry = self._temporary_database[
key
] = self._get_register_original_entry(key)
# If it has not been loaded request it from the raw database.
entry = self._temporary_database[
key
] = self._get_register_original_entry(key)
if entry is None:
raise self.DoesNotExistError
return entry
Expand All @@ -101,15 +165,18 @@ def _get_register_original_entry(self, key: EntryKeyType) -> EntryType:
if key in self._history_database:
raise Exception(f"The entry for {key} has already been registered.")
try:
entry = self._get_entry_from_world(key)
entry = self._raw_get_entry(key)
except EntryDoesNotExist:
entry = None
self._history_database[key] = self._create_new_revision_manager(key, entry)
return entry

@abstractmethod
def _get_entry_from_world(self, key: EntryKeyType) -> EntryType:
"""If the entry was not found in the database request it from the world."""
def _raw_get_entry(self, key: EntryKeyType) -> EntryType:
"""
Get the entry from the raw database.
Will be called if the key is not present in the loaded database.
"""
raise NotImplementedError

@staticmethod
Expand Down Expand Up @@ -186,6 +253,21 @@ def restore_last_undo_point(self):
with self._lock:
self._temporary_database.clear()

def unload(self, *args, **kwargs):
"""Unload the entries loaded in RAM."""
with self._lock:
self._temporary_database.clear()

def unload_unchanged(self, *args, **kwargs):
"""Unload all entries from RAM that have not been marked as changed."""
with self._lock:
unchanged = []
for key, chunk in self._temporary_database.items():
if not chunk.changed:
unchanged.append(key)
for key in unchanged:
del self._temporary_database[key]

def purge(self):
"""Unload all cached data. Effectively returns the class to its starting state."""
with self._lock:
Expand Down
51 changes: 41 additions & 10 deletions amulet/api/level/base_level/base_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
from .clone import clone
from amulet.api import wrapper as api_wrapper, level as api_level
import PyMCTranslate
from amulet.api.player import Player
from .player_manager import PlayerManager


class BaseLevel:
Expand Down Expand Up @@ -64,8 +66,10 @@ def __init__(self, path: str, format_wrapper: api_wrapper.FormatWrapper):
self._history_manager = MetaHistoryManager()

self._chunks: ChunkManager = ChunkManager(self._prefix, self)
self._players = PlayerManager(self)

self.history_manager.register(self._chunks, True)
self.history_manager.register(self._players, True)

@property
def level_wrapper(self) -> api_wrapper.FormatWrapper:
Expand Down Expand Up @@ -132,12 +136,7 @@ def get_block(self, x: int, y: int, z: int, dimension: Dimension) -> Block:

return self.get_chunk(cx, cz, dimension).get_block(offset_x, y, offset_z)

def _chunk_box(
self,
cx: int,
cz: int,
sub_chunk_size: Optional[int] = None,
):
def _chunk_box(self, cx: int, cz: int, sub_chunk_size: Optional[int] = None):
"""Get a SelectionBox containing the whole of a given chunk"""
if sub_chunk_size is None:
sub_chunk_size = self.sub_chunk_size
Expand Down Expand Up @@ -291,10 +290,7 @@ def get_moved_coord_slice_box(
for (src_cx, src_cz), box in self.get_coord_box(
dimension, selection, yield_missing_chunks=yield_missing_chunks
):
dst_full_box = SelectionBox(
offset + box.min,
offset + box.max,
)
dst_full_box = SelectionBox(offset + box.min, offset + box.max)

first_chunk = block_coords_to_chunk_coords(
dst_full_box.min_x,
Expand Down Expand Up @@ -928,3 +924,38 @@ def restore_last_undo_point(self):
This will revert those changes.
"""
self.history_manager.restore_last_undo_point()

@property
def players(self) -> PlayerManager:
"""
The player container.
Most methods from :class:`PlayerManager` also exists in the level class.
"""
return self._players

def all_player_ids(self) -> Set[str]:
"""
Returns a set of all player ids that are present in the level.
"""
return self.players.all_player_ids()

def has_player(self, player_id: str) -> bool:
"""
Is the given player id present in the level
:param player_id: The player id to check
:return: True if the player id is present, False otherwise
"""
return self.players.has_player(player_id)

def get_player(self, player_id: str) -> Player:
"""
Gets the :class:`Player` object that belongs to the specified player id
If no parameter is supplied, the data of the local player will be returned
:param player_id: The desired player id
:return: A Player instance
"""
return self.players.get_player(player_id)

0 comments on commit 26be4ff

Please sign in to comment.