Esempio n. 1
0
async def set_client_location_options(sid: str, data: LocationOptions):
    pr: PlayerRoom = game_state.get(sid)

    LocationUserOption.update(
        pan_x=data["pan_x"],
        pan_y=data["pan_y"],
        zoom_factor=data["zoom_factor"],
    ).where(
        (LocationUserOption.location == pr.active_location)
        & (LocationUserOption.user == pr.player)
    ).execute()
Esempio n. 2
0
async def set_client(sid: str, data: ClientOptions):
    pr: PlayerRoom = game_state.get(sid)

    with db.atomic():
        for option, value in data.items():
            if option != "location_options":
                setattr(pr.player, option, value)
        pr.player.save()

    if "location_options" in data:
        LocationUserOption.update(
            pan_x=data["location_options"]["pan_x"],
            pan_y=data["location_options"]["pan_y"],
            zoom_factor=data["location_options"]["zoom_factor"],
        ).where((LocationUserOption.location == pr.active_location)
                & (LocationUserOption.user == pr.player)).execute()
Esempio n. 3
0
async def load_location(sid, location):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    room = sid_data["room"]

    sid_data["location"] = location

    data = {}
    data["locations"] = [l.name for l in room.locations]
    if user == room.creator:
        data["layers"] = [
            l.as_dict(user, True) for l in location.layers.order_by(Layer.index)
        ]
    else:
        data["layers"] = [
            l.as_dict(user, False)
            for l in location.layers.order_by(Layer.index).where(Layer.player_visible)
        ]
    client_options = user.as_dict()
    client_options.update(
        **LocationUserOption.get(user=user, location=location).as_dict()
    )

    await sio.emit("Board.Set", data, room=sid, namespace="/planarally")
    await sio.emit(
        "Location.Set", location.as_dict(), room=sid, namespace="/planarally"
    )
    await sio.emit(
        "Client.Options.Set", client_options, room=sid, namespace="/planarally"
    )
    await sio.emit(
        "Notes.Set",
        [
            note.as_dict()
            for note in Note.select().where((Note.user == user) & (Note.room == room))
        ],
        room=sid,
        namespace="/planarally",
    )

    sorted_initiatives = [
        init.as_dict()
        for init in Initiative.select()
        .join(Shape, JOIN.LEFT_OUTER, on=(Initiative.uuid == Shape.uuid))
        .join(Layer)
        .where((Layer.location == location))
        .order_by(Initiative.index)
    ]
    location_data = InitiativeLocationData.get_or_none(location=location)
    if location_data:
        await send_client_initiatives(room, location, user)
        await sio.emit(
            "Initiative.Round.Update",
            location_data.round,
            room=sid,
            namespace="/planarally",
        )
        await sio.emit(
            "Initiative.Turn.Set", location_data.turn, room=sid, namespace="/planarally"
        )
Esempio n. 4
0
async def focus_location(sid):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    room = sid_data["room"]
    location = sid_data["location"]

    if room.creator != user:
        logger.warning(f"{user.name} attempted to focus everyone")
        return

    location_dm_option = LocationUserOption.get(user=user, location=location)
    for room_player in room.players:
        location_option = LocationUserOption.get(user=room_player.player, location=location)
        location_option.pan_x = location_dm_option.pan_x
        location_option.pan_y = location_dm_option.pan_y
        location_option.zoom_factor = location_dm_option.zoom_factor
        location_option.save()
        for psid in state.get_sids(user=room_player.player, room=room):
            await load_location(psid, location)
Esempio n. 5
0
async def set_client(sid: int, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    with db.atomic():
        for option in [
            ("gridColour", "grid_colour"),
            ("fowColour", "fow_colour"),
            ("rulerColour", "ruler_colour"),
            ("invertAlt", "invert_alt"),
        ]:
            if option[0] in data:
                setattr(pr.player, option[1], data[option[0]])
        pr.player.save()
    if "locationOptions" in data:
        LocationUserOption.update(
            pan_x=data["locationOptions"]["panX"],
            pan_y=data["locationOptions"]["panY"],
            zoom_factor=data["locationOptions"]["zoomFactor"],
        ).where((LocationUserOption.location == pr.active_location)
                & (LocationUserOption.user == pr.player)).execute()
Esempio n. 6
0
async def load_location(sid: int, location: Location):
    pr: PlayerRoom = game_state.get(sid)
    if pr.active_location != location:
        pr.active_location = location
        pr.save()

    data = {}
    data["locations"] = [
        {"id": l.id, "name": l.name} for l in pr.room.locations.order_by(Location.index)
    ]
    data["floors"] = [
        f.as_dict(pr.player, pr.player == pr.room.creator)
        for f in location.floors.order_by(Floor.index)
    ]
    client_options = pr.player.as_dict()
    client_options.update(
        **LocationUserOption.get(user=pr.player, location=location).as_dict()
    )

    await sio.emit("Board.Set", data, room=sid, namespace=GAME_NS)
    await sio.emit("Location.Set", location.as_dict(), room=sid, namespace=GAME_NS)
    await sio.emit("Client.Options.Set", client_options, room=sid, namespace=GAME_NS)
    await sio.emit(
        "Notes.Set",
        [
            note.as_dict()
            for note in Note.select().where(
                (Note.user == pr.player) & (Note.room == pr.room)
            )
        ],
        room=sid,
        namespace=GAME_NS,
    )
    await sio.emit(
        "Markers.Set",
        [
            marker.as_string()
            for marker in Marker.select(Marker.shape_id).where(
                (Marker.user == pr.player) & (Marker.location == location)
            )
        ],
        room=sid,
        namespace=GAME_NS,
    )

    location_data = InitiativeLocationData.get_or_none(location=location)
    if location_data:
        await send_client_initiatives(pr, pr.player)
        await sio.emit(
            "Initiative.Round.Update", location_data.round, room=sid, namespace=GAME_NS,
        )
        await sio.emit(
            "Initiative.Turn.Set", location_data.turn, room=sid, namespace=GAME_NS
        )
Esempio n. 7
0
async def set_client(sid, data):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    location = sid_data["location"]

    with db.atomic():
        for option in [
            ("gridColour", "grid_colour"),
            ("fowColour", "fow_colour"),
            ("rulerColour", "ruler_colour"),
        ]:
            if option[0] in data:
                setattr(user, option[1], data[option[0]])
        user.save()
    if "locationOptions" in data:
        LocationUserOption.update(
            pan_x=data["locationOptions"]["panX"],
            pan_y=data["locationOptions"]["panY"],
            zoom_factor=data["locationOptions"]["zoomFactor"],
        ).where((LocationUserOption.location == location)
                & (LocationUserOption.user == user)).execute()
Esempio n. 8
0
async def set_layer(sid: int, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    try:
        floor = pr.active_location.floors.select().where(Floor.name == data["floor"])[0]
        layer = floor.layers.select().where(Layer.name == data["layer"])[0]
    except IndexError:
        pass
    else:
        luo = LocationUserOption.get(user=pr.player, location=pr.active_location)
        luo.active_layer = layer
        luo.save()
Esempio n. 9
0
async def set_layer(sid, data):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    location = sid_data["location"]

    try:
        layer = location.layers.select().where(Layer.name == data)[0]
    except IndexError:
        pass
    else:
        luo = LocationUserOption.get(user=user, location=location)
        luo.active_layer = layer
        luo.save()
Esempio n. 10
0
async def load_location(sid, location):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    room = sid_data["room"]

    sid_data["location"] = location

    data = {}
    data["locations"] = [l.name for l in room.locations]
    data["floors"] = [
        f.as_dict(user, user == room.creator)
        for f in location.floors.order_by(Floor.index)
    ]
    client_options = user.as_dict()
    client_options.update(
        **LocationUserOption.get(user=user, location=location).as_dict())

    await sio.emit("Board.Set", data, room=sid, namespace="/planarally")
    await sio.emit("Location.Set",
                   location.as_dict(),
                   room=sid,
                   namespace="/planarally")
    await sio.emit("Client.Options.Set",
                   client_options,
                   room=sid,
                   namespace="/planarally")
    await sio.emit(
        "Notes.Set",
        [
            note.as_dict()
            for note in Note.select().where((Note.user == user)
                                            & (Note.room == room))
        ],
        room=sid,
        namespace="/planarally",
    )

    location_data = InitiativeLocationData.get_or_none(location=location)
    if location_data:
        await send_client_initiatives(room, location, user)
        await sio.emit(
            "Initiative.Round.Update",
            location_data.round,
            room=sid,
            namespace="/planarally",
        )
        await sio.emit("Initiative.Turn.Set",
                       location_data.turn,
                       room=sid,
                       namespace="/planarally")
Esempio n. 11
0
async def update_client_location(player: int, room: int, sid: str,
                                 data: LocationOptions):
    pr = PlayerRoom.get(player=player, room=room)

    LocationUserOption.update(
        pan_x=data["pan_x"],
        pan_y=data["pan_y"],
        zoom_display=data["zoom_display"],
    ).where((LocationUserOption.location == pr.active_location)
            & (LocationUserOption.user == pr.player)).execute()

    if pr.role != Role.DM:
        for p_sid, p_player in game_state.get_t(skip_sid=sid):
            if p_player.role == Role.DM or p_player.player.id == player:
                await sio.emit(
                    "Client.Move",
                    {
                        "player": pr.player.id,
                        **data
                    },
                    room=p_sid,
                    namespace=GAME_NS,
                )
Esempio n. 12
0
def upgrade(version):
    if version == 3:
        from models import GridLayer

        db.execute_sql(
            "CREATE TEMPORARY TABLE _grid_layer AS SELECT * FROM grid_layer")
        db.drop_tables([GridLayer])
        db.create_tables([GridLayer])
        db.execute_sql("INSERT INTO grid_layer SELECT * FROM _grid_layer")
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 4:
        from models import Location

        db.foreign_keys = False
        db.execute_sql(
            "CREATE TEMPORARY TABLE _location AS SELECT * FROM location")
        db.execute_sql("DROP TABLE location")
        db.create_tables([Location])
        db.execute_sql("INSERT INTO location SELECT * FROM _location")
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 5:
        from models import Layer

        migrator = SqliteMigrator(db)
        field = ForeignKeyField(Layer,
                                Layer.id,
                                backref="active_users",
                                null=True)
        with db.atomic():
            migrate(
                migrator.add_column("location_user_option", "active_layer_id",
                                    field))
            from models import LocationUserOption

            LocationUserOption._meta.add_field("active_layer", field)
            for luo in LocationUserOption.select():
                luo.active_layer = luo.location.layers.select().where(
                    Layer.name == "tokens")[0]
                luo.save()
            migrate(
                migrator.add_not_null("location_user_option",
                                      "active_layer_id"))
            Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 6:
        migrator = SqliteMigrator(db)
        migrate(
            migrator.drop_not_null("location_user_option", "active_layer_id"))
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 7:
        # Remove shape index unique constraint
        from models import Shape

        db.foreign_keys = False
        db.execute_sql("CREATE TEMPORARY TABLE _shape AS SELECT * FROM shape")
        db.execute_sql("DROP TABLE shape")
        db.create_tables([Shape])
        db.execute_sql("INSERT INTO shape SELECT * FROM _shape")
        db.foreign_keys = True
        # Check all indices and reset to 0 index
        logger.info("Validating all shape indices")
        from models import Layer

        with db.atomic():
            for layer in Layer.select():
                shapes = layer.shapes.order_by(fn.ABS(Shape.index))
                for i, shape in enumerate(shapes):
                    shape.index = i
                    shape.save()
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 8:
        from models import Polygon

        db.create_tables([Polygon])
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 9:
        from models import Location

        db.foreign_keys = False
        migrator = SqliteMigrator(db)
        with db.atomic():
            migrate(
                migrator.add_column("location", "vision_mode",
                                    Location.vision_mode),
                migrator.add_column("location", "vision_min_range",
                                    Location.vision_min_range),
                migrator.add_column("location", "vision_max_range",
                                    Location.vision_max_range),
            )
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    else:
        raise Exception(
            f"No upgrade code for save format {version} was found.")
Esempio n. 13
0
async def load_location(sid: str, location: Location, *, complete=False):
    pr: PlayerRoom = game_state.get(sid)
    if pr.active_location != location:
        pr.active_location = location
        pr.save()

    # 1. Load client options

    client_options = pr.player.as_dict()
    client_options["location_user_options"] = LocationUserOption.get(
        user=pr.player, location=location).as_dict()
    client_options["default_user_options"] = pr.player.default_options.as_dict(
    )

    if pr.user_options:
        client_options["room_user_options"] = pr.user_options.as_dict()

    await sio.emit("Client.Options.Set",
                   client_options,
                   room=sid,
                   namespace=GAME_NS)

    # 2. Load room info

    if complete:
        await sio.emit(
            "Room.Info.Set",
            {
                "name":
                pr.room.name,
                "creator":
                pr.room.creator.name,
                "invitationCode":
                str(pr.room.invitation_code),
                "isLocked":
                pr.room.is_locked,
                "default_options":
                pr.room.default_options.as_dict(),
                "players": [{
                    "id": rp.player.id,
                    "name": rp.player.name,
                    "location": rp.active_location.id,
                    "role": rp.role,
                } for rp in pr.room.players],
                "publicName":
                config.get("General", "public_name", fallback=""),
            },
            room=sid,
            namespace=GAME_NS,
        )

    # 3. Load location

    await sio.emit("Location.Set",
                   location.as_dict(),
                   room=sid,
                   namespace=GAME_NS)

    # 4. Load all location settings (DM)

    if complete and pr.role == Role.DM:
        await sio.emit(
            "Locations.Settings.Set",
            {
                l.id: {} if l.options is None else l.options.as_dict()
                for l in pr.room.locations
            },
            room=sid,
            namespace=GAME_NS,
        )

    # 5. Load Board

    locations = [{
        "id": l.id,
        "name": l.name,
        "archived": l.archived
    } for l in pr.room.locations.order_by(Location.index)]
    await sio.emit("Board.Locations.Set",
                   locations,
                   room=sid,
                   namespace=GAME_NS)

    floors = [floor for floor in location.floors.order_by(Floor.index)]

    if "active_floor" in client_options["location_user_options"]:
        index = next(i for i, f in enumerate(floors) if f.name ==
                     client_options["location_user_options"]["active_floor"])
        lower_floors = floors[index - 1::-1] if index > 0 else []
        higher_floors = floors[index + 1:] if index < len(floors) else []
        floors = [floors[index], *lower_floors, *higher_floors]

    for floor in floors:
        await sio.emit(
            "Board.Floor.Set",
            floor.as_dict(pr.player, pr.role == Role.DM),
            room=sid,
            namespace=GAME_NS,
        )

    # 6. Load Initiative

    location_data = Initiative.get_or_none(location=location)
    if location_data:
        await sio.emit("Initiative.Set",
                       location_data.as_dict(),
                       room=sid,
                       namespace=GAME_NS)

    # 7. Load labels

    if complete:
        labels = Label.select().where((Label.user == pr.player)
                                      | (Label.visible == True))
        label_filters = LabelSelection.select().where(
            (LabelSelection.user == pr.player)
            & (LabelSelection.room == pr.room))

        await sio.emit(
            "Labels.Set",
            [l.as_dict() for l in labels],
            room=sid,
            namespace=GAME_NS,
        )
        await sio.emit(
            "Labels.Filters.Set",
            [l.label.uuid for l in label_filters],
            room=sid,
            namespace=GAME_NS,
        )

    # 8. Load Notes

    await sio.emit(
        "Notes.Set",
        [
            note.as_dict()
            for note in Note.select().where((Note.user == pr.player)
                                            & (Note.room == pr.room))
        ],
        room=sid,
        namespace=GAME_NS,
    )

    # 9. Load Markers

    await sio.emit(
        "Markers.Set",
        [
            marker.as_string()
            for marker in Marker.select(Marker.shape_id).where(
                (Marker.user == pr.player) & (Marker.location == location))
        ],
        room=sid,
        namespace=GAME_NS,
    )

    # 10. Load Assets

    if complete:
        await sio.emit(
            "Asset.List.Set",
            Asset.get_user_structure(pr.player),
            room=sid,
            namespace=GAME_NS,
        )
Esempio n. 14
0
def upgrade(version):
    if version == 3:
        from models import GridLayer

        db.execute_sql("CREATE TEMPORARY TABLE _grid_layer AS SELECT * FROM grid_layer")
        db.drop_tables([GridLayer])
        db.create_tables([GridLayer])
        db.execute_sql("INSERT INTO grid_layer SELECT * FROM _grid_layer")
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 4:
        from models import Location

        db.foreign_keys = False
        db.execute_sql("CREATE TEMPORARY TABLE _location AS SELECT * FROM location")
        db.execute_sql("DROP TABLE location")
        db.create_tables([Location])
        db.execute_sql("INSERT INTO location SELECT * FROM _location")
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 5:
        from models import Layer

        migrator = SqliteMigrator(db)
        field = ForeignKeyField(Layer, Layer.id, backref="active_users", null=True)
        with db.atomic():
            migrate(
                migrator.add_column("location_user_option", "active_layer_id", field)
            )
            from models import LocationUserOption

            LocationUserOption._meta.add_field("active_layer", field)
            for luo in LocationUserOption.select():
                luo.active_layer = luo.location.layers.select().where(
                    Layer.name == "tokens"
                )[0]
                luo.save()
            migrate(migrator.add_not_null("location_user_option", "active_layer_id"))
            Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 6:
        migrator = SqliteMigrator(db)
        migrate(migrator.drop_not_null("location_user_option", "active_layer_id"))
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 7:
        # Remove shape index unique constraint
        from models import Shape

        db.foreign_keys = False
        db.execute_sql("CREATE TEMPORARY TABLE _shape AS SELECT * FROM shape")
        db.execute_sql("DROP TABLE shape")
        db.create_tables([Shape])
        db.execute_sql("INSERT INTO shape SELECT * FROM _shape")
        db.foreign_keys = True
        # Check all indices and reset to 0 index
        logger.info("Validating all shape indices")
        from models import Layer

        with db.atomic():
            for layer in Layer.select():
                shapes = layer.shapes.order_by(fn.ABS(Shape.index))
                for i, shape in enumerate(shapes):
                    shape.index = i
                    shape.save()
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 8:
        from models import Polygon

        db.create_tables([Polygon])
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 9:
        from models import Location

        db.foreign_keys = False
        migrator = SqliteMigrator(db)
        with db.atomic():
            migrate(
                migrator.add_column("location", "vision_mode", Location.vision_mode),
                migrator.add_column(
                    "location", "vision_min_range", Location.vision_min_range
                ),
                migrator.add_column(
                    "location", "vision_max_range", Location.vision_max_range
                ),
            )
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 10:
        from models import Shape

        db.foreign_keys = False
        migrator = SqliteMigrator(db)
        with db.atomic():
            migrate(migrator.add_column("shape", "name_visible", Shape.name_visible))
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 11:
        from models import Label, LocationUserOption, ShapeLabel

        db.foreign_keys = False
        migrator = SqliteMigrator(db)
        with db.atomic():
            db.create_tables([Label, ShapeLabel])
            migrate(
                migrator.add_column(
                    "location_user_option",
                    "active_filters",
                    LocationUserOption.active_filters,
                )
            )
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 12:
        from models import Label, LabelSelection

        db.foreign_keys = False
        migrator = SqliteMigrator(db)
        with db.atomic():
            try:
                migrate(migrator.add_column("label", "category", Label.category))
            except OperationalError as e:
                if e.args[0] != "duplicate column name: category":
                    raise e
            db.create_tables([LabelSelection])
        with db.atomic():
            for label in Label:
                if ":" not in label.name:
                    continue
                cat, *name = label.name.split(":")
                label.category = cat
                label.name = ":".join(name)
                label.save()
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 13:
        from models import LocationUserOption, MultiLine, Polygon

        db.foreign_keys = False
        migrator = SqliteMigrator(db)

        migrate(migrator.drop_column("location_user_option", "active_filters"))

        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 14:
        db.foreign_keys = False
        migrator = SqliteMigrator(db)

        from models import GridLayer, Layer

        db.execute_sql('CREATE TABLE IF NOT EXISTS "base_rect" ("shape_id" TEXT NOT NULL PRIMARY KEY, "width" REAL NOT NULL, "height" REAL NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)')
        db.execute_sql('CREATE TABLE IF NOT EXISTS "shape_type" ("shape_id" TEXT NOT NULL PRIMARY KEY, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)')

        shape_types = [
            "asset_rect",
            "circle",
            "circular_token",
            "line",
            "multi_line",
            "polygon",
            "rect",
            "text",
        ]
        with db.atomic():
            for table in shape_types:
                db.execute_sql(
                    f"CREATE TEMPORARY TABLE _{table} AS SELECT * FROM {table}"
                )
                db.execute_sql(f"DROP TABLE {table}")
            for query in [
                'CREATE TABLE IF NOT EXISTS "asset_rect" ("shape_id" TEXT NOT NULL PRIMARY KEY, "width" REAL NOT NULL, "height" REAL NOT NULL, "src" TEXT NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "circle" ("shape_id" TEXT NOT NULL PRIMARY KEY, "radius" REAL NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "circular_token" ("shape_id" TEXT NOT NULL PRIMARY KEY, "radius" REAL NOT NULL, "text" TEXT NOT NULL, "font" TEXT NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "line" ("shape_id" TEXT NOT NULL PRIMARY KEY, "x2" REAL NOT NULL, "y2" REAL NOT NULL, "line_width" INTEGER NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "multi_line" ("shape_id" TEXT NOT NULL PRIMARY KEY, "line_width" INTEGER NOT NULL, "points" TEXT NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "polygon" ("shape_id" TEXT NOT NULL PRIMARY KEY, "vertices" TEXT NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "rect" ("shape_id" TEXT NOT NULL PRIMARY KEY, "width" REAL NOT NULL, "height" REAL NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
                'CREATE TABLE IF NOT EXISTS "text" ("shape_id" TEXT NOT NULL PRIMARY KEY, "text" TEXT NOT NULL, "font" TEXT NOT NULL, "angle" REAL NOT NULL, FOREIGN KEY ("shape_id") REFERENCES "shape" ("uuid") ON DELETE CASCADE)',
            ]:
                db.execute_sql(query)
            for table in shape_types:
                db.execute_sql(
                    f"INSERT INTO {table} SELECT _{table}.* FROM _{table} INNER JOIN shape ON shape.uuid = _{table}.uuid"
                )
        field = ForeignKeyField(Layer, Layer.id, null=True)
        with db.atomic():
            migrate(migrator.add_column("grid_layer", "layer_id", field))
            for gl in GridLayer.select():
                l = Layer.get_or_none(id=gl.id)
                if l:
                    gl.layer = l
                    gl.save()
                else:
                    gl.delete_instance()
            migrate(migrator.add_not_null("grid_layer", "layer_id"))

        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    elif version == 15:
        from peewee import BooleanField
        migrator = SqliteMigrator(db)
        db.foreign_keys = False
        with db.atomic():
            migrate(
                migrator.add_column("room", "is_locked", BooleanField(default=False))
            )
        db.foreign_keys = True
        Constants.update(save_version=Constants.save_version + 1).execute()
    else:
        raise Exception(f"No upgrade code for save format {version} was found.")