diff --git a/000.png b/000.png new file mode 100644 index 00000000..fb074bc4 Binary files /dev/null and b/000.png differ diff --git a/001.png b/001.png new file mode 100644 index 00000000..e3c583e4 Binary files /dev/null and b/001.png differ diff --git a/main.go b/main.go index 8db45050..2e8ffb8e 100644 --- a/main.go +++ b/main.go @@ -17,26 +17,31 @@ import ( ) var webRTC *webrtc.WebRTC -var width = 800 -var height = 600 +var width = 256 +var height = 240 func init() { - webRTC = webrtc.NewWebRTC() - director := ui.NewDirector() - director.Start("games/supermariobros.rom") - // start screenshot loop, wait for connection - go screenshotLoop(director.GetImageChannel()) +} + +func startGame(path string, imageChannel chan *image.RGBA) { + ui.Run([]string{path}, imageChannel) } func main() { fmt.Println("http://localhost:8000") + webRTC = webrtc.NewWebRTC() router := mux.NewRouter() router.HandleFunc("/", getWeb).Methods("GET") router.HandleFunc("/session", postSession).Methods("POST") - http.ListenAndServe(":8000", router) + go http.ListenAndServe(":8000", router) + // start screenshot loop, wait for connection + imageChannel := make(chan *image.RGBA, 2) + go screenshotLoop(imageChannel) + startGame("games/supermariobros.rom", imageChannel) + time.Sleep(time.Minute) } func getWeb(w http.ResponseWriter, r *http.Request) { diff --git a/mario_main.go b/mario_main.go deleted file mode 100644 index 18f4629b..00000000 --- a/mario_main.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -import ( - "github.com/giongto35/game-online/ui" -) - -func startGame(path string) { - ui.Run(path) -} diff --git a/ui/audio.go b/ui/audio.go new file mode 100644 index 00000000..6508b2cb --- /dev/null +++ b/ui/audio.go @@ -0,0 +1,54 @@ +package ui + +import "github.com/gordonklaus/portaudio" + +type Audio struct { + stream *portaudio.Stream + sampleRate float64 + outputChannels int + channel chan float32 +} + +func NewAudio() *Audio { + a := Audio{} + a.channel = make(chan float32, 44100) + return &a +} + +func (a *Audio) Start() error { + host, err := portaudio.DefaultHostApi() + if err != nil { + return err + } + parameters := portaudio.HighLatencyParameters(nil, host.DefaultOutputDevice) + stream, err := portaudio.OpenStream(parameters, a.Callback) + if err != nil { + return err + } + if err := stream.Start(); err != nil { + return err + } + a.stream = stream + a.sampleRate = parameters.SampleRate + a.outputChannels = parameters.Output.Channels + return nil +} + +func (a *Audio) Stop() error { + return a.stream.Close() +} + +func (a *Audio) Callback(out []float32) { + var output float32 + for i := range out { + if i%a.outputChannels == 0 { + select { + case sample := <-a.channel: + output = sample + default: + output = 0 + } + } + out[i] = output + } +} diff --git a/ui/director.go b/ui/director.go index 33a04ba8..680664f2 100644 --- a/ui/director.go +++ b/ui/director.go @@ -3,28 +3,39 @@ package ui import ( "image" "log" - "time" - "github.com/fogleman/nes/nes" + "github.com/giongto35/game-online/nes" + "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/glfw/v3.2/glfw" ) type View interface { Enter() Exit() - GetImageChannel() chan *image.RGBA Update(t, dt float64) } type Director struct { - view View - timestamp float64 + window *glfw.Window + audio *Audio + view View + menuView View + timestamp float64 + imageChannel chan *image.RGBA } -func NewDirector() *Director { +func NewDirector(window *glfw.Window, audio *Audio, imageChannel chan *image.RGBA) *Director { director := Director{} + director.window = window + director.audio = audio + director.imageChannel = imageChannel return &director } +func (d *Director) SetTitle(title string) { + d.window.SetTitle(title) +} + func (d *Director) SetView(view View) { if d.view != nil { d.view.Exit() @@ -33,12 +44,12 @@ func (d *Director) SetView(view View) { if d.view != nil { d.view.Enter() } - d.timestamp = float64(time.Now().Unix()) + d.timestamp = glfw.GetTime() } func (d *Director) Step() { - //timestamp := glfw.GetTime() - timestamp := float64(time.Now().Unix()) + gl.Clear(gl.COLOR_BUFFER_BIT) + timestamp := glfw.GetTime() dt := timestamp - d.timestamp d.timestamp = timestamp if d.view != nil { @@ -46,12 +57,22 @@ func (d *Director) Step() { } } -func (d *Director) Start(path string) { - d.PlayGame(path) +func (d *Director) Start(paths []string) { + d.menuView = NewMenuView(d, paths) + if len(paths) == 1 { + d.PlayGame(paths[0]) + } else { + d.ShowMenu() + } d.Run() } func (d *Director) Run() { + for !d.window.ShouldClose() { + d.Step() + d.window.SwapBuffers() + glfw.PollEvents() + } d.SetView(nil) } @@ -64,9 +85,9 @@ func (d *Director) PlayGame(path string) { if err != nil { log.Fatalln(err) } - d.SetView(NewGameView(d, console, path, hash)) + d.SetView(NewGameView(d, console, path, hash, d.imageChannel)) } -func (d *Director) GetImageChannel() chan *image.RGBA { - return d.view.GetImageChannel() +func (d *Director) ShowMenu() { + d.SetView(d.menuView) } diff --git a/ui/font.go b/ui/font.go new file mode 100644 index 00000000..10291bc4 --- /dev/null +++ b/ui/font.go @@ -0,0 +1,160 @@ +package ui + +import ( + "bytes" + "image" + "image/color" + "image/draw" + "image/png" + "log" + "strings" +) + +var fontMask image.Image + +func init() { + im, err := png.Decode(bytes.NewBuffer(fontData)) + if err != nil { + log.Fatalln(err) + } + size := im.Bounds().Size() + mask := image.NewRGBA(im.Bounds()) + for y := 0; y < size.Y; y++ { + for x := 0; x < size.X; x++ { + r, _, _, _ := im.At(x, y).RGBA() + if r > 0 { + mask.Set(x, y, color.Opaque) + } + } + } + fontMask = mask +} + +func CreateGenericThumbnail(text string) image.Image { + im := image.NewRGBA(image.Rect(0, 0, 256, 240)) + draw.Draw(im, im.Rect, &image.Uniform{color.Black}, image.ZP, draw.Src) + DrawCenteredText(im, text, 1, 2, color.RGBA{128, 128, 128, 255}) + DrawCenteredText(im, text, 0, 0, color.White) + return im +} + +func WordWrap(text string, maxLength int) []string { + var rows []string + words := strings.Fields(text) + if len(words) == 0 { + return rows + } + row := words[0] + for _, word := range words[1:] { + newRow := row + " " + word + if len(newRow) <= maxLength { + row = newRow + } else { + rows = append(rows, row) + row = word + } + } + rows = append(rows, row) + return rows +} + +func DrawCenteredText(dst draw.Image, text string, dx, dy int, c color.Color) { + rows := WordWrap(text, 15) + for i, row := range rows { + x := 128 - len(row)*8 + y := 120 - len(rows)*12 + i*24 + DrawText(dst, x+dx, y+dy, row, c) + } +} + +func DrawCharacter(dst draw.Image, x, y int, ch byte, c color.Color) { + if ch < 32 || ch > 128 { + return + } + cx := int((ch-32)%16) * 16 + cy := int((ch-32)/16) * 16 + r := image.Rect(x, y, x+16, y+16) + src := &image.Uniform{c} + sp := image.Pt(cx, cy) + draw.DrawMask(dst, r, src, sp, fontMask, sp, draw.Over) +} + +func DrawText(dst draw.Image, x, y int, text string, c color.Color) { + for i := range text { + DrawCharacter(dst, x, y, text[i], c) + x += 16 + } +} + +var fontData = []byte{ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, + 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x60, + 0x02, 0x03, 0x00, 0x00, 0x00, 0x8F, 0x9F, 0x44, 0x1B, 0x00, 0x00, 0x00, + 0x06, 0x50, 0x4C, 0x54, 0x45, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFE, 0x6A, + 0x62, 0xC8, 0x2E, 0x00, 0x00, 0x00, 0x01, 0x62, 0x4B, 0x47, 0x44, 0x00, + 0x88, 0x05, 0x1D, 0x48, 0x00, 0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, + 0x00, 0x00, 0x0E, 0xC4, 0x00, 0x00, 0x0E, 0xC4, 0x01, 0x95, 0x2B, 0x0E, + 0x1B, 0x00, 0x00, 0x02, 0xD2, 0x49, 0x44, 0x41, 0x54, 0x78, 0xDA, 0xC5, + 0x98, 0x49, 0x62, 0x23, 0x21, 0x0C, 0x45, 0xB5, 0xE1, 0x7E, 0xDA, 0xFC, + 0xFB, 0x5F, 0xA5, 0x4B, 0xE8, 0x4B, 0x88, 0x72, 0x2A, 0x1D, 0x07, 0xB9, + 0x9B, 0xC4, 0x2E, 0x8C, 0xE1, 0x19, 0x8D, 0x0C, 0x22, 0x56, 0x86, 0x5E, + 0xFF, 0x98, 0x2F, 0x81, 0x78, 0x81, 0xB5, 0xC5, 0x77, 0xF2, 0x50, 0xE2, + 0x9B, 0x63, 0x00, 0x90, 0x80, 0x71, 0xD5, 0xAD, 0xE6, 0x9F, 0xC5, 0x81, + 0x90, 0xD9, 0x92, 0xF0, 0x2C, 0xF9, 0x6B, 0x4D, 0x00, 0x00, 0xEA, 0xFD, + 0x31, 0x45, 0x40, 0x4C, 0xD2, 0xDE, 0x50, 0xC4, 0xBB, 0x4B, 0xD0, 0x02, + 0xF0, 0xCF, 0x26, 0x82, 0xBA, 0xE2, 0xE0, 0xDF, 0x06, 0x00, 0x5E, 0xD8, + 0x1D, 0xBB, 0x04, 0xE7, 0x00, 0x4E, 0xC5, 0x44, 0x98, 0xAD, 0xF6, 0x3F, + 0x20, 0xF2, 0x17, 0x11, 0x96, 0x79, 0xBB, 0x00, 0xAE, 0x48, 0xA5, 0x11, + 0xCB, 0x34, 0xAB, 0x19, 0xB1, 0x2C, 0x08, 0x69, 0x03, 0x24, 0x87, 0xD3, + 0xB7, 0xEE, 0x48, 0xEA, 0xEE, 0xCA, 0x05, 0xA0, 0x7D, 0x80, 0xE7, 0x68, + 0x79, 0x0C, 0x9F, 0x9F, 0xB4, 0xFE, 0x10, 0x90, 0x8E, 0x33, 0x03, 0xC9, + 0x2A, 0xEE, 0x28, 0x70, 0xD7, 0x66, 0xBB, 0xBC, 0x2A, 0x5C, 0xDC, 0xE9, + 0x8E, 0x01, 0x23, 0x03, 0x67, 0xD0, 0x89, 0x90, 0x41, 0x54, 0x82, 0x7C, + 0x99, 0x70, 0x33, 0x23, 0x1A, 0x00, 0x48, 0x3B, 0x79, 0x0E, 0x63, 0x58, + 0xB3, 0x1F, 0xE4, 0x0E, 0x70, 0xAF, 0x66, 0xA5, 0x17, 0x10, 0x8A, 0xA3, + 0x00, 0xD1, 0x29, 0x95, 0x7B, 0xF3, 0xA3, 0x45, 0xEB, 0x02, 0xB8, 0x4D, + 0xE7, 0x40, 0x95, 0x32, 0x6D, 0x57, 0xEE, 0x4B, 0xE0, 0xC6, 0xE0, 0x06, + 0xC0, 0x15, 0x36, 0x9E, 0xB2, 0x62, 0x36, 0x37, 0xC5, 0x2D, 0xF3, 0x2E, + 0x33, 0xA2, 0x26, 0x97, 0x63, 0xC0, 0xB4, 0x9E, 0xA5, 0x73, 0xBA, 0xED, + 0x1C, 0x12, 0xEE, 0x6B, 0x7A, 0x02, 0x13, 0x6D, 0x00, 0x8A, 0x2B, 0x3B, + 0xE4, 0x18, 0xF0, 0x46, 0x79, 0x23, 0xEA, 0xDF, 0x00, 0xF8, 0x54, 0xB5, + 0x84, 0xB0, 0xFA, 0x12, 0xE7, 0x15, 0xF5, 0xF4, 0x36, 0xE5, 0xF4, 0x7A, + 0x09, 0xB2, 0x39, 0xF6, 0x18, 0x80, 0x4C, 0xE2, 0xF6, 0xF4, 0x5A, 0xD5, + 0xD7, 0x0A, 0xB6, 0xE8, 0x65, 0xBF, 0x67, 0x7F, 0x69, 0xEA, 0x53, 0x00, + 0xA7, 0xC6, 0x0D, 0x8E, 0xAC, 0xE7, 0xEE, 0xEA, 0x13, 0x90, 0x71, 0x67, + 0x22, 0xB6, 0x03, 0x80, 0x1C, 0x00, 0x44, 0x52, 0xA1, 0x32, 0x03, 0xB0, + 0x12, 0x8A, 0x82, 0x0A, 0x6E, 0x02, 0x00, 0x0F, 0x22, 0x70, 0x9A, 0x54, + 0x76, 0xB0, 0x30, 0x4C, 0x9C, 0x36, 0x40, 0xE9, 0xF8, 0x6A, 0xC6, 0x1B, + 0x60, 0xA0, 0xA4, 0xFB, 0x2E, 0xC0, 0x80, 0xA6, 0xE2, 0x36, 0x57, 0xF6, + 0x3D, 0x4F, 0xBA, 0xF2, 0xA0, 0x52, 0x07, 0x96, 0xD9, 0x19, 0x0B, 0x87, + 0x80, 0xFF, 0x5F, 0xC0, 0xF4, 0x05, 0xA6, 0xAE, 0x3A, 0xC5, 0x4D, 0xB9, + 0x53, 0x0C, 0x6E, 0x7F, 0x7D, 0x59, 0xEF, 0x02, 0x6C, 0x9D, 0xDD, 0x57, + 0xB7, 0xB6, 0x19, 0xB8, 0x1E, 0xC4, 0xB9, 0xC4, 0xF8, 0xC7, 0x8F, 0x00, + 0x64, 0x07, 0x0C, 0x37, 0xD5, 0x04, 0x8C, 0x72, 0x00, 0xE1, 0x36, 0xA0, + 0x1B, 0xC0, 0x85, 0x25, 0x00, 0x1A, 0x89, 0x35, 0x96, 0xFE, 0x75, 0x08, + 0x8B, 0x3E, 0x4D, 0x00, 0x33, 0xE1, 0x0C, 0xE9, 0x75, 0xC6, 0x0B, 0x37, + 0xCD, 0xE5, 0xCE, 0x25, 0xE0, 0x10, 0xEC, 0x2B, 0xCC, 0x39, 0x80, 0x5B, + 0x29, 0x0B, 0xD5, 0x1A, 0xBA, 0x73, 0xB1, 0x71, 0x41, 0x34, 0x96, 0xFF, + 0x3C, 0x04, 0xB5, 0x03, 0x2E, 0x09, 0x36, 0x25, 0xFA, 0xF3, 0x96, 0xD2, + 0xE8, 0xCA, 0x57, 0x89, 0x67, 0x13, 0xE0, 0x30, 0x92, 0xD1, 0x00, 0xC0, + 0xC3, 0x11, 0x8E, 0xD1, 0x52, 0xF5, 0x95, 0x8E, 0xF4, 0xD5, 0x01, 0xBC, + 0x1F, 0x50, 0x1F, 0x7C, 0xFB, 0x10, 0x80, 0x1B, 0x6D, 0xDF, 0xAE, 0xF8, + 0xB6, 0x46, 0xD6, 0x2A, 0xAA, 0x37, 0x11, 0x34, 0x0E, 0xE6, 0xEC, 0xDF, + 0x01, 0xE0, 0xE4, 0x06, 0x13, 0x57, 0xD6, 0x81, 0x78, 0x16, 0x11, 0x06, + 0x3D, 0xD9, 0xF6, 0x1A, 0x31, 0xE2, 0x23, 0x00, 0xC4, 0xF1, 0x87, 0xAD, + 0x09, 0xD0, 0x7F, 0x05, 0x58, 0x97, 0x0E, 0x3B, 0x20, 0x82, 0xAD, 0x1D, + 0x60, 0x99, 0xE3, 0x66, 0xC6, 0x32, 0xED, 0xF1, 0x22, 0x4E, 0x00, 0xD2, + 0x8C, 0xE7, 0x80, 0xE7, 0x78, 0xFD, 0xBA, 0xFE, 0x46, 0xC0, 0xFF, 0x02, + 0xF0, 0x5D, 0x3F, 0xE6, 0xF6, 0x99, 0x60, 0xF1, 0x74, 0x7E, 0xF9, 0x28, + 0x60, 0x9D, 0x01, 0xFC, 0xA2, 0x6A, 0x74, 0x00, 0xC2, 0x7C, 0xB6, 0x9C, + 0x4A, 0x6C, 0x75, 0x96, 0x63, 0xC1, 0x5F, 0x74, 0xEF, 0x02, 0x58, 0xF7, + 0x2F, 0xC7, 0x80, 0x0C, 0x5F, 0x8A, 0x84, 0xEA, 0xDA, 0xB6, 0xDE, 0x84, + 0xCB, 0x22, 0x77, 0x35, 0x32, 0xD6, 0xF5, 0x61, 0x23, 0x40, 0x25, 0xAF, + 0xFC, 0x36, 0x80, 0x75, 0x56, 0xD4, 0xE3, 0xBE, 0x48, 0xD9, 0xDD, 0x34, + 0x00, 0x52, 0x89, 0xB9, 0xC3, 0xF2, 0x04, 0x7E, 0xA5, 0xAF, 0x10, 0xA1, + 0xDC, 0x56, 0x7C, 0x02, 0xA0, 0x92, 0x8B, 0x06, 0xF7, 0xFF, 0xB2, 0xD2, + 0x3A, 0xAF, 0x21, 0xFD, 0x88, 0xAC, 0xD5, 0x95, 0xF3, 0x0A, 0xB5, 0x0D, + 0xF0, 0xEB, 0x7B, 0x83, 0x53, 0xC0, 0x1F, 0xEF, 0x0D, 0xA2, 0x4D, 0x77, + 0x69, 0xB8, 0xB7, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, + 0x42, 0x60, 0x82, +} diff --git a/ui/gameview.go b/ui/gameview.go index 31a1a7da..a796a495 100644 --- a/ui/gameview.go +++ b/ui/gameview.go @@ -3,7 +3,9 @@ package ui import ( "image" - "github.com/fogleman/nes/nes" + "github.com/giongto35/game-online/nes" + "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/glfw/v3.2/glfw" ) const padding = 0 @@ -13,21 +15,23 @@ type GameView struct { console *nes.Console title string hash string + texture uint32 record bool frames []image.Image imageChannel chan *image.RGBA } -func NewGameView(director *Director, console *nes.Console, title, hash string) View { - imageChannel := make(chan *image.RGBA, 2) - return &GameView{director, console, title, hash, false, nil, imageChannel} -} - -func (view *GameView) GetImageChannel() chan *image.RGBA { - return view.imageChannel +func NewGameView(director *Director, console *nes.Console, title, hash string, imageChannel chan *image.RGBA) View { + texture := createTexture() + return &GameView{director, console, title, hash, texture, false, nil, imageChannel} } func (view *GameView) Enter() { + gl.ClearColor(0, 0, 0, 1) + view.director.SetTitle(view.title) + view.console.SetAudioChannel(view.director.audio.channel) + view.console.SetAudioSampleRate(view.director.audio.sampleRate) + view.director.window.SetKeyCallback(view.onKey) // load state if err := view.console.LoadState(savePath(view.hash)); err == nil { return @@ -44,6 +48,7 @@ func (view *GameView) Enter() { } func (view *GameView) Exit() { + view.director.window.SetKeyCallback(nil) view.console.SetAudioChannel(nil) view.console.SetAudioSampleRate(0) // save sram @@ -59,40 +64,79 @@ func (view *GameView) Update(t, dt float64) { if dt > 1 { dt = 0 } + window := view.director.window console := view.console - //if readKey(window, glfw.KeyEscape) { - //view.director.ShowMenu() - //} - //updateControllers(window, console) - view.imageChannel <- console.Buffer() + if joystickReset(glfw.Joystick1) { + view.director.ShowMenu() + } + if joystickReset(glfw.Joystick2) { + view.director.ShowMenu() + } + if readKey(window, glfw.KeyEscape) { + view.director.ShowMenu() + } + updateControllers(window, console) console.StepSeconds(dt) + gl.BindTexture(gl.TEXTURE_2D, view.texture) + setTexture(console.Buffer()) + view.imageChannel <- console.Buffer() + drawBuffer(view.director.window) + gl.BindTexture(gl.TEXTURE_2D, 0) if view.record { view.frames = append(view.frames, copyImage(console.Buffer())) } } -//func (view *GameView) onKey(window *glfw.Window, -//key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { -//if action == glfw.Press { -//switch key { -//case glfw.KeyR: -//view.console.Reset() -//case glfw.KeyTab: -//if view.record { -//view.record = false -//view.frames = nil -//} else { -//view.record = true -//} -//} -//} -//} +func (view *GameView) onKey(window *glfw.Window, + key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { + if action == glfw.Press { + switch key { + case glfw.KeySpace: + screenshot(view.console.Buffer()) + case glfw.KeyR: + view.console.Reset() + case glfw.KeyTab: + if view.record { + view.record = false + animation(view.frames) + view.frames = nil + } else { + view.record = true + } + } + } +} -//func updateControllers(window *glfw.Window, console *nes.Console) { -//turbo := console.PPU.Frame%6 < 3 -//k1 := readKeys(window, turbo) -//j1 := readJoystick(glfw.Joystick1, turbo) -//j2 := readJoystick(glfw.Joystick2, turbo) -//console.SetButtons1(combineButtons(k1, j1)) -//console.SetButtons2(j2) -//} +func drawBuffer(window *glfw.Window) { + w, h := window.GetFramebufferSize() + s1 := float32(w) / 256 + s2 := float32(h) / 240 + f := float32(1 - padding) + var x, y float32 + if s1 >= s2 { + x = f * s2 / s1 + y = f + } else { + x = f + y = f * s1 / s2 + } + gl.Begin(gl.QUADS) + gl.TexCoord2f(0, 1) + gl.Vertex2f(-x, -y) + gl.TexCoord2f(1, 1) + gl.Vertex2f(x, -y) + gl.TexCoord2f(1, 0) + gl.Vertex2f(x, y) + gl.TexCoord2f(0, 0) + gl.Vertex2f(-x, y) + gl.End() +} + +func updateControllers(window *glfw.Window, console *nes.Console) { + turbo := console.PPU.Frame%6 < 3 + k1 := readKeys(window, turbo) + j1 := readJoystick(glfw.Joystick1, turbo) + j2 := readJoystick(glfw.Joystick2, turbo) + console.SetButtons1(combineButtons(k1, j1)) + console.SetButtons2(j2) +} diff --git a/ui/menuview.go b/ui/menuview.go new file mode 100644 index 00000000..1685d1b0 --- /dev/null +++ b/ui/menuview.go @@ -0,0 +1,250 @@ +package ui + +import ( + "path" + "strings" + + "github.com/giongto35/game-online/nes" + "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/glfw/v3.2/glfw" +) + +const ( + border = 10 + margin = 10 + initialDelay = 0.3 + repeatDelay = 0.1 + typeDelay = 0.5 +) + +type MenuView struct { + director *Director + paths []string + texture *Texture + nx, ny, i, j int + scroll int + t float64 + buttons [8]bool + times [8]float64 + typeBuffer string + typeTime float64 +} + +func NewMenuView(director *Director, paths []string) View { + view := MenuView{} + view.director = director + view.paths = paths + view.texture = NewTexture() + return &view +} + +func (view *MenuView) checkButtons() { + window := view.director.window + k1 := readKeys(window, false) + j1 := readJoystick(glfw.Joystick1, false) + j2 := readJoystick(glfw.Joystick2, false) + buttons := combineButtons(combineButtons(j1, j2), k1) + now := glfw.GetTime() + for i := range buttons { + if buttons[i] && !view.buttons[i] { + view.times[i] = now + initialDelay + view.onPress(i) + } else if !buttons[i] && view.buttons[i] { + view.onRelease(i) + } else if buttons[i] && now >= view.times[i] { + view.times[i] = now + repeatDelay + view.onPress(i) + } + } + view.buttons = buttons +} + +func (view *MenuView) onPress(index int) { + switch index { + case nes.ButtonUp: + view.j-- + case nes.ButtonDown: + view.j++ + case nes.ButtonLeft: + view.i-- + case nes.ButtonRight: + view.i++ + default: + return + } + view.t = glfw.GetTime() +} + +func (view *MenuView) onRelease(index int) { + switch index { + case nes.ButtonStart: + view.onSelect() + } +} + +func (view *MenuView) onSelect() { + index := view.nx*(view.j+view.scroll) + view.i + if index >= len(view.paths) { + return + } + view.director.PlayGame(view.paths[index]) +} + +func (view *MenuView) onChar(window *glfw.Window, char rune) { + now := glfw.GetTime() + if now > view.typeTime { + view.typeBuffer = "" + } + view.typeTime = now + typeDelay + view.typeBuffer = strings.ToLower(view.typeBuffer + string(char)) + for index, p := range view.paths { + _, p = path.Split(strings.ToLower(p)) + if p >= view.typeBuffer { + view.highlight(index) + return + } + } +} + +func (view *MenuView) highlight(index int) { + view.scroll = index/view.nx - (view.ny-1)/2 + view.clampScroll(false) + view.i = index % view.nx + view.j = (index-view.i)/view.nx - view.scroll +} + +func (view *MenuView) Enter() { + gl.ClearColor(0.333, 0.333, 0.333, 1) + view.director.SetTitle("Select Game") + view.director.window.SetCharCallback(view.onChar) +} + +func (view *MenuView) Exit() { + view.director.window.SetCharCallback(nil) +} + +func (view *MenuView) Update(t, dt float64) { + view.checkButtons() + view.texture.Purge() + window := view.director.window + w, h := window.GetFramebufferSize() + sx := 256 + margin*2 + sy := 240 + margin*2 + nx := (w - border*2) / sx + ny := (h - border*2) / sy + ox := (w-nx*sx)/2 + margin + oy := (h-ny*sy)/2 + margin + if nx < 1 { + nx = 1 + } + if ny < 1 { + ny = 1 + } + view.nx = nx + view.ny = ny + view.clampSelection() + gl.PushMatrix() + gl.Ortho(0, float64(w), float64(h), 0, -1, 1) + view.texture.Bind() + for j := 0; j < ny; j++ { + for i := 0; i < nx; i++ { + x := float32(ox + i*sx) + y := float32(oy + j*sy) + index := nx*(j+view.scroll) + i + if index >= len(view.paths) || index < 0 { + continue + } + path := view.paths[index] + tx, ty, tw, th := view.texture.Lookup(path) + drawThumbnail(x, y, tx, ty, tw, th) + } + } + view.texture.Unbind() + if int((t-view.t)*4)%2 == 0 { + x := float32(ox + view.i*sx) + y := float32(oy + view.j*sy) + drawSelection(x, y, 8, 4) + } + gl.PopMatrix() +} + +func (view *MenuView) clampSelection() { + if view.i < 0 { + view.i = view.nx - 1 + } + if view.i >= view.nx { + view.i = 0 + } + if view.j < 0 { + view.j = 0 + view.scroll-- + } + if view.j >= view.ny { + view.j = view.ny - 1 + view.scroll++ + } + view.clampScroll(true) +} + +func (view *MenuView) clampScroll(wrap bool) { + n := len(view.paths) + rows := n / view.nx + if n%view.nx > 0 { + rows++ + } + maxScroll := rows - view.ny + if view.scroll < 0 { + if wrap { + view.scroll = maxScroll + view.j = view.ny - 1 + } else { + view.scroll = 0 + view.j = 0 + } + } + if view.scroll > maxScroll { + if wrap { + view.scroll = 0 + view.j = 0 + } else { + view.scroll = maxScroll + view.j = view.ny - 1 + } + } +} + +func drawThumbnail(x, y, tx, ty, tw, th float32) { + sx := x + 4 + sy := y + 4 + gl.Disable(gl.TEXTURE_2D) + gl.Color3f(0.2, 0.2, 0.2) + gl.Begin(gl.QUADS) + gl.Vertex2f(sx, sy) + gl.Vertex2f(sx+256, sy) + gl.Vertex2f(sx+256, sy+240) + gl.Vertex2f(sx, sy+240) + gl.End() + gl.Enable(gl.TEXTURE_2D) + gl.Color3f(1, 1, 1) + gl.Begin(gl.QUADS) + gl.TexCoord2f(tx, ty) + gl.Vertex2f(x, y) + gl.TexCoord2f(tx+tw, ty) + gl.Vertex2f(x+256, y) + gl.TexCoord2f(tx+tw, ty+th) + gl.Vertex2f(x+256, y+240) + gl.TexCoord2f(tx, ty+th) + gl.Vertex2f(x, y+240) + gl.End() +} + +func drawSelection(x, y, p, w float32) { + gl.LineWidth(w) + gl.Begin(gl.LINE_STRIP) + gl.Vertex2f(x-p, y-p) + gl.Vertex2f(x+256+p, y-p) + gl.Vertex2f(x+256+p, y+240+p) + gl.Vertex2f(x-p, y+240+p) + gl.Vertex2f(x-p, y-p) + gl.End() +} diff --git a/ui/run.go b/ui/run.go index 3e49b5c0..d20fe88e 100644 --- a/ui/run.go +++ b/ui/run.go @@ -1,7 +1,13 @@ package ui import ( + "image" + "log" "runtime" + + "github.com/go-gl/gl/v2.1/gl" + "github.com/go-gl/glfw/v3.2/glfw" + "github.com/gordonklaus/portaudio" ) const ( @@ -19,16 +25,39 @@ func init() { runtime.LockOSThread() } -func Run(path string) { +func Run(paths []string, imageChannel chan *image.RGBA) { // initialize audio - //portaudio.Initialize() - //defer portaudio.Terminate() + portaudio.Initialize() + defer portaudio.Terminate() - //audio := NewAudio() - //if err := audio.Start(); err != nil { - //log.Fatalln(err) - //} - //defer audio.Stop() + audio := NewAudio() + if err := audio.Start(); err != nil { + log.Fatalln(err) + } + defer audio.Stop() + + // initialize glfw + if err := glfw.Init(); err != nil { + log.Fatalln(err) + } + defer glfw.Terminate() + + // create window + glfw.WindowHint(glfw.ContextVersionMajor, 2) + glfw.WindowHint(glfw.ContextVersionMinor, 1) + window, err := glfw.CreateWindow(width*scale, height*scale, title, nil, nil) + if err != nil { + log.Fatalln(err) + } + window.MakeContextCurrent() + + // initialize gl + if err := gl.Init(); err != nil { + log.Fatalln(err) + } + gl.Enable(gl.TEXTURE_2D) // run director + director := NewDirector(window, audio, imageChannel) + director.Start(paths) } diff --git a/ui/texture.go b/ui/texture.go new file mode 100644 index 00000000..48704d4f --- /dev/null +++ b/ui/texture.go @@ -0,0 +1,161 @@ +package ui + +import ( + "image" + "io" + "net/http" + "os" + "path" + "strings" + + "github.com/go-gl/gl/v2.1/gl" +) + +const textureSize = 4096 +const textureDim = textureSize / 256 +const textureCount = textureDim * textureDim + +type Texture struct { + texture uint32 + lookup map[string]int + reverse [textureCount]string + access [textureCount]int + counter int + ch chan string +} + +func NewTexture() *Texture { + texture := createTexture() + gl.BindTexture(gl.TEXTURE_2D, texture) + gl.TexImage2D( + gl.TEXTURE_2D, 0, gl.RGBA, + textureSize, textureSize, + 0, gl.RGBA, gl.UNSIGNED_BYTE, nil) + gl.BindTexture(gl.TEXTURE_2D, 0) + t := Texture{} + t.texture = texture + t.lookup = make(map[string]int) + t.ch = make(chan string, 1024) + return &t +} + +func (t *Texture) Purge() { + for { + select { + case path := <-t.ch: + delete(t.lookup, path) + default: + return + } + } +} + +func (t *Texture) Bind() { + gl.BindTexture(gl.TEXTURE_2D, t.texture) +} + +func (t *Texture) Unbind() { + gl.BindTexture(gl.TEXTURE_2D, 0) +} + +func (t *Texture) Lookup(path string) (x, y, dx, dy float32) { + if index, ok := t.lookup[path]; ok { + return t.coord(index) + } else { + return t.coord(t.load(path)) + } +} + +func (t *Texture) mark(index int) { + t.counter++ + t.access[index] = t.counter +} + +func (t *Texture) lru() int { + minIndex := 0 + minValue := t.counter + 1 + for i, n := range t.access { + if n < minValue { + minIndex = i + minValue = n + } + } + return minIndex +} + +func (t *Texture) coord(index int) (x, y, dx, dy float32) { + x = float32(index%textureDim) / textureDim + y = float32(index/textureDim) / textureDim + dx = 1.0 / textureDim + dy = dx * 240 / 256 + return +} + +func (t *Texture) load(path string) int { + index := t.lru() + delete(t.lookup, t.reverse[index]) + t.mark(index) + t.lookup[path] = index + t.reverse[index] = path + x := int32((index % textureDim) * 256) + y := int32((index / textureDim) * 256) + im := copyImage(t.loadThumbnail(path)) + size := im.Rect.Size() + gl.TexSubImage2D( + gl.TEXTURE_2D, 0, x, y, int32(size.X), int32(size.Y), + gl.RGBA, gl.UNSIGNED_BYTE, gl.Ptr(im.Pix)) + return index +} + +func (t *Texture) loadThumbnail(romPath string) image.Image { + _, name := path.Split(romPath) + name = strings.TrimSuffix(name, ".nes") + name = strings.Replace(name, "_", " ", -1) + name = strings.Title(name) + im := CreateGenericThumbnail(name) + hash, err := hashFile(romPath) + if err != nil { + return im + } + filename := thumbnailPath(hash) + if _, err := os.Stat(filename); os.IsNotExist(err) { + go t.downloadThumbnail(romPath, hash) + return im + } else { + thumbnail, err := loadPNG(filename) + if err != nil { + return im + } + return thumbnail + } +} + +func (t *Texture) downloadThumbnail(romPath, hash string) error { + url := thumbnailURL(hash) + filename := thumbnailPath(hash) + dir, _ := path.Split(filename) + + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + if _, err := io.Copy(file, resp.Body); err != nil { + return err + } + + t.ch <- romPath + + return nil +} diff --git a/ui/util.go b/ui/util.go index 48581a24..91ef925d 100644 --- a/ui/util.go +++ b/ui/util.go @@ -15,7 +15,7 @@ import ( "os/user" "path" - "github.com/fogleman/nes/nes" + "github.com/giongto35/game-online/nes" "github.com/go-gl/gl/v2.1/gl" "github.com/go-gl/glfw/v3.2/glfw" ) @@ -189,6 +189,26 @@ func saveGIF(path string, frames []image.Image) error { return gif.EncodeAll(file, &g) } +func screenshot(im image.Image) { + for i := 0; i < 1000; i++ { + path := fmt.Sprintf("%03d.png", i) + if _, err := os.Stat(path); os.IsNotExist(err) { + savePNG(path, im) + return + } + } +} + +func animation(frames []image.Image) { + for i := 0; i < 1000; i++ { + path := fmt.Sprintf("%03d.gif", i) + if _, err := os.Stat(path); os.IsNotExist(err) { + saveGIF(path, frames) + return + } + } +} + func writeSRAM(filename string, sram []byte) error { dir, _ := path.Split(filename) if err := os.MkdirAll(dir, 0755); err != nil { diff --git a/util/roms.go b/util/roms.go index 0f2a416a..cade65d1 100644 --- a/util/roms.go +++ b/util/roms.go @@ -8,7 +8,7 @@ import ( "path" "strings" - "github.com/fogleman/nes/nes" + "github.com/giongto35/game-online/nes" ) func testRom(path string) (err error) {