diff --git a/assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..c67f7c8e Binary files /dev/null and b/assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so differ diff --git a/assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..ff9713c9 Binary files /dev/null and b/assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so differ diff --git a/assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..352e7620 Binary files /dev/null and b/assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so differ diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..6c857e1b Binary files /dev/null and b/assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so differ diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg b/assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg new file mode 100644 index 00000000..6fa02e37 --- /dev/null +++ b/assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg @@ -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 diff --git a/assets/emulator/libretro/cores/mupen64plus_next_libretro.so b/assets/emulator/libretro/cores/mupen64plus_next_libretro.so new file mode 100755 index 00000000..b21692cd Binary files /dev/null and b/assets/emulator/libretro/cores/mupen64plus_next_libretro.so differ diff --git a/assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..607397a4 Binary files /dev/null and b/assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so differ diff --git a/assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so b/assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so new file mode 100755 index 00000000..92fcdc25 Binary files /dev/null and b/assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so differ diff --git a/go.mod b/go.mod index 38dd805a..f8514e11 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/pkg/config/config.go b/pkg/config/config.go index 94db6983..1ebfd614 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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"} diff --git a/pkg/coordinator/config.go b/pkg/coordinator/config.go index a1f0f916..f36e1860 100644 --- a/pkg/coordinator/config.go +++ b/pkg/coordinator/config.go @@ -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 } diff --git a/pkg/coordinator/coordinator.go b/pkg/coordinator/coordinator.go index 0ba33932..68b7df7e 100644 --- a/pkg/coordinator/coordinator.go +++ b/pkg/coordinator/coordinator.go @@ -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) } diff --git a/pkg/coordinator/handlers.go b/pkg/coordinator/handlers.go index 3ded9905..6b0c2f36 100644 --- a/pkg/coordinator/handlers.go +++ b/pkg/coordinator/handlers.go @@ -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) } diff --git a/pkg/emulator/libretro/nanoarch/cfuncs.go b/pkg/emulator/libretro/nanoarch/cfuncs.go index abda9b8b..d71a250c 100644 --- a/pkg/emulator/libretro/nanoarch/cfuncs.go +++ b/pkg/emulator/libretro/nanoarch/cfuncs.go @@ -2,6 +2,7 @@ package nanoarch /* #include "libretro.h" +#include #include #include #include @@ -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" diff --git a/pkg/emulator/libretro/nanoarch/configscanner.go b/pkg/emulator/libretro/nanoarch/configscanner.go new file mode 100644 index 00000000..2a40932d --- /dev/null +++ b/pkg/emulator/libretro/nanoarch/configscanner.go @@ -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 +} diff --git a/pkg/emulator/libretro/nanoarch/naemulator.go b/pkg/emulator/libretro/nanoarch/naemulator.go index c1b70691..2ca3eaae 100644 --- a/pkg/emulator/libretro/nanoarch/naemulator.go +++ b/pkg/emulator/libretro/nanoarch/naemulator.go @@ -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 diff --git a/pkg/emulator/libretro/nanoarch/nanoarch.go b/pkg/emulator/libretro/nanoarch/nanoarch.go index 76850cc1..228fd7b5 100644 --- a/pkg/emulator/libretro/nanoarch/nanoarch.go +++ b/pkg/emulator/libretro/nanoarch/nanoarch.go @@ -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 } diff --git a/pkg/webrtc/webrtc.go b/pkg/webrtc/webrtc.go index f933c768..87e90e56 100644 --- a/pkg/webrtc/webrtc.go +++ b/pkg/webrtc/webrtc.go @@ -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() { diff --git a/pkg/worker/handlers.go b/pkg/worker/handlers.go index c57d2fcf..ddb6d25b 100644 --- a/pkg/worker/handlers.go +++ b/pkg/worker/handlers.go @@ -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) } } diff --git a/pkg/worker/room/room.go b/pkg/worker/room/room.go index 4258e15c..6483a870 100644 --- a/pkg/worker/room/room.go +++ b/pkg/worker/room/room.go @@ -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: } } diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index 2b298f02..d822831a 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -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) } diff --git a/web/css/main.css b/web/css/main.css index 7b555a96..2df283b2 100644 --- a/web/css/main.css +++ b/web/css/main.css @@ -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); +} diff --git a/web/game.html b/web/game.html index d5a2f1fa..d3c33a54 100644 --- a/web/game.html +++ b/web/game.html @@ -76,6 +76,11 @@
+ + Fork me on GitHub diff --git a/web/img/help_overlay.png b/web/img/help_overlay.png index a4c6a5b9..faec66ac 100644 Binary files a/web/img/help_overlay.png and b/web/img/help_overlay.png differ diff --git a/web/img/ui/bong full.png b/web/img/ui/bong full.png new file mode 100644 index 00000000..e2d1acf8 Binary files /dev/null and b/web/img/ui/bong full.png differ diff --git a/web/img/ui/bt MOVE EMPTY.png b/web/img/ui/bt MOVE EMPTY.png new file mode 100644 index 00000000..cae7a2d0 Binary files /dev/null and b/web/img/ui/bt MOVE EMPTY.png differ diff --git a/web/js/controller.js b/web/js/controller.js index 94edce1a..dacf2451 100644 --- a/web/js/controller.js +++ b/web/js/controller.js @@ -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', () => { diff --git a/web/js/event/event.js b/web/js/event/event.js index e7d58ec1..11606396 100644 --- a/web/js/event/event.js +++ b/web/js/event/event.js @@ -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'; diff --git a/web/js/input/input.js b/web/js/input/input.js index 76892d6c..9116b01a 100644 --- a/web/js/input/input.js +++ b/web/js/input/input.js @@ -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); diff --git a/web/js/input/joystick.js b/web/js/input/joystick.js index 75b06916..caab2f8b 100644 --- a/web/js/input/joystick.js +++ b/web/js/input/joystick.js @@ -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 diff --git a/web/js/input/keyboard.js b/web/js/input/keyboard.js index 2531b232..6f59d292 100644 --- a/web/js/input/keyboard.js +++ b/web/js/input/keyboard.js @@ -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'); } } diff --git a/web/js/input/keys.js b/web/js/input/keys.js index 590102d8..bef203cb 100644 --- a/web/js/input/keys.js +++ b/web/js/input/keys.js @@ -23,5 +23,10 @@ const KEY = (() => { PAD3: 'pad3', PAD4: 'pad4', STATS: 'stats', + DTOGGLE: 'dtoggle', + L2: 'l2', + R2: 'r2', + L3: 'l3', + R3: 'r3', } })(); diff --git a/web/js/input/touch.js b/web/js/input/touch.js index fe17ca5f..42a01cc3 100644 --- a/web/js/input/touch.js +++ b/web/js/input/touch.js @@ -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 🤦