home · contact · privacy
Server/py: Undummify remake_map().
[plomrogue] / plomrogue-server.py
1 import argparse
2 import errno
3 import os
4 import shlex
5 import shutil
6 import time
7 import ctypes
8
9
10 class RandomnessIO:
11     """"Interface to libplomrogue's pseudo-randomness generator."""
12
13     def set_seed(self, seed):
14         libpr.seed_rrand(1, seed)
15
16     def get_seed(self):
17         return libpr.seed_rrand(0, 0)
18
19     def next(self):
20         return libpr.rrand()
21
22     seed = property(get_seed, set_seed)
23
24
25 def prep_library():
26     """Prepare ctypes library at ./libplomrogue.so"""
27     libpath = ("./libplomrogue.so")
28     if not os.access(libpath, os.F_OK):
29         raise SystemExit("No library " + libpath + ", run ./compile.sh first?")
30     libpr = ctypes.cdll.LoadLibrary(libpath)
31     libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32]
32     libpr.seed_rrand.restype = ctypes.c_uint32
33     libpr.rrand.argtypes = []
34     libpr.rrand.restype = ctypes.c_uint16
35     return libpr
36
37
38 def strong_write(file, string):
39     """Apply write(string), flush(), and os.fsync() to file."""
40     file.write(string)
41     file.flush()
42     os.fsync(file)
43
44
45 def setup_server_io():
46     """Fill IO files DB with proper file( path)s. Write process IO test string.
47
48     Ensure IO files directory at server/. Remove any old input file if found.
49     Set up new input file for reading, and new output file for writing. Start
50     output file with process hash line of format PID + " " + floated UNIX time
51     (io_db["teststring"]). Raise SystemExit if file is found at path of either
52     record or save file plus io_db["tmp_suffix"].
53     """
54     def detect_atomic_leftover(path, tmp_suffix):
55         path_tmp = path + tmp_suffix
56         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
57               "aborted previous attempt to write '" + path + "'. Aborting " \
58              "until matter is resolved by removing it from its current path."
59         if os.access(path_tmp, os.F_OK):
60             raise SystemExit(msg)
61     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
62     os.makedirs(io_db["path_server"], exist_ok=True)
63     io_db["file_out"] = open(io_db["path_out"], "w")
64     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
65     if os.access(io_db["path_in"], os.F_OK):
66         os.remove(io_db["path_in"])
67     io_db["file_in"] = open(io_db["path_in"], "w")
68     io_db["file_in"].close()
69     io_db["file_in"] = open(io_db["path_in"], "r")
70     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
71     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
72
73
74 def cleanup_server_io():
75     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
76     def helper(file_key, path_key):
77         if file_key in io_db:
78             io_db[file_key].close()
79             if not io_db["kicked_by_rival"] \
80                and os.access(io_db[path_key], os.F_OK):
81                 os.remove(io_db[path_key])
82     helper("file_out", "path_out")
83     helper("file_in", "path_in")
84     helper("file_worldstate", "path_worldstate")
85     if "file_record" in io_db:
86         io_db["file_record"].close()
87
88
89 def obey(command, prefix, replay=False, do_record=False):
90     """Call function from commands_db mapped to command's first token.
91
92     Tokenize command string with shlex.split(comments=True). If replay is set,
93     a non-meta command from the commands_db merely triggers obey() on the next
94     command from the records file. If not, non-meta commands set
95     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
96     do_record is set, are recorded via record(), and save_world() is called.
97     The prefix string is inserted into the server's input message between its
98     beginning 'input ' & ':'. All activity is preceded by a server_test() call.
99     """
100     server_test()
101     print("input " + prefix + ": " + command)
102     try:
103         tokens = shlex.split(command, comments=True)
104     except ValueError as err:
105         print("Can't tokenize command string: " + str(err) + ".")
106         return
107     if len(tokens) > 0 and tokens[0] in commands_db \
108        and len(tokens) == commands_db[tokens[0]][0] + 1:
109         if commands_db[tokens[0]][1]:
110             commands_db[tokens[0]][2](*tokens[1:])
111         elif replay:
112             print("Due to replay mode, reading command as 'go on in record'.")
113             line = io_db["file_record"].readline()
114             if len(line) > 0:
115                 obey(line.rstrip(), io_db["file_record"].prefix
116                      + str(io_db["file_record"].line_n))
117                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
118             else:
119                 print("Reached end of record file.")
120         else:
121             commands_db[tokens[0]][2](*tokens[1:])
122             if do_record:
123                 record(command)
124                 save_world()
125             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
126     elif 0 != len(tokens):
127         print("Invalid command/argument, or bad number of tokens.")
128
129
130 def atomic_write(path, text, do_append=False):
131     """Atomic write of text to file at path, appended if do_append is set."""
132     path_tmp = path + io_db["tmp_suffix"]
133     mode = "w"
134     if do_append:
135         mode = "a"
136         if os.access(path, os.F_OK):
137             shutil.copyfile(path, path_tmp)
138     file = open(path_tmp, mode)
139     strong_write(file, text)
140     file.close()
141     if os.access(path, os.F_OK):
142         os.remove(path)
143     os.rename(path_tmp, path)
144
145
146 def record(command):
147     """Append command string plus newline to record file. (Atomic.)"""
148     # This misses some optimizations from the original record(), namely only
149     # finishing the atomic write with expensive flush() and fsync() every 15
150     # seconds unless explicitely forced. Implement as needed.
151     atomic_write(io_db["path_record"], command + "\n", do_append=True)
152
153
154 def save_world():
155     """Save all commands needed to reconstruct current world state."""
156     # TODO: Misses same optimizations as record() from the original record().
157
158     def quote(string):
159         string = string.replace("\u005C", '\u005C\u005C')
160         return '"' + string.replace('"', '\u005C"') + '"'
161
162     def mapsetter(key):
163         def helper(id):
164             string = ""
165             if world_db["Things"][id][key]:
166                 map = world_db["Things"][id][key]
167                 length = world_db["MAP_LENGTH"]
168                 for i in range(length):
169                     line = map[i * length:(i * length) + length].decode()
170                     string = string + key + " " + str(i) + " " + quote(line) \
171                              + "\n"
172             return string
173         return helper
174
175     def memthing(id):
176         string = ""
177         for memthing in world_db["Things"][id]["T_MEMTHING"]:
178             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
179                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
180         return string
181
182     def helper(category, id_string, special_keys={}):
183         string = ""
184         for id in world_db[category]:
185             string = string + id_string + " " + str(id) + "\n"
186             for key in world_db[category][id]:
187                 if not key in special_keys:
188                     x = world_db[category][id][key]
189                     argument = quote(x) if str == type(x) else str(x)
190                     string = string + key + " " + argument + "\n"
191                 elif special_keys[key]:
192                     string = string + special_keys[key](id)
193         return string
194
195     string = ""
196     for key in world_db:
197         if dict != type(world_db[key]) and key != "MAP":
198             string = string + key + " " + str(world_db[key]) + "\n"
199     string = string + helper("ThingActions", "TA_ID")
200     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
201     for id in world_db["ThingTypes"]:
202         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
203                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
204     string = string + helper("Things", "T_ID",
205                              {"T_CARRIES": False, "carried": False,
206                               "T_MEMMAP": mapsetter("T_MEMMAP"),
207                               "T_MEMTHING": memthing, "fovmap": False,
208                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
209     for id in world_db["Things"]:
210         if [] != world_db["Things"][id]["T_CARRIES"]:
211             string = string + "T_ID " + str(id) + "\n"
212             for carried_id in world_db["Things"][id]["T_CARRIES"]:
213                 string = string + "T_CARRIES " + str(carried_id) + "\n"
214     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
215              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
216     atomic_write(io_db["path_save"], string)
217
218
219 def obey_lines_in_file(path, name, do_record=False):
220     """Call obey() on each line of path's file, use name in input prefix."""
221     file = open(path, "r")
222     line_n = 1
223     for line in file.readlines():
224         obey(line.rstrip(), name + "file line " + str(line_n),
225              do_record=do_record)
226         line_n = line_n + 1
227     file.close()
228
229
230 def parse_command_line_arguments():
231     """Return settings values read from command line arguments."""
232     parser = argparse.ArgumentParser()
233     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
234                         action='store')
235     opts, unknown = parser.parse_known_args()
236     return opts
237
238
239 def server_test():
240     """Ensure valid server out file belonging to current process.
241
242     This is done by comparing io_db["teststring"] to what's found at the start
243     of the current file at io_db["path_out"]. On failure, set
244     io_db["kicked_by_rival"] and raise SystemExit.
245     """
246     if not os.access(io_db["path_out"], os.F_OK):
247         raise SystemExit("Server output file has disappeared.")
248     file = open(io_db["path_out"], "r")
249     test = file.readline().rstrip("\n")
250     file.close()
251     if test != io_db["teststring"]:
252         io_db["kicked_by_rival"] = True
253         msg = "Server test string in server output file does not match. This" \
254               " indicates that the current server process has been " \
255               "superseded by another one."
256         raise SystemExit(msg)
257
258
259 def read_command():
260     """Return next newline-delimited command from server in file.
261
262     Keep building return string until a newline is encountered. Pause between
263     unsuccessful reads, and after too much waiting, run server_test().
264     """
265     wait_on_fail = 1
266     max_wait = 5
267     now = time.time()
268     command = ""
269     while True:
270         add = io_db["file_in"].readline()
271         if len(add) > 0:
272             command = command + add
273             if len(command) > 0 and "\n" == command[-1]:
274                 command = command[:-1]
275                 break
276         else:
277             time.sleep(wait_on_fail)
278             if now + max_wait < time.time():
279                 server_test()
280                 now = time.time()
281     return command
282
283
284 def try_worldstate_update():
285     """Write worldstate file if io_db["worldstate_updateable"] is set."""
286     if io_db["worldstate_updateable"]:
287
288         def draw_visible_Things(map, run):
289             for id in world_db["Things"]:
290                 type = world_db["Things"][id]["T_TYPE"]
291                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
292                 alive = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]
293                 if (0 == run and not consumable and not alive) \
294                    or (1 == run and consumable and not alive) \
295                    or (2 == run and alive):
296                     y = world_db["Things"][id]["T_POSY"]
297                     x = world_db["Things"][id]["T_POSX"]
298                     fovflag = world_db["Things"][0]["fovmap"][(y * length) + x]
299                     if 'v' == chr(fovflag):
300                         c = world_db["ThingTypes"][type]["TT_SYMBOL"]
301                         map[(y * length) + x] = ord(c)
302
303         def write_map(string, map):
304             for i in range(length):
305                 line = map[i * length:(i * length) + length].decode()
306                 string = string + line + "\n"
307             return string
308
309         inventory = ""
310         if [] == world_db["Things"][0]["T_CARRIES"]:
311             inventory = "(none)\n"
312         else:
313             for id in world_db["Things"][0]["T_CARRIES"]:
314                 type_id = world_db["Things"][id]["T_TYPE"]
315                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
316                 inventory = inventory + name + "\n"
317         string = str(world_db["TURN"]) + "\n" + \
318                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
319                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
320                  inventory + "%\n" + \
321                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
322                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
323                  str(world_db["MAP_LENGTH"]) + "\n"
324         length = world_db["MAP_LENGTH"]
325         fov = bytearray(b' ' * (length ** 2))
326         for pos in range(length ** 2):
327             if 'v' == chr(world_db["Things"][0]["fovmap"][pos]):
328                 fov[pos] = world_db["MAP"][pos]
329         for i in range(3):
330             draw_visible_Things(fov, i)
331         string = write_map(string, fov)
332         mem = world_db["Things"][0]["T_MEMMAP"][:]
333         for i in range(2):
334             for memthing in world_db["Things"][0]["T_MEMTHING"]:
335                 type = world_db["Things"][memthing[0]]["T_TYPE"]
336                 consumable = world_db["ThingTypes"][type]["TT_CONSUMABLE"]
337                 if (i == 0 and not consumable) or (i == 1 and consumable):
338                     c = world_db["ThingTypes"][type]["TT_SYMBOL"]
339                     mem[(memthing[1] * length) + memthing[2]] = ord(c)
340         string = write_map(string, mem)
341         atomic_write(io_db["path_worldstate"], string)
342         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
343         io_db["worldstate_updateable"] = False
344
345
346 def replay_game():
347     """Replay game from record file.
348
349     Use opts.replay as breakpoint turn to which to replay automatically before
350     switching to manual input by non-meta commands in server input file
351     triggering further reads of record file. Ensure opts.replay is at least 1.
352     Run try_worldstate_update() before each interactive obey()/read_command().
353     """
354     if opts.replay < 1:
355         opts.replay = 1
356     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
357           " (if so late a turn is to be found).")
358     if not os.access(io_db["path_record"], os.F_OK):
359         raise SystemExit("No record file found to replay.")
360     io_db["file_record"] = open(io_db["path_record"], "r")
361     io_db["file_record"].prefix = "record file line "
362     io_db["file_record"].line_n = 1
363     while world_db["TURN"] < opts.replay:
364         line = io_db["file_record"].readline()
365         if "" == line:
366             break
367         obey(line.rstrip(), io_db["file_record"].prefix
368              + str(io_db["file_record"].line_n))
369         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
370     while True:
371         try_worldstate_update()
372         obey(read_command(), "in file", replay=True)
373
374
375 def play_game():
376     """Play game by server input file commands. Before, load save file found.
377
378     If no save file is found, a new world is generated from the commands in the
379     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
380     command and all that follow via the server input file. Run
381     try_worldstate_update() before each interactive obey()/read_command().
382     """
383     if os.access(io_db["path_save"], os.F_OK):
384         obey_lines_in_file(io_db["path_save"], "save")
385     else:
386         if not os.access(io_db["path_worldconf"], os.F_OK):
387             msg = "No world config file from which to start a new world."
388             raise SystemExit(msg)
389         obey_lines_in_file(io_db["path_worldconf"], "world config ",
390                            do_record=True)
391         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
392     while True:
393         try_worldstate_update()
394         obey(read_command(), "in file", do_record=True)
395
396
397 def remake_map():
398     """(Re-)make island map.
399
400     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
401     start with one land cell in the middle, then go into cycle of repeatedly
402     selecting a random sea cell and transforming it into land if it is neighbor
403     to land. The cycle ends when a land cell is due to be created at the map's
404     border. Then put some trees on the map (TODO: more precise algorithm desc).
405     """
406     def is_neighbor(coordinates, type):
407         y = coordinates[0]
408         x = coordinates[1]
409         length = world_db["MAP_LENGTH"]
410         ind = y % 2
411         diag_west = x + (ind > 0)
412         diag_east = x + (ind < (length - 1))
413         pos = (y * length) + x
414         if (y > 0 and diag_east
415             and type == chr(world_db["MAP"][pos - length + ind])) \
416            or (x < (length - 1)
417                and type == chr(world_db["MAP"][pos + 1])) \
418            or (y < (length - 1) and diag_east
419                and type == chr(world_db["MAP"][pos + length + ind])) \
420            or (y > 0 and diag_west
421                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
422            or (x > 0
423                and type == chr(world_db["MAP"][pos - 1])) \
424            or (y < (length - 1) and diag_west
425                and type == chr(world_db["MAP"][pos + length - (not ind)])):
426             return True
427         return False
428     store_seed = rand.seed
429     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
430     length = world_db["MAP_LENGTH"]
431     add_half_width = (not (length % 2)) * int(length / 2)
432     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
433     while (1):
434         y = rand.next() % length
435         x = rand.next() % length
436         pos = (y * length) + x
437         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
438             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
439                 break
440             world_db["MAP"][pos] = ord(".")
441     n_trees = int((length ** 2) / 16)
442     i_trees = 0
443     while (i_trees <= n_trees):
444         single_allowed = rand.next() % 32
445         y = rand.next() % length
446         x = rand.next() % length
447         pos = (y * length) + x
448         if "." == chr(world_db["MAP"][pos]) \
449           and ((not single_allowed) or is_neighbor((y, x), "X")):
450             world_db["MAP"][pos] = ord("X")
451             i_trees += 1
452     rand.seed = store_seed
453     # This all-too-precise replica of the original C code misses iter_limit().
454
455
456 def update_map_memory(t):
457     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
458     if not t["T_MEMMAP"]:
459         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
460     if not t["T_MEMDEPTHMAP"]:
461         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
462     for pos in range(world_db["MAP_LENGTH"] ** 2):
463         if "v" == chr(t["fovmap"][pos]):
464             t["T_MEMDEPTHMAP"][pos] = ord("0")
465             if " " == chr(t["T_MEMMAP"][pos]):
466                 t["T_MEMMAP"][pos] = world_db["MAP"][pos]
467             continue
468         # TODO: Aging of MEMDEPTHMAP.
469     for memthing in t["T_MEMTHING"]:
470         y = world_db["Things"][memthing[0]]["T_POSY"]
471         x = world_db["Things"][memthing[1]]["T_POSY"]
472         if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
473             t["T_MEMTHING"].remove(memthing)
474     for id in world_db["Things"]:
475         type = world_db["Things"][id]["T_TYPE"]
476         if not world_db["ThingTypes"][type]["TT_LIFEPOINTS"]:
477             y = world_db["Things"][id]["T_POSY"]
478             x = world_db["Things"][id]["T_POSY"]
479             if "v" == chr(t["fovmap"][(y * world_db["MAP_LENGTH"]) + x]):
480                 t["T_MEMTHING"].append((type, y, x))
481
482
483 def set_world_inactive():
484     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
485     server_test()
486     if os.access(io_db["path_worldstate"], os.F_OK):
487         os.remove(io_db["path_worldstate"])
488     world_db["WORLD_ACTIVE"] = 0
489
490
491 def integer_test(val_string, min, max):
492     """Return val_string if possible integer >= min and <= max, else None."""
493     try:
494         val = int(val_string)
495         if val < min or val > max:
496             raise ValueError
497         return val
498     except ValueError:
499         print("Ignoring: Please use integer >= " + str(min) + " and <= " +
500               str(max) + ".")
501         return None
502
503
504 def setter(category, key, min, max):
505     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
506     if category is None:
507         def f(val_string):
508             val = integer_test(val_string, min, max)
509             if None != val:
510                 world_db[key] = val
511     else:
512         if category == "Thing":
513             id_store = command_tid
514             decorator = test_Thing_id
515         elif category == "ThingType":
516             id_store = command_ttid
517             decorator = test_ThingType_id
518         elif category == "ThingAction":
519             id_store = command_taid
520             decorator = test_ThingAction_id
521
522         @decorator
523         def f(val_string):
524             val = integer_test(val_string, min, max)
525             if None != val:
526                 world_db[category + "s"][id_store.id][key] = val
527     return f
528
529
530 def build_fov_map(t):
531     """Build Thing's FOV map."""
532     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
533     # DUMMY so far. Just builds an all-visible map.
534
535
536 def actor_wait(t):
537     """Make t do nothing (but loudly, if player avatar)."""
538     if t == world_db["Things"][0]:
539         strong_write(io_db["file_out"], "LOG You wait.\n")
540
541
542 def actor_move(t):
543     pass
544
545
546 def actor_pick_up(t):
547     """Make t pick up (topmost?) Thing from ground into inventory."""
548     # Topmostness is actually not defined so far.
549     ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
550            if not world_db["Things"][id]["carried"]
551            if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
552            if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
553     if len(ids):
554         world_db["Things"][ids[0]]["carried"] = True
555         t["T_CARRIES"].append(ids[0])
556         if t == world_db["Things"][0]:
557             strong_write(io_db["file_out"], "LOG You pick up an object.\n")
558     elif t == world_db["Things"][0]:
559         err = "You try to pick up an object, but there is none."
560         strong_write(io_db["file_out"], "LOG " + err + "\n")
561
562
563 def actor_drop(t):
564     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
565     # TODO: Handle case where T_ARGUMENT matches nothing.
566     if len(t["T_CARRIES"]):
567         id = t["T_CARRIES"][t["T_ARGUMENT"]]
568         t["T_CARRIES"].remove(id)
569         world_db["Things"][id]["carried"] = False
570         if t == world_db["Things"][0]:
571             strong_write(io_db["file_out"], "LOG You drop an object.\n")
572     elif t == world_db["Things"][0]:
573         err = "You try to drop an object, but you own none."
574         strong_write(io_db["file_out"], "LOG " + err + "\n")
575
576
577 def actor_use(t):
578     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
579     # Original wrongly featured lifepoints increase through consumable!
580     # TODO: Handle case where T_ARGUMENT matches nothing.
581     if len(t["T_CARRIES"]):
582         id = t["T_CARRIES"][t["T_ARGUMENT"]]
583         type = world_db["Things"][id]["T_TYPE"]
584         if world_db["ThingTypes"][type]["TT_CONSUMABLE"]:
585             t["T_CARRIES"].remove(id)
586             del world_db["Things"][id]
587             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_CONSUMABLE"]
588             strong_write(io_db["file_out"], "LOG You consume this object.\n")
589         else:
590             strong_write(io_db["file_out"], "LOG You try to use this object," +
591                                             "but fail.\n")
592     else:
593         strong_write(io_db["file_out"], "LOG You try to use an object, but " +
594                                         "you own none.\n")
595
596
597 def turn_over():
598     """Run game world and its inhabitants until new player input expected."""
599     id = 0
600     whilebreaker = False
601     while world_db["Things"][0]["T_LIFEPOINTS"]:
602         for id in [id for id in world_db["Things"]
603                    if world_db["Things"][id]["T_LIFEPOINTS"]]:
604             Thing = world_db["Things"][id]
605             if Thing["T_LIFEPOINTS"]:
606                 if not Thing["T_COMMAND"]:
607                     update_map_memory(Thing)
608                     if 0 == id:
609                         whilebreaker = True
610                         break
611                     # DUMMY: ai(thing)
612                     Thing["T_COMMAND"] = 1
613                 # DUMMY: try_healing
614                 Thing["T_PROGRESS"] += 1
615                 taid = [a for a in world_db["ThingActions"]
616                           if a == Thing["T_COMMAND"]][0]
617                 ThingAction = world_db["ThingActions"][taid]
618                 if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
619                     eval("actor_" + ThingAction["TA_NAME"])(Thing)
620                     Thing["T_COMMAND"] = 0
621                     Thing["T_PROGRESS"] = 0
622                 # DUMMY: hunger
623             # DUMMY: thingproliferation
624         if whilebreaker:
625             break
626         world_db["TURN"] += 1
627
628
629 def new_Thing(type, pos=(0,0)):
630     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
631     thing = {
632         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
633         "T_ARGUMENT": 0,
634         "T_PROGRESS": 0,
635         "T_SATIATION": 0,
636         "T_COMMAND": 0,
637         "T_TYPE": type,
638         "T_POSY": pos[0],
639         "T_POSX": pos[1],
640         "T_CARRIES": [],
641         "carried": False,
642         "T_MEMTHING": [],
643         "T_MEMMAP": False,
644         "T_MEMDEPTHMAP": False,
645         "fovmap": False
646     }
647     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
648         build_fov_map(thing)
649     return thing
650
651
652 def id_setter(id, category, id_store=False, start_at_1=False):
653     """Set ID of object of category to manipulate ID unused? Create new one.
654
655     The ID is stored as id_store.id (if id_store is set). If the integer of the
656     input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <=
657     32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID
658     >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no
659     new object is created, otherwise the new object's ID.
660     """
661     min = 0 if start_at_1 else -32768
662     max = 255 if start_at_1 else 32767
663     if str == type(id):
664         id = integer_test(id, min, max)
665     if None != id:
666         if id in world_db[category]:
667             if id_store:
668                 id_store.id = id
669             return None
670         else:
671             if (start_at_1 and 0 == id) \
672                or ((not start_at_1) and (id < 0 or id > 255)):
673                 id = -1
674                 while 1:
675                     id = id + 1
676                     if id not in world_db[category]:
677                         break
678                 if id > 255:
679                     print("Ignoring: "
680                           "No unused ID available to add to ID list.")
681                     return None
682             if id_store:
683                 id_store.id = id
684     return id
685
686
687 def command_ping():
688     """Send PONG line to server output file."""
689     strong_write(io_db["file_out"], "PONG\n")
690
691
692 def command_quit():
693     """Abort server process."""
694     raise SystemExit("received QUIT command")
695
696
697 def command_thingshere(str_y, str_x):
698     """Write to out file list of Things known to player at coordinate y, x."""
699     def write_thing_if_here():
700         if y == world_db["Things"][id]["T_POSY"] \
701            and x == world_db["Things"][id]["T_POSX"] \
702            and not world_db["Things"][id]["carried"]:
703             type = world_db["Things"][id]["T_TYPE"]
704             name = world_db["ThingTypes"][type]["TT_NAME"]
705             strong_write(io_db["file_out"], name + "\n")
706     if world_db["WORLD_ACTIVE"]:
707         y = integer_test(str_y, 0, 255)
708         x = integer_test(str_x, 0, 255)
709         length = world_db["MAP_LENGTH"]
710         if None != y and None != x and y < length and x < length:
711             pos = (y * world_db["MAP_LENGTH"]) + x
712             strong_write(io_db["file_out"], "THINGS_HERE START\n")
713             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
714                 for id in world_db["Things"]:
715                     write_thing_if_here()
716             else:
717                 for id in world_db["Things"]["T_MEMTHING"]:
718                     write_thing_if_here()
719             strong_write(io_db["file_out"], "THINGS_HERE END\n")
720         else:
721             print("Ignoring: Invalid map coordinates.")
722     else:
723         print("Ignoring: Command only works on existing worlds.")
724
725
726 def play_commander(action, args=False):
727     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
728
729     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
730     """
731
732     def set_command():
733         id = [x for x in world_db["ThingActions"]
734                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
735         world_db["Things"][0]["T_COMMAND"] = id
736         turn_over()
737
738     def set_command_and_argument_int(str_arg):
739         val = integer_test(str_arg, 0, 255)
740         if None != val:
741             world_db["Things"][0]["T_ARGUMENT"] = val
742             set_command()
743         else:
744             print("Ignoring: Argument must be integer >= 0 <=255.")
745
746     def set_command_and_argument_movestring(str_arg):
747         dirs = {"east": "d", "south-east": "c", "south-west": "x",
748                 "west": "s", "north-west": "w", "north-east": "e"}
749         if str_arg in dirs:
750             world_db["Things"][0]["T_ARGUMENT"] = dirs[str_arg]
751             set_command()
752         else:
753             print("Ignoring: Argument must be valid direction string.")
754
755     if action == "move":
756         return set_command_and_argument_movestring
757     elif args:
758         return set_command_and_argument_int
759     else:
760         return set_command
761
762
763 def command_seedrandomness(seed_string):
764     """Set rand seed to int(seed_string)."""
765     val = integer_test(seed_string, 0, 4294967295)
766     if None != val:
767         rand.seed = val
768     else:
769         print("Ignoring: Value must be integer >= 0, <= 4294967295.")
770
771
772 def command_seedmap(seed_string):
773     """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
774     setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
775     remake_map()
776
777
778 def command_makeworld(seed_string):
779     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
780
781     Seed rand with seed, fill it into world_db["SEED_MAP"]. Do more only with a
782     "wait" ThingAction and world["PLAYER_TYPE"] matching ThingType of
783     TT_START_NUMBER > 0. Then, world_db["Things"] emptied, call remake_map()
784     and set world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
785     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
786     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
787     other. Init player's memory map. Write "NEW_WORLD" line to out file.
788     """
789
790     def free_pos():
791         i = 0
792         while 1:
793             err = "Space to put thing on too hard to find. Map too small?"
794             while 1:
795                 y = rand.next() % world_db["MAP_LENGTH"]
796                 x = rand.next() % world_db["MAP_LENGTH"]
797                 if "." == chr(world_db["MAP"][y * world_db["MAP_LENGTH"] + x]):
798                     break
799                 i += 1
800                 if i == 65535:
801                     raise SystemExit(err)
802             # Replica of C code, wrongly ignores animatedness of new Thing.
803             pos_clear = (0 == len([id for id in world_db["Things"]
804                                    if world_db["Things"][id]["T_LIFEPOINTS"]
805                                    if world_db["Things"][id]["T_POSY"] == y
806                                    if world_db["Things"][id]["T_POSX"] == x]))
807             if pos_clear:
808                 break
809         return (y, x)
810
811     val = integer_test(seed_string, 0, 4294967295)
812     if None == val:
813         print("Ignoring: Value must be integer >= 0, <= 4294967295.")
814         return
815     rand.seed = val
816     world_db["SEED_MAP"] = val
817     player_will_be_generated = False
818     playertype = world_db["PLAYER_TYPE"]
819     for ThingType in world_db["ThingTypes"]:
820         if playertype == ThingType:
821             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
822                 player_will_be_generated = True
823             break
824     if not player_will_be_generated:
825         print("Ignoring beyond SEED_MAP: " +
826               "No player type with start number >0 defined.")
827         return
828     wait_action = False
829     for ThingAction in world_db["ThingActions"]:
830         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
831             wait_action = True
832     if not wait_action:
833         print("Ignoring beyond SEED_MAP: " +
834               "No thing action with name 'wait' defined.")
835         return
836     world_db["Things"] = {}
837     remake_map()
838     world_db["WORLD_ACTIVE"] = 1
839     world_db["TURN"] = 1
840     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
841         id = id_setter(-1, "Things")
842         world_db["Things"][id] = new_Thing(playertype, free_pos())
843     update_map_memory(world_db["Things"][0])
844     for type in world_db["ThingTypes"]:
845         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
846             if type != playertype:
847                 id = id_setter(-1, "Things")
848                 world_db["Things"][id] = new_Thing(type, free_pos())
849     strong_write(io_db["file_out"], "NEW_WORLD\n")
850
851
852 def command_maplength(maplength_string):
853     """Redefine map length. Invalidate map, therefore lose all things on it."""
854     set_world_inactive()
855     world_db["Things"] = {}
856     setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
857
858
859 def command_worldactive(worldactive_string):
860     """Toggle world_db["WORLD_ACTIVE"] if possible.
861
862     An active world can always be set inactive. An inactive world can only be
863     set active with a "wait" ThingAction, and a player Thing (of ID 0). On
864     activation, rebuild all Things' FOVs, and the player's map memory.
865     """
866     # In original version, map existence was also tested (unnecessarily?).
867     val = integer_test(worldactive_string, 0, 1)
868     if val:
869         if 0 != world_db["WORLD_ACTIVE"]:
870             if 0 == val:
871                 set_world_inactive()
872             else:
873                 print("World already active.")
874         elif 0 == world_db["WORLD_ACTIVE"]:
875             wait_exists = False
876             for ThingAction in world_db["ThingActions"]:
877                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
878                     wait_exists = True
879                     break
880             player_exists = False
881             for Thing in world_db["Things"]:
882                 if 0 == Thing:
883                     player_exists = True
884                     break
885             if wait_exists and player_exists:
886                 for id in world_db["Things"]:
887                     if world_db["Things"][id]["T_LIFEPOINTS"]:
888                         build_fov_map(world_db["Things"][id])
889                         if 0 == id:
890                             update_map_memory(world_db["Things"][id])
891                 world_db["WORLD_ACTIVE"] = 1
892
893
894 def test_for_id_maker(object, category):
895     """Return decorator testing for object having "id" attribute."""
896     def decorator(f):
897         def helper(*args):
898             if hasattr(object, "id"):
899                 f(*args)
900             else:
901                 print("Ignoring: No " + category +
902                       " defined to manipulate yet.")
903         return helper
904     return decorator
905
906
907 def command_tid(id_string):
908     """Set ID of Thing to manipulate. ID unused? Create new one.
909
910     Default new Thing's type to the first available ThingType, others: zero.
911     """
912     id = id_setter(id_string, "Things", command_tid)
913     if None != id:
914         if world_db["ThingTypes"] == {}:
915             print("Ignoring: No ThingType to settle new Thing in.")
916             return
917         type = list(world_db["ThingTypes"].keys())[0]
918         world_db["Things"][id] = new_Thing(type)
919
920
921 test_Thing_id = test_for_id_maker(command_tid, "Thing")
922
923
924 @test_Thing_id
925 def command_tcommand(str_int):
926     """Set T_COMMAND of selected Thing."""
927     val = integer_test(str_int, 0, 255)
928     if None != val:
929         if 0 == val or val in world_db["ThingActions"]:
930             world_db["Things"][command_tid.id]["T_COMMAND"] = val
931         else:
932             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
933
934
935 @test_Thing_id
936 def command_ttype(str_int):
937     """Set T_TYPE of selected Thing."""
938     val = integer_test(str_int, 0, 255)
939     if None != val:
940         if val in world_db["ThingTypes"]:
941             world_db["Things"][command_tid.id]["T_TYPE"] = val
942         else:
943             print("Ignoring: ThingType ID belongs to no known ThingType.")
944
945
946 @test_Thing_id
947 def command_tcarries(str_int):
948     """Append int(str_int) to T_CARRIES of selected Thing.
949
950     The ID int(str_int) must not be of the selected Thing, and must belong to a
951     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
952     """
953     val = integer_test(str_int, 0, 255)
954     if None != val:
955         if val == command_tid.id:
956             print("Ignoring: Thing cannot carry itself.")
957         elif val in world_db["Things"] \
958              and not world_db["Things"][val]["carried"]:
959             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
960             world_db["Things"][val]["carried"] = True
961         else:
962             print("Ignoring: Thing not available for carrying.")
963     # Note that the whole carrying structure is different from the C version:
964     # Carried-ness is marked by a "carried" flag, not by Things containing
965     # Things internally.
966
967
968 @test_Thing_id
969 def command_tmemthing(str_t, str_y, str_x):
970     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
971
972     The type must fit to an existing ThingType, and the position into the map.
973     """
974     type = integer_test(str_t, 0, 255)
975     posy = integer_test(str_y, 0, 255)
976     posx = integer_test(str_x, 0, 255)
977     if None != type and None != posy and None != posx:
978         if type not in world_db["ThingTypes"] \
979            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
980             print("Ignoring: Illegal value for thing type or position.")
981         else:
982             memthing = (type, posy, posx)
983             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
984
985
986 def setter_map(maptype):
987     """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
988
989     If Thing has no map of maptype yet, initialize it with ' ' bytes first.
990     """
991     @test_Thing_id
992     def helper(str_int, mapline):
993         val = integer_test(str_int, 0, 255)
994         if None != val:
995             if val >= world_db["MAP_LENGTH"]:
996                 print("Illegal value for map line number.")
997             elif len(mapline) != world_db["MAP_LENGTH"]:
998                 print("Map line length is unequal map width.")
999             else:
1000                 length = world_db["MAP_LENGTH"]
1001                 map = None
1002                 if not world_db["Things"][command_tid.id][maptype]:
1003                     map = bytearray(b' ' * (length ** 2))
1004                 else:
1005                     map = world_db["Things"][command_tid.id][maptype]
1006                 map[val * length:(val * length) + length] = mapline.encode()
1007                 world_db["Things"][command_tid.id][maptype] = map
1008     return helper
1009
1010
1011 def setter_tpos(axis):
1012     """Generate setter for T_POSX or  T_POSY of selected Thing.
1013
1014     If world is active, rebuilds animate things' fovmap, player's memory map.
1015     """
1016     @test_Thing_id
1017     def helper(str_int):
1018         val = integer_test(str_int, 0, 255)
1019         if None != val:
1020             if val < world_db["MAP_LENGTH"]:
1021                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1022                 if world_db["WORLD_ACTIVE"] \
1023                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1024                     build_fov_map(world_db["Things"][command_tid.id])
1025                     if 0 == command_tid.id:
1026                         update_map_memory(world_db["Things"][command_tid.id])
1027             else:
1028                 print("Ignoring: Position is outside of map.")
1029     return helper
1030
1031
1032 def command_ttid(id_string):
1033     """Set ID of ThingType to manipulate. ID unused? Create new one.
1034
1035     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
1036     """
1037     id = id_setter(id_string, "ThingTypes", command_ttid)
1038     if None != id:
1039         world_db["ThingTypes"][id] = {
1040             "TT_NAME": "(none)",
1041             "TT_CONSUMABLE": 0,
1042             "TT_LIFEPOINTS": 0,
1043             "TT_PROLIFERATE": 0,
1044             "TT_START_NUMBER": 0,
1045             "TT_SYMBOL": "?",
1046             "TT_CORPSE_ID": id
1047         }
1048
1049
1050 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1051
1052
1053 @test_ThingType_id
1054 def command_ttname(name):
1055     """Set TT_NAME of selected ThingType."""
1056     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1057
1058
1059 @test_ThingType_id
1060 def command_ttsymbol(char):
1061     """Set TT_SYMBOL of selected ThingType. """
1062     if 1 == len(char):
1063         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1064     else:
1065         print("Ignoring: Argument must be single character.")
1066
1067
1068 @test_ThingType_id
1069 def command_ttcorpseid(str_int):
1070     """Set TT_CORPSE_ID of selected ThingType."""
1071     val = integer_test(str_int, 0, 255)
1072     if None != val:
1073         if val in world_db["ThingTypes"]:
1074             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1075         else:
1076             print("Ignoring: Corpse ID belongs to no known ThignType.")
1077
1078
1079 def command_taid(id_string):
1080     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1081
1082     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1083     """
1084     id = id_setter(id_string, "ThingActions", command_taid, True)
1085     if None != id:
1086         world_db["ThingActions"][id] = {
1087             "TA_EFFORT": 1,
1088             "TA_NAME": "wait"
1089         }
1090
1091
1092 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1093
1094
1095 @test_ThingAction_id
1096 def command_taname(name):
1097     """Set TA_NAME of selected ThingAction.
1098
1099     The name must match a valid thing action function. If after the name
1100     setting no ThingAction with name "wait" remains, call set_world_inactive().
1101     """
1102     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1103        or name == "pick_up":
1104         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1105         if 1 == world_db["WORLD_ACTIVE"]:
1106             wait_defined = False
1107             for id in world_db["ThingActions"]:
1108                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1109                     wait_defined = True
1110                     break
1111             if not wait_defined:
1112                 set_world_inactive()
1113     else:
1114         print("Ignoring: Invalid action name.")
1115     # In contrast to the original,naming won't map a function to a ThingAction.
1116
1117
1118 """Commands database.
1119
1120 Map command start tokens to ([0]) number of expected command arguments, ([1])
1121 the command's meta-ness (i.e. is it to be written to the record file, is it to
1122 be ignored in replay mode if read from server input file), and ([2]) a function
1123 to be called on it.
1124 """
1125 commands_db = {
1126     "QUIT": (0, True, command_quit),
1127     "PING": (0, True, command_ping),
1128     "THINGS_HERE": (2, True, command_thingshere),
1129     "MAKE_WORLD": (1, False, command_makeworld),
1130     "SEED_MAP": (1, False, command_seedmap),
1131     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
1132     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
1133     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
1134     "MAP_LENGTH": (1, False, command_maplength),
1135     "WORLD_ACTIVE": (1, False, command_worldactive),
1136     "TA_ID": (1, False, command_taid),
1137     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
1138     "TA_NAME": (1, False, command_taname),
1139     "TT_ID": (1, False, command_ttid),
1140     "TT_NAME": (1, False, command_ttname),
1141     "TT_SYMBOL": (1, False, command_ttsymbol),
1142     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
1143     "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
1144                                        0, 65535)),
1145     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
1146                                          0, 255)),
1147     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
1148                                         0, 255)),
1149     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
1150     "T_ID": (1, False, command_tid),
1151     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
1152     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
1153     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
1154     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
1155     "T_COMMAND": (1, False, command_tcommand),
1156     "T_TYPE": (1, False, command_ttype),
1157     "T_CARRIES": (1, False, command_tcarries),
1158     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
1159     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
1160     "T_MEMTHING": (3, False, command_tmemthing),
1161     "T_POSY": (1, False, setter_tpos("Y")),
1162     "T_POSX": (1, False, setter_tpos("X")),
1163     "wait": (0, False, play_commander("wait")),
1164     "move": (1, False, play_commander("move")),
1165     "pick_up": (0, False, play_commander("pick_up")),
1166     "drop": (1, False, play_commander("drop", True)),
1167     "use": (1, False, play_commander("use", True)),
1168 }
1169
1170
1171 """World state database. With sane default values. (Randomness is in rand.)"""
1172 world_db = {
1173     "TURN": 0,
1174     "SEED_MAP": 0,
1175     "PLAYER_TYPE": 0,
1176     "MAP_LENGTH": 64,
1177     "WORLD_ACTIVE": 0,
1178     "ThingActions": {},
1179     "ThingTypes": {},
1180     "Things": {}
1181 }
1182
1183
1184 """File IO database."""
1185 io_db = {
1186     "path_save": "save",
1187     "path_record": "record",
1188     "path_worldconf": "confserver/world",
1189     "path_server": "server/",
1190     "path_in": "server/in",
1191     "path_out": "server/out",
1192     "path_worldstate": "server/worldstate",
1193     "tmp_suffix": "_tmp",
1194     "kicked_by_rival": False,
1195     "worldstate_updateable": False
1196 }
1197
1198
1199 try:
1200     libpr = prep_library()
1201     rand = RandomnessIO()
1202     opts = parse_command_line_arguments()
1203     setup_server_io()
1204     if None != opts.replay:
1205         replay_game()
1206     else:
1207         play_game()
1208 except SystemExit as exit:
1209     print("ABORTING: " + exit.args[0])
1210 except:
1211     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
1212     raise
1213 finally:
1214     cleanup_server_io()