home · contact · privacy
Fix some breaking leftovers from last commits, remove GOD_MOOD.
[plomrogue] / roguelike-server
1 #!/usr/bin/python3
2
3 # This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
4 # or any later version. For details on its copyright, license, and warranties,
5 # see the file NOTICE in the root directory of the PlomRogue source package.
6
7
8 import argparse
9 import errno
10 import os
11 import shlex
12 import shutil
13 import time
14 import ctypes
15 import math  # #
16
17
18 class RandomnessIO:
19     """"Interface to libplomrogue's pseudo-randomness generator."""
20
21     def set_seed(self, seed):
22         libpr.seed_rrand(1, seed)
23
24     def get_seed(self):
25         return libpr.seed_rrand(0, 0)
26
27     def next(self):
28         return libpr.rrand()
29
30     seed = property(get_seed, set_seed)
31
32
33 def prep_library():
34     """Prepare ctypes library at ./libplomrogue.so"""
35     libpath = ("./libplomrogue.so")
36     if not os.access(libpath, os.F_OK):
37         raise SystemExit("No library " + libpath + ", run ./redo first?")
38     libpr = ctypes.cdll.LoadLibrary(libpath)
39     libpr.seed_rrand.restype = ctypes.c_uint32
40     return libpr
41
42
43 def c_pointer_to_bytearray(ba):
44     """Return C char * pointer to ba."""
45     type = ctypes.c_char * len(ba)
46     return type.from_buffer(ba)
47
48
49 def strong_write(file, string):
50     """Apply write(string), then flush()."""
51     file.write(string)
52     file.flush()
53
54
55 def setup_server_io():
56     """Fill IO files DB with proper file( path)s. Write process IO test string.
57
58     Ensure IO files directory at server/. Remove any old input file if found.
59     Set up new input file for reading, and new output file for writing. Start
60     output file with process hash line of format PID + " " + floated UNIX time
61     (io_db["teststring"]). Raise SystemExit if file is found at path of either
62     record or save file plus io_db["tmp_suffix"].
63     """
64     def detect_atomic_leftover(path, tmp_suffix):
65         path_tmp = path + tmp_suffix
66         msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
67               "aborted previous attempt to write '" + path + "'. Aborting " \
68              "until matter is resolved by removing it from its current path."
69         if os.access(path_tmp, os.F_OK):
70             raise SystemExit(msg)
71     io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
72     io_db["save_wait"] = 0
73     io_db["verbose"] = False
74     io_db["record_chunk"] = ""
75     os.makedirs(io_db["path_server"], exist_ok=True)
76     io_db["file_out"] = open(io_db["path_out"], "w")
77     strong_write(io_db["file_out"], io_db["teststring"] + "\n")
78     if os.access(io_db["path_in"], os.F_OK):
79         os.remove(io_db["path_in"])
80     io_db["file_in"] = open(io_db["path_in"], "w")
81     io_db["file_in"].close()
82     io_db["file_in"] = open(io_db["path_in"], "r")
83     detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
84     detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
85
86
87 def cleanup_server_io():
88     """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
89     def helper(file_key, path_key):
90         if file_key in io_db:
91             io_db[file_key].close()
92         if not io_db["kicked_by_rival"] \
93            and os.access(io_db[path_key], os.F_OK):
94             os.remove(io_db[path_key])
95     helper("file_in", "path_in")
96     helper("file_out", "path_out")
97     helper("file_worldstate", "path_worldstate")
98     if "file_record" in io_db:
99         io_db["file_record"].close()
100
101
102 def log(msg):
103     """Send "msg" to log."""
104     strong_write(io_db["file_out"], "LOG " + msg + "\n")
105
106
107 def obey(command, prefix, replay=False, do_record=False):
108     """Call function from commands_db mapped to command's first token.
109
110     Tokenize command string with shlex.split(comments=True). If replay is set,
111     a non-meta command from the commands_db merely triggers obey() on the next
112     command from the records file. If not, non-meta commands set
113     io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if
114     do_record is set, are recorded to io_db["record_chunk"], and save_world()
115     is called (and io_db["record_chunk"] written) if 15 seconds have passed
116     since the last time it was called. The prefix string is inserted into the
117     server's input message between its beginning 'input ' and ':'. All activity
118     is preceded by a server_test() call. Commands that start with a lowercase
119     letter are ignored when world_db["WORLD_ACTIVE"] is False/0.
120     """
121     server_test()
122     if io_db["verbose"]:
123         print("input " + prefix + ": " + command)
124     try:
125         tokens = shlex.split(command, comments=True)
126     except ValueError as err:
127         print("Can't tokenize command string: " + str(err) + ".")
128         return
129     if len(tokens) > 0 and tokens[0] in commands_db \
130        and len(tokens) == commands_db[tokens[0]][0] + 1:
131         if commands_db[tokens[0]][1]:
132             commands_db[tokens[0]][2](*tokens[1:])
133         elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
134             print("Ignoring lowercase-starting commands when world inactive.")
135         elif replay:
136             print("Due to replay mode, reading command as 'go on in record'.")
137             line = io_db["file_record"].readline()
138             if len(line) > 0:
139                 obey(line.rstrip(), io_db["file_record"].prefix
140                      + str(io_db["file_record"].line_n))
141                 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
142             else:
143                 print("Reached end of record file.")
144         else:
145             commands_db[tokens[0]][2](*tokens[1:])
146             if do_record:
147                 io_db["record_chunk"] += command + "\n"
148                 if time.time() > io_db["save_wait"] + 300:
149                     atomic_write(io_db["path_record"], io_db["record_chunk"],
150                                  do_append=True)
151                     if world_db["WORLD_ACTIVE"]:
152                         save_world()
153                     io_db["record_chunk"] = ""
154                     io_db["save_wait"] = time.time()
155             io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
156     elif 0 != len(tokens):
157         print("Invalid command/argument, or bad number of tokens.")
158
159
160 def atomic_write(path, text, do_append=False, delete=True):
161     """Atomic write of text to file at path, appended if do_append is set."""
162     path_tmp = path + io_db["tmp_suffix"]
163     mode = "w"
164     if do_append:
165         mode = "a"
166         if os.access(path, os.F_OK):
167             shutil.copyfile(path, path_tmp)
168     file = open(path_tmp, mode)
169     strong_write(file, text)
170     file.close()
171     if delete and os.access(path, os.F_OK):
172         os.remove(path)
173     os.rename(path_tmp, path)
174
175
176 def save_world():
177     """Save all commands needed to reconstruct current world state."""
178
179     def quote(string):
180         string = string.replace("\u005C", '\u005C\u005C')
181         return '"' + string.replace('"', '\u005C"') + '"'
182
183     def mapsetter(key):
184         def helper(id=None):
185             string = ""
186             if key == "MAP" or world_db["Things"][id][key]:
187                 map = world_db["MAP"] if key == "MAP" \
188                                       else world_db["Things"][id][key]
189                 length = world_db["MAP_LENGTH"]
190                 for i in range(length):
191                     line = map[i * length:(i * length) + length].decode()
192                     string = string + key + " " + str(i) + " " + quote(line) \
193                              + "\n"
194             return string
195         return helper
196
197     def memthing(id):
198         string = ""
199         for memthing in world_db["Things"][id]["T_MEMTHING"]:
200             string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
201                      str(memthing[1]) + " " + str(memthing[2]) + "\n"
202         return string
203
204     def helper(category, id_string, special_keys={}):
205         string = ""
206         for id in world_db[category]:
207             string = string + id_string + " " + str(id) + "\n"
208             for key in world_db[category][id]:
209                 if not key in special_keys:
210                     x = world_db[category][id][key]
211                     argument = quote(x) if str == type(x) else str(x)
212                     string = string + key + " " + argument + "\n"
213                 elif special_keys[key]:
214                     string = string + special_keys[key](id)
215         return string
216
217     string = ""
218     for key in world_db:
219         if (dict != type(world_db[key])
220             and key != "altar"  # #
221             and key != "MAP" and key != "WORLD_ACTIVE"):
222             string = string + key + " " + str(world_db[key]) + "\n"
223     string = string + mapsetter("MAP")()
224     string = string + helper("ThingActions", "TA_ID")
225     string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
226     for id in world_db["ThingTypes"]:
227         string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
228                  str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
229     string = string + helper("Things", "T_ID",
230                              {"T_CARRIES": False, "carried": False,
231                               "T_MEMMAP": mapsetter("T_MEMMAP"),
232                               "T_MEMTHING": memthing, "fovmap": False,
233                               "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
234     for id in world_db["Things"]:
235         if [] != world_db["Things"][id]["T_CARRIES"]:
236             string = string + "T_ID " + str(id) + "\n"
237             for carried_id in world_db["Things"][id]["T_CARRIES"]:
238                 string = string + "T_CARRIES " + str(carried_id) + "\n"
239     string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
240              "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
241     atomic_write(io_db["path_save"], string)
242
243
244 def obey_lines_in_file(path, name, do_record=False):
245     """Call obey() on each line of path's file, use name in input prefix."""
246     file = open(path, "r")
247     line_n = 1
248     for line in file.readlines():
249         obey(line.rstrip(), name + "file line " + str(line_n),
250              do_record=do_record)
251         line_n = line_n + 1
252     file.close()
253
254
255 def parse_command_line_arguments():
256     """Return settings values read from command line arguments."""
257     parser = argparse.ArgumentParser()
258     parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
259                         action='store')
260     parser.add_argument('-l', nargs="?", const="save", dest='savefile',
261                         action="store")
262     parser.add_argument('-v', dest='verbose', action='store_true')
263     opts, unknown = parser.parse_known_args()
264     return opts
265
266
267 def server_test():
268     """Ensure valid server out file belonging to current process.
269
270     This is done by comparing io_db["teststring"] to what's found at the start
271     of the current file at io_db["path_out"]. On failure, set
272     io_db["kicked_by_rival"] and raise SystemExit.
273     """
274     if not os.access(io_db["path_out"], os.F_OK):
275         raise SystemExit("Server output file has disappeared.")
276     file = open(io_db["path_out"], "r")
277     test = file.readline().rstrip("\n")
278     file.close()
279     if test != io_db["teststring"]:
280         io_db["kicked_by_rival"] = True
281         msg = "Server test string in server output file does not match. This" \
282               " indicates that the current server process has been " \
283               "superseded by another one."
284         raise SystemExit(msg)
285
286
287 def read_command():
288     """Return next newline-delimited command from server in file.
289
290     Keep building return string until a newline is encountered. Pause between
291     unsuccessful reads, and after too much waiting, run server_test().
292     """
293     wait_on_fail = 0.03333
294     max_wait = 5
295     now = time.time()
296     command = ""
297     while True:
298         add = io_db["file_in"].readline()
299         if len(add) > 0:
300             command = command + add
301             if len(command) > 0 and "\n" == command[-1]:
302                 command = command[:-1]
303                 break
304         else:
305             time.sleep(wait_on_fail)
306             if now + max_wait < time.time():
307                 server_test()
308                 now = time.time()
309     return command
310
311
312 def try_worldstate_update():
313     """Write worldstate file if io_db["worldstate_updateable"] is set."""
314     if io_db["worldstate_updateable"]:
315
316         def write_map(string, map):
317             for i in range(length):
318                 line = map[i * length:(i * length) + length].decode()
319                 string = string + line + "\n"
320             return string
321
322         inventory = ""
323         if [] == world_db["Things"][0]["T_CARRIES"]:
324             inventory = "(none)\n"
325         else:
326             for id in world_db["Things"][0]["T_CARRIES"]:
327                 type_id = world_db["Things"][id]["T_TYPE"]
328                 name = world_db["ThingTypes"][type_id]["TT_NAME"]
329                 inventory = inventory + name + "\n"
330         # 7DRL additions: GOD_FAVOR
331         string = str(world_db["TURN"]) + "\n" + \
332                  str(world_db["GOD_FAVOR"]) + "\n" + \
333                  str(world_db["Things"][0]["T_LIFEPOINTS"]) + "\n" + \
334                  str(world_db["Things"][0]["T_SATIATION"]) + "\n" + \
335                  inventory + "%\n" + \
336                  str(world_db["Things"][0]["T_POSY"]) + "\n" + \
337                  str(world_db["Things"][0]["T_POSX"]) + "\n" + \
338                  str(world_db["MAP_LENGTH"]) + "\n"
339         length = world_db["MAP_LENGTH"]
340
341         fov = bytearray(b' ' * (length ** 2))
342         ord_v = ord("v")
343         for pos in [pos for pos in range(length ** 2)
344                         if ord_v == world_db["Things"][0]["fovmap"][pos]]:
345             fov[pos] = world_db["MAP"][pos]
346         for id in [id for tid in reversed(sorted(list(world_db["ThingTypes"])))
347                       for id in world_db["Things"]
348                       if not world_db["Things"][id]["carried"]
349                       if world_db["Things"][id]["T_TYPE"] == tid
350                       if world_db["Things"][0]["fovmap"][
351                            world_db["Things"][id]["T_POSY"] * length
352                            + world_db["Things"][id]["T_POSX"]] == ord_v]:
353             type = world_db["Things"][id]["T_TYPE"]
354             c = ord(world_db["ThingTypes"][type]["TT_SYMBOL"])
355             fov[world_db["Things"][id]["T_POSY"] * length
356                 + world_db["Things"][id]["T_POSX"]] = c
357         string = write_map(string, fov)
358
359         mem = world_db["Things"][0]["T_MEMMAP"][:]
360         for mt in [mt for tid in reversed(sorted(list(world_db["ThingTypes"])))
361                       for mt in world_db["Things"][0]["T_MEMTHING"]
362                       if mt[0] == tid]:
363              c = world_db["ThingTypes"][mt[0]]["TT_SYMBOL"]
364              mem[(mt[1] * length) + mt[2]] = ord(c)
365         string = write_map(string, mem)
366
367         metamapA = bytearray(b'0' * (length ** 2))  # #
368         for id in [id for id in world_db["Things"]  # #
369                       if not world_db["Things"][id]["carried"]  # #
370                       if world_db["Things"][id]["T_LIFEPOINTS"]  # #
371                       if world_db["Things"][0]["fovmap"][  # #
372                            world_db["Things"][id]["T_POSY"] * length  # #
373                            + world_db["Things"][id]["T_POSX"]] == ord_v]:  # #
374             pos = (world_db["Things"][id]["T_POSY"] * length  # #
375                   + world_db["Things"][id]["T_POSX"])  # #
376             if id == 0 or world_db["EMPATHY"]:  # #
377                 type = world_db["Things"][id]["T_TYPE"]  # #
378                 max_hp = world_db["ThingTypes"][type]["TT_LIFEPOINTS"]  # #
379                 third_of_hp = max_hp / 3  # #
380                 hp = world_db["Things"][id]["T_LIFEPOINTS"]  # #
381                 add = 0  # #
382                 if hp > 2 * third_of_hp:  # #
383                      add = 2  # #
384                 elif hp > third_of_hp:  # #
385                     add = 1  # #
386                 metamapA[pos] = ord('a') + add # #
387             else:  # #
388                 metamapA[pos] = ord('X')  # #
389         for mt in world_db["Things"][0]["T_MEMTHING"]:  # #
390             pos = mt[1] * length + mt[2]  # #
391             if metamapA[pos] < ord('2'):  # #
392                 metamapA[pos] += 1  # #
393         string = write_map(string, metamapA)  # #
394         
395         metamapB = bytearray(b' ' * (length ** 2))  # #
396         for id in [id for id in world_db["Things"]  # #
397                       if not world_db["Things"][id]["carried"]  # #
398                       if world_db["Things"][id]["T_LIFEPOINTS"]  # #
399                       if world_db["Things"][0]["fovmap"][  # #
400                            world_db["Things"][id]["T_POSY"] * length  # #
401                            + world_db["Things"][id]["T_POSX"]] == ord_v]:  # #
402             pos = (world_db["Things"][id]["T_POSY"] * length  # #
403                   + world_db["Things"][id]["T_POSX"])  # #
404             if id == 0 or world_db["EMPATHY"]:  # #
405                 action = world_db["Things"][id]["T_COMMAND"]  # #
406                 if 0 != action:  # #
407                     name = world_db["ThingActions"][action]["TA_NAME"]  # #
408                 else:  # #
409                     name = " "  # #
410                 metamapB[pos] = ord(name[0]) # #
411         string = write_map(string, metamapB)  # #
412
413         atomic_write(io_db["path_worldstate"], string, delete=False)
414         strong_write(io_db["file_out"], "WORLD_UPDATED\n")
415         io_db["worldstate_updateable"] = False
416
417
418 def replay_game():
419     """Replay game from record file.
420
421     Use opts.replay as breakpoint turn to which to replay automatically before
422     switching to manual input by non-meta commands in server input file
423     triggering further reads of record file. Ensure opts.replay is at least 1.
424     Run try_worldstate_update() before each interactive obey()/read_command().
425     """
426     if opts.replay < 1:
427         opts.replay = 1
428     print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
429           " (if so late a turn is to be found).")
430     if not os.access(io_db["path_record"], os.F_OK):
431         raise SystemExit("No record file found to replay.")
432     io_db["file_record"] = open(io_db["path_record"], "r")
433     io_db["file_record"].prefix = "record file line "
434     io_db["file_record"].line_n = 1
435     while world_db["TURN"] < opts.replay:
436         line = io_db["file_record"].readline()
437         if "" == line:
438             break
439         obey(line.rstrip(), io_db["file_record"].prefix
440              + str(io_db["file_record"].line_n))
441         io_db["file_record"].line_n = io_db["file_record"].line_n + 1
442     while True:
443         try_worldstate_update()
444         obey(read_command(), "in file", replay=True)
445
446
447 def play_game():
448     """Play game by server input file commands. Before, load save file found.
449
450     If no save file is found, a new world is generated from the commands in the
451     world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
452     command and all that follow via the server input file. Run
453     try_worldstate_update() before each interactive obey()/read_command().
454     """
455     if os.access(io_db["path_save"], os.F_OK):
456         obey_lines_in_file(io_db["path_save"], "save")
457     else:
458         if not os.access(io_db["path_worldconf"], os.F_OK):
459             msg = "No world config file from which to start a new world."
460             raise SystemExit(msg)
461         obey_lines_in_file(io_db["path_worldconf"], "world config ",
462                            do_record=True)
463         obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
464     while True:
465         try_worldstate_update()
466         obey(read_command(), "in file", do_record=True)
467
468
469 def make_map():
470     """(Re-)make island map.
471
472     Let "~" represent water, "." land, "X" trees: Build island shape randomly,
473     start with one land cell in the middle, then go into cycle of repeatedly
474     selecting a random sea cell and transforming it into land if it is neighbor
475     to land. The cycle ends when a land cell is due to be created at the map's
476     border. Then put some trees on the map (TODO: more precise algorithm desc).
477     """
478     # 7DRL: Also add some ":" cells, and (not surrounded by trees!) "_" altar.
479
480     def is_neighbor(coordinates, type):
481         y = coordinates[0]
482         x = coordinates[1]
483         length = world_db["MAP_LENGTH"]
484         ind = y % 2
485         diag_west = x + (ind > 0)
486         diag_east = x + (ind < (length - 1))
487         pos = (y * length) + x
488         if (y > 0 and diag_east
489             and type == chr(world_db["MAP"][pos - length + ind])) \
490            or (x < (length - 1)
491                and type == chr(world_db["MAP"][pos + 1])) \
492            or (y < (length - 1) and diag_east
493                and type == chr(world_db["MAP"][pos + length + ind])) \
494            or (y > 0 and diag_west
495                and type == chr(world_db["MAP"][pos - length - (not ind)])) \
496            or (x > 0
497                and type == chr(world_db["MAP"][pos - 1])) \
498            or (y < (length - 1) and diag_west
499                and type == chr(world_db["MAP"][pos + length - (not ind)])):
500             return True
501         return False
502
503     world_db["MAP"] = bytearray(b'~' * (world_db["MAP_LENGTH"] ** 2))
504     length = world_db["MAP_LENGTH"]
505     add_half_width = (not (length % 2)) * int(length / 2)
506     world_db["MAP"][int((length ** 2) / 2) + add_half_width] = ord(".")
507     while (1):
508         y = rand.next() % length
509         x = rand.next() % length
510         pos = (y * length) + x
511         if "~" == chr(world_db["MAP"][pos]) and is_neighbor((y, x), "."):
512             if y == 0 or y == (length - 1) or x == 0 or x == (length - 1):
513                 break
514             world_db["MAP"][pos] = ord(".")
515     n_trees = int((length ** 2) / 16)
516     i_trees = 0
517     while (i_trees <= n_trees):
518         single_allowed = rand.next() % 32
519         y = rand.next() % length
520         x = rand.next() % length
521         pos = (y * length) + x
522         if "." == chr(world_db["MAP"][pos]) \
523           and ((not single_allowed) or is_neighbor((y, x), "X")):
524             world_db["MAP"][pos] = ord("X")
525             i_trees += 1
526     # This all-too-precise replica of the original C code misses iter_limit().
527     n_colons = int((length ** 2) / 16)  # #
528     i_colons = 0  # #
529     while (i_colons <= n_colons):  # #
530         single_allowed = rand.next() % 256  # #
531         y = rand.next() % length  # #
532         x = rand.next() % length  # #
533         pos = (y * length) + x  # #
534         if ("." == chr(world_db["MAP"][pos])  # #
535           and ((not single_allowed) or is_neighbor((y, x), ":"))):  # #
536             world_db["MAP"][pos] = ord(":")  # #
537             i_colons += 1  # #
538     altar_placed = False  # #
539     while not altar_placed:  # #
540         y = rand.next() % length  # #
541         x = rand.next() % length  # #
542         pos = (y * length) + x  # #
543         if (("." == chr(world_db["MAP"][pos]  # #
544              or ":" == chr(world_db["MAP"][pos]))
545             and not is_neighbor((y, x), "X"))):  # #
546             world_db["MAP"][pos] = ord("_")  # #
547             world_db["altar"] = (y, x)  # #
548             altar_placed = True  # #
549
550
551 def eat_vs_hunger_threshold(thingtype):
552     """Return satiation cost of eating for type. Good food for it must be >."""
553     hunger_unit = hunger_per_turn(thingtype)
554     actiontype = [id for id in world_db["ThingActions"]
555                if world_db["ThingActions"][id]["TA_NAME"] == "use"][0]
556     return world_db["ThingActions"][actiontype]["TA_EFFORT"] * hunger_unit
557
558
559 def update_map_memory(t, age_map=True):
560     """Update t's T_MEMMAP with what's in its FOV now,age its T_MEMMEPTHMAP."""
561
562     def age_some_memdepthmap_on_nonfov_cells():
563         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
564         # ord_v = ord("v")
565         # ord_0 = ord("0")
566         # ord_9 = ord("9")
567         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
568         #             if not ord_v == t["fovmap"][pos]
569         #             if ord_0 <= t["T_MEMDEPTHMAP"][pos]
570         #             if ord_9 > t["T_MEMDEPTHMAP"][pos]
571         #             if not rand.next() % (2 **
572         #                                   (t["T_MEMDEPTHMAP"][pos] - 48))]:
573         #     t["T_MEMDEPTHMAP"][pos] += 1
574         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
575         fovmap = c_pointer_to_bytearray(t["fovmap"])
576         libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap)
577
578     def update_mem_and_memdepthmap_via_fovmap():
579         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
580         # for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2)
581         #             if ord_v == t["fovmap"][pos]]:
582         #     t["T_MEMDEPTHMAP"][pos] = ord_0
583         #     t["T_MEMMAP"][pos] = world_db["MAP"][pos]
584         memdepthmap = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
585         memmap = c_pointer_to_bytearray(t["T_MEMMAP"])
586         fovmap = c_pointer_to_bytearray(t["fovmap"])
587         map = c_pointer_to_bytearray(world_db["MAP"])
588         libpr.update_mem_and_memdepthmap_via_fovmap(map, fovmap, memdepthmap,
589                                                     memmap)
590
591     if not t["T_MEMMAP"]:
592         t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
593     if not t["T_MEMDEPTHMAP"]:
594         t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
595     update_mem_and_memdepthmap_via_fovmap()
596     if age_map:
597         age_some_memdepthmap_on_nonfov_cells()
598     ord_v = ord("v")
599     t["T_MEMTHING"] = [mt for mt in t["T_MEMTHING"]
600                        if ord_v != t["fovmap"][(mt[1] * world_db["MAP_LENGTH"])
601                                                + mt[2]]]
602     maplength = world_db["MAP_LENGTH"]
603     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])  # #
604     [t["T_MEMTHING"].append((world_db["Things"][id]["T_TYPE"],
605                              world_db["Things"][id]["T_POSY"],
606                              world_db["Things"][id]["T_POSX"]))
607      for id in world_db["Things"]
608      if not world_db["Things"][id]["carried"]
609      if not world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
610                                                               ["TT_LIFEPOINTS"]
611      if (t == world_db["Things"][0] or
612          (world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
613                                                           ["TT_TOOL"] == "food"
614           and 
615           world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
616                                                     ["TT_TOOLPOWER"] > eat_cost
617          )
618         )
619      if ord_v == t["fovmap"][(world_db["Things"][id]["T_POSY"] * maplength)
620                                        + world_db["Things"][id]["T_POSX"]]]
621
622
623 def set_world_inactive():
624     """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
625     server_test()
626     if os.access(io_db["path_worldstate"], os.F_OK):
627         os.remove(io_db["path_worldstate"])
628     world_db["WORLD_ACTIVE"] = 0
629
630
631 def integer_test(val_string, min, max=None):
632     """Return val_string if possible integer >= min and <= max, else None."""
633     try:
634         val = int(val_string)
635         if val < min or (max is not None and val > max):
636             raise ValueError
637         return val
638     except ValueError:
639         msg = "Ignoring: Please use integer >= " + str(min)
640         if max is not None:
641             msg += " and <= " + str(max)
642         msg += "."
643         print(msg)
644         return None
645
646
647 def setter(category, key, min, max=None):
648     """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
649     if category is None:
650         def f(val_string):
651             val = integer_test(val_string, min, max)
652             if None != val:
653                 world_db[key] = val
654     else:
655         if category == "Thing":
656             id_store = command_tid
657             decorator = test_Thing_id
658         elif category == "ThingType":
659             id_store = command_ttid
660             decorator = test_ThingType_id
661         elif category == "ThingAction":
662             id_store = command_taid
663             decorator = test_ThingAction_id
664
665         @decorator
666         def f(val_string):
667             val = integer_test(val_string, min, max)
668             if None != val:
669                 world_db[category + "s"][id_store.id][key] = val
670     return f
671
672
673 def build_fov_map(t):
674     """Build Thing's FOV map."""
675     t["fovmap"] = bytearray(b'v' * (world_db["MAP_LENGTH"] ** 2))
676     fovmap = c_pointer_to_bytearray(t["fovmap"])
677     map = c_pointer_to_bytearray(world_db["MAP"])
678     if libpr.build_fov_map(t["T_POSY"], t["T_POSX"], fovmap, map):
679         raise RuntimeError("Malloc error in build_fov_Map().")
680
681
682 def log_help():
683     """Send quick usage info to log."""
684     log("Use 'w'/'e'/'s'/'d'/'x'/'c' to move, and 'w' to wait.")
685     log("Use 'p' to pick up objects, and 'D' to drop them.")
686     log("Some objects can be used (such as: eaten) by 'u' if they are in "
687         + "your inventory. Use 'Up'/'Down' to navigate the inventory.")
688     log("Use 'l' to toggle 'look' mode (move an exploration cursor instead of "
689         + "the player over the map).")
690     log("Use 'PgUp'/PgDn' to scroll the 'Things here' window.")
691     log("See README file for more details.")
692
693
694 def decrement_lifepoints(t):
695     """Decrement t's lifepoints by 1, and if to zero, corpse it.
696
697     If t is the player avatar, only blank its fovmap, so that the client may
698     still display memory data. On non-player things, erase fovmap and memory.
699     Dying actors drop all their things.
700     """
701     # 7DRL: Return 1 if death, else 0.
702     t["T_LIFEPOINTS"] -= 1
703     if 0 == t["T_LIFEPOINTS"]:
704         for id in t["T_CARRIES"]:
705             t["T_CARRIES"].remove(id)
706             world_db["Things"][id]["T_POSY"] = t["T_POSY"]
707             world_db["Things"][id]["T_POSX"] = t["T_POSX"]
708             world_db["Things"][id]["carried"] = False
709         t["T_TYPE"] = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
710         if world_db["Things"][0] == t:
711             t["fovmap"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2))
712             log("You die.")
713             log("See README on how to start over.")
714         else:
715             t["fovmap"] = False
716             t["T_MEMMAP"] = False
717             t["T_MEMDEPTHMAP"] = False
718             t["T_MEMTHING"] = []
719         return world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
720     return 0  # #
721
722
723 def mv_yx_in_dir_legal(dir, y, x):
724     """Wrapper around libpr.mv_yx_in_dir_legal to simplify its use."""
725     dir_c = dir.encode("ascii")[0]
726     test = libpr.mv_yx_in_dir_legal_wrap(dir_c, y, x)
727     if -1 == test:
728         raise RuntimeError("Too much wrapping in mv_yx_in_dir_legal_wrap()!")
729     return (test, libpr.result_y(), libpr.result_x())
730
731
732 def enter_altar():  # #
733      """What happens when the player enters the altar."""
734      if world_db["FAVOR_STAGE"] > 9000:
735         log("You step on a soul-less slab of stone.")
736         return
737      log("YOU ENTER SACRED GROUND.")
738      if world_db["FAVOR_STAGE"] == 0:
739          world_db["FAVOR_STAGE"] = 1
740          log("The Island God speaks to you: \"I don't trust you. You intrude "
741               + "on the island's affairs. I think you're a nuisance at best, "
742               + "and a danger to my children at worst. I will give you a "
743               + "chance to lighten my mood, however: For a while now, I've "
744               + "been trying to spread the plant "
745               + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_NAME"] + " (\""
746               + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_SYMBOL"]
747               + "\"). I have not been very successful so far. Maybe you can "
748               + "make yourself useful there. I will count each further "
749               + world_db["ThingTypes"][world_db["PLANT_0"]]["TT_NAME"]
750               + " that grows to your favor.\"")
751      elif world_db["FAVOR_STAGE"] == 1 and world_db["GOD_FAVOR"] >= 100:
752          world_db["FAVOR_STAGE"] = 2
753          log("The Island God speaks to you: \"You could have done worse so "
754              + "far. Maybe you are not the worst to happen to this island "
755              + "since the metal birds threw the great lightning ball. Maybe "
756              + "you can help me spread another plant. It multiplies faster, "
757              + "and it is highly nutritious: "
758              + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_NAME"] + " (\""
759              + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_SYMBOL"]
760              + "\"). It is new. I give you the only example. Be very careful "
761              + "with it! I also give you another tool that may be helpful.\"")
762          id = id_setter(-1, "Things")
763          world_db["Things"][id] = new_Thing(world_db["PLANT_1"],
764                                             world_db["altar"])
765          id = id_setter(-1, "Things")
766          world_db["Things"][id] = new_Thing(world_db["TOOL_0"],
767                                             world_db["altar"])
768      elif world_db["FAVOR_STAGE"] == 2 and \
769           0 == len([id for id in world_db["Things"]
770                    if world_db["Things"][id]["T_TYPE"]
771                       == world_db["PLANT_1"]]):
772          log("The Island God speaks to you: \"I am greatly disappointed that "
773              + "you lost all "
774              + world_db["ThingTypes"][world_db["PLANT_1"]]["TT_NAME"]
775              + " this island had. Here is another one. It cost me great work. "
776              + "Be more careful this time.\"")
777          id = id_setter(-1, "Things")
778          world_db["Things"][id] = new_Thing(world_db["PLANT_1"],
779                                             world_db["altar"])
780          world_db["GOD_FAVOR"] -= 250
781      elif world_db["GOD_FAVOR"] > 9000:
782          world_db["FAVOR_STAGE"] = 9001
783          log("The Island God speaks to you: \"You have proven yourself worthy"
784               + " of my respect. You were a good citizen to the island, and "
785               + "sometimes a better steward to its inhabitants than me. The "
786               + "island shall miss you when you leave. But you have earned "
787               + "the right to do so. Take this "
788               + world_db["ThingTypes"][world_db["SLIPPERS"]]["TT_NAME"]
789               + " and USE it when you please. It will take you to where you "
790               + "came from. (But do feel free to stay here as long as you "
791               + "like.)\"")
792          id = id_setter(-1, "Things")
793          world_db["Things"][id] = new_Thing(world_db["SLIPPERS"],
794                                             world_db["altar"])
795
796
797 def actor_wait(t):
798     """Make t do nothing (but loudly, if player avatar)."""
799     if t == world_db["Things"][0]:
800         log("You wait.")
801
802
803 def actor_move(t):
804     """If passable, move/collide(=attack) thing into T_ARGUMENT's direction."""
805     # 7DRL: Player wounding (worse: killing) others will lower God's favor.
806     # 7DRL: Player entering the altar triggers enter_altar().
807     # 7DRL: Player with axe chops down trees.
808     passable = False
809     move_result = mv_yx_in_dir_legal(chr(t["T_ARGUMENT"]),
810                                      t["T_POSY"], t["T_POSX"])
811     if 1 == move_result[0]:
812         pos = (move_result[1] * world_db["MAP_LENGTH"]) + move_result[2]
813         hitted = [id for id in world_db["Things"]
814                   if world_db["Things"][id] != t
815                   if world_db["Things"][id]["T_LIFEPOINTS"]
816                   if world_db["Things"][id]["T_POSY"] == move_result[1]
817                   if world_db["Things"][id]["T_POSX"] == move_result[2]]
818         if len(hitted):
819             hit_id = hitted[0]
820             if t == world_db["Things"][0]:
821                 hitted_type = world_db["Things"][hit_id]["T_TYPE"]
822                 hitted_name = world_db["ThingTypes"][hitted_type]["TT_NAME"]
823                 log("You wound " + hitted_name + ".")
824                 world_db["GOD_FAVOR"] -= -1  # #
825             elif 0 == hit_id:
826                 hitter_name = world_db["ThingTypes"][t["T_TYPE"]]["TT_NAME"]
827                 log(hitter_name +" wounds you.")
828             test = decrement_lifepoints(world_db["Things"][hit_id])  # #(test=)
829             if test and t == world_db["Things"][0]:  # #
830                 world_db["GOD_FAVOR"] -test  # #
831             return
832         if (ord("X") == world_db["MAP"][pos]  # #
833             or ord("|") == world_db["MAP"][pos]):  # #
834             carries_axe = False  # #
835             for id in t["T_CARRIES"]:  # # 
836                 type = world_db["Things"][id]["T_TYPE"]  # #
837                 if world_db["ThingTypes"][type]["TT_TOOL"] == "axe":  # #
838                     carries_axe = True  # #
839                     break  # #
840             if carries_axe:  # #
841                 axe_name = world_db["ThingTypes"][type]["TT_NAME"]  # #
842                 if t == world_db["Things"][0]:  # #
843                     log("With your " + axe_name + ", you chop!\n")  # #
844                     if ord("X") == world_db["MAP"][pos]:  # #
845                         world_db["GOD_FAVOR"] = -1  # #
846                 chop_power = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
847                 case_X = world_db["MAP"][pos] == ord("X")  # #
848                 if (chop_power > 0  # #
849                     and ((case_X and  # #
850                           0 == int(rand.next() / chop_power))  # #
851                     or (not case_X and  # #
852                              0 == int(rand.next() / (3 * chop_power))))):  # #
853                     if t == world_db["Things"][0]:  # #
854                         log("You chop it down.")  # #
855                     if world_db["MAP"][pos] == ord("X"):  # #
856                         world_db["GOD_FAVOR"] = -10  # #
857                     world_db["MAP"][pos] = ord(".")   # #
858                     i = 3 if case_X else 1  # #
859                     for i in range(i):  # #
860                         id = id_setter(-1, "Things")  # #
861                         world_db["Things"][id] = \
862                           new_Thing(world_db["LUMBER"],  # #
863                                     (move_result[1], move_result[2]))  # # 
864                     build_fov_map(t)  # #
865                 return   # #
866         passable = ("." == chr(world_db["MAP"][pos]) or
867                     ":" == chr(world_db["MAP"][pos]) or # #
868                     "_" == chr(world_db["MAP"][pos])) # #
869     dir = [dir for dir in directions_db
870            if directions_db[dir] == chr(t["T_ARGUMENT"])][0]
871     if passable:
872         t["T_POSY"] = move_result[1]
873         t["T_POSX"] = move_result[2]
874         for id in t["T_CARRIES"]:
875             world_db["Things"][id]["T_POSY"] = move_result[1]
876             world_db["Things"][id]["T_POSX"] = move_result[2]
877         build_fov_map(t)
878         if t == world_db["Things"][0]:
879             log("You move " + dir + ".")
880             if (move_result[1] == world_db["altar"][0] and  # #
881                 move_result[2] == world_db["altar"][1]):  # #
882                 enter_altar()  # #
883     elif t == world_db["Things"][0]:
884         log("You fail to move " + dir + ".")
885
886
887 def actor_pick_up(t):
888     """Make t pick up (topmost?) Thing from ground into inventory.
889
890     Define topmostness by how low the thing's type ID is.
891     """
892     # 7DRL: Non-player picking up player-dropped consumable -> GOD_FAVOR gain.
893     # 7DRL: Non-players pick up nothing but food of good value to them.
894     used_slots = len(t["T_CARRIES"]) # #
895     if used_slots < world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]: # #
896         ids = [id for id in world_db["Things"] if world_db["Things"][id] != t
897                if not world_db["Things"][id]["carried"]
898                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
899                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]
900         if len(ids):
901             lowest_tid = -1
902             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"]) # #
903             for iid in ids:
904                 tid = world_db["Things"][iid]["T_TYPE"] 
905                 if lowest_tid == -1 or tid < lowest_tid:
906                     if (t != world_db["Things"][0] and  # #
907                         (world_db["ThingTypes"][tid]["TT_TOOL"] != "food"  # #
908                          or (world_db["ThingTypes"][tid]["TT_TOOLPOWER"]  # #
909                              <= eat_cost))):  # #
910                         continue  # #
911                     id = iid
912                     lowest_tid = tid
913             world_db["Things"][id]["carried"] = True
914             type = world_db["Things"][id]["T_TYPE"]  # #
915             if (t != world_db["Things"][0] # #
916                 and world_db["Things"][id]["T_PLAYERDROP"]  # #
917                 and world_db["ThingTypes"][type]["TT_TOOL"] == "food"):  # #
918                 score = world_db["ThingTypes"][type]["TT_TOOLPOWER"] / 32  # #
919                 world_db["GOD_FAVOR"] += score  # #
920                 world_db["Things"][id]["T_PLAYERDROP"] = 0  # #
921             t["T_CARRIES"].append(id)
922             if t == world_db["Things"][0]:
923                 log("You pick up an object.")
924         elif t == world_db["Things"][0]:
925             log("You try to pick up an object, but there is none.")
926     elif t == world_db["Things"][0]: # #
927         log("Can't pick up object: No storage room to carry more.") # #
928
929
930 def actor_drop(t):
931     """Make t rop Thing from inventory to ground indexed by T_ARGUMENT."""
932     # TODO: Handle case where T_ARGUMENT matches nothing.
933     if len(t["T_CARRIES"]):
934         id = t["T_CARRIES"][t["T_ARGUMENT"]]
935         t["T_CARRIES"].remove(id)
936         world_db["Things"][id]["carried"] = False
937         if t == world_db["Things"][0]:
938             log("You drop an object.")
939             world_db["Things"][id]["T_PLAYERDROP"] = 1  # #
940     elif t == world_db["Things"][0]:
941        log("You try to drop an object, but you own none.")
942
943
944 def actor_use(t):
945     """Make t use (for now: consume) T_ARGUMENT-indexed Thing in inventory."""
946     # TODO: Handle case where T_ARGUMENT matches nothing.
947     # 7DLR: Handle SLIPPERS-type Thing use.
948     # 7DRL: Player with fertilizer fertilizes
949     if len(t["T_CARRIES"]):
950         id = t["T_CARRIES"][t["T_ARGUMENT"]]
951         type = world_db["Things"][id]["T_TYPE"]
952         if type == world_db["SLIPPERS"]:  # #
953             if t == world_db["Things"][0]:  # #
954                 log("You use the " + world_db["ThingTypes"][type]["TT_NAME"]  # #
955                     + ". It glows in wondrous colors, and emits a sound as "  # #
956                     + "if from a dying cat. The Island God laughs.\n")  # #
957             t["T_LIFEPOINTS"] = 1  # #
958             decrement_lifepoints(t)  # #
959         elif (world_db["ThingTypes"][type]["TT_TOOL"] == "axe"  # #
960               and t == world_db["Things"][0]):  # #
961                 log("To use this item for chopping, move towards a tree while "
962                      + "carrying it in your inventory.")  # #
963         elif (world_db["ThingTypes"][type]["TT_TOOL"] == "carpentry"):  # #
964             pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
965             if (world_db["MAP"][pos] == ord("X")  # #
966                 or world_db["MAP"][pos] == ord("|")):  # #
967                 log("Can't build when standing on barrier.")  # #
968                 return
969             for id in [id for id in world_db["Things"]
970                        if not world_db["Things"][id] == t
971                        if not world_db["Things"][id]["carried"]
972                        if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
973                        if world_db["Things"][id]["T_POSX"] == t["T_POSX"]]:
974                  log("Can't build when standing objects.")  # #
975                  return
976             for id in t["T_CARRIES"]:  # #
977                 type_tool = world_db["Things"][id]["T_TYPE"]  # #
978                 if (world_db["ThingTypes"][type_tool]["TT_TOOL"]  # #
979                     == "carpentry"):  # #
980                     break  # #
981             wood_id = None  # #
982             for id in t["T_CARRIES"]:  # #
983                 type_material = world_db["Things"][id]["T_TYPE"]  # #
984                 if (world_db["ThingTypes"][type_material]["TT_TOOL"]  # #
985                     == "wood"):  # #
986                     wood_id = id  # #
987                     break  # #
988             if wood_id != None:  # #
989                 t["T_CARRIES"].remove(wood_id)  # #
990                 del world_db["Things"][wood_id]  # #
991                 world_db["MAP"][pos] = ord("|")  # #
992                 log("With your "  # #
993                     + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
994                     + " you build a wooden barrier from your "  # #
995                     + world_db["ThingTypes"][type_material]["TT_NAME"]  # #
996                     + ".")  # #
997             else:  # #
998                 log("You can't use a "  # #
999                     + world_db["ThingTypes"][type_tool]["TT_NAME"]  # #
1000                     + " without some wood in your inventory.")  # #
1001         elif world_db["ThingTypes"][type]["TT_TOOL"] == "fertilizer":  # #
1002             pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1003             if world_db["MAP"][pos] == ord("."):
1004                 world_db["MAP"][pos] = ord(":")
1005             else:
1006                 log("Can only fertilize on unfertilized earth.")
1007         elif world_db["ThingTypes"][type]["TT_TOOL"] == "food":
1008             t["T_CARRIES"].remove(id)
1009             del world_db["Things"][id]
1010             t["T_SATIATION"] += world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1011             if t == world_db["Things"][0]:
1012                 log("You consume this object.")
1013         elif t == world_db["Things"][0]:
1014             log("You try to use this object, but fail.")
1015     elif t == world_db["Things"][0]:
1016         log("You try to use an object, but you own none.")
1017
1018
1019 def thingproliferation(t, prol_map):
1020     """To chance of 1/TT_PROLIFERATE, create t offspring in open neighbor cell.
1021
1022     Naturally only works with TT_PROLIFERATE > 0. The neighbor cell must be be
1023     marked "." in prol_map. If there are several map cell candidates, one is
1024     selected randomly.
1025     """
1026     # 7DRL: success increments God's mood
1027     # 7DRL: Plants (no TT_LIFEPOINTS) proliferate only on ":" ground.
1028     prolscore = world_db["ThingTypes"][t["T_TYPE"]]["TT_PROLIFERATE"]
1029     if prolscore and (1 == prolscore or 1 == (rand.next() % prolscore)):
1030         candidates = []
1031         for dir in [directions_db[key] for key in directions_db]:
1032             mv_result = mv_yx_in_dir_legal(dir, t["T_POSY"], t["T_POSX"])
1033             pos = mv_result[1] * world_db["MAP_LENGTH"] + mv_result[2]
1034             if mv_result[0] and \
1035                (ord(":") == prol_map[pos]  # #
1036                 or (world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]  # #
1037                     and ord(".") == prol_map[pos])):
1038             # if mv_result[0] and ord(".") == prol_map[mv_result[1]
1039             #                                          * world_db["MAP_LENGTH"]
1040             #                                          + mv_result[2]]:
1041                 candidates.append((mv_result[1], mv_result[2]))
1042         if len(candidates):
1043             i = rand.next() % len(candidates)
1044             id = id_setter(-1, "Things")
1045             newT = new_Thing(t["T_TYPE"], (candidates[i][0], candidates[i][1]))
1046             world_db["Things"][id] = newT
1047             if (world_db["FAVOR_STAGE"] > 0  # #
1048                 and t["T_TYPE"] == world_db["PLANT_0"]):  # #
1049                 world_db["GOD_FAVOR"] += 5  # #
1050             elif t["T_TYPE"] == world_db["PLANT_1"]:  # #
1051                 world_db["GOD_FAVOR"] += 25  # #
1052
1053
1054 def try_healing(t):
1055     """If t's HP < max, increment them if well-nourished, maybe waiting."""
1056     # 7DRL: Successful heals increment God's mood.
1057     if t["T_LIFEPOINTS"] < \
1058        world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]:
1059         wait_id = [id for id in world_db["ThingActions"]
1060                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1061         wait_divider = 8 if t["T_COMMAND"] == wait_id else 1
1062         testval = int(abs(t["T_SATIATION"]) / wait_divider)
1063         if (testval <= 1 or 1 == (rand.next() % testval)):
1064             t["T_LIFEPOINTS"] += 1
1065             if t == world_db["Things"][0]:
1066                 log("You heal.")
1067
1068
1069 def hunger_per_turn(type_id):
1070     """The amount of satiation score lost per turn for things of given type."""
1071     return int(math.sqrt(world_db["ThingTypes"][type_id]["TT_LIFEPOINTS"]))
1072
1073
1074 def hunger(t):
1075     """Decrement t's satiation,dependent on it trigger lifepoint dec chance."""
1076     if t["T_SATIATION"] > -32768:
1077         #max_hp = world_db["ThingTypes"][t["T_TYPE"]]["TT_LIFEPOINTS"]
1078         t["T_SATIATION"] -= hunger_per_turn(t["T_TYPE"]) # int(math.sqrt(max_hp))
1079     if 0 != t["T_SATIATION"] and 0 == int(rand.next() / abs(t["T_SATIATION"])):
1080         if t == world_db["Things"][0]:
1081             if t["T_SATIATION"] < 0:
1082                 log("You suffer from hunger.")
1083             else:
1084                 log("You suffer from over-eating.")
1085         decrement_lifepoints(t)
1086
1087
1088 def get_dir_to_target(t, filter):
1089     """Try to set T_COMMAND/T_ARGUMENT for move to "filter"-determined target.
1090
1091     The path-wise nearest target is chosen, via the shortest available path.
1092     Target must not be t. On succcess, return positive value, else False.
1093     Filters:
1094     "a": Thing in FOV is animate, but of ThingType, starts out weaker than t
1095          is, and its corpse would be healthy food for t
1096     "f": move away from an enemy – any visible actor whose thing type has more
1097          TT_LIFEPOINTS than t LIFEPOINTS, and might find t's corpse healthy
1098          food – if it is closer than n steps, where n will shrink as t's hunger
1099          grows; if enemy is too close, move towards (attack) the enemy instead;
1100          if no fleeing is possible, nor attacking useful, wait; don't tread on
1101          non-enemies for fleeing
1102     "c": Thing in memorized map is consumable of sufficient nutrition for t
1103     "s": memory map cell with greatest-reachable degree of unexploredness
1104     """
1105
1106     def zero_score_map_where_char_on_memdepthmap(c):
1107         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
1108         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
1109         #           if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]:
1110         #     set_map_score(i, 0)
1111         map = c_pointer_to_bytearray(t["T_MEMDEPTHMAP"])
1112         if libpr.zero_score_map_where_char_on_memdepthmap(c, map):
1113             raise RuntimeError("No score map allocated for "
1114                                "zero_score_map_where_char_on_memdepthmap().")
1115
1116     def set_map_score_at_thingpos(id, score):
1117         pos = world_db["Things"][id]["T_POSY"] * world_db["MAP_LENGTH"] \
1118                                      + world_db["Things"][id]["T_POSX"]
1119         set_map_score(pos, score)
1120
1121     def set_map_score(pos, score):
1122         test = libpr.set_map_score(pos, score)
1123         if test:
1124             raise RuntimeError("No score map allocated for set_map_score().")
1125
1126     def get_map_score(pos):
1127         result = libpr.get_map_score(pos)
1128         if result < 0:
1129             raise RuntimeError("No score map allocated for get_map_score().")
1130         return result
1131
1132     def animate_in_fov(Thing, maplength): # maplength needed for optimization?
1133         if not Thing["T_LIFEPOINTS"] or Thing["carried"] or Thing == t:
1134             return False
1135         pos = Thing["T_POSY"] * maplength + Thing["T_POSX"]
1136         if 118 == t["fovmap"][pos]: # optimization: 118 = ord("v")
1137             return True
1138
1139     def good_attack_target(v):
1140         eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1141         type = world_db["ThingTypes"][v["T_TYPE"]]
1142         type_corpse = world_db["ThingTypes"][type["TT_CORPSE_ID"]]
1143         if t["T_LIFEPOINTS"] > type["TT_LIFEPOINTS"] \
1144         and type_corpse["TT_TOOL"] == "food" \
1145         and type_corpse["TT_TOOLPOWER"] > eat_cost:
1146             return True
1147         return False
1148
1149     def good_flee_target(m):
1150         own_corpse_id = world_db["ThingTypes"][t["T_TYPE"]]["TT_CORPSE_ID"]
1151         corpse_type = world_db["ThingTypes"][own_corpse_id]
1152         targetness = 0 if corpse_type["TT_TOOL"] != "food" \
1153                        else corpse_type["TT_TOOLPOWER"]
1154         type = world_db["ThingTypes"][m["T_TYPE"]]
1155         if t["T_LIFEPOINTS"] < type["TT_LIFEPOINTS"] \
1156         and targetness > eat_vs_hunger_threshold(m["T_TYPE"]):
1157             return True
1158         return False
1159
1160     def seeing_thing():
1161         maplength = world_db["MAP_LENGTH"]
1162         if t["fovmap"] and "a" == filter:
1163             for id in world_db["Things"]:
1164                 if animate_in_fov(world_db["Things"][id], maplength):
1165                     if good_attack_target(world_db["Things"][id]):
1166                         return True
1167         elif t["fovmap"] and "f" == filter:
1168             for id in world_db["Things"]:
1169                 if animate_in_fov(world_db["Things"][id], maplength):
1170                     if good_flee_target(world_db["Things"][id]):
1171                         return True
1172         elif t["T_MEMMAP"] and "c" == filter:
1173             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1174             ord_blank = ord(" ")
1175             for mt in t["T_MEMTHING"]:
1176                 if ord_blank != t["T_MEMMAP"][(mt[1] * world_db["MAP_LENGTH"])
1177                                             + mt[2]] and \
1178                    (t != world_db["Things"][0] or \
1179                     (world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food"
1180                       and world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"]
1181                        > eat_cost)):
1182                     return True
1183         return False
1184
1185     def set_cells_passable_on_memmap_to_65534_on_scoremap():
1186         # OUTSOURCED FOR PERFORMANCE REASONS TO libplomrogue.so:
1187         # ord_dot = ord(".")
1188         # memmap = t["T_MEMMAP"]
1189         # for i in [i for i in range(world_db["MAP_LENGTH"] ** 2)
1190         #            if ord_dot == memmap[i]]:
1191         #     set_map_score(i, 65534) # i.e. 65535-1
1192         map = c_pointer_to_bytearray(t["T_MEMMAP"])
1193         if libpr.set_cells_passable_on_memmap_to_65534_on_scoremap(map):
1194             raise RuntimeError("No score map allocated for "
1195                         "set_cells_passable_on_memmap_to_65534_on_scoremap().")
1196
1197     def init_score_map():
1198         test = libpr.init_score_map()
1199         if test:
1200             raise RuntimeError("Malloc error in init_score_map().")
1201         set_cells_passable_on_memmap_to_65534_on_scoremap()
1202         maplength = world_db["MAP_LENGTH"]
1203         if "a" == filter:
1204             [set_map_score_at_thingpos(id, 0)
1205              for id in world_db["Things"]
1206              if animate_in_fov(world_db["Things"][id], maplength)
1207              if good_attack_target(world_db["Things"][id])]
1208         elif "f" == filter:
1209             [set_map_score_at_thingpos(id, 0)
1210              for id in world_db["Things"]
1211              if animate_in_fov(world_db["Things"][id], maplength)
1212              if good_flee_target(world_db["Things"][id])]
1213         elif "c" == filter:
1214             eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1215             ord_blank = ord(" ")
1216             [set_map_score(mt[1] * maplength + mt[2], 0)
1217              for mt in t["T_MEMTHING"]
1218              if ord_blank != t["T_MEMMAP"][mt[1] * maplength + mt[2]]
1219              if t != world_db["Things"][0] or
1220                 (world_db["ThingTypes"][mt[0]]["TT_TOOL"] == "food" and
1221                  world_db["ThingTypes"][mt[0]]["TT_TOOLPOWER"] > eat_cost)]
1222         elif "s" == filter:
1223             zero_score_map_where_char_on_memdepthmap(mem_depth_c[0])
1224         if "f" == filter:
1225             [set_map_score_at_thingpos(id, 65535)
1226              for id in world_db["Things"]
1227              if animate_in_fov(world_db["Things"][id], maplength)
1228              if get_map_score(world_db["Things"][id]["T_POSY"] * maplength
1229                               + world_db["Things"][id]["T_POSX"])]
1230         elif "a" != filter:
1231             [set_map_score_at_thingpos(id, 65535)
1232              for id in world_db["Things"]
1233              if animate_in_fov(world_db["Things"][id], maplength)]
1234
1235     def rand_target_dir(neighbors, cmp, dirs):
1236         candidates = []
1237         n_candidates = 0
1238         for i in range(len(dirs)):
1239             if cmp == neighbors[i]:
1240                 candidates.append(dirs[i])
1241                 n_candidates += 1
1242         return candidates[rand.next() % n_candidates] if n_candidates else 0
1243
1244     def get_neighbor_scores(dirs, eye_pos):
1245         scores = []
1246         if libpr.ready_neighbor_scores(eye_pos):
1247             raise RuntimeError("No score map allocated for " +
1248                                "ready_neighbor_scores.()")
1249         for i in range(len(dirs)):
1250             scores.append(libpr.get_neighbor_score(i))
1251         return scores
1252
1253     def get_dir_from_neighbors():
1254         dir_to_target = False
1255         dirs = "edcxsw"
1256         eye_pos = t["T_POSY"] * world_db["MAP_LENGTH"] + t["T_POSX"]
1257         neighbors = get_neighbor_scores(dirs, eye_pos)
1258         minmax_start = 0 if "f" == filter else 65535 - 1
1259         minmax_neighbor = minmax_start
1260         for i in range(len(dirs)):
1261             if ("f" == filter and get_map_score(eye_pos) < neighbors[i] and
1262                 minmax_neighbor < neighbors[i] and 65535 != neighbors[i]) \
1263                or ("f" != filter and minmax_neighbor > neighbors[i]):
1264                 minmax_neighbor = neighbors[i]
1265         if minmax_neighbor != minmax_start:
1266             dir_to_target = rand_target_dir(neighbors, minmax_neighbor, dirs)
1267         if "f" == filter:
1268             distance = get_map_score(eye_pos)
1269             fear_distance = world_db["MAP_LENGTH"]
1270             if t["T_SATIATION"] < 0 and math.sqrt(-t["T_SATIATION"]) > 0:
1271                 fear_distance = fear_distance / math.sqrt(-t["T_SATIATION"])
1272             attack_distance = 1
1273             if not dir_to_target:
1274                 if attack_distance >= distance:
1275                     dir_to_target = rand_target_dir(neighbors,
1276                                                     distance - 1, dirs)
1277                 elif fear_distance >= distance:
1278                     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1279                                       if
1280                                       world_db["ThingActions"][id]["TA_NAME"]
1281                                          == "wait"][0]
1282                     return 1
1283             elif dir_to_target and fear_distance < distance:
1284                 dir_to_target = 0
1285         return dir_to_target
1286
1287     dir_to_target = False
1288     mem_depth_c = b' '
1289     run_i = 9 + 1 if "s" == filter else 1
1290     while run_i and not dir_to_target and ("s" == filter or seeing_thing()):
1291         run_i -= 1
1292         init_score_map()
1293         mem_depth_c = b'9' if b' ' == mem_depth_c \
1294                            else bytes([mem_depth_c[0] - 1])
1295         if libpr.dijkstra_map():
1296             raise RuntimeError("No score map allocated for dijkstra_map().")
1297         dir_to_target = get_dir_from_neighbors()
1298         libpr.free_score_map()
1299         if dir_to_target and str == type(dir_to_target):
1300             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1301                               if world_db["ThingActions"][id]["TA_NAME"]
1302                                  == "move"][0]
1303             t["T_ARGUMENT"] = ord(dir_to_target)
1304     return dir_to_target
1305
1306
1307 def standing_on_food(t):
1308     """Return True/False whether t is standing on healthy consumable."""
1309     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1310     for id in [id for id in world_db["Things"] if world_db["Things"][id] != t
1311                if not world_db["Things"][id]["carried"]
1312                if world_db["Things"][id]["T_POSY"] == t["T_POSY"]
1313                if world_db["Things"][id]["T_POSX"] == t["T_POSX"]
1314                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1315                   ["TT_TOOL"] == "food"
1316                if world_db["ThingTypes"][world_db["Things"][id]["T_TYPE"]]
1317                   ["TT_TOOLPOWER"] > eat_cost]:
1318         return True
1319     return False
1320
1321
1322 def get_inventory_slot_to_consume(t):
1323     """Return invent. slot of healthiest consumable(if any healthy),else -1."""
1324     cmp_food = -1
1325     selection = -1
1326     i = 0
1327     eat_cost = eat_vs_hunger_threshold(t["T_TYPE"])
1328     for id in t["T_CARRIES"]:
1329         type = world_db["Things"][id]["T_TYPE"]
1330         if world_db["ThingTypes"][type]["TT_TOOL"] == "food" \
1331            and world_db["ThingTypes"][type]["TT_TOOLPOWER"]:
1332             nutvalue = world_db["ThingTypes"][type]["TT_TOOLPOWER"]
1333             tmp_cmp = abs(t["T_SATIATION"] + nutvalue - eat_cost)
1334             if (cmp_food < 0 and tmp_cmp < abs(t["T_SATIATION"])) \
1335             or tmp_cmp < cmp_food:
1336                 cmp_food = tmp_cmp
1337                 selection = i
1338         i += 1
1339     return selection
1340
1341
1342 def ai(t):
1343     """Determine next command/argment for actor t via AI algorithms."""
1344     # 7DRL add: Don't pick up or search things when inventory is full.
1345     t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1346                       if world_db["ThingActions"][id]["TA_NAME"] == "wait"][0]
1347     if get_dir_to_target(t, "f"):
1348         return
1349     sel = get_inventory_slot_to_consume(t)
1350     if -1 != sel:
1351         t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1352                           if world_db["ThingActions"][id]["TA_NAME"]
1353                              == "use"][0]
1354         t["T_ARGUMENT"] = sel
1355     elif standing_on_food(t):
1356         if  (len(t["T_CARRIES"]) <  # #
1357              world_db["ThingTypes"][t["T_TYPE"]]["TT_STORAGE"]):  # #
1358             t["T_COMMAND"] = [id for id in world_db["ThingActions"]
1359                                  if world_db["ThingActions"][id]["TA_NAME"]
1360                                  == "pick_up"][0]
1361     else:
1362         going_to_known_food_spot = get_dir_to_target(t, "c")
1363         if not going_to_known_food_spot:
1364             aiming_for_walking_food = get_dir_to_target(t, "a")
1365             if not aiming_for_walking_food:
1366                 get_dir_to_target(t, "s")
1367
1368
1369 def turn_over():
1370     """Run game world and its inhabitants until new player input expected."""
1371     # 7DRL: effort of move action is TA_EFFORT / sqrt(TT_LIFEPOINTS)
1372     id = 0
1373     whilebreaker = False
1374     while world_db["Things"][0]["T_LIFEPOINTS"]:
1375         proliferable_map = world_db["MAP"][:]
1376         for id in [id for id in world_db["Things"]
1377                    if not world_db["Things"][id]["carried"]]:
1378             y = world_db["Things"][id]["T_POSY"]
1379             x = world_db["Things"][id]["T_POSX"]
1380             proliferable_map[y * world_db["MAP_LENGTH"] + x] = ord("X")
1381         for id in [id for id in world_db["Things"]]:  # Only what's from start!
1382             if not id in world_db["Things"] or \
1383                world_db["Things"][id]["carried"]:   # May have been consumed or
1384                 continue                            # picked up during turn …
1385             Thing = world_db["Things"][id]
1386             if Thing["T_LIFEPOINTS"]:
1387                 if not Thing["T_COMMAND"]:
1388                     update_map_memory(Thing)
1389                     if 0 == id:
1390                         whilebreaker = True
1391                         break
1392                     ai(Thing)
1393                 try_healing(Thing)
1394                 hunger(Thing)
1395                 if Thing["T_LIFEPOINTS"]:
1396                     Thing["T_PROGRESS"] += 1
1397                     taid = [a for a in world_db["ThingActions"]
1398                               if a == Thing["T_COMMAND"]][0]
1399                     ThingAction = world_db["ThingActions"][taid]
1400                     #if Thing["T_PROGRESS"] == ThingAction["TA_EFFORT"]:
1401                     effort = ThingAction["TA_EFFORT"]  # #
1402                     if ThingAction["TA_NAME"] == "move":  # #
1403                         type = Thing["T_TYPE"]  # #
1404                         max_hp = (world_db["ThingTypes"][type]  # #
1405                                  ["TT_LIFEPOINTS"])  # #
1406                         effort = int(effort / math.sqrt(max_hp))  # #
1407                     if Thing["T_PROGRESS"] == effort:  # #
1408                         eval("actor_" + ThingAction["TA_NAME"])(Thing)
1409                         Thing["T_COMMAND"] = 0
1410                         Thing["T_PROGRESS"] = 0
1411             thingproliferation(Thing, proliferable_map)
1412         if whilebreaker:
1413             break
1414         world_db["TURN"] += 1
1415
1416
1417 def new_Thing(type, pos=(0, 0)):
1418     """Return Thing of type T_TYPE, with fovmap if alive and world active."""
1419     thing = {
1420         "T_LIFEPOINTS": world_db["ThingTypes"][type]["TT_LIFEPOINTS"],
1421         "T_ARGUMENT": 0,
1422         "T_PROGRESS": 0,
1423         "T_SATIATION": 0,
1424         "T_COMMAND": 0,
1425         "T_PLAYERDROP": 0,  # #
1426         "T_TYPE": type,
1427         "T_POSY": pos[0],
1428         "T_POSX": pos[1],
1429         "T_CARRIES": [],
1430         "carried": False,
1431         "T_MEMTHING": [],
1432         "T_MEMMAP": False,
1433         "T_MEMDEPTHMAP": False,
1434         "fovmap": False
1435     }
1436     if world_db["WORLD_ACTIVE"] and thing["T_LIFEPOINTS"]:
1437         build_fov_map(thing)
1438     return thing
1439
1440
1441 def id_setter(id, category, id_store=False, start_at_1=False):
1442     """Set ID of object of category to manipulate ID unused? Create new one.
1443
1444     The ID is stored as id_store.id (if id_store is set). If the integer of the
1445     input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1)
1446     <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is
1447     always returned when no new object is created, otherwise the new object's
1448     ID.
1449     """
1450     min = 0 if start_at_1 else -1
1451     if str == type(id):
1452         id = integer_test(id, min)
1453     if None != id:
1454         if id in world_db[category]:
1455             if id_store:
1456                 id_store.id = id
1457             return None
1458         else:
1459             if (start_at_1 and 0 == id) \
1460                or ((not start_at_1) and (id < 0)):
1461                 id = 0 if start_at_1 else -1
1462                 while 1:
1463                     id = id + 1
1464                     if id not in world_db[category]:
1465                         break
1466             if id_store:
1467                 id_store.id = id
1468     return id
1469
1470
1471 def command_ping():
1472     """Send PONG line to server output file."""
1473     strong_write(io_db["file_out"], "PONG\n")
1474
1475
1476 def command_quit():
1477     """Abort server process."""
1478     if None == opts.replay:
1479         if world_db["WORLD_ACTIVE"]:
1480             save_world()
1481         atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True)
1482     raise SystemExit("received QUIT command")
1483
1484
1485 def command_thingshere(str_y, str_x):
1486     """Write to out file list of Things known to player at coordinate y, x."""
1487     # 7DRL: terrain, too
1488     if world_db["WORLD_ACTIVE"]:
1489         y = integer_test(str_y, 0, 255)
1490         x = integer_test(str_x, 0, 255)
1491         length = world_db["MAP_LENGTH"]
1492         if None != y and None != x and y < length and x < length:
1493             pos = (y * world_db["MAP_LENGTH"]) + x
1494             strong_write(io_db["file_out"], "THINGS_HERE START\n")
1495             pos = y * world_db["MAP_LENGTH"] + x;  # #
1496             if "v" == chr(world_db["Things"][0]["fovmap"][pos]):
1497                 for id in [id for tid in sorted(list(world_db["ThingTypes"]))
1498                               for id in world_db["Things"]
1499                               if not world_db["Things"][id]["carried"]
1500                               if world_db["Things"][id]["T_TYPE"] == tid
1501                               if y == world_db["Things"][id]["T_POSY"]
1502                               if x == world_db["Things"][id]["T_POSX"]]:
1503                     type = world_db["Things"][id]["T_TYPE"]
1504                     name = world_db["ThingTypes"][type]["TT_NAME"]
1505                     strong_write(io_db["file_out"], name + "\n")
1506             else:
1507                 for mt in [mt for tid in sorted(list(world_db["ThingTypes"]))
1508                               for mt in world_db["Things"][0]["T_MEMTHING"]
1509                               if mt[0] == tid if y == mt[1] if x == mt[2]]:
1510                     name = world_db["ThingTypes"][mt[0]]["TT_NAME"]
1511                     strong_write(io_db["file_out"], name + "\n")
1512             if world_db["Things"][0]["T_MEMMAP"][pos] == ord("~"):  # #
1513                 name = "(terrain: SEA)"  # #
1514             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("."):  # #
1515                 name = "(terrain: EARTH)"  # #
1516             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord(":"):  # #
1517                 name = "(terrain: SOIL)"  # #
1518             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("X"):  # #
1519                 name = "(terrain: TREE)"  # #
1520             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("|"):  # #
1521                 name = "(terrain: WALL)"  # #
1522             elif world_db["Things"][0]["T_MEMMAP"][pos] == ord("_"):  # #
1523                 name = "(terrain: ALTAR)"  # #
1524             else:  # #
1525                 name = "(?)"  # #
1526             strong_write(io_db["file_out"], name + "\n")  # #
1527             strong_write(io_db["file_out"], "THINGS_HERE END\n")
1528         else:
1529             print("Ignoring: Invalid map coordinates.")
1530     else:
1531         print("Ignoring: Command only works on existing worlds.")
1532
1533
1534 def play_commander(action, args=False):
1535     """Setter for player's T_COMMAND and T_ARGUMENT, then calling turn_over().
1536
1537     T_ARGUMENT is set to direction char if action=="wait",or 8-bit int if args.
1538     """
1539
1540     def set_command():
1541         id = [x for x in world_db["ThingActions"]
1542                 if world_db["ThingActions"][x]["TA_NAME"] == action][0]
1543         world_db["Things"][0]["T_COMMAND"] = id
1544         turn_over()
1545
1546     def set_command_and_argument_int(str_arg):
1547         val = integer_test(str_arg, 0, 255)
1548         if None != val:
1549             world_db["Things"][0]["T_ARGUMENT"] = val
1550             set_command()
1551
1552     def set_command_and_argument_movestring(str_arg):
1553         if str_arg in directions_db:
1554             world_db["Things"][0]["T_ARGUMENT"] = ord(directions_db[str_arg])
1555             set_command()
1556         else:
1557             print("Ignoring: Argument must be valid direction string.")
1558
1559     if action == "move":
1560         return set_command_and_argument_movestring
1561     elif args:
1562         return set_command_and_argument_int
1563     else:
1564         return set_command
1565
1566
1567 def command_seedrandomness(seed_string):
1568     """Set rand seed to int(seed_string)."""
1569     val = integer_test(seed_string, 0, 4294967295)
1570     if None != val:
1571         rand.seed = val
1572
1573
1574 def command_makeworld(seed_string):
1575     """(Re-)build game world, i.e. map, things, to a new turn 1 from seed.
1576
1577     Seed rand with seed. Do more only with a "wait" ThingAction and
1578     world["PLAYER_TYPE"] matching ThingType of TT_START_NUMBER > 0. Then,
1579     world_db["Things"] emptied, call make_map() and set
1580     world_db["WORLD_ACTIVE"], world_db["TURN"] to 1. Build new Things
1581     according to ThingTypes' TT_START_NUMBERS, with Thing of ID 0 to ThingType
1582     of ID = world["PLAYER_TYPE"]. Place Things randomly, and actors not on each
1583     other. Init player's memory map. Write "NEW_WORLD" line to out file.
1584     Call log_help().
1585     """
1586
1587     # def free_pos(plant=False):
1588     def free_pos(plant=False):  # #
1589         i = 0
1590         while 1:
1591             err = "Space to put thing on too hard to find. Map too small?"
1592             while 1:
1593                 y = rand.next() % world_db["MAP_LENGTH"]
1594                 x = rand.next() % world_db["MAP_LENGTH"]
1595                 pos = y * world_db["MAP_LENGTH"] + x;
1596                 if (not plant  # #
1597                     and "." == chr(world_db["MAP"][pos])) \
1598                    or ":" == chr(world_db["MAP"][pos]):  # #
1599                     break
1600                 i += 1
1601                 if i == 65535:
1602                     raise SystemExit(err)
1603             # Replica of C code, wrongly ignores animatedness of new Thing.
1604             pos_clear = (0 == len([id for id in world_db["Things"]
1605                                    if world_db["Things"][id]["T_LIFEPOINTS"]
1606                                    if world_db["Things"][id]["T_POSY"] == y
1607                                    if world_db["Things"][id]["T_POSX"] == x]))
1608             if pos_clear:
1609                 break
1610         return (y, x)
1611
1612     val = integer_test(seed_string, 0, 4294967295)
1613     if None == val:
1614         return
1615     rand.seed = val
1616     player_will_be_generated = False
1617     playertype = world_db["PLAYER_TYPE"]
1618     for ThingType in world_db["ThingTypes"]:
1619         if playertype == ThingType:
1620             if 0 < world_db["ThingTypes"][ThingType]["TT_START_NUMBER"]:
1621                 player_will_be_generated = True
1622             break
1623     if not player_will_be_generated:
1624         print("Ignoring: No player type with start number >0 defined.")
1625         return
1626     wait_action = False
1627     for ThingAction in world_db["ThingActions"]:
1628         if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1629             wait_action = True
1630     if not wait_action:
1631         print("Ignoring: No thing action with name 'wait' defined.")
1632         return
1633     for name in specials:  # #
1634         if world_db[name] not in world_db["ThingTypes"]:  # #
1635             print("Ignoring: No valid " + name + " set.")  # #
1636             return  # #
1637     world_db["Things"] = {}
1638     make_map()
1639     world_db["WORLD_ACTIVE"] = 1
1640     world_db["TURN"] = 1
1641     for i in range(world_db["ThingTypes"][playertype]["TT_START_NUMBER"]):
1642         id = id_setter(-1, "Things")
1643         world_db["Things"][id] = new_Thing(playertype, free_pos())
1644     if not world_db["Things"][0]["fovmap"]:
1645         empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1646         world_db["Things"][0]["fovmap"] = empty_fovmap
1647     update_map_memory(world_db["Things"][0])
1648     for type in world_db["ThingTypes"]:
1649         for i in range(world_db["ThingTypes"][type]["TT_START_NUMBER"]):
1650             if type != playertype:
1651                 id = id_setter(-1, "Things")
1652                 plantness = world_db["ThingTypes"][type]["TT_PROLIFERATE"]  # #
1653                 world_db["Things"][id] = new_Thing(type, free_pos(plantness))
1654     strong_write(io_db["file_out"], "NEW_WORLD\n")
1655     log_help()
1656
1657
1658 def command_maplength(maplength_string):
1659     """Redefine map length. Invalidate map, therefore lose all things on it."""
1660     val = integer_test(maplength_string, 1, 256)
1661     if None != val:
1662         world_db["MAP_LENGTH"] = val
1663         world_db["MAP"] = False
1664         set_world_inactive()
1665         world_db["Things"] = {}
1666         libpr.set_maplength(val)
1667
1668
1669 def command_worldactive(worldactive_string):
1670     """Toggle world_db["WORLD_ACTIVE"] if possible.
1671
1672     An active world can always be set inactive. An inactive world can only be
1673     set active with a "wait" ThingAction, and a player Thing (of ID 0), and a
1674     map. On activation, rebuild all Things' FOVs, and the player's map memory.
1675     Also call log_help().
1676     """
1677     # 7DRL: altar must be on map, and specials must be set for active world.
1678     val = integer_test(worldactive_string, 0, 1)
1679     if None != val:
1680         if 0 != world_db["WORLD_ACTIVE"]:
1681             if 0 == val:
1682                 set_world_inactive()
1683             else:
1684                 print("World already active.")
1685         elif 0 == world_db["WORLD_ACTIVE"]:
1686             wait_exists = False
1687             for ThingAction in world_db["ThingActions"]:
1688                 if "wait" == world_db["ThingActions"][ThingAction]["TA_NAME"]:
1689                     wait_exists = True
1690                     break
1691             player_exists = False
1692             for Thing in world_db["Things"]:
1693                 if 0 == Thing:
1694                     player_exists = True
1695                     break
1696             specials_set = True  # #
1697             for name in specials:  # #
1698                 if world_db[name] not in world_db["ThingTypes"]:  # #
1699                     specials_set = False  # #
1700             altar_found = False  # #
1701             if world_db["MAP"]:  # #
1702                 pos = world_db["MAP"].find(b'_')  # #
1703                 if pos > 0:  # #
1704                     y = int(pos / world_db["MAP_LENGTH"])  # #
1705                     x = pos % world_db["MAP_LENGTH"]  # #
1706                     world_db["altar"] = (y, x)  # #
1707                     altar_found = True  # #
1708             if wait_exists and player_exists and world_db["MAP"] \
1709                and specials_set:  # #
1710                 for id in world_db["Things"]:
1711                     if world_db["Things"][id]["T_LIFEPOINTS"]:
1712                         build_fov_map(world_db["Things"][id])
1713                         if 0 == id:
1714                             update_map_memory(world_db["Things"][id], False)
1715                 if not world_db["Things"][0]["T_LIFEPOINTS"]:
1716                     empty_fovmap = bytearray(b" " * world_db["MAP_LENGTH"] ** 2)
1717                     world_db["Things"][0]["fovmap"] = empty_fovmap
1718                 world_db["WORLD_ACTIVE"] = 1
1719                 log_help()
1720             else:
1721                 print("Ignoring: Not all conditions for world activation met.")
1722
1723
1724 def specialtypesetter(name):  # #
1725     """Setter world_db[name], deactivating world if set int no ThingType."""
1726     def helper(str_int):
1727         val = integer_test(str_int, 0)
1728         if None != val:
1729             world_db[name] = val
1730             if world_db["WORLD_ACTIVE"] \
1731                and world_db[name] not in world_db["ThingTypes"]:
1732                 world_db["WORLD_ACTIVE"] = 0
1733                 print(name + " fits no known ThingType, deactivating world.")
1734     return helper
1735
1736
1737 def test_for_id_maker(object, category):
1738     """Return decorator testing for object having "id" attribute."""
1739     def decorator(f):
1740         def helper(*args):
1741             if hasattr(object, "id"):
1742                 f(*args)
1743             else:
1744                 print("Ignoring: No " + category +
1745                       " defined to manipulate yet.")
1746         return helper
1747     return decorator
1748
1749
1750 def command_tid(id_string):
1751     """Set ID of Thing to manipulate. ID unused? Create new one.
1752
1753     Default new Thing's type to the first available ThingType, others: zero.
1754     """
1755     id = id_setter(id_string, "Things", command_tid)
1756     if None != id:
1757         if world_db["ThingTypes"] == {}:
1758             print("Ignoring: No ThingType to settle new Thing in.")
1759             return
1760         type = list(world_db["ThingTypes"].keys())[0]
1761         world_db["Things"][id] = new_Thing(type)
1762
1763
1764 test_Thing_id = test_for_id_maker(command_tid, "Thing")
1765
1766
1767 @test_Thing_id
1768 def command_tcommand(str_int):
1769     """Set T_COMMAND of selected Thing."""
1770     val = integer_test(str_int, 0)
1771     if None != val:
1772         if 0 == val or val in world_db["ThingActions"]:
1773             world_db["Things"][command_tid.id]["T_COMMAND"] = val
1774         else:
1775             print("Ignoring: ThingAction ID belongs to no known ThingAction.")
1776
1777
1778 @test_Thing_id
1779 def command_ttype(str_int):
1780     """Set T_TYPE of selected Thing."""
1781     val = integer_test(str_int, 0)
1782     if None != val:
1783         if val in world_db["ThingTypes"]:
1784             world_db["Things"][command_tid.id]["T_TYPE"] = val
1785         else:
1786             print("Ignoring: ThingType ID belongs to no known ThingType.")
1787
1788
1789 @test_Thing_id
1790 def command_tcarries(str_int):
1791     """Append int(str_int) to T_CARRIES of selected Thing.
1792
1793     The ID int(str_int) must not be of the selected Thing, and must belong to a
1794     Thing with unset "carried" flag. Its "carried" flag will be set on owning.
1795     """
1796     val = integer_test(str_int, 0)
1797     if None != val:
1798         if val == command_tid.id:
1799             print("Ignoring: Thing cannot carry itself.")
1800         elif val in world_db["Things"] \
1801              and not world_db["Things"][val]["carried"]:
1802             world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
1803             world_db["Things"][val]["carried"] = True
1804         else:
1805             print("Ignoring: Thing not available for carrying.")
1806     # Note that the whole carrying structure is different from the C version:
1807     # Carried-ness is marked by a "carried" flag, not by Things containing
1808     # Things internally.
1809
1810
1811 @test_Thing_id
1812 def command_tmemthing(str_t, str_y, str_x):
1813     """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
1814
1815     The type must fit to an existing ThingType, and the position into the map.
1816     """
1817     type = integer_test(str_t, 0)
1818     posy = integer_test(str_y, 0, 255)
1819     posx = integer_test(str_x, 0, 255)
1820     if None != type and None != posy and None != posx:
1821         if type not in world_db["ThingTypes"] \
1822            or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
1823             print("Ignoring: Illegal value for thing type or position.")
1824         else:
1825             memthing = (type, posy, posx)
1826             world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
1827
1828
1829 def setter_map(maptype):
1830     """Set (world or Thing's) map of maptype's int(str_int)-th line to mapline.
1831
1832     If no map of maptype exists yet, initialize it with ' ' bytes first.
1833     """
1834
1835     def valid_map_line(str_int, mapline):
1836         val = integer_test(str_int, 0, 255)
1837         if None != val:
1838             if val >= world_db["MAP_LENGTH"]:
1839                 print("Illegal value for map line number.")
1840             elif len(mapline) != world_db["MAP_LENGTH"]:
1841                 print("Map line length is unequal map width.")
1842             else:
1843                 return val
1844         return None
1845
1846     def nonThingMap_helper(str_int, mapline):
1847         val = valid_map_line(str_int, mapline)
1848         if None != val:
1849             length = world_db["MAP_LENGTH"]
1850             if not world_db["MAP"]:
1851                 map = bytearray(b' ' * (length ** 2))
1852             else:
1853                 map = world_db["MAP"]
1854             map[val * length:(val * length) + length] = mapline.encode()
1855             if not world_db["MAP"]:
1856                 world_db["MAP"] = map
1857
1858     @test_Thing_id
1859     def ThingMap_helper(str_int, mapline):
1860         val = valid_map_line(str_int, mapline)
1861         if None != val:
1862             length = world_db["MAP_LENGTH"]
1863             if not world_db["Things"][command_tid.id][maptype]:
1864                 map = bytearray(b' ' * (length ** 2))
1865             else:
1866                 map = world_db["Things"][command_tid.id][maptype]
1867             map[val * length:(val * length) + length] = mapline.encode()
1868             if not world_db["Things"][command_tid.id][maptype]:
1869                 world_db["Things"][command_tid.id][maptype] = map
1870
1871     return nonThingMap_helper if maptype == "MAP" else ThingMap_helper
1872
1873
1874 def setter_tpos(axis):
1875     """Generate setter for T_POSX or  T_POSY of selected Thing.
1876
1877     If world is active, rebuilds animate things' fovmap, player's memory map.
1878     """
1879     @test_Thing_id
1880     def helper(str_int):
1881         val = integer_test(str_int, 0, 255)
1882         if None != val:
1883             if val < world_db["MAP_LENGTH"]:
1884                 world_db["Things"][command_tid.id]["T_POS" + axis] = val
1885                 if world_db["WORLD_ACTIVE"] \
1886                    and world_db["Things"][command_tid.id]["T_LIFEPOINTS"]:
1887                     build_fov_map(world_db["Things"][command_tid.id])
1888                     if 0 == command_tid.id:
1889                         update_map_memory(world_db["Things"][command_tid.id])
1890             else:
1891                 print("Ignoring: Position is outside of map.")
1892     return helper
1893
1894
1895 def command_ttid(id_string):
1896     """Set ID of ThingType to manipulate. ID unused? Create new one.
1897
1898     Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, TT_TOOL to
1899     "", others: 0.
1900     """
1901     id = id_setter(id_string, "ThingTypes", command_ttid)
1902     if None != id:
1903         world_db["ThingTypes"][id] = {
1904             "TT_NAME": "(none)",
1905             "TT_TOOLPOWER": 0,
1906             "TT_LIFEPOINTS": 0,
1907             "TT_PROLIFERATE": 0,
1908             "TT_START_NUMBER": 0,
1909             "TT_STORAGE": 0, # #
1910             "TT_SYMBOL": "?",
1911             "TT_CORPSE_ID": id,
1912             "TT_TOOL": ""
1913         }
1914
1915
1916 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
1917
1918
1919 @test_ThingType_id
1920 def command_ttname(name):
1921     """Set TT_NAME of selected ThingType."""
1922     world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
1923
1924
1925 @test_ThingType_id
1926 def command_tttool(name):
1927     """Set TT_TOOL of selected ThingType."""
1928     world_db["ThingTypes"][command_ttid.id]["TT_TOOL"] = name
1929
1930
1931 @test_ThingType_id
1932 def command_ttsymbol(char):
1933     """Set TT_SYMBOL of selected ThingType. """
1934     if 1 == len(char):
1935         world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
1936     else:
1937         print("Ignoring: Argument must be single character.")
1938
1939
1940 @test_ThingType_id
1941 def command_ttcorpseid(str_int):
1942     """Set TT_CORPSE_ID of selected ThingType."""
1943     val = integer_test(str_int, 0)
1944     if None != val:
1945         if val in world_db["ThingTypes"]:
1946             world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
1947         else:
1948             print("Ignoring: Corpse ID belongs to no known ThignType.")
1949
1950
1951 def command_taid(id_string):
1952     """Set ID of ThingAction to manipulate. ID unused? Create new one.
1953
1954     Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
1955     """
1956     id = id_setter(id_string, "ThingActions", command_taid, True)
1957     if None != id:
1958         world_db["ThingActions"][id] = {
1959             "TA_EFFORT": 1,
1960             "TA_NAME": "wait"
1961         }
1962
1963
1964 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
1965
1966
1967 @test_ThingAction_id
1968 def command_taname(name):
1969     """Set TA_NAME of selected ThingAction.
1970
1971     The name must match a valid thing action function. If after the name
1972     setting no ThingAction with name "wait" remains, call set_world_inactive().
1973     """
1974     if name == "wait" or name == "move" or name == "use" or name == "drop" \
1975        or name == "pick_up":
1976         world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
1977         if 1 == world_db["WORLD_ACTIVE"]:
1978             wait_defined = False
1979             for id in world_db["ThingActions"]:
1980                 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
1981                     wait_defined = True
1982                     break
1983             if not wait_defined:
1984                 set_world_inactive()
1985     else:
1986         print("Ignoring: Invalid action name.")
1987     # In contrast to the original,naming won't map a function to a ThingAction.
1988
1989
1990 def command_ai():
1991     """Call ai() on player Thing, then turn_over()."""
1992     ai(world_db["Things"][0])
1993     turn_over()
1994
1995
1996 """Commands database.
1997
1998 Map command start tokens to ([0]) number of expected command arguments, ([1])
1999 the command's meta-ness (i.e. is it to be written to the record file, is it to
2000 be ignored in replay mode if read from server input file), and ([2]) a function
2001 to be called on it.
2002 """
2003 commands_db = {
2004     "QUIT": (0, True, command_quit),
2005     "PING": (0, True, command_ping),
2006     "THINGS_HERE": (2, True, command_thingshere),
2007     "MAKE_WORLD": (1, False, command_makeworld),
2008     "SEED_RANDOMNESS": (1, False, command_seedrandomness),
2009     "TURN": (1, False, setter(None, "TURN", 0, 65535)),
2010     "GOD_FAVOR": (1, False, setter(None, "GOD_FAVOR", -32768, 32767)),  # #
2011     "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)),
2012     "MAP_LENGTH": (1, False, command_maplength),
2013     "WORLD_ACTIVE": (1, False, command_worldactive),
2014     "MAP": (2, False, setter_map("MAP")),
2015     "FAVOR_STAGE": (1, False, setter(None, "FAVOR_STAGE", 0, 65535)),  # #
2016     "SLIPPERS": (1, False, specialtypesetter("SLIPPERS")),  # #
2017     "PLANT_0": (1, False, specialtypesetter("PLANT_0")),  # #
2018     "PLANT_1": (1, False, specialtypesetter("PLANT_1")),  # #
2019     "LUMBER": (1, False, specialtypesetter("LUMBER")),  # #
2020     "TOOL_0": (1, False, specialtypesetter("TOOL_0")),  # #
2021     "TOOL_1": (1, False, specialtypesetter("TOOL_1")),  # #
2022     "EMPATHY": (1, False, setter(None, "EMPATHY", 0, 1)),  # #
2023     "TA_ID": (1, False, command_taid),
2024     "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
2025     "TA_NAME": (1, False, command_taname),
2026     "TT_ID": (1, False, command_ttid),
2027     "TT_NAME": (1, False, command_ttname),
2028     "TT_TOOL": (1, False, command_tttool),
2029     "TT_SYMBOL": (1, False, command_ttsymbol),
2030     "TT_CORPSE_ID": (1, False, command_ttcorpseid),
2031     "TT_TOOLPOWER": (1, False, setter("ThingType", "TT_TOOLPOWER", 0, 65535)),
2032     "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
2033                                          0, 255)),
2034     "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
2035                                         0, 65535)),
2036     "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
2037     "TT_STORAGE": (1, False, setter("ThingType", "TT_STORAGE", 0, 255)),  # #
2038     "T_ID": (1, False, command_tid),
2039     "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
2040     "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
2041     "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
2042     "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
2043     "T_COMMAND": (1, False, command_tcommand),
2044     "T_TYPE": (1, False, command_ttype),
2045     "T_CARRIES": (1, False, command_tcarries),
2046     "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
2047     "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
2048     "T_MEMTHING": (3, False, command_tmemthing),
2049     "T_POSY": (1, False, setter_tpos("Y")),
2050     "T_POSX": (1, False, setter_tpos("X")),
2051     "T_PLAYERDROP": (1, False, setter("Thing", "T_PLAYERDROP", 0, 1)),  # #
2052     "wait": (0, False, play_commander("wait")),
2053     "move": (1, False, play_commander("move")),
2054     "pick_up": (0, False, play_commander("pick_up")),
2055     "drop": (1, False, play_commander("drop", True)),
2056     "use": (1, False, play_commander("use", True)),
2057     "ai": (0, False, command_ai)
2058 }
2059 # TODO: Unhandled cases: (Un-)killing animates (esp. player!) with T_LIFEPOINTS.
2060
2061
2062 """World state database. With sane default values. (Randomness is in rand.)"""
2063 world_db = {
2064     "TURN": 0,
2065     "MAP_LENGTH": 64,
2066     "PLAYER_TYPE": 0,
2067     "WORLD_ACTIVE": 0,
2068     "GOD_FAVOR": 0,  # #
2069     "MAP": False,
2070     "FAVOR_STAGE": 0,  # #
2071     "SLIPPERS": 0,  # #
2072     "PLANT_0": 0,  # #
2073     "PLANT_1": 0,  # #
2074     "LUMBER": 0,  # #
2075     "TOOL_0": 0,  # #
2076     "TOOL_1": 0,  # #
2077     "EMPATHY": 1,  # #
2078     "ThingActions": {},
2079     "ThingTypes": {},
2080     "Things": {}
2081 }
2082
2083 # 7DRL-specific!
2084 """Special type settings."""
2085 specials = ["SLIPPERS", "PLANT_0", "PLANT_1", "LUMBER", "TOOL_0", "TOOL_1"]  # #
2086
2087 """Mapping of direction names to internal direction chars."""
2088 directions_db = {"east": "d", "south-east": "c", "south-west": "x",
2089                  "west": "s", "north-west": "w", "north-east": "e"}
2090
2091 """File IO database."""
2092 io_db = {
2093     "path_save": "save",
2094     "path_record": "record_save",
2095     "path_worldconf": "confserver/world",
2096     "path_server": "server/",
2097     "path_in": "server/in",
2098     "path_out": "server/out",
2099     "path_worldstate": "server/worldstate",
2100     "tmp_suffix": "_tmp",
2101     "kicked_by_rival": False,
2102     "worldstate_updateable": False
2103 }
2104
2105
2106 try:
2107     libpr = prep_library()
2108     rand = RandomnessIO()
2109     opts = parse_command_line_arguments()
2110     if opts.savefile:
2111         io_db["path_save"] = opts.savefile
2112         io_db["path_record"] = "record_" + opts.savefile
2113     setup_server_io()
2114     if opts.verbose:
2115         io_db["verbose"] = True
2116     if None != opts.replay:
2117         replay_game()
2118     else:
2119         play_game()
2120 except SystemExit as exit:
2121     print("ABORTING: " + exit.args[0])
2122 except:
2123     print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
2124     raise
2125 finally:
2126     cleanup_server_io()