10 """Fill IO files DB with proper file( path)s. Write process IO test string.
12 Ensure IO files directory at server/. Remove any old input file if found.
13 Set up new input file for reading, and new output file for writing. Start
14 output file with process hash line of format PID + " " + floated UNIX time
15 (io_db["teststring"]). Raise SystemExit if file is found at path of either
16 record or save file plus io_db["tmp_suffix"].
18 def detect_atomic_leftover(path, tmp_suffix):
19 path_tmp = path + tmp_suffix
20 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
21 "aborted previous attempt to write '" + path + "'. Aborting " \
22 "until matter is resolved by removing it from its current path."
23 if os.access(path_tmp, os.F_OK):
25 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
26 os.makedirs(io_db["path_server"], exist_ok=True)
27 io_db["file_out"] = open(io_db["path_out"], "w")
28 io_db["file_out"].write(io_db["teststring"] + "\n")
29 io_db["file_out"].flush()
30 if os.access(io_db["path_in"], os.F_OK):
31 os.remove(io_db["path_in"])
32 io_db["file_in"] = open(io_db["path_in"], "w")
33 io_db["file_in"].close()
34 io_db["file_in"] = open(io_db["path_in"], "r")
35 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
36 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
39 def cleanup_server_io():
40 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
41 def helper(file_key, path_key):
43 io_db[file_key].close()
44 if not io_db["kicked_by_rival"] \
45 and os.access(io_db[path_key], os.F_OK):
46 os.remove(io_db[path_key])
47 helper("file_out", "path_out")
48 helper("file_in", "path_in")
49 helper("file_worldstate", "path_worldstate")
50 if "file_record" in io_db:
51 io_db["file_record"].close()
54 def obey(command, prefix, replay=False, do_record=False):
55 """Call function from commands_db mapped to command's first token.
57 The command string is tokenized by shlex.split(comments=True). If replay is
58 set, a non-meta command from the commands_db merely triggers obey() on the
59 next command from the records file. If not, and do do_record is set,
60 non-meta commands are recorded via record(), and save_world() is called.
61 The prefix string is inserted into the server's input message between its
62 beginning 'input ' & ':'. All activity is preceded by a server_test() call.
65 print("input " + prefix + ": " + command)
67 tokens = shlex.split(command, comments=True)
68 except ValueError as err:
69 print("Can't tokenize command string: " + str(err) + ".")
71 if len(tokens) > 0 and tokens[0] in commands_db \
72 and len(tokens) == commands_db[tokens[0]][0] + 1:
73 if commands_db[tokens[0]][1]:
74 commands_db[tokens[0]][2]()
76 print("Due to replay mode, reading command as 'go on in record'.")
77 line = io_db["file_record"].readline()
79 obey(line.rstrip(), io_db["file_record"].prefix
80 + str(io_db["file_record"].line_n))
81 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
83 print("Reached end of record file.")
85 commands_db[tokens[0]][2](*tokens[1:])
89 elif 0 != len(tokens):
90 print("Invalid command/argument, or bad number of tokens.")
93 def atomic_write(path, text, do_append=False):
94 """Atomic write of text to file at path, appended if do_append is set."""
95 path_tmp = path + io_db["tmp_suffix"]
99 if os.access(path, os.F_OK):
100 shutil.copyfile(path, path_tmp)
101 file = open(path_tmp, mode)
104 os.fsync(file.fileno())
106 if os.access(path, os.F_OK):
108 os.rename(path_tmp, path)
112 """Append command string plus newline to record file. (Atomic.)"""
113 # This misses some optimizations from the original record(), namely only
114 # finishing the atomic write with expensive flush() and fsync() every 15
115 # seconds unless explicitely forced. Implement as needed.
116 atomic_write(io_db["path_record"], command + "\n", do_append=True)
120 """Save all commands needed to reconstruct current world state."""
121 # TODO: Misses same optimizations as record() from the original record().
124 string = string.replace("\u005C", '\u005C\u005C')
125 return '"' + string.replace('"', '\u005C"') + '"'
130 if world_db["Things"][id][key]:
131 rmap = world_db["Things"][id][key]
132 length = world_db["MAP_LENGTH"]
133 for i in range(world_db["MAP_LENGTH"]):
134 line = rmap[i * length:(i * length) + length].decode()
135 string = string + key + " " + str(i) + quote(line) + "\n"
141 for memthing in world_db["Things"][id]["T_MEMTHING"]:
142 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
143 str(memthing[1]) + " " + str(memthing[2]) + "\n"
146 def helper(category, id_string, special_keys={}):
148 for id in world_db[category]:
149 string = string + id_string + " " + str(id) + "\n"
150 for key in world_db[category][id]:
151 if not key in special_keys:
152 x = world_db[category][id][key]
153 argument = quote(x) if str == type(x) else str(x)
154 string = string + key + " " + argument + "\n"
155 elif special_keys[key]:
156 string = string + special_keys[key](id)
161 if dict != type(world_db[key]):
162 string = string + key + " " + str(world_db[key]) + "\n"
163 string = string + helper("ThingActions", "TA_ID")
164 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
165 for id in world_db["ThingTypes"]:
166 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
167 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
168 string = string + helper("Things", "T_ID",
169 {"T_CARRIES": False, "carried": False,
170 "T_MEMMAP": mapsetter("T_MEMMAP"),
171 "T_MEMTHING": memthing,
172 "T_MEMDEPTHMAP": mapsetter("T_MEMDEPTHMAP")})
173 for id in world_db["Things"]:
174 if [] != world_db["Things"][id]["T_CARRIES"]:
175 string = string + "T_ID " + str(id) + "\n"
176 for carried_id in world_db["Things"][id]["T_CARRIES"]:
177 string = string + "T_CARRIES " + str(carried_id) + "\n"
178 atomic_write(io_db["path_save"], string)
181 def obey_lines_in_file(path, name, do_record=False):
182 """Call obey() on each line of path's file, use name in input prefix."""
183 file = open(path, "r")
185 for line in file.readlines():
186 obey(line.rstrip(), name + "file line " + str(line_n),
192 def parse_command_line_arguments():
193 """Return settings values read from command line arguments."""
194 parser = argparse.ArgumentParser()
195 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
197 opts, unknown = parser.parse_known_args()
202 """Ensure valid server out file belonging to current process.
204 This is done by comparing io_db["teststring"] to what's found at the start
205 of the current file at io_db["path_out"]. On failure, set
206 io_db["kicked_by_rival"] and raise SystemExit.
208 if not os.access(io_db["path_out"], os.F_OK):
209 raise SystemExit("Server output file has disappeared.")
210 file = open(io_db["path_out"], "r")
211 test = file.readline().rstrip("\n")
213 if test != io_db["teststring"]:
214 io_db["kicked_by_rival"] = True
215 msg = "Server test string in server output file does not match. This" \
216 " indicates that the current server process has been " \
217 "superseded by another one."
218 raise SystemExit(msg)
222 """Return next newline-delimited command from server in file.
224 Keep building return string until a newline is encountered. Pause between
225 unsuccessful reads, and after too much waiting, run server_test().
232 add = io_db["file_in"].readline()
234 command = command + add
235 if len(command) > 0 and "\n" == command[-1]:
236 command = command[:-1]
239 time.sleep(wait_on_fail)
240 if now + max_wait < time.time():
247 """Replay game from record file.
249 Use opts.replay as breakpoint turn to which to replay automatically before
250 switching to manual input by non-meta commands in server input file
251 triggering further reads of record file. Ensure opts.replay is at least 1.
255 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
256 " (if so late a turn is to be found).")
257 if not os.access(io_db["path_record"], os.F_OK):
258 raise SystemExit("No record file found to replay.")
259 io_db["file_record"] = open(io_db["path_record"], "r")
260 io_db["file_record"].prefix = "record file line "
261 io_db["file_record"].line_n = 1
262 while world_db["TURN"] < opts.replay:
263 line = io_db["file_record"].readline()
266 obey(line.rstrip(), io_db["file_record"].prefix
267 + str(io_db["file_record"].line_n))
268 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
270 obey(read_command(), "in file", replay=True)
274 """Play game by server input file commands. Before, load save file found.
276 If no save file is found, a new world is generated from the commands in the
277 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
278 command and all that follow via the server input file.
280 if os.access(io_db["path_save"], os.F_OK):
281 obey_lines_in_file(io_db["path_save"], "save")
283 if not os.access(io_db["path_worldconf"], os.F_OK):
284 msg = "No world config file from which to start a new world."
285 raise SystemExit(msg)
286 obey_lines_in_file(io_db["path_worldconf"], "world config ",
288 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
290 obey(read_command(), "in file", do_record=True)
295 print("I'd (re-)make the map now, if only I knew how.")
298 def set_world_inactive():
299 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
301 if os.access(io_db["path_worldstate"], os.F_OK):
302 os.remove(io_db["path_worldstate"])
303 world_db["WORLD_ACTIVE"] = 0
306 def integer_test(val_string, min, max):
307 """Return val_string if possible integer >= min and <= max, else None."""
309 val = int(val_string)
310 if val < min or val > max:
314 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
319 def setter(category, key, min, max):
320 """Build setter for world_db([category + "s"][id])[key] to >=min/<=max."""
323 val = integer_test(val_string, min, max)
327 if category == "Thing":
328 id_store = command_tid
329 decorator = test_Thing_id
330 elif category == "ThingType":
331 id_store = command_ttid
332 decorator = test_ThingType_id
333 elif category == "ThingAction":
334 id_store = command_taid
335 decorator = test_ThingAction_id
339 val = integer_test(val_string, min, max)
341 world_db[category + "s"][id_store.id][key] = val
346 """Send PONG line to server output file."""
347 io_db["file_out"].write("PONG\n")
348 io_db["file_out"].flush()
352 """Abort server process."""
353 raise SystemExit("received QUIT command")
356 def command_seedmap(seed_string):
357 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
358 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
362 def command_makeworld(seed_string):
364 setter(None, "SEED_MAP", 0, 4294967295)(seed_string)
365 setter(None, "SEED_RANDOMNESS", 0, 4294967295)(seed_string)
366 # TODO: Test for existence of player thing and 'wait' thing action?
369 def command_maplength(maplength_string):
372 # TODO: remove things, map
373 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
376 def command_worldactive(worldactive_string):
378 val = integer_test(worldactive_string, 0, 1)
380 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
382 elif 0 == world_db["WORLD_ACTIVE"]:
384 player_exists = False
386 # TODO: perform tests:
387 # Is there thing action of name 'wait'?
388 # Is there a player thing?
390 if wait_exists and player_exists and map_exists:
391 # TODO: rebuild al things' FOVs, map memories
392 world_db["WORLD_ACTIVE"] = 1
395 def id_setter(id_string, category, id_store, start_at_1=False):
396 """Set ID of object of category to manipulate ID unused? Create new one.
398 The ID is stored as id_store.id. If the integer of the input is valid (if
399 start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
400 start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
401 >= 1, and <= 255. None is always returned when no new object is created,
402 otherwise the new object's ID.
404 min = 0 if start_at_1 else -32768
405 max = 255 if start_at_1 else 32767
406 id = integer_test(id_string, min, max)
408 if id in world_db[category]:
412 if (start_at_1 and 0 == id) \
413 or ((not start_at_1) and (id < 0 or id > 255)):
417 if id not in world_db[category]:
421 "No unused ID available to add to ID list.")
427 def test_for_id_maker(object, category):
428 """Return decorator testing for object having "id" attribute."""
431 if hasattr(object, "id"):
434 print("Ignoring: No " + category +
435 " defined to manipulate yet.")
440 def command_tid(id_string):
441 """Set ID of Thing to manipulate. ID unused? Create new one.
443 Default new Thing's type to the first available ThingType, others: zero.
445 id = id_setter(id_string, "Things", command_tid)
447 if world_db["ThingTypes"] == {}:
448 print("Ignoring: No ThingType to settle new Thing in.")
450 world_db["Things"][id] = {
456 "T_TYPE": list(world_db["ThingTypes"].keys())[0],
463 "T_MEMDEPTHMAP": False
467 test_Thing_id = test_for_id_maker(command_tid, "Thing")
471 def command_tcommand(str_int):
472 """Set T_COMMAND of selected Thing."""
473 val = integer_test(str_int, 0, 255)
475 if 0 == val or val in world_db["ThingActions"]:
476 world_db["Things"][command_tid.id]["T_COMMAND"] = val
478 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
482 def command_ttype(str_int):
483 """Set T_TYPE of selected Thing."""
484 val = integer_test(str_int, 0, 255)
486 if val in world_db["ThingTypes"]:
487 world_db["Things"][command_tid.id]["T_TYPE"] = val
489 print("Ignoring: ThingType ID belongs to no known ThingType.")
493 def command_tcarries(str_int):
494 """Append int(str_int) to T_CARRIES of selected Thing.
496 The ID int(str_int) must not be of the selected Thing, and must belong to a
497 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
499 val = integer_test(str_int, 0, 255)
501 if val == command_tid.id:
502 print("Ignoring: Thing cannot carry itself.")
503 elif val in world_db["Things"] \
504 and not world_db["Things"][val]["carried"]:
505 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
506 world_db["Things"][val]["carried"] = True
508 print("Ignoring: Thing not available for carrying.")
509 # Note that the whole carrying structure is different from the C version:
510 # Carried-ness is marked by a "carried" flag, not by Things containing
515 def command_tmemthing(str_t, str_y, str_x):
516 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
518 The type must fit to an existing ThingType, and the position into the map.
520 type = integer_test(str_t, 0, 255)
521 posy = integer_test(str_y, 0, 255)
522 posx = integer_test(str_x, 0, 255)
523 if None != type and None != posy and None != posx:
524 if type not in world_db["ThingTypes"] \
525 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
526 print("Ignoring: Illegal value for thing type or position.")
528 memthing = (type, posy, posx)
529 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
532 def setter_map(maptype):
533 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
535 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
538 def helper(str_int, mapline):
539 val = integer_test(str_int, 0, 255)
541 if val >= world_db["MAP_LENGTH"]:
542 print("Illegal value for map line number.")
543 elif len(mapline) != world_db["MAP_LENGTH"]:
544 print("Map line length is unequal map width.")
546 length = world_db["MAP_LENGTH"]
548 if not world_db["Things"][command_tid.id][maptype]:
549 rmap = bytearray(b' ' * (length ** 2))
551 rmap = world_db["Things"][command_tid.id][maptype]
552 rmap[val * length:(val * length) + length] = mapline.encode()
553 world_db["Things"][command_tid.id][maptype] = rmap
557 def setter_tpos(axis):
558 """Generate setter for T_POSX or T_POSY of selected Thing."""
561 val = integer_test(str_int, 0, 255)
563 if val < world_db["MAP_LENGTH"]:
564 world_db["Things"][command_tid.id]["T_POS" + axis] = val
565 # TODO: Delete Thing's FOV, and rebuild it if world is active.
567 print("Ignoring: Position is outside of map.")
571 def command_ttid(id_string):
572 """Set ID of ThingType to manipulate. ID unused? Create new one.
574 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
576 id = id_setter(id_string, "ThingTypes", command_ttid)
578 world_db["ThingTypes"][id] = {
583 "TT_START_NUMBER": 0,
589 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
593 def command_ttname(name):
594 """Set TT_NAME of selected ThingType."""
595 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
599 def command_ttsymbol(char):
600 """Set TT_SYMBOL of selected ThingType. """
602 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
604 print("Ignoring: Argument must be single character.")
608 def command_ttcorpseid(str_int):
609 """Set TT_CORPSE_ID of selected ThingType."""
610 val = integer_test(str_int, 0, 255)
612 if val in world_db["ThingTypes"]:
613 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
615 print("Ignoring: Corpse ID belongs to no known ThignType.")
618 def command_taid(id_string):
619 """Set ID of ThingAction to manipulate. ID unused? Create new one.
621 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
623 id = id_setter(id_string, "ThingActions", command_taid, True)
625 world_db["ThingActions"][id] = {
631 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
635 def command_taname(name):
636 """Set TA_NAME of selected ThingAction.
638 The name must match a valid thing action function. If after the name
639 setting no ThingAction with name "wait" remains, call set_world_inactive().
641 if name == "wait" or name == "move" or name == "use" or name == "drop" \
642 or name == "pick_up":
643 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
644 if 1 == world_db["WORLD_ACTIVE"]:
646 for id in world_db["ThingActions"]:
647 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
653 print("Ignoring: Invalid action name.")
654 # In contrast to the original,naming won't map a function to a ThingAction.
657 """Commands database.
659 Map command start tokens to ([0]) number of expected command arguments, ([1])
660 the command's meta-ness (i.e. is it to be written to the record file, is it to
661 be ignored in replay mode if read from server input file), and ([2]) a function
665 "QUIT": (0, True, command_quit),
666 "PING": (0, True, command_ping),
667 "MAKE_WORLD": (1, False, command_makeworld),
668 "SEED_MAP": (1, False, command_seedmap),
669 "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
671 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
672 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
673 "MAP_LENGTH": (1, False, command_maplength),
674 "WORLD_ACTIVE": (1, False, command_worldactive),
675 "TA_ID": (1, False, command_taid),
676 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
677 "TA_NAME": (1, False, command_taname),
678 "TT_ID": (1, False, command_ttid),
679 "TT_NAME": (1, False, command_ttname),
680 "TT_SYMBOL": (1, False, command_ttsymbol),
681 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
682 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
684 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
686 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
688 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
689 "T_ID": (1, False, command_tid),
690 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
691 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
692 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
693 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
694 "T_COMMAND": (1, False, command_tcommand),
695 "T_TYPE": (1, False, command_ttype),
696 "T_CARRIES": (1, False, command_tcarries),
697 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
698 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
699 "T_MEMTHING": (3, False, command_tmemthing),
700 "T_POSY": (1, False, setter_tpos("Y")),
701 "T_POSX": (1, False, setter_tpos("X")),
705 """World state database. With sane default values."""
709 "SEED_RANDOMNESS": 0,
719 """File IO database."""
722 "path_record": "record",
723 "path_worldconf": "confserver/world",
724 "path_server": "server/",
725 "path_in": "server/in",
726 "path_out": "server/out",
727 "path_worldstate": "server/worldstate",
728 "tmp_suffix": "_tmp",
729 "kicked_by_rival": False
734 opts = parse_command_line_arguments()
736 # print("DUMMY: Run game.")
737 if None != opts.replay:
741 except SystemExit as exit:
742 print("ABORTING: " + exit.args[0])
744 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
748 # print("DUMMY: (Clean up C heap.)")