634 lines
19 KiB
Python
634 lines
19 KiB
Python
# This file is part of MarMOTS.
|
|
#
|
|
# MarMOTS is free software: you can redistribute it and/or modify it under the terms of the GNU Affero
|
|
# General Public License as published by the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# MarMOTS 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 Affero General
|
|
# Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with MarMOTS. If not,
|
|
# see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# Copyright 2009, 2010, 2011, 2020 Jeremy Penner
|
|
|
|
import stackless
|
|
import greenlet
|
|
import random
|
|
import logging
|
|
import pickle
|
|
import time
|
|
import ansi_cython as ansi
|
|
import auth
|
|
import config
|
|
import telnet
|
|
import traceback
|
|
from twisted.internet import reactor
|
|
from tpers import TPrs, TPLS, Version
|
|
import tpers
|
|
|
|
|
|
# TPrs - persistent base class; includes migration facilities and the ability to selectively opt-out of
|
|
# being persisted. Most long-lived objects should probably be derived from TPrs.
|
|
# Ownable - has a parent ownable; die() will be called when its parent dies.
|
|
# Taggable - ownable, and also has tags. Assumes an eventual Game ancestor where the tag index is stored.
|
|
# Token - taggable, and also has a tasklet that runs for as long as it's alive.
|
|
|
|
class Event(TPrs):
|
|
def InitPersistent(self, game):
|
|
self.game = game
|
|
|
|
def InitTransient(self):
|
|
self.rgtk = TPLS()
|
|
self.queueValue = []
|
|
|
|
def oob(self, tok, dgoob):
|
|
return Oob(self, tok, dgoob)
|
|
|
|
def receive(self, tok):
|
|
chNotify = tok.chEv
|
|
self.rgtk.append(tok)
|
|
while True:
|
|
ev, value = chNotify.receive()
|
|
if ev == self:
|
|
return value
|
|
ev.rgtk.append(tok)
|
|
if ev in tok.mpevDgoob and tok.mpevDgoob[ev](value):
|
|
raise Event.OobBreakException()
|
|
|
|
@classmethod
|
|
def select(cls, tok, *rgev):
|
|
for ev in rgev:
|
|
ev.rgtk.append(tok)
|
|
chNotify = tok.chEv
|
|
fOob = False
|
|
while True:
|
|
evRcv, value = chNotify.receive()
|
|
if evRcv in tok.mpevDgoob:
|
|
if tok.mpevDgoob[ev](value):
|
|
fOob = True
|
|
break
|
|
else:
|
|
break
|
|
for ev in rgev:
|
|
if ev != evRcv:
|
|
ev.rgtk.remove(tok)
|
|
if fOob:
|
|
raise Event.OobBreakException()
|
|
return evRcv, value
|
|
|
|
@classmethod
|
|
def selectDg(cls, tok, *rgev_dg):
|
|
rgev, rgdg = zip(*rgev_dg)
|
|
ev, value = cls.select(tok, *rgev)
|
|
return rgdg[rgev.index(ev)](value)
|
|
|
|
def fire(self, value = None):
|
|
self.queueValue.append(value)
|
|
self.game.queueEv.append(self)
|
|
self.game.ensureRun()
|
|
|
|
def fireI(self):
|
|
rgtkOld = self.rgtk
|
|
value = self.queueValue[0]
|
|
self.queueValue = self.queueValue[1:]
|
|
self.rgtk = TPLS()
|
|
for tk in rgtkOld:
|
|
if tk.fRunning:
|
|
tk.chEv.send((self,value))
|
|
stackless.run()
|
|
|
|
class Oob(object):
|
|
def __init__(self, ev, tok, dgoob):
|
|
self.ev = ev
|
|
self.tok = tok
|
|
self.dgoob = dgoob
|
|
def __enter__(self):
|
|
self.ev.rgtk.append(self.tok)
|
|
self.tok.mpevDgoob[self.ev] = self.dgoob
|
|
def __exit__(self, etype, eval, tb):
|
|
if etype != greenlet.GreenletExit:
|
|
try:
|
|
self.tok.mpevDgoob.pop(self.ev)
|
|
self.ev.rgtk.remove(self.tok)
|
|
if etype == OobBreakException:
|
|
return True
|
|
except Exception:
|
|
print "exception exiting oob:"
|
|
traceback.print_exc()
|
|
print "oob:", etype, eval
|
|
traceback.print_tb(tb)
|
|
|
|
class OobBreakException(Exception):
|
|
pass
|
|
|
|
class Runner(TPrs):
|
|
def InitPersistent(self):
|
|
self.mpuser_cld = {}
|
|
print "New Runner created"
|
|
|
|
def InitTransient(self):
|
|
self.rgclient = []
|
|
|
|
def GetCld(self, user):
|
|
cld = self.mpuser_cld.get(user)
|
|
if cld == None:
|
|
cld = Cld(user)
|
|
self.mpuser_cld[user] = cld
|
|
return cld
|
|
|
|
def RunGameI(self, client):
|
|
client.runner = self
|
|
self.rgclient.append(client)
|
|
def OnDisconnect():
|
|
self.rgclient.remove(client)
|
|
client.addDgQuit(OnDisconnect)
|
|
return self.RunGame(client)
|
|
|
|
def RunServer(self):
|
|
for client in self.rgclient:
|
|
client.quit(True)
|
|
telnet.RunServer(self.RunGameI)
|
|
def Run(clsRunner, fn):
|
|
runner = tpers.Odb.Load(fn) or clsRunner()
|
|
|
|
def ExecCmd(stCmd):
|
|
if stCmd == "save":
|
|
tpers.Odb.Save(runner, fn)
|
|
return "Saved to " + fn
|
|
return "Unrecognized command: '" + stCmd + "'"
|
|
telnet.RunCmdServer(ExecCmd)
|
|
|
|
runner.RunServer()
|
|
|
|
class Ownable(TPrs):
|
|
def InitPersistent(self, owner = None):
|
|
self.owner = owner
|
|
self.rgtokOwn = TPLS()
|
|
if owner != None:
|
|
owner.rgtokOwn.append(self)
|
|
def FPersist(self):
|
|
if self.owner != None and not self.owner.FPersist():
|
|
return False
|
|
return TPrs.FPersist(self)
|
|
def TransferToOwner(self, ownerNew):
|
|
self.owner.rgtokOwn.remove(self)
|
|
ownerNew.rgtokOwn.append(self)
|
|
self.owner = ownerNew
|
|
def die(self):
|
|
if self.owner != None:
|
|
self.owner.rgtokOwn.remove(self)
|
|
self.owner = None
|
|
for tok in [x for x in self.rgtokOwn]:
|
|
tok.die()
|
|
self.DontPersist()
|
|
|
|
@Version(3)
|
|
class Game(Ownable):
|
|
def InitPersistent(self):
|
|
Ownable.InitPersistent(self)
|
|
self.fQuit = False
|
|
self.running = False
|
|
self.mptagTokenset = {}
|
|
self.rgdgdie = []
|
|
self.evJoin = Event(self)
|
|
self.evLeave = Event(self)
|
|
self.evActivate = Event(self)
|
|
self.evSuspend = Event(self)
|
|
self.mpuser_cldg = {}
|
|
self.CreateRgtok(self.GetRgclsTokPers(), True)
|
|
|
|
def InitTransient(self):
|
|
self.MakeTransient("fScheduled", False)
|
|
self.rgclientActive = []
|
|
self.rgclientSuspended = []
|
|
self.rgclientLeaving = []
|
|
self.rgproc = [PTimer(), PEvent(), PDraw(*self.GetRgtagDraw())]
|
|
for proc in self.rgproc:
|
|
proc.setGame(self)
|
|
# don't recreate tokens when game has died (shouldn't even be persisting)
|
|
if not self.FDead():
|
|
self.CreateRgtok(self.GetRgclsTokTrans(), False)
|
|
else:
|
|
print "Dead", self.__class__, "has persisted"
|
|
|
|
def FDead(self):
|
|
try:
|
|
rc = self.rc
|
|
return True
|
|
except:
|
|
return False
|
|
def GetCldg(self, user):
|
|
cldg = self.mpuser_cldg.get(user)
|
|
if cldg == None:
|
|
cldg = Cldg()
|
|
self.mpuser_cldg[user] = cldg
|
|
return cldg
|
|
|
|
def GetRgclsTokPers(self):
|
|
return []
|
|
|
|
def GetRgclsTokTrans(self):
|
|
return []
|
|
|
|
def GetRgtagDraw(self):
|
|
return ["background", "solid", "overlay"]
|
|
|
|
def CreateRgtok(self, rgclsTok, fPersist):
|
|
for clsTok in rgclsTok:
|
|
if FIter(clsTok):
|
|
tok = clsTok[0](self, *clsTok[1:])
|
|
else:
|
|
tok = clsTok(self)
|
|
if not fPersist:
|
|
tok.DontPersist()
|
|
|
|
def RgclientConnected(self):
|
|
return self.rgclientActive + self.rgclientSuspended
|
|
|
|
def tagToken(self, token, tag):
|
|
if not (tag in self.mptagTokenset):
|
|
self.mptagTokenset[tag] = set([token])
|
|
else:
|
|
self.mptagTokenset[tag].add(token)
|
|
|
|
def untagToken(self, token, tag):
|
|
if tag in self.mptagTokenset:
|
|
self.mptagTokenset[tag].discard(token)
|
|
|
|
def rgtoken(self, tag, rgpriority = []):
|
|
if not (tag in self.mptagTokenset):
|
|
return []
|
|
|
|
rgtoken = []
|
|
tokenSet = self.mptagTokenset[tag].copy()
|
|
for priority in rgpriority:
|
|
if (priority in self.mptagTokenset):
|
|
ptokenSet = self.mptagTokenset[priority] & tokenSet
|
|
for token in ptokenSet:
|
|
rgtoken.append(token)
|
|
tokenSet -= ptokenSet
|
|
for token in tokenSet:
|
|
rgtoken.append(token)
|
|
|
|
return rgtoken
|
|
|
|
def quit(self):
|
|
assert len(self.rgclientSuspended) == 0
|
|
self.running = False
|
|
self.fQuit = True
|
|
|
|
def finish(self, rc = None):
|
|
assert len(self.rgclientSuspended) == 0
|
|
self.running = False
|
|
self.rc = rc
|
|
class QuitException(Exception):
|
|
pass
|
|
|
|
class LoadException(Exception):
|
|
pass
|
|
|
|
def die(self):
|
|
for dgdie in self.rgdgdie:
|
|
dgdie()
|
|
Ownable.die(self)
|
|
def runStep(self):
|
|
try:
|
|
if self.running:
|
|
for proc in self.rgproc:
|
|
proc.process()
|
|
if not self.running:
|
|
self.die()
|
|
for client in self.rgclientActive:
|
|
if client not in self.rgclientLeaving:
|
|
client.leaveGame(self.rc)
|
|
deadClients = list(self.rgclientLeaving)
|
|
del self.rgclientLeaving[:]
|
|
for client in deadClients:
|
|
if client in self.rgclientActive:
|
|
self.rgclientActive.remove(client)
|
|
if client in self.rgclientSuspended:
|
|
self.rgclientSuspended.remove(client)
|
|
client.postRunStep()
|
|
finally:
|
|
self.fScheduled = False
|
|
return self.running
|
|
def joinGame(self,client):
|
|
self.rgclientActive.append(client)
|
|
self.evJoin.fire(client)
|
|
self.evActivate.fire(client)
|
|
|
|
def leaveGame(self, client):
|
|
if self.running:
|
|
if client in self.rgclientActive:
|
|
self.evSuspend.fire(client)
|
|
self.evLeave.fire(client)
|
|
self.rgclientLeaving.append(client)
|
|
|
|
def reactivateClient(self, client):
|
|
assert client in self.rgclientSuspended
|
|
self.rgclientSuspended.remove(client)
|
|
self.rgclientActive.append(client)
|
|
self.evActivate.fire(client)
|
|
|
|
def suspendClient(self, client):
|
|
assert client in self.rgclientActive
|
|
self.rgclientActive.remove(client)
|
|
self.rgclientSuspended.append(client)
|
|
self.evSuspend.fire(client)
|
|
|
|
def ensureRun(self):
|
|
self.running = True
|
|
if not self.fScheduled:
|
|
self.fScheduled = True
|
|
reactor.callLater(0, self.runStep)
|
|
def UpgradeFrom(self, versionOld):
|
|
if versionOld < 2:
|
|
self.mpuser_cldg = {}
|
|
if versionOld < 3:
|
|
self.evActivate = Event(self)
|
|
self.evSuspend = Event(self)
|
|
|
|
# Client data
|
|
@Version(2)
|
|
class Cld(TPrs):
|
|
def InitPersistent(self, user):
|
|
self.user = user
|
|
def UpgradeFrom(self, versionOld):
|
|
if versionOld < 2:
|
|
del self.mpgame_cldg
|
|
|
|
#per-game client data
|
|
class Cldg(TPrs):
|
|
def FPersist(self):
|
|
if len(self._persistent) == 0:
|
|
return False
|
|
return TPrs.FPersist(self)
|
|
|
|
class Client(TPrs):
|
|
def InitTransient(self):
|
|
self.chRun = stackless.channel()
|
|
self.gameCurr = None
|
|
self.mpgame_evKey = {}
|
|
self.rggameSuspended = []
|
|
self.chCont = None
|
|
self.rgdgQuit = []
|
|
self.fQuit = False
|
|
self.cld = None
|
|
self.runner = None
|
|
|
|
def FPersist(self):
|
|
return False
|
|
def go(self, fnRun, protocol):
|
|
self.protocol = protocol
|
|
self.tskRun = stackless.tasklet(fnRun)
|
|
self.tskRun(self)
|
|
if self._runTskRun():
|
|
self.gameCurr.ensureRun()
|
|
def joinGame(self, game):
|
|
if not self.gameCurr:
|
|
chCont = stackless.channel()
|
|
self.chRun.send((game, chCont))
|
|
return chCont.receive()
|
|
else:
|
|
self.gameCurr.suspendClient(self)
|
|
self.rggameSuspended.append(self.gameCurr)
|
|
self.gameCurr = game
|
|
self.gameCurr.joinGame(self)
|
|
|
|
def leaveGame(self, rc = None):
|
|
self.gameCurr.leaveGame(self)
|
|
if self.gameCurr in self.mpgame_evKey:
|
|
del self.mpgame_evKey[self.gameCurr]
|
|
|
|
if len(self.rggameSuspended) == 0:
|
|
self.gameCurr = None
|
|
self.rc = rc
|
|
else:
|
|
self.gameCurr = self.rggameSuspended.pop()
|
|
self.gameCurr.reactivateClient(self)
|
|
|
|
def quit(self):
|
|
while self.gameCurr:
|
|
self.leaveGame(None)
|
|
self.fQuit = True
|
|
|
|
def _runTskRun(self):
|
|
"run self.tskRun after it has been scheduled, and await results"
|
|
stackless.run()
|
|
if not self.tskRun.scheduled:
|
|
return False
|
|
self.gameCurr, self.chCont = self.chRun.receive()
|
|
self.gameCurr.joinGame(self)
|
|
return True
|
|
|
|
def postRunStep(self):
|
|
if not self.gameCurr:
|
|
if not self.fQuit:
|
|
self.chCont.send(self.rc)
|
|
self.fQuit = not self._runTskRun()
|
|
if self.fQuit:
|
|
for dgQuit in self.rgdgQuit:
|
|
dgQuit()
|
|
del self.rgdgQuit[:]
|
|
return self.fQuit
|
|
def beep(self):
|
|
pass
|
|
def login(self, user, passwd):
|
|
authr = getattr(auth, config.AUTH + "Auth")()
|
|
if (authr.FAuthorized(user, passwd)):
|
|
self.cld = self.runner.GetCld(user)
|
|
return True
|
|
return False
|
|
def addDgQuit(self, dgquit):
|
|
self.rgdgQuit.append(dgquit)
|
|
|
|
def removeDgQuit(self, dgquit):
|
|
self.rgdgQuit.remove(dgquit)
|
|
|
|
def EvKey(self, game):
|
|
if game:
|
|
if game not in self.mpgame_evKey:
|
|
self.mpgame_evKey[game] = Event(game)
|
|
return self.mpgame_evKey[game]
|
|
|
|
def KeyPressed(self, ch):
|
|
if self.gameCurr:
|
|
self.EvKey(self.gameCurr).fire(ch)
|
|
|
|
def Cldg(self, game):
|
|
return game.GetCldg(self.cld.user)
|
|
|
|
class Processor(TPrs):
|
|
def setGame(self, game):
|
|
self.game = game
|
|
|
|
def process(self):
|
|
pass
|
|
|
|
class PTimer(Processor):
|
|
def msNow(self):
|
|
return int(time.time() * 1000)
|
|
def setGame(self, game):
|
|
self.game = game
|
|
self.fReset = False
|
|
game.evTick = Event(game)
|
|
game.ms = 0
|
|
game.msDelta = 0
|
|
self.dms = -self.msNow()
|
|
|
|
def __setstate__(self, dict):
|
|
Processor.__setstate__(self, dict)
|
|
self.fReset = True
|
|
def process(self):
|
|
if self.fReset:
|
|
# keep game.ms constant
|
|
self.dms = self.game.ms - self.msNow()
|
|
self.fReset = False
|
|
msNew = self.msNow() + self.dms
|
|
self.game.msDelta = msNew - self.game.ms
|
|
self.game.ms = msNew
|
|
self.game.evTick.fire(self.game.msDelta)
|
|
|
|
class PEvent(Processor):
|
|
def setGame(self, game):
|
|
self.game = game
|
|
game.queueEv = []
|
|
game.rgdgdie.append(self.die)
|
|
|
|
def die(self):
|
|
for ev in self.game.queueEv:
|
|
for tk in ev.rgtk:
|
|
tk.die()
|
|
ev.stkValue = []
|
|
self.game.queueEv = []
|
|
|
|
def process(self):
|
|
while self.game.queueEv:
|
|
self.game.queueEv[0].fireI()
|
|
self.game.queueEv.pop(0)
|
|
|
|
class PDraw(Processor):
|
|
def InitPersistent(self, *rgprio):
|
|
self.rgprio = rgprio
|
|
self.msDrawAgain = -1
|
|
self.fTimerSet = False
|
|
|
|
def process(self):
|
|
if self.game.ms >= self.msDrawAgain:
|
|
for client in self.game.rgclientActive:
|
|
ascr = ansi.Ascr()
|
|
for tok in self.game.rgtoken("drawable", self.rgprio):
|
|
try:
|
|
tok.draw(ascr, client)
|
|
except Exception:
|
|
print "error drawing"
|
|
traceback.print_exc()
|
|
|
|
client.protocol.Draw(ascr)
|
|
self.msDrawAgain = self.game.ms + 120
|
|
self.fTimerSet = False
|
|
elif not self.fTimerSet:
|
|
reactor.callLater((self.msDrawAgain - self.game.ms) / 1000.0, self.game.ensureRun)
|
|
self.fTimerSet = True
|
|
class Taggable(Ownable):
|
|
def InitPersistent(self, owner, *rgtag):
|
|
Ownable.InitPersistent(self, owner)
|
|
game = owner
|
|
while not isinstance(game, Game):
|
|
game = game.owner
|
|
self.game = game
|
|
self.tags = set()
|
|
self.tag(*rgtag)
|
|
|
|
def tag(self, *rgtag):
|
|
self.tags |= set(rgtag)
|
|
for tag in rgtag:
|
|
self.game.tagToken(self, tag)
|
|
|
|
def untag(self, *rgtag):
|
|
self.tags -= set(rgtag)
|
|
for tag in rgtag:
|
|
self.game.untagToken(self, tag)
|
|
|
|
def die(self):
|
|
for tag in self.tags.copy():
|
|
self.untag(tag)
|
|
Ownable.die(self)
|
|
|
|
class Token(Taggable):
|
|
def InitPersistent(self, owner, *rgtag):
|
|
Taggable.InitPersistent(self, owner, *rgtag)
|
|
self.msGamePreWait = 0
|
|
self.ms = 0
|
|
self.fRunning = True
|
|
self.rgtokOwn = TPLS()
|
|
|
|
def InitTransient(self):
|
|
Taggable.InitTransient(self)
|
|
self.chEv = stackless.channel()
|
|
self.tasklet = stackless.tasklet(self.runI)
|
|
self.mpevDgoob = {}
|
|
|
|
self._fWriteToPersistent = True
|
|
self.tasklet()
|
|
def __enter__(self):
|
|
return self
|
|
def __exit__(self, etype, eval, traceback):
|
|
self.die()
|
|
def wait(self, msDelta):
|
|
# have we waited once already this tick? If not, reset
|
|
if (self.msGamePreWait != self.game.ms):
|
|
self.ms = self.game.ms
|
|
|
|
msEnd = self.ms + msDelta
|
|
|
|
delay = (msEnd - self.game.ms) / 1000.0
|
|
if delay > 0:
|
|
reactor.callLater(delay, self.game.ensureRun)
|
|
|
|
while (self.game.ms < msEnd):
|
|
self.game.evTick.receive(self)
|
|
self.ms = msEnd
|
|
|
|
self.msGamePreWait = self.game.ms
|
|
|
|
|
|
def die(self):
|
|
self.fRunning = False
|
|
Taggable.die(self)
|
|
self.tasklet.kill() # the token may be killing itself; make sure it's properly marked as dead before killing the tasklet
|
|
|
|
def run(self):
|
|
pass
|
|
|
|
def runI(self):
|
|
try:
|
|
self.run()
|
|
except greenlet.GreenletExit:
|
|
raise
|
|
except Exception:
|
|
print "token script crashed:"
|
|
traceback.print_exc()
|
|
def FIter(l):
|
|
try:
|
|
iter(l)
|
|
return True
|
|
except Exception:
|
|
return False
|
|
|
|
class TokenClient(Token):
|
|
def InitPersistent(self, owner, client, *rgtag):
|
|
Token.InitPersistent(self, owner, *rgtag)
|
|
self.client = client
|
|
|
|
def FPersist(self):
|
|
return False
|
|
|
|
def Cldg(self):
|
|
return self.game.GetCldg(self.client)
|
|
|
|
def EvKey(self):
|
|
return self.client.EvKey(self.game)
|