Skip to content

Commit

Permalink
Add song preview API
Browse files Browse the repository at this point in the history
hope nobody else has been using the coroutine utilities yet
  • Loading branch information
hedgehog1029 committed Jun 5, 2023
1 parent fe4b28b commit 146e11a
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 9 deletions.
1 change: 1 addition & 0 deletions BaboonAPI/BaboonAPI.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<Compile Include="patch\SaverLoaderPatch.fs" />
<Compile Include="patch\InitializerPatch.fs" />
<Compile Include="patch\TrackScorePatch.fs" />
<Compile Include="patch\PreviewPatch.fs" />
<Compile Include="api-post\TrackLookup.fs" />
<Compile Include="api-post\Initializer.fs" />
<Compile Include="Library.fs" />
Expand Down
1 change: 1 addition & 0 deletions BaboonAPI/Library.fs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ type BaboonPlugin() =
typeof<LoaderPatch>
typeof<GameControllerPatch>
typeof<PausePatches>
typeof<PreviewPatch>
typeof<SaverLoaderPatch>
typeof<TrackScorePatches>
] |> List.iter harmony.PatchAll
Expand Down
10 changes: 10 additions & 0 deletions BaboonAPI/api/TrackRegistry.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

open System
open BaboonAPI.Event
open BaboonAPI.Utility.Coroutines
open UnityEngine

/// <namespacedoc>
Expand Down Expand Up @@ -88,6 +89,15 @@ type public PauseAware =
/// Called when this track is resumed (after the countdown).
abstract OnResume: PauseContext -> unit

/// LoadedTromboneTrack extension for preview clips
type public Previewable =
/// <summary>Called when attempting to load a clip for preview.</summary>
/// <remarks>
/// The implementation should return a valid audio clip on successful load.
/// The implementation may return a Error result with an error message if loading fails for any reason.
/// </remarks>
abstract LoadClip: unit -> YieldTask<Result<TrackAudio, string>>

/// <summary>
/// Event-based API for registering new tracks.
/// </summary>
Expand Down
12 changes: 11 additions & 1 deletion BaboonAPI/patch/BaseTracksLoaderPatch.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ open System.IO
open System.Runtime.Serialization.Formatters.Binary
open BaboonAPI.Hooks.Tracks
open BaboonAPI.Internal
open BaboonAPI.Utility
open BaboonAPI.Utility.Unity
open BepInEx.Logging
open HarmonyLib
open UnityEngine
Expand All @@ -25,7 +27,7 @@ type internal BaseGameLoadedTrack(trackref: string, bundle: AssetBundle) =
()

member this.trackref = trackref

interface PauseAware with
member this.CanResume = true

Expand Down Expand Up @@ -63,6 +65,14 @@ type internal BaseGameTrack(data: string[]) =
use stream = File.Open(path, FileMode.Open)
BinaryFormatter().Deserialize(stream) :?> SavedLevel

interface Previewable with
member this.LoadClip() =
let trackref = (this :> TromboneTrack).trackref
let path = $"{Application.streamingAssetsPath}/trackclips/{trackref}-sample.ogg"

loadAudioClip (path, AudioType.OGGVORBIS)
|> Coroutines.map (Result.map (fun audioClip -> { Clip = audioClip; Volume = 0.9f }))

type internal BaseGameTrackRegistry(songs: SongData) =
/// List of base game trackrefs
member _.trackrefs =
Expand Down
52 changes: 52 additions & 0 deletions BaboonAPI/patch/PreviewPatch.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace BaboonAPI.Patch

open System.Reflection
open BaboonAPI.Hooks.Tracks
open BaboonAPI.Internal
open BaboonAPI.Utility.Coroutines
open BepInEx.Logging
open HarmonyLib
open UnityEngine

[<HarmonyPatch>]
type PreviewPatch() =
static let logger = Logger.CreateLogSource "BaboonAPI.PreviewPatch"

static let setClipAndFade (clip: TrackAudio) (player: LevelSelectClipPlayer) =
let pt = player.GetType()
pt.GetField("clip_volume", BindingFlags.NonPublic ||| BindingFlags.Instance).SetValue (player, clip.Volume)

let clipPlayer: AudioSource = unbox (pt.GetField("clipPlayer", BindingFlags.NonPublic ||| BindingFlags.Instance).GetValue player)
clipPlayer.clip <- clip.Clip
clipPlayer.volume <- 0f
clipPlayer.Play()

pt.GetMethod("startCrossFade", BindingFlags.NonPublic ||| BindingFlags.Instance).Invoke (player, [| 1f |])
|> ignore

static let doClipNotFound (player: LevelSelectClipPlayer) =
player.Invoke("doDefaultClipNotFoundEvent", 0f)

[<HarmonyPrefix>]
[<HarmonyPatch(typeof<LevelSelectClipPlayer>, "beginClipSearch")>]
static member ClipSearchPrefix(__instance: LevelSelectClipPlayer, ___current_trackref: string inref) =
let track =
TrackAccessor.tryFetchRegisteredTrack ___current_trackref
|> Option.map (fun rt -> rt.track)

match track with
| Some (:? Previewable as preview) ->
coroutine {
let! clip = preview.LoadClip()

match clip with
| Ok audio ->
setClipAndFade audio __instance
| Error msg ->
logger.LogError $"Failed to load song preview clip: {msg}"
doClipNotFound __instance
} |> __instance.StartCoroutine |> ignore
| _ ->
doClipNotFound __instance

false
16 changes: 13 additions & 3 deletions BaboonAPI/utility/Coroutines.fs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ open UnityEngine
/// printf $"Loaded {assetBundle.name}"
///}</code>
/// </remarks>
type IYieldWithResult<'r> =
type YieldTask<'r> =
abstract Coroutine : YieldInstruction
abstract Result : 'r

/// Await an AsyncOperation
let awaitAsyncOperation<'r, 'op when 'op :> AsyncOperation> (binder: 'op -> 'r) (op: 'op) =
{ new IYieldWithResult<'r> with
{ new YieldTask<'r> with
member _.Coroutine = op
member _.Result = binder op }

Expand All @@ -34,7 +34,7 @@ type CoroutineBuilder() =

member _.YieldFrom (syi: YieldInstruction seq) = syi

member _.Bind (src: IYieldWithResult<'a>, binder: 'a -> YieldInstruction seq) =
member _.Bind (src: YieldTask<'a>, binder: 'a -> YieldInstruction seq) =
seq {
yield src.Coroutine // run the coroutine
yield! binder(src.Result) // then call the binder with the result
Expand All @@ -58,3 +58,13 @@ type CoroutineBuilder() =

/// Unity coroutine computation expression
let coroutine = CoroutineBuilder()

/// Transform a YieldTask
let map (binder: 'a -> 'b) (task: YieldTask<'a>): YieldTask<'b> =
{ new YieldTask<'b> with
member _.Coroutine = task.Coroutine
member _.Result = binder task.Result }

/// Consume a YieldTask into an IEnumerator, allowing it to be started as a Unity coroutine
let run (task: YieldTask<'a>) =
(Seq.delay (fun () -> Seq.singleton task.Coroutine)).GetEnumerator()
13 changes: 8 additions & 5 deletions BaboonAPI/utility/UnityUtilities.fs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ let public loadAsset (name: string) (bundle: AssetBundle) =
|> awaitAsyncOperation (fun op -> op.asset)

let public loadAudioClip (path: string, audioType: AudioType) =
use www = UnityWebRequestMultimedia.GetAudioClip (path, audioType)
let www = UnityWebRequestMultimedia.GetAudioClip (path, audioType)

let mapResult (op: UnityWebRequestAsyncOperation) =
if op.webRequest.isHttpError || op.webRequest.isNetworkError then
Error op.webRequest.error
else
Ok (DownloadHandlerAudioClip.GetContent op.webRequest)
try
if op.webRequest.isHttpError || op.webRequest.isNetworkError then
Error op.webRequest.error
else
Ok (DownloadHandlerAudioClip.GetContent op.webRequest)
finally
op.webRequest.Dispose()

awaitAsyncOperation mapResult (www.SendWebRequest ())

0 comments on commit 146e11a

Please sign in to comment.