Start work on server-side saving worlds

This commit includes a a few more changes because I'm too lazy to
clean them up:
 - fix chunk modification times
 - improve command line arguments
 - load multiple chunks at once
This commit is contained in:
Joscha 2017-04-15 23:49:59 +00:00
parent e7f85ff421
commit 9e5b5f874a
6 changed files with 195 additions and 47 deletions

View file

@ -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:

View file

@ -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__":

View file

@ -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)

View file

@ -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 + "")

View file

@ -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()

View file

@ -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__":