diff --git a/pkg/network/webrtc/webrtc.go b/pkg/network/webrtc/webrtc.go
index 92e5b0074..72699a175 100644
--- a/pkg/network/webrtc/webrtc.go
+++ b/pkg/network/webrtc/webrtc.go
@@ -17,6 +17,7 @@ type Peer struct {
log *logger.Logger
OnMessage func(data []byte)
OnKeyboard func(data []byte)
+ OnMouse func(data []byte)
a *webrtc.TrackLocalStaticSample
v *webrtc.TrackLocalStaticSample
@@ -109,6 +110,17 @@ func (p *Peer) NewCall(vCodec, aCodec string, onICECandidate func(ice any)) (sdp
})
p.log.Debug().Msg("Added [keyboard] chan")
+ mChan, err := p.addDataChannel("mouse")
+ if err != nil {
+ return "", err
+ }
+ mChan.OnMessage(func(m webrtc.DataChannelMessage) {
+ if p.OnMouse != nil {
+ p.OnMouse(m.Data)
+ }
+ })
+ p.log.Debug().Msg("Added [mouse] chan")
+
p.conn.OnICEConnectionStateChange(p.handleICEState(func() { p.log.Info().Msg("Connected") }))
// Stream provider supposes to send offer
offer, err := p.conn.CreateOffer(nil)
diff --git a/pkg/worker/caged/app/app.go b/pkg/worker/caged/app/app.go
index 8bf008eca..fc37827eb 100644
--- a/pkg/worker/caged/app/app.go
+++ b/pkg/worker/caged/app/app.go
@@ -14,6 +14,7 @@ type App interface {
SetDataCb(func([]byte))
InputGamepad(port int, data []byte)
InputKeyboard(port int, data []byte)
+ InputMouse(port int, data []byte)
}
type Audio struct {
diff --git a/pkg/worker/caged/libretro/caged.go b/pkg/worker/caged/libretro/caged.go
index e2c8532f5..979868e12 100644
--- a/pkg/worker/caged/libretro/caged.go
+++ b/pkg/worker/caged/libretro/caged.go
@@ -88,6 +88,7 @@ func (c *Caged) ViewportSize() (int, int) { return c.base.ViewportSiz
func (c *Caged) Scale() float64 { return c.Emulator.Scale() }
func (c *Caged) InputGamepad(port int, data []byte) { c.base.Input(port, RetroPad, data) }
func (c *Caged) InputKeyboard(port int, data []byte) { c.base.Input(port, Keyboard, data) }
+func (c *Caged) InputMouse(port int, data []byte) { c.base.Input(port, Mouse, data) }
func (c *Caged) Start() { go c.Emulator.Start() }
func (c *Caged) SetSaveOnClose(v bool) { c.base.SaveOnClose = v }
func (c *Caged) SetSessionId(name string) { c.base.SetSessionId(name) }
diff --git a/pkg/worker/caged/libretro/frontend.go b/pkg/worker/caged/libretro/frontend.go
index ce527779f..d46316800 100644
--- a/pkg/worker/caged/libretro/frontend.go
+++ b/pkg/worker/caged/libretro/frontend.go
@@ -73,6 +73,7 @@ type Device byte
const (
RetroPad = Device(nanoarch.RetroPad)
Keyboard = Device(nanoarch.Keyboard)
+ Mouse = Device(nanoarch.Mouse)
)
var (
@@ -300,6 +301,8 @@ func (f *Frontend) Input(port int, d Device, data []byte) {
f.nano.InputRetropad(port, data)
case Keyboard:
f.nano.InputKeyboard(port, data)
+ case Mouse:
+ f.nano.InputMouse(port, data)
}
}
diff --git a/pkg/worker/caged/libretro/nanoarch/input.go b/pkg/worker/caged/libretro/nanoarch/input.go
index f13591fb9..5bd033923 100644
--- a/pkg/worker/caged/libretro/nanoarch/input.go
+++ b/pkg/worker/caged/libretro/nanoarch/input.go
@@ -12,8 +12,6 @@ import "C"
const KeyPressed = C.int16_t(1)
const KeyReleased = C.int16_t(0)
-const RetroDeviceTypeShift = 8
-
// InputState stores full controller state.
// It consists of:
// - uint16 button values
@@ -29,6 +27,14 @@ type (
mod uint16
mu sync.Mutex
}
+ MouseState struct {
+ x, y int16
+ mp sync.Mutex
+ l bool
+ r bool
+ m bool
+ mb sync.Mutex
+ }
)
type Device byte
@@ -36,6 +42,12 @@ type Device byte
const (
RetroPad Device = iota
Keyboard
+ Mouse
+)
+
+const (
+ MouseMove = iota
+ MouseButton
)
const (
@@ -64,8 +76,6 @@ func (s *InputState) IsDpadTouched(port uint, axis uint) (shift C.int16_t) {
return C.int16_t(atomic.LoadInt32(&s[port].axes[axis]))
}
-func RetroDeviceSubclass(base, id int) int { return ((id + 1) << RetroDeviceTypeShift) | base }
-
func NewKeyboardState() KeyboardState {
return KeyboardState{
keys: make(map[uint]struct{}),
@@ -73,15 +83,35 @@ func NewKeyboardState() KeyboardState {
}
}
-func (ks *KeyboardState) Set(press bool, key uint, mod uint16) {
+// SetKey sets keyboard state.
+//
+// data format
+//
+// 0 1 2 3 4 5 6
+// [ KEY ] P MOD
+//
+// KEY contains Libretro code of the keyboard key (4 bytes).
+// P contains 0 or 1 if the key is pressed (1 byte).
+// MOD contains bitmask for Alt | Ctrl | Meta | Shift keys press state (2 bytes).
+//
+// Returns decoded state from the input bytes.
+func (ks *KeyboardState) SetKey(data []byte) (pressed bool, key uint, mod uint16) {
+ if len(data) != 7 {
+ return
+ }
+
+ pressed = data[4] == 1
+ key = uint(binary.BigEndian.Uint32(data))
+ mod = binary.BigEndian.Uint16(data[5:])
ks.mu.Lock()
- if press {
+ if pressed {
ks.keys[key] = struct{}{}
} else {
delete(ks.keys, key)
}
ks.mod = mod
ks.mu.Unlock()
+ return
}
func (ks *KeyboardState) Pressed(key uint) C.int16_t {
@@ -94,9 +124,69 @@ func (ks *KeyboardState) Pressed(key uint) C.int16_t {
return KeyReleased
}
-func decodeKeyboardState(data []byte) (bool, uint, uint16) {
- press := data[4] == 1
- key := uint(binary.LittleEndian.Uint32(data))
- mod := binary.LittleEndian.Uint16(data[5:])
- return press, key, mod
+// ShiftPos sets mouse relative position state.
+//
+// data format
+//
+// 0 1 2 3
+// [x] [y]
+//
+// x and y contain relative (to the previous values)
+// X, Y positions as signed int.
+func (ms *MouseState) ShiftPos(data []byte) {
+ if len(data) != 4 {
+ return
+ }
+ dx := int16(data[0])<<8 + int16(data[1])
+ dy := int16(data[2])<<8 + int16(data[3])
+ ms.mp.Lock()
+ ms.x += dx
+ ms.y += dy
+ ms.mp.Unlock()
+}
+
+func (ms *MouseState) PopX() C.int16_t {
+ var x int16
+ ms.mp.Lock()
+ x = ms.x
+ ms.x = 0
+ ms.mp.Unlock()
+ return C.int16_t(x)
+}
+
+func (ms *MouseState) PopY() C.int16_t {
+ var y int16
+ ms.mp.Lock()
+ y = ms.y
+ ms.y = 0
+ ms.mp.Unlock()
+ return C.int16_t(y)
+}
+
+// SetButtons sets the state of mouse buttons.
+//
+// data format
+//
+// 0 1 2
+// L R M
+//
+// L is the left button, R is right, and M is the middle button.
+func (ms *MouseState) SetButtons(data []byte) {
+ if len(data) != 3 {
+ return
+ }
+ ms.mb.Lock()
+ ms.l = data[0] == 1
+ ms.r = data[1] == 1
+ ms.m = data[2] == 1
+ ms.mb.Unlock()
+}
+
+func (ms *MouseState) Buttons() (l, r, m bool) {
+ ms.mb.Lock()
+ l = ms.l
+ r = ms.r
+ m = ms.m
+ ms.mb.Unlock()
+ return
}
diff --git a/pkg/worker/caged/libretro/nanoarch/nanoarch.go b/pkg/worker/caged/libretro/nanoarch/nanoarch.go
index 81af66c50..a9f35db5b 100644
--- a/pkg/worker/caged/libretro/nanoarch/nanoarch.go
+++ b/pkg/worker/caged/libretro/nanoarch/nanoarch.go
@@ -37,10 +37,10 @@ var (
type Nanoarch struct {
Handlers
- // keyboard keeps pressed buttons for input poll
keyboard KeyboardState
+ mouse MouseState
+ retropad InputState
- retropad InputState
keyboardCb *C.struct_retro_keyboard_callback
LastFrameTime int64
LibCo bool
@@ -169,6 +169,8 @@ func (n *Nanoarch) CoreLoad(meta Metadata) {
n.keyboardCb = nil
n.keyboard = NewKeyboardState()
+ n.mouse = MouseState{}
+
n.options = &meta.Options
filePath := meta.LibPath
@@ -381,12 +383,26 @@ func (n *Nanoarch) InputKeyboard(_ int, data []byte) {
return
}
- press, key, mod := decodeKeyboardState(data)
-
- n.keyboard.Set(press, key, mod)
- C.bridge_retro_keyboard_callback(unsafe.Pointer(n.keyboardCb), C.bool(press),
+ // we should preserve the state of pressed buttons for the input poll function (each retro_run)
+ // and explicitly call the retro_keyboard_callback function when a keyboard event happens
+ pressed, key, mod := n.keyboard.SetKey(data)
+ C.bridge_retro_keyboard_callback(unsafe.Pointer(n.keyboardCb), C.bool(pressed),
C.unsigned(key), C.uint32_t(0), C.uint16_t(mod))
}
+func (n *Nanoarch) InputMouse(_ int, data []byte) {
+ if len(data) == 0 {
+ return
+ }
+
+ t := data[0]
+ state := data[1:]
+ switch t {
+ case MouseMove:
+ n.mouse.ShiftPos(state)
+ case MouseButton:
+ n.mouse.SetButtons(state)
+ }
+}
func videoSetPixelFormat(format uint32) (C.bool, error) {
switch format {
@@ -615,10 +631,6 @@ func coreInputPoll() {}
func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
//Nan0.log.Debug().Msgf("%v %v %v %v", port, device, index, id)
- //if uint(port) >= uint(MaxPort) {
- // return KeyReleased
- //}
-
switch device {
case C.RETRO_DEVICE_JOYPAD:
return Nan0.retropad.IsKeyPressed(uint(port), int(id))
@@ -631,6 +643,33 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
}
case C.RETRO_DEVICE_KEYBOARD:
return Nan0.keyboard.Pressed(uint(id))
+ case C.RETRO_DEVICE_MOUSE:
+ switch id {
+ case C.RETRO_DEVICE_ID_MOUSE_X:
+ x := Nan0.mouse.PopX()
+ return x
+ case C.RETRO_DEVICE_ID_MOUSE_Y:
+ y := Nan0.mouse.PopY()
+ return y
+ case C.RETRO_DEVICE_ID_MOUSE_LEFT:
+ if l, _, _ := Nan0.mouse.Buttons(); l {
+ return KeyPressed
+ }
+ case C.RETRO_DEVICE_ID_MOUSE_RIGHT:
+ if _, r, _ := Nan0.mouse.Buttons(); r {
+ return KeyPressed
+ }
+ case C.RETRO_DEVICE_ID_MOUSE_WHEELUP:
+ case C.RETRO_DEVICE_ID_MOUSE_WHEELDOWN:
+ case C.RETRO_DEVICE_ID_MOUSE_MIDDLE:
+ if _, _, m := Nan0.mouse.Buttons(); m {
+ return KeyPressed
+ }
+ case C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP:
+ case C.RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN:
+ case C.RETRO_DEVICE_ID_MOUSE_BUTTON_4:
+ case C.RETRO_DEVICE_ID_MOUSE_BUTTON_5:
+ }
}
return KeyReleased
@@ -791,7 +830,7 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
}
cInfo.WriteString(fmt.Sprintf("%v: %v%s", cd[i].id, C.GoString(cd[i].desc), delim))
}
- Nan0.log.Debug().Msgf("%v", cInfo.String())
+ //Nan0.log.Debug().Msgf("%v", cInfo.String())
}
return true
case C.RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS:
diff --git a/pkg/worker/coordinatorhandlers.go b/pkg/worker/coordinatorhandlers.go
index ca170fecb..c37e2377b 100644
--- a/pkg/worker/coordinatorhandlers.go
+++ b/pkg/worker/coordinatorhandlers.go
@@ -172,6 +172,7 @@ func (c *coordinator) HandleGameStart(rq api.StartGameRequest[com.Uid], w *Worke
s := room.WithWebRTC(user.Session)
s.OnMessage = func(data []byte) { r.App().InputGamepad(user.Index, data) }
s.OnKeyboard = func(data []byte) { r.App().InputKeyboard(user.Index, data) }
+ s.OnMouse = func(data []byte) { r.App().InputMouse(user.Index, data) }
c.RegisterRoom(r.Id())
diff --git a/web/index.html b/web/index.html
index 952614920..c6a6666ee 100644
--- a/web/index.html
+++ b/web/index.html
@@ -120,25 +120,24 @@
Options
-
-
+
-
-
+
-
+
-
-
+
+
+
diff --git a/web/js/api/api.js b/web/js/api/api.js
index 20de8312b..93d219b59 100644
--- a/web/js/api/api.js
+++ b/web/js/api/api.js
@@ -1,3 +1,5 @@
+let libretro = {};
+
/**
* Server API.
*
@@ -25,6 +27,11 @@ const api = (() => {
APP_VIDEO_CHANGE: 150,
});
+ const mouse = {
+ MOVEMENT: 0,
+ BUTTONS: 1
+ }
+
const packet = (type, payload, id) => {
const packet = {t: type};
if (id !== undefined) packet.id = id;
@@ -35,36 +42,295 @@ const api = (() => {
const decodeBytes = (b) => String.fromCharCode.apply(null, new Uint8Array(b))
+ const keyboardPress = (pressed = false, e) => {
+ const buffer = new ArrayBuffer(7);
+ const dv = new DataView(buffer);
+
+ // 0 1 2 3 4 5 6
+ dv.setUint8(4, pressed ? 1 : 0)
+ const k = libretro.map('', e.code);
+ dv.setUint32(0, k)
+
+ // meta keys
+ let mod = 0;
+
+ e.altKey && (mod |= libretro.mod.ALT)
+ e.ctrlKey && (mod |= libretro.mod.CTRL)
+ e.metaKey && (mod |= libretro.mod.META)
+ e.shiftKey && (mod |= libretro.mod.SHIFT)
+
+ dv.setUint16(5, mod)
+
+ webrtc.keyboard(buffer);
+ }
+
+ const mouseMove = (() => {
+ // 0 1 2 3 4
+ // T DX DY
+ const buffer = new ArrayBuffer(5);
+ const dv = new DataView(buffer);
+
+ return (dx = 0, dy = 0) => {
+ dv.setUint8(0, mouse.MOVEMENT);
+ dv.setInt16(1, dx);
+ dv.setInt16(3, dy);
+ webrtc.mouse(buffer);
+ }
+ })();
+
+ const mousePress = (() => {
+ // 0 1 2 3
+ // T L R M
+ const buffer = new ArrayBuffer(4);
+ const dv = new DataView(buffer);
+
+ // #define RETRO_DEVICE_ID_MOUSE_LEFT 2
+ // #define RETRO_DEVICE_ID_MOUSE_RIGHT 3
+ // #define RETRO_DEVICE_ID_MOUSE_WHEELUP 4
+ // #define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5
+ // #define RETRO_DEVICE_ID_MOUSE_MIDDLE 6
+ // #define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7
+ // #define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8
+ // #define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9
+ // #define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10
+
+ // 0: Main button pressed, usually the left button or the un-initialized state
+ // 1: Auxiliary button pressed, usually the wheel button or the middle button (if present)
+ // 2: Secondary button pressed, usually the right button
+ // 3: Fourth button, typically the Browser Back button
+ // 4: Fifth button, typically the Browser Forward button
+
+ const b2r = [1, 3, 2, 9, 10] // browser mouse button to retro button
+
+ /* @param buttons contains pressed state of mouse buttons according to
+ * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
+ */
+ return (button = 0, pressed = false) => {
+ dv.setUint32(0, 0);
+ dv.setUint8(0, mouse.BUTTONS);
+ dv.setUint8(b2r[button], +pressed);
+ webrtc.mouse(buffer);
+ }
+ })();
+
return Object.freeze({
endpoint: endpoints,
decode: (b) => JSON.parse(decodeBytes(b)),
- server:
- {
- initWebrtc: () => packet(endpoints.INIT_WEBRTC),
- sendIceCandidate: (candidate) => packet(endpoints.ICE_CANDIDATE, btoa(JSON.stringify(candidate))),
- sendSdp: (sdp) => packet(endpoints.ANSWER, btoa(JSON.stringify(sdp))),
- latencyCheck: (id, list) => packet(endpoints.LATENCY_CHECK, list, id),
- getWorkerList: () => packet(endpoints.GET_WORKER_LIST),
+ server: {
+ initWebrtc: () => packet(endpoints.INIT_WEBRTC),
+ sendIceCandidate: (candidate) => packet(endpoints.ICE_CANDIDATE, btoa(JSON.stringify(candidate))),
+ sendSdp: (sdp) => packet(endpoints.ANSWER, btoa(JSON.stringify(sdp))),
+ latencyCheck: (id, list) => packet(endpoints.LATENCY_CHECK, list, id),
+ getWorkerList: () => packet(endpoints.GET_WORKER_LIST),
+ },
+ game: {
+ input: {
+ keyboard: {
+ press: keyboardPress,
+ },
+ mouse: {
+ move: mouseMove,
+ press: mousePress,
+ }
},
- game:
- {
- load: () => packet(endpoints.GAME_LOAD),
- save: () => packet(endpoints.GAME_SAVE),
- setPlayerIndex: (i) => packet(endpoints.GAME_SET_PLAYER_INDEX, i),
- start: (game, roomId, record, recordUser, player) => packet(endpoints.GAME_START, {
- game_name: game,
- room_id: roomId,
- player_index: player,
- record: record,
- record_user: recordUser,
+ load: () => packet(endpoints.GAME_LOAD),
+ save: () => packet(endpoints.GAME_SAVE),
+ setPlayerIndex: (i) => packet(endpoints.GAME_SET_PLAYER_INDEX, i),
+ start: (game, roomId, record, recordUser, player) => packet(endpoints.GAME_START, {
+ game_name: game,
+ room_id: roomId,
+ player_index: player,
+ record: record,
+ record_user: recordUser,
+ }),
+ toggleMultitap: () => packet(endpoints.GAME_TOGGLE_MULTITAP),
+ toggleRecording: (active = false, userName = '') =>
+ packet(endpoints.GAME_RECORDING, {
+ active: active,
+ user: userName,
}),
- toggleMultitap: () => packet(endpoints.GAME_TOGGLE_MULTITAP),
- toggleRecording: (active = false, userName = '') =>
- packet(endpoints.GAME_RECORDING, {
- active: active,
- user: userName,
- }),
- quit: (roomId) => packet(endpoints.GAME_QUIT, {room_id: roomId}),
- }
+ quit: (roomId) => packet(endpoints.GAME_QUIT, {room_id: roomId}),
+ }
})
-})(socket);
+})(socket, webrtc, libretro);
+
+libretro = function () {// RETRO_KEYBOARD
+ const retro = {
+ '': 0,
+ 'Unknown': 0, // ???
+ 'First': 0, // ???
+ 'Backspace': 8,
+ 'Tab': 9,
+ 'Clear': 12,
+ 'Enter': 13, 'Return': 13,
+ 'Pause': 19,
+ 'Escape': 27,
+ 'Space': 32,
+ 'Exclaim': 33,
+ 'Quotedbl': 34,
+ 'Hash': 35,
+ 'Dollar': 36,
+ 'Ampersand': 38,
+ 'Quote': 39,
+ 'Leftparen': 40, '(': 40,
+ 'Rightparen': 41, ')': 41,
+ 'Asterisk': 42,
+ 'Plus': 43,
+ 'Comma': 44,
+ 'Minus': 45,
+ 'Period': 46,
+ 'Slash': 47,
+ 'Digit0': 48,
+ 'Digit1': 49,
+ 'Digit2': 50,
+ 'Digit3': 51,
+ 'Digit4': 52,
+ 'Digit5': 53,
+ 'Digit6': 54,
+ 'Digit7': 55,
+ 'Digit8': 56,
+ 'Digit9': 57,
+ 'Colon': 58, ':': 58,
+ 'Semicolon': 59, ';': 59,
+ 'Less': 60, '<': 60,
+ 'Equal': 61, '=': 61,
+ 'Greater': 62, '>': 62,
+ 'Question': 63, '?': 63,
+ // RETROK_AT = 64,
+ 'BracketLeft': 91, '[': 91,
+ 'Backslash': 92, '\\': 92,
+ 'BracketRight': 93, ']': 93,
+ // RETROK_CARET = 94,
+ // RETROK_UNDERSCORE = 95,
+ 'Backquote': 96, '`': 96,
+ 'KeyA': 97,
+ 'KeyB': 98,
+ 'KeyC': 99,
+ 'KeyD': 100,
+ 'KeyE': 101,
+ 'KeyF': 102,
+ 'KeyG': 103,
+ 'KeyH': 104,
+ 'KeyI': 105,
+ 'KeyJ': 106,
+ 'KeyK': 107,
+ 'KeyL': 108,
+ 'KeyM': 109,
+ 'KeyN': 110,
+ 'KeyO': 111,
+ 'KeyP': 112,
+ 'KeyQ': 113,
+ 'KeyR': 114,
+ 'KeyS': 115,
+ 'KeyT': 116,
+ 'KeyU': 117,
+ 'KeyV': 118,
+ 'KeyW': 119,
+ 'KeyX': 120,
+ 'KeyY': 121,
+ 'KeyZ': 122,
+ // RETROK_LEFTBRACE = 123,
+ // RETROK_BAR = 124,
+ // RETROK_RIGHTBRACE = 125,
+ 'Tilde': 126, '~': 126,
+ 'Delete': 127,
+
+ // RETROK_KP0 = 256,
+ // RETROK_KP1 = 257,
+ // RETROK_KP2 = 258,
+ // RETROK_KP3 = 259,
+ // RETROK_KP4 = 260,
+ // RETROK_KP5 = 261,
+ // RETROK_KP6 = 262,
+ // RETROK_KP7 = 263,
+ // RETROK_KP8 = 264,
+ // RETROK_KP9 = 265,
+ // RETROK_KP_PERIOD = 266,
+ // RETROK_KP_DIVIDE = 267,
+ // RETROK_KP_MULTIPLY = 268,
+ // RETROK_KP_MINUS = 269,
+ // RETROK_KP_PLUS = 270,
+ // RETROK_KP_ENTER = 271,
+ // RETROK_KP_EQUALS = 272,
+
+ 'ArrowUp': 273,
+ 'ArrowDown': 274,
+ 'ArrowRight': 275,
+ 'ArrowLeft': 276,
+ 'Insert': 277,
+ 'Home': 278,
+ 'End': 279,
+ 'PageUp': 280,
+ 'PageDown': 281,
+
+ 'F1': 282,
+ 'F2': 283,
+ 'F3': 284,
+ 'F4': 285,
+ 'F5': 286,
+ 'F6': 287,
+ 'F7': 288,
+ 'F8': 289,
+ 'F9': 290,
+ 'F10': 291,
+ 'F11': 292,
+ 'F12': 293,
+ 'F13': 294,
+ 'F14': 295,
+ 'F15': 296,
+
+ 'NumLock': 300,
+ 'CapsLock': 301,
+ 'ScrollLock': 302,
+ 'ShiftRight': 303,
+ 'ShiftLeft': 304,
+ 'ControlRight': 305,
+ 'ControlLeft': 306,
+ 'AltRight': 307,
+ 'AltLeft': 308,
+ 'MetaRight': 309,
+ 'MetaLeft': 310,
+ // RETROK_LSUPER = 311,
+ // RETROK_RSUPER = 312,
+ // RETROK_MODE = 313,
+ // RETROK_COMPOSE = 314,
+
+ // RETROK_HELP = 315,
+ // RETROK_PRINT = 316,
+ // RETROK_SYSREQ = 317,
+ // RETROK_BREAK = 318,
+ // RETROK_MENU = 319,
+ // RETROK_POWER = 320,
+ // RETROK_EURO = 321,
+ // RETROK_UNDO = 322,
+ // RETROK_OEM_102 = 323,
+
+ // RETROK_LAST,
+
+ // RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
+ };
+
+ const retroMod = {
+ NONE: 0x0000,
+
+ SHIFT: 0x01,
+ CTRL: 0x02,
+ ALT: 0x04,
+ META: 0x08,
+
+ // RETROKMOD_NUMLOCK = 0x10,
+ // RETROKMOD_CAPSLOCK = 0x20,
+ // RETROKMOD_SCROLLOCK = 0x40,
+
+ // RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
+ };
+
+ const _map = (key = '', code = '') => {
+ return retro[code] || retro[key] || 0
+ }
+
+ return {
+ map: _map,
+ mod: retroMod,
+ }
+}()
diff --git a/web/js/controller.js b/web/js/controller.js
index 7e294e941..42bb72f85 100644
--- a/web/js/controller.js
+++ b/web/js/controller.js
@@ -126,12 +126,6 @@
stream.play()
- // TODO get current game from the URL and not from the list?
- // if we are opening a share link it will send the default game name to the server
- // currently it's a game with the index 1
- // on the server this game is ignored and the actual game will be extracted from the share link
- // so there's no point in doing this and this' really confusing
-
api.game.start(
gameList.selected,
room.getId(),
@@ -217,25 +211,6 @@
state.keyPress(data.key, data.code);
};
- // Feature detection.
- const supportsKeyboardLock =
- ('keyboard' in navigator) && ('lock' in navigator.keyboard);
-
- if (supportsKeyboardLock) {
- document.addEventListener('fullscreenchange', async () => {
- if (document.fullscreenElement) {
- await navigator.keyboard.lock();
- keyboard.toggle(false);
- return console.log('Keyboard locked.');
- }
- navigator.keyboard.unlock();
- keyboard.toggle(true);
- console.log('Keyboard unlocked.');
- });
- } else {
- console.warn('Browser doesn\'t support keyboard lock!');
- }
-
// pre-state key release handler
const onKeyRelease = data => {
const button = keyButtons[data.key];
@@ -295,7 +270,7 @@
const handleRecordingStatus = (data) => {
if (data === 'ok') {
- message.show(`Recording ${recording.isActive() ? 'on' : 'off'}`, true)
+ message.show(`Recording ${recording.isActive() ? 'on' : 'off'}`)
if (recording.isActive()) {
recording.setIndicator(true)
}
@@ -303,7 +278,7 @@
message.show(`Recording failed ):`)
recording.setIndicator(false)
}
- console.log("recording is ", recording.isActive())
+ log.debug("recording is ", recording.isActive())
}
const _default = {
@@ -386,31 +361,13 @@
..._default,
name: 'game',
axisChanged: (id, value) => input.setAxisChanged(id, value),
- keyboardInput: (down, e) => {
- const buffer = new ArrayBuffer(7);
- const dv = new DataView(buffer);
-
- // 0 1 2 3 4 5 6
- dv.setUint8(4, down ? 1 : 0)
- const k = libretro.map('', e.code);
- dv.setUint32(0, k, true)
-
- // meta keys
- let mod = 0;
-
- e.altKey && (mod |= libretro.mod.ALT)
- e.ctrlKey && (mod |= libretro.mod.CTRL)
- e.metaKey && (mod |= libretro.mod.META)
- e.shiftKey && (mod |= libretro.mod.SHIFT)
-
- dv.setUint16(5, mod, true)
-
- webrtc.keyboard(buffer);
- },
- keyPress: (key, code) => {
+ keyboardInput: (pressed, e) => api.game.input.keyboard.press(pressed, e),
+ mouseMove: (e) => api.game.input.mouse.move(e.dx, e.dy),
+ mousePress: (e) => api.game.input.mouse.press(e.b, e.p),
+ keyPress: (key) => {
input.setKeyState(key, true);
},
- keyRelease: function (key, code) {
+ keyRelease: function (key) {
input.setKeyState(key, false);
switch (key) {
@@ -462,6 +419,15 @@
}
};
+ // Browser lock API
+ document.onpointerlockchange = () => {
+ event.pub(POINTER_LOCK_CHANGE, document.pointerLockElement);
+ }
+
+ document.onfullscreenchange = async () => {
+ event.pub(FULLSCREEN_CHANGE, document.fullscreenElement);
+ }
+
// subscriptions
event.sub(MESSAGE, onMessage);
@@ -499,11 +465,19 @@
event.sub(MENU_HANDLER_ATTACHED, (data) => {
menuScreen.addEventListener(data.event, data.handler, {passive: true});
});
- // separate keyboard handler
+
+ // keyboard handler in the Screen Lock mode
event.sub(KEYBOARD_KEY_DOWN, (v) => state.keyboardInput?.(true, v));
event.sub(KEYBOARD_KEY_UP, (v) => state.keyboardInput?.(false, v));
+
+ // mouse handler in the Screen Lock mode
+ event.sub(MOUSE_MOVED, (e) => state.mouseMove?.(e))
+ event.sub(MOUSE_PRESSED, (e) => state.mousePress?.(e))
+
+ // general keyboard handler
event.sub(KEY_PRESSED, onKeyPress);
event.sub(KEY_RELEASED, onKeyRelease);
+
event.sub(SETTINGS_CHANGED, () => message.show('Settings have been updated'));
event.sub(SETTINGS_CLOSED, () => {
state.keyRelease(KEY.SETTINGS);
diff --git a/web/js/event/event.js b/web/js/event/event.js
index 15c530fbf..9bc57ea82 100644
--- a/web/js/event/event.js
+++ b/web/js/event/event.js
@@ -93,6 +93,11 @@ const KEYBOARD_KEY_DOWN = 'keyboardKeyDown';
const KEYBOARD_KEY_UP = 'keyboardKeyUp';
const AXIS_CHANGED = 'axisChanged';
const CONTROLLER_UPDATED = 'controllerUpdated';
+const MOUSE_MOVED = 'mouseMoved';
+const MOUSE_PRESSED = 'mousePressed';
+
+const FULLSCREEN_CHANGE = 'fsc';
+const POINTER_LOCK_CHANGE = 'plc';
const DPAD_TOGGLE = 'dpadToggle';
const STATS_TOGGLE = 'statsToggle';
diff --git a/web/js/input/keyboard.js b/web/js/input/keyboard.js
index 8f19f2596..9c798d212 100644
--- a/web/js/input/keyboard.js
+++ b/web/js/input/keyboard.js
@@ -101,6 +101,19 @@ const keyboard = (() => {
event.sub(DPAD_TOGGLE, (data) => onDpadToggle(data.checked));
+ const supportsKeyboardLock =
+ ('keyboard' in navigator) && ('lock' in navigator.keyboard);
+
+ if (supportsKeyboardLock) {
+ event.sub(FULLSCREEN_CHANGE, async (fullscreenEl) => {
+ enabled = !fullscreenEl;
+ enabled ? navigator.keyboard.unlock() : await navigator.keyboard.lock();
+ log.debug(`Keyboard lock: ${!enabled}`);
+ })
+ } else {
+ log.warn('Browser doesn\'t support keyboard lock!');
+ }
+
return {
init: () => {
keyMap = settings.loadOr(opts.INPUT_KEYBOARD_MAP, defaultMap);
@@ -136,8 +149,5 @@ const keyboard = (() => {
}, settings: {
remap
},
- toggle: (v) => {
- enabled = v !== undefined ? v : !enabled;
- }
}
})(event, document, KEY, log, opts, settings);
diff --git a/web/js/input/libretro.js b/web/js/input/libretro.js
deleted file mode 100644
index fd34ae16c..000000000
--- a/web/js/input/libretro.js
+++ /dev/null
@@ -1,182 +0,0 @@
-// RETRO_KEYBOARD
-const retro = {
- '': 0,
- 'Unknown': 0, // ???
- 'First': 0, // ???
- 'Backspace': 8,
- 'Tab': 9,
- 'Clear': 12,
- 'Enter': 13, 'Return': 13,
- 'Pause': 19,
- 'Escape': 27,
- 'Space': 32,
- 'Exclaim': 33,
- 'Quotedbl': 34,
- 'Hash': 35,
- 'Dollar': 36,
- 'Ampersand': 38,
- 'Quote': 39,
- 'Leftparen': 40, '(': 40,
- 'Rightparen': 41, ')': 41,
- 'Asterisk': 42,
- 'Plus': 43,
- 'Comma': 44,
- 'Minus': 45,
- 'Period': 46,
- 'Slash': 47,
- 'Digit0': 48,
- 'Digit1': 49,
- 'Digit2': 50,
- 'Digit3': 51,
- 'Digit4': 52,
- 'Digit5': 53,
- 'Digit6': 54,
- 'Digit7': 55,
- 'Digit8': 56,
- 'Digit9': 57,
- 'Colon': 58, ':': 58,
- 'Semicolon': 59, ';': 59,
- 'Less': 60, '<': 60,
- 'Equal': 61, '=': 61,
- 'Greater': 62, '>': 62,
- 'Question': 63, '?': 63,
- // RETROK_AT = 64,
- 'BracketLeft': 91, '[': 91,
- 'Backslash': 92, '\\': 92,
- 'BracketRight': 93, ']': 93,
- // RETROK_CARET = 94,
- // RETROK_UNDERSCORE = 95,
- 'Backquote': 96, '`': 96,
- 'KeyA': 97,
- 'KeyB': 98,
- 'KeyC': 99,
- 'KeyD': 100,
- 'KeyE': 101,
- 'KeyF': 102,
- 'KeyG': 103,
- 'KeyH': 104,
- 'KeyI': 105,
- 'KeyJ': 106,
- 'KeyK': 107,
- 'KeyL': 108,
- 'KeyM': 109,
- 'KeyN': 110,
- 'KeyO': 111,
- 'KeyP': 112,
- 'KeyQ': 113,
- 'KeyR': 114,
- 'KeyS': 115,
- 'KeyT': 116,
- 'KeyU': 117,
- 'KeyV': 118,
- 'KeyW': 119,
- 'KeyX': 120,
- 'KeyY': 121,
- 'KeyZ': 122,
- // RETROK_LEFTBRACE = 123,
- // RETROK_BAR = 124,
- // RETROK_RIGHTBRACE = 125,
- 'Tilde': 126, '~': 126,
- 'Delete': 127,
-
- // RETROK_KP0 = 256,
- // RETROK_KP1 = 257,
- // RETROK_KP2 = 258,
- // RETROK_KP3 = 259,
- // RETROK_KP4 = 260,
- // RETROK_KP5 = 261,
- // RETROK_KP6 = 262,
- // RETROK_KP7 = 263,
- // RETROK_KP8 = 264,
- // RETROK_KP9 = 265,
- // RETROK_KP_PERIOD = 266,
- // RETROK_KP_DIVIDE = 267,
- // RETROK_KP_MULTIPLY = 268,
- // RETROK_KP_MINUS = 269,
- // RETROK_KP_PLUS = 270,
- // RETROK_KP_ENTER = 271,
- // RETROK_KP_EQUALS = 272,
-
- 'ArrowUp': 273,
- 'ArrowDown': 274,
- 'ArrowRight': 275,
- 'ArrowLeft': 276,
- 'Insert': 277,
- 'Home': 278,
- 'End': 279,
- 'PageUp': 280,
- 'PageDown': 281,
-
- 'F1': 282,
- 'F2': 283,
- 'F3': 284,
- 'F4': 285,
- 'F5': 286,
- 'F6': 287,
- 'F7': 288,
- 'F8': 289,
- 'F9': 290,
- 'F10': 291,
- 'F11': 292,
- 'F12': 293,
- 'F13': 294,
- 'F14': 295,
- 'F15': 296,
-
- 'NumLock': 300,
- 'CapsLock': 301,
- 'ScrollLock': 302,
- 'ShiftRight': 303,
- 'ShiftLeft': 304,
- 'ControlRight': 305,
- 'ControlLeft': 306,
- 'AltRight': 307,
- 'AltLeft': 308,
- 'MetaRight': 309,
- 'MetaLeft': 310,
- // RETROK_LSUPER = 311,
- // RETROK_RSUPER = 312,
- // RETROK_MODE = 313,
- // RETROK_COMPOSE = 314,
-
- // RETROK_HELP = 315,
- // RETROK_PRINT = 316,
- // RETROK_SYSREQ = 317,
- // RETROK_BREAK = 318,
- // RETROK_MENU = 319,
- // RETROK_POWER = 320,
- // RETROK_EURO = 321,
- // RETROK_UNDO = 322,
- // RETROK_OEM_102 = 323,
-
- // RETROK_LAST,
-
- // RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
-};
-
-const retroMod = {
- NONE: 0x0000,
-
- SHIFT: 0x01,
- CTRL: 0x02,
- ALT: 0x04,
- META: 0x08,
-
- // RETROKMOD_NUMLOCK = 0x10,
- // RETROKMOD_CAPSLOCK = 0x20,
- // RETROKMOD_SCROLLOCK = 0x40,
-
- // RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */
-};
-
-
-const libretro = (() => {
- const _map = (key = '', code = '') => {
- return retro[code] || retro[key] || 0
- }
-
- return {
- map: _map,
- mod: retroMod,
- }
-})()
diff --git a/web/js/network/webrtc.js b/web/js/network/webrtc.js
index 8757bad4c..2129b20f8 100644
--- a/web/js/network/webrtc.js
+++ b/web/js/network/webrtc.js
@@ -14,6 +14,7 @@ const webrtc = (() => {
let connection;
let dataChannel;
let keyboardChannel;
+ let mouseChannel;
let mediaStream;
let candidates = [];
let isAnswered = false;
@@ -36,18 +37,24 @@ const webrtc = (() => {
if (e.channel.label === 'keyboard') {
keyboardChannel = e.channel;
- } else {
- dataChannel = e.channel;
- dataChannel.onopen = () => {
- log.info('[rtc] the input channel has been opened');
- inputReady = true;
- event.pub(WEBRTC_CONNECTION_READY)
- };
- if (onData) {
- dataChannel.onmessage = onData;
- }
- dataChannel.onclose = () => log.info('[rtc] the input channel has been closed');
+ return;
+ }
+
+ if (e.channel.label === 'mouse') {
+ mouseChannel = e.channel
+ return;
}
+
+ dataChannel = e.channel;
+ dataChannel.onopen = () => {
+ log.info('[rtc] the input channel has been opened');
+ inputReady = true;
+ event.pub(WEBRTC_CONNECTION_READY)
+ };
+ if (onData) {
+ dataChannel.onmessage = onData;
+ }
+ dataChannel.onclose = () => log.info('[rtc] the input channel has been closed');
}
connection.oniceconnectionstatechange = ice.onIceConnectionStateChange;
connection.onicegatheringstatechange = ice.onIceStateChange;
@@ -77,6 +84,10 @@ const webrtc = (() => {
keyboardChannel.close();
keyboardChannel = null;
}
+ if (mouseChannel) {
+ mouseChannel.close();
+ mouseChannel = null;
+ }
candidates = Array();
log.info('[rtc] WebRTC has been closed');
}
@@ -155,7 +166,6 @@ const webrtc = (() => {
event.pub(WEBRTC_SDP_ANSWER, {sdp: answer});
media.srcObject = mediaStream;
},
- // setMessageHandler: (handler) => onMessage = handler,
addCandidate: (data) => {
if (data === '') {
event.pub(WEBRTC_ICE_CANDIDATES_FLUSH);
@@ -175,16 +185,8 @@ const webrtc = (() => {
});
isFlushing = false;
},
- // message: (mess = '') => {
- // try {
- // inputChannel.send(mess)
- // return true
- // } catch (error) {
- // log.error('[rtc] input channel broken ' + error)
- // return false
- // }
- // },
keyboard: (data) => keyboardChannel.send(data),
+ mouse: (data) => mouseChannel.send(data),
input: (data) => dataChannel.send(data),
isConnected: () => connected,
isInputReady: () => inputReady,
diff --git a/web/js/stream/stream.js b/web/js/stream/stream.js
index b15e36e2b..5659663fd 100644
--- a/web/js/stream/stream.js
+++ b/web/js/stream/stream.js
@@ -89,31 +89,8 @@ const stream = (() => {
screen.blur();
})
- screen.addEventListener('fullscreenchange', () => {
- const fullscreen = document.fullscreenElement
-
- const w = window.screen.width ?? window.innerWidth;
- const h = window.screen.height ?? window.innerHeight;
-
- const ww = document.documentElement.innerWidth;
- const hh = document.documentElement.innerHeight;
-
- screen.style.padding = '0'
- if (fullscreen) {
- const dw = (w - ww * state.aspect) / 2
- screen.style.padding = `0 ${dw}px`
- // chrome bug
- setTimeout(() => {
- const dw = (h - hh * state.aspect) / 2
- screen.style.padding = `0 ${dw}px`
- }, 1)
- }
- makeFullscreen(!!fullscreen);
-
- screen.blur();
-
- // !to flipped
- })
+ const handlePointerDown = (e) => event.pub(MOUSE_PRESSED, {b: e.button, p: true});
+ const handlePointerUp = (e) => event.pub(MOUSE_PRESSED, {b: e.button, p: false});
const makeFullscreen = (make = false) => {
screen.classList.toggle('no-media-controls', make)
@@ -175,6 +152,51 @@ const stream = (() => {
}
});
+ let pointerLocked = false;
+
+ event.sub(FULLSCREEN_CHANGE, async (fullscreenEl) => {
+ const w = window.screen.width ?? window.innerWidth;
+ const h = window.screen.height ?? window.innerHeight;
+
+ const ww = document.documentElement.innerWidth;
+ const hh = document.documentElement.innerHeight;
+
+ const fullscreen = !!fullscreenEl;
+
+ screen.style.padding = '0'
+ if (fullscreen) {
+ const dw = (w - ww * state.aspect) / 2
+ screen.style.padding = `0 ${dw}px`
+ // chrome bug
+ setTimeout(() => {
+ const dw = (h - hh * state.aspect) / 2
+ screen.style.padding = `0 ${dw}px`
+ }, 1)
+ }
+ makeFullscreen(fullscreen);
+
+ screen.blur();
+
+ if (fullscreen && !pointerLocked) {
+ // event.pub(POINTER_LOCK_CHANGE, screen);
+ await screen.requestPointerLock();
+ }
+
+ screen.onpointerdown = fullscreen ? handlePointerDown : null;
+ screen.onpointerup = fullscreen ? handlePointerUp : null;
+
+ // !to flipped
+ })
+
+ const handlePointerMove = (e) => {
+ event.pub(MOUSE_MOVED, {dx: e.movementX, dy: e.movementY});
+ }
+
+ event.sub(POINTER_LOCK_CHANGE, (lockedEl) => {
+ pointerLocked = lockedEl === screen;
+ screen.onpointermove = pointerLocked ? handlePointerMove : null;
+ log.debug(`Pointer lock: ${pointerLocked}`);
+ });
const fit = 'contain'