diff --git a/chunks.py b/chunks.py index 76c1460..1b253b2 100644 --- a/chunks.py +++ b/chunks.py @@ -112,8 +112,7 @@ class Chunk(): self._content = ChunkDiff() self._modifications = ChunkDiff() - self.last_modified = None - self.touch() + self.last_modified = 0 def set(self, x, y, character): self._modifications.set(x, y, character) @@ -151,7 +150,7 @@ class Chunk(): self.last_modified = now or time.time() def age(self, now=None): - return self.last_modified - (now or time.time()) + return (now or time.time()) - self.last_modified def lines(self): return self.as_diff().lines() @@ -195,7 +194,8 @@ class ChunkPool(): for dchunk in diffs: pos = dchunk[0] diff = dchunk[1] - chunk = self.load(pos) + chunk = self.get(pos) or self.create(pos) + #chunk = self.load(pos) if not diff.empty(): chunk.apply_diff(diff) @@ -204,7 +204,8 @@ class ChunkPool(): for dchunk in diffs: pos = dchunk[0] diff = dchunk[1] - chunk = self.load(pos) + chunk = self.get(pos) or self.create(pos) + #chunk = self.load(pos) if not diff.empty(): chunk.commit_diff(diff) @@ -213,8 +214,9 @@ class ChunkPool(): changes = [] for pos, chunk in self._chunks.items(): - changes.append((pos, chunk.get_changes())) - chunk.commit_changes() + if chunk.modified(): + changes.append((pos, chunk.get_changes())) + chunk.commit_changes() return changes @@ -222,7 +224,9 @@ class ChunkPool(): self.commit_changes() def load(self, pos): - return self.get(pos) or self.create(pos) + if not self.get(pos): + self.create(pos) + #return self.get(pos) or self.create(pos) def load_list(self, coords): for pos in coords: diff --git a/client.py b/client.py index a343d25..0f3646c 100644 --- a/client.py +++ b/client.py @@ -14,14 +14,14 @@ from utils import Position from clientchunkpool import ClientChunkPool class Client(): - def __init__(self, address, logfile=None): + def __init__(self, address, port=None, logfile=None): self.stopping = False self.map_ = None self.chunkmap = None self.chunkmap_active = False - self.address = f"ws://{address}/" + self.address = f"ws://{address}:{port}/" self._drawevent = threading.Event() self.pool = ClientChunkPool(self) @@ -171,17 +171,32 @@ class Client(): self._ws.send(json.dumps(message)) def main(argv): - if len(argv) == 2: - client = Client(argv[1]) - elif len(argv) == 3: - client = Client(argv[1], argv[2]) - else: + if len(argv) == 1 or len(argv) > 4: print("Usage:") - print(f" {argv[0]} address [logfile]") + print(f" {argv[0]} address [port [logfile]]") + print(" default port: 8000") return + address = argv[1] + + if len(argv) >= 3: + try: + port = int(argv[2]) + except ValueError: + print("Invalid port") + return + else: + port = 8000 + + # only for debugging, will be removed later + if len(argv) >= 4: + logfile = argv[3] + else: + logfile = None + os.environ.setdefault('ESCDELAY', '25') # only a 25 millisecond delay + client = Client(address, port, logfile) curses.wrapper(client.launch) if __name__ == "__main__": diff --git a/clientchunkpool.py b/clientchunkpool.py index 94dd9b3..2b873a8 100644 --- a/clientchunkpool.py +++ b/clientchunkpool.py @@ -36,6 +36,9 @@ class ClientChunkPool(ChunkPool): if diffs: self._client.send_changes(diffs) + def load(self, pos): + raise Exception + def load_list(self, coords): coords = [pos for pos in coords if pos not in self._chunks] if coords: @@ -43,6 +46,6 @@ class ClientChunkPool(ChunkPool): def unload_list(self, coords): if coords: - #self.save_changes() self._client.unload_chunks(coords) - super().unload_list(coords) + + super().unload_list(coords) diff --git a/dbchunkpool.py b/dbchunkpool.py index 0d9b6a0..0a89cca 100644 --- a/dbchunkpool.py +++ b/dbchunkpool.py @@ -1,17 +1,127 @@ +import sqlite3 +import time +import threading + from chunks import ChunkPool +from utils import Position class ChunkDB(): """ Load and save chunks to a SQLite db. """ - pass + def __init__(self, filename): + self.dbfilename = filename + + def transaction(func): + def wrapper(self, *args, **kwargs): + con = sqlite3.connect(self.dbfilename) + try: + with con: + return func(self, con, *args, **kwargs) + finally: + con.close() + + return wrapper + + @transaction + def save_many(self, con, chunks): + print("save_many") + + @transaction + def load_many(self, con, coords): + print("load_many") + return [(coor, None) for coor in coords] class DBChunkPool(ChunkPool): """ A ChunkPool that can load/save chunks from/to a database. """ - #def __init__(self, filename): - #super().init() - #self._chunkdb = ChunkDB(filename) + def __init__(self, filename): + super().__init__() + self._chunkdb = ChunkDB(filename) + + self.save_period = 10 # save and clean up every minute + self.max_age = 20 # ca. one minute until a chunk is unloaded again + + self.save_thread = threading.Thread( + target=self.perodic_save, + name="save_thread", + daemon=True + ) + self.save_thread.start() + + def save_changes(self): + diffs = self.commit_changes() + + changed_chunks = [] + for dchunk in diffs: + pos = dchunk[0] + chunk = self.get(pos) + changed_chunks.append((pos, chunk)) + + self._chunkdb.save_many(changed_chunks) + + def load(self, pos): + print("Loading individual chunk...") + raise Exception + + def load_list(self, coords): + print("Loading chunk list...") + to_load = [pos for pos in coords if pos not in self._chunks] + chunks = self._chunkdb.load_many(to_load) + for dchunk in chunks: + pos = dchunk[0] + chunk = dchunk[1] + if chunk: + self.set(pos, chunk) + else: + self.create(pos) + + def perodic_save(self): + while True: + time.sleep(self.save_period) + + with self: + print("BEFORE:::") + self.print_chunks() + + self.save_changes() + + # unload old chunks + now = time.time() + for pos, chunk in self._chunks.items(): + print(f"p{pos} :: t{now} :: m{chunk.last_modified} :: a{chunk.age(now)}") + self.clean_up(condition=lambda pos, chunk: chunk.age(now) > self.max_age) + + print("AFTER:::") + self.print_chunks() + + def get_min_max(self): + minx = min(pos.x for pos in self._chunks) + maxx = max(pos.x for pos in self._chunks) + miny = min(pos.y for pos in self._chunks) + maxy = max(pos.y for pos in self._chunks) + + return minx, maxx, miny, maxy + + def print_chunks(self): + if self._chunks: + minx, maxx, miny, maxy = self.get_min_max() + sizex, sizey = maxx - minx + 1, maxy - miny + 1 + print("┌" + "─"*sizex*2 + "┐") + for y in range(miny, maxy + 1): + line = [] + for x in range(minx, maxx + 1): + chunk = self._chunks.get(Position(x, y)) + if chunk: + if chunk.empty(): + line.append("()") + else: + line.append("[]") + else: + line.append(" ") + line = "".join(line) + print("│" + line + "│") + print("└" + "─"*sizex*2 + "┘") diff --git a/maps.py b/maps.py index 354c1d8..8eea62b 100644 --- a/maps.py +++ b/maps.py @@ -144,6 +144,7 @@ class Map(): def delete(self): with self.chunkpool as pool: chunk = pool.get(Position(chunkx(self.cursorx-1), chunky(self.cursory))) + if chunk: chunk.delete(inchunkx(self.cursorx-1), inchunky(self.cursory)) pool.save_changes_delayed() diff --git a/server.py b/server.py index a36f6d8..382111f 100644 --- a/server.py +++ b/server.py @@ -8,16 +8,17 @@ from utils import Position from chunks import ChunkDiff, jsonify_diffs, dejsonify_diffs from dbchunkpool import DBChunkPool -pool = DBChunkPool() -clients = [] +from chunks import ChunkPool class WotServer(WebSocket): def handle_request_chunks(self, coords): diffs = [] - with pool: - for coor in coords: - pos = Position(coor[0], coor[1]) - chunk = pool.load(pos) + with self.pool as pool: + coords = [Position(coor[0], coor[1]) for coor in coords] + pool.load_list(coords) + + for pos in coords: + chunk = pool.get(pos) diffs.append((pos, chunk.as_diff())) self.loaded_chunks.add(pos) @@ -46,10 +47,10 @@ class WotServer(WebSocket): illegitimate_diffs.append(dchunk) if legitimate_diffs: - with pool: + with self.pool as pool: pool.apply_diffs(legitimate_diffs) - for client in clients: + for client in self.clients: if client: client.send_changes(legitimate_diffs) @@ -60,12 +61,16 @@ class WotServer(WebSocket): self.sendMessage(json.dumps(message)) def reverse_diffs(self, diffs): - with pool: + coords = [dchunk[0] for dchunk in diffs] + + with self.pool as pool: + pool.load_list(coords) + reverse_diffs = [] for dchunk in diffs: pos = dchunk[0] diff = dchunk[1] - chunk = pool.load(pos) + chunk = pool.get(pos) reverse_diff = diff.diff(chunk.as_diff()) reverse_diffs.append((pos, reverse_diff)) @@ -92,45 +97,55 @@ class WotServer(WebSocket): self.loaded_chunks = set() try: - i = clients.index(None) - clients[i] = self + i = self.clients.index(None) + self.clients[i] = self except ValueError: - clients.append(self) - i = len(clients) - 1 + self.clients.append(self) + i = len(self.clients) - 1 - graphstr = "".join(["┯" if j == i else ("│" if v else " ") for j, v in enumerate(clients)]) - print(f"{graphstr} {self.address[0]}") + graphstr = "".join(["┯" if j == i else ("│" if v else " ") for j, v in enumerate(self.clients)]) + print(f"{graphstr} {self.address[0]}") def handleClose(self): - i = clients.index(self) + i = self.clients.index(self) - graphstr = "".join(["┷" if j == i else ("│" if v else " ") for j, v in enumerate(clients)]) + graphstr = "".join(["┷" if j == i else ("│" if v else " ") for j, v in enumerate(self.clients)]) print(graphstr) #print(f"{graphstr} {self.address[0]}") - clients[i] = None - while clients and not clients[-1]: - clients.pop() + self.clients[i] = None + while self.clients and not self.clients[-1]: + self.clients.pop() def main(argv): - if len(argv) > 2: + if len(argv) == 1 or len(argv) > 3: print("Usage:") - print(f" {argv[0]} [port]") + print(f" {argv[0]} dbfile [port]") print(" default port: 8000") return - elif len(argv) > 1: + + dbfile = argv[1] + + if len(argv) >= 3: try: - port = int(argv[1]) + port = int(argv[2]) except ValueError: print("Invalid port") return else: port = 8000 + print("Connecting to db") + WotServer.pool = DBChunkPool(dbfile) + WotServer.clients = [] + server = SimpleWebSocketServer('', port, WotServer) try: server.serveforever() except KeyboardInterrupt: + print("") + print("Saving recent changes.") + WotServer.pool.save_changes() print("Stopped.") if __name__ == "__main__":