diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..5df826d0 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,117 @@ +name: Build danser + +on: + workflow_dispatch: + inputs: + version: + description: 'Danser version' + type: string + required: true + draft: + description: 'Create draft release' + type: boolean + required: false + default: true + +jobs: + build_windows: + name: Building windows version + runs-on: windows-latest + defaults: + run: + shell: bash + steps: + - name: Checkout master branch + uses: actions/checkout@v3 + + - name: Install winlibs + uses: bwoodsend/setup-winlibs-action@v1 + + - name: Install golang + uses: actions/setup-go@v3 + with: + go-version: '1.19' + cache: true + + - name: Build danser + run: | + version="${{ github.event.inputs.version }}" + + ./dist-win.sh $version + + if [ ! -f "dist/artifacts/danser-${version// /-s}-win.zip" ]; then + echo "Danser failed to build" + exit 1 + fi + id: build + + - name: Upload artifact + uses: actions/upload-artifact@v3 + if: ${{ !failure() && steps.build.conclusion != 'failure' }} + with: + name: danser + path: dist/artifacts/* + + build_linux: + name: Building linux version + runs-on: ubuntu-latest + steps: + - name: Checkout master branch + uses: actions/checkout@v3 + + - name: Install needed packages + run: sudo apt-get install xorg-dev libgl1-mesa-dev libgtk-3-dev + + - name: Install golang + uses: actions/setup-go@v3 + with: + go-version: '1.19' + cache: true + + - name: Build danser + run: | + version="${{ github.event.inputs.version }}" + + chmod +x dist-linux.sh + ./dist-linux.sh $version + + if [ ! -f "dist/artifacts/danser-${version// /-s}-linux.zip" ]; then + echo "Danser failed to build" + exit 1 + fi + id: build + + - name: Upload artifact + uses: actions/upload-artifact@v3 + if: ${{ !failure() && steps.build.conclusion != 'failure' }} + with: + name: danser + path: dist/artifacts/* + + publish_release: + name: Publish draft release + if: ${{ !cancelled() && needs.build_windows.result == 'success' && needs.build_linux.result == 'success' && github.event.inputs.draft }} + needs: [build_windows, build_linux] + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: danser + path: artifacts + - name: Create release + id: create_release + run: | + set -xe + shopt -s nullglob + + version="${{ github.event.inputs.version }}" + + NAME="${version// / snapshot }" + TAGNAME="${version// /-s}" + + hub release create --draft $(for a in artifacts/*.{zip,tar.xz}; do echo -a $a; done) -m "$NAME" -t "master" "$TAGNAME" + env: + GITHUB_TOKEN: ${{ github.token }} \ No newline at end of file diff --git a/README.md b/README.md index b22ada61..0b2388e2 100644 --- a/README.md +++ b/README.md @@ -105,11 +105,10 @@ You need to clone it or download as a .zip (and unpack it to desired directory) #### Prerequisites -* [64-bit go (1.18 at least)](https://golang.org/dl/) -* gcc (Linux/Unix), [WinLibs](http://winlibs.com/) (Windows, TDM-GCC won't work, mingw-w64 is outdated) -* OpenGL library (shipped with drivers) -* xorg-dev (Linux) -* libgtk-3 and libgtk-3-dev (Linux) +* [64-bit go (1.19 at least)](https://go.dev/dl/) +* gcc/g++ (Linux/Unix), [WinLibs](http://winlibs.com/) (Windows, TDM-GCC won't work, mingw-w64 is outdated) +* OpenGL library (shipped with drivers, `libgl1-mesa-dev` when building on Linux servers) +* xorg-dev, libgtk-3 and libgtk-3-dev (Linux) #### Building and running the project diff --git a/app/app.go b/app/app.go index d26fc5a7..34050e27 100644 --- a/app/app.go +++ b/app/app.go @@ -51,7 +51,6 @@ import ( "runtime" "strings" "time" - "unsafe" ) const ( @@ -84,6 +83,8 @@ var screenshotTime float64 var preciseProgress bool +var monitorHz int + func run() { defer func() { if err := recover(); err != nil { @@ -109,16 +110,16 @@ func run() { creator := flag.String("creator", "", creatorDesc) flag.StringVar(creator, "c", "", creatorDesc+shorthand) - settingsVersion := flag.String("settings", "", "Specify settings version, -settings=abc means that settings/abc.json will be loaded") + settingsVersion := flag.String("settings", "", "Specify settings version, -settings=b/abc means that settings/b/abc.json will be loaded. \"Credentials\"") cursors := flag.Int("cursors", 1, "How many repeated cursors should be visible, recommended 2 for mirror, 8 for mandala") tag := flag.Int("tag", 1, "How many cursors should be \"playing\" specific map. 2 means that 1st cursor clicks the 1st object, 2nd clicks 2nd object, 1st clicks 3rd and so on") - knockout := flag.Bool("knockout", false, "Use knockout feature") - knockout2 := flag.String("knockout2", "", "Use knockout feature, but using compatible replays provided in a JSON list") + knockout := flag.Bool("knockout", false, "Use (classic) knockout feature. Replays are sourced from \"replays/{a}\" where {a} is an md5 hash of .osu file. Danser automatically organizes replay files put directly in \"replays\", using maps' md5s provided by the replay files.") + knockout2 := flag.String("knockout2", "", "Use (new) knockout feature, JSON list of paths to compatible replay files has to be provided. \"Knockout.ExcludeMods\" and \"Knockout.MaxPlayers\" options are ignored, they have to be filtered beforehand.") speed := flag.Float64("speed", 1.0, "Specify music's speed, set to 1.5 to have DoubleTime mod experience") pitch := flag.Float64("pitch", 1.0, "Specify music's pitch, set to 1.5 with -speed=1.5 to have Nightcore mod experience") - debug := flag.Bool("debug", false, "Show info about map and rendering engine, overrides Graphics.ShowFPS setting") + debug := flag.Bool("debug", false, "Show info about map and rendering engine, overrides Graphics.ShowFPS setting. Ignored in record/screenshot modes.") gldebug := flag.Bool("gldebug", false, "Turns on OpenGL debug logging, may reduce performance heavily") @@ -134,14 +135,14 @@ func run() { out := flag.String("out", "", "If -ss flag is used, sets the name of screenshot, extension is PNG. If not, it overrides -record flag, specifies the name of recorded video file, extension is managed by settings") ss := flag.Float64("ss", math.NaN(), "Screenshot mode. Snap single frame from danser at given time in seconds. Specify the name of file by -out, resolution is managed by Recording settings") - mods := flag.String("mods", "", "Specify beatmap/play mods. If NC/DT/HT is selected, overrides -speed and -pitch flags") + mods := flag.String("mods", "", "Specify beatmap/play mods") replay := flag.String("replay", "", replayDesc) flag.StringVar(replay, "r", "", replayDesc+shorthand) skin := flag.String("skin", "", "Replace Skin.CurrentSkin setting temporarily") - noDbCheck := flag.Bool("nodbcheck", false, "Don't validate the database and import new beatmaps if there are any. Useful for slow drives.") + noDbCheck := flag.Bool("nodbcheck", false, "Don't validate the database and only import new beatmap sets if there are any. Useful for slow drives.") noUpdCheck := flag.Bool("noupdatecheck", strings.HasPrefix(env.LibDir(), "/usr/lib/"), "Don't check for updates. Speeds up startup if older version of danser is needed for various reasons. Has no effect if danser is running as a linux package") ar := flag.Float64("ar", math.NaN(), "Modify map's AR, only in cursordance/play modes") @@ -334,10 +335,8 @@ func run() { panic("Failed to initialize GLFW: " + err.Error()) } - glfw.WindowHint(glfw.ContextVersionMajor, 3) - glfw.WindowHint(glfw.ContextVersionMinor, 3) - glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) - glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) + platform.SetupContext() + glfw.WindowHint(glfw.Resizable, glfw.False) glfw.WindowHint(glfw.Samples, 0) glfw.WindowHint(glfw.Visible, glfw.False) @@ -345,6 +344,8 @@ func run() { monitor := glfw.GetPrimaryMonitor() mWidth, mHeight := monitor.GetVideoMode().Width, monitor.GetVideoMode().Height + monitorHz = monitor.GetVideoMode().RefreshRate + if newSettings { settings.Graphics.SetDefaults(int64(mWidth), int64(mHeight)) settings.Save() @@ -437,51 +438,9 @@ func run() { log.Println("GLFW initialized!") - gl.Init() - - extensionCheck() - - glVendor := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.VENDOR)))) - glRenderer := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.RENDERER)))) - glVersion := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.VERSION)))) - glslVersion := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.SHADING_LANGUAGE_VERSION)))) - - // HACK HACK HACK: please see github.com/wieku/danser-go/framework/graphics/buffer.IsIntel for more info - if strings.Contains(strings.ToLower(glVendor), "intel") { - buffer.IsIntel = true - } - - var extensions string - - var numExtensions int32 - gl.GetIntegerv(gl.NUM_EXTENSIONS, &numExtensions) - - for i := int32(0); i < numExtensions; i++ { - extensions += C.GoString((*C.char)(unsafe.Pointer(gl.GetStringi(gl.EXTENSIONS, uint32(i))))) - extensions += " " - } - - log.Println("GL Vendor: ", glVendor) - log.Println("GL Renderer: ", glRenderer) - log.Println("GL Version: ", glVersion) - log.Println("GLSL Version: ", glslVersion) - log.Println("GL Extensions:", extensions) - log.Println("OpenGL initialized!") - - if *gldebug { - gl.Enable(gl.DEBUG_OUTPUT) - gl.DebugMessageCallback(func( - source uint32, - gltype uint32, - id uint32, - severity uint32, - length int32, - message string, - userParam unsafe.Pointer) { - log.Println("GL:", message) - }, gl.Ptr(nil)) - - gl.DebugMessageControl(gl.DONT_CARE, gl.DONT_CARE, gl.DONT_CARE, 0, nil, true) + err = platform.GLInit(*gldebug) + if err != nil { + panic("Failed to initialize OpenGL: " + err.Error()) } if !settings.RECORD { @@ -701,14 +660,20 @@ func mainLoopNormal() { win.SetKeyCallback(func(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { if action == glfw.Press { switch key { - case glfw.KeyF2: - scheduleScreenshot = true case glfw.KeyEscape: win.SetShouldClose(true) case glfw.KeyMinus: settings.DIVIDES = mutils.Max(1, settings.DIVIDES-1) case glfw.KeyEqual: settings.DIVIDES += 1 + case glfw.KeyO: + if mods == glfw.ModControl { + log.Println("Launcher: Open settings") + } + default: + if platform.GetKeyName(key, scancode) == settings.Input.ScreenshotKey { + scheduleScreenshot = true + } } } @@ -741,6 +706,13 @@ func mainLoopNormal() { win.SwapBuffers() if !settings.Graphics.VSync { + fCap := int(settings.Graphics.FPSCap) + + if fCap < 0 { + fCap = -fCap*monitorHz + } + + limiter.FPS = fCap limiter.Sync() } @@ -750,28 +722,6 @@ func mainLoopNormal() { settings.CloseWatcher() } -func extensionCheck() { - extensions := []string{ - "GL_ARB_clear_texture", - "GL_ARB_direct_state_access", - "GL_ARB_texture_storage", - "GL_ARB_vertex_attrib_binding", - "GL_ARB_buffer_storage", - } - - var notSupported []string - - for _, ext := range extensions { - if !glfw.ExtensionSupported(ext) { - notSupported = append(notSupported, ext) - } - } - - if len(notSupported) > 0 { - panic(fmt.Sprintf("Your GPU does not support one or more required OpenGL extensions: %s. Please update your graphics drivers or upgrade your GPU", notSupported)) - } -} - func pushFrame() { statistic.Reset() @@ -825,7 +775,7 @@ func checkForUpdates() { log.Println("You're using the newest version of danser.") case utils.Snapshot: log.Println("You're using a snapshot version of danser.") - log.Println("For newer version of snapshots please visit an official danser discord server at:", url) + log.Println("For newer version of snapshots please visit the official danser discord server at:", url) case utils.UpdateAvailable: log.Println("You're using an older version of danser.") log.Println("You can download a newer version here:", url) diff --git a/app/audio/osuaudio.go b/app/audio/osuaudio.go index ee6af619..18640128 100644 --- a/app/audio/osuaudio.go +++ b/app/audio/osuaudio.go @@ -270,3 +270,10 @@ func LoadBeatmapSamples(dir string) { func LoadSample(name string) *bass.Sample { return skin.GetSample(name) } + +func PlayFailSound() { + sample := LoadSample("failsound") + if sample != nil { + sample.Play() + } +} diff --git a/app/beatmap/objects/circle.go b/app/beatmap/objects/circle.go index fc19672a..400df8e0 100644 --- a/app/beatmap/objects/circle.go +++ b/app/beatmap/objects/circle.go @@ -132,7 +132,7 @@ func (circle *Circle) PlaySound() { audio.PlaySample(sampleSet, circle.BasicHitSound.AdditionSet, circle.sample, index, point.SampleVolume, circle.HitObjectID, circle.GetStackedStartPosition().X64()) } -func (circle *Circle) SetTiming(timings *Timings, _ bool) { +func (circle *Circle) SetTiming(timings *Timings, _ int, _ bool) { circle.Timings = timings } diff --git a/app/beatmap/objects/hitobject.go b/app/beatmap/objects/hitobject.go index ab1b6bcf..cae10591 100644 --- a/app/beatmap/objects/hitobject.go +++ b/app/beatmap/objects/hitobject.go @@ -8,7 +8,7 @@ import ( type IHitObject interface { Update(time float64) bool - SetTiming(timings *Timings, diffCalcOnly bool) //diffCalcOnly skips stables' path generation which is quite memory consuming + SetTiming(timings *Timings, beatmapVersion int, diffCalcOnly bool) //diffCalcOnly skips stables' path generation which is quite memory consuming UpdateStacking() SetDifficulty(difficulty *difficulty.Difficulty) @@ -94,7 +94,7 @@ type HitObject struct { func (hitObject *HitObject) Update(_ float64) bool { return true } -func (hitObject *HitObject) SetTiming(_ *Timings, _ bool) {} +func (hitObject *HitObject) SetTiming(_ *Timings, _ int, _ bool) {} func (hitObject *HitObject) UpdateStacking() {} diff --git a/app/beatmap/objects/slider.go b/app/beatmap/objects/slider.go index 98e912db..dda5c101 100644 --- a/app/beatmap/objects/slider.go +++ b/app/beatmap/objects/slider.go @@ -88,6 +88,8 @@ type Slider struct { EndTimeLazer float64 ScorePointsLazer []TickPoint spanDuration float64 + + updatedAtLeastOnce bool } func NewSlider(data []string) *Slider { @@ -168,6 +170,10 @@ func NewSlider(data []string) *Slider { return slider } +func (slider *Slider) GetLength() float32 { + return slider.multiCurve.GetLength() +} + func (slider *Slider) GetHalf() vector.Vector2f { return slider.multiCurve.PointAt(0.5).Add(slider.StackOffset) } @@ -275,7 +281,7 @@ func (slider *Slider) createDummyCircle(time float64, inheritStart, inheritEnd b return circle } -func (slider *Slider) SetTiming(timings *Timings, diffCalcOnly bool) { +func (slider *Slider) SetTiming(timings *Timings, beatmapVersion int, diffCalcOnly bool) { slider.Timings = timings slider.TPoint = timings.GetPointAt(slider.StartTime) @@ -294,7 +300,11 @@ func (slider *Slider) SetTiming(timings *Timings, diffCalcOnly bool) { slider.EndTimeLazer = slider.StartTime + cLength*1000*float64(slider.RepeatCount)/velocity minDistanceFromEnd := velocity * 0.01 + tickDistance := slider.Timings.GetTickDistance(slider.TPoint) + if beatmapVersion < 8 { + tickDistance = slider.Timings.GetScoringDistance() + } if slider.multiCurve.GetLength() > 0 && tickDistance > slider.pixelLength { tickDistance = slider.pixelLength @@ -456,18 +466,6 @@ func (slider *Slider) SetDifficulty(diff *difficulty.Difficulty) { slider.sliderSnakeTail = animation.NewGlider(0) slider.sliderSnakeHead = animation.NewGlider(0) - fadeMultiplier := 1.0 - mutils.ClampF(settings.Objects.Sliders.Snaking.FadeMultiplier, 0.0, 1.0) - durationMultiplier := mutils.ClampF(settings.Objects.Sliders.Snaking.DurationMultiplier, 0.0, 1.0) - - slSnInS := slider.StartTime - diff.Preempt - slSnInE := slider.StartTime - diff.Preempt*2/3*fadeMultiplier + slider.partLen*durationMultiplier - - if settings.Objects.Sliders.Snaking.In { - slider.sliderSnakeTail.AddEvent(slSnInS, slSnInE, 1) - } else { - slider.sliderSnakeTail.SetValue(1) - } - slider.fade = animation.NewGlider(0) slider.fade.AddEvent(slider.StartTime-diff.Preempt, slider.StartTime-(diff.Preempt-diff.TimeFadeIn), 1) @@ -476,10 +474,6 @@ func (slider *Slider) SetDifficulty(diff *difficulty.Difficulty) { if diff.CheckModActive(difficulty.Hidden) { slider.bodyFade.AddEventEase(slider.StartTime-diff.Preempt+diff.TimeFadeIn, slider.EndTime, 0, easing.OutQuad) - } else if settings.Objects.Sliders.Snaking.Out && settings.Objects.Sliders.Snaking.OutFadeInstant { - slider.bodyFade.AddEvent(slider.EndTime, slider.EndTime, 0) - } else { - slider.bodyFade.AddEvent(slider.EndTime, slider.EndTime+difficulty.HitFadeOut, 0) } slider.fade.AddEvent(slider.EndTime, slider.EndTime+difficulty.HitFadeOut, 0) @@ -534,7 +528,7 @@ func (slider *Slider) SetDifficulty(diff *difficulty.Difficulty) { circle.StackOffset = slider.StackOffset circle.StackOffsetHR = slider.StackOffsetHR circle.StackOffsetEZ = slider.StackOffsetEZ - circle.SetTiming(slider.Timings, false) + circle.SetTiming(slider.Timings, 14, false) circle.SetDifficulty(diff) slider.endCircles = append(slider.endCircles, circle) @@ -548,26 +542,6 @@ func (slider *Slider) SetDifficulty(diff *difficulty.Difficulty) { } for i, p := range slider.TickPoints { - a := (p.Time-slider.StartTime)/2 + slider.StartTime - diff.Preempt*2/3 - - fs := (p.Time - slider.StartTime) / slider.partLen - - if fs < 1.0 { - a = math.Max(fs*(slSnInE-slSnInS)+slSnInS, a) - } - - endTime := math.Min(a+150, p.Time-36) - - p.scale.AddEventS(a, endTime, 0.5, 1.2) - p.scale.AddEventSEase(endTime, endTime+150, 1.2, 1.0, easing.OutQuad) - p.fade.AddEventS(a, endTime, 0.0, 1.0) - - if diff.CheckModActive(difficulty.Hidden) { - p.fade.AddEventS(math.Max(endTime, p.Time-1000), p.Time, 1.0, 0.0) - } else { - p.fade.AddEventS(p.Time, p.Time, 1.0, 0.0) - } - p.Pos = slider.GetStackedPositionAtMod(p.Time, slider.diff.Mods) slider.TickPoints[i] = p @@ -587,6 +561,12 @@ func (slider *Slider) IsRetarded() bool { } func (slider *Slider) Update(time float64) bool { + if !slider.updatedAtLeastOnce { + slider.initSnakeIn() + + slider.updatedAtLeastOnce = true + } + if (!settings.PLAY && !settings.KNOCKOUT) || settings.PLAYERS > 1 { for i := int64(0); i <= slider.RepeatCount; i++ { edgeTime := slider.StartTime + math.Floor(float64(i)*slider.partLen) @@ -749,6 +729,63 @@ func (slider *Slider) ArmStart(clicked bool, time float64) { } } } + + if !slider.diff.CheckModActive(difficulty.Hidden) { + if settings.Objects.Sliders.Snaking.Out && settings.Objects.Sliders.Snaking.OutFadeInstant { + slider.bodyFade.AddEvent(slider.EndTime, slider.EndTime, 0) + } else { + slider.bodyFade.AddEvent(slider.EndTime, slider.EndTime+difficulty.HitFadeOut, 0) + } + } +} + +func (slider *Slider) initSnakeIn() { + slSnInS := slider.StartTime - slider.diff.Preempt + slSnInE := slider.StartTime - slider.diff.Preempt*2/3 + + if settings.Objects.Sliders.Snaking.In { + fadeMultiplier := 1.0 - mutils.ClampF(settings.Objects.Sliders.Snaking.FadeMultiplier, 0.0, 1.0) + durationMultiplier := mutils.ClampF(settings.Objects.Sliders.Snaking.DurationMultiplier, 0.0, 1.0) + + slSnInE = slider.StartTime - slider.diff.Preempt*2/3*fadeMultiplier + slider.partLen*durationMultiplier + + slider.sliderSnakeTail.AddEvent(slSnInS, slSnInE, 1) + } else { + slider.sliderSnakeTail.SetValue(1) + } + + for i, p := range slider.TickPoints { + var startTime, endTime float64 + + repeatProgress := (p.Time - slider.StartTime) / slider.partLen + + if repeatProgress < 1.0 { + normalStart := (p.Time-slider.StartTime)/2 + slider.StartTime - slider.diff.Preempt*2/3 + + startTime = math.Max(repeatProgress*(slSnInE-slSnInS)+slSnInS, normalStart) + + endTime = math.Min(startTime+150, p.Time-36) + } else { + rStart := slider.StartTime + slider.partLen*math.Floor(repeatProgress) + + endTime = rStart + (p.Time-rStart)/2 + startTime = endTime - 200 + } + + p.scale.AddEventS(startTime, endTime, 0.5, 1.2) + p.scale.AddEventSEase(endTime, endTime+150, 1.2, 1.0, easing.OutQuad) + p.fade.AddEventS(startTime, endTime, 0.0, 1.0) + + if slider.diff.CheckModActive(difficulty.Hidden) { + p.fade.AddEventS(math.Max(endTime, p.Time-1000), p.Time, 1.0, 0.0) + } else { + p.fade.AddEventS(p.Time, p.Time, 1.0, 0.0) + } + + p.Pos = slider.GetStackedPositionAtMod(p.Time, slider.diff.Mods) + + slider.TickPoints[i] = p + } } func (slider *Slider) InitSlide(time float64) { diff --git a/app/beatmap/objects/spinner.go b/app/beatmap/objects/spinner.go index 504da696..fc696da8 100644 --- a/app/beatmap/objects/spinner.go +++ b/app/beatmap/objects/spinner.go @@ -86,7 +86,7 @@ func (spinner *Spinner) GetPosition() vector.Vector2f { return spinner.pos } -func (spinner *Spinner) SetTiming(timings *Timings, _ bool) { +func (spinner *Spinner) SetTiming(timings *Timings, _ int, _ bool) { spinner.Timings = timings } @@ -127,7 +127,15 @@ func (spinner *Spinner) SetDifficulty(diff *difficulty.Difficulty) { spinner.middle.ResetValuesToTransforms() } else { spinner.background = sprite.NewSpriteSingle(skin.GetTexture("spinner-background"), 0.0, vector.NewVec2d(spinner.ScaledWidth/2, 46.5+350.4), vector.Centre) - spinner.metre = sprite.NewSpriteSingle(skin.GetTexture("spinner-metre"), 2.0, vector.NewVec2d(spinner.ScaledWidth/2-512, 47.5), vector.TopLeft) //nolint:misspell + + sMetre := skin.GetTexture("spinner-metre") + + metreHeight := 0.0 + if sMetre != nil { + metreHeight = float64(sMetre.Height) + } + + spinner.metre = sprite.NewSpriteSingle(sMetre, 2.0, vector.NewVec2d(spinner.ScaledWidth/2-512, 47.5+metreHeight), vector.BottomLeft) //nolint:misspell spinner.metre.SetCutOrigin(vector.BottomCentre) spinner.middle2 = sprite.NewSpriteSingle(skin.GetTexture("spinner-circle"), 1.0, spinner.StartPosRaw.Copy64(), vector.Centre) diff --git a/app/beatmap/parser.go b/app/beatmap/parser.go index 923bdcfc..71fa3d2d 100644 --- a/app/beatmap/parser.go +++ b/app/beatmap/parser.go @@ -378,7 +378,7 @@ func ParseObjects(beatMap *BeatMap, diffCalcOnly, parseColors bool) { } for _, obj := range beatMap.HitObjects { - obj.SetTiming(beatMap.Timings, diffCalcOnly) + obj.SetTiming(beatMap.Timings, beatMap.Version, diffCalcOnly) } calculateStackLeniency(beatMap) diff --git a/app/bmath/camera/camera.go b/app/bmath/camera/camera.go index 6506e5d6..d2c89b7c 100644 --- a/app/bmath/camera/camera.go +++ b/app/bmath/camera/camera.go @@ -66,7 +66,7 @@ func (camera *Camera) SetViewport(width, height int, yDown bool) { camera.viewDirty = true } -func (camera *Camera) SetOsuViewport(width, height int, scale float64, offset bool) { +func (camera *Camera) SetOsuViewport(width, height int, scale float64, shift, osuOffset bool) { baseScale := float64(height) / OsuHeight if OsuWidth/OsuHeight > float64(width)/float64(height) { baseScale = float64(width) / OsuWidth @@ -74,14 +74,19 @@ func (camera *Camera) SetOsuViewport(width, height int, scale float64, offset bo scl := baseScale * 0.8 * scale - shift := settings.Playfield.ShiftY - if offset { - shift = 8 + shiftX := 0.0 + shiftY := 0.0 + + if osuOffset { + shiftY = 8 + } else if shift { + shiftY = settings.Playfield.ShiftY + shiftX = settings.Playfield.ShiftX } camera.SetViewport(width, height, true) camera.originV = vector.NewVec2d(OsuWidth/2, OsuHeight/2).Scl(-1) - camera.positionV = vector.NewVec2d(settings.Playfield.ShiftX, shift).Scl(scl) + camera.positionV = vector.NewVec2d(shiftX, shiftY).Scl(scl) camera.scaleV = vector.NewVec2d(scl, scl) camera.Update() diff --git a/app/dance/controller.go b/app/dance/controller.go index 0b825131..e8ba7a3d 100644 --- a/app/dance/controller.go +++ b/app/dance/controller.go @@ -47,35 +47,11 @@ func (controller *GenericController) InitCursors() { mover = strings.ToLower(settings.CursorDance.Movers[i%len(settings.CursorDance.Movers)].Mover) } - var moverCtor func() movers.MultiPointMover - - switch mover { - case "spline": - moverCtor = movers.NewSplineMover - case "bezier": - moverCtor = movers.NewBezierMover - case "circular": - moverCtor = movers.NewHalfCircleMover - case "linear": - moverCtor = movers.NewLinearMover - case "axis": - moverCtor = movers.NewAxisMover - case "exgon": - moverCtor = movers.NewExGonMover - case "aggressive": - moverCtor = movers.NewAggressiveMover - case "momentum": - moverCtor = movers.NewMomentumMover - case "pippi": - moverCtor = movers.NewPippiMover - default: - moverCtor = movers.NewAngleOffsetMover - mover = "flower" - } + moverCtor, mName := movers.GetMoverCtorByName(mover) - controller.schedulers[i] = schedulers.NewGenericScheduler(moverCtor, i, counter[mover]) + controller.schedulers[i] = schedulers.NewGenericScheduler(moverCtor, i, counter[mName]) - counter[mover]++ + counter[mName]++ } type Queue struct { diff --git a/app/dance/input/input.go b/app/dance/input/input.go index 9f1500ee..06afab6c 100644 --- a/app/dance/input/input.go +++ b/app/dance/input/input.go @@ -2,6 +2,7 @@ package input import ( "github.com/wieku/danser-go/app/beatmap/objects" + "github.com/wieku/danser-go/app/dance/movers" "github.com/wieku/danser-go/app/graphics" "github.com/wieku/danser-go/framework/math/mutils" ) @@ -18,10 +19,12 @@ type NaturalInputProcessor struct { previousEnd float64 releaseLeftAt float64 releaseRightAt float64 + mover movers.MultiPointMover } -func NewNaturalInputProcessor(objs []objects.IHitObject, cursor *graphics.Cursor) *NaturalInputProcessor { +func NewNaturalInputProcessor(objs []objects.IHitObject, cursor *graphics.Cursor, mover movers.MultiPointMover) *NaturalInputProcessor { processor := new(NaturalInputProcessor) + processor.mover = mover processor.cursor = cursor processor.queue = make([]objects.IHitObject, len(objs)) processor.releaseLeftAt = -10000000 @@ -36,18 +39,22 @@ func (processor *NaturalInputProcessor) Update(time float64) { if len(processor.queue) > 0 { for i := 0; i < len(processor.queue); i++ { g := processor.queue[i] - if g.GetStartTime() > time { + + gStartTime := processor.mover.GetObjectsStartTime(g) + gEndTime := processor.mover.GetObjectsEndTime(g) + + if gStartTime > time { break } - if processor.lastTime < g.GetStartTime() && time >= g.GetStartTime() { - startTime := g.GetStartTime() - endTime := g.GetEndTime() + if processor.lastTime < gStartTime && time >= gStartTime { + startTime := gStartTime + endTime := gEndTime releaseAt := endTime + 50.0 if i+1 < len(processor.queue) { - nTime := processor.queue[mutils.Min(i+2, len(processor.queue)-1)].GetStartTime() + nTime := processor.mover.GetObjectsStartTime(processor.queue[mutils.Min(i+2, len(processor.queue)-1)]) releaseAt = mutils.ClampF(nTime-2, endTime+1, releaseAt) } diff --git a/app/dance/movers/linear.go b/app/dance/movers/linear.go index 2eb761cb..a10758cd 100644 --- a/app/dance/movers/linear.go +++ b/app/dance/movers/linear.go @@ -40,12 +40,12 @@ func (mover *LinearMover) SetObjects(objs []objects.IHitObject) int { mover.line = curves.NewLinear(startPos, endPos) if mover.simple { - mover.startTime = math.Max(mover.startTime, end.GetStartTime()-(mover.diff.Preempt-100*mover.diff.Speed)) + mover.startTime = math.Max(mover.startTime, mover.endTime-(mover.diff.Preempt-100*mover.diff.Speed)) } else { config := settings.CursorDance.MoverSettings.Linear[mover.id%len(settings.CursorDance.MoverSettings.Linear)] if config.WaitForPreempt { - mover.startTime = math.Max(mover.startTime, end.GetStartTime()-(mover.diff.Preempt-config.ReactionTime*mover.diff.Speed)) + mover.startTime = math.Max(mover.startTime, mover.endTime-(mover.diff.Preempt-config.ReactionTime*mover.diff.Speed)) } } diff --git a/app/dance/movers/mover.go b/app/dance/movers/mover.go index 4b9762ec..cbd3df21 100644 --- a/app/dance/movers/mover.go +++ b/app/dance/movers/mover.go @@ -4,6 +4,7 @@ import ( "github.com/wieku/danser-go/app/beatmap/difficulty" "github.com/wieku/danser-go/app/beatmap/objects" "github.com/wieku/danser-go/framework/math/vector" + "strings" ) const sixtyTime = 1000.0 / 60 @@ -12,6 +13,10 @@ type MultiPointMover interface { Reset(diff *difficulty.Difficulty, id int) SetObjects(objs []objects.IHitObject) int Update(time float64) vector.Vector2f + GetObjectsStartTime(object objects.IHitObject) float64 + GetObjectsEndTime(object objects.IHitObject) float64 + GetObjectsStartPosition(object objects.IHitObject) vector.Vector2f + GetObjectsEndPosition(object objects.IHitObject) vector.Vector2f GetObjectsPosition(time float64, object objects.IHitObject) vector.Vector2f GetStartTime() float64 GetEndTime() float64 @@ -19,11 +24,11 @@ type MultiPointMover interface { type basicMover struct { startTime float64 - endTime float64 + endTime float64 - id int + id int - diff *difficulty.Difficulty + diff *difficulty.Difficulty } func (mover *basicMover) Reset(diff *difficulty.Difficulty, id int) { @@ -31,6 +36,22 @@ func (mover *basicMover) Reset(diff *difficulty.Difficulty, id int) { mover.id = id } +func (mover *basicMover) GetObjectsStartTime(object objects.IHitObject) float64 { + return object.GetStartTime() +} + +func (mover *basicMover) GetObjectsEndTime(object objects.IHitObject) float64 { + return object.GetEndTime() +} + +func (mover *basicMover) GetObjectsStartPosition(object objects.IHitObject) vector.Vector2f { + return object.GetStackedStartPositionMod(mover.diff.Mods) +} + +func (mover *basicMover) GetObjectsEndPosition(object objects.IHitObject) vector.Vector2f { + return object.GetStackedEndPositionMod(mover.diff.Mods) +} + func (mover *basicMover) GetObjectsPosition(time float64, object objects.IHitObject) vector.Vector2f { return object.GetStackedPositionAtMod(time, mover.diff.Mods) } @@ -42,3 +63,39 @@ func (mover *basicMover) GetStartTime() float64 { func (mover *basicMover) GetEndTime() float64 { return mover.endTime } + +func GetMoverByName(name string) MultiPointMover { + ctor, _ := GetMoverCtorByName(name) + + return ctor() +} + +func GetMoverCtorByName(name string) (moverCtor func() MultiPointMover, finalName string) { + finalName = strings.ToLower(name) + + switch finalName { + case "spline": + moverCtor = NewSplineMover + case "bezier": + moverCtor = NewBezierMover + case "circular": + moverCtor = NewHalfCircleMover + case "linear": + moverCtor = NewLinearMover + case "axis": + moverCtor = NewAxisMover + case "exgon": + moverCtor = NewExGonMover + case "aggressive": + moverCtor = NewAggressiveMover + case "momentum": + moverCtor = NewMomentumMover + case "pippi": + moverCtor = NewPippiMover + default: + moverCtor = NewAngleOffsetMover + finalName = "flower" + } + + return +} diff --git a/app/dance/pcontroller.go b/app/dance/pcontroller.go index 51e10dc7..3598bf3b 100644 --- a/app/dance/pcontroller.go +++ b/app/dance/pcontroller.go @@ -15,6 +15,7 @@ import ( "github.com/wieku/danser-go/app/settings" "github.com/wieku/danser-go/app/utils" "github.com/wieku/danser-go/framework/math/vector" + "github.com/wieku/danser-go/framework/platform" "log" "strings" "time" @@ -34,8 +35,8 @@ type PlayerController struct { previousPos vector.Vector2f position vector.Vector2f - rawInput bool - inside bool + rawInput bool + inside bool quickRestart bool quickRestartTime float64 @@ -83,7 +84,7 @@ func (controller *PlayerController) KeyEvent(w *glfw.Window, key glfw.Key, scanc return } - if strings.EqualFold(glfw.GetKeyName(key, scancode), settings.Input.LeftKey) { + if strings.EqualFold(platform.GetKeyName(key, scancode), settings.Input.LeftKey) { if action == glfw.Press { controller.cursors[0].LeftKey = true } else if action == glfw.Release { @@ -91,7 +92,7 @@ func (controller *PlayerController) KeyEvent(w *glfw.Window, key glfw.Key, scanc } } - if strings.EqualFold(glfw.GetKeyName(key, scancode), settings.Input.RightKey) { + if strings.EqualFold(platform.GetKeyName(key, scancode), settings.Input.RightKey) { if action == glfw.Press { controller.cursors[0].RightKey = true } else if action == glfw.Release { @@ -99,7 +100,7 @@ func (controller *PlayerController) KeyEvent(w *glfw.Window, key glfw.Key, scanc } } - if strings.EqualFold(glfw.GetKeyName(key, scancode), settings.Input.RestartKey) { + if strings.EqualFold(platform.GetKeyName(key, scancode), settings.Input.RestartKey) { if action == glfw.Press { controller.quickRestartTime = controller.lastTime controller.quickRestart = true @@ -108,7 +109,7 @@ func (controller *PlayerController) KeyEvent(w *glfw.Window, key glfw.Key, scanc } } - if strings.EqualFold(glfw.GetKeyName(key, scancode), settings.Input.SmokeKey) { + if strings.EqualFold(platform.GetKeyName(key, scancode), settings.Input.SmokeKey) { if action == glfw.Press { controller.cursors[0].SmokeKey = true } else if action == glfw.Release { @@ -147,7 +148,7 @@ func (controller *PlayerController) Update(time float64, delta float64) { controller.relaxController.Update(time) } - if controller.quickRestart && time - controller.quickRestartTime > 500 { + if controller.quickRestart && time-controller.quickRestartTime > 500 { controller.quickRestart = false utils.QuickRestart() @@ -206,7 +207,7 @@ func (controller *PlayerController) updateRaw(mousePos vector.Vector2f) { if controller.inside && (controller.position.X < 0 || controller.position.X64() > settings.Graphics.GetWidthF() || - controller.position.Y < 0 || controller.position.Y64() > settings.Graphics.GetHeightF() || !hovered) { + controller.position.Y < 0 || controller.position.Y64() > settings.Graphics.GetHeightF() || !hovered) { controller.setRawStatus(false) } else if input2.Focused && hovered && !controller.inside { controller.setRawStatus(true) diff --git a/app/dance/rcontroller.go b/app/dance/rcontroller.go index bc363cbc..2bfe31cd 100644 --- a/app/dance/rcontroller.go +++ b/app/dance/rcontroller.go @@ -428,9 +428,9 @@ func (controller *ReplayController) updateMain(nTime float64) { } if int64(nTime) != c.lastTime { - controller.ruleset.UpdatePostFor(controller.cursors[i], int64(nTime), false) controller.ruleset.UpdateClickFor(controller.cursors[i], int64(nTime)) controller.ruleset.UpdateNormalFor(controller.cursors[i], int64(nTime), false) + controller.ruleset.UpdatePostFor(controller.cursors[i], int64(nTime), false) } c.lastTime = int64(nTime) diff --git a/app/dance/schedulers/generic.go b/app/dance/schedulers/generic.go index 3909d0ad..8eaafaa3 100644 --- a/app/dance/schedulers/generic.go +++ b/app/dance/schedulers/generic.go @@ -34,7 +34,7 @@ func (scheduler *GenericScheduler) Init(objs []objects.IHitObject, diff *difficu scheduler.queue = objs if initKeys { - scheduler.input = input.NewNaturalInputProcessor(objs, cursor) + scheduler.input = input.NewNaturalInputProcessor(objs, cursor, scheduler.mover) } scheduler.mover.Reset(diff, scheduler.id) @@ -101,25 +101,28 @@ func (scheduler *GenericScheduler) Update(time float64) { for i := 0; i < len(scheduler.queue); i++ { g := scheduler.queue[i] - if g.GetStartTime() > time { + gStartTime := scheduler.mover.GetObjectsStartTime(g) + gEndTime := scheduler.mover.GetObjectsEndTime(g) + + if gStartTime > time { break } - lastEndTime = math.Max(lastEndTime, g.GetEndTime()) + lastEndTime = math.Max(lastEndTime, gEndTime) - if scheduler.lastTime <= g.GetStartTime() || time <= g.GetEndTime() { - if scheduler.lastTime <= g.GetStartTime() { // brief movement lock for ExGon mover + if scheduler.lastTime <= gStartTime || time <= gEndTime { + if scheduler.lastTime <= gStartTime { // brief movement lock for ExGon mover useMover = false } scheduler.cursor.SetPos(scheduler.mover.GetObjectsPosition(time, g)) } - if time > g.GetEndTime() { + if time > gEndTime { upperLimit := len(scheduler.queue) for j := i; j < len(scheduler.queue); j++ { - if scheduler.queue[j].GetEndTime() >= lastEndTime { + if scheduler.mover.GetObjectsEndTime(scheduler.queue[j]) >= lastEndTime { break } diff --git a/app/dance/spinners/circle.go b/app/dance/spinners/circle.go index 698f2676..de25a475 100644 --- a/app/dance/spinners/circle.go +++ b/app/dance/spinners/circle.go @@ -21,6 +21,6 @@ func (c *CircleMover) Init(start, _ float64, id int) { } func (c *CircleMover) GetPositionAt(time float64) vector.Vector2f { - radius := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)].Radius - return vector.NewVec2fRad(rpms*float32(time-c.start)*2*math32.Pi, float32(radius)).Add(center) + spS := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)] + return vector.NewVec2fRad(rpms*float32(time-c.start)*2*math32.Pi, float32(spS.Radius)).Add(center.AddS(float32(spS.CenterOffsetX), float32(spS.CenterOffsetY))) } diff --git a/app/dance/spinners/cube.go b/app/dance/spinners/cube.go index 4b3bd8be..13c83917 100644 --- a/app/dance/spinners/cube.go +++ b/app/dance/spinners/cube.go @@ -36,12 +36,12 @@ func (c *CubeMover) Init(start, _ float64, id int) { } func (c *CubeMover) GetPositionAt(time float64) vector.Vector2f { - radius := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)].Radius + spS := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)] radY := math32.Sin(float32(time-c.start)/9000*2*math32.Pi) * 3.0 / 18 * math32.Pi radX := math32.Sin(float32(time-c.start)/5000*2*math32.Pi) * 3.0 / 18 * math32.Pi - scale := (1.0 + math32.Sin(float32(time-c.start)/4500*2*math32.Pi)*0.3) * float32(radius) + scale := (1.0 + math32.Sin(float32(time-c.start)/4500*2*math32.Pi)*0.3) * float32(spS.Radius) mat := mgl32.HomogRotate3DY(radY).Mul4(mgl32.HomogRotate3DX(radX)).Mul4(mgl32.Scale3D(scale, scale, scale)) @@ -66,5 +66,5 @@ func (c *CubeMover) GetPositionAt(time float64) vector.Vector2f { pt[0] *= 1 + pt[2]/scale/10 pt[1] *= 1 + pt[2]/scale/10 - return vector.NewVec2f(pt.X(), pt.Y()).Add(center) + return vector.NewVec2f(pt.X(), pt.Y()).Add(center.AddS(float32(spS.CenterOffsetX), float32(spS.CenterOffsetY))) } diff --git a/app/dance/spinners/heart.go b/app/dance/spinners/heart.go index 7138ac5b..724c444d 100644 --- a/app/dance/spinners/heart.go +++ b/app/dance/spinners/heart.go @@ -21,10 +21,10 @@ func (c *HeartMover) Init(start, _ float64, id int) { } func (c *HeartMover) GetPositionAt(time float64) vector.Vector2f { - radius := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)].Radius + spS := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)] rad := rpms * float32(time-c.start) * 2 * math32.Pi x := math32.Pow(math32.Sin(rad), 3) y := (13*math32.Cos(rad) - 5*math32.Cos(2*rad) - 2*math32.Cos(3*rad) - math32.Cos(4*rad)) / 16 - return vector.NewVec2f(x, y).Mult(vector.NewVec2f(float32(radius), -float32(radius))).Add(center) + return vector.NewVec2f(x, y).Mult(vector.NewVec2f(float32(spS.Radius), -float32(spS.Radius))).Add(center.AddS(float32(spS.CenterOffsetX), float32(spS.CenterOffsetY))) } diff --git a/app/dance/spinners/square.go b/app/dance/spinners/square.go index 6e879864..10e6d522 100644 --- a/app/dance/spinners/square.go +++ b/app/dance/spinners/square.go @@ -25,9 +25,9 @@ func (c *SquareMover) Init(start, _ float64, id int) { } func (c *SquareMover) GetPositionAt(time float64) vector.Vector2f { - radius := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)].Radius + spS := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)] - mat := mgl32.Rotate3DZ(float32(time-c.start) / 2000 * 2 * math32.Pi).Mul3(mgl32.Scale2D(float32(radius), float32(radius))) + mat := mgl32.Rotate3DZ(float32(time-c.start) / 2000 * 2 * math32.Pi).Mul3(mgl32.Scale2D(float32(spS.Radius), float32(spS.Radius))) startIndex := (int64(math.Max(0, time-c.start)) / 10) % 4 @@ -43,5 +43,5 @@ func (c *SquareMover) GetPositionAt(time float64) vector.Vector2f { t := float32(int64(time-c.start)%10) / 10 - return vector.NewVec2f((pt2.X()-pt1.X())*t+pt1.X(), (pt2.Y()-pt1.Y())*t+pt1.Y()).Add(center) + return vector.NewVec2f((pt2.X()-pt1.X())*t+pt1.X(), (pt2.Y()-pt1.Y())*t+pt1.Y()).Add(center.AddS(float32(spS.CenterOffsetX), float32(spS.CenterOffsetY))) } diff --git a/app/dance/spinners/triangle.go b/app/dance/spinners/triangle.go index 34919e5a..003d16e7 100644 --- a/app/dance/spinners/triangle.go +++ b/app/dance/spinners/triangle.go @@ -25,9 +25,9 @@ func (c *TriangleMover) Init(start, _ float64, id int) { } func (c *TriangleMover) GetPositionAt(time float64) vector.Vector2f { - radius := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)].Radius + spS := settings.CursorDance.Spinners[c.id%len(settings.CursorDance.Spinners)] - mat := mgl32.Rotate3DZ(float32(time-c.start) / 2000 * 2 * math32.Pi).Mul3(mgl32.Scale2D(float32(radius), float32(radius))) + mat := mgl32.Rotate3DZ(float32(time-c.start) / 2000 * 2 * math32.Pi).Mul3(mgl32.Scale2D(float32(spS.Radius), float32(spS.Radius))) startIndex := (int64(math.Max(0, time-c.start)) / 10) % 3 @@ -43,5 +43,5 @@ func (c *TriangleMover) GetPositionAt(time float64) vector.Vector2f { t := float32(int64(time-c.start)%10) / 10 - return vector.NewVec2f((pt2.X()-pt1.X())*t+pt1.X(), (pt2.Y()-pt1.Y())*t+pt1.Y()).Add(center) + return vector.NewVec2f((pt2.X()-pt1.X())*t+pt1.X(), (pt2.Y()-pt1.Y())*t+pt1.Y()).Add(center.AddS(float32(spS.CenterOffsetX), float32(spS.CenterOffsetY))) } diff --git a/app/rulesets/osu/ruleset.go b/app/rulesets/osu/ruleset.go index a3ffae23..f8ae5cdd 100644 --- a/app/rulesets/osu/ruleset.go +++ b/app/rulesets/osu/ruleset.go @@ -112,6 +112,7 @@ type subSet struct { recoveries int failed bool sdpfFail bool + forceFail bool } type hitListener func(cursor *graphics.Cursor, time int64, number int64, position vector.Vector2d, result HitResult, comboResult ComboResult, ppResults performance.PPv2Results, score int64) @@ -375,7 +376,7 @@ func (set *OsuRuleSet) UpdateClickFor(cursor *graphics.Cursor, time int64) { } } - if len(set.processed) > 0 { + if len(set.processed) > 0 && !set.cursors[cursor].failed { for i := 0; i < len(set.processed); i++ { g := set.processed[i] @@ -638,12 +639,12 @@ func (set *OsuRuleSet) CanBeHit(time int64, object HitObject, player *difficulty func (set *OsuRuleSet) failInternal(player *difficultyPlayer) { subSet := set.cursors[player.cursor] - if player.diff.CheckModActive(difficulty.NoFail | difficulty.Relax | difficulty.Relax2) { + if !subSet.forceFail && player.diff.CheckModActive(difficulty.NoFail | difficulty.Relax | difficulty.Relax2) { return } // EZ mod gives 2 additional lives - if subSet.recoveries > 0 && !subSet.sdpfFail { + if subSet.recoveries > 0 && !subSet.sdpfFail && !subSet.forceFail { subSet.hp.Increase(160, false) subSet.recoveries-- @@ -662,7 +663,7 @@ func (set *OsuRuleSet) PlayerStopped(cursor *graphics.Cursor, time int64) { subSet := set.cursors[cursor] if time < int64(set.beatMap.HitObjects[len(set.beatMap.HitObjects)-1].GetEndTime())+subSet.player.diff.Hit50+20 { - subSet.sdpfFail = true + subSet.forceFail = true subSet.hp.Increase(-10000, true) } } diff --git a/app/settings/audio.go b/app/settings/audio.go index 1f7bd355..96e96456 100644 --- a/app/settings/audio.go +++ b/app/settings/audio.go @@ -31,10 +31,10 @@ type audio struct { HitsoundPositionMultiplier float64 IgnoreBeatmapSamples bool `label:"Ignore beatmap hitsounds"` //= false IgnoreBeatmapSampleVolume bool `label:"Ignore hitsound volume changes"` //= false - PlayNightcoreSamples bool `label:"Play nightcore beats"` + PlayNightcoreSamples bool `label:"Play nightcore beats" liveedit:"false"` BeatScale float64 `min:"1.0" max:"2.0"` BeatUseTimingPoints bool `label:"Add metronome to Beat scale"` - NonWindows *nonWindows `json:"Linux/Unix" label:"Linux/Unix only"` + NonWindows *nonWindows `json:"Linux/Unix" label:"Linux/Unix only" liveedit:"false"` } type nonWindows struct { diff --git a/app/settings/dancenew.go b/app/settings/dancenew.go index 13e5365c..f7582d47 100644 --- a/app/settings/dancenew.go +++ b/app/settings/dancenew.go @@ -58,8 +58,11 @@ func (d *defaultsFactory) InitMover() *mover { } type spinner struct { - Mover string `combo:"heart,triangle,square,cube,circle"` - Radius float64 `max:"200" format:"%.0fo!px"` + Mover string `combo:"heart,triangle,square,cube,circle"` + centerOffset string `vector:"true" left:"CenterOffsetX" right:"CenterOffsetY"` + CenterOffsetX float64 `min:"-1000" max:"1000"` + CenterOffsetY float64 `min:"-1000" max:"1000"` + Radius float64 `max:"200" format:"%.0fo!px"` } func (d *defaultsFactory) InitSpinner() *spinner { @@ -72,10 +75,10 @@ func (d *defaultsFactory) InitSpinner() *spinner { type cursorDance struct { Movers []*mover `new:"InitMover"` Spinners []*spinner `new:"InitSpinner"` - ComboTag bool - Battle bool - DoSpinnersTogether bool - TAGSliderDance bool `label:"TAG slider dance"` + ComboTag bool `liveedit:"false"` + Battle bool `liveedit:"false"` + DoSpinnersTogether bool `liveedit:"false"` + TAGSliderDance bool `label:"TAG slider dance" liveedit:"false"` MoverSettings *moverSettings } diff --git a/app/settings/gameplay.go b/app/settings/gameplay.go index 73af833f..9e83f7db 100644 --- a/app/settings/gameplay.go +++ b/app/settings/gameplay.go @@ -110,14 +110,31 @@ func initGameplay() *gameplay { XPosition: 5, YPosition: 190, }, - Color: []*HSV{ - { - Hue: 0, - Saturation: 0, - Value: 1, - }, + Color300: &HSV{ + Hue: 0, + Saturation: 0, + Value: 1, + }, + Color100: &HSV{ + Hue: 0, + Saturation: 0, + Value: 1, + }, + Color50: &HSV{ + Hue: 0, + Saturation: 0, + Value: 1, + }, + ColorMiss: &HSV{ + Hue: 0, + Saturation: 0, + Value: 1, + }, + ColorSB: &HSV{ + Hue: 0, + Saturation: 0, + Value: 1, }, - Spacing: 48, FontScale: 1, Align: "Left", @@ -231,15 +248,15 @@ type gameplay struct { Mods *mods Boundaries *boundaries Underlay *underlay - HUDFont string `file:"Select HUD font" filter:"TrueType/OpenType Font (*.ttf, *.otf)|ttf,otf"` - ShowResultsScreen bool - ResultsScreenTime float64 `label:"Results screen duration" min:"1" max:"20" format:"%.1fs"` + HUDFont string `label:"Overlay (HUD) font" file:"Select HUD font" filter:"TrueType/OpenType Font (*.ttf, *.otf)|ttf,otf" tooltip:"Sets the font that will be used for PP/UR/hit counts" liveedit:"false"` + ShowResultsScreen bool `liveedit:"false"` + ResultsScreenTime float64 `label:"Results screen duration" min:"1" max:"20" format:"%.1fs" liveedit:"false"` ResultsUseLocalTimeZone bool `label:"Show PC's time zone instead of UTC"` ShowWarningArrows bool ShowHitLighting bool FlashlightDim float64 - PlayUsername string - UseLazerPP bool + PlayUsername string `liveedit:"false"` + UseLazerPP bool `liveedit:"false"` } type boundaries struct { @@ -325,7 +342,12 @@ type ppCounter struct { type hitCounter struct { *hudElementPosition - Color []*HSV `new:"InitHSV" label:"Color list"` + Color []*HSV `json:",omitempty" new:"InitHSV" label:"Color list" skip:"true"` + Color300 *HSV `label:"Color of 300s"` + Color100 *HSV `label:"Color of 100s"` + Color50 *HSV `label:"Color of 50s"` + ColorMiss *HSV `label:"Color of misses"` + ColorSB *HSV `label:"Color of slider breaks"` Spacing float64 `string:"true" min:"0" max:"1366"` FontScale float64 `min:"0.1" max:"5" scale:"100" format:"%.0f%%"` Align string `combo:"TopLeft,Top,TopRight,Left,Centre,Right,BottomLeft,Bottom,BottomRight"` @@ -337,8 +359,8 @@ type hitCounter struct { type scoreBoard struct { *hudElementOffset - ModsOnly bool - AlignRight bool + ModsOnly bool `label:"Show mod leaderboard"` + AlignRight bool `label:"Align to the right" label:"Simulates the second team of osu! multiplayer"` HideOthers bool ShowAvatars bool ExplosionScale float64 `min:"0.1" max:"2" scale:"100" format:"%.0f%%"` @@ -370,6 +392,6 @@ type strainGraph struct { } type underlay struct { - Path string `file:"Select underlay image" filter:"PNG file (*.png)|png"` - AboveHpBar bool + Path string `file:"Select underlay image" filter:"PNG file (*.png)|png" tooltip:"PNG file that will be used as HUD background (similar to custom HP bar backgrounds). It's scaled automatically to fit the screen vertically" liveedit:"false"` + AboveHpBar bool `label:"Show underlay above HP bar" tooltip:"Use this if HP bar background is large"` } diff --git a/app/settings/globals.go b/app/settings/globals.go index 4d7a3af2..12655b31 100644 --- a/app/settings/globals.go +++ b/app/settings/globals.go @@ -10,7 +10,7 @@ var END = math.Inf(1) var KNOCKOUT = false var KNOCKOUTREPLAYS []string = nil var PLAYERS = 1 -var DIVIDES = 2 +var DIVIDES = 1 var SPEED = 1.0 var PITCH = 1.0 var TAG = 1 diff --git a/app/settings/graphics.go b/app/settings/graphics.go index 2877d9b7..8f7e68bd 100644 --- a/app/settings/graphics.go +++ b/app/settings/graphics.go @@ -20,16 +20,16 @@ func initGraphics() *graphics { } type graphics struct { - fSize string `vector:"true" label:"Fullscreen resolution" left:"Width" right:"Height"` + fSize string `vector:"true" label:"Fullscreen resolution" left:"Width" right:"Height" liveedit:"false"` Width int64 `min:"1" max:"30720"` Height int64 `min:"1" max:"17280"` - wSize string `vector:"true" label:"Windowed resolution" left:"WindowWidth" right:"WindowHeight"` + wSize string `vector:"true" label:"Windowed resolution" left:"WindowWidth" right:"WindowHeight" liveedit:"false"` WindowWidth int64 `min:"1" max:"30720"` WindowHeight int64 `min:"1" max:"17280"` - Fullscreen bool //true - VSync bool `label:"Vertical Sync"` //false - FPSCap int64 `label:"FPS limit" min:"0" max:"1000"` //1000 - MSAA int32 `combo:"0|OFF,2|2x,4|4x,8|8x,16|16x"` //16 + Fullscreen bool `liveedit:"false"` + VSync bool `label:"Vertical Sync"` + FPSCap int64 `label:"Custom FPS limit" min:"1" max:"5000" combo:"0|OFF,-1|(Not) VSync,-2|2x VSync,-4|4x VSync,-8|8x VSync,custom" showif:"VSync=false"` + MSAA int32 `combo:"0|OFF,2|2x,4|4x,8|8x,16|16x"` ShowFPS bool Experimental *experimental } diff --git a/app/settings/input.go b/app/settings/input.go index c7c40e08..4ac03ec8 100644 --- a/app/settings/input.go +++ b/app/settings/input.go @@ -8,6 +8,7 @@ func initInput() *input { RightKey: "X", RestartKey: "`", SmokeKey: "C", + ScreenshotKey: "F2", MouseButtonsDisabled: true, MouseHighPrecision: false, MouseSensitivity: 1, @@ -15,10 +16,11 @@ func initInput() *input { } type input struct { - LeftKey string - RightKey string - RestartKey string - SmokeKey string + LeftKey string `key:"true"` + RightKey string `key:"true"` + RestartKey string `key:"true"` + SmokeKey string `key:"true"` + ScreenshotKey string `key:"true"` MouseButtonsDisabled bool `label:"Disable mouse buttons"` MouseHighPrecision bool `label:"Mouse raw input"` MouseSensitivity float64 `label:"Raw input sensitivity" min:"0.4" max:"6"` diff --git a/app/settings/knockout.go b/app/settings/knockout.go index 5a93926d..532175dc 100644 --- a/app/settings/knockout.go +++ b/app/settings/knockout.go @@ -23,7 +23,7 @@ func initKnockout() *knockout { type knockout struct { // Knockout mode. More info below - Mode KnockoutMode `combo:"0|Combo Break,1|Max Combo,2|Replay Showcase,3|Vs Mode,4|SS or Quit"` + Mode KnockoutMode `combo:"0|Combo Break,1|Max Combo,2|Replay Showcase,3|Vs Mode,4|SS or Quit" liveedit:"false"` // In Mode = ComboBreak it won't knock out the player if they break combo before GraceEndTime (in seconds) GraceEndTime float64 `string:"true" min:"-10" max:"1000000" showif:"Mode=0"` @@ -32,10 +32,10 @@ type knockout struct { BubbleMinimumCombo int `label:"Minimum combo to show break bubble" string:"true" min:"1" max:"1000000" showif:"Mode=2"` // Exclude plays which contain one of the mods set here - ExcludeMods string `label:"Excluded mods (legacy)" tooltip:"Applicable only to classic knockout"` + ExcludeMods string `label:"Excluded mods (legacy)" tooltip:"Applicable only to classic knockout" liveedit:"false"` // Hide specific mods from being displayed in overlay (like NF) - HideMods string + HideMods string `liveedit:"false"` // Max players shown (excluding danser) on a map. Caps at 50. MaxPlayers int `label:"Max players loaded (legacy)" string:"true" min:"0" max:"100" tooltip:"Applicable only to classic knockout"` @@ -62,8 +62,8 @@ type knockout struct { MaxCursorSize float64 `min:"1" max:"20"` // Self explanatory - AddDanser bool - DanserName string `label:"Danser's name" tooltip:"It's also used in danser replay mode"` + AddDanser bool `liveedit:"false"` + DanserName string `label:"Danser's name" tooltip:"It's also used in danser replay mode" liveedit:"false"` } type KnockoutMode int diff --git a/app/settings/objects.go b/app/settings/objects.go index 1f9fc77c..d5deec42 100644 --- a/app/settings/objects.go +++ b/app/settings/objects.go @@ -100,9 +100,9 @@ type objects struct { DrawApproachCircles bool //true DrawComboNumbers bool DrawFollowPoints bool - LoadSpinners bool + LoadSpinners bool `liveedit:"false"` ScaleToTheBeat bool //true, objects size is changing with music peak amplitude - StackEnabled bool `label:"Enable stack leniency"` //true, stack leniency + StackEnabled bool `label:"Enable stack leniency" liveedit:"false"` //true, stack leniency Sliders *sliders Colors *objectcolors } @@ -113,7 +113,7 @@ type sliders struct { DrawSliderFollowCircle bool DrawScorePoints bool //true SliderMerge bool - SliderDistortions bool //true, osu!stable slider distortions on aspire maps + SliderDistortions bool `liveedit:"false"` //true, osu!stable slider distortions on aspire maps BorderWidth float64 `max:"9"` Quality *quality Snaking *snaking diff --git a/app/settings/playfield.go b/app/settings/playfield.go index 350000b4..c5eca5b6 100644 --- a/app/settings/playfield.go +++ b/app/settings/playfield.go @@ -11,6 +11,7 @@ func initPlayfield() *playfield { ShiftY: 0, ShiftX: 0, ScaleStoryboardWithPlayfield: false, + MoveStoryboardWithPlayfield: false, LeadInTime: 5, LeadInHold: 2, FadeOutTime: 5, @@ -73,16 +74,17 @@ func initPlayfield() *playfield { type playfield struct { DrawObjects bool DrawCursors bool - Scale float64 `label:"Playfield scale" min:"0.1" max:"2"` //1, scale the playfield (1 means that 384 will be rescaled to 900 on FullHD monitor) - OsuShift bool `label:"Position the playfield like in osu!"` //false, offset the playfield like in osu! | Overrides ShiftY - playfieldShift string `vector:"true" label:"Playfield shift" left:"ShiftX" right:"ShiftY"` - ShiftX float64 `min:"-512" max:"512"` //offset the playfield by X osu!pixels - ShiftY float64 `min:"-512" max:"512"` //offset the playfield by Y osu!pixels - ScaleStoryboardWithPlayfield bool - LeadInTime float64 `max:"10" format:"%.1fs"` //5 - LeadInHold float64 `max:"10" format:"%.1fs"` //2 - FadeOutTime float64 `max:"10" format:"%.1fs"` //5 - SeizureWarning *seizure + Scale float64 `label:"Playfield scale" min:"0.1" max:"2" liveedit:"false"` //1, scale the playfield (1 means that 384 will be rescaled to 900 on FullHD monitor) + OsuShift bool `label:"Position the playfield like in osu!" liveedit:"false"` //false, offset the playfield like in osu! | Overrides ShiftY + playfieldShift string `vector:"true" label:"Playfield shift" left:"ShiftX" right:"ShiftY" showif:"OsuShift=false" liveedit:"false"` + ShiftX float64 `min:"-512" max:"512"` //offset the playfield by X osu!pixels + ShiftY float64 `min:"-512" max:"512"` //offset the playfield by Y osu!pixels + ScaleStoryboardWithPlayfield bool `liveedit:"false"` + MoveStoryboardWithPlayfield bool `tooltip:"Even if selected, \"Position the playfield like in osu!\" option won't affect the storyboard" liveedit:"false"` + LeadInTime float64 `max:"10" format:"%.1fs" liveedit:"false"` //5 + LeadInHold float64 `max:"10" format:"%.1fs" liveedit:"false"` //2 + FadeOutTime float64 `max:"10" format:"%.1fs" liveedit:"false"` //5 + SeizureWarning *seizure `liveedit:"false"` Background *background Logo *logo Bloom *bloom @@ -98,15 +100,15 @@ type seizure struct { // Background controls type background struct { // Whether storyboards should be loaded - LoadStoryboards bool + LoadStoryboards bool `liveedit:"false"` // Whether videos should be loaded - LoadVideos bool + LoadVideos bool `liveedit:"false"` FlashToTheBeat bool // Dim controls - Dim *dim + Dim *dim `liveedit:"false"` Parallax *parallax @@ -130,7 +132,7 @@ type parallax struct { type blur struct { Enabled bool - Values *dim2 + Values *dim2 `liveedit:"false"` } type triangles struct { @@ -146,7 +148,7 @@ type triangles struct { type logo struct { Enabled bool DrawSpectrum bool `label:"Draw spectrum analyzer"` - Dim *dim + Dim *dim `liveedit:"false"` } type dim struct { diff --git a/app/settings/recording.go b/app/settings/recording.go index 4717d749..9bea0c26 100644 --- a/app/settings/recording.go +++ b/app/settings/recording.go @@ -109,9 +109,9 @@ type recording struct { resolution string `vector:"true" left:"FrameWidth" right:"FrameHeight"` FrameWidth int `min:"1" max:"30720"` FrameHeight int `min:"1" max:"17280"` - FPS int `string:"true" min:"1" max:"10727"` - EncodingFPSCap int `string:"true" min:"0" max:"10727" label:"Max Encoding FPS (Speed)"` - Encoder string `combo:"libx264|Software x264 (AVC),libx265|Software x265 (HEVC),h264_nvenc|NVIDIA NVENC H.264 (AVC),hevc_nvenc|NVIDIA NVENC H.265 (HEVC),h264_qsv|Intel QuickSync H.264 (AVC),hevc_qsv|Intel QuickSync H.265 (HEVC),libvpx-vp9|VP9"` + FPS int `label:"FPS (PLEASE READ TOOLTIP)" string:"true" min:"1" max:"10727" tooltip:"IMPORTANT: If you plan to have a \"high fps\" video, use Motion Blur below instead of setting FPS to absurd numbers. Setting the value too high will result in a broken video!"` + EncodingFPSCap int `string:"true" min:"0" max:"10727" label:"Max Encoding FPS (Speed)" tooltip:"Limits the speed at which danser renders the video. If FPS is set to 60 and this option to 30, then it means 2 minute map will take at least 4 minutes to render"` + Encoder string `combo:"libx264|Software x264 (AVC),libx265|Software x265 (HEVC),h264_nvenc|NVIDIA NVENC H.264 (AVC),hevc_nvenc|NVIDIA NVENC H.265 (HEVC),h264_qsv|Intel QuickSync H.264 (AVC),hevc_qsv|Intel QuickSync H.265 (HEVC)" tooltip:"Even if AMD cards have their own hardware encoder, you will still get better results with software encoders"` X264Settings *x264Settings `json:"libx264" label:"Software x264 (AVC) Settings" showif:"Encoder=libx264"` X265Settings *x265Settings `json:"libx265" label:"Software x265 (HEVC) Settings" showif:"Encoder=libx265"` H264NvencSettings *h264NvencSettings `json:"h264_nvenc" label:"NVIDIA NVENC H.264 (AVC) Settings" showif:"Encoder=h264_nvenc"` @@ -130,7 +130,7 @@ type recording struct { //AudioOptions string `label:"Audio Encoder Options"` AudioFilters string `label:"FFmpeg Audio Filters"` OutputDir string `path:"Select video output directory"` - Container string `combo:"mp4,mkv,webm"` + Container string `combo:"mp4,mkv"` ShowFFmpegLogs bool MotionBlur *motionblur @@ -188,13 +188,13 @@ func (g *recording) GetOutputDir() string { type motionblur struct { Enabled bool OversampleMultiplier int `string:"true" min:"1" max:"512"` - BlendFrames int `string:"true" min:"1" max:"512"` + BlendFrames int `string:"true" min:"1" max:"512" tooltip:"How many frames should be blended together.\nValue 1.5x bigger than Oversample multiplier is recommended"` BlendWeights *blendWeights } type blendWeights struct { UseManualWeights bool - ManualWeights string + ManualWeights string `showif:"UseManualWeights=true"` AutoWeightsID int `combo:"0|Flat,1|Linear,2|InQuad,3|OutQuad,4|InOutQuad,5|InCubic,6|OutCubic,7|InOutCubic,8|InQuart,9|OutQuart,10|InOutQuart,11|InQuint,12|OutQuint,13|InOutQuint,14|InSine,15|OutSine,16|InOutSine,17|InExpo,18|OutExpo,19|InOutExpo,20|InCirc,21|OutCirc,22|InOutCirc,23|InBack,24|OutBack,25|InOutBack,26|Gauss,27|GaussSymmetric,28|PyramidSymmetric,29|SemiCircle"` GaussWeightsMult float64 `string:"true" min:"0" max:"10"` } diff --git a/app/settings/settings.go b/app/settings/settings.go index fe824aab..b3cdf5b7 100644 --- a/app/settings/settings.go +++ b/app/settings/settings.go @@ -21,35 +21,35 @@ type Config struct { srcPath string srcData []byte - General *general `icon:"\uF0AD"` // wrench - Graphics *graphics `icon:"\uE163"` // display - Audio *audio `icon:"\uF028"` // volume-high - Input *input `icon:"\uF11C"` // keyboard - Gameplay *gameplay `icon:"\uF192"` // circle-dot - Skin *skin `icon:"\uF1FC"` // paintbrush - Cursor *cursor `icon:"\uF245"` // arrow-pointer - Objects *objects `icon:"\uF1E0"` // share-nodes - Playfield *playfield `icon:"\uF43C"` // chess-board + General *general `icon:"\uF0AD"` // wrench + Graphics *graphics `icon:"\uE163" liveedit:"false"` // display + Audio *audio `icon:"\uF028"` // volume-high + Input *input `icon:"\uF11C"` // keyboard + Gameplay *gameplay `icon:"\uF192"` // circle-dot + Skin *skin `icon:"\uF1FC"` // paintbrush + Cursor *cursor `icon:"\uF245"` // arrow-pointer + Objects *objects `icon:"\uF1E0"` // share-nodes + Playfield *playfield `icon:"\uF43C"` // chess-board + CursorDance *cursorDance `icon:"\uE599"` // worm + Knockout *knockout `icon:"\uF0CB"` // list-ol + Recording *recording `icon:"\uF03D"` // video Dance *danceOld `json:",omitempty" icon:"\uF5B7"` - CursorDance *cursorDance `icon:"\uE599"` // worm - Knockout *knockout `icon:"\uF0CB"` // list-ol - Recording *recording `icon:"\uF03D"` // video } type CombinedConfig struct { - Credentials *credentials `icon:"\uF084" label:"Credentials (Global)"` // key - General *general `icon:"\uF0AD"` // wrench - Graphics *graphics `icon:"\uE163"` // display - Audio *audio `icon:"\uF028"` // volume-high - Input *input `icon:"\uF11C"` // keyboard - Gameplay *gameplay `icon:"\uF192"` // circle-dot - Skin *skin `icon:"\uF1FC"` // paintbrush - Cursor *cursor `icon:"\uF245"` // arrow-pointer - Objects *objects `icon:"\uF1E0"` // share-nodes - Playfield *playfield `icon:"\uF43C"` // chess-board - CursorDance *cursorDance `icon:"\uE599"` // worm - Knockout *knockout `icon:"\uF0CB"` // list-ol - Recording *recording `icon:"\uF03D"` // video + Credentials *credentials `icon:"\uF084" label:"Credentials (Global)" liveedit:"false"` // key + General *general `icon:"\uF0AD" liveedit:"false"` // wrench + Graphics *graphics `icon:"\uE163"` // display + Audio *audio `icon:"\uF028"` // volume-high + Input *input `icon:"\uF11C"` // keyboard + Gameplay *gameplay `icon:"\uF192"` // circle-dot + Skin *skin `icon:"\uF1FC"` // paintbrush + Cursor *cursor `icon:"\uF245"` // arrow-pointer + Objects *objects `icon:"\uF1E0"` // share-nodes + Playfield *playfield `icon:"\uF43C"` // chess-board + CursorDance *cursorDance `icon:"\uE599"` // worm + Knockout *knockout `icon:"\uF0CB"` // list-ol + Recording *recording `icon:"\uF03D"` // video } func LoadConfig(file *os.File) (*Config, error) { @@ -79,6 +79,7 @@ func LoadConfig(file *os.File) (*Config, error) { } config.migrateCursorDance() + config.migrateHitCounterColors() if config.General.OsuReplaysDir == "" { // Set the replay directory if it hasn't been loaded config.General.OsuReplaysDir = filepath.Join(filepath.Dir(config.General.OsuSongsDir), "Replays") @@ -136,33 +137,76 @@ func (config *Config) migrateCursorDance() { config.CursorDance.DoSpinnersTogether = config.Dance.DoSpinnersTogether config.CursorDance.TAGSliderDance = config.Dance.TAGSliderDance - config.CursorDance.MoverSettings.Bezier = []*bezier{ - config.Dance.Bezier, + if config.Dance.Bezier != nil { + config.CursorDance.MoverSettings.Bezier = []*bezier{ + config.Dance.Bezier, + } } - config.CursorDance.MoverSettings.Flower = []*flower{ - config.Dance.Flower, + if config.Dance.Flower != nil { + config.CursorDance.MoverSettings.Flower = []*flower{ + config.Dance.Flower, + } } - config.CursorDance.MoverSettings.HalfCircle = []*circular{ - config.Dance.HalfCircle, + if config.Dance.HalfCircle != nil { + config.CursorDance.MoverSettings.HalfCircle = []*circular{ + config.Dance.HalfCircle, + } } - config.CursorDance.MoverSettings.Spline = []*spline{ - config.Dance.Spline, + if config.Dance.Spline != nil { + config.CursorDance.MoverSettings.Spline = []*spline{ + config.Dance.Spline, + } } - config.CursorDance.MoverSettings.Momentum = []*momentum{ - config.Dance.Momentum, + if config.Dance.Momentum != nil { + config.CursorDance.MoverSettings.Momentum = []*momentum{ + config.Dance.Momentum, + } } - config.CursorDance.MoverSettings.ExGon = []*exgon{ - config.Dance.ExGon, + if config.Dance.ExGon != nil { + config.CursorDance.MoverSettings.ExGon = []*exgon{ + config.Dance.ExGon, + } } config.Dance = nil } +func (config *Config) migrateHitCounterColors() { + if config.Gameplay.HitCounter.Color == nil { + return + } + + idx := 0 + + ln := len(config.Gameplay.HitCounter.Color) + + if config.Gameplay.HitCounter.Show300 { + config.Gameplay.HitCounter.Color300 = config.Gameplay.HitCounter.Color[idx%ln] + idx++ + } + + config.Gameplay.HitCounter.Color100 = config.Gameplay.HitCounter.Color[idx%ln] + idx++ + + config.Gameplay.HitCounter.Color50 = config.Gameplay.HitCounter.Color[idx%ln] + idx++ + + config.Gameplay.HitCounter.ColorMiss = config.Gameplay.HitCounter.Color[idx%ln] + idx++ + + if config.Gameplay.HitCounter.ShowSliderBreaks { + config.Gameplay.HitCounter.ColorSB = config.Gameplay.HitCounter.Color[idx%ln] + idx++ + } + + config.Gameplay.HitCounter.Color = nil +} + func (config *Config) attachToGlobals() { General = config.General Graphics = config.Graphics diff --git a/app/settings/skin.go b/app/settings/skin.go index dd03e729..35840226 100644 --- a/app/settings/skin.go +++ b/app/settings/skin.go @@ -27,8 +27,8 @@ func initSkin() *skin { } type skin struct { - CurrentSkin string `combo:"true" comboSrc:"SkinOptions" search:"true"` - FallbackSkin string `combo:"true" comboSrc:"SkinOptions" search:"true"` + CurrentSkin string `combo:"true" comboSrc:"SkinOptions" search:"true" liveedit:"false"` + FallbackSkin string `combo:"true" comboSrc:"SkinOptions" search:"true" liveedit:"false"` UseColorsFromSkin bool UseBeatmapColors bool diff --git a/app/skin/info.go b/app/skin/info.go index 14eb9b88..2cb6790a 100644 --- a/app/skin/info.go +++ b/app/skin/info.go @@ -141,6 +141,15 @@ func ParseFloat(text, errType string) float64 { return value } +func ParseBool(text, errType string) bool { + value, err := strconv.Atoi(text) + if err != nil { + panic(fmt.Sprintf("Error while parsing %s: %s", errType, text)) + } + + return value != 0 +} + func ParseColor(text, errType string) color.Color { clr := color.NewL(1) @@ -214,19 +223,19 @@ func LoadInfo(path string, local bool) (*SkinInfo, error) { case "AnimationFramerate": info.AnimationFramerate = ParseFloat(tokenized[1], tokenized[0]) case "SpinnerFadePlayfield": - info.SpinnerFadePlayfield = tokenized[1] == "1" + info.SpinnerFadePlayfield = ParseBool(tokenized[1], tokenized[0]) case "SpinnerNoBlink": - info.SpinnerNoBlink = tokenized[1] == "1" + info.SpinnerNoBlink = ParseBool(tokenized[1], tokenized[0]) case "SpinnerFrequencyModulate": - info.SpinnerFrequencyModulate = tokenized[1] == "1" + info.SpinnerFrequencyModulate = ParseBool(tokenized[1], tokenized[0]) case "LayeredHitSounds": - info.LayeredHitSounds = tokenized[1] == "1" + info.LayeredHitSounds = ParseBool(tokenized[1], tokenized[0]) case "CursorCentre": - info.CursorCentre = tokenized[1] == "1" + info.CursorCentre = ParseBool(tokenized[1], tokenized[0]) case "CursorExpand": - info.CursorExpand = tokenized[1] == "1" + info.CursorExpand = ParseBool(tokenized[1], tokenized[0]) case "CursorRotate": - info.CursorRotate = tokenized[1] == "1" + info.CursorRotate = ParseBool(tokenized[1], tokenized[0]) case "Combo1", "Combo2", "Combo3", "Combo4", "Combo5", "Combo6", "Combo7", "Combo8": index, _ := strconv.ParseInt(strings.TrimPrefix(tokenized[0], "Combo"), 10, 64) colorsI = append(colorsI, colorI{ @@ -234,11 +243,11 @@ func LoadInfo(path string, local bool) (*SkinInfo, error) { color: ParseColor(tokenized[1], tokenized[0]), }) case "DefaultSkinFollowpointBehavior": - info.DefaultSkinFollowpointBehavior = tokenized[1] == "1" + info.DefaultSkinFollowpointBehavior = ParseBool(tokenized[1], tokenized[0]) case "AllowSliderBallTint": - info.SliderBallTint = tokenized[1] == "1" + info.SliderBallTint = ParseBool(tokenized[1], tokenized[0]) case "SliderBallFlip": - info.SliderBallFlip = tokenized[1] == "1" + info.SliderBallFlip = ParseBool(tokenized[1], tokenized[0]) case "SliderBorder": info.SliderBorder = ParseColor(tokenized[1], tokenized[0]) case "SliderTrackOverride": @@ -258,7 +267,7 @@ func LoadInfo(path string, local bool) (*SkinInfo, error) { case "HitCircleOverlap": info.HitCircleOverlap = ParseFloat(tokenized[1], tokenized[0]) case "HitCircleOverlayAboveNumber", "HitCircleOverlayAboveNumer": - info.HitCircleOverlayAboveNumber = tokenized[1] == "1" + info.HitCircleOverlayAboveNumber = ParseBool(tokenized[1], tokenized[0]) case "ScorePrefix": info.ScorePrefix = tokenized[1] case "ScoreOverlap": diff --git a/app/states/components/common/background.go b/app/states/components/common/background.go index 7f855d70..dd8b3c8b 100644 --- a/app/states/components/common/background.go +++ b/app/states/components/common/background.go @@ -10,6 +10,7 @@ import ( "github.com/wieku/danser-go/app/storyboard" "github.com/wieku/danser-go/framework/assets" "github.com/wieku/danser-go/framework/bass" + "github.com/wieku/danser-go/framework/goroutines" "github.com/wieku/danser-go/framework/graphics/batch" "github.com/wieku/danser-go/framework/graphics/effects" "github.com/wieku/danser-go/framework/graphics/texture" @@ -25,18 +26,21 @@ import ( ) type Background struct { - blur *effects.BlurEffect - scale vector.Vector2d - position vector.Vector2d + lastTime float64 + + scaling scaling.Scaling + background *texture.TextureSingle storyboard *storyboard.Storyboard - lastTime float64 - //bMap *beatmap.BeatMap - triangles *drawables.Triangles + triangles *drawables.Triangles + + blur *effects.BlurEffect blurVal float64 blurredTexture texture.Texture - scaling scaling.Scaling forceRedraw bool + + parallaxPosition vector.Vector2d + parallaxScale float64 } func NewBackground(loadDefault bool) *Background { @@ -61,6 +65,12 @@ func NewBackground(loadDefault bool) *Background { bg.triangles = drawables.NewTriangles(nil) } + parallax := settings.Playfield.Background.Parallax + + if parallax.Enabled && settings.DIVIDES == 1 && math.Abs(parallax.Amount) > 0.0001 { // avoid scaling the background during fade in + bg.parallaxScale = math.Abs(parallax.Amount) + } + return bg } @@ -94,7 +104,7 @@ func (bg *Background) SetBeatmap(beatMap *beatmap.BeatMap, loadDefault, loadStor if settings.RECORD { bgLoadFunc() } else { - go bgLoadFunc() + goroutines.Run(bgLoadFunc) } if loadStoryboards { @@ -135,17 +145,24 @@ func (bg *Background) Update(time float64, x, y float64) { parallax := settings.Playfield.Background.Parallax - if parallax.Enabled && math.Abs(parallax.Amount) > 0.0001 && !math.IsNaN(x) && !math.IsNaN(y) && settings.DIVIDES == 1 { - pX = mutils.ClampF(x, -1, 1) * parallax.Amount - pY = mutils.ClampF(y, -1, 1) * parallax.Amount + parallaxTarget := 0.0 + + if parallax.Enabled && settings.DIVIDES == 1 && math.Abs(parallax.Amount) > 0.0001 { + parallaxTarget = math.Abs(parallax.Amount) + + if !math.IsNaN(x) && !math.IsNaN(y) { + pX = mutils.ClampF(x, -1, 1) * parallax.Amount + pY = mutils.ClampF(y, -1, 1) * parallax.Amount + } } delta := math.Abs(time - bg.lastTime) p := math.Pow(1-parallax.Speed, delta/100) - bg.position.X = pX*(1-p) + p*bg.position.X - bg.position.Y = pY*(1-p) + p*bg.position.Y + bg.parallaxPosition.X = pX*(1-p) + p*bg.parallaxPosition.X + bg.parallaxPosition.Y = pY*(1-p) + p*bg.parallaxPosition.Y + bg.parallaxScale = parallaxTarget*(1-p) + p*bg.parallaxScale bg.lastTime = time } @@ -214,8 +231,8 @@ func (bg *Background) Draw(time float64, batch *batch.QuadBatch, blurVal, bgAlph size := bg.scaling.Apply(float32(bg.background.GetWidth()), float32(bg.background.GetHeight()), float32(settings.Graphics.GetWidthF()), float32(settings.Graphics.GetHeightF())).Scl(0.5) if !settings.Playfield.Background.Blur.Enabled { - batch.SetTranslation(bg.position.Mult(vector.NewVec2d(1, -1)).Mult(vector.NewVec2d(settings.Graphics.GetSizeF()).Scl(0.5))) - size = size.Scl(float32(1 + math.Abs(settings.Playfield.Background.Parallax.Amount))) + batch.SetTranslation(bg.parallaxPosition.Mult(vector.NewVec2d(1, -1)).Mult(vector.NewVec2d(settings.Graphics.GetSizeF()).Scl(0.5))) + size = size.Scl(float32(1 + bg.parallaxScale)) } batch.SetScale(size.X64(), size.Y64()) @@ -234,8 +251,8 @@ func (bg *Background) Draw(time float64, batch *batch.QuadBatch, blurVal, bgAlph cam := camera if !settings.Playfield.Background.Blur.Enabled { - scale := float32(1 + math.Abs(settings.Playfield.Background.Parallax.Amount)) - cam = mgl32.Translate3D(bg.position.X32(), bg.position.Y32(), 0).Mul4(mgl32.Scale3D(scale, scale, 1)).Mul4(cam) + scale := float32(1 + bg.parallaxScale) + cam = mgl32.Translate3D(bg.parallaxPosition.X32(), bg.parallaxPosition.Y32(), 0).Mul4(mgl32.Scale3D(scale, scale, 1)).Mul4(cam) } batch.SetCamera(cam) @@ -269,8 +286,8 @@ func (bg *Background) Draw(time float64, batch *batch.QuadBatch, blurVal, bgAlph batch.SetAdditive(false) batch.SetColor(1, 1, 1, bgAlpha) batch.SetCamera(mgl32.Ortho(-1, 1, -1, 1, 1, -1)) - batch.SetTranslation(bg.position) - batch.SetScale(1+math.Abs(settings.Playfield.Background.Parallax.Amount), 1+math.Abs(settings.Playfield.Background.Parallax.Amount)) + batch.SetTranslation(bg.parallaxPosition) + batch.SetScale(1+bg.parallaxScale, 1+bg.parallaxScale) batch.DrawUnit(bg.blurredTexture.GetRegion()) batch.Flush() batch.SetColor(1, 1, 1, 1) @@ -296,8 +313,8 @@ func (bg *Background) drawTriangles(batch *batch.QuadBatch, bgAlpha float64, blu batch.SetColor(bgAlpha, bgAlpha, bgAlpha, 1) subScale := float32(settings.Playfield.Background.Triangles.ParallaxMultiplier) - scale := 1 + math32.Abs(float32(settings.Playfield.Background.Parallax.Amount))*math32.Abs(subScale) - cam = mgl32.Translate3D(bg.position.X32()*subScale, bg.position.Y32()*subScale, 0).Mul4(mgl32.Scale3D(scale, scale, 1)).Mul4(cam) + scale := 1 + float32(bg.parallaxScale)*math32.Abs(subScale) + cam = mgl32.Translate3D(bg.parallaxPosition.X32()*subScale, bg.parallaxPosition.Y32()*subScale, 0).Mul4(mgl32.Scale3D(scale, scale, 1)).Mul4(cam) } else { batch.SetColor(1, 1, 1, 1) } @@ -327,8 +344,8 @@ func (bg *Background) DrawOverlay(time float64, batch *batch.QuadBatch, bgAlpha batch.ResetTransform() batch.SetAdditive(false) - scale := float32(1 + math.Abs(settings.Playfield.Background.Parallax.Amount)) - cam := mgl32.Translate3D(bg.position.X32(), -bg.position.Y32(), 0).Mul4(mgl32.Scale3D(scale, scale, 1)).Mul4(camera) + scale := float32(1 + bg.parallaxScale) + cam := mgl32.Translate3D(bg.parallaxPosition.X32(), -bg.parallaxPosition.Y32(), 0).Mul4(mgl32.Scale3D(scale, scale, 1)).Mul4(camera) batch.SetCamera(cam) bg.storyboard.DrawOverlay(time, batch) diff --git a/app/states/components/common/beatsynced.go b/app/states/components/common/beatsynced.go index 84fb5133..5cf51ead 100644 --- a/app/states/components/common/beatsynced.go +++ b/app/states/components/common/beatsynced.go @@ -6,6 +6,7 @@ import ( "github.com/wieku/danser-go/framework/bass" "github.com/wieku/danser-go/framework/graphics/sprite" "github.com/wieku/danser-go/framework/math/mutils" + "github.com/wieku/danser-go/framework/math/vector" "math" ) @@ -36,7 +37,7 @@ type BeatSynced struct { func NewBeatSynced() *BeatSynced { return &BeatSynced{ - Sprite: &sprite.Sprite{}, + Sprite: sprite.NewSpriteSingle(nil, 0, vector.NewVec2d(0, 0), vector.Centre), lastTime: math.NaN(), Divisor: 1, } diff --git a/app/states/components/containers/objects.go b/app/states/components/containers/objects.go index bd3131b3..c39f9f4b 100644 --- a/app/states/components/containers/objects.go +++ b/app/states/components/containers/objects.go @@ -173,7 +173,7 @@ func (container *HitObjectContainer) preProcessQueue(time float64) { } } -func (container *HitObjectContainer) Draw(batch *batch.QuadBatch, cameras []mgl32.Mat4, time float64, scale, alpha float32) { +func (container *HitObjectContainer) Draw(batch *batch.QuadBatch, baseCamera mgl32.Mat4, cameras []mgl32.Mat4, time float64, scale, alpha float32) { divides := len(cameras) container.preProcessQueue(time) @@ -213,7 +213,7 @@ func (container *HitObjectContainer) Draw(batch *batch.QuadBatch, cameras []mgl3 for i := len(container.renderables) - 1; i >= 0; i-- { if s, ok := container.renderables[i].renderable.(*objects.Slider); ok && container.renderables[i].isSliderBody { - s.DrawBodyBase(time, cameras[0]) + s.DrawBodyBase(time, baseCamera) } } diff --git a/app/states/components/overlays/play/hitdisplay.go b/app/states/components/overlays/play/hitdisplay.go index ac21efb2..5805cb17 100644 --- a/app/states/components/overlays/play/hitdisplay.go +++ b/app/states/components/overlays/play/hitdisplay.go @@ -77,68 +77,66 @@ func (sprite *HitDisplay) Update(_ float64) { } func (sprite *HitDisplay) Draw(batch *batch.QuadBatch, alpha float64) { - if !settings.Gameplay.HitCounter.Show || settings.Gameplay.HitCounter.Opacity*alpha < 0.01 { + hCS := settings.Gameplay.HitCounter + + if !hCS.Show || hCS.Opacity*alpha < 0.01 { return } batch.ResetTransform() - alpha *= settings.Gameplay.HitCounter.Opacity - scale := settings.Gameplay.HitCounter.Scale - hSpacing := settings.Gameplay.HitCounter.Spacing * scale + alpha *= hCS.Opacity + scale := hCS.Scale + hSpacing := hCS.Spacing * scale vSpacing := 0.0 - if settings.Gameplay.HitCounter.Vertical { + if hCS.Vertical { vSpacing = hSpacing hSpacing = 0 } - fontScale := scale * settings.Gameplay.HitCounter.FontScale + fontScale := scale * hCS.FontScale - align := vector.ParseOrigin(settings.Gameplay.HitCounter.Align).AddS(1, 1).Scl(0.5) + align := vector.ParseOrigin(hCS.Align).AddS(1, 1).Scl(0.5) bC := 3.0 - if settings.Gameplay.HitCounter.Show300 { + if hCS.Show300 { bC += 1.0 } - if settings.Gameplay.HitCounter.ShowSliderBreaks { + if hCS.ShowSliderBreaks { bC += 1.0 } - valueAlign := vector.ParseOrigin(settings.Gameplay.HitCounter.ValueAlign) - - baseX := settings.Gameplay.HitCounter.XPosition - align.X*hSpacing*(bC-1) - baseY := settings.Gameplay.HitCounter.YPosition - align.Y*vSpacing*(bC-1) + valueAlign := vector.ParseOrigin(hCS.ValueAlign) - offsetI := 0 + baseX := hCS.XPosition - align.X*hSpacing*(bC-1) + baseY := hCS.YPosition - align.Y*vSpacing*(bC-1) - if settings.Gameplay.HitCounter.Show300 { - sprite.drawShadowed(batch, baseX, baseY, valueAlign, fontScale, 0, float32(alpha), sprite.hit300Text) + if hCS.Show300 { + sprite.drawShadowed(batch, baseX, baseY, valueAlign, fontScale, hCS.Color300, float32(alpha), sprite.hit300Text) - offsetI = 1 baseX += hSpacing baseY += vSpacing } - sprite.drawShadowed(batch, baseX, baseY, valueAlign, fontScale, offsetI, float32(alpha), sprite.hit100Text) - sprite.drawShadowed(batch, baseX+hSpacing, baseY+vSpacing, valueAlign, fontScale, offsetI+1, float32(alpha), sprite.hit50Text) - sprite.drawShadowed(batch, baseX+hSpacing*2, baseY+vSpacing*2, valueAlign, fontScale, offsetI+2, float32(alpha), sprite.hitMissText) + sprite.drawShadowed(batch, baseX, baseY, valueAlign, fontScale, hCS.Color100, float32(alpha), sprite.hit100Text) + sprite.drawShadowed(batch, baseX+hSpacing, baseY+vSpacing, valueAlign, fontScale, hCS.Color50, float32(alpha), sprite.hit50Text) + sprite.drawShadowed(batch, baseX+hSpacing*2, baseY+vSpacing*2, valueAlign, fontScale, hCS.ColorMiss, float32(alpha), sprite.hitMissText) - if settings.Gameplay.HitCounter.ShowSliderBreaks { - sprite.drawShadowed(batch, baseX+hSpacing*3, baseY+vSpacing*3, valueAlign, fontScale, offsetI+3, float32(alpha), sprite.sliderBreaksText) + if hCS.ShowSliderBreaks { + sprite.drawShadowed(batch, baseX+hSpacing*3, baseY+vSpacing*3, valueAlign, fontScale, hCS.ColorSB, float32(alpha), sprite.sliderBreaksText) } batch.ResetTransform() } -func (sprite *HitDisplay) drawShadowed(batch *batch.QuadBatch, x, y float64, origin vector.Vector2d, size float64, cI int, alpha float32, text string) { - cS := settings.Gameplay.HitCounter.Color[cI%len(settings.Gameplay.HitCounter.Color)] - color := color2.NewHSVA(float32(cS.Hue), float32(cS.Saturation), float32(cS.Value), alpha) +func (sprite *HitDisplay) drawShadowed(batch *batch.QuadBatch, x, y float64, origin vector.Vector2d, size float64, color *settings.HSV, alpha float32, text string) { + rgba := color2.NewHSVA(float32(color.Hue), float32(color.Saturation), float32(color.Value), alpha) - batch.SetColor(0, 0, 0, float64(color.A)*0.8) + batch.SetColor(0, 0, 0, float64(rgba.A)*0.8) sprite.fnt.DrawOrigin(batch, x+size, y+size, origin, 20*size, true, text) - batch.SetColorM(color) + batch.SetColorM(rgba) sprite.fnt.DrawOrigin(batch, x, y, origin, 20*size, true, text) } diff --git a/app/states/components/overlays/play/straingraph.go b/app/states/components/overlays/play/straingraph.go index 95d0b313..9e250d85 100644 --- a/app/states/components/overlays/play/straingraph.go +++ b/app/states/components/overlays/play/straingraph.go @@ -45,17 +45,14 @@ func NewStrainGraph(ruleset *osu.OsuRuleSet) *StrainGraph { screenWidth: 768 * settings.Graphics.GetAspectRatio(), } - graph.leftSprite = sprite.NewSpriteSingle(nil, 0, vector.NewVec2d(graph.screenWidth, 728), vector.BottomRight) + graph.leftSprite = sprite.NewSpriteSingle(nil, 0, vector.NewVec2d(graph.screenWidth, 728), vector.TopLeft) graph.leftSprite.SetColor(color.NewIRGB(231, 141, 235)) graph.leftSprite.SetCutOrigin(vector.CentreLeft) - graph.rightSprite = sprite.NewSpriteSingle(nil, 0, vector.NewVec2d(graph.screenWidth, 728), vector.BottomRight) + graph.rightSprite = sprite.NewSpriteSingle(nil, 0, vector.NewVec2d(graph.screenWidth, 728), vector.TopRight) graph.rightSprite.SetColor(color.NewL(0.2)) graph.rightSprite.SetCutOrigin(vector.CentreRight) - graph.leftSprite.SetScale(768 / settings.Graphics.GetHeightF()) - graph.rightSprite.SetScale(768 / settings.Graphics.GetHeightF()) - return graph } @@ -104,7 +101,7 @@ func (graph *StrainGraph) drawFBO(batch *batch.QuadBatch) { graph.fbo.Dispose() } - graph.fbo = buffer.NewFrameMultisample(int(w), int(h), 8) + graph.fbo = buffer.NewFrameMultisample(int(math.Round(w)), int(math.Round(h)), 8) graph.fbo.Bind() graph.fbo.ClearColor(1, 1, 1, 0) @@ -147,6 +144,10 @@ func (graph *StrainGraph) drawFBO(batch *batch.QuadBatch) { region := graph.fbo.Texture().GetRegion() + // Reestablish scaling using final FBO sizes because 768/screenHeight was causing 1px gaps in some scenarios + graph.leftSprite.SetScaleV(vector.NewVec2d(graph.size.X/float64(region.Width), graph.size.Y/float64(region.Height))) + graph.rightSprite.SetScaleV(vector.NewVec2d(graph.size.X/float64(region.Width), graph.size.Y/float64(region.Height))) + graph.leftSprite.Texture = ®ion graph.rightSprite.Texture = ®ion } @@ -168,14 +169,14 @@ func (graph *StrainGraph) Draw(batch *batch.QuadBatch, alpha float64) { batch.SetColor(1, 1, 1, sgAlpha) - origin := vector.ParseOrigin(conf.Align) - pos := vector.NewVec2d(conf.XPosition, conf.YPosition) + origin := vector.ParseOrigin(conf.Align).AddS(1, 1).Scl(0.5) + basePos := vector.NewVec2d(conf.XPosition, conf.YPosition) - graph.leftSprite.SetPosition(pos) - graph.rightSprite.SetPosition(pos) + pos1 := basePos.Sub(origin.Mult(graph.size)) + pos2 := pos1.AddS(graph.size.X, 0) - graph.leftSprite.SetOrigin(origin) - graph.rightSprite.SetOrigin(origin) + graph.leftSprite.SetPosition(pos1) + graph.rightSprite.SetPosition(pos2) graph.leftSprite.SetColor(color.NewHSV(float32(conf.FgColor.Hue), float32(conf.FgColor.Saturation), float32(conf.FgColor.Value))) graph.rightSprite.SetColor(color.NewHSV(float32(conf.BgColor.Hue), float32(conf.BgColor.Saturation), float32(conf.BgColor.Value))) diff --git a/app/states/components/overlays/scoreoverlay.go b/app/states/components/overlays/scoreoverlay.go index b4012707..fa973078 100644 --- a/app/states/components/overlays/scoreoverlay.go +++ b/app/states/components/overlays/scoreoverlay.go @@ -128,6 +128,7 @@ type ScoreOverlay struct { strainGraph *play.StrainGraph underlay *sprite.Sprite + failed bool } func loadFonts() { @@ -444,7 +445,7 @@ func (overlay *ScoreOverlay) updateNormal(time float64) { if overlay.panel != nil { overlay.panel.Update(time) - } else if settings.Gameplay.ShowResultsScreen && !overlay.created && overlay.audioTime >= overlay.beatmapEnd { + } else if !overlay.failed && settings.Gameplay.ShowResultsScreen && !overlay.created && overlay.audioTime >= overlay.beatmapEnd { overlay.created = true cTime := overlay.normalTime @@ -456,7 +457,10 @@ func (overlay *ScoreOverlay) updateNormal(time float64) { resultsTime := settings.Gameplay.ResultsScreenTime * 1000 overlay.resultsFade.AddEventS(s, s+500, 0, 1) - overlay.resultsFade.AddEventS(s+resultsTime+500, s+resultsTime+1000, 1, 0) + + if !settings.PLAY { + overlay.resultsFade.AddEventS(s+resultsTime+500, s+resultsTime+1000, 1, 0) + } } if settings.RECORD { @@ -496,7 +500,10 @@ func (overlay *ScoreOverlay) updateNormal(time float64) { overlay.ppDisplay.Update(time) overlay.hitCounts.Update(time) - currentStates := [4]bool{overlay.cursor.LeftKey, overlay.cursor.RightKey, overlay.cursor.LeftMouse && !overlay.cursor.LeftKey, overlay.cursor.RightMouse && !overlay.cursor.RightKey} + var currentStates [4]bool + if !overlay.failed { + currentStates = [4]bool{overlay.cursor.LeftKey, overlay.cursor.RightKey, overlay.cursor.LeftMouse && !overlay.cursor.LeftKey, overlay.cursor.RightMouse && !overlay.cursor.RightKey} + } for i, state := range currentStates { color := color2.Color{R: 1.0, G: 222.0 / 255, B: 0, A: 0} @@ -538,6 +545,10 @@ func (overlay *ScoreOverlay) updateNormal(time float64) { } func (overlay *ScoreOverlay) updateBreaks(time float64) { + if overlay.failed { + return + } + inBreak := false for _, b := range overlay.ruleset.GetBeatMap().Pauses { @@ -644,7 +655,7 @@ func (overlay *ScoreOverlay) DrawHUD(batch *batch.QuadBatch, _ []color2.Color, a batch.ResetTransform() } - if settings.Gameplay.ShowWarningArrows { + if !overlay.failed && settings.Gameplay.ShowWarningArrows { overlay.arrows.Draw(overlay.audioTime, batch) } @@ -1001,3 +1012,7 @@ func (overlay *ScoreOverlay) SetBeatmapEnd(end float64) { func (overlay *ScoreOverlay) ShouldDrawHUDBeforeCursor() bool { return true } + +func (overlay *ScoreOverlay) Fail(fail bool) { + overlay.failed = fail +} diff --git a/app/states/player.go b/app/states/player.go index 95410240..f74fb7e9 100644 --- a/app/states/player.go +++ b/app/states/player.go @@ -3,6 +3,7 @@ package states import ( "fmt" "github.com/go-gl/mathgl/mgl32" + "github.com/wieku/danser-go/app/audio" "github.com/wieku/danser-go/app/beatmap" "github.com/wieku/danser-go/app/beatmap/difficulty" camera2 "github.com/wieku/danser-go/app/bmath/camera" @@ -10,6 +11,7 @@ import ( "github.com/wieku/danser-go/app/discord" "github.com/wieku/danser-go/app/graphics" "github.com/wieku/danser-go/app/input" + "github.com/wieku/danser-go/app/rulesets/osu" "github.com/wieku/danser-go/app/settings" "github.com/wieku/danser-go/app/states/components/common" "github.com/wieku/danser-go/app/states/components/containers" @@ -32,6 +34,7 @@ import ( "github.com/wieku/danser-go/framework/statistic" "log" "math" + "math/rand" "path/filepath" "runtime" "strconv" @@ -64,9 +67,10 @@ type Player struct { profiler *frame.Counter profilerU *frame.Counter - mainCamera *camera2.Camera - bgCamera *camera2.Camera - uiCamera *camera2.Camera + mainCamera *camera2.Camera + objectCamera *camera2.Camera + bgCamera *camera2.Camera + uiCamera *camera2.Camera dimGlider *animation.Glider blurGlider *animation.Glider @@ -84,9 +88,10 @@ type Player struct { hudGlider *animation.Glider - volumeGlider *animation.Glider - speedGlider *animation.Glider - pitchGlider *animation.Glider + volumeGlider *animation.Glider + speedGlider *animation.Glider + pitchGlider *animation.Glider + frequencyGlider *animation.Glider startPoint float64 startPointE float64 @@ -108,6 +113,16 @@ type Player struct { ScaledHeight float64 nightcore *common.NightcoreProcessor + + realTime float64 + objectsAlphaFail *animation.Glider + failOX *animation.Glider + failOY *animation.Glider + failRotation *animation.Glider + + failing bool + failAt float64 + failed bool } func NewPlayer(beatMap *beatmap.BeatMap) *Player { @@ -192,9 +207,13 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { player.background.SetBeatmap(beatMap, true, true) player.mainCamera = camera2.NewCamera() - player.mainCamera.SetOsuViewport(int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()), settings.Playfield.Scale, settings.Playfield.OsuShift) + player.mainCamera.SetOsuViewport(int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()), settings.Playfield.Scale, true, settings.Playfield.OsuShift) player.mainCamera.Update() + player.objectCamera = camera2.NewCamera() + player.objectCamera.SetOsuViewport(int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()), settings.Playfield.Scale, true, settings.Playfield.OsuShift) + player.objectCamera.Update() + player.bgCamera = camera2.NewCamera() sbScale := 1.0 @@ -202,7 +221,7 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { sbScale = settings.Playfield.Scale } - player.bgCamera.SetOsuViewport(int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()), sbScale, false) + player.bgCamera.SetOsuViewport(int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()), sbScale, !settings.Playfield.OsuShift && settings.Playfield.MoveStoryboardWithPlayfield, false) player.bgCamera.Update() player.ScaledHeight = 1080.0 @@ -252,6 +271,7 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { player.volumeGlider = animation.NewGlider(1) player.speedGlider = animation.NewGlider(settings.SPEED) player.pitchGlider = animation.NewGlider(settings.PITCH) + player.frequencyGlider = animation.NewGlider(1) player.hudGlider = animation.NewGlider(0) player.hudGlider.SetEasing(easing.OutQuad) @@ -267,9 +287,12 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { player.epiGlider = animation.NewGlider(0) player.objectsAlpha = animation.NewGlider(1) - if _, ok := player.overlay.(*overlays.ScoreOverlay); ok && player.controller.GetCursors()[0].IsPlayer && !player.controller.GetCursors()[0].IsAutoplay { - player.cursorGlider.SetValue(1.0) - } + player.objectsAlphaFail = animation.NewGlider(1) + player.failOX = animation.NewGlider(0) + player.failOY = animation.NewGlider(0) + player.failRotation = animation.NewGlider(0) + + player.trySetupFail() preempt := math.Min(1800, beatMap.Diff.Preempt) @@ -451,7 +474,7 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { player.profilerU = frame.NewCounter() - player.baseLimit = 2000 + player.baseLimit = 1000 player.updateLimiter = frame.NewLimiter(player.baseLimit) @@ -504,7 +527,12 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { platformOffset = windowsOffset } - player.progressMsF = player.rawPositionF + (platformOffset+float64(settings.Audio.Offset)+float64(settings.LOCALOFFSET))*speed + oldOffset := 0.0 + if player.bMap.Version < 5 { + oldOffset = -24 + } + + player.progressMsF = player.rawPositionF + (platformOffset+float64(settings.Audio.Offset)+float64(settings.LOCALOFFSET))*speed + oldOffset player.updateMain(delta) @@ -520,6 +548,54 @@ func NewPlayer(beatMap *beatmap.BeatMap) *Player { return player } +func (player *Player) trySetupFail() { + if sO, ok := player.overlay.(*overlays.ScoreOverlay); ok { + var ruleset *osu.OsuRuleSet + + if rC, ok1 := player.controller.(*dance.ReplayController); ok1 { + ruleset = rC.GetRuleset() + } else if rP, ok2 := player.controller.(*dance.PlayerController); ok2 { + ruleset = rP.GetRuleset() + } + + if ruleset != nil { + ruleset.SetFailListener(func(cursor *graphics.Cursor) { + if !settings.RECORD { + audio.PlayFailSound() + } + + log.Println("Player failed!") + + sO.Fail(true) + + player.frequencyGlider.AddEvent(player.realTime, player.realTime+2400, 0.0) + player.objectsAlphaFail.AddEvent(player.realTime, player.realTime+2400, 0.0) + + player.failOX.AddEvent(player.realTime, player.realTime+2400, camera2.OsuWidth*(rand.Float64()-0.5)/2) + player.failOY.AddEvent(player.realTime, player.realTime+2400, -camera2.OsuHeight*(1+rand.Float64()*0.2)) + + rotBase := rand.Float64() + + player.failRotation.AddEvent(player.realTime, player.realTime+2400, math.Copysign((math.Abs(rotBase)*0.5+0.5)/6*math.Pi, rotBase)) + + player.failing = true + player.failAt = player.realTime + 2400 + + player.dimGlider.Reset() + player.blurGlider.Reset() + player.hudGlider.Reset() + player.fxGlider.Reset() + player.cursorGlider.Reset() + player.objectsAlpha.Reset() + }) + } + + if player.controller.GetCursors()[0].IsPlayer && !player.controller.GetCursors()[0].IsAutoplay { + player.cursorGlider.SetValue(1.0) + } + } +} + func (player *Player) Update(delta float64) bool { speed := 1.0 @@ -531,7 +607,12 @@ func (player *Player) Update(delta float64) bool { player.rawPositionF += delta * speed - player.progressMsF = player.rawPositionF + float64(settings.LOCALOFFSET)*speed + oldOffset := 0.0 + if player.bMap.Version < 5 { + oldOffset = -24 + } + + player.progressMsF = player.rawPositionF + float64(settings.LOCALOFFSET)*speed + oldOffset player.updateMain(delta) @@ -554,6 +635,8 @@ func (player *Player) GetTimeOffset() float64 { } func (player *Player) updateMain(delta float64) { + player.realTime += delta + if player.rawPositionF >= player.startPoint && !player.start { player.musicPlayer.Play() @@ -574,9 +657,29 @@ func (player *Player) updateMain(delta float64) { player.speedGlider.Update(player.progressMsF) player.pitchGlider.Update(player.progressMsF) + player.frequencyGlider.Update(player.realTime) + + player.objectsAlphaFail.Update(player.realTime) + player.failOX.Update(player.realTime) + player.failOY.Update(player.realTime) + player.failRotation.Update(player.realTime) + + player.objectCamera.SetOrigin(vector.NewVec2d(player.failOX.GetValue(), player.failOY.GetValue())) + player.objectCamera.SetRotation(player.failRotation.GetValue()) + player.objectCamera.Update() + + if player.failing && player.realTime >= player.failAt { + if !player.failed { + player.musicPlayer.Pause() + player.MapEnd = player.progressMsF + } + + player.failed = true + } player.musicPlayer.SetTempo(player.speedGlider.GetValue()) player.musicPlayer.SetPitch(player.pitchGlider.GetValue()) + player.musicPlayer.SetRelativeFrequency(player.frequencyGlider.GetValue()) if player.progressMsF >= player.startPointE { if _, ok := player.controller.(*dance.GenericController); ok { @@ -593,7 +696,7 @@ func (player *Player) updateMain(delta float64) { if player.nightcore != nil { player.nightcore.Update(player.progressMsF) } - } else { + } else if settings.Gameplay.ShowResultsScreen { if player.overlay != nil { player.overlay.DisableAudioSubmission(true) } @@ -679,7 +782,8 @@ func (player *Player) Draw(float64) { player.profiler.PutSample(timMs) player.lastTime = tim - cameras := player.mainCamera.GenRotated(settings.DIVIDES, -2*math.Pi/float64(settings.DIVIDES)) + objectCameras := player.objectCamera.GenRotated(settings.DIVIDES, -2*math.Pi/float64(settings.DIVIDES)) + cursorCameras := player.mainCamera.GenRotated(settings.DIVIDES, -2*math.Pi/float64(settings.DIVIDES)) bgAlpha := player.dimGlider.GetValue() if settings.Playfield.Background.FlashToTheBeat { @@ -697,7 +801,7 @@ func (player *Player) Draw(float64) { cursorColors := settings.Cursor.GetColors(settings.DIVIDES, len(player.controller.GetCursors()), player.Scl, player.cursorGlider.GetValue()) if player.overlay != nil { - player.drawOverlayPart(player.overlay.DrawBackground, cursorColors, cameras[0]) + player.drawOverlayPart(player.overlay.DrawBackground, cursorColors, cursorCameras[0], 1) } player.drawEpilepsyWarning() @@ -728,19 +832,19 @@ func (player *Player) Draw(float64) { } if player.overlay != nil { - player.drawOverlayPart(player.overlay.DrawBeforeObjects, cursorColors, cameras[0]) + player.drawOverlayPart(player.overlay.DrawBeforeObjects, cursorColors, objectCameras[0], player.objectsAlphaFail.GetValue()) } - player.objectContainer.Draw(player.batch, cameras, player.progressMsF, float32(player.Scl), float32(player.objectsAlpha.GetValue())) + player.objectContainer.Draw(player.batch, player.mainCamera.GetProjectionView(), objectCameras, player.progressMsF, float32(player.Scl), float32(player.objectsAlpha.GetValue()*player.objectsAlphaFail.GetValue())) if player.overlay != nil { - player.drawOverlayPart(player.overlay.DrawNormal, cursorColors, cameras[0]) + player.drawOverlayPart(player.overlay.DrawNormal, cursorColors, objectCameras[0], 1) } player.background.DrawOverlay(player.progressMsF, player.batch, bgAlpha, player.bgCamera.GetProjectionView()) if player.overlay != nil && player.overlay.ShouldDrawHUDBeforeCursor() { - player.drawOverlayPart(player.overlay.DrawHUD, cursorColors, player.uiCamera.GetProjectionView()) + player.drawOverlayPart(player.overlay.DrawHUD, cursorColors, player.uiCamera.GetProjectionView(), 1) } if settings.Playfield.DrawCursors { @@ -753,7 +857,7 @@ func (player *Player) Draw(float64) { graphics.BeginCursorRender() for j := 0; j < settings.DIVIDES; j++ { - player.batch.SetCamera(cameras[j]) + player.batch.SetCamera(cursorCameras[j]) for i, g := range player.controller.GetCursors() { if player.overlay != nil && player.overlay.IsBroken(g) { @@ -780,7 +884,7 @@ func (player *Player) Draw(float64) { player.batch.SetAdditive(false) if player.overlay != nil && !player.overlay.ShouldDrawHUDBeforeCursor() { - player.drawOverlayPart(player.overlay.DrawHUD, cursorColors, player.uiCamera.GetProjectionView()) + player.drawOverlayPart(player.overlay.DrawHUD, cursorColors, player.uiCamera.GetProjectionView(), 1) } if bloomEnabled { @@ -827,14 +931,14 @@ func (player *Player) drawCoin() { player.batch.End() } -func (player *Player) drawOverlayPart(drawFunc func(*batch2.QuadBatch, []color2.Color, float64), cursorColors []color2.Color, camera mgl32.Mat4) { +func (player *Player) drawOverlayPart(drawFunc func(*batch2.QuadBatch, []color2.Color, float64), cursorColors []color2.Color, camera mgl32.Mat4, alpha float64) { player.batch.Begin() player.batch.ResetTransform() player.batch.SetColor(1, 1, 1, 1) player.batch.SetCamera(camera) - drawFunc(player.batch, cursorColors, player.hudGlider.GetValue()) + drawFunc(player.batch, cursorColors, player.hudGlider.GetValue()*alpha) player.batch.End() player.batch.ResetTransform() diff --git a/app/storyboard/loop.go b/app/storyboard/loop.go index 3f27dcdb..32e85a4b 100644 --- a/app/storyboard/loop.go +++ b/app/storyboard/loop.go @@ -55,12 +55,12 @@ func (loop *LoopProcessor) Unwind() []*animation.Transformation { iterationTime := endTime - startTime - for i := int64(0); i < loop.repeats; i++ { - partStart := float64(loop.start) + float64(i)*iterationTime + for _, t := range loop.transforms { + t2 := t.Clone(float64(loop.start)+t.GetStartTime(), float64(loop.start)+t.GetEndTime()) + + t2.SetLoop(int(loop.repeats), iterationTime) - for _, t := range loop.transforms { - transforms = append(transforms, t.Clone(partStart+t.GetStartTime(), partStart+t.GetEndTime())) - } + transforms = append(transforms, t2) } return transforms diff --git a/app/storyboard/storyboard.go b/app/storyboard/storyboard.go index db311031..3d1724b7 100644 --- a/app/storyboard/storyboard.go +++ b/app/storyboard/storyboard.go @@ -133,7 +133,11 @@ func NewStoryboard(beatMap *beatmap.BeatMap) *Storyboard { spl := strings.Split(line, ",") startTime, _ := strconv.ParseFloat(spl[1], 64) - volume, _ := strconv.ParseFloat(spl[4], 64) + + volume := 100.0 + if len(spl) > 4 { + volume, _ = strconv.ParseFloat(spl[4], 64) + } sample := strings.TrimSpace(strings.ReplaceAll(spl[3], `"`, "")) @@ -216,7 +220,7 @@ func NewStoryboard(beatMap *beatmap.BeatMap) *Storyboard { log.Println("Storyboard loaded") storyboard.currentTime = -1000000 - storyboard.limiter = frame.NewLimiter(2000) + storyboard.limiter = frame.NewLimiter(1000) storyboard.counter = frame.NewCounter() return storyboard diff --git a/assets/default-skin/failsound.wav b/assets/default-skin/failsound.wav new file mode 100644 index 00000000..ea226590 Binary files /dev/null and b/assets/default-skin/failsound.wav differ diff --git a/assets/shaders/imgui.fsh b/assets/shaders/imgui.fsh index 7b5f050a..478a4e7d 100644 --- a/assets/shaders/imgui.fsh +++ b/assets/shaders/imgui.fsh @@ -2,11 +2,15 @@ uniform sampler2DArray tex; +uniform int texRGBA; + in vec2 uv; in vec4 color; out vec4 out_color; void main() { - out_color = texture(tex, vec3(uv, 0)) * color;//vec4(color.rgb, color.a * texture(tex, vec3(uv, 0)).r); + vec4 cColor = texRGBA == 0 ? vec4(vec3(1.0), texture(tex, vec3(uv, 0)).r) : texture(tex, vec3(uv, 0)); + + out_color = cColor * color;//vec4(color.rgb, color.a * texture(tex, vec3(uv, 0)).r); } \ No newline at end of file diff --git a/assets/shaders/rgbyuv.fsh b/assets/shaders/rgbyuv.fsh index 959b52cf..e4047e91 100644 --- a/assets/shaders/rgbyuv.fsh +++ b/assets/shaders/rgbyuv.fsh @@ -10,25 +10,25 @@ out vec4 color; // BT.709 matrix according to ITU document: https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-6-201506-I!!PDF-E.pdf // BT.601 matrix according to ITU document: https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf -const mat4x3 rgbToYuvBT709Full = mat4x3(0.2126, -0.114572, 0.5, - 0.7152, -0.385428, -0.454152, - 0.0722, 0.5, -0.045847, - 0, 0.5, 0.5); - -const mat4x3 rgbToYuvBT601Full = mat4x3(0.299, -0.168736, 0.5, - 0.587, -0.331264, -0.418688, - 0.114, 0.5, -0.081312, - 0, 0.5, 0.5); - -const mat4x3 rgbToYuvBT709TV = mat4x3(0.182586, -0.100644, 0.439216, - 0.614231, -0.338572, -0.398941, - 0.062007, 0.439216, -0.040273, - 0.0625, 0.5, 0.5); - -const mat4x3 rgbToYuvBT601TV = mat4x3(0.256788, -0.148223, 0.439216, - 0.504129, -0.290993, -0.367789, - 0.097906, 0.439216, -0.071427, - 0.0625, 0.5, 0.5); +const mat4x3 rgbToYuvBT709Full = mat4x3(0.213434, -0.115021, 0.501961, + 0.718005, -0.386939, -0.455933, + 0.072483, 0.501961, -0.046027, + 0, 0.501961, 0.501961); + +const mat4x3 rgbToYuvBT601Full = mat4x3(0.300173, -0.169398, 0.501961, + 0.589302, -0.332563, -0.420330, + 0.114447, 0.501961, -0.081631, + 0, 0.501961, 0.501961); + +const mat4x3 rgbToYuvBT709TV = mat4x3(0.183302, -0.101039, 0.440938, + 0.616640, -0.339900, -0.400505, + 0.062250, 0.440938, -0.040431, + 0.062745, 0.501961, 0.501961); + +const mat4x3 rgbToYuvBT601TV = mat4x3(0.257796, -0.148804, 0.440937, + 0.506106, -0.292133, -0.369231, + 0.098290, 0.440937, -0.071706, + 0.062745, 0.501961, 0.501961); void main() { vec3 src = texture(tex, vec3(tex_coord, 0)).rgb; diff --git a/build/version.go b/build/version.go index c5104c2d..4edc227f 100644 --- a/build/version.go +++ b/build/version.go @@ -9,6 +9,8 @@ var VERSION = "dev" var Stream = "Dev" +var DanserExec = "danser-cli" + func init() { if VERSION == "dev" { if bI, ok := debug.ReadBuildInfo(); ok { diff --git a/dist-linux.sh b/dist-linux.sh index 43178df0..7d2504a9 100644 --- a/dist-linux.sh +++ b/dist-linux.sh @@ -4,6 +4,8 @@ export GOARCH=amd64 export CGO_ENABLED=1 export CC=gcc export CXX=g++ +export BUILD_DIR=./dist/build-linux +export TARGET_DIR=./dist/artifacts exec=$1 build=$1 @@ -13,16 +15,26 @@ then build+='-snapshot'$2 fi -go run tools/assets/assets.go ./ +mkdir -p $BUILD_DIR -go build -trimpath -ldflags "-s -w -X 'github.com/wieku/danser-go/build.VERSION=$build' -X 'github.com/wieku/danser-go/build.Stream=Release'" -buildmode=c-shared -o danser-core.so -v -x +go run tools/assets/assets.go ./ $BUILD_DIR/ -mv danser-core.so libdanser-core.so +go build -trimpath -ldflags "-s -w -X 'github.com/wieku/danser-go/build.VERSION=$build' -X 'github.com/wieku/danser-go/build.Stream=Release'" -buildmode=c-shared -o $BUILD_DIR/danser-core.so -v -x -gcc -no-pie --verbose -O3 -o danser -I. cmain/main_danser.c -Wl,-rpath,. -L. -ldanser-core +mv $BUILD_DIR/danser-core.so $BUILD_DIR/libdanser-core.so -gcc -no-pie --verbose -O3 -D LAUNCHER -o danser-launcher -I. cmain/main_danser.c -Wl,-rpath,. -L. -ldanser-core +gcc -no-pie --verbose -O3 -o $BUILD_DIR/danser-cli -I. cmain/main_danser.c -I$BUILD_DIR/ -Wl,-rpath,. -L$BUILD_DIR/ -ldanser-core -go run tools/pack/pack.go danser-$exec-linux.zip libdanser-core.so danser danser-launcher libbass.so libbass_fx.so libbassmix.so libyuv.so assets.dpak +gcc -no-pie --verbose -O3 -D LAUNCHER -o $BUILD_DIR/danser -I. cmain/main_danser.c -I$BUILD_DIR/ -Wl,-rpath,. -L$BUILD_DIR/ -ldanser-core -rm -f danser danser-launcher libdanser-core.so danser-core.h assets.dpak \ No newline at end of file +rm $BUILD_DIR/danser-core.h + +cp {libbass.so,libbass_fx.so,libbassmix.so,libyuv.so} $BUILD_DIR/ + +go run tools/ffmpeg/ffmpeg.go $BUILD_DIR/ + +mkdir -p $TARGET_DIR + +go run tools/pack2/pack.go $TARGET_DIR/danser-$exec-linux.zip $BUILD_DIR/ + +rm -rf $BUILD_DIR \ No newline at end of file diff --git a/dist-win.sh b/dist-win.sh index 093f66a7..4611ec01 100644 --- a/dist-win.sh +++ b/dist-win.sh @@ -6,6 +6,8 @@ export CC=x86_64-w64-mingw32-gcc export CXX=x86_64-w64-mingw32-g++ export CGO_LDFLAGS="-static-libstdc++ -static-libgcc -Wl,-Bstatic -lstdc++ -lpthread -Wl,-Bdynamic" export WINDRESFLAGS="-F pe-x86-64" +export BUILD_DIR=./dist/build-win +export TARGET_DIR=./dist/artifacts exec=$1 build=$1 @@ -49,7 +51,9 @@ END 2 ICON assets/textures/favicon.ico ' -resgen='windres -l 0 '$WINDRESFLAGS' -o danser.syso' +mkdir -p $BUILD_DIR + +resgen='windres -l 0 '$WINDRESFLAGS' -o '$BUILD_DIR'/danser.syso' resCore=$preRC'-core.dll'$postRC resDanser=$preRC''$postRC @@ -57,18 +61,30 @@ resLauncher=$preRC' launcher'$postRC $resgen <<< $resCore -go run tools/assets/assets.go ./ +go run tools/assets/assets.go ./ $BUILD_DIR/ + +cp $BUILD_DIR/danser.syso danser.syso -go build -trimpath -ldflags "-s -w -X 'github.com/wieku/danser-go/build.VERSION=$build' -X 'github.com/wieku/danser-go/build.Stream=Release'" -buildmode=c-shared -o danser-core.dll -v -x +go build -trimpath -ldflags "-s -w -X 'github.com/wieku/danser-go/build.VERSION=$build' -X 'github.com/wieku/danser-go/build.Stream=Release'" -buildmode=c-shared -o $BUILD_DIR/danser-core.dll -v -x + +rm -f danser.syso $resgen <<< $resDanser -gcc --verbose -O3 -o danser.exe -I. cmain/main_danser.c -L. -ldanser-core danser.syso -municode +$CC <<< --verbose -O3 -o $BUILD_DIR/danser-cli.exe -I. cmain/main_danser.c -I$BUILD_DIR/ -L$BUILD_DIR/ -ldanser-core $BUILD_DIR/danser.syso -municode $resgen <<< $resLauncher -gcc --verbose -O3 -D LAUNCHER -o danser-launcher.exe -I. cmain/main_danser.c -L. -ldanser-core danser.syso -municode +$CC <<< --verbose -O3 -D LAUNCHER -o $BUILD_DIR/danser.exe -I. cmain/main_danser.c -I$BUILD_DIR/ -L$BUILD_DIR/ -ldanser-core $BUILD_DIR/danser.syso -municode + +cp {bass.dll,bass_fx.dll,bassmix.dll,libyuv.dll} $BUILD_DIR/ + +rm $BUILD_DIR/{danser.syso,danser-core.h} + +go run tools/ffmpeg/ffmpeg.go $BUILD_DIR/ + +mkdir -p $TARGET_DIR -go run tools/pack/pack.go danser-$exec-win.zip danser-core.dll danser.exe danser-launcher.exe bass.dll bass_fx.dll bassmix.dll libyuv.dll assets.dpak +go run tools/pack2/pack.go $TARGET_DIR/danser-$exec-win.zip $BUILD_DIR/ -rm -f danser.exe danser-launcher.exe danser-core.dll danser-core.h assets.dpak danser.syso \ No newline at end of file +rm -rf $BUILD_DIR \ No newline at end of file diff --git a/framework/frame/limiter.go b/framework/frame/limiter.go index 44bf3493..16d0b44b 100644 --- a/framework/frame/limiter.go +++ b/framework/frame/limiter.go @@ -7,55 +7,105 @@ import ( "time" ) +// Full credits: LWJGL Team +// Ported from Java +// Original source: https://github.com/LWJGL/lwjgl/blob/master/src/java/org/lwjgl/opengl/Sync.java + +const ( + nanosInSecond = 1e9 + dampenThreshold = 10e6 // 10ms + dampenFactor = 0.9 // don't change: 0.9f is exactly right! +) + type Limiter struct { - FPS int - variableYieldTime int64 - lastTime int64 + FPS int + nextFrame int64 + initialised bool + + sleepDurations *runningAvg + yieldDurations *runningAvg } func NewLimiter(fps int) *Limiter { - return &Limiter{fps, 0, 0} + return &Limiter{ + FPS: fps, + sleepDurations: newRunningAvg(10), + yieldDurations: newRunningAvg(10), + } } -/** - * An accurate sync method that adapts automatically - * to the system it runs on to provide reliable results. - * - * @author kappa (On the LWJGL Forums) - */ func (limiter *Limiter) Sync() { if limiter.FPS <= 0 { return } - sleepTime := int64(1000000000) / int64(limiter.FPS) // nanoseconds to sleep this frame - // yieldTime + remainder micro & nano seconds if smaller than sleepTime - yieldTime := mutils.Min(sleepTime, limiter.variableYieldTime+sleepTime%int64(1000*1000)) - overSleep := int64(0) // time the sync goes over by + if !limiter.initialised { + limiter.initialised = true - for { - t := qpc.GetNanoTime() - limiter.lastTime + limiter.sleepDurations.init(1000 * 1000) + limiter.yieldDurations.init(int64(-float64(qpc.GetNanoTime()-qpc.GetNanoTime()) * 1.333)) - if t < sleepTime-yieldTime { - time.Sleep(time.Millisecond) - } else if t < sleepTime { - // burn the last few CPU cycles to ensure accuracy - runtime.Gosched() - } else { - overSleep = t - sleepTime - break // exit while loop - } + limiter.nextFrame = qpc.GetNanoTime() + } + + for t0, t1 := qpc.GetNanoTime(), int64(0); (limiter.nextFrame - t0) > limiter.sleepDurations.avg(); t0 = t1 { + time.Sleep(time.Millisecond) + + t1 = qpc.GetNanoTime() + + limiter.sleepDurations.add(t1 - t0) } - limiter.lastTime = qpc.GetNanoTime() - mutils.Min(overSleep, sleepTime) + limiter.sleepDurations.dampenForLowResTicker() + + for t0, t1 := qpc.GetNanoTime(), int64(0); (limiter.nextFrame - t0) > limiter.yieldDurations.avg(); t0 = t1 { + runtime.Gosched() + + t1 = qpc.GetNanoTime() - // auto tune the time sync should yield - if overSleep > limiter.variableYieldTime { - // increase by 200 microseconds (1/5 a ms) - limiter.variableYieldTime = mutils.Min(limiter.variableYieldTime+200*1000, sleepTime) - } else if overSleep < limiter.variableYieldTime-200*1000 { - // decrease by 2 microseconds - limiter.variableYieldTime = mutils.Max(limiter.variableYieldTime-2*1000, 0) + limiter.yieldDurations.add(t1 - t0) } + limiter.nextFrame = mutils.Max(limiter.nextFrame+nanosInSecond/int64(limiter.FPS), qpc.GetNanoTime()) +} + +type runningAvg struct { + slots []int64 + offset int +} + +func newRunningAvg(slotCount int) *runningAvg { + return &runningAvg{ + slots: make([]int64, slotCount), + offset: 0, + } +} + +func (ra *runningAvg) init(value int64) { + for i := 0; i < len(ra.slots); i++ { + ra.slots[i] = value + } +} + +func (ra *runningAvg) add(value int64) { + ra.slots[ra.offset] = value + ra.offset = (ra.offset + 1) % len(ra.slots) +} + +func (ra *runningAvg) avg() int64 { + var sum int64 + + for i := 0; i < len(ra.slots); i++ { + sum += ra.slots[i] + } + + return sum / int64(len(ra.slots)) +} + +func (ra *runningAvg) dampenForLowResTicker() { + if ra.avg() > dampenThreshold { + for i := 0; i < len(ra.slots); i++ { + ra.slots[i] = int64(float64(ra.slots[i]) * dampenFactor) + } + } } diff --git a/framework/graphics/buffer/ibo.go b/framework/graphics/buffer/ibo.go index ddb1ae62..d3a78772 100644 --- a/framework/graphics/buffer/ibo.go +++ b/framework/graphics/buffer/ibo.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/faiface/mainthread" "github.com/go-gl/gl/v3.3-core/gl" + "github.com/wieku/danser-go/framework/graphics/hacks" "github.com/wieku/danser-go/framework/graphics/history" "github.com/wieku/danser-go/framework/statistic" "runtime" @@ -62,7 +63,7 @@ func (ibo *IndexBufferObject) DrawPart(offset, length int) { gl.DrawElements(gl.TRIANGLES, int32(length), gl.UNSIGNED_SHORT, gl.PtrOffset(offset*2)) - if IsIntel { + if hacks.IsIntel { gl.Flush() } } @@ -75,7 +76,7 @@ func (ibo *IndexBufferObject) DrawPartInstanced(offset, length, baseInstance, in gl.DrawElementsInstancedBaseInstance(gl.TRIANGLES, int32(length), gl.UNSIGNED_SHORT, gl.PtrOffset(offset), int32(instanceCount), uint32(baseInstance)) - if IsIntel { + if hacks.IsIntel { gl.Flush() } } diff --git a/framework/graphics/buffer/intel.go b/framework/graphics/buffer/intel.go deleted file mode 100644 index bd60caa2..00000000 --- a/framework/graphics/buffer/intel.go +++ /dev/null @@ -1,5 +0,0 @@ -package buffer - -// Intel has some weird problems with DSA glBufferSubData that causes Draw calls not to be flushed immediately, -// resulting in broken image in some cases -var IsIntel bool diff --git a/framework/graphics/buffer/vao.go b/framework/graphics/buffer/vao.go index 4c8e5eac..d9482be2 100644 --- a/framework/graphics/buffer/vao.go +++ b/framework/graphics/buffer/vao.go @@ -5,6 +5,7 @@ import ( "github.com/faiface/mainthread" "github.com/go-gl/gl/v3.3-core/gl" "github.com/wieku/danser-go/framework/graphics/attribute" + "github.com/wieku/danser-go/framework/graphics/hacks" "github.com/wieku/danser-go/framework/graphics/history" "github.com/wieku/danser-go/framework/graphics/shader" "github.com/wieku/danser-go/framework/statistic" @@ -215,7 +216,7 @@ func (vao *VertexArrayObject) DrawPart(offset, length int) { gl.DrawArrays(gl.TRIANGLES, int32(offset), int32(length)) - if IsIntel { + if hacks.IsIntel { gl.Flush() } } @@ -233,7 +234,7 @@ func (vao *VertexArrayObject) DrawPartInstanced(offset, length, baseInstance, in gl.DrawArraysInstancedBaseInstance(gl.TRIANGLES, int32(offset), int32(length), int32(instanceCount), uint32(baseInstance)) - if IsIntel { + if hacks.IsIntel { gl.Flush() } } @@ -302,4 +303,4 @@ func (vao *VertexArrayObject) AttachIBO(ibo *IndexBufferObject) { ibo.attached = true vao.ibo = ibo gl.VertexArrayElementBuffer(vao.handle, ibo.handle) -} \ No newline at end of file +} diff --git a/framework/graphics/hacks/hacks.go b/framework/graphics/hacks/hacks.go new file mode 100644 index 00000000..e0fb040c --- /dev/null +++ b/framework/graphics/hacks/hacks.go @@ -0,0 +1,9 @@ +package hacks + +// Intel has some weird problems with DSA glBufferSubData that causes Draw calls not to be flushed immediately, +// resulting in broken image in some cases +var IsIntel bool + +// Old AMD drivers on TeraScale GPUs (15.201.xxx.xxx) have some weird problems with texture upload. +// To work correctly, it has to be uploaded on another unit than the one that's frequently used for rendering +var IsOldAMD bool diff --git a/framework/graphics/sprite/sprite.go b/framework/graphics/sprite/sprite.go index e4753993..477ecb1f 100644 --- a/framework/graphics/sprite/sprite.go +++ b/framework/graphics/sprite/sprite.go @@ -6,7 +6,9 @@ import ( "github.com/wieku/danser-go/framework/math/animation" color2 "github.com/wieku/danser-go/framework/math/color" "github.com/wieku/danser-go/framework/math/math32" + "github.com/wieku/danser-go/framework/math/mutils" "github.com/wieku/danser-go/framework/math/vector" + "golang.org/x/exp/slices" "math" "sort" ) @@ -34,6 +36,8 @@ type Sprite struct { cutX float64 cutY float64 cutOrigin vector.Vector2d + + nextTransformID int64 } func NewSpriteSingle(tex *texture.TextureRegion, depth float64, position vector.Vector2d, origin vector.Vector2d) *Sprite { @@ -59,89 +63,97 @@ func (sprite *Sprite) Update(time float64) { sprite.updateTransform(transform, time) if time >= transform.GetEndTime() { - copy(sprite.transforms[i:], sprite.transforms[i+1:]) - sprite.transforms = sprite.transforms[:len(sprite.transforms)-1] - i-- + if transform.IsLoop() { + transform.UpdateLoop() + + n := sort.Search(len(sprite.transforms)-i-1, func(f int) bool { + b := sprite.transforms[f+i+1] + + r := mutils.Compare(transform.GetStartTime(), b.GetStartTime()) + return r == -1 || (r == 0 && transform.GetID() < b.GetID()) + }) + + if n != 0 { + copy(sprite.transforms[i:], sprite.transforms[i+1:n+i+1]) + sprite.transforms[n+i] = transform + + i-- + } + } else { + copy(sprite.transforms[i:], sprite.transforms[i+1:]) + sprite.transforms = sprite.transforms[:len(sprite.transforms)-1] + i-- + } } } } func (sprite *Sprite) updateTransform(transform *animation.Transformation, time float64) { //nolint:gocyclo switch transform.GetType() { - case animation.Fade, animation.Scale, animation.Rotate, animation.MoveX, animation.MoveY: - value := transform.GetSingle(time) - - switch transform.GetType() { - case animation.Fade: - sprite.color.A = float32(value) - case animation.Scale: - sprite.scale.X = value - sprite.scale.Y = value - case animation.Rotate: - sprite.rotation = value - case animation.MoveX: - sprite.position.X = value - case animation.MoveY: - sprite.position.Y = value - } - case animation.Move, animation.ScaleVector: - x, y := transform.GetDouble(time) - - switch transform.GetType() { - case animation.Move: - sprite.position.X = x - sprite.position.Y = y - case animation.ScaleVector: - sprite.scale.X = x - sprite.scale.Y = y - } - case animation.Additive, animation.HorizontalFlip, animation.VerticalFlip: - value := transform.GetBoolean(time) - - switch transform.GetType() { - case animation.Additive: - sprite.additive = value - case animation.HorizontalFlip: - sprite.flipX = value - case animation.VerticalFlip: - sprite.flipY = value - } - case animation.Color3, animation.Color4: + case animation.Fade: + sprite.color.A = float32(transform.GetSingle(time)) + case animation.Scale: + s := transform.GetSingle(time) + + sprite.scale.X = s + sprite.scale.Y = s + case animation.Rotate: + sprite.rotation = transform.GetSingle(time) + case animation.MoveX: + sprite.position.X = transform.GetSingle(time) + case animation.MoveY: + sprite.position.Y = transform.GetSingle(time) + case animation.Move: + sprite.position.X, sprite.position.Y = transform.GetDouble(time) + case animation.ScaleVector: + sprite.scale.X, sprite.scale.Y = transform.GetDouble(time) + case animation.Additive: + sprite.additive = transform.GetBoolean(time) + case animation.HorizontalFlip: + sprite.flipX = transform.GetBoolean(time) + case animation.VerticalFlip: + sprite.flipY = transform.GetBoolean(time) + case animation.Color3: color := transform.GetColor(time) sprite.color.R = color.R sprite.color.G = color.G sprite.color.B = color.B - - if transform.GetType() == animation.Color4 { - sprite.color.A = color.A - } + case animation.Color4: + sprite.color = transform.GetColor(time) } } func (sprite *Sprite) AddTransform(transformation *animation.Transformation) { - sprite.transforms = append(sprite.transforms, transformation) - + sprite.AddTransformUnordered(transformation) sprite.SortTransformations() } func (sprite *Sprite) AddTransforms(transformations []*animation.Transformation) { - sprite.transforms = append(sprite.transforms, transformations...) - + sprite.AddTransformsUnordered(transformations) sprite.SortTransformations() } func (sprite *Sprite) AddTransformUnordered(transformation *animation.Transformation) { + transformation.SetID(sprite.nextTransformID) + sprite.nextTransformID++ + sprite.transforms = append(sprite.transforms, transformation) } func (sprite *Sprite) AddTransformsUnordered(transformations []*animation.Transformation) { - sprite.transforms = append(sprite.transforms, transformations...) + for _, t := range transformations { + t.SetID(sprite.nextTransformID) + sprite.nextTransformID++ + + sprite.transforms = append(sprite.transforms, t) + } } func (sprite *Sprite) SortTransformations() { - sort.SliceStable(sprite.transforms, func(i, j int) bool { - return sprite.transforms[i].GetStartTime() < sprite.transforms[j].GetStartTime() + slices.SortFunc(sprite.transforms, func(a, b *animation.Transformation) bool { + r := mutils.Compare(a.GetStartTime(), b.GetStartTime()) + return r == -1 || (r == 0 && a.GetID() < b.GetID()) }) } @@ -221,10 +233,6 @@ func (sprite *Sprite) Draw(time float64, batch *batch.QuadBatch) { scale := sprite.scale.Abs() if sprite.cutX > 0.0 { - if math.Abs(sprite.origin.X-sprite.cutOrigin.X) > 0 { - position.X -= sprite.origin.X * float64(region.Width) * scale.X * sprite.cutX - } - ratio := float32(1 - sprite.cutX) middle := float32(sprite.cutOrigin.X)/2*math32.Abs(region.U2-region.U1) + (region.U1+region.U2)/2 @@ -234,10 +242,6 @@ func (sprite *Sprite) Draw(time float64, batch *batch.QuadBatch) { } if sprite.cutY > 0.0 { - if math.Abs(sprite.origin.Y-sprite.cutOrigin.Y) > 0 { - position.Y -= sprite.origin.Y * float64(region.Height) * scale.Y * sprite.cutY - } - ratio := float32(1 - sprite.cutY) middle := float32(sprite.cutOrigin.Y)/2*math32.Abs(region.V2-region.V1) + (region.V1+region.V2)/2 diff --git a/framework/graphics/texture/multilayer.go b/framework/graphics/texture/multilayer.go index 84640dd3..d479bbab 100644 --- a/framework/graphics/texture/multilayer.go +++ b/framework/graphics/texture/multilayer.go @@ -52,15 +52,7 @@ func (texture *TextureMultiLayer) NewLayer() { } func (texture *TextureMultiLayer) SetData(x, y, width, height, layer int, data []uint8) { - if len(data) != width*height*texture.store.format.Size() { - panic("Wrong number of pixels given!") - } - - gl.TextureSubImage3D(texture.store.id, 0, int32(x), int32(y), int32(layer), int32(width), int32(height), 1, texture.store.format.Format(), texture.store.format.Type(), gl.Ptr(data)) - - if texture.store.mipmaps > 1 && !texture.manualMipmaps { - gl.GenerateTextureMipmap(texture.store.id) - } + texture.store.SetData(x, y, width, height, layer, data, !texture.manualMipmaps) } func (texture *TextureMultiLayer) GenerateMipmaps() { diff --git a/framework/graphics/texture/single.go b/framework/graphics/texture/single.go index e52bea94..2f5aecf3 100644 --- a/framework/graphics/texture/single.go +++ b/framework/graphics/texture/single.go @@ -2,7 +2,6 @@ package texture import ( "github.com/faiface/mainthread" - "github.com/go-gl/gl/v3.3-core/gl" "image" "runtime" ) @@ -33,15 +32,7 @@ func LoadTextureSingle(img *image.RGBA, mipmaps int) *TextureSingle { } func (texture *TextureSingle) SetData(x, y, width, height int, data []uint8) { - if len(data) != width*height*texture.store.format.Size() { - panic("Wrong number of pixels given!") - } - - gl.TextureSubImage3D(texture.store.id, 0, int32(x), int32(y), 0, int32(width), int32(height), 1, texture.store.format.Format(), texture.store.format.Type(), gl.Ptr(data)) - - if texture.store.mipmaps > 1 { - gl.GenerateTextureMipmap(texture.store.id) - } + texture.store.SetData(x, y, width, height, 0, data, true) } func (texture *TextureSingle) GetID() uint32 { diff --git a/framework/graphics/texture/texture.go b/framework/graphics/texture/texture.go index b1ca0c57..4261d2b6 100644 --- a/framework/graphics/texture/texture.go +++ b/framework/graphics/texture/texture.go @@ -2,6 +2,7 @@ package texture import ( "github.com/go-gl/gl/v3.3-core/gl" + "github.com/wieku/danser-go/framework/graphics/hacks" ) type Filter int32 @@ -74,6 +75,28 @@ func newStore(layerNum, width, height int, format Format, mipmaps int) *textureS return store } +func (store *textureStore) SetData(x, y, width, height, layer int, data []uint8, generateMipmaps bool) { + if len(data) != width*height*store.format.Size() { + panic("Wrong number of pixels given!") + } + + amdHack := store.binding == 0 && hacks.IsOldAMD + + if amdHack { + gl.BindTextureUnit(11, store.id) + } + + gl.TextureSubImage3D(store.id, 0, int32(x), int32(y), int32(layer), int32(width), int32(height), 1, store.format.Format(), store.format.Type(), gl.Ptr(data)) + + if amdHack { + gl.BindTextureUnit(uint32(store.binding), store.id) + } + + if store.mipmaps > 1 && generateMipmaps { + gl.GenerateTextureMipmap(store.id) + } +} + func (store *textureStore) Bind(loc uint) { store.binding = loc diff --git a/framework/graphics/video/decoder.go b/framework/graphics/video/decoder.go index 29136929..d6bae2db 100644 --- a/framework/graphics/video/decoder.go +++ b/framework/graphics/video/decoder.go @@ -87,6 +87,10 @@ func (dec *VideoDecoder) StartFFmpeg(millis int64) { var args []string + if dec.Metadata.IsMOV { + args = append(args, "-ignore_editlist", "1") + } + if millis != 0 { args = append(args, "-ss", fmt.Sprintf("%d.%d", millis/1000, millis%1000), diff --git a/framework/graphics/video/metadata.go b/framework/graphics/video/metadata.go index 7daf998b..0b63a262 100644 --- a/framework/graphics/video/metadata.go +++ b/framework/graphics/video/metadata.go @@ -18,6 +18,7 @@ type Metadata struct { FPS float64 Duration float64 PixFmt string + IsMOV bool // We need that info to determine if we can apply "ignore_editlist" parameter } type probeOutput struct { @@ -35,7 +36,8 @@ type stream struct { } type format struct { - Duration string `json:"duration"` + FormatName string `json:"format_name"` + Duration string `json:"duration"` } func LoadMetadata(path string) *Metadata { @@ -50,31 +52,9 @@ func LoadMetadata(path string) *Metadata { return nil } - output, err := exec.Command( - probeExec, - "-i", path, - "-select_streams", "v:0", - "-show_entries", "stream", - "-show_entries", "format", - "-of", "json", - "-loglevel", "quiet", - ).Output() - - if err != nil { - if strings.Contains(err.Error(), "127") || strings.Contains(strings.ToLower(err.Error()), "0xc0000135") { - log.Println(fmt.Sprintf("ffmpeg was installed incorrectly! Please make sure needed libraries (libs/*.so or bin/*.dll) are installed as well. Follow download instructions at https://github.com/Wieku/danser-go/wiki/FFmpeg. Error: %s", err)) - } else { - log.Println(fmt.Sprintf("ffprobe: Failed to get media info. Error: %s", err)) - } - - return nil - } - - mData := new(probeOutput) + mData := getProbeOutput(probeExec, path, false) - err = json.Unmarshal(output, mData) - if err != nil { - log.Println("Failed to parse video metadata:", err) + if mData == nil { return nil } @@ -99,7 +79,51 @@ func LoadMetadata(path string) *Metadata { FPS: math.Min(aFPS, rFPS), Duration: parseRate(mData.Streams[0].Duration), PixFmt: mData.Streams[0].PixFmt, + IsMOV: strings.Contains(mData.Format.FormatName, "mov"), + } +} + +func getProbeOutput(probeExec, path string, mov bool) *probeOutput { + var args []string + + if mov { + args = append(args, "-ignore_editlist", "1") + } + + args = append(args, "-i", path, + "-select_streams", "v:0", + "-show_entries", "stream", + "-show_entries", "format", + "-of", "json", + "-loglevel", "quiet", + ) + + output, err := exec.Command(probeExec, args...).Output() + + if err != nil { + if strings.Contains(err.Error(), "127") || strings.Contains(strings.ToLower(err.Error()), "0xc0000135") { + log.Println(fmt.Sprintf("ffmpeg was installed incorrectly! Please make sure needed libraries (libs/*.so or bin/*.dll) are installed as well. Follow download instructions at https://github.com/Wieku/danser-go/wiki/FFmpeg. Error: %s", err)) + } else { + log.Println(fmt.Sprintf("ffprobe: Failed to get media info. Error: %s", err)) + } + + return nil } + + mData := new(probeOutput) + + err = json.Unmarshal(output, mData) + if err != nil { + log.Println("Failed to parse video metadata:", err) + return nil + } + + // Run a second pass if mov/mp4 is detected + if !mov && strings.Contains(mData.Format.FormatName, "mov") { + return getProbeOutput(probeExec, path, true) + } + + return mData } func parseRate(rate string) float64 { diff --git a/framework/math/animation/transformation.go b/framework/math/animation/transformation.go index e4f35fe5..530a2632 100644 --- a/framework/math/animation/transformation.go +++ b/framework/math/animation/transformation.go @@ -48,6 +48,11 @@ type Transformation struct { endValues [4]float64 easing func(float64) float64 startTime, endTime float64 + + repetitions int + loopDelay float64 + + id int64 } func NewBooleanTransform(transformationType TransformationType, startTime, endTime float64) *Transformation { @@ -181,3 +186,29 @@ func (t *Transformation) Clone(startTime, endTime float64) *Transformation { endTime: endTime, } } + +func (t *Transformation) SetLoop(runs int, delay float64) { + t.repetitions = mutils.Max(0, runs-1) + t.loopDelay = delay +} + +func (t *Transformation) IsLoop() bool { + return t.repetitions > 0 +} + +func (t *Transformation) UpdateLoop() { + if t.repetitions > 0 { + + t.startTime += t.loopDelay + t.endTime += t.loopDelay + t.repetitions-- + } +} + +func (t *Transformation) SetID(id int64) { + t.id = id +} + +func (t *Transformation) GetID() int64 { + return t.id +} diff --git a/framework/platform/gl.go b/framework/platform/gl.go new file mode 100644 index 00000000..d78b4299 --- /dev/null +++ b/framework/platform/gl.go @@ -0,0 +1,115 @@ +package platform + +import "C" +import ( + "fmt" + "github.com/go-gl/gl/v3.3-core/gl" + "github.com/go-gl/glfw/v3.3/glfw" + "github.com/wieku/danser-go/framework/graphics/hacks" + "log" + "strings" + "unsafe" +) + +// SetupContext sets glfw hints about OpenGL version +func SetupContext() { + glfw.WindowHint(glfw.ContextVersionMajor, 3) + glfw.WindowHint(glfw.ContextVersionMinor, 3) + glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) + glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) +} + +// GLInit initializes OpenGL, checks for needed extensions, eventually sets up GPU debug logs +func GLInit(debugLogs bool, additionalExtensions ...string) error { + log.Println("Initializing OpenGL...") + + err := gl.Init() + if err != nil { + return err + } + + err = extensionCheck(additionalExtensions) + if err != nil { + return err + } + + glVendor := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.VENDOR)))) + glRenderer := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.RENDERER)))) + glVersion := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.VERSION)))) + glslVersion := C.GoString((*C.char)(unsafe.Pointer(gl.GetString(gl.SHADING_LANGUAGE_VERSION)))) + + lVendor := strings.ToLower(glVendor) + + // HACK HACK HACK: please see github.com/wieku/danser-go/framework/graphics/hacks.IsIntel for more info + if strings.Contains(lVendor, "intel") { + hacks.IsIntel = true + } + + // HACK HACK HACK: please see github.com/wieku/danser-go/framework/graphics/hacks.IsOldAMD for more info + if (strings.Contains(lVendor, "amd") || strings.Contains(lVendor, "ati")) && strings.Contains(glVersion, "15.201.") { + hacks.IsOldAMD = true + } + + var extensions string + + var numExtensions int32 + gl.GetIntegerv(gl.NUM_EXTENSIONS, &numExtensions) + + for i := int32(0); i < numExtensions; i++ { + extensions += C.GoString((*C.char)(unsafe.Pointer(gl.GetStringi(gl.EXTENSIONS, uint32(i))))) + extensions += " " + } + + log.Println("GL Vendor: ", glVendor) + log.Println("GL Renderer: ", glRenderer) + log.Println("GL Version: ", glVersion) + log.Println("GLSL Version: ", glslVersion) + log.Println("GL Extensions:", extensions) + log.Println("OpenGL initialized!") + + if debugLogs { + gl.Enable(gl.DEBUG_OUTPUT) + gl.DebugMessageCallback(func( + source uint32, + glType uint32, + id uint32, + severity uint32, + length int32, + message string, + userParam unsafe.Pointer) { + log.Println("GL:", message) + }, gl.Ptr(nil)) + + gl.DebugMessageControl(gl.DONT_CARE, gl.DONT_CARE, gl.DONT_CARE, 0, nil, true) + } + + return nil +} + +func extensionCheck(additionalExtensions []string) error { + extensions := []string{ + "GL_ARB_clear_texture", + "GL_ARB_direct_state_access", + "GL_ARB_texture_storage", + "GL_ARB_vertex_attrib_binding", + "GL_ARB_buffer_storage", + } + + if additionalExtensions != nil { + extensions = append(extensions, additionalExtensions...) + } + + var notSupported []string + + for _, ext := range extensions { + if !glfw.ExtensionSupported(ext) { + notSupported = append(notSupported, ext) + } + } + + if len(notSupported) > 0 { + return fmt.Errorf("your GPU does not support one or more required OpenGL extensions: %s. Please update your graphics drivers or upgrade your GPU", notSupported) + } + + return nil +} diff --git a/framework/platform/keyname.go b/framework/platform/keyname.go new file mode 100644 index 00000000..97816815 --- /dev/null +++ b/framework/platform/keyname.go @@ -0,0 +1,91 @@ +package platform + +import "C" +import ( + "github.com/go-gl/glfw/v3.3/glfw" + "strconv" + "strings" +) + +func GetKeyName(key glfw.Key, scancode int) string { + if key >= glfw.KeyF1 && key <= glfw.KeyF25 { + return "F" + strconv.Itoa(int(key-glfw.KeyF1)+1) + } else if key >= glfw.KeyKP0 && key <= glfw.KeyKP9 { + return "NUMPAD" + strconv.Itoa(int(key-glfw.KeyKP0)) + } else { + switch key { + case glfw.KeyKPDecimal: + return "NUMPADDECIMAL" + case glfw.KeyKPDivide: + return "NUMPADDIVIDE" + case glfw.KeyKPMultiply: + return "NUMPADMULTIPLY" + case glfw.KeyKPSubtract: + return "NUMPADSUBTRACT" + case glfw.KeyKPAdd: + return "NUMPADADD" + case glfw.KeyKPEnter: + return "NUMPADENTER" + case glfw.KeyKPEqual: + return "NUMPADEQUAL" + case glfw.KeyEscape: + return "ESCAPE" + case glfw.KeyEnter: + return "ENTER" + case glfw.KeyTab: + return "TAB" + case glfw.KeyBackspace: + return "BACKSPACE" + case glfw.KeyInsert: + return "INSERT" + case glfw.KeyDelete: + return "DELETE" + case glfw.KeyRight: + return "RIGHT" + case glfw.KeyLeft: + return "LEFT" + case glfw.KeyDown: + return "DOWN" + case glfw.KeyUp: + return "UP" + case glfw.KeyPageUp: + return "PAGEUP" + case glfw.KeyPageDown: + return "PAGEDOWN" + case glfw.KeyHome: + return "HOME" + case glfw.KeyEnd: + return "END" + case glfw.KeyCapsLock: + return "CAPS" + case glfw.KeyScrollLock: + return "SCROLLLOCK" + case glfw.KeyNumLock: + return "NUMLOCK" + case glfw.KeyPrintScreen: + return "PRINTSCREEN" + case glfw.KeyPause: + return "PAUSE" + case glfw.KeyLeftShift: + return "LSHIFT" + case glfw.KeyLeftControl: + return "LCTRL" + case glfw.KeyLeftAlt: + return "LALT" + case glfw.KeyLeftSuper: + return "LSUPER" + case glfw.KeyRightShift: + return "RSHIFT" + case glfw.KeyRightControl: + return "RCTRL" + case glfw.KeyRightAlt: + return "RALT" + case glfw.KeyRightSuper: + return "RSUPER" + case glfw.KeySpace: + return "SPACE" + default: + return strings.ToUpper(glfw.GetKeyName(key, scancode)) + } + } +} diff --git a/go.mod b/go.mod index 4d803cca..8de742be 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/wieku/danser-go -go 1.18 +go 1.19 require ( github.com/EdlinOrg/prominentcolor v1.0.0 diff --git a/launcher/configeditor.go b/launcher/configeditor.go index e1b4b5b9..0cb8e726 100644 --- a/launcher/configeditor.go +++ b/launcher/configeditor.go @@ -2,6 +2,7 @@ package launcher import ( "fmt" + "github.com/go-gl/glfw/v3.3/glfw" "github.com/inkyblackness/imgui-go/v4" "github.com/sqweek/dialog" "github.com/wieku/danser-go/app/settings" @@ -9,7 +10,7 @@ import ( "github.com/wieku/danser-go/framework/math/color" "github.com/wieku/danser-go/framework/math/math32" "github.com/wieku/danser-go/framework/math/mutils" - "log" + "github.com/wieku/danser-go/framework/platform" "os" "path/filepath" "reflect" @@ -40,6 +41,13 @@ type settingsEditor struct { lastActive string pwShowHide map[string]bool comboSearch map[string]string + + keyChange string + keyChangeVal reflect.Value + keyChangeOpened bool + danserRunning bool + + saveListener func() } func newSettingsEditor(config *settings.Config) *settingsEditor { @@ -61,92 +69,149 @@ func newSettingsEditor(config *settings.Config) *settingsEditor { return editor } +func (editor *settingsEditor) updateKey(_ *glfw.Window, key glfw.Key, scancode int, action glfw.Action, _ glfw.ModifierKey) { + if editor.opened && editor.keyChange != "" && action == glfw.Press { + keyText := platform.GetKeyName(key, scancode) + + if keyText != "" { + editor.keyChangeVal.SetString(keyText) + editor.keyChangeOpened = false + editor.keyChange = "" + } + } +} + +func (editor *settingsEditor) setDanserRunning(running bool) { + editor.danserRunning = running +} + +func (editor *settingsEditor) setSaveListener(saveListener func()) { + editor.saveListener = saveListener +} + func (editor *settingsEditor) drawEditor() { + imgui.PushItemFlag(imgui.ItemFlagsDisabled, false) + settings.General.OsuSkinsDir = editor.combined.General.OsuSkinsDir imgui.PushStyleColor(imgui.StyleColorWindowBg, vec4(0, 0, 0, .9)) imgui.PushStyleColor(imgui.StyleColorFrameBg, vec4(.2, .2, .2, 1)) - imgui.PushStyleVarVec2(imgui.StyleVarCellPadding, vec2(2, 0)) + currentRunning := editor.danserRunning - if imgui.BeginTableV("Edit main table", 2, imgui.TableFlagsSizingStretchProp, vec2(-1, -1), -1) { - imgui.PopStyleVar() + imgui.PushFont(Font20) - imgui.TableSetupColumnV("Edit main table 1", imgui.TableColumnFlagsWidthFixed, 0, uint(0)) - imgui.TableSetupColumnV("Edit main table 2", imgui.TableColumnFlagsWidthStretch, 0, uint(1)) + height := imgui.ContentRegionAvail().Y + if currentRunning { + height -= imgui.FrameHeightWithSpacing() + imgui.CurrentStyle().ItemSpacing().Y + } - imgui.TableNextColumn() + imgui.PopFont() + + if imgui.BeginChildV("##EditorUp", vec2(-1, height), false, 0) { + imgui.PushStyleVarVec2(imgui.StyleVarCellPadding, vec2(2, 0)) + if imgui.BeginTableV("Edit main table", 2, imgui.TableFlagsSizingStretchProp, vec2(-1, -1), -1) { + imgui.PopStyleVar() - imgui.PushStyleColor(imgui.StyleColorChildBg, vec4(0, 0, 0, .5)) + imgui.TableSetupColumnV("Edit main table 1", imgui.TableColumnFlagsWidthFixed, 0, uint(0)) + imgui.TableSetupColumnV("Edit main table 2", imgui.TableColumnFlagsWidthStretch, 0, uint(1)) - imgui.PushFont(FontAw) - { + imgui.TableNextColumn() - imgui.PushStyleVarFloat(imgui.StyleVarScrollbarSize, 9) + imgui.PushStyleColor(imgui.StyleColorChildBg, vec4(0, 0, 0, .5)) - if imgui.BeginChildV("##Editor navigation", vec2(imgui.FontSize()*1.5+9, -1), false, imgui.WindowFlagsAlwaysVerticalScrollbar) { - editor.scrollTo = "" + imgui.PushFont(FontAw) + { - imgui.PushStyleVarFloat(imgui.StyleVarFrameRounding, 0) - imgui.PushStyleVarFloat(imgui.StyleVarFrameBorderSize, 0) - imgui.PushStyleVarVec2(imgui.StyleVarItemSpacing, vzero()) + imgui.PushStyleVarFloat(imgui.StyleVarScrollbarSize, 9) - editor.buildNavigationFor(editor.combined) + if imgui.BeginChildV("##Editor navigation", vec2(imgui.FontSize()*1.5+9, -1), false, imgui.WindowFlagsAlwaysVerticalScrollbar) { + editor.scrollTo = "" + + imgui.PushStyleVarFloat(imgui.StyleVarFrameRounding, 0) + imgui.PushStyleVarFloat(imgui.StyleVarFrameBorderSize, 0) + imgui.PushStyleVarVec2(imgui.StyleVarItemSpacing, vzero()) + + editor.buildNavigationFor(editor.combined) + + imgui.PopStyleVar() + imgui.PopStyleVar() + imgui.PopStyleVar() + } imgui.PopStyleVar() - imgui.PopStyleVar() - imgui.PopStyleVar() - } - imgui.PopStyleVar() + imgui.EndChild() + } + imgui.PopFont() - imgui.EndChild() - } - imgui.PopFont() + imgui.PopStyleColor() - imgui.PopStyleColor() + imgui.TableNextColumn() - imgui.TableNextColumn() + imgui.PushFont(Font32) + { + imgui.SetNextItemWidth(-1) - imgui.PushFont(Font32) - { - imgui.SetNextItemWidth(-1) + if searchBox("##Editor search", &editor.searchString) { + editor.search() + } - if searchBox("##Editor search", &editor.searchString) { - editor.search() + if !editor.blockSearch && !imgui.IsAnyItemActive() && !imgui.IsMouseClicked(0) { + imgui.SetKeyboardFocusHereV(-1) + } } + imgui.PopFont() - if !editor.blockSearch && !imgui.IsAnyItemActive() && !imgui.IsMouseClicked(0) { - imgui.SetKeyboardFocusHereV(-1) - } - } - imgui.PopFont() + imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, vec2(5, 0)) - imgui.PushStyleVarVec2(imgui.StyleVarWindowPadding, vec2(5, 0)) + if imgui.BeginChildV("##Editor main", vec2(-1, -1), false, imgui.WindowFlagsAlwaysUseWindowPadding) { + imgui.PopStyleVar() - if imgui.BeginChildV("##Editor main", vec2(-1, -1), false, imgui.WindowFlagsAlwaysUseWindowPadding) { - imgui.PopStyleVar() + editor.blockSearch = false - editor.blockSearch = false + imgui.PushFont(Font20) - imgui.PushFont(Font20) + editor.drawSettings() - editor.drawSettings() + imgui.PopFont() + } else { + imgui.PopStyleVar() + } - imgui.PopFont() + imgui.EndChild() + + imgui.EndTable() } else { imgui.PopStyleVar() } + } - imgui.EndChild() + imgui.EndChild() - imgui.EndTable() - } else { - imgui.PopStyleVar() + imgui.PushFont(Font20) + + if currentRunning { + centerTable("tabdanser is running", -1, func() { + imgui.AlignTextToFramePadding() + imgui.Text("Danser is running! Click") + imgui.SameLine() + if imgui.Button("Apply##drunning") { + if editor.saveListener != nil { + editor.saveListener() + } + } + imgui.SameLine() + imgui.Text("to see changes.") + }) } + imgui.PopFont() + imgui.PopStyleColor() imgui.PopStyleColor() + + imgui.PopItemFlag() } func (editor *settingsEditor) search() { @@ -310,7 +375,7 @@ func (editor *settingsEditor) drawSettings() { if drawNew { iSc1 := imgui.CursorPos().Y - editor.buildMainSection("##"+dF.Name, "Main."+lbl, lbl, field) + editor.buildMainSection("##"+dF.Name, "Main."+lbl, lbl, field, dF) iSc2 := imgui.CursorPos().Y @@ -328,7 +393,9 @@ func (editor *settingsEditor) drawSettings() { } } -func (editor *settingsEditor) buildMainSection(jsonPath, sPath, name string, u reflect.Value) { +func (editor *settingsEditor) buildMainSection(jsonPath, sPath, name string, u reflect.Value, d reflect.StructField) { + dRunLock := editor.tryLockLive(d) + posLocal := imgui.CursorPos() imgui.PushFont(Font48) @@ -345,17 +412,13 @@ func (editor *settingsEditor) buildMainSection(jsonPath, sPath, name string, u r if scrY >= posLocal.Y-padY*2 && scrY <= posLocal1.Y { editor.active = name } -} -func (editor *settingsEditor) subSectionTempl(sPath, name string, first, last bool, afterTitle, content func()) { - if editor.searchCache[sPath] == 0 { - return - } - - if !first { - imgui.Dummy(vec2(0, padY/2)) + if dRunLock { + editor.unlockLive(true) } +} +func (editor *settingsEditor) subSectionTempl(name string, afterTitle, content func()) { pos := imgui.CursorScreenPos() imgui.Dummy(vec2(3, 0)) @@ -383,20 +446,22 @@ func (editor *settingsEditor) subSectionTempl(sPath, name string, first, last bo pos1.X = pos.X imgui.WindowDrawList().AddLine(pos, pos1, imgui.PackedColorFromVec4(vec4(1.0, 1.0, 1.0, 1.0))) - - if !last { - imgui.Dummy(vec2(0, padY/2)) - } } -func (editor *settingsEditor) buildSubSection(jsonPath, sPath, name string, u reflect.Value, d reflect.StructField, first, last bool) { - editor.subSectionTempl(sPath, name, first, last, func() {}, func() { +func (editor *settingsEditor) buildSubSection(jsonPath, sPath, name string, u reflect.Value, d reflect.StructField) { + dRunLock := editor.tryLockLive(d) + + editor.subSectionTempl(name, func() {}, func() { editor.traverseChildren(jsonPath, sPath, u, d) }) + + if dRunLock { + editor.unlockLive(true) + } } -func (editor *settingsEditor) buildArray(jsonPath, sPath, name string, u reflect.Value, d reflect.StructField, first, last bool) { - editor.subSectionTempl(sPath, name, first, last, func() { +func (editor *settingsEditor) buildArray(jsonPath, sPath, name string, u reflect.Value, d reflect.StructField) { + editor.subSectionTempl(name, func() { imgui.SameLine() imgui.Dummy(vec2(2, 0)) imgui.SameLine() @@ -499,7 +564,11 @@ func (editor *settingsEditor) traverseChildren(jsonPath, lPath string, u reflect skipMap := make(map[string]uint8) consumed := make(map[string]uint8) - for i, index := 0, 0; i < count; i++ { + notFirst := false + wasRendered := false + wasSection := false + + for i := 0; i < count; i++ { field := typ.Field(i) dF := def.Field(i) @@ -529,57 +598,81 @@ func (editor *settingsEditor) traverseChildren(jsonPath, lPath string, u reflect } } - if index > 0 { + if wasRendered { + notFirst = true + imgui.Dummy(vec2(0, 2)) } + wasRendered = true + switch field.Type().Kind() { - case reflect.String: - if _, ok := dF.Tag.Lookup("vector"); ok { - lName, ok1 := dF.Tag.Lookup("left") - rName, ok2 := dF.Tag.Lookup("right") - if !ok1 || !ok2 { - break - } + case reflect.String, reflect.Float64, reflect.Int64, reflect.Int, reflect.Int32, reflect.Bool, reflect.Slice, reflect.Ptr: + if wasSection { + imgui.Dummy(vec2(0, padY/2)) + } - l := typ.FieldByName(lName) - ld, _ := def.FieldByName(lName) + isSection := false - r := typ.FieldByName(rName) - rd, _ := def.FieldByName(rName) + switch field.Type().Kind() { + case reflect.String: + if _, ok := dF.Tag.Lookup("vector"); ok { + lName, ok1 := dF.Tag.Lookup("left") + rName, ok2 := dF.Tag.Lookup("right") + if !ok1 || !ok2 { + break + } - jsonPathL := jsonPath + "." + lName - jsonPathR := jsonPath + "." + rName + l := typ.FieldByName(lName) + ld, _ := def.FieldByName(lName) - editor.buildVector(jsonPathL, jsonPathR, dF, l, ld, r, rd) - } else { - editor.buildString(jsonPath1, field, dF) - } - case reflect.Float64: - editor.buildFloat(jsonPath1, field, dF) - case reflect.Int64, reflect.Int, reflect.Int32: - editor.buildInt(jsonPath1, field, dF) - case reflect.Bool: - editor.buildBool(jsonPath1, field, dF) - case reflect.Slice: - editor.buildArray(jsonPath1, sPath2, label, field, dF, index == 0, index == count-1) - case reflect.Ptr: - if field.Type().AssignableTo(reflect.TypeOf(&settings.HSV{})) { - editor.buildColor(jsonPath1, field, dF, true) - } else if !field.IsNil() { - if dF.Anonymous { - editor.traverseChildren(jsonPath, sPath2, field, dF) - } else if field.CanInterface() { - editor.buildSubSection(jsonPath1, sPath2, label, field, dF, index == 0, index == count-1) + r := typ.FieldByName(rName) + rd, _ := def.FieldByName(rName) + + jsonPathL := jsonPath + "." + lName + jsonPathR := jsonPath + "." + rName + + editor.buildVector(jsonPathL, jsonPathR, dF, l, ld, r, rd) } else { - index-- + editor.buildString(jsonPath1, field, dF) + } + case reflect.Float64: + editor.buildFloat(jsonPath1, field, dF) + case reflect.Int64, reflect.Int, reflect.Int32: + editor.buildInt(jsonPath1, field, dF) + case reflect.Bool: + editor.buildBool(jsonPath1, field, dF) + case reflect.Slice: + if notFirst { + imgui.Dummy(vec2(0, padY/2)) + } + + editor.buildArray(jsonPath1, sPath2, label, field, dF) + isSection = true + case reflect.Ptr: + if field.Type().AssignableTo(reflect.TypeOf(&settings.HSV{})) { + editor.buildColor(jsonPath1, field, dF, true) + } else if !field.IsNil() { + if dF.Anonymous { + editor.traverseChildren(jsonPath, sPath2, field, dF) + } else if field.CanInterface() { + if notFirst { + imgui.Dummy(vec2(0, padY/2)) + } + + editor.buildSubSection(jsonPath1, sPath2, label, field, dF) + isSection = true + } else { + isSection = wasSection + wasRendered = false + } } } + + wasSection = isSection default: - index-- + wasRendered = false } - - index++ } } @@ -657,7 +750,7 @@ func (editor *settingsEditor) getLabel(d reflect.StructField) string { } func (editor *settingsEditor) buildBool(jsonPath string, f reflect.Value, d reflect.StructField) { - editor.drawComponent(jsonPath, editor.getLabel(d), false, true, d, func() { + editor.drawComponent(jsonPath, editor.getLabel(d), false, true, -1, d, func() { base := f.Bool() if imgui.Checkbox(jsonPath, &base) { @@ -668,7 +761,7 @@ func (editor *settingsEditor) buildBool(jsonPath string, f reflect.Value, d refl } func (editor *settingsEditor) buildVector(jsonPath1, jsonPath2 string, d reflect.StructField, l reflect.Value, ld reflect.StructField, r reflect.Value, rd reflect.StructField) { - editor.drawComponent(jsonPath1+"\n"+jsonPath2, editor.getLabel(d), false, false, d, func() { + editor.drawComponent(jsonPath1+"\n"+jsonPath2, editor.getLabel(d), false, false, -1, d, func() { contentAvail := imgui.ContentRegionAvail().X if imgui.BeginTableV("tv"+jsonPath1, 3, imgui.TableFlagsSizingStretchProp, vec2(contentAvail, 0), contentAvail) { @@ -741,7 +834,14 @@ func (editor *settingsEditor) buildIntBox(jsonPath string, f reflect.Value, d re } func (editor *settingsEditor) buildString(jsonPath string, f reflect.Value, d reflect.StructField) { - editor.drawComponent(jsonPath, editor.getLabel(d), d.Tag.Get("long") != "", false, d, func() { + cWidth := float32(-1) + _, okKey := d.Tag.Lookup("key") + + if okKey { + cWidth = 120 + } + + editor.drawComponent(jsonPath, editor.getLabel(d), d.Tag.Get("long") != "", false, cWidth, d, func() { imgui.SetNextItemWidth(-1) base := f.String() @@ -752,7 +852,34 @@ func (editor *settingsEditor) buildString(jsonPath string, f reflect.Value, d re cFunc, okCS := d.Tag.Lookup("comboSrc") _, okPW := d.Tag.Lookup("password") - if okP || okF { + if okKey { + if imgui.ButtonV(base+"##"+jsonPath, vec2(-1, 0)) { + editor.keyChangeVal = f + editor.keyChange = jsonPath + editor.keyChangeOpened = true + } + + if editor.keyChange == jsonPath { + imgui.SetNextWindowFocus() + + popupSmall("KeyChange"+jsonPath, &editor.keyChangeOpened, true, func() { + width := imgui.CalcTextSize("Click outside this box to cancel", false, 0).X + 30 + + centerTable("KeyChange1"+jsonPath, width, func() { + imgui.Text("Press any key...") + }) + + centerTable("KeyChange2"+jsonPath, width, func() { + imgui.Text("Click outside this box to cancel") + }) + }) + + if !editor.keyChangeOpened { + editor.keyChange = "" + } + } + + } else if okP || okF { if imgui.BeginTableV("tbr"+jsonPath, 2, imgui.TableFlagsSizingStretchProp, vec2(-1, 0), -1) { imgui.TableSetupColumnV("tbr1"+jsonPath, imgui.TableColumnFlagsWidthStretch, 0, uint(0)) imgui.TableSetupColumnV("tbr2"+jsonPath, imgui.TableColumnFlagsWidthFixed, 0, uint(1)) @@ -768,16 +895,10 @@ func (editor *settingsEditor) buildString(jsonPath string, f reflect.Value, d re imgui.TableNextColumn() if imgui.Button("Browse" + jsonPath) { - dir := filepath.Join(env.DataDir(), base) + dir := getAbsPath(base) - if strings.TrimSpace(base) != "" { - if filepath.IsAbs(base) { - dir = base - } - - if okF { - dir = filepath.Dir(dir) - } + if strings.TrimSpace(base) != "" && okF { + dir = filepath.Dir(dir) } if _, err := os.Lstat(dir); err != nil { @@ -795,15 +916,11 @@ func (editor *settingsEditor) buildString(jsonPath string, f reflect.Value, d re } if err == nil { - log.Println(p) - log.Println(env.DataDir()) oD := strings.TrimSuffix(strings.ReplaceAll(base, "\\", "/"), "/") nD := strings.TrimSuffix(strings.ReplaceAll(p, "\\", "/"), "/") - dD := strings.TrimSuffix(strings.ReplaceAll(env.DataDir(), "\\", "/"), "/") + "/" - if nD != oD { - f.SetString(strings.ReplaceAll(strings.TrimPrefix(nD, dD), "/", string(os.PathSeparator))) + f.SetString(getRelativeOrABSPath(p)) } } } @@ -966,7 +1083,7 @@ func (editor *settingsEditor) buildInt(jsonPath string, f reflect.Value, d refle _, okS := d.Tag.Lookup("string") cSpec, okC := d.Tag.Lookup("combo") - editor.drawComponent(jsonPath, editor.getLabel(d), !okS && !okC, false, d, func() { + editor.drawComponent(jsonPath, editor.getLabel(d), !okS && !okC, false, -1, d, func() { imgui.SetNextItemWidth(-1) format := firstOf(d.Tag.Get("format"), "%d") @@ -977,7 +1094,14 @@ func (editor *settingsEditor) buildInt(jsonPath string, f reflect.Value, d refle lb := fmt.Sprintf(format, base) + hasCustom := false + for _, s := range strings.Split(cSpec, ",") { + if s == "custom" { + hasCustom = true + continue + } + splt := strings.Split(s, "|") c, _ := strconv.Atoi(splt[0]) @@ -1005,6 +1129,32 @@ func (editor *settingsEditor) buildInt(jsonPath string, f reflect.Value, d refle } } + if hasCustom { + min := parseIntOr(d.Tag.Get("min"), 0) + max := parseIntOr(d.Tag.Get("max"), 100) + + if base >= int32(min) { + pad := vec2(imgui.CurrentStyle().FramePadding().X, imgui.CurrentStyle().ItemSpacing().Y*0.5) + scPos := imgui.CursorScreenPos().Minus(pad) + + imgui.WindowDrawList().AddRectFilled(scPos, scPos.Plus(vec2(imgui.ContentRegionAvail().X, imgui.FrameHeight()).Plus(pad.Times(2))), imgui.PackedColorFromVec4(imgui.CurrentStyle().Color(imgui.StyleColorHeader))) + } else { + base = 0 + } + + imgui.AlignTextToFramePadding() + imgui.Text("Custom:") + + imgui.SameLine() + + imgui.SetNextItemWidth(imgui.ContentRegionAvail().X) + + if imgui.InputIntV(jsonPath, &base, 1, 1, 0) { + base = mutils.Clamp(base, int32(min), int32(max)) + f.SetInt(int64(base)) + } + } + imgui.EndCombo() } } else if okS { @@ -1034,7 +1184,7 @@ func (editor *settingsEditor) buildInt(jsonPath string, f reflect.Value, d refle } func (editor *settingsEditor) buildFloat(jsonPath string, f reflect.Value, d reflect.StructField) { - editor.drawComponent(jsonPath, editor.getLabel(d), d.Tag.Get("string") == "", false, d, func() { + editor.drawComponent(jsonPath, editor.getLabel(d), d.Tag.Get("string") == "", false, -1, d, func() { imgui.SetNextItemWidth(-1) if d.Tag.Get("string") != "" { @@ -1092,16 +1242,22 @@ func (editor *settingsEditor) buildColor(jsonPath string, f reflect.Value, d ref } if withLabel { - editor.drawComponent(jsonPath, editor.getLabel(d), false, false, d, dComp) + editor.drawComponent(jsonPath, editor.getLabel(d), false, false, -1, d, dComp) } else { dComp() } } -func (editor *settingsEditor) drawComponent(jsonPath, label string, long, checkbox bool, d reflect.StructField, draw func()) { +func (editor *settingsEditor) drawComponent(jsonPath, label string, long, checkbox bool, customWidth float32, d reflect.StructField, draw func()) { + dRunLock := editor.tryLockLive(d) + width := imgui.FontSize() + imgui.CurrentStyle().FramePadding().X*2 - 1 // + imgui.CurrentStyle().ItemSpacing().X if !checkbox { - width = 240 + imgui.CalcTextSize("x", false, 0).X + imgui.CurrentStyle().FramePadding().X*4 + if customWidth > 0 { + width = customWidth + } else { + width = 240 + imgui.CalcTextSize("x", false, 0).X + imgui.CurrentStyle().FramePadding().X*4 + } } cCount := 1 @@ -1111,7 +1267,7 @@ func (editor *settingsEditor) drawComponent(jsonPath, label string, long, checkb contentAvail := imgui.ContentRegionAvail().X - if imgui.BeginTableV("lbl"+jsonPath, cCount, imgui.TableFlagsSizingStretchProp|imgui.TableFlagsNoPadInnerX|imgui.TableFlagsNoPadOuterX, vec2(contentAvail, 0), contentAvail) { + if imgui.BeginTableV("lbl"+jsonPath, cCount, imgui.TableFlagsSizingStretchProp|imgui.TableFlagsNoPadInnerX|imgui.TableFlagsNoPadOuterX|imgui.TableFlagsNoClip, vec2(contentAvail, 0), contentAvail) { if !long { imgui.TableSetupColumnV("lbl1"+jsonPath, imgui.TableColumnFlagsWidthFixed, contentAvail-width, uint(0)) imgui.TableSetupColumnV("lbl2"+jsonPath, imgui.TableColumnFlagsWidthFixed, width, uint(1)) @@ -1121,34 +1277,45 @@ func (editor *settingsEditor) drawComponent(jsonPath, label string, long, checkb imgui.TableNextColumn() + tooltip, hasTooltip := d.Tag.Lookup("tooltip") + + if hasTooltip { + label = "(!) " + label + } + imgui.BeginGroup() imgui.AlignTextToFramePadding() imgui.Text(label) imgui.EndGroup() if imgui.IsItemHovered() { - imgui.BeginTooltip() + _, hidePath := d.Tag.Lookup("hidePath") - _, hPath := d.Tag.Lookup("hidePath") + showPath := !hidePath && launcherConfig.ShowJSONPaths - tTip := "" - if !hPath { - tTip = strings.ReplaceAll(jsonPath, "#", "") - } + if showPath || hasTooltip { + imgui.BeginTooltip() - if t, ok := d.Tag.Lookup("tooltip"); ok { - if !hPath { - tTip += "\n\n" + tTip := "" + if showPath { + tTip = strings.ReplaceAll(jsonPath, "#", "") } - tTip += t - } - imgui.PushTextWrapPosV(400) + if hasTooltip { + if showPath { + tTip += "\n\n" + } + + tTip += tooltip + } + + imgui.PushTextWrapPosV(400) - imgui.Text(tTip) + imgui.Text(tTip) - imgui.PopTextWrapPos() - imgui.EndTooltip() + imgui.PopTextWrapPos() + imgui.EndTooltip() + } } imgui.TableNextColumn() @@ -1157,6 +1324,46 @@ func (editor *settingsEditor) drawComponent(jsonPath, label string, long, checkb imgui.EndTable() } + + if dRunLock { + editor.unlockLive(false) + } +} + +func (editor *settingsEditor) tryLockLive(d reflect.StructField) bool { + liveEdit := true + + if l, ok := d.Tag.Lookup("liveedit"); ok && l == "false" { + liveEdit = false + } + + dRunLock := !liveEdit && editor.danserRunning + + if dRunLock { + imgui.BeginGroup() + imgui.PushItemFlag(imgui.ItemFlagsDisabled, true) + imgui.PushStyleColor(imgui.StyleColorText, vec4(0.6, 0.6, 0.6, 1)) + } + + return dRunLock +} + +func (editor *settingsEditor) unlockLive(plural bool) { + imgui.PopStyleColor() + imgui.PopItemFlag() + imgui.EndGroup() + + if imgui.IsItemHovered() { + imgui.BeginTooltip() + + if plural { + imgui.Text("These options can't be edited while danser is running.") + } else { + imgui.Text("This option can't be edited while danser is running.") + } + + imgui.EndTooltip() + } } func parseIntOr(value string, alt int) int { diff --git a/launcher/imgui.go b/launcher/imgui.go index e579f386..3374cc38 100644 --- a/launcher/imgui.go +++ b/launcher/imgui.go @@ -119,13 +119,13 @@ func SetupImgui(win *glfw.Window) { img0 := ImIO.Fonts().TextureDataAlpha8() img1 := ImIO.Fonts().TextureDataRGBA32() - tex = texture.NewTextureSingleFormat(img1.Width, img1.Height, texture.RGBA, 0) // mip-mapping fails miserably because igui doesn't apply padding to sub-textures + tex = texture.NewTextureSingleFormat(img0.Width, img0.Height, texture.Red, 0) // mip-mapping fails miserably because igui doesn't apply padding to sub-textures - size := img1.Width * img1.Height * 4 + size := img0.Width * img0.Height - pixels := (*[1 << 30]uint8)(img1.Pixels)[:size:size] // cast from unsafe pointer to uint8 slice + pixels := (*[1 << 30]uint8)(img0.Pixels)[:size:size] // cast from unsafe pointer to uint8 slice - tex.SetData(0, 0, img1.Width, img1.Height, pixels) + tex.SetData(0, 0, img0.Width, img0.Height, pixels) C.free(img0.Pixels) //Reduce some memory, seems that imgui doesn't explode C.free(img1.Pixels) //Reduce some memory, seems that imgui doesn't explode @@ -157,6 +157,8 @@ func SetupImgui(win *glfw.Window) { }) input.Win.SetKeyCallback(func(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { + input.CallListeners(w, key, scancode, action, mods) + if action != glfw.Press && action != glfw.Release { return } @@ -425,12 +427,15 @@ var lastTime float64 func Begin() { x, y := input.Win.GetCursorPos() - ImIO.AddMousePosEvent(imgui.Vec2{X: float32(x), Y: float32(y)}) + w, h := int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()) //input.Win.GetFramebufferSize() + _, h1 := glfw.GetCurrentContext().GetFramebufferSize() + + scaling := float32(h1) / float32(h) + + ImIO.AddMousePosEvent(imgui.Vec2{X: float32(x) / scaling, Y: float32(y) / scaling}) ImIO.AddMouseButtonEvent(0, input.Win.GetMouseButton(glfw.MouseButtonLeft) == glfw.Press) ImIO.AddMouseButtonEvent(1, input.Win.GetMouseButton(glfw.MouseButtonRight) == glfw.Press) - w, h := input.Win.GetFramebufferSize() - ImIO.SetDisplaySize(imgui.Vec2{X: float32(w), Y: float32(h)}) time := glfw.GetTime() @@ -455,12 +460,13 @@ func DrawImgui() { rShader.Bind() - w, h := input.Win.GetFramebufferSize() + w, h := int(settings.Graphics.GetWidth()), int(settings.Graphics.GetHeight()) //input.Win.GetFramebufferSize() rShader.SetUniform("proj", mgl32.Ortho(0, float32(w), float32(h), 0, -1, 1)) tex.Bind(0) rShader.SetUniform("tex", 0) + rShader.SetUniform("texRGBA", 0) lastBound := imgui.TextureID(0) @@ -471,6 +477,10 @@ func DrawImgui() { blend.Enable() blend.SetFunction(blend.SrcAlpha, blend.OneMinusSrcAlpha) + _, h1 := glfw.GetCurrentContext().GetFramebufferSize() + + scaling := float32(h1) / float32(h) + for _, list := range drawData.CommandLists() { vertexBuffer, vertexBufferSize := list.VertexBuffer() vertexBufferSize /= 4 // convert size in bytes to size in float32 @@ -494,8 +504,10 @@ func DrawImgui() { cId := cmd.TextureID() if cId != lastBound { if cId == 0 { + rShader.SetUniform("texRGBA", 0) tex.Bind(0) } else { + rShader.SetUniform("texRGBA", 1) gl.BindTextureUnit(0, uint32(cId)) } @@ -505,8 +517,9 @@ func DrawImgui() { if cmd.HasUserCallback() { cmd.CallUserCallback(list) } else { - clipRect := cmd.ClipRect() - viewport.PushScissorPos(int(clipRect.X), int(settings.Graphics.GetHeight())-int(clipRect.W), int(clipRect.Z-clipRect.X), int(clipRect.W-clipRect.Y)) + clipRect := cmd.ClipRect().Times(scaling) + + viewport.PushScissorPos(int(clipRect.X), int(float32(h1)-clipRect.W), int(clipRect.Z-clipRect.X), int(clipRect.W-clipRect.Y)) ibo.DrawPart(cmd.IndexOffset(), cmd.ElementCount()) diff --git a/launcher/knockoutmanager.go b/launcher/knockoutmanager.go new file mode 100644 index 00000000..67463aea --- /dev/null +++ b/launcher/knockoutmanager.go @@ -0,0 +1,149 @@ +package launcher + +import ( + "fmt" + "github.com/inkyblackness/imgui-go/v4" + "github.com/wieku/danser-go/app/beatmap/difficulty" + "github.com/wieku/danser-go/app/utils" + "github.com/wieku/danser-go/framework/math/mutils" + "strconv" +) + +type knockoutManagerPopup struct { + *popup + + bld *builder + + includeSwitch bool + + lastSelected int + + countEnabled int +} + +func newKnockoutManagerPopup(bld *builder) *knockoutManagerPopup { + rm := &knockoutManagerPopup{ + popup: newPopup("Replay manager", popBig), + bld: bld, + includeSwitch: true, + lastSelected: -1, + } + + rm.internalDraw = rm.drawManager + + rm.refreshCount() + + return rm +} + +func (km *knockoutManagerPopup) refreshCount() { + countIncluded := 0 + + for _, replay := range km.bld.knockoutReplays { + if replay.included { + countIncluded++ + } + } + + if countIncluded == 0 { + km.includeSwitch = false + } else if countIncluded == len(km.bld.knockoutReplays) { + km.includeSwitch = true + } + + km.countEnabled = countIncluded +} + +func (km *knockoutManagerPopup) drawManager() { + imgui.PushFont(Font20) + + numText := "No replays" + if km.countEnabled == 1 { + numText = "1 replay" + } else if km.countEnabled > 1 { + numText = fmt.Sprintf("%d replays", km.countEnabled) + } + + imgui.Text(numText + " selected") + + imgui.PopFont() + + if imgui.BeginTableV("replay table", 9, imgui.TableFlagsBorders|imgui.TableFlagsScrollY, vec2(-1, imgui.ContentRegionAvail().Y), -1) { + imgui.TableSetupScrollFreeze(0, 1) + + imgui.TableSetupColumnV("", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(0)) + imgui.TableSetupColumnV("Name", imgui.TableColumnFlagsWidthStretch|imgui.TableColumnFlagsNoSort, 0, uint(1)) + imgui.TableSetupColumnV("Score", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(2)) + imgui.TableSetupColumnV("Mods", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(3)) + imgui.TableSetupColumnV("300", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(4)) + imgui.TableSetupColumnV("100", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(5)) + imgui.TableSetupColumnV("50", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(6)) + imgui.TableSetupColumnV("Miss", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(7)) + imgui.TableSetupColumnV("Combo", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(8)) + + imgui.TableHeadersRow() + + imgui.TableSetColumnIndex(0) + + imgui.PushFont(Font20) + + if imgui.Checkbox("##mass replay disable", &km.includeSwitch) { + for _, replay := range km.bld.knockoutReplays { + replay.included = km.includeSwitch + } + + km.refreshCount() + } + + imgui.TableNextRow() + + changed := -1 + + for i, replay := range km.bld.knockoutReplays { + pReplay := replay.parsedReplay + + imgui.TableNextColumn() + + if imgui.Checkbox("##Use"+strconv.Itoa(i), &replay.included) { + changed = i + } + + textColumn(pReplay.Username) + + textColumn(utils.Humanize(pReplay.Score)) + + textColumn(difficulty.Modifier(pReplay.Mods).String()) + + textColumn(utils.Humanize(pReplay.Count300)) + + textColumn(utils.Humanize(pReplay.Count100)) + + textColumn(utils.Humanize(pReplay.Count50)) + + textColumn(utils.Humanize(pReplay.CountMiss)) + + textColumn(utils.Humanize(pReplay.MaxCombo)) + } + + if changed > -1 { + if km.lastSelected > -1 && (imgui.IsKeyDown(imgui.KeyLeftShift) || imgui.IsKeyDown(imgui.KeyRightShift)) { + value := km.bld.knockoutReplays[changed].included + + lower := mutils.Min(km.lastSelected, changed) + higher := mutils.Max(km.lastSelected, changed) + + for i := lower; i <= higher; i++ { + km.bld.knockoutReplays[i].included = value + } + } + + km.lastSelected = changed + + km.refreshCount() + } + + imgui.PopFont() + + imgui.EndTable() + } +} diff --git a/launcher/launcher.go b/launcher/launcher.go index 97b53a1a..24e87a87 100644 --- a/launcher/launcher.go +++ b/launcher/launcher.go @@ -44,6 +44,7 @@ import ( "github.com/wieku/danser-go/framework/env" "github.com/wieku/danser-go/framework/goroutines" "github.com/wieku/danser-go/framework/graphics/batch" + "github.com/wieku/danser-go/framework/graphics/viewport" "github.com/wieku/danser-go/framework/math/animation" "github.com/wieku/danser-go/framework/math/animation/easing" "github.com/wieku/danser-go/framework/math/mutils" @@ -180,6 +181,13 @@ type launcher struct { prevMap *beatmap.BeatMap configSearch string + + lastReplayDir string + lastKnockoutDir string + + knockoutManager *knockoutManagerPopup + + currentEditor *settingsEditor } func StartLauncher() { @@ -217,7 +225,8 @@ func StartLauncher() { settings.Playfield.Background.Triangles.Enabled = true settings.Playfield.Background.Triangles.DrawOverBlur = true settings.Playfield.Background.Blur.Enabled = false - settings.Playfield.Background.Parallax.Enabled = false + settings.Playfield.Background.Parallax.Enabled = true + settings.Playfield.Background.Parallax.Amount = 0.02 assets.Init(build.Stream == "Dev") @@ -304,11 +313,10 @@ func (l *launcher) startGLFW() { l.currentConfig = c - glfw.WindowHint(glfw.ContextVersionMajor, 3) - glfw.WindowHint(glfw.ContextVersionMinor, 3) - glfw.WindowHint(glfw.OpenGLProfile, glfw.OpenGLCoreProfile) - glfw.WindowHint(glfw.OpenGLForwardCompatible, glfw.True) + platform.SetupContext() + glfw.WindowHint(glfw.Resizable, glfw.False) + glfw.WindowHint(glfw.ScaleToMonitor, glfw.True) glfw.WindowHint(glfw.Samples, 4) settings.Graphics.Fullscreen = false @@ -342,9 +350,10 @@ func (l *launcher) startGLFW() { log.Println("GLFW initialized!") - gl.Init() - - extensionCheck() + err = platform.GLInit(false) + if err != nil { + panic("Failed to initialize OpenGL: " + err.Error()) + } glfw.SwapInterval(1) @@ -374,6 +383,12 @@ func (l *launcher) startGLFW() { checkForUpdates(false) } }) + + input.RegisterListener(func(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) { + if l.currentEditor != nil { + l.currentEditor.updateKey(w, key, scancode, action, mods) + } + }) } func (l *launcher) loadBeatmaps() { @@ -425,12 +440,11 @@ func (l *launcher) loadBeatmaps() { } l.win.SetDropCallback(func(w *glfw.Window, names []string) { - if !strings.HasSuffix(names[0], ".osr") { - showMessage(mError, "It's not a replay file!") - return + if len(names) > 1 { + l.trySelectReplaysFromPaths(names) + } else { + l.trySelectReplayFromPath(names[0]) } - - l.trySelectReplayFromPath(names[0]) }) l.win.SetCloseCallback(func(w *glfw.Window) { @@ -450,71 +464,65 @@ func (l *launcher) loadBeatmaps() { } }) - if len(os.Args) > 1 { //won't work in combined mode + if len(os.Args) > 2 { //won't work in combined mode + l.trySelectReplaysFromPaths(os.Args[1:]) + } else if len(os.Args) > 1 { l.trySelectReplayFromPath(os.Args[1]) } else if launcherConfig.LoadLatestReplay { - type lastModPath struct { - tStamp time.Time - path string - } + l.loadLatestReplay() + } - var list []*lastModPath + l.mapsLoaded = true +} - filepath.Walk(l.currentConfig.General.GetReplaysDir(), func(path string, info fs.FileInfo, err error) error { - if info.IsDir() && path != l.currentConfig.General.GetReplaysDir() { - return filepath.SkipDir - } +func (l *launcher) loadLatestReplay() { + replaysDir := l.currentConfig.General.GetReplaysDir() - if !info.IsDir() && strings.HasSuffix(path, ".osr") { - list = append(list, &lastModPath{ - tStamp: info.ModTime(), - path: path, - }) - } + type lastModPath struct { + tStamp time.Time + name string + } - return nil - }) + var list []*lastModPath - slices.SortFunc(list, func(a, b *lastModPath) bool { - return a.tStamp.After(b.tStamp) - }) + entries, err := os.ReadDir(replaysDir) + if err != nil { + return + } - // Load the newest that can be used - for _, lMP := range list { - r, err := l.loadReplay(lMP.path) - if err == nil { - l.trySelectReplay(r) - break + for _, d := range entries { + if !d.IsDir() && strings.HasSuffix(d.Name(), ".osr") { + if info, err1 := d.Info(); err1 == nil { + list = append(list, &lastModPath{ + tStamp: info.ModTime(), + name: d.Name(), + }) } } } - l.mapsLoaded = true -} - -func extensionCheck() { - extensions := []string{ - "GL_ARB_clear_texture", - "GL_ARB_direct_state_access", - "GL_ARB_texture_storage", - "GL_ARB_vertex_attrib_binding", - "GL_ARB_buffer_storage", + if list == nil { + return } - var notSupported []string + slices.SortFunc(list, func(a, b *lastModPath) bool { + return a.tStamp.After(b.tStamp) + }) - for _, ext := range extensions { - if !glfw.ExtensionSupported(ext) { - notSupported = append(notSupported, ext) + // Load the newest that can be used + for _, lMP := range list { + r, err := l.loadReplay(filepath.Join(replaysDir, lMP.name)) + if err == nil { + l.trySelectReplay(r) + break } } - - if len(notSupported) > 0 { - panic(fmt.Sprintf("Your GPU does not support one or more required OpenGL extensions: %s. Please update your graphics drivers or upgrade your GPU", notSupported)) - } } func (l *launcher) Draw() { + w, h := l.win.GetFramebufferSize() + viewport.Push(w, h) + if l.bg.HasBackground() { gl.ClearColor(0, 0, 0, 1.0) } else { @@ -524,8 +532,7 @@ func (l *launcher) Draw() { gl.Clear(gl.COLOR_BUFFER_BIT) gl.Enable(gl.SCISSOR_TEST) - w, h := int(settings.Graphics.WindowWidth), int(settings.Graphics.WindowHeight) - gl.Viewport(0, 0, int32(w), int32(h)) + w, h = int(settings.Graphics.WindowWidth), int(settings.Graphics.WindowHeight) settings.Graphics.Fullscreen = false settings.Graphics.WindowWidth = int64(w) @@ -542,7 +549,10 @@ func (l *launcher) Draw() { settings.Playfield.Background.Triangles.Speed = l.triangleSpeed.GetValue() - l.bg.Update(t, 0, 0) + pX := (float64(imgui.MousePos().X) * 2 / settings.Graphics.GetWidthF()) - 1 + pY := (float64(imgui.MousePos().Y) * 2 / settings.Graphics.GetHeightF()) - 1 + + l.bg.Update(t, -pX, pY) l.batch.SetCamera(mgl32.Ortho(-float32(w)/2, float32(w)/2, float32(h)/2, -float32(h)/2, -1, 1)) @@ -588,11 +598,15 @@ func (l *launcher) Draw() { l.batch.End() l.drawImgui() + + viewport.Pop() } func (l *launcher) drawImgui() { Begin() + resetPopupHierarchyInfo() + lock := l.danserRunning if lock { @@ -850,6 +864,68 @@ func (l *launcher) trySelectReplayFromPath(p string) { l.trySelectReplay(replay) } +func (l *launcher) trySelectReplaysFromPaths(p []string) { + var errorCollection string + var replays []*knockoutReplay + + for _, rPath := range p { + replay, err := l.loadReplay(rPath) + + if err != nil { + if errorCollection != "" { + errorCollection += "\n" + } + + errorCollection += fmt.Sprintf("%s:\n\t%s", filepath.Base(rPath), err) + } else { + replays = append(replays, replay) + } + } + + if errorCollection != "" { + showMessage(mError, "There were errors opening replays:\n%s", errorCollection) + } + + if replays != nil && len(replays) > 0 { + found := false + + for _, replay := range replays { + for _, bMap := range l.beatmaps { + if strings.ToLower(bMap.MD5) == strings.ToLower(replay.parsedReplay.BeatmapMD5) { + l.bld.currentMode = NewKnockout + l.bld.setMap(bMap) + + found = true + break + } + } + + if found { + break + } + } + + if !found { + showMessage(mError, "Replays use an unknown map. Please download the map beforehand.") + } else { + var finalReplays []*knockoutReplay + + for _, replay := range replays { + if strings.ToLower(l.bld.currentMap.MD5) == strings.ToLower(replay.parsedReplay.BeatmapMD5) { + finalReplays = append(finalReplays, replay) + } + } + + slices.SortFunc(finalReplays, func(a, b *knockoutReplay) bool { + return a.parsedReplay.Score > b.parsedReplay.Score + }) + + l.bld.knockoutReplays = finalReplays + l.knockoutManager = newKnockoutManagerPopup(l.bld) + } + } +} + func (l *launcher) trySelectReplay(replay *knockoutReplay) { for _, bMap := range l.beatmaps { if strings.ToLower(bMap.MD5) == strings.ToLower(replay.parsedReplay.BeatmapMD5) { @@ -871,66 +947,19 @@ func (l *launcher) newKnockout() { imgui.PushFont(Font32) if imgui.ButtonV("Select replays", bSize) { - p, err := dialog.File().Filter("osu! replay file (*.osr)", "osr").Title("Select replay files").SetStartDir(env.DataDir()).LoadMultiple() - if err == nil { - var errorCollection string - var replays []*knockoutReplay - - for _, rPath := range p { - replay, err := l.loadReplay(rPath) - - if err != nil { - if errorCollection != "" { - errorCollection += "\n" - } - - errorCollection += fmt.Sprintf("%s:\n\t%s", filepath.Base(rPath), err) - } else { - replays = append(replays, replay) - } - } - - if errorCollection != "" { - showMessage(mError, "There were errors opening replays:\n%s", errorCollection) - } + kPath := getAbsPath(launcherConfig.LastKnockoutPath) - if replays != nil && len(replays) > 0 { - found := false - - for _, replay := range replays { - for _, bMap := range l.beatmaps { - if strings.ToLower(bMap.MD5) == strings.ToLower(replay.parsedReplay.BeatmapMD5) { - l.bld.currentMode = NewKnockout - l.bld.setMap(bMap) - - found = true - break - } - } - - if found { - break - } - } - - if !found { - showMessage(mError, "Replays use an unknown map. Please download the map beforehand.") - } else { - var finalReplays []*knockoutReplay - - for _, replay := range replays { - if strings.ToLower(l.bld.currentMap.MD5) == strings.ToLower(replay.parsedReplay.BeatmapMD5) { - finalReplays = append(finalReplays, replay) - } - } + _, err := os.Lstat(kPath) + if err != nil { + kPath = env.DataDir() + } - slices.SortFunc(finalReplays, func(a, b *knockoutReplay) bool { - return a.parsedReplay.Score > b.parsedReplay.Score - }) + p, err := dialog.File().Filter("osu! replay file (*.osr)", "osr").Title("Select replay files").SetStartDir(kPath).LoadMultiple() + if err == nil { + launcherConfig.LastKnockoutPath = getRelativeOrABSPath(filepath.Dir(p[0])) + saveLauncherConfig() - l.bld.knockoutReplays = finalReplays - } - } + l.trySelectReplaysFromPaths(p) } } @@ -955,10 +984,8 @@ func (l *launcher) newKnockout() { imgui.SameLine() - if imgui.Button("Manage##knockout") { - l.openPopup(newPopupF("Manage replays", popBig, func() { - drawReplayManager(l.bld) - })) + if imgui.Button("Manage##knockout") && l.knockoutManager != nil { + l.openPopup(l.knockoutManager) } } else { imgui.Text("No replays selected") @@ -970,6 +997,10 @@ func (l *launcher) newKnockout() { } func (l *launcher) loadReplay(p string) (*knockoutReplay, error) { + if !strings.HasSuffix(p, ".osr") { + return nil, fmt.Errorf("it's not a replay file") + } + rData, err := os.ReadFile(p) if err != nil { return nil, fmt.Errorf("failed to open file: %s", err) @@ -980,6 +1011,10 @@ func (l *launcher) loadReplay(p string) (*knockoutReplay, error) { return nil, fmt.Errorf("failed to parse replay: %s", err) } + if replay.PlayMode != 0 { + return nil, errors.New("only osu!standard mode is supported") + } + if replay.ReplayData == nil || len(replay.ReplayData) < 2 { return nil, errors.New("replay is missing input data") } @@ -1184,6 +1219,10 @@ func (l *launcher) drawLowerPanel() { } func (l *launcher) drawConfigPanel() { + if l.currentEditor != nil { + l.currentEditor.setDanserRunning(l.danserRunning && l.bld.currentPMode == Watch) + } + w := imgui.WindowContentRegionMax().X imgui.SetCursorPos(vec2(imgui.WindowContentRegionMax().X-float32(w)/2.5, 20)) @@ -1322,19 +1361,18 @@ func (l *launcher) drawConfigPanel() { imgui.TableNextColumn() - if imgui.ButtonV("Edit", vec2(-1, 0)) { - sEditor := newSettingsEditor(l.currentConfig) + dRun := l.danserRunning && l.bld.currentPMode == Watch - sEditor.setCloseListener(func() { - settings.SaveCredentials(false) - l.currentConfig.Save("", false) + if dRun { + imgui.PushItemFlag(imgui.ItemFlagsDisabled, false) + } - if !compareDirs(l.currentConfig.General.OsuSongsDir, settings.General.OsuSongsDir) { - showMessage(mInfo, "This config has different osu! Songs directory.\nRestart the launcher to see updated maps") - } - }) + if imgui.ButtonV("Edit", vec2(-1, 0)) { + l.openCurrentSettingsEditor() + } - l.openPopup(sEditor) + if dRun { + imgui.PopItemFlag() } imgui.EndTable() @@ -1351,20 +1389,7 @@ func (l *launcher) drawConfigPanel() { imgui.SetNextItemWidth(imgui.TextLineHeight() * 10) - if imgui.InputTextV("##nclonename", &l.newCloneName /*imgui.InputTextFlagsCallbackAlways|*/, imgui.InputTextFlagsCallbackCharFilter, func(data imgui.InputTextCallbackData) int32 { - if data.EventFlag() == imgui.InputTextFlagsCallbackCharFilter { - run := data.EventChar() - - switch run { - case '\\': - data.SetEventChar('/') - case '<', '>', '|', '?', '*', ':', '"': - data.SetEventChar(0) - } - } - - return 0 - }) { + if imgui.InputTextV("##nclonename", &l.newCloneName, imgui.InputTextFlagsCallbackCharFilter, imguiPathFilter) { l.newCloneName = strings.TrimSpace(l.newCloneName) } @@ -1384,7 +1409,7 @@ func (l *launcher) drawConfigPanel() { imgui.PushItemFlag(imgui.ItemFlagsDisabled, true) } - if imgui.Button("Save##newclone") { + if imgui.Button("Save##newclone") || (!e && (imgui.IsKeyPressed(imgui.KeyEnter) || imgui.IsKeyPressed(imgui.KeyKeypadEnter))) { _, err := os.Stat(filepath.Join(env.ConfigDir(), l.newCloneName+".json")) if err == nil { showMessage(mError, "Config with that name already exists!\nPlease pick a different name") @@ -1414,6 +1439,25 @@ func (l *launcher) drawConfigPanel() { } } +func (l *launcher) openCurrentSettingsEditor() { + saveFunc := func() { + settings.SaveCredentials(false) + l.currentConfig.Save("", false) + + if !compareDirs(l.currentConfig.General.OsuSongsDir, settings.General.OsuSongsDir) { + showMessage(mInfo, "This config has different osu! Songs directory.\nRestart the launcher to see updated maps") + } + } + + l.currentEditor = newSettingsEditor(l.currentConfig) + + l.currentEditor.setDanserRunning(l.danserRunning) + l.currentEditor.setCloseListener(saveFunc) + l.currentEditor.setSaveListener(saveFunc) + + l.openPopup(l.currentEditor) +} + func (l *launcher) tryCreateDefaultConfig() { _, err := os.Stat(filepath.Join(env.ConfigDir(), "default.json")) if err != nil { @@ -1525,19 +1569,6 @@ func (l *launcher) setConfig(s string) { } } -// covers cases: -// one of them is an abs path to data dir -// has path separator suffixed -// one of them has backwards slashes -func compareDirs(str1, str2 string) bool { - abPath := strings.TrimSuffix(strings.ReplaceAll(env.DataDir(), "\\", "/"), "/") + "/" - - str1D := strings.TrimSuffix(strings.ReplaceAll(str1, "\\", "/"), "/") - str2D := strings.TrimSuffix(strings.ReplaceAll(str2, "\\", "/"), "/") - - return strings.TrimPrefix(str1D, abPath) == strings.TrimPrefix(str2D, abPath) -} - func (l *launcher) startDanser() { l.recordProgress = 0 l.recordStatus = "" @@ -1548,7 +1579,7 @@ func (l *launcher) startDanser() { dExec := os.Args[0] if build.Stream == "Release" { - dExec = filepath.Join(env.LibDir(), "danser") + dExec = filepath.Join(env.LibDir(), build.DanserExec) } l.danserCmd = exec.Command(dExec, l.bld.getArguments()...) @@ -1591,6 +1622,15 @@ func (l *launcher) startDanser() { for sc.Scan() { line := sc.Text() + if strings.Contains(line, "Launcher: Open settings") { + if l.currentEditor == nil || !l.currentEditor.opened { + l.openCurrentSettingsEditor() + } + + l.win.Restore() + l.win.Focus() + } + if strings.Contains(line, "panic:") { panicMessage = line[strings.Index(line, "panic:"):] panicWait.Done() diff --git a/launcher/launcheronfig.go b/launcher/launcheronfig.go index a9e4f516..44282947 100644 --- a/launcher/launcheronfig.go +++ b/launcher/launcheronfig.go @@ -22,6 +22,7 @@ var launcherConfig = &launcherConf{ SortAscending: true, LoadLatestReplay: false, SkipMapUpdate: false, + ShowJSONPaths: false, } type launcherConf struct { @@ -34,6 +35,8 @@ type launcherConf struct { SortAscending bool LoadLatestReplay bool SkipMapUpdate bool + ShowJSONPaths bool + LastKnockoutPath string } func loadLauncherConfig() { diff --git a/launcher/simplepopups.go b/launcher/simplepopups.go index 4d7d2247..3e8a4577 100644 --- a/launcher/simplepopups.go +++ b/launcher/simplepopups.go @@ -2,8 +2,6 @@ package launcher import ( "github.com/inkyblackness/imgui-go/v4" - "github.com/wieku/danser-go/app/beatmap/difficulty" - "github.com/wieku/danser-go/app/utils" "github.com/wieku/danser-go/build" "github.com/wieku/danser-go/framework/graphics/texture" "github.com/wieku/danser-go/framework/math/mutils" @@ -113,7 +111,7 @@ func drawRecordMenu(bld *builder) { imgui.SetNextItemWidth(-1) - imgui.InputText("##oname", &bld.outputName) + imgui.InputTextV("##oname", &bld.outputName, imgui.InputTextFlagsCallbackCharFilter, imguiPathFilter) if bld.currentPMode == Screenshot { imgui.TableNextColumn() @@ -157,70 +155,6 @@ func drawRecordMenu(bld *builder) { } } -func drawReplayManager(bld *builder) { - if imgui.BeginTableV("replay table", 9, imgui.TableFlagsBorders|imgui.TableFlagsScrollY, vec2(-1, imgui.ContentRegionAvail().Y), -1) { - imgui.TableSetupScrollFreeze(0, 1) - - imgui.TableSetupColumnV("", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(0)) - imgui.TableSetupColumnV("Name", imgui.TableColumnFlagsWidthStretch|imgui.TableColumnFlagsNoSort, 0, uint(1)) - imgui.TableSetupColumnV("Score", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(2)) - imgui.TableSetupColumnV("Mods", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(3)) - imgui.TableSetupColumnV("300", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(4)) - imgui.TableSetupColumnV("100", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(5)) - imgui.TableSetupColumnV("50", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(6)) - imgui.TableSetupColumnV("Miss", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(7)) - imgui.TableSetupColumnV("Combo", imgui.TableColumnFlagsWidthFixed|imgui.TableColumnFlagsNoSort, 0, uint(8)) - - imgui.TableHeadersRow() - - imgui.PushFont(Font20) - - for i, replay := range bld.knockoutReplays { - pReplay := replay.parsedReplay - - imgui.TableNextColumn() - - imgui.Checkbox("##Use"+strconv.Itoa(i), &replay.included) - - imgui.TableNextColumn() - - imgui.Text(pReplay.Username) - - imgui.TableNextColumn() - - imgui.Text(utils.Humanize(pReplay.Score)) - - imgui.TableNextColumn() - - imgui.Text(difficulty.Modifier(pReplay.Mods).String()) - - imgui.TableNextColumn() - - imgui.Text(utils.Humanize(pReplay.Count300)) - - imgui.TableNextColumn() - - imgui.Text(utils.Humanize(pReplay.Count100)) - - imgui.TableNextColumn() - - imgui.Text(utils.Humanize(pReplay.Count50)) - - imgui.TableNextColumn() - - imgui.Text(utils.Humanize(pReplay.CountMiss)) - - imgui.TableNextColumn() - - imgui.Text(utils.Humanize(pReplay.MaxCombo)) - } - - imgui.PopFont() - - imgui.EndTable() - } -} - func drawAbout(dTex texture.Texture) { centerTable("about1", -1, func() { imgui.Image(imgui.TextureID(dTex.GetID()), vec2(100, 100)) @@ -280,67 +214,45 @@ func drawAbout(dTex texture.Texture) { func drawLauncherConfig() { imgui.PushStyleVarVec2(imgui.StyleVarCellPadding, vec2(imgui.CurrentStyle().CellPadding().X, 10)) - if imgui.BeginTableV("lconfigtable", 2, 0, vec2(-1, 0), -1) { - imgui.TableSetupColumnV("1lconfigtable", imgui.TableColumnFlagsWidthStretch, 0, uint(0)) - imgui.TableSetupColumnV("2lconfigtable", imgui.TableColumnFlagsWidthFixed, 0, uint(1)) - - imgui.TableNextColumn() - - imgui.AlignTextToFramePadding() - imgui.Text("Check for updates on startup") - - imgui.TableNextColumn() - - imgui.Checkbox("##CheckForUpdates", &launcherConfig.CheckForUpdates) - - imgui.TableNextColumn() - - imgui.AlignTextToFramePadding() - imgui.Text("Load latest replay on startup") - - imgui.TableNextColumn() - - imgui.Checkbox("##LoadLatestReplay", &launcherConfig.LoadLatestReplay) + checkboxOption := func(text string, value *bool) { + if imgui.BeginTableV(text+"table", 2, 0, vec2(-1, 0), -1) { + imgui.TableSetupColumnV(text+"table1", imgui.TableColumnFlagsWidthStretch, 0, uint(0)) + imgui.TableSetupColumnV(text+"table2", imgui.TableColumnFlagsWidthFixed, 0, uint(1)) - imgui.TableNextColumn() + imgui.TableNextColumn() - posLocalSMU := imgui.CursorPos() + pos1 := imgui.CursorPos() - imgui.AlignTextToFramePadding() - imgui.Text("Speed up startup on slow HDDs.\nWon't detect deleted/updated\nmaps!") + imgui.AlignTextToFramePadding() - posLocalSMU1 := imgui.CursorPos() + imgui.PushTextWrapPos() - imgui.TableNextColumn() + imgui.Text(text) - imgui.SetCursorPos(vec2(imgui.CursorPosX(), (posLocalSMU.Y+posLocalSMU1.Y-imgui.FrameHeightWithSpacing())/2)) - imgui.Checkbox("##SkipMapUpdate", &launcherConfig.SkipMapUpdate) + imgui.PopTextWrapPos() - imgui.TableNextColumn() - - posLocalSFA := imgui.CursorPos() + pos2 := imgui.CursorPos() - imgui.AlignTextToFramePadding() - imgui.Text("Show exported videos/images\nin explorer") + imgui.TableNextColumn() - posLocalSFA1 := imgui.CursorPos() + imgui.SetCursorPos(vec2(imgui.CursorPosX(), (pos1.Y+pos2.Y-imgui.FrameHeightWithSpacing())/2)) + imgui.Checkbox("##ck"+text, value) - imgui.TableNextColumn() + imgui.EndTable() + } + } - imgui.SetCursorPos(vec2(imgui.CursorPosX(), (posLocalSFA.Y+posLocalSFA1.Y-imgui.FrameHeightWithSpacing())/2)) - imgui.Checkbox("##ShowFileAfter", &launcherConfig.ShowFileAfter) + checkboxOption("Check for updates on startup", &launcherConfig.CheckForUpdates) - imgui.TableNextColumn() + checkboxOption("Load latest replay on startup", &launcherConfig.LoadLatestReplay) - imgui.AlignTextToFramePadding() - imgui.Text("Preview selected maps") + checkboxOption("Speed up startup on slow HDDs.\nWon't detect deleted/updated\nmaps!", &launcherConfig.SkipMapUpdate) - imgui.TableNextColumn() + checkboxOption("Show JSON paths in config editor", &launcherConfig.ShowJSONPaths) - imgui.Checkbox("##PreviewSelected", &launcherConfig.PreviewSelected) + checkboxOption("Show exported videos/images\nin explorer", &launcherConfig.ShowFileAfter) - imgui.EndTable() - } + checkboxOption("Preview selected maps", &launcherConfig.PreviewSelected) imgui.AlignTextToFramePadding() imgui.Text("Preview volume") diff --git a/launcher/songselect.go b/launcher/songselect.go index b6325577..06b9891b 100644 --- a/launcher/songselect.go +++ b/launcher/songselect.go @@ -473,11 +473,8 @@ func (m *songSelectPopup) drawSongSelect() { imgui.TableSetupColumnV("btooltip4", imgui.TableColumnFlagsWidthFixed, imgui.CalcTextSize("9.9", false, 0).X, uint(1)) tRow := func(text string, text2 string, args ...any) { - imgui.TableNextColumn() - imgui.Text(text) - - imgui.TableNextColumn() - imgui.Textf(text2, args...) + textColumn(text) + textColumn(fmt.Sprintf(text2, args...)) } tRow("Stars: ", sR) diff --git a/launcher/utils.go b/launcher/utils.go index 9b68e53c..bf6017c4 100644 --- a/launcher/utils.go +++ b/launcher/utils.go @@ -5,7 +5,10 @@ import ( "github.com/inkyblackness/imgui-go/v4" "github.com/sqweek/dialog" "github.com/wieku/danser-go/app/utils" + "github.com/wieku/danser-go/framework/env" "github.com/wieku/danser-go/framework/platform" + "os" + "path/filepath" "strings" ) @@ -51,7 +54,7 @@ func checkForUpdates(pingUpToDate bool) { case utils.Failed: showMessage(mError, "Can't get version from GitHub:", err) case utils.Snapshot: - if showMessage(mQuestion, "You're using a snapshot version of danser.\nFor newer version of snapshots please visit an official danser discord server at: %s\n\nDo you want to go there?", url) { + if showMessage(mQuestion, "You're using a snapshot version of danser.\nFor newer version of snapshots please visit the official danser discord server at: %s\n\nDo you want to go there?", url) { platform.OpenURL(url) } case utils.UpdateAvailable: @@ -61,6 +64,26 @@ func checkForUpdates(pingUpToDate bool) { } } +func textColumn(text string) { + imgui.TableNextColumn() + imgui.Text(text) +} + +func imguiPathFilter(data imgui.InputTextCallbackData) int32 { + if data.EventFlag() == imgui.InputTextFlagsCallbackCharFilter { + run := data.EventChar() + + switch run { + case '\\': + data.SetEventChar('/') + case '<', '>', '|', '?', '*', ':', '"': + data.SetEventChar(0) + } + } + + return 0 +} + func vec2(x, y float32) imgui.Vec2 { return imgui.Vec2{ X: x, @@ -80,3 +103,32 @@ func vec4(x, y, z, w float32) imgui.Vec4 { func vzero() imgui.Vec2 { return vec2(0, 0) } + +// covers cases: +// one of them is an abs path to data dir +// has path separator suffixed +// one of them has backwards slashes +func compareDirs(str1, str2 string) bool { + abPath := strings.TrimSuffix(strings.ReplaceAll(env.DataDir(), "\\", "/"), "/") + "/" + + str1D := strings.TrimSuffix(strings.ReplaceAll(str1, "\\", "/"), "/") + str2D := strings.TrimSuffix(strings.ReplaceAll(str2, "\\", "/"), "/") + + return strings.TrimPrefix(str1D, abPath) == strings.TrimPrefix(str2D, abPath) +} + +func getAbsPath(path string) string { + if strings.TrimSpace(path) != "" && filepath.IsAbs(path) { + return path + } + + return filepath.Join(env.DataDir(), path) +} + +func getRelativeOrABSPath(path string) string { + slashPath := strings.TrimSuffix(strings.ReplaceAll(path, "\\", "/"), "/") + + slashBase := strings.TrimSuffix(strings.ReplaceAll(env.DataDir(), "\\", "/"), "/") + "/" + + return strings.ReplaceAll(strings.TrimPrefix(slashPath, slashBase), "/", string(os.PathSeparator)) +} diff --git a/launcher/widgets.go b/launcher/widgets.go index e74c85ce..238c5e04 100644 --- a/launcher/widgets.go +++ b/launcher/widgets.go @@ -3,6 +3,7 @@ package launcher import ( "fmt" "github.com/inkyblackness/imgui-go/v4" + "github.com/wieku/danser-go/app/settings" "github.com/wieku/danser-go/framework/math/math32" "github.com/wieku/danser-go/framework/math/mutils" "golang.org/x/exp/constraints" @@ -83,10 +84,16 @@ func (p *popup) setCloseListener(closeListener func()) { p.closeListener = closeListener } +var openedAbove bool + +func resetPopupHierarchyInfo() { + openedAbove = false +} + func popupSmall(name string, opened *bool, dynamicSize bool, content func()) { - wSize := imgui.WindowSize() + width := float32(settings.Graphics.WindowWidth) - sX := wSize.X / 2 + sX := width / 2 if dynamicSize { sX = 0 } @@ -100,7 +107,7 @@ func popupWide(name string, opened *bool, content func()) { } func popupInter(name string, opened *bool, size imgui.Vec2, content func()) { - wSize := imgui.WindowSize() + wSizeX, wSizeY := float32(settings.Graphics.WindowWidth), float32(settings.Graphics.WindowHeight) if *opened { if !imgui.IsPopupOpen("##" + name) { @@ -110,8 +117,8 @@ func popupInter(name string, opened *bool, size imgui.Vec2, content func()) { imgui.SetNextWindowSize(size) imgui.SetNextWindowPosV(imgui.Vec2{ - X: wSize.X / 2, - Y: wSize.Y / 2, + X: wSizeX / 2, + Y: wSizeY / 2, }, imgui.ConditionAppearing, imgui.Vec2{ X: 0.5, Y: 0.5, @@ -120,12 +127,14 @@ func popupInter(name string, opened *bool, size imgui.Vec2, content func()) { if imgui.BeginPopupModalV("##"+name, opened, imgui.WindowFlagsNoCollapse|imgui.WindowFlagsNoResize|imgui.WindowFlagsAlwaysAutoResize|imgui.WindowFlagsNoMove|imgui.WindowFlagsNoTitleBar) { content() - hovered := imgui.IsWindowHoveredV(imgui.HoveredFlagsRootAndChildWindows | imgui.HoveredFlagsAllowWhenBlockedByActiveItem | imgui.HoveredFlagsAllowWhenBlockedByPopup) + hovered := imgui.IsWindowHoveredV(imgui.HoveredFlagsRootAndChildWindows|imgui.HoveredFlagsAllowWhenBlockedByActiveItem|imgui.HoveredFlagsAllowWhenBlockedByPopup) || openedAbove if ((imgui.IsMouseClicked(0) || imgui.IsMouseClicked(1)) && !hovered) || imgui.IsKeyPressed(imgui.KeyEscape) { *opened = false } + openedAbove = true + imgui.EndPopup() } } diff --git a/tools/assets/assets.go b/tools/assets/assets.go index 19a9bf12..3bb5f472 100644 --- a/tools/assets/assets.go +++ b/tools/assets/assets.go @@ -17,6 +17,13 @@ func main() { path, _ := filepath.Abs(os.Args[1]) path += string(filepath.Separator) + destPath := path + + if len(os.Args) > 2 { + destPath, _ = filepath.Abs(os.Args[2]) + destPath += string(filepath.Separator) + } + buf := new(bytes.Buffer) writer := zip.NewWriter(buf) @@ -58,9 +65,9 @@ func main() { copy(data, ro2d) - log.Println("Saving to:", filepath.Join(path, "assets.dpak")) + log.Println("Saving to:", filepath.Join(destPath, "assets.dpak")) - err := ioutil.WriteFile(filepath.Join(path, "assets.dpak"), data, 0644) + err := ioutil.WriteFile(filepath.Join(destPath, "assets.dpak"), data, 0644) if err != nil { panic(err) } diff --git a/tools/assets/go.mod b/tools/assets/go.mod index b0e862e1..8fc06bd2 100644 --- a/tools/assets/go.mod +++ b/tools/assets/go.mod @@ -1,3 +1,3 @@ module github.com/wieku/danser-go/tools/assets -go 1.15 +go 1.18 diff --git a/tools/ffmpeg/ffmpeg.go b/tools/ffmpeg/ffmpeg.go new file mode 100644 index 00000000..056244d5 --- /dev/null +++ b/tools/ffmpeg/ffmpeg.go @@ -0,0 +1,116 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" +) + +func main() { + targetDir, _ := filepath.Abs(os.Args[1]) + + archiveName := "ffmpeg-n5.1-latest-linux64-gpl-shared-5.1" + if runtime.GOOS == "windows" { + archiveName = "ffmpeg-n5.1-latest-win64-gpl-shared-5.1" + } + + downloadUrl := "https://github.com/Wieku/FFmpeg-Builds/releases/download/latest/" + archiveName + ".zip" + + file := download(downloadUrl) + + info, err := file.Stat() + if err != nil { + panic(err) + } + + zipFile, err := zip.NewReader(file, info.Size()) + if err != nil { + panic(err) + } + + os.MkdirAll(filepath.Join(targetDir, "ffmpeg"), 0755) + + log.Println("Starting unpacking...") + + for _, f := range zipFile.File { + strName := strings.TrimPrefix(f.Name, archiveName+"/") + + if strings.HasPrefix(strName, "bin/") && !strings.HasSuffix(strName, "bin/") { + unpack(f, filepath.Join(targetDir, "ffmpeg", strings.TrimPrefix(strName, "bin/"))) + } + + if runtime.GOOS == "linux" { + if strings.HasPrefix(strName, "lib/lib") { + matches := len(strings.Split(strName, ".")) == 3 // matching lib***.so.version + + if matches { + unpack(f, filepath.Join(targetDir, "ffmpeg", strings.TrimPrefix(strName, "lib/"))) + } + } + } + } + + file.Close() + + os.Remove(file.Name()) + + log.Println("Finished.") +} + +func unpack(f *zip.File, s string) { + log.Println(fmt.Sprintf("Unpacking \"%s\" to \"%s\"...", f.Name, s)) + out, err := os.OpenFile(s, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0755) + if err != nil { + panic(err) + } + + defer out.Close() + + zEntry, err := f.Open() + if err != nil { + panic(err) + } + + defer zEntry.Close() + + _, err = io.Copy(out, zEntry) + if err != nil { + panic(err) + } +} + +func download(url string) *os.File { + log.Println("Downloading ffmpeg...") + + out, err := os.CreateTemp("", "ffmpeg-dist") + if err != nil { + panic(err) + } + + resp, err := http.Get(url) + if err != nil { + panic(err) + } + + defer resp.Body.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + panic(err) + } + + _, err = out.Seek(0, 0) + if err != nil { + panic(err) + } + + log.Println("Download complete.") + + return out +} diff --git a/tools/ffmpeg/go.mod b/tools/ffmpeg/go.mod new file mode 100644 index 00000000..781eb7c1 --- /dev/null +++ b/tools/ffmpeg/go.mod @@ -0,0 +1,3 @@ +module github.com/wieku/danser-go/tools/ffmpeg + +go 1.18 diff --git a/tools/pack/go.mod b/tools/pack/go.mod index 4234b92c..1bce4989 100644 --- a/tools/pack/go.mod +++ b/tools/pack/go.mod @@ -1,3 +1,3 @@ module github.com/wieku/danser-go/tools/pack -go 1.15 +go 1.18 diff --git a/tools/pack2/go.mod b/tools/pack2/go.mod new file mode 100644 index 00000000..6a14fd0c --- /dev/null +++ b/tools/pack2/go.mod @@ -0,0 +1,3 @@ +module github.com/wieku/danser-go/tools/pack2 + +go 1.18 diff --git a/tools/pack2/pack.go b/tools/pack2/pack.go new file mode 100644 index 00000000..7857e862 --- /dev/null +++ b/tools/pack2/pack.go @@ -0,0 +1,54 @@ +package main + +import ( + "archive/zip" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +func main() { + dstFile, _ := filepath.Abs(os.Args[1]) + + file, err := os.Create(dstFile) + if err != nil { + panic(err) + } + + srcPath, _ := filepath.Abs(os.Args[2]) + srcPath += string(os.PathSeparator) + + writer := zip.NewWriter(file) + + filepath.Walk(srcPath, func(osFilePath string, info os.FileInfo, err error) error { + if !info.IsDir() { + trunc := strings.ReplaceAll(strings.TrimPrefix(osFilePath, srcPath), "\\", "/") + log.Println("Packing:", osFilePath) + fileWriter, err := writer.Create(trunc) + if err != nil { + panic(err) + } + + fileReader, err := os.Open(osFilePath) + if err != nil { + panic(err) + } + + defer fileReader.Close() + + _, err = io.Copy(fileWriter, fileReader) + if err != nil { + panic(err) + } + } + + return nil + }) + + writer.Close() + file.Close() + + log.Println("Finished") +}