mirror of
https://github.com/RHeavenStudio/HeavenStudio.git
synced 2025-05-01 22:44:28 +02:00

fix input scheduling not taking into account what minigame is actually active at the target time fix input disable and autoplay jank prep "friendly program name" define title screen adjustments remove bread2unity
1239 lines
46 KiB
C#
1239 lines
46 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEngine;
|
|
using UnityEngine.Pool;
|
|
|
|
using Starpelly;
|
|
using Jukebox;
|
|
using HeavenStudio.Util;
|
|
using HeavenStudio.Games;
|
|
using HeavenStudio.Common;
|
|
using Cysharp.Threading.Tasks;
|
|
|
|
namespace HeavenStudio
|
|
{
|
|
public class GameManager : MonoBehaviour
|
|
{
|
|
const int SoundPoolSizeMin = 32;
|
|
const int SoundPoolSizeMax = 32;
|
|
|
|
[Header("Lists")]
|
|
[NonSerialized] public RiqBeatmap Beatmap = new();
|
|
private List<GameObject> preloadedGames = new();
|
|
[NonSerialized] public ObjectPool<Sound> SoundObjects;
|
|
|
|
[Header("Components")]
|
|
[NonSerialized] public Camera GameCamera, CursorCam, OverlayCamera, StaticCamera;
|
|
[NonSerialized] public CircleCursor CircleCursor;
|
|
[NonSerialized] public GameObject GamesHolder;
|
|
[NonSerialized] public Games.Global.Flash fade;
|
|
[NonSerialized] public Games.Global.Filter filter;
|
|
|
|
[Header("Games")]
|
|
[NonSerialized] public string currentGame;
|
|
Coroutine currentGameSwitchIE;
|
|
|
|
[Header("Properties")]
|
|
[NonSerialized] public string txt = null;
|
|
[NonSerialized] public string ext = null;
|
|
|
|
[NonSerialized]
|
|
public int currentEvent, currentTempoEvent, currentVolumeEvent, currentSectionEvent,
|
|
currentPreEvent, currentPreSwitch, currentPreSequence;
|
|
[NonSerialized] public double endBeat;
|
|
[NonSerialized] public float startOffset;
|
|
[NonSerialized] public bool playMode;
|
|
[NonSerialized] public double startBeat;
|
|
[NonSerialized] public GameObject currentGameO;
|
|
private Minigame _currentMinigame;
|
|
[NonSerialized] public bool autoplay;
|
|
[NonSerialized] public bool canInput = true;
|
|
[NonSerialized] public RiqEntity lastSection, currentSection;
|
|
[NonSerialized] public double nextSectionBeat;
|
|
public double SectionProgress { get; private set; }
|
|
public float MarkerWeight { get; private set; }
|
|
public int MarkerCategory { get; private set; }
|
|
|
|
public bool GameHasSplitColours
|
|
{
|
|
get
|
|
{
|
|
var inf = GetGameInfo(currentGame);
|
|
if (inf == null) return false;
|
|
return inf.splitColorL != null && inf.splitColorR != null;
|
|
}
|
|
}
|
|
|
|
bool AudioLoadDone;
|
|
bool ChartLoadError;
|
|
|
|
List<double> eventBeats, tempoBeats, volumeBeats, sectionBeats;
|
|
List<RiqEntity> allGameSwitches;
|
|
|
|
public event Action<double> onBeatChanged;
|
|
public event Action<RiqEntity, RiqEntity> onSectionChange;
|
|
public event Action<double> onBeatPulse;
|
|
public event Action<double> onPlay;
|
|
public event Action<double> onPause;
|
|
public event Action<double> onUnPause;
|
|
|
|
public int BeatmapEntities()
|
|
{
|
|
return Beatmap.Entities.Count + Beatmap.TempoChanges.Count + Beatmap.VolumeChanges.Count + Beatmap.SectionMarkers.Count;
|
|
}
|
|
|
|
public static GameManager instance { get; private set; }
|
|
private EventCaller eventCaller;
|
|
|
|
// average input accuracy (msec)
|
|
List<int> inputOffsetSamples = new List<int>();
|
|
float averageInputOffset = 0;
|
|
public float AvgInputOffset
|
|
{
|
|
get
|
|
{
|
|
return averageInputOffset;
|
|
}
|
|
set
|
|
{
|
|
inputOffsetSamples.Add((int)value);
|
|
averageInputOffset = (float)inputOffsetSamples.Average();
|
|
}
|
|
}
|
|
|
|
// input accuracy (%)
|
|
double totalInputs = 0;
|
|
double totalPlayerAccuracy = 0;
|
|
public double PlayerAccuracy
|
|
{
|
|
get
|
|
{
|
|
if (totalInputs == 0) return 0;
|
|
return totalPlayerAccuracy / totalInputs;
|
|
}
|
|
}
|
|
bool skillStarCollected = false;
|
|
|
|
// cleared sections
|
|
List<bool> clearedSections = new List<bool>();
|
|
public bool ClearedSection
|
|
{
|
|
set
|
|
{
|
|
clearedSections.Add(value);
|
|
}
|
|
}
|
|
|
|
JudgementManager.JudgementInfo judgementInfo;
|
|
|
|
private void Awake()
|
|
{
|
|
// autoplay = true;
|
|
instance = this;
|
|
}
|
|
|
|
public void Init(bool preLoaded = false)
|
|
{
|
|
AudioLoadDone = false;
|
|
ChartLoadError = false;
|
|
currentPreEvent = 0;
|
|
currentPreSwitch = 0;
|
|
currentPreSequence = 0;
|
|
|
|
GameObject filter = new GameObject("filter");
|
|
this.filter = filter.AddComponent<Games.Global.Filter>();
|
|
|
|
eventCaller = this.gameObject.AddComponent<EventCaller>();
|
|
eventCaller.GamesHolder = GamesHolder.transform;
|
|
eventCaller.Init();
|
|
|
|
// note: serialize this shit in the inspector //
|
|
GameObject textbox = Instantiate(Resources.Load<GameObject>("Prefabs/Common/Textbox"));
|
|
textbox.name = "Textbox";
|
|
|
|
GameObject timingDisp = Instantiate(Resources.Load<GameObject>("Prefabs/Common/Overlays/TimingAccuracy"));
|
|
timingDisp.name = "TimingDisplay";
|
|
|
|
GameObject skillStarDisp = Instantiate(Resources.Load<GameObject>("Prefabs/Common/Overlays/SkillStar"));
|
|
skillStarDisp.name = "SkillStar";
|
|
|
|
GameObject overlays = Instantiate(Resources.Load<GameObject>("Prefabs/Common/Overlays"));
|
|
overlays.name = "Overlays";
|
|
|
|
GoForAPerfect.instance.Disable();
|
|
/////
|
|
|
|
SoundObjects = new ObjectPool<Sound>(CreatePooledSound, OnTakePooledSound, OnReturnPooledSound, OnDestroyPooledSound, true, SoundPoolSizeMin, SoundPoolSizeMax);
|
|
|
|
|
|
if (preLoaded)
|
|
{
|
|
LoadRemix(false);
|
|
}
|
|
else
|
|
{
|
|
RiqFileHandler.ClearCache();
|
|
NewRemix();
|
|
}
|
|
|
|
SortEventsList();
|
|
Conductor.instance.SetBpm(Beatmap.TempoChanges[0]["tempo"]);
|
|
Conductor.instance.SetVolume(Beatmap.VolumeChanges[0]["volume"]);
|
|
Conductor.instance.firstBeatOffset = Beatmap.data.offset;
|
|
|
|
if (Beatmap.Entities.Count >= 1)
|
|
{
|
|
string game = Beatmap.Entities[0].datamodel.Split(0);
|
|
SetCurrentGame(game);
|
|
StartCoroutine(WaitAndSetGame(game));
|
|
}
|
|
else
|
|
{
|
|
SetGame("noGame");
|
|
}
|
|
|
|
if (playMode)
|
|
{
|
|
StartCoroutine(WaitReadyAndPlayCo(startBeat, 1f));
|
|
}
|
|
}
|
|
|
|
Sound CreatePooledSound()
|
|
{
|
|
GameObject oneShot = new GameObject($"Pooled Scheduled Sound");
|
|
oneShot.transform.SetParent(transform);
|
|
|
|
AudioSource audioSource = oneShot.AddComponent<AudioSource>();
|
|
audioSource.playOnAwake = false;
|
|
|
|
Sound snd = oneShot.AddComponent<Sound>();
|
|
|
|
oneShot.SetActive(false);
|
|
|
|
return snd;
|
|
}
|
|
|
|
// Called when an item is returned to the pool using Release
|
|
void OnReturnPooledSound(Sound snd)
|
|
{
|
|
snd.Stop();
|
|
}
|
|
|
|
// Called when an item is taken from the pool using Get
|
|
void OnTakePooledSound(Sound snd)
|
|
{
|
|
snd.gameObject.SetActive(true);
|
|
}
|
|
|
|
// If the pool capacity is reached then any items returned will be destroyed.
|
|
void OnDestroyPooledSound(Sound snd)
|
|
{
|
|
snd.Stop();
|
|
Destroy(snd.gameObject);
|
|
}
|
|
|
|
public void NewRemix()
|
|
{
|
|
Debug.Log("Creating new remix");
|
|
AudioLoadDone = false;
|
|
Beatmap = new("1", "HeavenStudio");
|
|
Beatmap.data.properties = new(Minigames.propertiesModel);
|
|
Beatmap.AddNewTempoChange(0, 120f);
|
|
Beatmap.AddNewVolumeChange(0, 100f);
|
|
Beatmap.data.offset = 0f;
|
|
Conductor.instance.musicSource.clip = null;
|
|
RiqFileHandler.WriteRiq(Beatmap);
|
|
AudioLoadDone = true;
|
|
}
|
|
|
|
public IEnumerator LoadMusic()
|
|
{
|
|
ChartLoadError = false;
|
|
IEnumerator load = RiqFileHandler.LoadSong();
|
|
while (true)
|
|
{
|
|
object current = load.Current;
|
|
try
|
|
{
|
|
if (load.MoveNext() == false)
|
|
{
|
|
break;
|
|
}
|
|
current = load.Current;
|
|
}
|
|
catch (System.IO.FileNotFoundException f)
|
|
{
|
|
Debug.LogWarning("chart has no music: " + f.Message);
|
|
Conductor.instance.musicSource.clip = null;
|
|
AudioLoadDone = true;
|
|
yield break;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"Failed to load music: {e.Message}");
|
|
GlobalGameManager.ShowErrorMessage("Error Loading Music", e.Message + "\n\n" + e.StackTrace);
|
|
AudioLoadDone = true;
|
|
ChartLoadError = true;
|
|
yield break;
|
|
}
|
|
yield return current;
|
|
}
|
|
Conductor.instance.musicSource.clip = RiqFileHandler.StreamedAudioClip;
|
|
AudioLoadDone = true;
|
|
}
|
|
|
|
public void LoadRemix(bool editor = false)
|
|
{
|
|
AudioLoadDone = false;
|
|
ChartLoadError = false;
|
|
try
|
|
{
|
|
Beatmap = RiqFileHandler.ReadRiq();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogError($"Failed to load remix: {e.Message}");
|
|
GlobalGameManager.ShowErrorMessage("Error Loading RIQ", e.Message + "\n\n" + e.StackTrace);
|
|
ChartLoadError = true;
|
|
return;
|
|
}
|
|
if (!editor)
|
|
StartCoroutine(LoadMusic());
|
|
SortEventsList();
|
|
Conductor.instance.SetBpm(Beatmap.TempoChanges[0]["tempo"]);
|
|
Conductor.instance.SetVolume(Beatmap.VolumeChanges[0]["volume"]);
|
|
Conductor.instance.firstBeatOffset = Beatmap.data.offset;
|
|
if (!playMode)
|
|
{
|
|
Stop(0);
|
|
}
|
|
SetCurrentEventToClosest(0);
|
|
|
|
if (Beatmap.Entities.Count >= 1)
|
|
{
|
|
string game = Beatmap.Entities[0].datamodel.Split(0);
|
|
SetCurrentGame(game);
|
|
StartCoroutine(WaitAndSetGame(game));
|
|
}
|
|
else
|
|
{
|
|
SetGame("noGame");
|
|
}
|
|
|
|
if (editor)
|
|
{
|
|
Debug.Log(Beatmap.data.riqOrigin);
|
|
if (Beatmap.data.riqOrigin != "HeavenStudio")
|
|
{
|
|
string origin = Beatmap.data.riqOrigin?.DisplayName() ?? "Unknown Origin";
|
|
GlobalGameManager.ShowErrorMessage("Warning",
|
|
$"This chart came from\n<alpha=#AA>{origin}</color>\nand uses content not included in Heaven Studio.\n\n<color=\"yellow\">You may be able to edit this chart in Heaven Studio to be used in its original program.</color>");
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ScoreInputAccuracy(double beat, double accuracy, bool late, double time, float weight = 1, bool doDisplay = true)
|
|
{
|
|
// push the hit event to the timing display
|
|
if (doDisplay)
|
|
TimingAccuracyDisplay.instance.MakeAccuracyVfx(time, late);
|
|
|
|
if (weight > 0 && MarkerWeight > 0)
|
|
{
|
|
totalInputs += weight * MarkerWeight;
|
|
totalPlayerAccuracy += Math.Abs(accuracy) * weight * MarkerWeight;
|
|
|
|
judgementInfo.inputs.Add(new JudgementManager.InputInfo
|
|
{
|
|
beat = beat,
|
|
accuracyState = accuracy,
|
|
timeOffset = time,
|
|
weight = weight * MarkerWeight,
|
|
category = MarkerCategory
|
|
});
|
|
}
|
|
|
|
if (accuracy < Minigame.rankOkThreshold && weight > 0)
|
|
{
|
|
SkillStarManager.instance.KillStar();
|
|
}
|
|
|
|
if (SkillStarManager.instance.IsEligible && !skillStarCollected && accuracy >= 1f)
|
|
{
|
|
if (SkillStarManager.instance.DoStarJust())
|
|
skillStarCollected = true;
|
|
}
|
|
}
|
|
|
|
public void DoSectionCompletion(double beat, bool clear, string name, double score)
|
|
{
|
|
judgementInfo.medals.Add(new JudgementManager.MedalInfo
|
|
{
|
|
beat = beat,
|
|
cleared = clear
|
|
});
|
|
}
|
|
|
|
static bool StringStartsWith(string a, string b)
|
|
{
|
|
int aLen = a.Length;
|
|
int bLen = b.Length;
|
|
|
|
int ap = 0; int bp = 0;
|
|
|
|
while (ap < aLen && bp < bLen && a[ap] == b[bp])
|
|
{
|
|
ap++;
|
|
bp++;
|
|
}
|
|
|
|
return (bp == bLen);
|
|
}
|
|
|
|
public List<Minigames.Minigame> SeekAheadAndPreload(double start, float seekTime = 8f)
|
|
{
|
|
List<Minigames.Minigame> gamesToPreload = new();
|
|
List<RiqEntity> entitiesAtSameBeat = ListPool<RiqEntity>.Get();
|
|
Minigames.Minigame inf;
|
|
|
|
//seek ahead to preload games that have assetbundles
|
|
if (currentPreSwitch < allGameSwitches.Count && currentPreSwitch >= 0)
|
|
{
|
|
if (start + seekTime >= allGameSwitches[currentPreSwitch].beat)
|
|
{
|
|
string gameName = allGameSwitches[currentPreSwitch].datamodel.Split('/')[2];
|
|
inf = GetGameInfo(gameName);
|
|
if (inf != null && !(inf.inferred || inf.fxOnly))
|
|
{
|
|
if (inf.usesAssetBundle && !inf.AssetsLoaded)
|
|
{
|
|
gamesToPreload.Add(inf);
|
|
Debug.Log($"ASYNC loading assetbundles for game {gameName}");
|
|
inf.LoadAssetsAsync().Forget();
|
|
}
|
|
}
|
|
currentPreSwitch++;
|
|
}
|
|
}
|
|
//then check game entities
|
|
if (currentPreEvent < Beatmap.Entities.Count && currentPreEvent >= 0)
|
|
{
|
|
if (start + seekTime >= eventBeats[currentPreEvent])
|
|
{
|
|
foreach (RiqEntity entity in Beatmap.Entities)
|
|
{
|
|
if (entity.beat == Beatmap.Entities[currentPreEvent].beat && !EventCaller.FXOnlyGames().Contains(eventCaller.GetMinigame(entity.datamodel.Split('/')[0])))
|
|
{
|
|
entitiesAtSameBeat.Add(entity);
|
|
}
|
|
}
|
|
SortEventsByPriority(entitiesAtSameBeat);
|
|
foreach (RiqEntity entity in entitiesAtSameBeat)
|
|
{
|
|
string gameName = entity.datamodel.Split('/')[0];
|
|
inf = GetGameInfo(gameName);
|
|
if (inf != null && !(inf.inferred || inf.fxOnly))
|
|
{
|
|
if (inf.usesAssetBundle && !inf.AssetsLoaded)
|
|
{
|
|
gamesToPreload.Add(inf);
|
|
Debug.Log($"ASYNC loading assetbundles for game {gameName}");
|
|
inf.LoadAssetsAsync().Forget();
|
|
}
|
|
}
|
|
currentPreEvent++;
|
|
}
|
|
}
|
|
}
|
|
ListPool<RiqEntity>.Release(entitiesAtSameBeat);
|
|
return gamesToPreload;
|
|
}
|
|
|
|
public void SeekAheadAndDoPreEvent(double start)
|
|
{
|
|
if (currentPreSequence < Beatmap.Entities.Count && currentPreSequence >= 0)
|
|
{
|
|
List<RiqEntity> entitiesAtSameBeat = ListPool<RiqEntity>.Get();
|
|
RiqEntity seekEntity = Beatmap.Entities[currentPreSequence];
|
|
|
|
foreach (RiqEntity entity in Beatmap.Entities)
|
|
{
|
|
if (entity.beat == seekEntity.beat)
|
|
{
|
|
entitiesAtSameBeat.Add(entity);
|
|
}
|
|
}
|
|
SortEventsByPriority(entitiesAtSameBeat);
|
|
|
|
string[] seekEntityDatamodel = seekEntity.datamodel.Split('/');
|
|
|
|
float seekTime = eventCaller.GetGameAction(seekEntityDatamodel[0], seekEntityDatamodel[1]).preFunctionLength;
|
|
|
|
if (start + seekTime >= eventBeats[currentPreSequence])
|
|
{
|
|
foreach (RiqEntity entity in entitiesAtSameBeat)
|
|
{
|
|
currentPreSequence++;
|
|
string gameName = entity.datamodel.Split('/')[0];
|
|
var inf = GetGameInfo(gameName);
|
|
if (inf != null && inf.usesAssetBundle && inf.AssetsLoaded && !inf.SequencesPreloaded)
|
|
{
|
|
Debug.Log($"Preloading game {gameName}");
|
|
PreloadGameSequences(gameName);
|
|
}
|
|
eventCaller.CallPreEvent(entity);
|
|
}
|
|
}
|
|
ListPool<RiqEntity>.Release(entitiesAtSameBeat);
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (BeatmapEntities() < 1)
|
|
return;
|
|
if (!Conductor.instance.isPlaying)
|
|
return;
|
|
Conductor cond = Conductor.instance;
|
|
double clampedBeat = Math.Max(cond.songPositionInBeatsAsDouble, 0);
|
|
|
|
if (currentTempoEvent < Beatmap.TempoChanges.Count && currentTempoEvent >= 0)
|
|
{
|
|
if (cond.songPositionInBeatsAsDouble >= tempoBeats[currentTempoEvent])
|
|
{
|
|
cond.SetBpm(Beatmap.TempoChanges[currentTempoEvent]["tempo"]);
|
|
currentTempoEvent++;
|
|
}
|
|
}
|
|
|
|
if (currentVolumeEvent < Beatmap.VolumeChanges.Count && currentVolumeEvent >= 0)
|
|
{
|
|
if (cond.songPositionInBeatsAsDouble >= volumeBeats[currentVolumeEvent])
|
|
{
|
|
cond.SetVolume(Beatmap.VolumeChanges[currentVolumeEvent]["volume"]);
|
|
currentVolumeEvent++;
|
|
}
|
|
}
|
|
|
|
if (currentSectionEvent < Beatmap.SectionMarkers.Count && currentSectionEvent >= 0)
|
|
{
|
|
if (cond.songPositionInBeatsAsDouble >= sectionBeats[currentSectionEvent])
|
|
{
|
|
RiqEntity marker = Beatmap.SectionMarkers[currentSectionEvent];
|
|
if (!string.IsNullOrEmpty(marker["sectionName"]))
|
|
{
|
|
Debug.Log("Section " + marker["sectionName"] + " started");
|
|
lastSection = currentSection;
|
|
if (currentSectionEvent < Beatmap.SectionMarkers.Count)
|
|
currentSection = marker;
|
|
else
|
|
currentSection = null;
|
|
nextSectionBeat = endBeat;
|
|
foreach (RiqEntity futureSection in Beatmap.SectionMarkers)
|
|
{
|
|
if (futureSection.beat < marker.beat) continue;
|
|
if (futureSection == marker) continue;
|
|
if (!string.IsNullOrEmpty(futureSection["sectionName"]))
|
|
{
|
|
nextSectionBeat = futureSection.beat;
|
|
break;
|
|
}
|
|
}
|
|
onSectionChange?.Invoke(currentSection, lastSection);
|
|
}
|
|
|
|
if (OverlaysManager.OverlaysEnabled)
|
|
{
|
|
if (PersistentDataManager.gameSettings.perfectChallengeType != PersistentDataManager.PerfectChallengeType.Off)
|
|
{
|
|
if (marker["startPerfect"] && GoForAPerfect.instance != null && GoForAPerfect.instance.perfect && !GoForAPerfect.instance.gameObject.activeSelf)
|
|
{
|
|
GoForAPerfect.instance.Enable(marker.beat);
|
|
}
|
|
}
|
|
}
|
|
|
|
MarkerWeight = marker["weight"];
|
|
MarkerCategory = marker["category"];
|
|
currentSectionEvent++;
|
|
}
|
|
}
|
|
|
|
if (cond.songPositionInBeatsAsDouble >= Math.Ceiling(_playStartBeat) + _pulseTally)
|
|
{
|
|
if (_currentMinigame != null) _currentMinigame.OnBeatPulse(Math.Ceiling(_playStartBeat) + _pulseTally);
|
|
onBeatPulse?.Invoke(Math.Ceiling(_playStartBeat) + _pulseTally);
|
|
_pulseTally++;
|
|
}
|
|
|
|
float seekTime = 8f;
|
|
//seek ahead to preload games that have assetbundles
|
|
SeekAheadAndPreload(clampedBeat, seekTime);
|
|
SeekAheadAndDoPreEvent(clampedBeat);
|
|
|
|
if (currentEvent < Beatmap.Entities.Count && currentEvent >= 0)
|
|
{
|
|
if (clampedBeat >= eventBeats[currentEvent])
|
|
{
|
|
List<RiqEntity> entitiesAtSameBeat = ListPool<RiqEntity>.Get();
|
|
List<RiqEntity> fxEntities = ListPool<RiqEntity>.Get();
|
|
|
|
// allows for multiple events on the same beat to be executed on the same frame, so no more 1-frame delay
|
|
using (PooledObject<List<RiqEntity>> pool = ListPool<RiqEntity>.Get(out List<RiqEntity> currentBeatEntities))
|
|
{
|
|
currentBeatEntities = Beatmap.Entities.FindAll(c => c.beat == Beatmap.Entities[currentEvent].beat);
|
|
foreach (RiqEntity entity in currentBeatEntities)
|
|
{
|
|
if (EventCaller.FXOnlyGames().Contains(eventCaller.GetMinigame(entity.datamodel.Split('/')[0])))
|
|
{
|
|
fxEntities.Add(entity);
|
|
}
|
|
else
|
|
{
|
|
entitiesAtSameBeat.Add(entity);
|
|
}
|
|
}
|
|
}
|
|
|
|
SortEventsByPriority(fxEntities);
|
|
SortEventsByPriority(entitiesAtSameBeat);
|
|
|
|
// FX entities should ALWAYS execute before gameplay entities
|
|
foreach (RiqEntity entity in fxEntities)
|
|
{
|
|
eventCaller.CallEvent(entity, true);
|
|
currentEvent++;
|
|
}
|
|
|
|
foreach (RiqEntity entity in entitiesAtSameBeat)
|
|
{
|
|
// if game isn't loaded, preload game so whatever event that would be called will still run outside if needed
|
|
if (entity.datamodel.Split('/')[0] != currentGame)
|
|
{
|
|
eventCaller.CallEvent(entity, false);
|
|
}
|
|
else
|
|
{
|
|
eventCaller.CallEvent(entity, true);
|
|
}
|
|
|
|
// Thank you to @shshwdr for bring this to my attention
|
|
currentEvent++;
|
|
}
|
|
|
|
ListPool<RiqEntity>.Release(entitiesAtSameBeat);
|
|
ListPool<RiqEntity>.Release(fxEntities);
|
|
}
|
|
}
|
|
|
|
if (currentSection == null)
|
|
{
|
|
SectionProgress = 0;
|
|
}
|
|
else
|
|
{
|
|
double currectSectionStart = cond.GetSongPosFromBeat(currentSection.beat);
|
|
|
|
SectionProgress = (cond.songPosition - currectSectionStart) / (cond.GetSongPosFromBeat(nextSectionBeat) - currectSectionStart);
|
|
}
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
OverlaysManager.instance.TogleOverlaysVisibility(Editor.Editor.instance == null || Editor.Editor.instance.fullscreen || ((PersistentDataManager.gameSettings.overlaysInEditor) && (!Editor.Editor.instance.fullscreen)) || HeavenStudio.Editor.GameSettings.InPreview);
|
|
|
|
if (!Conductor.instance.isPlaying)
|
|
return;
|
|
|
|
if (Conductor.instance.songPositionInBeatsAsDouble >= Math.Ceiling(_playStartBeat) + _latePulseTally)
|
|
{
|
|
if (_currentMinigame != null) _currentMinigame.OnLateBeatPulse(Math.Ceiling(_playStartBeat) + _latePulseTally);
|
|
onBeatPulse?.Invoke(Math.Ceiling(_playStartBeat) + _latePulseTally);
|
|
_latePulseTally++;
|
|
}
|
|
}
|
|
|
|
public void ToggleInputs(bool inputs)
|
|
{
|
|
canInput = inputs;
|
|
}
|
|
|
|
#region Play Events
|
|
|
|
private double _playStartBeat = 0;
|
|
private int _pulseTally = 0;
|
|
private int _latePulseTally = 0;
|
|
|
|
public void Play(double beat, float delay = 0f)
|
|
{
|
|
bool paused = Conductor.instance.isPaused;
|
|
Debug.Log("Playing at " + beat);
|
|
_playStartBeat = beat;
|
|
_pulseTally = 0;
|
|
_latePulseTally = 0;
|
|
canInput = true;
|
|
if (!paused)
|
|
{
|
|
inputOffsetSamples.Clear();
|
|
averageInputOffset = 0;
|
|
|
|
totalInputs = 0;
|
|
totalPlayerAccuracy = 0;
|
|
|
|
TimingAccuracyDisplay.instance.ResetArrow();
|
|
SkillStarManager.instance.Reset();
|
|
skillStarCollected = false;
|
|
|
|
GoForAPerfect.instance.perfect = true;
|
|
GoForAPerfect.instance.Disable();
|
|
|
|
SectionMedalsManager.instance.Reset();
|
|
clearedSections.Clear();
|
|
|
|
judgementInfo = new JudgementManager.JudgementInfo
|
|
{
|
|
inputs = new List<JudgementManager.InputInfo>(),
|
|
medals = new List<JudgementManager.MedalInfo>()
|
|
};
|
|
|
|
MarkerWeight = 1;
|
|
MarkerCategory = 0;
|
|
|
|
if (playMode && delay > 0)
|
|
{
|
|
GlobalGameManager.ForceFade(0, delay * 0.5f, delay * 0.5f);
|
|
}
|
|
}
|
|
|
|
StartCoroutine(PlayCo(beat, delay));
|
|
//onBeatChanged?.Invoke(beat);
|
|
}
|
|
|
|
private IEnumerator PlayCo(double beat, float delay = 0f)
|
|
{
|
|
bool paused = Conductor.instance.isPaused;
|
|
|
|
if (!paused)
|
|
{
|
|
Conductor.instance.SetBpm(Beatmap.TempoChanges[0]["tempo"]);
|
|
Conductor.instance.SetVolume(Beatmap.VolumeChanges[0]["volume"]);
|
|
Conductor.instance.firstBeatOffset = Beatmap.data.offset;
|
|
SetCurrentEventToClosest(beat);
|
|
KillAllSounds();
|
|
|
|
if (delay > 0)
|
|
{
|
|
yield return new WaitForSeconds(delay);
|
|
}
|
|
}
|
|
|
|
if (!paused)
|
|
{
|
|
Conductor.instance.PlaySetup(beat);
|
|
Minigame miniGame = null;
|
|
if (currentGameO != null && currentGameO.TryGetComponent<Minigame>(out miniGame))
|
|
{
|
|
if (miniGame != null)
|
|
{
|
|
miniGame.OnPlay(beat);
|
|
}
|
|
}
|
|
onPlay?.Invoke(beat);
|
|
|
|
bool hasStartPerfect = false;
|
|
foreach (RiqEntity marker in Beatmap.SectionMarkers)
|
|
{
|
|
if (marker["startPerfect"])
|
|
{
|
|
hasStartPerfect = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (OverlaysManager.OverlaysEnabled && !hasStartPerfect)
|
|
{
|
|
if (PersistentDataManager.gameSettings.perfectChallengeType != PersistentDataManager.PerfectChallengeType.Off)
|
|
{
|
|
GoForAPerfect.instance.Enable(0);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
onUnPause?.Invoke(beat);
|
|
}
|
|
|
|
if (playMode)
|
|
{
|
|
CircleCursor.LockCursor(true);
|
|
}
|
|
Application.backgroundLoadingPriority = ThreadPriority.Low;
|
|
Conductor.instance.Play(beat);
|
|
}
|
|
|
|
public void Pause()
|
|
{
|
|
Conductor.instance.Pause();
|
|
Util.SoundByte.PauseOneShots();
|
|
onPause?.Invoke(Conductor.instance.songPositionInBeatsAsDouble);
|
|
canInput = false;
|
|
}
|
|
|
|
public void Stop(double beat, bool restart = false, float restartDelay = 0f)
|
|
{
|
|
// I feel like I should standardize the names
|
|
if (Conductor.instance.isPlaying)
|
|
{
|
|
SkillStarManager.instance.KillStar();
|
|
TimingAccuracyDisplay.instance.StopStarFlash();
|
|
GoForAPerfect.instance.Disable();
|
|
SectionMedalsManager.instance.OnRemixEnd(endBeat, currentSection);
|
|
}
|
|
|
|
Minigame miniGame;
|
|
if (currentGameO != null && currentGameO.TryGetComponent<Minigame>(out miniGame))
|
|
{
|
|
if (miniGame != null)
|
|
{
|
|
miniGame.OnStop(beat);
|
|
}
|
|
}
|
|
|
|
Conductor.instance.Stop(beat);
|
|
SetCurrentEventToClosest(beat);
|
|
|
|
KillAllSounds();
|
|
if (restart)
|
|
{
|
|
Play(0, restartDelay);
|
|
}
|
|
else if (playMode)
|
|
{
|
|
judgementInfo.finalScore = (float)PlayerAccuracy;
|
|
judgementInfo.star = skillStarCollected;
|
|
judgementInfo.perfect = GoForAPerfect.instance.perfect;
|
|
judgementInfo.time = DateTime.Now;
|
|
|
|
JudgementManager.SetPlayInfo(judgementInfo, Beatmap);
|
|
GlobalGameManager.LoadScene("Judgement", 0.35f, 0f);
|
|
CircleCursor.LockCursor(false);
|
|
}
|
|
Application.backgroundLoadingPriority = ThreadPriority.Normal;
|
|
}
|
|
|
|
public void SafePlay(double beat, float delay, bool discord)
|
|
{
|
|
StartCoroutine(WaitReadyAndPlayCo(beat, delay, discord));
|
|
}
|
|
|
|
private IEnumerator WaitReadyAndPlayCo(double beat, float delay = 1f, bool discord = true)
|
|
{
|
|
WaitUntil yieldOverlays = new WaitUntil(() => OverlaysManager.OverlaysReady);
|
|
WaitUntil yieldBeatmap = new WaitUntil(() => Beatmap != null && BeatmapEntities() > 0);
|
|
WaitUntil yieldAudio = new WaitUntil(() => AudioLoadDone || (ChartLoadError && !GlobalGameManager.IsShowingDialog));
|
|
WaitUntil yieldGame = null;
|
|
List<Minigames.Minigame> gamesToPreload = SeekAheadAndPreload(beat, 4f);
|
|
Debug.Log($"Preloading {gamesToPreload.Count} games");
|
|
if (gamesToPreload.Count > 0)
|
|
{
|
|
yieldGame = new WaitUntil(() => gamesToPreload.All(x => x.AssetsLoaded));
|
|
}
|
|
|
|
// wait for overlays to be ready
|
|
Debug.Log("waiting for overlays");
|
|
yield return yieldOverlays;
|
|
// wait for beatmap to be loaded
|
|
Debug.Log("waiting for beatmap");
|
|
yield return yieldBeatmap;
|
|
//wait for audio clip to be loaded
|
|
Debug.Log("waiting for audio");
|
|
yield return yieldAudio;
|
|
//wait for games to be loaded
|
|
Debug.Log("waiting for minigames");
|
|
if (yieldGame != null)
|
|
yield return yieldGame;
|
|
|
|
SkillStarManager.instance.KillStar();
|
|
TimingAccuracyDisplay.instance.StopStarFlash();
|
|
GoForAPerfect.instance.Disable();
|
|
SectionMedalsManager.instance?.Reset();
|
|
|
|
if (discord)
|
|
{
|
|
GlobalGameManager.UpdateDiscordStatus(Beatmap["remixtitle"].ToString(), false, true);
|
|
}
|
|
|
|
Play(beat, delay);
|
|
yield break;
|
|
}
|
|
|
|
public void KillAllSounds()
|
|
{
|
|
Debug.Log("Killing all sounds");
|
|
SoundObjects.Clear();
|
|
Util.SoundByte.KillOneShots();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region List Functions
|
|
|
|
public void SortEventsList()
|
|
{
|
|
Beatmap.Entities.Sort((x, y) => x.beat.CompareTo(y.beat));
|
|
Beatmap.TempoChanges.Sort((x, y) => x.beat.CompareTo(y.beat));
|
|
Beatmap.VolumeChanges.Sort((x, y) => x.beat.CompareTo(y.beat));
|
|
Beatmap.SectionMarkers.Sort((x, y) => x.beat.CompareTo(y.beat));
|
|
|
|
eventBeats = Beatmap.Entities.Select(c => c.beat).ToList();
|
|
tempoBeats = Beatmap.TempoChanges.Select(c => c.beat).ToList();
|
|
volumeBeats = Beatmap.VolumeChanges.Select(c => c.beat).ToList();
|
|
sectionBeats = Beatmap.SectionMarkers.Select(c => c.beat).ToList();
|
|
|
|
allGameSwitches = EventCaller.GetAllInGameManagerList("gameManager", new string[] { "switchGame" });
|
|
}
|
|
|
|
void SortEventsByPriority(List<RiqEntity> entities)
|
|
{
|
|
string[] xDatamodel;
|
|
string[] yDatamodel;
|
|
entities.Sort((x, y) =>
|
|
{
|
|
xDatamodel = x.datamodel.Split('/');
|
|
yDatamodel = y.datamodel.Split('/');
|
|
|
|
Minigames.GameAction xAction = eventCaller.GetGameAction(xDatamodel[0], xDatamodel[1]);
|
|
Minigames.GameAction yAction = eventCaller.GetGameAction(yDatamodel[0], yDatamodel[1]);
|
|
|
|
return yAction.priority.CompareTo(xAction.priority);
|
|
});
|
|
|
|
}
|
|
|
|
public static double GetClosestInList(List<double> list, double compareTo)
|
|
{
|
|
if (list.Count > 0)
|
|
return list.Aggregate((x, y) => Math.Abs(x - compareTo) < Math.Abs(y - compareTo) ? x : y);
|
|
else
|
|
return double.MinValue;
|
|
}
|
|
|
|
public static int GetIndexAfter(List<double> list, double compareTo)
|
|
{
|
|
list.Sort();
|
|
if (list.Count > 0)
|
|
{
|
|
foreach (double item in list)
|
|
{
|
|
if (item >= compareTo)
|
|
{
|
|
return Math.Max(list.IndexOf(item), 0);
|
|
}
|
|
}
|
|
return list.Count;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public static int GetIndexBefore(List<double> list, double compareTo)
|
|
{
|
|
list.Sort();
|
|
if (list.Count > 0)
|
|
{
|
|
foreach (double item in list)
|
|
{
|
|
if (item >= compareTo)
|
|
{
|
|
return Math.Max(list.IndexOf(item) - 1, 0);
|
|
}
|
|
}
|
|
return list.Count - 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
public void SetCurrentEventToClosest(double beat)
|
|
{
|
|
SortEventsList();
|
|
onBeatChanged?.Invoke(beat);
|
|
if (Beatmap.Entities.Count > 0)
|
|
{
|
|
currentEvent = GetIndexAfter(eventBeats, beat);
|
|
currentPreEvent = GetIndexAfter(eventBeats, beat);
|
|
currentPreSequence = GetIndexAfter(eventBeats, beat);
|
|
|
|
var gameSwitchs = Beatmap.Entities.FindAll(c => c.datamodel.Split("/")[1] == "switchGame");
|
|
|
|
string newGame = Beatmap.Entities[Math.Min(currentEvent, eventBeats.Count - 1)].datamodel.Split(0);
|
|
|
|
if (gameSwitchs.Count > 0)
|
|
{
|
|
int index = GetIndexBefore(gameSwitchs.Select(c => c.beat).ToList(), beat);
|
|
currentPreSwitch = index;
|
|
var closestGameSwitch = gameSwitchs[index];
|
|
if (closestGameSwitch.beat <= beat)
|
|
{
|
|
newGame = closestGameSwitch.datamodel.Split(2);
|
|
}
|
|
else if (closestGameSwitch.beat > beat)
|
|
{
|
|
if (index == 0)
|
|
{
|
|
newGame = Beatmap.Entities[0].datamodel.Split(0);
|
|
}
|
|
else
|
|
{
|
|
if (index - 1 >= 0)
|
|
{
|
|
newGame = gameSwitchs[index - 1].datamodel.Split(2);
|
|
}
|
|
else
|
|
{
|
|
newGame = Beatmap.Entities[Beatmap.Entities.IndexOf(closestGameSwitch) - 1].datamodel.Split(0);
|
|
}
|
|
}
|
|
}
|
|
// newGame = gameSwitchs[gameSwitchs.IndexOf(gameSwitchs.Find(c => c.beat == Mathp.GetClosestInList(gameSwitchs.Select(c => c.beat).ToList(), beat)))].datamodel.Split(2);
|
|
}
|
|
|
|
if (!GetGameInfo(newGame).fxOnly)
|
|
{
|
|
SetGame(newGame);
|
|
}
|
|
|
|
List<RiqEntity> allEnds = EventCaller.GetAllInGameManagerList("gameManager", new string[] { "end" });
|
|
if (allEnds.Count > 0)
|
|
endBeat = allEnds.Select(c => c.beat).Min();
|
|
else
|
|
endBeat = Conductor.instance.SongLengthInBeatsAsDouble();
|
|
}
|
|
else
|
|
{
|
|
SetGame("noGame");
|
|
endBeat = Conductor.instance.SongLengthInBeatsAsDouble();
|
|
}
|
|
|
|
if (Beatmap.TempoChanges.Count > 0)
|
|
{
|
|
currentTempoEvent = 0;
|
|
|
|
//for tempo changes, just go over all of em until the last one we pass
|
|
for (int t = 0; t < tempoBeats.Count; t++)
|
|
{
|
|
// Debug.Log("checking tempo event " + t + " against beat " + beat + "( tc beat " + tempoChanges[t] + ")");
|
|
if (tempoBeats[t] > beat)
|
|
{
|
|
break;
|
|
}
|
|
currentTempoEvent = t;
|
|
}
|
|
// Debug.Log("currentTempoEvent is now " + currentTempoEvent);
|
|
}
|
|
|
|
if (Beatmap.VolumeChanges.Count > 0)
|
|
{
|
|
currentVolumeEvent = 0;
|
|
|
|
for (int t = 0; t < volumeBeats.Count; t++)
|
|
{
|
|
if (volumeBeats[t] > beat)
|
|
{
|
|
break;
|
|
}
|
|
currentVolumeEvent = t;
|
|
}
|
|
}
|
|
|
|
lastSection = null;
|
|
currentSection = null;
|
|
if (Beatmap.SectionMarkers.Count > 0)
|
|
{
|
|
currentSectionEvent = 0;
|
|
|
|
for (int t = 0; t < sectionBeats.Count; t++)
|
|
{
|
|
if (sectionBeats[t] > beat)
|
|
{
|
|
break;
|
|
}
|
|
currentSectionEvent = t;
|
|
}
|
|
}
|
|
onSectionChange?.Invoke(currentSection, lastSection);
|
|
|
|
SeekAheadAndPreload(beat);
|
|
}
|
|
|
|
#endregion
|
|
|
|
public void SwitchGame(string game, double beat, bool flash)
|
|
{
|
|
if (game != currentGame)
|
|
{
|
|
if (currentGameSwitchIE != null)
|
|
StopCoroutine(currentGameSwitchIE);
|
|
currentGameSwitchIE = StartCoroutine(SwitchGameIE(game, beat, flash));
|
|
}
|
|
}
|
|
|
|
IEnumerator SwitchGameIE(string game, double beat, bool flash)
|
|
{
|
|
if (flash)
|
|
{
|
|
HeavenStudio.StaticCamera.instance.ToggleCanvasVisibility(false);
|
|
}
|
|
|
|
SetGame(game, false);
|
|
|
|
Minigame miniGame;
|
|
if (currentGameO != null && currentGameO.TryGetComponent<Minigame>(out miniGame))
|
|
{
|
|
if (miniGame != null)
|
|
{
|
|
miniGame.OnGameSwitch(beat);
|
|
}
|
|
}
|
|
|
|
while (beat + 0.25 > Math.Max(Conductor.instance.songPositionInBeatsAsDouble, 0))
|
|
{
|
|
if (!Conductor.instance.isPlaying)
|
|
{
|
|
HeavenStudio.StaticCamera.instance.ToggleCanvasVisibility(true);
|
|
SetAmbientGlowToCurrentMinigameColor();
|
|
StopCoroutine(currentGameSwitchIE);
|
|
}
|
|
yield return null;
|
|
}
|
|
|
|
HeavenStudio.StaticCamera.instance.ToggleCanvasVisibility(true);
|
|
SetAmbientGlowToCurrentMinigameColor();
|
|
}
|
|
|
|
private void SetGame(string game, bool useMinigameColor = true)
|
|
{
|
|
ResetCamera(); // resetting camera before setting new minigame so minigames can set camera values in their awake call - Rasmus
|
|
|
|
Destroy(currentGameO);
|
|
|
|
currentGameO = Instantiate(GetGame(game));
|
|
if (currentGameO.TryGetComponent<Minigame>(out var minigame))
|
|
{
|
|
_currentMinigame = minigame;
|
|
minigame.minigameName = game;
|
|
}
|
|
Vector3 originalScale = currentGameO.transform.localScale;
|
|
currentGameO.transform.parent = eventCaller.GamesHolder.transform;
|
|
currentGameO.transform.localScale = originalScale;
|
|
currentGameO.name = game;
|
|
|
|
SetCurrentGame(game, useMinigameColor);
|
|
}
|
|
|
|
private IEnumerator WaitAndSetGame(string game, bool useMinigameColor = true)
|
|
{
|
|
var inf = GetGameInfo(game);
|
|
if (inf != null && inf.usesAssetBundle && !inf.AssetsLoaded)
|
|
{
|
|
Debug.Log($"ASYNC loading assetbundles for game {game}");
|
|
inf.LoadAssetsAsync().Forget();
|
|
yield return new WaitUntil(() => inf.AssetsLoaded);
|
|
}
|
|
SetGame(game, useMinigameColor);
|
|
}
|
|
|
|
public void PreloadGameSequences(string game)
|
|
{
|
|
var gameInfo = GetGameInfo(game);
|
|
//load the games' sound sequences
|
|
// TODO: sound sequences sould be stored in a ScriptableObject
|
|
if (gameInfo != null && gameInfo.LoadedSoundSequences == null)
|
|
gameInfo.LoadedSoundSequences = GetGame(game).GetComponent<Minigame>().SoundSequences;
|
|
}
|
|
|
|
public GameObject GetGame(string name)
|
|
{
|
|
var gameInfo = GetGameInfo(name);
|
|
if (gameInfo != null)
|
|
{
|
|
if (gameInfo.inferred)
|
|
{
|
|
return Resources.Load<GameObject>($"Games/noGame");
|
|
}
|
|
if (gameInfo.fxOnly)
|
|
{
|
|
var gameInfos = Beatmap.Entities
|
|
.Select(x => x.datamodel.Split(0))
|
|
.Select(x => GetGameInfo(x))
|
|
.Where(x => x != null)
|
|
.Where(x => !x.fxOnly)
|
|
.Select(x => x.LoadableName);
|
|
name = gameInfos.FirstOrDefault() ?? "noGame";
|
|
}
|
|
else
|
|
{
|
|
if (gameInfo.usesAssetBundle)
|
|
{
|
|
//game is packed in an assetbundle, load from that instead
|
|
if (gameInfo.AssetsLoaded && gameInfo.LoadedPrefab != null) return gameInfo.LoadedPrefab;
|
|
|
|
try
|
|
{
|
|
return gameInfo.GetCommonAssetBundle().LoadAsset<GameObject>(name);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"Failed to load assetbundle for game {name}, using sync loading: {e.Message}");
|
|
return Resources.Load<GameObject>($"Games/{name}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Resources.Load<GameObject>($"Games/{name}");
|
|
}
|
|
|
|
public Minigames.Minigame GetGameInfo(string name)
|
|
{
|
|
return eventCaller.GetMinigame(name);
|
|
}
|
|
|
|
Color colMain;
|
|
public void SetCurrentGame(string game, bool useMinigameColor = true)
|
|
{
|
|
currentGame = game;
|
|
if (GetGameInfo(currentGame) != null)
|
|
{
|
|
colMain = Colors.Hex2RGB(GetGameInfo(currentGame).color);
|
|
CircleCursor.SetCursorColors(colMain, Colors.Hex2RGB(GetGameInfo(currentGame).splitColorL), Colors.Hex2RGB(GetGameInfo(currentGame).splitColorR));
|
|
if (useMinigameColor) HeavenStudio.StaticCamera.instance.SetAmbientGlowColour(colMain, true);
|
|
else HeavenStudio.StaticCamera.instance.SetAmbientGlowColour(Color.black, false);
|
|
}
|
|
else
|
|
{
|
|
CircleCursor.SetCursorColors(Color.white, Color.white, Color.white);
|
|
HeavenStudio.StaticCamera.instance.SetAmbientGlowColour(Color.black, false);
|
|
}
|
|
CircleCursor.ClearTrail(false);
|
|
}
|
|
|
|
private void SetAmbientGlowToCurrentMinigameColor()
|
|
{
|
|
if (GetGameInfo(currentGame) != null)
|
|
HeavenStudio.StaticCamera.instance.SetAmbientGlowColour(Colors.Hex2RGB(GetGameInfo(currentGame).color), true);
|
|
}
|
|
|
|
private bool SongPosLessThanClipLength(float t)
|
|
{
|
|
if (Conductor.instance.musicSource.clip != null)
|
|
return Conductor.instance.GetSongPosFromBeat(t) < Conductor.instance.musicSource.clip.length;
|
|
else
|
|
return true;
|
|
}
|
|
|
|
public void ResetCamera()
|
|
{
|
|
HeavenStudio.GameCamera.ResetAdditionalTransforms();
|
|
}
|
|
}
|
|
} |