1 | # |
||
2 | # Copyright 2001 - 2016 Ludek Smid [http://www.ospace.net/] |
||
3 | # |
||
4 | # This file is part of Outer Space. |
||
5 | # |
||
6 | # Outer Space is free software; you can redistribute it and/or modify |
||
7 | # it under the terms of the GNU General Public License as published by |
||
8 | # the Free Software Foundation; either version 2 of the License, or |
||
9 | # (at your option) any later version. |
||
10 | # |
||
11 | # Outer Space is distributed in the hope that it will be useful, |
||
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
14 | # GNU General Public License for more details. |
||
15 | # |
||
16 | # You should have received a copy of the GNU General Public License |
||
17 | # along with Outer Space; if not, write to the Free Software |
||
18 | # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA |
||
19 | # |
||
20 | |||
21 | # Game manager |
||
22 | # Multiple instances can be created (one for each game) |
||
23 | import os |
||
24 | import os.path |
||
25 | import time |
||
26 | import random, hashlib # TODO: remove after 0.5.74 release |
||
27 | |||
28 | import ige |
||
29 | import log |
||
30 | import Const |
||
31 | |||
32 | from SQLiteDatabase import Database |
||
33 | from Index import Index |
||
34 | from IObject import IDataHolder |
||
35 | from Transaction import Transaction |
||
36 | |||
37 | class GameMngr: |
||
38 | |||
39 | def __init__(self, gameID, config, clientMngr, msgMngr, database, configDir, gameName = None): |
||
40 | log.debug("Runtime mode", ige.igeRuntimeMode) |
||
41 | self.status = Const.GS_INIT |
||
42 | self.gameID = gameID |
||
43 | self.gameName = gameName or gameID |
||
44 | self.clientMngr = clientMngr |
||
45 | self.msgMngr = msgMngr |
||
46 | self.cmdPool = {} |
||
47 | self.db = database |
||
48 | self.config = config |
||
49 | self.configDir = configDir |
||
50 | # register command objects |
||
51 | # None here |
||
52 | |||
53 | def init(self): |
||
54 | pass |
||
55 | |||
56 | def start(self): |
||
57 | if self.status == Const.GS_RUNNING: |
||
58 | return |
||
59 | # start timer |
||
60 | self.status = Const.GS_RUNNING |
||
61 | |||
62 | def stop(self, checkpoint = 1): |
||
63 | if self.status == Const.GS_STOPPED: |
||
64 | return |
||
65 | # stop timer |
||
66 | self.status = Const.GS_STOPPED |
||
67 | if checkpoint: |
||
68 | self.db.checkpoint() |
||
69 | self.msgMngr.checkpoint() |
||
70 | self.clientMngr.checkpoint() |
||
71 | |||
72 | def shutdown(self): |
||
73 | if self.status == Const.GS_SDOWN: |
||
74 | return |
||
75 | self.stop(checkpoint = 0) |
||
76 | self.status = Const.GS_SDOWN |
||
77 | self.db.shutdown() |
||
78 | |||
79 | def upgrade(self): |
||
80 | oldStatus = self.status |
||
81 | self.status = Const.GS_MAINT |
||
82 | tran = Transaction(self, Const.OID_ADMIN) |
||
83 | # used objects |
||
84 | objIDs = {} |
||
85 | for objID in self.db.keys(): |
||
86 | objIDs[objID] = None |
||
87 | del objIDs[1] |
||
88 | del objIDs[Const.OID_ADMIN] |
||
89 | del objIDs[Const.OID_I_LOGIN2OID] |
||
90 | # stats |
||
91 | types = {} |
||
92 | typesMin = {} |
||
93 | typesMax = {} |
||
94 | typesSum = {} |
||
95 | # TODO: remove after 0.5.74 |
||
96 | # hash passwords in database |
||
97 | for accountID in self.clientMngr.accounts.keys(): |
||
98 | account = self.clientMngr.accounts[accountID] |
||
99 | if hasattr(account, 'passwdHashed'): |
||
100 | continue |
||
101 | if isinstance(account.passwd, unicode): |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
![]() |
|||
102 | account.passwd = account.passwd.encode('utf-8') |
||
103 | elif not isinstance(account.passwd, str): |
||
104 | # unexpected! |
||
105 | raise TypeError |
||
106 | # hash passwords of normal players |
||
107 | if account.isAI or account.login == Const.ADMIN_LOGIN: |
||
108 | account.passwdHashed = False |
||
109 | else: |
||
110 | account.passwdHashed = True |
||
111 | account.setPassword(account.passwd) |
||
112 | # upgrade all objects in database |
||
113 | # and collect all not referenced objects |
||
114 | for id in self.db.keys(): |
||
115 | try: |
||
116 | obj = self.db[id] |
||
117 | except: |
||
118 | log.warning("Cannot upgrade object", id, "no such id in db") |
||
119 | if not isinstance(obj, IDataHolder): |
||
120 | #@log.debug('Upgrade - skiping', id) |
||
121 | continue |
||
122 | #@log.debug('Upgrade - upgrading', id, obj.type) |
||
123 | types[obj.type] = types.get(obj.type, 0) + 1 |
||
124 | size = self.db.getItemLength(id) |
||
125 | typesMin[obj.type] = min(typesMin.get(obj.type, 1000000), size) |
||
126 | typesMax[obj.type] = max(typesMax.get(obj.type, 0), size) |
||
127 | typesSum[obj.type] = typesSum.get(obj.type, 0) + size |
||
128 | if self.cmdPool.has_key(obj.type): |
||
129 | try: |
||
130 | self.cmdPool[obj.type].upgrade(tran, obj) |
||
131 | except Exception, e: |
||
132 | log.warning("Cannot upgrade object", id) |
||
133 | references = self.cmdPool[obj.type].getReferences(tran, obj) |
||
134 | if references: |
||
135 | for tmpID in references: |
||
136 | if tmpID in objIDs: |
||
137 | del objIDs[tmpID] |
||
138 | # delete all not referenced objects |
||
139 | for objID in objIDs: |
||
140 | log.debug(objID, "is not referenced, deleting it") |
||
141 | del tran.db[objID] |
||
142 | # print stats |
||
143 | log.debug("*****") |
||
144 | for t in types: |
||
145 | log.debug("Object type %d:" % t) |
||
146 | log.debug(" occurences : %d" % types[t]) |
||
147 | log.debug(" size interval : %d - %d bytes" % (typesMin[t], typesMax[t])) |
||
148 | log.debug(" total size : %d (avg %d) bytes" % (typesSum[t], typesSum[t] / types[t])) |
||
149 | self.status = oldStatus |
||
150 | |||
151 | def reset(self): |
||
152 | # cleanup database |
||
153 | self.db.clear() |
||
154 | self.msgMngr.clear() |
||
155 | # create indexes |
||
156 | self.db.create(Index(), Const.OID_I_LOGIN2OID) |
||
157 | # create admin |
||
158 | self.registerPlayer(Const.ADMIN_LOGIN, self.createAdmin(), Const.OID_ADMIN) |
||
159 | # create universe |
||
160 | self.createUniverse() |
||
161 | # save all informations |
||
162 | self.db.checkpoint() |
||
163 | self.msgMngr.checkpoint() |
||
164 | self.clientMngr.checkpoint() |
||
165 | |||
166 | def processTurn(self, sid, turns = 1): |
||
167 | session = self.clientMngr.getSession(sid) |
||
168 | if session.login != Const.ADMIN_LOGIN: |
||
169 | raise ige.SecurityException('You cannot issue this command.') |
||
170 | for turn in xrange(turns): |
||
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
|
|||
171 | log.message("--- TURN PROCESSING STARTED ---") |
||
172 | # commit player's changes |
||
173 | #if ige.igeRuntimeMode: |
||
174 | # self.db.checkpoint() |
||
175 | # get turn phases |
||
176 | turn, turnspec, data = self.getTurnData(sid)[0] |
||
177 | log.debug('Processing turn %d' % turn) |
||
178 | tran = Transaction(self, session.cid, session) |
||
179 | counter = 0 |
||
180 | # phases |
||
181 | for objIDs, phases in turnspec: |
||
182 | # process all objects |
||
183 | for objID in objIDs: |
||
184 | # process all phases |
||
185 | for phase in phases: |
||
186 | todo = [objID] |
||
187 | t0 = time.time() |
||
188 | cnt0 = self.db.statCount |
||
189 | log.debug('Processing turn %d phase %d.%s' % (turn, objID, phase)) |
||
190 | while todo: |
||
191 | tmpID = todo.pop(0) |
||
192 | #@log.debug('Processing obj', tmpID) |
||
193 | try: |
||
194 | counter += 1 |
||
195 | obj = self.db[tmpID] |
||
196 | method = getattr(self.cmdPool[obj.type], 'process%sPhase' % phase,) |
||
197 | result = method(tran, obj, data) |
||
198 | if result: |
||
199 | todo.extend(result) |
||
200 | obj = None |
||
201 | except: |
||
202 | log.warning('Cannot execute %s on %d' % (phase, tmpID)) |
||
203 | log.debug('STATS -- time: %.3f sec, db accesses: %d' % (time.time() - t0, tran.db.statCount - cnt0)) |
||
204 | log.message('Processed commands:', counter) |
||
205 | # turn processing has finished |
||
206 | self.turnFinished(sid) |
||
207 | log.message("--- TURN PROCESSING FINISHED ---") |
||
208 | return 1, None |
||
209 | |||
210 | def getTurnData(self, sid): |
||
211 | # disable command execution during turn processing |
||
212 | self.status = Const.GS_TURNINPROG |
||
213 | return 1, None |
||
214 | |||
215 | def turnFinished(self, sid): |
||
216 | # notify logged player's about finished turn |
||
217 | for sessionID in self.clientMngr.sessions.keys(): |
||
218 | session = self.clientMngr.getSession(sessionID) |
||
219 | session.messages[Const.SMESSAGE_NEWTURN] = None |
||
220 | # commit only in normal mode |
||
221 | log.debug("Runtime mode", ige.igeRuntimeMode) |
||
222 | if ige.igeRuntimeMode: |
||
223 | self.db.checkpoint() |
||
224 | self.msgMngr.checkpoint() |
||
225 | self.clientMngr.checkpoint() |
||
226 | # enable normal operations |
||
227 | self.status = Const.GS_RUNNING |
||
228 | return 1, None |
||
229 | |||
230 | def backup(self, sid, basename): |
||
231 | session = self.clientMngr.getSession(sid) |
||
232 | if session.login != Const.ADMIN_LOGIN: |
||
233 | raise ige.SecurityException('You cannot issue this command.') |
||
234 | self.db.backup(basename) |
||
235 | self.clientMngr.backup(basename) |
||
236 | self.msgMngr.backup(basename) |
||
237 | return True, None |
||
238 | |||
239 | def commitDatabases(self, sid): |
||
240 | session = self.clientMngr.getSession(sid) |
||
241 | if session.login != Const.ADMIN_LOGIN: |
||
242 | raise ige.SecurityException('You cannot issue this command.') |
||
243 | self.db.checkpoint() |
||
244 | self.clientMngr.checkpoint() |
||
245 | self.msgMngr.checkpoint() |
||
246 | return True, None |
||
247 | |||
248 | def createAdmin(self): |
||
249 | """ Return Player object which will act as administrator of the game.""" |
||
250 | raise NotImplementedError |
||
251 | |||
252 | def createUniverse(self): |
||
253 | """ Create gaming universe. """ |
||
254 | raise NotImplementedError |
||
255 | |||
256 | def createPlayer(self, sid, *args, **kwargs): |
||
257 | raise NotImplementedError |
||
258 | |||
259 | def removePlayer(self, sid, *args, **kwargs): |
||
260 | raise NotImplementedError |
||
261 | |||
262 | def selectPlayer(self, sid, playerID): |
||
263 | """ Selects which of the player objects of the account is going to be |
||
264 | used for this particular session.""" |
||
265 | |||
266 | session = self.clientMngr.getSession(sid) |
||
267 | if session.cid: |
||
268 | raise ige.GameException('You already selected a player object.') |
||
269 | try: |
||
270 | accounts_player_objects = self.db[Const.OID_I_LOGIN2OID].get(session.login, []) |
||
271 | except AttributeError: |
||
272 | raise ige.SecurityException('Not logged in.') |
||
273 | |||
274 | if playerID not in accounts_player_objects: |
||
275 | raise ige.NoAccountException('Player object not on this account.') |
||
276 | |||
277 | log.debug('Adding cid to session', playerID) |
||
278 | session.cid = playerID |
||
279 | # validate client |
||
280 | if not self.validateClient(session): |
||
281 | raise ige.GameException('Wrong version of client.') |
||
282 | # notify object, that player has logged in |
||
283 | player = self.db[playerID] |
||
284 | self.cmdPool[player.type].loggedIn(Transaction(self), player) |
||
285 | return True, None |
||
286 | |||
287 | def registerPlayer(self, login, playerObj, oid = None, force = 0): |
||
288 | raise NotImplementedError |
||
289 | |||
290 | def unregisterPlayer(self, playerObj): |
||
291 | log.debug('unregisterPlayer', playerObj.login, playerObj.name) |
||
292 | # preconditions |
||
293 | if not self.db[Const.OID_I_LOGIN2OID].has_key(playerObj.login): |
||
294 | log.debug("Account %s does not exist" % playerObj.login) |
||
295 | # try to remove it |
||
296 | try: |
||
297 | self.db[Const.OID_I_LOGIN2OID][playerObj.login].remove(playerObj.oid) |
||
298 | except: |
||
299 | log.warning("Cannot remove '%s' from LOGIN2OID index" % playerObj.login) |
||
300 | try: |
||
301 | self.db.delete(playerObj.oid) |
||
302 | except: |
||
303 | log.warning("Cannot remove player %d from database" % playerObj.oid) |
||
304 | |||
305 | def validateClient(self, session): |
||
306 | raise NotImplementedError |
||
307 | |||
308 | def registerObject(self, cls): |
||
309 | cmdObj = cls(self) |
||
310 | self.cmdPool[cmdObj.typeID] = cmdObj |
||
311 | |||
312 | def sendMessage(self, tran, sourceID, msgID, locationID, turn, data): |
||
313 | #@log.debug('Message', sourceID, msgID, locationID, turn, data) |
||
314 | obj = self.db[sourceID] |
||
315 | # notify owner |
||
316 | if obj.owner == Const.OID_NONE: |
||
317 | log.warning('OID', sourceID, 'has no owner - no target for a message') |
||
318 | else: |
||
319 | owner = self.db[obj.owner] |
||
320 | # new style messages |
||
321 | message = { |
||
322 | "sender": obj.name, |
||
323 | "senderID": sourceID, |
||
324 | "forum": "EVENTS", |
||
325 | "data": (sourceID, msgID, locationID, turn, data), |
||
326 | "topic": "EVENT", |
||
327 | } |
||
328 | self.cmdPool[owner.type].sendAdminMsg(tran, owner, message) |
||
329 | session = self.clientMngr.getSessionByCID(obj.owner) |
||
330 | if session: |
||
331 | session.messages[Const.SMESSAGE_NEWMESSAGE] = None |
||
332 | |||
333 | # dispatch command |
||
334 | def execute(self, sid, command, oid, *args): |
||
335 | #@startTime = time.time() |
||
336 | log.debug('execute', sid, oid, command, args) |
||
337 | # check client id |
||
338 | session = self.clientMngr.getSession(sid) |
||
339 | if not session.cid: |
||
340 | raise ige.SecurityException('No player object selected.') |
||
341 | if not self.validateClient(session): |
||
342 | raise ige.GameException('Wrong version of client.') |
||
343 | # check game status (admin is allowed anytime) |
||
344 | if self.status != Const.GS_RUNNING and session.cid != Const.OID_ADMIN: |
||
345 | raise ige.ServerStatusException(self.status) |
||
346 | # check existence of the commander |
||
347 | if not self.db.has_key(session.cid): |
||
348 | raise ige.GameException('This player does not exist. He/she could lose.') |
||
349 | # update client's liveness |
||
350 | session.touch() |
||
351 | # find correct object type |
||
352 | try: |
||
353 | obj = self.db[oid] |
||
354 | except KeyError: |
||
355 | raise ige.NoSuchObjectException('Object %d does not exist.' % oid) |
||
356 | cmdObj = getattr(self.cmdPool[obj.type], command) |
||
357 | if not hasattr(cmdObj, 'public') or not cmdObj.public: |
||
358 | raise ige.SecurityException('Access denied - method is not public.') |
||
359 | # get acces level of the commander |
||
360 | accLevel = Const.AL_NONE |
||
361 | if obj.owner == session.cid: |
||
362 | accLevel = Const.AL_OWNER |
||
363 | if session.cid == Const.OID_ADMIN: |
||
364 | accLevel = Const.AL_ADMIN |
||
365 | #@log.debug('access rights', accLevel, cmdObj.accLevel) |
||
366 | if cmdObj.accLevel > accLevel: |
||
367 | raise ige.SecurityException('Access denied - low access level.') |
||
368 | # create transaction (TODO - cache it!) |
||
369 | tran = Transaction(self, session.cid, session) |
||
370 | # invoke command on it |
||
371 | result = cmdObj(*(tran, obj) + args) |
||
372 | # commit transaction |
||
373 | tran.commit() |
||
374 | #@log.debug('result', result) |
||
375 | # session messages |
||
376 | #@log.debug('Messages:', session.messages.items()) |
||
377 | messages = session.messages.items() |
||
378 | session.messages.clear() |
||
379 | #@log.debug("Execution time", time.time() - startTime) |
||
380 | return result, messages |
||
381 | |||
382 |