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 world_db["MAP"] = bytearray(b'.' * (world_db["MAP_LENGTH"] ** 2))
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):
373 world_db["Things"] = {}
374 setter(None, "MAP_LENGTH", 1, 256)(maplength_string)
377 def command_worldactive(worldactive_string):
379 val = integer_test(worldactive_string, 0, 1)
381 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
383 elif 0 == world_db["WORLD_ACTIVE"]:
385 player_exists = False
387 # TODO: perform tests:
388 # Is there thing action of name 'wait'?
389 # Is there a player thing?
391 if wait_exists and player_exists and map_exists:
392 # TODO: rebuild al things' FOVs, map memories
393 world_db["WORLD_ACTIVE"] = 1
396 def id_setter(id_string, category, id_store, start_at_1=False):
397 """Set ID of object of category to manipulate ID unused? Create new one.
399 The ID is stored as id_store.id. If the integer of the input is valid (if
400 start_at_1, >= 0 and <= 255, else >= -32768 and <= 32767), but <0 or (if
401 start_at_1) <1, calculate new ID: lowest unused ID >=0 or (if start_at_1)
402 >= 1, and <= 255. None is always returned when no new object is created,
403 otherwise the new object's ID.
405 min = 0 if start_at_1 else -32768
406 max = 255 if start_at_1 else 32767
407 id = integer_test(id_string, min, max)
409 if id in world_db[category]:
413 if (start_at_1 and 0 == id) \
414 or ((not start_at_1) and (id < 0 or id > 255)):
418 if id not in world_db[category]:
422 "No unused ID available to add to ID list.")
428 def test_for_id_maker(object, category):
429 """Return decorator testing for object having "id" attribute."""
432 if hasattr(object, "id"):
435 print("Ignoring: No " + category +
436 " defined to manipulate yet.")
441 def command_tid(id_string):
442 """Set ID of Thing to manipulate. ID unused? Create new one.
444 Default new Thing's type to the first available ThingType, others: zero.
446 id = id_setter(id_string, "Things", command_tid)
448 if world_db["ThingTypes"] == {}:
449 print("Ignoring: No ThingType to settle new Thing in.")
451 world_db["Things"][id] = {
457 "T_TYPE": list(world_db["ThingTypes"].keys())[0],
464 "T_MEMDEPTHMAP": False
468 test_Thing_id = test_for_id_maker(command_tid, "Thing")
472 def command_tcommand(str_int):
473 """Set T_COMMAND of selected Thing."""
474 val = integer_test(str_int, 0, 255)
476 if 0 == val or val in world_db["ThingActions"]:
477 world_db["Things"][command_tid.id]["T_COMMAND"] = val
479 print("Ignoring: ThingAction ID belongs to no known ThingAction.")
483 def command_ttype(str_int):
484 """Set T_TYPE of selected Thing."""
485 val = integer_test(str_int, 0, 255)
487 if val in world_db["ThingTypes"]:
488 world_db["Things"][command_tid.id]["T_TYPE"] = val
490 print("Ignoring: ThingType ID belongs to no known ThingType.")
494 def command_tcarries(str_int):
495 """Append int(str_int) to T_CARRIES of selected Thing.
497 The ID int(str_int) must not be of the selected Thing, and must belong to a
498 Thing with unset "carried" flag. Its "carried" flag will be set on owning.
500 val = integer_test(str_int, 0, 255)
502 if val == command_tid.id:
503 print("Ignoring: Thing cannot carry itself.")
504 elif val in world_db["Things"] \
505 and not world_db["Things"][val]["carried"]:
506 world_db["Things"][command_tid.id]["T_CARRIES"].append(val)
507 world_db["Things"][val]["carried"] = True
509 print("Ignoring: Thing not available for carrying.")
510 # Note that the whole carrying structure is different from the C version:
511 # Carried-ness is marked by a "carried" flag, not by Things containing
516 def command_tmemthing(str_t, str_y, str_x):
517 """Add (int(str_t), int(str_y), int(str_x)) to selected Thing's T_MEMTHING.
519 The type must fit to an existing ThingType, and the position into the map.
521 type = integer_test(str_t, 0, 255)
522 posy = integer_test(str_y, 0, 255)
523 posx = integer_test(str_x, 0, 255)
524 if None != type and None != posy and None != posx:
525 if type not in world_db["ThingTypes"] \
526 or posy >= world_db["MAP_LENGTH"] or posx >= world_db["MAP_LENGTH"]:
527 print("Ignoring: Illegal value for thing type or position.")
529 memthing = (type, posy, posx)
530 world_db["Things"][command_tid.id]["T_MEMTHING"].append(memthing)
533 def setter_map(maptype):
534 """Set selected Thing's map of maptype's int(str_int)-th line to mapline.
536 If Thing has no map of maptype yet, initialize it with ' ' bytes first.
539 def helper(str_int, mapline):
540 val = integer_test(str_int, 0, 255)
542 if val >= world_db["MAP_LENGTH"]:
543 print("Illegal value for map line number.")
544 elif len(mapline) != world_db["MAP_LENGTH"]:
545 print("Map line length is unequal map width.")
547 length = world_db["MAP_LENGTH"]
549 if not world_db["Things"][command_tid.id][maptype]:
550 rmap = bytearray(b' ' * (length ** 2))
552 rmap = world_db["Things"][command_tid.id][maptype]
553 rmap[val * length:(val * length) + length] = mapline.encode()
554 world_db["Things"][command_tid.id][maptype] = rmap
558 def setter_tpos(axis):
559 """Generate setter for T_POSX or T_POSY of selected Thing."""
562 val = integer_test(str_int, 0, 255)
564 if val < world_db["MAP_LENGTH"]:
565 world_db["Things"][command_tid.id]["T_POS" + axis] = val
566 # TODO: Delete Thing's FOV, and rebuild it if world is active.
568 print("Ignoring: Position is outside of map.")
572 def command_ttid(id_string):
573 """Set ID of ThingType to manipulate. ID unused? Create new one.
575 Default new ThingType's TT_SYMBOL to "?", TT_CORPSE_ID to self, others: 0.
577 id = id_setter(id_string, "ThingTypes", command_ttid)
579 world_db["ThingTypes"][id] = {
584 "TT_START_NUMBER": 0,
590 test_ThingType_id = test_for_id_maker(command_ttid, "ThingType")
594 def command_ttname(name):
595 """Set TT_NAME of selected ThingType."""
596 world_db["ThingTypes"][command_ttid.id]["TT_NAME"] = name
600 def command_ttsymbol(char):
601 """Set TT_SYMBOL of selected ThingType. """
603 world_db["ThingTypes"][command_ttid.id]["TT_SYMBOL"] = char
605 print("Ignoring: Argument must be single character.")
609 def command_ttcorpseid(str_int):
610 """Set TT_CORPSE_ID of selected ThingType."""
611 val = integer_test(str_int, 0, 255)
613 if val in world_db["ThingTypes"]:
614 world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val
616 print("Ignoring: Corpse ID belongs to no known ThignType.")
619 def command_taid(id_string):
620 """Set ID of ThingAction to manipulate. ID unused? Create new one.
622 Default new ThingAction's TA_EFFORT to 1, its TA_NAME to "wait".
624 id = id_setter(id_string, "ThingActions", command_taid, True)
626 world_db["ThingActions"][id] = {
632 test_ThingAction_id = test_for_id_maker(command_taid, "ThingAction")
636 def command_taname(name):
637 """Set TA_NAME of selected ThingAction.
639 The name must match a valid thing action function. If after the name
640 setting no ThingAction with name "wait" remains, call set_world_inactive().
642 if name == "wait" or name == "move" or name == "use" or name == "drop" \
643 or name == "pick_up":
644 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
645 if 1 == world_db["WORLD_ACTIVE"]:
647 for id in world_db["ThingActions"]:
648 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
654 print("Ignoring: Invalid action name.")
655 # In contrast to the original,naming won't map a function to a ThingAction.
658 """Commands database.
660 Map command start tokens to ([0]) number of expected command arguments, ([1])
661 the command's meta-ness (i.e. is it to be written to the record file, is it to
662 be ignored in replay mode if read from server input file), and ([2]) a function
666 "QUIT": (0, True, command_quit),
667 "PING": (0, True, command_ping),
668 "MAKE_WORLD": (1, False, command_makeworld),
669 "SEED_MAP": (1, False, command_seedmap),
670 "SEED_RANDOMNESS": (1, False, setter(None, "SEED_RANDOMNESS",
672 "TURN": (1, False, setter(None, "TURN", 0, 65535)),
673 "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)),
674 "MAP_LENGTH": (1, False, command_maplength),
675 "WORLD_ACTIVE": (1, False, command_worldactive),
676 "TA_ID": (1, False, command_taid),
677 "TA_EFFORT": (1, False, setter("ThingAction", "TA_EFFORT", 0, 255)),
678 "TA_NAME": (1, False, command_taname),
679 "TT_ID": (1, False, command_ttid),
680 "TT_NAME": (1, False, command_ttname),
681 "TT_SYMBOL": (1, False, command_ttsymbol),
682 "TT_CORPSE_ID": (1, False, command_ttcorpseid),
683 "TT_CONSUMABLE": (1, False, setter("ThingType", "TT_CONSUMABLE",
685 "TT_START_NUMBER": (1, False, setter("ThingType", "TT_START_NUMBER",
687 "TT_PROLIFERATE": (1, False, setter("ThingType", "TT_PROLIFERATE",
689 "TT_LIFEPOINTS": (1, False, setter("ThingType", "TT_LIFEPOINTS", 0, 255)),
690 "T_ID": (1, False, command_tid),
691 "T_ARGUMENT": (1, False, setter("Thing", "T_ARGUMENT", 0, 255)),
692 "T_PROGRESS": (1, False, setter("Thing", "T_PROGRESS", 0, 255)),
693 "T_LIFEPOINTS": (1, False, setter("Thing", "T_LIFEPOINTS", 0, 255)),
694 "T_SATIATION": (1, False, setter("Thing", "T_SATIATION", -32768, 32767)),
695 "T_COMMAND": (1, False, command_tcommand),
696 "T_TYPE": (1, False, command_ttype),
697 "T_CARRIES": (1, False, command_tcarries),
698 "T_MEMMAP": (2, False, setter_map("T_MEMMAP")),
699 "T_MEMDEPTHMAP": (2, False, setter_map("T_MEMDEPTHMAP")),
700 "T_MEMTHING": (3, False, command_tmemthing),
701 "T_POSY": (1, False, setter_tpos("Y")),
702 "T_POSX": (1, False, setter_tpos("X")),
706 """World state database. With sane default values."""
710 "SEED_RANDOMNESS": 0,
720 """File IO database."""
723 "path_record": "record",
724 "path_worldconf": "confserver/world",
725 "path_server": "server/",
726 "path_in": "server/in",
727 "path_out": "server/out",
728 "path_worldstate": "server/worldstate",
729 "tmp_suffix": "_tmp",
730 "kicked_by_rival": False
735 opts = parse_command_line_arguments()
737 # print("DUMMY: Run game.")
738 if None != opts.replay:
742 except SystemExit as exit:
743 print("ABORTING: " + exit.args[0])
745 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
749 # print("DUMMY: (Clean up C heap.)")