space

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 12, 2024 License: MIT Imports: 4 Imported by: 0

README

Space

Go doc

计划提供游戏中常见的空间设计,例如房间、地图等。开发者可以使用它来快速构建游戏中的常见空间,例如多人房间、地图等。

Room 房间

房间在 Minotaur 中仅仅只是一个可以为任意可比较类型的 ID,当需要将现有或新设计的房间纳入 RoomManager 管理时,需要实现 Room 管理时,仅需要实现 generic.IdR 接口即可。

该功能由 RoomManagerRoomController 组成。

当创建一个新的房间并纳入 RoomManager 管理后,将会得到一个 RoomController。通过 RoomController 可以对房间进行管理,例如:获取房间信息、加入房间、退出房间等。

使用示例
package main

import (
    "fmt"
    "github.com/kercylan98/minotaur/game/space"
)

type Room struct {
	Id int64
}

func (r *Room) GetId() int64 {
	return r.Id
}

type Player struct {
	Id string
}

func (p *Player) GetId() string {
	return p.Id
}

func main() {
	var rm = space.NewRoomManager[string, int64, *Player, *Room]()
	var room = &Room{Id: 1}
	var controller = rm.AssumeControl(room)

	if err := controller.AddEntity(&Player{Id: "1"}); err != nil {
		// 房间密码不匹配或者房间已满
		panic(err)
	}

	fmt.Println(controller.GetEntityCount()) // 1
}

Documentation

Overview

Package space 游戏中常见的空间设计,例如房间、地图等

Index

Examples

Constants

View Source
const UnknownSeat = -1 // 未知座位号

Variables

View Source
var (
	// ErrRoomFull 房间已满
	ErrRoomFull = errors.New("room is full")
	// ErrSeatNotEmpty 座位上已经有实体
	ErrSeatNotEmpty = errors.New("seat is not empty")
	// ErrNotInRoom 实体不在房间中
	ErrNotInRoom = errors.New("not in room")
	// ErrRoomPasswordNotMatch 房间密码不匹配
	ErrRoomPasswordNotMatch = errors.New("room password not match")
	// ErrPermissionDenied 权限不足
	ErrPermissionDenied = errors.New("permission denied")
)

Functions

This section is empty.

Types

type RoomAddEntityEventHandle

type RoomAddEntityEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity)

type RoomAssumeControlEventHandle

type RoomAssumeControlEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room])

type RoomChangePasswordEventHandle

type RoomChangePasswordEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], oldPassword, password *string)

type RoomController

type RoomController[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
	// contains filtered or unexported fields
}

RoomController 对房间进行操作的控制器,由 RoomManager 接管后返回

func (*RoomController[EntityID, RoomID, Entity, Room]) AddEntity

func (rc *RoomController[EntityID, RoomID, Entity, Room]) AddEntity(entity Entity) error

AddEntity 添加实体,如果房间存在密码,应使用 AddEntityByPassword 函数进行添加,否则将始终返回 ErrRoomPasswordNotMatch 错误

  • 当房间已满时,将会返回 ErrRoomFull 错误

func (*RoomController[EntityID, RoomID, Entity, Room]) AddEntityByPassword

func (rc *RoomController[EntityID, RoomID, Entity, Room]) AddEntityByPassword(entity Entity, password string) error

AddEntityByPassword 通过房间密码添加实体到该房间中

  • 当未设置房间密码时,password 参数将会被忽略
  • 当房间密码不匹配时,将会返回 ErrRoomPasswordNotMatch 错误
  • 当房间已满时,将会返回 ErrRoomFull 错误

func (*RoomController[EntityID, RoomID, Entity, Room]) Broadcast

func (rc *RoomController[EntityID, RoomID, Entity, Room]) Broadcast(handler func(Entity), conditions ...func(Entity) bool)

Broadcast 广播,该函数会将所有房间中满足 conditions 的对象传入 handler 中进行处理

func (*RoomController[EntityID, RoomID, Entity, Room]) ChangePassword

func (rc *RoomController[EntityID, RoomID, Entity, Room]) ChangePassword(password *string)

ChangePassword 修改房间密码

  • 当房间密码为 nil 时,将会取消密码

func (*RoomController[EntityID, RoomID, Entity, Room]) ContainEntity

func (rc *RoomController[EntityID, RoomID, Entity, Room]) ContainEntity(id EntityID) bool

ContainEntity 房间内是否包含实体

func (*RoomController[EntityID, RoomID, Entity, Room]) DelOwner added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) DelOwner()

DelOwner 删除房主,将房间设置为无主的状态

func (*RoomController[EntityID, RoomID, Entity, Room]) Destroy

func (rc *RoomController[EntityID, RoomID, Entity, Room]) Destroy()

Destroy 销毁房间,房间会从 RoomManager 中移除,同时所有房间的实体、座位等数据都会被清空

  • 该函数与 RoomManager.DestroyRoom 相同,RoomManager.DestroyRoom 函数为该函数的快捷方式

func (*RoomController[EntityID, RoomID, Entity, Room]) GetEmptySeat

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEmptySeat() []int

GetEmptySeat 获取空座位

  • 空座位需要在有对象离开座位后才可能出现

func (*RoomController[EntityID, RoomID, Entity, Room]) GetEntities

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntities() map[EntityID]Entity

GetEntities 获取所有实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetEntity

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntity(id EntityID) Entity

GetEntity 获取实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetEntityCount

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntityCount() int

GetEntityCount 获取实体数量

func (*RoomController[EntityID, RoomID, Entity, Room]) GetEntityExist added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntityExist(id EntityID) (Entity, bool)

GetEntityExist 获取实体,并返回实体是否存在的状态

func (*RoomController[EntityID, RoomID, Entity, Room]) GetEntityIDs

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetEntityIDs() []EntityID

GetEntityIDs 获取所有实体ID

func (*RoomController[EntityID, RoomID, Entity, Room]) GetFirstEmptySeatEntity added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetFirstEmptySeatEntity() (entity Entity)

GetFirstEmptySeatEntity 获取第一个空座位上的实体,如果没有空座位,将返回空实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetFirstNotEmptySeat added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetFirstNotEmptySeat() int

GetFirstNotEmptySeat 获取第一个非空座位号,如果没有非空座位,将返回 UnknownSeat

func (*RoomController[EntityID, RoomID, Entity, Room]) GetNotEmptySeat

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetNotEmptySeat() []int

GetNotEmptySeat 获取非空座位

func (*RoomController[EntityID, RoomID, Entity, Room]) GetOwner added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetOwner() Entity

GetOwner 获取房主

func (*RoomController[EntityID, RoomID, Entity, Room]) GetOwnerExist added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetOwnerExist() (Entity, bool)

GetOwnerExist 获取房间,并返回房主是否存在的状态

func (*RoomController[EntityID, RoomID, Entity, Room]) GetOwnerID added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetOwnerID() EntityID

GetOwnerID 获取房主 ID

func (*RoomController[EntityID, RoomID, Entity, Room]) GetRandomEntity added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRandomEntity() (entity Entity)

GetRandomEntity 获取随机实体,如果房间中没有实体,将返回空实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetRoom

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRoom() Room

GetRoom 获取原始房间实例,该实例为被接管的房间的原始实例

func (*RoomController[EntityID, RoomID, Entity, Room]) GetRoomID

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRoomID() RoomID

GetRoomID 获取房间 ID

func (*RoomController[EntityID, RoomID, Entity, Room]) GetRoomManager

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetRoomManager() *RoomManager[EntityID, RoomID, Entity, Room]

GetRoomManager 获取该房间控制器所属的房间管理器

func (*RoomController[EntityID, RoomID, Entity, Room]) GetSeat

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeat(entityId EntityID) int

GetSeat 获取座位

func (*RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntities

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntities() map[EntityID]Entity

GetSeatEntities 获取座位上的实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntitiesByOrdered

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntitiesByOrdered() []Entity

GetSeatEntitiesByOrdered 有序的获取座位上的实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntitiesByOrderedAndContainsEmpty

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntitiesByOrderedAndContainsEmpty() []Entity

GetSeatEntitiesByOrderedAndContainsEmpty 获取有序的座位上的实体,包含空座位

func (*RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntity

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntity(seat int) (entity Entity)

GetSeatEntity 获取座位上的实体

func (*RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntityCount

func (rc *RoomController[EntityID, RoomID, Entity, Room]) GetSeatEntityCount() int

GetSeatEntityCount 获取座位上的实体数量

func (*RoomController[EntityID, RoomID, Entity, Room]) HasEntity

func (rc *RoomController[EntityID, RoomID, Entity, Room]) HasEntity(id EntityID) bool

HasEntity 判断是否有实体

func (*RoomController[EntityID, RoomID, Entity, Room]) HasOwner added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) HasOwner() bool

HasOwner 判断是否有房主

func (*RoomController[EntityID, RoomID, Entity, Room]) HasSeat

func (rc *RoomController[EntityID, RoomID, Entity, Room]) HasSeat(entityId EntityID) bool

HasSeat 判断是否有座位

func (*RoomController[EntityID, RoomID, Entity, Room]) IsOwner added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) IsOwner(entityId EntityID) bool

IsOwner 判断是否为房主

func (*RoomController[EntityID, RoomID, Entity, Room]) JoinSeat

func (rc *RoomController[EntityID, RoomID, Entity, Room]) JoinSeat(entityId EntityID, seat ...int) error

JoinSeat 设置特定对象加入座位,当具体的座位不存在的时候,将会自动分配座位

  • 当目标座位存在玩家或未添加到房间中的时候,将会返回错误

func (*RoomController[EntityID, RoomID, Entity, Room]) LeaveSeat

func (rc *RoomController[EntityID, RoomID, Entity, Room]) LeaveSeat(entityId EntityID)

LeaveSeat 离开座位

func (*RoomController[EntityID, RoomID, Entity, Room]) RemoveAllEntities

func (rc *RoomController[EntityID, RoomID, Entity, Room]) RemoveAllEntities()

RemoveAllEntities 移除该房间中的所有实体

  • 当实体被移除时如果实体在座位上,将会自动离开座位
  • 如果实体为房主,将会根据 RoomControllerOptions.WithOwnerInherit 函数的设置进行继承

func (*RoomController[EntityID, RoomID, Entity, Room]) RemoveEntity

func (rc *RoomController[EntityID, RoomID, Entity, Room]) RemoveEntity(id EntityID)

RemoveEntity 移除实体

  • 当实体被移除时如果实体在座位上,将会自动离开座位
  • 如果实体为房主,将会根据 RoomControllerOptions.WithOwnerInherit 函数的设置进行继承

func (*RoomController[EntityID, RoomID, Entity, Room]) SetOwner added in v0.4.1

func (rc *RoomController[EntityID, RoomID, Entity, Room]) SetOwner(entityId EntityID)

SetOwner 设置房主

type RoomControllerOptions

type RoomControllerOptions[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
	// contains filtered or unexported fields
}

func NewRoomControllerOptions

func NewRoomControllerOptions[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]]() *RoomControllerOptions[EntityID, RoomID, Entity, Room]

NewRoomControllerOptions 创建房间控制器选项

func (*RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithMaxEntityCount

func (rco *RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithMaxEntityCount(maxEntityCount int) *RoomControllerOptions[EntityID, RoomID, Entity, Room]

WithMaxEntityCount 设置房间最大实体数量

func (*RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithOwnerInherit added in v0.4.1

func (rco *RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithOwnerInherit(inherit bool, inheritHandler ...func(controller *RoomController[EntityID, RoomID, Entity, Room]) *EntityID) *RoomControllerOptions[EntityID, RoomID, Entity, Room]

WithOwnerInherit 设置房间所有者是否继承,默认为 false

  • inherit: 是否继承,当未设置 inheritHandler 且 inherit 为 true 时,将会按照随机或根据座位号顺序继承房间所有者
  • inheritHandler: 继承处理函数,当 inherit 为 true 时,该函数将会被调用,传入当前房间中的所有实体,返回值为新的房间所有者

func (*RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithPassword

func (rco *RoomControllerOptions[EntityID, RoomID, Entity, Room]) WithPassword(password string) *RoomControllerOptions[EntityID, RoomID, Entity, Room]

WithPassword 设置房间密码

type RoomDestroyEventHandle

type RoomDestroyEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room])

type RoomManager

type RoomManager[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] struct {
	// contains filtered or unexported fields
}

RoomManager 房间管理器是用于对房间进行管理的基本单元,通过该实例可以对房间进行增删改查等操作

  • 该实例是线程安全的

func NewRoomManager

func NewRoomManager[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]]() *RoomManager[EntityID, RoomID, Entity, Room]

NewRoomManager 创建房间管理器 RoomManager 的实例

Example
package main

import (
	"fmt"
	"github.com/kercylan98/minotaur/game/space"
)

type Room struct {
	Id int64
}

func (r *Room) GetId() int64 {
	return r.Id
}

type Player struct {
	Id string
}

func (p *Player) GetId() string {
	return p.Id
}

func main() {
	var rm = space.NewRoomManager[string, int64, *Player, *Room]()
	fmt.Println(rm == nil)

}
Output:

false

func (*RoomManager[EntityID, RoomID, Entity, Room]) AssumeControl

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) AssumeControl(room Room, options ...*RoomControllerOptions[EntityID, RoomID, Entity, Room]) *RoomController[EntityID, RoomID, Entity, Room]

AssumeControl 将房间控制权交由 RoomManager 接管,返回 RoomController 实例

  • 当任何房间需要被 RoomManager 管理时,都应该调用该方法获取到 RoomController 实例后进行操作
  • 房间被接管后需要在释放房间控制权时调用 RoomController.Destroy 方法,否则将会导致 RoomManager 一直持有房间资源
Example
package main

import (
	"fmt"
	"github.com/kercylan98/minotaur/game/space"
)

type Room struct {
	Id int64
}

func (r *Room) GetId() int64 {
	return r.Id
}

type Player struct {
	Id string
}

func (p *Player) GetId() string {
	return p.Id
}

func main() {
	var rm = space.NewRoomManager[string, int64, *Player, *Room]()
	var room = &Room{Id: 1}
	var controller = rm.AssumeControl(room)

	if err := controller.AddEntity(&Player{Id: "1"}); err != nil {
		// 房间密码不匹配或者房间已满
		panic(err)
	}

	fmt.Println(controller.GetEntityCount())

}
Output:

1

func (*RoomManager[EntityID, RoomID, Entity, Room]) Broadcast

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) Broadcast(handler func(Entity), conditions ...func(Entity) bool)

Broadcast 向所有房间对象广播消息,该方法将会遍历所有房间控制器并调用 RoomController.Broadcast 方法

func (*RoomManager[EntityID, RoomID, Entity, Room]) DestroyRoom

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) DestroyRoom(id RoomID)

DestroyRoom 销毁房间,该函数为 RoomController.Destroy 的快捷方式

func (*RoomManager[EntityID, RoomID, Entity, Room]) GetEntityRooms

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetEntityRooms(entityId EntityID) map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]

GetEntityRooms 获取特定对象所在的房间,返回值为房间 ID 到对应控制器 RoomController 的映射

  • 由于一个对象可能在多个房间中,因此返回值为 map 类型

func (*RoomManager[EntityID, RoomID, Entity, Room]) GetRoom

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRoom(id RoomID) *RoomController[EntityID, RoomID, Entity, Room]

GetRoom 通过房间 ID 获取对应房间的控制器 RoomController,当房间不存在时将返回 nil

func (*RoomManager[EntityID, RoomID, Entity, Room]) GetRoomCount

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRoomCount() int

GetRoomCount 获取房间管理器接管的房间数量

func (*RoomManager[EntityID, RoomID, Entity, Room]) GetRoomIDs

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRoomIDs() []RoomID

GetRoomIDs 获取房间管理器接管的所有房间 ID

func (*RoomManager[EntityID, RoomID, Entity, Room]) GetRooms

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) GetRooms() map[RoomID]*RoomController[EntityID, RoomID, Entity, Room]

GetRooms 获取包含所有房间 ID 到对应控制器 RoomController 的映射

  • 返回值的 map 为拷贝对象,可安全的对其进行增删等操作

func (*RoomManager[EntityID, RoomID, Entity, Room]) HasEntity

func (rm *RoomManager[EntityID, RoomID, Entity, Room]) HasEntity(entityId EntityID) bool

HasEntity 判断特定对象是否在任一房间中,当对象不在任一房间中时将返回 false

func (RoomManager) OnRoomAddEntityEvent

func (rme RoomManager) OnRoomAddEntityEvent(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity)

OnRoomAddEntityEvent 房间添加对象事件

func (RoomManager) OnRoomAssumeControlEvent

func (rme RoomManager) OnRoomAssumeControlEvent(controller *RoomController[EntityID, RoomID, Entity, Room])

OnRoomAssumeControlEvent 房间接管事件

func (RoomManager) OnRoomChangePasswordEvent

func (rme RoomManager) OnRoomChangePasswordEvent(controller *RoomController[EntityID, RoomID, Entity, Room], oldPassword, password *string)

OnRoomChangePasswordEvent 房间修改密码事件

func (RoomManager) OnRoomDestroyEvent

func (rme RoomManager) OnRoomDestroyEvent(controller *RoomController[EntityID, RoomID, Entity, Room])

OnRoomDestroyEvent 房间销毁事件

func (RoomManager) OnRoomOwnerChangeEvent added in v0.4.1

func (rme RoomManager) OnRoomOwnerChangeEvent(controller *RoomController[EntityID, RoomID, Entity, Room], oldOwner, owner *EntityID)

OnRoomOwnerChangeEvent 房间所有者变更事件

func (RoomManager) OnRoomRemoveEntityEvent

func (rme RoomManager) OnRoomRemoveEntityEvent(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity)

OnRoomRemoveEntityEvent 房间移除对象事件

func (RoomManager) RegRoomAddEntityEvent

func (rme RoomManager) RegRoomAddEntityEvent(handle RoomAddEntityEventHandle[EntityID, RoomID, Entity, Room])

RegRoomAddEntityEvent 注册房间添加对象事件

func (RoomManager) RegRoomAssumeControlEvent

func (rme RoomManager) RegRoomAssumeControlEvent(handle RoomAssumeControlEventHandle[EntityID, RoomID, Entity, Room])

RegRoomAssumeControlEvent 注册房间接管事件

func (RoomManager) RegRoomChangePasswordEvent

func (rme RoomManager) RegRoomChangePasswordEvent(handle RoomChangePasswordEventHandle[EntityID, RoomID, Entity, Room])

RegRoomChangePasswordEvent 注册房间修改密码事件

func (RoomManager) RegRoomDestroyEvent

func (rme RoomManager) RegRoomDestroyEvent(handle RoomDestroyEventHandle[EntityID, RoomID, Entity, Room])

RegRoomDestroyEvent 注册房间销毁事件

func (RoomManager) RegRoomOwnerChangeEvent added in v0.4.1

func (rme RoomManager) RegRoomOwnerChangeEvent(handle RoomOwnerChangeEventHandle[EntityID, RoomID, Entity, Room])

RegRoomOwnerChangeEvent 注册房间所有者变更事件,当触发事件时,房间所有者已经被修改

func (RoomManager) RegRoomRemoveEntityEvent

func (rme RoomManager) RegRoomRemoveEntityEvent(handle RoomRemoveEntityEventHandle[EntityID, RoomID, Entity, Room])

RegRoomRemoveEntityEvent 注册房间移除对象事件

type RoomOwnerChangeEventHandle added in v0.4.1

type RoomOwnerChangeEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], oldOwner, owner *EntityID)

type RoomRemoveEntityEventHandle

type RoomRemoveEntityEventHandle[EntityID comparable, RoomID comparable, Entity generic.IdR[EntityID], Room generic.IdR[RoomID]] func(controller *RoomController[EntityID, RoomID, Entity, Room], entity Entity)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL