mirror of
https://github.com/LostArtefacts/TRX.git
synced 2025-04-28 20:58:07 +03:00
tr2/savegame: introduce strategy concept
This introduces the concept of savegame strategies, like TR1X, with the legacy strategy being the only one currently.
This commit is contained in:
parent
49ad919de3
commit
699c1d9873
6 changed files with 214 additions and 101 deletions
|
@ -115,14 +115,13 @@ static DECLARE_GF_EVENT_HANDLER(M_HandlePlayLevel)
|
|||
const int16_t slot_num = Savegame_GetBoundSlot();
|
||||
if (!Savegame_Load(slot_num)) {
|
||||
LOG_ERROR("Failed to load save file!");
|
||||
Game_SetCurrentLevel(nullptr);
|
||||
GF_SetCurrentLevel(nullptr);
|
||||
return (GF_COMMAND) { .action = GF_EXIT_TO_TITLE };
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (level->type == GFL_NORMAL || level->type == GFL_BONUS) {
|
||||
Savegame_SetInitialVersion(SAVEGAME_CURRENT_VERSION);
|
||||
GF_InventoryModifier_Scan(Game_GetCurrentLevel());
|
||||
GF_InventoryModifier_Apply(Game_GetCurrentLevel(), GF_INV_REGULAR);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,43 @@
|
|||
|
||||
#include "game/game_flow/types.h"
|
||||
|
||||
#include <libtrx/filesystem.h>
|
||||
#include <libtrx/game/savegame.h>
|
||||
|
||||
#define SAVEGAME_CURRENT_VERSION -1
|
||||
|
||||
typedef enum {
|
||||
VERSION_LEGACY = -1,
|
||||
} SAVEGAME_VERSION;
|
||||
|
||||
typedef enum {
|
||||
SAVEGAME_FORMAT_INVALID = 0,
|
||||
SAVEGAME_FORMAT_LEGACY = 1,
|
||||
SAVEGAME_FORMAT_BSON = 2,
|
||||
} SAVEGAME_FORMAT;
|
||||
|
||||
typedef struct {
|
||||
SAVEGAME_FORMAT format;
|
||||
char *full_path;
|
||||
int32_t counter;
|
||||
int32_t level_num;
|
||||
char *level_title;
|
||||
int16_t initial_version;
|
||||
} SAVEGAME_INFO;
|
||||
|
||||
typedef struct {
|
||||
bool allow_load;
|
||||
bool allow_save;
|
||||
SAVEGAME_FORMAT format;
|
||||
const char *(*get_save_file_pattern_func)(void);
|
||||
bool (*fill_info_func)(MYFILE *fp, SAVEGAME_INFO *info);
|
||||
bool (*load_from_file_func)(MYFILE *fp);
|
||||
void (*save_to_file_func)(MYFILE *fp);
|
||||
} SAVEGAME_STRATEGY;
|
||||
|
||||
void Savegame_Init(void);
|
||||
void Savegame_Shutdown(void);
|
||||
void Savegame_RegisterStrategy(SAVEGAME_STRATEGY strategy);
|
||||
|
||||
void Savegame_InitCurrentInfo(void);
|
||||
|
||||
|
@ -22,9 +55,17 @@ void Savegame_HighlightNewestSlot(void);
|
|||
int32_t Savegame_GetLevelNumber(int32_t slot_idx);
|
||||
int32_t Savegame_GetCounter(void);
|
||||
int32_t Savegame_GetTotalCount(void);
|
||||
SAVEGAME_VERSION Savegame_GetInitialVersion(void);
|
||||
void Savegame_SetInitialVersion(SAVEGAME_VERSION version);
|
||||
|
||||
void Savegame_ProcessItemsBeforeSave(void);
|
||||
void Savegame_ProcessItemsBeforeLoad(void);
|
||||
|
||||
void Savegame_SetDefaultStats(const GF_LEVEL *level, STATS_COMMON stats);
|
||||
STATS_COMMON Savegame_GetDefaultStats(const GF_LEVEL *level);
|
||||
|
||||
#define REGISTER_SAVEGAME_STRATEGY(strategy_) \
|
||||
__attribute__((__constructor__)) static void M_Register(void) \
|
||||
{ \
|
||||
Savegame_RegisterStrategy(strategy_); \
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
#include "game/inventory.h"
|
||||
#include "game/requester.h"
|
||||
#include "game/savegame.h"
|
||||
#include "game/savegame/savegame_legacy.h"
|
||||
#include "global/vars.h"
|
||||
|
||||
#include <libtrx/benchmark.h>
|
||||
|
@ -18,6 +17,8 @@
|
|||
#include <libtrx/strings.h>
|
||||
#include <libtrx/utils.h>
|
||||
|
||||
#define MAX_STRATEGIES 1
|
||||
|
||||
static STATS_COMMON *m_DefaultStats = nullptr;
|
||||
static RESUME_INFO *m_ResumeInfos = nullptr;
|
||||
static int32_t m_SaveSlots = 0;
|
||||
|
@ -29,9 +30,14 @@ static SAVEGAME_INFO *m_SavegameInfo = nullptr;
|
|||
static uint32_t m_ReqFlags1[MAX_REQUESTER_ITEMS] = {};
|
||||
static uint32_t m_ReqFlags2[MAX_REQUESTER_ITEMS] = {};
|
||||
|
||||
static int32_t m_StrategyCount = 0;
|
||||
static SAVEGAME_STRATEGY m_Strategies[MAX_STRATEGIES];
|
||||
static SAVEGAME_VERSION m_InitialVersion = VERSION_LEGACY;
|
||||
|
||||
static void M_ClearSlots(void);
|
||||
static bool M_FillSlot(const int32_t slot_num, const char *const path);
|
||||
static void M_ScanSavedGamesDir(const char *const dir_path);
|
||||
static bool M_FillSlot(
|
||||
SAVEGAME_STRATEGY strategy, int32_t slot_num, const char *path);
|
||||
static void M_ScanSavedGamesDir(const char *dir_path);
|
||||
|
||||
static void M_ClearSlots(void)
|
||||
{
|
||||
|
@ -41,6 +47,7 @@ static void M_ClearSlots(void)
|
|||
|
||||
for (int32_t i = 0; i < m_SaveSlots; i++) {
|
||||
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[i];
|
||||
savegame_info->format = SAVEGAME_FORMAT_INVALID;
|
||||
savegame_info->counter = -1;
|
||||
savegame_info->level_num = -1;
|
||||
Memory_FreePointer(&savegame_info->full_path);
|
||||
|
@ -48,58 +55,24 @@ static void M_ClearSlots(void)
|
|||
}
|
||||
}
|
||||
|
||||
static bool M_FillSlot(const int32_t slot_num, const char *const path)
|
||||
static bool M_FillSlot(
|
||||
const SAVEGAME_STRATEGY strategy, const int32_t slot_num,
|
||||
const char *const path)
|
||||
{
|
||||
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_num];
|
||||
if (strategy.format <= savegame_info->format) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool result = false;
|
||||
MYFILE *const fp = File_Open(path, FILE_OPEN_READ);
|
||||
if (fp != nullptr) {
|
||||
// TODO: make strategy->fill_info
|
||||
{
|
||||
char level_title[75];
|
||||
File_ReadData(fp, level_title, 75);
|
||||
savegame_info->level_title = Memory_DupStr(level_title);
|
||||
savegame_info->counter = File_ReadS32(fp);
|
||||
|
||||
for (int32_t i = 0; i < 24; i++) {
|
||||
File_Skip(fp, sizeof(uint16_t)); // pistol ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // magnum ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // uzi ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // shotgun ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // m16 ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // grenade ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // harpoon ammo
|
||||
File_Skip(fp, sizeof(uint8_t)); // small medis
|
||||
File_Skip(fp, sizeof(uint8_t)); // big medis
|
||||
File_Skip(fp, sizeof(uint8_t)); // reserved
|
||||
File_Skip(fp, sizeof(uint8_t)); // flares
|
||||
File_Skip(fp, sizeof(int8_t)); // gun status
|
||||
File_Skip(fp, sizeof(int8_t)); // gun type
|
||||
File_Skip(fp, sizeof(uint16_t)); // flags
|
||||
File_Skip(fp, sizeof(uint16_t)); // unused
|
||||
File_Skip(fp, sizeof(uint32_t)); // timer
|
||||
File_Skip(fp, sizeof(uint32_t)); // ammo used
|
||||
File_Skip(fp, sizeof(uint32_t)); // hits
|
||||
File_Skip(fp, sizeof(uint32_t)); // distance
|
||||
File_Skip(fp, sizeof(uint16_t)); // kills
|
||||
File_Skip(fp, sizeof(uint8_t)); // secret flags
|
||||
File_Skip(fp, sizeof(uint8_t)); // medis used
|
||||
}
|
||||
|
||||
File_Skip(fp, sizeof(uint32_t)); // timer
|
||||
File_Skip(fp, sizeof(uint32_t)); // ammo used
|
||||
File_Skip(fp, sizeof(uint32_t)); // hits
|
||||
File_Skip(fp, sizeof(uint32_t)); // distance
|
||||
File_Skip(fp, sizeof(uint16_t)); // kills
|
||||
File_Skip(fp, sizeof(uint8_t)); // secret flags
|
||||
File_Skip(fp, sizeof(uint8_t)); // medis used
|
||||
|
||||
savegame_info->level_num = File_ReadS16(fp);
|
||||
if (strategy.fill_info_func(fp, savegame_info)) {
|
||||
savegame_info->format = strategy.format;
|
||||
Memory_FreePointer(&savegame_info->full_path);
|
||||
savegame_info->full_path = Memory_DupStr(path);
|
||||
result = true;
|
||||
}
|
||||
|
||||
Memory_FreePointer(&savegame_info->full_path);
|
||||
savegame_info->full_path = Memory_DupStr(path);
|
||||
result = true;
|
||||
File_Close(fp);
|
||||
}
|
||||
return result;
|
||||
|
@ -121,13 +94,20 @@ static void M_ScanSavedGamesDir(const char *const dir_path)
|
|||
continue;
|
||||
}
|
||||
|
||||
int32_t slot = -1;
|
||||
int32_t parsed =
|
||||
sscanf(file_name, g_GameFlow.savegame_fmt_legacy, &slot);
|
||||
if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) {
|
||||
char *file_path = String_Format("%s/%s", dir_path, file_name);
|
||||
M_FillSlot(slot, file_path);
|
||||
Memory_FreePointer(&file_path);
|
||||
for (int32_t i = 0; i < m_StrategyCount; i++) {
|
||||
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
|
||||
if (!strategy.allow_load) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int32_t slot = -1;
|
||||
const int32_t parsed =
|
||||
sscanf(file_name, strategy.get_save_file_pattern_func(), &slot);
|
||||
if (parsed == 1 && slot >= 0 && slot < m_SaveSlots) {
|
||||
char *file_path = String_Format("%s/%s", dir_path, file_name);
|
||||
M_FillSlot(strategy, slot, file_path);
|
||||
Memory_FreePointer(&file_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -165,6 +145,13 @@ static void M_LoadPostprocess(void)
|
|||
MovableBlock_SetupFloor();
|
||||
}
|
||||
|
||||
void Savegame_RegisterStrategy(const SAVEGAME_STRATEGY strategy)
|
||||
{
|
||||
ASSERT(m_StrategyCount < MAX_STRATEGIES);
|
||||
m_Strategies[m_StrategyCount] = strategy;
|
||||
m_StrategyCount++;
|
||||
}
|
||||
|
||||
void Savegame_Init(void)
|
||||
{
|
||||
m_ResumeInfos = Memory_Alloc(
|
||||
|
@ -277,6 +264,16 @@ int32_t Savegame_GetTotalCount(void)
|
|||
return m_SavedGames;
|
||||
}
|
||||
|
||||
SAVEGAME_VERSION Savegame_GetInitialVersion(void)
|
||||
{
|
||||
return m_InitialVersion;
|
||||
}
|
||||
|
||||
void Savegame_SetInitialVersion(const SAVEGAME_VERSION version)
|
||||
{
|
||||
m_InitialVersion = version;
|
||||
}
|
||||
|
||||
void Savegame_ProcessItemsBeforeSave(void)
|
||||
{
|
||||
for (int32_t i = 0; i < Item_GetLevelCount(); i++) {
|
||||
|
@ -530,7 +527,8 @@ void Savegame_PersistGameToCurrentInfo(const GF_LEVEL *const level)
|
|||
|
||||
bool Savegame_Save(const int32_t slot_idx)
|
||||
{
|
||||
bool ret = false;
|
||||
bool result = false;
|
||||
Savegame_BindSlot(slot_idx);
|
||||
|
||||
const GF_LEVEL *const current_level = Game_GetCurrentLevel();
|
||||
const char *const level_title = current_level->title;
|
||||
|
@ -540,24 +538,33 @@ bool Savegame_Save(const int32_t slot_idx)
|
|||
SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx];
|
||||
const bool was_slot_empty = savegame_info->full_path == nullptr;
|
||||
|
||||
char *file_name = String_Format(g_GameFlow.savegame_fmt_legacy, slot_idx);
|
||||
MYFILE *const fp = File_Open(file_name, FILE_OPEN_WRITE);
|
||||
if (fp != nullptr) {
|
||||
m_SaveCounter++;
|
||||
Savegame_Legacy_SaveToFile(fp);
|
||||
m_SaveCounter++;
|
||||
for (int32_t i = 0; i < m_StrategyCount; i++) {
|
||||
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
|
||||
if (!strategy.allow_save || strategy.save_to_file_func == nullptr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Memory_FreePointer(&savegame_info->full_path);
|
||||
savegame_info->full_path = Memory_DupStr(File_GetPath(fp));
|
||||
savegame_info->counter = m_SaveCounter;
|
||||
savegame_info->level_num = current_level->num;
|
||||
savegame_info->level_title =
|
||||
level_title != nullptr ? Memory_DupStr(level_title) : nullptr;
|
||||
File_Close(fp);
|
||||
ret = true;
|
||||
char *file_name =
|
||||
String_Format(strategy.get_save_file_pattern_func(), slot_idx);
|
||||
MYFILE *const fp = File_Open(file_name, FILE_OPEN_WRITE);
|
||||
if (fp != nullptr) {
|
||||
strategy.save_to_file_func(fp);
|
||||
savegame_info->format = strategy.format;
|
||||
Memory_FreePointer(&savegame_info->full_path);
|
||||
savegame_info->full_path = Memory_DupStr(File_GetPath(fp));
|
||||
savegame_info->counter = m_SaveCounter;
|
||||
savegame_info->level_num = current_level->num;
|
||||
savegame_info->level_title =
|
||||
level_title != nullptr ? Memory_DupStr(level_title) : nullptr;
|
||||
File_Close(fp);
|
||||
result = true;
|
||||
}
|
||||
|
||||
Memory_FreePointer(&file_name);
|
||||
}
|
||||
Memory_FreePointer(&file_name);
|
||||
|
||||
if (ret) {
|
||||
if (result) {
|
||||
char save_num_text[16];
|
||||
sprintf(save_num_text, "%d", m_SaveCounter);
|
||||
Requester_ChangeItem(
|
||||
|
@ -572,27 +579,37 @@ bool Savegame_Save(const int32_t slot_idx)
|
|||
m_SavedGames++;
|
||||
}
|
||||
Savegame_HighlightNewestSlot();
|
||||
} else {
|
||||
m_SaveCounter--;
|
||||
}
|
||||
|
||||
return ret;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Savegame_Load(const int32_t slot_idx)
|
||||
{
|
||||
const SAVEGAME_INFO *const savegame_info = &m_SavegameInfo[slot_idx];
|
||||
ASSERT(savegame_info->format != 0);
|
||||
|
||||
M_LoadPreprocess();
|
||||
|
||||
bool result = false;
|
||||
char *file_name = String_Format(g_GameFlow.savegame_fmt_legacy, slot_idx);
|
||||
MYFILE *const fp = File_Open(file_name, FILE_OPEN_READ);
|
||||
if (fp != nullptr) {
|
||||
Savegame_Legacy_LoadFromFile(fp);
|
||||
File_Close(fp);
|
||||
result = true;
|
||||
for (int32_t i = 0; i < m_StrategyCount; i++) {
|
||||
const SAVEGAME_STRATEGY strategy = m_Strategies[i];
|
||||
if (strategy.format != savegame_info->format) {
|
||||
continue;
|
||||
}
|
||||
|
||||
MYFILE *const fp = File_Open(savegame_info->full_path, FILE_OPEN_READ);
|
||||
if (fp != nullptr) {
|
||||
result = strategy.load_from_file_func(fp);
|
||||
File_Close(fp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (result) {
|
||||
M_LoadPostprocess();
|
||||
}
|
||||
M_LoadPostprocess();
|
||||
Savegame_SetInitialVersion(m_SavegameInfo[slot_idx].initial_version);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
#include "game/savegame/savegame_legacy.h"
|
||||
|
||||
#include "game/camera.h"
|
||||
#include "game/game.h"
|
||||
#include "game/game_flow.h"
|
||||
|
@ -76,6 +74,23 @@ static void M_WriteLaraArm(const LARA_ARM *arm);
|
|||
static void M_WriteAmmoInfo(const AMMO_INFO *ammo_info);
|
||||
static void M_WriteFlares(void);
|
||||
|
||||
static const char *M_GetSaveFilePattern(void);
|
||||
static bool M_FillInfo(MYFILE *fp, SAVEGAME_INFO *info);
|
||||
static void M_SaveToFile(MYFILE *fp);
|
||||
static bool M_LoadFromFile(MYFILE *fp);
|
||||
|
||||
static SAVEGAME_STRATEGY m_Strategy = {
|
||||
// clang-format off
|
||||
.allow_load = true,
|
||||
.allow_save = true,
|
||||
.format = SAVEGAME_FORMAT_LEGACY,
|
||||
.get_save_file_pattern_func = M_GetSaveFilePattern,
|
||||
.fill_info_func = M_FillInfo,
|
||||
.load_from_file_func = M_LoadFromFile,
|
||||
.save_to_file_func = M_SaveToFile,
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static void M_Reset(char *const buffer)
|
||||
{
|
||||
m_BufPos = 0;
|
||||
|
@ -642,7 +657,58 @@ static void M_WriteFlares(void)
|
|||
}
|
||||
}
|
||||
|
||||
void Savegame_Legacy_SaveToFile(MYFILE *const fp)
|
||||
static const char *M_GetSaveFilePattern(void)
|
||||
{
|
||||
return g_GameFlow.savegame_fmt_legacy;
|
||||
}
|
||||
|
||||
static bool M_FillInfo(MYFILE *const fp, SAVEGAME_INFO *const savegame_info)
|
||||
{
|
||||
char level_title[75];
|
||||
File_ReadData(fp, level_title, 75);
|
||||
savegame_info->level_title = Memory_DupStr(level_title);
|
||||
savegame_info->counter = File_ReadS32(fp);
|
||||
|
||||
for (int32_t i = 0; i < 24; i++) {
|
||||
File_Skip(fp, sizeof(uint16_t)); // pistol ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // magnum ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // uzi ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // shotgun ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // m16 ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // grenade ammo
|
||||
File_Skip(fp, sizeof(uint16_t)); // harpoon ammo
|
||||
File_Skip(fp, sizeof(uint8_t)); // small medis
|
||||
File_Skip(fp, sizeof(uint8_t)); // big medis
|
||||
File_Skip(fp, sizeof(uint8_t)); // reserved
|
||||
File_Skip(fp, sizeof(uint8_t)); // flares
|
||||
File_Skip(fp, sizeof(int8_t)); // gun status
|
||||
File_Skip(fp, sizeof(int8_t)); // gun type
|
||||
File_Skip(fp, sizeof(uint16_t)); // flags
|
||||
File_Skip(fp, sizeof(uint16_t)); // unused
|
||||
File_Skip(fp, sizeof(uint32_t)); // timer
|
||||
File_Skip(fp, sizeof(uint32_t)); // ammo used
|
||||
File_Skip(fp, sizeof(uint32_t)); // hits
|
||||
File_Skip(fp, sizeof(uint32_t)); // distance
|
||||
File_Skip(fp, sizeof(uint16_t)); // kills
|
||||
File_Skip(fp, sizeof(uint8_t)); // secret flags
|
||||
File_Skip(fp, sizeof(uint8_t)); // medis used
|
||||
}
|
||||
|
||||
File_Skip(fp, sizeof(uint32_t)); // timer
|
||||
File_Skip(fp, sizeof(uint32_t)); // ammo used
|
||||
File_Skip(fp, sizeof(uint32_t)); // hits
|
||||
File_Skip(fp, sizeof(uint32_t)); // distance
|
||||
File_Skip(fp, sizeof(uint16_t)); // kills
|
||||
File_Skip(fp, sizeof(uint8_t)); // secret flags
|
||||
File_Skip(fp, sizeof(uint8_t)); // medis used
|
||||
|
||||
savegame_info->level_num = File_ReadS16(fp);
|
||||
savegame_info->initial_version = VERSION_LEGACY;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void M_SaveToFile(MYFILE *const fp)
|
||||
{
|
||||
char *buffer = Memory_Alloc(SAVEGAME_LEGACY_TOTAL_SIZE);
|
||||
M_Reset(buffer);
|
||||
|
@ -712,7 +778,7 @@ void Savegame_Legacy_SaveToFile(MYFILE *const fp)
|
|||
Memory_FreePointer(&buffer);
|
||||
}
|
||||
|
||||
void Savegame_Legacy_LoadFromFile(MYFILE *const fp)
|
||||
static bool M_LoadFromFile(MYFILE *const fp)
|
||||
{
|
||||
char *buffer = Memory_Alloc(File_Size(fp));
|
||||
File_Seek(fp, 0, FILE_SEEK_SET);
|
||||
|
@ -798,4 +864,7 @@ void Savegame_Legacy_LoadFromFile(MYFILE *const fp)
|
|||
M_ReadFlares();
|
||||
|
||||
Memory_FreePointer(&buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
REGISTER_SAVEGAME_STRATEGY(m_Strategy)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <libtrx/filesystem.h>
|
||||
|
||||
void Savegame_Legacy_SaveToFile(MYFILE *fp);
|
||||
void Savegame_Legacy_LoadFromFile(MYFILE *fp);
|
|
@ -128,13 +128,6 @@ typedef struct {
|
|||
LEVEL_STATS stats;
|
||||
} RESUME_INFO;
|
||||
|
||||
typedef struct {
|
||||
char *full_path;
|
||||
int32_t counter;
|
||||
int32_t level_num;
|
||||
char *level_title;
|
||||
} SAVEGAME_INFO;
|
||||
|
||||
typedef struct {
|
||||
int16_t lock_angles[4];
|
||||
int16_t left_angles[4];
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue