1 # This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3
2 # or any later version. For details on its copyright, license, and warranties,
3 # see the file NOTICE in the root directory of the PlomRogue source package.
9 from server.config.world_data import world_db
10 from server.config.io import io_db
14 """Ensure valid server out file belonging to current process.
16 This is done by comparing io_db["teststring"] to what's found at the start
17 of the current file at io_db["path_out"]. On failure, set
18 io_db["kicked_by_rival"] and raise SystemExit.
20 if not os.access(io_db["path_out"], os.F_OK):
21 raise SystemExit("Server output file has disappeared.")
22 file = open(io_db["path_out"], "r")
23 test = file.readline().rstrip("\n")
25 if test != io_db["teststring"]:
26 io_db["kicked_by_rival"] = True
27 msg = "Server test string in server output file does not match. This" \
28 " indicates that the current server process has been " \
29 "superseded by another one."
33 def safely_remove_worldstate_file():
34 from server.io import server_test
35 if os.access(io_db["path_worldstate"], os.F_OK):
37 os.remove(io_db["path_worldstate"])
40 def atomic_write(path, text, do_append=False, delete=True):
41 """Atomic write of text to file at path, appended if do_append is set."""
42 path_tmp = path + io_db["tmp_suffix"]
46 if os.access(path, os.F_OK):
47 from shutil import copyfile
48 copyfile(path, path_tmp)
49 file = open(path_tmp, mode)
50 strong_write(file, text)
52 if delete and os.access(path, os.F_OK):
54 os.rename(path_tmp, path)
57 def strong_write(file, string):
58 """Apply write(string), then flush()."""
63 def setup_server_io():
64 """Fill IO files DB with proper file( path)s. Write process IO test string.
66 Ensure IO files directory at server/. Remove any old input file if found.
67 Set up new input file for reading, and new output file for appending. Start
68 output file with process hash line of format PID + " " + floated UNIX time
69 (io_db["teststring"]). Raise SystemExit if file is found at path of either
70 record or save file plus io_db["tmp_suffix"].
72 def detect_atomic_leftover(path, tmp_suffix):
73 path_tmp = path + tmp_suffix
74 msg = "Found file '" + path_tmp + "' that may be a leftover from an " \
75 "aborted previous attempt to write '" + path + "'. Aborting " \
76 "until matter is resolved by removing it from its current path."
77 if os.access(path_tmp, os.F_OK):
79 io_db["teststring"] = str(os.getpid()) + " " + str(time.time())
80 io_db["save_wait_start"] = 0
81 io_db["verbose"] = False
82 io_db["record_chunk"] = ""
83 os.makedirs(io_db["path_server"], exist_ok=True)
84 io_db["file_out"] = open(io_db["path_out"], "a")
85 strong_write(io_db["file_out"], io_db["teststring"] + "\n")
86 if os.access(io_db["path_in"], os.F_OK):
87 os.remove(io_db["path_in"])
88 io_db["file_in"] = open(io_db["path_in"], "w")
89 io_db["file_in"].close()
90 io_db["file_in"] = open(io_db["path_in"], "r")
91 detect_atomic_leftover(io_db["path_save"], io_db["tmp_suffix"])
92 detect_atomic_leftover(io_db["path_record"], io_db["tmp_suffix"])
95 def cleanup_server_io():
96 """Close and (if io_db["kicked_by_rival"] false) remove files in io_db."""
97 def helper(file_key, path_key):
99 io_db[file_key].close()
100 if not io_db["kicked_by_rival"] \
101 and os.access(io_db[path_key], os.F_OK):
102 os.remove(io_db[path_key])
103 helper("file_in", "path_in")
104 helper("file_out", "path_out")
105 helper("file_worldstate", "path_worldstate")
106 if "file_record" in io_db:
107 io_db["file_record"].close()
110 """Return next newline-delimited command from server in file.
112 Keep building return string until a newline is encountered. Pause between
113 unsuccessful reads, and after too much waiting, run server_test().
115 wait_on_fail = io_db["wait_on_read_fail"]
116 max_wait = io_db["max_wait_on_read_fail"]
120 add = io_db["file_in"].readline()
122 command = command + add
123 if len(command) > 0 and "\n" == command[-1]:
124 command = command[:-1]
127 time.sleep(wait_on_fail)
128 if now + max_wait < time.time():
135 """Send "msg" to log."""
136 strong_write(io_db["file_out"], "LOG " + msg + "\n")
140 """Save all commands needed to reconstruct current world state."""
141 from server.utils import rand
143 def quote_escape(string):
144 string = string.replace("\u005C", '\u005C\u005C')
145 return '"' + string.replace('"', '\u005C"') + '"'
150 if key == "MAP" or world_db["Things"][id][key]:
151 map = world_db["MAP"] if key == "MAP" \
152 else world_db["Things"][id][key]
153 length = world_db["MAP_LENGTH"]
154 for i in range(length):
155 line = map[i * length:(i * length) + length].decode()
156 string = string + key + " " + str(i) + " " + \
157 quote_escape(line) + "\n"
163 for memthing in world_db["Things"][id]["T_MEMTHING"]:
164 string = string + "T_MEMTHING " + str(memthing[0]) + " " + \
165 str(memthing[1]) + " " + str(memthing[2]) + "\n"
168 def helper(category, id_string, special_keys={}):
170 for _id in sorted(world_db[category].keys()):
171 string = string + id_string + " " + str(_id) + "\n"
172 for key in sorted(world_db[category][_id].keys()):
173 if key.isupper() and not key in special_keys:
174 x = world_db[category][_id][key]
175 argument = quote_escape(x) if str == type(x) else str(x)
176 string = string + key + " " + argument + "\n"
181 memmap = mapsetter("T_MEMMAP")
182 memdepthmap = mapsetter("T_MEMDEPTHMAP")
183 for tid in sorted(world_db["Things"].keys()):
184 string += "T_ID " + str(tid) + "\n"
185 t = world_db["Things"][tid]
186 for key in sorted(t.keys()):
187 if key not in {"T_CARRIES", "carried", "fovmap", "T_MEMMAP",
188 "T_MEMTHING", "T_MEMDEPTHMAP", "pos"}:
190 string += key + " " + (quote_escape(argument) if \
191 str == type(argument) else str(argument)) + "\n"
192 string += memthing(tid) + memmap(tid) + memdepthmap(tid)
196 for plugin in world_db["PLUGIN"]:
197 string = string + "PLUGIN " + plugin + "\n"
198 for key in sorted(world_db.keys()):
199 if (not isinstance(world_db[key], dict) and
200 not isinstance(world_db[key], list)) and key != "MAP" and \
201 key != "WORLD_ACTIVE" and key[0].isupper():
202 string = string + key + " " + str(world_db[key]) + "\n"
203 string = string + mapsetter("MAP")()
204 string = string + helper("ThingActions", "TA_ID")
205 string = string + helper("ThingTypes", "TT_ID", {"TT_CORPSE_ID": False})
206 for id in sorted(world_db["ThingTypes"].keys()):
207 string = string + "TT_ID " + str(id) + "\n" + "TT_CORPSE_ID " + \
208 str(world_db["ThingTypes"][id]["TT_CORPSE_ID"]) + "\n"
209 string += helper_things()
210 for tid in sorted(world_db["Things"].keys()):
211 if [] != world_db["Things"][tid]["T_CARRIES"]:
212 string = string + "T_ID " + str(tid) + "\n"
213 for carried in sorted(world_db["Things"][tid]["T_CARRIES"]):
214 string = string + "T_CARRIES " + str(carried) + "\n"
215 string = string + io_db["hook_save"]()
216 string = string + "SEED_RANDOMNESS " + str(rand.seed) + "\n" + \
217 "WORLD_ACTIVE " + str(world_db["WORLD_ACTIVE"])
218 atomic_write(io_db["path_save"], string)
221 def obey(command, prefix, replay=False, do_record=False):
222 """Call function from commands_db mapped to command's first token.
224 Tokenize command string with shlex.split(comments=True). If replay is set,
225 a non-meta command from the commands_db merely triggers obey() on the next
226 command from the records file. If not, non-meta commands set
227 io_db["worldstate_updateable"] to world_db["WORLD_ACTIVE"], and, if
228 do_record is set, are recorded to io_db["record_chunk"], and save_world()
229 is called (and io_db["record_chunk"] written) if io_db["save_wait"] seconds
230 have passed since the last time it was called. The prefix string is
231 inserted into the server's input message between its beginning 'input ' and
232 ':'. All activity is preceded by a server_test() call. Commands that start
233 with a lowercase letter are ignored when world_db["WORLD_ACTIVE"] is
237 from server.config.commands import commands_db
240 print("input " + prefix + ": " + command)
242 tokens = shlex.split(command, comments=True)
243 except ValueError as err:
244 print("Can't tokenize command string: " + str(err) + ".")
246 if len(tokens) > 0 and tokens[0] in commands_db \
247 and len(tokens) == commands_db[tokens[0]][0] + 1:
248 if commands_db[tokens[0]][1]:
249 commands_db[tokens[0]][2](*tokens[1:])
250 elif tokens[0][0].islower() and not world_db["WORLD_ACTIVE"]:
251 print("Ignoring lowercase-starting commands when world inactive.")
253 print("Due to replay mode, reading command as 'go on in record'.")
254 line = io_db["file_record"].readline()
256 obey(line.rstrip(), io_db["file_record"].prefix
257 + str(io_db["file_record"].line_n))
258 io_db["file_record"].line_n = io_db["file_record"].line_n + 1
260 print("Reached end of record file.")
262 commands_db[tokens[0]][2](*tokens[1:])
264 io_db["record_chunk"] += command + "\n"
265 if time.time() > io_db["save_wait_start"] + io_db["save_wait"]:
266 atomic_write(io_db["path_record"], io_db["record_chunk"],
268 if world_db["WORLD_ACTIVE"]:
270 io_db["record_chunk"] = ""
271 io_db["save_wait_start"] = time.time()
272 io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"]
273 elif 0 != len(tokens):
274 print("Invalid command/argument, or bad number of tokens: " + command)
277 def obey_lines_in_file(path, name, do_record=False):
278 """Call obey() on each line of path's file, use name in input prefix."""
279 file = open(path, "r")
281 for line in file.readlines():
282 obey(line.rstrip(), name + "file line " + str(line_n),
288 def try_worldstate_update():
289 """Write worldstate file if io_db["worldstate_updateable"] is set."""
290 if world_db["WORLD_ACTIVE"] and io_db["worldstate_updateable"]:
292 for entry in io_db["worldstate_write_order"]:
293 if entry[1] == "world_int":
294 string += str(world_db[entry[0]]) + "\n"
295 elif entry[1] == "player_int":
296 string += str(world_db["Things"][0][entry[0]]) + "\n"
297 elif entry[1] == "func":
299 atomic_write(io_db["path_worldstate"], string, delete=False)
300 strong_write(io_db["file_out"], "WORLD_UPDATED\n")
301 io_db["worldstate_updateable"] = False