N64 Support (#195)

* Allow HTTP access to Raspberry Pi over local network
Lower audio buffer maximum theoretical size to get the worker code to compile on Raspberry Pi

* Add https port flag to run https worker and coordinator on the same machine
Add https chain and key flags to allow to use an existing certificate and bypass letsencrypt
Note the ping address resolution is still broken with this configuration

* Add option to define a ping server in the coordinator
This is useful when it is not predicatable what address and port the worker will be runnning at
This only works when there is a single worker

* Free temporarily allocated CStrings
Store constant CString

* Only load core once and unload it when done

* Add Nintendo 64 support!
Disclaimer: only tested with Mupen64plus and Mupen64plusNext on Raspberry Pi.
It probably needs more work to run on every system and with other OpenGL libretro libraries.
Input controls are hacked together, it really needs analog stick and remapping support to play in a nicer way.
I am worried there might be a memory leak when unloading Mupen64plus but this needs further investigation.

* Add analog sticks + R2,L2,R3,L3 support

* Add client logic to control left analog stick via keyboard and touch
Add client logic to toggle between dpad mode and analog mode (even for joystick)
Might need to revisit if and when remapping is implemented
Tocuh sensitivity of analog stick is pretty high, might need tweaking

* Add cores for Raspberry Pi
Add N64 core for linux x86_64

* Reset use OpenGL flag on nanoarch shutdown (line lost in refactoring)
This commit is contained in:
88hcsif 2020-06-17 11:07:10 +01:00 committed by GitHub
parent 9d17435d7e
commit 091b086bcb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 931 additions and 147 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,53 @@
mupen64plus-169screensize = 480x270
mupen64plus-43screensize = 320x240
mupen64plus-alt-map = False
mupen64plus-aspect = 4:3
mupen64plus-astick-deadzone = 15
mupen64plus-astick-sensitivity = 100
mupen64plus-BackgroundMode = OnePiece
mupen64plus-BilinearMode = standard
mupen64plus-CorrectTexrectCoords = Off
mupen64plus-CountPerOp = 0
mupen64plus-cpucore = dynamic_recompiler
mupen64plus-CropMode = Auto
mupen64plus-d-cbutton = C3
mupen64plus-EnableCopyColorToRDRAM = Off
mupen64plus-EnableCopyDepthToRDRAM = Software
mupen64plus-EnableEnhancedHighResStorage = False
mupen64plus-EnableEnhancedTextureStorage = False
mupen64plus-EnableFBEmulation = True
mupen64plus-EnableFragmentDepthWrite = False
mupen64plus-EnableHWLighting = False
mupen64plus-EnableLegacyBlending = True
mupen64plus-EnableLODEmulation = True
mupen64plus-EnableNativeResTexrects = Disabled
mupen64plus-EnableOverscan = Enabled
mupen64plus-EnableShadersStorage = True
mupen64plus-EnableTextureCache = True
mupen64plus-ForceDisableExtraMem = False
mupen64plus-FrameDuping = False
mupen64plus-Framerate = Original
mupen64plus-FXAA = 0
mupen64plus-l-cbutton = C2
mupen64plus-MaxTxCacheSize = 8000
mupen64plus-NoiseEmulation = True
mupen64plus-OverscanBottom = 0
mupen64plus-OverscanLeft = 0
mupen64plus-OverscanRight = 0
mupen64plus-OverscanTop = 0
mupen64plus-pak1 = memory
mupen64plus-pak2 = none
mupen64plus-pak3 = none
mupen64plus-pak4 = none
mupen64plus-r-cbutton = C1
mupen64plus-rdp-plugin = gliden64
mupen64plus-rsp-plugin = hle
mupen64plus-rspmode = HLE
mupen64plus-txCacheCompression = True
mupen64plus-txEnhancementMode = None
mupen64plus-txFilterIgnoreBG = True
mupen64plus-txFilterMode = None
mupen64plus-txHiresEnable = False
mupen64plus-txHiresFullAlphaChannel = False
mupen64plus-u-cbutton = C4
mupen64plus-virefresh = Auto

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
go.mod vendored
View file

@ -4,14 +4,17 @@ go 1.12
require (
cloud.google.com/go v0.43.0
github.com/disintegration/imaging v1.6.2
github.com/gen2brain/x264-go v0.0.0-20200517120223-c08131f6fc8a
github.com/gofrs/uuid v3.2.0+incompatible
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.0
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7
github.com/pion/webrtc/v2 v2.2.0
github.com/prometheus/client_golang v1.1.0
github.com/spf13/pflag v1.0.3
github.com/veandco/go-sdl2 v0.4.4
golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8
golang.org/x/sys v0.0.0-20191218084908-4a24b4065292 // indirect

View file

@ -19,6 +19,9 @@ var Port = flag.String("port", "8000", "Port of the game")
var FrontendSTUNTURN = flag.String("stunturn", DefaultSTUNTURN, "Frontend STUN TURN servers")
var Mode = flag.String("mode", "dev", "Environment")
var StunTurnTemplate = `[{"urls":"stun:stun.l.google.com:19302"},{"urls":"stun:%s:3478"},{"urls":"turn:%s:3478","username":"root","credential":"root"}]`
var HttpsPort = flag.Int("httpsPort", 443, "Https Port")
var HttpsKey = flag.String("httpsKey", "", "Https Key")
var HttpsChain = flag.String("httpsChain", "", "Https Chain")
var WSWait = 20 * time.Second
var MatchWorkerRandom = false
@ -38,12 +41,16 @@ var FileTypeToEmulator = map[string]string{
"swc": "snes",
"fig": "snes",
"bs": "snes",
"n64": "n64",
"v64": "n64",
"z64": "n64",
}
// There is no good way to determine main width and height of the emulator.
// When game run, frame width and height can scale abnormally.
type EmulatorMeta struct {
Path string
Config string
Width int
Height int
AudioSampleRate int
@ -52,6 +59,8 @@ type EmulatorMeta struct {
BaseHeight int
Ratio float64
Rotation image.Rotate
IsGlAllowed bool
UsesLibCo bool
}
var EmulatorConfig = map[string]EmulatorMeta{
@ -80,6 +89,14 @@ var EmulatorConfig = map[string]EmulatorMeta{
Width: 240,
Height: 160,
},
"n64": {
Path: "assets/emulator/libretro/cores/mupen64plus_next_libretro",
Config: "assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg",
Width: 320,
Height: 240,
IsGlAllowed: true,
UsesLibCo: true,
},
}
var EmulatorExtension = []string{".so", ".dylib", ".dll"}
var EmulatorExtension = []string{".so", ".armv7-neon-hf.so", ".dylib", ".dll"}

View file

@ -8,6 +8,7 @@ import (
type Config struct {
Port int
PublicDomain string
PingServer string
URLPrefix string
DebugHost string
@ -18,6 +19,7 @@ func NewDefaultConfig() Config {
return Config{
Port: 8800,
PublicDomain: "http://localhost:8000",
PingServer: "",
MonitoringConfig: monitoring.ServerMonitoringConfig{
Port: 6601,
@ -36,7 +38,8 @@ func (c *Config) AddFlags(fs *pflag.FlagSet) *Config {
fs.IntVarP(&c.MonitoringConfig.Port, "monitoring.port", "", c.MonitoringConfig.Port, "Monitoring server port")
fs.StringVarP(&c.MonitoringConfig.URLPrefix, "monitoring.prefix", "", c.MonitoringConfig.URLPrefix, "Monitoring server url prefix")
fs.StringVarP(&c.DebugHost, "debughost", "d", "", "Specify the server want to connect directly to debug")
fs.StringVarP(&c.PublicDomain, "domain", "n", c.PublicDomain, "Specify the server want to connect directly to debug")
fs.StringVarP(&c.PublicDomain, "domain", "n", c.PublicDomain, "Specify the public domain of the coordinator")
fs.StringVarP(&c.PingServer, "pingServer", "", c.PingServer, "Specify the worker address that the client can ping (with protocol and port)")
return c
}

View file

@ -102,27 +102,33 @@ func (o *Coordinator) initializeCoordinator() {
log.Println("Initializing Coordinator Server")
if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv {
var leurl string
if *config.Mode == config.StagingEnv {
leurl = stagingLEURL
} else {
leurl = acme.LetsEncryptURL
}
certManager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(o.cfg.PublicDomain),
Cache: autocert.DirCache("assets/cache"),
Client: &acme.Client{DirectoryURL: leurl},
}
httpsSrv = makeHTTPServer(coordinator)
httpsSrv.Addr = ":443"
httpsSrv.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
httpsSrv.Addr = fmt.Sprintf(":%d", *config.HttpsPort)
if *config.HttpsChain == "" || *config.HttpsKey == "" {
*config.HttpsChain = ""
*config.HttpsKey = ""
var leurl string
if *config.Mode == config.StagingEnv {
leurl = stagingLEURL
} else {
leurl = acme.LetsEncryptURL
}
certManager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist(o.cfg.PublicDomain),
Cache: autocert.DirCache("assets/cache"),
Client: &acme.Client{DirectoryURL: leurl},
}
httpsSrv.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
}
go func() {
fmt.Printf("Starting HTTPS server on %s\n", httpsSrv.Addr)
err := httpsSrv.ListenAndServeTLS("", "")
err := httpsSrv.ListenAndServeTLS(*config.HttpsChain, *config.HttpsKey)
if err != nil {
log.Fatalf("httpsSrv.ListendAndServeTLS() failed with %s", err)
}

View file

@ -74,6 +74,10 @@ func (o *Server) GetWeb(w http.ResponseWriter, r *http.Request) {
// getPingServer returns the server for latency check of a zone. In latency check to find best worker step, we use this server to find the closest worker.
func (o *Server) getPingServer(zone string) string {
if o.cfg.PingServer != "" {
return fmt.Sprintf("%s/echo", o.cfg.PingServer)
}
if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv {
return fmt.Sprintf(pingServerTemp, zone, o.cfg.PublicDomain)
}

View file

@ -2,6 +2,7 @@ package nanoarch
/*
#include "libretro.h"
#include <pthread.h>
#include <stdbool.h>
#include <stdarg.h>
#include <stdio.h>
@ -123,5 +124,72 @@ void coreLog_cgo(enum retro_log_level level, const char *fmt, ...) {
coreLog(level, msg);
}
uintptr_t coreGetCurrentFramebuffer_cgo() {
uintptr_t coreGetCurrentFramebuffer();
return coreGetCurrentFramebuffer();
}
retro_proc_address_t coreGetProcAddress_cgo(const char *sym) {
retro_proc_address_t coreGetProcAddress(const char *sym);
return coreGetProcAddress(sym);
}
void bridge_context_reset(retro_hw_context_reset_t f) {
f();
}
void initVideo_cgo() {
void initVideo();
return initVideo();
}
void deinitVideo_cgo() {
void deinitVideo();
return deinitVideo();
}
void* function;
pthread_t thread;
int initialized = 0;
pthread_mutex_t run_mutex;
pthread_cond_t run_cv;
pthread_mutex_t done_mutex;
pthread_cond_t done_cv;
void *run_loop(void *unused) {
pthread_mutex_lock(&done_mutex);
pthread_mutex_lock(&run_mutex);
pthread_cond_signal(&done_cv);
pthread_mutex_unlock(&done_mutex);
while(1) {
pthread_cond_wait(&run_cv, &run_mutex);
((void (*)(void))function)();
pthread_mutex_lock(&done_mutex);
pthread_cond_signal(&done_cv);
pthread_mutex_unlock(&done_mutex);
}
pthread_mutex_unlock(&run_mutex);
}
void bridge_execute(void *f) {
if (!initialized) {
initialized = 1;
pthread_mutex_init(&run_mutex, NULL);
pthread_cond_init(&run_cv, NULL);
pthread_mutex_init(&done_mutex, NULL);
pthread_cond_init(&done_cv, NULL);
pthread_mutex_lock(&done_mutex);
pthread_create(&thread, NULL, run_loop, NULL);
pthread_cond_wait(&done_cv, &done_mutex);
pthread_mutex_unlock(&done_mutex);
}
pthread_mutex_lock(&run_mutex);
pthread_mutex_lock(&done_mutex);
function = f;
pthread_cond_signal(&run_cv);
pthread_mutex_unlock(&run_mutex);
pthread_cond_wait(&done_cv, &done_mutex);
pthread_mutex_unlock(&done_mutex);
}
*/
import "C"

View file

@ -0,0 +1,46 @@
package nanoarch
import (
"bufio"
"os"
"strings"
"log"
)
import "C"
type ConfigProperties map[string]*C.char
func ScanConfigFile(filename string) ConfigProperties {
config := ConfigProperties{}
if len(filename) == 0 {
return config
}
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
return config
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if equal := strings.Index(line, "="); equal >= 0 {
if key := strings.TrimSpace(line[:equal]); len(key) > 0 {
value := ""
if len(line) > equal {
value = strings.TrimSpace(line[equal+1:])
}
config[key] = C.CString(value)
}
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return config
}

View file

@ -46,6 +46,13 @@ void coreLog_cgo(enum retro_log_level level, const char *msg);
*/
import "C"
const numAxes = 4
type constrollerState struct {
keyState uint16
axes [numAxes]int16
}
// naEmulator implements CloudEmulator
type naEmulator struct {
imageChannel chan<- *image.RGBA
@ -58,15 +65,15 @@ type naEmulator struct {
gameName string
isSavingLoading bool
keysMap map[string][]int
done chan struct{}
controllersMap map[string][]constrollerState
done chan struct{}
// lock to lock uninteruptable operation
lock *sync.Mutex
}
type InputEvent struct {
KeyState int
RawState []byte
PlayerIdx int
ConnID string
}
@ -83,14 +90,14 @@ func NewNAEmulator(etype string, roomID string, inputChannel <-chan InputEvent)
audioChannel := make(chan []int16, 30)
return &naEmulator{
meta: meta,
imageChannel: imageChannel,
audioChannel: audioChannel,
inputChannel: inputChannel,
keysMap: map[string][]int{},
roomID: roomID,
done: make(chan struct{}, 1),
lock: &sync.Mutex{},
meta: meta,
imageChannel: imageChannel,
audioChannel: audioChannel,
inputChannel: inputChannel,
controllersMap: map[string][]constrollerState{},
roomID: roomID,
done: make(chan struct{}, 1),
lock: &sync.Mutex{},
}, imageChannel, audioChannel
}
@ -108,24 +115,27 @@ func (na *naEmulator) listenInput() {
// input from javascript follows bitmap. Ex: 00110101
// we decode the bitmap and send to channel
for inpEvent := range NAEmulator.inputChannel {
inpBitmap := inpEvent.KeyState
inpBitmap := uint16(inpEvent.RawState[1])<<8 + uint16(inpEvent.RawState[0])
if inpBitmap == -1 {
if inpBitmap == 0xFFFF {
// terminated
delete(na.keysMap, inpEvent.ConnID)
delete(na.controllersMap, inpEvent.ConnID)
continue
}
if _, ok := na.keysMap[inpEvent.ConnID]; !ok {
na.keysMap[inpEvent.ConnID] = make([]int, maxPort)
if _, ok := na.controllersMap[inpEvent.ConnID]; !ok {
na.controllersMap[inpEvent.ConnID] = make([]constrollerState, maxPort)
}
na.keysMap[inpEvent.ConnID][inpEvent.PlayerIdx] = inpBitmap
na.controllersMap[inpEvent.ConnID][inpEvent.PlayerIdx].keyState = inpBitmap
for i := 0; i < numAxes && (i+1)*2+1 < len(inpEvent.RawState); i++ {
na.controllersMap[inpEvent.ConnID][inpEvent.PlayerIdx].axes[i] = int16(inpEvent.RawState[(i+1)*2+1])<<8 + int16(inpEvent.RawState[(i+1)*2])
}
}
}
func (na *naEmulator) LoadMeta(path string) config.EmulatorMeta {
coreLoad(na.meta.Path)
coreLoad(na.meta.Path, na.meta.IsGlAllowed, na.meta.UsesLibCo, na.meta.Config)
coreLoadGame(path)
na.gamePath = path

View file

@ -4,14 +4,19 @@ import (
"bufio"
"errors"
"fmt"
stdimage "image"
"log"
"os"
"os/user"
"runtime"
"sync"
"unsafe"
"github.com/disintegration/imaging"
"github.com/giongto35/cloud-game/pkg/config"
"github.com/giongto35/cloud-game/pkg/emulator/libretro/image"
"github.com/go-gl/gl/v4.1-core/gl"
"github.com/veandco/go-sdl2/sdl"
)
/*
@ -49,19 +54,35 @@ void coreAudioSample_cgo(int16_t left, int16_t right);
size_t coreAudioSampleBatch_cgo(const int16_t *data, size_t frames);
int16_t coreInputState_cgo(unsigned port, unsigned device, unsigned index, unsigned id);
void coreLog_cgo(enum retro_log_level level, const char *msg);
uintptr_t coreGetCurrentFramebuffer_cgo();
retro_proc_address_t coreGetProcAddress_cgo(const char *sym);
void bridge_context_reset(retro_hw_context_reset_t f);
void initVideo_cgo();
void deinitVideo_cgo();
void bridge_execute(void *f);
*/
import "C"
var mu sync.Mutex
var video struct {
program uint32
vao uint32
pitch uint32
pixFmt uint32
pixType uint32
bpp uint32
rotation image.Angle
pitch uint32
pixFmt uint32
bpp uint32
rotation image.Angle
fbo uint32
rbo uint32
tex uint32
hw *C.struct_retro_hw_render_callback
window *sdl.Window
context sdl.GLContext
isGl bool
max_width int32
max_height int32
base_width int32
base_height int32
}
// default core pix format converter
@ -72,6 +93,13 @@ const bufSize = 1024 * 4
const joypadNumKeys = int(C.RETRO_DEVICE_ID_JOYPAD_R3 + 1)
var joy [joypadNumKeys]bool
var isGlAllowed bool
var usesLibCo bool
var coreConfig ConfigProperties
var systemDirectory = C.CString("./pkg/emulator/libretro/system")
var saveDirectory = C.CString(".")
var currentUser *C.char
var bindKeysMap = map[int]int{
C.RETRO_DEVICE_ID_JOYPAD_A: 0,
@ -86,6 +114,10 @@ var bindKeysMap = map[int]int{
C.RETRO_DEVICE_ID_JOYPAD_DOWN: 9,
C.RETRO_DEVICE_ID_JOYPAD_LEFT: 10,
C.RETRO_DEVICE_ID_JOYPAD_RIGHT: 11,
C.RETRO_DEVICE_ID_JOYPAD_R2: 12,
C.RETRO_DEVICE_ID_JOYPAD_L2: 13,
C.RETRO_DEVICE_ID_JOYPAD_R3: 14,
C.RETRO_DEVICE_ID_JOYPAD_L3: 15,
}
type CloudEmulator interface {
@ -103,6 +135,21 @@ func coreVideoRefresh(data unsafe.Pointer, width C.unsigned, height C.unsigned,
return
}
if (data == C.RETRO_HW_FRAME_BUFFER_VALID) {
im := stdimage.NewNRGBA(stdimage.Rect(0, 0, int(width), int(height)))
gl.BindFramebuffer(gl.FRAMEBUFFER, video.fbo)
gl.ReadPixels(0, 0, int32(width), int32(height), gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(im.Pix))
gl.BindFramebuffer(gl.FRAMEBUFFER, 0)
im = imaging.FlipV(im)
rgba := &stdimage.RGBA{
Pix: im.Pix,
Stride: im.Stride,
Rect: im.Rect,
}
NAEmulator.imageChannel <- rgba
return
}
// calculate real frame width in pixels from packed data (realWidth >= width)
packedWidth := int(uint32(pitch) / video.bpp)
@ -131,6 +178,19 @@ func coreInputPoll() {
//export coreInputState
func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.unsigned) C.int16_t {
if device == C.RETRO_DEVICE_ANALOG {
if index > C.RETRO_DEVICE_INDEX_ANALOG_RIGHT || id > C.RETRO_DEVICE_ID_ANALOG_Y {
return 0
}
axis := index * 2 + id
for k := range NAEmulator.controllersMap {
value := NAEmulator.controllersMap[k][port].axes[axis]
if value != 0 {
return (C.int16_t)(value)
}
}
}
if id >= 255 || index > 0 || device != C.RETRO_DEVICE_JOYPAD {
return 0
}
@ -142,8 +202,8 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
}
// check if any player is pressing that key
for k := range NAEmulator.keysMap {
if ((NAEmulator.keysMap[k][port] >> uint(key)) & 1) == 1 {
for k := range NAEmulator.controllersMap {
if ((NAEmulator.controllersMap[k][port].keyState >> uint(key)) & 1) == 1 {
return 1
}
}
@ -153,7 +213,7 @@ func coreInputState(port C.unsigned, device C.unsigned, index C.unsigned, id C.u
func audioWrite2(buf unsafe.Pointer, frames C.size_t) C.size_t {
// !to make it mono/stereo independent
samples := int(frames) * 2
pcm := (*[1 << 30]int16)(buf)[:samples:samples]
pcm := (*[(1 << 30) - 1]int16)(buf)[:samples:samples]
p := make([]int16, samples)
// copy because pcm slice refer to buf underlying pointer, and buf pointer is the same in continuos frames
@ -183,17 +243,30 @@ func coreLog(level C.enum_retro_log_level, msg *C.char) {
fmt.Print("[Log]: ", C.GoString(msg))
}
//export coreGetCurrentFramebuffer
func coreGetCurrentFramebuffer() C.uintptr_t {
return (C.uintptr_t)(video.fbo)
}
//export coreGetProcAddress
func coreGetProcAddress(sym *C.char) C.retro_proc_address_t {
return (C.retro_proc_address_t) (sdl.GLGetProcAddress(C.GoString(sym)))
}
//export coreEnvironment
func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
switch cmd {
case C.RETRO_ENVIRONMENT_GET_USERNAME:
username := (**C.char)(data)
currentUser, err := user.Current()
if err != nil {
*username = C.CString("")
} else {
*username = C.CString(currentUser.Username)
if currentUser == nil {
currentUserGo, err := user.Current()
if err != nil {
currentUser = C.CString("")
} else {
currentUser = C.CString(currentUserGo.Username)
}
}
*username = currentUser
break
case C.RETRO_ENVIRONMENT_GET_LOG_INTERFACE:
cb := (*C.struct_retro_log_callback)(data)
@ -211,11 +284,11 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
return videoSetPixelFormat(*format)
case C.RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY:
path := (**C.char)(data)
*path = C.CString("./pkg/emulator/libretro/system")
*path = systemDirectory
return true
case C.RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY:
path := (**C.char)(data)
*path = C.CString(".")
*path = saveDirectory
return true
case C.RETRO_ENVIRONMENT_SHUTDOWN:
//window.SetShouldClose(true)
@ -228,6 +301,26 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
case C.RETRO_ENVIRONMENT_SET_ROTATION:
setRotation(*(*int)(data) % 4)
return true
case C.RETRO_ENVIRONMENT_GET_VARIABLE:
variable := (*C.struct_retro_variable)(data)
key := C.GoString(variable.key)
if val, ok := coreConfig[key]; ok {
fmt.Printf("[Env]: get variable: key:%v value:%v\n", key, C.GoString(val))
variable.value = val
return true
}
// fmt.Printf("[Env]: get variable: key:%v not found\n", key)
return false
case C.RETRO_ENVIRONMENT_SET_HW_RENDER:
if (isGlAllowed) {
video.isGl = true
// runtime.LockOSThread()
video.hw = (*C.struct_retro_hw_render_callback)(data)
video.hw.get_current_framebuffer = (C.retro_hw_get_current_framebuffer_t)(C.coreGetCurrentFramebuffer_cgo)
video.hw.get_proc_address = (C.retro_hw_get_proc_address_t)(C.coreGetProcAddress_cgo)
return true
}
return false
default:
//fmt.Println("[Env]: command not implemented", cmd)
return false
@ -238,6 +331,117 @@ func coreEnvironment(cmd C.unsigned, data unsafe.Pointer) C.bool {
func init() {
}
var sdlInitialized = false
//export initVideo
func initVideo() {
// create_window()
var winTitle string = "CloudRetro"
var winWidth, winHeight int32 = 1, 1
var err error
if !sdlInitialized {
sdlInitialized = true
if err = sdl.Init(sdl.INIT_EVERYTHING); err != nil {
panic(err)
}
}
switch video.hw.context_type {
case C.RETRO_HW_CONTEXT_OPENGL_CORE:
fmt.Println("RETRO_HW_CONTEXT_OPENGL_CORE")
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
break
case C.RETRO_HW_CONTEXT_OPENGLES2:
fmt.Println("RETRO_HW_CONTEXT_OPENGLES2")
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_ES)
sdl.GLSetAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, 3)
sdl.GLSetAttribute(sdl.GL_CONTEXT_MINOR_VERSION, 0)
break
case C.RETRO_HW_CONTEXT_OPENGL:
fmt.Println("RETRO_HW_CONTEXT_OPENGL")
if video.hw.version_major >= 3 {
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_COMPATIBILITY)
}
break
default:
fmt.Println("Unsupported hw context:", video.hw.context_type)
}
video.window, err = sdl.CreateWindow(winTitle, sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED, winWidth, winHeight, sdl.WINDOW_OPENGL)
if err != nil {
panic(err)
}
video.context, err = video.window.GLCreateContext()
if err != nil {
panic(err)
}
if err = gl.InitWithProcAddrFunc(sdl.GLGetProcAddress); err != nil {
panic(err)
}
version := gl.GoStr(gl.GetString(gl.VERSION))
fmt.Println("OpenGL version: ", version)
// init_texture()
gl.GenTextures(1, &video.tex)
if video.tex < 0 {
panic(fmt.Sprintf("GenTextures: 0x%X", video.tex))
}
gl.BindTexture(gl.TEXTURE_2D, video.tex)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
gl.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
gl.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, video.max_width, video.max_height, 0, gl.RGBA, gl.UNSIGNED_BYTE, nil)
gl.BindTexture(gl.TEXTURE_2D, 0)
//init_framebuffer()
gl.GenFramebuffers(1, &video.fbo)
gl.BindFramebuffer(gl.FRAMEBUFFER, video.fbo)
gl.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, video.tex, 0)
if video.hw.depth {
gl.GenRenderbuffers(1, &video.rbo);
gl.BindRenderbuffer(gl.RENDERBUFFER, video.rbo)
if video.hw.stencil {
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH24_STENCIL8, video.base_width, video.base_height);
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, video.rbo);
} else {
gl.RenderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT24, video.base_width, video.base_height);
gl.FramebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, video.rbo);
}
gl.BindRenderbuffer(gl.RENDERBUFFER, 0)
}
status := gl.CheckFramebufferStatus(gl.FRAMEBUFFER)
if status != gl.FRAMEBUFFER_COMPLETE {
if e := gl.GetError(); e != gl.NO_ERROR {
panic(fmt.Sprintf("GL error: 0x%X, Frame status: 0x%X", e, status))
}
panic(fmt.Sprintf("Frame status: 0x%X", status))
}
C.bridge_context_reset(video.hw.context_reset)
}
//export deinitVideo
func deinitVideo() {
if video.hw.depth {
gl.DeleteRenderbuffers(1, &video.rbo);
}
gl.DeleteFramebuffers(1, &video.fbo)
gl.DeleteTextures(1, &video.tex)
sdl.GLDeleteContext(video.context)
video.window.Destroy()
video.isGl = false
}
var retroHandle unsafe.Pointer
var retroInit unsafe.Pointer
var retroDeinit unsafe.Pointer
var retroAPIVersion unsafe.Pointer
@ -258,41 +462,52 @@ var retroSerializeSize unsafe.Pointer
var retroSerialize unsafe.Pointer
var retroUnserialize unsafe.Pointer
func coreLoad(pathNoExt string) {
func loadFunction(handle unsafe.Pointer, name string) unsafe.Pointer {
cs := C.CString(name)
pointer := C.dlsym(handle, cs)
C.free(unsafe.Pointer(cs))
return pointer
}
func coreLoad(pathNoExt string, isGlAllowedParam bool, usesLibCoParam bool, pathToCfg string) {
isGlAllowed = isGlAllowedParam
usesLibCo = usesLibCoParam
coreConfig = ScanConfigFile(pathToCfg)
mu.Lock()
// Different OS requires different library, bruteforce till it finish
h := C.dlopen(C.CString(pathNoExt+".so"), C.RTLD_LAZY)
for _, ext := range config.EmulatorExtension {
pathWithExt := pathNoExt + ext
h = C.dlopen(C.CString(pathWithExt), C.RTLD_LAZY)
if h != nil {
cs := C.CString(pathWithExt)
retroHandle = C.dlopen(cs, C.RTLD_LAZY)
C.free(unsafe.Pointer(cs))
if retroHandle != nil {
break
}
}
if h == nil {
if retroHandle == nil {
err := C.dlerror()
log.Fatalf("error loading %s, err %+v", pathNoExt, *err)
}
retroInit = C.dlsym(h, C.CString("retro_init"))
retroDeinit = C.dlsym(h, C.CString("retro_deinit"))
retroAPIVersion = C.dlsym(h, C.CString("retro_api_version"))
retroGetSystemInfo = C.dlsym(h, C.CString("retro_get_system_info"))
retroGetSystemAVInfo = C.dlsym(h, C.CString("retro_get_system_av_info"))
retroSetEnvironment = C.dlsym(h, C.CString("retro_set_environment"))
retroSetVideoRefresh = C.dlsym(h, C.CString("retro_set_video_refresh"))
retroSetInputPoll = C.dlsym(h, C.CString("retro_set_input_poll"))
retroSetInputState = C.dlsym(h, C.CString("retro_set_input_state"))
retroSetAudioSample = C.dlsym(h, C.CString("retro_set_audio_sample"))
retroSetAudioSampleBatch = C.dlsym(h, C.CString("retro_set_audio_sample_batch"))
retroRun = C.dlsym(h, C.CString("retro_run"))
retroLoadGame = C.dlsym(h, C.CString("retro_load_game"))
retroUnloadGame = C.dlsym(h, C.CString("retro_unload_game"))
retroSerializeSize = C.dlsym(h, C.CString("retro_serialize_size"))
retroSerialize = C.dlsym(h, C.CString("retro_serialize"))
retroUnserialize = C.dlsym(h, C.CString("retro_unserialize"))
retroInit = loadFunction(retroHandle, "retro_init")
retroDeinit = loadFunction(retroHandle, "retro_deinit")
retroAPIVersion = loadFunction(retroHandle, "retro_api_version")
retroGetSystemInfo = loadFunction(retroHandle, "retro_get_system_info")
retroGetSystemAVInfo = loadFunction(retroHandle, "retro_get_system_av_info")
retroSetEnvironment = loadFunction(retroHandle, "retro_set_environment")
retroSetVideoRefresh = loadFunction(retroHandle, "retro_set_video_refresh")
retroSetInputPoll = loadFunction(retroHandle, "retro_set_input_poll")
retroSetInputState = loadFunction(retroHandle, "retro_set_input_state")
retroSetAudioSample = loadFunction(retroHandle, "retro_set_audio_sample")
retroSetAudioSampleBatch = loadFunction(retroHandle, "retro_set_audio_sample_batch")
retroRun = loadFunction(retroHandle, "retro_run")
retroLoadGame = loadFunction(retroHandle, "retro_load_game")
retroUnloadGame = loadFunction(retroHandle, "retro_unload_game")
retroSerializeSize = loadFunction(retroHandle, "retro_serialize_size")
retroSerialize = loadFunction(retroHandle, "retro_serialize")
retroUnserialize = loadFunction(retroHandle, "retro_unserialize")
mu.Unlock()
@ -339,8 +554,10 @@ func coreLoadGame(filename string) {
fmt.Println("ROM size:", size)
csFilename := C.CString(filename)
defer C.free(unsafe.Pointer(csFilename))
gi := C.struct_retro_game_info{
path: C.CString(filename),
path: csFilename,
size: C.size_t(size),
}
@ -361,8 +578,8 @@ func coreLoadGame(filename string) {
panic(err)
}
cstr := C.CString(string(bytes))
defer C.free(unsafe.Pointer(cstr))
gi.data = unsafe.Pointer(cstr)
}
ok := C.bridge_retro_load_game(retroLoadGame, &gi)
@ -400,6 +617,18 @@ func coreLoadGame(filename string) {
fmt.Println(" Sample rate: ", avi.timing.sample_rate) /* Sampling rate of audio. */
fmt.Println(" FPS: ", avi.timing.fps) /* FPS of video content. */
fmt.Println("-----------------------------------")
video.max_width = int32(avi.geometry.max_width)
video.max_height = int32(avi.geometry.max_height)
video.base_width = int32(avi.geometry.base_width)
video.base_height = int32(avi.geometry.base_height)
if video.isGl {
if usesLibCo {
C.bridge_execute(C.initVideo_cgo)
} else {
initVideo()
}
}
}
// serializeSize returns the amount of data the implementation requires to serialize
@ -435,14 +664,49 @@ func unserialize(bytes []byte, size uint) error {
}
func nanoarchShutdown() {
C.bridge_retro_unload_game(retroUnloadGame)
C.bridge_retro_deinit(retroDeinit)
if usesLibCo {
C.bridge_execute(retroUnloadGame)
C.bridge_execute(retroDeinit)
if video.isGl {
C.bridge_execute(C.deinitVideo_cgo)
}
} else {
if video.isGl {
// running inside a go routine, lock the thread to make sure the OpenGL context stays current
runtime.LockOSThread()
video.window.GLMakeCurrent(video.context)
}
C.bridge_retro_unload_game(retroUnloadGame)
C.bridge_retro_deinit(retroDeinit)
if video.isGl {
deinitVideo()
runtime.UnlockOSThread()
}
}
setRotation(0)
if r := C.dlclose(retroHandle); r != 0 {
fmt.Println("error closing core")
}
for _, element := range coreConfig {
C.free(unsafe.Pointer(element))
}
}
func nanoarchRun() {
C.bridge_retro_run(retroRun)
if usesLibCo {
C.bridge_execute(retroRun)
} else {
if video.isGl {
// running inside a go routine, lock the thread to make sure the OpenGL context stays current
runtime.LockOSThread()
video.window.GLMakeCurrent(video.context)
}
C.bridge_retro_run(retroRun)
if video.isGl {
runtime.UnlockOSThread()
}
}
}
func videoSetPixelFormat(format uint32) C.bool {
@ -467,7 +731,7 @@ func videoSetPixelFormat(format uint32) C.bool {
log.Fatalf("Unknown pixel type %v", format)
}
fmt.Printf("Video pixel: %v %v %v %v %v", video, format, C.RETRO_PIXEL_FORMAT_0RGB1555, C.RETRO_PIXEL_FORMAT_XRGB8888, C.RETRO_PIXEL_FORMAT_RGB565)
fmt.Printf("Video pixel: %v %v %v %v %v\n", video, format, C.RETRO_PIXEL_FORMAT_0RGB1555, C.RETRO_PIXEL_FORMAT_XRGB8888, C.RETRO_PIXEL_FORMAT_RGB565)
return true
}

View file

@ -37,7 +37,7 @@ type WebRTC struct {
AudioChannel chan []byte
VoiceInChannel chan []byte
VoiceOutChannel chan []byte
InputChannel chan int
InputChannel chan []byte
Done bool
lastTime time.Time
@ -90,7 +90,7 @@ func NewWebRTC() *WebRTC {
AudioChannel: make(chan []byte, 1),
VoiceInChannel: make(chan []byte, 1),
VoiceOutChannel: make(chan []byte, 1),
InputChannel: make(chan int, 100),
InputChannel: make(chan []byte, 100),
}
return w
}
@ -156,8 +156,8 @@ func (w *WebRTC) StartClient(isMobile bool, iceCB OnIceCallback) (string, error)
// Register text message handling
inputTrack.OnMessage(func(msg webrtc.DataChannelMessage) {
// TODO: Can add recover here + generalize
w.InputChannel <- int(msg.Data[1])<<8 + int(msg.Data[0])
// TODO: Can add recover here
w.InputChannel <- msg.Data
})
inputTrack.OnClose(func() {

View file

@ -137,7 +137,7 @@ func (h *Handler) detachPeerConn(pc *webrtc.WebRTC) {
room.Close()
// Signal end of input Channel
log.Println("Signal input chan")
pc.InputChannel <- -1
pc.InputChannel <- []byte{0xFF, 0xFF}
close(pc.InputChannel)
}
}

View file

@ -246,7 +246,7 @@ func (r *Room) startWebRTCSession(peerconnection *webrtc.WebRTC) {
if peerconnection.IsConnected() {
select {
case r.inputChannel <- nanoarch.InputEvent{KeyState: input, PlayerIdx: peerconnection.GameMeta.PlayerIndex, ConnID: peerconnection.ID}:
case r.inputChannel <- nanoarch.InputEvent{RawState: input, PlayerIdx: peerconnection.GameMeta.PlayerIndex, ConnID: peerconnection.ID}:
default:
}
}
@ -270,7 +270,7 @@ func (r *Room) RemoveSession(w *webrtc.WebRTC) {
}
// Detach input. Send end signal
select {
case r.inputChannel <- nanoarch.InputEvent{KeyState: -1, ConnID: w.ID}:
case r.inputChannel <- nanoarch.InputEvent{RawState: []byte{0xFF, 0xFF}, ConnID: w.ID}:
default:
}
}

View file

@ -94,26 +94,32 @@ func (o *Worker) spawnServer(port int) {
var httpsSrv *http.Server
if *config.Mode == config.ProdEnv || *config.Mode == config.StagingEnv {
var leurl string
if *config.Mode == config.StagingEnv {
leurl = stagingLEURL
} else {
leurl = acme.LetsEncryptURL
}
certManager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache("assets/cache"),
Client: &acme.Client{DirectoryURL: leurl},
}
httpsSrv = makeHTTPServer()
httpsSrv.Addr = ":443"
httpsSrv.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
httpsSrv.Addr = fmt.Sprintf(":%d", *config.HttpsPort)
if *config.HttpsChain == "" || *config.HttpsKey == "" {
*config.HttpsChain = ""
*config.HttpsKey = ""
var leurl string
if *config.Mode == config.StagingEnv {
leurl = stagingLEURL
} else {
leurl = acme.LetsEncryptURL
}
certManager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
Cache: autocert.DirCache("assets/cache"),
Client: &acme.Client{DirectoryURL: leurl},
}
httpsSrv.TLSConfig = &tls.Config{GetCertificate: certManager.GetCertificate}
}
go func() {
fmt.Printf("Starting HTTPS server on %s\n", httpsSrv.Addr)
err := httpsSrv.ListenAndServeTLS("", "")
err := httpsSrv.ListenAndServeTLS(*config.HttpsChain, *config.HttpsKey)
if err != nil {
log.Printf("httpsSrv.ListendAndServeTLS() failed with %s", err)
}

61
web/css/main.css vendored
View file

@ -74,6 +74,10 @@ body {
background-image: url('/static/img/ui/bt MOVE.png');
}
.dpad-empty {
background-image: url('/static/img/ui/bt MOVE EMPTY.png') !important;
}
#guide-txt {
color: #bababa;
font-size: 8px;
@ -97,6 +101,11 @@ body {
}
.bong-full {
opacity: 1.0 !important;
background-image: url('/static/img/ui/bong full.png') !important;
}
.dpad {
width: 20px;
height: 20px;
@ -652,3 +661,55 @@ body {
-ms-user-select: none;
user-select: none;
}
.dpad-toggle-label {
position: relative;
display: inline-block;
width: 35px;
height: 20px;
top: 15px;
left: 85px;
}
.dpad-toggle-label input {
opacity: 0;
width: 0;
height: 0;
}
.dpad-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #515151;
-webkit-transition: .4s;
transition: .4s;
border-radius: 20px;
}
.dpad-toggle-slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: #5f5f5f;
-webkit-transition: .4s;
transition: .4s;
border-radius: 50%;
}
input:checked + .dpad-toggle-slider {
background-color: #515151;
}
input:checked + .dpad-toggle-slider:before {
-webkit-transform: translateX(15px);
-ms-transform: translateX(15px);
transform: translateX(15px);
}

5
web/game.html vendored
View file

@ -76,6 +76,11 @@
<div id="help-overlay-detail"></div>
</div>
<div id="btn-help" unselectable="on" class="btn unselectable" value="help"></div>
<label class="dpad-toggle-label">
<input type="checkbox" id="dpad-toggle" checked>
<span class="dpad-toggle-slider"></span>
</label>
</div>
<a id="ribbon" style="position: fixed; right: 0; top: 0;" href="https://github.com/giongto35/cloud-game"><img width="149" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_right_gray_6d6d6d.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></a>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Before After
Before After

BIN
web/img/ui/bong full.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
web/img/ui/bt MOVE EMPTY.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

62
web/js/controller.js vendored
View file

@ -12,6 +12,15 @@
// used for mute/unmute
let interacted = false;
const DIR = (() => {
return {
IDLE: 'idle',
UP: 'up',
DOWN: 'down',
}
})();
let prevDir = DIR.IDLE;
// UI elements
// use $element[0] for DOM element
const gameScreen = $('#game-screen');
@ -201,17 +210,35 @@
state.keyRelease(data.key);
};
const onAxisChanged = (data) => {
// maybe move it somewhere
if (!interacted) {
// unmute when there is user interaction
gameScreen[0].muted = false;
interacted = true;
}
state.axisChanged(data.id, data.value);
};
const updatePlayerIndex = (idx) => {
var slider = document.getElementById('playeridx');
slider.value = idx + 1;
socket.updatePlayerIndex(idx);
};
const handleToggle = () => {
var toggle = document.getElementById('dpad-toggle');
toggle.checked = !toggle.checked;
event.pub(DPAD_TOGGLE, {checked: toggle.checked});
};
const app = {
state: {
eden: {
name: 'eden',
axisChanged: () => {
},
keyPress: () => {
},
keyRelease: () => {
@ -223,6 +250,8 @@
help: {
name: 'help',
axisChanged: () => {
},
keyPress: () => {
},
keyRelease: () => {
@ -242,6 +271,27 @@
menu: {
name: 'menu',
axisChanged: (id, value) => {
if (id === 1) { // Left Stick, Y Axis
let dir = DIR.IDLE;
if (value < -0.5) dir = DIR.UP;
if (value > 0.5) dir = DIR.DOWN;
if (dir !== prevDir) {
prevDir = dir;
switch (dir) {
case DIR.IDLE:
gameList.stopGamePickerTimer();
break;
case DIR.UP:
gameList.startGamePickerTimer(true);
break;
case DIR.DOWN:
gameList.startGamePickerTimer(false);
break;
}
}
}
},
keyPress: (key) => {
switch (key) {
case KEY.UP:
@ -277,6 +327,9 @@
case KEY.STATS:
event.pub(STATS_TOGGLE);
break;
case KEY.DTOGGLE:
handleToggle();
break;
}
},
menuReady: () => {
@ -285,6 +338,9 @@
game: {
name: 'game',
axisChanged: (id, value) => {
input.setAxisChanged(id, value);
},
keyPress: (key) => {
input.setKeyState(key, true);
},
@ -339,6 +395,9 @@
case KEY.STATS:
event.pub(STATS_TOGGLE);
break;
case KEY.DTOGGLE:
handleToggle();
break;
}
},
@ -373,7 +432,8 @@
});
event.sub(KEY_PRESSED, onKeyPress);
event.sub(KEY_RELEASED, onKeyRelease);
event.sub(KEY_STATE_UPDATED, data => rtcp.input(data));
event.sub(AXIS_CHANGED, onAxisChanged);
event.sub(CONTROLLER_UPDATED, data => rtcp.input(data));
// game screen stuff
gameScreen.on('loadstart', () => {

View file

@ -82,7 +82,9 @@ const MENU_RELEASED = 'menuReleased';
const KEY_PRESSED = 'keyPressed';
const KEY_RELEASED = 'keyReleased';
const KEY_STATE_UPDATED = 'keyStateUpdated';
const AXIS_CHANGED = 'axisChanged';
const CONTROLLER_UPDATED = 'controllerUpdated';
const DPAD_TOGGLE = 'dpadToggle';
const STATS_TOGGLE = 'statsToggle';
const HELP_OVERLAY_TOGGLED = 'helpOverlayToggled';

47
web/js/input/input.js vendored
View file

@ -1,7 +1,7 @@
const input = (() => {
let pollIntervalMs = 10;
let pollIntervalId = 0;
let isStateChanged = false;
let controllerChangedIndex = -1;
let controllerState = {
// control
@ -17,9 +17,16 @@ const input = (() => {
[KEY.UP]: false,
[KEY.DOWN]: false,
[KEY.LEFT]: false,
[KEY.RIGHT]: false
[KEY.RIGHT]: false,
// extra
[KEY.R2]: false,
[KEY.L2]: false,
[KEY.R3]: false,
[KEY.L3]: false
};
const controllerEncoded = new Array(5).fill(0);
const keys = Object.keys(controllerState);
const poll = () => {
@ -29,7 +36,7 @@ const input = (() => {
if (pollIntervalId > 0) return;
log.info(`[input] poll set to ${pollIntervalMs}ms`);
pollIntervalId = setInterval(sendKeyState, pollIntervalMs)
pollIntervalId = setInterval(sendControllerState, pollIntervalMs)
},
disable: () => {
if (pollIntervalId < 1) return;
@ -41,39 +48,47 @@ const input = (() => {
}
};
const sendKeyState = () => {
if (isStateChanged) {
event.pub(KEY_STATE_UPDATED, _encodeState());
isStateChanged = false;
const sendControllerState = () => {
if (controllerChangedIndex >= 0) {
event.pub(CONTROLLER_UPDATED, _encodeState());
controllerChangedIndex = -1;
}
};
const setKeyState = (name, state) => {
if (controllerState[name] !== undefined) {
controllerState[name] = state;
isStateChanged = true;
controllerChangedIndex = Math.max(controllerChangedIndex, 0);
}
};
const setAxisChanged = (index, value) => {
if (controllerEncoded[index+1] !== undefined) {
controllerEncoded[index+1] = Math.floor(32767 * value);
controllerChangedIndex = Math.max(controllerChangedIndex, index+1);
}
};
/**
* Converts controller state into a binary number.
* Converts key state into a bitmap and prepends it to the axes state.
*
* @returns {Uint8Array} The controller state.
* First byte is controller state.
* Second byte is d-pad state converted (shifted) into a byte.
* So the whole state is just splitted by 8 bits.
* @returns {Uint16Array} The controller state.
* First uint16 is the controller state bitmap.
* The other uint16 are the axes values.
* Truncated to the last value changed.
*
* @private
*/
const _encodeState = () => {
let result = 0;
for (let i = 0, len = keys.length; i < len; i++) result += controllerState[keys[i]] ? 1 << i : 0;
controllerEncoded[0] = 0;
for (let i = 0, len = keys.length; i < len; i++) controllerEncoded[0] += controllerState[keys[i]] ? 1 << i : 0;
return new Uint8Array([result & ((1 << 8) - 1), result >> 8]);
return new Uint16Array(controllerEncoded.slice(0, controllerChangedIndex+1));
}
return {
poll,
setKeyState,
setAxisChanged,
}
})(event, KEY);

View file

@ -17,10 +17,33 @@
* @version 1
*/
const joystick = (() => {
const deadZone = 0.1;
let joystickMap;
let joystickState;
let joystickState = {};
let joystickAxes = [];
let joystickIdx;
let joystickTimer = null;
let dpadMode = true;
function onDpadToggle(checked) {
if (dpadMode === checked) {
return //error?
}
if (dpadMode) {
dpadMode = false;
// reset dpad keys pressed before moving to analog stick mode
checkJoystickAxisState(KEY.LEFT, false);
checkJoystickAxisState(KEY.RIGHT, false);
checkJoystickAxisState(KEY.UP, false);
checkJoystickAxisState(KEY.DOWN, false);
} else {
dpadMode = true;
// reset analog stick axes before moving to dpad mode
joystickAxes.forEach(function (value, index) {
checkJoystickAxis(index, 0);
});
}
}
// check state for each axis -> dpad
function checkJoystickAxisState(name, state) {
@ -30,17 +53,31 @@ const joystick = (() => {
}
}
function checkJoystickAxis(axis, value) {
if (-deadZone < value && value < deadZone) value = 0;
if (joystickAxes[axis] !== value) {
joystickAxes[axis] = value;
event.pub(AXIS_CHANGED, {id: axis, value: value});
}
}
// loop timer for checking joystick state
function checkJoystickState() {
let gamepad = navigator.getGamepads()[joystickIdx];
if (gamepad) {
// axis -> dpad
let corX = gamepad.axes[0]; // -1 -> 1, left -> right
let corY = gamepad.axes[1]; // -1 -> 1, up -> down
checkJoystickAxisState(KEY.LEFT, corX <= -0.5);
checkJoystickAxisState(KEY.RIGHT, corX >= 0.5);
checkJoystickAxisState(KEY.UP, corY <= -0.5);
checkJoystickAxisState(KEY.DOWN, corY >= 0.5);
if (dpadMode) {
// axis -> dpad
let corX = gamepad.axes[0]; // -1 -> 1, left -> right
let corY = gamepad.axes[1]; // -1 -> 1, up -> down
checkJoystickAxisState(KEY.LEFT, corX <= -0.5);
checkJoystickAxisState(KEY.RIGHT, corX >= 0.5);
checkJoystickAxisState(KEY.UP, corY <= -0.5);
checkJoystickAxisState(KEY.DOWN, corY >= 0.5);
} else {
gamepad.axes.forEach(function (value, index) {
checkJoystickAxis(index, value);
});
}
// normal button map
Object.keys(joystickMap).forEach(function (btnIdx) {
@ -58,8 +95,8 @@ const joystick = (() => {
}
// we only capture the last plugged joystick
const onGamepadConnected = (event) => {
let gamepad = event.gamepad;
const onGamepadConnected = (e) => {
let gamepad = e.gamepad;
log.info(`Gamepad connected at index ${gamepad.index}: ${gamepad.id}. ${gamepad.buttons.length} buttons, ${gamepad.axes.length} axes.`);
joystickIdx = gamepad.index;
@ -167,12 +204,30 @@ const joystick = (() => {
};
}
// https://bugs.chromium.org/p/chromium/issues/detail?id=1076272
if (browser === 'chrome' && gamepad.id.includes('PLAYSTATION(R)3')) {
joystickMap = {
0: KEY.A,
1: KEY.B,
2: KEY.Y,
3: KEY.X,
4: KEY.L,
5: KEY.R,
8: KEY.SELECT,
9: KEY.START,
10: KEY.DTOGGLE,
11: KEY.R3,
};
}
// reset state
joystickState = {[KEY.LEFT]: false, [KEY.RIGHT]: false, [KEY.UP]: false, [KEY.DOWN]: false};
Object.keys(joystickMap).forEach(function (btnIdx) {
joystickState[btnIdx] = false;
});
joystickAxes = new Array(gamepad.axes.length).fill(0);
// looper, too intense?
if (joystickTimer !== null) {
clearInterval(joystickTimer);
@ -182,6 +237,8 @@ const joystick = (() => {
event.pub(GAMEPAD_CONNECTED);
};
event.sub(DPAD_TOGGLE, (data) => onDpadToggle(data.checked));
return {
init: () => {
// we only capture the last plugged joystick

View file

@ -30,17 +30,69 @@ const keyboard = (() => {
70: KEY.FULL, // f
72: KEY.HELP, // h
220: KEY.STATS, // backslash
84: KEY.DTOGGLE, // t
};
const onKey = (code, callback) => {
if (code in KEYBOARD_MAP) callback(KEYBOARD_MAP[code]);
let dpadMode = true;
let dpadState = {[KEY.LEFT]: false, [KEY.RIGHT]: false, [KEY.UP]: false, [KEY.DOWN]: false};
function onDpadToggle(checked) {
if (dpadMode === checked) {
return //error?
}
if (dpadMode) {
dpadMode = false;
// reset dpad keys pressed before moving to analog stick mode
for (const key in dpadState) {
if (dpadState[key] === true) {
dpadState[key] = false;
event.pub(KEY_RELEASED, {key: key});
}
}
} else {
dpadMode = true;
// reset analog stick axes before moving to dpad mode
value = (dpadState[KEY.RIGHT] === true ? 1 : 0) - (dpadState[KEY.LEFT] === true ? 1 : 0)
if (value !== 0) {
event.pub(AXIS_CHANGED, {id: 0, value: 0});
}
value = (dpadState[KEY.DOWN] === true ? 1 : 0) - (dpadState[KEY.UP] === true ? 1 : 0)
if (value !== 0) {
event.pub(AXIS_CHANGED, {id: 1, value: 0});
}
dpadState = {[KEY.LEFT]: false, [KEY.RIGHT]: false, [KEY.UP]: false, [KEY.DOWN]: false};
}
}
const onKey = (code, callback, state) => {
if (code in KEYBOARD_MAP) {
key = KEYBOARD_MAP[code]
if (key in dpadState) {
dpadState[key] = state
if (dpadMode) {
callback(key);
} else {
if (key === KEY.LEFT || key == KEY.RIGHT) {
value = (dpadState[KEY.RIGHT] === true ? 1 : 0) - (dpadState[KEY.LEFT] === true ? 1 : 0)
event.pub(AXIS_CHANGED, {id: 0, value: value});
} else {
value = (dpadState[KEY.DOWN] === true ? 1 : 0) - (dpadState[KEY.UP] === true ? 1 : 0)
event.pub(AXIS_CHANGED, {id: 1, value: value});
}
}
} else {
callback(key);
}
}
};
event.sub(DPAD_TOGGLE, (data) => onDpadToggle(data.checked));
return {
init: () => {
const body = $('body');
body.on('keyup', ev => onKey(ev.keyCode, key => event.pub(KEY_RELEASED, {key: key})));
body.on('keydown', ev => onKey(ev.keyCode, key => event.pub(KEY_PRESSED, {key: key})));
body.on('keyup', ev => onKey(ev.keyCode, key => event.pub(KEY_RELEASED, {key: key}), false));
body.on('keydown', ev => onKey(ev.keyCode, key => event.pub(KEY_PRESSED, {key: key}), true));
log.info('[input] keyboard has been initialized');
}
}

View file

@ -23,5 +23,10 @@ const KEY = (() => {
PAD3: 'pad3',
PAD4: 'pad4',
STATS: 'stats',
DTOGGLE: 'dtoggle',
L2: 'l2',
R2: 'r2',
L3: 'l3',
R3: 'r3',
}
})();

65
web/js/input/touch.js vendored
View file

@ -12,6 +12,7 @@ const touch = (() => {
// vpad state, use for mouse button down
let vpadState = {[KEY.UP]: false, [KEY.DOWN]: false, [KEY.LEFT]: false, [KEY.RIGHT]: false};
let analogState = [0, 0];
let vpadTouchIdx = null;
let vpadTouchDrag = null;
@ -23,12 +24,42 @@ const touch = (() => {
const playerSlider = $("#playeridx")
const dpad = $(".dpad");
const dpadToggle = document.getElementById('dpad-toggle')
dpadToggle.addEventListener('change', (e) => {
event.pub(DPAD_TOGGLE, {checked: e.target.checked});
});
let dpadMode = true;
const deadZone = 0.1;
function onDpadToggle(checked) {
if (dpadMode === checked) {
return //error?
}
if (dpadMode) {
dpadMode = false;
vpadHolder.addClass('dpad-empty');
vpadCircle.addClass('bong-full');
// reset dpad keys pressed before moving to analog stick mode
resetVpadState()
} else {
dpadMode = true;
vpadHolder.removeClass('dpad-empty');
vpadCircle.removeClass('bong-full');
}
}
function resetVpadState() {
// trigger up event?
checkVpadState(KEY.UP, false);
checkVpadState(KEY.DOWN, false);
checkVpadState(KEY.LEFT, false);
checkVpadState(KEY.RIGHT, false);
if (dpadMode) {
// trigger up event?
checkVpadState(KEY.UP, false);
checkVpadState(KEY.DOWN, false);
checkVpadState(KEY.LEFT, false);
checkVpadState(KEY.RIGHT, false);
} else {
checkAnalogState(0, 0);
checkAnalogState(1, 0);
}
vpadTouchDrag = null;
vpadTouchIdx = null;
@ -42,6 +73,14 @@ const touch = (() => {
}
}
function checkAnalogState(axis, value) {
if (-deadZone < value && value < deadZone) value = 0;
if (analogState[axis] !== value) {
analogState[axis] = value;
event.pub(AXIS_CHANGED, {id: axis, value: value});
}
}
function handleVpadJoystickDown(event) {
vpadCircle.css('transition', '0s');
vpadCircle.css('-moz-transition', '0s');
@ -105,10 +144,16 @@ const touch = (() => {
let xRatio = xNew / MAX_DIFF;
let yRatio = yNew / MAX_DIFF;
checkVpadState(KEY.LEFT, xRatio <= -0.5);
checkVpadState(KEY.RIGHT, xRatio >= 0.5);
checkVpadState(KEY.UP, yRatio <= -0.5);
checkVpadState(KEY.DOWN, yRatio >= 0.5);
if (dpadMode) {
checkVpadState(KEY.LEFT, xRatio <= -0.5);
checkVpadState(KEY.RIGHT, xRatio >= 0.5);
checkVpadState(KEY.UP, yRatio <= -0.5);
checkVpadState(KEY.DOWN, yRatio >= 0.5);
} else {
checkAnalogState(0, xRatio);
checkAnalogState(1, yRatio);
}
}
/*
@ -247,6 +292,8 @@ const touch = (() => {
event.pub(MENU_HANDLER_ATTACHED, {event: 'touchstart', handler: handleMenuDown});
event.pub(MENU_HANDLER_ATTACHED, {event: 'touchend', handler: handleMenuUp});
event.sub(DPAD_TOGGLE, (data) => onDpadToggle(data.checked));
return {
init: () => {
// add buttons into the state 🤦