mirror of
https://github.com/giongto35/cloud-game.git
synced 2026-01-23 10:35:44 +00:00
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:
parent
9d17435d7e
commit
091b086bcb
33 changed files with 931 additions and 147 deletions
BIN
assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/fbneo_libretro.armv7-neon-hf.so
vendored
Executable file
Binary file not shown.
BIN
assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/mednafen_snes_libretro.armv7-neon-hf.so
vendored
Executable file
Binary file not shown.
BIN
assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/mgba_libretro.armv7-neon-hf.so
vendored
Executable file
Binary file not shown.
BIN
assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/mupen64plus_next_libretro.armv7-neon-hf.so
vendored
Executable file
Binary file not shown.
53
assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg
vendored
Normal file
53
assets/emulator/libretro/cores/mupen64plus_next_libretro.cfg
vendored
Normal 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
|
||||
BIN
assets/emulator/libretro/cores/mupen64plus_next_libretro.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/mupen64plus_next_libretro.so
vendored
Executable file
Binary file not shown.
BIN
assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/nestopia_libretro.armv7-neon-hf.so
vendored
Executable file
Binary file not shown.
BIN
assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so
vendored
Executable file
BIN
assets/emulator/libretro/cores/pcsx_rearmed_libretro.armv7-neon-hf.so
vendored
Executable file
Binary file not shown.
3
go.mod
vendored
3
go.mod
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
46
pkg/emulator/libretro/nanoarch/configscanner.go
Normal file
46
pkg/emulator/libretro/nanoarch/configscanner.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
61
web/css/main.css
vendored
|
|
@ -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
5
web/game.html
vendored
|
|
@ -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>
|
||||
|
|
|
|||
BIN
web/img/help_overlay.png
vendored
BIN
web/img/help_overlay.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 18 KiB |
BIN
web/img/ui/bong full.png
vendored
Normal file
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
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
62
web/js/controller.js
vendored
|
|
@ -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', () => {
|
||||
|
|
|
|||
4
web/js/event/event.js
vendored
4
web/js/event/event.js
vendored
|
|
@ -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
47
web/js/input/input.js
vendored
|
|
@ -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);
|
||||
|
|
|
|||
77
web/js/input/joystick.js
vendored
77
web/js/input/joystick.js
vendored
|
|
@ -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
|
||||
|
|
|
|||
60
web/js/input/keyboard.js
vendored
60
web/js/input/keyboard.js
vendored
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
5
web/js/input/keys.js
vendored
5
web/js/input/keys.js
vendored
|
|
@ -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
65
web/js/input/touch.js
vendored
|
|
@ -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 🤦
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue