mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-07 08:15:59 -10:00
* avoid undefined behavior from UNREACHABLE on reachable code paths
UNREACHABLE expands to __assume(false) / __builtin_unreachable() in
release builds, which is undefined behavior if the code is ever actually
reached. Many UNREACHABLE sites only guard against unexpected-but-
possible runtime states (values from tables, mission/save files, network
packets, scripting, or user input), so reaching them should degrade
gracefully rather than invoke UB.
Replace UNREACHABLE with Assertion(false, ...) at all such sites. It
still pops an error in debug builds, but in release it is a harmless
no-op that simply falls through to the following code. Sites that are
genuinely unreachable by local invariants (e.g. a switch over a bounded
random number) are left as UNREACHABLE.
A few converted sites had no safe fall-through -- an uninitialized read,
null dereference, or null call would have followed -- so they also get a
minimal recovery: initialize the value, guard the dereference, return
early, or skip the offending item.
Where the converted site was a simple guard of the form
if (x) { Assertion(false, ...); return; }
hoist the check onto the invariant instead:
Assertion(!x, ...);
if (x) { return; }
This states the expectation directly and shows the offending value in
the debug popup, while keeping the same graceful runtime degradation.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* addendum
After consultation with Lafiel, use UNREACHABLE in situations that are effectively impossible even if not provably impossible. UNREACHABLE = "this shouldn't ever happen"; Assertion = "if this ever happens, it's a coding error". Also fix a few other sites and change a couple to Warnings.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
1496 lines
49 KiB
C++
1496 lines
49 KiB
C++
|
|
#include "controlconfig/controlsconfig.h"
|
|
#include "controlconfig/presets.h"
|
|
#include "freespace.h"
|
|
#include "gamesnd/eventmusic.h"
|
|
#include "hud/hudconfig.h"
|
|
#include "hud/hudsquadmsg.h"
|
|
#include "io/joy.h"
|
|
#include "io/mouse.h"
|
|
#include "localization/localize.h"
|
|
#include "menuui/techmenu.h"
|
|
#include "network/multi.h"
|
|
#include "options/OptionsManager.h"
|
|
#include "osapi/osregistry.h"
|
|
#include "parse/sexp_container.h"
|
|
#include "pilotfile/pilotfile.h"
|
|
#include "pilotfile/BinaryFileHandler.h"
|
|
#include "pilotfile/JSONFileHandler.h"
|
|
#include "pilotfile/plr_hudprefs.h"
|
|
#include "playerman/managepilot.h"
|
|
#include "playerman/player.h"
|
|
#include "scripting/hook_api.h"
|
|
#include "scripting/api/objs/player.h"
|
|
#include "ship/ship.h"
|
|
#include "sound/audiostr.h"
|
|
#include "stats/medals.h"
|
|
#include "weapon/weapon.h"
|
|
|
|
const auto OnPlayerLoadedHook = scripting::Hook<>::Factory(
|
|
"On Player Loaded", "Called when a player has been successfully loaded",
|
|
{
|
|
{ "Player", "object", "The player object" },
|
|
});
|
|
|
|
namespace {
|
|
void read_multi_stats(pilot::FileHandler* handler, scoring_special_t* scoring) {
|
|
scoring->score = handler->readInt("score");
|
|
scoring->rank = handler->readInt("rank");
|
|
scoring->assists = handler->readInt("assists");
|
|
scoring->kill_count = handler->readInt("kill_count");
|
|
scoring->kill_count_ok = handler->readInt("kill_count_ok");
|
|
scoring->bonehead_kills = handler->readInt("bonehead_kills");
|
|
|
|
scoring->p_shots_fired = handler->readUInt("p_shots_fired");
|
|
scoring->p_shots_hit = handler->readUInt("p_shots_hit");
|
|
scoring->p_bonehead_hits = handler->readUInt("p_bonehead_hits");
|
|
|
|
scoring->s_shots_fired = handler->readUInt("s_shots_fired");
|
|
scoring->s_shots_hit = handler->readUInt("s_shots_hit");
|
|
scoring->s_bonehead_hits = handler->readUInt("s_bonehead_hits");
|
|
|
|
scoring->flight_time = handler->readUInt("flight_time");
|
|
scoring->missions_flown = handler->readUInt("missions_flown");
|
|
scoring->last_flown = (_fs_time_t)handler->readInt("last_flown");
|
|
scoring->last_backup = (_fs_time_t)handler->readInt("last_backup");
|
|
|
|
// ship kills (contains ships across all mods, not just current)
|
|
auto list_size = handler->startArrayRead("kills");
|
|
scoring->ship_kills.reserve(list_size);
|
|
|
|
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
|
|
index_list_t ilist;
|
|
ilist.name = handler->readString("name");
|
|
ilist.index = ship_info_lookup(ilist.name.c_str());
|
|
ilist.val = handler->readInt("val");
|
|
|
|
scoring->ship_kills.push_back(std::move(ilist));
|
|
}
|
|
handler->endArrayRead();
|
|
|
|
// medals earned (contains medals across all mods, not just current)
|
|
list_size = handler->startArrayRead("medals");
|
|
scoring->medals_earned.reserve(list_size);
|
|
|
|
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
|
|
index_list_t ilist;
|
|
ilist.name = handler->readString("name");
|
|
ilist.index = medals_info_lookup(ilist.name.c_str());
|
|
ilist.val = handler->readInt("val");
|
|
|
|
scoring->medals_earned.push_back(std::move(ilist));
|
|
}
|
|
handler->endArrayRead();
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_read_flags()
|
|
{
|
|
// tips?
|
|
p->tips = (int)handler->readUByte("tips");
|
|
|
|
// saved flags
|
|
p->save_flags = handler->readInt("save_flags");
|
|
|
|
// listing mode (single or campaign missions
|
|
p->readyroom_listing_mode = handler->readInt("readyroom_listing_mode");
|
|
|
|
// briefing auto-play
|
|
p->auto_advance = handler->readInt("auto_advance");
|
|
|
|
// special rank setting (to avoid having to read all stats on verify)
|
|
// will be the multi rank
|
|
// if there's a valid CSG, this will be overwritten
|
|
p->stats.rank = handler->readInt("multi_rank");
|
|
|
|
if (plr_ver > 0) {
|
|
p->player_was_multi = handler->readInt("was_multi");
|
|
} else {
|
|
p->player_was_multi = 0; // Default to single player
|
|
}
|
|
|
|
// which language was this pilot created with
|
|
if (plr_ver > 1) {
|
|
handler->readString("language", p->language, sizeof(p->language));
|
|
} else {
|
|
// if we don't know, default to the current language setting
|
|
lcl_get_language_name(p->language);
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_write_flags()
|
|
{
|
|
handler->startSectionWrite(Section::Flags);
|
|
|
|
// tips
|
|
handler->writeUByte("tips", (unsigned char)p->tips);
|
|
|
|
// saved flags
|
|
handler->writeInt("save_flags", p->save_flags);
|
|
|
|
// listing mode (single or campaign missions)
|
|
handler->writeInt("readyroom_listing_mode", p->readyroom_listing_mode);
|
|
|
|
// briefing auto-play
|
|
handler->writeInt("auto_advance", p->auto_advance);
|
|
|
|
// special rank setting (to avoid having to read all stats on verify)
|
|
// should be multi only from now on
|
|
handler->writeInt("multi_rank", multi_stats.rank);
|
|
|
|
// What game mode we were in last on this pilot
|
|
handler->writeInt("was_multi", p->player_was_multi);
|
|
|
|
// which language was this pilot created with
|
|
handler->writeString("language", p->language);
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_info()
|
|
{
|
|
// pilot image
|
|
handler->readString("image_filename", p->image_filename, MAX_FILENAME_LEN);
|
|
|
|
// multi squad name
|
|
handler->readString("squad_name", p->m_squad_name, NAME_LENGTH);
|
|
|
|
// squad image
|
|
handler->readString("squad_filename", p->m_squad_filename, MAX_FILENAME_LEN);
|
|
|
|
// active campaign
|
|
handler->readString("current_campaign", p->current_campaign, MAX_FILENAME_LEN);
|
|
}
|
|
|
|
void pilotfile::plr_write_info()
|
|
{
|
|
handler->startSectionWrite(Section::Info);
|
|
|
|
// pilot image
|
|
handler->writeString("image_filename", p->image_filename);
|
|
|
|
// multi squad name
|
|
handler->writeString("squad_name", p->m_squad_name);
|
|
|
|
// squad image
|
|
handler->writeString("squad_filename", p->m_squad_filename);
|
|
|
|
// active campaign
|
|
handler->writeString("current_campaign", p->current_campaign);
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_hud()
|
|
{
|
|
const HC_gauge_mappings& gauge_map = HC_gauge_mappings::get_instance();
|
|
|
|
int strikes = 0;
|
|
// flags
|
|
int show_flags = handler->readInt("show_flags");
|
|
int show_flags2 = handler->readInt("show_flags2");
|
|
|
|
int popup_flags = handler->readInt("popup_flags");
|
|
int popup_flags2 = handler->readInt("popup_flags2");
|
|
|
|
// Convert show_flags (0-31) and show_flags2 (32-63)
|
|
for (int i = 0; i < 64; i++) {
|
|
bool is_set = (i < 32) ? (show_flags & (1 << i)) : (show_flags2 & (1 << (i - 32)));
|
|
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
|
|
|
|
if (!gauge_id.empty()) {
|
|
HUD_config.set_gauge_visibility(gauge_id, is_set);
|
|
}
|
|
}
|
|
|
|
// Convert popup_flags (0-31) and popup_flags2 (32-63)
|
|
for (int i = 0; i < 64; i++) {
|
|
bool is_set = (i < 32) ? (popup_flags & (1 << i)) : (popup_flags2 & (1 << (i - 32)));
|
|
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
|
|
|
|
if (!gauge_id.empty()) {
|
|
HUD_config.set_gauge_popup(gauge_id, is_set);
|
|
}
|
|
}
|
|
|
|
// settings
|
|
SCP_UNUSED(handler->readUByte("num_msg_window_lines"));// Deprecated but still read for file compatibility 3/7/2025
|
|
SCP_UNUSED(handler->readInt("rp_flags"));// Deprecated but still read for file compatibility 3/7/2025
|
|
|
|
HUD_config.rp_dist = handler->readInt("rp_dist");
|
|
if (HUD_config.rp_dist < 0 || HUD_config.rp_dist >= RR_MAX_RANGES) {
|
|
ReleaseWarning(LOCATION, "Player file has invalid radar range %d, setting to default.\n", HUD_config.rp_dist);
|
|
HUD_config.rp_dist = RR_INFINITY;
|
|
strikes++;
|
|
}
|
|
|
|
// basic colors
|
|
HUD_config.main_color = handler->readInt("main_color");
|
|
if (HUD_config.main_color < 0 || HUD_config.main_color >= NUM_HUD_COLOR_PRESETS) {
|
|
ReleaseWarning(LOCATION, "Player file has invalid main color selection %i, setting to default.\n", HUD_config.main_color);
|
|
HUD_config.main_color = HUD_COLOR_PRESET_1;
|
|
strikes++;
|
|
}
|
|
|
|
HUD_color_alpha = handler->readInt("color_alpha");
|
|
if (HUD_color_alpha < HUD_COLOR_ALPHA_USER_MIN || HUD_color_alpha > HUD_COLOR_ALPHA_USER_MAX) {
|
|
ReleaseWarning(LOCATION, "Player file has invalid alpha color %i, setting to default.\n", HUD_color_alpha);
|
|
HUD_color_alpha = HUD_COLOR_ALPHA_DEFAULT;
|
|
strikes++;
|
|
}
|
|
|
|
if (strikes == 3) {
|
|
ReleaseWarning(LOCATION, "Player file has too many hud config errors, and is likely corrupted. Please verify and save your settings in the hud config menu.");
|
|
}
|
|
|
|
// This seemed to be previously used to ensure a valid color was set
|
|
// but under the new method we can rely on the getter to return a valid color
|
|
// in all cases. So comment this out to prevent forcing a default color on
|
|
// custom gauges that rely on color by their gauge type
|
|
//hud_config_set_color(HUD_config.main_color);
|
|
|
|
// gauge-specific colors
|
|
auto num_gauges = handler->startArrayRead("hud_gauges");
|
|
for (int idx = 0; idx < static_cast<int>(num_gauges); idx++, handler->nextArraySection()) {
|
|
ubyte red = handler->readUByte("red");
|
|
ubyte green = handler->readUByte("green");
|
|
ubyte blue = handler->readUByte("blue");
|
|
ubyte alpha = handler->readUByte("alpha");
|
|
|
|
|
|
if (idx >= NUM_HUD_GAUGES) {
|
|
continue;
|
|
}
|
|
|
|
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(idx);
|
|
if (!gauge_id.empty()) {
|
|
color clr;
|
|
gr_init_alphacolor(&clr, red, green, blue, alpha);
|
|
HUD_config.set_gauge_color(gauge_id, clr);
|
|
}
|
|
}
|
|
handler->endArrayRead();
|
|
}
|
|
|
|
void pilotfile::plr_write_hud()
|
|
{
|
|
handler->startSectionWrite(Section::HUD);
|
|
|
|
// Get gauge mappings instance
|
|
const HC_gauge_mappings& gauge_map = HC_gauge_mappings::get_instance();
|
|
|
|
// Initialize bitfields
|
|
int show_flags = 0, show_flags2 = 0;
|
|
int popup_flags = 0, popup_flags2 = 0;
|
|
|
|
// Convert show_flags_map to bitfield
|
|
for (int i = 0; i < 64; i++) {
|
|
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
|
|
if (!gauge_id.empty() && HUD_config.is_gauge_visible(gauge_id)) {
|
|
if (i < 32) {
|
|
show_flags |= (1 << i);
|
|
} else {
|
|
show_flags2 |= (1 << (i - 32));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert popup_flags_map to bitfield
|
|
for (int i = 0; i < 64; i++) {
|
|
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(i);
|
|
if (!gauge_id.empty() && HUD_config.is_gauge_popup(gauge_id)) {
|
|
if (i < 32) {
|
|
popup_flags |= (1 << i);
|
|
} else {
|
|
popup_flags2 |= (1 << (i - 32));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write converted bitfields
|
|
handler->writeInt("show_flags", show_flags);
|
|
handler->writeInt("show_flags2", show_flags2);
|
|
|
|
handler->writeInt("popup_flags", popup_flags);
|
|
handler->writeInt("popup_flags2", popup_flags2);
|
|
|
|
// settings
|
|
handler->writeUByte("num_msg_window_lines", 0);// Deprecated but still written for file compatibility 3/7/2025
|
|
handler->writeInt("rp_flags", 0);// Deprecated but still written for file compatibility 3/7/2025
|
|
|
|
handler->writeInt("rp_dist", HUD_config.rp_dist);
|
|
|
|
// basic colors
|
|
handler->writeInt("main_color", HUD_config.main_color);
|
|
handler->writeInt("color_alpha", HUD_color_alpha);
|
|
|
|
// gauge-specific colors
|
|
handler->startArrayWrite("hud_gauges", NUM_HUD_GAUGES);
|
|
for (int idx = 0; idx < NUM_HUD_GAUGES; idx++) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
|
|
// Get the gauge string ID from numeric ID
|
|
SCP_string gauge_id = gauge_map.get_string_id_from_numeric_id(idx);
|
|
color clr = HUD_config.get_gauge_color(gauge_id);
|
|
|
|
handler->writeUByte("red", clr.red);
|
|
handler->writeUByte("green", clr.green);
|
|
handler->writeUByte("blue", clr.blue);
|
|
handler->writeUByte("alpha", clr.alpha);
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_variables()
|
|
{
|
|
auto list_size = handler->startArrayRead("variables");
|
|
|
|
p->variables.reserve(list_size);
|
|
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
|
|
sexp_variable n_var;
|
|
n_var.type = handler->readInt("type");
|
|
handler->readString("text", n_var.text, TOKEN_LENGTH);
|
|
handler->readString("variable_name", n_var.variable_name, TOKEN_LENGTH);
|
|
|
|
Assert(n_var.type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE);
|
|
if (n_var.type & SEXP_VARIABLE_SAVE_TO_PLAYER_FILE) {
|
|
p->variables.push_back(std::move(n_var));
|
|
}
|
|
}
|
|
handler->endArrayRead();
|
|
}
|
|
|
|
void pilotfile::plr_write_variables()
|
|
{
|
|
handler->startSectionWrite(Section::Variables);
|
|
|
|
handler->startArrayWrite("variables", p->variables.size());
|
|
for (auto& var : p->variables) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
|
|
handler->writeInt("type", var.type);
|
|
handler->writeString("text", var.text);
|
|
handler->writeString("variable_name", var.variable_name);
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_containers()
|
|
{
|
|
const size_t list_size = handler->startArrayRead("containers");
|
|
|
|
p->containers.reserve(list_size);
|
|
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
|
|
p->containers.emplace_back();
|
|
auto& container = p->containers.back();
|
|
|
|
container.container_name = handler->readString("container_name");
|
|
|
|
container.type = (ContainerType)handler->readInt("type");
|
|
container.opf_type = handler->readInt("opf_type");
|
|
|
|
const int size = handler->readInt("size");
|
|
Assert(size >= 0);
|
|
|
|
if (container.is_list()) {
|
|
for (int i = 0; i < size; ++i) {
|
|
SCP_string data_idx_str = SCP_string("data_") + std::to_string(i);
|
|
container.list_data.emplace_back(handler->readString(data_idx_str.c_str()));
|
|
}
|
|
} else if (container.is_map()) {
|
|
for (int i = 0; i < size; ++i) {
|
|
SCP_string key_idx_str = SCP_string("key_") + std::to_string(i);
|
|
SCP_string data_idx_str = SCP_string("data_") + std::to_string(i);
|
|
SCP_string key = handler->readString(key_idx_str.c_str());
|
|
SCP_string data = handler->readString(data_idx_str.c_str());
|
|
container.map_data.emplace(key, data);
|
|
}
|
|
} else {
|
|
UNREACHABLE("Unknown container type %d", static_cast<int>(container.type));
|
|
}
|
|
|
|
Assert(container.is_eternal());
|
|
if (!container.is_eternal()) {
|
|
p->containers.pop_back();
|
|
}
|
|
}
|
|
handler->endArrayRead();
|
|
}
|
|
|
|
void pilotfile::plr_write_containers()
|
|
{
|
|
handler->startSectionWrite(Section::Containers);
|
|
|
|
handler->startArrayWrite("containers", p->containers.size());
|
|
for (const auto& container : p->containers) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
|
|
handler->writeString("container_name", container.container_name.c_str());
|
|
|
|
handler->writeInt("type", (int)container.type);
|
|
handler->writeInt("opf_type", container.opf_type);
|
|
|
|
handler->writeInt("size", container.size());
|
|
|
|
int i = 0;
|
|
|
|
if (container.is_list()) {
|
|
for (const auto& data : container.list_data) {
|
|
SCP_string data_idx_str = SCP_string("data_") + std::to_string(i);
|
|
handler->writeString(data_idx_str.c_str(), data.c_str());
|
|
++i;
|
|
}
|
|
} else if (container.is_map()) {
|
|
for (const auto& key_data : container.map_data) {
|
|
SCP_string key_idx_str = SCP_string("key_") + std::to_string(i);
|
|
SCP_string data_idx_str = SCP_string("data_") + std::to_string(i);
|
|
handler->writeString(key_idx_str.c_str(), key_data.first.c_str());
|
|
handler->writeString(data_idx_str.c_str(), key_data.second.c_str());
|
|
++i;
|
|
}
|
|
} else {
|
|
UNREACHABLE("Unknown container type %d", static_cast<int>(container.type));
|
|
}
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_multiplayer()
|
|
{
|
|
// netgame options
|
|
p->m_server_options.squad_set = handler->readUByte("squad_set");
|
|
p->m_server_options.endgame_set = handler->readUByte("endgame_set");
|
|
p->m_server_options.flags = handler->readInt("server_flags");
|
|
p->m_server_options.respawn = handler->readUInt("respawn");
|
|
p->m_server_options.max_observers = handler->readUByte("max_observers");
|
|
p->m_server_options.skill_level = handler->readUByte("skill_level");
|
|
p->m_server_options.voice_qos = handler->readUByte("voice_qos");
|
|
p->m_server_options.voice_token_wait = handler->readInt("voice_token_wait");
|
|
p->m_server_options.voice_record_time = handler->readInt("voice_record_time");
|
|
p->m_server_options.mission_time_limit = (fix)handler->readInt("mission_time_limit");
|
|
p->m_server_options.kill_limit = handler->readInt("kill_limit");
|
|
|
|
// local options
|
|
p->m_local_options.flags = handler->readInt("local_flags");
|
|
p->m_local_options.obj_update_level = handler->readInt("obj_update_level");
|
|
|
|
// Make sure multi options from player file are reflected by the OptionsManager
|
|
options::OptionsManager::instance()->set_ingame_binary_option("Multi.LocalBroadcast", (p->m_local_options.flags & MLO_FLAG_LOCAL_BROADCAST));
|
|
options::OptionsManager::instance()->set_ingame_binary_option("Multi.FlushCache", (p->m_local_options.flags & MLO_FLAG_FLUSH_CACHE));
|
|
options::OptionsManager::instance()->set_ingame_binary_option("Multi.TransferMissions", (p->m_local_options.flags & MLO_FLAG_XFER_MULTIDATA));
|
|
|
|
// netgame protocol
|
|
Multi_options_g.protocol = handler->readInt("protocol");
|
|
|
|
if (Multi_options_g.protocol == NET_VMT) {
|
|
Multi_options_g.protocol = NET_TCP;
|
|
Multi_options_g.pxo = true;
|
|
// also update PXO in-game value
|
|
options::OptionsManager::instance()->set_ingame_binary_option("Multi.TogglePXO", Multi_options_g.pxo);
|
|
} else if (Multi_options_g.protocol != NET_TCP) {
|
|
Multi_options_g.protocol = NET_TCP;
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_write_multiplayer()
|
|
{
|
|
handler->startSectionWrite(Section::Multiplayer);
|
|
|
|
// netgame options
|
|
handler->writeUByte("squad_set", p->m_server_options.squad_set);
|
|
handler->writeUByte("endgame_set", p->m_server_options.endgame_set);
|
|
handler->writeInt("server_flags", p->m_server_options.flags);
|
|
handler->writeUInt("respawn", p->m_server_options.respawn);
|
|
handler->writeUByte("max_observers", p->m_server_options.max_observers);
|
|
handler->writeUByte("skill_level", p->m_server_options.skill_level);
|
|
handler->writeUByte("voice_qos", p->m_server_options.voice_qos);
|
|
handler->writeInt("voice_token_wait", p->m_server_options.voice_token_wait);
|
|
handler->writeInt("voice_record_time", p->m_server_options.voice_record_time);
|
|
handler->writeInt("mission_time_limit", (int)p->m_server_options.mission_time_limit);
|
|
handler->writeInt("kill_limit", p->m_server_options.kill_limit);
|
|
|
|
// local options
|
|
handler->writeInt("local_flags", p->m_local_options.flags);
|
|
handler->writeInt("obj_update_level", p->m_local_options.obj_update_level);
|
|
|
|
// netgame protocol
|
|
if (Multi_options_g.pxo) {
|
|
const int protocol = NET_VMT;
|
|
handler->writeInt("protocol", protocol);
|
|
} else {
|
|
handler->writeInt("protocol", Multi_options_g.protocol);
|
|
}
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_stats()
|
|
{
|
|
// global, all-time stats (used only until campaign stats are loaded)
|
|
read_multi_stats(handler.get(), &all_time_stats);
|
|
|
|
// if not in multiplayer mode then set these stats as the player stats
|
|
if ( !(Game_mode & GM_MULTIPLAYER) ) {
|
|
p->stats.score = all_time_stats.score;
|
|
p->stats.rank = all_time_stats.rank;
|
|
p->stats.assists = all_time_stats.assists;
|
|
p->stats.kill_count = all_time_stats.kill_count;
|
|
p->stats.kill_count_ok = all_time_stats.kill_count_ok;
|
|
p->stats.bonehead_kills = all_time_stats.bonehead_kills;
|
|
|
|
p->stats.p_shots_fired = all_time_stats.p_shots_fired;
|
|
p->stats.p_shots_hit = all_time_stats.p_shots_hit;
|
|
p->stats.p_bonehead_hits = all_time_stats.p_bonehead_hits;
|
|
|
|
p->stats.s_shots_fired = all_time_stats.s_shots_fired;
|
|
p->stats.s_shots_hit = all_time_stats.s_shots_hit;
|
|
p->stats.s_bonehead_hits = all_time_stats.s_bonehead_hits;
|
|
|
|
p->stats.flight_time = all_time_stats.flight_time;
|
|
p->stats.missions_flown = all_time_stats.missions_flown;
|
|
p->stats.last_flown = all_time_stats.last_flown;
|
|
p->stats.last_backup = all_time_stats.last_backup;
|
|
|
|
// ship kills (have to find ones that match content)
|
|
auto list_size = all_time_stats.ship_kills.size();
|
|
for (size_t idx = 0; idx < list_size; idx++) {
|
|
auto j = all_time_stats.ship_kills[idx].index;
|
|
|
|
if (j >= 0) {
|
|
p->stats.kills[j] = all_time_stats.ship_kills[idx].val;
|
|
}
|
|
}
|
|
|
|
// medals earned (have to find ones that match content)
|
|
list_size = all_time_stats.medals_earned.size();
|
|
for (size_t idx = 0; idx < list_size; idx++) {
|
|
auto j = all_time_stats.medals_earned[idx].index;
|
|
|
|
if (j >= 0) {
|
|
p->stats.medal_counts[j] = all_time_stats.medals_earned[idx].val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_write_stats()
|
|
{
|
|
handler->startSectionWrite(Section::Scoring);
|
|
|
|
// global, all-time stats
|
|
handler->writeInt("score", all_time_stats.score);
|
|
handler->writeInt("rank", all_time_stats.rank);
|
|
handler->writeInt("assists", all_time_stats.assists);
|
|
handler->writeInt("kill_count", all_time_stats.kill_count);
|
|
handler->writeInt("kill_count_ok", all_time_stats.kill_count_ok);
|
|
handler->writeInt("bonehead_kills", all_time_stats.bonehead_kills);
|
|
|
|
handler->writeUInt("p_shots_fired", all_time_stats.p_shots_fired);
|
|
handler->writeUInt("p_shots_hit", all_time_stats.p_shots_hit);
|
|
handler->writeUInt("p_bonehead_hits", all_time_stats.p_bonehead_hits);
|
|
|
|
handler->writeUInt("s_shots_fired", all_time_stats.s_shots_fired);
|
|
handler->writeUInt("s_shots_hit", all_time_stats.s_shots_hit);
|
|
handler->writeUInt("s_bonehead_hits", all_time_stats.s_bonehead_hits);
|
|
|
|
handler->writeUInt("flight_time", all_time_stats.flight_time);
|
|
handler->writeUInt("missions_flown", all_time_stats.missions_flown);
|
|
handler->writeInt("last_flown", (int)all_time_stats.last_flown);
|
|
handler->writeInt("last_backup", (int)all_time_stats.last_backup);
|
|
|
|
// ship kills (contains ships across all mods, not just current)
|
|
handler->startArrayWrite("kills", all_time_stats.ship_kills.size());
|
|
for (auto& kill : all_time_stats.ship_kills) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
handler->writeString("name", kill.name.c_str());
|
|
handler->writeInt("val", kill.val);
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
// medals earned (contains medals across all mods, not just current)
|
|
handler->startArrayWrite("medals", all_time_stats.medals_earned.size());
|
|
for (auto& medal : all_time_stats.medals_earned) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
handler->writeString("name", medal.name.c_str());
|
|
handler->writeInt("val", medal.val);
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_stats_multi()
|
|
{
|
|
// global, all-time stats (used only until campaign stats are loaded)
|
|
read_multi_stats(handler.get(), &multi_stats);
|
|
|
|
// if in multiplayer mode then set these stats as the player stats
|
|
if (Game_mode & GM_MULTIPLAYER) {
|
|
p->stats.score = multi_stats.score;
|
|
p->stats.rank = multi_stats.rank;
|
|
p->stats.assists = multi_stats.assists;
|
|
p->stats.kill_count = multi_stats.kill_count;
|
|
p->stats.kill_count_ok = multi_stats.kill_count_ok;
|
|
p->stats.bonehead_kills = multi_stats.bonehead_kills;
|
|
|
|
p->stats.p_shots_fired = multi_stats.p_shots_fired;
|
|
p->stats.p_shots_hit = multi_stats.p_shots_hit;
|
|
p->stats.p_bonehead_hits = multi_stats.p_bonehead_hits;
|
|
|
|
p->stats.s_shots_fired = multi_stats.s_shots_fired;
|
|
p->stats.s_shots_hit = multi_stats.s_shots_hit;
|
|
p->stats.s_bonehead_hits = multi_stats.s_bonehead_hits;
|
|
|
|
p->stats.flight_time = multi_stats.flight_time;
|
|
p->stats.missions_flown = multi_stats.missions_flown;
|
|
p->stats.last_flown = multi_stats.last_flown;
|
|
p->stats.last_backup = multi_stats.last_backup;
|
|
|
|
// ship kills (have to find ones that match content)
|
|
auto list_size = multi_stats.ship_kills.size();
|
|
for (size_t idx = 0; idx < list_size; idx++) {
|
|
auto j = multi_stats.ship_kills[idx].index;
|
|
|
|
if (j >= 0) {
|
|
p->stats.kills[j] = multi_stats.ship_kills[idx].val;
|
|
}
|
|
}
|
|
|
|
// medals earned (have to find ones that match content)
|
|
list_size = (int)multi_stats.medals_earned.size();
|
|
for (size_t idx = 0; idx < list_size; idx++) {
|
|
auto j = multi_stats.medals_earned[idx].index;
|
|
|
|
if (j >= 0) {
|
|
p->stats.medal_counts[j] = multi_stats.medals_earned[idx].val;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_write_stats_multi()
|
|
{
|
|
handler->startSectionWrite(Section::ScoringMulti);
|
|
|
|
// global, all-time stats
|
|
handler->writeInt("score", multi_stats.score);
|
|
handler->writeInt("rank", multi_stats.rank);
|
|
handler->writeInt("assists", multi_stats.assists);
|
|
handler->writeInt("kill_count", multi_stats.kill_count);
|
|
handler->writeInt("kill_count_ok", multi_stats.kill_count_ok);
|
|
handler->writeInt("bonehead_kills", multi_stats.bonehead_kills);
|
|
|
|
handler->writeUInt("p_shots_fired", multi_stats.p_shots_fired);
|
|
handler->writeUInt("p_shots_hit", multi_stats.p_shots_hit);
|
|
handler->writeUInt("p_bonehead_hits", multi_stats.p_bonehead_hits);
|
|
|
|
handler->writeUInt("s_shots_fired", multi_stats.s_shots_fired);
|
|
handler->writeUInt("s_shots_hit", multi_stats.s_shots_hit);
|
|
handler->writeUInt("s_bonehead_hits", multi_stats.s_bonehead_hits);
|
|
|
|
handler->writeUInt("flight_time", multi_stats.flight_time);
|
|
handler->writeUInt("missions_flown", multi_stats.missions_flown);
|
|
handler->writeInt("last_flown", (int)multi_stats.last_flown);
|
|
handler->writeInt("last_backup", (int)multi_stats.last_backup);
|
|
|
|
// ship kills (contains medals across all mods, not just current)
|
|
handler->startArrayWrite("kills", multi_stats.ship_kills.size());
|
|
for (auto& kill : multi_stats.ship_kills) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
handler->writeString("name", kill.name.c_str());
|
|
handler->writeInt("val", kill.val);
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
// medals earned (contains medals across all mods, not just current)
|
|
handler->startArrayWrite("medals", multi_stats.medals_earned.size());
|
|
for (auto& medal : multi_stats.medals_earned) {
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
handler->writeString("name", medal.name.c_str());
|
|
handler->writeInt("val", medal.val);
|
|
handler->endSectionWrite();
|
|
}
|
|
handler->endArrayWrite();
|
|
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_read_controls()
|
|
{
|
|
if (plr_ver < 4) {
|
|
// PLR < 4
|
|
short id1, id2;
|
|
int axi, inv;
|
|
|
|
// Set up preset name, we'll populate the rest of the preset's data later
|
|
CC_preset preset;
|
|
preset.name = filename;
|
|
|
|
// strip off extension
|
|
auto n = preset.name.find_last_of('.');
|
|
preset.name.resize(n);
|
|
|
|
// Load in the bindings to Control_config
|
|
// ...First the digital controls
|
|
auto list_size = handler->startArrayRead("controls", true);
|
|
for (size_t idx = 0; idx < list_size; idx++, handler->nextArraySection()) {
|
|
id1 = handler->readShort("key");
|
|
id2 = handler->readShort("joystick");
|
|
handler->readShort("mouse");
|
|
|
|
if (idx < Control_config.size()) {
|
|
// Force the bindings into Control_config
|
|
Control_config[idx].take(CC_bind(CID_KEYBOARD, id1), 0);
|
|
Control_config[idx].take(CC_bind(CID_JOY0, id2), 1);
|
|
}
|
|
}
|
|
handler->endArrayRead();
|
|
|
|
// ...Then the analog controls
|
|
auto list_axis = handler->startArrayRead("axes");
|
|
for (size_t idx = 0; idx < list_axis; idx++, handler->nextArraySection()) {
|
|
axi = handler->readInt("axis_map");
|
|
inv = handler->readInt("invert_axis");
|
|
|
|
if (idx < NUM_JOY_AXIS_ACTIONS) {
|
|
Control_config[idx + JOY_AXIS_BEGIN].take(CC_bind(CID_JOY0, static_cast<short>(axi), CCF_AXIS), 0);
|
|
Control_config[idx + JOY_AXIS_BEGIN].first.invert(inv != 0);
|
|
Control_config[idx + JOY_AXIS_BEGIN].second.invert(inv != 0);
|
|
}
|
|
}
|
|
handler->endArrayRead();
|
|
|
|
// Check that these bindings are in a preset. If it is not, create a new preset file
|
|
auto it = control_config_get_current_preset();
|
|
if (it == Control_config_presets.end()) {
|
|
std::copy(Control_config.begin(), Control_config.end(), std::back_inserter(preset.bindings));
|
|
Control_config_presets.push_back(preset);
|
|
|
|
// Try to save the preset file
|
|
if (!save_preset_file(preset, false)) {
|
|
Warning(LOCATION, "Could not save controls preset file (%s)", preset.name.c_str());
|
|
}
|
|
}
|
|
return;
|
|
|
|
} else {
|
|
// read PLR >= 4
|
|
SCP_string buf = handler->readString("preset");
|
|
|
|
auto it = std::find_if(Control_config_presets.begin(), Control_config_presets.end(),
|
|
[&buf](const CC_preset& preset) { return preset.name == buf; });
|
|
|
|
if (it == Control_config_presets.end()) {
|
|
Assertion(!Control_config_presets.empty(), "[PLR] Error reading Controls! Control_config_presets empty! Get a coder!");
|
|
|
|
// Couldn't find the preset, use defaults
|
|
ReleaseWarning(LOCATION, "Could not find preset %s, using defaults\n", buf.c_str());
|
|
it = Control_config_presets.begin();
|
|
}
|
|
|
|
control_config_use_preset(*it);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_write_controls()
|
|
{
|
|
handler->startSectionWrite(Section::Controls);
|
|
|
|
// Save the default bindings to prevent crash. See github issue 3902
|
|
auto& Control_config_default = Control_config_presets[0].bindings;
|
|
handler->startArrayWrite("controls", JOY_AXIS_BEGIN, true);
|
|
for (size_t i = 0; i < JOY_AXIS_BEGIN; i++) {
|
|
auto& item = Control_config_default[i];
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
|
|
handler->writeShort("key", item.get_btn(CID_KEYBOARD));
|
|
handler->writeShort("joystick", item.get_btn(CID_JOY0));
|
|
handler->writeShort("mouse", -1);
|
|
|
|
handler->endSectionWrite(); // Section::Unnamed
|
|
}
|
|
handler->endArrayWrite(); // Array::controls
|
|
|
|
handler->startArrayWrite("axes", NUM_JOY_AXIS_ACTIONS);
|
|
for (size_t idx = JOY_AXIS_BEGIN; idx < JOY_AXIS_END; idx++) {
|
|
auto& item = Control_config_default[idx];
|
|
|
|
handler->startSectionWrite(Section::Unnamed);
|
|
|
|
// Combine mouse and joy0 to joy0 axis. Needed because controlsconfigdefaults.tbl may change the defaults.
|
|
int joy = static_cast<int>(item.get_btn(CID_JOY0));
|
|
int mouse = static_cast<int>(item.get_btn(CID_MOUSE));
|
|
if (joy >= 0) {
|
|
handler->writeInt("axis_map", joy);
|
|
|
|
} else if (mouse >= 0) {
|
|
handler->writeInt("axis_map", mouse);
|
|
|
|
} else {
|
|
handler->writeInt("axis_map", -1);
|
|
}
|
|
|
|
handler->writeInt("invert_axis", (item.first.is_inverted() || item.second.is_inverted()) ? 1 : 0);
|
|
|
|
handler->endSectionWrite(); // Section::Unnamed
|
|
}
|
|
handler->endArrayWrite();
|
|
// End issue 3902
|
|
|
|
|
|
// As of PLR v4, control bindings are saved outside of the PLR file into PST files.
|
|
// Save the currently selected preset
|
|
auto it = control_config_get_current_preset();
|
|
|
|
if (it == Control_config_presets.end()) {
|
|
// No current preset selected. what? Might be a new player...
|
|
Assertion(!Control_config_presets.empty(), "[PLR] Error saving controls! Control_config_presets empty! Get a coder!");
|
|
|
|
// Just bash it to defaults
|
|
it = Control_config_presets.begin();
|
|
}
|
|
|
|
handler->writeString("preset", it->name.c_str());
|
|
|
|
handler->endSectionWrite(); // Section::controls
|
|
}
|
|
|
|
void pilotfile::plr_read_settings()
|
|
{
|
|
clamped_range_warnings.clear();
|
|
// sound/voice/music
|
|
float temp_volume = handler->readFloat("master_sound_volume");
|
|
clamp_value_with_warn(&temp_volume, 0.f, 1.f, "Effects Volume");
|
|
snd_set_effects_volume(temp_volume);
|
|
options::OptionsManager::instance()->set_ingame_range_option("Audio.Effects", Master_sound_volume);
|
|
|
|
temp_volume = handler->readFloat("master_event_music_volume");
|
|
clamp_value_with_warn(&temp_volume, 0.f, 1.f, "Music Volume");
|
|
event_music_set_volume(temp_volume);
|
|
options::OptionsManager::instance()->set_ingame_range_option("Audio.Music", Master_event_music_volume);
|
|
|
|
temp_volume = handler->readFloat("aster_voice_volume");
|
|
clamp_value_with_warn(&temp_volume, 0.f, 1.f, "Voice Volume");
|
|
snd_set_voice_volume(temp_volume);
|
|
options::OptionsManager::instance()->set_ingame_range_option("Audio.Voice", Master_voice_volume);
|
|
|
|
Briefing_voice_enabled = handler->readInt("briefing_voice_enabled") != 0;
|
|
options::OptionsManager::instance()->set_ingame_binary_option("Audio.BriefingVoice", Briefing_voice_enabled);
|
|
|
|
// skill level
|
|
Game_skill_level = handler->readInt("game_skill_level");
|
|
clamp_value_with_warn(&Game_skill_level, 0, 4, "Skill Level");
|
|
options::OptionsManager::instance()->set_ingame_range_option("Game.SkillLevel", Game_skill_level);
|
|
|
|
// input options
|
|
Use_mouse_to_fly = handler->readInt("use_mouse_to_fly") != 0;
|
|
options::OptionsManager::instance()->set_ingame_binary_option("Input.UseMouse", Use_mouse_to_fly);
|
|
|
|
Mouse_sensitivity = handler->readInt("mouse_sensitivity");
|
|
clamp_value_with_warn(&Mouse_sensitivity, 0, 9, "Mouse Sensitivity");
|
|
options::OptionsManager::instance()->set_ingame_range_option("Input.MouseSensitivity", Mouse_sensitivity);
|
|
|
|
Joy_sensitivity = handler->readInt("joy_sensitivity");
|
|
clamp_value_with_warn(&Joy_sensitivity, 0, 9, "Joystick Sensitivity");
|
|
options::OptionsManager::instance()->set_ingame_range_option("Input.JoystickSensitivity", Joy_sensitivity);
|
|
|
|
Joy_dead_zone_size = handler->readInt("joy_dead_zone_size");
|
|
clamp_value_with_warn(&Joy_dead_zone_size, 0, 45, "Joystick Deadzone");
|
|
options::OptionsManager::instance()->set_ingame_range_option("Input.JoystickDeadZome", Joy_dead_zone_size);
|
|
|
|
// detail
|
|
//Preset not handled by OptionsManager
|
|
int setting_value = handler->readInt("setting");
|
|
clamp_value_with_warn(&setting_value, -1, static_cast<int>(DefaultDetailPreset::Num_detail_presets) - 1, "Detail Level Preset");
|
|
Detail.setting = static_cast<DefaultDetailPreset>(setting_value);
|
|
|
|
Detail.nebula_detail = handler->readInt("nebula_detail");
|
|
clamp_value_with_warn(&Detail.nebula_detail, 0, MAX_DETAIL_VALUE, "Nebula Detail");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.NebulaDetail", Detail.nebula_detail);
|
|
|
|
Detail.detail_distance = handler->readInt("detail_distance");
|
|
clamp_value_with_warn(&Detail.detail_distance, 0, MAX_DETAIL_VALUE, "Model Detail");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.Detail", Detail.detail_distance);
|
|
|
|
Detail.hardware_textures = handler->readInt("hardware_textures");
|
|
clamp_value_with_warn(&Detail.hardware_textures, 0, MAX_DETAIL_VALUE, "3D Hardware Textures");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.Texture", Detail.hardware_textures);
|
|
|
|
Detail.num_small_debris = handler->readInt("num_small_debris");
|
|
clamp_value_with_warn(&Detail.num_small_debris, 0, MAX_DETAIL_VALUE, "Impact Effects");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.SmallDebris", Detail.num_small_debris);
|
|
|
|
Detail.num_particles = handler->readInt("num_particles");
|
|
clamp_value_with_warn(&Detail.num_particles, 0, MAX_DETAIL_VALUE, "Particles");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.Particles", Detail.num_particles);
|
|
|
|
Detail.num_stars = handler->readInt("num_stars");
|
|
clamp_value_with_warn(&Detail.num_stars, 0, MAX_DETAIL_VALUE, "Stars");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.Stars", Detail.num_stars);
|
|
|
|
Detail.shield_effects = handler->readInt("shield_effects");
|
|
clamp_value_with_warn(&Detail.shield_effects, 0, MAX_DETAIL_VALUE, "Shield Hit Effects");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.ShieldEffects", Detail.shield_effects);
|
|
|
|
Detail.lighting = handler->readInt("lighting");
|
|
clamp_value_with_warn(&Detail.lighting, 0, MAX_DETAIL_VALUE, "Lighting");
|
|
options::OptionsManager::instance()->set_ingame_multi_option("Graphics.Lighting", Detail.lighting);
|
|
|
|
//Rest not handled by OptionsManager
|
|
Detail.targetview_model = handler->readInt("targetview_model");
|
|
Detail.planets_suns = handler->readInt("planets_suns");
|
|
Detail.weapon_extras = handler->readInt("weapon_extras");
|
|
|
|
if (!clamped_range_warnings.empty()) {
|
|
ReleaseWarning(LOCATION, "The following values in the pilot file were out of bounds and were automatically reset:\n%s\nPlease check your settings!\n", clamped_range_warnings.c_str());
|
|
clamped_range_warnings.clear();
|
|
}
|
|
}
|
|
|
|
void pilotfile::plr_write_settings()
|
|
{
|
|
handler->startSectionWrite(Section::Settings);
|
|
|
|
// sound/voice/music
|
|
clamp_value_with_warn(&Master_sound_volume, 0.f, 1.f, "Effects Volume");
|
|
handler->writeFloat("master_sound_volume", Master_sound_volume);
|
|
clamp_value_with_warn(&Master_event_music_volume, 0.f, 1.f, "Music Volume");
|
|
handler->writeFloat("master_event_music_volume", Master_event_music_volume);
|
|
clamp_value_with_warn(&Master_voice_volume, 0.f, 1.f, "Voice Volume");
|
|
handler->writeFloat("aster_voice_volume", Master_voice_volume);
|
|
|
|
handler->writeInt("briefing_voice_enabled", Briefing_voice_enabled ? 1 : 0);
|
|
|
|
// skill level
|
|
clamp_value_with_warn(&Game_skill_level, 0, 4, "Skill Level");
|
|
handler->writeInt("game_skill_level", Game_skill_level);
|
|
|
|
// input options
|
|
handler->writeInt("use_mouse_to_fly", Use_mouse_to_fly);
|
|
clamp_value_with_warn(&Mouse_sensitivity, 0, 9, "Mouse Sensitivity");
|
|
handler->writeInt("mouse_sensitivity", Mouse_sensitivity);
|
|
clamp_value_with_warn(&Joy_sensitivity, 0, 9, "Joystick Sensitivity");
|
|
handler->writeInt("joy_sensitivity", Joy_sensitivity);
|
|
clamp_value_with_warn(&Joy_dead_zone_size, 0, 45, "Joystick Deadzone");
|
|
handler->writeInt("joy_dead_zone_size", Joy_dead_zone_size);
|
|
|
|
// detail
|
|
int setting_value = static_cast<int>(Detail.setting); // Convert enum to int for clamping
|
|
clamp_value_with_warn(&setting_value, -1, static_cast<int>(DefaultDetailPreset::Num_detail_presets) - 1, "Detail Level Preset");
|
|
Detail.setting = static_cast<DefaultDetailPreset>(setting_value); // Convert back to enum
|
|
handler->writeInt("setting", setting_value);
|
|
|
|
clamp_value_with_warn(&Detail.nebula_detail, 0, MAX_DETAIL_VALUE, "Nebula Detail");
|
|
handler->writeInt("nebula_detail", Detail.nebula_detail);
|
|
clamp_value_with_warn(&Detail.detail_distance, 0, MAX_DETAIL_VALUE, "Model Detail");
|
|
handler->writeInt("detail_distance", Detail.detail_distance);
|
|
clamp_value_with_warn(&Detail.hardware_textures, 0, MAX_DETAIL_VALUE, "3D Hardware Textures");
|
|
handler->writeInt("hardware_textures", Detail.hardware_textures);
|
|
clamp_value_with_warn(&Detail.num_small_debris, 0, MAX_DETAIL_VALUE, "Impact Effects");
|
|
handler->writeInt("num_small_debris", Detail.num_small_debris);
|
|
clamp_value_with_warn(&Detail.num_particles, 0, MAX_DETAIL_VALUE, "Particles");
|
|
handler->writeInt("num_particles", Detail.num_particles);
|
|
clamp_value_with_warn(&Detail.num_stars, 0, MAX_DETAIL_VALUE, "Stars");
|
|
handler->writeInt("num_stars", Detail.num_stars);
|
|
clamp_value_with_warn(&Detail.shield_effects, 0, MAX_DETAIL_VALUE, "Shield Hit Effects");
|
|
handler->writeInt("shield_effects", Detail.shield_effects);
|
|
clamp_value_with_warn(&Detail.lighting, 0, MAX_DETAIL_VALUE, "Lighting");
|
|
handler->writeInt("lighting", Detail.lighting);
|
|
handler->writeInt("targetview_model", Detail.targetview_model);
|
|
handler->writeInt("planets_suns", Detail.planets_suns);
|
|
handler->writeInt("weapon_extras", Detail.weapon_extras);
|
|
|
|
if (!clamped_range_warnings.empty()) {
|
|
ReleaseWarning(LOCATION, "The following values were out of bounds when saving the Pilot file and were automatically reset.\n%s\nThis shouldn't be possible, please contact the FreeSpace 2 Open Source Code Project!\n", clamped_range_warnings.c_str());
|
|
clamped_range_warnings.clear();
|
|
}
|
|
handler->endSectionWrite();
|
|
}
|
|
|
|
void pilotfile::plr_reset_data(bool reset_all)
|
|
{
|
|
// internals
|
|
m_have_flags = false;
|
|
m_have_info = false;
|
|
|
|
m_data_invalid = false;
|
|
|
|
// if we aren't reloading all data (such as just a verify) then skip the rest
|
|
if ( !reset_all ) {
|
|
return;
|
|
}
|
|
|
|
Assertion(p != nullptr, "player pointer is null during data reset!");
|
|
|
|
// set all the entries in the control config arrays to -1 (undefined)
|
|
control_config_clear();
|
|
|
|
// init stats
|
|
p->stats.init();
|
|
|
|
// reset scoring lists
|
|
scoring_special_t blank_score;
|
|
|
|
all_time_stats = blank_score;
|
|
multi_stats = std::move(blank_score);
|
|
|
|
// clear variables
|
|
p->variables.clear();
|
|
|
|
// clear containers
|
|
p->containers.clear();
|
|
|
|
// reset techroom to defaults (CSG will override this, multi will use defaults)
|
|
tech_reset_to_default();
|
|
}
|
|
|
|
void pilotfile::plr_close()
|
|
{
|
|
if (cfp) {
|
|
cfclose(cfp);
|
|
cfp = NULL;
|
|
}
|
|
if (handler) {
|
|
handler.reset();
|
|
}
|
|
|
|
p = NULL;
|
|
filename = "";
|
|
|
|
ship_list.clear();
|
|
weapon_list.clear();
|
|
intel_list.clear();
|
|
medals_list.clear();
|
|
|
|
m_have_flags = false;
|
|
m_have_info = false;
|
|
|
|
plr_ver = PLR_VERSION_INVALID;
|
|
}
|
|
|
|
bool pilotfile::load_player(const char* callsign, player* _p, bool force_binary)
|
|
{
|
|
// if we're a standalone server in multiplayer, just fill in some bogus values
|
|
// since we don't have a pilot file
|
|
if ( (Game_mode & GM_MULTIPLAYER) && (Game_mode & GM_STANDALONE_SERVER) ) {
|
|
Player->insignia_texture = -1;
|
|
strcpy_s(Player->callsign, NOX("Standalone"));
|
|
strcpy_s(Player->short_callsign, NOX("Standalone"));
|
|
|
|
return true;
|
|
}
|
|
|
|
// set player ptr first thing
|
|
p = _p;
|
|
|
|
if ( !p ) {
|
|
Assert( (Player_num >= 0) && (Player_num < MAX_PLAYERS) );
|
|
p = &Players[Player_num];
|
|
}
|
|
|
|
filename = callsign;
|
|
if (force_binary) {
|
|
// Caller want to read the binary file
|
|
filename += ".plr";
|
|
} else {
|
|
// The default is the JSON file
|
|
filename += ".json";
|
|
}
|
|
|
|
if ( filename.size() == 4 ) {
|
|
mprintf(("PLR => Invalid filename '%s'!\n", filename.c_str()));
|
|
return false;
|
|
}
|
|
|
|
auto fp = cfopen(filename.c_str(), "rb", CF_TYPE_PLAYERS, false,
|
|
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
|
|
if ( !fp ) {
|
|
mprintf(("PLR => Unable to open '%s' for reading!\n", filename.c_str()));
|
|
return false;
|
|
}
|
|
|
|
if (force_binary) {
|
|
handler.reset(new pilot::BinaryFileHandler(fp));
|
|
} else {
|
|
try {
|
|
handler.reset(new pilot::JSONFileHandler(fp, true));
|
|
} catch (const std::exception& e) {
|
|
mprintf(("PLR => Failed to parse JSON: %s\n", e.what()));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
unsigned int plr_id = handler->readUInt("signature");
|
|
|
|
if (plr_id != PLR_FILE_ID) {
|
|
mprintf(("PLR => Invalid header id for '%s'!\n", filename.c_str()));
|
|
plr_close();
|
|
return false;
|
|
}
|
|
|
|
// version, now used
|
|
plr_ver = handler->readUByte("version");
|
|
|
|
mprintf(("PLR => Loading '%s' with version %d...\n", filename.c_str(), plr_ver));
|
|
|
|
//true resets everything, false sets up file verify.
|
|
plr_reset_data(true);
|
|
|
|
// the point of all this: read in the PLR contents
|
|
handler->beginSectionRead();
|
|
while (handler->hasMoreSections()) {
|
|
auto section_id = handler->nextSection();
|
|
try {
|
|
switch (section_id) {
|
|
case Section::Flags:
|
|
mprintf(("PLR => Parsing: Flags...\n"));
|
|
plr_read_flags();
|
|
break;
|
|
|
|
case Section::Info:
|
|
mprintf(("PLR => Parsing: Info...\n"));
|
|
plr_read_info();
|
|
break;
|
|
|
|
case Section::Variables:
|
|
mprintf(("PLR => Parsing: Variables...\n"));
|
|
plr_read_variables();
|
|
break;
|
|
|
|
case Section::Containers:
|
|
mprintf(("PLR => Parsing: Containers...\n"));
|
|
plr_read_containers();
|
|
break;
|
|
|
|
case Section::HUD:
|
|
mprintf(("PLR => Parsing: HUD...\n"));
|
|
plr_read_hud();
|
|
break;
|
|
|
|
case Section::Scoring:
|
|
mprintf(("PLR => Parsing: Scoring...\n"));
|
|
plr_read_stats();
|
|
break;
|
|
|
|
case Section::ScoringMulti:
|
|
mprintf(("PLR => Parsing: ScoringMulti...\n"));
|
|
plr_read_stats_multi();
|
|
break;
|
|
|
|
case Section::Multiplayer:
|
|
mprintf(("PLR => Parsing: Multiplayer...\n"));
|
|
plr_read_multiplayer();
|
|
break;
|
|
|
|
case Section::Controls:
|
|
mprintf(("PLR => Parsing: Controls...\n"));
|
|
plr_read_controls();
|
|
break;
|
|
|
|
case Section::Settings:
|
|
mprintf(("PLR => Parsing: Settings...\n"));
|
|
plr_read_settings();
|
|
break;
|
|
|
|
case Section::Invalid:
|
|
plr_close();
|
|
return false;
|
|
|
|
default:
|
|
mprintf(("PLR => Skipping unknown section 0x%04x!\n", (uint32_t)section_id));
|
|
break;
|
|
}
|
|
} catch (const cfile::max_read_length &msg) {
|
|
// read to max section size, move to next section, discarding
|
|
// extra/unknown data
|
|
mprintf(("PLR => (0x%04x) %s\n", (uint32_t)section_id, msg.what()));
|
|
} catch (const char *err) {
|
|
mprintf(("PLR => ERROR: %s\n", err));
|
|
plr_close();
|
|
return false;
|
|
}
|
|
}
|
|
handler->endSectionRead();
|
|
|
|
mprintf(("HUDPREFS => Loading extended player HUD preferences...\n"));
|
|
hud_config_load_player_prefs(callsign);
|
|
|
|
// Probably don't need to persist these to disk but it'll make sure on next boot we start with these player options set
|
|
// The github tests don't know what to do with the ini file so I guess we'll skip this for now
|
|
//options::OptionsManager::instance()->persistChanges();
|
|
|
|
// restore the callsign into the Player structure
|
|
strcpy_s(p->callsign, callsign);
|
|
|
|
// restore the truncated callsign into Player structure
|
|
pilot_set_short_callsign(p, SHORT_CALLSIGN_PIXEL_W);
|
|
|
|
player_set_squad_bitmap(p, p->m_squad_filename, true);
|
|
|
|
// Flags to signal the main UI the state of the loaded player file
|
|
// Do these here after player_read_flags() so they don't get trashed!
|
|
if (plr_ver < 4) {
|
|
p->save_flags |= PLAYER_FLAGS_PLR_VER_PRE_CONTROLS5;
|
|
}
|
|
|
|
if (plr_ver < PLR_VERSION) {
|
|
p->flags |= PLAYER_FLAGS_PLR_VER_IS_LOWER;
|
|
|
|
} else if (plr_ver > PLR_VERSION) {
|
|
p->flags |= PLAYER_FLAGS_PLR_VER_IS_HIGHER;
|
|
}
|
|
|
|
mprintf(("PLR => Loading complete!\n"));
|
|
if (OnPlayerLoadedHook->isActive()) {
|
|
OnPlayerLoadedHook->run(scripting::hook_param_list(scripting::hook_param("Player", 'o', scripting::api::l_Player.Set(scripting::api::player_h(p)))));
|
|
}
|
|
|
|
// cleanup and return
|
|
plr_close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool pilotfile::save_player(player *_p)
|
|
{
|
|
// never save a pilot file for the standalone server in multiplayer
|
|
if ( (Game_mode & GM_MULTIPLAYER) && (Game_mode & GM_STANDALONE_SERVER) ) {
|
|
return false;
|
|
}
|
|
|
|
// set player ptr first thing
|
|
p = _p;
|
|
|
|
if ( !p ) {
|
|
Assert( (Player_num >= 0) && (Player_num < MAX_PLAYERS) );
|
|
p = &Players[Player_num];
|
|
}
|
|
|
|
if ( !strlen(p->callsign) ) {
|
|
return false;
|
|
}
|
|
|
|
filename = p->callsign;
|
|
if ( filename.empty() ) {
|
|
mprintf(("PLR => Invalid filename '%s'!\n", filename.c_str()));
|
|
return false;
|
|
}
|
|
|
|
// We always write the player file as JSON now
|
|
filename += ".json";
|
|
|
|
// open it, hopefully...
|
|
auto fp = cfopen(filename.c_str(), "wb", CF_TYPE_PLAYERS, false,
|
|
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
|
|
|
|
if ( !fp ) {
|
|
mprintf(("PLR => Unable to open '%s' for saving!\n", filename.c_str()));
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
handler.reset(new pilot::JSONFileHandler(fp, false));
|
|
} catch (const std::exception& e) {
|
|
mprintf(("PLR => Failed to parse JSON: %s\n", e.what()));
|
|
return false;
|
|
}
|
|
|
|
// header and version
|
|
handler->writeInt("signature", PLR_FILE_ID);
|
|
handler->writeUByte("version", PLR_VERSION);
|
|
|
|
mprintf(("PLR => Saving '%s' with version %d...\n", filename.c_str(), (int)PLR_VERSION));
|
|
|
|
handler->beginWritingSections();
|
|
|
|
// flags and info sections go first
|
|
mprintf(("PLR => Saving: Flags...\n"));
|
|
plr_write_flags();
|
|
mprintf(("PLR => Saving: Info...\n"));
|
|
plr_write_info();
|
|
|
|
// everything else is next, not order specific
|
|
mprintf(("PLR => Saving: Scoring...\n"));
|
|
plr_write_stats();
|
|
mprintf(("PLR => Saving: ScoringMulti...\n"));
|
|
plr_write_stats_multi();
|
|
mprintf(("PLR => Saving: HUD...\n"));
|
|
plr_write_hud();
|
|
mprintf(("PLR => Saving: Variables...\n"));
|
|
plr_write_variables();
|
|
mprintf(("PLR => Saving: Containers...\n"));
|
|
plr_write_containers();
|
|
mprintf(("PLR => Saving: Multiplayer...\n"));
|
|
plr_write_multiplayer();
|
|
mprintf(("PLR => Saving: Controls...\n"));
|
|
plr_write_controls();
|
|
mprintf(("PLR => Saving: Settings...\n"));
|
|
plr_write_settings();
|
|
|
|
handler->endWritingSections();
|
|
|
|
handler->flush();
|
|
|
|
mprintf(("HUDPREFS => Saving player HUD preferences (testing)...\n"));
|
|
hud_config_save_player_prefs(p->callsign);
|
|
|
|
// Done!
|
|
mprintf(("PLR => Saving complete!\n"));
|
|
|
|
plr_close();
|
|
|
|
return true;
|
|
}
|
|
|
|
bool pilotfile::verify(const char *fname, int *rank, char *valid_language, int* flags)
|
|
{
|
|
player t_plr;
|
|
|
|
t_plr.reset(); // Ensure t_plr is cleanly init
|
|
p = &t_plr; // Set player* so the rest of the file handler can work
|
|
|
|
filename = fname;
|
|
|
|
if ( filename.size() == 4 ) {
|
|
mprintf(("PLR => Invalid filename '%s'!\n", filename.c_str()));
|
|
return false;
|
|
}
|
|
|
|
auto fp = cfopen(filename.c_str(), "rb", CF_TYPE_PLAYERS, false,
|
|
CF_LOCATION_ROOT_USER | CF_LOCATION_ROOT_GAME | CF_LOCATION_TYPE_ROOT);
|
|
|
|
if ( !fp ) {
|
|
mprintf(("PLR => Unable to open '%s'!\n", filename.c_str()));
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
handler.reset(new pilot::JSONFileHandler(fp, true));
|
|
} catch (const std::exception& e) {
|
|
mprintf(("PLR => Failed to parse JSON: %s\n", e.what()));
|
|
return false;
|
|
}
|
|
|
|
unsigned int plr_id = handler->readUInt("signature");
|
|
|
|
if (plr_id != PLR_FILE_ID) {
|
|
mprintf(("PLR => Invalid header id for '%s'!\n", filename.c_str()));
|
|
plr_close();
|
|
return false;
|
|
}
|
|
|
|
// version, now used
|
|
plr_ver = handler->readUByte("version");
|
|
|
|
mprintf(("PLR => Verifying '%s' with version %d...\n", filename.c_str(), plr_ver));
|
|
|
|
// true resets everything, false sets up file verify.
|
|
plr_reset_data(false);
|
|
|
|
bool have_flags = false;
|
|
bool have_info = false;
|
|
// the point of all this: read in the PLR contents
|
|
handler->beginSectionRead();
|
|
while (!(have_flags && have_info) && handler->hasMoreSections()) {
|
|
auto section_id = handler->nextSection();
|
|
try {
|
|
switch (section_id) {
|
|
case Section::Flags:
|
|
mprintf(("PLR => Parsing: Flags...\n"));
|
|
have_flags = true;
|
|
plr_read_flags();
|
|
break;
|
|
|
|
// now reading the Info section to get the campaign
|
|
// and be able to lookup the campaign rank
|
|
case Section::Info:
|
|
mprintf(("PLR => Parsing: Info...\n"));
|
|
have_info = true;
|
|
plr_read_info();
|
|
break;
|
|
|
|
case Section::Invalid:
|
|
plr_close();
|
|
return false;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
} catch (cfile::max_read_length &msg) {
|
|
// read to max section size, move to next section, discarding
|
|
// extra/unknown data
|
|
mprintf(("PLR => (0x%04x) %s\n", (uint32_t)section_id, msg.what()));
|
|
} catch (const char *err) {
|
|
mprintf(("PLR => ERROR: %s\n", err));
|
|
plr_close();
|
|
return false;
|
|
}
|
|
}
|
|
handler->endSectionRead();
|
|
|
|
// Flags to signal the main UI the state of the loaded player file
|
|
// Do these here after player_read_flags() so they don't get trashed!
|
|
if (plr_ver < 4) {
|
|
t_plr.save_flags |= PLAYER_FLAGS_PLR_VER_PRE_CONTROLS5;
|
|
}
|
|
|
|
if (plr_ver < PLR_VERSION) {
|
|
t_plr.flags |= PLAYER_FLAGS_PLR_VER_IS_LOWER;
|
|
|
|
} else if (plr_ver > PLR_VERSION) {
|
|
t_plr.flags |= PLAYER_FLAGS_PLR_VER_IS_HIGHER;
|
|
}
|
|
|
|
if (valid_language) {
|
|
strcpy(valid_language, t_plr.language);
|
|
}
|
|
|
|
// need to cleanup early to ensure everything is OK for use in the CSG next
|
|
|
|
// Save any player flags, if caller wants them
|
|
if (flags != nullptr) {
|
|
*flags = t_plr.flags;
|
|
}
|
|
|
|
plr_close();
|
|
|
|
if (rank) {
|
|
// maybe get the rank from the CSG
|
|
if ( !(Game_mode & GM_MULTIPLAYER) ) {
|
|
// build the csg filename
|
|
// since filename/fname was validated above, perform less safety checks here
|
|
filename = fname;
|
|
filename = filename.replace(filename.find_last_of('.')+1,filename.npos, t_plr.current_campaign);
|
|
filename.append(".csg");
|
|
|
|
if (!this->get_csg_rank(rank)) {
|
|
// if we failed to get the csg rank, default to multi rank
|
|
*rank = t_plr.stats.rank;
|
|
}
|
|
} else {
|
|
// if the CSG isn't valid, or for multi, use this rank
|
|
*rank = t_plr.stats.rank;
|
|
}
|
|
}
|
|
|
|
mprintf(("PLR => Verifying complete!\n"));
|
|
|
|
return true;
|
|
}
|