1102 lines
44 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Syroot.BinaryData;
using Syroot.Worms.Core;
namespace Syroot.Worms.Armageddon
{
/// <summary>
/// Represents a scheme stored in a WSC file which contains game settings and armory configuration, including
/// RubberWorm settings encoded in those.
/// Used by WA and WWP. S. https://worms2d.info/Game_scheme_file.
/// </summary>
public class Scheme : ILoadableFile, ISaveableFile
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const string _signature = "SCHM";
// Lookup tables.
private static readonly Dictionary<byte, byte> _mapWaterRiseToRaw = new Dictionary<byte, byte>() { [0] = 0,
[5] = 1, [13] = 19, [20] = 2, [21] = 55, [29] = 43, [37] = 47, [45] = 3, [52] = 26, [53] = 25, [61] = 27,
[64] = 8, [69] = 33, [77] = 13, [80] = 4, [84] = 18, [85] = 23, [93] = 11, [101] = 15, [109] = 29,
[116] = 22, [117] = 57, [125] = 5, [133] = 63, [141] = 45, [148] = 30, [149] = 9, [157] = 21, [165] = 17,
[173] = 61, [180] = 6, [181] = 39, [189] = 37, [197] = 31, [205] = 51, [208] = 12, [212] = 14, [213] = 41,
[221] = 53, [229] = 49, [237] = 35, [244] = 10, [245] = 7, [253] = 59 };
private static readonly byte[] _objectCounts = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14,
15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85,
90, 95, 100, 110, 120, 130, 140, 150, 160, 170, 180, 190, 200, 210, 220, 230, 240, 250 };
// ---- MEMBERS ------------------------------------------------------------------------------------------------
private byte _mineDelay;
private byte _turnTime;
private byte _roundTimeMinutes;
private byte _roundTimeSeconds;
private byte _numberOfWins;
private sbyte _rwGravity;
private sbyte _rwGravityConstBlackHole;
private sbyte _rwGravityPropBlackHole;
private byte _rwKaosMod;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes a new instance of the <see cref="Scheme"/> class.
/// </summary>
public Scheme()
{
Version = SchemeVersion.Extended;
Weapons = new SchemeWeaponSetting[GetWeaponCount()];
}
/// <summary>
/// Initializes a new instance of the <see cref="Scheme"/> class, loading the data from the given
/// <see cref="Stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to load the data from.</param>
public Scheme(Stream stream)
{
Load(stream);
}
/// <summary>
/// Initializes a new instance of the <see cref="Scheme"/> class, loading the data from the given file.
/// </summary>
/// <param name="fileName">The name of the file to load the data from.</param>
public Scheme(string fileName)
{
Load(fileName);
}
// ---- PROPERTIES ---------------------------------------------------------------------------------------------
/// <summary>
/// Gets or sets the <see cref="SchemeVersion"/> of this scheme, controlling whether super weapon settings
/// are stored or not.
/// </summary>
public SchemeVersion Version { get; set; }
/// <summary>
/// Gets or sets the delay in seconds between each team's turn to allow relaxed switching of seats.
/// </summary>
public byte HotSeatDelay { get; set; }
/// <summary>
/// Gets or sets the time in seconds available for a worm to retreat after using a weapon which ends the turn
/// while standing on land.
/// </summary>
public byte RetreatTime { get; set; }
/// <summary>
/// Gets or sets the time in seconds available for a worm to retreat after using a weapon which ends the turn
/// while on a rope.
/// </summary>
public byte RetreatTimeRope { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the total round time until sudden death will be displayed in the
/// turn timer.
/// </summary>
public bool ShowRoundTime { get; set; }
/// <summary>
/// Gets or sets a value indicating whether significant turns will be replayed in offline games.
/// </summary>
public bool AutomaticReplays { get; set; }
/// <summary>
/// Gets or sets the percentual amount of fall damage applied, relative to normal fall damage being 100%.
/// </summary>
public SchemeFallDamage FallDamage { get; set; }
/// <summary>
/// Gets or sets a value indicating whether worms cannot walk and are mostly stuck at their current position
/// without using any utilities.
/// </summary>
public bool ArtilleryMode { get; set; }
/// <summary>
/// Gets or sets a value indicating the scheme editor used to modify this scheme. Originally used to indicate
/// the unimplemented Bounty Mode.
/// </summary>
public SchemeEditor SchemeEditor { get; set; }
/// <summary>
/// Gets or sets a value indicating the stockpiling of armory between game rounds.
/// </summary>
public SchemeStockpiling StockpilingMode { get; set; }
/// <summary>
/// Gets or sets a value indicating the worm selection order determining the next worm to be played.
/// </summary>
public SchemeWormSelect WormSelectMode { get; set; }
/// <summary>
/// Gets or sets a value indicating the action triggered when Sudden Death starts.
/// </summary>
public SchemeSuddenDeathEvent SuddenDeathEvent { get; set; }
/// <summary>
/// Gets or sets the amount in pixels which the water will rise between turns after Sudden Death was triggered.
/// </summary>
public SchemeWaterRise WaterRiseRate { get; set; }
/// <summary>
/// Gets or sets the percentual probability of a weapon crate to drop between turns. Negative values might crash
/// the game.
/// </summary>
public sbyte WeaponCrateProbability { get; set; }
/// <summary>
/// Gets or sets a value indicating whether donor cards can spawn upon a worm's death.
/// </summary>
public bool DonorCards { get; set; }
/// <summary>
/// Gets or sets the percentual probability of a health crate to drop between turns. Negative values might crash
/// the game.
/// </summary>
public sbyte HealthCrateProbability { get; set; }
/// <summary>
/// Gets or sets the amount of health included in a health crate added to the collecting worm's energy.
/// </summary>
public byte HealthCrateEnergy { get; set; }
/// <summary>
/// Gets or sets the percentual probability of a utility crate to drop between turns. Negative values might
/// crash the game.
/// </summary>
public sbyte UtilityCrateProbability { get; set; }
/// <summary>
/// Gets or sets the type of objects which can be placed on the map.
/// </summary>
public SchemeObjectType ObjectTypes { get; set; }
/// <summary>
/// Gets or sets the maximum number of objects (mines or oil drums) on the map.
/// </summary>
public SchemeObjectCount ObjectCount { get; set; }
/// <summary>
/// Gets or sets the number of seconds a mine requires to explode. Can be 1-3 and 5-127 seconds.
/// </summary>
public byte MineDelay
{
get
{
return _mineDelay;
}
set
{
if (value == 4 || value > 0x7F)
{
throw new ArgumentException("Mine delay must be between 0-127 and not be 4.", nameof(value));
}
_mineDelay = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether the mine fuse will be randomly chosen between fractions of 1 to 3
/// seconds. If <c>true</c>, the <see cref="MineDelay"/> setting will be ignored.
/// </summary>
public bool MineDelayRandom { get; set; }
/// <summary>
/// Gets or sets a value indicating whether mines can refuse to explode after their count down.
/// </summary>
public bool DudMines { get; set; }
/// <summary>
/// Gets or sets a value indicating whether worms are placed manually by the players for their initial position
/// at round start.
/// </summary>
public bool ManualWormPlacement { get; set; }
/// <summary>
/// Gets or sets the initial worm energy at round start.
/// </summary>
public byte WormEnergy { get; set; }
/// <summary>
/// Gets or sets the turn time in seconds available for the player to move. Must be in the range of 0-127.
/// </summary>
public byte TurnTime
{
get
{
return _turnTime;
}
set
{
if (value > 0x7F)
{
throw new ArgumentException("Turn time must be between 0-127.", nameof(value));
}
_turnTime = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether the turn time is unlimited. If <c>true</c>, the
/// <see cref="TurnTime"/> setting will be ignored.
/// </summary>
public bool TurnTimeInfinite { get; set; }
/// <summary>
/// Gets or sets the round time before sudden death is triggered between 0-127 minutes.
/// </summary>
public byte RoundTimeMinutes
{
get
{
return _roundTimeMinutes;
}
set
{
if (value > 0x7F)
{
throw new ArgumentException("Round time must be between 0-127 minutes.", nameof(value));
}
_roundTimeMinutes = value;
}
}
/// <summary>
/// Gets or sets the round time before sudden death is triggered in between 0-128 seconds. When 0,
/// <see cref="RoundTimeMinutes"/> is used instead.
/// </summary>
public byte RoundTimeSeconds
{
get
{
return _roundTimeSeconds;
}
set
{
if (value > 0x80)
{
throw new ArgumentException("Round time must be between 0-128 seconds.", nameof(value));
}
_roundTimeSeconds = value;
}
}
/// <summary>
/// Gets or sets the number of round wins required to win the game. Must not be 0.
/// </summary>
public byte NumberOfWins
{
get
{
return _numberOfWins;
}
set
{
if (value == 0)
{
throw new ArgumentException("Number of wins must not be 0.", nameof(value));
}
_numberOfWins = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether blood effects are enabled.
/// </summary>
public bool Blood { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Super Sheep weapon gets upgraded to the Aqua Sheep, which can
/// fly underwater.
/// </summary>
public bool AquaSheep { get; set; }
/// <summary>
/// Gets or sets a value indicating whether sheeps will jump out of exploding crates.
/// </summary>
public bool SheepHeaven { get; set; }
/// <summary>
/// Gets or sets a value indicating whether worms have infinity energy, killable only by drowning them.
/// </summary>
public bool GodWorms { get; set; }
/// <summary>
/// Gets or sets a value indicating whether terrain cannot be destroyed by explosions.
/// </summary>
public bool IndestructibleLand { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Grenade weapon is more powerful.
/// </summary>
public bool UpgradedGrenade { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Shotgun weapon shoots twice for each of the two shots.
/// </summary>
public bool UpgradedShotgun { get; set; }
/// <summary>
/// Gets or sets a value indicating whether cluster weapon explode into more clusters.
/// </summary>
public bool UpgradedCluster { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Longbow weapon is more powerful.
/// </summary>
public bool UpgradedLongbow { get; set; }
/// <summary>
/// Gets or sets a value indicating whether team weapons will be given to the teams, overriding the default
/// weapon settings for them.
/// </summary>
public bool EnableTeamWeapons { get; set; }
/// <summary>
/// Gets or sets a value indicating whether super weapons can be collected from crates.
/// </summary>
public bool EnableSuperWeapons { get; set; }
/// <summary>
/// Gets the array of <see cref="SchemeWeaponSetting"/> instances, each mapping to one weapon at the index of
/// the <see cref="SchemeWeapon"/> enumeration. Depending on the scheme <see cref="Version"/>, super weapons
/// might not be stored in this array.
/// </summary>
public SchemeWeaponSetting[] Weapons { get; set; }
// ---- RubberWorm Settings ----
/// <summary>
/// Gets or sets a value whether power is unlocked like in TestStuff. Configurable with the /alp command.
/// </summary>
public bool RwAntiLockPower { get; set; }
/// <summary>
/// Gets or sets a value indicating whether worms falling into water will be reset to the last solid location
/// they stood on. Configurable with the /antisink command.
/// </summary>
public bool RwAntiSink { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the aim direction of a weapon will be reset at turn start.
/// Configurable with the /reaim command.
/// </summary>
public bool RwAutoReaim { get; set; }
/// <summary>
/// Gets or sets a value whether the aim of a weapon loops in full rather than half circles. Configurable with
/// the /cira command.
/// </summary>
public bool RwCircularAim { get; set; }
/// <summary>
/// Gets or sets the maximum number of crates existing on the map at the same time. 0 disables this feature,
/// default limit is 5. Configurable with the /cratelimit command.
/// </summary>
public byte RwCrateLimit { get; set; }
/// <summary>
/// Gets or sets the maximum number of crates spawning per turn and enables the crate counter. Configurable with
/// the /craterate command.
/// </summary>
public byte RwCrateRate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether crate shower is enabled throughout a turn. Configurable with the
/// /crateshower command.
/// </summary>
public bool RwCrateShower { get; set; }
/// <summary>
/// Gets or sets a value indicating whether weapon fuses can be selected between 0-9 seconds and herd counts
/// between 1-10 animals. Configurable with the /fuseex command.
/// </summary>
public bool RwExtendedFuse { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the turn timer is paused while launching a weapon. Configurable with
/// the /fdpt or /nopause commands.
/// </summary>
public bool RwFireDoesntPauseTimer { get; set; }
/// <summary>
/// Gets or sets the maximum number of flame particles active at the same time in the scale of 100x.
/// Configurable with the /flames command.
/// </summary>
public byte RwFlameLimit { get; set; }
/// <summary>
/// Gets or sets the friction deaccelerating objects touching solid ground. 96 is default, 100 is no friction,
/// higher values accelerate objects. Configurable with the /friction command.
/// </summary>
public byte RwFriction { get; set; }
/// <summary>
/// Gets or sets the amount of gravity. Ranges from -64-63 where 0 and 12 are default gravity and negative
/// values pull objects upwards. Configurable with the /gravity command. When set,
/// <see cref="RwGravityConstBlackHole"/> and <see cref="RwGravityPropBlackHole"/> are reset.
/// </summary>
public sbyte RwGravity
{
get
{
return _rwGravity;
}
set
{
if (value != 0)
{
RwGravityConstBlackHole = 0;
RwGravityPropBlackHole = 0;
}
_rwGravity = value;
}
}
/// <summary>
/// gets or sets the amount of gravity acting as a constant black hole. Ranges from -32 to 31. Configurable with
/// the /cbh command. When set, <see cref="RwGravity"/> and <see cref="RwGravityPropBlackHole"/> are reset.
/// </summary>
public sbyte RwGravityConstBlackHole
{
get
{
return _rwGravityConstBlackHole;
}
set
{
if (value != 0)
{
RwGravity = 0;
RwGravityPropBlackHole = 0;
}
_rwGravityConstBlackHole = value;
}
}
/// <summary>
/// Gets or sets the amount of gravity acting as a proportional black hole. Ranges from -32 to 31. Configurable
/// with the /pbh command. When set, <see cref="RwGravity"/> and <see cref="RwGravityConstBlackHole"/> are
/// reset.
/// </summary>
public sbyte RwGravityPropBlackHole
{
get
{
return _rwGravityPropBlackHole;
}
set
{
if (value != 0)
{
RwGravity = 0;
RwGravityConstBlackHole = 0;
}
_rwGravityPropBlackHole = value;
}
}
/// <summary>
/// Gets or sets the Kaos game scheme mod. 0 for none, 1-5 for the corresponding mod. Configurable with the
/// /kaosmod command.
/// </summary>
public byte RwKaosMod
{
get
{
return _rwKaosMod;
}
set
{
if (value > 0xF)
{
throw new ArgumentException("Kaos mod must not be greater than 15.");
}
_rwKaosMod = value;
}
}
/// <summary>
/// Gets or sets the rope knocking power in percent, where 100 is the default power. Configurable with the
/// /knock command.
/// </summary>
public byte RwKnockForce { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the loss of control while moving a worm ends does no longer end the
/// turn and allows the player to continue moving. Configurable with the /ldet or /stoicworm commands.
/// </summary>
public bool RwLoseControlDoesntEndTurn { get; set; }
/// <summary>
/// Gets or sets the maximum speed a player can reach while roping. 16 is default, 32 is like in TestStuff, 255
/// is unlimited. Configurable with the /speed command.
/// </summary>
public byte RwMaxRopeSpeed { get; set; }
/// <summary>
/// Gets or sets a value indicating whether using a weapon does no longer end the turn and allows to shoot
/// multiple weapons in one turn. Configurable with the /sdet or /multishot commands.
/// </summary>
public bool RwShotDoesntEndTurn { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the weapons Armageddon, Earthquake and Indian Nuke Test are affected
/// by <see cref="RwShotDoesntEndTurn"/> aswell. Configurable with the /usw command.
/// </summary>
public bool RwShotDoesntEndTurnAll { get; set; }
/// <summary>
/// Gets or sets a value indicating whether all objects are pushed by explosions near them. Configurable with
/// the /ope command.
/// </summary>
public bool RwObjectPushByExplosion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether worms can be selected at any time during the turn, as long as worm
/// selection is activated. Configurable with the /swat command.
/// </summary>
public bool RwSelectWormAnytime { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Ninja Rope is more powerful. Configurable with the /ir or /rope+
/// commands.
/// </summary>
public bool RwUpgradedRope { get; set; }
/// <summary>
/// Gets or sets the (special) game version to be forced. Configurable with the /version command.
/// S. http://worms2d.info/List_of_Worms_Armageddon_logic_versions.
/// </summary>
public ushort RwVersionOverride { get; set; }
/// <summary>
/// Gets or sets the amount of air viscosity affecting objects. Odd numbers affect worms too. Configurable with
/// the /visc command.
/// </summary>
public byte RwViscosity { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the selected weapon no longer resets to a remembered one upon
/// shooting it. Configurable with the /wdca command.
/// </summary>
public bool RwWeaponsDontChange { get; set; }
/// <summary>
/// Gets or sets the influence power of the wind affecting all weapons. 255 is the same influence as the
/// Bazooka. Odd numbers affect worms too. Configurable with the /wind command.
/// </summary>
public byte RwWindPower { get; set; }
/// <summary>
/// Gets or sets the power with which worms bounce off terrain, where 0 disables this feature and 255 fully
/// bounces worms back without speed loss. Configurable with the /rubber command.
/// </summary>
public byte RwWormBouncyness { get; set; }
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
/// <summary>
/// Loads the data from the given <see cref="Stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to load the data from.</param>
public void Load(Stream stream)
{
using (BinaryStream reader = new BinaryStream(stream, encoding: Encoding.ASCII, leaveOpen: true))
{
// Read the header.
if (reader.ReadString(_signature.Length) != _signature)
{
throw new InvalidDataException("Invalid WSC file signature.");
}
Version = reader.ReadEnum<SchemeVersion>(true);
// Read the options.
HotSeatDelay = reader.Read1Byte();
RetreatTime = reader.Read1Byte();
RetreatTimeRope = reader.Read1Byte();
ShowRoundTime = reader.ReadBoolean();
AutomaticReplays = reader.ReadBoolean();
FallDamage = (SchemeFallDamage)(reader.ReadByte() * 50 % 0x100 * 2);
ArtilleryMode = reader.ReadBoolean();
SchemeEditor = reader.ReadEnum<SchemeEditor>(false);
StockpilingMode = reader.ReadEnum<SchemeStockpiling>(true);
WormSelectMode = reader.ReadEnum<SchemeWormSelect>(true);
SuddenDeathEvent = reader.ReadEnum<SchemeSuddenDeathEvent>(true);
WaterRiseRate = (SchemeWaterRise)(Math.Pow(reader.ReadByte(), 2) * 5 % 0x100);
WeaponCrateProbability = reader.ReadSByte();
DonorCards = reader.ReadBoolean();
HealthCrateProbability = reader.ReadSByte();
HealthCrateEnergy = reader.Read1Byte();
UtilityCrateProbability = reader.ReadSByte();
LoadObjectTypesAndCount(reader);
LoadMineDelayConfig(reader);
DudMines = reader.ReadBoolean();
ManualWormPlacement = reader.ReadBoolean();
WormEnergy = reader.Read1Byte();
LoadTurnTimeConfig(reader);
LoadRoundTimeConfig(reader);
NumberOfWins = (byte)Math.Max(1, (int)reader.ReadByte());
Blood = reader.ReadBoolean();
AquaSheep = reader.ReadBoolean();
SheepHeaven = reader.ReadBoolean();
GodWorms = reader.ReadBoolean();
IndestructibleLand = reader.ReadBoolean();
UpgradedGrenade = reader.ReadBoolean();
UpgradedShotgun = reader.ReadBoolean();
UpgradedCluster = reader.ReadBoolean();
UpgradedLongbow = reader.ReadBoolean();
EnableTeamWeapons = reader.ReadBoolean();
EnableSuperWeapons = reader.ReadBoolean();
// Read the weapon settings. Old versions do not store super weapon settings.
Weapons = new SchemeWeaponSetting[64];
int weaponCount = GetWeaponCount();
for (int i = 0; i < weaponCount; i++)
{
Weapons[i] = reader.ReadStruct<SchemeWeaponSetting>();
}
// Ignore possible unknown WWP trash at the end of the file.
// Parse the RubberWorm settings.
LoadRubberWormSettings();
}
}
/// <summary>
/// Loads the data from the given file.
/// </summary>
/// <param name="fileName">The name of the file to load the data from.</param>
public void Load(string fileName)
{
using (FileStream stream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Load(stream);
}
}
/// <summary>
/// Saves the data into the given <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to save the data to.</param>
public void Save(Stream stream)
{
Save(stream, SchemeSaveFormat.ExtendedWithObjectCount);
}
/// <summary>
/// Saves the data into the given <paramref name="stream"/> with the specified <paramref name="format"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to save the data to.</param>
/// <param name="format">The <see cref="SchemeSaveFormat"/> to respect when storing the settings.</param>
public void Save(Stream stream, SchemeSaveFormat format)
{
using (BinaryStream writer = new BinaryStream(stream, encoding: Encoding.ASCII))
{
// Write the header.
writer.Write(_signature, StringCoding.Raw);
writer.Write((byte)Version);
// Write the options.
writer.Write(HotSeatDelay);
writer.Write(RetreatTime);
writer.Write(RetreatTimeRope);
writer.Write(ShowRoundTime);
writer.Write(AutomaticReplays);
writer.Write((byte)((int)FallDamage / 4 * 41 % 0x80));
writer.Write(ArtilleryMode);
writer.WriteEnum(SchemeEditor, false);
writer.WriteEnum(StockpilingMode, true);
writer.WriteEnum(WormSelectMode, true);
writer.WriteEnum(SuddenDeathEvent, true);
writer.Write(_mapWaterRiseToRaw[(byte)WaterRiseRate]);
writer.Write(WeaponCrateProbability);
writer.Write(DonorCards);
writer.Write(HealthCrateProbability);
writer.Write(HealthCrateEnergy);
writer.Write(UtilityCrateProbability);
SaveObjectTypesAndCount(writer, format);
SaveMineDelayConfig(writer);
writer.Write(DudMines);
writer.Write(ManualWormPlacement);
writer.Write(WormEnergy);
SaveTurnTimeConfig(writer);
SaveRoundTimeConfig(writer);
writer.Write(NumberOfWins);
writer.Write(Blood);
writer.Write(AquaSheep);
writer.Write(SheepHeaven);
writer.Write(GodWorms);
writer.Write(IndestructibleLand);
writer.Write(UpgradedGrenade);
writer.Write(UpgradedShotgun);
writer.Write(UpgradedCluster);
writer.Write(UpgradedLongbow);
writer.Write(EnableTeamWeapons);
writer.Write(EnableSuperWeapons);
// Transfer the RubberWorm settings to unused weapon configuration.
SaveRubberWormSettings();
// Write the weapon settings. Old versions do not store super weapon settings.
int weaponCount = GetWeaponCount();
foreach (SchemeWeaponSetting weapon in Weapons)
{
writer.Write(weapon);
}
// Ignore possible unknown WWP trash at the end of the file.
}
}
/// <summary>
/// Saves the data in the given file.
/// </summary>
/// <param name="fileName">The name of the file to save the data in.</param>
public void Save(string fileName)
{
Save(fileName, SchemeSaveFormat.ExtendedWithObjectCount);
}
/// <summary>
/// Saves the data in the given file with the specified <paramref name="format"/>.
/// </summary>
/// <param name="fileName">The name of the file to save the data in.</param>
/// <param name="format">The <see cref="SchemeSaveFormat"/> to respect when storing the settings.</param>
public void Save(string fileName, SchemeSaveFormat format)
{
using (FileStream stream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
{
Save(stream, format);
}
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private void LoadObjectTypesAndCount(BinaryStream reader)
{
// Invalid values default to 8 mines.
ObjectTypes = SchemeObjectType.Mines;
ObjectCount = SchemeObjectCount.Count8;
byte raw = reader.Read1Byte();
if (raw < 12)
{
// WA before 3.6.28.0 and WWP only store object type.
switch (raw)
{
case 0x00:
ObjectTypes = SchemeObjectType.None;
break;
case 0x02:
ObjectTypes = SchemeObjectType.OilDrums;
break;
case 0x05:
ObjectTypes = SchemeObjectType.Mines | SchemeObjectType.OilDrums;
break;
}
}
else
{
// WA since 3.6.28.0 encodes object type and count in one byte.
int modulo = raw % 4;
switch (modulo)
{
case 0x00:
ObjectTypes = SchemeObjectType.None;
break;
case 0x02:
ObjectTypes = SchemeObjectType.OilDrums;
break;
case 0x03:
ObjectTypes = SchemeObjectType.Mines | SchemeObjectType.OilDrums;
break;
}
ObjectCount = (SchemeObjectCount)_objectCounts[(raw - (8 + modulo)) / 4];
}
}
private void LoadMineDelayConfig(BinaryStream reader)
{
byte raw = reader.Read1Byte();
if (raw == 4 || raw > 0x7F)
{
MineDelay = 0;
MineDelayRandom = true;
}
else
{
MineDelay = raw;
MineDelayRandom = false;
}
}
private void LoadTurnTimeConfig(BinaryStream reader)
{
byte raw = reader.Read1Byte();
if (raw > 0x7F)
{
TurnTime = 0;
TurnTimeInfinite = true;
}
else
{
TurnTime = raw;
TurnTimeInfinite = false;
}
}
private void LoadRoundTimeConfig(BinaryStream reader)
{
byte raw = reader.Read1Byte();
if (raw > 0x7F)
{
RoundTimeMinutes = 0;
RoundTimeSeconds = (byte)(raw - 0x7F);
}
else
{
RoundTimeMinutes = raw;
RoundTimeSeconds = 0;
}
}
private void LoadRubberWormSettings()
{
RwEarthquakeProb earthquakeProb = (RwEarthquakeProb)Weapons[(int)SchemeWeapon.Earthquake].Probability;
RwAntiLockPower = earthquakeProb.HasFlag(RwEarthquakeProb.AntiLockPower);
RwAutoReaim = earthquakeProb.HasFlag(RwEarthquakeProb.AutoReaim);
RwCircularAim = earthquakeProb.HasFlag(RwEarthquakeProb.CircularAim);
RwShotDoesntEndTurnAll = earthquakeProb.HasFlag(RwEarthquakeProb.ShotDoesntEndTurnAll);
RwKaosMod = ((byte)earthquakeProb).DecodeByte(4, 4);
RwAntiSink = Weapons[(int)SchemeWeapon.SheepStrike].Probability != 0;
RwCrateLimit = (byte)Weapons[(int)SchemeWeapon.MagicBullet].Probability;
RwCrateRate = (byte)Weapons[(int)SchemeWeapon.NuclearTest].Probability;
RwMoleSquadronProb moleSquadronProb = (RwMoleSquadronProb)Weapons[(int)SchemeWeapon.MoleSquadron]
.Probability;
RwCrateShower = moleSquadronProb.HasFlag(RwMoleSquadronProb.CrateShower);
RwExtendedFuse = moleSquadronProb.HasFlag(RwMoleSquadronProb.ExtendedFuse);
RwFireDoesntPauseTimer = moleSquadronProb.HasFlag(RwMoleSquadronProb.FireDoesntPauseTimer);
RwLoseControlDoesntEndTurn = moleSquadronProb.HasFlag(RwMoleSquadronProb.LoseControlDoesntEndTurn);
RwObjectPushByExplosion = moleSquadronProb.HasFlag(RwMoleSquadronProb.ObjectPushByExplosion);
RwShotDoesntEndTurn = moleSquadronProb.HasFlag(RwMoleSquadronProb.ShotDoesntEndTurn);
RwUpgradedRope = moleSquadronProb.HasFlag(RwMoleSquadronProb.UpgradedRope);
RwWeaponsDontChange = moleSquadronProb.HasFlag(RwMoleSquadronProb.WeaponsDontChange);
RwFlameLimit = (byte)Weapons[(int)SchemeWeapon.ScalesOfJustice].Probability;
RwFriction = (byte)Weapons[(int)SchemeWeapon.SalvationArmy].Probability;
// 8th and 7th bit control constant / proportional black hole gravity, otherwise normal gravity.
byte mailStrikeProb = (byte)Weapons[(int)SchemeWeapon.MailStrike].Probability;
if (mailStrikeProb.GetBit(7))
{
if (mailStrikeProb.GetBit(6))
{
RwGravityPropBlackHole = mailStrikeProb.DecodeSByte(6);
}
else
{
RwGravityConstBlackHole = mailStrikeProb.DecodeSByte(6);
}
}
else
{
RwGravity = mailStrikeProb.DecodeSByte(7);
}
RwKnockForce = (byte)Weapons[(int)SchemeWeapon.SuperBananaBomb].Probability;
RwMaxRopeSpeed = (byte)Weapons[(int)SchemeWeapon.MineStrike].Probability;
RwSelectWormAnytime = ((byte)Weapons[(int)SchemeWeapon.MBBomb].Probability).GetBit(1);
byte[] versionBytes = new byte[2];
versionBytes[0] = (byte)Weapons[(int)SchemeWeapon.SelectWorm].Probability;
versionBytes[1] = (byte)Weapons[(int)SchemeWeapon.Freeze].Probability;
RwVersionOverride = BitConverter.ToUInt16(versionBytes, 0);
RwViscosity = (byte)Weapons[(int)SchemeWeapon.ConcreteDonkey].Probability;
RwWindPower = (byte)Weapons[(int)SchemeWeapon.SuicideBomber].Probability;
RwWormBouncyness = (byte)Weapons[(int)SchemeWeapon.Armageddon].Probability;
}
private int GetWeaponCount()
{
// Old versions do not store super weapon settings.
return Version == SchemeVersion.Extended ? 64 : 45;
}
private void SaveObjectTypesAndCount(BinaryStream writer, SchemeSaveFormat format)
{
byte raw = 0;
if (format == SchemeSaveFormat.ExtendedWithObjectCount)
{
// WA since 3.6.28.0 encodes object type and count in one byte.
switch (ObjectTypes)
{
case SchemeObjectType.Mines:
raw = 0x01;
break;
case SchemeObjectType.OilDrums:
raw = 0x02;
break;
case SchemeObjectType.Mines | SchemeObjectType.OilDrums:
raw = 0x03;
break;
}
// Get the index of the object count and compute the raw value from that.
int index = Array.IndexOf(_objectCounts, (byte)ObjectCount);
raw = (byte)(index * 4 + 8 + raw);
}
else
{
// WA before 3.6.28.0 and WWP only store object type.
switch (ObjectTypes)
{
case SchemeObjectType.Mines:
raw = 0x01;
break;
case SchemeObjectType.OilDrums:
raw = 0x02;
break;
case SchemeObjectType.Mines | SchemeObjectType.OilDrums:
raw = 0x05;
break;
}
}
writer.Write(raw);
}
private void SaveMineDelayConfig(BinaryStream writer)
{
if (MineDelayRandom)
{
writer.Write((byte)4);
}
else
{
writer.Write(MineDelay);
}
}
private void SaveTurnTimeConfig(BinaryStream writer)
{
if (TurnTimeInfinite)
{
writer.Write((byte)0xFF);
}
else
{
writer.Write(TurnTime);
}
}
private void SaveRoundTimeConfig(BinaryStream writer)
{
if (RoundTimeSeconds > 0)
{
writer.Write((byte)(0xFF - (RoundTimeSeconds - 1)));
}
else
{
writer.Write(RoundTimeMinutes);
}
}
private void SaveRubberWormSettings()
{
byte earthquakeProb = 0;
if (RwAntiLockPower) earthquakeProb |= (byte)RwEarthquakeProb.AntiLockPower;
if (RwAutoReaim) earthquakeProb |= (byte)RwEarthquakeProb.AutoReaim;
if (RwCircularAim) earthquakeProb |= (byte)RwEarthquakeProb.CircularAim;
if (RwShotDoesntEndTurnAll) earthquakeProb |= (byte)RwEarthquakeProb.ShotDoesntEndTurnAll;
earthquakeProb = earthquakeProb.Encode(RwKaosMod, 4);
Weapons[(int)SchemeWeapon.Earthquake].Probability = (sbyte)earthquakeProb;
Weapons[(int)SchemeWeapon.SheepStrike].Probability = (sbyte)(RwAntiSink ? 1 : 0);
Weapons[(int)SchemeWeapon.MagicBullet].Probability = (sbyte)RwCrateLimit;
Weapons[(int)SchemeWeapon.NuclearTest].Probability = (sbyte)RwCrateRate;
RwMoleSquadronProb moleSquadronProb = RwMoleSquadronProb.None;
if (RwCrateShower) moleSquadronProb |= RwMoleSquadronProb.CrateShower;
if (RwExtendedFuse) moleSquadronProb |= RwMoleSquadronProb.ExtendedFuse;
if (RwFireDoesntPauseTimer) moleSquadronProb |= RwMoleSquadronProb.FireDoesntPauseTimer;
if (RwLoseControlDoesntEndTurn) moleSquadronProb |= RwMoleSquadronProb.LoseControlDoesntEndTurn;
if (RwObjectPushByExplosion) moleSquadronProb |= RwMoleSquadronProb.ObjectPushByExplosion;
if (RwShotDoesntEndTurn) moleSquadronProb |= RwMoleSquadronProb.ShotDoesntEndTurn;
if (RwUpgradedRope) moleSquadronProb |= RwMoleSquadronProb.UpgradedRope;
if (RwWeaponsDontChange) moleSquadronProb |= RwMoleSquadronProb.WeaponsDontChange;
Weapons[(int)SchemeWeapon.MoleSquadron].Probability = (sbyte)moleSquadronProb;
Weapons[(int)SchemeWeapon.ScalesOfJustice].Probability = (sbyte)RwFlameLimit;
Weapons[(int)SchemeWeapon.SalvationArmy].Probability = (sbyte)RwFriction;
// 8th and 7th bit control constant / proportional black hole gravity, otherwise normal gravity.
byte mailStrikeProb = 0;
if (RwGravity != 0)
{
mailStrikeProb = mailStrikeProb.Encode(RwGravity, 7);
}
else if (RwGravityConstBlackHole != 0)
{
mailStrikeProb = mailStrikeProb.EnableBit(7);
mailStrikeProb = mailStrikeProb.Encode(RwGravityConstBlackHole, 6);
}
else if (RwGravityPropBlackHole != 0)
{
mailStrikeProb = mailStrikeProb.EnableBit(7);
mailStrikeProb = mailStrikeProb.EnableBit(6);
mailStrikeProb = mailStrikeProb.Encode(RwGravityPropBlackHole, 6);
}
Weapons[(int)SchemeWeapon.MailStrike].Probability = (sbyte)mailStrikeProb;
Weapons[(int)SchemeWeapon.SuperBananaBomb].Probability = (sbyte)RwKnockForce;
Weapons[(int)SchemeWeapon.MineStrike].Probability = (sbyte)RwMaxRopeSpeed;
byte mbBombProb = ((byte)Weapons[(int)SchemeWeapon.MBBomb].Probability).SetBit(0, RwSelectWormAnytime);
Weapons[(int)SchemeWeapon.MBBomb].Probability = (sbyte)mbBombProb;
byte[] versionBytes = BitConverter.GetBytes(RwVersionOverride);
Weapons[(int)SchemeWeapon.SelectWorm].Probability = (sbyte)versionBytes[0];
Weapons[(int)SchemeWeapon.Freeze].Probability = (sbyte)versionBytes[1];
Weapons[(int)SchemeWeapon.ConcreteDonkey].Probability = (sbyte)RwViscosity;
Weapons[(int)SchemeWeapon.SuicideBomber].Probability = (sbyte)RwWindPower;
Weapons[(int)SchemeWeapon.Armageddon].Probability = (sbyte)RwWormBouncyness;
}
// ---- ENUMERATIONS -------------------------------------------------------------------------------------------
[Flags]
private enum RwEarthquakeProb : byte
{
None = 0,
AutoReaim = 1 << 0,
CircularAim = 1 << 1,
AntiLockPower = 1 << 2,
ShotDoesntEndTurnAll = 1 << 3
// Remaining bits represent kaosmod version.
}
[Flags]
private enum RwMoleSquadronProb : byte
{
None = 0,
ShotDoesntEndTurn = 1 << 0,
LoseControlDoesntEndTurn = 1 << 1,
FireDoesntPauseTimer = 1 << 2,
UpgradedRope = 1 << 3,
CrateShower = 1 << 4,
ObjectPushByExplosion = 1 << 5,
WeaponsDontChange = 1 << 6,
ExtendedFuse = 1 << 7
}
}
}