mirror of
https://github.com/LostArtefacts/TRX.git
synced 2025-04-28 12:47:58 +03:00
misc: import libtrx codebase
This commit is contained in:
parent
d46069af22
commit
61ae36ee44
193 changed files with 29958 additions and 0 deletions
62
src/libtrx/benchmark.c
Normal file
62
src/libtrx/benchmark.c
Normal file
|
@ -0,0 +1,62 @@
|
|||
#include "benchmark.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <SDL2/SDL_timer.h>
|
||||
|
||||
static void M_Log(
|
||||
BENCHMARK *const b, const char *file, int32_t line, const char *func,
|
||||
Uint64 current, const char *message)
|
||||
{
|
||||
const Uint64 freq = SDL_GetPerformanceFrequency();
|
||||
const double elapsed_start =
|
||||
(double)(current - b->start) * 1000.0 / (double)freq;
|
||||
const double elapsed_last =
|
||||
(double)(current - b->last) * 1000.0 / (double)freq;
|
||||
|
||||
if (b->last != b->start) {
|
||||
if (message == NULL) {
|
||||
Log_Message(
|
||||
file, line, func, "took %.02f ms (%.02f ms)", elapsed_start,
|
||||
elapsed_last);
|
||||
} else {
|
||||
Log_Message(
|
||||
file, line, func, "%s: took %.02f ms (%.02f ms)", message,
|
||||
elapsed_start, elapsed_last);
|
||||
}
|
||||
} else {
|
||||
if (message == NULL) {
|
||||
Log_Message(file, line, func, "took %.02f ms", elapsed_start);
|
||||
} else {
|
||||
Log_Message(
|
||||
file, line, func, "%s: took %.02f ms (%.02f ms)", message,
|
||||
elapsed_start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BENCHMARK *Benchmark_Start(void)
|
||||
{
|
||||
BENCHMARK *const b = Memory_Alloc(sizeof(BENCHMARK));
|
||||
b->start = SDL_GetPerformanceCounter();
|
||||
b->last = b->start;
|
||||
return b;
|
||||
}
|
||||
|
||||
void Benchmark_Tick_Impl(
|
||||
BENCHMARK *const b, const char *const file, const int32_t line,
|
||||
const char *const func, const char *const message)
|
||||
{
|
||||
const Uint64 current = SDL_GetPerformanceCounter();
|
||||
M_Log(b, file, line, func, current, message);
|
||||
b->last = current;
|
||||
}
|
||||
|
||||
void Benchmark_End_Impl(
|
||||
BENCHMARK *b, const char *const file, const int32_t line,
|
||||
const char *const func, const char *const message)
|
||||
{
|
||||
Benchmark_Tick_Impl(b, file, line, func, message);
|
||||
Memory_FreePointer(&b);
|
||||
}
|
60
src/libtrx/config/common.c
Normal file
60
src/libtrx/config/common.c
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include "config/common.h"
|
||||
|
||||
#include "config/file.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
EVENT_MANAGER *m_EventManager = NULL;
|
||||
|
||||
void Config_Init(void)
|
||||
{
|
||||
m_EventManager = EventManager_Create();
|
||||
}
|
||||
|
||||
void Config_Shutdown(void)
|
||||
{
|
||||
EventManager_Free(m_EventManager);
|
||||
m_EventManager = NULL;
|
||||
}
|
||||
|
||||
bool Config_Read(void)
|
||||
{
|
||||
const bool result = ConfigFile_Read(Config_GetPath(), &Config_LoadFromJSON);
|
||||
if (result) {
|
||||
Config_Sanitize();
|
||||
Config_ApplyChanges();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Config_Write(void)
|
||||
{
|
||||
Config_Sanitize();
|
||||
const bool updated = ConfigFile_Write(Config_GetPath(), &Config_DumpToJSON);
|
||||
if (updated) {
|
||||
Config_ApplyChanges();
|
||||
if (m_EventManager != NULL) {
|
||||
const EVENT event = {
|
||||
.name = "write",
|
||||
.sender = NULL,
|
||||
.data = NULL,
|
||||
};
|
||||
EventManager_Fire(m_EventManager, &event);
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
int32_t Config_SubscribeChanges(
|
||||
const EVENT_LISTENER listener, void *const user_data)
|
||||
{
|
||||
assert(m_EventManager != NULL);
|
||||
return EventManager_Subscribe(
|
||||
m_EventManager, "write", NULL, listener, user_data);
|
||||
}
|
||||
|
||||
void Config_UnsubscribeChanges(const int32_t listener_id)
|
||||
{
|
||||
assert(m_EventManager != NULL);
|
||||
return EventManager_Unsubscribe(m_EventManager, listener_id);
|
||||
}
|
197
src/libtrx/config/file.c
Normal file
197
src/libtrx/config/file.c
Normal file
|
@ -0,0 +1,197 @@
|
|||
#include "config/file.h"
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static bool M_ReadFromJSON(
|
||||
const char *json, void (*load)(JSON_OBJECT *root_obj));
|
||||
static char *M_WriteToJSON(void (*dump)(JSON_OBJECT *root_obj));
|
||||
static const char *M_ResolveOptionName(const char *option_name);
|
||||
|
||||
static bool M_ReadFromJSON(
|
||||
const char *cfg_data, void (*load)(JSON_OBJECT *root_obj))
|
||||
{
|
||||
bool result = false;
|
||||
|
||||
JSON_PARSE_RESULT parse_result;
|
||||
JSON_VALUE *root = JSON_ParseEx(
|
||||
cfg_data, strlen(cfg_data), JSON_PARSE_FLAGS_ALLOW_JSON5, NULL, NULL,
|
||||
&parse_result);
|
||||
if (root != NULL) {
|
||||
result = true;
|
||||
} else {
|
||||
LOG_ERROR(
|
||||
"failed to parse config file: %s in line %d, char %d",
|
||||
JSON_GetErrorDescription(parse_result.error),
|
||||
parse_result.error_line_no, parse_result.error_row_no);
|
||||
// continue to supply the default values
|
||||
}
|
||||
|
||||
JSON_OBJECT *root_obj = JSON_ValueAsObject(root);
|
||||
load(root_obj);
|
||||
|
||||
if (root) {
|
||||
JSON_ValueFree(root);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static char *M_WriteToJSON(void (*dump)(JSON_OBJECT *root_obj))
|
||||
{
|
||||
JSON_OBJECT *root_obj = JSON_ObjectNew();
|
||||
|
||||
dump(root_obj);
|
||||
|
||||
JSON_VALUE *root = JSON_ValueFromObject(root_obj);
|
||||
size_t size;
|
||||
char *data = JSON_WritePretty(root, " ", "\n", &size);
|
||||
JSON_ValueFree(root);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
static const char *M_ResolveOptionName(const char *option_name)
|
||||
{
|
||||
const char *dot = strrchr(option_name, '.');
|
||||
if (dot) {
|
||||
return dot + 1;
|
||||
}
|
||||
return option_name;
|
||||
}
|
||||
|
||||
bool ConfigFile_Read(const char *path, void (*load)(JSON_OBJECT *root_obj))
|
||||
{
|
||||
bool result = false;
|
||||
char *cfg_data = NULL;
|
||||
|
||||
if (!File_Load(path, &cfg_data, NULL)) {
|
||||
LOG_WARNING("'%s' not loaded - default settings will apply", path);
|
||||
result = M_ReadFromJSON("{}", load);
|
||||
} else {
|
||||
result = M_ReadFromJSON(cfg_data, load);
|
||||
}
|
||||
|
||||
Memory_FreePointer(&cfg_data);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ConfigFile_Write(const char *path, void (*dump)(JSON_OBJECT *root_obj))
|
||||
{
|
||||
LOG_INFO("Saving user settings");
|
||||
|
||||
char *old_data;
|
||||
File_Load(path, &old_data, NULL);
|
||||
|
||||
bool updated = false;
|
||||
char *data = M_WriteToJSON(dump);
|
||||
if (old_data == NULL || strcmp(data, old_data) != 0) {
|
||||
MYFILE *const fp = File_Open(path, FILE_OPEN_WRITE);
|
||||
if (fp == NULL) {
|
||||
LOG_ERROR("Failed to write settings!");
|
||||
} else {
|
||||
File_WriteData(fp, data, strlen(data));
|
||||
File_Close(fp);
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
Memory_FreePointer(&data);
|
||||
return updated;
|
||||
}
|
||||
|
||||
void ConfigFile_LoadOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options)
|
||||
{
|
||||
const CONFIG_OPTION *opt = options;
|
||||
while (opt->target) {
|
||||
switch (opt->type) {
|
||||
case COT_BOOL:
|
||||
*(bool *)opt->target = JSON_ObjectGetBool(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(bool *)opt->default_value);
|
||||
break;
|
||||
|
||||
case COT_INT32:
|
||||
*(int32_t *)opt->target = JSON_ObjectGetInt(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(int32_t *)opt->default_value);
|
||||
break;
|
||||
|
||||
case COT_FLOAT:
|
||||
*(float *)opt->target = JSON_ObjectGetDouble(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(float *)opt->default_value);
|
||||
break;
|
||||
|
||||
case COT_DOUBLE:
|
||||
*(double *)opt->target = JSON_ObjectGetDouble(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(double *)opt->default_value);
|
||||
break;
|
||||
|
||||
case COT_ENUM:
|
||||
*(int *)opt->target = ConfigFile_ReadEnum(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(int *)opt->default_value, opt->param);
|
||||
}
|
||||
opt++;
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigFile_DumpOptions(JSON_OBJECT *root_obj, const CONFIG_OPTION *options)
|
||||
{
|
||||
const CONFIG_OPTION *opt = options;
|
||||
while (opt->target) {
|
||||
switch (opt->type) {
|
||||
case COT_BOOL:
|
||||
JSON_ObjectAppendBool(
|
||||
root_obj, M_ResolveOptionName(opt->name), *(bool *)opt->target);
|
||||
break;
|
||||
|
||||
case COT_INT32:
|
||||
JSON_ObjectAppendInt(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(int32_t *)opt->target);
|
||||
break;
|
||||
|
||||
case COT_FLOAT:
|
||||
JSON_ObjectAppendDouble(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(float *)opt->target);
|
||||
break;
|
||||
|
||||
case COT_DOUBLE:
|
||||
JSON_ObjectAppendDouble(
|
||||
root_obj, M_ResolveOptionName(opt->name),
|
||||
*(double *)opt->target);
|
||||
break;
|
||||
|
||||
case COT_ENUM:
|
||||
ConfigFile_WriteEnum(
|
||||
root_obj, M_ResolveOptionName(opt->name), *(int *)opt->target,
|
||||
(const char *)opt->param);
|
||||
break;
|
||||
}
|
||||
opt++;
|
||||
}
|
||||
}
|
||||
|
||||
int ConfigFile_ReadEnum(
|
||||
JSON_OBJECT *const obj, const char *const name, const int default_value,
|
||||
const char *const enum_name)
|
||||
{
|
||||
const char *value_str = JSON_ObjectGetString(obj, name, NULL);
|
||||
if (value_str != NULL) {
|
||||
return EnumMap_Get(enum_name, value_str, default_value);
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
void ConfigFile_WriteEnum(
|
||||
JSON_OBJECT *obj, const char *name, int value, const char *enum_name)
|
||||
{
|
||||
JSON_ObjectAppendString(obj, name, EnumMap_ToString(enum_name, value));
|
||||
}
|
128
src/libtrx/engine/audio.c
Normal file
128
src/libtrx/engine/audio.c
Normal file
|
@ -0,0 +1,128 @@
|
|||
#include "audio_internal.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_error.h>
|
||||
#include <SDL2/SDL_stdinc.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
SDL_AudioDeviceID g_AudioDeviceID = 0;
|
||||
static int32_t m_RefCount = 0;
|
||||
static size_t m_MixBufferCapacity = 0;
|
||||
static float *m_MixBuffer = NULL;
|
||||
static Uint8 m_Silence = 0;
|
||||
|
||||
static void M_MixerCallback(void *userdata, Uint8 *stream_data, int32_t len);
|
||||
|
||||
static void M_MixerCallback(void *userdata, Uint8 *stream_data, int32_t len)
|
||||
{
|
||||
memset(m_MixBuffer, m_Silence, len);
|
||||
Audio_Stream_Mix(m_MixBuffer, len);
|
||||
Audio_Sample_Mix(m_MixBuffer, len);
|
||||
memcpy(stream_data, m_MixBuffer, len);
|
||||
}
|
||||
|
||||
bool Audio_Init(void)
|
||||
{
|
||||
m_RefCount++;
|
||||
if (g_AudioDeviceID) {
|
||||
// already initialized
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t result = SDL_Init(SDL_INIT_AUDIO);
|
||||
if (result < 0) {
|
||||
LOG_ERROR("Error while calling SDL_Init: 0x%lx", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_AudioSpec desired;
|
||||
SDL_memset(&desired, 0, sizeof(desired));
|
||||
desired.freq = AUDIO_WORKING_RATE;
|
||||
desired.format = AUDIO_WORKING_FORMAT;
|
||||
desired.channels = AUDIO_WORKING_CHANNELS;
|
||||
desired.samples = AUDIO_SAMPLES;
|
||||
desired.callback = M_MixerCallback;
|
||||
desired.userdata = NULL;
|
||||
|
||||
SDL_AudioSpec delivered;
|
||||
g_AudioDeviceID = SDL_OpenAudioDevice(NULL, 0, &desired, &delivered, 0);
|
||||
|
||||
if (!g_AudioDeviceID) {
|
||||
LOG_ERROR("Failed to open audio device: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Silence = desired.silence;
|
||||
m_MixBufferCapacity = desired.samples * desired.channels
|
||||
* SDL_AUDIO_BITSIZE(desired.format) / 8;
|
||||
|
||||
m_MixBuffer = Memory_Alloc(m_MixBufferCapacity);
|
||||
|
||||
SDL_PauseAudioDevice(g_AudioDeviceID, 0);
|
||||
|
||||
Audio_Sample_Init();
|
||||
Audio_Stream_Init();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Shutdown(void)
|
||||
{
|
||||
m_RefCount--;
|
||||
if (m_RefCount > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (g_AudioDeviceID) {
|
||||
SDL_PauseAudioDevice(g_AudioDeviceID, 1);
|
||||
SDL_CloseAudioDevice(g_AudioDeviceID);
|
||||
g_AudioDeviceID = 0;
|
||||
}
|
||||
|
||||
Memory_FreePointer(&m_MixBuffer);
|
||||
|
||||
Audio_Sample_Shutdown();
|
||||
Audio_Stream_Shutdown();
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t Audio_GetAVChannelLayout(const int32_t channels)
|
||||
{
|
||||
switch (channels) {
|
||||
// clang-format off
|
||||
case 1: return AV_CH_LAYOUT_MONO;
|
||||
case 2: return AV_CH_LAYOUT_STEREO;
|
||||
default: return AV_CH_LAYOUT_MONO;
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
|
||||
int32_t Audio_GetAVAudioFormat(const int32_t sample_fmt)
|
||||
{
|
||||
switch (sample_fmt) {
|
||||
// clang-format off
|
||||
case AUDIO_U8: return AV_SAMPLE_FMT_U8;
|
||||
case AUDIO_S16: return AV_SAMPLE_FMT_S16;
|
||||
case AUDIO_S32: return AV_SAMPLE_FMT_S32;
|
||||
case AUDIO_F32: return AV_SAMPLE_FMT_FLT;
|
||||
default: return -1;
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
|
||||
int32_t Audio_GetSDLAudioFormat(const enum AVSampleFormat sample_fmt)
|
||||
{
|
||||
// clang-format off
|
||||
switch (sample_fmt) {
|
||||
case AV_SAMPLE_FMT_U8: return AUDIO_U8;
|
||||
case AV_SAMPLE_FMT_S16: return AUDIO_S16;
|
||||
case AV_SAMPLE_FMT_S32: return AUDIO_S32;
|
||||
case AV_SAMPLE_FMT_FLT: return AUDIO_F32;
|
||||
default: return -1;
|
||||
}
|
||||
// clang-format on
|
||||
}
|
25
src/libtrx/engine/audio_internal.h
Normal file
25
src/libtrx/engine/audio_internal.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#pragma once
|
||||
|
||||
#include "engine/audio.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#define AUDIO_WORKING_RATE 44100
|
||||
#define AUDIO_WORKING_FORMAT AUDIO_F32
|
||||
#define AUDIO_SAMPLES 500
|
||||
#define AUDIO_WORKING_CHANNELS 2
|
||||
|
||||
extern SDL_AudioDeviceID g_AudioDeviceID;
|
||||
|
||||
int32_t Audio_GetAVChannelLayout(int32_t sample_fmt);
|
||||
int32_t Audio_GetAVAudioFormat(int32_t sample_fmt);
|
||||
int32_t Audio_GetSDLAudioFormat(enum AVSampleFormat sample_fmt);
|
||||
|
||||
void Audio_Sample_Init(void);
|
||||
void Audio_Sample_Shutdown(void);
|
||||
void Audio_Sample_Mix(float *dst_buffer, size_t len);
|
||||
|
||||
void Audio_Stream_Init(void);
|
||||
void Audio_Stream_Shutdown(void);
|
||||
void Audio_Stream_Mix(float *dst_buffer, size_t len);
|
776
src/libtrx/engine/audio_sample.c
Normal file
776
src/libtrx/engine/audio_sample.c
Normal file
|
@ -0,0 +1,776 @@
|
|||
#include "audio_internal.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <SDL2/SDL_audio.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavcodec/codec.h>
|
||||
#include <libavcodec/packet.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavformat/avio.h>
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <libavutil/mem.h>
|
||||
#include <libavutil/samplefmt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
|
||||
typedef struct {
|
||||
char *original_data;
|
||||
size_t original_size;
|
||||
|
||||
float *sample_data;
|
||||
int32_t channels;
|
||||
int32_t num_samples;
|
||||
} AUDIO_SAMPLE;
|
||||
|
||||
typedef struct {
|
||||
bool is_used;
|
||||
bool is_looped;
|
||||
bool is_playing;
|
||||
float volume_l; // sample gain multiplier
|
||||
float volume_r; // sample gain multiplier
|
||||
|
||||
float pitch;
|
||||
int32_t volume; // volume specified in hundredths of decibel
|
||||
int32_t pan; // pan specified in hundredths of decibel
|
||||
|
||||
// pitch shift means the same samples can be reused twice, hence float
|
||||
float current_sample;
|
||||
|
||||
AUDIO_SAMPLE *sample;
|
||||
} AUDIO_SAMPLE_SOUND;
|
||||
|
||||
typedef struct {
|
||||
const char *data;
|
||||
const char *ptr;
|
||||
int32_t size;
|
||||
int32_t remaining;
|
||||
} AUDIO_AV_BUFFER;
|
||||
|
||||
static int32_t m_LoadedSamplesCount = 0;
|
||||
static AUDIO_SAMPLE m_LoadedSamples[AUDIO_MAX_SAMPLES] = { 0 };
|
||||
static AUDIO_SAMPLE_SOUND m_Samples[AUDIO_MAX_ACTIVE_SAMPLES] = { 0 };
|
||||
|
||||
static double M_DecibelToMultiplier(double db_gain);
|
||||
static bool M_RecalculateChannelVolumes(int32_t sound_id);
|
||||
static int32_t M_ReadAVBuffer(void *opaque, uint8_t *dst, int32_t dst_size);
|
||||
static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence);
|
||||
static bool M_Convert(const int32_t sample_id);
|
||||
|
||||
static double M_DecibelToMultiplier(double db_gain)
|
||||
{
|
||||
return pow(2.0, db_gain / 600.0);
|
||||
}
|
||||
|
||||
static bool M_RecalculateChannelVolumes(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];
|
||||
sound->volume_l = M_DecibelToMultiplier(
|
||||
sound->volume - (sound->pan > 0 ? sound->pan : 0));
|
||||
sound->volume_r = M_DecibelToMultiplier(
|
||||
sound->volume + (sound->pan < 0 ? sound->pan : 0));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int32_t M_ReadAVBuffer(void *opaque, uint8_t *dst, int32_t dst_size)
|
||||
{
|
||||
assert(opaque != NULL);
|
||||
assert(dst != NULL);
|
||||
AUDIO_AV_BUFFER *src = opaque;
|
||||
int32_t read = dst_size >= src->remaining ? src->remaining : dst_size;
|
||||
if (!read) {
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
memcpy(dst, src->ptr, read);
|
||||
src->ptr += read;
|
||||
src->remaining -= read;
|
||||
return read;
|
||||
}
|
||||
|
||||
static int64_t M_SeekAVBuffer(void *opaque, int64_t offset, int32_t whence)
|
||||
{
|
||||
assert(opaque != NULL);
|
||||
AUDIO_AV_BUFFER *src = opaque;
|
||||
if (whence & AVSEEK_SIZE) {
|
||||
return src->size;
|
||||
}
|
||||
switch (whence) {
|
||||
case SEEK_SET:
|
||||
if (src->size - offset < 0) {
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
src->ptr = src->data + offset;
|
||||
src->remaining = src->size - offset;
|
||||
break;
|
||||
case SEEK_CUR:
|
||||
if (src->remaining - offset < 0) {
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
src->ptr += offset;
|
||||
src->remaining -= offset;
|
||||
break;
|
||||
case SEEK_END:
|
||||
if (src->size + offset < 0) {
|
||||
return AVERROR_EOF;
|
||||
}
|
||||
src->ptr = src->data - offset;
|
||||
src->remaining = src->size + offset;
|
||||
break;
|
||||
}
|
||||
return src->ptr - src->data;
|
||||
}
|
||||
|
||||
static bool M_Convert(const int32_t sample_id)
|
||||
{
|
||||
assert(sample_id >= 0 && sample_id < m_LoadedSamplesCount);
|
||||
|
||||
bool result = false;
|
||||
AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];
|
||||
|
||||
if (sample->sample_data != NULL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const clock_t time_start = clock();
|
||||
size_t working_buffer_size = 0;
|
||||
float *working_buffer = NULL;
|
||||
|
||||
struct {
|
||||
size_t read_buffer_size;
|
||||
AVIOContext *avio_context;
|
||||
AVStream *stream;
|
||||
AVFormatContext *format_ctx;
|
||||
const AVCodec *codec;
|
||||
AVCodecContext *codec_ctx;
|
||||
AVPacket *packet;
|
||||
AVFrame *frame;
|
||||
} av = {
|
||||
.read_buffer_size = 8192,
|
||||
.avio_context = NULL,
|
||||
.stream = NULL,
|
||||
.format_ctx = NULL,
|
||||
.codec = NULL,
|
||||
.codec_ctx = NULL,
|
||||
.packet = NULL,
|
||||
.frame = NULL,
|
||||
};
|
||||
|
||||
struct {
|
||||
int32_t src_format;
|
||||
int32_t src_channels;
|
||||
int32_t src_sample_rate;
|
||||
int32_t dst_format;
|
||||
int32_t dst_channels;
|
||||
int32_t dst_sample_rate;
|
||||
SwrContext *ctx;
|
||||
} swr = { 0 };
|
||||
|
||||
int32_t error_code;
|
||||
|
||||
unsigned char *read_buffer = av_malloc(av.read_buffer_size);
|
||||
if (!read_buffer) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
AUDIO_AV_BUFFER av_buf = {
|
||||
.data = sample->original_data,
|
||||
.ptr = sample->original_data,
|
||||
.size = sample->original_size,
|
||||
.remaining = sample->original_size,
|
||||
};
|
||||
|
||||
av.avio_context = avio_alloc_context(
|
||||
read_buffer, av.read_buffer_size, 0, &av_buf, M_ReadAVBuffer, NULL,
|
||||
M_SeekAVBuffer);
|
||||
|
||||
av.format_ctx = avformat_alloc_context();
|
||||
av.format_ctx->pb = av.avio_context;
|
||||
error_code =
|
||||
avformat_open_input(&av.format_ctx, "dummy_filename", NULL, NULL);
|
||||
if (error_code != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = avformat_find_stream_info(av.format_ctx, NULL);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
av.stream = NULL;
|
||||
for (uint32_t i = 0; i < av.format_ctx->nb_streams; i++) {
|
||||
AVStream *current_stream = av.format_ctx->streams[i];
|
||||
if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
av.stream = current_stream;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!av.stream) {
|
||||
error_code = AVERROR_STREAM_NOT_FOUND;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
av.codec = avcodec_find_decoder(av.stream->codecpar->codec_id);
|
||||
if (!av.codec) {
|
||||
error_code = AVERROR_DEMUXER_NOT_FOUND;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
av.codec_ctx = avcodec_alloc_context3(av.codec);
|
||||
if (!av.codec_ctx) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code =
|
||||
avcodec_parameters_to_context(av.codec_ctx, av.stream->codecpar);
|
||||
if (error_code) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = avcodec_open2(av.codec_ctx, av.codec, NULL);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
av.packet = av_packet_alloc();
|
||||
if (!av.packet) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
av.frame = av_frame_alloc();
|
||||
if (!av.frame) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
while (1) {
|
||||
error_code = av_read_frame(av.format_ctx, av.packet);
|
||||
if (error_code == AVERROR_EOF) {
|
||||
av_packet_unref(av.packet);
|
||||
error_code = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (error_code < 0) {
|
||||
av_packet_unref(av.packet);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = avcodec_send_packet(av.codec_ctx, av.packet);
|
||||
if (error_code < 0) {
|
||||
av_packet_unref(av.packet);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
if (!swr.ctx) {
|
||||
swr.src_sample_rate = av.codec_ctx->sample_rate;
|
||||
swr.src_channels = av.codec_ctx->channels;
|
||||
swr.src_format = av.codec_ctx->sample_fmt;
|
||||
swr.dst_sample_rate = AUDIO_WORKING_RATE;
|
||||
swr.dst_channels = 1;
|
||||
swr.dst_format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT);
|
||||
swr.ctx = swr_alloc_set_opts(
|
||||
swr.ctx, swr.dst_channels, swr.dst_format, swr.dst_sample_rate,
|
||||
swr.src_channels, swr.src_format, swr.src_sample_rate, 0, 0);
|
||||
if (!swr.ctx) {
|
||||
av_packet_unref(av.packet);
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = swr_init(swr.ctx);
|
||||
if (error_code != 0) {
|
||||
av_packet_unref(av.packet);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
while (1) {
|
||||
error_code = avcodec_receive_frame(av.codec_ctx, av.frame);
|
||||
if (error_code == AVERROR(EAGAIN)) {
|
||||
av_frame_unref(av.frame);
|
||||
break;
|
||||
}
|
||||
|
||||
if (error_code < 0) {
|
||||
av_packet_unref(av.packet);
|
||||
av_frame_unref(av.frame);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
uint8_t *out_buffer = NULL;
|
||||
const int32_t out_samples =
|
||||
swr_get_out_samples(swr.ctx, av.frame->nb_samples);
|
||||
av_samples_alloc(
|
||||
&out_buffer, NULL, swr.dst_channels, out_samples,
|
||||
swr.dst_format, 1);
|
||||
int32_t resampled_size = swr_convert(
|
||||
swr.ctx, &out_buffer, out_samples,
|
||||
(const uint8_t **)av.frame->data, av.frame->nb_samples);
|
||||
while (resampled_size > 0) {
|
||||
int32_t out_buffer_size = av_samples_get_buffer_size(
|
||||
NULL, swr.dst_channels, resampled_size, swr.dst_format, 1);
|
||||
|
||||
if (out_buffer_size > 0) {
|
||||
working_buffer = Memory_Realloc(
|
||||
working_buffer, working_buffer_size + out_buffer_size);
|
||||
if (out_buffer) {
|
||||
memcpy(
|
||||
(uint8_t *)working_buffer + working_buffer_size,
|
||||
out_buffer, out_buffer_size);
|
||||
}
|
||||
working_buffer_size += out_buffer_size;
|
||||
}
|
||||
|
||||
resampled_size =
|
||||
swr_convert(swr.ctx, &out_buffer, out_samples, NULL, 0);
|
||||
}
|
||||
|
||||
av_freep(&out_buffer);
|
||||
av_frame_unref(av.frame);
|
||||
}
|
||||
|
||||
av_packet_unref(av.packet);
|
||||
}
|
||||
|
||||
int32_t sample_format_bytes = av_get_bytes_per_sample(swr.dst_format);
|
||||
sample->num_samples =
|
||||
working_buffer_size / sample_format_bytes / swr.dst_channels;
|
||||
sample->channels = swr.src_channels;
|
||||
sample->sample_data = working_buffer;
|
||||
result = true;
|
||||
|
||||
const clock_t time_end = clock();
|
||||
const double time_delta =
|
||||
(((double)(time_end - time_start)) / CLOCKS_PER_SEC) * 1000.0f;
|
||||
LOG_DEBUG(
|
||||
"Sample %d decoded (%.0f ms)", sample_id, sample->original_size,
|
||||
time_delta);
|
||||
|
||||
cleanup:
|
||||
if (error_code != 0) {
|
||||
LOG_ERROR(
|
||||
"Error while opening sample ID %d: %s", sample_id,
|
||||
av_err2str(error_code));
|
||||
}
|
||||
|
||||
if (swr.ctx) {
|
||||
swr_free(&swr.ctx);
|
||||
}
|
||||
|
||||
if (av.frame) {
|
||||
av_frame_free(&av.frame);
|
||||
}
|
||||
|
||||
if (av.packet) {
|
||||
av_packet_free(&av.packet);
|
||||
}
|
||||
|
||||
av.codec = NULL;
|
||||
|
||||
if (!result) {
|
||||
sample->sample_data = NULL;
|
||||
sample->original_data = NULL;
|
||||
sample->original_size = 0;
|
||||
sample->num_samples = 0;
|
||||
sample->channels = 0;
|
||||
Memory_FreePointer(&working_buffer);
|
||||
}
|
||||
|
||||
if (av.codec_ctx) {
|
||||
avcodec_close(av.codec_ctx);
|
||||
av_freep(&av.codec_ctx);
|
||||
}
|
||||
|
||||
if (av.format_ctx) {
|
||||
avformat_close_input(&av.format_ctx);
|
||||
}
|
||||
|
||||
if (av.avio_context) {
|
||||
av_freep(&av.avio_context->buffer);
|
||||
avio_context_free(&av.avio_context);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void Audio_Sample_Init(void)
|
||||
{
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;
|
||||
sound_id++) {
|
||||
AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];
|
||||
sound->is_used = false;
|
||||
sound->is_playing = false;
|
||||
sound->volume = 0.0f;
|
||||
sound->pitch = 1.0f;
|
||||
sound->pan = 0.0f;
|
||||
sound->current_sample = 0.0f;
|
||||
sound->sample = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void Audio_Sample_Shutdown(void)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return;
|
||||
}
|
||||
|
||||
Audio_Sample_CloseAll();
|
||||
Audio_Sample_UnloadAll();
|
||||
}
|
||||
|
||||
bool Audio_Sample_Unload(const int32_t sample_id)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
LOG_ERROR("Unitialized audio device");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sample_id < 0 || sample_id >= AUDIO_MAX_SAMPLES) {
|
||||
LOG_ERROR("Maximum allowed samples: %d", AUDIO_MAX_SAMPLES);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = false;
|
||||
AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];
|
||||
if (sample->sample_data == NULL) {
|
||||
LOG_ERROR("Sample %d is already unloaded", sample_id);
|
||||
return false;
|
||||
}
|
||||
Memory_FreePointer(&sample->sample_data);
|
||||
Memory_FreePointer(&sample->original_data);
|
||||
m_LoadedSamplesCount--;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_UnloadAll(void)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
LOG_ERROR("Unitialized audio device");
|
||||
return false;
|
||||
}
|
||||
|
||||
m_LoadedSamplesCount = 0;
|
||||
for (int32_t i = 0; i < AUDIO_MAX_SAMPLES; i++) {
|
||||
AUDIO_SAMPLE *const sample = &m_LoadedSamples[i];
|
||||
Memory_FreePointer(&sample->sample_data);
|
||||
Memory_FreePointer(&sample->original_data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_LoadSingle(
|
||||
const int32_t sample_id, const char *const data, const size_t size)
|
||||
{
|
||||
assert(data != NULL);
|
||||
|
||||
if (!g_AudioDeviceID) {
|
||||
LOG_ERROR("Unitialized audio device");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sample_id < 0 || sample_id >= AUDIO_MAX_SAMPLES) {
|
||||
LOG_ERROR("Maximum allowed samples: %d", AUDIO_MAX_SAMPLES);
|
||||
return false;
|
||||
}
|
||||
|
||||
AUDIO_SAMPLE *const sample = &m_LoadedSamples[sample_id];
|
||||
if (sample->original_data != NULL) {
|
||||
LOG_ERROR(
|
||||
"Sample %d is already loaded (trying to overwrite with %d bytes)",
|
||||
sample_id, size);
|
||||
return false;
|
||||
}
|
||||
|
||||
sample->original_data = Memory_Alloc(size);
|
||||
sample->original_size = size;
|
||||
memcpy(sample->original_data, data, size);
|
||||
m_LoadedSamplesCount++;
|
||||
LOG_ERROR("Sample %d loaded (%d bytes)", sample_id, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_LoadMany(size_t count, const char **contents, size_t *sizes)
|
||||
{
|
||||
assert(contents != NULL);
|
||||
assert(sizes != NULL);
|
||||
|
||||
if (!g_AudioDeviceID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(count <= AUDIO_MAX_SAMPLES);
|
||||
|
||||
Audio_Sample_CloseAll();
|
||||
Audio_Sample_UnloadAll();
|
||||
|
||||
bool result = true;
|
||||
for (int32_t i = 0; i < (int32_t)count; i++) {
|
||||
result &= Audio_Sample_LoadSingle(i, contents[i], sizes[i]);
|
||||
}
|
||||
if (!result) {
|
||||
Audio_Sample_UnloadAll();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t Audio_Sample_Play(
|
||||
int32_t sample_id, int32_t volume, float pitch, int32_t pan, bool is_looped)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
LOG_ERROR("audio device is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sample_id < 0 || sample_id >= m_LoadedSamplesCount) {
|
||||
LOG_DEBUG("Invalid sample id: %d", sample_id);
|
||||
return AUDIO_NO_SOUND;
|
||||
}
|
||||
|
||||
int32_t result = AUDIO_NO_SOUND;
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;
|
||||
sound_id++) {
|
||||
AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];
|
||||
if (sound->is_used) {
|
||||
continue;
|
||||
}
|
||||
|
||||
M_Convert(sample_id);
|
||||
|
||||
sound->is_used = true;
|
||||
sound->is_playing = true;
|
||||
sound->volume = volume;
|
||||
sound->pitch = pitch;
|
||||
sound->pan = pan;
|
||||
sound->is_looped = is_looped;
|
||||
sound->current_sample = 0.0f;
|
||||
sound->sample = &m_LoadedSamples[sample_id];
|
||||
|
||||
M_RecalculateChannelVolumes(sound_id);
|
||||
|
||||
result = sound_id;
|
||||
break;
|
||||
}
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
if (result == AUDIO_NO_SOUND) {
|
||||
LOG_ERROR("All sample buffers are used!");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Audio_Sample_IsPlaying(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_Samples[sound_id].is_playing;
|
||||
}
|
||||
|
||||
bool Audio_Sample_Pause(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Samples[sound_id].is_playing) {
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Samples[sound_id].is_playing = false;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_PauseAll(void)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;
|
||||
sound_id++) {
|
||||
if (m_Samples[sound_id].is_used) {
|
||||
Audio_Sample_Pause(sound_id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_Unpause(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_Samples[sound_id].is_playing) {
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Samples[sound_id].is_playing = true;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_UnpauseAll(void)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;
|
||||
sound_id++) {
|
||||
if (m_Samples[sound_id].is_used) {
|
||||
Audio_Sample_Unpause(sound_id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_Close(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Samples[sound_id].is_used = false;
|
||||
m_Samples[sound_id].is_playing = false;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_CloseAll(void)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;
|
||||
sound_id++) {
|
||||
if (m_Samples[sound_id].is_used) {
|
||||
Audio_Sample_Close(sound_id);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_SetPan(int32_t sound_id, int32_t pan)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Samples[sound_id].pan = pan;
|
||||
M_RecalculateChannelVolumes(sound_id);
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_SetVolume(int32_t sound_id, int32_t volume)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Samples[sound_id].volume = volume;
|
||||
M_RecalculateChannelVolumes(sound_id);
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Sample_SetPitch(int32_t sound_id, float pitch)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_SAMPLES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Samples[sound_id].pitch = pitch;
|
||||
M_RecalculateChannelVolumes(sound_id);
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Audio_Sample_Mix(float *dst_buffer, size_t len)
|
||||
{
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_SAMPLES;
|
||||
sound_id++) {
|
||||
AUDIO_SAMPLE_SOUND *sound = &m_Samples[sound_id];
|
||||
if (!sound->is_playing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t samples_requested =
|
||||
len / sizeof(AUDIO_WORKING_FORMAT) / AUDIO_WORKING_CHANNELS;
|
||||
float src_sample_idx = sound->current_sample;
|
||||
const float *src_buffer = sound->sample->sample_data;
|
||||
float *dst_ptr = dst_buffer;
|
||||
|
||||
while ((dst_ptr - dst_buffer) / AUDIO_WORKING_CHANNELS
|
||||
< samples_requested) {
|
||||
|
||||
// because we handle 3d sound ourselves, downmix to mono
|
||||
float src_sample = 0.0f;
|
||||
for (int32_t i = 0; i < sound->sample->channels; i++) {
|
||||
src_sample += src_buffer
|
||||
[(int32_t)src_sample_idx * sound->sample->channels + i];
|
||||
}
|
||||
src_sample /= (float)sound->sample->channels;
|
||||
|
||||
*dst_ptr++ += src_sample * sound->volume_l;
|
||||
*dst_ptr++ += src_sample * sound->volume_r;
|
||||
src_sample_idx += sound->pitch;
|
||||
|
||||
if ((int32_t)src_sample_idx >= sound->sample->num_samples) {
|
||||
if (sound->is_looped) {
|
||||
src_sample_idx = 0.0f;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sound->current_sample = src_sample_idx;
|
||||
if (sound->current_sample >= sound->sample->num_samples
|
||||
&& !sound->is_looped) {
|
||||
Audio_Sample_Close(sound_id);
|
||||
}
|
||||
}
|
||||
}
|
719
src/libtrx/engine/audio_stream.c
Normal file
719
src/libtrx/engine/audio_stream.c
Normal file
|
@ -0,0 +1,719 @@
|
|||
#include "audio_internal.h"
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <SDL2/SDL_audio.h>
|
||||
#include <SDL2/SDL_error.h>
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavcodec/codec.h>
|
||||
#include <libavcodec/packet.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavformat/avio.h>
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <libavutil/mem.h>
|
||||
#include <libavutil/rational.h>
|
||||
#include <libavutil/samplefmt.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#define READ_BUFFER_SIZE \
|
||||
(AUDIO_SAMPLES * AUDIO_WORKING_CHANNELS * sizeof(AUDIO_WORKING_FORMAT))
|
||||
|
||||
typedef struct {
|
||||
bool is_used;
|
||||
bool is_playing;
|
||||
bool is_read_done;
|
||||
bool is_looped;
|
||||
float volume;
|
||||
double duration;
|
||||
double timestamp;
|
||||
|
||||
double start_at;
|
||||
double stop_at;
|
||||
|
||||
void (*finish_callback)(int32_t sound_id, void *user_data);
|
||||
void *finish_callback_user_data;
|
||||
|
||||
struct {
|
||||
AVStream *stream;
|
||||
AVFormatContext *format_ctx;
|
||||
const AVCodec *codec;
|
||||
AVCodecContext *codec_ctx;
|
||||
AVPacket *packet;
|
||||
AVFrame *frame;
|
||||
} av;
|
||||
|
||||
struct {
|
||||
int32_t src_format;
|
||||
int32_t src_channels;
|
||||
int32_t src_sample_rate;
|
||||
int32_t dst_format;
|
||||
int32_t dst_channels;
|
||||
int32_t dst_sample_rate;
|
||||
SwrContext *ctx;
|
||||
} swr;
|
||||
|
||||
struct {
|
||||
SDL_AudioStream *stream;
|
||||
} sdl;
|
||||
} AUDIO_STREAM_SOUND;
|
||||
|
||||
extern SDL_AudioDeviceID g_AudioDeviceID;
|
||||
|
||||
static AUDIO_STREAM_SOUND m_Streams[AUDIO_MAX_ACTIVE_STREAMS] = { 0 };
|
||||
static float m_MixBuffer[AUDIO_SAMPLES * AUDIO_WORKING_CHANNELS] = { 0 };
|
||||
|
||||
static size_t m_DecodeBufferCapacity = 0;
|
||||
static float *m_DecodeBuffer = NULL;
|
||||
|
||||
static void M_SeekToStart(AUDIO_STREAM_SOUND *stream);
|
||||
static bool M_DecodeFrame(AUDIO_STREAM_SOUND *stream);
|
||||
static bool M_EnqueueFrame(AUDIO_STREAM_SOUND *stream);
|
||||
static bool M_InitialiseFromPath(int32_t sound_id, const char *file_path);
|
||||
static void M_Clear(AUDIO_STREAM_SOUND *stream);
|
||||
|
||||
static void M_SeekToStart(AUDIO_STREAM_SOUND *stream)
|
||||
{
|
||||
assert(stream != NULL);
|
||||
|
||||
stream->timestamp = stream->start_at;
|
||||
if (stream->start_at <= 0.0) {
|
||||
// reset to start of file
|
||||
avio_seek(stream->av.format_ctx->pb, 0, SEEK_SET);
|
||||
avformat_seek_file(
|
||||
stream->av.format_ctx, -1, 0, 0, 0, AVSEEK_FLAG_FRAME);
|
||||
} else {
|
||||
// seek to specific timestamp
|
||||
const double time_base_sec = av_q2d(stream->av.stream->time_base);
|
||||
av_seek_frame(
|
||||
stream->av.format_ctx, 0, stream->start_at / time_base_sec,
|
||||
AVSEEK_FLAG_ANY);
|
||||
}
|
||||
}
|
||||
|
||||
static bool M_DecodeFrame(AUDIO_STREAM_SOUND *stream)
|
||||
{
|
||||
assert(stream != NULL);
|
||||
|
||||
if (stream->stop_at > 0.0 && stream->timestamp >= stream->stop_at) {
|
||||
if (stream->is_looped) {
|
||||
M_SeekToStart(stream);
|
||||
return M_DecodeFrame(stream);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t error_code =
|
||||
av_read_frame(stream->av.format_ctx, stream->av.packet);
|
||||
|
||||
if (error_code == AVERROR_EOF && stream->is_looped) {
|
||||
M_SeekToStart(stream);
|
||||
return M_DecodeFrame(stream);
|
||||
}
|
||||
|
||||
if (error_code < 0) {
|
||||
LOG_ERROR("error while decoding audio stream: %d", error_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (stream->av.packet->stream_index != stream->av.stream->index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
error_code = avcodec_send_packet(stream->av.codec_ctx, stream->av.packet);
|
||||
if (error_code < 0) {
|
||||
av_packet_unref(stream->av.packet);
|
||||
LOG_ERROR(
|
||||
"Got an error when decoding frame: %s", av_err2str(error_code));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool M_EnqueueFrame(AUDIO_STREAM_SOUND *stream)
|
||||
{
|
||||
assert(stream != NULL);
|
||||
|
||||
int32_t error_code;
|
||||
|
||||
if (!stream->swr.ctx) {
|
||||
stream->swr.src_sample_rate = stream->av.codec_ctx->sample_rate;
|
||||
stream->swr.src_channels = stream->av.codec_ctx->channels;
|
||||
stream->swr.src_format = stream->av.codec_ctx->sample_fmt;
|
||||
stream->swr.dst_sample_rate = AUDIO_WORKING_RATE;
|
||||
stream->swr.dst_channels = AUDIO_WORKING_CHANNELS;
|
||||
stream->swr.dst_format = Audio_GetAVAudioFormat(AUDIO_WORKING_FORMAT);
|
||||
stream->swr.ctx = swr_alloc_set_opts(
|
||||
stream->swr.ctx, Audio_GetAVChannelLayout(stream->swr.dst_channels),
|
||||
stream->swr.dst_format, stream->swr.dst_sample_rate,
|
||||
Audio_GetAVChannelLayout(stream->swr.src_channels),
|
||||
stream->swr.src_format, stream->swr.src_sample_rate, 0, 0);
|
||||
if (!stream->swr.ctx) {
|
||||
av_packet_unref(stream->av.packet);
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = swr_init(stream->swr.ctx);
|
||||
if (error_code != 0) {
|
||||
av_packet_unref(stream->av.packet);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
while (1) {
|
||||
error_code =
|
||||
avcodec_receive_frame(stream->av.codec_ctx, stream->av.frame);
|
||||
if (error_code == AVERROR(EAGAIN)) {
|
||||
av_frame_unref(stream->av.frame);
|
||||
error_code = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if (error_code < 0) {
|
||||
av_frame_unref(stream->av.frame);
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t *out_buffer = NULL;
|
||||
const int32_t out_samples =
|
||||
swr_get_out_samples(stream->swr.ctx, stream->av.frame->nb_samples);
|
||||
av_samples_alloc(
|
||||
&out_buffer, NULL, stream->swr.dst_channels, out_samples,
|
||||
stream->swr.dst_format, 1);
|
||||
int32_t resampled_size = swr_convert(
|
||||
stream->swr.ctx, &out_buffer, out_samples,
|
||||
(const uint8_t **)stream->av.frame->data,
|
||||
stream->av.frame->nb_samples);
|
||||
|
||||
size_t out_pos = 0;
|
||||
while (resampled_size > 0) {
|
||||
const size_t out_buffer_size = av_samples_get_buffer_size(
|
||||
NULL, stream->swr.dst_channels, resampled_size,
|
||||
stream->swr.dst_format, 1);
|
||||
|
||||
if (out_pos + out_buffer_size > m_DecodeBufferCapacity) {
|
||||
m_DecodeBufferCapacity = out_pos + out_buffer_size;
|
||||
m_DecodeBuffer =
|
||||
Memory_Realloc(m_DecodeBuffer, m_DecodeBufferCapacity);
|
||||
}
|
||||
if (out_buffer) {
|
||||
memcpy(
|
||||
(uint8_t *)m_DecodeBuffer + out_pos, out_buffer,
|
||||
out_buffer_size);
|
||||
}
|
||||
out_pos += out_buffer_size;
|
||||
|
||||
resampled_size =
|
||||
swr_convert(stream->swr.ctx, &out_buffer, out_samples, NULL, 0);
|
||||
}
|
||||
|
||||
if (SDL_AudioStreamPut(stream->sdl.stream, m_DecodeBuffer, out_pos)) {
|
||||
LOG_ERROR("Got an error when decoding frame: %s", SDL_GetError());
|
||||
av_frame_unref(stream->av.frame);
|
||||
break;
|
||||
}
|
||||
|
||||
double time_base_sec = av_q2d(stream->av.stream->time_base);
|
||||
stream->timestamp =
|
||||
stream->av.frame->best_effort_timestamp * time_base_sec;
|
||||
av_freep(&out_buffer);
|
||||
av_frame_unref(stream->av.frame);
|
||||
}
|
||||
|
||||
av_packet_unref(stream->av.packet);
|
||||
|
||||
cleanup:
|
||||
if (error_code > 0) {
|
||||
LOG_ERROR(
|
||||
"Got an error when decoding frame: %d, %s", error_code,
|
||||
av_err2str(error_code));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool M_InitialiseFromPath(int32_t sound_id, const char *file_path)
|
||||
{
|
||||
assert(file_path != NULL);
|
||||
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ret = false;
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
int32_t error_code;
|
||||
char *full_path = File_GetFullPath(file_path);
|
||||
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
|
||||
error_code =
|
||||
avformat_open_input(&stream->av.format_ctx, full_path, NULL, NULL);
|
||||
if (error_code != 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = avformat_find_stream_info(stream->av.format_ctx, NULL);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
stream->av.stream = NULL;
|
||||
for (uint32_t i = 0; i < stream->av.format_ctx->nb_streams; i++) {
|
||||
AVStream *current_stream = stream->av.format_ctx->streams[i];
|
||||
if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
|
||||
stream->av.stream = current_stream;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!stream->av.stream) {
|
||||
error_code = AVERROR_STREAM_NOT_FOUND;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
stream->av.codec =
|
||||
avcodec_find_decoder(stream->av.stream->codecpar->codec_id);
|
||||
if (!stream->av.codec) {
|
||||
error_code = AVERROR_DEMUXER_NOT_FOUND;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
stream->av.codec_ctx = avcodec_alloc_context3(stream->av.codec);
|
||||
if (!stream->av.codec_ctx) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = avcodec_parameters_to_context(
|
||||
stream->av.codec_ctx, stream->av.stream->codecpar);
|
||||
if (error_code) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
error_code = avcodec_open2(stream->av.codec_ctx, stream->av.codec, NULL);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
stream->av.packet = av_packet_alloc();
|
||||
if (!stream->av.packet) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
stream->av.frame = av_frame_alloc();
|
||||
if (!stream->av.frame) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
M_DecodeFrame(stream);
|
||||
|
||||
int32_t sdl_sample_rate = stream->av.codec_ctx->sample_rate;
|
||||
int32_t sdl_channels = stream->av.codec_ctx->channels;
|
||||
|
||||
stream->is_read_done = false;
|
||||
stream->is_used = true;
|
||||
stream->is_playing = true;
|
||||
stream->is_looped = false;
|
||||
stream->volume = 1.0f;
|
||||
stream->timestamp = 0.0;
|
||||
stream->finish_callback = NULL;
|
||||
stream->finish_callback_user_data = NULL;
|
||||
stream->duration =
|
||||
(double)stream->av.format_ctx->duration / (double)AV_TIME_BASE;
|
||||
stream->start_at = -1.0; // negative value means unset
|
||||
stream->stop_at = -1.0; // negative value means unset
|
||||
|
||||
stream->sdl.stream = SDL_NewAudioStream(
|
||||
AUDIO_WORKING_FORMAT, sdl_channels, AUDIO_WORKING_RATE,
|
||||
AUDIO_WORKING_FORMAT, sdl_channels, AUDIO_WORKING_RATE);
|
||||
if (!stream->sdl.stream) {
|
||||
LOG_ERROR("Failed to create SDL stream: %s", SDL_GetError());
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
M_EnqueueFrame(stream);
|
||||
|
||||
cleanup:
|
||||
if (error_code) {
|
||||
LOG_ERROR(
|
||||
"Error while opening audio %s: %s", file_path,
|
||||
av_err2str(error_code));
|
||||
}
|
||||
|
||||
if (!ret) {
|
||||
Audio_Stream_Close(sound_id);
|
||||
}
|
||||
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
Memory_FreePointer(&full_path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static void M_Clear(AUDIO_STREAM_SOUND *stream)
|
||||
{
|
||||
assert(stream != NULL);
|
||||
|
||||
stream->is_used = false;
|
||||
stream->is_playing = false;
|
||||
stream->is_read_done = true;
|
||||
stream->is_looped = false;
|
||||
stream->volume = 0.0f;
|
||||
stream->duration = 0.0;
|
||||
stream->timestamp = 0.0;
|
||||
stream->sdl.stream = NULL;
|
||||
stream->finish_callback = NULL;
|
||||
stream->finish_callback_user_data = NULL;
|
||||
}
|
||||
|
||||
void Audio_Stream_Init(void)
|
||||
{
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;
|
||||
sound_id++) {
|
||||
M_Clear(&m_Streams[sound_id]);
|
||||
}
|
||||
}
|
||||
|
||||
void Audio_Stream_Shutdown(void)
|
||||
{
|
||||
Memory_FreePointer(&m_DecodeBuffer);
|
||||
if (!g_AudioDeviceID) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;
|
||||
sound_id++) {
|
||||
if (m_Streams[sound_id].is_used) {
|
||||
Audio_Stream_Close(sound_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Audio_Stream_Pause(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Streams[sound_id].is_playing) {
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Streams[sound_id].is_playing = false;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Stream_Unpause(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_Streams[sound_id].is_playing) {
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
m_Streams[sound_id].is_playing = true;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
int32_t Audio_Stream_CreateFromFile(const char *file_path)
|
||||
{
|
||||
if (!g_AudioDeviceID) {
|
||||
return AUDIO_NO_SOUND;
|
||||
}
|
||||
|
||||
assert(file_path != NULL);
|
||||
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;
|
||||
sound_id++) {
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
if (stream->is_used) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!M_InitialiseFromPath(sound_id, file_path)) {
|
||||
return AUDIO_NO_SOUND;
|
||||
}
|
||||
|
||||
return sound_id;
|
||||
}
|
||||
|
||||
return AUDIO_NO_SOUND;
|
||||
}
|
||||
|
||||
bool Audio_Stream_Close(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
|
||||
if (stream->av.codec_ctx) {
|
||||
avcodec_close(stream->av.codec_ctx);
|
||||
av_free(stream->av.codec_ctx);
|
||||
stream->av.codec_ctx = NULL;
|
||||
}
|
||||
|
||||
if (stream->av.format_ctx) {
|
||||
avformat_close_input(&stream->av.format_ctx);
|
||||
stream->av.format_ctx = NULL;
|
||||
}
|
||||
|
||||
if (stream->swr.ctx) {
|
||||
swr_free(&stream->swr.ctx);
|
||||
}
|
||||
|
||||
if (stream->av.frame) {
|
||||
av_frame_free(&stream->av.frame);
|
||||
stream->av.frame = NULL;
|
||||
}
|
||||
|
||||
if (stream->av.packet) {
|
||||
av_packet_free(&stream->av.packet);
|
||||
stream->av.packet = NULL;
|
||||
}
|
||||
|
||||
stream->av.stream = NULL;
|
||||
stream->av.codec = NULL;
|
||||
|
||||
if (stream->sdl.stream) {
|
||||
SDL_FreeAudioStream(stream->sdl.stream);
|
||||
}
|
||||
|
||||
void (*finish_callback)(int32_t, void *) = stream->finish_callback;
|
||||
void *finish_callback_user_data = stream->finish_callback_user_data;
|
||||
|
||||
M_Clear(stream);
|
||||
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
|
||||
if (finish_callback) {
|
||||
finish_callback(sound_id, finish_callback_user_data);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Stream_SetVolume(int32_t sound_id, float volume)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Streams[sound_id].volume = volume;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Stream_IsLooped(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return m_Streams[sound_id].is_looped;
|
||||
}
|
||||
|
||||
bool Audio_Stream_SetIsLooped(int32_t sound_id, bool is_looped)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Streams[sound_id].is_looped = is_looped;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Stream_SetFinishCallback(
|
||||
int32_t sound_id, void (*callback)(int32_t sound_id, void *user_data),
|
||||
void *user_data)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Streams[sound_id].finish_callback = callback;
|
||||
m_Streams[sound_id].finish_callback_user_data = user_data;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Audio_Stream_Mix(float *dst_buffer, size_t len)
|
||||
{
|
||||
for (int32_t sound_id = 0; sound_id < AUDIO_MAX_ACTIVE_STREAMS;
|
||||
sound_id++) {
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
if (!stream->is_playing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while ((SDL_AudioStreamAvailable(stream->sdl.stream) < (int32_t)len)
|
||||
&& !stream->is_read_done) {
|
||||
if (M_DecodeFrame(stream)) {
|
||||
M_EnqueueFrame(stream);
|
||||
} else {
|
||||
stream->is_read_done = true;
|
||||
}
|
||||
}
|
||||
|
||||
memset(m_MixBuffer, 0, READ_BUFFER_SIZE);
|
||||
int32_t bytes_gotten = SDL_AudioStreamGet(
|
||||
stream->sdl.stream, m_MixBuffer, READ_BUFFER_SIZE);
|
||||
if (bytes_gotten < 0) {
|
||||
LOG_ERROR("Error reading from sdl.stream: %s", SDL_GetError());
|
||||
stream->is_playing = false;
|
||||
stream->is_used = false;
|
||||
stream->is_read_done = true;
|
||||
} else if (bytes_gotten == 0) {
|
||||
// legit end of stream. looping is handled in
|
||||
// M_DecodeFrame
|
||||
stream->is_playing = false;
|
||||
stream->is_used = false;
|
||||
stream->is_read_done = true;
|
||||
} else {
|
||||
int32_t samples_gotten = bytes_gotten
|
||||
/ (AUDIO_WORKING_CHANNELS * sizeof(AUDIO_WORKING_FORMAT));
|
||||
|
||||
const float *src_ptr = &m_MixBuffer[0];
|
||||
float *dst_ptr = dst_buffer;
|
||||
|
||||
if (stream->av.codec_ctx->channels == AUDIO_WORKING_CHANNELS) {
|
||||
for (int32_t s = 0; s < samples_gotten; s++) {
|
||||
for (int32_t c = 0; c < AUDIO_WORKING_CHANNELS; c++) {
|
||||
*dst_ptr++ += *src_ptr++ * stream->volume;
|
||||
}
|
||||
}
|
||||
} else if (stream->av.codec_ctx->channels == 1) {
|
||||
for (int32_t s = 0; s < samples_gotten; s++) {
|
||||
for (int32_t c = 0; c < AUDIO_WORKING_CHANNELS; c++) {
|
||||
*dst_ptr++ += *src_ptr * stream->volume;
|
||||
}
|
||||
src_ptr++;
|
||||
}
|
||||
} else {
|
||||
for (int32_t s = 0; s < samples_gotten; s++) {
|
||||
// downmix to mono
|
||||
float src_sample = 0.0f;
|
||||
for (int32_t i = 0; i < stream->av.codec_ctx->channels;
|
||||
i++) {
|
||||
src_sample += *src_ptr++;
|
||||
}
|
||||
src_sample /= (float)stream->av.codec_ctx->channels;
|
||||
for (int32_t c = 0; c < AUDIO_WORKING_CHANNELS; c++) {
|
||||
*dst_ptr++ += src_sample * stream->volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stream->is_used) {
|
||||
Audio_Stream_Close(sound_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double Audio_Stream_GetTimestamp(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
double timestamp = -1.0;
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
|
||||
if (stream->duration > 0.0) {
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
timestamp = stream->timestamp;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
}
|
||||
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
double Audio_Stream_GetDuration(int32_t sound_id)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
double duration = stream->duration;
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
return duration;
|
||||
}
|
||||
|
||||
bool Audio_Stream_SeekTimestamp(int32_t sound_id, double timestamp)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_Streams[sound_id].is_playing) {
|
||||
SDL_LockAudioDevice(g_AudioDeviceID);
|
||||
AUDIO_STREAM_SOUND *stream = &m_Streams[sound_id];
|
||||
const double time_base_sec = av_q2d(stream->av.stream->time_base);
|
||||
av_seek_frame(
|
||||
stream->av.format_ctx, 0, timestamp / time_base_sec,
|
||||
AVSEEK_FLAG_ANY);
|
||||
SDL_UnlockAudioDevice(g_AudioDeviceID);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Streams[sound_id].start_at = timestamp;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Audio_Stream_SetStopTimestamp(int32_t sound_id, double timestamp)
|
||||
{
|
||||
if (!g_AudioDeviceID || sound_id < 0
|
||||
|| sound_id >= AUDIO_MAX_ACTIVE_STREAMS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_Streams[sound_id].stop_at = timestamp;
|
||||
return true;
|
||||
}
|
560
src/libtrx/engine/image.c
Normal file
560
src/libtrx/engine/image.c
Normal file
|
@ -0,0 +1,560 @@
|
|||
#include "engine/image.h"
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavcodec/codec.h>
|
||||
#include <libavcodec/codec_id.h>
|
||||
#include <libavcodec/packet.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/avutil.h>
|
||||
#include <libavutil/error.h>
|
||||
#include <libavutil/frame.h>
|
||||
#include <libavutil/imgutils.h>
|
||||
#include <libavutil/mem.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
#include <libavutil/rational.h>
|
||||
#include <libswscale/swscale.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
struct {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
} src, dst;
|
||||
} IMAGE_BLIT;
|
||||
|
||||
typedef struct {
|
||||
int error_code;
|
||||
AVFormatContext *format_ctx;
|
||||
AVCodecContext *codec_ctx;
|
||||
const AVCodec *codec;
|
||||
AVFrame *frame;
|
||||
AVPacket *packet;
|
||||
} IMAGE_READER_CONTEXT;
|
||||
|
||||
static bool M_Init(const char *path, IMAGE_READER_CONTEXT *ctx);
|
||||
static void M_Free(IMAGE_READER_CONTEXT *ctx);
|
||||
static IMAGE *M_ConstructImage(
|
||||
IMAGE_READER_CONTEXT *ctx, int32_t target_width, int32_t target_height,
|
||||
IMAGE_FIT_MODE fit_mode);
|
||||
static IMAGE_BLIT M_GetBlit(
|
||||
int32_t source_width, int32_t source_height, int32_t target_width,
|
||||
int32_t target_height, IMAGE_FIT_MODE fit_mode);
|
||||
|
||||
static bool M_Init(const char *const path, IMAGE_READER_CONTEXT *const ctx)
|
||||
{
|
||||
assert(ctx != NULL);
|
||||
ctx->format_ctx = NULL;
|
||||
ctx->codec = NULL;
|
||||
ctx->codec_ctx = NULL;
|
||||
ctx->frame = NULL;
|
||||
ctx->packet = NULL;
|
||||
|
||||
char *full_path = File_GetFullPath(path);
|
||||
int32_t error_code =
|
||||
avformat_open_input(&ctx->format_ctx, full_path, NULL, NULL);
|
||||
Memory_FreePointer(&full_path);
|
||||
|
||||
if (error_code != 0) {
|
||||
goto finish;
|
||||
}
|
||||
|
||||
#if 0
|
||||
error_code = avformat_find_stream_info(format_ctx, NULL);
|
||||
if (error_code < 0) {
|
||||
goto finish;
|
||||
}
|
||||
#endif
|
||||
|
||||
AVStream *video_stream = NULL;
|
||||
for (unsigned int i = 0; i < ctx->format_ctx->nb_streams; i++) {
|
||||
AVStream *current_stream = ctx->format_ctx->streams[i];
|
||||
if (current_stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
|
||||
video_stream = current_stream;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (video_stream == NULL) {
|
||||
error_code = AVERROR_STREAM_NOT_FOUND;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
ctx->codec = avcodec_find_decoder(video_stream->codecpar->codec_id);
|
||||
if (ctx->codec == NULL) {
|
||||
error_code = AVERROR_DEMUXER_NOT_FOUND;
|
||||
goto finish;
|
||||
}
|
||||
|
||||
ctx->codec_ctx = avcodec_alloc_context3(ctx->codec);
|
||||
if (ctx->codec_ctx == NULL) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
error_code =
|
||||
avcodec_parameters_to_context(ctx->codec_ctx, video_stream->codecpar);
|
||||
if (error_code) {
|
||||
goto finish;
|
||||
}
|
||||
|
||||
#if 0
|
||||
ctx->codec_ctx->thread_count = 0;
|
||||
if (ctx->codec->capabilities & AV_CODEC_CAP_FRAME_THREADS)
|
||||
ctx->codec_ctx->thread_type = FF_THREAD_FRAME;
|
||||
else if (ctx->codec->capabilities & AV_CODEC_CAP_SLICE_THREADS)
|
||||
ctx->codec_ctx->thread_type = FF_THREAD_SLICE;
|
||||
else
|
||||
ctx->codec_ctx->thread_count = 1; //don't use multithreading
|
||||
#endif
|
||||
|
||||
error_code = avcodec_open2(ctx->codec_ctx, ctx->codec, NULL);
|
||||
if (error_code < 0) {
|
||||
goto finish;
|
||||
}
|
||||
|
||||
ctx->packet = av_packet_alloc();
|
||||
av_new_packet(ctx->packet, 0);
|
||||
error_code = av_read_frame(ctx->format_ctx, ctx->packet);
|
||||
if (error_code < 0) {
|
||||
goto finish;
|
||||
}
|
||||
|
||||
error_code = avcodec_send_packet(ctx->codec_ctx, ctx->packet);
|
||||
if (error_code < 0) {
|
||||
goto finish;
|
||||
}
|
||||
|
||||
ctx->frame = av_frame_alloc();
|
||||
if (ctx->frame == NULL) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto finish;
|
||||
}
|
||||
|
||||
error_code = avcodec_receive_frame(ctx->codec_ctx, ctx->frame);
|
||||
if (error_code < 0) {
|
||||
goto finish;
|
||||
}
|
||||
error_code = 0;
|
||||
|
||||
finish:
|
||||
if (error_code != 0) {
|
||||
LOG_ERROR(
|
||||
"Error while opening image %s: %s", path, av_err2str(error_code));
|
||||
M_Free(ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void M_Free(IMAGE_READER_CONTEXT *const ctx)
|
||||
{
|
||||
if (ctx->packet != NULL) {
|
||||
av_packet_free(&ctx->packet);
|
||||
}
|
||||
|
||||
if (ctx->frame != NULL) {
|
||||
av_frame_free(&ctx->frame);
|
||||
}
|
||||
|
||||
if (ctx->codec_ctx != NULL) {
|
||||
avcodec_close(ctx->codec_ctx);
|
||||
av_free(ctx->codec_ctx);
|
||||
ctx->codec_ctx = NULL;
|
||||
}
|
||||
|
||||
if (ctx->format_ctx != NULL) {
|
||||
avformat_close_input(&ctx->format_ctx);
|
||||
}
|
||||
}
|
||||
|
||||
IMAGE *Image_Create(const int width, const int height)
|
||||
{
|
||||
IMAGE *image = Memory_Alloc(sizeof(IMAGE));
|
||||
image->width = width;
|
||||
image->height = height;
|
||||
image->data = Memory_Alloc(width * height * sizeof(IMAGE_PIXEL));
|
||||
return image;
|
||||
}
|
||||
|
||||
static IMAGE *M_ConstructImage(
|
||||
IMAGE_READER_CONTEXT *const ctx, const int32_t target_width,
|
||||
const int32_t target_height, IMAGE_FIT_MODE fit_mode)
|
||||
{
|
||||
assert(ctx != NULL);
|
||||
assert(target_width > 0);
|
||||
assert(target_height > 0);
|
||||
|
||||
IMAGE_BLIT blit = M_GetBlit(
|
||||
ctx->frame->width, ctx->frame->height, target_width, target_height,
|
||||
fit_mode);
|
||||
|
||||
if (blit.src.y != 0 || blit.src.x != 0) {
|
||||
ctx->frame->crop_top = blit.src.y;
|
||||
ctx->frame->crop_left = blit.src.x;
|
||||
av_frame_apply_cropping(ctx->frame, AV_FRAME_CROP_UNALIGNED);
|
||||
}
|
||||
|
||||
struct SwsContext *const sws_ctx = sws_getContext(
|
||||
blit.src.width, blit.src.height, ctx->frame->format, blit.dst.width,
|
||||
blit.dst.height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
|
||||
if (sws_ctx == NULL) {
|
||||
LOG_ERROR("Failed to get SWS context");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
IMAGE *const target_image = Image_Create(target_width, target_height);
|
||||
|
||||
uint8_t *dst_planes[4] = { (uint8_t *)target_image->data
|
||||
+ (blit.dst.y * target_image->width
|
||||
+ blit.dst.x)
|
||||
* sizeof(IMAGE_PIXEL),
|
||||
NULL, NULL, NULL };
|
||||
int dst_linesize[4] = { target_image->width * sizeof(IMAGE_PIXEL), 0, 0,
|
||||
0 };
|
||||
|
||||
sws_scale(
|
||||
sws_ctx, (const uint8_t *const *)ctx->frame->data, ctx->frame->linesize,
|
||||
0, blit.src.height, dst_planes, dst_linesize);
|
||||
|
||||
sws_freeContext(sws_ctx);
|
||||
return target_image;
|
||||
}
|
||||
|
||||
static IMAGE_BLIT M_GetBlit(
|
||||
const int32_t source_width, const int32_t source_height,
|
||||
const int32_t target_width, const int32_t target_height,
|
||||
IMAGE_FIT_MODE fit_mode)
|
||||
{
|
||||
const float source_ratio = source_width / (float)source_height;
|
||||
const float target_ratio = target_width / (float)target_height;
|
||||
|
||||
if (fit_mode == IMAGE_FIT_SMART) {
|
||||
const float ar_diff =
|
||||
(source_ratio > target_ratio ? source_ratio / target_ratio
|
||||
: target_ratio / source_ratio)
|
||||
- 1.0f;
|
||||
if (ar_diff <= 0.1f) {
|
||||
// if the difference between aspect ratios is under 10%, just
|
||||
// stretch it
|
||||
fit_mode = IMAGE_FIT_STRETCH;
|
||||
} else if (source_ratio <= target_ratio) {
|
||||
// if the viewport is too wide, center the image
|
||||
fit_mode = IMAGE_FIT_LETTERBOX;
|
||||
} else {
|
||||
// if the image is too wide, crop the image
|
||||
fit_mode = IMAGE_FIT_CROP;
|
||||
}
|
||||
}
|
||||
|
||||
IMAGE_BLIT blit;
|
||||
|
||||
switch (fit_mode) {
|
||||
case IMAGE_FIT_STRETCH:
|
||||
blit.src.width = source_width;
|
||||
blit.src.height = source_height;
|
||||
blit.src.x = 0;
|
||||
blit.src.y = 0;
|
||||
|
||||
blit.dst.width = target_width;
|
||||
blit.dst.height = target_height;
|
||||
blit.dst.x = 0;
|
||||
blit.dst.y = 0;
|
||||
break;
|
||||
|
||||
case IMAGE_FIT_CROP:
|
||||
blit.src.width = source_ratio < target_ratio
|
||||
? source_width
|
||||
: source_height * target_ratio;
|
||||
blit.src.height = source_ratio < target_ratio
|
||||
? source_width / target_ratio
|
||||
: source_height;
|
||||
blit.src.x = (source_width - blit.src.width) / 2;
|
||||
blit.src.y = (source_height - blit.src.height) / 2;
|
||||
|
||||
blit.dst.width = target_width;
|
||||
blit.dst.height = target_height;
|
||||
blit.dst.x = 0;
|
||||
blit.dst.y = 0;
|
||||
break;
|
||||
|
||||
case IMAGE_FIT_LETTERBOX:
|
||||
blit.src.width = source_width;
|
||||
blit.src.height = source_height;
|
||||
blit.src.x = 0;
|
||||
blit.src.y = 0;
|
||||
|
||||
blit.dst.width = (source_ratio > target_ratio)
|
||||
? target_width
|
||||
: target_height * source_ratio;
|
||||
blit.dst.height = (source_ratio > target_ratio)
|
||||
? target_width / source_ratio
|
||||
: target_height;
|
||||
blit.dst.x = (target_width - blit.dst.width) / 2;
|
||||
blit.dst.y = (target_height - blit.dst.height) / 2;
|
||||
break;
|
||||
|
||||
default:
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
return blit;
|
||||
}
|
||||
|
||||
IMAGE *Image_CreateFromFile(const char *const path)
|
||||
{
|
||||
assert(path != NULL);
|
||||
|
||||
IMAGE_READER_CONTEXT ctx;
|
||||
if (!M_Init(path, &ctx)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
IMAGE *target_image = M_ConstructImage(
|
||||
&ctx, ctx.frame->width, ctx.frame->height, IMAGE_FIT_STRETCH);
|
||||
|
||||
M_Free(&ctx);
|
||||
|
||||
return target_image;
|
||||
}
|
||||
|
||||
IMAGE *Image_CreateFromFileInto(
|
||||
const char *const path, const int32_t target_width,
|
||||
const int32_t target_height, const IMAGE_FIT_MODE fit_mode)
|
||||
{
|
||||
assert(path != NULL);
|
||||
|
||||
IMAGE_READER_CONTEXT ctx;
|
||||
if (!M_Init(path, &ctx)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
IMAGE *target_image =
|
||||
M_ConstructImage(&ctx, target_width, target_height, fit_mode);
|
||||
|
||||
M_Free(&ctx);
|
||||
|
||||
return target_image;
|
||||
}
|
||||
|
||||
bool Image_SaveToFile(const IMAGE *const image, const char *const path)
|
||||
{
|
||||
assert(image);
|
||||
assert(path);
|
||||
|
||||
bool result = false;
|
||||
|
||||
int error_code = 0;
|
||||
const AVCodec *codec = NULL;
|
||||
AVCodecContext *codec_ctx = NULL;
|
||||
AVFrame *frame = NULL;
|
||||
AVPacket *packet = NULL;
|
||||
struct SwsContext *sws_ctx = NULL;
|
||||
MYFILE *fp = NULL;
|
||||
|
||||
enum AVPixelFormat src_pix_fmt = AV_PIX_FMT_RGB24;
|
||||
enum AVPixelFormat dst_pix_fmt;
|
||||
enum AVCodecID codec_id;
|
||||
|
||||
if (strstr(path, ".jpg")) {
|
||||
dst_pix_fmt = AV_PIX_FMT_YUVJ420P;
|
||||
codec_id = AV_CODEC_ID_MJPEG;
|
||||
} else if (strstr(path, ".png")) {
|
||||
dst_pix_fmt = AV_PIX_FMT_RGB24;
|
||||
codec_id = AV_CODEC_ID_PNG;
|
||||
} else {
|
||||
LOG_ERROR("Cannot determine image format based on path '%s'", path);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
fp = File_Open(path, FILE_OPEN_WRITE);
|
||||
if (fp == NULL) {
|
||||
LOG_ERROR("Cannot create image file: %s", path);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
codec = avcodec_find_encoder(codec_id);
|
||||
if (codec == NULL) {
|
||||
error_code = AVERROR_MUXER_NOT_FOUND;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (codec_ctx == NULL) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
codec_ctx->bit_rate = 400000;
|
||||
codec_ctx->width = image->width;
|
||||
codec_ctx->height = image->height;
|
||||
codec_ctx->time_base = (AVRational) { 1, 25 };
|
||||
codec_ctx->pix_fmt = dst_pix_fmt;
|
||||
|
||||
if (codec_id == AV_CODEC_ID_MJPEG) {
|
||||
// 9 JPEG quality
|
||||
codec_ctx->flags |= AV_CODEC_FLAG_QSCALE;
|
||||
codec_ctx->global_quality = FF_QP2LAMBDA * 9;
|
||||
}
|
||||
|
||||
error_code = avcodec_open2(codec_ctx, codec, NULL);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
frame = av_frame_alloc();
|
||||
if (frame == NULL) {
|
||||
error_code = AVERROR(ENOMEM);
|
||||
goto cleanup;
|
||||
}
|
||||
frame->format = codec_ctx->pix_fmt;
|
||||
frame->width = codec_ctx->width;
|
||||
frame->height = codec_ctx->height;
|
||||
frame->pts = 0;
|
||||
|
||||
error_code = av_image_alloc(
|
||||
frame->data, frame->linesize, codec_ctx->width, codec_ctx->height,
|
||||
codec_ctx->pix_fmt, 32);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
packet = av_packet_alloc();
|
||||
av_new_packet(packet, 0);
|
||||
|
||||
sws_ctx = sws_getContext(
|
||||
image->width, image->height, src_pix_fmt, frame->width, frame->height,
|
||||
dst_pix_fmt, SWS_BILINEAR, NULL, NULL, NULL);
|
||||
|
||||
if (sws_ctx == NULL) {
|
||||
LOG_ERROR("Failed to get SWS context");
|
||||
error_code = AVERROR_EXTERNAL;
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
uint8_t *src_planes[4];
|
||||
int src_linesize[4];
|
||||
av_image_fill_arrays(
|
||||
src_planes, src_linesize, (const uint8_t *)image->data, src_pix_fmt,
|
||||
image->width, image->height, 1);
|
||||
|
||||
sws_scale(
|
||||
sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,
|
||||
image->height, frame->data, frame->linesize);
|
||||
|
||||
error_code = avcodec_send_frame(codec_ctx, frame);
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
while (error_code >= 0) {
|
||||
error_code = avcodec_receive_packet(codec_ctx, packet);
|
||||
if (error_code == AVERROR(EAGAIN) || error_code == AVERROR_EOF) {
|
||||
error_code = 0;
|
||||
break;
|
||||
}
|
||||
if (error_code < 0) {
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
File_WriteData(fp, packet->data, packet->size);
|
||||
av_packet_unref(packet);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (error_code) {
|
||||
LOG_ERROR(
|
||||
"Error while saving image %s: %s", path, av_err2str(error_code));
|
||||
} else {
|
||||
result = true;
|
||||
}
|
||||
|
||||
if (fp) {
|
||||
File_Close(fp);
|
||||
fp = NULL;
|
||||
}
|
||||
|
||||
if (sws_ctx) {
|
||||
sws_freeContext(sws_ctx);
|
||||
}
|
||||
|
||||
if (packet) {
|
||||
av_packet_free(&packet);
|
||||
}
|
||||
|
||||
if (codec) {
|
||||
avcodec_close(codec_ctx);
|
||||
av_free(codec_ctx);
|
||||
codec_ctx = NULL;
|
||||
}
|
||||
|
||||
if (frame) {
|
||||
av_freep(&frame->data[0]);
|
||||
av_frame_free(&frame);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
IMAGE *Image_Scale(
|
||||
const IMAGE *const source_image, size_t target_width, size_t target_height,
|
||||
IMAGE_FIT_MODE fit_mode)
|
||||
{
|
||||
assert(source_image);
|
||||
assert(source_image->data);
|
||||
assert(target_width > 0);
|
||||
assert(target_height > 0);
|
||||
|
||||
IMAGE_BLIT blit = M_GetBlit(
|
||||
source_image->width, source_image->height, target_width, target_height,
|
||||
fit_mode);
|
||||
|
||||
struct SwsContext *const sws_ctx = sws_getContext(
|
||||
blit.src.width, blit.src.height, AV_PIX_FMT_RGB24, blit.dst.width,
|
||||
blit.dst.height, AV_PIX_FMT_RGB24, SWS_BILINEAR, NULL, NULL, NULL);
|
||||
if (sws_ctx == NULL) {
|
||||
LOG_ERROR("Failed to get SWS context");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
IMAGE *const target_image = Image_Create(target_width, target_height);
|
||||
|
||||
uint8_t *src_planes[4] = { (uint8_t *)source_image->data
|
||||
+ (blit.src.y * source_image->width
|
||||
+ blit.src.x)
|
||||
* sizeof(IMAGE_PIXEL),
|
||||
NULL, NULL, NULL };
|
||||
int src_linesize[4] = { source_image->width * sizeof(IMAGE_PIXEL), 0, 0,
|
||||
0 };
|
||||
|
||||
uint8_t *dst_planes[4] = { (uint8_t *)target_image->data
|
||||
+ (blit.dst.y * target_image->width
|
||||
+ blit.dst.x)
|
||||
* sizeof(IMAGE_PIXEL),
|
||||
NULL, NULL, NULL };
|
||||
int dst_linesize[4] = { target_image->width * sizeof(IMAGE_PIXEL), 0, 0,
|
||||
0 };
|
||||
|
||||
sws_scale(
|
||||
sws_ctx, (const uint8_t *const *)src_planes, src_linesize, 0,
|
||||
blit.src.height, (uint8_t *const *)dst_planes, dst_linesize);
|
||||
|
||||
sws_freeContext(sws_ctx);
|
||||
return target_image;
|
||||
}
|
||||
|
||||
void Image_Free(IMAGE *image)
|
||||
{
|
||||
if (image) {
|
||||
Memory_FreePointer(&image->data);
|
||||
}
|
||||
Memory_FreePointer(&image);
|
||||
}
|
99
src/libtrx/enum_map.c
Normal file
99
src/libtrx/enum_map.c
Normal file
|
@ -0,0 +1,99 @@
|
|||
#include "enum_map.h"
|
||||
|
||||
#include "memory.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <uthash.h>
|
||||
|
||||
typedef struct {
|
||||
char *key;
|
||||
int32_t value;
|
||||
UT_hash_handle hh;
|
||||
} M_ENTRY;
|
||||
|
||||
typedef struct {
|
||||
char *key;
|
||||
char *str_value;
|
||||
UT_hash_handle hh;
|
||||
} M_INVERSE_ENTRY;
|
||||
|
||||
static M_ENTRY *m_Map = NULL;
|
||||
static M_INVERSE_ENTRY *m_InverseMap = NULL;
|
||||
|
||||
void EnumMap_Define(
|
||||
const char *const enum_name, const int32_t enum_value,
|
||||
const char *const str_value)
|
||||
{
|
||||
{
|
||||
const size_t key_len = strlen(enum_name) + strlen(str_value) + 2;
|
||||
char *const key = Memory_Alloc(key_len);
|
||||
snprintf(key, key_len, "%s|%s", enum_name, str_value);
|
||||
|
||||
M_ENTRY *const entry = Memory_Alloc(sizeof(M_ENTRY));
|
||||
entry->key = key;
|
||||
entry->value = enum_value;
|
||||
HASH_ADD_KEYPTR(hh, m_Map, entry->key, strlen(entry->key), entry);
|
||||
}
|
||||
|
||||
{
|
||||
const size_t key_len =
|
||||
snprintf(NULL, 0, "%s|%d", enum_name, enum_value) + 1;
|
||||
char *const key = Memory_Alloc(key_len);
|
||||
snprintf(key, key_len, "%s|%d", enum_name, enum_value);
|
||||
|
||||
M_INVERSE_ENTRY *const entry = Memory_Alloc(sizeof(M_INVERSE_ENTRY));
|
||||
entry->key = key;
|
||||
entry->str_value = Memory_DupStr(str_value);
|
||||
HASH_ADD_KEYPTR(
|
||||
hh, m_InverseMap, entry->key, strlen(entry->key), entry);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t EnumMap_Get(
|
||||
const char *const enum_name, const char *const str_value,
|
||||
int32_t default_value)
|
||||
{
|
||||
size_t key_len = strlen(enum_name) + strlen(str_value) + 2;
|
||||
char key[key_len];
|
||||
snprintf(key, key_len, "%s|%s", enum_name, str_value);
|
||||
|
||||
M_ENTRY *entry;
|
||||
HASH_FIND_STR(m_Map, key, entry);
|
||||
return entry != NULL ? entry->value : default_value;
|
||||
}
|
||||
|
||||
const char *EnumMap_ToString(
|
||||
const char *const enum_name, const int32_t enum_value)
|
||||
{
|
||||
size_t key_len = snprintf(NULL, 0, "%s|%d", enum_name, enum_value) + 1;
|
||||
char key[key_len];
|
||||
snprintf(key, key_len, "%s|%d", enum_name, enum_value);
|
||||
|
||||
M_INVERSE_ENTRY *entry;
|
||||
HASH_FIND_STR(m_InverseMap, key, entry);
|
||||
return entry != NULL ? entry->str_value : NULL;
|
||||
}
|
||||
|
||||
void EnumMap_Shutdown(void)
|
||||
{
|
||||
{
|
||||
M_ENTRY *current, *tmp;
|
||||
HASH_ITER(hh, m_Map, current, tmp)
|
||||
{
|
||||
HASH_DEL(m_Map, current);
|
||||
Memory_Free(current->key);
|
||||
Memory_Free(current);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
M_INVERSE_ENTRY *current, *tmp;
|
||||
HASH_ITER(hh, m_InverseMap, current, tmp)
|
||||
{
|
||||
HASH_DEL(m_InverseMap, current);
|
||||
Memory_Free(current->str_value);
|
||||
Memory_Free(current->key);
|
||||
Memory_Free(current);
|
||||
}
|
||||
}
|
||||
}
|
73
src/libtrx/event_manager.c
Normal file
73
src/libtrx/event_manager.c
Normal file
|
@ -0,0 +1,73 @@
|
|||
#include "event_manager.h"
|
||||
|
||||
#include "memory.h"
|
||||
#include "vector.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
int32_t listener_id;
|
||||
const char *event_name;
|
||||
const void *sender;
|
||||
EVENT_LISTENER listener;
|
||||
void *user_data;
|
||||
} M_LISTENER;
|
||||
|
||||
typedef struct EVENT_MANAGER {
|
||||
VECTOR *listeners;
|
||||
int32_t listener_id;
|
||||
} EVENT_MANAGER;
|
||||
|
||||
EVENT_MANAGER *EventManager_Create(void)
|
||||
{
|
||||
EVENT_MANAGER *manager = Memory_Alloc(sizeof(EVENT_MANAGER));
|
||||
manager->listeners = Vector_Create(sizeof(M_LISTENER));
|
||||
manager->listener_id = 0;
|
||||
return manager;
|
||||
}
|
||||
|
||||
void EventManager_Free(EVENT_MANAGER *const manager)
|
||||
{
|
||||
Vector_Free(manager->listeners);
|
||||
Memory_Free(manager);
|
||||
}
|
||||
|
||||
int32_t EventManager_Subscribe(
|
||||
EVENT_MANAGER *const manager, const char *const event_name,
|
||||
const void *const sender, const EVENT_LISTENER listener,
|
||||
void *const user_data)
|
||||
{
|
||||
M_LISTENER entry = {
|
||||
.listener_id = manager->listener_id++,
|
||||
.event_name = event_name,
|
||||
.sender = sender,
|
||||
.listener = listener,
|
||||
.user_data = user_data,
|
||||
};
|
||||
Vector_Add(manager->listeners, &entry);
|
||||
return entry.listener_id;
|
||||
}
|
||||
|
||||
void EventManager_Unsubscribe(
|
||||
EVENT_MANAGER *const manager, const int32_t listener_id)
|
||||
{
|
||||
for (int32_t i = 0; i < manager->listeners->count; i++) {
|
||||
M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i);
|
||||
if (entry.listener_id == listener_id) {
|
||||
Vector_RemoveAt(manager->listeners, i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void EventManager_Fire(EVENT_MANAGER *const manager, const EVENT *const event)
|
||||
{
|
||||
for (int32_t i = 0; i < manager->listeners->count; i++) {
|
||||
M_LISTENER entry = *(M_LISTENER *)Vector_Get(manager->listeners, i);
|
||||
if (strcmp(entry.event_name, event->name) == 0
|
||||
&& entry.sender == event->sender) {
|
||||
entry.listener(event, entry.user_data);
|
||||
}
|
||||
}
|
||||
}
|
436
src/libtrx/filesystem.c
Normal file
436
src/libtrx/filesystem.c
Normal file
|
@ -0,0 +1,436 @@
|
|||
#include "filesystem.h"
|
||||
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <SDL2/SDL_filesystem.h>
|
||||
#include <assert.h>
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#define PATH_SEPARATOR "\\"
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#define PATH_SEPARATOR "/"
|
||||
#endif
|
||||
|
||||
struct MYFILE {
|
||||
FILE *fp;
|
||||
const char *path;
|
||||
};
|
||||
|
||||
const char *m_GameDir = NULL;
|
||||
|
||||
static void M_PathAppendSeparator(char *path);
|
||||
static void M_PathAppendPart(char *path, const char *part);
|
||||
static char *M_CasePath(char const *path);
|
||||
static bool M_ExistsRaw(const char *path);
|
||||
|
||||
static void M_PathAppendSeparator(char *path)
|
||||
{
|
||||
if (!String_EndsWith(path, PATH_SEPARATOR)) {
|
||||
strcat(path, PATH_SEPARATOR);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_PathAppendPart(char *path, const char *part)
|
||||
{
|
||||
M_PathAppendSeparator(path);
|
||||
strcat(path, part);
|
||||
}
|
||||
|
||||
static char *M_CasePath(char const *path)
|
||||
{
|
||||
assert(path);
|
||||
|
||||
char *path_copy = Memory_DupStr(path);
|
||||
if (M_ExistsRaw(path)) {
|
||||
return path_copy;
|
||||
}
|
||||
|
||||
char *path_piece = path_copy;
|
||||
char *current_path = Memory_Alloc(strlen(path) + 2);
|
||||
|
||||
if (path_copy[0] == '/') {
|
||||
strcpy(current_path, "/");
|
||||
path_piece++;
|
||||
} else if (strstr(path_copy, ":\\")) {
|
||||
strcpy(current_path, path_copy);
|
||||
strstr(current_path, ":\\")[1] = '\0';
|
||||
path_piece += 3;
|
||||
} else {
|
||||
strcpy(current_path, ".");
|
||||
}
|
||||
|
||||
while (path_piece) {
|
||||
char *delim = strpbrk(path_piece, "/\\");
|
||||
char old_delim = delim ? *delim : '\0';
|
||||
if (delim) {
|
||||
*delim = '\0';
|
||||
}
|
||||
|
||||
DIR *path_dir = opendir(current_path);
|
||||
if (!path_dir) {
|
||||
Memory_FreePointer(&path_copy);
|
||||
Memory_FreePointer(¤t_path);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct dirent *cur_file = readdir(path_dir);
|
||||
while (cur_file) {
|
||||
if (String_Equivalent(path_piece, cur_file->d_name)) {
|
||||
M_PathAppendPart(current_path, cur_file->d_name);
|
||||
break;
|
||||
}
|
||||
cur_file = readdir(path_dir);
|
||||
}
|
||||
closedir(path_dir);
|
||||
|
||||
if (!cur_file) {
|
||||
M_PathAppendPart(current_path, path_piece);
|
||||
}
|
||||
|
||||
if (delim) {
|
||||
*delim = old_delim;
|
||||
path_piece = delim + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Memory_FreePointer(&path_copy);
|
||||
|
||||
char *result;
|
||||
if (current_path[0] == '.'
|
||||
&& strcmp(current_path + 1, PATH_SEPARATOR)
|
||||
== 0) { /* strip leading ./ */
|
||||
result = Memory_DupStr(current_path + 1 + strlen(PATH_SEPARATOR));
|
||||
} else {
|
||||
result = Memory_DupStr(current_path);
|
||||
}
|
||||
Memory_FreePointer(¤t_path);
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool M_ExistsRaw(const char *path)
|
||||
{
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (fp) {
|
||||
fclose(fp);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool File_IsAbsolute(const char *path)
|
||||
{
|
||||
return path && (path[0] == '/' || strstr(path, ":\\"));
|
||||
}
|
||||
|
||||
bool File_IsRelative(const char *path)
|
||||
{
|
||||
return path && !File_IsAbsolute(path);
|
||||
}
|
||||
|
||||
const char *File_GetGameDirectory(void)
|
||||
{
|
||||
if (m_GameDir == NULL) {
|
||||
m_GameDir = SDL_GetBasePath();
|
||||
if (!m_GameDir) {
|
||||
LOG_ERROR("Can't get module handle");
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return m_GameDir;
|
||||
}
|
||||
|
||||
bool File_DirExists(const char *path)
|
||||
{
|
||||
char *full_path = File_GetFullPath(path);
|
||||
DIR *dir = opendir(path);
|
||||
Memory_FreePointer(&full_path);
|
||||
if (dir != NULL) {
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool File_Exists(const char *path)
|
||||
{
|
||||
char *full_path = File_GetFullPath(path);
|
||||
bool ret = M_ExistsRaw(full_path);
|
||||
Memory_FreePointer(&full_path);
|
||||
return ret;
|
||||
}
|
||||
|
||||
char *File_GetFullPath(const char *path)
|
||||
{
|
||||
char *full_path = NULL;
|
||||
if (File_IsRelative(path)) {
|
||||
const char *game_dir = File_GetGameDirectory();
|
||||
if (game_dir) {
|
||||
full_path = Memory_Alloc(strlen(game_dir) + strlen(path) + 1);
|
||||
sprintf(full_path, "%s%s", game_dir, path);
|
||||
}
|
||||
}
|
||||
if (!full_path) {
|
||||
full_path = Memory_DupStr(path);
|
||||
}
|
||||
|
||||
char *case_path = M_CasePath(full_path);
|
||||
if (case_path) {
|
||||
Memory_FreePointer(&full_path);
|
||||
return case_path;
|
||||
}
|
||||
|
||||
return full_path;
|
||||
}
|
||||
|
||||
char *File_GetParentDirectory(const char *path)
|
||||
{
|
||||
char *full_path = File_GetFullPath(path);
|
||||
char *const last_delim =
|
||||
MAX(strrchr(full_path, '/'), strrchr(full_path, '\\'));
|
||||
if (last_delim != NULL) {
|
||||
*last_delim = '\0';
|
||||
}
|
||||
return full_path;
|
||||
}
|
||||
|
||||
char *File_GuessExtension(const char *path, const char **extensions)
|
||||
{
|
||||
if (!File_Exists(path)) {
|
||||
const char *dot = strrchr(path, '.');
|
||||
if (dot) {
|
||||
for (const char **ext = &extensions[0]; *ext; ext++) {
|
||||
size_t out_size = dot - path + strlen(*ext) + 1;
|
||||
char *out = Memory_Alloc(out_size);
|
||||
strncpy(out, path, dot - path);
|
||||
out[dot - path] = '\0';
|
||||
strcat(out, *ext);
|
||||
if (File_Exists(out)) {
|
||||
return out;
|
||||
}
|
||||
Memory_FreePointer(&out);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Memory_DupStr(path);
|
||||
}
|
||||
|
||||
MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode)
|
||||
{
|
||||
char *full_path = File_GetFullPath(path);
|
||||
MYFILE *file = Memory_Alloc(sizeof(MYFILE));
|
||||
file->path = Memory_DupStr(path);
|
||||
switch (mode) {
|
||||
case FILE_OPEN_WRITE:
|
||||
file->fp = fopen(full_path, "wb");
|
||||
break;
|
||||
case FILE_OPEN_READ:
|
||||
file->fp = fopen(full_path, "rb");
|
||||
break;
|
||||
case FILE_OPEN_READ_WRITE:
|
||||
file->fp = fopen(full_path, "r+b");
|
||||
break;
|
||||
default:
|
||||
file->fp = NULL;
|
||||
break;
|
||||
}
|
||||
Memory_FreePointer(&full_path);
|
||||
if (!file->fp) {
|
||||
Memory_FreePointer(&file->path);
|
||||
Memory_FreePointer(&file);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
void File_ReadData(MYFILE *const file, void *const data, const size_t size)
|
||||
{
|
||||
fread(data, size, 1, file->fp);
|
||||
}
|
||||
|
||||
void File_ReadItems(
|
||||
MYFILE *const file, void *data, const size_t count, const size_t item_size)
|
||||
{
|
||||
fread(data, item_size, count, file->fp);
|
||||
}
|
||||
|
||||
int8_t File_ReadS8(MYFILE *const file)
|
||||
{
|
||||
int8_t result;
|
||||
fread(&result, sizeof(result), 1, file->fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
int16_t File_ReadS16(MYFILE *const file)
|
||||
{
|
||||
int16_t result;
|
||||
fread(&result, sizeof(result), 1, file->fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
int32_t File_ReadS32(MYFILE *const file)
|
||||
{
|
||||
int32_t result;
|
||||
fread(&result, sizeof(result), 1, file->fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t File_ReadU8(MYFILE *const file)
|
||||
{
|
||||
uint8_t result;
|
||||
fread(&result, sizeof(result), 1, file->fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint16_t File_ReadU16(MYFILE *const file)
|
||||
{
|
||||
uint16_t result;
|
||||
fread(&result, sizeof(result), 1, file->fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
uint32_t File_ReadU32(MYFILE *const file)
|
||||
{
|
||||
uint32_t result;
|
||||
fread(&result, sizeof(result), 1, file->fp);
|
||||
return result;
|
||||
}
|
||||
|
||||
void File_WriteData(
|
||||
MYFILE *const file, const void *const data, const size_t size)
|
||||
{
|
||||
fwrite(data, size, 1, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteItems(
|
||||
MYFILE *const file, const void *const data, const size_t count,
|
||||
const size_t item_size)
|
||||
{
|
||||
fwrite(data, item_size, count, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteS8(MYFILE *const file, const int8_t value)
|
||||
{
|
||||
fwrite(&value, sizeof(value), 1, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteS16(MYFILE *const file, const int16_t value)
|
||||
{
|
||||
fwrite(&value, sizeof(value), 1, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteS32(MYFILE *const file, const int32_t value)
|
||||
{
|
||||
fwrite(&value, sizeof(value), 1, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteU8(MYFILE *const file, const uint8_t value)
|
||||
{
|
||||
fwrite(&value, sizeof(value), 1, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteU16(MYFILE *const file, const uint16_t value)
|
||||
{
|
||||
fwrite(&value, sizeof(value), 1, file->fp);
|
||||
}
|
||||
|
||||
void File_WriteU32(MYFILE *const file, const uint32_t value)
|
||||
{
|
||||
fwrite(&value, sizeof(value), 1, file->fp);
|
||||
}
|
||||
|
||||
void File_Skip(MYFILE *file, size_t bytes)
|
||||
{
|
||||
File_Seek(file, bytes, FILE_SEEK_CUR);
|
||||
}
|
||||
|
||||
void File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case FILE_SEEK_SET:
|
||||
fseek(file->fp, pos, SEEK_SET);
|
||||
break;
|
||||
case FILE_SEEK_CUR:
|
||||
fseek(file->fp, pos, SEEK_CUR);
|
||||
break;
|
||||
case FILE_SEEK_END:
|
||||
fseek(file->fp, pos, SEEK_END);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
size_t File_Pos(MYFILE *file)
|
||||
{
|
||||
return ftell(file->fp);
|
||||
}
|
||||
|
||||
size_t File_Size(MYFILE *file)
|
||||
{
|
||||
size_t old = ftell(file->fp);
|
||||
fseek(file->fp, 0, SEEK_END);
|
||||
size_t size = ftell(file->fp);
|
||||
fseek(file->fp, old, SEEK_SET);
|
||||
return size;
|
||||
}
|
||||
|
||||
const char *File_GetPath(MYFILE *file)
|
||||
{
|
||||
return file->path;
|
||||
}
|
||||
|
||||
void File_Close(MYFILE *file)
|
||||
{
|
||||
fclose(file->fp);
|
||||
Memory_FreePointer(&file->path);
|
||||
Memory_FreePointer(&file);
|
||||
}
|
||||
|
||||
bool File_Load(const char *path, char **output_data, size_t *output_size)
|
||||
{
|
||||
assert(output_data != NULL);
|
||||
|
||||
MYFILE *fp = File_Open(path, FILE_OPEN_READ);
|
||||
if (!fp) {
|
||||
LOG_ERROR("Can't open file %s", path);
|
||||
*output_data = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t data_size = File_Size(fp);
|
||||
char *data = Memory_Alloc(data_size + 1);
|
||||
File_ReadData(fp, data, data_size);
|
||||
if (File_Pos(fp) != data_size) {
|
||||
*output_data = NULL;
|
||||
LOG_ERROR("Can't read file %s", path);
|
||||
Memory_FreePointer(&data);
|
||||
File_Close(fp);
|
||||
return false;
|
||||
}
|
||||
File_Close(fp);
|
||||
data[data_size] = '\0';
|
||||
|
||||
*output_data = data;
|
||||
if (output_size != NULL) {
|
||||
*output_size = data_size;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void File_CreateDirectory(const char *path)
|
||||
{
|
||||
char *full_path = File_GetFullPath(path);
|
||||
assert(full_path);
|
||||
#if defined(_WIN32)
|
||||
_mkdir(full_path);
|
||||
#else
|
||||
mkdir(full_path, 0775);
|
||||
#endif
|
||||
Memory_FreePointer(&full_path);
|
||||
}
|
10
src/libtrx/game/backpack.c
Normal file
10
src/libtrx/game/backpack.c
Normal file
|
@ -0,0 +1,10 @@
|
|||
#include "game/backpack.h"
|
||||
|
||||
bool Backpack_AddItemNTimes(const GAME_OBJECT_ID object_id, const int32_t n)
|
||||
{
|
||||
bool result = false;
|
||||
for (int32_t i = 0; i < n; i++) {
|
||||
result |= Backpack_AddItem(object_id);
|
||||
}
|
||||
return result;
|
||||
}
|
278
src/libtrx/game/console/cmd/config.c
Normal file
278
src/libtrx/game/console/cmd/config.c
Normal file
|
@ -0,0 +1,278 @@
|
|||
#include "game/console/cmd/config.h"
|
||||
|
||||
#include "config/common.h"
|
||||
#include "config/map.h"
|
||||
#include "game/game_string.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *M_Resolve(const char *option_name);
|
||||
static bool M_SameKey(const char *key1, const char *key2);
|
||||
static char *M_NormalizeKey(const char *key);
|
||||
|
||||
static bool M_GetCurrentValue(
|
||||
const CONFIG_OPTION *option, char *target, size_t target_size);
|
||||
static bool M_SetCurrentValue(
|
||||
const CONFIG_OPTION *option, const char *new_value);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static const char *M_Resolve(const char *const option_name)
|
||||
{
|
||||
const char *dot = strrchr(option_name, '.');
|
||||
if (dot) {
|
||||
return dot + 1;
|
||||
}
|
||||
return option_name;
|
||||
}
|
||||
|
||||
static bool M_SameKey(const char *key1, const char *key2)
|
||||
{
|
||||
key1 = M_Resolve(key1);
|
||||
key2 = M_Resolve(key2);
|
||||
const size_t len1 = strlen(key1);
|
||||
const size_t len2 = strlen(key2);
|
||||
if (len1 != len2) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t i = 0; i < len1; i++) {
|
||||
char c1 = key1[i];
|
||||
char c2 = key2[i];
|
||||
if (c1 == '_') {
|
||||
c1 = '-';
|
||||
}
|
||||
if (c2 == '_') {
|
||||
c2 = '-';
|
||||
}
|
||||
if (c1 != c2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static char *M_NormalizeKey(const char *key)
|
||||
{
|
||||
// TODO: Once we support arbitrary glyphs, this conversion should
|
||||
// no longer be necessary.
|
||||
char *result = Memory_DupStr(key);
|
||||
for (uint32_t i = 0; i < strlen(result); i++) {
|
||||
if (result[i] == '_') {
|
||||
result[i] = '-';
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool M_GetCurrentValue(
|
||||
const CONFIG_OPTION *const option, char *target, const size_t target_size)
|
||||
{
|
||||
if (option == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
assert(option->target != NULL);
|
||||
switch (option->type) {
|
||||
case COT_BOOL:
|
||||
snprintf(
|
||||
target, target_size, "%s",
|
||||
*(bool *)option->target ? GS(MISC_ON) : GS(MISC_OFF));
|
||||
break;
|
||||
case COT_INT32:
|
||||
snprintf(target, target_size, "%d", *(int32_t *)option->target);
|
||||
break;
|
||||
case COT_FLOAT:
|
||||
snprintf(target, target_size, "%.2f", *(float *)option->target);
|
||||
break;
|
||||
case COT_DOUBLE:
|
||||
snprintf(target, target_size, "%.2f", *(double *)option->target);
|
||||
break;
|
||||
case COT_ENUM:
|
||||
snprintf(
|
||||
target, target_size, "%s",
|
||||
EnumMap_ToString(option->param, *(int32_t *)option->target));
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool M_SetCurrentValue(
|
||||
const CONFIG_OPTION *const option, const char *const new_value)
|
||||
{
|
||||
if (option == NULL) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
assert(option->target != NULL);
|
||||
switch (option->type) {
|
||||
case COT_BOOL:
|
||||
if (String_Match(new_value, "on|true|1")) {
|
||||
*(bool *)option->target = true;
|
||||
return true;
|
||||
} else if (String_Match(new_value, "off|false|0")) {
|
||||
*(bool *)option->target = false;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
case COT_INT32: {
|
||||
int32_t new_value_typed;
|
||||
if (sscanf(new_value, "%d", &new_value_typed) == 1) {
|
||||
*(int32_t *)option->target = new_value_typed;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COT_FLOAT: {
|
||||
float new_value_typed;
|
||||
if (sscanf(new_value, "%f", &new_value_typed) == 1) {
|
||||
*(float *)option->target = new_value_typed;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COT_DOUBLE: {
|
||||
double new_value_typed;
|
||||
if (sscanf(new_value, "%lf", &new_value_typed) == 1) {
|
||||
*(double *)option->target = new_value_typed;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case COT_ENUM: {
|
||||
const int32_t new_value_typed =
|
||||
EnumMap_Get(option->param, new_value, -1);
|
||||
if (new_value_typed != -1) {
|
||||
*(int32_t *)option->target = new_value_typed;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
COMMAND_RESULT result = CR_BAD_INVOCATION;
|
||||
|
||||
char *key = Memory_DupStr(ctx->args);
|
||||
char *const space = strchr(key, ' ');
|
||||
const char *new_value = NULL;
|
||||
if (space != NULL) {
|
||||
new_value = space + 1;
|
||||
space[0] = '\0'; // NULL-terminate the key
|
||||
}
|
||||
|
||||
const CONFIG_OPTION *const option =
|
||||
Console_Cmd_Config_GetOptionFromKey(key);
|
||||
if (option == NULL) {
|
||||
result = CR_FAILURE;
|
||||
} else {
|
||||
result = Console_Cmd_Config_Helper(option, new_value);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
Memory_FreePointer(&key);
|
||||
return result;
|
||||
}
|
||||
|
||||
const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromKey(const char *const key)
|
||||
{
|
||||
VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));
|
||||
|
||||
for (const CONFIG_OPTION *option = Config_GetOptionMap();
|
||||
option->name != NULL; option++) {
|
||||
STRING_FUZZY_SOURCE source_item = {
|
||||
.key = (const char *)M_NormalizeKey(option->name),
|
||||
.value = (void *)option,
|
||||
.weight = 1,
|
||||
};
|
||||
Vector_Add(source, &source_item);
|
||||
}
|
||||
|
||||
VECTOR *matches = String_FuzzyMatch(key, source);
|
||||
const CONFIG_OPTION *result = NULL;
|
||||
if (matches->count == 0) {
|
||||
Console_Log(GS(OSD_CONFIG_OPTION_UNKNOWN_OPTION), key);
|
||||
} else if (matches->count == 1) {
|
||||
const STRING_FUZZY_MATCH *const match = Vector_Get(matches, 0);
|
||||
result = match->value;
|
||||
} else if (matches->count == 2) {
|
||||
const STRING_FUZZY_MATCH *const match1 = Vector_Get(matches, 0);
|
||||
const STRING_FUZZY_MATCH *const match2 = Vector_Get(matches, 1);
|
||||
Console_Log(GS(OSD_AMBIGUOUS_INPUT_2), match1->key, match2->key);
|
||||
} else if (matches->count >= 3) {
|
||||
const STRING_FUZZY_MATCH *const match1 = Vector_Get(matches, 0);
|
||||
const STRING_FUZZY_MATCH *const match2 = Vector_Get(matches, 1);
|
||||
Console_Log(GS(OSD_AMBIGUOUS_INPUT_3), match1->key, match2->key);
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < source->count; i++) {
|
||||
const STRING_FUZZY_SOURCE *const source_item = Vector_Get(source, i);
|
||||
Memory_Free((char *)source_item->key);
|
||||
}
|
||||
|
||||
Vector_Free(matches);
|
||||
Vector_Free(source);
|
||||
return result;
|
||||
}
|
||||
|
||||
const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget(
|
||||
const void *const target)
|
||||
{
|
||||
for (const CONFIG_OPTION *option = Config_GetOptionMap();
|
||||
option->name != NULL; option++) {
|
||||
if (option->target == target) {
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
COMMAND_RESULT Console_Cmd_Config_Helper(
|
||||
const CONFIG_OPTION *const option, const char *const new_value)
|
||||
{
|
||||
assert(option != NULL);
|
||||
|
||||
char *normalized_name = M_NormalizeKey(option->name);
|
||||
|
||||
COMMAND_RESULT result = CR_BAD_INVOCATION;
|
||||
if (new_value == NULL || String_IsEmpty(new_value)) {
|
||||
char cur_value[128];
|
||||
if (M_GetCurrentValue(option, cur_value, 128)) {
|
||||
Console_Log(GS(OSD_CONFIG_OPTION_GET), normalized_name, cur_value);
|
||||
result = CR_SUCCESS;
|
||||
} else {
|
||||
result = CR_FAILURE;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
if (M_SetCurrentValue(option, new_value)) {
|
||||
Config_Write();
|
||||
|
||||
char final_value[128];
|
||||
assert(M_GetCurrentValue(option, final_value, 128));
|
||||
Console_Log(GS(OSD_CONFIG_OPTION_SET), normalized_name, final_value);
|
||||
result = CR_SUCCESS;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
Memory_FreePointer(&normalized_name);
|
||||
return result;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Config = {
|
||||
.prefix = "set",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
41
src/libtrx/game/console/cmd/die.c
Normal file
41
src/libtrx/game/console/cmd/die.c
Normal file
|
@ -0,0 +1,41 @@
|
|||
#include "game/console/cmd/die.h"
|
||||
|
||||
#include "game/effects/exploding_death.h"
|
||||
#include "game/items.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "game/objects/common.h"
|
||||
#include "game/objects/ids.h"
|
||||
#include "game/sound.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
if (!Object_GetObject(O_LARA)->loaded) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
LARA_INFO *const lara = Lara_GetLaraInfo();
|
||||
ITEM *const lara_item = Lara_GetItem();
|
||||
if (lara_item->hit_points <= 0) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Sound_Effect(SFX_LARA_FALL, &lara_item->pos, SPM_NORMAL);
|
||||
Sound_Effect(SFX_EXPLOSION_CHEAT, &lara_item->pos, SPM_NORMAL);
|
||||
Effect_ExplodingDeath(lara->item_num, -1, 1);
|
||||
|
||||
lara_item->hit_points = 0;
|
||||
lara_item->flags |= IF_INVISIBLE;
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Die = {
|
||||
.prefix = "abortion|natla-?s(uc|tin)ks",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
26
src/libtrx/game/console/cmd/end_level.c
Normal file
26
src/libtrx/game/console/cmd/end_level.c
Normal file
|
@ -0,0 +1,26 @@
|
|||
#include "game/console/cmd/end_level.h"
|
||||
|
||||
#include "game/game.h"
|
||||
#include "game/lara/cheat.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
if (Game_GetCurrentLevelType() == GFL_TITLE) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
Lara_Cheat_EndLevel();
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_EndLevel = {
|
||||
.prefix = "end-?level|next-?level",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
21
src/libtrx/game/console/cmd/exit_game.c
Normal file
21
src/libtrx/game/console/cmd/exit_game.c
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "game/console/cmd/exit_game.h"
|
||||
|
||||
#include "game/gameflow/common.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
Gameflow_OverrideCommand((GAMEFLOW_COMMAND) { .action = GF_EXIT_GAME });
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_ExitGame = {
|
||||
.prefix = "exit|quit",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
21
src/libtrx/game/console/cmd/exit_to_title.c
Normal file
21
src/libtrx/game/console/cmd/exit_to_title.c
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "game/console/cmd/exit_to_title.h"
|
||||
|
||||
#include "game/gameflow/common.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
Gameflow_OverrideCommand((GAMEFLOW_COMMAND) { .action = GF_EXIT_TO_TITLE });
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_ExitToTitle = {
|
||||
.prefix = "title",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
38
src/libtrx/game/console/cmd/flipmap.c
Normal file
38
src/libtrx/game/console/cmd/flipmap.c
Normal file
|
@ -0,0 +1,38 @@
|
|||
#include "game/console/cmd/flipmap.h"
|
||||
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/rooms.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (Game_GetCurrentLevelType() == GFL_TITLE) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
bool new_state = Room_GetFlipStatus();
|
||||
if (String_IsEmpty(ctx->args)) {
|
||||
new_state = !new_state;
|
||||
} else if (!String_ParseBool(ctx->args, &new_state)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
if (Room_GetFlipStatus() == new_state) {
|
||||
Console_Log(
|
||||
new_state ? GS(OSD_FLIPMAP_FAIL_ALREADY_ON)
|
||||
: GS(OSD_FLIPMAP_FAIL_ALREADY_OFF));
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
Room_FlipMap();
|
||||
Console_Log(new_state ? GS(OSD_FLIPMAP_ON) : GS(OSD_FLIPMAP_OFF));
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_FlipMap = {
|
||||
.prefix = "flip|flipmap",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
43
src/libtrx/game/console/cmd/fly.c
Normal file
43
src/libtrx/game/console/cmd/fly.c
Normal file
|
@ -0,0 +1,43 @@
|
|||
#include "game/console/cmd/fly.h"
|
||||
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/lara/cheat.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!Game_IsPlayable()) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
bool enable;
|
||||
if (String_ParseBool(ctx->args, &enable)) {
|
||||
if (enable) {
|
||||
Lara_Cheat_EnterFlyMode();
|
||||
} else {
|
||||
Lara_Cheat_ExitFlyMode();
|
||||
}
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
const LARA_INFO *const lara = Lara_GetLaraInfo();
|
||||
if (lara->water_status == LWS_CHEAT) {
|
||||
Lara_Cheat_ExitFlyMode();
|
||||
} else {
|
||||
Lara_Cheat_EnterFlyMode();
|
||||
}
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Fly = {
|
||||
.prefix = "fly",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
86
src/libtrx/game/console/cmd/give_item.c
Normal file
86
src/libtrx/game/console/cmd/give_item.c
Normal file
|
@ -0,0 +1,86 @@
|
|||
#include "game/console/cmd/give_item.h"
|
||||
|
||||
#include "game/backpack.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/lara/cheat.h"
|
||||
#include "game/objects/common.h"
|
||||
#include "game/objects/names.h"
|
||||
#include "game/objects/vars.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static bool M_CanTargetObjectPickup(GAME_OBJECT_ID object_id);
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static bool M_CanTargetObjectPickup(const GAME_OBJECT_ID object_id)
|
||||
{
|
||||
return Object_IsObjectType(object_id, g_PickupObjects)
|
||||
&& Object_GetObject(object_id)->loaded;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!Game_IsPlayable()) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
if (String_Equivalent(ctx->args, "keys")) {
|
||||
return Lara_Cheat_GiveAllKeys() ? CR_SUCCESS : CR_FAILURE;
|
||||
}
|
||||
|
||||
if (String_Equivalent(ctx->args, "guns")) {
|
||||
return Lara_Cheat_GiveAllGuns() ? CR_SUCCESS : CR_FAILURE;
|
||||
}
|
||||
|
||||
if (String_Equivalent(ctx->args, "all")) {
|
||||
return Lara_Cheat_GiveAllItems() ? CR_SUCCESS : CR_FAILURE;
|
||||
}
|
||||
|
||||
int32_t num = 1;
|
||||
const char *args = ctx->args;
|
||||
if (sscanf(ctx->args, "%d ", &num) == 1) {
|
||||
args = strstr(args, " ");
|
||||
if (args == NULL) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
args++;
|
||||
}
|
||||
|
||||
if (String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
bool found = false;
|
||||
int32_t match_count = 0;
|
||||
GAME_OBJECT_ID *matching_objs =
|
||||
Object_IdsFromName(args, &match_count, M_CanTargetObjectPickup);
|
||||
for (int32_t i = 0; i < match_count; i++) {
|
||||
const GAME_OBJECT_ID object_id = matching_objs[i];
|
||||
if (Object_GetObject(object_id)->loaded) {
|
||||
const char *obj_name = Object_GetName(object_id);
|
||||
if (obj_name == NULL) {
|
||||
obj_name = args;
|
||||
}
|
||||
Backpack_AddItemNTimes(object_id, num);
|
||||
Console_Log(GS(OSD_GIVE_ITEM), obj_name);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
Memory_FreePointer(&matching_objs);
|
||||
|
||||
if (!found) {
|
||||
Console_Log(GS(OSD_INVALID_ITEM), args);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_GiveItem = {
|
||||
.prefix = "give",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
37
src/libtrx/game/console/cmd/heal.c
Normal file
37
src/libtrx/game/console/cmd/heal.c
Normal file
|
@ -0,0 +1,37 @@
|
|||
#include "game/console/cmd/heal.h"
|
||||
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "game/lara/const.h"
|
||||
#include "game/lara/misc.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
if (!Game_IsPlayable()) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
ITEM *const lara_item = Lara_GetItem();
|
||||
if (lara_item->hit_points == LARA_MAX_HITPOINTS) {
|
||||
Console_Log(GS(OSD_HEAL_ALREADY_FULL_HP));
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
lara_item->hit_points = LARA_MAX_HITPOINTS;
|
||||
Lara_Extinguish();
|
||||
Console_Log(GS(OSD_HEAL_SUCCESS));
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Heal = {
|
||||
.prefix = "heal",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
135
src/libtrx/game/console/cmd/kill.c
Normal file
135
src/libtrx/game/console/cmd/kill.c
Normal file
|
@ -0,0 +1,135 @@
|
|||
#include "game/console/cmd/kill.h"
|
||||
|
||||
#include "game/const.h"
|
||||
#include "game/creature.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/items.h"
|
||||
#include "game/lara/cheat.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "game/lara/misc.h"
|
||||
#include "game/objects/common.h"
|
||||
#include "game/objects/ids.h"
|
||||
#include "game/objects/names.h"
|
||||
#include "game/objects/vars.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
|
||||
static bool M_CanTargetObjectCreature(GAME_OBJECT_ID object_id);
|
||||
static COMMAND_RESULT M_KillAllEnemies(void);
|
||||
static COMMAND_RESULT M_KillNearestEnemies(void);
|
||||
static COMMAND_RESULT M_KillEnemyType(const char *enemy_name);
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static bool M_CanTargetObjectCreature(const GAME_OBJECT_ID object_id)
|
||||
{
|
||||
return Object_IsObjectType(object_id, g_EnemyObjects)
|
||||
|| Object_IsObjectType(object_id, g_AllyObjects);
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_KillAllEnemies(void)
|
||||
{
|
||||
int32_t num_killed = 0;
|
||||
for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {
|
||||
const ITEM *const item = Item_Get(item_num);
|
||||
if (!Creature_IsEnemy(item)) {
|
||||
continue;
|
||||
}
|
||||
if (Lara_Cheat_KillEnemy(item_num)) {
|
||||
num_killed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (num_killed == 0) {
|
||||
Console_Log(GS(OSD_KILL_ALL_FAIL));
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
Console_Log(GS(OSD_KILL_ALL), num_killed);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_KillNearestEnemies(void)
|
||||
{
|
||||
bool found = false;
|
||||
while (true) {
|
||||
const int16_t best_item_num = Lara_GetNearestEnemy();
|
||||
if (best_item_num == NO_ITEM) {
|
||||
break;
|
||||
}
|
||||
|
||||
const ITEM *const lara_item = Lara_GetItem();
|
||||
const ITEM *const item = Item_Get(best_item_num);
|
||||
const int32_t distance = Item_GetDistance(item, &lara_item->pos);
|
||||
found |= Lara_Cheat_KillEnemy(best_item_num);
|
||||
if (distance >= WALL_L) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
Console_Log(GS(OSD_KILL_FAIL));
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
Console_Log(GS(OSD_KILL));
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_KillEnemyType(const char *const enemy_name)
|
||||
{
|
||||
bool matches_found = false;
|
||||
int32_t num_killed = 0;
|
||||
int32_t match_count = 0;
|
||||
GAME_OBJECT_ID *matching_objs =
|
||||
Object_IdsFromName(enemy_name, &match_count, M_CanTargetObjectCreature);
|
||||
|
||||
for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {
|
||||
const ITEM *const item = Item_Get(item_num);
|
||||
|
||||
bool is_matched = false;
|
||||
for (int32_t i = 0; i < match_count; i++) {
|
||||
if (matching_objs[i] == item->object_id) {
|
||||
is_matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_matched) {
|
||||
continue;
|
||||
}
|
||||
matches_found = true;
|
||||
|
||||
if (Lara_Cheat_KillEnemy(item_num)) {
|
||||
num_killed++;
|
||||
}
|
||||
}
|
||||
Memory_FreePointer(&matching_objs);
|
||||
|
||||
if (!matches_found) {
|
||||
Console_Log(GS(OSD_INVALID_OBJECT), enemy_name);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
if (num_killed == 0) {
|
||||
Console_Log(GS(OSD_OBJECT_NOT_FOUND), enemy_name);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
Console_Log(GS(OSD_KILL_ALL), num_killed);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (String_Equivalent(ctx->args, "all")) {
|
||||
return M_KillAllEnemies();
|
||||
}
|
||||
|
||||
if (String_IsEmpty(ctx->args)) {
|
||||
return M_KillNearestEnemies();
|
||||
}
|
||||
|
||||
return M_KillEnemyType(ctx->args);
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Kill = {
|
||||
.prefix = "kill",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
41
src/libtrx/game/console/cmd/load_game.c
Normal file
41
src/libtrx/game/console/cmd/load_game.c
Normal file
|
@ -0,0 +1,41 @@
|
|||
#include "game/console/cmd/load_game.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/gameflow/common.h"
|
||||
#include "game/savegame.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
int32_t slot_num;
|
||||
if (!String_ParseInteger(ctx->args, &slot_num)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
const int32_t slot_idx = slot_num - 1; // convert 1-indexing to 0-indexing
|
||||
|
||||
if (slot_idx < 0 || slot_idx >= Savegame_GetSlotCount()) {
|
||||
Console_Log(GS(OSD_LOAD_GAME_FAIL_INVALID_SLOT), slot_num);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
if (Savegame_IsSlotFree(slot_idx)) {
|
||||
Console_Log(GS(OSD_LOAD_GAME_FAIL_UNAVAILABLE_SLOT), slot_num);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
Gameflow_OverrideCommand((GAMEFLOW_COMMAND) {
|
||||
.action = GF_START_SAVED_GAME,
|
||||
.param = slot_idx,
|
||||
});
|
||||
Console_Log(GS(OSD_LOAD_GAME), slot_num);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_LoadGame = {
|
||||
.prefix = "load",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
21
src/libtrx/game/console/cmd/play_demo.c
Normal file
21
src/libtrx/game/console/cmd/play_demo.c
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "game/console/cmd/play_demo.h"
|
||||
|
||||
#include "game/gameflow/common.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
Gameflow_OverrideCommand((GAMEFLOW_COMMAND) { .action = GF_START_DEMO });
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_PlayDemo = {
|
||||
.prefix = "demo",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
86
src/libtrx/game/console/cmd/play_level.c
Normal file
86
src/libtrx/game/console/cmd/play_level.c
Normal file
|
@ -0,0 +1,86 @@
|
|||
#include "game/console/cmd/play_level.h"
|
||||
|
||||
#include "game/game_string.h"
|
||||
#include "game/gameflow/common.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
VECTOR *source = NULL;
|
||||
VECTOR *matches = NULL;
|
||||
int32_t level_to_load = -1;
|
||||
|
||||
if (String_ParseInteger(ctx->args, &level_to_load)) {
|
||||
goto matched;
|
||||
}
|
||||
|
||||
source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));
|
||||
for (int32_t level_num = 0; level_num < Gameflow_GetLevelCount();
|
||||
level_num++) {
|
||||
STRING_FUZZY_SOURCE source_item = {
|
||||
.key = Gameflow_GetLevelTitle(level_num),
|
||||
.value = (void *)(intptr_t)level_num,
|
||||
.weight = 1,
|
||||
};
|
||||
Vector_Add(source, &source_item);
|
||||
}
|
||||
|
||||
const int32_t gym_level_num = Gameflow_GetGymLevelNumber();
|
||||
if (gym_level_num != -1) {
|
||||
STRING_FUZZY_SOURCE source_item = {
|
||||
.key = "gym",
|
||||
.value = (void *)(intptr_t)gym_level_num,
|
||||
.weight = 1,
|
||||
};
|
||||
Vector_Add(source, &source_item);
|
||||
}
|
||||
|
||||
COMMAND_RESULT result;
|
||||
matches = String_FuzzyMatch(ctx->args, source);
|
||||
|
||||
if (matches->count == 0) {
|
||||
Console_Log(GS(OSD_INVALID_LEVEL));
|
||||
result = CR_BAD_INVOCATION;
|
||||
goto cleanup;
|
||||
} else if (matches->count >= 1) {
|
||||
const STRING_FUZZY_MATCH *const match = Vector_Get(matches, 0);
|
||||
level_to_load = (int32_t)(intptr_t)match->value;
|
||||
goto matched;
|
||||
}
|
||||
|
||||
matched:
|
||||
if (level_to_load >= 0 && level_to_load < Gameflow_GetLevelCount()) {
|
||||
Gameflow_OverrideCommand((GAMEFLOW_COMMAND) {
|
||||
.action = GF_START_GAME,
|
||||
.param = level_to_load,
|
||||
});
|
||||
Console_Log(GS(OSD_PLAY_LEVEL), Gameflow_GetLevelTitle(level_to_load));
|
||||
result = CR_SUCCESS;
|
||||
} else {
|
||||
Console_Log(GS(OSD_INVALID_LEVEL));
|
||||
result = CR_FAILURE;
|
||||
}
|
||||
|
||||
cleanup:
|
||||
if (matches != NULL) {
|
||||
Vector_Free(matches);
|
||||
matches = NULL;
|
||||
}
|
||||
if (source != NULL) {
|
||||
Vector_Free(source);
|
||||
source = NULL;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_PlayLevel = {
|
||||
.prefix = "play|level",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
43
src/libtrx/game/console/cmd/pos.c
Normal file
43
src/libtrx/game/console/cmd/pos.c
Normal file
|
@ -0,0 +1,43 @@
|
|||
#include "game/console/cmd/pos.h"
|
||||
|
||||
#include "game/console/common.h"
|
||||
#include "game/const.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "game/objects/common.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!String_IsEmpty(ctx->args)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
const OBJECT *const object = Object_GetObject(O_LARA);
|
||||
if (!object->loaded) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const ITEM *const lara_item = Lara_GetItem();
|
||||
|
||||
// clang-format off
|
||||
Console_Log(
|
||||
GS(OSD_POS_GET),
|
||||
lara_item->room_num,
|
||||
lara_item->pos.x / (float)WALL_L,
|
||||
lara_item->pos.y / (float)WALL_L,
|
||||
lara_item->pos.z / (float)WALL_L,
|
||||
lara_item->rot.x * 360.0f / (float)PHD_ONE,
|
||||
lara_item->rot.y * 360.0f / (float)PHD_ONE,
|
||||
lara_item->rot.z * 360.0f / (float)PHD_ONE);
|
||||
// clang-format on
|
||||
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Pos = {
|
||||
.prefix = "pos",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
37
src/libtrx/game/console/cmd/save_game.c
Normal file
37
src/libtrx/game/console/cmd/save_game.c
Normal file
|
@ -0,0 +1,37 @@
|
|||
#include "game/console/cmd/save_game.h"
|
||||
|
||||
#include "config.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/gameflow/common.h"
|
||||
#include "game/savegame.h"
|
||||
#include "strings.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!Game_IsPlayable()) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
int32_t slot_num;
|
||||
if (!String_ParseInteger(ctx->args, &slot_num)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
const int32_t slot_idx = slot_num - 1; // convert 1-indexing to 0-indexing
|
||||
|
||||
if (slot_idx < 0 || slot_idx >= Savegame_GetSlotCount()) {
|
||||
Console_Log(GS(OSD_SAVE_GAME_FAIL_INVALID_SLOT), slot_num);
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
Savegame_Save(slot_idx);
|
||||
Console_Log(GS(OSD_SAVE_GAME), slot_num);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_SaveGame = {
|
||||
.prefix = "save",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
40
src/libtrx/game/console/cmd/set_health.c
Normal file
40
src/libtrx/game/console/cmd/set_health.c
Normal file
|
@ -0,0 +1,40 @@
|
|||
|
||||
#include "game/console/cmd/pos.h"
|
||||
#include "game/console/common.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "game/lara/const.h"
|
||||
#include "game/objects/common.h"
|
||||
#include "strings.h"
|
||||
#include "utils.h"
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!Game_IsPlayable()) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
ITEM *const lara_item = Lara_GetItem();
|
||||
if (String_IsEmpty(ctx->args)) {
|
||||
Console_Log(GS(OSD_CURRENT_HEALTH_GET), lara_item->hit_points);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
int32_t hp;
|
||||
if (!String_ParseInteger(ctx->args, &hp)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
CLAMP(hp, 0, LARA_MAX_HITPOINTS);
|
||||
|
||||
lara_item->hit_points = hp;
|
||||
Console_Log(GS(OSD_CURRENT_HEALTH_SET), hp);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_SetHealth = {
|
||||
.prefix = "hp",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
85
src/libtrx/game/console/cmd/sfx.c
Normal file
85
src/libtrx/game/console/cmd/sfx.c
Normal file
|
@ -0,0 +1,85 @@
|
|||
#include "game/console/cmd/sfx.h"
|
||||
|
||||
#include "game/console/common.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/sound.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static char *M_CreateRangeString(void);
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static char *M_CreateRangeString(void)
|
||||
{
|
||||
size_t buffer_size = 64;
|
||||
char *result = Memory_Alloc(buffer_size);
|
||||
|
||||
int32_t prev = -1;
|
||||
int32_t start = -1;
|
||||
for (int32_t i = 0; i <= SFX_NUMBER_OF; i++) {
|
||||
const bool valid = Sound_IsAvailable(i);
|
||||
|
||||
if (valid && start == -1) {
|
||||
start = i;
|
||||
}
|
||||
if (!valid && start != -1) {
|
||||
char temp[32];
|
||||
if (start == prev) {
|
||||
sprintf(temp, "%d, ", prev);
|
||||
} else {
|
||||
sprintf(temp, "%d-%d, ", start, prev);
|
||||
}
|
||||
|
||||
const int32_t len = strlen(temp);
|
||||
if (strlen(result) + len >= buffer_size) {
|
||||
buffer_size *= 2;
|
||||
result = Memory_Realloc(result, buffer_size);
|
||||
}
|
||||
|
||||
strcat(result, temp);
|
||||
start = -1;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
prev = i;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the trailing comma and space
|
||||
result[strlen(result) - 2] = '\0';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (String_IsEmpty(ctx->args)) {
|
||||
char *ranges = M_CreateRangeString();
|
||||
Console_Log(GS(OSD_SOUND_AVAILABLE_SAMPLES), ranges);
|
||||
Memory_FreePointer(&ranges);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
int32_t sfx_id;
|
||||
if (!String_ParseInteger(ctx->args, &sfx_id)) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
if (!Sound_IsAvailable(sfx_id)) {
|
||||
Console_Log(GS(OSD_INVALID_SAMPLE), sfx_id);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
Console_Log(GS(OSD_SOUND_PLAYING_SAMPLE), sfx_id);
|
||||
Sound_Effect(sfx_id, NULL, SPM_ALWAYS);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_SFX = {
|
||||
.prefix = "sfx",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
184
src/libtrx/game/console/cmd/teleport.c
Normal file
184
src/libtrx/game/console/cmd/teleport.c
Normal file
|
@ -0,0 +1,184 @@
|
|||
#include "game/console/cmd/teleport.h"
|
||||
|
||||
#include "game/const.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/items.h"
|
||||
#include "game/lara/cheat.h"
|
||||
#include "game/lara/common.h"
|
||||
#include "game/objects/common.h"
|
||||
#include "game/objects/names.h"
|
||||
#include "game/objects/vars.h"
|
||||
#include "game/random.h"
|
||||
#include "game/rooms.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static bool M_CanTargetObject(GAME_OBJECT_ID object_id);
|
||||
static bool M_IsFloatRound(float num);
|
||||
|
||||
static COMMAND_RESULT M_TeleportToXYZ(float x, float y, float z);
|
||||
static COMMAND_RESULT M_TeleportToRoom(int16_t room_num);
|
||||
static COMMAND_RESULT M_TeleportToObject(const char *user_input);
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *ctx);
|
||||
|
||||
static bool M_CanTargetObject(const GAME_OBJECT_ID object_id)
|
||||
{
|
||||
return !Object_IsObjectType(object_id, g_NullObjects)
|
||||
&& !Object_IsObjectType(object_id, g_AnimObjects)
|
||||
&& !Object_IsObjectType(object_id, g_InvObjects);
|
||||
}
|
||||
|
||||
static inline bool M_IsFloatRound(const float num)
|
||||
{
|
||||
return (fabsf(num) - roundf(num)) < 0.0001f;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_TeleportToXYZ(float x, const float y, float z)
|
||||
{
|
||||
if (M_IsFloatRound(x)) {
|
||||
x += 0.5f;
|
||||
}
|
||||
if (M_IsFloatRound(z)) {
|
||||
z += 0.5f;
|
||||
}
|
||||
|
||||
if (!Lara_Cheat_Teleport(x * WALL_L, y * WALL_L, z * WALL_L)) {
|
||||
Console_Log(GS(OSD_POS_SET_POS_FAIL), x, y, z);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
Console_Log(GS(OSD_POS_SET_POS), x, y, z);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_TeleportToRoom(const int16_t room_num)
|
||||
{
|
||||
if (room_num < 0 || room_num >= Room_GetTotalCount()) {
|
||||
Console_Log(GS(OSD_INVALID_ROOM), room_num, Room_GetTotalCount() - 1);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
const ROOM *const room = Room_Get(room_num);
|
||||
const int32_t x1 = room->pos.x + WALL_L;
|
||||
const int32_t x2 = room->pos.x + (room->size.x << WALL_SHIFT) - WALL_L;
|
||||
const int32_t y1 = room->min_floor;
|
||||
const int32_t y2 = room->max_ceiling;
|
||||
const int32_t z1 = room->pos.z + WALL_L;
|
||||
const int32_t z2 = room->pos.z + (room->size.z << WALL_SHIFT) - WALL_L;
|
||||
|
||||
bool success = false;
|
||||
for (int32_t i = 0; i < 100; i++) {
|
||||
int32_t x = x1 + Random_GetControl() * (x2 - x1) / 0x7FFF;
|
||||
int32_t y = y1;
|
||||
int32_t z = z1 + Random_GetControl() * (z2 - z1) / 0x7FFF;
|
||||
if (Lara_Cheat_Teleport(x, y, z)) {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
Console_Log(GS(OSD_POS_SET_ROOM_FAIL), room_num);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
Console_Log(GS(OSD_POS_SET_ROOM), room_num);
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_TeleportToObject(const char *const user_input)
|
||||
{
|
||||
// Nearest item of this name
|
||||
if (String_Equivalent(user_input, "")) {
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
int32_t match_count = 0;
|
||||
GAME_OBJECT_ID *matching_objs =
|
||||
Object_IdsFromName(user_input, &match_count, M_CanTargetObject);
|
||||
|
||||
const ITEM *const lara_item = Lara_GetItem();
|
||||
const ITEM *best_item = NULL;
|
||||
int32_t best_distance = INT32_MAX;
|
||||
|
||||
for (int16_t item_num = 0; item_num < Item_GetTotalCount(); item_num++) {
|
||||
const ITEM *const item = Item_Get(item_num);
|
||||
if (Object_IsObjectType(item->object_id, g_PickupObjects)
|
||||
&& (item->status == IS_INVISIBLE || item->status == IS_DEACTIVATED
|
||||
|| item->room_num == NO_ROOM)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (item->flags & IF_KILLED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool is_matched = false;
|
||||
for (int32_t i = 0; i < match_count; i++) {
|
||||
if (matching_objs[i] == item->object_id) {
|
||||
is_matched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_matched) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32_t distance = Item_GetDistance(item, &lara_item->pos);
|
||||
if (distance < best_distance) {
|
||||
best_distance = distance;
|
||||
best_item = item;
|
||||
}
|
||||
}
|
||||
|
||||
if (best_item == NULL) {
|
||||
Console_Log(GS(OSD_POS_SET_ITEM_FAIL), user_input);
|
||||
return CR_FAILURE;
|
||||
}
|
||||
|
||||
const char *obj_name = Object_GetName(best_item->object_id);
|
||||
if (obj_name == NULL) {
|
||||
obj_name = user_input;
|
||||
}
|
||||
|
||||
if (Lara_Cheat_Teleport(
|
||||
best_item->pos.x, best_item->pos.y - STEP_L, best_item->pos.z)) {
|
||||
Console_Log(GS(OSD_POS_SET_ITEM), obj_name);
|
||||
} else {
|
||||
Console_Log(GS(OSD_POS_SET_ITEM_FAIL), obj_name);
|
||||
}
|
||||
return CR_SUCCESS;
|
||||
}
|
||||
|
||||
static COMMAND_RESULT M_Entrypoint(const COMMAND_CONTEXT *const ctx)
|
||||
{
|
||||
if (!Game_IsPlayable()) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
const ITEM *const lara_item = Lara_GetItem();
|
||||
if (!lara_item->hit_points) {
|
||||
return CR_UNAVAILABLE;
|
||||
}
|
||||
|
||||
float x, y, z;
|
||||
if (sscanf(ctx->args, "%f %f %f", &x, &y, &z) == 3) {
|
||||
return M_TeleportToXYZ(x, y, z);
|
||||
}
|
||||
|
||||
int16_t room_num = -1;
|
||||
if (sscanf(ctx->args, "%hd", &room_num) == 1) {
|
||||
return M_TeleportToRoom(room_num);
|
||||
}
|
||||
|
||||
return M_TeleportToObject(ctx->args);
|
||||
}
|
||||
|
||||
CONSOLE_COMMAND g_Console_Cmd_Teleport = {
|
||||
.prefix = "tp",
|
||||
.proc = M_Entrypoint,
|
||||
};
|
189
src/libtrx/game/console/common.c
Normal file
189
src/libtrx/game/console/common.c
Normal file
|
@ -0,0 +1,189 @@
|
|||
#include "game/console/common.h"
|
||||
|
||||
#include "game/console/extern.h"
|
||||
#include "game/game_string.h"
|
||||
#include "game/ui/widgets/console.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static bool m_IsOpened = false;
|
||||
static UI_WIDGET *m_Console;
|
||||
|
||||
static void M_LogMultiline(const char *text);
|
||||
static void M_Log(const char *text);
|
||||
|
||||
static void M_LogMultiline(const char *const text)
|
||||
{
|
||||
assert(text != NULL);
|
||||
char *wrapped_text = String_WordWrap(text, Console_GetMaxLineLength());
|
||||
|
||||
const char *start = wrapped_text;
|
||||
while (true) {
|
||||
const char *newline = strchr(start, '\n');
|
||||
if (newline == NULL) {
|
||||
break;
|
||||
}
|
||||
char temp[newline - start + 1];
|
||||
strncpy(temp, start, newline - start);
|
||||
temp[newline - start] = '\0';
|
||||
M_Log(temp);
|
||||
start = newline + 1;
|
||||
}
|
||||
if (*start != '\0') {
|
||||
M_Log(start);
|
||||
}
|
||||
Memory_FreePointer(&wrapped_text);
|
||||
}
|
||||
|
||||
static void M_Log(const char *text)
|
||||
{
|
||||
assert(text != NULL);
|
||||
UI_Console_HandleLog(m_Console, text);
|
||||
}
|
||||
|
||||
void Console_Init(void)
|
||||
{
|
||||
m_Console = UI_Console_Create();
|
||||
}
|
||||
|
||||
void Console_Shutdown(void)
|
||||
{
|
||||
if (m_Console != NULL) {
|
||||
m_Console->free(m_Console);
|
||||
m_Console = NULL;
|
||||
}
|
||||
|
||||
m_IsOpened = false;
|
||||
}
|
||||
|
||||
void Console_Open(void)
|
||||
{
|
||||
if (m_IsOpened) {
|
||||
UI_Console_HandleClose(m_Console);
|
||||
}
|
||||
m_IsOpened = true;
|
||||
UI_Console_HandleOpen(m_Console);
|
||||
}
|
||||
|
||||
void Console_Close(void)
|
||||
{
|
||||
UI_Console_HandleClose(m_Console);
|
||||
m_IsOpened = false;
|
||||
}
|
||||
|
||||
bool Console_IsOpened(void)
|
||||
{
|
||||
return m_IsOpened;
|
||||
}
|
||||
|
||||
void Console_ScrollLogs(void)
|
||||
{
|
||||
UI_Console_ScrollLogs(m_Console);
|
||||
}
|
||||
|
||||
int32_t Console_GetVisibleLogCount(void)
|
||||
{
|
||||
return UI_Console_GetVisibleLogCount(m_Console);
|
||||
}
|
||||
|
||||
int32_t Console_GetMaxLogCount(void)
|
||||
{
|
||||
return UI_Console_GetMaxLogCount(m_Console);
|
||||
}
|
||||
|
||||
void Console_Log(const char *fmt, ...)
|
||||
{
|
||||
assert(fmt != NULL);
|
||||
|
||||
va_list va;
|
||||
|
||||
va_start(va, fmt);
|
||||
const size_t text_length = vsnprintf(NULL, 0, fmt, va);
|
||||
char *text = Memory_Alloc(text_length + 1);
|
||||
va_end(va);
|
||||
|
||||
va_start(va, fmt);
|
||||
vsnprintf(text, text_length + 1, fmt, va);
|
||||
va_end(va);
|
||||
|
||||
LOG_INFO("%s", text);
|
||||
M_LogMultiline(text);
|
||||
Memory_FreePointer(&text);
|
||||
}
|
||||
|
||||
COMMAND_RESULT Console_Eval(const char *const cmdline)
|
||||
{
|
||||
LOG_INFO("executing command: %s", cmdline);
|
||||
|
||||
const CONSOLE_COMMAND *matching_cmd = NULL;
|
||||
CONSOLE_COMMAND **cmd = Console_GetCommands();
|
||||
while (*cmd != NULL) {
|
||||
char regex[strlen((*cmd)->prefix) + 13];
|
||||
sprintf(regex, "^(%s)(\\s+.*)?$", (*cmd)->prefix);
|
||||
if (String_Match(cmdline, regex)) {
|
||||
matching_cmd = *cmd;
|
||||
break;
|
||||
}
|
||||
*cmd++;
|
||||
}
|
||||
|
||||
if (matching_cmd == NULL) {
|
||||
Console_Log(GS(OSD_UNKNOWN_COMMAND), cmdline);
|
||||
return CR_BAD_INVOCATION;
|
||||
}
|
||||
|
||||
char *prefix = Memory_DupStr(cmdline);
|
||||
char *args = "";
|
||||
char *space = strchr(prefix, ' ');
|
||||
if (space != NULL) {
|
||||
*space = '\0';
|
||||
args = space + 1;
|
||||
}
|
||||
|
||||
const COMMAND_CONTEXT ctx = {
|
||||
.cmd = matching_cmd,
|
||||
.prefix = prefix,
|
||||
.args = args,
|
||||
};
|
||||
assert(matching_cmd->proc != NULL);
|
||||
const COMMAND_RESULT result = matching_cmd->proc(&ctx);
|
||||
Memory_FreePointer(&prefix);
|
||||
|
||||
switch (result) {
|
||||
case CR_BAD_INVOCATION:
|
||||
Console_Log(GS(OSD_COMMAND_BAD_INVOCATION), cmdline);
|
||||
break;
|
||||
|
||||
case CR_UNAVAILABLE:
|
||||
Console_Log(GS(OSD_COMMAND_UNAVAILABLE));
|
||||
break;
|
||||
|
||||
case CR_SUCCESS:
|
||||
case CR_FAILURE:
|
||||
// The commands themselves are responsible for handling logging in
|
||||
// these scenarios.
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Console_Draw(void)
|
||||
{
|
||||
if (m_Console == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
Console_ScrollLogs();
|
||||
|
||||
if (Console_IsOpened() || Console_GetVisibleLogCount() > 0) {
|
||||
Console_DrawBackdrop();
|
||||
}
|
||||
|
||||
m_Console->draw(m_Console);
|
||||
}
|
57
src/libtrx/game/game_string.c
Normal file
57
src/libtrx/game/game_string.c
Normal file
|
@ -0,0 +1,57 @@
|
|||
#include "game/game_string.h"
|
||||
|
||||
#include "memory.h"
|
||||
|
||||
#include <uthash.h>
|
||||
|
||||
typedef struct {
|
||||
char *key;
|
||||
char *value;
|
||||
UT_hash_handle hh;
|
||||
} M_STRING_ENTRY;
|
||||
|
||||
static M_STRING_ENTRY *m_StringTable = NULL;
|
||||
|
||||
void GameString_Define(const char *key, const char *value)
|
||||
{
|
||||
M_STRING_ENTRY *entry;
|
||||
|
||||
HASH_FIND_STR(m_StringTable, key, entry);
|
||||
if (entry == NULL) {
|
||||
entry = (M_STRING_ENTRY *)Memory_Alloc(sizeof(M_STRING_ENTRY));
|
||||
entry->key = Memory_DupStr(key);
|
||||
entry->value = Memory_DupStr(value);
|
||||
HASH_ADD_KEYPTR(
|
||||
hh, m_StringTable, entry->key, strlen(entry->key), entry);
|
||||
} else {
|
||||
Memory_Free(entry->value);
|
||||
entry->value = Memory_DupStr(value);
|
||||
}
|
||||
}
|
||||
|
||||
bool GameString_IsKnown(const char *key)
|
||||
{
|
||||
M_STRING_ENTRY *entry;
|
||||
HASH_FIND_STR(m_StringTable, key, entry);
|
||||
return entry != NULL;
|
||||
}
|
||||
|
||||
const char *GameString_Get(const char *key)
|
||||
{
|
||||
M_STRING_ENTRY *entry;
|
||||
HASH_FIND_STR(m_StringTable, key, entry);
|
||||
return entry ? entry->value : NULL;
|
||||
}
|
||||
|
||||
void GameString_Clear(void)
|
||||
{
|
||||
M_STRING_ENTRY *entry, *tmp;
|
||||
|
||||
HASH_ITER(hh, m_StringTable, entry, tmp)
|
||||
{
|
||||
HASH_DEL(m_StringTable, entry);
|
||||
Memory_Free(entry->key);
|
||||
Memory_Free(entry->value);
|
||||
Memory_Free(entry);
|
||||
}
|
||||
}
|
21
src/libtrx/game/items.c
Normal file
21
src/libtrx/game/items.c
Normal file
|
@ -0,0 +1,21 @@
|
|||
#include "game/items.h"
|
||||
|
||||
#include "game/const.h"
|
||||
#include "utils.h"
|
||||
|
||||
void Item_TakeDamage(
|
||||
ITEM *const item, const int16_t damage, const bool hit_status)
|
||||
{
|
||||
#if TR_VERSION == 1
|
||||
if (item->hit_points == DONT_TARGET) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
item->hit_points -= damage;
|
||||
CLAMPL(item->hit_points, 0);
|
||||
|
||||
if (hit_status) {
|
||||
item->hit_status = 1;
|
||||
}
|
||||
}
|
82
src/libtrx/game/objects/names.c
Normal file
82
src/libtrx/game/objects/names.c
Normal file
|
@ -0,0 +1,82 @@
|
|||
#include "game/objects/names.h"
|
||||
|
||||
#include "game/game_string.h"
|
||||
#include "memory.h"
|
||||
#include "strings/fuzzy_match.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
typedef struct {
|
||||
char *name;
|
||||
} M_NAME_ENTRY;
|
||||
|
||||
static M_NAME_ENTRY m_NamesTable[O_NUMBER_OF] = { 0 };
|
||||
|
||||
static void M_ClearNames(void);
|
||||
|
||||
static void M_ClearNames(void)
|
||||
{
|
||||
for (GAME_OBJECT_ID object_id = 0; object_id < O_NUMBER_OF; object_id++) {
|
||||
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
|
||||
Memory_FreePointer(&entry->name);
|
||||
}
|
||||
}
|
||||
|
||||
void Object_SetName(const GAME_OBJECT_ID object_id, const char *const name)
|
||||
{
|
||||
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
|
||||
Memory_FreePointer(&entry->name);
|
||||
assert(name != NULL);
|
||||
entry->name = Memory_DupStr(name);
|
||||
}
|
||||
|
||||
const char *Object_GetName(const GAME_OBJECT_ID object_id)
|
||||
{
|
||||
M_NAME_ENTRY *const entry = &m_NamesTable[object_id];
|
||||
return entry != NULL ? entry->name : NULL;
|
||||
}
|
||||
|
||||
void Object_ResetNames(void)
|
||||
{
|
||||
M_ClearNames();
|
||||
|
||||
#define OBJ_NAME_DEFINE(object_id, name) Object_SetName(object_id, name);
|
||||
#include "game/objects/names.def"
|
||||
}
|
||||
|
||||
GAME_OBJECT_ID *Object_IdsFromName(
|
||||
const char *user_input, int32_t *out_match_count,
|
||||
bool (*filter)(GAME_OBJECT_ID))
|
||||
{
|
||||
VECTOR *source = Vector_Create(sizeof(STRING_FUZZY_SOURCE));
|
||||
|
||||
for (GAME_OBJECT_ID object_id = 0; object_id < O_NUMBER_OF; object_id++) {
|
||||
if (filter != NULL && !filter(object_id)) {
|
||||
continue;
|
||||
}
|
||||
STRING_FUZZY_SOURCE source_item = {
|
||||
.key = Object_GetName(object_id),
|
||||
.value = (void *)(intptr_t)object_id,
|
||||
.weight = 1,
|
||||
};
|
||||
Vector_Add(source, &source_item);
|
||||
}
|
||||
|
||||
VECTOR *matches = String_FuzzyMatch(user_input, source);
|
||||
GAME_OBJECT_ID *results =
|
||||
Memory_Alloc(sizeof(GAME_OBJECT_ID) * (matches->count + 1));
|
||||
for (int32_t i = 0; i < matches->count; i++) {
|
||||
const STRING_FUZZY_MATCH *const match = Vector_Get(matches, i);
|
||||
results[i] = (GAME_OBJECT_ID)(intptr_t)match->value;
|
||||
}
|
||||
results[matches->count] = NO_OBJECT;
|
||||
if (out_match_count != NULL) {
|
||||
*out_match_count = matches->count;
|
||||
}
|
||||
|
||||
Vector_Free(matches);
|
||||
Vector_Free(source);
|
||||
matches = NULL;
|
||||
|
||||
return results;
|
||||
}
|
43
src/libtrx/game/ui/common.c
Normal file
43
src/libtrx/game/ui/common.c
Normal file
|
@ -0,0 +1,43 @@
|
|||
#include "game/ui/common.h"
|
||||
|
||||
#include "vector.h"
|
||||
|
||||
void UI_Init(void)
|
||||
{
|
||||
UI_Events_Init();
|
||||
}
|
||||
|
||||
void UI_Shutdown(void)
|
||||
{
|
||||
UI_Events_Shutdown();
|
||||
}
|
||||
|
||||
void UI_HandleKeyDown(const uint32_t key)
|
||||
{
|
||||
const EVENT event = {
|
||||
.name = "key_down",
|
||||
.sender = NULL,
|
||||
.data = (void *)UI_TranslateInput(key),
|
||||
};
|
||||
UI_Events_Fire(&event);
|
||||
}
|
||||
|
||||
void UI_HandleKeyUp(const uint32_t key)
|
||||
{
|
||||
const EVENT event = {
|
||||
.name = "key_up",
|
||||
.sender = NULL,
|
||||
.data = (void *)UI_TranslateInput(key),
|
||||
};
|
||||
UI_Events_Fire(&event);
|
||||
}
|
||||
|
||||
void UI_HandleTextEdit(const char *const text)
|
||||
{
|
||||
const EVENT event = {
|
||||
.name = "text_edit",
|
||||
.sender = NULL,
|
||||
.data = (void *)text,
|
||||
};
|
||||
UI_Events_Fire(&event);
|
||||
}
|
49
src/libtrx/game/ui/events.c
Normal file
49
src/libtrx/game/ui/events.c
Normal file
|
@ -0,0 +1,49 @@
|
|||
#include "game/ui/events.h"
|
||||
|
||||
#include "config/common.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
static EVENT_MANAGER *m_EventManager = NULL;
|
||||
|
||||
static void M_HandleConfigChange(const EVENT *event, void *data);
|
||||
|
||||
static void M_HandleConfigChange(const EVENT *const event, void *const data)
|
||||
{
|
||||
const EVENT new_event = {
|
||||
.name = "canvas_resize",
|
||||
.sender = NULL,
|
||||
.data = NULL,
|
||||
};
|
||||
EventManager_Fire(m_EventManager, &new_event);
|
||||
}
|
||||
|
||||
void UI_Events_Init(void)
|
||||
{
|
||||
m_EventManager = EventManager_Create();
|
||||
Config_SubscribeChanges(M_HandleConfigChange, NULL);
|
||||
}
|
||||
|
||||
void UI_Events_Shutdown(void)
|
||||
{
|
||||
EventManager_Free(m_EventManager);
|
||||
m_EventManager = NULL;
|
||||
}
|
||||
|
||||
int32_t UI_Events_Subscribe(
|
||||
const char *const event_name, const UI_WIDGET *const sender,
|
||||
const EVENT_LISTENER listener, void *const user_data)
|
||||
{
|
||||
return EventManager_Subscribe(
|
||||
m_EventManager, event_name, sender, listener, user_data);
|
||||
}
|
||||
|
||||
void UI_Events_Unsubscribe(const int32_t listener_id)
|
||||
{
|
||||
EventManager_Unsubscribe(m_EventManager, listener_id);
|
||||
}
|
||||
|
||||
void UI_Events_Fire(const EVENT *const event)
|
||||
{
|
||||
EventManager_Fire(m_EventManager, event);
|
||||
}
|
235
src/libtrx/game/ui/widgets/console.c
Normal file
235
src/libtrx/game/ui/widgets/console.c
Normal file
|
@ -0,0 +1,235 @@
|
|||
#include "game/ui/widgets/console.h"
|
||||
|
||||
#include "game/clock.h"
|
||||
#include "game/console/common.h"
|
||||
#include "game/ui/common.h"
|
||||
#include "game/ui/events.h"
|
||||
#include "game/ui/widgets/label.h"
|
||||
#include "game/ui/widgets/prompt.h"
|
||||
#include "game/ui/widgets/spacer.h"
|
||||
#include "game/ui/widgets/stack.h"
|
||||
#include "memory.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#define WINDOW_MARGIN 5
|
||||
#define LOG_HEIGHT 16
|
||||
#define LOG_MARGIN 10
|
||||
#define MAX_LOG_LINES 20
|
||||
#define LOG_SCALE 0.8
|
||||
#define DELAY_PER_CHAR 0.2
|
||||
|
||||
typedef struct {
|
||||
UI_WIDGET_VTABLE vtable;
|
||||
UI_WIDGET *container;
|
||||
UI_WIDGET *prompt;
|
||||
UI_WIDGET *spacer;
|
||||
char *log_lines;
|
||||
int32_t logs_on_screen;
|
||||
|
||||
int32_t listener1;
|
||||
int32_t listener2;
|
||||
int32_t listener3;
|
||||
|
||||
struct {
|
||||
double expire_at;
|
||||
UI_WIDGET *label;
|
||||
} logs[MAX_LOG_LINES];
|
||||
} UI_CONSOLE;
|
||||
|
||||
static void M_HandlePromptCancel(const EVENT *event, void *data);
|
||||
static void M_HandlePromptConfirm(const EVENT *event, void *data);
|
||||
static void M_HandleCanvasResize(const EVENT *event, void *data);
|
||||
static void M_UpdateLogCount(UI_CONSOLE *self);
|
||||
|
||||
static int32_t M_GetWidth(const UI_CONSOLE *self);
|
||||
static int32_t M_GetHeight(const UI_CONSOLE *self);
|
||||
static void M_SetPosition(UI_CONSOLE *self, int32_t x, int32_t y);
|
||||
static void M_Control(UI_CONSOLE *self);
|
||||
static void M_Draw(UI_CONSOLE *self);
|
||||
static void M_Free(UI_CONSOLE *self);
|
||||
|
||||
static void M_HandlePromptCancel(const EVENT *const event, void *const data)
|
||||
{
|
||||
Console_Close();
|
||||
}
|
||||
|
||||
static void M_HandlePromptConfirm(const EVENT *const event, void *const data)
|
||||
{
|
||||
const char *text = event->data;
|
||||
Console_Eval(text);
|
||||
Console_Close();
|
||||
}
|
||||
|
||||
static void M_HandleCanvasResize(const EVENT *event, void *data)
|
||||
{
|
||||
UI_CONSOLE *const self = (UI_CONSOLE *)data;
|
||||
UI_Stack_SetSize(self->container, M_GetWidth(self), M_GetHeight(self));
|
||||
}
|
||||
|
||||
static void M_UpdateLogCount(UI_CONSOLE *const self)
|
||||
{
|
||||
self->logs_on_screen = 0;
|
||||
for (int32_t i = MAX_LOG_LINES - 1; i >= 0; i--) {
|
||||
if (self->logs[i].expire_at) {
|
||||
self->logs_on_screen = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static int32_t M_GetWidth(const UI_CONSOLE *const self)
|
||||
{
|
||||
return UI_GetCanvasWidth() - 2 * WINDOW_MARGIN;
|
||||
}
|
||||
|
||||
static int32_t M_GetHeight(const UI_CONSOLE *const self)
|
||||
{
|
||||
return UI_GetCanvasHeight() - 2 * WINDOW_MARGIN;
|
||||
}
|
||||
|
||||
static void M_SetPosition(UI_CONSOLE *const self, int32_t x, int32_t y)
|
||||
{
|
||||
return self->container->set_position(self->container, x, y);
|
||||
}
|
||||
|
||||
static void M_Control(UI_CONSOLE *const self)
|
||||
{
|
||||
if (self->container->control != NULL) {
|
||||
self->container->control(self->container);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Draw(UI_CONSOLE *const self)
|
||||
{
|
||||
if (self->container->draw != NULL) {
|
||||
self->container->draw(self->container);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Free(UI_CONSOLE *const self)
|
||||
{
|
||||
self->spacer->free(self->spacer);
|
||||
self->prompt->free(self->prompt);
|
||||
self->container->free(self->container);
|
||||
UI_Events_Unsubscribe(self->listener1);
|
||||
UI_Events_Unsubscribe(self->listener2);
|
||||
UI_Events_Unsubscribe(self->listener3);
|
||||
Memory_Free(self);
|
||||
}
|
||||
|
||||
UI_WIDGET *UI_Console_Create(void)
|
||||
{
|
||||
UI_CONSOLE *const self = Memory_Alloc(sizeof(UI_CONSOLE));
|
||||
self->vtable = (UI_WIDGET_VTABLE) {
|
||||
.control = (UI_WIDGET_CONTROL)M_Control,
|
||||
.draw = (UI_WIDGET_DRAW)M_Draw,
|
||||
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
|
||||
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
|
||||
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
|
||||
.free = (UI_WIDGET_FREE)M_Free,
|
||||
};
|
||||
|
||||
self->container = UI_Stack_Create(
|
||||
UI_STACK_LAYOUT_VERTICAL, M_GetWidth(self), M_GetHeight(self));
|
||||
UI_Stack_SetVAlign(self->container, UI_STACK_V_ALIGN_BOTTOM);
|
||||
|
||||
for (int32_t i = MAX_LOG_LINES - 1; i >= 0; i--) {
|
||||
self->logs[i].label =
|
||||
UI_Label_Create("", UI_LABEL_AUTO_SIZE, LOG_HEIGHT * LOG_SCALE);
|
||||
UI_Label_SetScale(self->logs[i].label, LOG_SCALE);
|
||||
UI_Stack_AddChild(self->container, self->logs[i].label);
|
||||
}
|
||||
|
||||
self->spacer = UI_Spacer_Create(LOG_MARGIN, LOG_MARGIN);
|
||||
UI_Stack_AddChild(self->container, self->spacer);
|
||||
|
||||
self->prompt = UI_Prompt_Create(UI_LABEL_AUTO_SIZE, UI_LABEL_AUTO_SIZE);
|
||||
UI_Stack_AddChild(self->container, self->prompt);
|
||||
|
||||
M_SetPosition(self, WINDOW_MARGIN, WINDOW_MARGIN);
|
||||
|
||||
self->listener1 = UI_Events_Subscribe(
|
||||
"confirm", self->prompt, M_HandlePromptConfirm, NULL);
|
||||
self->listener2 =
|
||||
UI_Events_Subscribe("cancel", self->prompt, M_HandlePromptCancel, NULL);
|
||||
self->listener3 =
|
||||
UI_Events_Subscribe("canvas_resize", NULL, M_HandleCanvasResize, self);
|
||||
|
||||
return (UI_WIDGET *)self;
|
||||
}
|
||||
|
||||
void UI_Console_HandleOpen(UI_WIDGET *const widget)
|
||||
{
|
||||
UI_CONSOLE *const self = (UI_CONSOLE *)widget;
|
||||
UI_Prompt_SetFocus(self->prompt, true);
|
||||
}
|
||||
|
||||
void UI_Console_HandleClose(UI_WIDGET *const widget)
|
||||
{
|
||||
UI_CONSOLE *const self = (UI_CONSOLE *)widget;
|
||||
UI_Prompt_SetFocus(self->prompt, false);
|
||||
UI_Prompt_Clear(self->prompt);
|
||||
}
|
||||
|
||||
void UI_Console_HandleLog(UI_WIDGET *const widget, const char *const text)
|
||||
{
|
||||
UI_CONSOLE *const self = (UI_CONSOLE *)widget;
|
||||
|
||||
int32_t dst_idx = -1;
|
||||
for (int32_t i = MAX_LOG_LINES - 1; i > 0; i--) {
|
||||
if (self->logs[i].label == NULL) {
|
||||
continue;
|
||||
}
|
||||
UI_Label_ChangeText(
|
||||
self->logs[i].label, UI_Label_GetText(self->logs[i - 1].label));
|
||||
self->logs[i].expire_at = self->logs[i - 1].expire_at;
|
||||
}
|
||||
|
||||
if (self->logs[0].label == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
self->logs[0].expire_at =
|
||||
Clock_GetHighPrecisionCounter() + 1000 * strlen(text) * DELAY_PER_CHAR;
|
||||
UI_Label_ChangeText(self->logs[0].label, text);
|
||||
|
||||
UI_Stack_DoLayout(self->container);
|
||||
M_UpdateLogCount(self);
|
||||
}
|
||||
|
||||
void UI_Console_ScrollLogs(UI_WIDGET *const widget)
|
||||
{
|
||||
UI_CONSOLE *const self = (UI_CONSOLE *)widget;
|
||||
|
||||
int32_t i = MAX_LOG_LINES - 1;
|
||||
while (i >= 0 && !self->logs[i].expire_at) {
|
||||
i--;
|
||||
}
|
||||
|
||||
bool need_layout = false;
|
||||
while (i >= 0 && self->logs[i].expire_at
|
||||
&& Clock_GetHighPrecisionCounter() >= self->logs[i].expire_at) {
|
||||
self->logs[i].expire_at = 0;
|
||||
UI_Label_ChangeText(self->logs[i].label, "");
|
||||
need_layout = true;
|
||||
i--;
|
||||
}
|
||||
|
||||
if (need_layout) {
|
||||
M_UpdateLogCount(self);
|
||||
UI_Stack_DoLayout(self->container);
|
||||
}
|
||||
}
|
||||
|
||||
int32_t UI_Console_GetVisibleLogCount(UI_WIDGET *const widget)
|
||||
{
|
||||
UI_CONSOLE *const self = (UI_CONSOLE *)widget;
|
||||
return self->logs_on_screen;
|
||||
}
|
||||
|
||||
int32_t UI_Console_GetMaxLogCount(UI_WIDGET *const widget)
|
||||
{
|
||||
return MAX_LOG_LINES;
|
||||
}
|
307
src/libtrx/game/ui/widgets/prompt.c
Normal file
307
src/libtrx/game/ui/widgets/prompt.c
Normal file
|
@ -0,0 +1,307 @@
|
|||
#include "game/ui/widgets/prompt.h"
|
||||
|
||||
#include "game/input.h"
|
||||
#include "game/ui/common.h"
|
||||
#include "game/ui/events.h"
|
||||
#include "game/ui/widgets/label.h"
|
||||
#include "memory.h"
|
||||
#include "strings.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
static const char m_ValidPromptChars[] =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.- ";
|
||||
|
||||
typedef struct {
|
||||
UI_WIDGET_VTABLE vtable;
|
||||
UI_WIDGET *label;
|
||||
UI_WIDGET *caret;
|
||||
|
||||
int32_t listener1;
|
||||
int32_t listener2;
|
||||
|
||||
struct {
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
} pos;
|
||||
bool is_focused;
|
||||
int32_t current_text_capacity;
|
||||
char *current_text;
|
||||
int32_t caret_pos;
|
||||
} UI_PROMPT;
|
||||
|
||||
static void M_UpdatePromptLabel(UI_PROMPT *self);
|
||||
static void M_UpdateCaretLabel(UI_PROMPT *self);
|
||||
static void M_MoveCaretLeft(UI_PROMPT *self);
|
||||
static void M_MoveCaretRight(UI_PROMPT *self);
|
||||
static void M_MoveCaretStart(UI_PROMPT *self);
|
||||
static void M_MoveCaretEnd(UI_PROMPT *self);
|
||||
static void M_DeleteCharBack(UI_PROMPT *self);
|
||||
static void M_Confirm(UI_PROMPT *self);
|
||||
static void M_Cancel(UI_PROMPT *self);
|
||||
static void M_Clear(UI_PROMPT *self);
|
||||
|
||||
static int32_t M_GetWidth(const UI_PROMPT *self);
|
||||
static int32_t M_GetHeight(const UI_PROMPT *self);
|
||||
static void M_SetPosition(UI_PROMPT *self, int32_t x, int32_t y);
|
||||
static void M_Control(UI_PROMPT *self);
|
||||
static void M_Draw(UI_PROMPT *self);
|
||||
static void M_Free(UI_PROMPT *self);
|
||||
static void M_HandleKeyDown(const EVENT *event, void *user_data);
|
||||
static void M_HandleTextEdit(const EVENT *event, void *user_data);
|
||||
|
||||
static void M_UpdatePromptLabel(UI_PROMPT *const self)
|
||||
{
|
||||
UI_Label_ChangeText(self->label, self->current_text);
|
||||
}
|
||||
|
||||
static void M_UpdateCaretLabel(UI_PROMPT *const self)
|
||||
{
|
||||
const char old = self->current_text[self->caret_pos];
|
||||
self->current_text[self->caret_pos] = '\0';
|
||||
UI_Label_ChangeText(self->label, self->current_text);
|
||||
const int32_t width = UI_Label_MeasureTextWidth(self->label);
|
||||
self->current_text[self->caret_pos] = old;
|
||||
UI_Label_ChangeText(self->label, self->current_text);
|
||||
|
||||
self->caret->set_position(self->caret, self->pos.x + width, self->pos.y);
|
||||
}
|
||||
|
||||
static int32_t M_GetWidth(const UI_PROMPT *const self)
|
||||
{
|
||||
return self->label->get_width(self->label);
|
||||
}
|
||||
|
||||
static int32_t M_GetHeight(const UI_PROMPT *const self)
|
||||
{
|
||||
return self->label->get_height(self->label);
|
||||
}
|
||||
|
||||
static void M_SetPosition(
|
||||
UI_PROMPT *const self, const int32_t x, const int32_t y)
|
||||
{
|
||||
self->pos.x = x;
|
||||
self->pos.y = y;
|
||||
self->label->set_position(self->label, x, y);
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
static void M_Control(UI_PROMPT *const self)
|
||||
{
|
||||
if (self->label->control != NULL) {
|
||||
self->label->control(self->label);
|
||||
}
|
||||
if (self->caret->control != NULL) {
|
||||
self->caret->control(self->caret);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Draw(UI_PROMPT *const self)
|
||||
{
|
||||
if (self->label->draw != NULL) {
|
||||
self->label->draw(self->label);
|
||||
}
|
||||
if (self->caret->draw != NULL) {
|
||||
self->caret->draw(self->caret);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Free(UI_PROMPT *const self)
|
||||
{
|
||||
self->label->free(self->label);
|
||||
self->caret->free(self->caret);
|
||||
UI_Events_Unsubscribe(self->listener1);
|
||||
UI_Events_Unsubscribe(self->listener2);
|
||||
Memory_FreePointer(&self->current_text);
|
||||
Memory_Free(self);
|
||||
}
|
||||
|
||||
static void M_MoveCaretLeft(UI_PROMPT *const self)
|
||||
{
|
||||
if (self->caret_pos > 0) {
|
||||
self->caret_pos--;
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_MoveCaretRight(UI_PROMPT *const self)
|
||||
{
|
||||
if (self->caret_pos < (int32_t)strlen(self->current_text)) {
|
||||
self->caret_pos++;
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
}
|
||||
|
||||
static void M_MoveCaretStart(UI_PROMPT *const self)
|
||||
{
|
||||
self->caret_pos = 0;
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
static void M_MoveCaretEnd(UI_PROMPT *const self)
|
||||
{
|
||||
self->caret_pos = strlen(self->current_text);
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
static void M_DeleteCharBack(UI_PROMPT *const self)
|
||||
{
|
||||
if (self->caret_pos <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
memmove(
|
||||
self->current_text + self->caret_pos - 1,
|
||||
self->current_text + self->caret_pos,
|
||||
strlen(self->current_text) + 1 - self->caret_pos);
|
||||
|
||||
self->caret_pos--;
|
||||
M_UpdatePromptLabel(self);
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
static void M_Confirm(UI_PROMPT *const self)
|
||||
{
|
||||
if (String_IsEmpty(self->current_text)) {
|
||||
M_Cancel(self);
|
||||
return;
|
||||
}
|
||||
const EVENT event = {
|
||||
.name = "confirm",
|
||||
.sender = self,
|
||||
.data = self->current_text,
|
||||
};
|
||||
UI_Events_Fire(&event);
|
||||
M_Clear(self);
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
static void M_Cancel(UI_PROMPT *const self)
|
||||
{
|
||||
const EVENT event = {
|
||||
.name = "cancel",
|
||||
.sender = self,
|
||||
.data = self->current_text,
|
||||
};
|
||||
UI_Events_Fire(&event);
|
||||
M_Clear(self);
|
||||
}
|
||||
|
||||
static void M_Clear(UI_PROMPT *const self)
|
||||
{
|
||||
strcpy(self->current_text, "");
|
||||
self->caret_pos = 0;
|
||||
M_UpdatePromptLabel(self);
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
static void M_HandleKeyDown(const EVENT *const event, void *const user_data)
|
||||
{
|
||||
const UI_INPUT key = (UI_INPUT)(uintptr_t)event->data;
|
||||
UI_PROMPT *const self = user_data;
|
||||
|
||||
if (!self->is_focused) {
|
||||
return;
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
switch (key) {
|
||||
case UI_KEY_LEFT: M_MoveCaretLeft(self); break;
|
||||
case UI_KEY_RIGHT: M_MoveCaretRight(self); break;
|
||||
case UI_KEY_HOME: M_MoveCaretStart(self); break;
|
||||
case UI_KEY_END: M_MoveCaretEnd(self); break;
|
||||
case UI_KEY_BACK: M_DeleteCharBack(self); break;
|
||||
case UI_KEY_RETURN: M_Confirm(self); break;
|
||||
case UI_KEY_ESCAPE: M_Cancel(self); break;
|
||||
}
|
||||
// clang-format on
|
||||
}
|
||||
|
||||
static void M_HandleTextEdit(const EVENT *const event, void *const user_data)
|
||||
{
|
||||
const char *insert_string = event->data;
|
||||
const size_t insert_length = strlen(insert_string);
|
||||
UI_PROMPT *const self = user_data;
|
||||
|
||||
if (!self->is_focused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strlen(insert_string) != 1
|
||||
|| !strstr(m_ValidPromptChars, insert_string)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t available_space =
|
||||
self->current_text_capacity - strlen(self->current_text);
|
||||
if (insert_length >= available_space) {
|
||||
self->current_text_capacity *= 2;
|
||||
self->current_text =
|
||||
Memory_Realloc(self->current_text, self->current_text_capacity);
|
||||
}
|
||||
|
||||
memmove(
|
||||
self->current_text + self->caret_pos + insert_length,
|
||||
self->current_text + self->caret_pos,
|
||||
strlen(self->current_text) + 1 - self->caret_pos);
|
||||
memcpy(self->current_text + self->caret_pos, insert_string, insert_length);
|
||||
|
||||
self->caret_pos += insert_length;
|
||||
M_UpdatePromptLabel(self);
|
||||
M_UpdateCaretLabel(self);
|
||||
}
|
||||
|
||||
UI_WIDGET *UI_Prompt_Create(const int32_t width, const int32_t height)
|
||||
{
|
||||
UI_PROMPT *const self = Memory_Alloc(sizeof(UI_PROMPT));
|
||||
self->vtable = (UI_WIDGET_VTABLE) {
|
||||
.control = (UI_WIDGET_CONTROL)M_Control,
|
||||
.draw = (UI_WIDGET_DRAW)M_Draw,
|
||||
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
|
||||
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
|
||||
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
|
||||
.free = (UI_WIDGET_FREE)M_Free,
|
||||
};
|
||||
|
||||
self->current_text_capacity = 1;
|
||||
self->current_text = Memory_Alloc(self->current_text_capacity);
|
||||
self->label = UI_Label_Create(self->current_text, width, height);
|
||||
self->caret = UI_Label_Create("", width, height);
|
||||
UI_Label_SetZIndex(self->label, 16);
|
||||
UI_Label_SetZIndex(self->caret, 8);
|
||||
self->is_focused = false;
|
||||
|
||||
self->listener1 =
|
||||
UI_Events_Subscribe("key_down", NULL, M_HandleKeyDown, self);
|
||||
self->listener2 =
|
||||
UI_Events_Subscribe("text_edit", NULL, M_HandleTextEdit, self);
|
||||
|
||||
return (UI_WIDGET *)self;
|
||||
}
|
||||
|
||||
void UI_Prompt_SetSize(
|
||||
UI_WIDGET *const widget, const int32_t width, const int32_t height)
|
||||
{
|
||||
UI_PROMPT *const self = (UI_PROMPT *)widget;
|
||||
UI_Label_SetSize(self->label, width, height);
|
||||
}
|
||||
|
||||
void UI_Prompt_SetFocus(UI_WIDGET *const widget, const bool is_focused)
|
||||
{
|
||||
UI_PROMPT *const self = (UI_PROMPT *)widget;
|
||||
self->is_focused = is_focused;
|
||||
if (is_focused) {
|
||||
Input_EnterListenMode();
|
||||
UI_Label_ChangeText(self->caret, UI_Prompt_GetPromptChar());
|
||||
UI_Label_Flash(self->caret, 1, UI_Prompt_GetCaretFlashRate());
|
||||
} else {
|
||||
Input_ExitListenMode();
|
||||
UI_Label_ChangeText(self->caret, "");
|
||||
}
|
||||
}
|
||||
|
||||
void UI_Prompt_Clear(UI_WIDGET *const widget)
|
||||
{
|
||||
UI_PROMPT *const self = (UI_PROMPT *)widget;
|
||||
M_Clear(self);
|
||||
}
|
60
src/libtrx/game/ui/widgets/spacer.c
Normal file
60
src/libtrx/game/ui/widgets/spacer.c
Normal file
|
@ -0,0 +1,60 @@
|
|||
#include "game/ui/widgets/spacer.h"
|
||||
|
||||
#include "memory.h"
|
||||
|
||||
typedef struct {
|
||||
UI_WIDGET_VTABLE vtable;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
} UI_SPACER;
|
||||
|
||||
static int32_t M_GetWidth(const UI_SPACER *self);
|
||||
static int32_t M_GetHeight(const UI_SPACER *self);
|
||||
static void M_SetPosition(UI_SPACER *self, int32_t x, int32_t y);
|
||||
static void M_Control(UI_SPACER *self);
|
||||
static void M_Draw(UI_SPACER *self);
|
||||
static void M_Free(UI_SPACER *self);
|
||||
|
||||
static int32_t M_GetWidth(const UI_SPACER *const self)
|
||||
{
|
||||
return self->width;
|
||||
}
|
||||
|
||||
static int32_t M_GetHeight(const UI_SPACER *const self)
|
||||
{
|
||||
return self->height;
|
||||
}
|
||||
|
||||
static void M_SetPosition(
|
||||
UI_SPACER *const self, const int32_t x, const int32_t y)
|
||||
{
|
||||
}
|
||||
|
||||
static void M_Free(UI_SPACER *const self)
|
||||
{
|
||||
Memory_Free(self);
|
||||
}
|
||||
|
||||
UI_WIDGET *UI_Spacer_Create(const int32_t width, const int32_t height)
|
||||
{
|
||||
UI_SPACER *const self = Memory_Alloc(sizeof(UI_SPACER));
|
||||
self->vtable = (UI_WIDGET_VTABLE) {
|
||||
.control = NULL,
|
||||
.draw = NULL,
|
||||
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
|
||||
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
|
||||
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
|
||||
.free = (UI_WIDGET_FREE)M_Free,
|
||||
};
|
||||
self->width = width;
|
||||
self->height = height;
|
||||
return (UI_WIDGET *)self;
|
||||
}
|
||||
|
||||
void UI_Spacer_SetSize(
|
||||
UI_WIDGET *const widget, const int32_t width, const int32_t height)
|
||||
{
|
||||
UI_SPACER *const self = (UI_SPACER *)widget;
|
||||
self->width = width;
|
||||
self->height = height;
|
||||
}
|
255
src/libtrx/game/ui/widgets/stack.c
Normal file
255
src/libtrx/game/ui/widgets/stack.c
Normal file
|
@ -0,0 +1,255 @@
|
|||
#include "game/ui/widgets/stack.h"
|
||||
|
||||
#include "memory.h"
|
||||
#include "utils.h"
|
||||
#include "vector.h"
|
||||
|
||||
typedef struct {
|
||||
UI_WIDGET_VTABLE vtable;
|
||||
|
||||
struct {
|
||||
UI_STACK_H_ALIGN h;
|
||||
UI_STACK_V_ALIGN v;
|
||||
} align;
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
int32_t x;
|
||||
int32_t y;
|
||||
UI_STACK_LAYOUT layout;
|
||||
VECTOR *children;
|
||||
} UI_STACK;
|
||||
|
||||
static int32_t M_GetChildrenWidth(const UI_STACK *self);
|
||||
static int32_t M_GetChildrenHeight(const UI_STACK *self);
|
||||
static int32_t M_GetHeight(const UI_STACK *self);
|
||||
static int32_t M_GetWidth(const UI_STACK *self);
|
||||
static void M_SetPosition(UI_STACK *self, int32_t x, int32_t y);
|
||||
static void M_Control(UI_STACK *self);
|
||||
static void M_Draw(UI_STACK *self);
|
||||
static void M_Free(UI_STACK *self);
|
||||
|
||||
static int32_t M_GetChildrenWidth(const UI_STACK *const self)
|
||||
{
|
||||
int32_t result = 0;
|
||||
for (int32_t i = 0; i < self->children->count; i++) {
|
||||
const UI_WIDGET *const child =
|
||||
*(UI_WIDGET **)Vector_Get(self->children, i);
|
||||
switch (self->layout) {
|
||||
case UI_STACK_LAYOUT_HORIZONTAL:
|
||||
result += child->get_width(child);
|
||||
break;
|
||||
case UI_STACK_LAYOUT_VERTICAL:
|
||||
result = MAX(result, child->get_width(child));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static int32_t M_GetChildrenHeight(const UI_STACK *const self)
|
||||
{
|
||||
int32_t result = 0;
|
||||
for (int32_t i = 0; i < self->children->count; i++) {
|
||||
const UI_WIDGET *const child =
|
||||
*(UI_WIDGET **)Vector_Get(self->children, i);
|
||||
switch (self->layout) {
|
||||
case UI_STACK_LAYOUT_HORIZONTAL:
|
||||
result = MAX(result, child->get_height(child));
|
||||
break;
|
||||
case UI_STACK_LAYOUT_VERTICAL:
|
||||
result += child->get_height(child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static int32_t M_GetWidth(const UI_STACK *const self)
|
||||
{
|
||||
if (self->width != UI_STACK_AUTO_SIZE) {
|
||||
return self->width;
|
||||
}
|
||||
return M_GetChildrenWidth(self);
|
||||
}
|
||||
|
||||
static int32_t M_GetHeight(const UI_STACK *const self)
|
||||
{
|
||||
if (self->height != UI_STACK_AUTO_SIZE) {
|
||||
return self->height;
|
||||
}
|
||||
return M_GetChildrenHeight(self);
|
||||
}
|
||||
|
||||
static void M_SetPosition(
|
||||
UI_STACK *const self, const int32_t x, const int32_t y)
|
||||
{
|
||||
self->x = x;
|
||||
self->y = y;
|
||||
UI_Stack_DoLayout((UI_WIDGET *)self);
|
||||
}
|
||||
|
||||
static void M_Control(UI_STACK *const self)
|
||||
{
|
||||
for (int32_t i = 0; i < self->children->count; i++) {
|
||||
UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i);
|
||||
if (child->control != NULL) {
|
||||
child->control(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Draw(UI_STACK *const self)
|
||||
{
|
||||
for (int32_t i = 0; i < self->children->count; i++) {
|
||||
UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i);
|
||||
if (child->draw != NULL) {
|
||||
child->draw(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Free(UI_STACK *const self)
|
||||
{
|
||||
Vector_Free(self->children);
|
||||
Memory_Free(self);
|
||||
}
|
||||
|
||||
void UI_Stack_AddChild(UI_WIDGET *const widget, UI_WIDGET *const child)
|
||||
{
|
||||
UI_STACK *const self = (UI_STACK *)widget;
|
||||
Vector_Add(self->children, (void *)&child);
|
||||
}
|
||||
|
||||
UI_WIDGET *UI_Stack_Create(
|
||||
const UI_STACK_LAYOUT layout, const int32_t width, const int32_t height)
|
||||
{
|
||||
UI_STACK *const self = Memory_Alloc(sizeof(UI_STACK));
|
||||
self->vtable = (UI_WIDGET_VTABLE) {
|
||||
.control = (UI_WIDGET_CONTROL)M_Control,
|
||||
.draw = (UI_WIDGET_DRAW)M_Draw,
|
||||
.get_width = (UI_WIDGET_GET_WIDTH)M_GetWidth,
|
||||
.get_height = (UI_WIDGET_GET_HEIGHT)M_GetHeight,
|
||||
.set_position = (UI_WIDGET_SET_POSITION)M_SetPosition,
|
||||
.free = (UI_WIDGET_FREE)M_Free,
|
||||
};
|
||||
|
||||
self->align.h = UI_STACK_H_ALIGN_LEFT;
|
||||
self->align.v = UI_STACK_V_ALIGN_TOP;
|
||||
self->width = width;
|
||||
self->height = height;
|
||||
self->layout = layout;
|
||||
self->children = Vector_Create(sizeof(UI_WIDGET *));
|
||||
return (UI_WIDGET *)self;
|
||||
}
|
||||
|
||||
void UI_Stack_SetHAlign(UI_WIDGET *const widget, const UI_STACK_H_ALIGN align)
|
||||
{
|
||||
UI_STACK *const self = (UI_STACK *)widget;
|
||||
self->align.h = align;
|
||||
}
|
||||
|
||||
void UI_Stack_SetVAlign(UI_WIDGET *const widget, const UI_STACK_V_ALIGN align)
|
||||
{
|
||||
UI_STACK *const self = (UI_STACK *)widget;
|
||||
self->align.v = align;
|
||||
}
|
||||
|
||||
void UI_Stack_SetSize(
|
||||
UI_WIDGET *const widget, const int32_t width, const int32_t height)
|
||||
{
|
||||
UI_STACK *const self = (UI_STACK *)widget;
|
||||
self->width = width;
|
||||
self->height = height;
|
||||
UI_Stack_DoLayout(widget);
|
||||
}
|
||||
|
||||
void UI_Stack_DoLayout(UI_WIDGET *const widget)
|
||||
{
|
||||
UI_STACK *const self = (UI_STACK *)widget;
|
||||
const int32_t self_width = M_GetWidth(self);
|
||||
const int32_t self_height = M_GetHeight(self);
|
||||
const int32_t children_width = M_GetChildrenWidth(self);
|
||||
const int32_t children_height = M_GetChildrenHeight(self);
|
||||
|
||||
// calculate main axis placement
|
||||
int32_t x = -999;
|
||||
int32_t y = -999;
|
||||
switch (self->layout) {
|
||||
case UI_STACK_LAYOUT_HORIZONTAL:
|
||||
switch (self->align.h) {
|
||||
case UI_STACK_H_ALIGN_LEFT:
|
||||
x = self->x;
|
||||
break;
|
||||
case UI_STACK_H_ALIGN_CENTER:
|
||||
x = self->x + (self_width - children_width) / 2;
|
||||
break;
|
||||
case UI_STACK_H_ALIGN_RIGHT:
|
||||
x = self->x + self_width - children_width;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case UI_STACK_LAYOUT_VERTICAL:
|
||||
switch (self->align.v) {
|
||||
case UI_STACK_V_ALIGN_TOP:
|
||||
y = self->y;
|
||||
break;
|
||||
case UI_STACK_V_ALIGN_CENTER:
|
||||
y = self->y + (self_height - children_height) / 2;
|
||||
break;
|
||||
case UI_STACK_V_ALIGN_BOTTOM:
|
||||
y = self->y + self_height - children_height;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for (int32_t i = 0; i < self->children->count; i++) {
|
||||
UI_WIDGET *const child = *(UI_WIDGET **)Vector_Get(self->children, i);
|
||||
const int32_t child_width = child->get_width(child);
|
||||
const int32_t child_height = child->get_height(child);
|
||||
|
||||
// calculate other axis placement
|
||||
switch (self->layout) {
|
||||
case UI_STACK_LAYOUT_HORIZONTAL:
|
||||
switch (self->align.v) {
|
||||
case UI_STACK_V_ALIGN_TOP:
|
||||
y = self->y;
|
||||
break;
|
||||
case UI_STACK_V_ALIGN_CENTER:
|
||||
y = self->y + (self_height - child_height) / 2;
|
||||
break;
|
||||
case UI_STACK_V_ALIGN_BOTTOM:
|
||||
y = self->y + self_height - child_height;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case UI_STACK_LAYOUT_VERTICAL:
|
||||
switch (self->align.h) {
|
||||
case UI_STACK_H_ALIGN_LEFT:
|
||||
x = self->x;
|
||||
break;
|
||||
case UI_STACK_H_ALIGN_CENTER:
|
||||
x = self->x + (self_width - child_width) / 2;
|
||||
break;
|
||||
case UI_STACK_H_ALIGN_RIGHT:
|
||||
x = self->x + self_width - child_width;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
child->set_position(child, x, y);
|
||||
|
||||
// calculate main axis offset
|
||||
switch (self->layout) {
|
||||
case UI_STACK_LAYOUT_HORIZONTAL:
|
||||
x += child_width;
|
||||
break;
|
||||
case UI_STACK_LAYOUT_VERTICAL:
|
||||
y += child_height;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
118
src/libtrx/gfx/2d/2d_renderer.c
Normal file
118
src/libtrx/gfx/2d/2d_renderer.c
Normal file
|
@ -0,0 +1,118 @@
|
|||
#include "gfx/2d/2d_renderer.h"
|
||||
|
||||
#include "gfx/gl/gl_core_3_3.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
void GFX_2D_Renderer_Init(GFX_2D_RENDERER *renderer)
|
||||
{
|
||||
LOG_INFO("");
|
||||
assert(renderer);
|
||||
|
||||
GFX_GL_Buffer_Init(&renderer->surface_buffer, GL_ARRAY_BUFFER);
|
||||
GFX_GL_Buffer_Bind(&renderer->surface_buffer);
|
||||
GLfloat verts[] = { 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
|
||||
0.0, 1.0, 1.0, 0.0, 1.0, 1.0 };
|
||||
GFX_GL_Buffer_Data(
|
||||
&renderer->surface_buffer, sizeof(verts), verts, GL_STATIC_DRAW);
|
||||
|
||||
GFX_GL_VertexArray_Init(&renderer->surface_format);
|
||||
GFX_GL_VertexArray_Bind(&renderer->surface_format);
|
||||
GFX_GL_VertexArray_Attribute(
|
||||
&renderer->surface_format, 0, 2, GL_FLOAT, GL_FALSE, 0, 0);
|
||||
|
||||
GFX_GL_Texture_Init(&renderer->surface_texture, GL_TEXTURE_2D);
|
||||
|
||||
GFX_GL_Sampler_Init(&renderer->sampler);
|
||||
GFX_GL_Sampler_Bind(&renderer->sampler, 0);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
|
||||
GFX_GL_Program_Init(&renderer->program);
|
||||
GFX_GL_Program_AttachShader(
|
||||
&renderer->program, GL_VERTEX_SHADER, "shaders/2d.glsl");
|
||||
GFX_GL_Program_AttachShader(
|
||||
&renderer->program, GL_FRAGMENT_SHADER, "shaders/2d.glsl");
|
||||
GFX_GL_Program_Link(&renderer->program);
|
||||
GFX_GL_Program_FragmentData(&renderer->program, "fragColor");
|
||||
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_2D_Renderer_Close(GFX_2D_RENDERER *renderer)
|
||||
{
|
||||
LOG_INFO("");
|
||||
assert(renderer);
|
||||
|
||||
GFX_GL_VertexArray_Close(&renderer->surface_format);
|
||||
GFX_GL_Buffer_Close(&renderer->surface_buffer);
|
||||
GFX_GL_Texture_Close(&renderer->surface_texture);
|
||||
GFX_GL_Sampler_Close(&renderer->sampler);
|
||||
GFX_GL_Program_Close(&renderer->program);
|
||||
}
|
||||
|
||||
void GFX_2D_Renderer_Upload(
|
||||
GFX_2D_RENDERER *renderer, GFX_2D_SURFACE_DESC *desc, const uint8_t *data)
|
||||
{
|
||||
const uint32_t width = desc->width;
|
||||
const uint32_t height = desc->height;
|
||||
|
||||
GFX_GL_Texture_Bind(&renderer->surface_texture);
|
||||
|
||||
// TODO: implement texture packs
|
||||
|
||||
// update buffer if the size is unchanged, otherwise create a new one
|
||||
if (width != renderer->width || height != renderer->height) {
|
||||
renderer->width = width;
|
||||
renderer->height = height;
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D, 0, GL_RGBA, renderer->width, renderer->height, 0,
|
||||
desc->tex_format, desc->tex_type, data);
|
||||
GFX_GL_CheckError();
|
||||
} else {
|
||||
glTexSubImage2D(
|
||||
GL_TEXTURE_2D, 0, 0, 0, renderer->width, renderer->height,
|
||||
desc->tex_format, desc->tex_type, data);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_2D_Renderer_Render(GFX_2D_RENDERER *renderer)
|
||||
{
|
||||
GFX_GL_Program_Bind(&renderer->program);
|
||||
GFX_GL_Buffer_Bind(&renderer->surface_buffer);
|
||||
GFX_GL_VertexArray_Bind(&renderer->surface_format);
|
||||
GFX_GL_Texture_Bind(&renderer->surface_texture);
|
||||
GFX_GL_Sampler_Bind(&renderer->sampler, 0);
|
||||
|
||||
GLboolean blend = glIsEnabled(GL_BLEND);
|
||||
if (blend) {
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
|
||||
GLboolean depth_test = glIsEnabled(GL_DEPTH_TEST);
|
||||
if (depth_test) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (blend) {
|
||||
glEnable(GL_BLEND);
|
||||
}
|
||||
|
||||
if (depth_test) {
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
}
|
129
src/libtrx/gfx/2d/2d_surface.c
Normal file
129
src/libtrx/gfx/2d/2d_surface.c
Normal file
|
@ -0,0 +1,129 @@
|
|||
#include "gfx/2d/2d_surface.h"
|
||||
|
||||
#include "gfx/context.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
GFX_2D_SURFACE *GFX_2D_Surface_Create(const GFX_2D_SURFACE_DESC *desc)
|
||||
{
|
||||
GFX_2D_SURFACE *surface = Memory_Alloc(sizeof(GFX_2D_SURFACE));
|
||||
GFX_2D_Surface_Init(surface, desc);
|
||||
return surface;
|
||||
}
|
||||
|
||||
GFX_2D_SURFACE *GFX_2D_Surface_CreateFromImage(const IMAGE *image)
|
||||
{
|
||||
GFX_2D_SURFACE *surface = Memory_Alloc(sizeof(GFX_2D_SURFACE));
|
||||
surface->is_locked = false;
|
||||
surface->is_dirty = true;
|
||||
surface->desc.width = image->width;
|
||||
surface->desc.height = image->height;
|
||||
surface->desc.bit_count = 24;
|
||||
surface->desc.tex_format = GL_RGB;
|
||||
surface->desc.tex_type = GL_UNSIGNED_BYTE;
|
||||
surface->desc.pitch = surface->desc.width * (surface->desc.bit_count / 8);
|
||||
surface->desc.pixels = NULL;
|
||||
surface->buffer = Memory_Alloc(surface->desc.pitch * surface->desc.height);
|
||||
memcpy(
|
||||
surface->buffer, image->data,
|
||||
surface->desc.pitch * surface->desc.height);
|
||||
return surface;
|
||||
}
|
||||
|
||||
void GFX_2D_Surface_Free(GFX_2D_SURFACE *surface)
|
||||
{
|
||||
if (surface) {
|
||||
GFX_2D_Surface_Close(surface);
|
||||
Memory_FreePointer(&surface);
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_2D_Surface_Init(
|
||||
GFX_2D_SURFACE *surface, const GFX_2D_SURFACE_DESC *desc)
|
||||
{
|
||||
surface->is_locked = false;
|
||||
surface->is_dirty = false;
|
||||
surface->desc = *desc;
|
||||
|
||||
GFX_2D_SURFACE_DESC display_desc = {
|
||||
.bit_count = 32,
|
||||
.width = GFX_Context_GetDisplayWidth(),
|
||||
.height = GFX_Context_GetDisplayHeight(),
|
||||
};
|
||||
|
||||
if (!surface->desc.width || !surface->desc.height) {
|
||||
surface->desc.width = display_desc.width;
|
||||
surface->desc.height = display_desc.height;
|
||||
}
|
||||
|
||||
if (!surface->desc.bit_count) {
|
||||
surface->desc.bit_count = display_desc.bit_count;
|
||||
}
|
||||
|
||||
if (!surface->desc.tex_format) {
|
||||
surface->desc.tex_format = GL_BGRA;
|
||||
}
|
||||
if (!surface->desc.tex_type) {
|
||||
surface->desc.tex_type = GL_UNSIGNED_INT_8_8_8_8_REV;
|
||||
}
|
||||
|
||||
surface->desc.pitch = surface->desc.width * (surface->desc.bit_count / 8);
|
||||
|
||||
surface->buffer = Memory_Alloc(surface->desc.pitch * surface->desc.height);
|
||||
surface->desc.pixels = NULL;
|
||||
}
|
||||
|
||||
void GFX_2D_Surface_Close(GFX_2D_SURFACE *surface)
|
||||
{
|
||||
Memory_FreePointer(&surface->buffer);
|
||||
}
|
||||
|
||||
bool GFX_2D_Surface_Clear(GFX_2D_SURFACE *surface)
|
||||
{
|
||||
if (surface->is_locked) {
|
||||
LOG_ERROR("Surface is locked");
|
||||
return false;
|
||||
}
|
||||
|
||||
surface->is_dirty = true;
|
||||
memset(surface->buffer, 0, surface->desc.pitch * surface->desc.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GFX_2D_Surface_Lock(GFX_2D_SURFACE *surface, GFX_2D_SURFACE_DESC *out_desc)
|
||||
{
|
||||
assert(surface != NULL);
|
||||
if (surface->is_locked) {
|
||||
LOG_ERROR("Surface is busy");
|
||||
return false;
|
||||
}
|
||||
|
||||
// assign pixels
|
||||
surface->desc.pixels = surface->buffer;
|
||||
|
||||
surface->is_locked = true;
|
||||
surface->is_dirty = true;
|
||||
|
||||
*out_desc = surface->desc;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GFX_2D_Surface_Unlock(GFX_2D_SURFACE *surface)
|
||||
{
|
||||
// ensure that the surface is actually locked
|
||||
if (!surface->is_locked) {
|
||||
LOG_ERROR("Surface is not locked");
|
||||
return false;
|
||||
}
|
||||
|
||||
// unassign pixels
|
||||
surface->desc.pixels = NULL;
|
||||
|
||||
surface->is_locked = false;
|
||||
|
||||
return true;
|
||||
}
|
359
src/libtrx/gfx/3d/3d_renderer.c
Normal file
359
src/libtrx/gfx/3d/3d_renderer.c
Normal file
|
@ -0,0 +1,359 @@
|
|||
#include "gfx/3d/3d_renderer.h"
|
||||
|
||||
#include "gfx/context.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "log.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
|
||||
static void M_SelectTextureImpl(GFX_3D_RENDERER *renderer, int texture_num);
|
||||
|
||||
static void M_SelectTextureImpl(GFX_3D_RENDERER *renderer, int texture_num)
|
||||
{
|
||||
assert(renderer);
|
||||
|
||||
GFX_GL_TEXTURE *texture = NULL;
|
||||
if (texture_num == GFX_ENV_MAP_TEXTURE) {
|
||||
texture = renderer->env_map_texture;
|
||||
} else if (texture_num != GFX_NO_TEXTURE) {
|
||||
assert(texture_num >= 0);
|
||||
assert(texture_num < GFX_MAX_TEXTURES);
|
||||
texture = renderer->textures[texture_num];
|
||||
}
|
||||
|
||||
if (texture == NULL) {
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
GFX_GL_CheckError();
|
||||
return;
|
||||
}
|
||||
|
||||
GFX_GL_Texture_Bind(texture);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_Init(
|
||||
GFX_3D_RENDERER *renderer, const GFX_CONFIG *const config)
|
||||
{
|
||||
LOG_INFO("");
|
||||
assert(renderer);
|
||||
|
||||
renderer->config = config;
|
||||
|
||||
renderer->selected_texture_num = GFX_NO_TEXTURE;
|
||||
for (int i = 0; i < GFX_MAX_TEXTURES; i++) {
|
||||
renderer->textures[i] = NULL;
|
||||
}
|
||||
|
||||
GFX_GL_Sampler_Init(&renderer->sampler);
|
||||
GFX_GL_Sampler_Bind(&renderer->sampler, 0);
|
||||
GFX_GL_Sampler_Parameterf(
|
||||
&renderer->sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, 0);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
|
||||
GFX_GL_Program_Init(&renderer->program);
|
||||
GFX_GL_Program_AttachShader(
|
||||
&renderer->program, GL_VERTEX_SHADER, "shaders/3d.glsl");
|
||||
GFX_GL_Program_AttachShader(
|
||||
&renderer->program, GL_FRAGMENT_SHADER, "shaders/3d.glsl");
|
||||
GFX_GL_Program_Link(&renderer->program);
|
||||
|
||||
renderer->loc_mat_projection =
|
||||
GFX_GL_Program_UniformLocation(&renderer->program, "matProjection");
|
||||
renderer->loc_mat_model_view =
|
||||
GFX_GL_Program_UniformLocation(&renderer->program, "matModelView");
|
||||
renderer->loc_texturing_enabled =
|
||||
GFX_GL_Program_UniformLocation(&renderer->program, "texturingEnabled");
|
||||
renderer->loc_smoothing_enabled =
|
||||
GFX_GL_Program_UniformLocation(&renderer->program, "smoothingEnabled");
|
||||
|
||||
GFX_GL_Program_FragmentData(&renderer->program, "fragColor");
|
||||
GFX_GL_Program_Bind(&renderer->program);
|
||||
|
||||
// negate Z axis so the model is rendered behind the viewport, which is
|
||||
// better than having a negative z_near in the ortho matrix, which seems
|
||||
// to mess up depth testing
|
||||
GLfloat model_view[4][4] = {
|
||||
{ +1.0f, +0.0f, +0.0f, +0.0f },
|
||||
{ +0.0f, +1.0f, +0.0f, +0.0f },
|
||||
{ +0.0f, +0.0f, -1.0f, +0.0f },
|
||||
{ +0.0f, +0.0f, +0.0f, +1.0f },
|
||||
};
|
||||
GFX_GL_Program_UniformMatrix4fv(
|
||||
&renderer->program, renderer->loc_mat_model_view, 1, GL_FALSE,
|
||||
&model_view[0][0]);
|
||||
|
||||
GFX_3D_VertexStream_Init(&renderer->vertex_stream);
|
||||
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_Close(GFX_3D_RENDERER *renderer)
|
||||
{
|
||||
LOG_INFO("");
|
||||
assert(renderer);
|
||||
|
||||
GFX_3D_VertexStream_Close(&renderer->vertex_stream);
|
||||
GFX_GL_Program_Close(&renderer->program);
|
||||
GFX_GL_Sampler_Close(&renderer->sampler);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_RenderBegin(GFX_3D_RENDERER *renderer)
|
||||
{
|
||||
assert(renderer);
|
||||
glEnable(GL_BLEND);
|
||||
|
||||
glLineWidth(renderer->config->line_width);
|
||||
glPolygonMode(
|
||||
GL_FRONT_AND_BACK,
|
||||
renderer->config->enable_wireframe ? GL_LINE : GL_FILL);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
GFX_GL_Program_Bind(&renderer->program);
|
||||
GFX_3D_VertexStream_Bind(&renderer->vertex_stream);
|
||||
GFX_GL_Sampler_Bind(&renderer->sampler, 0);
|
||||
|
||||
GFX_3D_Renderer_RestoreTexture(renderer);
|
||||
|
||||
const float left = 0.0f;
|
||||
const float top = 0.0f;
|
||||
const float right = GFX_Context_GetDisplayWidth();
|
||||
const float bottom = GFX_Context_GetDisplayHeight();
|
||||
const float z_near = -1e6;
|
||||
const float z_far = 1e6;
|
||||
GLfloat projection[4][4] = {
|
||||
{ 2.0f / (right - left), 0.0f, 0.0f, 0.0f },
|
||||
{ 0.0f, 2.0f / (top - bottom), 0.0f, 0.0f },
|
||||
{ 0.0f, 0.0f, -2.0f / (z_far - z_near), 0.0f },
|
||||
{ -(right + left) / (right - left), -(top + bottom) / (top - bottom),
|
||||
-(z_far + z_near) / (z_far - z_near), 1.0f }
|
||||
};
|
||||
|
||||
GFX_GL_Program_UniformMatrix4fv(
|
||||
&renderer->program, renderer->loc_mat_projection, 1, GL_FALSE,
|
||||
&projection[0][0]);
|
||||
|
||||
glDepthFunc(GL_LEQUAL);
|
||||
glDepthMask(GL_TRUE);
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_RenderEnd(GFX_3D_RENDERER *renderer)
|
||||
{
|
||||
assert(renderer);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_ClearDepth(GFX_3D_RENDERER *renderer)
|
||||
{
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
int GFX_3D_Renderer_RegisterEnvironmentMap(GFX_3D_RENDERER *const renderer)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
assert(renderer->env_map_texture == NULL);
|
||||
|
||||
GFX_GL_TEXTURE *texture = GFX_GL_Texture_Create(GL_TEXTURE_2D);
|
||||
renderer->env_map_texture = texture;
|
||||
|
||||
GFX_3D_Renderer_RestoreTexture(renderer);
|
||||
GFX_GL_CheckError();
|
||||
return GFX_ENV_MAP_TEXTURE;
|
||||
}
|
||||
|
||||
bool GFX_3D_Renderer_UnregisterEnvironmentMap(
|
||||
GFX_3D_RENDERER *const renderer, const int texture_num)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
|
||||
GFX_GL_TEXTURE *const texture = renderer->env_map_texture;
|
||||
if (texture == NULL) {
|
||||
LOG_ERROR("No environment map registered");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (texture_num != GFX_ENV_MAP_TEXTURE) {
|
||||
LOG_ERROR("Invalid environment map texture ID");
|
||||
return false;
|
||||
}
|
||||
|
||||
// unbind texture if currently bound
|
||||
if (renderer->selected_texture_num == texture_num) {
|
||||
M_SelectTextureImpl(renderer, GFX_NO_TEXTURE);
|
||||
renderer->selected_texture_num = GFX_NO_TEXTURE;
|
||||
}
|
||||
|
||||
GFX_GL_Texture_Free(texture);
|
||||
renderer->env_map_texture = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_FillEnvironmentMap(GFX_3D_RENDERER *const renderer)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
|
||||
GFX_GL_TEXTURE *const env_map = renderer->env_map_texture;
|
||||
if (env_map != NULL) {
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
GFX_GL_Texture_LoadFromBackBuffer(env_map);
|
||||
GFX_3D_Renderer_RestoreTexture(renderer);
|
||||
}
|
||||
}
|
||||
|
||||
int GFX_3D_Renderer_RegisterTexturePage(
|
||||
GFX_3D_RENDERER *renderer, const void *data, int width, int height)
|
||||
{
|
||||
assert(renderer);
|
||||
assert(data);
|
||||
GFX_GL_TEXTURE *texture = GFX_GL_Texture_Create(GL_TEXTURE_2D);
|
||||
GFX_GL_Texture_Load(texture, data, width, height, GL_RGBA, GL_RGBA);
|
||||
|
||||
int texture_num = GFX_NO_TEXTURE;
|
||||
for (int i = 0; i < GFX_MAX_TEXTURES; i++) {
|
||||
if (!renderer->textures[i]) {
|
||||
renderer->textures[i] = texture;
|
||||
texture_num = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GFX_3D_Renderer_RestoreTexture(renderer);
|
||||
|
||||
GFX_GL_CheckError();
|
||||
return texture_num;
|
||||
}
|
||||
|
||||
bool GFX_3D_Renderer_UnregisterTexturePage(
|
||||
GFX_3D_RENDERER *renderer, int texture_num)
|
||||
{
|
||||
assert(renderer);
|
||||
assert(texture_num >= 0);
|
||||
assert(texture_num < GFX_MAX_TEXTURES);
|
||||
|
||||
GFX_GL_TEXTURE *texture = renderer->textures[texture_num];
|
||||
if (!texture) {
|
||||
LOG_ERROR("Invalid texture handle");
|
||||
return false;
|
||||
}
|
||||
|
||||
// unbind texture if currently bound
|
||||
if (texture_num == renderer->selected_texture_num) {
|
||||
M_SelectTextureImpl(renderer, GFX_NO_TEXTURE);
|
||||
renderer->selected_texture_num = GFX_NO_TEXTURE;
|
||||
}
|
||||
|
||||
GFX_GL_Texture_Free(texture);
|
||||
renderer->textures[texture_num] = NULL;
|
||||
return true;
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_RenderPrimStrip(
|
||||
GFX_3D_RENDERER *renderer, GFX_3D_VERTEX *vertices, int count)
|
||||
{
|
||||
assert(renderer);
|
||||
assert(vertices);
|
||||
GFX_3D_VertexStream_PushPrimStrip(
|
||||
&renderer->vertex_stream, vertices, count);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_RenderPrimFan(
|
||||
GFX_3D_RENDERER *renderer, GFX_3D_VERTEX *vertices, int count)
|
||||
{
|
||||
assert(renderer);
|
||||
assert(vertices);
|
||||
GFX_3D_VertexStream_PushPrimFan(&renderer->vertex_stream, vertices, count);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_RenderPrimList(
|
||||
GFX_3D_RENDERER *renderer, GFX_3D_VERTEX *vertices, int count)
|
||||
{
|
||||
assert(renderer);
|
||||
assert(vertices);
|
||||
GFX_3D_VertexStream_PushPrimList(&renderer->vertex_stream, vertices, count);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_SelectTexture(GFX_3D_RENDERER *renderer, int texture_num)
|
||||
{
|
||||
assert(renderer);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
renderer->selected_texture_num = texture_num;
|
||||
M_SelectTextureImpl(renderer, texture_num);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_RestoreTexture(GFX_3D_RENDERER *renderer)
|
||||
{
|
||||
assert(renderer);
|
||||
M_SelectTextureImpl(renderer, renderer->selected_texture_num);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_SetPrimType(
|
||||
GFX_3D_RENDERER *renderer, GFX_3D_PRIM_TYPE value)
|
||||
{
|
||||
assert(renderer);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
GFX_3D_VertexStream_SetPrimType(&renderer->vertex_stream, value);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_SetTextureFilter(
|
||||
GFX_3D_RENDERER *renderer, GFX_TEXTURE_FILTER filter)
|
||||
{
|
||||
assert(renderer);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_MAG_FILTER,
|
||||
filter == GFX_TF_BILINEAR ? GL_LINEAR : GL_NEAREST);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&renderer->sampler, GL_TEXTURE_MIN_FILTER,
|
||||
filter == GFX_TF_BILINEAR ? GL_LINEAR : GL_NEAREST);
|
||||
GFX_GL_Program_Uniform1i(
|
||||
&renderer->program, renderer->loc_smoothing_enabled,
|
||||
filter == GFX_TF_BILINEAR);
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_SetDepthTestEnabled(
|
||||
GFX_3D_RENDERER *renderer, bool is_enabled)
|
||||
{
|
||||
assert(renderer);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
if (is_enabled) {
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
} else {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_SetBlendingMode(
|
||||
GFX_3D_RENDERER *const renderer, const GFX_BLEND_MODE blend_mode)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
|
||||
switch (blend_mode) {
|
||||
case GFX_BLEND_MODE_OFF:
|
||||
glBlendFunc(GL_ONE, GL_ZERO);
|
||||
break;
|
||||
case GFX_BLEND_MODE_NORMAL:
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
break;
|
||||
case GFX_BLEND_MODE_MULTIPLY:
|
||||
glBlendFunc(GL_DST_COLOR, GL_SRC_COLOR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_3D_Renderer_SetTexturingEnabled(
|
||||
GFX_3D_RENDERER *renderer, bool is_enabled)
|
||||
{
|
||||
assert(renderer);
|
||||
GFX_3D_VertexStream_RenderPending(&renderer->vertex_stream);
|
||||
GFX_GL_Program_Uniform1i(
|
||||
&renderer->program, renderer->loc_texturing_enabled, is_enabled);
|
||||
}
|
160
src/libtrx/gfx/3d/vertex_stream.c
Normal file
160
src/libtrx/gfx/3d/vertex_stream.c
Normal file
|
@ -0,0 +1,160 @@
|
|||
#include "gfx/3d/vertex_stream.h"
|
||||
|
||||
#include "gfx/gl/gl_core_3_3.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
static const GLenum GL_PRIM_MODES[] = {
|
||||
GL_LINES, // GFX_3D_PRIM_LINE
|
||||
GL_TRIANGLES, // GFX_3D_PRIM_TRI
|
||||
};
|
||||
|
||||
static void M_PushVertex(
|
||||
GFX_3D_VERTEX_STREAM *vertex_stream, GFX_3D_VERTEX *vertex);
|
||||
|
||||
static void M_PushVertex(
|
||||
GFX_3D_VERTEX_STREAM *vertex_stream, GFX_3D_VERTEX *vertex)
|
||||
{
|
||||
if (vertex_stream->pending_vertices.count + 1
|
||||
>= vertex_stream->pending_vertices.capacity) {
|
||||
vertex_stream->pending_vertices.capacity += 1000;
|
||||
vertex_stream->pending_vertices.data = Memory_Realloc(
|
||||
vertex_stream->pending_vertices.data,
|
||||
vertex_stream->pending_vertices.capacity * sizeof(GFX_3D_VERTEX));
|
||||
}
|
||||
|
||||
vertex_stream->pending_vertices
|
||||
.data[vertex_stream->pending_vertices.count++] = *vertex;
|
||||
}
|
||||
|
||||
void GFX_3D_VertexStream_Init(GFX_3D_VERTEX_STREAM *vertex_stream)
|
||||
{
|
||||
vertex_stream->prim_type = GFX_3D_PRIM_TRI;
|
||||
vertex_stream->buffer_size = 0;
|
||||
vertex_stream->pending_vertices.data = NULL;
|
||||
vertex_stream->pending_vertices.count = 0;
|
||||
vertex_stream->pending_vertices.capacity = 0;
|
||||
|
||||
GFX_GL_Buffer_Init(&vertex_stream->buffer, GL_ARRAY_BUFFER);
|
||||
GFX_GL_Buffer_Bind(&vertex_stream->buffer);
|
||||
|
||||
GFX_GL_VertexArray_Init(&vertex_stream->vtc_format);
|
||||
GFX_GL_VertexArray_Bind(&vertex_stream->vtc_format);
|
||||
GFX_GL_VertexArray_Attribute(
|
||||
&vertex_stream->vtc_format, 0, 3, GL_FLOAT, GL_FALSE, 40, 0);
|
||||
GFX_GL_VertexArray_Attribute(
|
||||
&vertex_stream->vtc_format, 1, 3, GL_FLOAT, GL_FALSE, 40, 12);
|
||||
GFX_GL_VertexArray_Attribute(
|
||||
&vertex_stream->vtc_format, 2, 4, GL_FLOAT, GL_FALSE, 40, 24);
|
||||
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_3D_VertexStream_Close(GFX_3D_VERTEX_STREAM *vertex_stream)
|
||||
{
|
||||
GFX_GL_VertexArray_Close(&vertex_stream->vtc_format);
|
||||
GFX_GL_Buffer_Close(&vertex_stream->buffer);
|
||||
|
||||
Memory_FreePointer(&vertex_stream->pending_vertices.data);
|
||||
}
|
||||
|
||||
void GFX_3D_VertexStream_Bind(GFX_3D_VERTEX_STREAM *vertex_stream)
|
||||
{
|
||||
GFX_GL_Buffer_Bind(&vertex_stream->buffer);
|
||||
}
|
||||
|
||||
void GFX_3D_VertexStream_SetPrimType(
|
||||
GFX_3D_VERTEX_STREAM *vertex_stream, GFX_3D_PRIM_TYPE prim_type)
|
||||
{
|
||||
vertex_stream->prim_type = prim_type;
|
||||
}
|
||||
|
||||
bool GFX_3D_VertexStream_PushPrimStrip(
|
||||
GFX_3D_VERTEX_STREAM *vertex_stream, GFX_3D_VERTEX *vertices, int count)
|
||||
{
|
||||
if (vertex_stream->prim_type != GFX_3D_PRIM_TRI) {
|
||||
LOG_ERROR("Unsupported prim type: %d", vertex_stream->prim_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count <= 2) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
M_PushVertex(vertex_stream, &vertices[i]);
|
||||
}
|
||||
} else {
|
||||
// convert strip to raw triangles
|
||||
for (int i = 2; i < count; i++) {
|
||||
M_PushVertex(vertex_stream, &vertices[i - 2]);
|
||||
M_PushVertex(vertex_stream, &vertices[i - 1]);
|
||||
M_PushVertex(vertex_stream, &vertices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GFX_3D_VertexStream_PushPrimFan(
|
||||
GFX_3D_VERTEX_STREAM *vertex_stream, GFX_3D_VERTEX *vertices, int count)
|
||||
{
|
||||
if (vertex_stream->prim_type != GFX_3D_PRIM_TRI) {
|
||||
LOG_ERROR("Unsupported prim type: %d", vertex_stream->prim_type);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count <= 2) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
M_PushVertex(vertex_stream, &vertices[i]);
|
||||
}
|
||||
} else {
|
||||
// convert fan to raw triangles
|
||||
for (int i = 2; i < count; i++) {
|
||||
M_PushVertex(vertex_stream, &vertices[0]);
|
||||
M_PushVertex(vertex_stream, &vertices[i - 1]);
|
||||
M_PushVertex(vertex_stream, &vertices[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GFX_3D_VertexStream_PushPrimList(
|
||||
GFX_3D_VERTEX_STREAM *vertex_stream, GFX_3D_VERTEX *vertices, int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++) {
|
||||
M_PushVertex(vertex_stream, &vertices[i]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GFX_3D_VertexStream_RenderPending(GFX_3D_VERTEX_STREAM *vertex_stream)
|
||||
{
|
||||
if (!vertex_stream->pending_vertices.count) {
|
||||
return;
|
||||
}
|
||||
|
||||
GFX_GL_VertexArray_Bind(&vertex_stream->vtc_format);
|
||||
|
||||
// resize GPU buffer if required
|
||||
size_t buffer_size =
|
||||
sizeof(GFX_3D_VERTEX) * vertex_stream->pending_vertices.count;
|
||||
if (buffer_size > vertex_stream->buffer_size) {
|
||||
LOG_INFO(
|
||||
"Vertex buffer resize: %d -> %d", vertex_stream->buffer_size,
|
||||
buffer_size);
|
||||
GFX_GL_Buffer_Data(
|
||||
&vertex_stream->buffer, buffer_size, NULL, GL_STREAM_DRAW);
|
||||
vertex_stream->buffer_size = buffer_size;
|
||||
}
|
||||
|
||||
GFX_GL_Buffer_SubData(
|
||||
&vertex_stream->buffer, 0, buffer_size,
|
||||
vertex_stream->pending_vertices.data);
|
||||
|
||||
glDrawArrays(
|
||||
GL_PRIM_MODES[vertex_stream->prim_type], 0,
|
||||
vertex_stream->pending_vertices.count);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
vertex_stream->pending_vertices.count = 0;
|
||||
}
|
329
src/libtrx/gfx/context.c
Normal file
329
src/libtrx/gfx/context.c
Normal file
|
@ -0,0 +1,329 @@
|
|||
#include "gfx/context.h"
|
||||
|
||||
#include "game/shell.h"
|
||||
#include "gfx/gl/gl_core_3_3.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "gfx/renderers/fbo_renderer.h"
|
||||
#include "gfx/renderers/legacy_renderer.h"
|
||||
#include "gfx/screenshot.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <SDL2/SDL_video.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
SDL_GLContext context;
|
||||
SDL_Window *window_handle;
|
||||
|
||||
GFX_CONFIG config;
|
||||
GFX_RENDER_MODE render_mode;
|
||||
int32_t display_width;
|
||||
int32_t display_height;
|
||||
int32_t window_width;
|
||||
int32_t window_height;
|
||||
|
||||
char *scheduled_screenshot_path;
|
||||
GFX_RENDERER *renderer;
|
||||
GFX_2D_RENDERER renderer_2d;
|
||||
GFX_3D_RENDERER renderer_3d;
|
||||
} GFX_CONTEXT;
|
||||
|
||||
static GFX_CONTEXT m_Context = { 0 };
|
||||
|
||||
static bool M_IsExtensionSupported(const char *name);
|
||||
static void M_CheckExtensionSupport(const char *name);
|
||||
|
||||
static bool M_IsExtensionSupported(const char *name)
|
||||
{
|
||||
int number_of_extensions;
|
||||
|
||||
glGetIntegerv(GL_NUM_EXTENSIONS, &number_of_extensions);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
for (int i = 0; i < number_of_extensions; i++) {
|
||||
const char *gl_ext = (const char *)glGetStringi(GL_EXTENSIONS, i);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (gl_ext && !strcmp(gl_ext, name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void M_CheckExtensionSupport(const char *name)
|
||||
{
|
||||
LOG_INFO(
|
||||
"%s supported: %s", name, M_IsExtensionSupported(name) ? "yes" : "no");
|
||||
}
|
||||
|
||||
void GFX_Context_SwitchToWindowViewport(void)
|
||||
{
|
||||
glViewport(0, 0, m_Context.window_width, m_Context.window_height);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_Context_SwitchToWindowViewportAR(void)
|
||||
{
|
||||
// switch to window viewport at the aspect ratio of the display viewport
|
||||
int vp_width = m_Context.window_width;
|
||||
int vp_height = m_Context.window_height;
|
||||
|
||||
// default to bottom left corner of the window
|
||||
int vp_x = 0;
|
||||
int vp_y = 0;
|
||||
|
||||
int hw = m_Context.display_height * vp_width;
|
||||
int wh = m_Context.display_width * vp_height;
|
||||
|
||||
// create viewport offset if the window has a different
|
||||
// aspect ratio than the current display mode
|
||||
if (hw > wh) {
|
||||
int max_w = wh / m_Context.display_height;
|
||||
vp_x = (vp_width - max_w) / 2;
|
||||
vp_width = max_w;
|
||||
} else if (hw < wh) {
|
||||
int max_h = hw / m_Context.display_width;
|
||||
vp_y = (vp_height - max_h) / 2;
|
||||
vp_height = max_h;
|
||||
}
|
||||
|
||||
glViewport(vp_x, vp_y, vp_width, vp_height);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_Context_SwitchToDisplayViewport(void)
|
||||
{
|
||||
glViewport(0, 0, m_Context.display_width, m_Context.display_height);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_Context_Attach(void *window_handle)
|
||||
{
|
||||
const char *shading_ver;
|
||||
|
||||
if (m_Context.window_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Attaching to window %p", window_handle);
|
||||
|
||||
m_Context.config.line_width = 1;
|
||||
m_Context.config.enable_wireframe = false;
|
||||
m_Context.render_mode = -1;
|
||||
SDL_GetWindowSize(
|
||||
window_handle, &m_Context.window_width, &m_Context.window_height);
|
||||
m_Context.display_width = m_Context.window_width;
|
||||
m_Context.display_height = m_Context.window_height;
|
||||
|
||||
m_Context.window_handle = window_handle;
|
||||
|
||||
if (GFX_GL_DEFAULT_BACKEND == GFX_GL_33C) {
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(
|
||||
SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
}
|
||||
|
||||
m_Context.context = SDL_GL_CreateContext(m_Context.window_handle);
|
||||
|
||||
if (!m_Context.context) {
|
||||
Shell_ExitSystem("Can't create OpenGL context");
|
||||
}
|
||||
|
||||
if (SDL_GL_MakeCurrent(m_Context.window_handle, m_Context.context)) {
|
||||
Shell_ExitSystem("Can't activate OpenGL context");
|
||||
}
|
||||
|
||||
LOG_INFO("OpenGL vendor string: %s", glGetString(GL_VENDOR));
|
||||
LOG_INFO("OpenGL renderer string: %s", glGetString(GL_RENDERER));
|
||||
LOG_INFO("OpenGL version string: %s", glGetString(GL_VERSION));
|
||||
|
||||
shading_ver = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
|
||||
if (shading_ver != NULL) {
|
||||
LOG_INFO("Shading version string: %s", shading_ver);
|
||||
} else {
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
// Check the availability of non-Core Profile extensions for OpenGL 2.1
|
||||
if (GFX_GL_DEFAULT_BACKEND == GFX_GL_21) {
|
||||
M_CheckExtensionSupport("GL_ARB_explicit_attrib_location");
|
||||
M_CheckExtensionSupport("GL_EXT_gpu_shader4");
|
||||
}
|
||||
|
||||
glClearColor(0, 0, 0, 0);
|
||||
glClearDepth(1);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
// VSync defaults to on unless user disabled it in runtime json
|
||||
SDL_GL_SetSwapInterval(1);
|
||||
|
||||
GFX_2D_Renderer_Init(&m_Context.renderer_2d);
|
||||
GFX_3D_Renderer_Init(&m_Context.renderer_3d, &m_Context.config);
|
||||
}
|
||||
|
||||
void GFX_Context_Detach(void)
|
||||
{
|
||||
if (!m_Context.window_handle) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_Context.renderer != NULL && m_Context.renderer->shutdown != NULL) {
|
||||
m_Context.renderer->shutdown(m_Context.renderer);
|
||||
}
|
||||
|
||||
GFX_2D_Renderer_Close(&m_Context.renderer_2d);
|
||||
GFX_3D_Renderer_Close(&m_Context.renderer_3d);
|
||||
|
||||
SDL_GL_MakeCurrent(NULL, NULL);
|
||||
|
||||
if (m_Context.context != NULL) {
|
||||
SDL_GL_DeleteContext(m_Context.context);
|
||||
m_Context.context = NULL;
|
||||
}
|
||||
m_Context.window_handle = NULL;
|
||||
}
|
||||
|
||||
void GFX_Context_SetDisplayFilter(const GFX_TEXTURE_FILTER filter)
|
||||
{
|
||||
m_Context.config.display_filter = filter;
|
||||
}
|
||||
|
||||
void GFX_Context_SetWireframeMode(const bool enable)
|
||||
{
|
||||
m_Context.config.enable_wireframe = enable;
|
||||
}
|
||||
|
||||
void GFX_Context_SetLineWidth(const int32_t line_width)
|
||||
{
|
||||
m_Context.config.line_width = line_width;
|
||||
}
|
||||
|
||||
void GFX_Context_SetAnisotropyFilter(float value)
|
||||
{
|
||||
GFX_GL_Sampler_Bind(&m_Context.renderer_3d.sampler, 0);
|
||||
GFX_GL_Sampler_Parameterf(
|
||||
&m_Context.renderer_3d.sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, value);
|
||||
}
|
||||
|
||||
void GFX_Context_SetVSync(bool vsync)
|
||||
{
|
||||
SDL_GL_SetSwapInterval(vsync);
|
||||
}
|
||||
|
||||
void GFX_Context_SetWindowSize(int32_t width, int32_t height)
|
||||
{
|
||||
LOG_INFO("Window size: %dx%d", width, height);
|
||||
m_Context.window_width = width;
|
||||
m_Context.window_height = height;
|
||||
}
|
||||
|
||||
void GFX_Context_SetDisplaySize(int32_t width, int32_t height)
|
||||
{
|
||||
if (width == m_Context.display_width
|
||||
&& height == m_Context.display_height) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Display size: %dx%d", width, height);
|
||||
if (width <= 0 || height <= 0) {
|
||||
LOG_INFO("invalid size, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
m_Context.display_width = width;
|
||||
m_Context.display_height = height;
|
||||
|
||||
if (m_Context.renderer != NULL && m_Context.renderer->reset != NULL) {
|
||||
m_Context.renderer->reset(m_Context.renderer);
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_Context_SetRenderingMode(GFX_RENDER_MODE target_mode)
|
||||
{
|
||||
GFX_RENDER_MODE current_mode = m_Context.render_mode;
|
||||
if (current_mode == target_mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Render mode: %d", target_mode);
|
||||
if (m_Context.renderer != NULL && m_Context.renderer->shutdown != NULL) {
|
||||
m_Context.renderer->shutdown(m_Context.renderer);
|
||||
}
|
||||
switch (target_mode) {
|
||||
case GFX_RM_FRAMEBUFFER:
|
||||
m_Context.renderer = &g_GFX_Renderer_FBO;
|
||||
break;
|
||||
case GFX_RM_LEGACY:
|
||||
m_Context.renderer = &g_GFX_Renderer_Legacy;
|
||||
break;
|
||||
}
|
||||
if (m_Context.renderer != NULL && m_Context.renderer->init != NULL) {
|
||||
m_Context.renderer->init(m_Context.renderer, &m_Context.config);
|
||||
}
|
||||
m_Context.render_mode = target_mode;
|
||||
}
|
||||
|
||||
void *GFX_Context_GetWindowHandle(void)
|
||||
{
|
||||
return m_Context.window_handle;
|
||||
}
|
||||
|
||||
int32_t GFX_Context_GetDisplayWidth(void)
|
||||
{
|
||||
return m_Context.display_width;
|
||||
}
|
||||
|
||||
int32_t GFX_Context_GetDisplayHeight(void)
|
||||
{
|
||||
return m_Context.display_height;
|
||||
}
|
||||
|
||||
void GFX_Context_Clear(void)
|
||||
{
|
||||
if (m_Context.config.enable_wireframe) {
|
||||
glClearColor(1.0, 1.0, 1.0, 0.0);
|
||||
} else {
|
||||
glClearColor(0.0, 0.0, 0.0, 0.0);
|
||||
}
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
void GFX_Context_SwapBuffers(void)
|
||||
{
|
||||
glFinish();
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (m_Context.renderer != NULL
|
||||
&& m_Context.renderer->swap_buffers != NULL) {
|
||||
m_Context.renderer->swap_buffers(m_Context.renderer);
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_Context_ScheduleScreenshot(const char *path)
|
||||
{
|
||||
Memory_FreePointer(&m_Context.scheduled_screenshot_path);
|
||||
m_Context.scheduled_screenshot_path = Memory_DupStr(path);
|
||||
}
|
||||
|
||||
const char *GFX_Context_GetScheduledScreenshotPath(void)
|
||||
{
|
||||
return m_Context.scheduled_screenshot_path;
|
||||
}
|
||||
|
||||
void GFX_Context_ClearScheduledScreenshotPath(void)
|
||||
{
|
||||
Memory_FreePointer(&m_Context.scheduled_screenshot_path);
|
||||
}
|
||||
|
||||
GFX_2D_RENDERER *GFX_Context_GetRenderer2D(void)
|
||||
{
|
||||
return &m_Context.renderer_2d;
|
||||
}
|
||||
|
||||
GFX_3D_RENDERER *GFX_Context_GetRenderer3D(void)
|
||||
{
|
||||
return &m_Context.renderer_3d;
|
||||
}
|
67
src/libtrx/gfx/gl/buffer.c
Normal file
67
src/libtrx/gfx/gl/buffer.c
Normal file
|
@ -0,0 +1,67 @@
|
|||
#include "gfx/gl/buffer.h"
|
||||
|
||||
#include "gfx/gl/utils.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
void GFX_GL_Buffer_Init(GFX_GL_BUFFER *buf, GLenum target)
|
||||
{
|
||||
assert(buf);
|
||||
buf->target = target;
|
||||
glGenBuffers(1, &buf->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Buffer_Close(GFX_GL_BUFFER *buf)
|
||||
{
|
||||
assert(buf);
|
||||
glDeleteBuffers(1, &buf->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Buffer_Bind(GFX_GL_BUFFER *buf)
|
||||
{
|
||||
assert(buf);
|
||||
glBindBuffer(buf->target, buf->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Buffer_Data(
|
||||
GFX_GL_BUFFER *buf, GLsizei size, const void *data, GLenum usage)
|
||||
{
|
||||
assert(buf);
|
||||
glBufferData(buf->target, size, data, usage);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Buffer_SubData(
|
||||
GFX_GL_BUFFER *buf, GLsizei offset, GLsizei size, const void *data)
|
||||
{
|
||||
assert(buf);
|
||||
glBufferSubData(buf->target, offset, size, data);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void *GFX_GL_Buffer_Map(GFX_GL_BUFFER *buf, GLenum access)
|
||||
{
|
||||
assert(buf);
|
||||
void *ret = glMapBuffer(buf->target, access);
|
||||
GFX_GL_CheckError();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void GFX_GL_Buffer_Unmap(GFX_GL_BUFFER *buf)
|
||||
{
|
||||
assert(buf);
|
||||
glUnmapBuffer(buf->target);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
GLint GFX_GL_Buffer_Parameter(GFX_GL_BUFFER *buf, GLenum pname)
|
||||
{
|
||||
assert(buf);
|
||||
GLint params = 0;
|
||||
glGetBufferParameteriv(buf->target, pname, ¶ms);
|
||||
GFX_GL_CheckError();
|
||||
return params;
|
||||
}
|
8745
src/libtrx/gfx/gl/gl_core_3_3.c
Normal file
8745
src/libtrx/gfx/gl/gl_core_3_3.c
Normal file
File diff suppressed because it is too large
Load diff
204
src/libtrx/gfx/gl/program.c
Normal file
204
src/libtrx/gfx/gl/program.c
Normal file
|
@ -0,0 +1,204 @@
|
|||
#include "gfx/gl/program.h"
|
||||
|
||||
#include "filesystem.h"
|
||||
#include "game/shell.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
bool GFX_GL_Program_Init(GFX_GL_PROGRAM *program)
|
||||
{
|
||||
assert(program);
|
||||
program->id = glCreateProgram();
|
||||
GFX_GL_CheckError();
|
||||
if (!program->id) {
|
||||
LOG_ERROR("Can't create shader program");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GFX_GL_Program_Close(GFX_GL_PROGRAM *program)
|
||||
{
|
||||
if (program->id) {
|
||||
glDeleteProgram(program->id);
|
||||
GFX_GL_CheckError();
|
||||
program->id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_GL_Program_Bind(GFX_GL_PROGRAM *program)
|
||||
{
|
||||
glUseProgram(program->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
char *GFX_GL_Program_PreprocessShader(
|
||||
const char *content, GLenum type, GFX_GL_BACKEND backend)
|
||||
{
|
||||
const char *version_ogl21 =
|
||||
"#version 120\n"
|
||||
"#extension GL_ARB_explicit_attrib_location: enable\n"
|
||||
"#extension GL_EXT_gpu_shader4: enable\n";
|
||||
const char *version_ogl33c = "#version 330 core\n";
|
||||
const char *define_vertex = "#define VERTEX\n";
|
||||
const char *define_ogl33c = "#define OGL33C\n";
|
||||
|
||||
size_t bufsize = strlen(content) + 1;
|
||||
|
||||
if (backend == GFX_GL_33C) {
|
||||
bufsize += strlen(version_ogl33c);
|
||||
bufsize += strlen(define_ogl33c);
|
||||
} else {
|
||||
bufsize += strlen(version_ogl21);
|
||||
}
|
||||
|
||||
if (type == GL_VERTEX_SHADER) {
|
||||
bufsize += strlen(define_vertex);
|
||||
}
|
||||
|
||||
char *processed_content = Memory_Alloc(bufsize);
|
||||
if (!processed_content) {
|
||||
return NULL;
|
||||
}
|
||||
processed_content[0] = '\0';
|
||||
|
||||
if (backend == GFX_GL_33C) {
|
||||
strcpy(processed_content, version_ogl33c);
|
||||
strcat(processed_content, define_ogl33c);
|
||||
} else {
|
||||
strcpy(processed_content, version_ogl21);
|
||||
}
|
||||
|
||||
if (type == GL_VERTEX_SHADER) {
|
||||
strcat(processed_content, define_vertex);
|
||||
}
|
||||
|
||||
strcat(processed_content, content);
|
||||
return processed_content;
|
||||
}
|
||||
|
||||
void GFX_GL_Program_AttachShader(
|
||||
GFX_GL_PROGRAM *program, GLenum type, const char *path)
|
||||
{
|
||||
GLuint shader_id = glCreateShader(type);
|
||||
GFX_GL_CheckError();
|
||||
if (!shader_id) {
|
||||
Shell_ExitSystem("Failed to create shader");
|
||||
}
|
||||
|
||||
char *content = NULL;
|
||||
if (!File_Load(path, &content, NULL)) {
|
||||
Shell_ExitSystemFmt("Unable to find shader file: %s", path);
|
||||
}
|
||||
|
||||
char *processed_content =
|
||||
GFX_GL_Program_PreprocessShader(content, type, GFX_GL_DEFAULT_BACKEND);
|
||||
Memory_FreePointer(&content);
|
||||
if (!processed_content) {
|
||||
Shell_ExitSystemFmt("Failed to pre-process shader source: %s", path);
|
||||
}
|
||||
|
||||
glShaderSource(shader_id, 1, (const char *const *)&processed_content, NULL);
|
||||
|
||||
GFX_GL_CheckError();
|
||||
glCompileShader(shader_id);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
int compile_status;
|
||||
glGetShaderiv(shader_id, GL_COMPILE_STATUS, &compile_status);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (compile_status != GL_TRUE) {
|
||||
GLsizei info_log_size = 4096;
|
||||
char info_log[info_log_size];
|
||||
glGetShaderInfoLog(shader_id, info_log_size, &info_log_size, info_log);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (info_log[0]) {
|
||||
Shell_ExitSystemFmt("Shader compilation failed:\n%s", info_log);
|
||||
} else {
|
||||
Shell_ExitSystemFmt("Shader compilation failed.");
|
||||
}
|
||||
}
|
||||
|
||||
Memory_FreePointer(&processed_content);
|
||||
|
||||
glAttachShader(program->id, shader_id);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glDeleteShader(shader_id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Program_Link(GFX_GL_PROGRAM *program)
|
||||
{
|
||||
glLinkProgram(program->id);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
GLint linkStatus;
|
||||
glGetProgramiv(program->id, GL_LINK_STATUS, &linkStatus);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (!linkStatus) {
|
||||
GLsizei info_log_size = 4096;
|
||||
char info_log[info_log_size];
|
||||
glGetProgramInfoLog(
|
||||
program->id, info_log_size, &info_log_size, info_log);
|
||||
GFX_GL_CheckError();
|
||||
if (info_log[0]) {
|
||||
Shell_ExitSystemFmt("Shader linking failed:\n%s", info_log);
|
||||
} else {
|
||||
Shell_ExitSystemFmt("Shader linking failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_GL_Program_FragmentData(GFX_GL_PROGRAM *program, const char *name)
|
||||
{
|
||||
glBindFragDataLocation(program->id, 0, name);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
GLint GFX_GL_Program_UniformLocation(GFX_GL_PROGRAM *program, const char *name)
|
||||
{
|
||||
GLint location = glGetUniformLocation(program->id, name);
|
||||
GFX_GL_CheckError();
|
||||
if (location == -1) {
|
||||
LOG_INFO("Shader uniform not found: %s", name);
|
||||
}
|
||||
return location;
|
||||
}
|
||||
|
||||
void GFX_GL_Program_Uniform3f(
|
||||
GFX_GL_PROGRAM *program, GLint loc, GLfloat v0, GLfloat v1, GLfloat v2)
|
||||
{
|
||||
glUniform3f(loc, v0, v1, v2);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Program_Uniform4f(
|
||||
GFX_GL_PROGRAM *program, GLint loc, GLfloat v0, GLfloat v1, GLfloat v2,
|
||||
GLfloat v3)
|
||||
{
|
||||
glUniform4f(loc, v0, v1, v2, v3);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Program_Uniform1i(GFX_GL_PROGRAM *program, GLint loc, GLint v0)
|
||||
{
|
||||
glUniform1i(loc, v0);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Program_UniformMatrix4fv(
|
||||
GFX_GL_PROGRAM *program, GLint loc, GLsizei count, GLboolean transpose,
|
||||
const GLfloat *value)
|
||||
{
|
||||
glUniformMatrix4fv(loc, count, transpose, value);
|
||||
GFX_GL_CheckError();
|
||||
}
|
35
src/libtrx/gfx/gl/sampler.c
Normal file
35
src/libtrx/gfx/gl/sampler.c
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include "gfx/gl/sampler.h"
|
||||
|
||||
#include "gfx/gl/utils.h"
|
||||
|
||||
void GFX_GL_Sampler_Init(GFX_GL_SAMPLER *sampler)
|
||||
{
|
||||
glGenSamplers(1, &sampler->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Sampler_Close(GFX_GL_SAMPLER *sampler)
|
||||
{
|
||||
glDeleteSamplers(1, &sampler->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Sampler_Bind(GFX_GL_SAMPLER *sampler, GLuint unit)
|
||||
{
|
||||
glBindSampler(unit, sampler->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Sampler_Parameteri(
|
||||
GFX_GL_SAMPLER *sampler, GLenum pname, GLint param)
|
||||
{
|
||||
glSamplerParameteri(sampler->id, pname, param);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Sampler_Parameterf(
|
||||
GFX_GL_SAMPLER *sampler, GLenum pname, GLfloat param)
|
||||
{
|
||||
glSamplerParameterf(sampler->id, pname, param);
|
||||
GFX_GL_CheckError();
|
||||
}
|
88
src/libtrx/gfx/gl/texture.c
Normal file
88
src/libtrx/gfx/gl/texture.c
Normal file
|
@ -0,0 +1,88 @@
|
|||
#include "gfx/gl/texture.h"
|
||||
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "memory.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
GFX_GL_TEXTURE *GFX_GL_Texture_Create(GLenum target)
|
||||
{
|
||||
GFX_GL_TEXTURE *texture = Memory_Alloc(sizeof(GFX_GL_TEXTURE));
|
||||
GFX_GL_Texture_Init(texture, target);
|
||||
return texture;
|
||||
}
|
||||
|
||||
void GFX_GL_Texture_Free(GFX_GL_TEXTURE *texture)
|
||||
{
|
||||
if (texture != NULL) {
|
||||
GFX_GL_Texture_Close(texture);
|
||||
Memory_FreePointer(&texture);
|
||||
}
|
||||
}
|
||||
|
||||
void GFX_GL_Texture_Init(GFX_GL_TEXTURE *texture, GLenum target)
|
||||
{
|
||||
assert(texture != NULL);
|
||||
texture->target = target;
|
||||
glGenTextures(1, &texture->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Texture_Close(GFX_GL_TEXTURE *texture)
|
||||
{
|
||||
assert(texture != NULL);
|
||||
glDeleteTextures(1, &texture->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Texture_Bind(GFX_GL_TEXTURE *texture)
|
||||
{
|
||||
assert(texture != NULL);
|
||||
glBindTexture(texture->target, texture->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Texture_Load(
|
||||
GFX_GL_TEXTURE *texture, const void *data, int width, int height,
|
||||
GLint internal_format, GLint format)
|
||||
{
|
||||
assert(texture != NULL);
|
||||
|
||||
GFX_GL_Texture_Bind(texture);
|
||||
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(
|
||||
GL_TEXTURE_2D, 0, internal_format, width, height, 0, format,
|
||||
GL_UNSIGNED_BYTE, data);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_Texture_LoadFromBackBuffer(GFX_GL_TEXTURE *const texture)
|
||||
{
|
||||
assert(texture != NULL);
|
||||
|
||||
GFX_GL_Texture_Bind(texture);
|
||||
|
||||
GLint viewport[4];
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
const GLint vp_x = viewport[0];
|
||||
const GLint vp_y = viewport[1];
|
||||
const GLint vp_w = viewport[2];
|
||||
const GLint vp_h = viewport[3];
|
||||
|
||||
const int32_t side = MIN(vp_w, vp_h);
|
||||
const int32_t x = vp_x + (vp_w - side) / 2;
|
||||
const int32_t y = vp_y + (vp_h - side) / 2;
|
||||
const int32_t w = side;
|
||||
const int32_t h = side;
|
||||
|
||||
glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, x, y, w, h, 0);
|
||||
GFX_GL_CheckError();
|
||||
}
|
27
src/libtrx/gfx/gl/utils.c
Normal file
27
src/libtrx/gfx/gl/utils.c
Normal file
|
@ -0,0 +1,27 @@
|
|||
#include "gfx/gl/utils.h"
|
||||
|
||||
#include "gfx/gl/gl_core_3_3.h"
|
||||
|
||||
const char *GFX_GL_GetErrorString(GLenum err)
|
||||
{
|
||||
switch (err) {
|
||||
case GL_NO_ERROR:
|
||||
return "GL_NO_ERROR";
|
||||
case GL_INVALID_ENUM:
|
||||
return "GL_INVALID_ENUM";
|
||||
case GL_INVALID_VALUE:
|
||||
return "GL_INVALID_VALUE";
|
||||
case GL_INVALID_OPERATION:
|
||||
return "GL_INVALID_OPERATION";
|
||||
case GL_INVALID_FRAMEBUFFER_OPERATION:
|
||||
return "GL_INVALID_FRAMEBUFFER_OPERATION";
|
||||
case GL_OUT_OF_MEMORY:
|
||||
return "GL_OUT_OF_MEMORY";
|
||||
case GL_STACK_UNDERFLOW:
|
||||
return "GL_STACK_UNDERFLOW";
|
||||
case GL_STACK_OVERFLOW:
|
||||
return "GL_STACK_OVERFLOW";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
40
src/libtrx/gfx/gl/vertex_array.c
Normal file
40
src/libtrx/gfx/gl/vertex_array.c
Normal file
|
@ -0,0 +1,40 @@
|
|||
#include "gfx/gl/vertex_array.h"
|
||||
|
||||
#include "gfx/gl/utils.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void GFX_GL_VertexArray_Init(GFX_GL_VERTEX_ARRAY *array)
|
||||
{
|
||||
assert(array);
|
||||
glGenVertexArrays(1, &array->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_VertexArray_Close(GFX_GL_VERTEX_ARRAY *array)
|
||||
{
|
||||
assert(array);
|
||||
glDeleteVertexArrays(1, &array->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_VertexArray_Bind(GFX_GL_VERTEX_ARRAY *array)
|
||||
{
|
||||
assert(array);
|
||||
glBindVertexArray(array->id);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
void GFX_GL_VertexArray_Attribute(
|
||||
GFX_GL_VERTEX_ARRAY *array, GLuint index, GLint size, GLenum type,
|
||||
GLboolean normalized, GLsizei stride, GLsizei offset)
|
||||
{
|
||||
assert(array);
|
||||
glEnableVertexAttribArray(index);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glVertexAttribPointer(
|
||||
index, size, type, normalized, stride, (void *)(intptr_t)offset);
|
||||
GFX_GL_CheckError();
|
||||
}
|
245
src/libtrx/gfx/renderers/fbo_renderer.c
Normal file
245
src/libtrx/gfx/renderers/fbo_renderer.c
Normal file
|
@ -0,0 +1,245 @@
|
|||
#include "gfx/renderers/fbo_renderer.h"
|
||||
|
||||
#include "gfx/common.h"
|
||||
#include "gfx/context.h"
|
||||
#include "gfx/gl/buffer.h"
|
||||
#include "gfx/gl/gl_core_3_3.h"
|
||||
#include "gfx/gl/program.h"
|
||||
#include "gfx/gl/sampler.h"
|
||||
#include "gfx/gl/texture.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "gfx/gl/vertex_array.h"
|
||||
#include "gfx/screenshot.h"
|
||||
#include "log.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <SDL2/SDL_video.h>
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
const GFX_CONFIG *config;
|
||||
|
||||
GLuint fbo;
|
||||
GLuint rbo;
|
||||
|
||||
GFX_GL_VERTEX_ARRAY vertex_array;
|
||||
GFX_GL_BUFFER buffer;
|
||||
GFX_GL_TEXTURE texture;
|
||||
GFX_GL_SAMPLER sampler;
|
||||
GFX_GL_PROGRAM program;
|
||||
} M_CONTEXT;
|
||||
|
||||
static void M_SwapBuffers(GFX_RENDERER *renderer);
|
||||
static void M_Init(GFX_RENDERER *renderer, const GFX_CONFIG *config);
|
||||
static void M_Shutdown(GFX_RENDERER *renderer);
|
||||
static void M_Reset(GFX_RENDERER *renderer);
|
||||
|
||||
static void M_Render(GFX_RENDERER *renderer);
|
||||
static void M_Bind(const GFX_RENDERER *renderer);
|
||||
static void M_Unbind(const GFX_RENDERER *renderer);
|
||||
|
||||
static void M_SwapBuffers(GFX_RENDERER *renderer)
|
||||
{
|
||||
if (GFX_Context_GetScheduledScreenshotPath()) {
|
||||
GFX_Screenshot_CaptureToFile(GFX_Context_GetScheduledScreenshotPath());
|
||||
GFX_Context_ClearScheduledScreenshotPath();
|
||||
}
|
||||
|
||||
GFX_Context_SwitchToWindowViewportAR();
|
||||
M_Render(renderer);
|
||||
|
||||
SDL_GL_SwapWindow(GFX_Context_GetWindowHandle());
|
||||
|
||||
GFX_Context_SwitchToWindowViewport();
|
||||
M_Unbind(renderer);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
M_Bind(renderer);
|
||||
GFX_Context_SwitchToDisplayViewport();
|
||||
}
|
||||
|
||||
static void M_Init(GFX_RENDERER *const renderer, const GFX_CONFIG *const config)
|
||||
{
|
||||
LOG_INFO("");
|
||||
|
||||
assert(renderer != NULL);
|
||||
renderer->priv = (M_CONTEXT *)Memory_Alloc(sizeof(M_CONTEXT));
|
||||
M_CONTEXT *priv = renderer->priv;
|
||||
assert(priv != NULL);
|
||||
|
||||
priv->config = config;
|
||||
|
||||
int32_t fbo_width = GFX_Context_GetDisplayWidth();
|
||||
int32_t fbo_height = GFX_Context_GetDisplayHeight();
|
||||
|
||||
GFX_GL_Buffer_Init(&priv->buffer, GL_ARRAY_BUFFER);
|
||||
GFX_GL_Buffer_Bind(&priv->buffer);
|
||||
GLfloat verts[] = { 0.0, 0.0, 1.0, 0.0, 0.0, 1.0,
|
||||
0.0, 1.0, 1.0, 0.0, 1.0, 1.0 };
|
||||
GFX_GL_Buffer_Data(&priv->buffer, sizeof(verts), verts, GL_STATIC_DRAW);
|
||||
|
||||
GFX_GL_VertexArray_Init(&priv->vertex_array);
|
||||
GFX_GL_VertexArray_Bind(&priv->vertex_array);
|
||||
GFX_GL_VertexArray_Attribute(
|
||||
&priv->vertex_array, 0, 2, GL_FLOAT, GL_FALSE, 0, 0);
|
||||
|
||||
GFX_GL_Texture_Init(&priv->texture, GL_TEXTURE_2D);
|
||||
|
||||
GFX_GL_Sampler_Init(&priv->sampler);
|
||||
GFX_GL_Sampler_Bind(&priv->sampler, 0);
|
||||
GFX_GL_Sampler_Parameteri(&priv->sampler, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
GFX_GL_Sampler_Parameteri(&priv->sampler, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&priv->sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
|
||||
GFX_GL_Sampler_Parameteri(
|
||||
&priv->sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
|
||||
|
||||
GFX_GL_Program_Init(&priv->program);
|
||||
GFX_GL_Program_AttachShader(
|
||||
&priv->program, GL_VERTEX_SHADER, "shaders/fbo.glsl");
|
||||
GFX_GL_Program_AttachShader(
|
||||
&priv->program, GL_FRAGMENT_SHADER, "shaders/fbo.glsl");
|
||||
GFX_GL_Program_Link(&priv->program);
|
||||
GFX_GL_Program_FragmentData(&priv->program, "fragColor");
|
||||
|
||||
glGenFramebuffers(1, &priv->fbo);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, priv->fbo);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
GFX_GL_Texture_Load(
|
||||
&priv->texture, NULL, fbo_width, fbo_height, GL_RGB, GL_RGB);
|
||||
|
||||
glFramebufferTexture2D(
|
||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, priv->texture.id,
|
||||
0);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glGenRenderbuffers(1, &priv->rbo);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, priv->rbo);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glRenderbufferStorage(
|
||||
GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, fbo_width, fbo_height);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glBindRenderbuffer(GL_RENDERBUFFER, 0);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glFramebufferRenderbuffer(
|
||||
GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
|
||||
priv->rbo);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||
LOG_ERROR("framebuffer is not complete!");
|
||||
}
|
||||
}
|
||||
|
||||
static void M_Shutdown(GFX_RENDERER *renderer)
|
||||
{
|
||||
LOG_INFO("");
|
||||
|
||||
assert(renderer != NULL);
|
||||
M_CONTEXT *priv = renderer->priv;
|
||||
assert(priv != NULL);
|
||||
|
||||
if (!priv->fbo) {
|
||||
return;
|
||||
}
|
||||
|
||||
glDeleteFramebuffers(1, &priv->fbo);
|
||||
priv->fbo = 0;
|
||||
GFX_GL_VertexArray_Close(&priv->vertex_array);
|
||||
GFX_GL_Buffer_Close(&priv->buffer);
|
||||
GFX_GL_Texture_Close(&priv->texture);
|
||||
GFX_GL_Sampler_Close(&priv->sampler);
|
||||
GFX_GL_Program_Close(&priv->program);
|
||||
|
||||
Memory_FreePointer(&renderer->priv);
|
||||
}
|
||||
|
||||
static void M_Reset(GFX_RENDERER *renderer)
|
||||
{
|
||||
M_CONTEXT *const priv = renderer->priv;
|
||||
const GFX_CONFIG *const config = priv->config;
|
||||
|
||||
renderer->shutdown(renderer);
|
||||
renderer->init(renderer, config);
|
||||
}
|
||||
|
||||
static void M_Render(GFX_RENDERER *renderer)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
M_CONTEXT *priv = renderer->priv;
|
||||
assert(priv != NULL);
|
||||
|
||||
const GLuint filter = priv->config->display_filter == GFX_TF_BILINEAR
|
||||
? GL_LINEAR
|
||||
: GL_NEAREST;
|
||||
|
||||
glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
GFX_GL_Program_Bind(&priv->program);
|
||||
GFX_GL_Buffer_Bind(&priv->buffer);
|
||||
GFX_GL_VertexArray_Bind(&priv->vertex_array);
|
||||
GFX_GL_Texture_Bind(&priv->texture);
|
||||
GFX_GL_Sampler_Bind(&priv->sampler, 0);
|
||||
|
||||
GFX_GL_Sampler_Parameteri(&priv->sampler, GL_TEXTURE_MAG_FILTER, filter);
|
||||
GFX_GL_Sampler_Parameteri(&priv->sampler, GL_TEXTURE_MIN_FILTER, filter);
|
||||
|
||||
GLboolean blend = glIsEnabled(GL_BLEND);
|
||||
if (blend) {
|
||||
glDisable(GL_BLEND);
|
||||
}
|
||||
|
||||
GLboolean depth_test = glIsEnabled(GL_DEPTH_TEST);
|
||||
if (depth_test) {
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
}
|
||||
|
||||
glDrawArrays(GL_TRIANGLES, 0, 6);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (blend) {
|
||||
glEnable(GL_BLEND);
|
||||
}
|
||||
|
||||
if (depth_test) {
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
}
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, priv->fbo);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
static void M_Bind(const GFX_RENDERER *renderer)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
M_CONTEXT *priv = renderer->priv;
|
||||
assert(priv != NULL);
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, priv->fbo);
|
||||
}
|
||||
|
||||
static void M_Unbind(const GFX_RENDERER *renderer)
|
||||
{
|
||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||
}
|
||||
|
||||
GFX_RENDERER g_GFX_Renderer_FBO = {
|
||||
.swap_buffers = &M_SwapBuffers,
|
||||
.init = &M_Init,
|
||||
.shutdown = &M_Shutdown,
|
||||
.reset = &M_Reset,
|
||||
};
|
35
src/libtrx/gfx/renderers/legacy_renderer.c
Normal file
35
src/libtrx/gfx/renderers/legacy_renderer.c
Normal file
|
@ -0,0 +1,35 @@
|
|||
#include "gfx/renderers/legacy_renderer.h"
|
||||
|
||||
#include "gfx/context.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "gfx/screenshot.h"
|
||||
|
||||
#include <SDL2/SDL_video.h>
|
||||
#include <assert.h>
|
||||
|
||||
static void M_SwapBuffers(GFX_RENDERER *renderer);
|
||||
|
||||
static void M_SwapBuffers(GFX_RENDERER *renderer)
|
||||
{
|
||||
assert(renderer != NULL);
|
||||
|
||||
GFX_Context_SwitchToWindowViewportAR();
|
||||
if (GFX_Context_GetScheduledScreenshotPath()) {
|
||||
GFX_Screenshot_CaptureToFile(GFX_Context_GetScheduledScreenshotPath());
|
||||
GFX_Context_ClearScheduledScreenshotPath();
|
||||
}
|
||||
|
||||
SDL_GL_SwapWindow(GFX_Context_GetWindowHandle());
|
||||
|
||||
glDrawBuffer(GL_BACK);
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||
GFX_GL_CheckError();
|
||||
}
|
||||
|
||||
GFX_RENDERER g_GFX_Renderer_Legacy = {
|
||||
.priv = NULL,
|
||||
.swap_buffers = &M_SwapBuffers,
|
||||
.init = NULL,
|
||||
.reset = NULL,
|
||||
.shutdown = NULL,
|
||||
};
|
76
src/libtrx/gfx/screenshot.c
Normal file
76
src/libtrx/gfx/screenshot.c
Normal file
|
@ -0,0 +1,76 @@
|
|||
#include "gfx/screenshot.h"
|
||||
|
||||
#include "engine/image.h"
|
||||
#include "gfx/gl/utils.h"
|
||||
#include "memory.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
bool GFX_Screenshot_CaptureToFile(const char *path)
|
||||
{
|
||||
bool ret = false;
|
||||
|
||||
GLint width;
|
||||
GLint height;
|
||||
GFX_Screenshot_CaptureToBuffer(
|
||||
NULL, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE, true);
|
||||
|
||||
IMAGE *image = Image_Create(width, height);
|
||||
assert(image);
|
||||
|
||||
GFX_Screenshot_CaptureToBuffer(
|
||||
(uint8_t *)image->data, &width, &height, 3, GL_RGB, GL_UNSIGNED_BYTE,
|
||||
true);
|
||||
|
||||
ret = Image_SaveToFile(image, path);
|
||||
|
||||
if (image) {
|
||||
Image_Free(image);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void GFX_Screenshot_CaptureToBuffer(
|
||||
uint8_t *out_buffer, GLint *out_width, GLint *out_height, GLint depth,
|
||||
GLenum format, GLenum type, bool vflip)
|
||||
{
|
||||
assert(out_width);
|
||||
assert(out_height);
|
||||
|
||||
GLint viewport[4];
|
||||
glGetIntegerv(GL_VIEWPORT, viewport);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
GLint x = viewport[0];
|
||||
GLint y = viewport[1];
|
||||
*out_width = viewport[2];
|
||||
*out_height = viewport[3];
|
||||
|
||||
if (!out_buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
GLint pitch = *out_width * depth;
|
||||
|
||||
glPixelStorei(GL_PACK_ALIGNMENT, 1);
|
||||
GFX_GL_CheckError();
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
glReadBuffer(GL_BACK);
|
||||
GFX_GL_CheckError();
|
||||
glReadPixels(x, y, *out_width, *out_height, format, type, out_buffer);
|
||||
GFX_GL_CheckError();
|
||||
|
||||
if (vflip) {
|
||||
uint8_t *scanline = Memory_Alloc(pitch);
|
||||
for (int y1 = 0, middle = *out_height / 2; y1 < middle; y1++) {
|
||||
int y2 = *out_height - 1 - y1;
|
||||
memcpy(scanline, &out_buffer[y1 * pitch], pitch);
|
||||
memcpy(&out_buffer[y1 * pitch], &out_buffer[y2 * pitch], pitch);
|
||||
memcpy(&out_buffer[y2 * pitch], scanline, pitch);
|
||||
}
|
||||
Memory_FreePointer(&scanline);
|
||||
}
|
||||
}
|
24
src/libtrx/include/libtrx/benchmark.h
Normal file
24
src/libtrx/include/libtrx/benchmark.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <SDL2/SDL_stdinc.h>
|
||||
|
||||
typedef struct {
|
||||
Uint64 start;
|
||||
Uint64 last;
|
||||
} BENCHMARK;
|
||||
|
||||
BENCHMARK *Benchmark_Start(void);
|
||||
|
||||
#define Benchmark_End(b, ...) \
|
||||
Benchmark_End_Impl(b, __FILE__, __LINE__, __func__, __VA_ARGS__)
|
||||
|
||||
#define Benchmark_Tick(b, ...) \
|
||||
Benchmark_Tick_Impl(b, __FILE__, __LINE__, __func__, __VA_ARGS__)
|
||||
|
||||
void Benchmark_End_Impl(
|
||||
BENCHMARK *b, const char *file, int32_t line, const char *func,
|
||||
const char *message);
|
||||
|
||||
void Benchmark_Tick_Impl(
|
||||
BENCHMARK *b, const char *file, int32_t line, const char *func,
|
||||
const char *message);
|
29
src/libtrx/include/libtrx/bson.h
Normal file
29
src/libtrx/include/libtrx/bson.h
Normal file
|
@ -0,0 +1,29 @@
|
|||
#pragma once
|
||||
|
||||
#include "json.h"
|
||||
|
||||
typedef enum {
|
||||
BSON_PARSE_ERROR_NONE = 0,
|
||||
BSON_PARSE_ERROR_INVALID_VALUE,
|
||||
BSON_PARSE_ERROR_PREMATURE_END_OF_BUFFER,
|
||||
BSON_PARSE_ERROR_UNEXPECTED_TRAILING_BYTES,
|
||||
BSON_PARSE_ERROR_UNKNOWN,
|
||||
} BSON_PARSE_ERROR;
|
||||
|
||||
typedef struct {
|
||||
BSON_PARSE_ERROR error;
|
||||
size_t error_offset;
|
||||
} BSON_PARSE_RESULT;
|
||||
|
||||
// Parse a BSON file, returning a pointer to the root of the JSON structure.
|
||||
// Returns NULL if an error occurred (malformed BSON input, or malloc failed).
|
||||
JSON_VALUE *BSON_Parse(const char *src, size_t src_size);
|
||||
|
||||
JSON_VALUE *BSON_ParseEx(
|
||||
const char *src, size_t src_size, BSON_PARSE_RESULT *result);
|
||||
|
||||
const char *BSON_GetErrorDescription(BSON_PARSE_ERROR error);
|
||||
|
||||
/* Write out a BSON binary string. Return 0 if an error occurred (malformed
|
||||
* JSON input, or malloc failed). The out_size parameter is optional. */
|
||||
void *BSON_Write(const JSON_VALUE *value, size_t *out_size);
|
6
src/libtrx/include/libtrx/config.h
Normal file
6
src/libtrx/include/libtrx/config.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include "config/common.h"
|
||||
#include "config/file.h"
|
||||
#include "config/map.h"
|
||||
#include "config/option.h"
|
27
src/libtrx/include/libtrx/config/common.h
Normal file
27
src/libtrx/include/libtrx/config/common.h
Normal file
|
@ -0,0 +1,27 @@
|
|||
#pragma once
|
||||
|
||||
#include "../event_manager.h"
|
||||
#include "../json.h"
|
||||
#include "./option.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
void Config_Init(void);
|
||||
void Config_Shutdown(void);
|
||||
|
||||
bool Config_Read(void);
|
||||
bool Config_Write(void);
|
||||
|
||||
int32_t Config_SubscribeChanges(EVENT_LISTENER listener, void *user_data);
|
||||
void Config_UnsubscribeChanges(int32_t listener_id);
|
||||
|
||||
extern const char *Config_GetPath(void);
|
||||
|
||||
extern void Config_Sanitize(void);
|
||||
extern void Config_ApplyChanges(void);
|
||||
|
||||
extern const CONFIG_OPTION *Config_GetOptionMap(void);
|
||||
|
||||
extern void Config_LoadFromJSON(JSON_OBJECT *root_obj);
|
||||
extern void Config_DumpToJSON(JSON_OBJECT *root_obj);
|
22
src/libtrx/include/libtrx/config/file.h
Normal file
22
src/libtrx/include/libtrx/config/file.h
Normal file
|
@ -0,0 +1,22 @@
|
|||
#pragma once
|
||||
|
||||
#include "../enum_map.h"
|
||||
#include "../json.h"
|
||||
#include "./option.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool ConfigFile_Read(const char *path, void (*load)(JSON_OBJECT *root_obj));
|
||||
bool ConfigFile_Write(const char *path, void (*dump)(JSON_OBJECT *root_obj));
|
||||
|
||||
void ConfigFile_LoadOptions(
|
||||
JSON_OBJECT *root_obj, const CONFIG_OPTION *options);
|
||||
void ConfigFile_DumpOptions(
|
||||
JSON_OBJECT *root_obj, const CONFIG_OPTION *options);
|
||||
|
||||
int ConfigFile_ReadEnum(
|
||||
JSON_OBJECT *obj, const char *name, int default_value,
|
||||
const char *enum_name);
|
||||
void ConfigFile_WriteEnum(
|
||||
JSON_OBJECT *obj, const char *name, int value, const char *enum_name);
|
42
src/libtrx/include/libtrx/config/map.h
Normal file
42
src/libtrx/include/libtrx/config/map.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#include "../enum_map.h"
|
||||
#include "../utils.h"
|
||||
#include "./option.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define CFG_BOOL(parent, target_, default_value_) \
|
||||
{ .name = QUOTE(target_), \
|
||||
.type = COT_BOOL, \
|
||||
.target = &parent.target_, \
|
||||
.default_value = &(bool) { default_value_ }, \
|
||||
.param = NULL },
|
||||
|
||||
#define CFG_INT32(parent, target_, default_value_) \
|
||||
{ .name = QUOTE(target_), \
|
||||
.type = COT_INT32, \
|
||||
.target = &parent.target_, \
|
||||
.default_value = &(int32_t) { default_value_ }, \
|
||||
.param = NULL },
|
||||
|
||||
#define CFG_FLOAT(parent, target_, default_value_) \
|
||||
{ .name = QUOTE(target_), \
|
||||
.type = COT_FLOAT, \
|
||||
.target = &parent.target_, \
|
||||
.default_value = &(float) { default_value_ }, \
|
||||
.param = NULL },
|
||||
|
||||
#define CFG_DOUBLE(parent, target_, default_value_) \
|
||||
{ .name = QUOTE(target_), \
|
||||
.type = COT_DOUBLE, \
|
||||
.target = &parent.target_, \
|
||||
.default_value = &(double) { default_value_ }, \
|
||||
.param = NULL },
|
||||
|
||||
#define CFG_ENUM(parent, target_, default_value_, enum_map) \
|
||||
{ .name = QUOTE(target_), \
|
||||
.type = COT_ENUM, \
|
||||
.target = &parent.target_, \
|
||||
.default_value = &(int32_t) { default_value_ }, \
|
||||
.param = ENUM_MAP_NAME(enum_map) },
|
17
src/libtrx/include/libtrx/config/option.h
Normal file
17
src/libtrx/include/libtrx/config/option.h
Normal file
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
typedef enum {
|
||||
COT_BOOL = 0,
|
||||
COT_INT32 = 1,
|
||||
COT_FLOAT = 2,
|
||||
COT_DOUBLE = 3,
|
||||
COT_ENUM = 4,
|
||||
} CONFIG_OPTION_TYPE;
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
CONFIG_OPTION_TYPE type;
|
||||
const void *target;
|
||||
const void *default_value;
|
||||
const void *param;
|
||||
} CONFIG_OPTION;
|
51
src/libtrx/include/libtrx/engine/audio.h
Normal file
51
src/libtrx/include/libtrx/engine/audio.h
Normal file
|
@ -0,0 +1,51 @@
|
|||
#pragma once
|
||||
|
||||
#include <SDL2/SDL_audio.h>
|
||||
#include <libavutil/samplefmt.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define AUDIO_MAX_SAMPLES 1000
|
||||
#define AUDIO_MAX_ACTIVE_SAMPLES 50
|
||||
#define AUDIO_MAX_ACTIVE_STREAMS 10
|
||||
#define AUDIO_NO_SOUND (-1)
|
||||
|
||||
bool Audio_Init(void);
|
||||
bool Audio_Shutdown(void);
|
||||
|
||||
bool Audio_Stream_Pause(int32_t sound_id);
|
||||
bool Audio_Stream_Unpause(int32_t sound_id);
|
||||
int32_t Audio_Stream_CreateFromFile(const char *path);
|
||||
bool Audio_Stream_Close(int32_t sound_id);
|
||||
bool Audio_Stream_IsLooped(int32_t sound_id);
|
||||
bool Audio_Stream_SetVolume(int32_t sound_id, float volume);
|
||||
bool Audio_Stream_SetIsLooped(int32_t sound_id, bool is_looped);
|
||||
bool Audio_Stream_SetFinishCallback(
|
||||
int32_t sound_id, void (*callback)(int32_t sound_id, void *user_data),
|
||||
void *user_data);
|
||||
double Audio_Stream_GetTimestamp(int32_t sound_id);
|
||||
double Audio_Stream_GetDuration(int32_t sound_id);
|
||||
bool Audio_Stream_SeekTimestamp(int32_t sound_id, double timestamp);
|
||||
bool Audio_Stream_SetStartTimestamp(int32_t sound_id, double timestamp);
|
||||
bool Audio_Stream_SetStopTimestamp(int32_t sound_id, double timestamp);
|
||||
|
||||
bool Audio_Sample_LoadMany(size_t count, const char **contents, size_t *sizes);
|
||||
bool Audio_Sample_LoadSingle(
|
||||
int32_t sample_num, const char *content, size_t size);
|
||||
bool Audio_Sample_Unload(int32_t sample_id);
|
||||
bool Audio_Sample_UnloadAll(void);
|
||||
|
||||
int32_t Audio_Sample_Play(
|
||||
int32_t sample_id, int32_t volume, float pitch, int32_t pan,
|
||||
bool is_looped);
|
||||
bool Audio_Sample_IsPlaying(int32_t sound_id);
|
||||
bool Audio_Sample_Pause(int32_t sound_id);
|
||||
bool Audio_Sample_PauseAll(void);
|
||||
bool Audio_Sample_Unpause(int32_t sound_id);
|
||||
bool Audio_Sample_UnpauseAll(void);
|
||||
bool Audio_Sample_Close(int32_t sound_id);
|
||||
bool Audio_Sample_CloseAll(void);
|
||||
bool Audio_Sample_SetPan(int32_t sound_id, int32_t pan);
|
||||
bool Audio_Sample_SetVolume(int32_t sound_id, int32_t volume);
|
||||
bool Audio_Sample_SetPitch(int32_t sound_id, float pan);
|
42
src/libtrx/include/libtrx/engine/image.h
Normal file
42
src/libtrx/include/libtrx/engine/image.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
} IMAGE_PIXEL;
|
||||
|
||||
typedef struct {
|
||||
int32_t width;
|
||||
int32_t height;
|
||||
IMAGE_PIXEL *data;
|
||||
} IMAGE;
|
||||
|
||||
typedef enum {
|
||||
IMAGE_FIT_STRETCH,
|
||||
IMAGE_FIT_CROP,
|
||||
IMAGE_FIT_LETTERBOX,
|
||||
IMAGE_FIT_SMART,
|
||||
} IMAGE_FIT_MODE;
|
||||
|
||||
IMAGE *Image_Create(int width, int height);
|
||||
|
||||
IMAGE *Image_CreateFromFile(const char *path);
|
||||
|
||||
IMAGE *Image_CreateFromFileInto(
|
||||
const char *path, int32_t target_width, int32_t target_height,
|
||||
IMAGE_FIT_MODE fit_mode);
|
||||
|
||||
void Image_Free(IMAGE *image);
|
||||
|
||||
bool Image_GetFileInfo(const char *path, int32_t *width, int32_t *height);
|
||||
|
||||
bool Image_SaveToFile(const IMAGE *image, const char *path);
|
||||
|
||||
IMAGE *Image_Scale(
|
||||
const IMAGE *source_image, size_t target_width, size_t target_height,
|
||||
IMAGE_FIT_MODE fit_mode);
|
25
src/libtrx/include/libtrx/enum_map.h
Normal file
25
src/libtrx/include/libtrx/enum_map.h
Normal file
|
@ -0,0 +1,25 @@
|
|||
#include <stdint.h>
|
||||
|
||||
#define ENUM_MAP_DEFINE(enum_name, enum_value, str_value) \
|
||||
EnumMap_Define(ENUM_MAP_NAME(enum_name), enum_value, str_value);
|
||||
#define ENUM_MAP_DEFINE_SELF(enum_name, enum_value) \
|
||||
EnumMap_Define(ENUM_MAP_NAME(enum_name), enum_value, #enum_value);
|
||||
|
||||
#define ENUM_MAP_GET(enum_name, str_value, default_value) \
|
||||
EnumMap_Get(ENUM_MAP_NAME(enum_name), str_value, default_value)
|
||||
|
||||
#define ENUM_MAP_TO_STRING(enum_name, enum_value) \
|
||||
EnumMap_ToString(ENUM_MAP_NAME(enum_name), enum_value)
|
||||
|
||||
#define ENUM_MAP_NAME(enum_name) #enum_name
|
||||
|
||||
// The function to put the EnumMap_Define calls in
|
||||
extern void EnumMap_Init(void);
|
||||
|
||||
void EnumMap_Shutdown(void);
|
||||
|
||||
void EnumMap_Define(
|
||||
const char *enum_name, int32_t enum_value, const char *str_value);
|
||||
int32_t EnumMap_Get(
|
||||
const char *enum_name, const char *str_value, int32_t default_value);
|
||||
const char *EnumMap_ToString(const char *enum_name, int32_t enum_value);
|
24
src/libtrx/include/libtrx/event_manager.h
Normal file
24
src/libtrx/include/libtrx/event_manager.h
Normal file
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const void *sender;
|
||||
void *data;
|
||||
} EVENT;
|
||||
|
||||
typedef void (*EVENT_LISTENER)(const EVENT *, void *user_data);
|
||||
|
||||
typedef struct EVENT_MANAGER EVENT_MANAGER;
|
||||
|
||||
EVENT_MANAGER *EventManager_Create(void);
|
||||
void EventManager_Free(EVENT_MANAGER *manager);
|
||||
|
||||
int32_t EventManager_Subscribe(
|
||||
EVENT_MANAGER *manager, const char *event_name, const void *sender,
|
||||
EVENT_LISTENER listener, void *user_data);
|
||||
|
||||
void EventManager_Unsubscribe(EVENT_MANAGER *manager, int32_t listener_id);
|
||||
|
||||
void EventManager_Fire(EVENT_MANAGER *manager, const EVENT *event);
|
76
src/libtrx/include/libtrx/filesystem.h
Normal file
76
src/libtrx/include/libtrx/filesystem.h
Normal file
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
FILE_SEEK_SET,
|
||||
FILE_SEEK_CUR,
|
||||
FILE_SEEK_END,
|
||||
} FILE_SEEK_MODE;
|
||||
|
||||
typedef enum {
|
||||
FILE_OPEN_READ,
|
||||
FILE_OPEN_READ_WRITE,
|
||||
FILE_OPEN_WRITE,
|
||||
} FILE_OPEN_MODE;
|
||||
|
||||
typedef struct MYFILE MYFILE;
|
||||
|
||||
bool File_DirExists(const char *path);
|
||||
|
||||
bool File_IsAbsolute(const char *path);
|
||||
|
||||
bool File_IsRelative(const char *path);
|
||||
|
||||
bool File_Exists(const char *path);
|
||||
|
||||
const char *File_GetGameDirectory(void);
|
||||
|
||||
// Get the absolute path to the given file, if possible.
|
||||
// Internaly all operations on files within filesystem.c
|
||||
// perform this normalization, so calling this function should
|
||||
// only be necessary when interacting with external libraries.
|
||||
char *File_GetFullPath(const char *path);
|
||||
|
||||
char *File_GetParentDirectory(const char *path);
|
||||
|
||||
char *File_GuessExtension(const char *path, const char **extensions);
|
||||
|
||||
MYFILE *File_Open(const char *path, FILE_OPEN_MODE mode);
|
||||
|
||||
void File_ReadData(MYFILE *file, void *data, size_t size);
|
||||
void File_ReadItems(MYFILE *file, void *data, size_t count, size_t item_size);
|
||||
int8_t File_ReadS8(MYFILE *file);
|
||||
int16_t File_ReadS16(MYFILE *file);
|
||||
int32_t File_ReadS32(MYFILE *file);
|
||||
uint8_t File_ReadU8(MYFILE *file);
|
||||
uint16_t File_ReadU16(MYFILE *file);
|
||||
uint32_t File_ReadU32(MYFILE *file);
|
||||
|
||||
void File_WriteData(MYFILE *file, const void *data, size_t size);
|
||||
void File_WriteItems(
|
||||
MYFILE *file, const void *data, size_t count, size_t item_size);
|
||||
void File_WriteS8(MYFILE *file, int8_t value);
|
||||
void File_WriteS16(MYFILE *file, int16_t value);
|
||||
void File_WriteS32(MYFILE *file, int32_t value);
|
||||
void File_WriteU8(MYFILE *file, uint8_t value);
|
||||
void File_WriteU16(MYFILE *file, uint16_t value);
|
||||
void File_WriteU32(MYFILE *file, uint32_t value);
|
||||
|
||||
size_t File_Pos(MYFILE *file);
|
||||
|
||||
size_t File_Size(MYFILE *file);
|
||||
|
||||
const char *File_GetPath(MYFILE *file);
|
||||
|
||||
void File_Skip(MYFILE *file, size_t bytes);
|
||||
|
||||
void File_Seek(MYFILE *file, size_t pos, FILE_SEEK_MODE mode);
|
||||
|
||||
void File_Close(MYFILE *file);
|
||||
|
||||
bool File_Load(const char *path, char **output_data, size_t *output_size);
|
||||
|
||||
void File_CreateDirectory(const char *path);
|
46
src/libtrx/include/libtrx/game/anims.h
Normal file
46
src/libtrx/include/libtrx/game/anims.h
Normal file
|
@ -0,0 +1,46 @@
|
|||
#pragma once
|
||||
|
||||
#include "math.h"
|
||||
|
||||
typedef struct __PACKING {
|
||||
int16_t goal_anim_state;
|
||||
int16_t num_ranges;
|
||||
int16_t range_idx;
|
||||
} ANIM_CHANGE;
|
||||
|
||||
typedef struct __PACKING {
|
||||
int16_t start_frame;
|
||||
int16_t end_frame;
|
||||
int16_t link_anim_num;
|
||||
int16_t link_frame_num;
|
||||
} ANIM_RANGE;
|
||||
|
||||
#if TR_VERSION == 1
|
||||
typedef struct __PACKING {
|
||||
BOUNDS_16 bounds;
|
||||
XYZ_16 offset;
|
||||
int16_t nmeshes;
|
||||
int32_t *mesh_rots;
|
||||
} FRAME_INFO;
|
||||
#endif
|
||||
|
||||
typedef struct __PACKING {
|
||||
#if TR_VERSION == 1
|
||||
FRAME_INFO *frame_ptr;
|
||||
uint32_t frame_ofs;
|
||||
#elif TR_VERSION == 2
|
||||
int16_t *frame_ptr;
|
||||
#endif
|
||||
int16_t interpolation;
|
||||
int16_t current_anim_state;
|
||||
int32_t velocity;
|
||||
int32_t acceleration;
|
||||
int16_t frame_base;
|
||||
int16_t frame_end;
|
||||
int16_t jump_anim_num;
|
||||
int16_t jump_frame_num;
|
||||
int16_t num_changes;
|
||||
int16_t change_idx;
|
||||
int16_t num_commands;
|
||||
int16_t command_idx;
|
||||
} ANIM;
|
9
src/libtrx/include/libtrx/game/backpack.h
Normal file
9
src/libtrx/include/libtrx/game/backpack.h
Normal file
|
@ -0,0 +1,9 @@
|
|||
#pragma once
|
||||
|
||||
#include "objects/ids.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
bool Backpack_AddItem(GAME_OBJECT_ID object_id);
|
||||
bool Backpack_AddItemNTimes(GAME_OBJECT_ID object_id, int32_t n);
|
3
src/libtrx/include/libtrx/game/clock.h
Normal file
3
src/libtrx/include/libtrx/game/clock.h
Normal file
|
@ -0,0 +1,3 @@
|
|||
#pragma once
|
||||
|
||||
extern double Clock_GetHighPrecisionCounter(void);
|
78
src/libtrx/include/libtrx/game/collision.h
Normal file
78
src/libtrx/include/libtrx/game/collision.h
Normal file
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include "math.h"
|
||||
|
||||
#if TR_VERSION == 1
|
||||
typedef struct __PACKING {
|
||||
int32_t mid_floor;
|
||||
int32_t mid_ceiling;
|
||||
int32_t mid_type;
|
||||
int32_t front_floor;
|
||||
int32_t front_ceiling;
|
||||
int32_t front_type;
|
||||
int32_t left_floor;
|
||||
int32_t left_ceiling;
|
||||
int32_t left_type;
|
||||
int32_t right_floor;
|
||||
int32_t right_ceiling;
|
||||
int32_t right_type;
|
||||
int32_t radius;
|
||||
int32_t bad_pos;
|
||||
int32_t bad_neg;
|
||||
int32_t bad_ceiling;
|
||||
XYZ_32 shift;
|
||||
XYZ_32 old;
|
||||
int16_t facing;
|
||||
DIRECTION quadrant;
|
||||
int16_t coll_type;
|
||||
int8_t tilt_x;
|
||||
int8_t tilt_z;
|
||||
int8_t hit_by_baddie;
|
||||
int8_t hit_static;
|
||||
uint16_t slopes_are_walls : 1;
|
||||
uint16_t slopes_are_pits : 1;
|
||||
uint16_t lava_is_pit : 1;
|
||||
uint16_t enable_baddie_push : 1;
|
||||
uint16_t enable_spaz : 1;
|
||||
} COLL_INFO;
|
||||
|
||||
#elif TR_VERSION == 2
|
||||
typedef struct __PACKING {
|
||||
int32_t floor;
|
||||
int32_t ceiling;
|
||||
int32_t type;
|
||||
} COLL_SIDE;
|
||||
|
||||
typedef struct __PACKING {
|
||||
COLL_SIDE side_mid;
|
||||
COLL_SIDE side_front;
|
||||
COLL_SIDE side_left;
|
||||
COLL_SIDE side_right;
|
||||
int32_t radius;
|
||||
int32_t bad_pos;
|
||||
int32_t bad_neg;
|
||||
int32_t bad_ceiling;
|
||||
XYZ_32 shift;
|
||||
XYZ_32 old;
|
||||
int16_t old_anim_state;
|
||||
int16_t old_anim_num;
|
||||
int16_t old_frame_num;
|
||||
int16_t facing;
|
||||
int16_t quadrant;
|
||||
int16_t coll_type;
|
||||
int16_t *trigger;
|
||||
int8_t x_tilt;
|
||||
int8_t z_tilt;
|
||||
int8_t hit_by_baddie;
|
||||
int8_t hit_static;
|
||||
// clang-format off
|
||||
uint16_t slopes_are_walls: 1; // 0x01 1
|
||||
uint16_t slopes_are_pits: 1; // 0x02 2
|
||||
uint16_t lava_is_pit: 1; // 0x04 4
|
||||
uint16_t enable_baddie_push: 1; // 0x08 8
|
||||
uint16_t enable_spaz: 1; // 0x10 16
|
||||
uint16_t hit_ceiling: 1; // 0x20 32
|
||||
uint16_t pad: 10;
|
||||
// clang-format on
|
||||
} COLL_INFO;
|
||||
#endif
|
11
src/libtrx/include/libtrx/game/console/cmd/config.h
Normal file
11
src/libtrx/include/libtrx/game/console/cmd/config.h
Normal file
|
@ -0,0 +1,11 @@
|
|||
#pragma once
|
||||
|
||||
#include "../../../config/option.h"
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Config;
|
||||
|
||||
const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromKey(const char *key);
|
||||
const CONFIG_OPTION *Console_Cmd_Config_GetOptionFromTarget(const void *target);
|
||||
COMMAND_RESULT Console_Cmd_Config_Helper(
|
||||
const CONFIG_OPTION *option, const char *new_value);
|
5
src/libtrx/include/libtrx/game/console/cmd/die.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/die.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Die;
|
5
src/libtrx/include/libtrx/game/console/cmd/end_level.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/end_level.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_EndLevel;
|
5
src/libtrx/include/libtrx/game/console/cmd/exit_game.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/exit_game.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_ExitGame;
|
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_ExitToTitle;
|
5
src/libtrx/include/libtrx/game/console/cmd/flipmap.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/flipmap.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_FlipMap;
|
5
src/libtrx/include/libtrx/game/console/cmd/fly.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/fly.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Fly;
|
5
src/libtrx/include/libtrx/game/console/cmd/give_item.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/give_item.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_GiveItem;
|
5
src/libtrx/include/libtrx/game/console/cmd/heal.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/heal.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Heal;
|
5
src/libtrx/include/libtrx/game/console/cmd/kill.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/kill.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Kill;
|
5
src/libtrx/include/libtrx/game/console/cmd/load_game.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/load_game.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_LoadGame;
|
5
src/libtrx/include/libtrx/game/console/cmd/play_demo.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/play_demo.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_PlayDemo;
|
5
src/libtrx/include/libtrx/game/console/cmd/play_level.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/play_level.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_PlayLevel;
|
5
src/libtrx/include/libtrx/game/console/cmd/pos.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/pos.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Pos;
|
5
src/libtrx/include/libtrx/game/console/cmd/save_game.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/save_game.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_SaveGame;
|
5
src/libtrx/include/libtrx/game/console/cmd/set_health.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/set_health.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_SetHealth;
|
5
src/libtrx/include/libtrx/game/console/cmd/sfx.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/sfx.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_SFX;
|
5
src/libtrx/include/libtrx/game/console/cmd/teleport.h
Normal file
5
src/libtrx/include/libtrx/game/console/cmd/teleport.h
Normal file
|
@ -0,0 +1,5 @@
|
|||
#pragma once
|
||||
|
||||
#include "../common.h"
|
||||
|
||||
extern CONSOLE_COMMAND g_Console_Cmd_Teleport;
|
41
src/libtrx/include/libtrx/game/console/common.h
Normal file
41
src/libtrx/include/libtrx/game/console/common.h
Normal file
|
@ -0,0 +1,41 @@
|
|||
#pragma once
|
||||
|
||||
#include "../types.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
CR_SUCCESS,
|
||||
CR_FAILURE,
|
||||
CR_UNAVAILABLE,
|
||||
CR_BAD_INVOCATION,
|
||||
} COMMAND_RESULT;
|
||||
|
||||
typedef struct __PACKING {
|
||||
const struct __PACKING CONSOLE_COMMAND *cmd;
|
||||
const char *prefix;
|
||||
const char *args;
|
||||
} COMMAND_CONTEXT;
|
||||
|
||||
typedef struct __PACKING CONSOLE_COMMAND {
|
||||
const char *prefix;
|
||||
COMMAND_RESULT (*proc)(const COMMAND_CONTEXT *ctx);
|
||||
} CONSOLE_COMMAND;
|
||||
|
||||
void Console_Init(void);
|
||||
void Console_Shutdown(void);
|
||||
|
||||
void Console_Open(void);
|
||||
void Console_Close(void);
|
||||
bool Console_IsOpened(void);
|
||||
|
||||
void Console_ScrollLogs(void);
|
||||
int32_t Console_GetVisibleLogCount(void);
|
||||
int32_t Console_GetMaxLogCount(void);
|
||||
|
||||
void Console_Log(const char *fmt, ...);
|
||||
COMMAND_RESULT Console_Eval(const char *cmdline);
|
||||
|
||||
void Console_Draw(void);
|
||||
extern void Console_DrawBackdrop(void);
|
8
src/libtrx/include/libtrx/game/console/extern.h
Normal file
8
src/libtrx/include/libtrx/game/console/extern.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include "game/console/common.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
extern int32_t Console_GetMaxLineLength(void);
|
||||
extern CONSOLE_COMMAND **Console_GetCommands(void);
|
8
src/libtrx/include/libtrx/game/const.h
Normal file
8
src/libtrx/include/libtrx/game/const.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#define DONT_TARGET (-16384)
|
||||
|
||||
#define PHD_ONE 0x10000
|
||||
#define STEP_L 256
|
||||
#define WALL_L 1024 // 1 << WALL_SHIFT
|
||||
#define WALL_SHIFT 10
|
30
src/libtrx/include/libtrx/game/creature.h
Normal file
30
src/libtrx/include/libtrx/game/creature.h
Normal file
|
@ -0,0 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include "items.h"
|
||||
#include "lot.h"
|
||||
#include "math.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
typedef enum {
|
||||
MOOD_BORED = 0,
|
||||
MOOD_ATTACK = 1,
|
||||
MOOD_ESCAPE = 2,
|
||||
MOOD_STALK = 3,
|
||||
} MOOD_TYPE;
|
||||
|
||||
typedef struct __PACKING {
|
||||
int16_t head_rotation;
|
||||
int16_t neck_rotation;
|
||||
int16_t maximum_turn;
|
||||
uint16_t flags;
|
||||
int16_t item_num;
|
||||
MOOD_TYPE mood;
|
||||
LOT_INFO lot;
|
||||
XYZ_32 target;
|
||||
#if TR_VERSION == 2
|
||||
ITEM *enemy;
|
||||
#endif
|
||||
} CREATURE;
|
||||
|
||||
bool Creature_IsEnemy(const ITEM *item);
|
4
src/libtrx/include/libtrx/game/effects.h
Normal file
4
src/libtrx/include/libtrx/game/effects.h
Normal file
|
@ -0,0 +1,4 @@
|
|||
#pragma once
|
||||
|
||||
#include "effects/exploding_death.h"
|
||||
#include "effects/types.h"
|
6
src/libtrx/include/libtrx/game/effects/exploding_death.h
Normal file
6
src/libtrx/include/libtrx/game/effects/exploding_death.h
Normal file
|
@ -0,0 +1,6 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
int32_t Effect_ExplodingDeath(
|
||||
int16_t item_num, int32_t mesh_bits, int16_t damage);
|
35
src/libtrx/include/libtrx/game/effects/types.h
Normal file
35
src/libtrx/include/libtrx/game/effects/types.h
Normal file
|
@ -0,0 +1,35 @@
|
|||
#pragma once
|
||||
|
||||
#include "../math.h"
|
||||
#include "../objects/ids.h"
|
||||
#include "../types.h"
|
||||
|
||||
typedef struct __PACKING {
|
||||
XYZ_32 pos;
|
||||
XYZ_16 rot;
|
||||
int16_t room_num;
|
||||
#if TR_VERSION == 1
|
||||
GAME_OBJECT_ID object_id;
|
||||
#else
|
||||
int16_t object_id;
|
||||
#endif
|
||||
#if TR_VERSION == 1
|
||||
int16_t next_draw;
|
||||
#endif
|
||||
int16_t next_free;
|
||||
int16_t next_active;
|
||||
int16_t speed;
|
||||
int16_t fall_speed;
|
||||
int16_t frame_num;
|
||||
int16_t counter;
|
||||
int16_t shade;
|
||||
|
||||
#if TR_VERSION == 1
|
||||
struct __PACKING {
|
||||
struct __PACKING {
|
||||
XYZ_32 pos;
|
||||
XYZ_16 rot;
|
||||
} result, prev;
|
||||
} interp;
|
||||
#endif
|
||||
} FX;
|
8
src/libtrx/include/libtrx/game/game.h
Normal file
8
src/libtrx/include/libtrx/game/game.h
Normal file
|
@ -0,0 +1,8 @@
|
|||
#pragma once
|
||||
|
||||
#include "./gameflow/types.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
extern bool Game_IsPlayable(void);
|
||||
extern GAMEFLOW_LEVEL_TYPE Game_GetCurrentLevelType(void);
|
45
src/libtrx/include/libtrx/game/game_string.def
Normal file
45
src/libtrx/include/libtrx/game/game_string.def
Normal file
|
@ -0,0 +1,45 @@
|
|||
GS_DEFINE(OSD_POS_GET, "Room: %d\nPosition: %.3f, %.3f, %.3f\nRotation: %.3f,%.3f,%.3f")
|
||||
GS_DEFINE(OSD_CURRENT_HEALTH_GET, "Current Lara's health: %d")
|
||||
GS_DEFINE(OSD_CURRENT_HEALTH_SET, "Lara's health set to %d")
|
||||
GS_DEFINE(OSD_CONFIG_OPTION_GET, "%s is currently set to %s")
|
||||
GS_DEFINE(OSD_CONFIG_OPTION_SET, "%s changed to %s")
|
||||
GS_DEFINE(OSD_CONFIG_OPTION_UNKNOWN_OPTION, "Unknown option: %s")
|
||||
GS_DEFINE(MISC_ON, "On")
|
||||
GS_DEFINE(MISC_OFF, "Off")
|
||||
GS_DEFINE(OSD_HEAL_ALREADY_FULL_HP, "Lara's already at full health")
|
||||
GS_DEFINE(OSD_HEAL_SUCCESS, "Healed Lara back to full health")
|
||||
GS_DEFINE(OSD_GIVE_ITEM, "Added %s to Lara's inventory")
|
||||
GS_DEFINE(OSD_INVALID_ITEM, "Unknown item: %s")
|
||||
GS_DEFINE(OSD_KILL_ALL, "Poof! %d enemies gone!")
|
||||
GS_DEFINE(OSD_KILL_ALL_FAIL, "Uh-oh, there are no enemies left to kill...")
|
||||
GS_DEFINE(OSD_KILL, "Bye-bye!")
|
||||
GS_DEFINE(OSD_KILL_FAIL, "No enemy nearby...")
|
||||
GS_DEFINE(OSD_INVALID_OBJECT, "Invalid object")
|
||||
GS_DEFINE(OSD_INVALID_SAMPLE, "Invalid sound: %d")
|
||||
GS_DEFINE(OSD_OBJECT_NOT_FOUND, "Object not found")
|
||||
GS_DEFINE(OSD_SOUND_AVAILABLE_SAMPLES, "Available sounds: %s")
|
||||
GS_DEFINE(OSD_SOUND_PLAYING_SAMPLE, "Playing sound %d")
|
||||
GS_DEFINE(OSD_UNKNOWN_COMMAND, "Unknown command: %s")
|
||||
GS_DEFINE(OSD_COMMAND_BAD_INVOCATION, "Invalid invocation: %s")
|
||||
GS_DEFINE(OSD_COMMAND_UNAVAILABLE, "This command is not currently available")
|
||||
GS_DEFINE(OSD_INVALID_ROOM, "Invalid room: %d. Valid rooms are 0-%d")
|
||||
GS_DEFINE(OSD_POS_SET_POS, "Teleported to position: %.3f %.3f %.3f")
|
||||
GS_DEFINE(OSD_POS_SET_POS_FAIL, "Failed to teleport to position: %.3f %.3f %.3f")
|
||||
GS_DEFINE(OSD_POS_SET_ROOM, "Teleported to room: %d")
|
||||
GS_DEFINE(OSD_POS_SET_ROOM_FAIL, "Failed to teleport to room: %d")
|
||||
GS_DEFINE(OSD_POS_SET_ITEM, "Teleported to object: %s")
|
||||
GS_DEFINE(OSD_POS_SET_ITEM_FAIL, "Failed to teleport to object: %s")
|
||||
GS_DEFINE(OSD_INVALID_LEVEL, "Invalid level")
|
||||
GS_DEFINE(OSD_PLAY_LEVEL, "Loading %s")
|
||||
GS_DEFINE(OSD_LOAD_GAME, "Loaded game from save slot %d")
|
||||
GS_DEFINE(OSD_LOAD_GAME_FAIL_UNAVAILABLE_SLOT, "Save slot %d is not available")
|
||||
GS_DEFINE(OSD_LOAD_GAME_FAIL_INVALID_SLOT, "Invalid save slot %d")
|
||||
GS_DEFINE(OSD_SAVE_GAME, "Saved game to save slot %d")
|
||||
GS_DEFINE(OSD_SAVE_GAME_FAIL, "Cannot save the game in the current state")
|
||||
GS_DEFINE(OSD_SAVE_GAME_FAIL_INVALID_SLOT, "Invalid save slot %d")
|
||||
GS_DEFINE(OSD_FLIPMAP_ON, "Flipmap set to ON")
|
||||
GS_DEFINE(OSD_FLIPMAP_OFF, "Flipmap set to OFF")
|
||||
GS_DEFINE(OSD_FLIPMAP_FAIL_ALREADY_ON, "Flipmap is already ON")
|
||||
GS_DEFINE(OSD_FLIPMAP_FAIL_ALREADY_OFF, "Flipmap is already OFF")
|
||||
GS_DEFINE(OSD_AMBIGUOUS_INPUT_2, "Ambiguous input: %s and %s")
|
||||
GS_DEFINE(OSD_AMBIGUOUS_INPUT_3, "Ambiguous input: %s, %s, ...")
|
15
src/libtrx/include/libtrx/game/game_string.h
Normal file
15
src/libtrx/include/libtrx/game/game_string.h
Normal file
|
@ -0,0 +1,15 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define GS_DEFINE(id, value) GameString_Define(#id, value);
|
||||
#define GS(id) GameString_Get(#id)
|
||||
#define GS_ID(id) (#id)
|
||||
|
||||
typedef const char *GAME_STRING_ID;
|
||||
|
||||
void GameString_Define(const char *key, const char *value);
|
||||
bool GameString_IsKnown(const char *key);
|
||||
const char *GameString_Get(const char *key);
|
||||
void GameString_Clear(void);
|
10
src/libtrx/include/libtrx/game/gameflow/common.h
Normal file
10
src/libtrx/include/libtrx/game/gameflow/common.h
Normal file
|
@ -0,0 +1,10 @@
|
|||
#pragma once
|
||||
|
||||
#include "./types.h"
|
||||
|
||||
extern int32_t Gameflow_GetLevelCount(void);
|
||||
extern const char *Gameflow_GetLevelFileName(int32_t level_num);
|
||||
extern const char *Gameflow_GetLevelTitle(int32_t level_num);
|
||||
extern int32_t Gameflow_GetGymLevelNumber(void);
|
||||
|
||||
extern void Gameflow_OverrideCommand(GAMEFLOW_COMMAND action);
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue