Browse Source

Add holds

main
meutraa 6 months ago
parent
commit
5be4e253e3
Signed by: meutraa GPG Key ID: 82664A5F8DAC3400
  1. 3
      .gitignore
  2. 0
      LICENSE
  3. 2
      README.md
  4. 184
      benchmarks.md
  5. 14
      go.mod
  6. 91
      go.sum
  7. 73
      internal/config/main.go
  8. 0
      internal/game/bpm.go
  9. 1
      internal/game/chart.go
  10. 0
      internal/game/difficulty.go
  11. 0
      internal/game/input.go
  12. 11
      internal/game/judgement.go
  13. 7
      internal/game/note.go
  14. 56
      internal/input/default.go
  15. 23
      internal/parser/default.go
  16. 7
      internal/parser/parser.go
  17. 123
      internal/render/default.go
  18. 21
      internal/render/renderer.go
  19. 2
      internal/score/compact_test.go
  20. 4
      internal/score/default.go
  21. 0
      internal/score/distance_test.go
  22. 6
      internal/score/hit_test.go
  23. 2
      internal/score/scorer.go
  24. 2
      internal/testdata/main.go
  25. 20
      internal/theme/default.go
  26. 7
      internal/theme/theme.go
  27. 383
      main.go
  28. 442
      program.go

3
.gitignore

@ -1,4 +1,5 @@
songs
eott
eotw
eotw.exe
cpu.prof
scores.db

0
LICENSE

2
README.md

@ -1,3 +1,3 @@
# eott
# eotw
Rhythm game on the terminal

184
benchmarks.md

@ -1,184 +0,0 @@
./eott --profile=cpu.prof -r 1.4 songs/Something\ Wild/
Time: Apr 30, 2021 at 12:17pm (BST)
Duration: 1.64mins, Total samples = 10.48s (10.62%)
flat flat% sum% cum cum%
590ms 5.63% 5.63% 600ms 5.73% math.Round (partial-inline)
480ms 4.58% 10.21% 4310ms 41.13% main.run.func5
440ms 4.20% 14.41% 440ms 4.20% runtime.futex
390ms 3.72% 18.13% 390ms 3.72% <unknown>
350ms 3.34% 21.47% 390ms 3.72% syscall.Syscall
320ms 3.05% 24.52% 1780ms 16.98% runtime.findrunnable
290ms 2.77% 27.29% 1030ms 9.83% fmt.(*pp).doPrintf
280ms 2.67% 29.96% 280ms 2.67% runtime.memclrNoHeapPointers
280ms 2.67% 32.63% 280ms 2.67% runtime.nextFreeFast (inline)
250ms 2.39% 35.02% 250ms 2.39% github.com/jfreymuth/vorbis.imdct
250ms 2.39% 37.40% 1210ms 11.55% runtime.mallocgc
210ms 2.00% 39.41% 210ms 2.00% runtime.memmove
190ms 1.81% 41.22% 350ms 3.34% runtime.scanobject
160ms 1.53% 42.75% 1000ms 9.54% github.com/jfreymuth/oggvorbis.(*Reader).Read
140ms 1.34% 44.08% 190ms 1.81% git.lost.host/meutraa/eott/internal/score.(*DefaultScorer).Distance
140ms 1.34% 45.42% 450ms 4.29% runtime.checkTimers
140ms 1.34% 46.76% 180ms 1.72% sync.(*Pool).Get
130ms 1.24% 48.00% 130ms 1.24% [libpthread-2.32.so]
130ms 1.24% 49.24% 660ms 6.30% fmt.(*pp).printArg
130ms 1.24% 50.48% 320ms 3.05% github.com/jfreymuth/vorbis.(*residue).Decode
120ms 1.15% 51.62% 310ms 2.96% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
120ms 1.15% 52.77% 180ms 1.72% github.com/jfreymuth/vorbis.huffmanCode.Lookup (inline)
120ms 1.15% 53.91% 120ms 1.15% runtime.(*randomEnum).next (inline)
120ms 1.15% 55.06% 120ms 1.15% runtime.cgocall
120ms 1.15% 56.20% 120ms 1.15% runtime.epollwait
Time: Apr 30, 2021 at 2:39pm (BST)
Duration: 1.62mins, Total samples = 9.43s ( 9.68%)
flat flat% sum% cum cum%
0.61s 6.47% 6.47% 0.61s 6.47% runtime.futex
0.61s 6.47% 12.94% 0.65s 6.89% syscall.Syscall
0.37s 3.92% 16.86% 0.37s 3.92% git.lost.host/meutraa/eott/internal/score.(*DefaultScorer).Distance
0.34s 3.61% 20.47% 3.31s 35.10% main.run.func5
0.29s 3.08% 23.54% 0.49s 5.20% runtime.scanobject
0.26s 2.76% 26.30% 0.26s 2.76% <unknown>
0.26s 2.76% 29.06% 0.26s 2.76% runtime.memmove
0.24s 2.55% 31.60% 0.98s 10.39% runtime.mallocgc
0.22s 2.33% 33.93% 0.22s 2.33% github.com/jfreymuth/vorbis.imdct
0.20s 2.12% 36.06% 0.20s 2.12% [libpthread-2.32.so]
0.20s 2.12% 38.18% 1.65s 17.50% runtime.findrunnable
0.20s 2.12% 40.30% 0.20s 2.12% runtime.nextFreeFast (inline)
0.18s 1.91% 42.21% 0.18s 1.91% runtime.memclrNoHeapPointers
0.15s 1.59% 43.80% 0.98s 10.39% fmt.(*pp).doPrintf
0.14s 1.48% 45.28% 0.73s 7.74% github.com/jfreymuth/vorbis.(*Decoder).decodePacket
0.14s 1.48% 46.77% 0.16s 1.70% runtime.cgocall
0.13s 1.38% 48.14% 0.26s 2.76% fmt.(*fmt).fmtInteger
0.13s 1.38% 49.52% 0.87s 9.23% github.com/jfreymuth/oggvorbis.(*Reader).Read
0.12s 1.27% 50.80% 0.12s 1.27% runtime.(*randomEnum).next (inline)
0.12s 1.27% 52.07% 0.12s 1.27% runtime.pMask.read (inline)
0.11s 1.17% 53.23% 0.15s 1.59% runtime.lock2
0.11s 1.17% 54.40% 0.14s 1.48% runtime.nanotime (inline)
0.10s 1.06% 55.46% 0.10s 1.06% runtime.read
0.10s 1.06% 56.52% 0.13s 1.38% sync.(*Pool).Get
0.09s 0.95% 57.48% 0.69s 7.32% fmt.(*pp).printArg
Showing nodes accounting for 4520ms, 59.01% of 7660ms total
Dropped 150 nodes (cum <= 38.30ms)
Showing top 25 nodes out of 174
flat flat% sum% cum cum%
490ms 6.40% 6.40% 490ms 6.40% runtime.futex
490ms 6.40% 12.79% 540ms 7.05% syscall.Syscall
360ms 4.70% 17.49% 1780ms 23.24% runtime.findrunnable
240ms 3.13% 20.63% 260ms 3.39% github.com/jfreymuth/vorbis.imdct
230ms 3.00% 23.63% 540ms 7.05% github.com/jfreymuth/vorbis.(*residue).Decode
210ms 2.74% 26.37% 310ms 4.05% github.com/jfreymuth/vorbis.huffmanCode.Lookup (inline)
190ms 2.48% 28.85% 570ms 7.44% fmt.(*pp).printArg
180ms 2.35% 31.20% 180ms 2.35% <unknown>
180ms 2.35% 33.55% 180ms 2.35% runtime.memclrNoHeapPointers
160ms 2.09% 35.64% 160ms 2.09% runtime.madvise
150ms 1.96% 37.60% 150ms 1.96% runtime.cgocall
140ms 1.83% 39.43% 1400ms 18.28% github.com/jfreymuth/oggvorbis.(*Reader).Read
140ms 1.83% 41.25% 1230ms 16.06% github.com/jfreymuth/vorbis.(*Decoder).decodePacket
140ms 1.83% 43.08% 140ms 1.83% runtime.epollwait
140ms 1.83% 44.91% 140ms 1.83% runtime.nextFreeFast (inline)
130ms 1.70% 46.61% 130ms 1.70% runtime.(*randomEnum).next (inline)
130ms 1.70% 48.30% 130ms 1.70% runtime.memmove
120ms 1.57% 49.87% 280ms 3.66% runtime.checkTimers
120ms 1.57% 51.44% 190ms 2.48% runtime.scanobject
100ms 1.31% 52.74% 690ms 9.01% fmt.(*pp).doPrintf
100ms 1.31% 54.05% 290ms 3.79% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
100ms 1.31% 55.35% 100ms 1.31% github.com/jfreymuth/vorbis.(*bitReader).Read1 (inline)
100ms 1.31% 56.66% 650ms 8.49% runtime.mallocgc
100ms 1.31% 57.96% 100ms 1.31% runtime.read
80ms 1.04% 59.01% 130ms 1.70% fmt.(*fmt).fmtInteger
Showing nodes accounting for 2680ms, 68.37% of 3920ms total
Dropped 70 nodes (cum <= 19.60ms)
Showing top 25 nodes out of 172
flat flat% sum% cum cum%
400ms 10.20% 10.20% 400ms 10.20% runtime.futex
260ms 6.63% 16.84% 430ms 10.97% github.com/jfreymuth/vorbis.(*residue).Decode
250ms 6.38% 23.21% 970ms 24.74% runtime.findrunnable
140ms 3.57% 26.79% 140ms 3.57% <unknown>
130ms 3.32% 30.10% 140ms 3.57% github.com/jfreymuth/vorbis.imdct
130ms 3.32% 33.42% 140ms 3.57% syscall.Syscall
120ms 3.06% 36.48% 120ms 3.06% runtime.nanotime (inline)
120ms 3.06% 39.54% 120ms 3.06% runtime.unlock2
100ms 2.55% 42.09% 300ms 7.65% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
100ms 2.55% 44.64% 1010ms 25.77% github.com/jfreymuth/oggvorbis.(*Reader).Read
90ms 2.30% 46.94% 90ms 2.30% [libpthread-2.32.so]
90ms 2.30% 49.23% 170ms 4.34% github.com/jfreymuth/vorbis.huffmanCode.Lookup (inline)
90ms 2.30% 51.53% 120ms 3.06% runtime.cgocall
80ms 2.04% 53.57% 80ms 2.04% github.com/jfreymuth/vorbis.(*bitReader).Read1 (inline)
70ms 1.79% 55.36% 880ms 22.45% github.com/jfreymuth/vorbis.(*Decoder).decodePacket
70ms 1.79% 57.14% 260ms 6.63% runtime.checkTimers
60ms 1.53% 58.67% 60ms 1.53% github.com/jfreymuth/vorbis.(*Decoder).inverseCoupling
50ms 1.28% 59.95% 1090ms 27.81% github.com/faiface/beep.(*Mixer).Stream
50ms 1.28% 61.22% 50ms 1.28% runtime.(*randomEnum).next (inline)
50ms 1.28% 62.50% 50ms 1.28% runtime.lock2
50ms 1.28% 63.78% 50ms 1.28% runtime.madvise
50ms 1.28% 65.05% 50ms 1.28% runtime.memclrNoHeapPointers
50ms 1.28% 66.33% 50ms 1.28% runtime.read
40ms 1.02% 67.35% 1250ms 31.89% github.com/faiface/beep/speaker.update
40ms 1.02% 68.37% 40ms 1.02% runtime.epollwait
Showing nodes accounting for 2660ms, 67.34% of 3950ms total
Dropped 76 nodes (cum <= 19.75ms)
Showing top 25 nodes out of 172
flat flat% sum% cum cum%
470ms 11.90% 11.90% 470ms 11.90% runtime.futex
260ms 6.58% 18.48% 260ms 6.58% [libspa-audioconvert.so]
210ms 5.32% 23.80% 210ms 5.32% [libpthread-2.32.so]
180ms 4.56% 28.35% 180ms 4.56% github.com/jfreymuth/vorbis.imdct
150ms 3.80% 32.15% 920ms 23.29% runtime.findrunnable
120ms 3.04% 35.19% 870ms 22.03% github.com/jfreymuth/oggvorbis.(*Reader).Read
110ms 2.78% 37.97% 420ms 10.63% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
100ms 2.53% 40.51% 670ms 16.96% github.com/jfreymuth/vorbis.(*Decoder).decodePacket
90ms 2.28% 42.78% 110ms 2.78% syscall.Syscall
80ms 2.03% 44.81% 140ms 3.54% github.com/jfreymuth/vorbis.huffmanCode.Lookup (inline)
80ms 2.03% 46.84% 80ms 2.03% runtime.(*randomEnum).next (inline)
80ms 2.03% 48.86% 100ms 2.53% runtime.lock2
80ms 2.03% 50.89% 80ms 2.03% runtime.nanotime (inline)
70ms 1.77% 52.66% 210ms 5.32% github.com/jfreymuth/vorbis.(*residue).Decode
70ms 1.77% 54.43% 120ms 3.04% runtime.cgocall
70ms 1.77% 56.20% 100ms 2.53% runtime.scanobject
60ms 1.52% 57.72% 60ms 1.52% github.com/jfreymuth/vorbis.(*Decoder).inverseCoupling
60ms 1.52% 59.24% 60ms 1.52% github.com/jfreymuth/vorbis.(*bitReader).Read1 (inline)
50ms 1.27% 60.51% 50ms 1.27% [libc-2.32.so]
50ms 1.27% 61.77% 50ms 1.27% runtime.epollwait
50ms 1.27% 63.04% 50ms 1.27% runtime.madvise
50ms 1.27% 64.30% 50ms 1.27% runtime.runqgrab
40ms 1.01% 65.32% 40ms 1.01% github.com/jfreymuth/oggvorbis.crcUpdate
40ms 1.01% 66.33% 40ms 1.01% github.com/jfreymuth/vorbis.renderLine
40ms 1.01% 67.34% 40ms 1.01% runtime.memclrNoHeapPointers
Showing nodes accounting for 2020ms, 68.71% of 2940ms total
Dropped 53 nodes (cum <= 14.70ms)
Showing top 25 nodes out of 126
flat flat% sum% cum cum%
450ms 15.31% 15.31% 450ms 15.31% runtime.futex
230ms 7.82% 23.13% 230ms 7.82% [libspa-audioconvert.so]
190ms 6.46% 29.59% 890ms 30.27% runtime.findrunnable
110ms 3.74% 33.33% 130ms 4.42% runtime.cgocall
100ms 3.40% 36.73% 320ms 10.88% github.com/hajimehoshi/oto/internal/mux.(*Mux).Read
90ms 3.06% 39.80% 300ms 10.20% github.com/faiface/beep.signedToFloat
90ms 3.06% 42.86% 120ms 4.08% math.ldexp
80ms 2.72% 45.58% 90ms 3.06% runtime.nanotime (inline)
70ms 2.38% 47.96% 70ms 2.38% [libpthread-2.32.so]
70ms 2.38% 50.34% 190ms 6.46% math.expmulti
60ms 2.04% 52.38% 60ms 2.04% runtime.pMask.read (inline)
60ms 2.04% 54.42% 70ms 2.38% syscall.Syscall
50ms 1.70% 56.12% 370ms 12.59% github.com/faiface/beep.Format.decode
40ms 1.36% 57.48% 40ms 1.36% runtime.(*randomEnum).done (inline)
30ms 1.02% 58.50% 30ms 1.02% [libc-2.32.so]
30ms 1.02% 59.52% 550ms 18.71% github.com/faiface/beep/speaker.update
30ms 1.02% 60.54% 30ms 1.02% math.IsInf (inline)
30ms 1.02% 61.56% 30ms 1.02% runtime.(*guintptr).cas (inline)
30ms 1.02% 62.59% 200ms 6.80% runtime.checkTimers
30ms 1.02% 63.61% 30ms 1.02% runtime.dodeltimer0
30ms 1.02% 64.63% 30ms 1.02% runtime.epollwait
30ms 1.02% 65.65% 30ms 1.02% runtime.execute
30ms 1.02% 66.67% 40ms 1.36% runtime.lock2
30ms 1.02% 67.69% 70ms 2.38% runtime.netpoll
30ms 1.02% 68.71% 1190ms 40.48% runtime.schedule

14
go.mod

@ -1,20 +1,12 @@
module git.lost.host/meutraa/eott
module git.lost.host/meutraa/eotw
go 1.16
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/faiface/beep v1.0.2
github.com/hajimehoshi/go-mp3 v0.3.2 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
github.com/gen2brain/raylib-go v0.0.0-20210526111428-ace572fead21
github.com/mattn/go-sqlite3 v1.14.7
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/exp v0.0.0-20210417010653-0739314eea07 // indirect
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb // indirect
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 // indirect
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c // indirect
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72
github.com/stretchr/testify v1.7.0 // indirect
gopkg.in/alecthomas/kingpin.v2 v2.2.6
)

91
go.sum

@ -1,101 +1,22 @@
dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/faiface/beep v1.0.2 h1:UB5DiRNmA4erfUYnHbgU4UB6DlBOrsdEFRtcc8sCkdQ=
github.com/faiface/beep v1.0.2/go.mod h1:1yLb5yRdHMsovYYWVqYLioXkVuziCSITW1oarTeduQM=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/gopherjs/gopherjs v0.0.0-20180628210949-0892b62f0d9f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20180825215210-0210a2f0f73c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherwasm v0.1.1/go.mod h1:kx4n9a+MzHH0BJJhvlsQ65hqLFXDO/m256AsaDPQ+/4=
github.com/gopherjs/gopherwasm v1.0.0/go.mod h1:SkZ8z7CWBz5VXbhJel8TxCmAcsQqzgWGR/8nMhyhZSI=
github.com/hajimehoshi/go-mp3 v0.1.1/go.mod h1:4i+c5pDNKDrxl1iu9iG90/+fhP37lio6gNhjCx9WBJw=
github.com/hajimehoshi/go-mp3 v0.3.2 h1:xSYNE2F3lxtOu9BRjCWHHceg7S91IHfXfXp5+LYQI7s=
github.com/hajimehoshi/go-mp3 v0.3.2/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.1.1/go.mod h1:hUiLWeBQnbDu4pZsAhOnGqMI1ZGibS6e2qhQdfpwz04=
github.com/hajimehoshi/oto v0.3.1/go.mod h1:e9eTLBB9iZto045HLbzfHJIc+jP3xaKrjZTghvb6fdM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
github.com/jfreymuth/oggvorbis v1.0.0 h1:aOpiihGrFLXpsh2osOlEvTcg5/aluzGQeC7m3uYWOZ0=
github.com/jfreymuth/oggvorbis v1.0.0/go.mod h1:abe6F9QRjuU9l+2jek3gj46lu40N4qlYxh2grqkLEDM=
github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/gen2brain/raylib-go v0.0.0-20210526111428-ace572fead21 h1:w1duHOKelNSXQ3VKCKiTz5zuzTBma/E6RunaTtGZNsk=
github.com/gen2brain/raylib-go v0.0.0-20210526111428-ace572fead21/go.mod h1:LUVRDQbnxUaOgzLzW5lMS+IcbqlXHIqIA9wq8wxzmcA=
github.com/mattn/go-sqlite3 v1.14.7 h1:fxWBnXkxfM6sRiuH3bqJ4CfzZojMOLVc0UTsTglEghA=
github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mewkiz/flac v1.0.5/go.mod h1:EHZNU32dMF6alpurYyKHDLYpW1lYpBZ5WrXi/VuNIGs=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/exp v0.0.0-20180710024300-14dda7b62fcd/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20210417010653-0739314eea07 h1:B++6xOM/K7sWi0y6Ji36eIMkZLdBO9fkK46aYBaSasQ=
golang.org/x/exp v0.0.0-20210417010653-0739314eea07/go.mod h1:UjMNdUHwQ6WIvg3xW80lhIWsmxZLZf67lkFSwkMjyZ8=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb h1:fqpd0EBDzlHRCjiphRR5Zo/RSWWQlWv34418dnEixWk=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20180806140643-507816974b79/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mobile v0.0.0-20201217150744-e6ae53a27f4f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08 h1:h+GZ3ubjuWaQjGe8owMGcmMVCqs0xYJtRG5y2bpHaqU=
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.1.1-0.20191209134235-331c550502dd/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c h1:6L+uOeS3OQt/f4eFHXZcTxeZrGCuz+CLElgEBjbcTA4=
golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72 h1:VqE9gduFZ4dbR7XoL77lHFp0/DyDUBKSXK7CMFkVcV0=
golang.org/x/term v0.0.0-20210406210042-72f3dc4e9b72/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0/go.mod h1:OdE7CF6DbADk7lN8LIKRzRJTTZXIjtWgA5THM5lhBAw=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

73
internal/config/main.go

@ -7,38 +7,34 @@ import (
"strings"
"time"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eotw/internal/game"
rl "github.com/gen2brain/raylib-go/raylib"
"gopkg.in/alecthomas/kingpin.v2"
)
var (
Directory = kingpin.Arg("directory", "Song/chart directory").Required().ExistingDir()
CpuProfile = kingpin.Flag("profile", "Profile CPU").String()
DebugUpdateRate = kingpin.Flag("debug-update-rate", "Every n frames").Default("240").Uint16()
Input = kingpin.Flag("input", "Input device").Default("/dev/input/by-id/usb-OLKB_Planck-event-kbd").Short('i').ExistingFile()
Rate = kingpin.Flag("rate", "Playback % rate").Default("100").Short('r').Uint16()
Offset = kingpin.Flag("offset", "Global offset").Default("0ms").Short('o').Duration()
Delay = kingpin.Flag("delay", "Start delay").Default("1.5s").Short('d').Duration()
ColumnSpacing = kingpin.Flag("spacing", "Columns between keys").Default("12").Short('S').Uint16()
ColumnSpacing = kingpin.Flag("spacing", "Columns between keys").Default("120").Short('S').Int32()
RefreshRate = kingpin.Flag("refresh-rate", "Monitor refresh rate").Default("240.0").Short('R').Float()
FramePeriod = kingpin.Flag("frame-period", "Render frame period").Default("1ms").Short('p').Duration()
NoteRadius = kingpin.Flag("note-radius", "Radius of notes").Default("14").Float32()
scrollSpeedModifier = kingpin.Flag("scroll-speed", "Scroll speed, lower is faster").Default("3").Short('s').Uint()
keys4 = kingpin.Flag("keys-single", "Keys for 4k").Default("12,40,17,50").Short('k').String()
keys4 = kingpin.Flag("keys-single", "Keys for 4k").Default("73,69,83,67").Short('k').String()
keys6 = kingpin.Flag("keys-solo", "Keys for 6k").Default("23,18,24,20,31,46").String()
keys8 = kingpin.Flag("keys-double", "Keys for 8k").Default("23,18,24,49,35,20,31,46").String()
BarOffsetFromBottom = kingpin.Flag("bar-row", "Console row to render hit bar").Default("8").Uint16()
BarOffsetFromBottom = kingpin.Flag("bar-row", "Pixels from bottom to render hit bar").Default("220").Int32()
BarSym = kingpin.Flag("bar-decoration", "Decoration at the hitfield").Default("\033[2m\033[1D[ ]").String()
NoteSym = kingpin.Flag("note-symbol", "Restricted to 1 column").Default("⬤").String()
MineSym = kingpin.Flag("mine-symbol", "Restricted to 1 column").Default("⨯").String()
Keys4 [4]uint16
Keys6 [6]uint16
Keys8 [8]uint16
NsToRow float64
Judgements []game.Judgement
Keys4 [4]int32
Keys6 [6]int32
Keys8 [8]int32
PixelsPerNs float64
Judgements []game.Judgement
)
func Keys(nKeys uint8) []uint16 {
func Keys(nKeys uint8) []int32 {
switch nKeys {
case 4:
return Keys4[:]
@ -50,13 +46,13 @@ func Keys(nKeys uint8) []uint16 {
return Keys4[:]
}
func KeyColumn(r uint16, nKeys uint8) (uint8, error) {
func KeyColumn(r int32, nKeys uint8) (uint8, error) {
for i, c := range Keys(nKeys) {
if r == c {
return uint8(i), nil
}
}
return 0, errors.New("Key not mapped to index")
return 0, errors.New("key not mapped to index")
}
func Init() {
@ -65,21 +61,42 @@ func Init() {
keys := strings.Split(*keys4, ",")
for i, key := range keys {
p, err := strconv.ParseUint(key, 10, 16)
p, err := strconv.ParseInt(key, 10, 32)
if nil != err {
log.Fatalln(err)
}
Keys4[i] = uint16(p)
Keys4[i] = int32(p)
}
NsToRow = 1 / (float64(*scrollSpeedModifier) * 1000 / *RefreshRate * 1000000)
PixelsPerNs = 1 / (float64(*scrollSpeedModifier) * 40 / *RefreshRate * 1000000)
Judgements = []game.Judgement{
{Time: 11 * time.Millisecond, Name: " \033[1;31mE\033[38;5;208mx\033[1;33ma\033[1;32mc\033[38;5;153mt\033[0m"},
{Time: 22 * time.Millisecond, Name: " \033[38;5;141mMarvelous\033[0m"},
{Time: 45 * time.Millisecond, Name: " \033[38;5;117mPerfect\033[0m"},
{Time: 90 * time.Millisecond, Name: " \033[38;5;155mGreat\033[0m"},
{Time: 135 * time.Millisecond, Name: " \033[38;5;214mGood\033[0m"},
{Time: 180 * time.Millisecond, Name: " \033[38;5;208mBoo\033[0m"},
{Time: -1, Name: " \033[38;5;160mMiss\033[0m"},
{Time: 11 * time.Millisecond,
Name: " Exact",
Color: rl.NewColor(63, 0, 255, 255),
},
{Time: 22 * time.Millisecond,
Name: " Marvelous",
Color: rl.NewColor(175, 135, 255, 255),
},
{Time: 45 * time.Millisecond,
Name: " Perfect",
Color: rl.NewColor(135, 215, 255, 255),
},
{Time: 90 * time.Millisecond,
Name: " Great",
Color: rl.NewColor(175, 255, 95, 255),
},
{Time: 135 * time.Millisecond,
Name: " Good",
Color: rl.NewColor(255, 175, 0, 255),
},
{Time: 180 * time.Millisecond,
Name: " Boo",
Color: rl.NewColor(255, 135, 0, 255),
},
{Time: -1,
Name: " Miss",
Color: rl.NewColor(215, 0, 0, 255),
},
}
}

0
internal/game/bpm.go

1
internal/game/chart.go

@ -3,6 +3,7 @@ package game
type Chart struct {
Notes []*Note
NoteCount int64
HoldCount int64
MineCount int64
Difficulty Difficulty

0
internal/game/difficulty.go

0
internal/game/input.go

11
internal/game/judgement.go

@ -1,8 +1,13 @@
package game
import "time"
import (
"time"
rl "github.com/gen2brain/raylib-go/raylib"
)
type Judgement struct {
Time time.Duration
Name string
Time time.Duration
Color rl.Color
Name string
}

7
internal/game/note.go

@ -9,9 +9,10 @@ type Note struct {
Denom int // The beat length, as a denominator, 4 = 1/4 beat
IsMine bool
Time time.Duration // The time the note should be hit
TimeEnd time.Duration // TDurationhe time the note should be unhit
TimeEnd time.Duration // The time the note should be unhit
// This is state
Row uint16 // The current row this note is rendered on, for clearing
HitTime time.Duration // When the note was hit
HitTime time.Duration // When the note was hit
ReleaseTime time.Duration // When the note was released
MissTime time.Duration // When the note was missed
}

56
internal/input/default.go

@ -1,56 +0,0 @@
package input
// #include <linux/input-event-codes.h>
// #include <linux/input.h>
import "C"
import (
"encoding/binary"
"log"
"os"
"syscall"
)
type keyEvent struct {
Time syscall.Timeval
Type uint16
Code uint16
Value int32
}
type Event struct {
Pressed bool
Released bool
//https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h
Code uint16
Time syscall.Timeval
}
func ReadInput(kbd string, events chan *Event) error {
file, err := os.Open(kbd)
if err != nil {
return err
}
go func() {
defer file.Close()
var ev keyEvent
for {
err = binary.Read(file, binary.LittleEndian, &ev)
if nil != err {
log.Println(err, "unable to read keyboard input")
return
}
if ev.Type != C.EV_KEY {
continue
}
events <- &Event{
Pressed: ev.Value == 1,
Released: ev.Value == 0,
Code: ev.Code,
Time: ev.Time,
}
}
}()
return nil
}

23
internal/parser/default.go

@ -8,7 +8,7 @@ import (
"strings"
"time"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eotw/internal/game"
)
type DefaultParser struct{}
@ -40,7 +40,7 @@ func (p *DefaultParser) getSecondsPerNote(rates []game.BPM, currentBeat float64,
func (p *DefaultParser) mapToNote(ch byte) bool {
t := string(ch)
return t == "1" || t == "2" || t == "M"
return t == "1" || t == "2" || t == "4" || t == "M"
}
func (p *DefaultParser) Parse(file string) ([]*game.Chart, error) {
@ -115,6 +115,7 @@ func (p *DefaultParser) Parse(file string) ([]*game.Chart, error) {
notes := []*game.Note{}
mineCount := 0
holdCount := 0
noteCount := 0
blocks := strings.Split(difficulty.Section, "\n,")
@ -146,6 +147,8 @@ func (p *DefaultParser) Parse(file string) ([]*game.Chart, error) {
// log.Printf("(%v) %v/%v = %v%vth\033[0m", bpm, i, lineCount, (denom), denom)
if c == "M" {
mineCount++
} else if c == "2" || c == "4" {
holdCount++
} else {
noteCount++
}
@ -160,6 +163,21 @@ func (p *DefaultParser) Parse(file string) ([]*game.Chart, error) {
for i, c := range chs {
if p.mapToNote(c) {
notes = append(notes, createNote(uint8(i), string(c)))
} else if c == '3' {
// This is a release note of a previous head
// Find the last note of type head in this column and
// add this as the endtime to it
// Loop through notes in reverse
for j := len(notes) - 1; j >= 0; j-- {
note := notes[j]
if int(note.Index) != i {
continue
}
// This will be the matching note
note.TimeEnd = time.Duration(seconds * 1000 * 1000 * 1000)
break
}
}
}
@ -171,6 +189,7 @@ func (p *DefaultParser) Parse(file string) ([]*game.Chart, error) {
charts = append(charts, &game.Chart{
Notes: notes,
NoteCount: int64(noteCount),
HoldCount: int64(holdCount),
MineCount: int64(mineCount),
Difficulty: difficulty,
})

7
internal/parser/parser.go

@ -1,7 +0,0 @@
package parser
import "git.lost.host/meutraa/eott/internal/game"
type Parser interface {
Parse(file string) ([]*game.Chart, error)
}

123
internal/render/default.go

@ -1,123 +0,0 @@
package render
import (
"fmt"
"image/color"
"os"
"strconv"
"strings"
"time"
"git.lost.host/meutraa/eott/internal/config"
"golang.org/x/crypto/ssh/terminal"
)
type DefaultRenderer struct {
buffer strings.Builder
restoreState *terminal.State
decorations []*decoration
}
type decoration struct {
X, Y uint16
Content string
Frames int // remaining frames until removed
}
func (r *DefaultRenderer) Init() error {
state, err := terminal.MakeRaw(int(os.Stdout.Fd()))
if nil != err {
return err
}
r.restoreState = state
fmt.Printf("%s%s%s",
"\033[?1049h", // Enable alternate buffer
"\033[?25l", // Make the cursor invisible
"\033[J", // Clear the screen
)
return nil
}
func (r *DefaultRenderer) Deinit() error {
fmt.Printf("%s%s",
"\033[?1049l", // Disable alternate buffer
"\033[?25h", // Make the cursor visible
)
return terminal.Restore(int(os.Stdout.Fd()), r.restoreState)
}
func (r *DefaultRenderer) AddDecoration(col, row uint16, content string, frames int) {
r.decorations = append(r.decorations, &decoration{
X: col,
Y: row,
Content: content,
Frames: frames,
})
r.Fill(row, col, content)
}
func (r *DefaultRenderer) tickDecorations() {
nd := make([]*decoration, 0, len(r.decorations))
for _, d := range r.decorations {
if d.Frames == 0 {
r.Fill(d.Y, d.X, " ")
continue
}
nd = append(nd, d)
d.Frames--
}
r.decorations = nd
}
func (r *DefaultRenderer) RenderLoop(
delay time.Duration,
render func(startTime time.Time, duration time.Duration) bool,
endRender func(renderDuration time.Duration),
) {
cont := true
startTime := time.Now().Add(delay)
for cont {
now := time.Now()
duration := now.Sub(startTime)
deadline := now.Add(*config.FramePeriod)
cont = render(startTime, duration)
r.tickDecorations()
r.flush()
endRender(time.Now().Sub(now))
remainingTime := deadline.Sub(time.Now())
time.Sleep(remainingTime)
}
}
func (r *DefaultRenderer) Fill(row, column uint16, message string) {
r.buffer.WriteString("\033[")
r.buffer.WriteString(strconv.FormatInt(int64(row), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(column), 10))
r.buffer.WriteString("H")
r.buffer.WriteString(message)
}
func (r *DefaultRenderer) FillColor(row, column uint16, c color.RGBA, message string) {
r.buffer.WriteString("\033[")
r.buffer.WriteString(strconv.FormatInt(int64(row), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(column), 10))
r.buffer.WriteString("H\033[38;2;")
r.buffer.WriteString(strconv.FormatInt(int64(c.R), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(c.G), 10))
r.buffer.WriteString(";")
r.buffer.WriteString(strconv.FormatInt(int64(c.B), 10))
r.buffer.WriteString(message)
r.buffer.WriteString("\033[0m")
}
func (r *DefaultRenderer) flush() {
os.Stdout.Write([]byte(r.buffer.String()))
r.buffer.Reset()
}

21
internal/render/renderer.go

@ -1,21 +0,0 @@
package render
import (
"image/color"
"time"
)
type Renderer interface {
Init() error
Deinit() error
AddDecoration(col, row uint16, content string, frames int)
RenderLoop(
delay time.Duration,
render func(startTime time.Time, duration time.Duration) bool,
// Ran after rendering all the ui, this will render again, so don't do much
// but print debug info
endRender func(renderDuration time.Duration),
)
Fill(row, column uint16, message string)
FillColor(row, column uint16, color color.RGBA, message string)
}

2
internal/score/compact_test.go

@ -4,7 +4,7 @@ import (
"testing"
"time"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eotw/internal/game"
)
var compactTests = map[*([]game.Input)]([]InputsCompact){

4
internal/score/default.go

@ -8,8 +8,8 @@ import (
"log"
"time"
"git.lost.host/meutraa/eott/internal/config"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eotw/internal/config"
"git.lost.host/meutraa/eotw/internal/game"
_ "github.com/mattn/go-sqlite3"
)

0
internal/score/distance_test.go

6
internal/score/hit_test.go

@ -5,9 +5,9 @@ import (
"testing"
"time"
"git.lost.host/meutraa/eott/internal/config"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eott/internal/testdata"
"git.lost.host/meutraa/eotw/internal/config"
"git.lost.host/meutraa/eotw/internal/game"
"git.lost.host/meutraa/eotw/internal/testdata"
)
var hitTests = map[game.Input](*game.Note){

2
internal/score/scorer.go

@ -3,7 +3,7 @@ package score
import (
"time"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eotw/internal/game"
)
type Scorer interface {

2
internal/testdata/main.go

@ -3,7 +3,7 @@ package testdata
import (
"encoding/json"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eotw/internal/game"
)
func GetChart() (*game.Chart, error) {

20
internal/theme/default.go

@ -1,30 +1,12 @@
package theme
import (
"fmt"
"image/color"
"git.lost.host/meutraa/eott/internal/config"
)
type DefaultTheme struct {
}
func (t *DefaultTheme) RenderMine(column uint16, denom int) string {
r, g, b := getNoteColor(1)
return fmt.Sprintf("\033[38;2;%v;%v;%vm%v\033[0m", r, g, b, *config.MineSym)
}
func (t *DefaultTheme) RenderNote(column uint16, denom int) string {
r, g, b := getNoteColor(denom)
return fmt.Sprintf("\033[38;2;%v;%v;%vm%v\033[0m", r, g, b, *config.NoteSym)
}
func (t *DefaultTheme) RenderHitField(index uint8) string {
return *config.BarSym
}
var (
noteColors = map[int]color.RGBA{
1: {R: 236, G: 30, B: 0}, // 1/4 red
@ -44,7 +26,7 @@ var (
}
)
func getNoteColor(d int) (r, g, b uint8) {
func (t *DefaultTheme) GetNoteColor(d int) (r, g, b uint8) {
col, ok := noteColors[d]
if !ok {
col = noteColors[-1]

7
internal/theme/theme.go

@ -1,7 +0,0 @@
package theme
type Theme interface {
RenderMine(column uint16, denom int) string
RenderNote(column uint16, denom int) string
RenderHitField(index uint8) string
}

383
main.go

@ -1,36 +1,13 @@
package main
// #include <linux/input-event-codes.h>
// #include <linux/input.h>
import "C"
import (
"errors"
"fmt"
"log"
"math"
"os"
"os/signal"
"path"
"path/filepath"
"runtime/pprof"
"strconv"
"time"
"git.lost.host/meutraa/eott/internal/config"
"git.lost.host/meutraa/eott/internal/game"
"git.lost.host/meutraa/eott/internal/input"
"git.lost.host/meutraa/eott/internal/parser"
"git.lost.host/meutraa/eott/internal/render"
"git.lost.host/meutraa/eott/internal/score"
"git.lost.host/meutraa/eott/internal/theme"
"github.com/faiface/beep"
"github.com/faiface/beep/mp3"
"github.com/faiface/beep/speaker"
"github.com/faiface/beep/vorbis"
"golang.org/x/term"
rl "github.com/gen2brain/raylib-go/raylib"
_ "net/http/pprof"
"git.lost.host/meutraa/eotw/internal/config"
"git.lost.host/meutraa/eotw/internal/game"
)
func main() {
@ -40,13 +17,13 @@ func main() {
}
}
func getColumn(nKeys uint8, mc uint16, index uint8) uint16 {
func getColumn(nKeys uint8, mc int32, index uint8) int32 {
// 4 => 2
mid := nKeys >> 1
if index < mid {
return mc - *config.ColumnSpacing*uint16(nKeys-mid-index)
return mc - *config.ColumnSpacing*int32(nKeys-mid-index) + *config.ColumnSpacing>>1
} else {
return mc + *config.ColumnSpacing*uint16(index-mid)
return mc + *config.ColumnSpacing*int32(index-mid) + *config.ColumnSpacing>>1
}
}
@ -61,341 +38,53 @@ func judge(d time.Duration) (int, *game.Judgement) {
}
func run() error {
// Ensure our Default implementations are used as interfaces
var r render.Renderer = &render.DefaultRenderer{}
var th theme.Theme = &theme.DefaultTheme{}
var psr parser.Parser = &parser.DefaultParser{}
var scorer score.Scorer = &score.DefaultScorer{}
var totalFrameCounter, totalRenderDuration uint64 = 0, 0
quitChannel := make(chan os.Signal, 1)
signal.Notify(quitChannel, os.Interrupt)
// Get the dimensions of the terminal
_columns, _rows, err := term.GetSize(int(os.Stdout.Fd()))
if nil != err {
return fmt.Errorf("unable to get terminal size: %w", err)
}
totalRows, columnCount := uint16(_rows), uint16(_columns)
middleColumn, middleRow := columnCount>>1, totalRows>>1
hitRow := totalRows - *config.BarOffsetFromBottom
// Start reading keyboard input
in := make(chan *input.Event, 128)
input.ReadInput(*config.Input, in)
flags := rl.FlagVsyncHint | rl.FlagMsaa4xHint | rl.FlagWindowResizable
rl.SetConfigFlags(byte(flags))
var mp3File, ogg, chartFile string
rl.InitWindow(1080, 1360, "eotw")
defer rl.CloseWindow()
if err := filepath.Walk(*config.Directory, func(p string, info os.FileInfo, err error) error {
switch path.Ext(info.Name()) {
case ".mp3":
mp3File = p
case ".ogg":
ogg = p
case ".sm":
chartFile = p
}
return nil
}); nil != err {
return fmt.Errorf("unable to walk song directory: %w", err)
}
rl.InitAudioDevice()
defer rl.CloseAudioDevice()
if (mp3File == "" && ogg == "") || chartFile == "" {
return errors.New("unable to find .sm and .mp3/.ogg file in given directory")
}
charts, err := psr.Parse(chartFile)
if nil != err {
return err
}
err = scorer.Init()
if nil != err {
return err
}
defer func() {
scorer.Deinit()
}()
rl.SetTargetFPS(int32(*config.RefreshRate))
// Difficulty selection
for i, c := range charts {
histories := scorer.Load(c)
fmt.Printf(
"%2v) %v-key %3v %v\n\tNotes: %5v\n",
i,
c.Difficulty.NKeys,
c.Difficulty.Msd,
c.Difficulty.Name,
len(c.Notes),
)
for i, history := range histories {
sc := scorer.Score(c, &history)
fmt.Printf("\t\t%v: %v%% Misses: %4v Total Error: %v\n",
i,
history.Rate,
sc.MissCount,
sc.TotalError.Milliseconds(),
)
}
fmt.Printf("\n")
program := Program{}
for !rl.IsWindowReady() {
log.Println("Waiting on window")
time.Sleep(time.Millisecond * 5)
}
// TODO key := <-in
/*index, err := strconv.ParseInt(string(key.Rune), 10, 64)
if nil != err || index > int64(len(charts)-1) {
return err
}*/
index := 0
chart := charts[index]
audioFile := mp3File
if ogg != "" {
audioFile = ogg
}
log.Printf("Opening %v (%v)\n", audioFile, chartFile)
f, err := os.Open(audioFile)
if err != nil {
if err := program.Init(); nil != err {
return err
}
var streamer beep.StreamSeekCloser
var format beep.Format
if ogg != "" {
streamer, format, err = vorbis.Decode(f)
} else {
streamer, format, err = mp3.Decode(f)
}
if err != nil {
return err
}
buffer := beep.NewBuffer(format)
buffer.Append(streamer)
streamer.Close()
speaker.Init(beep.SampleRate(math.Round(0.01*float64(format.SampleRate)*float64(*config.Rate))), format.SampleRate.N(time.Second/60))
// Clear the screen and hide the cursor
r.Init()
defer func() {
// Restore the terminal state
r.Deinit()
}()
sideCol := getColumn(chart.Difficulty.NKeys, middleColumn, 0) - 36
if sideCol < 2 {
sideCol = 2
}
sideColData := 14 + sideCol
distanceError, sumOfDistance := time.Millisecond, time.Millisecond
counts := make([]int, len(config.Judgements))
var mean, stdev float64 = 0.0, 0.0
var totalHits uint64 = 0
inputs := []game.Input{}
music := rl.LoadMusicStream(program.audioFile)
music.Looping = false
defer rl.UnloadMusicStream(music)
program.music = &music
program.musicLength = rl.GetMusicTimeLength(music)
// Render the hit bar
for i := uint8(0); i < chart.Difficulty.NKeys; i++ {
r.Fill(totalRows-*config.BarOffsetFromBottom, getColumn(chart.Difficulty.NKeys, middleColumn, i), th.RenderHitField(i))
}
// Render the static stat ui
r.Fill(3, sideCol, " Render: ")
r.Fill(10, sideCol, " Error dt: ")
r.Fill(11, sideCol, " Stdev: ")
r.Fill(12, sideCol, " Mean: ")
r.Fill(13, sideCol, fmt.Sprintf(" Total: %6v", chart.NoteCount))
r.Fill(14, sideCol, fmt.Sprintf(" Mines: %6v", chart.MineCount))
for i, judgement := range config.Judgements {
r.Fill(uint16(18+i), sideCol, judgement.Name+": ")
}
rl.SetMusicPitch(music, float32(*config.Rate)/100)
go func() {
time.Sleep(*config.Delay + *config.Offset)
song := buffer.Streamer(0, buffer.Len())
speaker.Play(song)
for {
if song.Position() == song.Len() {
quitChannel <- os.Interrupt
}
time.Sleep(time.Second)
}
rl.PlayMusicStream(music)
}()
// I do not care about all the above, it does not affect gameplay
if *config.CpuProfile != "" {
f, err := os.Create(*config.CpuProfile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
r.RenderLoop(*config.Delay,
func(startTime time.Time, duration time.Duration) bool {
if len(quitChannel) != 0 {
return false
}
// get the key inputs that occured so far
for i := 0; i < len(in); i++ {
ev := <-in
if ev.Code == C.KEY_ESC {
return false
}
// Which index was hit
if ev.Released {
continue
}
index, err := config.KeyColumn(ev.Code, chart.Difficulty.NKeys)
if nil != err {
continue
}
input := game.Input{
Index: index,
HitTime: time.Unix(0, ev.Time.Nano()).Sub(startTime),
}
inputs = append(inputs, input)
// Get the column to render the hit splash at
col := getColumn(chart.Difficulty.NKeys, middleColumn, input.Index)
r.AddDecoration(col, totalRows-*config.BarOffsetFromBottom, "*", 120)
note, distance, abs := scorer.ApplyInputToChart(chart, &input, *config.Rate)
if note == nil {
continue
}
distanceError += abs
totalHits += 1
sumOfDistance += distance
// because distance is < missDistance, this should never be nil
idx, judgement := judge(abs)
r.AddDecoration(middleColumn, middleRow, judgement.Name, 120)
counts[idx]++
if totalHits > 1 {
stdev = 0.0
mean = float64(sumOfDistance) / float64(totalHits)
for _, n := range chart.Notes {
if n.HitTime == 0 {
continue
}
diff := scorer.Distance(*config.Rate, n.Time, n.HitTime)
xi := float64(diff) - mean
xi2 := xi * xi
stdev += xi2
}
stdev /= float64(totalHits - 1)
stdev = math.Sqrt(stdev)
}
// Update stats
r.Fill(10, sideColData, fmt.Sprintf("%6.0f ms", float64(distanceError)/float64(time.Millisecond)))
r.Fill(11, sideColData, fmt.Sprintf("%6.2f ms", stdev/float64(time.Millisecond)))
r.Fill(12, sideColData, fmt.Sprintf("%6.2f ms", mean/float64(time.Millisecond)))
r.Fill(uint16(18+idx), sideColData, fmt.Sprintf("%6v", counts[idx]))
}
// Adjust the active note range
// The first time this is called, the active slice is empty
// and start, end = 0, 0
active, start, end := chart.Active()
startOffset := 0
endOffset := 0
program.startTime = time.Now().Add(*config.Delay)
// Render notes
for _, note := range active {
col := getColumn(chart.Difficulty.NKeys, middleColumn, note.Index)
// rowCount = 60 = bottom of screen
// BarRow = 8 = 8 from bottom of screen
// nr = hitRow
// Calculate the new row based on time
// This is the main use of the Distance function
d := scorer.Distance(*config.Rate, note.Time, duration)
rowOffsetFromHitRow := int64(float64(d) * config.NsToRow)
// Check if this note will be rendered
if rowOffsetFromHitRow > int64(hitRow) {
// This is too far in the future and off the top of the screen
log.Fatalln("Active note should not be active: top")
} else if rowOffsetFromHitRow < -int64(*config.BarOffsetFromBottom) {
// This is scrolled past the bottom of the screen
// Check to see if the note was missed
if note.HitTime == 0 && !note.IsMine {
eidx := len(counts) - 1
counts[eidx] += 1
r.Fill(uint16(18+eidx), sideColData, fmt.Sprintf("%6v", counts[eidx]))
r.AddDecoration(col-1, middleRow-1, "\033[1;31m╭\033[0m", 240)
r.AddDecoration(col+1, middleRow-1, "\033[1;31m╮\033[0m", 240)
r.AddDecoration(col-1, middleRow, "\033[1;31m╰\033[0m", 240)
r.AddDecoration(col+1, middleRow, "\033[1;31m╯\033[0m", 240)
}
// Mark the active window to slide forward 1
startOffset++
// Mark the render loop to clear this note
r.Fill(note.Row, col, " ")
// TODO: probably do not need this anymore
note.Row = math.MaxUint16
} else {
// This is still an active note
renderRow := hitRow - uint16(rowOffsetFromHitRow)
// Only if this has changed position do we clear and render anew
if note.Row != renderRow && note.HitTime == 0 {
// TODO: there might be an optimization here
r.Fill(note.Row, col, " ")
note.Row = renderRow
if note.IsMine {
r.Fill(note.Row, col, th.RenderMine(col, note.Denom))
} else {
r.Fill(note.Row, col, th.RenderNote(col, note.Denom))
}
}
}
}
// At the end of this render loop I want to see which notes will require rendering next frame and slide the window
for _, note := range chart.Notes[end:] {
d := scorer.Distance(*config.Rate, note.Time, duration)
rowOffsetFromHitRow := int64(float64(d) * config.NsToRow)
// Check if this note will be rendered
if rowOffsetFromHitRow < int64(hitRow) {
endOffset++
} else {
break
}
}
// Update the sliding window
chart.SetActive(start+startOffset, end+endOffset)
return true
},
func(renderDuration time.Duration) {
totalRenderDuration += uint64(renderDuration)
totalFrameCounter++
if totalFrameCounter%uint64(*config.DebugUpdateRate) != 0 {
return
}
for !rl.WindowShouldClose() {
rl.UpdateMusicStream(music)
if rl.IsWindowResized() {
program.Resize()
}
duration := time.Since(program.startTime)
// Print debugging stats
active, start, end := chart.Active()
r.Fill(2, sideCol, fmt.Sprintf(" Window: %v - %v (%v)", start, end, len(active)))
r.Fill(3, sideColData, strconv.FormatUint(totalRenderDuration/totalFrameCounter, 10)+" ")
},
)
program.Update(duration)
program.Render(duration)
}
scorer.Save(chart, &inputs, *config.Rate)
program.Scorer.Save(&program.chart, &program.inputs, *config.Rate)
return nil
}

442
program.go

@ -0,0 +1,442 @@
package main
import (
"errors"
"fmt"
"log"
"math"
"os"
"path"
"path/filepath"
"time"
"git.lost.host/meutraa/eotw/internal/config"
"git.lost.host/meutraa/eotw/internal/game"
"git.lost.host/meutraa/eotw/internal/parser"
"git.lost.host/meutraa/eotw/internal/score"
"git.lost.host/meutraa/eotw/internal/theme"
rl "github.com/gen2brain/raylib-go/raylib"
)
type Position struct {
X, Y int32
}
type Decoration struct {
frames int
key int32
note *game.Note
startedCounting bool
startCounting func(note *game.Note, key int32) bool
render func(remaining int)
}
type Program struct {
Parser *parser.DefaultParser
Scorer *score.DefaultScorer
Theme *theme.DefaultTheme
startTime time.Time
frameCounter uint64
width, height int32
middle Position
hitRow int32
decorations []*Decoration
audioFile, chartFile string
music *rl.Music
musicLength float32 // In seconds
charts []*game.Chart
chart game.Chart
sideCol int32
// Stats for current chart
distanceError, sumOfDistance time.Duration
counts []int
mean, stdev float64
totalHits uint64
inputs []game.Input
}
func (p *Program) Resize() {
log.Println(rl.GetScreenHeight(), rl.GetScreenWidth())
p.width = int32(rl.GetScreenWidth())
p.height = int32(rl.GetScreenHeight())
p.middle = Position{X: p.width / 2, Y: p.height / 2}
p.hitRow = p.height - *config.BarOffsetFromBottom
p.sideCol = getColumn(p.chart.Difficulty.NKeys, p.middle.X, 0) - 360
if p.sideCol < 20 {
p.sideCol = 20
}
}
func (g *Program) Init() error {
// Ensure our Default implementations are used as interfaces
g.Parser = &parser.DefaultParser{}
g.Scorer = &score.DefaultScorer{}
g.Theme = &theme.DefaultTheme{}
if err := filepath.Walk(*config.Directory, func(p string, info os.FileInfo, err error) error {
switch path.Ext(info.Name()) {
case ".ogg", ".mp3", ".xm", ".mod", ".wav":
g.audioFile = p
case ".sm":
g.chartFile = p
}
return nil
}); nil != err {
return fmt.Errorf("unable to walk song directory: %w", err)
}
if (g.audioFile == "") || g.chartFile == "" {
return errors.New("unable to find .sm and .mp3/.ogg file in given directory")
}
var err error
g.charts, err = g.Parser.Parse(g.chartFile)
if nil != err {
return err
}
err = g.Scorer.Init()
if nil != err {
return err
}
defer func() {
g.Scorer.Deinit()
}()
g.chart = *g.charts[0]
g.counts = make([]int, len(config.Judgements))
g.inputs = []game.Input{}
g.Resize()
return nil
}
func (p *Program) Update(duration time.Duration) {
// get the key inputs that occured so far
for key := rl.GetKeyPressed(); key != 0; key = rl.GetKeyPressed() {
index, err := config.KeyColumn(key, p.chart.Difficulty.NKeys)
if nil != err {
log.Println("not a column index pressed")
continue
}
input := game.Input{Index: index, HitTime: duration}
p.inputs = append(p.inputs, input)
// Get the column to render the hit splash at
col := getColumn(p.chart.Difficulty.NKeys, p.middle.X, input.Index)
note, distance, abs := p.Scorer.ApplyInputToChart(&p.chart, &input, *config.Rate)
if note == nil {
// If this is hitting nothing
p.decorations = append(p.decorations, &Decoration{
frames: 24,
key: key,
note: note,
startCounting: func(note *game.Note, key int32) bool {
return rl.IsKeyReleased(key)
},
render: func(remaining int) {
g := rl.Gray
g.A = uint8(float32(255) * (float32(remaining) / 24))
rl.DrawCircle(col, p.hitRow, *config.NoteRadius-4, g)
},
})
continue
}
p.distanceError += abs
p.totalHits += 1
p.sumOfDistance += distance
// because distance is < missDistance, this should never be nil
idx, judgement := judge(abs)
p.decorations = append(p.decorations, &Decoration{
frames: 24,
key: key,
note: note,
startCounting: func(note *game.Note, key int32) bool {
released := rl.IsKeyReleased(key)
if released {
note.ReleaseTime = duration
return true
}
return false
},
render: func(remaining int) {
g := judgement.Color
gr := rl.Gray
gr.A = uint8(float32(255) * (float32(remaining) / 24))
rl.DrawCircle(col, p.hitRow, *config.NoteRadius, g)
rl.DrawCircle(col, p.hitRow, *config.NoteRadius-2, rl.Black)
rl.DrawCircle(col, p.hitRow, *config.NoteRadius-4, gr)
},
})
os := int32(2*-distance.Milliseconds()) + p.middle.X
p.decorations = append(p.decorations, &Decoration{
frames: 120,
render: func(remaining int) {
g := judgement.Color
g.A = uint8