X-Git-Url: https://plomlompom.com/repos/?a=blobdiff_plain;f=plomrogue-server.py;h=772277c8b3916de15234d838004589a9dff210de;hb=7bedc1d4ffc9c3b623c4195c484539ba7adb2dfd;hp=a7cee78262f7b3ac5515c926206087d6dc93aaaa;hpb=8bf4e35ebe159f9cf051c8dbfc0909d7fe5d3399;p=plomrogue diff --git a/plomrogue-server.py b/plomrogue-server.py index a7cee78..772277c 100755 --- a/plomrogue-server.py +++ b/plomrogue-server.py @@ -1,3 +1,10 @@ +#!/usr/bin/python3 + +# This file is part of PlomRogue. PlomRogue is licensed under the GPL version 3 +# or any later version. For details on its copyright, license, and warranties, +# see the file NOTICE in the root directory of the PlomRogue source package. + + import argparse import errno import os @@ -26,40 +33,17 @@ def prep_library(): """Prepare ctypes library at ./libplomrogue.so""" libpath = ("./libplomrogue.so") if not os.access(libpath, os.F_OK): - raise SystemExit("No library " + libpath + ", run ./compile.sh first?") + raise SystemExit("No library " + libpath + + ", run ./compile-server.sh first?") libpr = ctypes.cdll.LoadLibrary(libpath) - libpr.seed_rrand.argtypes = [ctypes.c_uint8, ctypes.c_uint32] libpr.seed_rrand.restype = ctypes.c_uint32 - libpr.rrand.argtypes = [] - libpr.rrand.restype = ctypes.c_uint16 - libpr.set_maplength.argtypes = [ctypes.c_uint16] - libpr.mv_yx_in_dir_legal_wrap.argtypes = [ctypes.c_char, ctypes.c_uint8, - ctypes.c_uint8] - libpr.mv_yx_in_dir_legal_wrap.restype = ctypes.c_uint8 - libpr.result_y.restype = ctypes.c_uint8 - libpr.result_x.restype = ctypes.c_uint8 - libpr.set_maplength(world_db["MAP_LENGTH"]) - libpr.build_fov_map.argtypes = [ctypes.c_uint8, ctypes.c_uint8, - ctypes.c_char_p, ctypes.c_char_p] - libpr.build_fov_map.restype = ctypes.c_uint8 - libpr.init_score_map.restype = ctypes.c_uint8 - libpr.set_map_score.argtypes = [ctypes.c_uint16, ctypes.c_uint16] - libpr.set_map_score.restype = ctypes.c_uint8 - libpr.get_map_score.argtypes = [ctypes.c_uint16] - libpr.get_map_score.restype = ctypes.c_int32 - libpr.get_neighbor_score.argtypes = [ctypes.c_uint8] - libpr.get_neighbor_score.restype = ctypes.c_uint16 - libpr.ready_neighbor_scores.argtpes = [ctypes.c_uint16] - libpr.ready_neighbor_scores.restype = ctypes.c_uint8 - libpr.dijkstra_map.restype = ctypes.c_uint8 return libpr def strong_write(file, string): - """Apply write(string), flush(), and os.fsync() to file.""" + """Apply write(string), then flush().""" file.write(string) file.flush() - os.fsync(file) def setup_server_io(): @@ -79,6 +63,9 @@ def setup_server_io(): if os.access(path_tmp, os.F_OK): raise SystemExit(msg) io_db["teststring"] = str(os.getpid()) + " " + str(time.time()) + io_db["save_wait"] = 0 + io_db["verbose"] = False + io_db["record_chunk"] = "" os.makedirs(io_db["path_server"], exist_ok=True) io_db["file_out"] = open(io_db["path_out"], "w") strong_write(io_db["file_out"], io_db["teststring"] + "\n") @@ -96,11 +83,11 @@ def cleanup_server_io(): def helper(file_key, path_key): if file_key in io_db: io_db[file_key].close() - if not io_db["kicked_by_rival"] \ - and os.access(io_db[path_key], os.F_OK): - os.remove(io_db[path_key]) - helper("file_out", "path_out") + if not io_db["kicked_by_rival"] \ + and os.access(io_db[path_key], os.F_OK): + os.remove(io_db[path_key]) helper("file_in", "path_in") + helper("file_out", "path_out") helper("file_worldstate", "path_worldstate") if "file_record" in io_db: io_db["file_record"].close() @@ -113,12 +100,15 @@ def obey(command, prefix, replay=False, do_record=False): a non-meta command from the commands_db merely triggers obey() on the next command from the records file. If not, non-meta commands set io_db["worldstate_updateable"] to world_db["WORLD_EXISTS"], and, if - do_record is set, are recorded via record(), and save_world() is called. - The prefix string is inserted into the server's input message between its - beginning 'input ' & ':'. All activity is preceded by a server_test() call. + do_record is set, are recorded to io_db["record_chunk"], and save_world() + is called (and io_db["record_chunk"] written) if 15 seconds have passed + since the last time it was called. The prefix string is inserted into the + server's input message between its beginning 'input ' and ':'. All activity + is preceded by a server_test() call. """ server_test() - print("input " + prefix + ": " + command) + if io_db["verbose"]: + print("input " + prefix + ": " + command) try: tokens = shlex.split(command, comments=True) except ValueError as err: @@ -140,8 +130,13 @@ def obey(command, prefix, replay=False, do_record=False): else: commands_db[tokens[0]][2](*tokens[1:]) if do_record: - record(command) - save_world() + io_db["record_chunk"] += command + "\n" + if time.time() > io_db["save_wait"] + 15: + atomic_write(io_db["path_record"], io_db["record_chunk"], + do_append=True) + save_world() + io_db["record_chunk"] = "" + io_db["save_wait"] = time.time() io_db["worldstate_updateable"] = world_db["WORLD_ACTIVE"] elif 0 != len(tokens): print("Invalid command/argument, or bad number of tokens.") @@ -163,17 +158,8 @@ def atomic_write(path, text, do_append=False): os.rename(path_tmp, path) -def record(command): - """Append command string plus newline to record file. (Atomic.)""" - # This misses some optimizations from the original record(), namely only - # finishing the atomic write with expensive flush() and fsync() every 15 - # seconds unless explicitely forced. Implement as needed. - atomic_write(io_db["path_record"], command + "\n", do_append=True) - - def save_world(): """Save all commands needed to reconstruct current world state.""" - # TODO: Misses same optimizations as record() from the original record(). def quote(string): string = string.replace("\u005C", '\u005C\u005C') @@ -254,6 +240,9 @@ def parse_command_line_arguments(): parser = argparse.ArgumentParser() parser.add_argument('-s', nargs='?', type=int, dest='replay', const=1, action='store') + parser.add_argument('-l', nargs="?", const="save", dest='savefile', + action="store") + parser.add_argument('-v', dest='verbose', action='store_true') opts, unknown = parser.parse_known_args() return opts @@ -481,20 +470,24 @@ def update_map_memory(t, age_map=True): t["T_MEMMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2)) if not t["T_MEMDEPTHMAP"]: t["T_MEMDEPTHMAP"] = bytearray(b' ' * (world_db["MAP_LENGTH"] ** 2)) - for pos in range(world_db["MAP_LENGTH"] ** 2): - if "v" == chr(t["fovmap"][pos]): - t["T_MEMDEPTHMAP"][pos] = ord("0") - if " " == chr(t["T_MEMMAP"][pos]): - t["T_MEMMAP"][pos] = world_db["MAP"][pos] - continue - if age_map and ord('0') <= t["T_MEMDEPTHMAP"][pos] \ - and ord('9') > t["T_MEMDEPTHMAP"][pos] \ - and not rand.next() % (2 ** (t["T_MEMDEPTHMAP"][pos] - 48)): - t["T_MEMDEPTHMAP"][pos] += 1 + ord_v = ord("v") + ord_0 = ord("0") + ord_9 = ord("9") + ord_space = ord(" ") + for pos in [pos for pos in range(world_db["MAP_LENGTH"] ** 2) + if ord_v == t["fovmap"][pos]]: + t["T_MEMDEPTHMAP"][pos] = ord_0 + if ord_space == t["T_MEMMAP"][pos]: + t["T_MEMMAP"][pos] = world_db["MAP"][pos] + if age_map: + maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"]) + memdepthmap = maptype.from_buffer(t["T_MEMDEPTHMAP"]) + fovmap = maptype.from_buffer(t["fovmap"]) + libpr.age_some_memdepthmap_on_nonfov_cells(memdepthmap, fovmap) for mt in [mt for mt in t["T_MEMTHING"] if "v" == chr(t["fovmap"][(mt[1] * world_db["MAP_LENGTH"]) + mt[2]])]: - t["T_MEMTHING"].remove(mt) + t["T_MEMTHING"].remove(mt) for id in [id for id in world_db["Things"] if not world_db["Things"][id]["carried"]]: type = world_db["Things"][id]["T_TYPE"] @@ -513,20 +506,23 @@ def set_world_inactive(): world_db["WORLD_ACTIVE"] = 0 -def integer_test(val_string, min, max): - """Return val_string if possible integer >= min and <= max, else None.""" +def integer_test(val_string, min, max=None): + """Return val_string if integer >= min & (if max set) <= max, else None.""" try: val = int(val_string) - if val < min or val > max: + if val < min or (max != None and val > max): raise ValueError return val except ValueError: - print("Ignoring: Please use integer >= " + str(min) + " and <= " + - str(max) + ".") + msg = "Ignoring: Please use integer >= " + str(min) + if max != None: + msg += " and <= " + str(max) + msg += "." + print(msg) return None -def setter(category, key, min, max): +def setter(category, key, min, max=None): """Build setter for world_db([category + "s"][id])[key] to >=min/<=max.""" if category is None: def f(val_string): @@ -785,6 +781,14 @@ def get_dir_to_target(t, filter): "s": memory map cell with greatest-reachable degree of unexploredness """ + def zero_score_map_where_char_on_memdepthmap(c): + maptype = ctypes.c_char * len(t["T_MEMDEPTHMAP"]) + map = maptype.from_buffer(t["T_MEMDEPTHMAP"]) + test = libpr.zero_score_map_where_char_on_memdepthmap(c, map) + if test: + raise RuntimeError("No score map allocated for " + "zero_score_map_where_char_on_memdepthmap().") + def set_map_score(pos, score): test = libpr.set_map_score(pos, score) if test: @@ -823,18 +827,22 @@ def get_dir_to_target(t, filter): test = libpr.init_score_map() if test: raise RuntimeError("Malloc error in init_score_map().") + ord_dot = ord(".") + ord_v = ord("v") + ord_blank = ord(" ") for i in [i for i in range(world_db["MAP_LENGTH"] ** 2) - if '.' == chr(t["T_MEMMAP"][i])]: + if ord_dot == t["T_MEMMAP"][i]]: set_map_score(i, 65535 - 1) if "a" == filter: for id in world_db["Things"]: Thing = world_db["Things"][id] - pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] + Thing["T_POSX"] + pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \ + + Thing["T_POSX"] if t != Thing and Thing["T_LIFEPOINTS"] and \ t["T_TYPE"] != Thing["T_TYPE"] and \ - 'v' == chr(t["fovmap"][pos]) and \ + ord_v == t["fovmap"][pos] and \ t["T_LIFEPOINTS"] > \ - world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]: + world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]: set_map_score(pos, 0) elif t["T_TYPE"] == Thing["T_TYPE"]: set_map_score(pos, 65535) @@ -842,24 +850,23 @@ def get_dir_to_target(t, filter): for id in [id for id in world_db["Things"] if world_db["Things"][id]["T_LIFEPOINTS"]]: Thing = world_db["Things"][id] - pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] + Thing["T_POSX"] + pos = Thing["T_POSY"] * world_db["MAP_LENGTH"] \ + + Thing["T_POSX"] if t["T_TYPE"] != Thing["T_TYPE"] and \ - 'v' == chr(t["fovmap"][pos]) and \ + ord_v == t["fovmap"][pos] and \ t["T_LIFEPOINTS"] <= \ - world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]: + world_db["ThingTypes"][Thing["T_TYPE"]]["TT_LIFEPOINTS"]: set_map_score(pos, 0) elif "c" == filter: for mt in [mt for mt in t["T_MEMTHING"] - if ' ' != chr(t["T_MEMMAP"][mt[1] - * world_db["MAP_LENGTH"] - + mt[2]]) + if ord_blank != t["T_MEMMAP"][mt[1] + * world_db["MAP_LENGTH"] + + mt[2]] if world_db["ThingTypes"][mt[0]]["TT_CONSUMABLE"]]: set_map_score(mt[1] * world_db["MAP_LENGTH"] + mt[2], 0) elif "s" == filter: - for i in [i for i in range(world_db["MAP_LENGTH"] ** 2) - if t["T_MEMDEPTHMAP"][i] == mem_depth_c[0]]: - set_map_score(i, 0) - + zero_score_map_where_char_on_memdepthmap(mem_depth_c[0]) + def rand_target_dir(neighbors, cmp, dirs): candidates = [] n_candidates = 0 @@ -911,9 +918,10 @@ def get_dir_to_target(t, filter): dir_to_target = rand_target_dir(neighbors, 0, dirs) elif 3 >= get_map_score(eye_pos): t["T_COMMAND"] = [id for id in world_db["ThingActions"] - if world_db["ThingActions"][id]["TA_NAME"] + if + world_db["ThingActions"][id]["TA_NAME"] == "wait"][0] - return 1; + return 1 elif dir_to_target and 3 < get_map_score(eye_pos): dir_to_target = 0 elif "a" == filter and 10 <= get_map_score(eye_pos): @@ -999,10 +1007,10 @@ def turn_over(): id = 0 whilebreaker = False while world_db["Things"][0]["T_LIFEPOINTS"]: - for id in [id for id in world_db["Things"]]: # Only what is from start! + for id in [id for id in world_db["Things"]]: # Only what's from start! if not id in world_db["Things"] or \ - world_db["Things"][id]["carried"]:# Thing may have been consumed - continue # or picked up during turn … + world_db["Things"][id]["carried"]: # May have been consumed or + continue # picked up during turn … Thing = world_db["Things"][id] if Thing["T_LIFEPOINTS"]: if not Thing["T_COMMAND"]: @@ -1052,17 +1060,14 @@ def new_Thing(type, pos=(0, 0)): def id_setter(id, category, id_store=False, start_at_1=False): """Set ID of object of category to manipulate ID unused? Create new one. - The ID is stored as id_store.id (if id_store is set). If the integer of the - input is valid (if start_at_1, >= 0 and <= 255, else >= -32768 and <= - 32767), but <0 or (if start_at_1) <1, calculate new ID: lowest unused ID - >=0 or (if start_at_1) >= 1, and <= 255. None is always returned when no - new object is created, otherwise the new object's ID. + input is valid (if start_at_1, >= 0, else >= -1), but <0 or (if start_at_1) + <1, calculate new ID: lowest unused ID >=0 or (if start_at_1) >= 1. None is + always returned when no new object is created, else the new object's ID. """ - min = 0 if start_at_1 else -32768 - max = 255 if start_at_1 else 32767 + min = 0 if start_at_1 else -1 if str == type(id): - id = integer_test(id, min, max) + id = integer_test(id, min) if None != id: if id in world_db[category]: if id_store: @@ -1070,15 +1075,12 @@ def id_setter(id, category, id_store=False, start_at_1=False): return None else: if (start_at_1 and 0 == id) \ - or ((not start_at_1) and (id < 0 or id > 255)): - id = -1 + or ((not start_at_1) and (id < 0)): + id = 0 if start_at_1 else -1 while 1: id = id + 1 if id not in world_db[category]: break - if id > 255: - print("Ignoring: " - "No unused ID available to add to ID list.") return None if id_store: id_store.id = id @@ -1092,6 +1094,8 @@ def command_ping(): def command_quit(): """Abort server process.""" + save_world() + atomic_write(io_db["path_record"], io_db["record_chunk"], do_append=True) raise SystemExit("received QUIT command") @@ -1321,7 +1325,7 @@ test_Thing_id = test_for_id_maker(command_tid, "Thing") @test_Thing_id def command_tcommand(str_int): """Set T_COMMAND of selected Thing.""" - val = integer_test(str_int, 0, 255) + val = integer_test(str_int, 0) if None != val: if 0 == val or val in world_db["ThingActions"]: world_db["Things"][command_tid.id]["T_COMMAND"] = val @@ -1332,7 +1336,7 @@ def command_tcommand(str_int): @test_Thing_id def command_ttype(str_int): """Set T_TYPE of selected Thing.""" - val = integer_test(str_int, 0, 255) + val = integer_test(str_int, 0) if None != val: if val in world_db["ThingTypes"]: world_db["Things"][command_tid.id]["T_TYPE"] = val @@ -1347,7 +1351,7 @@ def command_tcarries(str_int): The ID int(str_int) must not be of the selected Thing, and must belong to a Thing with unset "carried" flag. Its "carried" flag will be set on owning. """ - val = integer_test(str_int, 0, 255) + val = integer_test(str_int, 0) if None != val: if val == command_tid.id: print("Ignoring: Thing cannot carry itself.") @@ -1368,7 +1372,7 @@ def command_tmemthing(str_t, str_y, str_x): The type must fit to an existing ThingType, and the position into the map. """ - type = integer_test(str_t, 0, 255) + type = integer_test(str_t, 0) posy = integer_test(str_y, 0, 255) posx = integer_test(str_x, 0, 255) if None != type and None != posy and None != posx: @@ -1465,7 +1469,7 @@ def command_ttsymbol(char): @test_ThingType_id def command_ttcorpseid(str_int): """Set TT_CORPSE_ID of selected ThingType.""" - val = integer_test(str_int, 0, 255) + val = integer_test(str_int, 0) if None != val: if val in world_db["ThingTypes"]: world_db["ThingTypes"][command_ttid.id]["TT_CORPSE_ID"] = val @@ -1533,7 +1537,7 @@ commands_db = { "SEED_MAP": (1, False, command_seedmap), "SEED_RANDOMNESS": (1, False, command_seedrandomness), "TURN": (1, False, setter(None, "TURN", 0, 65535)), - "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0, 255)), + "PLAYER_TYPE": (1, False, setter(None, "PLAYER_TYPE", 0)), "MAP_LENGTH": (1, False, command_maplength), "WORLD_ACTIVE": (1, False, command_worldactive), "TA_ID": (1, False, command_taid), @@ -1591,7 +1595,7 @@ directions_db = {"east": "d", "south-east": "c", "south-west": "x", """File IO database.""" io_db = { "path_save": "save", - "path_record": "record", + "path_record": "record_save", "path_worldconf": "confserver/world", "path_server": "server/", "path_in": "server/in", @@ -1607,7 +1611,12 @@ try: libpr = prep_library() rand = RandomnessIO() opts = parse_command_line_arguments() + if opts.savefile: + io_db["path_save"] = opts.savefile + io_db["path_record"] = "record_" + opts.savefile setup_server_io() + if opts.verbose: + io_db["verbose"] = True if None != opts.replay: replay_game() else: