Initial public source release

Split reusable rendering and format support into vectorail-core and vectorail-gc.
This commit is contained in:
2026-08-02 17:05:27 +02:00
commit 831d96e562
109 changed files with 20558 additions and 0 deletions
@@ -0,0 +1,58 @@
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <string>
#include <vector>
class AudioManager {
public:
enum class GameplaySound : size_t {
Adlib = 0,
Tap1 = 1,
Tap2 = 2,
};
AudioManager();
~AudioManager();
bool loadMusic(const std::string& path, float gain = 1.0f);
bool loadMusicPair(const std::string& bgmPath, const std::string& shotPath,
float bgmGain, float shotGain);
bool loadGameplaySounds(const std::array<std::string, 3>& paths,
const std::array<float, 3>& gains);
void playGameplaySound(GameplaySound sound);
void play();
void pause();
void resume();
void setShotMuted(bool muted);
double getTime() const;
double getDuration() const; // Длина песни в секундах
bool isPlaying() const { return playing; }
private:
struct GameplaySoundSlot {
std::vector<Uint8> data;
std::array<SDL_AudioStream*, 2> voices{nullptr, nullptr};
size_t nextVoice = 0;
};
void clear();
void clearGameplaySounds();
bool openStreams(const SDL_AudioSpec& bgmSpec, const Uint8* bgmData, Uint32 bgmLen,
float bgmGain, const SDL_AudioSpec* shotSpec = nullptr,
const Uint8* shotData = nullptr, Uint32 shotLen = 0,
float shotGain = 1.0f);
SDL_AudioDeviceID device;
SDL_AudioStream* bgmStream;
SDL_AudioStream* shotStream;
std::array<GameplaySoundSlot, 3> gameplaySounds;
double duration; // Предрассчитанная длительность
float shotBaseGain;
bool shotMuted;
bool playing;
Uint64 startTime;
Uint64 accumulatedTicks;
};
@@ -0,0 +1,68 @@
#pragma once
#include <array>
#include <cstdint>
#include <string>
enum class CabinetInput : std::size_t {
Test,
Service,
Coin,
Select,
Enter,
LeftUp,
LeftDown,
LeftLeft,
LeftRight,
LeftButton,
RightUp,
RightDown,
RightLeft,
RightRight,
RightButton,
Count,
};
struct CabinetRgb {
std::uint8_t r = 0;
std::uint8_t g = 0;
std::uint8_t b = 0;
};
enum class CardReaderStatus {
Ready,
Disconnected,
Unformatted,
ReadError,
Timeout,
};
struct CardReaderState {
CardReaderStatus status = CardReaderStatus::Disconnected;
std::string cardId;
};
// Hardware boundary shared by gameplay and test mode. The software backend
// is deliberately useful on its own; an ACM implementation can replace it
// without teaching the test-mode forms about packets or device paths.
class CabinetBackend {
public:
virtual ~CabinetBackend() = default;
virtual void poll() = 0;
virtual bool input(CabinetInput input) const = 0;
virtual void setLed(std::size_t logicalIndex, CabinetRgb color) = 0;
virtual void clearLeds() = 0;
virtual void commitOutputs() = 0;
virtual const std::array<CabinetRgb, 118>& leds() const = 0;
virtual int headphoneVolume() const { return 0; }
virtual bool headphoneConnected() const { return false; }
virtual bool grooveStageConnected() const { return false; }
virtual CardReaderState cardReader() const { return {}; }
};
// Keyboard + in-memory outputs. This keeps service mode fully testable before
// the cabinet ACM and RFID transports are attached.
CabinetBackend& defaultCabinetBackend();
@@ -0,0 +1,105 @@
#pragma once
#include <array>
#include <vector>
#include <string>
#include <fstream>
#include <sstream>
#include <map>
#include <optional>
#include <cstdint>
#include <glm/glm.hpp>
#include "gc/StagePattern.hpp"
struct TrackPoint {
glm::vec3 position;
int type; // 0=Smooth, 1=Straight
bool visible = true; // NEW: Render rail or not
float timeMs = 0.0f;
};
struct Keyframe {
float t;
std::string param;
float value;
};
struct LevelNote {
float distance = 0.0f;
float timeMs = 0.0f;
float appearTimeMs = 0.0f;
float endTimeMs = 0.0f;
float endDistance = 0.0f;
unsigned int rawType = 0;
unsigned int effectiveType = 0;
// Wire +5 becomes runtime marker +8 in FUN_005ea800. Any non-zero value
// makes FUN_005e9410 select the ALB slot of the active gameplay SE set.
bool adlib = false;
// First signed 16-bit field at wire +6. DrawMark passes this value minus
// one as the UV-list selector for game effect 3.
int markEffectId = -1;
// game471 wire +55. The stage authors use this packed RRGGBBAA value for
// the path/body colour of duration targets.
uint32_t packedColor = 0xffffffffu;
uint32_t merryCount = 0;
// BuildTimingDataSub converts wire +25 (length) and +29/+33 (HPB angles)
// to this world-space vector. GameScene::buildGameData later projects it
// through the camera active at timeMs to obtain a fixed screen angle.
glm::vec3 directionVector{0.0f};
// Runtime +0x20 and +0xc4 in game471's marker object.
float beatDurationMs = 500.0f;
float earlyTimingMs = 250.0f;
float lateTimingMs = 250.0f;
float missTimingMs = 250.0f;
float muteTimingMs = 0.0f;
float markerFadeEndTimeMs = 0.0f;
bool gcTiming = false;
};
// Exact 59-byte stage camera record after decoding from big endian. The
// original game expands this to a 0x44-byte runtime key by adding rotationA.z
// = 0; keeping the wire fields here lets the player reproduce aMode/fMode
// instead of flattening them into the legacy editor timeline.
struct GcCameraKey {
uint32_t timeMs = 0;
uint8_t aMode = 0;
uint8_t fMode = 0;
float dist = 0.0f;
glm::vec3 rotationA{0.0f};
glm::vec3 originOff{0.0f};
uint8_t projType = 0;
glm::vec3 fieldFar{0.0f};
glm::vec3 fieldNear{0.0f};
float rotationB = 0.0f;
};
struct LevelData {
std::string title;
std::string author;
std::string audioPath;
std::string audioShotPath;
float audioBgmGain = 1.0f;
float audioShotGain = 1.0f;
// game471 SE set slots: ALB, TP1 and TP2. The arcade runtime allocates
// two playback channels for each slot so repeated taps can overlap.
std::array<std::string, 3> gameplaySoundPaths;
std::array<float, 3> gameplaySoundGains{1.0f, 1.0f, 1.0f};
std::string backgroundPath;
std::vector<TrackPoint> trackPoints;
std::vector<LevelNote> notes;
std::vector<GcCameraKey> gcCameraKeys;
// Optional arcade *_clip.dat table. It is object-major and contains one
// byte per 60 Hz frame; a zero byte suppresses that stage object for the
// frame. game471 consumes this before evaluating authored visibility.
uint32_t gcObjectClipFrameCount = 0;
std::vector<uint8_t> gcObjectClipVisibility;
// Complete decoded stage background: particle/visualizer keys, exact
// gradient fade flags and the animated 3D object scene.
std::optional<gc::ParsedStagePattern> gcStage;
std::vector<Keyframe> timeline;
std::map<std::string, float> config;
};
class LevelLoader {
public:
static LevelData load(const std::string& path);
};
@@ -0,0 +1,12 @@
#pragma once
#include <SDL3/SDL.h>
#include <filesystem>
class CabinetBackend;
// Runs the cabinet operator/test mode until Test/Escape returns to the caller.
// The existing OpenGL context remains owned by the caller.
void runServiceMenu(SDL_Window* window, CabinetBackend& cabinet,
const std::filesystem::path& contentPath = {});
@@ -0,0 +1,12 @@
#pragma once
#include <SDL3/SDL.h>
#include <filesystem>
#include <string>
// Runs the Groove Coaster-style song and difficulty selector. Returns false
// when the window is closed/cancelled, otherwise writes the chosen stage .dat.
bool runSongSelect(SDL_Window* window,
const std::filesystem::path& gcRoot,
std::string* selectedStagePath);