home · contact · privacy
Control login limit from optional file to be changed during server run.
[plomrogue2] / plomrogue / game.py
index d7be311f3d54dc02535fa1471c510d2e8db03263..b76860aa5ff7954c802c63334d22367a9acb7ffa 100755 (executable)
@@ -1,10 +1,9 @@
-from plomrogue.tasks import (Task_WAIT, Task_MOVE, Task_WRITE,
-                             Task_FLATTEN_SURROUNDINGS)
 from plomrogue.errors import GameError, PlayError
 from plomrogue.io import GameIO
 from plomrogue.misc import quote
 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex, Map
 import string
 from plomrogue.errors import GameError, PlayError
 from plomrogue.io import GameIO
 from plomrogue.misc import quote
 from plomrogue.mapping import YX, MapGeometrySquare, MapGeometryHex, Map
 import string
+import datetime
 
 
 
 
 
 
@@ -13,7 +12,7 @@ class GameBase:
     def __init__(self):
         self.turn = 0
         self.things = []
     def __init__(self):
         self.turn = 0
         self.things = []
-        self.map_geometry = MapGeometrySquare(YX(24, 40))
+        self.map_geometry = MapGeometrySquare(YX(32, 32))
         self.commands = {}
 
     def get_thing(self, id_):
         self.commands = {}
 
     def get_thing(self, id_):
@@ -51,7 +50,14 @@ class SaveableMap(Map):
             return False
         return True
 
             return False
         return True
 
-    def draw_presets(self, alternate_hex=0):
+    def draw_presets(self, big_yx, type_):
+        if type_ == 1:
+            if big_yx.y < 0:
+                self.terrain = 'X' * self.size_i
+        elif type_ == 2:
+            self.draw_presets_grid(big_yx)
+
+    def draw_presets_grid(self, big_yx):
         old_modified = self.modified
         if type(self.geometry) == MapGeometrySquare:
             self.set_line(0, 'X' * self.geometry.size.x)
         old_modified = self.modified
         if type(self.geometry) == MapGeometrySquare:
             self.set_line(0, 'X' * self.geometry.size.x)
@@ -60,15 +66,15 @@ class SaveableMap(Map):
             self.set_line(3, 'X' * self.geometry.size.x)
             self.set_line(4, 'X' * self.geometry.size.x)
             for y in range(self.geometry.size.y):
             self.set_line(3, 'X' * self.geometry.size.x)
             self.set_line(4, 'X' * self.geometry.size.x)
             for y in range(self.geometry.size.y):
-                self[YX(y,0)] = 'X'
-                self[YX(y,1)] = 'X'
-                self[YX(y,2)] = 'X'
-                self[YX(y,3)] = 'X'
-                self[YX(y,4)] = 'X'
+                self[YX(y, 0)] = 'X'
+                self[YX(y, 1)] = 'X'
+                self[YX(y, 2)] = 'X'
+                self[YX(y, 3)] = 'X'
+                self[YX(y, 4)] = 'X'
         elif type(self.geometry) == MapGeometryHex:
             # TODO: for this to work we need a map side length divisible by 6.
 
         elif type(self.geometry) == MapGeometryHex:
             # TODO: for this to work we need a map side length divisible by 6.
 
-            def draw_grid(offset=YX(0,0)):
+            def draw_grid(offset=YX(0, 0)):
                 dirs = ('DOWNRIGHT', 'RIGHT', 'UPRIGHT', 'RIGHT')
 
                 def draw_snake(start):
                 dirs = ('DOWNRIGHT', 'RIGHT', 'UPRIGHT', 'RIGHT')
 
                 def draw_snake(start):
@@ -89,22 +95,26 @@ class SaveableMap(Map):
                                 if self.inside(yx):
                                     self[yx] = 'X'
 
                                 if self.inside(yx):
                                     self[yx] = 'X'
 
+                alternate_hex = big_yx.y % 2
                 if alternate_hex:
                     draw_snake(offset + YX(0, 0))
                 if alternate_hex:
                     draw_snake(offset + YX(0, 0))
-                draw_snake(offset + YX((0 + alternate_hex) * distance, -int(1.5*distance)))
-                draw_snake(offset + YX((1 + alternate_hex) * distance, 0))
-                draw_snake(offset + YX((2 + alternate_hex) * distance, -int(1.5*distance)))
+                draw_snake(offset + YX((0 + alternate_hex) * distance,
+                           -int(1.5 * distance)))
+                draw_snake(offset + YX((1 + alternate_hex) * distance,
+                           0))
+                draw_snake(offset + YX((2 + alternate_hex) * distance,
+                           -int(1.5 * distance)))
 
             distance = self.geometry.size.y // 3
             draw_grid()
 
             distance = self.geometry.size.y // 3
             draw_grid()
-            draw_grid(YX(2,0))
-            draw_grid(YX(0,2))
-            draw_grid(YX(1,0))
-            draw_grid(YX(0,1))
-            draw_grid(YX(-1,0))
-            draw_grid(YX(0,-1))
-            draw_grid(YX(-2,0))
-            draw_grid(YX(0,-2))
+            draw_grid(YX(2, 0))
+            draw_grid(YX(0, 2))
+            draw_grid(YX(1, 0))
+            draw_grid(YX(0, 1))
+            draw_grid(YX(-1, 0))
+            draw_grid(YX(0, -1))
+            draw_grid(YX(-2, 0))
+            draw_grid(YX(0, -2))
         self.modified = old_modified
 
 
         self.modified = old_modified
 
 
@@ -113,31 +123,39 @@ import os
 class Game(GameBase):
 
     def __init__(self, save_file, *args, **kwargs):
 class Game(GameBase):
 
     def __init__(self, save_file, *args, **kwargs):
+        from plomrogue.misc import Terrain
         super().__init__(*args, **kwargs)
         self.changed = True
         super().__init__(*args, **kwargs)
         self.changed = True
+        self.changed_tiles = {'fov': [], 'other': []}
         self.io = GameIO(self, save_file)
         self.io = GameIO(self, save_file)
+        self.login_requests = []
         self.tasks = {}
         self.thing_types = {}
         self.sessions = {}
         self.tasks = {}
         self.thing_types = {}
         self.sessions = {}
+        self.faces = {}
+        self.hats = {}
         self.maps = {}
         self.map_controls = {}
         self.map_control_passwords = {}
         self.annotations = {}
         self.maps = {}
         self.map_controls = {}
         self.map_control_passwords = {}
         self.annotations = {}
+        self.spawn_points = []
         self.portals = {}
         self.portals = {}
+        self.intro_messages = []
         self.player_chars = string.digits + string.ascii_letters
         self.player_chars = string.digits + string.ascii_letters
+        self.players_hat_chars = {}
         self.player_char_i = -1
         self.admin_passwords = []
         self.player_char_i = -1
         self.admin_passwords = []
+        self.send_gamestate_min_interval = datetime.timedelta(seconds=0.04)
+        self.last_send_gamestate = datetime.datetime.now() -\
+            self.send_gamestate_min_interval
         self.terrains = {
         self.terrains = {
-            '.': 'floor',
-            'X': 'wall',
-            '=': 'window',
-            '#': 'bed',
-            'T': 'desk',
-            '8': 'cupboard',
-            '[': 'glass door',
-            'o': 'sink',
-            'O': 'toilet'
+            '.': Terrain('.', 'floor'),
+            'X': Terrain('X', 'wall', blocks_light=True, blocks_sound=True,
+                         blocks_movement=True),
+            '=': Terrain('=', 'glass', blocks_sound=True, blocks_movement=True),
+            'T': Terrain('T', 'table', blocks_movement=True),
         }
         }
+        self.draw_control_presets = 1
         if os.path.exists(self.io.save_file):
             if not os.path.isfile(self.io.save_file):
                 raise GameError('save file path refers to non-file')
         if os.path.exists(self.io.save_file):
             if not os.path.isfile(self.io.save_file):
                 raise GameError('save file path refers to non-file')
@@ -157,6 +175,12 @@ class Game(GameBase):
                 print("FILE INPUT LINE %5s: %s" % (i, line), end='')
                 self.io.handle_input(line, god_mode=True)
 
                 print("FILE INPUT LINE %5s: %s" % (i, line), end='')
                 self.io.handle_input(line, god_mode=True)
 
+    def can_do_thing_with_pw(self, thing, pw):
+        if thing.protection in self.map_control_passwords.keys():
+            if pw != self.map_control_passwords[thing.protection]:
+                return False
+        return True
+
     def can_do_tile_with_pw(self, big_yx, little_yx, pw):
         map_control = self.get_map(big_yx, 'control')
         tile_class = map_control[little_yx]
     def can_do_tile_with_pw(self, big_yx, little_yx, pw):
         map_control = self.get_map(big_yx, 'control')
         tile_class = map_control[little_yx]
@@ -168,7 +192,9 @@ class Game(GameBase):
 
     def get_string_options(self, string_option_type):
         if string_option_type == 'direction':
 
     def get_string_options(self, string_option_type):
         if string_option_type == 'direction':
-            return self.map_geometry.get_directions()
+            return self.map_geometry.directions
+        elif string_option_type == 'direction+here':
+            return ['HERE'] + self.map_geometry.directions
         elif string_option_type == 'char':
             return [c for c in
                     string.digits + string.ascii_letters + string.punctuation + ' ']
         elif string_option_type == 'char':
             return [c for c in
                     string.digits + string.ascii_letters + string.punctuation + ' ']
@@ -178,51 +204,191 @@ class Game(GameBase):
             return self.thing_types.keys()
         return None
 
             return self.thing_types.keys()
         return None
 
+    def get_default_spawn_point(self):
+        import random
+        if len(self.spawn_points) == 0:
+            return (YX(0, 0), YX(0, 0))
+        return random.choice(self.spawn_points)
+
     def get_map_geometry_shape(self):
         return self.map_geometry.__class__.__name__[len('MapGeometry'):]
 
     def get_player(self, connection_id):
     def get_map_geometry_shape(self):
         return self.map_geometry.__class__.__name__[len('MapGeometry'):]
 
     def get_player(self, connection_id):
-        if not connection_id in self.sessions:
+        if connection_id not in self.sessions:
             return None
         player = self.get_thing(self.sessions[connection_id]['thing_id'])
         return player
 
             return None
         player = self.get_thing(self.sessions[connection_id]['thing_id'])
         return player
 
+    def get_face(self, t):
+        if t.type_ == 'Player':
+            if t.name in self.faces:
+                return self.faces[t.name]
+            else:
+                return '/O  O\\' + '| oo |' + '\\>--</'
+        return None
+
+    def remove_thing(self, t):
+        if t.carrying:
+            t.uncarry()
+        self.things.remove(t)
+        self.record_change(t.position, 'other')
+        if t.blocks_light:
+            self.record_change(t.position, 'fov')
+
+    def add_thing(self, type_, position, id_=0):
+        t_old = None
+        if id_ > 0:
+            t_old = self.get_thing(id_)
+        t = self.thing_types[type_](self, id_=id_, position=position)
+        if t_old:
+            self.things[self.things.index(t_old)] = t
+        else:
+            self.things += [t]
+        self.record_change(t.position, 'other')
+        if t.blocks_light:
+            self.record_change(t.position, 'fov')
+        return t
+
     def send_gamestate(self, connection_id=None):
         """Send out game state data relevant to clients."""
 
     def send_gamestate(self, connection_id=None):
         """Send out game state data relevant to clients."""
 
-        self.io.send('TURN ' + str(self.turn))
-        for c_id in self.sessions:
+        # TODO: limit to connection_id if provided
+        from plomrogue.mapping import FovMap
+        import multiprocessing
+        if connection_id:
+            c_ids = [connection_id]
+        else:
+            c_ids = [c_id for c_id in self.sessions]
+        # Only recalc FOVs for players with ._fov = None
+        player_fovs = []
+        player_ids_send_fov = []
+        player_ids_send_other = []
+        for c_id in c_ids:
             player = self.get_player(c_id)
             player = self.get_player(c_id)
-            visible_terrain = player.fov_stencil_map()
-            self.io.send('FOV %s' % quote(player.fov_stencil.terrain), c_id)
-            self.io.send('MAP %s %s %s' % (self.get_map_geometry_shape(),
-                                           player.fov_stencil.geometry.size,
-                                           quote(visible_terrain)), c_id)
-            visible_control = player.fov_stencil_map('control')
-            self.io.send('MAP_CONTROL %s' % quote(visible_control), c_id)
-            for t in [t for t in self.things if player.fov_test(*t.position)]:
-                target_yx = player.fov_stencil.target_yx(*t.position)
-                self.io.send('THING %s %s %s' % (target_yx, t.type_, t.id_), c_id)
-                if hasattr(t, 'name'):
-                    self.io.send('THING_NAME %s %s' % (t.id_, quote(t.name)), c_id)
-                if hasattr(t, 'player_char'):
-                    self.io.send('THING_CHAR %s %s' % (t.id_,
-                                                       quote(t.player_char)), c_id)
-            for big_yx in self.portals:
-                for little_yx in [little_yx for little_yx in self.portals[big_yx]
-                                  if player.fov_test(big_yx, little_yx)]:
-                    target_yx = player.fov_stencil.target_yx(big_yx, little_yx)
-                    portal = self.portals[big_yx][little_yx]
+            if not player._fov:
+                player.prepare_multiprocessible_fov_stencil()
+                player_fovs += [player._fov]
+                player_ids_send_fov += [player.id_]
+            if None in (player._seen_things,
+                        player._seen_annotation_positions,
+                        player._seen_portal_positions):
+                player_ids_send_other += [player.id_]
+        new_fovs = []
+        single_core_until = 16  # since multiprocess has its own overhead
+        if len(player_fovs) > single_core_until:
+            pool = multiprocessing.Pool()
+            new_fovs = pool.map(FovMap.init_terrain, [fov for fov in player_fovs])
+            pool.close()
+            pool.join()
+        elif len(player_fovs) <= single_core_until:
+            for fov in player_fovs:
+                new_fovs += [fov.init_terrain()]
+        for i in range(len(player_ids_send_fov)):
+            id_ = player_ids_send_fov[i]
+            player = self.get_thing(id_)
+            player._fov = new_fovs[i]
+        for c_id in c_ids:
+            self.io.send('TURN ' + str(self.turn), c_id)
+            player = self.get_player(c_id)
+            self.io.send('PLAYERS_HAT_CHARS ' + quote(player.get_cookie_chars()),
+                         c_id)
+            self.io.send('STATS %s %s' % (player.need_for_toilet,
+                                          player.energy), c_id)
+            if player.id_ in player_ids_send_fov:
+                self.io.send('FOV %s' % quote(player.fov_stencil.terrain), c_id)
+                self.io.send('MAP %s %s %s' % (self.get_map_geometry_shape(),
+                                               player.fov_stencil.geometry.size,
+                                               quote(player.visible_terrain)), c_id)
+                self.io.send('MAP_CONTROL %s' % quote(player.visible_control), c_id)
+            if player.id_ in player_ids_send_other:
+                self.io.send('OTHER_WIPE', c_id)
+                for t in player.seen_things:
+                    target_yx = player.fov_stencil.target_yx(*t.position)
+                    self.io.send('THING %s %s %s %s %s %s'
+                                 % (target_yx, t.type_, quote(t.protection), t.id_,
+                                    int(t.portable), int(t.commandable)),
+                                 c_id)
+                    if hasattr(t, 'name'):
+                        self.io.send('THING_NAME %s %s' % (t.id_, quote(t.name)), c_id)
+                        if t.type_ == 'Player' and t.name in self.hats:
+                            hat = self.hats[t.name]
+                            self.io.send('THING_HAT %s %s' % (t.id_, quote(hat)), c_id)
+                    face = self.get_face(t)
+                    if face:
+                        self.io.send('THING_FACE %s %s' % (t.id_, quote(face)), c_id)
+                    if hasattr(t, 'thing_char'):
+                        self.io.send('THING_CHAR %s %s' % (t.id_,
+                                                           quote(t.thing_char)), c_id)
+                    if hasattr(t, 'installable') and not t.portable:
+                        self.io.send('THING_INSTALLED %s' % (t.id_), c_id)
+                    if hasattr(t, 'design'):
+                        self.io.send('THING_DESIGN %s %s %s'
+                                     % (t.id_, t.design_size, quote(t.design)),
+                                     c_id)
+                for t in [t for t in player.seen_things if t.carrying]:
+                    # send this last so all carryable things are already created
+                    self.io.send('THING_CARRYING %s %s' % (t.id_, t.carrying.id_),
+                                 c_id)
+                for position in player.seen_portal_positions:
+                    target_yx = player.fov_stencil.target_yx(position[0],
+                                                             position[1])
+                    portal = self.portals[position[0]][position[1]]
                     self.io.send('PORTAL %s %s' % (target_yx, quote(portal)), c_id)
                     self.io.send('PORTAL %s %s' % (target_yx, quote(portal)), c_id)
-            for big_yx in self.annotations:
-                for little_yx in [little_yx for little_yx in self.annotations[big_yx]
-                                  if player.fov_test(big_yx, little_yx)]:
-                    target_yx = player.fov_stencil.target_yx(big_yx, little_yx)
-                    annotation = self.annotations[big_yx][little_yx]
-                    self.io.send('ANNOTATION_HINT %s' % (target_yx,), c_id)
-        self.io.send('GAME_STATE_COMPLETE')
+                for position in player.seen_annotation_positions:
+                    target_yx = player.fov_stencil.target_yx(position[0],
+                                                             position[1])
+                    annotation = self.annotations[position[0]][position[1]]
+                    self.io.send('ANNOTATION %s %s' % (target_yx,
+                                                       quote(annotation)), c_id)
+            self.io.send('GAME_STATE_COMPLETE', c_id)
+
+    def record_change(self, position, type_):
+        big_yx, little_yx = position
+        self.changed_tiles[type_] += [self.map_geometry.undouble_yxyx(big_yx,
+                                                                      little_yx)]
+        self.changed = True
+
+    def login(self, nick, connection_id):
+        login_limit_filename = 'login_limit'
+        if os.path.exists(login_limit_filename):
+            with open(login_limit_filename, 'r') as f:
+                lines = f.readlines()
+                login_limit = int(lines[0])
+                if len(self.sessions) > login_limit - 1:
+                    print('DEBUG LOGIN TOO MANY FOR', nick, connection_id)
+                    self.io.send('CHAT "sorry, too many users currently '
+                                 'logged in, try again later '
+                                 'by re-entering your name"', connection_id)
+                    return
+        for t in [t for t in self.things
+                  if t.type_ == 'Player' and t.name == nick]:
+            self.io.send('GAME_ERROR ' + quote('name already in use'),
+                         connection_id)
+            return
+        t = self.add_thing('Player', self.get_default_spawn_point())
+        t.name = nick
+        t.thing_char = self.get_next_player_char()
+        self.sessions[connection_id] = {
+            'thing_id': t.id_,
+            'status': 'player'
+        }
+        print('DEBUG LOGIN', t.name, len(self.sessions))
+        self.io.send('PLAYER_ID %s' % t.id_, connection_id)
+        self.io.send('LOGIN_OK', connection_id)
+        for msg in self.intro_messages:
+            self.io.send('CHAT ' + quote(msg), connection_id)
+        self.io.send('CHAT ' + quote(t.name + ' entered the map.'))
+        for s in [s for s in self.things
+                  if s.type_ == 'SpawnPoint' and s.name == t.name]:
+            t.position = s.position
+            if s.temporary:
+                self.remove_thing(s)
+                break
+        t.try_to_sit()
 
     def run_tick(self):
 
     def run_tick(self):
+
+        # update player sessions
         to_delete = []
         for connection_id in self.sessions:
             connection_id_found = False
         to_delete = []
         for connection_id in self.sessions:
             connection_id_found = False
@@ -234,11 +400,19 @@ class Game(GameBase):
                 t = self.get_player(connection_id)
                 if hasattr(t, 'name'):
                     self.io.send('CHAT ' + quote(t.name + ' left the map.'))
                 t = self.get_player(connection_id)
                 if hasattr(t, 'name'):
                     self.io.send('CHAT ' + quote(t.name + ' left the map.'))
-                self.things.remove(t)
+                spawn_point = self.add_thing('SpawnPoint', t.position)
+                spawn_point.temporary = True
+                spawn_point.name = t.name
+                print('DEBUG LEFT MAP', t.name)
+                self.remove_thing(t)
                 to_delete += [connection_id]
         for connection_id in to_delete:
             del self.sessions[connection_id]
                 to_delete += [connection_id]
         for connection_id in to_delete:
             del self.sessions[connection_id]
-            self.changed = True
+        while len(self.login_requests) > 0:
+            login_request = self.login_requests.pop()
+            self.login(login_request[0], login_request[1])
+
+        # update game state
         for t in [t for t in self.things]:
             if t in self.things:
                 try:
         for t in [t for t in self.things]:
             if t in self.things:
                 try:
@@ -251,11 +425,43 @@ class Game(GameBase):
                     for connection_id in [c_id for c_id in self.sessions
                                           if self.sessions[c_id]['thing_id'] == t.id_]:
                         self.io.send('PLAY_ERROR ' + quote(str(e)), connection_id)
                     for connection_id in [c_id for c_id in self.sessions
                                           if self.sessions[c_id]['thing_id'] == t.id_]:
                         self.io.send('PLAY_ERROR ' + quote(str(e)), connection_id)
+
+        # send gamestate if it makes sense at this point
         if self.changed:
             self.turn += 1
         if self.changed:
             self.turn += 1
-            self.send_gamestate()
-            self.changed = False
-            self.save()
+            # send_gamestate() can be rather expensive, due to among other reasons
+            # re-calculating players' FOVs, so don't send it out too often
+            if self.last_send_gamestate < \
+               datetime.datetime.now() - self.send_gamestate_min_interval:
+                n_changes = 0
+                for type_ in self.changed_tiles:
+                    n_changes += len(self.changed_tiles[type_])
+                if n_changes > 0:
+                    for t in [t for t in self.things if t.type_ == 'Player']:
+                        fov_radius = 12  # TODO: un-hardcode
+                        absolute_position =\
+                            self.map_geometry.undouble_yxyx(t.position[0],
+                                                            t.position[1])
+                        y_range_start = absolute_position.y - fov_radius
+                        y_range_end = absolute_position.y + fov_radius
+                        x_range_start = absolute_position.x - fov_radius
+                        x_range_end = absolute_position.x + fov_radius
+                        # TODO: refactor with SourcedMap.inside?
+                        for type_ in self.changed_tiles:
+                            for position in self.changed_tiles[type_]:
+                                if position.y < y_range_start\
+                                   or position.y > y_range_end:
+                                    continue
+                                if position.x < x_range_start\
+                                   or position.x > x_range_end:
+                                    continue
+                                t.invalidate(type_)
+                                break
+                self.send_gamestate()
+                self.changed = False
+                self.changed_tiles = {'fov': [], 'other': []}
+                self.save()
+                self.last_send_gamestate = datetime.datetime.now()
 
     def get_command(self, command_name):
 
 
     def get_command(self, command_name):
 
@@ -300,50 +506,137 @@ class Game(GameBase):
             self.player_char_i = 0
         return self.player_chars[self.player_char_i]
 
             self.player_char_i = 0
         return self.player_chars[self.player_char_i]
 
+    def get_foo_blockers(self, foo):
+        foo_blockers = ''
+        for t in self.terrains.values():
+            block_attr = getattr(t, 'blocks_' + foo)
+            if block_attr:
+                foo_blockers += t.character
+        return foo_blockers
+
+    def get_sound_blockers(self):
+        return self.get_foo_blockers('sound')
+
+    def get_light_blockers(self):
+        return self.get_foo_blockers('light')
+
+    def get_movement_blockers(self):
+        return self.get_foo_blockers('movement')
+
+    def get_flatland(self):
+        for t in self.terrains.values():
+            if not t.blocks_movement:
+                return t.character
+
     def save(self):
 
     def save(self):
 
-      def write(f, msg):
-          f.write(msg + '\n')
-
-      with open(self.io.save_file, 'w') as f:
-          write(f, 'TURN %s' % self.turn)
-          map_geometry_shape = self.get_map_geometry_shape()
-          write(f, 'MAP %s %s' % (map_geometry_shape, self.map_geometry.size,))
-          for big_yx in [yx for yx in self.maps if self.maps[yx].modified]:
-              for y, line in self.maps[big_yx].lines():
-                  write(f, 'MAP_LINE %s %5s %s' % (big_yx, y, quote(line)))
-          for big_yx in self.annotations:
-              for little_yx in self.annotations[big_yx]:
-                  write(f, 'GOD_ANNOTATE %s %s %s' %
-                        (big_yx, little_yx, quote(self.annotations[big_yx][little_yx])))
-          for big_yx in self.portals:
-              for little_yx in self.portals[big_yx]:
-                  write(f, 'GOD_PORTAL %s %s %s' % (big_yx, little_yx,
-                                                    quote(self.portals[big_yx][little_yx])))
-          for big_yx in [yx for yx in self.map_controls
-                         if self.map_controls[yx].modified]:
-              for y, line in self.map_controls[big_yx].lines():
-                  write(f, 'MAP_CONTROL_LINE %s %5s %s' % (big_yx, y, quote(line)))
-          for tile_class in self.map_control_passwords:
-              write(f, 'MAP_CONTROL_PW %s %s' % (tile_class,
-                                                 self.map_control_passwords[tile_class]))
-          for pw in self.admin_passwords:
-                  write(f, 'ADMIN_PASSWORD %s' % pw)
-          for t in [t for t in self.things if not t.type_ == 'Player']:
-              write(f, 'THING %s %s %s %s' % (t.position[0],
-                                              t.position[1], t.type_, t.id_))
-              if hasattr(t, 'name'):
-                  write(f, 'THING_NAME %s %s' % (t.id_, quote(t.name)))
+        def write(f, msg):
+            f.write(msg + '\n')
+
+        with open(self.io.save_file, 'w') as f:
+            write(f, 'TURN %s' % self.turn)
+            map_geometry_shape = self.get_map_geometry_shape()
+            # must come before MAP, otherwise first get_map uses the default
+            # TODO: refactor into MAP
+            write(f, 'MAP_CONTROL_PRESETS %s' % self.draw_control_presets)
+            write(f, 'MAP %s %s' % (map_geometry_shape, self.map_geometry.size,))
+            for terrain in self.terrains.values():
+                write(f, 'TERRAIN %s %s %s %s %s' % (quote(terrain.character),
+                                                     quote(terrain.description),
+                                                     int(terrain.blocks_light),
+                                                     int(terrain.blocks_sound),
+                                                     int(terrain.blocks_movement)))
+                if len(terrain.tags) > 0:
+                    for tag in terrain.tags:
+                        write(f, 'TERRAIN_TAG %s %s' % (quote(terrain.character),
+                                                        quote(tag)))
+            for big_yx in [yx for yx in self.maps if self.maps[yx].modified]:
+                for y, line in self.maps[big_yx].lines():
+                    write(f, 'MAP_LINE %s %5s %s' % (big_yx, y, quote(line)))
+            for big_yx in self.annotations:
+                for little_yx in self.annotations[big_yx]:
+                    write(f, 'GOD_ANNOTATE %s %s %s' %
+                          (big_yx, little_yx, quote(self.annotations[big_yx][little_yx])))
+            for big_yx in self.portals:
+                for little_yx in self.portals[big_yx]:
+                    write(f, 'GOD_PORTAL %s %s %s' % (big_yx, little_yx,
+                                                      quote(self.portals[big_yx][little_yx])))
+            for big_yx in [yx for yx in self.map_controls
+                           if self.map_controls[yx].modified]:
+                for y, line in self.map_controls[big_yx].lines():
+                    write(f, 'MAP_CONTROL_LINE %s %5s %s' % (big_yx, y, quote(line)))
+            for tile_class in self.map_control_passwords:
+                write(f, 'MAP_CONTROL_PW %s %s' % (tile_class,
+                                                   self.map_control_passwords[tile_class]))
+            for pw in self.admin_passwords:
+                write(f, 'ADMIN_PASSWORD %s' % pw)
+            for name in self.faces:
+                write(f, 'GOD_PLAYER_FACE %s %s' % (quote(name),
+                                                    quote(self.faces[name])))
+            for name in self.hats:
+                write(f, 'GOD_PLAYER_HAT %s %s' % (quote(name),
+                                                   quote(self.hats[name])))
+            for name in self.players_hat_chars:
+                write(f, 'GOD_PLAYERS_HAT_CHARS %s %s' %
+                      (quote(name), quote(self.players_hat_chars[name])))
+            for t in [t for t in self.things if not t.type_ == 'Player']:
+                write(f, 'THING %s %s %s %s' % (t.position[0],
+                                                t.position[1], t.type_, t.id_))
+                write(f, 'GOD_THING_PROTECTION %s %s' % (t.id_, quote(t.protection)))
+                if hasattr(t, 'name'):
+                    write(f, 'GOD_THING_NAME %s %s' % (t.id_, quote(t.name)))
+                if hasattr(t, 'installable') and (not t.portable):
+                    write(f, 'THING_INSTALLED %s' % t.id_)
+                if hasattr(t, 'design'):
+                    if t.type_ != 'Hat':
+                        write(f, 'GOD_THING_DESIGN_SIZE %s %s' % (t.id_,
+                                                                  t.design_size))
+                    write(f, 'GOD_THING_DESIGN %s %s' % (t.id_, quote(t.design)))
+                if t.type_ == 'Door' and t.blocks_movement:
+                    write(f, 'THING_DOOR_CLOSED %s %s' % (t.id_, int(t.locked)))
+                elif t.type_ == 'MusicPlayer':
+                    write(f, 'THING_MUSICPLAYER_SETTINGS %s %s %s %s' %
+                          (t.id_, int(t.playing), t.playlist_index, int(t.repeat)))
+                    for item in t.playlist:
+                        write(f, 'THING_MUSICPLAYER_PLAYLIST_ITEM %s %s %s' %
+                              (t.id_, quote(item[0]), item[1]))
+                elif t.type_ == 'Bottle' and not t.full:
+                    write(f, 'THING_BOTTLE_EMPTY %s' % t.id_)
+                elif t.type_ == 'DoorKey':
+                    write(f, 'THING_DOOR_KEY %s %s' % (t.id_, t.door.id_))
+                elif t.type_ == 'Crate':
+                    for item in t.content:
+                        write(f, 'THING_CRATE_ITEM %s %s' % (t.id_, item.id_))
+                elif t.type_ == 'SpawnPoint':
+                    timestamp = 0
+                    if t.temporary:
+                        timestamp = int(t.created_at.timestamp())
+                    write(f, 'THING_SPAWNPOINT_CREATED %s %s' % (t.id_,
+                                                                 timestamp))
+            next_thing_id = self.new_thing_id()
+            for t in [t for t in self.things if t.type_ == 'Player']:
+                write(f, 'THING %s %s SpawnPoint %s'
+                      % (t.position[0], t.position[1], next_thing_id))
+                write(f, 'GOD_THING_NAME %s %s' % (next_thing_id, t.name))
+                write(f, 'THING_SPAWNPOINT_CREATED %s %s'
+                      % (next_thing_id, int(datetime.datetime.now().timestamp())))
+                next_thing_id += 1
+            for s in self.spawn_points:
+                write(f, 'SPAWN_POINT %s %s' % (s[0], s[1]))
+            for msg in self.intro_messages:
+                write(f, 'INTRO_MSG %s' % quote(msg))
+
+
 
     def get_map(self, big_yx, type_='normal'):
         if type_ == 'normal':
             maps = self.maps
         elif type_ == 'control':
             maps = self.map_controls
 
     def get_map(self, big_yx, type_='normal'):
         if type_ == 'normal':
             maps = self.maps
         elif type_ == 'control':
             maps = self.map_controls
-        if not big_yx in maps:
+        if big_yx not in maps:
             maps[big_yx] = SaveableMap(self.map_geometry)
             if type_ == 'control':
             maps[big_yx] = SaveableMap(self.map_geometry)
             if type_ == 'control':
-                maps[big_yx].draw_presets(big_yx.y % 2)
+                maps[big_yx].draw_presets(big_yx, self.draw_control_presets)
         return maps[big_yx]
 
     def new_world(self, map_geometry):
         return maps[big_yx]
 
     def new_world(self, map_geometry):
@@ -354,6 +647,6 @@ class Game(GameBase):
         self.admin_passwords = []
         self.map_geometry = map_geometry
         self.map_control_passwords = {'X': 'secret'}
         self.admin_passwords = []
         self.map_geometry = map_geometry
         self.map_control_passwords = {'X': 'secret'}
-        self.get_map(YX(0,0))
-        self.get_map(YX(0,0), 'control')
+        self.get_map(YX(0, 0))
+        self.get_map(YX(0, 0), 'control')
         self.annotations = {}
         self.annotations = {}