forked from tsuki/openroller
Initial public source release
Split reusable rendering and format support into vectorail-core and vectorail-gc.
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
TARGET = openroller_psp
|
||||
OBJS = src/main.o src/StageRuntime.o src/AudioPlayer.o src/Gameplay.o src/SongMenu.o
|
||||
|
||||
INCDIR = include ../include
|
||||
ifeq ($(DEBUG),1)
|
||||
CFLAGS = -Og -g3 -G0 -Wall -Wextra
|
||||
else
|
||||
CFLAGS = -O2 -G0 -Wall -Wextra
|
||||
endif
|
||||
CXXFLAGS = $(CFLAGS) -std=gnu++17 -fno-exceptions -fno-rtti
|
||||
CXXFLAGS += -DOPENROLLER_BUILD_TIMESTAMP=\"$(BUILD_TIMESTAMP)\"
|
||||
ASFLAGS = $(CFLAGS)
|
||||
|
||||
LIBDIR =
|
||||
LDFLAGS =
|
||||
LIBS = -lpspgum -lpspgu -lpspaudio -lpspmp3
|
||||
|
||||
BUILD_PRX = 1
|
||||
|
||||
EXTRA_TARGETS = EBOOT.PBP
|
||||
PSP_EBOOT_TITLE = OpenRoller PSP
|
||||
|
||||
# XMB media is optional so the public source tree can be built without
|
||||
# redistributing project-specific artwork or music.
|
||||
ifneq ($(wildcard assets/ICON0.PNG),)
|
||||
PSP_EBOOT_ICON = assets/ICON0.PNG
|
||||
endif
|
||||
ifneq ($(wildcard assets/PIC1.PNG),)
|
||||
PSP_EBOOT_PIC1 = assets/PIC1.PNG
|
||||
endif
|
||||
ifneq ($(wildcard assets/SND0.AT3),)
|
||||
PSP_EBOOT_SND0 = assets/SND0.AT3
|
||||
endif
|
||||
|
||||
PSPSDK := $(shell psp-config --pspsdk-path)
|
||||
include $(PSPSDK)/lib/build.mak
|
||||
@@ -0,0 +1,11 @@
|
||||
# Optional XMB media
|
||||
|
||||
The public source tree builds without any files in this directory. To customize
|
||||
the XMB entry for a local build, provide any of these conventional PSP assets:
|
||||
|
||||
- `ICON0.PNG` — application icon.
|
||||
- `PIC1.PNG` — full-screen background image.
|
||||
- `SND0.AT3` — short ATRAC3 background audio.
|
||||
|
||||
The GNUmakefile detects each file independently. These media files are ignored
|
||||
by Git and are not covered by the OpenRoller source license.
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace openroller::psp {
|
||||
|
||||
enum class AudioEffect : std::uint8_t {
|
||||
Adlib = 0,
|
||||
Tap1 = 1,
|
||||
Tap2 = 2,
|
||||
};
|
||||
|
||||
bool startAudioPlayer(
|
||||
const char* bgmPath,
|
||||
const char* shotPath,
|
||||
const char* effectDirectory);
|
||||
void stopAudioPlayer();
|
||||
bool audioPlayerRunning();
|
||||
bool audioPlayerFinished();
|
||||
std::uint32_t audioPlayerTimeMs();
|
||||
void setAudioPlayerPaused(bool paused);
|
||||
void seekAudioPlayer(std::uint32_t timeMs);
|
||||
void setAudioPlayerShotMuted(bool muted);
|
||||
void playAudioPlayerEffect(AudioEffect effect);
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "StageRuntime.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace openroller::psp {
|
||||
|
||||
enum class Judgment : std::uint8_t {
|
||||
// Values 0..3 match the original game's recovered enum.
|
||||
Miss = 0,
|
||||
Good = 1,
|
||||
Cool = 2,
|
||||
Great = 3,
|
||||
Pending = 0xff,
|
||||
};
|
||||
|
||||
struct GameplayState {
|
||||
struct NoteRuntime {
|
||||
std::uint8_t judgment = static_cast<std::uint8_t>(Judgment::Pending);
|
||||
std::uint8_t holding = 0;
|
||||
std::uint8_t shotMuteApplied = 0;
|
||||
std::uint8_t reserved = 0;
|
||||
float inputStartTimeMs = -1.0f;
|
||||
float lastInputTimeMs = -1.0f;
|
||||
std::uint32_t inputMask = 0;
|
||||
std::uint32_t lastInputBit = 0;
|
||||
};
|
||||
|
||||
NoteRuntime* notes = nullptr;
|
||||
std::uint32_t noteCount = 0;
|
||||
std::uint32_t nextPending = 0;
|
||||
std::uint32_t combo = 0;
|
||||
std::uint32_t maximumCombo = 0;
|
||||
std::uint32_t greatCount = 0;
|
||||
std::uint32_t coolCount = 0;
|
||||
std::uint32_t goodCount = 0;
|
||||
std::uint32_t missCount = 0;
|
||||
float previousClockMs = 0.0f;
|
||||
float lastJudgmentClockMs = -10000.0f;
|
||||
std::int32_t lastJudgedNote = -1;
|
||||
Judgment lastJudgment = Judgment::Pending;
|
||||
bool shotMuted = false;
|
||||
};
|
||||
|
||||
bool initializeGameplay(const StageView& stage, GameplayState* gameplay);
|
||||
void destroyGameplay(GameplayState* gameplay);
|
||||
void seekGameplay(const StageView& stage, GameplayState* gameplay, float clockMs);
|
||||
void updateGameplay(const StageView& stage, GameplayState* gameplay, float clockMs);
|
||||
Judgment tapGameplay(const StageView& stage, GameplayState* gameplay, float clockMs);
|
||||
Judgment pressGameplay(
|
||||
const StageView& stage,
|
||||
GameplayState* gameplay,
|
||||
float clockMs,
|
||||
std::uint32_t inputBit);
|
||||
Judgment releaseGameplay(
|
||||
const StageView& stage,
|
||||
GameplayState* gameplay,
|
||||
float clockMs,
|
||||
std::uint32_t inputBit);
|
||||
Judgment noteJudgment(const GameplayState& gameplay, std::uint32_t noteIndex);
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "openroller/psp/SongCatalog.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace openroller::psp {
|
||||
|
||||
struct SongMenu {
|
||||
void* catalogStorage = nullptr;
|
||||
std::size_t catalogSize = 0;
|
||||
const SongCatalogHeader* header = nullptr;
|
||||
const SongCatalogRecord* songs = nullptr;
|
||||
int selection = 0;
|
||||
int difficulty = 0;
|
||||
bool difficultyMode = false;
|
||||
void* jacketStorage = nullptr;
|
||||
std::size_t jacketSize = 0;
|
||||
const std::uint16_t* jacketPixels = nullptr;
|
||||
std::uint16_t jacketWidth = 0;
|
||||
std::uint16_t jacketHeight = 0;
|
||||
char rootPath[256]{};
|
||||
};
|
||||
|
||||
bool loadSongMenu(const char* catalogPath, SongMenu* menu, char* error, std::size_t errorCapacity);
|
||||
void unloadSongMenu(SongMenu* menu);
|
||||
bool moveSongSelection(SongMenu* menu, int delta);
|
||||
bool moveDifficultySelection(SongMenu* menu, int delta);
|
||||
const SongCatalogRecord* selectedSong(const SongMenu& menu);
|
||||
bool selectedSongPath(const SongMenu& menu, char* output, std::size_t capacity);
|
||||
bool selectedAudioPath(const SongMenu& menu, char* output, std::size_t capacity);
|
||||
bool selectedShotAudioPath(const SongMenu& menu, char* output, std::size_t capacity);
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "openroller/psp/StagePackage.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace openroller::psp {
|
||||
|
||||
struct Vec3 {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float z = 0.0f;
|
||||
};
|
||||
|
||||
struct CameraState {
|
||||
Vec3 eye;
|
||||
Vec3 target;
|
||||
Vec3 up{0.0f, 1.0f, 0.0f};
|
||||
float projectionBlend = 0.0f;
|
||||
};
|
||||
|
||||
struct StageView {
|
||||
void* storage = nullptr;
|
||||
std::size_t storageSize = 0;
|
||||
const StagePackageHeader* header = nullptr;
|
||||
const PackageTrackPoint* track = nullptr;
|
||||
const PackageNote* notes = nullptr;
|
||||
const PackageCameraPoint* cameras = nullptr;
|
||||
const PackageDrawDistancePoint* drawDistances = nullptr;
|
||||
const PackageBackgroundColorPoint* backgroundColors = nullptr;
|
||||
const PackageBackgroundModel* backgroundModels = nullptr;
|
||||
const PackageBackgroundVertex* backgroundVertices = nullptr;
|
||||
const PackageBackgroundObject* backgroundObjects = nullptr;
|
||||
const PackageVisibilityKey* visibilityKeys = nullptr;
|
||||
const PackageTransformKey* transformKeys = nullptr;
|
||||
const PackageObjectColorKey* objectColorKeys = nullptr;
|
||||
const PackageParticlePoint* particles = nullptr;
|
||||
const PackageVisualizerPoint* visualizer = nullptr;
|
||||
const PackageBpmPoint* bpmChanges = nullptr;
|
||||
};
|
||||
|
||||
struct BackgroundColors {
|
||||
std::uint32_t topRight = 0;
|
||||
std::uint32_t topLeft = 0;
|
||||
std::uint32_t bottomRight = 0;
|
||||
std::uint32_t bottomLeft = 0;
|
||||
};
|
||||
|
||||
bool loadStagePackage(const char* path, StageView* stage, char* error, std::size_t errorCapacity);
|
||||
void unloadStagePackage(StageView* stage);
|
||||
|
||||
Vec3 trackPositionAt(const StageView& stage, float timeMs);
|
||||
Vec3 trackTangentAt(const StageView& stage, float timeMs);
|
||||
CameraState evaluateCamera(const StageView& stage, float timeMs);
|
||||
BackgroundColors evaluateBackground(const StageView& stage, float timeMs);
|
||||
float evaluateDrawAhead(const StageView& stage, float timeMs);
|
||||
float evaluateBeatDurationMs(const StageView& stage, float timeMs);
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
namespace openroller::psp {
|
||||
|
||||
constexpr int kScreenWidth = 480;
|
||||
constexpr int kScreenHeight = 272;
|
||||
constexpr int kLogicalWidth = 720;
|
||||
constexpr int kLogicalHeight = 1280;
|
||||
constexpr float kLogicalScale = 3.0f / 8.0f;
|
||||
constexpr int kScaledWidth = 270;
|
||||
constexpr int kBorder = (kScreenHeight - kScaledWidth) / 2;
|
||||
|
||||
enum class TateSide {
|
||||
Clockwise,
|
||||
CounterClockwise,
|
||||
};
|
||||
|
||||
struct Point {
|
||||
float x;
|
||||
float y;
|
||||
};
|
||||
|
||||
inline Point toScreen(float logicalX, float logicalY, TateSide side) {
|
||||
if (side == TateSide::Clockwise) {
|
||||
return {
|
||||
static_cast<float>(kScreenWidth) - logicalY * kLogicalScale,
|
||||
static_cast<float>(kBorder) + logicalX * kLogicalScale,
|
||||
};
|
||||
}
|
||||
return {
|
||||
logicalY * kLogicalScale,
|
||||
static_cast<float>(kBorder + kScaledWidth) - logicalX * kLogicalScale,
|
||||
};
|
||||
}
|
||||
|
||||
inline Point screenDirectionToLogical(float screenX, float screenY, TateSide side) {
|
||||
if (side == TateSide::Clockwise) {
|
||||
return {screenY, -screenX};
|
||||
}
|
||||
return {-screenY, screenX};
|
||||
}
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,418 @@
|
||||
#include "AudioPlayer.hpp"
|
||||
|
||||
#include <pspaudio.h>
|
||||
#include <pspiofilemgr.h>
|
||||
#include <pspkernel.h>
|
||||
#include <pspmp3.h>
|
||||
#include <psputility.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace openroller::psp {
|
||||
namespace {
|
||||
|
||||
struct Mp3Decoder {
|
||||
int file = -1;
|
||||
int handle = -1;
|
||||
alignas(64) unsigned char streamBuffer[16 * 1024];
|
||||
alignas(64) unsigned char pcmBuffer[16 * (1152 / 2)];
|
||||
};
|
||||
|
||||
struct AudioState {
|
||||
struct Effect {
|
||||
short* samples = nullptr;
|
||||
std::uint32_t valueCount = 0;
|
||||
volatile std::uint32_t voices[2]{};
|
||||
volatile std::uint32_t nextVoice = 0;
|
||||
};
|
||||
|
||||
Mp3Decoder bgm;
|
||||
Effect effects[3];
|
||||
int channel = -1;
|
||||
int thread = -1;
|
||||
int sampleRate = 0;
|
||||
int channels = 0;
|
||||
int lastDecodedBytes = 0;
|
||||
volatile bool running = false;
|
||||
volatile bool paused = false;
|
||||
volatile bool finished = false;
|
||||
volatile int requestedSeekMs = -1;
|
||||
volatile std::uint32_t playedSamples = 0;
|
||||
int consecutiveOutputFailures = 0;
|
||||
bool resourceInitialized = false;
|
||||
bool avcodecLoaded = false;
|
||||
bool mp3ModuleLoaded = false;
|
||||
alignas(64) std::int32_t mixAccumulator[1152 * 2];
|
||||
// sceAudioSRCOutputBlocking waits until a buffer can be queued; it does
|
||||
// not make the submitted memory immediately reusable. Keep the buffer
|
||||
// currently consumed by the audio hardware separate from the one being
|
||||
// prepared by the decoder/mixer.
|
||||
alignas(64) short mixBuffers[2][1152 * 2];
|
||||
std::uint32_t mixBufferIndex = 0;
|
||||
};
|
||||
|
||||
AudioState gAudio;
|
||||
|
||||
short softLimit(std::int32_t sample) {
|
||||
// Preserve the quiet 75% of the range, then progressively compress peaks.
|
||||
// The old hard clamp produced flat-topped waves whenever BGM, SHOT and a
|
||||
// hit sound crossed 0 dBFS, which is heard as continuous crackle.
|
||||
const bool negative = sample < 0;
|
||||
std::int32_t magnitude = negative ? -sample : sample;
|
||||
if (magnitude > 24576) {
|
||||
if (magnitude <= 32768) {
|
||||
magnitude = 24576 + ((magnitude - 24576) >> 1);
|
||||
} else if (magnitude <= 65528) {
|
||||
magnitude = 28672 + ((magnitude - 32768) >> 3);
|
||||
} else {
|
||||
magnitude = 32767;
|
||||
}
|
||||
}
|
||||
const std::int32_t limited = negative ? -magnitude : magnitude;
|
||||
return static_cast<short>(std::clamp<std::int32_t>(
|
||||
limited, static_cast<std::int32_t>(-32768),
|
||||
static_cast<std::int32_t>(32767)));
|
||||
}
|
||||
|
||||
bool loadEffect(const char* path, AudioState::Effect* effect) {
|
||||
if (!path || !*path || !effect) return false;
|
||||
const int file = sceIoOpen(path, PSP_O_RDONLY, 0777);
|
||||
if (file < 0) return false;
|
||||
const int length = sceIoLseek32(file, 0, PSP_SEEK_END);
|
||||
if (length <= 0 || length > 1024 * 1024 || (length & 3) != 0 ||
|
||||
sceIoLseek32(file, 0, PSP_SEEK_SET) < 0) {
|
||||
sceIoClose(file);
|
||||
return false;
|
||||
}
|
||||
short* samples = static_cast<short*>(std::malloc(static_cast<std::size_t>(length)));
|
||||
if (!samples || sceIoRead(file, samples, length) != length) {
|
||||
std::free(samples);
|
||||
sceIoClose(file);
|
||||
return false;
|
||||
}
|
||||
sceIoClose(file);
|
||||
effect->samples = samples;
|
||||
effect->valueCount = static_cast<std::uint32_t>(length / 2);
|
||||
effect->voices[0] = effect->valueCount;
|
||||
effect->voices[1] = effect->valueCount;
|
||||
return true;
|
||||
}
|
||||
|
||||
void closeEffects() {
|
||||
for (AudioState::Effect& effect : gAudio.effects) {
|
||||
std::free(effect.samples);
|
||||
effect = {};
|
||||
}
|
||||
}
|
||||
|
||||
void closeDecoder(Mp3Decoder* decoder) {
|
||||
if (decoder->handle >= 0) {
|
||||
sceMp3ReleaseMp3Handle(decoder->handle);
|
||||
decoder->handle = -1;
|
||||
}
|
||||
if (decoder->file >= 0) {
|
||||
sceIoClose(decoder->file);
|
||||
decoder->file = -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool fillDecoder(Mp3Decoder* decoder) {
|
||||
SceUChar8* destination = nullptr;
|
||||
SceInt32 writable = 0;
|
||||
SceInt32 sourcePosition = 0;
|
||||
if (!decoder || decoder->handle < 0 ||
|
||||
sceMp3GetInfoToAddStreamData(
|
||||
decoder->handle, &destination, &writable, &sourcePosition) < 0) {
|
||||
return false;
|
||||
}
|
||||
if (sceIoLseek32(decoder->file, sourcePosition, PSP_SEEK_SET) < 0) return false;
|
||||
const int read = sceIoRead(decoder->file, destination, writable);
|
||||
if (read <= 0) return false;
|
||||
return sceMp3NotifyAddStreamData(decoder->handle, read) >= 0;
|
||||
}
|
||||
|
||||
bool openDecoder(const char* path, Mp3Decoder* decoder) {
|
||||
if (!path || !*path || !decoder) return false;
|
||||
decoder->file = sceIoOpen(path, PSP_O_RDONLY, 0777);
|
||||
if (decoder->file < 0) return false;
|
||||
SceMp3InitArg arguments{};
|
||||
arguments.mp3StreamStart = 0;
|
||||
arguments.mp3StreamEnd = sceIoLseek32(decoder->file, 0, PSP_SEEK_END);
|
||||
arguments.mp3Buf = decoder->streamBuffer;
|
||||
arguments.mp3BufSize = sizeof(decoder->streamBuffer);
|
||||
arguments.pcmBuf = decoder->pcmBuffer;
|
||||
arguments.pcmBufSize = sizeof(decoder->pcmBuffer);
|
||||
decoder->handle = sceMp3ReserveMp3Handle(&arguments);
|
||||
if (decoder->handle < 0 || !fillDecoder(decoder) ||
|
||||
sceMp3Init(decoder->handle) < 0 ||
|
||||
// The firmware decoder retains an implicit looping mode on some PSP
|
||||
// revisions. Zero means no repeats (play the stream exactly once).
|
||||
// Without making this explicit, sceMp3Decode jumps back to frame zero
|
||||
// instead of returning EOF, so the gameplay loop never sees
|
||||
// audioPlayerFinished().
|
||||
sceMp3SetLoopNum(decoder->handle, 0) < 0) {
|
||||
closeDecoder(decoder);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void resetDecoder(Mp3Decoder* decoder, std::uint32_t frame) {
|
||||
if (!decoder || decoder->handle < 0) return;
|
||||
if (frame == 0) sceMp3ResetPlayPosition(decoder->handle);
|
||||
else sceMp3ResetPlayPositionByFrame(decoder->handle, frame);
|
||||
}
|
||||
|
||||
int decode(Mp3Decoder* decoder, short** samples) {
|
||||
if (!decoder || decoder->handle < 0 || !samples) return 0;
|
||||
if (sceMp3CheckStreamDataNeeded(decoder->handle) > 0) fillDecoder(decoder);
|
||||
int decoded = sceMp3Decode(decoder->handle, samples);
|
||||
if (decoded <= 0 &&
|
||||
sceMp3CheckStreamDataNeeded(decoder->handle) > 0 &&
|
||||
fillDecoder(decoder)) {
|
||||
decoded = sceMp3Decode(decoder->handle, samples);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
void resetPlayback(std::uint32_t timeMs) {
|
||||
const std::uint64_t targetSamples =
|
||||
static_cast<std::uint64_t>(timeMs) *
|
||||
static_cast<std::uint32_t>(gAudio.sampleRate) / 1000u;
|
||||
const std::uint32_t frame = static_cast<std::uint32_t>(targetSamples / 1152u);
|
||||
resetDecoder(&gAudio.bgm, frame);
|
||||
gAudio.playedSamples = frame * 1152u;
|
||||
gAudio.finished = false;
|
||||
gAudio.consecutiveOutputFailures = 0;
|
||||
}
|
||||
|
||||
int outputSamples(const short* samples, int decodedBytes) {
|
||||
if (decodedBytes <= 0 || !samples) return -1;
|
||||
if (gAudio.channel < 0 || decodedBytes != gAudio.lastDecodedBytes) {
|
||||
if (gAudio.channel >= 0) sceAudioSRCChRelease();
|
||||
const int sampleCount = decodedBytes / (2 * gAudio.channels);
|
||||
gAudio.channel = sceAudioSRCChReserve(
|
||||
sampleCount, gAudio.sampleRate, gAudio.channels);
|
||||
gAudio.lastDecodedBytes = decodedBytes;
|
||||
}
|
||||
if (gAudio.channel < 0) return gAudio.channel;
|
||||
return sceAudioSRCOutputBlocking(
|
||||
PSP_AUDIO_VOLUME_MAX, const_cast<short*>(samples));
|
||||
}
|
||||
|
||||
int audioThread(SceSize, void*) {
|
||||
while (gAudio.running) {
|
||||
const int requestedSeek = gAudio.requestedSeekMs;
|
||||
if (requestedSeek >= 0) {
|
||||
resetPlayback(static_cast<std::uint32_t>(requestedSeek));
|
||||
gAudio.requestedSeekMs = -1;
|
||||
}
|
||||
if (gAudio.paused) {
|
||||
sceKernelDelayThread(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
short* bgmSamples = nullptr;
|
||||
int bgmBytes = decode(&gAudio.bgm, &bgmSamples);
|
||||
if (bgmBytes <= 0) {
|
||||
// The compressed stream is authoritative. Stage DAT duration
|
||||
// describes authored gameplay data and may end before or after
|
||||
// the actual mix; do not synthesize silence to match it.
|
||||
gAudio.finished = true;
|
||||
sceKernelDelayThread(5000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const int valueCount = bgmBytes / 2;
|
||||
short* const mixBuffer = gAudio.mixBuffers[gAudio.mixBufferIndex];
|
||||
for (int i = 0; i < valueCount; ++i) {
|
||||
gAudio.mixAccumulator[i] = static_cast<std::int32_t>(bgmSamples[i]);
|
||||
}
|
||||
for (AudioState::Effect& effect : gAudio.effects) {
|
||||
if (!effect.samples) continue;
|
||||
for (volatile std::uint32_t& voice : effect.voices) {
|
||||
std::uint32_t position = voice;
|
||||
if (position >= effect.valueCount) continue;
|
||||
const int available = std::min(
|
||||
valueCount,
|
||||
static_cast<int>(effect.valueCount - position));
|
||||
for (int i = 0; i < available; ++i) {
|
||||
gAudio.mixAccumulator[i] +=
|
||||
static_cast<std::int32_t>(effect.samples[position + i]);
|
||||
}
|
||||
position += static_cast<std::uint32_t>(available);
|
||||
voice = position;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < valueCount; ++i) {
|
||||
mixBuffer[i] = softLimit(gAudio.mixAccumulator[i]);
|
||||
}
|
||||
const int outputResult = outputSamples(mixBuffer, bgmBytes);
|
||||
if (outputResult >= 0) {
|
||||
// Some firmwares return the queued sample count and some audio
|
||||
// implementations return zero on success. The submitted frame
|
||||
// size is authoritative in either case.
|
||||
const int sampleCount = bgmBytes / (2 * gAudio.channels);
|
||||
gAudio.playedSamples += static_cast<std::uint32_t>(sampleCount);
|
||||
gAudio.mixBufferIndex ^= 1u;
|
||||
gAudio.consecutiveOutputFailures = 0;
|
||||
} else {
|
||||
if (++gAudio.consecutiveOutputFailures >= 8) {
|
||||
// Do not leave the application trapped forever in a chart if
|
||||
// the firmware loses its SRC channel.
|
||||
gAudio.finished = true;
|
||||
gAudio.paused = true;
|
||||
}
|
||||
sceKernelDelayThread(5000);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cleanupAudio() {
|
||||
if (gAudio.channel >= 0) {
|
||||
sceAudioSRCChRelease();
|
||||
gAudio.channel = -1;
|
||||
}
|
||||
closeDecoder(&gAudio.bgm);
|
||||
closeEffects();
|
||||
if (gAudio.resourceInitialized) {
|
||||
sceMp3TermResource();
|
||||
gAudio.resourceInitialized = false;
|
||||
}
|
||||
if (gAudio.mp3ModuleLoaded) {
|
||||
sceUtilityUnloadModule(PSP_MODULE_AV_MP3);
|
||||
gAudio.mp3ModuleLoaded = false;
|
||||
}
|
||||
if (gAudio.avcodecLoaded) {
|
||||
sceUtilityUnloadModule(PSP_MODULE_AV_AVCODEC);
|
||||
gAudio.avcodecLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool startAudioPlayer(
|
||||
const char* bgmPath,
|
||||
const char* shotPath,
|
||||
const char* effectDirectory) {
|
||||
stopAudioPlayer();
|
||||
// PSP-1000 cannot reliably decode and mix two independent MP3 streams
|
||||
// while the 3D stage is running. Release assets are pre-mixed, so SHOT is
|
||||
// intentionally ignored here; hit/ad-lib PCM effects remain interactive.
|
||||
(void)shotPath;
|
||||
if (!bgmPath || !*bgmPath) return false;
|
||||
if (sceUtilityLoadModule(PSP_MODULE_AV_AVCODEC) < 0) return false;
|
||||
gAudio.avcodecLoaded = true;
|
||||
if (sceUtilityLoadModule(PSP_MODULE_AV_MP3) < 0) {
|
||||
cleanupAudio();
|
||||
return false;
|
||||
}
|
||||
gAudio.mp3ModuleLoaded = true;
|
||||
if (sceMp3InitResource() < 0) {
|
||||
cleanupAudio();
|
||||
return false;
|
||||
}
|
||||
gAudio.resourceInitialized = true;
|
||||
if (!openDecoder(bgmPath, &gAudio.bgm)) {
|
||||
cleanupAudio();
|
||||
return false;
|
||||
}
|
||||
if (effectDirectory && *effectDirectory) {
|
||||
static constexpr const char* names[3] = {
|
||||
"adlib.pcm", "tap1.pcm", "tap2.pcm",
|
||||
};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
char path[384]{};
|
||||
std::snprintf(path, sizeof(path), "%s/%s", effectDirectory, names[i]);
|
||||
loadEffect(path, &gAudio.effects[i]);
|
||||
}
|
||||
}
|
||||
|
||||
gAudio.sampleRate = sceMp3GetSamplingRate(gAudio.bgm.handle);
|
||||
gAudio.channels = sceMp3GetMp3ChannelNum(gAudio.bgm.handle);
|
||||
if (gAudio.sampleRate <= 0 ||
|
||||
(gAudio.channels != 1 && gAudio.channels != 2)) {
|
||||
cleanupAudio();
|
||||
return false;
|
||||
}
|
||||
gAudio.playedSamples = 0;
|
||||
gAudio.mixBufferIndex = 0;
|
||||
gAudio.consecutiveOutputFailures = 0;
|
||||
gAudio.requestedSeekMs = -1;
|
||||
gAudio.paused = false;
|
||||
gAudio.finished = false;
|
||||
gAudio.running = true;
|
||||
gAudio.thread = sceKernelCreateThread(
|
||||
"OpenRoller audio", audioThread, 0x12, 0x5000,
|
||||
PSP_THREAD_ATTR_USER, nullptr);
|
||||
if (gAudio.thread < 0 ||
|
||||
sceKernelStartThread(gAudio.thread, 0, nullptr) < 0) {
|
||||
gAudio.running = false;
|
||||
cleanupAudio();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void stopAudioPlayer() {
|
||||
if (gAudio.running) {
|
||||
gAudio.running = false;
|
||||
if (gAudio.thread >= 0) sceKernelWaitThreadEnd(gAudio.thread, nullptr);
|
||||
}
|
||||
if (gAudio.thread >= 0) {
|
||||
sceKernelDeleteThread(gAudio.thread);
|
||||
gAudio.thread = -1;
|
||||
}
|
||||
cleanupAudio();
|
||||
gAudio.sampleRate = 0;
|
||||
gAudio.channels = 0;
|
||||
gAudio.lastDecodedBytes = 0;
|
||||
gAudio.playedSamples = 0;
|
||||
gAudio.mixBufferIndex = 0;
|
||||
gAudio.consecutiveOutputFailures = 0;
|
||||
gAudio.requestedSeekMs = -1;
|
||||
gAudio.paused = false;
|
||||
gAudio.finished = false;
|
||||
}
|
||||
|
||||
bool audioPlayerRunning() {
|
||||
return gAudio.running && gAudio.sampleRate > 0;
|
||||
}
|
||||
|
||||
bool audioPlayerFinished() {
|
||||
return gAudio.finished;
|
||||
}
|
||||
|
||||
std::uint32_t audioPlayerTimeMs() {
|
||||
if (gAudio.sampleRate <= 0) return 0;
|
||||
return static_cast<std::uint32_t>(
|
||||
static_cast<std::uint64_t>(gAudio.playedSamples) * 1000u /
|
||||
static_cast<std::uint32_t>(gAudio.sampleRate));
|
||||
}
|
||||
|
||||
void setAudioPlayerPaused(bool paused) {
|
||||
gAudio.paused = paused;
|
||||
}
|
||||
|
||||
void seekAudioPlayer(std::uint32_t timeMs) {
|
||||
if (gAudio.running) gAudio.requestedSeekMs = static_cast<int>(timeMs);
|
||||
}
|
||||
|
||||
void setAudioPlayerShotMuted(bool muted) {
|
||||
(void)muted;
|
||||
}
|
||||
|
||||
void playAudioPlayerEffect(AudioEffect effect) {
|
||||
const unsigned index = static_cast<unsigned>(effect);
|
||||
if (index >= 3 || !gAudio.effects[index].samples) return;
|
||||
AudioState::Effect& slot = gAudio.effects[index];
|
||||
const std::uint32_t voice = slot.nextVoice++ & 1u;
|
||||
slot.voices[voice] = 0;
|
||||
}
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,344 @@
|
||||
#include "Gameplay.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
|
||||
namespace openroller::psp {
|
||||
namespace {
|
||||
|
||||
bool tapTarget(std::uint8_t type) {
|
||||
return type == 1 || type == 2;
|
||||
}
|
||||
|
||||
bool dualTapTarget(std::uint8_t type) {
|
||||
return type == 9;
|
||||
}
|
||||
|
||||
bool holdTarget(std::uint8_t type) {
|
||||
return type == 3 || type == 15;
|
||||
}
|
||||
|
||||
bool rhythmLongTarget(std::uint8_t type) {
|
||||
return type == 4 || type == 5;
|
||||
}
|
||||
|
||||
bool supportedTarget(std::uint8_t type) {
|
||||
return tapTarget(type) || dualTapTarget(type) ||
|
||||
holdTarget(type) || rhythmLongTarget(type);
|
||||
}
|
||||
|
||||
Judgment tapJudgment(
|
||||
const PackageNote& note,
|
||||
float clockMs,
|
||||
float greatMinimumMs) {
|
||||
const float error = std::fabs(clockMs - static_cast<float>(note.timeMs));
|
||||
const float outer = std::max(
|
||||
0.0f,
|
||||
clockMs > static_cast<float>(note.timeMs)
|
||||
? note.lateTimingMs
|
||||
: note.earlyTimingMs);
|
||||
float great = outer * 0.25f;
|
||||
float cool = outer * 0.50f;
|
||||
if (great < greatMinimumMs) {
|
||||
great = greatMinimumMs;
|
||||
cool = std::min(outer, great + (outer - great) / 3.0f);
|
||||
}
|
||||
if (error < great) return Judgment::Great;
|
||||
if (error < cool) return Judgment::Cool;
|
||||
if (error < outer) return Judgment::Good;
|
||||
return Judgment::Miss;
|
||||
}
|
||||
|
||||
Judgment longJudgment(
|
||||
const PackageNote& note,
|
||||
float pressTimeMs,
|
||||
float releaseTimeMs) {
|
||||
const float durationMs = std::max(
|
||||
1.0f,
|
||||
note.endTimeMs - static_cast<float>(note.timeMs));
|
||||
const float heldStartMs = std::max(static_cast<float>(note.timeMs), pressTimeMs);
|
||||
const float heldEndMs = std::min(note.endTimeMs, releaseTimeMs);
|
||||
float heldMs = std::max(0.0f, heldEndMs - heldStartMs);
|
||||
const float finalTwentyPercentMs = durationMs * 0.20f;
|
||||
if (finalTwentyPercentMs < 66.666664f) {
|
||||
heldMs += 66.666664f - finalTwentyPercentMs;
|
||||
}
|
||||
const float heldPercent = heldMs * 100.0f / durationMs;
|
||||
if (heldPercent > 80.0f) return Judgment::Great;
|
||||
if (heldPercent > 60.0f) return Judgment::Cool;
|
||||
if (heldPercent > 40.0f) return Judgment::Good;
|
||||
return Judgment::Miss;
|
||||
}
|
||||
|
||||
void resetRuntime(GameplayState::NoteRuntime* note) {
|
||||
*note = {};
|
||||
note->judgment = static_cast<std::uint8_t>(Judgment::Pending);
|
||||
note->inputStartTimeMs = -1.0f;
|
||||
note->lastInputTimeMs = -1.0f;
|
||||
}
|
||||
|
||||
void advancePending(GameplayState* gameplay) {
|
||||
while (gameplay->nextPending < gameplay->noteCount &&
|
||||
gameplay->notes[gameplay->nextPending].judgment !=
|
||||
static_cast<std::uint8_t>(Judgment::Pending)) {
|
||||
++gameplay->nextPending;
|
||||
}
|
||||
}
|
||||
|
||||
void recordJudgment(
|
||||
GameplayState* gameplay,
|
||||
std::uint32_t noteIndex,
|
||||
Judgment judgment,
|
||||
float clockMs) {
|
||||
GameplayState::NoteRuntime& runtime = gameplay->notes[noteIndex];
|
||||
runtime.judgment = static_cast<std::uint8_t>(judgment);
|
||||
runtime.holding = 0;
|
||||
runtime.inputMask = 0;
|
||||
gameplay->lastJudgment = judgment;
|
||||
gameplay->lastJudgmentClockMs = clockMs;
|
||||
gameplay->lastJudgedNote = static_cast<std::int32_t>(noteIndex);
|
||||
gameplay->shotMuted = judgment == Judgment::Miss;
|
||||
if (judgment == Judgment::Great) {
|
||||
++gameplay->greatCount;
|
||||
++gameplay->combo;
|
||||
} else if (judgment == Judgment::Cool) {
|
||||
++gameplay->coolCount;
|
||||
++gameplay->combo;
|
||||
} else if (judgment == Judgment::Good) {
|
||||
++gameplay->goodCount;
|
||||
++gameplay->combo;
|
||||
} else {
|
||||
++gameplay->missCount;
|
||||
gameplay->combo = 0;
|
||||
}
|
||||
gameplay->maximumCombo = std::max(gameplay->maximumCombo, gameplay->combo);
|
||||
if (noteIndex == gameplay->nextPending) advancePending(gameplay);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool initializeGameplay(const StageView& stage, GameplayState* gameplay) {
|
||||
if (!gameplay || !stage.header || stage.header->notes.count == 0) return false;
|
||||
destroyGameplay(gameplay);
|
||||
gameplay->notes = static_cast<GameplayState::NoteRuntime*>(
|
||||
std::malloc(
|
||||
static_cast<std::size_t>(stage.header->notes.count) *
|
||||
sizeof(GameplayState::NoteRuntime)));
|
||||
if (!gameplay->notes) return false;
|
||||
gameplay->noteCount = stage.header->notes.count;
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
resetRuntime(&gameplay->notes[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void destroyGameplay(GameplayState* gameplay) {
|
||||
if (!gameplay) return;
|
||||
std::free(gameplay->notes);
|
||||
*gameplay = {};
|
||||
}
|
||||
|
||||
void seekGameplay(const StageView& stage, GameplayState* gameplay, float clockMs) {
|
||||
if (!gameplay || !gameplay->notes || !stage.header) return;
|
||||
gameplay->nextPending = 0;
|
||||
gameplay->combo = 0;
|
||||
gameplay->maximumCombo = 0;
|
||||
gameplay->greatCount = 0;
|
||||
gameplay->coolCount = 0;
|
||||
gameplay->goodCount = 0;
|
||||
gameplay->missCount = 0;
|
||||
gameplay->lastJudgment = Judgment::Pending;
|
||||
gameplay->lastJudgmentClockMs = -10000.0f;
|
||||
gameplay->lastJudgedNote = -1;
|
||||
gameplay->previousClockMs = clockMs;
|
||||
gameplay->shotMuted = false;
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
resetRuntime(&gameplay->notes[i]);
|
||||
const PackageNote& note = stage.notes[i];
|
||||
if (supportedTarget(note.effectiveType) &&
|
||||
static_cast<float>(note.timeMs) + note.lateTimingMs < clockMs) {
|
||||
gameplay->notes[i].judgment = static_cast<std::uint8_t>(Judgment::Miss);
|
||||
}
|
||||
}
|
||||
advancePending(gameplay);
|
||||
}
|
||||
|
||||
void updateGameplay(const StageView& stage, GameplayState* gameplay, float clockMs) {
|
||||
if (!gameplay || !gameplay->notes || !stage.header) return;
|
||||
if (clockMs + 1000.0f < gameplay->previousClockMs) {
|
||||
seekGameplay(stage, gameplay, clockMs);
|
||||
}
|
||||
gameplay->previousClockMs = clockMs;
|
||||
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
const PackageNote& note = stage.notes[i];
|
||||
GameplayState::NoteRuntime& runtime = gameplay->notes[i];
|
||||
if (!supportedTarget(note.effectiveType) ||
|
||||
runtime.judgment != static_cast<std::uint8_t>(Judgment::Pending)) {
|
||||
continue;
|
||||
}
|
||||
if (!runtime.holding && !runtime.shotMuteApplied &&
|
||||
clockMs >= static_cast<float>(note.timeMs) + note.muteTimingMs) {
|
||||
runtime.shotMuteApplied = 1;
|
||||
gameplay->shotMuted = true;
|
||||
}
|
||||
if (rhythmLongTarget(note.effectiveType) && runtime.holding) {
|
||||
const float enableMs = note.effectiveType == 4
|
||||
? stage.header->scratchEnableTimeMs
|
||||
: stage.header->beatEnableTimeMs;
|
||||
const bool bodyEnded = clockMs >= note.endTimeMs;
|
||||
const bool inputExpired = clockMs - runtime.lastInputTimeMs > enableMs;
|
||||
if (bodyEnded || inputExpired) {
|
||||
float coveredEndMs = runtime.lastInputTimeMs;
|
||||
if (bodyEnded &&
|
||||
note.endTimeMs - runtime.lastInputTimeMs < 1000.0f / 60.0f) {
|
||||
coveredEndMs = note.endTimeMs;
|
||||
}
|
||||
recordJudgment(
|
||||
gameplay,
|
||||
i,
|
||||
longJudgment(note, runtime.inputStartTimeMs, coveredEndMs),
|
||||
clockMs);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (holdTarget(note.effectiveType) && runtime.holding &&
|
||||
clockMs >= note.endTimeMs) {
|
||||
recordJudgment(
|
||||
gameplay,
|
||||
i,
|
||||
longJudgment(note, runtime.inputStartTimeMs, note.endTimeMs),
|
||||
clockMs);
|
||||
continue;
|
||||
}
|
||||
if (!runtime.holding &&
|
||||
clockMs > static_cast<float>(note.timeMs) + note.lateTimingMs) {
|
||||
recordJudgment(gameplay, i, Judgment::Miss, clockMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Judgment pressGameplay(
|
||||
const StageView& stage,
|
||||
GameplayState* gameplay,
|
||||
float clockMs,
|
||||
std::uint32_t inputBit) {
|
||||
if (!gameplay || !gameplay->notes || !stage.header || inputBit == 0) {
|
||||
return Judgment::Pending;
|
||||
}
|
||||
updateGameplay(stage, gameplay, clockMs);
|
||||
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
const PackageNote& note = stage.notes[i];
|
||||
GameplayState::NoteRuntime& runtime = gameplay->notes[i];
|
||||
if (runtime.judgment != static_cast<std::uint8_t>(Judgment::Pending) ||
|
||||
!runtime.holding || !rhythmLongTarget(note.effectiveType)) {
|
||||
continue;
|
||||
}
|
||||
if (note.effectiveType == 5 || inputBit != runtime.lastInputBit) {
|
||||
runtime.lastInputTimeMs = clockMs;
|
||||
runtime.lastInputBit = inputBit;
|
||||
}
|
||||
return Judgment::Pending;
|
||||
}
|
||||
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
const PackageNote& note = stage.notes[i];
|
||||
GameplayState::NoteRuntime& runtime = gameplay->notes[i];
|
||||
if (runtime.judgment != static_cast<std::uint8_t>(Judgment::Pending) ||
|
||||
runtime.inputMask == 0 || (runtime.inputMask & inputBit) != 0 ||
|
||||
!(dualTapTarget(note.effectiveType) ||
|
||||
(note.effectiveType == 15 && !runtime.holding))) {
|
||||
continue;
|
||||
}
|
||||
const float error = clockMs - static_cast<float>(note.timeMs);
|
||||
if (error < -note.earlyTimingMs || error > note.lateTimingMs) continue;
|
||||
runtime.inputMask |= inputBit;
|
||||
if (dualTapTarget(note.effectiveType)) {
|
||||
const Judgment result = tapJudgment(
|
||||
note, runtime.inputStartTimeMs,
|
||||
stage.header->greatMinimumTimeMs);
|
||||
recordJudgment(gameplay, i, result, clockMs);
|
||||
return result;
|
||||
}
|
||||
runtime.holding = 1;
|
||||
runtime.inputStartTimeMs = clockMs;
|
||||
gameplay->shotMuted = false;
|
||||
return Judgment::Pending;
|
||||
}
|
||||
|
||||
std::uint32_t candidate = gameplay->noteCount;
|
||||
float candidateError = 0.0f;
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
const PackageNote& note = stage.notes[i];
|
||||
const GameplayState::NoteRuntime& runtime = gameplay->notes[i];
|
||||
if (runtime.judgment != static_cast<std::uint8_t>(Judgment::Pending) ||
|
||||
runtime.inputMask != 0 || !supportedTarget(note.effectiveType)) {
|
||||
continue;
|
||||
}
|
||||
const float error = clockMs - static_cast<float>(note.timeMs);
|
||||
if (error < -note.earlyTimingMs || error > note.lateTimingMs) continue;
|
||||
if (candidate == gameplay->noteCount || std::fabs(error) < candidateError) {
|
||||
candidate = i;
|
||||
candidateError = std::fabs(error);
|
||||
}
|
||||
}
|
||||
if (candidate == gameplay->noteCount) return Judgment::Pending;
|
||||
|
||||
const PackageNote& note = stage.notes[candidate];
|
||||
GameplayState::NoteRuntime& runtime = gameplay->notes[candidate];
|
||||
if (tapTarget(note.effectiveType)) {
|
||||
const Judgment result = tapJudgment(
|
||||
note, clockMs, stage.header->greatMinimumTimeMs);
|
||||
recordJudgment(gameplay, candidate, result, clockMs);
|
||||
return result;
|
||||
}
|
||||
runtime.inputMask = inputBit;
|
||||
runtime.inputStartTimeMs = clockMs;
|
||||
runtime.lastInputTimeMs = clockMs;
|
||||
runtime.lastInputBit = inputBit;
|
||||
runtime.holding =
|
||||
note.effectiveType == 3 || rhythmLongTarget(note.effectiveType);
|
||||
if (runtime.holding) gameplay->shotMuted = false;
|
||||
return Judgment::Pending;
|
||||
}
|
||||
|
||||
Judgment releaseGameplay(
|
||||
const StageView& stage,
|
||||
GameplayState* gameplay,
|
||||
float clockMs,
|
||||
std::uint32_t inputBit) {
|
||||
if (!gameplay || !gameplay->notes || !stage.header || inputBit == 0) {
|
||||
return Judgment::Pending;
|
||||
}
|
||||
for (std::uint32_t i = 0; i < gameplay->noteCount; ++i) {
|
||||
const PackageNote& note = stage.notes[i];
|
||||
GameplayState::NoteRuntime& runtime = gameplay->notes[i];
|
||||
if (runtime.judgment != static_cast<std::uint8_t>(Judgment::Pending) ||
|
||||
!holdTarget(note.effectiveType) || (runtime.inputMask & inputBit) == 0) {
|
||||
continue;
|
||||
}
|
||||
if (runtime.holding) {
|
||||
const Judgment result =
|
||||
longJudgment(note, runtime.inputStartTimeMs, clockMs);
|
||||
recordJudgment(gameplay, i, result, clockMs);
|
||||
return result;
|
||||
}
|
||||
runtime.inputMask &= ~inputBit;
|
||||
if (runtime.inputMask == 0) runtime.inputStartTimeMs = -1.0f;
|
||||
return Judgment::Pending;
|
||||
}
|
||||
return Judgment::Pending;
|
||||
}
|
||||
|
||||
Judgment tapGameplay(const StageView& stage, GameplayState* gameplay, float clockMs) {
|
||||
return pressGameplay(stage, gameplay, clockMs, 1u);
|
||||
}
|
||||
|
||||
Judgment noteJudgment(const GameplayState& gameplay, std::uint32_t noteIndex) {
|
||||
if (!gameplay.notes || noteIndex >= gameplay.noteCount) return Judgment::Pending;
|
||||
return static_cast<Judgment>(gameplay.notes[noteIndex].judgment);
|
||||
}
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,201 @@
|
||||
#include "SongMenu.hpp"
|
||||
|
||||
#include <pspkernel.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace openroller::psp {
|
||||
namespace {
|
||||
|
||||
void setError(char* output, std::size_t capacity, const char* message) {
|
||||
if (output && capacity > 0) std::snprintf(output, capacity, "%s", message);
|
||||
}
|
||||
|
||||
int firstDifficulty(const SongCatalogRecord& song, int preferred) {
|
||||
if (preferred >= 0 && preferred < 4 && (song.availableMask & (1u << preferred)) != 0) {
|
||||
return preferred;
|
||||
}
|
||||
for (int distance = 1; distance < 4; ++distance) {
|
||||
const int lower = preferred - distance;
|
||||
const int upper = preferred + distance;
|
||||
if (lower >= 0 && (song.availableMask & (1u << lower)) != 0) return lower;
|
||||
if (upper < 4 && (song.availableMask & (1u << upper)) != 0) return upper;
|
||||
}
|
||||
for (int difficulty = 0; difficulty < 4; ++difficulty) {
|
||||
if ((song.availableMask & (1u << difficulty)) != 0) return difficulty;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool loadJacket(SongMenu* menu) {
|
||||
std::free(menu->jacketStorage);
|
||||
menu->jacketStorage = nullptr;
|
||||
menu->jacketPixels = nullptr;
|
||||
menu->jacketSize = 0;
|
||||
const SongCatalogRecord* song = selectedSong(*menu);
|
||||
if (!song) return false;
|
||||
char path[384]{};
|
||||
std::snprintf(path, sizeof(path), "%s/songs/%s/jacket.orpj", menu->rootPath, song->key);
|
||||
std::FILE* file = std::fopen(path, "rb");
|
||||
if (!file) return false;
|
||||
std::fseek(file, 0, SEEK_END);
|
||||
const long length = std::ftell(file);
|
||||
std::fseek(file, 0, SEEK_SET);
|
||||
if (length < static_cast<long>(sizeof(JacketHeader)) || length > 512 * 512 * 2 + 64) {
|
||||
std::fclose(file);
|
||||
return false;
|
||||
}
|
||||
void* storage = std::malloc(static_cast<std::size_t>(length));
|
||||
if (!storage || std::fread(storage, 1, static_cast<std::size_t>(length), file) !=
|
||||
static_cast<std::size_t>(length)) {
|
||||
std::fclose(file);
|
||||
std::free(storage);
|
||||
return false;
|
||||
}
|
||||
std::fclose(file);
|
||||
const auto* header = static_cast<const JacketHeader*>(storage);
|
||||
const std::size_t expected = static_cast<std::size_t>(header->width) * header->height * 2;
|
||||
if (std::memcmp(header->magic, kJacketMagic, sizeof(header->magic)) != 0 ||
|
||||
header->version != kJacketVersion || header->pixelFormat != 0 ||
|
||||
header->width == 0 || header->height == 0 || header->dataSize != expected ||
|
||||
sizeof(JacketHeader) + expected != static_cast<std::size_t>(length)) {
|
||||
std::free(storage);
|
||||
return false;
|
||||
}
|
||||
menu->jacketStorage = storage;
|
||||
menu->jacketSize = static_cast<std::size_t>(length);
|
||||
menu->jacketPixels = reinterpret_cast<const std::uint16_t*>(
|
||||
static_cast<const std::uint8_t*>(storage) + sizeof(JacketHeader));
|
||||
menu->jacketWidth = header->width;
|
||||
menu->jacketHeight = header->height;
|
||||
sceKernelDcacheWritebackRange(storage, static_cast<std::size_t>(length));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool loadSongMenu(const char* catalogPath, SongMenu* menu, char* error, std::size_t errorCapacity) {
|
||||
if (!catalogPath || !menu) return false;
|
||||
unloadSongMenu(menu);
|
||||
std::FILE* file = std::fopen(catalogPath, "rb");
|
||||
if (!file) {
|
||||
setError(error, errorCapacity, "catalog.orpc not found");
|
||||
return false;
|
||||
}
|
||||
std::fseek(file, 0, SEEK_END);
|
||||
const long length = std::ftell(file);
|
||||
std::fseek(file, 0, SEEK_SET);
|
||||
if (length < static_cast<long>(sizeof(SongCatalogHeader)) || length > 64 * 1024) {
|
||||
std::fclose(file);
|
||||
setError(error, errorCapacity, "invalid catalog size");
|
||||
return false;
|
||||
}
|
||||
void* storage = std::malloc(static_cast<std::size_t>(length));
|
||||
if (!storage || std::fread(storage, 1, static_cast<std::size_t>(length), file) !=
|
||||
static_cast<std::size_t>(length)) {
|
||||
std::fclose(file);
|
||||
std::free(storage);
|
||||
setError(error, errorCapacity, "could not read catalog");
|
||||
return false;
|
||||
}
|
||||
std::fclose(file);
|
||||
const auto* header = static_cast<const SongCatalogHeader*>(storage);
|
||||
const bool valid = std::memcmp(header->magic, kSongCatalogMagic, sizeof(header->magic)) == 0 &&
|
||||
header->version == kSongCatalogVersion && header->headerSize == sizeof(SongCatalogHeader) &&
|
||||
header->recordSize == sizeof(SongCatalogRecord) && header->songCount > 0 &&
|
||||
header->songCount <= kMaximumCatalogSongs && header->fileSize == static_cast<std::uint32_t>(length) &&
|
||||
sizeof(SongCatalogHeader) + header->songCount * sizeof(SongCatalogRecord) ==
|
||||
static_cast<std::size_t>(length);
|
||||
if (!valid) {
|
||||
std::free(storage);
|
||||
setError(error, errorCapacity, "invalid catalog header");
|
||||
return false;
|
||||
}
|
||||
menu->catalogStorage = storage;
|
||||
menu->catalogSize = static_cast<std::size_t>(length);
|
||||
menu->header = header;
|
||||
menu->songs = reinterpret_cast<const SongCatalogRecord*>(
|
||||
static_cast<const std::uint8_t*>(storage) + sizeof(SongCatalogHeader));
|
||||
std::snprintf(menu->rootPath, sizeof(menu->rootPath), "%s", catalogPath);
|
||||
char* slash = std::strrchr(menu->rootPath, '/');
|
||||
if (slash) *slash = '\0';
|
||||
else std::snprintf(menu->rootPath, sizeof(menu->rootPath), ".");
|
||||
menu->difficulty = firstDifficulty(menu->songs[0], 2);
|
||||
loadJacket(menu);
|
||||
setError(error, errorCapacity, "ok");
|
||||
return true;
|
||||
}
|
||||
|
||||
void unloadSongMenu(SongMenu* menu) {
|
||||
if (!menu) return;
|
||||
std::free(menu->jacketStorage);
|
||||
std::free(menu->catalogStorage);
|
||||
*menu = {};
|
||||
}
|
||||
|
||||
const SongCatalogRecord* selectedSong(const SongMenu& menu) {
|
||||
if (!menu.header || !menu.songs || menu.selection < 0 ||
|
||||
static_cast<std::uint32_t>(menu.selection) >= menu.header->songCount) return nullptr;
|
||||
return &menu.songs[menu.selection];
|
||||
}
|
||||
|
||||
bool moveSongSelection(SongMenu* menu, int delta) {
|
||||
if (!menu || !menu->header || menu->header->songCount == 0) return false;
|
||||
const int count = static_cast<int>(menu->header->songCount);
|
||||
menu->selection = (menu->selection + delta) % count;
|
||||
if (menu->selection < 0) menu->selection += count;
|
||||
menu->difficulty = firstDifficulty(menu->songs[menu->selection], menu->difficulty);
|
||||
loadJacket(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool moveDifficultySelection(SongMenu* menu, int delta) {
|
||||
const SongCatalogRecord* song = menu ? selectedSong(*menu) : nullptr;
|
||||
if (!song) return false;
|
||||
for (int step = 1; step <= 4; ++step) {
|
||||
int candidate = (menu->difficulty + delta * step) % 4;
|
||||
if (candidate < 0) candidate += 4;
|
||||
if ((song->availableMask & (1u << candidate)) != 0) {
|
||||
menu->difficulty = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool selectedSongPath(const SongMenu& menu, char* output, std::size_t capacity) {
|
||||
static constexpr const char* names[4] = {"easy", "normal", "hard", "extra"};
|
||||
const SongCatalogRecord* song = selectedSong(menu);
|
||||
if (!song || !output || capacity == 0 || menu.difficulty < 0 || menu.difficulty >= 4) return false;
|
||||
const int written = std::snprintf(
|
||||
output, capacity, "%s/songs/%s/%s.orps", menu.rootPath, song->key, names[menu.difficulty]);
|
||||
return written > 0 && static_cast<std::size_t>(written) < capacity;
|
||||
}
|
||||
|
||||
bool selectedAudioPath(const SongMenu& menu, char* output, std::size_t capacity) {
|
||||
static constexpr const char* names[4] = {"easy", "normal", "hard", "extra"};
|
||||
const SongCatalogRecord* song = selectedSong(menu);
|
||||
if (!song || !output || capacity == 0 || menu.difficulty < 0 || menu.difficulty >= 4) {
|
||||
return false;
|
||||
}
|
||||
const int written = std::snprintf(
|
||||
output, capacity, "%s/songs/%s/%s_bgm.mp3",
|
||||
menu.rootPath, song->key, names[menu.difficulty]);
|
||||
return written > 0 && static_cast<std::size_t>(written) < capacity;
|
||||
}
|
||||
|
||||
bool selectedShotAudioPath(const SongMenu& menu, char* output, std::size_t capacity) {
|
||||
static constexpr const char* names[4] = {"easy", "normal", "hard", "extra"};
|
||||
const SongCatalogRecord* song = selectedSong(menu);
|
||||
if (!song || !output || capacity == 0 || menu.difficulty < 0 || menu.difficulty >= 4) {
|
||||
return false;
|
||||
}
|
||||
const int written = std::snprintf(
|
||||
output, capacity, "%s/songs/%s/%s_shot.mp3",
|
||||
menu.rootPath, song->key, names[menu.difficulty]);
|
||||
return written > 0 && static_cast<std::size_t>(written) < capacity;
|
||||
}
|
||||
|
||||
} // namespace openroller::psp
|
||||
@@ -0,0 +1,444 @@
|
||||
#include "StageRuntime.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
namespace openroller::psp {
|
||||
namespace {
|
||||
|
||||
constexpr std::size_t kMaximumStageBytes = 4u * 1024u * 1024u;
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
|
||||
void setError(char* output, std::size_t capacity, const char* message) {
|
||||
if (!output || capacity == 0) return;
|
||||
std::snprintf(output, capacity, "%s", message);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool sectionValid(const StagePackageHeader& header, PackageSection section) {
|
||||
if ((section.offset & 15u) != 0 || section.offset < header.headerSize) return false;
|
||||
if (section.count > std::numeric_limits<std::uint32_t>::max() / sizeof(T)) return false;
|
||||
const std::uint32_t bytes = section.count * static_cast<std::uint32_t>(sizeof(T));
|
||||
return section.offset <= header.fileSize && bytes <= header.fileSize - section.offset;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
const T* sectionPointer(const void* storage, PackageSection section) {
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(storage);
|
||||
return reinterpret_cast<const T*>(bytes + section.offset);
|
||||
}
|
||||
|
||||
bool rangeValid(PackageRange range, std::uint32_t count) {
|
||||
return range.first <= count && range.count <= count - range.first;
|
||||
}
|
||||
|
||||
Vec3 add(Vec3 a, Vec3 b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; }
|
||||
Vec3 subtract(Vec3 a, Vec3 b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; }
|
||||
Vec3 multiply(Vec3 a, float value) { return {a.x * value, a.y * value, a.z * value}; }
|
||||
float dot(Vec3 a, Vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
|
||||
Vec3 cross(Vec3 a, Vec3 b) {
|
||||
return {
|
||||
a.y * b.z - a.z * b.y,
|
||||
a.z * b.x - a.x * b.z,
|
||||
a.x * b.y - a.y * b.x,
|
||||
};
|
||||
}
|
||||
float length(Vec3 value) { return std::sqrt(dot(value, value)); }
|
||||
Vec3 normalize(Vec3 value, Vec3 fallback = {0.0f, 0.0f, 0.0f}) {
|
||||
const float magnitude = length(value);
|
||||
return magnitude > 1.0e-6f ? multiply(value, 1.0f / magnitude) : fallback;
|
||||
}
|
||||
Vec3 mix(Vec3 a, Vec3 b, float u) { return add(a, multiply(subtract(b, a), u)); }
|
||||
float mix(float a, float b, float u) { return a + (b - a) * u; }
|
||||
|
||||
Vec3 fromArray(const float value[3]) { return {value[0], value[1], value[2]}; }
|
||||
|
||||
std::uint32_t lowerTrackIndex(const StageView& stage, float timeMs) {
|
||||
const std::uint32_t count = stage.header->track.count;
|
||||
std::uint32_t first = 0;
|
||||
std::uint32_t last = count;
|
||||
while (first < last) {
|
||||
const std::uint32_t middle = first + (last - first) / 2;
|
||||
if (static_cast<float>(stage.track[middle].timeMs) <= timeMs) first = middle + 1;
|
||||
else last = middle;
|
||||
}
|
||||
return first == 0 ? 0 : first - 1;
|
||||
}
|
||||
|
||||
std::uint32_t lowerCameraIndex(const StageView& stage, float timeMs) {
|
||||
const std::uint32_t count = stage.header->cameras.count;
|
||||
std::uint32_t first = 0;
|
||||
std::uint32_t last = count;
|
||||
while (first < last) {
|
||||
const std::uint32_t middle = first + (last - first) / 2;
|
||||
if (static_cast<float>(stage.cameras[middle].timeMs) <= timeMs) first = middle + 1;
|
||||
else last = middle;
|
||||
}
|
||||
return first == 0 ? 0 : first - 1;
|
||||
}
|
||||
|
||||
Vec3 cameraOrbit(const PackageCameraPoint& camera) {
|
||||
const float a = (-camera.rotationA[1] * kPi / 180.0f) * 0.5f;
|
||||
const float b = (camera.rotationA[0] * kPi / 180.0f) * 0.5f;
|
||||
const float c = 0.0f;
|
||||
const float ca = std::cos(a), cb = std::cos(b), cc = std::cos(c);
|
||||
const float sa = std::sin(a), sb = std::sin(b), sc = std::sin(c);
|
||||
const float qw = sc * sa * sb + cc * ca * cb;
|
||||
const float qx = sc * ca * sb + cc * sa * cb;
|
||||
const float qy = cc * ca * sb - sc * sa * cb;
|
||||
const float qz = cc * sa * sb - sc * ca * cb;
|
||||
return multiply({
|
||||
2.0f * (qz * qx + qw * qy),
|
||||
2.0f * (qy * qz - qw * qx),
|
||||
1.0f - 2.0f * (qx * qx + qy * qy),
|
||||
}, camera.distance);
|
||||
}
|
||||
|
||||
Vec3 rotateAround(Vec3 value, Vec3 axis, float degrees) {
|
||||
const float radians = degrees * kPi / 180.0f;
|
||||
const float cosine = std::cos(radians);
|
||||
const float sine = std::sin(radians);
|
||||
return add(
|
||||
add(multiply(value, cosine), multiply(cross(axis, value), sine)),
|
||||
multiply(axis, dot(axis, value) * (1.0f - cosine)));
|
||||
}
|
||||
|
||||
void adjustCameraUp(CameraState* camera, float rollDegrees) {
|
||||
const Vec3 view = normalize(subtract(camera->target, camera->eye));
|
||||
if (dot(view, view) < 1.0e-10f) {
|
||||
camera->up = {0.0f, 1.0f, 0.0f};
|
||||
return;
|
||||
}
|
||||
Vec3 reference{0.0f, 1.0f, 0.0f};
|
||||
Vec3 projected = subtract(reference, multiply(view, dot(reference, view)));
|
||||
if (dot(projected, projected) < 1.0e-8f) {
|
||||
reference = {0.0f, 0.0f, 1.0f};
|
||||
projected = subtract(reference, multiply(view, dot(reference, view)));
|
||||
}
|
||||
camera->up = normalize(projected, {0.0f, 1.0f, 0.0f});
|
||||
if (rollDegrees != 0.0f) camera->up = rotateAround(camera->up, view, rollDegrees);
|
||||
}
|
||||
|
||||
PackageCameraPoint mixCamera(const PackageCameraPoint& a, const PackageCameraPoint& b, float u) {
|
||||
PackageCameraPoint output = a;
|
||||
output.distance = mix(a.distance, b.distance, u);
|
||||
for (int i = 0; i < 2; ++i) output.rotationA[i] = mix(a.rotationA[i], b.rotationA[i], u);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
output.originOffset[i] = mix(a.originOffset[i], b.originOffset[i], u);
|
||||
output.fieldFar[i] = mix(a.fieldFar[i], b.fieldFar[i], u);
|
||||
output.fieldNear[i] = mix(a.fieldNear[i], b.fieldNear[i], u);
|
||||
}
|
||||
output.rotationB = mix(a.rotationB, b.rotationB, u);
|
||||
return output;
|
||||
}
|
||||
|
||||
CameraState evaluateCameraInternal(
|
||||
const StageView& stage,
|
||||
float timeMs,
|
||||
bool interpolate,
|
||||
int depth) {
|
||||
CameraState state{};
|
||||
if (stage.header->cameras.count == 0 || depth > 8) {
|
||||
state.target = trackPositionAt(stage, timeMs);
|
||||
state.eye = add(state.target, {0.0f, 0.0f, 10.0f});
|
||||
state.projectionBlend = 1.0f;
|
||||
return state;
|
||||
}
|
||||
|
||||
const std::uint32_t index = lowerCameraIndex(stage, timeMs);
|
||||
const PackageCameraPoint* key = &stage.cameras[index];
|
||||
PackageCameraPoint mixed{};
|
||||
const bool between = interpolate && key->fMode != 0 &&
|
||||
timeMs > static_cast<float>(key->timeMs) && index + 1 < stage.header->cameras.count &&
|
||||
timeMs < static_cast<float>(stage.cameras[index + 1].timeMs);
|
||||
if (between) {
|
||||
const PackageCameraPoint& following = stage.cameras[index + 1];
|
||||
const float span = static_cast<float>(following.timeMs - key->timeMs);
|
||||
const float u = span > 0.0f ? (timeMs - static_cast<float>(key->timeMs)) / span : 0.0f;
|
||||
if (key->fMode == 1) {
|
||||
CameraState from = evaluateCameraInternal(stage, static_cast<float>(key->timeMs), true, depth + 1);
|
||||
CameraState to = evaluateCameraInternal(stage, static_cast<float>(following.timeMs), true, depth + 1);
|
||||
adjustCameraUp(&from, 0.0f);
|
||||
adjustCameraUp(&to, 0.0f);
|
||||
state.eye = mix(from.eye, to.eye, u);
|
||||
state.target = mix(from.target, to.target, u);
|
||||
state.up = normalize(mix(from.up, to.up, u), {0.0f, 1.0f, 0.0f});
|
||||
state.projectionBlend = mix(from.projectionBlend, to.projectionBlend, u);
|
||||
const float roll = mix(key->rotationB, following.rotationB, u);
|
||||
const Vec3 axis = normalize(subtract(state.target, state.eye));
|
||||
if (roll != 0.0f && dot(axis, axis) > 0.0f) state.up = rotateAround(state.up, axis, roll);
|
||||
return state;
|
||||
}
|
||||
if (key->fMode == 2) {
|
||||
mixed = mixCamera(*key, following, u);
|
||||
key = &mixed;
|
||||
}
|
||||
}
|
||||
|
||||
const Vec3 orbit = cameraOrbit(*key);
|
||||
const Vec3 currentTrack = trackPositionAt(stage, timeMs);
|
||||
const Vec3 origin = fromArray(key->originOffset);
|
||||
switch (key->aMode) {
|
||||
case 0:
|
||||
state.target = add(currentTrack, origin);
|
||||
state.eye = add(state.target, orbit);
|
||||
break;
|
||||
case 1:
|
||||
state.target = add(trackPositionAt(stage, static_cast<float>(key->timeMs)), origin);
|
||||
state.eye = add(state.target, orbit);
|
||||
break;
|
||||
case 2:
|
||||
if (index > 1) {
|
||||
return evaluateCameraInternal(
|
||||
stage, static_cast<float>(stage.cameras[index].timeMs) - 1.0f, false, depth + 1);
|
||||
}
|
||||
state.target = add(currentTrack, origin);
|
||||
state.eye = add(state.target, orbit);
|
||||
break;
|
||||
case 3:
|
||||
state.target = add(currentTrack, origin);
|
||||
state.eye = add(add(trackPositionAt(stage, static_cast<float>(key->timeMs)), origin), orbit);
|
||||
break;
|
||||
case 4:
|
||||
state.target = fromArray(key->fieldFar);
|
||||
state.eye = fromArray(key->fieldNear);
|
||||
break;
|
||||
case 5:
|
||||
state.target = add(currentTrack, origin);
|
||||
state.eye = fromArray(key->fieldNear);
|
||||
break;
|
||||
case 6:
|
||||
state.target = fromArray(key->fieldFar);
|
||||
state.eye = add(currentTrack, orbit);
|
||||
break;
|
||||
default:
|
||||
state.target = add(currentTrack, origin);
|
||||
state.eye = add(state.target, orbit);
|
||||
break;
|
||||
}
|
||||
adjustCameraUp(&state, key->rotationB);
|
||||
state.projectionBlend = key->projectionType ? 1.0f : 0.0f;
|
||||
return state;
|
||||
}
|
||||
|
||||
std::uint8_t colorChannel(std::uint32_t color, int shift) {
|
||||
return static_cast<std::uint8_t>((color >> shift) & 0xffu);
|
||||
}
|
||||
|
||||
std::uint32_t mixColor(std::uint32_t a, std::uint32_t b, float u) {
|
||||
std::uint32_t output = 0;
|
||||
for (int shift = 0; shift <= 24; shift += 8) {
|
||||
const float value = mix(
|
||||
static_cast<float>(colorChannel(a, shift)),
|
||||
static_cast<float>(colorChannel(b, shift)), u);
|
||||
output |= static_cast<std::uint32_t>(std::clamp(value, 0.0f, 255.0f) + 0.5f) << shift;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool loadStagePackage(const char* path, StageView* stage, char* error, std::size_t errorCapacity) {
|
||||
if (!path || !stage) {
|
||||
setError(error, errorCapacity, "invalid stage loader arguments");
|
||||
return false;
|
||||
}
|
||||
unloadStagePackage(stage);
|
||||
|
||||
std::FILE* file = std::fopen(path, "rb");
|
||||
if (!file) {
|
||||
setError(error, errorCapacity, "stage.orps not found");
|
||||
return false;
|
||||
}
|
||||
if (std::fseek(file, 0, SEEK_END) != 0) {
|
||||
std::fclose(file);
|
||||
setError(error, errorCapacity, "could not seek stage.orps");
|
||||
return false;
|
||||
}
|
||||
const long length = std::ftell(file);
|
||||
if (length < static_cast<long>(sizeof(StagePackageHeader)) ||
|
||||
length > static_cast<long>(kMaximumStageBytes) ||
|
||||
std::fseek(file, 0, SEEK_SET) != 0) {
|
||||
std::fclose(file);
|
||||
setError(error, errorCapacity, "stage.orps has an invalid size");
|
||||
return false;
|
||||
}
|
||||
|
||||
void* storage = std::malloc(static_cast<std::size_t>(length));
|
||||
if (!storage) {
|
||||
std::fclose(file);
|
||||
setError(error, errorCapacity, "not enough memory for stage.orps");
|
||||
return false;
|
||||
}
|
||||
const std::size_t read = std::fread(storage, 1, static_cast<std::size_t>(length), file);
|
||||
std::fclose(file);
|
||||
if (read != static_cast<std::size_t>(length)) {
|
||||
std::free(storage);
|
||||
setError(error, errorCapacity, "could not read complete stage.orps");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto* header = static_cast<const StagePackageHeader*>(storage);
|
||||
const bool headerValid =
|
||||
std::memcmp(header->magic, kStagePackageMagic, sizeof(header->magic)) == 0 &&
|
||||
header->version == kStagePackageVersion &&
|
||||
header->headerSize == sizeof(StagePackageHeader) &&
|
||||
header->fileSize == static_cast<std::uint32_t>(length);
|
||||
const bool sectionsValid = headerValid &&
|
||||
sectionValid<PackageTrackPoint>(*header, header->track) &&
|
||||
sectionValid<PackageNote>(*header, header->notes) &&
|
||||
sectionValid<PackageCameraPoint>(*header, header->cameras) &&
|
||||
sectionValid<PackageDrawDistancePoint>(*header, header->drawDistances) &&
|
||||
sectionValid<PackageBackgroundColorPoint>(*header, header->backgroundColors) &&
|
||||
sectionValid<PackageBackgroundModel>(*header, header->backgroundModels) &&
|
||||
sectionValid<PackageBackgroundVertex>(*header, header->backgroundVertices) &&
|
||||
sectionValid<PackageBackgroundObject>(*header, header->backgroundObjects) &&
|
||||
sectionValid<PackageVisibilityKey>(*header, header->visibilityKeys) &&
|
||||
sectionValid<PackageTransformKey>(*header, header->transformKeys) &&
|
||||
sectionValid<PackageObjectColorKey>(*header, header->objectColorKeys) &&
|
||||
sectionValid<PackageParticlePoint>(*header, header->particles) &&
|
||||
sectionValid<PackageVisualizerPoint>(*header, header->visualizer) &&
|
||||
sectionValid<PackageBpmPoint>(*header, header->bpmChanges);
|
||||
bool contentsValid = sectionsValid;
|
||||
if (contentsValid) {
|
||||
const auto* models = sectionPointer<PackageBackgroundModel>(storage, header->backgroundModels);
|
||||
for (std::uint32_t i = 0; i < header->backgroundModels.count; ++i) {
|
||||
contentsValid = contentsValid &&
|
||||
rangeValid(models[i].triangles, header->backgroundVertices.count) &&
|
||||
rangeValid(models[i].solidLines, header->backgroundVertices.count) &&
|
||||
rangeValid(models[i].wireframeLines, header->backgroundVertices.count);
|
||||
}
|
||||
const auto* objects = sectionPointer<PackageBackgroundObject>(storage, header->backgroundObjects);
|
||||
for (std::uint32_t i = 0; i < header->backgroundObjects.count; ++i) {
|
||||
const bool parentValid = objects[i].parentIndex < 0 ||
|
||||
static_cast<std::uint32_t>(objects[i].parentIndex) < header->backgroundObjects.count;
|
||||
contentsValid = contentsValid &&
|
||||
(objects[i].model == std::numeric_limits<std::uint32_t>::max() ||
|
||||
objects[i].model < header->backgroundModels.count) && parentValid &&
|
||||
rangeValid(objects[i].visibility, header->visibilityKeys.count) &&
|
||||
rangeValid(objects[i].movement, header->transformKeys.count) &&
|
||||
rangeValid(objects[i].scaling, header->transformKeys.count) &&
|
||||
rangeValid(objects[i].rotations, header->transformKeys.count) &&
|
||||
rangeValid(objects[i].colorChanges, header->objectColorKeys.count);
|
||||
}
|
||||
}
|
||||
if (!contentsValid || header->track.count < 2 ||
|
||||
header->backgroundObjects.count > kMaximumPackageBackgroundObjects) {
|
||||
std::free(storage);
|
||||
setError(error, errorCapacity, "stage.orps header or sections are invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
stage->storage = storage;
|
||||
stage->storageSize = static_cast<std::size_t>(length);
|
||||
stage->header = header;
|
||||
stage->track = sectionPointer<PackageTrackPoint>(storage, header->track);
|
||||
stage->notes = sectionPointer<PackageNote>(storage, header->notes);
|
||||
stage->cameras = sectionPointer<PackageCameraPoint>(storage, header->cameras);
|
||||
stage->drawDistances = sectionPointer<PackageDrawDistancePoint>(storage, header->drawDistances);
|
||||
stage->backgroundColors = sectionPointer<PackageBackgroundColorPoint>(storage, header->backgroundColors);
|
||||
stage->backgroundModels = sectionPointer<PackageBackgroundModel>(storage, header->backgroundModels);
|
||||
stage->backgroundVertices = sectionPointer<PackageBackgroundVertex>(storage, header->backgroundVertices);
|
||||
stage->backgroundObjects = sectionPointer<PackageBackgroundObject>(storage, header->backgroundObjects);
|
||||
stage->visibilityKeys = sectionPointer<PackageVisibilityKey>(storage, header->visibilityKeys);
|
||||
stage->transformKeys = sectionPointer<PackageTransformKey>(storage, header->transformKeys);
|
||||
stage->objectColorKeys = sectionPointer<PackageObjectColorKey>(storage, header->objectColorKeys);
|
||||
stage->particles = sectionPointer<PackageParticlePoint>(storage, header->particles);
|
||||
stage->visualizer = sectionPointer<PackageVisualizerPoint>(storage, header->visualizer);
|
||||
stage->bpmChanges = sectionPointer<PackageBpmPoint>(storage, header->bpmChanges);
|
||||
setError(error, errorCapacity, "ok");
|
||||
return true;
|
||||
}
|
||||
|
||||
void unloadStagePackage(StageView* stage) {
|
||||
if (!stage) return;
|
||||
std::free(stage->storage);
|
||||
*stage = {};
|
||||
}
|
||||
|
||||
Vec3 trackPositionAt(const StageView& stage, float timeMs) {
|
||||
if (!stage.header || stage.header->track.count == 0) return {};
|
||||
if (timeMs <= static_cast<float>(stage.track[0].timeMs)) {
|
||||
return {stage.track[0].x, stage.track[0].y, stage.track[0].z};
|
||||
}
|
||||
const std::uint32_t last = stage.header->track.count - 1;
|
||||
if (timeMs >= static_cast<float>(stage.track[last].timeMs)) {
|
||||
return {stage.track[last].x, stage.track[last].y, stage.track[last].z};
|
||||
}
|
||||
const std::uint32_t index = lowerTrackIndex(stage, timeMs);
|
||||
const PackageTrackPoint& a = stage.track[index];
|
||||
const PackageTrackPoint& b = stage.track[index + 1];
|
||||
const float span = static_cast<float>(b.timeMs - a.timeMs);
|
||||
const float u = span > 0.0f ? (timeMs - static_cast<float>(a.timeMs)) / span : 0.0f;
|
||||
return mix({a.x, a.y, a.z}, {b.x, b.y, b.z}, u);
|
||||
}
|
||||
|
||||
Vec3 trackTangentAt(const StageView& stage, float timeMs) {
|
||||
const float first = static_cast<float>(stage.track[0].timeMs);
|
||||
const float last = static_cast<float>(stage.track[stage.header->track.count - 1].timeMs);
|
||||
const Vec3 before = trackPositionAt(stage, std::max(first, timeMs - 4.0f));
|
||||
const Vec3 after = trackPositionAt(stage, std::min(last, timeMs + 4.0f));
|
||||
return normalize(subtract(after, before), {0.0f, 0.0f, 1.0f});
|
||||
}
|
||||
|
||||
CameraState evaluateCamera(const StageView& stage, float timeMs) {
|
||||
return evaluateCameraInternal(stage, timeMs, true, 0);
|
||||
}
|
||||
|
||||
BackgroundColors evaluateBackground(const StageView& stage, float timeMs) {
|
||||
if (!stage.header || stage.header->backgroundColors.count == 0) return {};
|
||||
const std::uint32_t count = stage.header->backgroundColors.count;
|
||||
std::uint32_t index = 0;
|
||||
while (index + 1 < count && static_cast<float>(stage.backgroundColors[index + 1].timeMs) <= timeMs) {
|
||||
++index;
|
||||
}
|
||||
const PackageBackgroundColorPoint& a = stage.backgroundColors[index];
|
||||
if (index + 1 >= count) {
|
||||
return {a.topRightRgba, a.topLeftRgba, a.bottomRightRgba, a.bottomLeftRgba};
|
||||
}
|
||||
const PackageBackgroundColorPoint& b = stage.backgroundColors[index + 1];
|
||||
const float span = static_cast<float>(b.timeMs - a.timeMs);
|
||||
const float u = span > 0.0f
|
||||
? std::clamp((timeMs - static_cast<float>(a.timeMs)) / span, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
return {
|
||||
mixColor(a.topRightRgba, b.topRightRgba, u),
|
||||
mixColor(a.topLeftRgba, b.topLeftRgba, u),
|
||||
mixColor(a.bottomRightRgba, b.bottomRightRgba, u),
|
||||
mixColor(a.bottomLeftRgba, b.bottomLeftRgba, u),
|
||||
};
|
||||
}
|
||||
|
||||
float evaluateDrawAhead(const StageView& stage, float timeMs) {
|
||||
if (!stage.header || stage.header->drawDistances.count == 0) {
|
||||
return stage.header ? stage.header->forwardDrawDistance : 7.0f;
|
||||
}
|
||||
const std::uint32_t count = stage.header->drawDistances.count;
|
||||
std::uint32_t index = 0;
|
||||
while (index + 1 < count && static_cast<float>(stage.drawDistances[index + 1].timeMs) <= timeMs) ++index;
|
||||
const PackageDrawDistancePoint& a = stage.drawDistances[index];
|
||||
if (index + 1 >= count) return std::max(0.0f, a.distance);
|
||||
const PackageDrawDistancePoint& b = stage.drawDistances[index + 1];
|
||||
const float span = static_cast<float>(b.timeMs - a.timeMs);
|
||||
const float u = span > 0.0f
|
||||
? std::clamp((timeMs - static_cast<float>(a.timeMs)) / span, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
return std::max(0.0f, mix(a.distance, b.distance, u));
|
||||
}
|
||||
|
||||
float evaluateBeatDurationMs(const StageView& stage, float timeMs) {
|
||||
if (!stage.header || stage.header->bpmChanges.count == 0) return 500.0f;
|
||||
std::uint32_t active = 0;
|
||||
for (std::uint32_t i = 1; i < stage.header->bpmChanges.count; ++i) {
|
||||
if (static_cast<float>(stage.bpmChanges[i].timeMs) > timeMs) break;
|
||||
active = i;
|
||||
}
|
||||
const std::uint32_t bpm = stage.bpmChanges[active].bpm;
|
||||
return bpm > 0 ? 60000.0f / static_cast<float>(bpm) : 500.0f;
|
||||
}
|
||||
|
||||
} // namespace openroller::psp
|
||||
+2014
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user