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:])
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 # Dummy for saving all commands to reconstruct current world state.
121 # Misses same optimizations as record() from the original record().
123 for id in world_db["ThingActions"]:
124 ta = world_db["ThingActions"][id]
125 ta_string = ta_string + "TA_ID " + str(id) + "\n" + \
126 "TA_EFFORT " + str(ta["TA_EFFORT"]) + "\n" + \
127 "TA_NAME " + ta["TA_NAME"] + "\n"
128 atomic_write(io_db["path_save"],
129 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"]) + "\n" +
130 "MAP_LENGTH " + str(world_db["MAP_LENGTH"]) + "\n" +
131 "PLAYER_TYPE " + str(world_db["PLAYER_TYPE"]) + "\n" +
132 "TURN " + str(world_db["TURN"]) + "\n" +
133 "SEED_RANDOMNESS " + str(world_db["SEED_RANDOMNESS"]) + "\n" +
134 "SEED_MAP " + str(world_db["SEED_MAP"]) + "\n" +
136 # TODO: If all this ever does is just writing down what's in world_db, some
137 # loop over its entries should be all that's needed.
140 def obey_lines_in_file(path, name, do_record=False):
141 """Call obey() on each line of path's file, use name in input prefix."""
142 file = open(path, "r")
144 for line in file.readlines():
145 obey(line.rstrip(), name + "file line " + str(line_n),
151 def parse_command_line_arguments():
152 """Return settings values read from command line arguments."""
153 parser = argparse.ArgumentParser()
154 parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1,
156 opts, unknown = parser.parse_known_args()
161 """Ensure valid server out file belonging to current process.
163 This is done by comparing io_db["teststring"] to what's found at the start
164 of the current file at io_db["path_out"]. On failure, set
165 io_db["kicked_by_rival"] and raise SystemExit.
167 if not os.access(io_db["path_out"], os.F_OK):
168 raise SystemExit("Server output file has disappeared.")
169 file = open(io_db["path_out"], "r")
170 test = file.readline().rstrip("\n")
172 if test != io_db["teststring"]:
173 io_db["kicked_by_rival"] = True
174 msg = "Server test string in server output file does not match. This" \
175 " indicates that the current server process has been " \
176 "superseded by another one."
177 raise SystemExit(msg)
181 """Return next newline-delimited command from server in file.
183 Keep building return string until a newline is encountered. Pause between
184 unsuccessful reads, and after too much waiting, run server_test().
191 add = io_db["file_in"].readline()
193 command = command + add
194 if len(command) > 0 and "\n" == command[-1]:
195 command = command[:-1]
198 time.sleep(wait_on_fail)
199 if now + max_wait < time.time():
206 """Replay game from record file.
208 Use opts.replay as breakpoint turn to which to replay automatically before
209 switching to manual input by non-meta commands in server input file
210 triggering further reads of record file. Ensure opts.replay is at least 1.
214 print("Replay mode. Auto-replaying up to turn " + str(opts.replay) +
215 " (if so late a turn is to be found).")
216 if not os.access(io_db["path_record"], os.F_OK):
217 raise SystemExit("No record file found to replay.")
218 io_db["file_record"] = open(io_db["path_record"], "r")
219 io_db["file_record"].prefix = "record file line "
220 io_db["file_record"].line_n = 1
221 while world_db["TURN"] < opts.replay:
222 line = io_db["file_record"].readline()
225 obey(line.rstrip(), io_db["file_record"].prefix
226 + str(io_db["file_record"].line_n))
227 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
229 obey(read_command(), "in file", replay=True)
233 """Play game by server input file commands. Before, load save file found.
235 If no save file is found, a new world is generated from the commands in the
236 world config plus a 'MAKE WORLD [current Unix timestamp]'. Record this
237 command and all that follow via the server input file.
239 if os.access(io_db["path_save"], os.F_OK):
240 obey_lines_in_file(io_db["path_save"], "save")
242 if not os.access(io_db["path_worldconf"], os.F_OK):
243 msg = "No world config file from which to start a new world."
244 raise SystemExit(msg)
245 obey_lines_in_file(io_db["path_worldconf"], "world config ",
247 obey("MAKE_WORLD " + str(int(time.time())), "in file", do_record=True)
249 obey(read_command(), "in file", do_record=True)
254 print("I'd (re-)make the map now, if only I knew how.")
257 def set_world_inactive():
258 """Set world_db["WORLD_ACTIVE"] to 0 and remove worldstate file."""
260 if os.access(io_db["path_worldstate"], os.F_OK):
261 os.remove(io_db["path_worldstate"])
262 world_db["WORLD_ACTIVE"] = 0
265 def integer_test(val_string, min, max):
266 """Return val_string if possible integer >= min and <= max, else False."""
268 val = int(val_string)
269 if val < min or val > max:
273 print("Ignoring: Please use integer >= " + str(min) + " and <= " +
278 def worlddb_value_setter(key, min, max):
279 """Generate: Set world_db[key] to int(val_string) if >= min and <= max."""
280 def func(val_string):
281 val = integer_test(val_string, min, max)
288 """Send PONG line to server output file."""
289 io_db["file_out"].write("PONG\n")
290 io_db["file_out"].flush()
294 """Abort server process."""
295 raise SystemExit("received QUIT command")
298 def command_seedmap(seed_string):
299 """Set world_db["SEED_MAP"] to int(seed_string), then (re-)make map."""
300 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
304 def command_makeworld(seed_string):
306 worlddb_value_setter("SEED_MAP", 0, 4294967295)(seed_string)
307 worlddb_value_setter("SEED_RANDOMNESS", 0, 4294967295)(seed_string)
308 # TODO: Test for existence of player thing and 'wait' thing action?
311 def command_maplength(maplength_string):
314 # TODO: remove things, map
315 worlddb_value_setter("MAP_LENGTH", 1, 256)(maplength_string)
318 def command_worldactive(worldactive_string):
320 val = integer_test(worldactive_string, 0, 1)
322 if 0 != world_db["WORLD_ACTIVE"] and 0 == val:
324 elif 0 == world_db["WORLD_ACTIVE"]:
326 player_exists = False
328 # TODO: perform tests:
329 # Is there thing action of name 'wait'?
330 # Is there a player thing?
332 if wait_exists and player_exists and map_exists:
333 # TODO: rebuild al things' FOVs, map memories
334 world_db["WORLD_ACTIVE"] = 1
337 def command_taid(id_string):
338 """Set ID of ThingAction to manipulate. ID unused? Create new ThingAction.
340 The ID of the ThingAction to manipulate is stored as command_taid.id. If
341 the integer of the input value is valid (>= 0 and <= 255), but 0, a new ID
342 is calculated: The lowest unused ID >0 and <= 255. A new ThingAction's
343 "TA_EFFORT" defaults to 1, its "TA_NAME" to "wait".
345 id = integer_test(id_string, 0, 255)
347 if id in world_db["ThingActions"]:
353 if id not in world_db["ThingActions"]:
357 "No unused ID available to add to ID list.")
359 world_db["ThingActions"][id] = {
366 def command_taeffort(str_int):
367 """Set to int(str_int) effort value of ThingAction of command_taid.id."""
368 if hasattr(command_taid, "id"):
369 val = integer_test(str_int, 0, 255)
371 world_db["ThingActions"][command_taid.id]["TA_EFFORT"] = val
373 print("Ignoring: No thing action defined to manipulate yet.")
376 def command_taname(name):
377 """Set to name name value of ThingAction of command_taid.id.
379 The name must match a valid thing action function. If after the name
380 setting no ThingAction with name "wait" remains, call set_world_inactive().
382 if hasattr(command_taid, "id"):
383 if name == "wait" or name == "move" or name == "use" \
384 or name == "drop" or name == "pick_up":
385 world_db["ThingActions"][command_taid.id]["TA_NAME"] = name
386 if 1 == world_db["WORLD_ACTIVE"]:
388 for id in world_db["ThingActions"]:
389 if "wait" == world_db["ThingActions"][id]["TA_NAME"]:
395 print("Ignoring: Invalid action name.")
397 print("Ignoring: No thing action defined to manipulate yet.")
398 # In contrast to the original,naming won't map a function to a ThingAction.
401 """Commands database.
403 Map command start tokens to ([0]) number of expected command arguments, ([1])
404 the command's meta-ness (i.e. is it to be written to the record file, is it to
405 be ignored in replay mode if read from server input file), and ([2]) a function
409 "QUIT": (0, True, command_quit),
410 "PING": (0, True, command_ping),
411 "MAKE_WORLD": (1, False, command_makeworld),
412 "SEED_MAP": (1, False, command_seedmap),
413 "SEED_RANDOMNESS": (1, False, worlddb_value_setter("SEED_RANDOMNESS", 0,
415 "TURN": (1, False, worlddb_value_setter("TURN", 0, 65535)),
416 "PLAYER_TYPE": (1, False, worlddb_value_setter("PLAYER_TYPE", 0, 255)),
417 "MAP_LENGTH": (1, False, command_maplength),
418 "WORLD_ACTIVE": (1, False, command_worldactive),
419 "TA_ID": (1, False, command_taid),
420 "TA_EFFORT": (1, False, command_taeffort),
421 "TA_NAME": (1, False, command_taname)
425 """World state database. With sane default values."""
429 "SEED_RANDOMNESS": 0,
439 """File IO database."""
442 "path_record": "record",
443 "path_worldconf": "confserver/world",
444 "path_server": "server/",
445 "path_in": "server/in",
446 "path_out": "server/out",
447 "path_worldstate": "server/worldstate",
448 "tmp_suffix": "_tmp",
449 "kicked_by_rival": False
454 opts = parse_command_line_arguments()
456 # print("DUMMY: Run game.")
457 if None != opts.replay:
461 except SystemExit as exit:
462 print("ABORTING: " + exit.args[0])
464 print("SOMETHING WENT WRONG IN UNEXPECTED WAYS")
468 # print("DUMMY: (Clean up C heap.)")