Ejemplo n.º 1
0
async def update_shape_position(sid: str, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    if data["temporary"] and not has_ownership_temp(data["shape"], pr):
        logger.warning(
            f"User {pr.player.name} attempted to move a shape it does not own."
        )
        return

    shape, layer = await _get_shape(data, pr)

    # Overwrite the old data with the new data
    if not data["temporary"]:
        if not has_ownership(shape, pr):
            logger.warning(
                f"User {pr.player.name} attempted to move a shape it does not own."
            )
            return

        with db.atomic():
            # Shape
            update_model_from_dict(shape,
                                   reduce_data_to_model(Shape, data["shape"]))
            shape.save()
            if shape.type_ == "polygon":
                # Subshape
                type_instance = shape.subtype
                # no backrefs on these tables
                type_instance.update_from_dict(data["shape"],
                                               ignore_unknown=True)
                type_instance.save()

    await sync_shape_update(layer, pr, data, sid, shape)
Ejemplo n.º 2
0
async def update_initiative_option(sid: str, data: ServerInitiativeOption):
    pr: PlayerRoom = game_state.get(sid)

    shape = Shape.get_or_none(uuid=data["shape"])

    if not has_ownership(shape, pr):
        logger.warning(
            f"{pr.player.name} attempted to change initiative of an asset it does not own"
        )
        return

    location_data = Initiative.get_or_none(location=pr.active_location)
    if location_data is None:
        logger.error(
            "Initiative updated for location without initiative tracking")
        return

    json_data = json.loads(location_data.data)

    with db.atomic():
        for i, initiative_data in enumerate(json_data):
            if initiative_data["shape"] == data["shape"]:
                json_data[i][data["option"]] = data["value"]
                break

        location_data.data = json.dumps(json_data)
        location_data.save()

    await sio.emit(
        "Initiative.Option.Set",
        data,
        skip_sid=sid,
        room=pr.active_location.get_path(),
        namespace=GAME_NS,
    )
Ejemplo n.º 3
0
async def change_initiative_order(sid: str, data: ServerInitiativeOrderChange):
    pr: PlayerRoom = game_state.get(sid)

    if pr.role != Role.DM:
        logger.warning(f"{pr.player.name} attempted to reorder initiatives")
        return

    old_index = data["oldIndex"]
    new_index = data["newIndex"]

    with db.atomic():
        location_data = Initiative.get(location=pr.active_location)
        json_data = json.loads(location_data.data)

        if json_data[old_index]["shape"] != data["shape"]:
            return

        if json_data[new_index].get(
                "initiative", 0) != json_data[old_index].get("initiative", 0):
            location_data.sort = 2

        json_data.insert(new_index, json_data.pop(old_index))

        json_data = sort_initiative(json_data, location_data.sort)

        location_data.data = json.dumps(json_data)
        location_data.save()

    await sio.emit(
        "Initiative.Set",
        location_data.as_dict(),
        room=pr.active_location.get_path(),
        namespace=GAME_NS,
    )
Ejemplo n.º 4
0
async def create(request):
    allow_new_sessions = config.getboolean("Access control",
                                           "allow_new_sessions")
    if not allow_new_sessions:
        return web.HTTPConflict(
            reason="This feature is disabled by administrator")

    user = await check_authorized(request)
    data = await request.json()
    roomname = data["name"]
    if not roomname:
        return web.HTTPBadRequest()
    else:
        with db.atomic():
            default_options = LocationOptions.create()
            room = Room.create(name=roomname,
                               creator=user,
                               default_options=default_options)
            loc = Location.create(room=room, name="start", index=1)
            loc.create_floor()
            PlayerRoom.create(player=user,
                              room=room,
                              role=Role.DM,
                              active_location=loc)
            room.save()
        return web.HTTPOk()
Ejemplo n.º 5
0
async def create(request: web.Request):
    user: User = await check_authorized(request)
    data = await request.json()
    roomname = data["name"]
    logo = data["logo"]
    if not roomname:
        return web.HTTPBadRequest()
    else:

        if Room.get_or_none(name=roomname, creator=user):
            return web.HTTPConflict()

        with db.atomic():
            default_options = LocationOptions.create()
            room = Room.create(
                name=roomname,
                creator=user,
                default_options=default_options,
            )

            if logo >= 0:
                room.logo_id = logo

            loc = Location.create(room=room, name="start", index=1)
            loc.create_floor()
            PlayerRoom.create(player=user, room=room, role=Role.DM, active_location=loc)
            room.save()
        return web.HTTPOk()
Ejemplo n.º 6
0
async def update_shape_position(sid, data):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    room = sid_data["room"]
    location = sid_data["location"]

    shape, layer = await _get_shape(data, location, user)

    if not await has_ownership(layer, room, data, user, shape):
        return

    # Overwrite the old data with the new data
    if not data["temporary"]:
        with db.atomic():
            data["shape"]["layer"] = Layer.get(location=location,
                                               name=data["shape"]["layer"])
            # Shape
            model = reduce_data_to_model(Shape, data["shape"])
            update_model_from_dict(shape, model)
            shape.save()
            if shape.type_ == "polygon":
                # Subshape
                type_instance = shape.subtype
                # no backrefs on these tables
                type_instance.update_from_dict(data["shape"],
                                               ignore_unknown=True)
                type_instance.save()

    await sync_shape_update(layer, room, data, sid, shape)
Ejemplo n.º 7
0
async def add_shape(sid: int, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    if "temporary" not in data:
        data["temporary"] = False

    floor = pr.active_location.floors.select().where(
        Floor.name == data["shape"]["floor"])[0]
    layer = floor.layers.where(Layer.name == data["shape"]["layer"])[0]

    if pr.role != Role.DM and not layer.player_editable:
        logger.warning(
            f"{pr.player.name} attempted to add a shape to a dm layer")
        return
    if data["temporary"]:
        game_state.add_temp(sid, data["shape"]["uuid"])
    else:
        with db.atomic():
            data["shape"]["layer"] = layer
            data["shape"]["index"] = layer.shapes.count()
            # Shape itself
            shape = Shape.create(**reduce_data_to_model(Shape, data["shape"]))
            # Subshape
            type_table = get_table(shape.type_)
            type_table.create(
                shape=shape,
                **type_table.pre_create(
                    **reduce_data_to_model(type_table, data["shape"])),
            )
            # Owners
            for owner in data["shape"]["owners"]:
                ShapeOwner.create(
                    shape=shape,
                    user=User.by_name(owner["user"]),
                    edit_access=owner["edit_access"],
                    movement_access=owner["movement_access"],
                    vision_access=owner["vision_access"],
                )
            # Trackers
            for tracker in data["shape"]["trackers"]:
                Tracker.create(**reduce_data_to_model(Tracker, tracker),
                               shape=shape)
            # Auras
            for aura in data["shape"]["auras"]:
                Aura.create(**reduce_data_to_model(Aura, aura), shape=shape)

    for room_player in pr.room.players:
        is_dm = room_player.role == Role.DM
        for psid in game_state.get_sids(player=room_player.player,
                                        active_location=pr.active_location):
            if psid == sid:
                continue
            if not is_dm and not layer.player_visible:
                continue
            if not data["temporary"]:
                data["shape"] = shape.as_dict(room_player.player, is_dm)
            await sio.emit("Shape.Add",
                           data["shape"],
                           room=psid,
                           namespace=GAME_NS)
Ejemplo n.º 8
0
async def update_initiative_turn(sid: int, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    if pr.role != Role.DM:
        logger.warning(
            f"{pr.player.name} attempted to advance the initiative tracker")
        return

    location_data = InitiativeLocationData.get(location=pr.active_location)
    with db.atomic():
        location_data.turn = data
        location_data.save()

        effects = (InitiativeEffect.select().join(Initiative).where(
            Initiative.uuid == data))
        for effect in effects:
            if effect.turns <= 0:
                effect.delete_instance()
            else:
                effect.turns -= 1
            effect.save()

    await sio.emit(
        "Initiative.Turn.Update",
        data,
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 9
0
async def register(request):
    username = await authorized_userid(request)
    if username:
        return web.HTTPOk()

    data = await request.json()
    username = data["username"]
    password = data["password"]
    email = data.get("email", None)
    if User.by_name(username):
        return web.HTTPConflict(reason="Username already taken")
    elif not username:
        return web.HTTPBadRequest(reason="Please provide a username")
    elif not password:
        return web.HTTPBadRequest(reason="Please provide a password")
    else:
        try:
            with db.atomic():
                u = User(name=username)
                u.set_password(password)
                if email:
                    u.email = email
                u.save()
        except:
            return web.HTTPServerError(
                reason=
                "An unexpected error occured on the server during account creation.  Operation reverted."
            )
        response = web.HTTPOk()
        await remember(request, response, username)
        return response
Ejemplo n.º 10
0
async def update_initiative_round(sid: str, data: int):
    pr: PlayerRoom = game_state.get(sid)

    location_data = Initiative.get(location=pr.active_location)

    if pr.role != Role.DM:
        json_data = json.loads(location_data.data)
        if not has_ownership(
                Shape.get_or_none(uuid=json_data[location_data.turn]["shape"]),
                pr):
            logger.warning(
                f"{pr.player.name} attempted to advance the initiative tracker"
            )
            return

    with db.atomic():
        location_data.round = data
        location_data.save()

    await sio.emit(
        "Initiative.Round.Update",
        data,
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 11
0
async def register(request):
    allow_registration = config.getboolean("Access control",
                                           "allow_registration")
    if not allow_registration:
        return web.HTTPConflict(
            reason="This feature is disabled by administrator")

    username = await authorized_userid(request)
    if username:
        return web.HTTPOk()

    data = await request.json()
    username = data["username"]
    password = data["password"]
    if User.by_name(username):
        return web.HTTPConflict(reason="Username already taken")
    elif not username:
        return web.HTTPBadRequest(reason="Please provide a username")
    elif not password:
        return web.HTTPBadRequest(reason="Please provide a password")
    else:
        with db.atomic():
            u = User(name=username)
            u.set_password(password)
            u.save()
        response = web.HTTPOk()
        await remember(request, response, username)
        return response
Ejemplo n.º 12
0
async def update_shape_positions(sid: str, data: PositionUpdateList):
    pr: PlayerRoom = game_state.get(sid)

    shapes: List[Tuple[Shape, PositionUpdate]] = []

    for sh in data["shapes"]:
        shape = Shape.get_or_none(Shape.uuid == sh["uuid"])
        if shape is not None and not has_ownership(shape, pr, movement=True):
            logger.warning(
                f"User {pr.player.name} attempted to move a shape it does not own."
            )
            return
        shapes.append((shape, sh))

    if not data["temporary"]:
        with db.atomic():
            for db_shape, data_shape in shapes:
                points = data_shape["position"]["points"]
                db_shape.x = points[0][0]
                db_shape.y = points[0][1]
                db_shape.angle = data_shape["position"]["angle"]
                db_shape.save()

                if len(points) > 1:
                    # Subshape
                    type_instance = db_shape.subtype
                    type_instance.set_location(points[1:])

    await sio.emit(
        "Shapes.Position.Update",
        data["shapes"],
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 13
0
async def update_shape_options(sid: str, data: OptionUpdateList):
    pr: PlayerRoom = game_state.get(sid)

    shapes: List[Tuple[Shape, OptionUpdate]] = []

    for sh in data["options"]:
        shape = Shape.get_or_none(Shape.uuid == sh["uuid"])
        if shape is not None and not has_ownership(shape, pr, movement=True):
            logger.warning(
                f"User {pr.player.name} attempted to change options for a shape it does not own."
            )
            return
        shapes.append((shape, sh))

    if not data["temporary"]:
        with db.atomic():
            for db_shape, data_shape in shapes:
                db_shape.options = data_shape["option"]
                db_shape.save()

    await sio.emit(
        "Shapes.Options.Update",
        data["options"],
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 14
0
async def update_initiative_turn(sid, data):
    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 advance the initiative tracker")
        return

    location_data = InitiativeLocationData.get(location=location)
    with db.atomic():
        location_data.turn = data
        location_data.save()

        effects = (InitiativeEffect.select().join(Initiative).where(
            Initiative.uuid == data))
        for effect in effects:
            if effect.turns <= 0:
                effect.delete_instance()
            else:
                effect.turns -= 1
            effect.save()

    await sio.emit(
        "Initiative.Turn.Update",
        data,
        room=location.get_path(),
        skip_sid=sid,
        namespace="/planarally",
    )
Ejemplo n.º 15
0
async def set_initiative_sort(sid: str, sort: int):
    pr: PlayerRoom = game_state.get(sid)

    if pr.role != Role.DM:
        logger.warning(f"{pr.player.name} attempted to change initiative sort")
        return

    with db.atomic():
        location_data = Initiative.get(location=pr.active_location)
        location_data.sort = sort
        json_data = json.loads(location_data.data)

        json_data = sort_initiative(json_data, location_data.sort)

        location_data.data = json.dumps(json_data)
        location_data.save()

    await sio.emit(
        "Initiative.Sort.Set",
        room=pr.active_location.get_path(),
        namespace=GAME_NS,
    )
    await sio.emit(
        "Initiative.Set",
        location_data.as_dict(),
        room=pr.active_location.get_path(),
        namespace=GAME_NS,
    )
Ejemplo n.º 16
0
async def remove_initiative_effect(sid: str,
                                   data: ServerRemoveInitiativeEffectActor):
    pr: PlayerRoom = game_state.get(sid)

    if not has_ownership(Shape.get_or_none(uuid=data["shape"]), pr):
        logger.warning(
            f"{pr.player.name} attempted to remove an initiative effect")
        return

    location_data = Initiative.get(location=pr.active_location)
    with db.atomic():
        json_data = json.loads(location_data.data)

        for initiative in json_data:
            if initiative["shape"] == data["shape"]:
                initiative["effects"].pop(data["index"])

        location_data.data = json.dumps(json_data)
        location_data.save()

    await sio.emit(
        "Initiative.Effect.Remove",
        data,
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 17
0
async def remove_initiative(sid: str, data: str):
    pr: PlayerRoom = game_state.get(sid)

    shape = Shape.get_or_none(uuid=data)

    if shape is not None and not has_ownership(shape, pr):
        logger.warning(
            f"{pr.player.name} attempted to remove initiative of an asset it does not own"
        )
        return

    with db.atomic():
        location_data = Initiative.get(location=pr.active_location)
        json_data = json.loads(location_data.data)
        location_data.data = json.dumps([
            initiative for initiative in json_data
            if initiative["shape"] != data
        ])
        location_data.save()

    await sio.emit(
        "Initiative.Remove",
        data,
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 18
0
async def add_shape(sid, data):
    sid_data = state.sid_map[sid]
    user = sid_data["user"]
    room = sid_data["room"]
    location = sid_data["location"]

    if "temporary" not in data:
        data["temporary"] = False

    floor = location.floors.select().where(
        Floor.name == data["shape"]["floor"])[0]
    layer = floor.layers.where(Layer.name == data["shape"]["layer"])[0]

    if room.creator != user and not layer.player_editable:
        logger.warning(f"{user.name} attempted to add a shape to a dm layer")
        return
    if data["temporary"]:
        state.add_temp(sid, data["shape"]["uuid"])
    else:
        with db.atomic():
            data["shape"]["layer"] = layer
            data["shape"]["index"] = layer.shapes.count()
            # Shape itself
            shape = Shape.create(**reduce_data_to_model(Shape, data["shape"]))
            # Subshape
            type_table = get_table(shape.type_)
            type_table.create(shape=shape,
                              **reduce_data_to_model(type_table,
                                                     data["shape"]))
            # Owners
            ShapeOwner.create(shape=shape, user=user)
            # Trackers
            for tracker in data["shape"]["trackers"]:
                Tracker.create(**reduce_data_to_model(Tracker, tracker),
                               shape=shape)
            # Auras
            for aura in data["shape"]["auras"]:
                Aura.create(**reduce_data_to_model(Aura, aura), shape=shape)

    if layer.player_visible:
        for room_player in room.players:
            for psid in state.get_sids(user=room_player.player, room=room):
                if psid == sid:
                    continue
                if not data["temporary"]:
                    data["shape"] = shape.as_dict(room_player.player, False)
                await sio.emit("Shape.Add",
                               data["shape"],
                               room=psid,
                               namespace="/planarally")

    for csid in state.get_sids(user=room.creator, room=room):
        if csid == sid:
            continue
        if not data["temporary"]:
            data["shape"] = shape.as_dict(room.creator, True)
        await sio.emit("Shape.Add",
                       data["shape"],
                       room=csid,
                       namespace="/planarally")
Ejemplo n.º 19
0
async def set_initiative_value(sid: str, data: ServerSetInitiativeValue):
    pr: PlayerRoom = game_state.get(sid)

    shape = Shape.get_or_none(uuid=data)

    if shape is not None and not has_ownership(shape, pr):
        logger.warning(
            f"{pr.player.name} attempted to remove initiative of an asset it does not own"
        )
        return

    with db.atomic():
        location_data = Initiative.get(location=pr.active_location)
        json_data = json.loads(location_data.data)

        for initiative in json_data:
            if initiative["shape"] == data["shape"]:
                initiative["initiative"] = data["value"]
                break

        json_data = sort_initiative(json_data, location_data.sort)

        location_data.data = json.dumps(json_data)
        location_data.save()

    await sio.emit(
        "Initiative.Set",
        location_data.as_dict(),
        room=pr.active_location.get_path(),
        namespace=GAME_NS,
    )
Ejemplo n.º 20
0
async def update_initiative_turn(sid: str, data: str):
    pr: PlayerRoom = game_state.get(sid)

    if pr.role != Role.DM:
        logger.warning(
            f"{pr.player.name} attempted to advance the initiative tracker")
        return

    location_data = InitiativeLocationData.get(location=pr.active_location)
    with db.atomic():
        location_data.turn = data
        location_data.save()

        effects = (InitiativeEffect.select().join(Initiative).where(
            Initiative.uuid == data))
        for effect in effects:
            try:
                turns = int(effect.turns)
                if turns <= 0:
                    effect.delete_instance()
                else:
                    effect.turns = str(turns - 1)
            except ValueError:
                # For non-number inputs do not update the effect
                pass
            effect.save()

    await sio.emit(
        "Initiative.Turn.Update",
        data,
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )
Ejemplo n.º 21
0
async def set_client_room_options(sid: str, data: ClientOptions):
    pr: PlayerRoom = game_state.get(sid)

    with db.atomic():
        if pr.user_options is None:
            pr.user_options = UserOptions.create_empty()
            pr.save()

        UserOptions.update(**data).where(UserOptions.id == pr.user_options).execute()
Ejemplo n.º 22
0
async def create(request):
    user = await check_authorized(request)
    data = await request.json()
    roomname = data["name"]
    if not roomname:
        return web.HTTPBadRequest()
    else:
        with db.atomic():
            room = Room.create(name=roomname, creator=user)
            loc = Location.create(room=room, name="start")
            loc.create_floor()
            room.dm_location = loc.name
            room.player_location = loc.name
            room.save()
        return web.HTTPOk()
Ejemplo n.º 23
0
async def update_initiative_order(sid: int, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    if pr.role != Role.DM:
        logger.warning(
            f"{pr.player.name} attempted to change the initiative order")
        return

    with db.atomic():
        for i, uuid in enumerate(data):
            init = Initiative.get(uuid=uuid)
            init.index = i
            init.save()

    await send_client_initiatives(pr)
Ejemplo n.º 24
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()
Ejemplo n.º 25
0
async def create_room(request):
    user = await check_authorized(request)
    data = await request.post()
    roomname = data["room_name"]
    if not roomname:
        response = web.HTTPFound("/rooms")
    else:
        with db.atomic():
            room = Room.create(name=roomname, creator=user)
            loc = Location.create(room=room, name="start")
            loc.add_default_layers()
            room.dm_location = loc.name
            room.player_location = loc.name
            room.save()
        response = web.HTTPFound(f"/rooms/{user.name}/{roomname}")
    return response
Ejemplo n.º 26
0
async def update_note(sid: str, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    note = Note.get_or_none(uuid=data["uuid"])

    if not note:
        logger.warning(
            f"{pr.player.name} tried to update non-existant note with id: '{data['uuid']}'"
        )
        return

    if note.user != pr.player:
        logger.warn(f"{pr.player.name} tried to update note not belonging to him/her.")
    else:
        with db.atomic():
            note.title = data["title"]
            note.text = data["text"]
            note.save()
Ejemplo n.º 27
0
async def update_initiative_order(sid, data):
    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 change the initiative order")
        return

    location_data = InitiativeLocationData.get(location=location)

    with db.atomic():
        for i, uuid in enumerate(data):
            init = Initiative.get(uuid=uuid)
            init.index = i
            init.save()

    await send_client_initiatives(room, location)
Ejemplo n.º 28
0
async def create(request):
    user = await check_authorized(request)
    data = await request.json()
    roomname = data["name"]
    if not roomname:
        return web.HTTPBadRequest()
    else:
        with db.atomic():
            default_options = LocationOptions.create()
            room = Room.create(name=roomname,
                               creator=user,
                               default_options=default_options)
            loc = Location.create(room=room, name="start", index=1)
            loc.create_floor()
            PlayerRoom.create(player=user,
                              room=room,
                              role=Role.DM,
                              active_location=loc)
            room.save()
        return web.HTTPOk()
Ejemplo n.º 29
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()
Ejemplo n.º 30
0
async def update_initiative_round(sid: int, data: Dict[str, Any]):
    pr: PlayerRoom = game_state.get(sid)

    if pr.role != Role.DM:
        logger.warning(
            f"{pr.player.name} attempted to advance the initiative tracker")
        return

    location_data = InitiativeLocationData.get(location=pr.active_location)
    with db.atomic():
        location_data.round = data
        location_data.save()

    await sio.emit(
        "Initiative.Round.Update",
        data,
        room=pr.active_location.get_path(),
        skip_sid=sid,
        namespace=GAME_NS,
    )