Add server
This is a first try at connecting multiple clients using a server. The commit includes a lot of debugging messages. I will hopefully clean up the server and some of the client code.
This commit is contained in:
parent
63410fd99e
commit
320dd16889
6 changed files with 236 additions and 26 deletions
25
chunks.py
25
chunks.py
|
|
@ -2,6 +2,8 @@ import threading
|
||||||
import time
|
import time
|
||||||
from utils import CHUNK_WIDTH, CHUNK_HEIGHT, Position
|
from utils import CHUNK_WIDTH, CHUNK_HEIGHT, Position
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
class ChunkDiff():
|
class ChunkDiff():
|
||||||
"""
|
"""
|
||||||
Represents differences between two chunks (changes to be made to a chunk).
|
Represents differences between two chunks (changes to be made to a chunk).
|
||||||
|
|
@ -13,10 +15,16 @@ class ChunkDiff():
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._chars = {}
|
self._chars = {}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "cd" + str(self._chars)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "cd" + repr(self._chars)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, d):
|
def from_dict(cls, d):
|
||||||
diff = cls()
|
diff = cls()
|
||||||
diff._chars = d
|
diff._chars = {int(i): v for i, v in d.items()}
|
||||||
return diff
|
return diff
|
||||||
#self._chars = d.copy()
|
#self._chars = d.copy()
|
||||||
|
|
||||||
|
|
@ -99,6 +107,9 @@ class Chunk():
|
||||||
def get_changes(self):
|
def get_changes(self):
|
||||||
return self._modifications
|
return self._modifications
|
||||||
|
|
||||||
|
def as_diff(self):
|
||||||
|
return self._content.combine(self._modifications)
|
||||||
|
|
||||||
def touch(self, now=None):
|
def touch(self, now=None):
|
||||||
self.last_modified = now or time.time()
|
self.last_modified = now or time.time()
|
||||||
|
|
||||||
|
|
@ -111,7 +122,7 @@ class Chunk():
|
||||||
#y += 1
|
#y += 1
|
||||||
|
|
||||||
def lines(self):
|
def lines(self):
|
||||||
return self._content.combine(self._modifications).lines()
|
return self.as_diff().lines()
|
||||||
|
|
||||||
def modified(self):
|
def modified(self):
|
||||||
return not self._modifications.empty()
|
return not self._modifications.empty()
|
||||||
|
|
@ -150,14 +161,17 @@ class ChunkPool():
|
||||||
|
|
||||||
def apply_changes(self, changes):
|
def apply_changes(self, changes):
|
||||||
for change in changes:
|
for change in changes:
|
||||||
pos = Position(change[0][0], change[0][1])
|
#pos = Position(change[0][0], change[0][1])
|
||||||
|
pos = change[0]
|
||||||
diff = change[1]
|
diff = change[1]
|
||||||
|
|
||||||
chunk = self.get(pos)
|
chunk = self.get(pos)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
chunk = self.create(pos)
|
chunk = self.create(pos)
|
||||||
|
|
||||||
|
sys.stderr.write(f"Previous at {pos}: {chunk._content}\n")
|
||||||
chunk.commit_diff(diff)
|
chunk.commit_diff(diff)
|
||||||
|
sys.stderr.write(f"Afterwrd at {pos}: {chunk._content}\n")
|
||||||
|
|
||||||
def commit_changes(self):
|
def commit_changes(self):
|
||||||
changes = []
|
changes = []
|
||||||
|
|
@ -177,7 +191,8 @@ class ChunkPool():
|
||||||
|
|
||||||
def load_list(self, coords):
|
def load_list(self, coords):
|
||||||
for pos in coords:
|
for pos in coords:
|
||||||
self.load(pos)
|
if pos not in self._chunks:
|
||||||
|
self.load(pos)
|
||||||
|
|
||||||
def unload(self, pos):
|
def unload(self, pos):
|
||||||
if pos in self._chunks:
|
if pos in self._chunks:
|
||||||
|
|
@ -188,7 +203,7 @@ class ChunkPool():
|
||||||
self.unload(pos)
|
self.unload(pos)
|
||||||
|
|
||||||
def clean_up(self, except_for=[], condition=lambda pos, chunk: True):
|
def clean_up(self, except_for=[], condition=lambda pos, chunk: True):
|
||||||
# old list comprehension which became too long:
|
## old list comprehension which became too long:
|
||||||
#coords = [pos for pos, chunk in self._chunks.items() if not pos in except_for and condition(chunk)]
|
#coords = [pos for pos, chunk in self._chunks.items() if not pos in except_for and condition(chunk)]
|
||||||
|
|
||||||
#self.save_changes() # needs to be accounted for by the user
|
#self.save_changes() # needs to be accounted for by the user
|
||||||
|
|
|
||||||
76
client.py
76
client.py
|
|
@ -1,10 +1,15 @@
|
||||||
import curses
|
import curses
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import string
|
import string
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import websocket
|
||||||
|
from websocket import WebSocketException as WSException
|
||||||
|
|
||||||
from maps import Map, ChunkMap
|
from maps import Map, ChunkMap
|
||||||
from chunks import ChunkDiff
|
from chunks import ChunkDiff
|
||||||
|
from utils import Position
|
||||||
from clientchunkpool import ClientChunkPool
|
from clientchunkpool import ClientChunkPool
|
||||||
|
|
||||||
class Client():
|
class Client():
|
||||||
|
|
@ -12,7 +17,7 @@ class Client():
|
||||||
self.stopping = False
|
self.stopping = False
|
||||||
self.chunkmap_active = False
|
self.chunkmap_active = False
|
||||||
|
|
||||||
self.address = address
|
self.address = f"ws://{address}/"
|
||||||
self._drawevent = threading.Event()
|
self._drawevent = threading.Event()
|
||||||
self.pool = ClientChunkPool(self)
|
self.pool = ClientChunkPool(self)
|
||||||
#self.map_ = Map(sizex, sizey, self.pool)
|
#self.map_ = Map(sizex, sizey, self.pool)
|
||||||
|
|
@ -21,10 +26,28 @@ class Client():
|
||||||
#self.sock = socket.Socket(...)
|
#self.sock = socket.Socket(...)
|
||||||
|
|
||||||
def launch(self, stdscr):
|
def launch(self, stdscr):
|
||||||
|
# connect to server
|
||||||
|
try:
|
||||||
|
self._ws = websocket.create_connection(
|
||||||
|
self.address,
|
||||||
|
enable_multithread=True
|
||||||
|
)
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
sys.stderr.write(f"Could not connect to server: {self.address!r}\n")
|
||||||
|
return
|
||||||
|
|
||||||
|
# create map etc.
|
||||||
sizey, sizex = stdscr.getmaxyx()
|
sizey, sizex = stdscr.getmaxyx()
|
||||||
self.map_ = Map(sizex, sizey, self.pool, self)
|
self.map_ = Map(sizex, sizey, self.pool, self)
|
||||||
self.chunkmap = ChunkMap(self.map_)
|
self.chunkmap = ChunkMap(self.map_)
|
||||||
|
|
||||||
|
# start connection thread
|
||||||
|
self.connectionthread = threading.Thread(
|
||||||
|
target=self.connection_thread,
|
||||||
|
name="connectionthread"
|
||||||
|
)
|
||||||
|
self.connectionthread.start()
|
||||||
|
|
||||||
# start input thread
|
# start input thread
|
||||||
self.inputthread = threading.Thread(
|
self.inputthread = threading.Thread(
|
||||||
target=self.input_thread,
|
target=self.input_thread,
|
||||||
|
|
@ -34,6 +57,7 @@ class Client():
|
||||||
)
|
)
|
||||||
self.inputthread.start()
|
self.inputthread.start()
|
||||||
|
|
||||||
|
# update screen until stopped
|
||||||
while not self.stopping:
|
while not self.stopping:
|
||||||
self._drawevent.wait()
|
self._drawevent.wait()
|
||||||
self._drawevent.clear()
|
self._drawevent.clear()
|
||||||
|
|
@ -94,26 +118,60 @@ class Client():
|
||||||
|
|
||||||
else: sys.stderr.write(repr(i) + "\n")
|
else: sys.stderr.write(repr(i) + "\n")
|
||||||
|
|
||||||
|
def connection_thread(self):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
j = self._ws.recv()
|
||||||
|
self.handle_json(json.loads(j))
|
||||||
|
except (WSException, ConnectionResetError, OSError):
|
||||||
|
#self.stop()
|
||||||
|
return
|
||||||
|
|
||||||
|
def handle_json(self, message):
|
||||||
|
sys.stderr.write(f"message: {message}\n")
|
||||||
|
if message["type"] == "apply-changes":
|
||||||
|
changes = []
|
||||||
|
for chunk in message["data"]:
|
||||||
|
pos = Position(chunk[0][0], chunk[0][1])
|
||||||
|
change = ChunkDiff.from_dict(chunk[1])
|
||||||
|
changes.append((pos, change))
|
||||||
|
|
||||||
|
sys.stderr.write(f"Changes to apply: {changes}\n")
|
||||||
|
self.map_.apply_changes(changes)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
|
sys.stderr.write("Stopping!\n")
|
||||||
self.stopping = True
|
self.stopping = True
|
||||||
|
self._ws.close()
|
||||||
self.redraw()
|
self.redraw()
|
||||||
|
|
||||||
def request_chunks(self, coords):
|
def request_chunks(self, coords):
|
||||||
def execute():
|
#sys.stderr.write(f"requested chunks: {coords}\n")
|
||||||
changes = [(pos, ChunkDiff()) for pos in coords]
|
message = {"type": "request-chunks", "data": coords}
|
||||||
with self.pool as pool:
|
self._ws.send(json.dumps(message))
|
||||||
pool.apply_changes(changes)
|
|
||||||
|
|
||||||
tx = threading.Timer(1, execute)
|
#def execute():
|
||||||
tx.start()
|
#changes = [(pos, ChunkDiff()) for pos in coords]
|
||||||
|
#with self.pool as pool:
|
||||||
|
#pool.apply_changes(changes)
|
||||||
|
|
||||||
|
#tx = threading.Timer(1, execute)
|
||||||
|
#tx.start()
|
||||||
|
|
||||||
|
def unload_chunks(self, coords):
|
||||||
|
#sys.stderr.write(f"unloading chunks: {coords}\n")
|
||||||
|
message = {"type": "unload-chunks", "data": coords}
|
||||||
|
self._ws.send(json.dumps(message))
|
||||||
|
|
||||||
def send_changes(self, changes):
|
def send_changes(self, changes):
|
||||||
pass
|
#sys.stderr.write(f"sending changes: {changes}\n")
|
||||||
|
message = {"type": "save-changes", "data": changes}
|
||||||
|
self._ws.send(json.dumps(message))
|
||||||
|
|
||||||
def main(argv):
|
def main(argv):
|
||||||
if len(argv) != 2:
|
if len(argv) != 2:
|
||||||
print("Usage:")
|
print("Usage:")
|
||||||
print(" {} address".format(argv[0]))
|
print(f" {argv[0]} address")
|
||||||
return
|
return
|
||||||
|
|
||||||
os.environ.setdefault('ESCDELAY', '25') # only a 25 millisecond delay
|
os.environ.setdefault('ESCDELAY', '25') # only a 25 millisecond delay
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
import threading
|
||||||
from chunks import ChunkPool
|
from chunks import ChunkPool
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
class ClientChunkPool(ChunkPool):
|
class ClientChunkPool(ChunkPool):
|
||||||
"""
|
"""
|
||||||
A ChunkPool that requests/loads chunks from a client.
|
A ChunkPool that requests/loads chunks from a client.
|
||||||
|
|
@ -9,6 +12,10 @@ class ClientChunkPool(ChunkPool):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._client = client
|
self._client = client
|
||||||
|
self._save_thread = None
|
||||||
|
|
||||||
|
def set(self, pos, chunk):
|
||||||
|
super().set(pos, chunk)
|
||||||
|
|
||||||
#def commit_changes(self):
|
#def commit_changes(self):
|
||||||
#changes = []
|
#changes = []
|
||||||
|
|
@ -24,12 +31,44 @@ class ClientChunkPool(ChunkPool):
|
||||||
|
|
||||||
self._client.redraw()
|
self._client.redraw()
|
||||||
|
|
||||||
|
def save_changes_delayed(self):
|
||||||
|
sys.stderr.write("Pre-HEHEHE\n")
|
||||||
|
if not self._save_thread:
|
||||||
|
def threadf():
|
||||||
|
sys.stderr.write("HEHEHE\n")
|
||||||
|
self.save_changes()
|
||||||
|
self._save_thread = None
|
||||||
|
self._save_thread = threading.Timer(.25, threadf)
|
||||||
|
self._save_thread.start()
|
||||||
|
|
||||||
def save_changes(self):
|
def save_changes(self):
|
||||||
changes = self.commit_changes()
|
changes = self.commit_changes()
|
||||||
self._client.send_changes(changes)
|
dchanges = []
|
||||||
|
for pos, change in changes:
|
||||||
|
dchange = change.to_dict()
|
||||||
|
if dchange:
|
||||||
|
dchanges.append((pos, dchange))
|
||||||
|
if dchanges:
|
||||||
|
self._client.send_changes(dchanges)
|
||||||
|
|
||||||
def load(self, pos):
|
def load(self, pos):
|
||||||
raise Exception
|
raise Exception
|
||||||
|
|
||||||
def load_list(self, coords):
|
def load_list(self, coords):
|
||||||
self._client.request_chunks(coords)
|
coords = [pos for pos in coords if pos not in self._chunks]
|
||||||
|
if coords:
|
||||||
|
self._client.request_chunks(coords)
|
||||||
|
|
||||||
|
#def unload(self, pos):
|
||||||
|
#raise Exception
|
||||||
|
|
||||||
|
def unload_list(self, coords):
|
||||||
|
if coords:
|
||||||
|
#self.save_changes()
|
||||||
|
self._client.unload_chunks(coords)
|
||||||
|
super().unload_list(coords)
|
||||||
|
|
||||||
|
# What needs to happen differently from the default implementation:
|
||||||
|
# loading -> only ask server when necessary
|
||||||
|
# unloading -> send message to server
|
||||||
|
# unloading -> commit changes when anything is actually unloaded
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from .chunks.py import ChunkPool
|
from chunks import ChunkPool
|
||||||
|
|
||||||
class ChunkDB():
|
class ChunkDB():
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
15
maps.py
15
maps.py
|
|
@ -16,10 +16,10 @@ class Map():
|
||||||
self.chunkpreload = 0 # preload chunks in this radius (they will count as "visible")
|
self.chunkpreload = 0 # preload chunks in this radius (they will count as "visible")
|
||||||
self.chunkunload = 5 # don't unload chunks within this radius
|
self.chunkunload = 5 # don't unload chunks within this radius
|
||||||
self.cursorpadding = 2
|
self.cursorpadding = 2
|
||||||
self.worldx = 0
|
self.worldx = -self.cursorpadding
|
||||||
self.worldy = 0
|
self.worldy = -self.cursorpadding
|
||||||
self.cursorx = self.cursorpadding
|
self.cursorx = 0
|
||||||
self.cursory = self.cursorpadding
|
self.cursory = 0
|
||||||
self.lastcurx = self.cursorx
|
self.lastcurx = self.cursorx
|
||||||
self.lastcury = self.cursory
|
self.lastcury = self.cursory
|
||||||
|
|
||||||
|
|
@ -137,6 +137,7 @@ class Map():
|
||||||
|
|
||||||
if chunk:
|
if chunk:
|
||||||
chunk.set(inchunkx(self.cursorx), inchunky(self.cursory), char)
|
chunk.set(inchunkx(self.cursorx), inchunky(self.cursory), char)
|
||||||
|
pool.save_changes_delayed()
|
||||||
|
|
||||||
self.move_cursor(1, 0, False)
|
self.move_cursor(1, 0, False)
|
||||||
|
|
||||||
|
|
@ -145,8 +146,9 @@ class Map():
|
||||||
chunk = pool.get(Position(chunkx(self.cursorx-1), chunky(self.cursory)))
|
chunk = pool.get(Position(chunkx(self.cursorx-1), chunky(self.cursory)))
|
||||||
if chunk:
|
if chunk:
|
||||||
chunk.delete(inchunkx(self.cursorx-1), inchunky(self.cursory))
|
chunk.delete(inchunkx(self.cursorx-1), inchunky(self.cursory))
|
||||||
|
pool.save_changes_delayed()
|
||||||
|
|
||||||
self.move_cursor(-1, 0, False)
|
self.move_cursor(-1, 0, False)
|
||||||
|
|
||||||
def newline(self):
|
def newline(self):
|
||||||
self.set_cursor(self.lastcurx, self.lastcury+1)
|
self.set_cursor(self.lastcurx, self.lastcury+1)
|
||||||
|
|
@ -208,6 +210,9 @@ class Map():
|
||||||
|
|
||||||
#self.load_visible()
|
#self.load_visible()
|
||||||
|
|
||||||
|
def apply_changes(self, changes):
|
||||||
|
self.chunkpool.apply_changes(changes)
|
||||||
|
|
||||||
ChunkStyle = namedtuple("ChunkStyle", "string color")
|
ChunkStyle = namedtuple("ChunkStyle", "string color")
|
||||||
|
|
||||||
class ChunkMap():
|
class ChunkMap():
|
||||||
|
|
|
||||||
93
server.py
93
server.py
|
|
@ -1 +1,94 @@
|
||||||
# import from chunks, dbchunkpool
|
# import from chunks, dbchunkpool
|
||||||
|
import json
|
||||||
|
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket
|
||||||
|
|
||||||
|
from utils import Position
|
||||||
|
from chunks import ChunkDiff
|
||||||
|
from dbchunkpool import DBChunkPool
|
||||||
|
|
||||||
|
pool = DBChunkPool()
|
||||||
|
clients = set()
|
||||||
|
|
||||||
|
class WotServer(WebSocket):
|
||||||
|
def handle_request_chunks(self, coords):
|
||||||
|
changes = []
|
||||||
|
with pool:
|
||||||
|
for coor in coords:
|
||||||
|
pos = Position(coor[0], coor[1])
|
||||||
|
change = pool.get(pos) or pool.create(pos)
|
||||||
|
dchange = change.as_diff().to_dict()
|
||||||
|
changes.append((pos, dchange))
|
||||||
|
|
||||||
|
self.loaded_chunks.add(pos)
|
||||||
|
|
||||||
|
message = {"type": "apply-changes", "data": changes}
|
||||||
|
print(f"Message bong sent: {json.dumps(message)}")
|
||||||
|
self.sendMessage(json.dumps(message))
|
||||||
|
|
||||||
|
def handle_unload_chunks(self, coords):
|
||||||
|
for coor in coords:
|
||||||
|
pos = Position(coor)
|
||||||
|
self.loaded_chunks.remove(pos)
|
||||||
|
|
||||||
|
def handle_save_changes(self, dchanges):
|
||||||
|
changes = []
|
||||||
|
for chunk in dchanges:
|
||||||
|
print("CHUNK!", chunk)
|
||||||
|
pos = Position(chunk[0][0], chunk[0][1])
|
||||||
|
change = ChunkDiff.from_dict(chunk[1])
|
||||||
|
changes.append((pos, change))
|
||||||
|
|
||||||
|
with pool:
|
||||||
|
pool.apply_changes(changes)
|
||||||
|
|
||||||
|
#with pool:
|
||||||
|
#for chunk in changes:
|
||||||
|
#print("changed content:", pool.get(chunk[0])._content)
|
||||||
|
|
||||||
|
for client in clients:
|
||||||
|
client.send_changes(changes)
|
||||||
|
|
||||||
|
def send_changes(self, changes):
|
||||||
|
print("NORMAL CHANGES:", changes)
|
||||||
|
dchanges = []
|
||||||
|
for chunk in changes:
|
||||||
|
pos = chunk[0]
|
||||||
|
change = chunk[1]
|
||||||
|
if pos in self.loaded_chunks:
|
||||||
|
dchanges.append((pos, change.to_dict()))
|
||||||
|
print("LOADED CHANGES:", dchanges)
|
||||||
|
|
||||||
|
if dchanges:
|
||||||
|
print("Changes!")
|
||||||
|
message = {"type": "apply-changes", "data": dchanges}
|
||||||
|
print("Changes?")
|
||||||
|
print(f"Message bang sent: {json.dumps(message)}")
|
||||||
|
self.sendMessage(json.dumps(message))
|
||||||
|
|
||||||
|
def handleMessage(self):
|
||||||
|
message = json.loads(self.data)
|
||||||
|
print(f"message arrived: {message}")
|
||||||
|
if message["type"] == "request-chunks":
|
||||||
|
self.handle_request_chunks(message["data"])
|
||||||
|
elif message["type"] == "unload-chunks":
|
||||||
|
self.handle_unload_chunks(message["data"])
|
||||||
|
elif message["type"] == "save-changes":
|
||||||
|
self.handle_save_changes(message["data"])
|
||||||
|
|
||||||
|
print("Message received and dealt with.")
|
||||||
|
#changes = []
|
||||||
|
#for chunk in message["data"]:
|
||||||
|
#pass
|
||||||
|
#self.sendMessage(self.data)
|
||||||
|
|
||||||
|
def handleConnected(self):
|
||||||
|
print(self.address, 'connected')
|
||||||
|
clients.add(self)
|
||||||
|
self.loaded_chunks = set()
|
||||||
|
|
||||||
|
def handleClose(self):
|
||||||
|
print(self.address, 'closed')
|
||||||
|
clients.remove(self)
|
||||||
|
|
||||||
|
server = SimpleWebSocketServer('', 8000, WotServer)
|
||||||
|
server.serveforever()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue