use core::time::Duration;
use crate::WorldError;
use crate::content::EngineFacts;
use crate::engine_enum::Values;
use crate::engine_ids as ids;
use crate::{Host, Load, Off};
macro_rules! game_enum {
($(#[$doc:meta])* $name:ident: $field:ident { $($(#[$vdoc:meta])* $variant:ident = $id:path,)* }) => {
$(#[$doc])*
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum $name {
$($(#[$vdoc])* $variant,)*
Other(u8),
}
impl $name {
const COUNT: usize = [$(stringify!($variant)),*].len();
const IDS: [(Self, &'static str); Self::COUNT] = [$((Self::$variant, $id)),*];
}
impl Value for $name {
const SIZE: usize = 1;
fn decode(bytes: &[u8], enums: &Enums) -> Self {
let raw = bytes.first().copied().unwrap_or_default();
enums.$field.variant(u64::from(raw)).unwrap_or(Self::Other(raw))
}
fn encode(&self, out: &mut [u8], enums: &Enums) -> bool {
let value = match *self {
Self::Other(raw) => Some(raw),
variant => enums.$field.value(variant).and_then(|v| u8::try_from(v).ok()),
};
value.map(|value| out[0] = value).is_some()
}
}
};
}
game_enum! {
GameSettingsPreset: preset {
Default = ids::GAME_SETTINGS_PRESET_DEFAULT,
Relaxed = ids::GAME_SETTINGS_PRESET_RELAXED,
Hard = ids::GAME_SETTINGS_PRESET_HARD,
Survival = ids::GAME_SETTINGS_PRESET_SURVIVAL,
Custom = ids::GAME_SETTINGS_PRESET_CUSTOM,
}
}
game_enum! {
TombstoneMode: tombstone_mode {
AddBackpackMaterials = ids::TOMBSTONE_MODE_ADD_BACKPACK_MATERIALS,
Everything = ids::TOMBSTONE_MODE_EVERYTHING,
NoTombstone = ids::TOMBSTONE_MODE_NO_TOMBSTONE,
}
}
game_enum! {
WeatherFrequency: weather_frequency {
Disabled = ids::WEATHER_FREQUENCY_DISABLED,
Rare = ids::WEATHER_FREQUENCY_RARE,
Normal = ids::WEATHER_FREQUENCY_NORMAL,
Often = ids::WEATHER_FREQUENCY_OFTEN,
}
}
game_enum! {
GenericDifficulty: generic_difficulty {
VeryEasy = ids::GENERIC_DIFFICULTY_VERY_EASY,
Easy = ids::GENERIC_DIFFICULTY_EASY,
Normal = ids::GENERIC_DIFFICULTY_NORMAL,
Hard = ids::GENERIC_DIFFICULTY_HARD,
VeryHard = ids::GENERIC_DIFFICULTY_VERY_HARD,
}
}
game_enum! {
RandomSpawnerAmount: random_spawner_amount {
Few = ids::RANDOM_SPAWNER_AMOUNT_FEW,
Normal = ids::RANDOM_SPAWNER_AMOUNT_NORMAL,
Many = ids::RANDOM_SPAWNER_AMOUNT_MANY,
Extreme = ids::RANDOM_SPAWNER_AMOUNT_EXTREME,
Automatic = ids::RANDOM_SPAWNER_AMOUNT_AUTOMATIC,
}
}
game_enum! {
AggroPoolAmount: aggro_pool_amount {
Few = ids::AGGRO_POOL_AMOUNT_FEW,
Normal = ids::AGGRO_POOL_AMOUNT_NORMAL,
Many = ids::AGGRO_POOL_AMOUNT_MANY,
Extreme = ids::AGGRO_POOL_AMOUNT_EXTREME,
}
}
game_enum! {
TamingStartleRepercussion: taming_startle_repercussion {
KeepProgress = ids::TAMING_STARTLE_REPERCUSSION_KEEP_PROGRESS,
LoseSomeProgress = ids::TAMING_STARTLE_REPERCUSSION_LOSE_SOME_PROGRESS,
LoseAllProgress = ids::TAMING_STARTLE_REPERCUSSION_LOSE_ALL_PROGRESS,
}
}
game_enum! {
CurseModifier: curse_modifier {
Easy = ids::CURSE_MODIFIER_EASY,
Normal = ids::CURSE_MODIFIER_NORMAL,
Hard = ids::CURSE_MODIFIER_HARD,
}
}
#[derive(Clone, Copy, Debug)]
struct Enums {
preset: Values<GameSettingsPreset, { GameSettingsPreset::COUNT }>,
tombstone_mode: Values<TombstoneMode, { TombstoneMode::COUNT }>,
weather_frequency: Values<WeatherFrequency, { WeatherFrequency::COUNT }>,
generic_difficulty: Values<GenericDifficulty, { GenericDifficulty::COUNT }>,
random_spawner_amount: Values<RandomSpawnerAmount, { RandomSpawnerAmount::COUNT }>,
aggro_pool_amount: Values<AggroPoolAmount, { AggroPoolAmount::COUNT }>,
taming_startle_repercussion:
Values<TamingStartleRepercussion, { TamingStartleRepercussion::COUNT }>,
curse_modifier: Values<CurseModifier, { CurseModifier::COUNT }>,
}
impl Enums {
#[cfg(test)]
fn none() -> Self {
Self {
preset: Values::new(GameSettingsPreset::IDS.map(|(v, _)| (v, None))),
tombstone_mode: Values::new(TombstoneMode::IDS.map(|(v, _)| (v, None))),
weather_frequency: Values::new(WeatherFrequency::IDS.map(|(v, _)| (v, None))),
generic_difficulty: Values::new(GenericDifficulty::IDS.map(|(v, _)| (v, None))),
random_spawner_amount: Values::new(RandomSpawnerAmount::IDS.map(|(v, _)| (v, None))),
aggro_pool_amount: Values::new(AggroPoolAmount::IDS.map(|(v, _)| (v, None))),
taming_startle_repercussion: Values::new(
TamingStartleRepercussion::IDS.map(|(v, _)| (v, None)),
),
curse_modifier: Values::new(CurseModifier::IDS.map(|(v, _)| (v, None))),
}
}
fn resolve(load: &Load<'_>) -> Self {
Self {
preset: Values::resolve(load, GameSettingsPreset::IDS),
tombstone_mode: Values::resolve(load, TombstoneMode::IDS),
weather_frequency: Values::resolve(load, WeatherFrequency::IDS),
generic_difficulty: Values::resolve(load, GenericDifficulty::IDS),
random_spawner_amount: Values::resolve(load, RandomSpawnerAmount::IDS),
aggro_pool_amount: Values::resolve(load, AggroPoolAmount::IDS),
taming_startle_repercussion: Values::resolve(load, TamingStartleRepercussion::IDS),
curse_modifier: Values::resolve(load, CurseModifier::IDS),
}
}
}
trait Value: Sized {
const SIZE: usize;
fn decode(bytes: &[u8], enums: &Enums) -> Self;
fn encode(&self, out: &mut [u8], enums: &Enums) -> bool;
fn factor(&self) -> Option<f32> {
None
}
}
impl Value for f32 {
const SIZE: usize = 4;
fn decode(bytes: &[u8], _: &Enums) -> Self {
bytes
.try_into()
.map(Self::from_le_bytes)
.unwrap_or_default()
}
fn encode(&self, out: &mut [u8], _: &Enums) -> bool {
out.copy_from_slice(&self.to_le_bytes());
true
}
fn factor(&self) -> Option<f32> {
Some(*self)
}
}
impl Value for bool {
const SIZE: usize = 1;
fn decode(bytes: &[u8], _: &Enums) -> Self {
bytes.first().is_some_and(|&b| b != 0)
}
fn encode(&self, out: &mut [u8], _: &Enums) -> bool {
out[0] = u8::from(*self);
true
}
}
impl Value for u8 {
const SIZE: usize = 1;
fn decode(bytes: &[u8], _: &Enums) -> Self {
bytes.first().copied().unwrap_or_default()
}
fn encode(&self, out: &mut [u8], _: &Enums) -> bool {
out[0] = *self;
true
}
}
impl Value for Duration {
const SIZE: usize = 8;
fn decode(bytes: &[u8], _: &Enums) -> Self {
let ns = bytes.try_into().map(i64::from_le_bytes).unwrap_or_default();
Self::from_nanos(u64::try_from(ns).unwrap_or(0))
}
fn encode(&self, out: &mut [u8], _: &Enums) -> bool {
let ns = i64::try_from(self.as_nanos()).unwrap_or(i64::MAX);
out.copy_from_slice(&ns.to_le_bytes());
true
}
}
macro_rules! settings {
($($(#[$doc:meta])* $field:ident: $ty:ty = $id:path,)*) => {
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub struct Settings {
$($(#[$doc])* pub $field: $ty,)*
}
#[derive(Clone, Copy, Debug)]
struct Offsets {
$($field: usize,)*
}
impl Offsets {
fn resolve(load: &Load<'_>, size: usize) -> Result<Self, Off> {
Ok(Self {
$($field: {
let at = load.offset($id)?;
if at + <$ty as Value>::SIZE > size {
return Err(Off::new(format!("{}: past the end", $id)));
}
at
},)*
})
}
fn decode(&self, bytes: &[u8], enums: &Enums) -> Settings {
Settings {
$($field: <$ty as Value>::decode(&bytes[self.$field..self.$field + <$ty as Value>::SIZE], enums),)*
}
}
fn encode(&self, settings: &Settings, bytes: &mut [u8], enums: &Enums) -> bool {
true $(&& settings.$field.encode(&mut bytes[self.$field..self.$field + <$ty as Value>::SIZE], enums))*
}
}
#[cfg(test)]
const FIELD_IDS: &[(&str, &str)] = &[$((stringify!($field), $id)),*];
impl Settings {
fn factors(&self) -> impl Iterator<Item = f32> {
[$(self.$field.factor(),)*].into_iter().flatten()
}
}
};
}
settings! {
player_health_factor: f32 = ids::GAME_SETTINGS_PLAYER_HEALTH_FACTOR,
player_mana_factor: f32 = ids::GAME_SETTINGS_PLAYER_MANA_FACTOR,
player_stamina_factor: f32 = ids::GAME_SETTINGS_PLAYER_STAMINA_FACTOR,
player_body_heat_factor: f32 = ids::GAME_SETTINGS_PLAYER_BODY_HEAT_FACTOR,
player_diving_time_factor: f32 = ids::GAME_SETTINGS_PLAYER_DIVING_TIME_FACTOR,
enable_durability: bool = ids::GAME_SETTINGS_ENABLE_DURABILITY,
enable_starving_debuff: bool = ids::GAME_SETTINGS_ENABLE_STARVING_DEBUFF,
food_buff_duration_factor: f32 = ids::GAME_SETTINGS_FOOD_BUFF_DURATION_FACTOR,
from_hunger_to_starving: Duration = ids::GAME_SETTINGS_FROM_HUNGER_TO_STARVING,
shroud_time_factor: f32 = ids::GAME_SETTINGS_SHROUD_TIME_FACTOR,
tombstone_mode: TombstoneMode = ids::GAME_SETTINGS_TOMBSTONE_MODE,
enable_glider_turbulences: bool = ids::GAME_SETTINGS_ENABLE_GLIDER_TURBULENCES,
weather_frequency: WeatherFrequency = ids::GAME_SETTINGS_WEATHER_FREQUENCY,
fishing_difficulty: GenericDifficulty = ids::GAME_SETTINGS_FISHING_DIFFICULTY,
mining_damage_factor: f32 = ids::GAME_SETTINGS_MINING_DAMAGE_FACTOR,
plant_growth_speed_factor: f32 = ids::GAME_SETTINGS_PLANT_GROWTH_SPEED_FACTOR,
resource_drop_stack_amount_factor: f32 = ids::GAME_SETTINGS_RESOURCE_DROP_STACK_AMOUNT_FACTOR,
factory_production_speed_factor: f32 = ids::GAME_SETTINGS_FACTORY_PRODUCTION_SPEED_FACTOR,
perk_upgrade_recycling_factor: f32 = ids::GAME_SETTINGS_PERK_UPGRADE_RECYCLING_FACTOR,
perk_cost_factor: f32 = ids::GAME_SETTINGS_PERK_COST_FACTOR,
experience_combat_factor: f32 = ids::GAME_SETTINGS_EXPERIENCE_COMBAT_FACTOR,
experience_mining_factor: f32 = ids::GAME_SETTINGS_EXPERIENCE_MINING_FACTOR,
experience_exploration_quests_factor: f32 = ids::GAME_SETTINGS_EXPERIENCE_EXPLORATION_QUESTS_FACTOR,
random_spawner_amount: RandomSpawnerAmount = ids::GAME_SETTINGS_RANDOM_SPAWNER_AMOUNT,
aggro_pool_amount: AggroPoolAmount = ids::GAME_SETTINGS_AGGRO_POOL_AMOUNT,
enemy_damage_factor: f32 = ids::GAME_SETTINGS_ENEMY_DAMAGE_FACTOR,
enemy_health_factor: f32 = ids::GAME_SETTINGS_ENEMY_HEALTH_FACTOR,
enemy_stamina_factor: f32 = ids::GAME_SETTINGS_ENEMY_STAMINA_FACTOR,
enemy_perception_range_factor: f32 = ids::GAME_SETTINGS_ENEMY_PERCEPTION_RANGE_FACTOR,
boss_damage_factor: f32 = ids::GAME_SETTINGS_BOSS_DAMAGE_FACTOR,
boss_health_factor: f32 = ids::GAME_SETTINGS_BOSS_HEALTH_FACTOR,
threat_bonus: f32 = ids::GAME_SETTINGS_THREAT_BONUS,
pacify_all_enemies: bool = ids::GAME_SETTINGS_PACIFY_ALL_ENEMIES,
taming_startle_repercussion: TamingStartleRepercussion = ids::GAME_SETTINGS_TAMING_STARTLE_REPERCUSSION,
day_time_duration: Duration = ids::GAME_SETTINGS_DAY_TIME_DURATION,
night_time_duration: Duration = ids::GAME_SETTINGS_NIGHT_TIME_DURATION,
curse_modifier: CurseModifier = ids::GAME_SETTINGS_CURSE_MODIFIER,
flame_level_fog_removal: u8 = ids::GAME_SETTINGS_FLAME_LEVEL_FOG_REMOVAL,
}
impl Settings {
fn valid(&self) -> bool {
self.factors().all(|f| f.is_finite() && f >= 0.0)
&& !self.day_time_duration.is_zero()
&& !self.night_time_duration.is_zero()
}
}
#[derive(Clone, Copy, Debug)]
pub struct GameSettings {
host: Host,
offsets: Offsets,
enums: Enums,
size: usize,
}
impl GameSettings {
#[must_use]
pub fn read(&self) -> Option<(Settings, GameSettingsPreset)> {
let (bytes, preset) = self.raw().ok()?;
Some((self.offsets.decode(&bytes, &self.enums), preset))
}
pub fn change(&self, change: impl FnOnce(&mut Settings)) -> Result<Settings, WorldError> {
let (bytes, settings) = self.changed(change)?;
let size = u32::try_from(bytes.len()).map_err(|_| WorldError::Missing)?;
let status = (self.host.api().set_game_settings)(self.host.handle(), bytes.as_ptr(), size);
WorldError::from_status(status).map(|()| settings)
}
pub fn keep(&self, change: impl FnOnce(&mut Settings)) -> Result<Settings, WorldError> {
let (before, _preset) = self.raw()?;
let (bytes, settings) = self.changed(change)?;
let mask: Vec<u8> = before
.iter()
.zip(&bytes)
.map(|(a, b)| if a == b { 0 } else { 0xff })
.collect();
let size = u32::try_from(bytes.len()).map_err(|_| WorldError::Missing)?;
let status = (self.host.api().keep_game_settings)(
self.host.handle(),
bytes.as_ptr(),
mask.as_ptr(),
size,
);
WorldError::from_status(status).map(|()| settings)
}
pub fn forget_kept(&self) -> Result<(), WorldError> {
WorldError::from_status((self.host.api().forget_game_settings)(self.host.handle()))
}
fn changed(
&self,
change: impl FnOnce(&mut Settings),
) -> Result<(Vec<u8>, Settings), WorldError> {
let (mut bytes, _preset) = self.raw()?;
let mut settings = self.offsets.decode(&bytes, &self.enums);
change(&mut settings);
if !settings.valid() || !self.offsets.encode(&settings, &mut bytes, &self.enums) {
return Err(WorldError::Refused);
}
Ok((bytes, settings))
}
fn raw(&self) -> Result<(Vec<u8>, GameSettingsPreset), WorldError> {
let mut bytes = vec![0u8; self.size];
let size = u32::try_from(self.size).map_err(|_| WorldError::Missing)?;
let mut preset = 0u32;
let status = (self.host.api().game_settings)(
self.host.handle(),
bytes.as_mut_ptr(),
size,
&raw mut preset,
);
WorldError::from_status(status)?;
let preset = u8::try_from(preset).map_or(GameSettingsPreset::Other(u8::MAX), |raw| {
self.enums
.preset
.variant(u64::from(raw))
.unwrap_or(GameSettingsPreset::Other(raw))
});
Ok((bytes, preset))
}
}
impl Load<'_> {
pub fn game_settings(&mut self) -> Result<GameSettings, Off> {
let host = self.host();
let size = self.size(ids::GAME_SETTINGS)?;
Ok(GameSettings {
host,
offsets: Offsets::resolve(self, size)?,
enums: Enums::resolve(self),
size,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_field_and_variant_takes_the_game_s_name() {
for (field, id) in FIELD_IDS {
assert_eq!(Some(*field), id.strip_prefix("game_settings."), "{id}");
}
crate::engine_enum::assert_named_after_engine(&GameSettingsPreset::IDS);
crate::engine_enum::assert_named_after_engine(&TombstoneMode::IDS);
crate::engine_enum::assert_named_after_engine(&WeatherFrequency::IDS);
crate::engine_enum::assert_named_after_engine(&GenericDifficulty::IDS);
crate::engine_enum::assert_named_after_engine(&RandomSpawnerAmount::IDS);
crate::engine_enum::assert_named_after_engine(&AggroPoolAmount::IDS);
crate::engine_enum::assert_named_after_engine(&TamingStartleRepercussion::IDS);
crate::engine_enum::assert_named_after_engine(&CurseModifier::IDS);
}
#[test]
fn enums_read_the_game_s_values_and_keep_those_they_do_not_name() {
let mut enums = Enums::none();
enums.weather_frequency = Values::new([
(WeatherFrequency::Disabled, Some(7)),
(WeatherFrequency::Rare, Some(8)),
(WeatherFrequency::Normal, None),
(WeatherFrequency::Often, Some(5)),
]);
assert_eq!(
WeatherFrequency::decode(&[5], &enums),
WeatherFrequency::Often
);
assert_eq!(
WeatherFrequency::decode(&[9], &enums),
WeatherFrequency::Other(9)
);
let mut out = [0u8];
assert!(WeatherFrequency::Disabled.encode(&mut out, &enums));
assert_eq!(out, [7]);
assert!(WeatherFrequency::Other(9).encode(&mut out, &enums));
assert_eq!(out, [9]);
assert!(
!WeatherFrequency::Normal.encode(&mut out, &enums),
"a variant the build has no value for"
);
}
#[test]
fn durations_round_trip_through_nanoseconds() {
let mut bytes = [0u8; 8];
let enums = Enums::none();
assert!(Duration::from_mins(30).encode(&mut bytes, &enums));
assert_eq!(i64::from_le_bytes(bytes), 1_800_000_000_000);
assert_eq!(Duration::decode(&bytes, &enums), Duration::from_mins(30));
}
}