From d426892e6a17525bec64dc89aaf4e2cba8ca992a Mon Sep 17 00:00:00 2001 From: Nathan Anderson Date: Thu, 8 Aug 2024 18:51:51 -0600 Subject: [PATCH] Added rendering stuff, updated go mod --- cmd/renderer/main.go | 11 ++ flake.lock | 55 +++++++++ flake.nix | 9 ++ go.mod | 2 + go.sum | 4 + gomod2nix.toml | 15 +++ internal/ansii/ansii.go | 135 +++++++++++++++++++++ internal/renderer/renderer.go | 194 ++++++++++++++++++++++++++++++- internal/renderer/user_inputs.go | 25 ++++ 9 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 gomod2nix.toml create mode 100644 internal/ansii/ansii.go create mode 100644 internal/renderer/user_inputs.go diff --git a/cmd/renderer/main.go b/cmd/renderer/main.go index e69de29..15370ce 100644 --- a/cmd/renderer/main.go +++ b/cmd/renderer/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "fmt" + "sshpong/internal/renderer" +) + +func main() { + fmt.Println("Starting renderer") + renderer.Render() +} diff --git a/flake.lock b/flake.lock index cf99a35..4f05e6c 100644 --- a/flake.lock +++ b/flake.lock @@ -18,6 +18,45 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gomod2nix": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1722589758, + "narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=", + "owner": "tweag", + "repo": "gomod2nix", + "rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1", + "type": "github" + }, + "original": { + "owner": "tweag", + "repo": "gomod2nix", + "type": "github" + } + }, "nixpkgs": { "locked": { "lastModified": 1722421184, @@ -37,6 +76,7 @@ "root": { "inputs": { "flake-utils": "flake-utils", + "gomod2nix": "gomod2nix", "nixpkgs": "nixpkgs" } }, @@ -54,6 +94,21 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index b753109..6de704e 100644 --- a/flake.nix +++ b/flake.nix @@ -2,15 +2,22 @@ description = "An example project using flutter"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; inputs.flake-utils.url = "github:numtide/flake-utils"; + inputs.gomod2nix = { + url = "github:tweag/gomod2nix"; + inputs.nixpkgs.follows = "nixpkgs"; + inputs.utils.follows = "utils"; + }; outputs = { self, flake-utils, nixpkgs, + gomod2nix, ... } @ inputs: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; + overlays = [ gomod2nix.overlays.default ]; }; in { devShell = pkgs.mkShell { @@ -19,6 +26,8 @@ gopls gotools go-tools + gomod2nix.packages.${system}.default + sqlite-interactive ]; }; }); diff --git a/go.mod b/go.mod index ea8f265..eaa812f 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,6 @@ go 1.22.2 require ( github.com/google/uuid v1.6.0 google.golang.org/protobuf v1.34.2 + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 5d8aaa7..17d040b 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,7 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= diff --git a/gomod2nix.toml b/gomod2nix.toml new file mode 100644 index 0000000..437c32b --- /dev/null +++ b/gomod2nix.toml @@ -0,0 +1,15 @@ +schema = 3 + +[mod] + [mod."github.com/google/uuid"] + version = "v1.6.0" + hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw=" + [mod."golang.org/x/sys"] + version = "v0.23.0" + hash = "sha256-tC6QVLu72bADgINz26FUGdmYqKgsU45bHPg7sa0ZV7w=" + [mod."golang.org/x/term"] + version = "v0.22.0" + hash = "sha256-tRx/y4ZIZzGAlDJ/8JW3AycC9bRXlNuRqO4V48sAEEc=" + [mod."google.golang.org/protobuf"] + version = "v1.34.2" + hash = "sha256-nMTlrDEE2dbpWz50eQMPBQXCyQh4IdjrTIccaU0F3m0=" diff --git a/internal/ansii/ansii.go b/internal/ansii/ansii.go new file mode 100644 index 0000000..7b37fb7 --- /dev/null +++ b/internal/ansii/ansii.go @@ -0,0 +1,135 @@ +package ansii + +import ( + "fmt" + "os" + "strings" + + "golang.org/x/term" +) + +type ANSI string + +const ( + reset ANSI = "\033[0m" + plain ANSI = "" + bold ANSI = "\033[1m" + underline ANSI = "\033[4m" + black ANSI = "\033[30m" + red ANSI = "\033[31m" + green ANSI = "\033[32m" + yellow ANSI = "\033[33m" + blue ANSI = "\033[34m" + purple ANSI = "\033[35m" + cyan ANSI = "\033[36m" + white ANSI = "\033[37m" + blackBg ANSI = "\033[40m" + redBg ANSI = "\033[41m" + greenBg ANSI = "\033[42m" + yellowBg ANSI = "\033[43m" + blueBg ANSI = "\033[44m" + purpleBg ANSI = "\033[45m" + cyanBg ANSI = "\033[46m" + whiteBg ANSI = "\033[47m" + clearScreen ANSI = "\033[2J" + hideCursor ANSI = "\033[?25l" + showCursor ANSI = "\033[?25h" +) + +type Offset struct { + X int + Y int +} + +type style struct { + Reset ANSI + Plain ANSI + Bold ANSI + Underline ANSI +} + +type color struct { + Black ANSI + Red ANSI + Green ANSI + Yellow ANSI + Blue ANSI + Purple ANSI + Cyan ANSI + White ANSI +} + +type screen struct { + ClearScreen ANSI + HideCursor ANSI + ShowCursor ANSI +} + +type ascii struct { + Block string +} + +func GetTermSize() (width int, height int) { + var fd int = int(os.Stdout.Fd()) + width, height, err := term.GetSize(fd) + if err != nil { + fmt.Println(string(Screen.ClearScreen) + "Fatal: error getting terminal size.") + os.Exit(1) + } + return width, height +} + +func MakeTermRaw() (*term.State, error) { + var fd int = int(os.Stdout.Fd()) + return term.MakeRaw(fd) +} + +func RestoreTerm(prev *term.State) error { + var fd int = int(os.Stdout.Fd()) + return term.Restore(fd, prev) +} + +func (s screen) PlaceCursor(offset Offset) ANSI { + return ANSI(fmt.Sprintf("\033[%d;%dH", offset.Y, offset.X)) +} + +var ( + Styles = style{Bold: bold, Underline: underline, Reset: reset, Plain: plain} + Colors = color{Red: red, Green: green, Yellow: yellow, Blue: blue, Purple: purple, Cyan: cyan, White: white} + Screen = screen{ClearScreen: clearScreen, HideCursor: hideCursor, ShowCursor: showCursor} + Blocks = ascii{Block: "█"} +) + +// Draws a box of dimensions `height` and `width` at `offset`. +// The `offset` is the top left cell of the square. +// Blocks that would be placed off screen are clipped. +func DrawBox(builder *strings.Builder, offset Offset, height int, width int, style ANSI) { + builder.WriteString(string(style)) + for hIdx := 0; hIdx < height; hIdx++ { + + if hIdx == 0 || hIdx == height-1 { + for wIdx := 0; wIdx < width; wIdx++ { + DrawPixel(builder, Offset{X: offset.X + wIdx, Y: offset.Y + hIdx}) + } + } else { + DrawPixel(builder, Offset{X: offset.X, Y: offset.Y + hIdx}) + DrawPixel(builder, Offset{X: offset.X + width - 1, Y: offset.Y + hIdx}) + } + } + builder.WriteString(string(Styles.Reset)) + return +} + +func DrawPixel(builder *strings.Builder, offset Offset) { + termWidth, termHeight := GetTermSize() + if offset.X > termWidth || offset.Y > termHeight || offset.X < 0 || offset.Y < 0 { + return + } + builder.WriteString(string(Screen.PlaceCursor(offset) + ANSI(Blocks.Block))) +} + +func DrawPixelStyle(builder *strings.Builder, offset Offset, style ANSI) { + builder.WriteString(string(style)) + DrawPixel(builder, offset) + builder.WriteString(string(Styles.Reset)) +} diff --git a/internal/renderer/renderer.go b/internal/renderer/renderer.go index 531e94a..08c53ad 100644 --- a/internal/renderer/renderer.go +++ b/internal/renderer/renderer.go @@ -1,7 +1,195 @@ package renderer -import "fmt" +import ( + "fmt" + "os" + "sshpong/internal/ansii" + "strings" + "time" + "unicode/utf8" +) -func render() { - fmt.Println("Test") +var ( + targetFps float64 = 60.0 + targetFpMilli float64 = float64(targetFps) / 1000.0 + millisecondTimeFrame float64 = float64(1 / targetFpMilli) + quit chan bool + userInput chan rune + playerX int = 10 + playerY int = 10 +) + +func Render() { + now := time.Now() + err := doFpsTest() + if err != nil { + fmt.Println("Error ", err) + return + } + then := time.Now() + + total := float64(float64(then.UnixMicro()-now.UnixMicro()) / 1000.0) + fmt.Printf("\n\nTook %.3f milliseconds\n", total) +} + +func writeCheckerBoard(height int, width int, builder *strings.Builder) { + for i := 0; i < height; i++ { + for j := 0; j < width; j++ { + if i%2 == 0 { + if j%2 == 0 { + builder.WriteString("█") + } else { + + builder.WriteString(" ") + } + } else { + if j%2 == 0 { + builder.WriteString(" ") + } else { + builder.WriteString("█") + } + } + } + } +} + +func drawScreen(frameNum int, startMs float64) (frameTimeMs float64) { + _ = frameNum + _, height := ansii.GetTermSize() + var builder = strings.Builder{} + builder.WriteString(string(ansii.Screen.ClearScreen)) + // writeCheckerBoard(height, width, &builder) + // var xOffset = frameNum % width + // ansii.DrawBox(&builder, ansii.Offset{X: 0, Y: 0}, 5, 8, ansii.Colors.Purple) + ansii.DrawBox(&builder, ansii.Offset{X: playerX, Y: playerY}, 5, 1, ansii.Colors.Cyan) + ansii.DrawPixelStyle(&builder, ansii.Offset{X: playerX, Y: playerY}, ansii.Colors.Purple) + ansii.DrawPixelStyle(&builder, ansii.Offset{X: playerX, Y: playerY + 5}, ansii.Colors.Purple) + // Quit instructions + builder.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height}))) + builder.WriteString("q to quit") + + os.Stdout.WriteString(builder.String()) + var frameTimeMilli = (float64(time.Now().UnixNano()) / 1_000_000.0) - startMs + // builder.WriteString(string(ansii.Screen.Coordinate(0, 0))) + return frameTimeMilli +} + +func drawFrameStats(frameNum int, frameTimeMs float64) { + width, height := ansii.GetTermSize() + var spareTimeMilli = millisecondTimeFrame - frameTimeMs + os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: width - 12, Y: height - 2}))) + os.Stdout.WriteString(fmt.Sprintf("Frame #: %d", frameNum)) + os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: width - 19, Y: height - 1}))) + os.Stdout.WriteString(fmt.Sprintf("Frame Time: %.4fms", frameTimeMs)) + os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: width - 20, Y: height}))) + os.Stdout.WriteString(fmt.Sprintf("Spare Time: %.4fms", spareTimeMilli)) +} + +func handleInput(rawInput rune) { + action := ProcessInput(rawInput) + width, height := ansii.GetTermSize() + + switch action { + case Quit: + fmt.Println("Quitting...") + close(quit) + case Left, LeftArrow: + playerX = max(playerX-1, 0) + case Right, RightArrow: + playerX = min(playerX+1, width) + case Up, UpArrow: + playerY = max(playerY-1, 0) + case Down, DownArrow: + playerY = min(playerY+1, height) + case Unknown: + default: + os.Stdout.WriteString(string(ansii.Screen.PlaceCursor(ansii.Offset{X: 0, Y: height - 2}))) + os.Stdout.WriteString("Unrecognized Input: " + string(action)) + close(quit) + } +} + +func waitForFpsLock(startMs float64) { + for { + var nowMs = float64(time.Now().UnixNano()) / 1_000_000.0 + if nowMs-startMs >= millisecondTimeFrame { + break + } + } +} + +func doFpsTest() error { + prev, err := ansii.MakeTermRaw() + if err != nil { + return err + } + defer ansii.RestoreTerm(prev) + quit = make(chan bool, 1) + userInput = make(chan rune, 1) + + // User input loop + go func() { + for { + buf := make([]byte, 3) + n, err := os.Stdin.Read(buf) + if err != nil { + fmt.Fprintf(os.Stderr, "Error reading from stdin: %v\n", err) + continue + } + + if n > 0 { + if buf[0] == 0x1b { // ESC + if n > 1 && buf[1] == '[' { // ESC [ + switch buf[2] { + case 'A': + userInput <- '↑' // Up arrow + case 'B': + userInput <- '↓' // Down arrow + case 'C': + userInput <- '→' // Right arrow + case 'D': + userInput <- '←' // Left arrow + default: + userInput <- '?' + } + } else { + userInput <- '?' + } + } else { + r, _ := utf8.DecodeRune(buf) + userInput <- r + } + } + } + }() + // Rendering loop + go func() { + for i := 0; i <= 10_000; i++ { + startMs := float64(time.Now().UnixNano()) / 1_000_000.0 + select { + case <-quit: + return + default: + frameTimeMs := drawScreen(i, startMs) + drawFrameStats(i, frameTimeMs) + waitForFpsLock(startMs) + } + } + close(quit) + }() + + os.Stdout.WriteString(string(ansii.Screen.HideCursor)) + defer os.Stdout.WriteString(string(ansii.Screen.ShowCursor)) + for { + select { + case <-quit: + fmt.Println("Exiting") + return nil + // case ui := <-userInput: + case input := <-userInput: + handleInput(input) + // fmt.Println(ui) + default: + } + } } diff --git a/internal/renderer/user_inputs.go b/internal/renderer/user_inputs.go new file mode 100644 index 0000000..f547756 --- /dev/null +++ b/internal/renderer/user_inputs.go @@ -0,0 +1,25 @@ +package renderer + +type UiAction rune + +const ( + Unknown UiAction = iota + Quit UiAction = 81 // 'Q' + Left UiAction = 65 + Up UiAction = 87 + Right UiAction = 68 + Down UiAction = 83 + LeftArrow UiAction = 8592 + UpArrow UiAction = 8593 + RightArrow UiAction = 8594 + DownArrow UiAction = 8595 +) + +func ProcessInput(rawInput rune) (action UiAction) { + inputVal := int(rawInput) + // Convert to UpperCase + if inputVal >= 97 && inputVal <= 122 { + inputVal = inputVal - 32 + } + return UiAction(inputVal) +}