1485 lines
64 KiB
C#

using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Syroot.BinaryData;
using Syroot.Worms.Core;
using Syroot.Worms.IO;
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 partial class Scheme : IEquatable<Scheme>, ILoadableFile, ISaveableFile
{
// ---- CONSTANTS ----------------------------------------------------------------------------------------------
private const uint _signature = 0x4D484353; // "SCHM"
private const int _objectCount5Step = 30;
private const int _objectCount10Step = 44;
private const int _objectCountLastStep = 59;
// ---- FIELDS -------------------------------------------------------------------------------------------------
/// <summary>
/// The <see cref="SchemeVersion.Version3"/> or RubberWorm settings.
/// </summary>
public ExtendedOptions Extended = ExtendedOptions.Default;
private static readonly byte[] _waterRiseRates;
private int _fallDamage;
private byte _waterRiseIndex;
private byte _objectCount = 8;
private byte _mineDelay;
private byte _turnTime;
private byte _roundTimeMinutes;
private byte _roundTimeSeconds;
private ushort _rwVersion;
// ---- CONSTRUCTORS & DESTRUCTOR ------------------------------------------------------------------------------
/// <summary>
/// Initializes static members of the <see cref="Scheme"/> class.
/// </summary>
static Scheme()
{
// Generate water rise rate lookup array.
_waterRiseRates = new byte[256];
for (int i = 0; i < 256; i++)
_waterRiseRates[i] = (byte)(Math.Pow(i, 2) * 5 % 256);
}
/// <summary>
/// Initializes a new instance of the <see cref="Scheme"/> class.
/// </summary>
public Scheme() { }
/// <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 the available mine or oil drum object counts.
/// </summary>
public static IEnumerable<int> ValidObjectCounts
{
get
{
for (int i = 1; i < 30; i++)
yield return i;
for (int i = 30; i < 100; i += 5)
yield return i;
for (int i = 100; i <= 250; i += 10)
yield return i;
}
}
/// <summary>
/// Gets the available fall damage percentages.
/// </summary>
public static IEnumerable<int> ValidFallDamages
{
get
{
for (int i = 0; i <= 508; i += 4)
yield return i;
}
}
/// <summary>
/// Gets the available unique and ordered water rise rates in pixels.
/// </summary>
public static IEnumerable<byte> ValidWateRiseRates => _waterRiseRates.Distinct().OrderBy(x => x);
// ---- Header ----
/// <summary>
/// Gets or sets the <see cref="SchemeVersion"/> of this scheme, controlling which features are stored.
/// </summary>
public SchemeVersion Version { get; set; } = SchemeVersion.Version3;
// ---- Options ----
/// <summary>
/// Gets or sets the delay in seconds between each team's turn to allow relaxed switching of seats.
/// </summary>
public byte HotSeatTime { 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 Replays { get; set; }
/// <summary>
/// Gets or sets the percentual amount of fall damage applied, relative to normal fall damage being 100%. Valid
/// values lie between 0-508, at a granularity of 4. Note that changing this value rounds it down to a valid
/// granularity.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">The value is not between 0-508.</exception>
/// <remarks>Valid settings can be enumerated through <see cref="ValidFallDamages"/>.</remarks>
public int FallDamage
{
get => _fallDamage;
set
{
if (value < 0 || value > 508)
throw new ArgumentOutOfRangeException(nameof(value), "Fall damage must be between 0-508.");
_fallDamage = value >> 2 << 2;
}
}
/// <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 Stockpiling Stockpiling { get; set; }
/// <summary>
/// Gets or sets a value indicating the worm selection order determining the next worm to be played.
/// </summary>
public WormSelect WormSelect { get; set; }
/// <summary>
/// Gets or sets a value indicating the action triggered when Sudden Death starts.
/// </summary>
public SuddenDeathEvent SuddenDeathEvent { get; set; }
/// <summary>
/// Gets or sets the amount in pixels which the water will rise between turns after Sudden Death was triggered.
/// Note that changing this value rounds it down to a valid setting.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">The value is not between 0-253.</exception>
/// <remarks>Valid settings can be enumerated through <see cref="ValidWateRiseRates"/>.</remarks>
public byte WaterRiseRate
{
get => _waterRiseRates[_waterRiseIndex];
set
{
if (value > 253)
throw new ArgumentOutOfRangeException(nameof(value), "Water rise rate must be between 0-253.");
int smallestDeviation = Int32.MaxValue;
for (int i = 0; i < _waterRiseRates.Length; i++)
{
int deviation = value - _waterRiseRates[i];
if (deviation >= 0 && smallestDeviation > deviation)
{
smallestDeviation = deviation;
_waterRiseIndex = (byte)i;
}
}
}
}
/// <summary>
/// Gets or sets the percentual probability of a weapon crate to drop between turns. Negative values might crash
/// the game.
/// </summary>
public sbyte WeaponCrateProb { 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 HealthCrateProb { 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 UtilityCrateProb { get; set; }
/// <summary>
/// Gets or sets the type of objects which can be placed on the map.
/// </summary>
public MapObjectType ObjectTypes { get; set; }
/// <summary>
/// Gets or sets the maximum number of objects (mines or oil drums) on the map. Note that changing this value
/// rounds it down to a valid setting.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">The value is not between 1-250.</exception>
/// <remarks>Valid settings can be enumerated through <see cref="ValidObjectCounts"/>.</remarks>
public byte ObjectCount
{
get => _objectCount;
set
{
if (value < 1 || value > 250)
throw new ArgumentOutOfRangeException(nameof(value), "Object count must be between 1-250.");
if (value > 100)
_objectCount = (byte)(value - value % 10);
else if (value > 30)
_objectCount = (byte)(value - value % 5);
else if (value == 0)
_objectCount = 1;
else
_objectCount = value;
}
}
/// <summary>
/// Gets or sets the number of seconds a mine requires to explode. Can be 0-3 and 5-127 seconds.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">Value is not between 0-127 or 4.</exception>
public byte MineDelay
{
get => _mineDelay;
set
{
if (value == 4 || value > 127)
throw new ArgumentOutOfRangeException(nameof(value), "Mine delay must be between 0-127 and not be 4.");
_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 <see langword="true"/>, 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>
/// <exception cref="ArgumentOutOfRangeException">Value is not between 0-127.</exception>
public byte TurnTime
{
get => _turnTime;
set
{
if (value > 127)
throw new ArgumentOutOfRangeException(nameof(value), "Turn time must be between 0-127.");
_turnTime = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether the turn time is unlimited. If <see langword="true"/>, 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>
/// <exception cref="ArgumentOutOfRangeException">Value is not between 0-127.</exception>
public byte RoundTimeMinutes
{
get => _roundTimeMinutes;
set
{
if (value > 127)
throw new ArgumentOutOfRangeException(nameof(value), "Round time must be between 0-127 minutes.");
_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>
/// <exception cref="ArgumentOutOfRangeException">Value is not between 0-128.</exception>
public byte RoundTimeSeconds
{
get => _roundTimeSeconds;
set
{
if (value > 128)
throw new ArgumentOutOfRangeException(nameof(value), "Round time must be between 0-128 seconds.");
_roundTimeSeconds = value;
}
}
/// <summary>
/// Gets or sets the number of round wins required to win the game. If 0, only 1 round is played, no matter the
/// outcome.
/// </summary>
public byte NumberOfWins { get; set; }
// ---- Cheats ----
/// <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 IndiLand { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Grenade weapon is more powerful.
/// </summary>
public bool UpgradeGrenade { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Shotgun weapon shoots twice for each of the two shots.
/// </summary>
public bool UpgradeShotgun { get; set; }
/// <summary>
/// Gets or sets a value indicating whether cluster weapon explode into more clusters.
/// </summary>
public bool UpgradeCluster { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the Longbow weapon is more powerful.
/// </summary>
public bool UpgradeLongbow { get; set; }
// ---- Weapons ----
/// <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 TeamWeapons { get; set; }
/// <summary>
/// Gets or sets a value indicating whether super weapons can be collected from crates.
/// </summary>
public bool SuperWeapons { get; set; }
/// <summary>
/// Gets the list of <see cref="SchemeWeapon"/> instances, of which each weapon can be accessed by index or
/// <see cref="Weapon"/> name. Depending on the scheme <see cref="Version"/>, super weapon settings may not be
/// stored in the resulting scheme file.
/// </summary>
public WeaponList Weapons { get; set; } = new WeaponList();
// ---- Extended Options ----
/// <summary>
/// Gets or sets trailing bytes which could not be parsed and will be attached when saving the scheme anew.
/// These may be unknown <see cref="SchemeVersion.Version3"/> extended scheme options or WWP trash.
/// </summary>
public byte[] Attachment { get; set; } = Array.Empty<byte>();
/// <summary>
/// Gets or sets the game version to be forced. Settings this property updates all extended options to reflect
/// the effects of the forced version.
/// </summary>
/// <remarks>Configurable with the /version command in RubberWorm.
/// S. http://worms2d.info/List_of_Worms_Armageddon_logic_versions .</remarks>
public ushort RwVersion
{
get => _rwVersion;
set
{
_rwVersion = value;
// Configure V3 settings according to selected version.
void setBattyRope1To2() => Extended.BattyRope = true;
void setBattyRope3And5To9()
{
setBattyRope1To2();
Extended.RopeKnockForce = 100;
}
void set150Speed() => Extended.GameSpeed = 1.5f;
void setTestStuff1()
{
Extended.ExplosionFallDamage = true;
Extended.GirderRadiusAssist = true;
Extended.IndianRopeGlitch = true;
Extended.KeepControlHeadBump = true;
Extended.KeepControlSkim = SkimControlLoss.Kept;
Extended.RopeKnockForce = 100;
Extended.RopeRollDrops = RopeRollDrops.Rope;
Extended.SkipWalk = SkipWalk.Facilitated;
}
void setTestStuff2()
{
setTestStuff1();
Extended.BattyRope = true;
Extended.RopeRollDrops = RopeRollDrops.RopeJump;
Extended.KeepControlXImpact = XImpactControlLoss.Kept;
}
void setTestStuff3()
{
setTestStuff2();
Extended.ProjectileMaxSpeed = 0;
Extended.RopeMaxSpeed = 0;
}
void setTestStuff4()
{
setTestStuff3();
Extended.AntiLockPower = true;
Extended.CircularAim = true;
Extended.GirderRadiusAssist = false;
Extended.JetpackMaxSpeed = 5 * 2;
Extended.ProjectileMaxSpeed = 32 * 2;
Extended.RopeMaxSpeed = 16 * 2;
}
void setTestStuff5()
{
setTestStuff4();
Extended.ProjectileMaxSpeed = 0;
}
void setTestStuff6To9()
{
setTestStuff5();
Extended.GirderRadiusAssist = true;
}
void setRacingStuffPre()
{
Extended.WormPhasingAlly = WormPhasing.WormsWeapons;
Extended.WormPhasingEnemy = WormPhasing.WormsWeapons;
Extended.WormSelectKeepHotSeat = true;
}
void setRacingStuff()
{
Extended.WormPhasingAlly = WormPhasing.Worms;
Extended.WormPhasingEnemy = WormPhasing.Worms;
Extended.WormSelectKeepHotSeat = true;
}
void setBoomRacing()
{
Extended.WormPhasingEnemy = WormPhasing.WormsWeaponsDamage;
Extended.WormSelectKeepHotSeat = true;
}
switch (_rwVersion)
{
case 11: // 3.5 Beta 3pre15[BattyRope1]
case 12: // 3.5 Beta 3pre15[BattyRope2]
setBattyRope1To2();
break;
case 15: // 3.5 Beta 3pre17[BattyRope3]
case 36: // 3.6.23.0[BattyRope5]
case 63: // 3.6.27.9[BattyRope5]
case 73: // 3.6.28.0[BattyRope5]
case 86: // 3.6.29.3[BattyRope6]
case 96: // 3.6.29.20[BattyRope6]
case 102: // 3.6.29.21[BattyRope6]
case 108: // 3.6.29.22[BattyRope7]
case 115: // 3.6.29.23[BattyRope7]
case 122: // 3.6.29.24[BattyRope7]
case 138: // 3.6.29.47[BattyRope8]
case 145: // 3.6.29.49[BattyRope8]
case 154: // 3.6.30.0[BattyRope8]
case 162: // 3.6.31.0[BattyRope8]
case 183: // 3.6.31.3[BattyRope8]
case 190: // 3.6.31.5[BattyRope8]
case 197: // 3.6.31.7[BattyRope8]
case 204: // 3.6.31.12[BattyRope8]
case 211: // 3.6.31.14[BattyRope8]
case 218: // 3.6.31.22[BattyRope8]
case 225: // 3.6.31.23[BattyRope8]
case 232: // 3.6.31.31[BattyRope9]
case 239: // 3.7.0.0 RC1-3[BattyRope9]
case 246: // 3.7.0.0[BattyRope9]
case 253: // 3.7.0.2[BattyRope9]
case 260: // 3.7.0.3[BattyRope9]
case 267: // 3.7.0.4[BattyRope9]
case 274: // 3.7.0.5[BattyRope9]
case 281: // 3.7.1.0[BattyRope9]
case 288: // 3.7.1.3[BattyRope9]
case 295: // 3.7.2.1[BattyRope9]
case 302: // 3.7.2.4[BattyRope9]
case 309: // 3.7.2.8[BattyRope9]
case 316: // 3.7.2.15[BattyRope9]
case 323: // 3.7.2.20[BattyRope9]
case 330: // 3.7.2.24[BattyRope9]
case 337: // 3.7.2.25[BattyRope9]
case 344: // 3.7.2.26[BattyRope9]
case 351: // 3.7.2.27[BattyRope9]
case 358: // 3.7.2.29[BattyRope9]
case 365: // 3.7.2.31[BattyRope9]
case 372: // 3.7.2.32[BattyRope9]
case 379: // 3.7.2.33[BattyRope9]
case 386: // 3.7.2.34[BattyRope9]
case 393: // 3.7.2.36[BattyRope9]
case 400: // 3.7.2.40[BattyRope9]
case 407: // 3.7.2.42[BattyRope9]
case 414: // 3.7.2.44[BattyRope9]
case 421: // 3.7.2.45[BattyRope9]
case 428: // 3.7.2.73[BattyRope9]
case 435: // 3.7.2.74[BattyRope9]
case 442: // 3.7.2.78[BattyRope9]
case 449: // 3.7.2.82[BattyRope9]
case 456: // 3.7.2.90[BattyRope9]
case 463: // 3.7.2.91[BattyRope9]
case 470: // 3.7.2.92[BattyRope9]
case 477: // 3.7.2.95[BattyRope9]
setBattyRope3And5To9();
break;
case 25: // 3.6.19.16[150% speed]
case 27: // 3.6.19.17[150% speed]
case 64: // 3.6.27.9[150% speed]
case 74: // 3.6.28.0[150% speed]
case 87: // 3.6.29.3[150% speed]
case 97: // 3.6.29.20[150% speed]
case 103: // 3.6.29.21[150% speed]
case 109: // 3.6.29.22[150% speed]
case 116: // 3.6.29.23[150% speed]
case 123: // 3.6.29.24[150% speed]
case 139: // 3.6.29.47[150% speed]
case 146: // 3.6.29.49[150% speed]
case 155: // 3.6.30.0[150% speed]
case 163: // 3.6.31.0[150% speed]
case 184: // 3.6.31.3[150% speed]
case 191: // 3.6.31.5[150% speed]
case 198: // 3.6.31.7[150% speed]
case 205: // 3.6.31.12[150% speed]
case 212: // 3.6.31.14[150% speed]
case 219: // 3.6.31.22[150% speed]
case 226: // 3.6.31.23[150% speed]
case 233: // 3.6.31.31[150% speed]
case 240: // 3.7.0.0 RC1-3[150% speed]
case 247: // 3.7.0.0[150% speed]
case 254: // 3.7.0.2[150% speed]
case 261: // 3.7.0.3[150% speed]
case 268: // 3.7.0.4[150% speed]
case 275: // 3.7.0.5[150% speed]
case 282: // 3.7.1.0[150% speed]
case 289: // 3.7.1.3[150% speed]
case 296: // 3.7.2.1[150% speed]
case 303: // 3.7.2.4[150% speed]
case 310: // 3.7.2.8[150% speed]
case 317: // 3.7.2.15[150% speed]
case 324: // 3.7.2.20[150% speed]
case 331: // 3.7.2.24[150% speed]
case 338: // 3.7.2.25[150% speed]
case 345: // 3.7.2.26[150% speed]
case 352: // 3.7.2.27[150% speed]
case 359: // 3.7.2.29[150% speed]
case 366: // 3.7.2.31[150% speed]
case 373: // 3.7.2.32[150% speed]
case 380: // 3.7.2.33[150% speed]
case 387: // 3.7.2.34[150% speed]
case 394: // 3.7.2.36[150% speed]
case 401: // 3.7.2.40[150% speed]
case 408: // 3.7.2.42[150% speed]
case 415: // 3.7.2.44[150% speed]
case 422: // 3.7.2.45[150% speed]
case 429: // 3.7.2.73[150% speed]
case 436: // 3.7.2.74[150% speed]
case 443: // 3.7.2.78[150% speed]
case 450: // 3.7.2.82[150% speed]
case 457: // 3.7.2.90[150% speed]
case 464: // 3.7.2.91[150% speed]
case 471: // 3.7.2.92[150% speed]
case 478: // 3.7.2.95[150% speed]
set150Speed();
break;
case 39: // 3.6.24.1[TestStuff1]
setTestStuff1();
break;
case 40: // 3.6.24.1[TestStuff2]
setTestStuff2();
break;
case 46: // 3.6.26.4c[TestStuff3]
case 65: // 3.6.27.9[TestStuff3]
setTestStuff3();
break;
case 69: // 3.6.27.11[TestStuff4]
setTestStuff4();
break;
case 75: // 3.6.28.0[TestStuff5]
setTestStuff5();
break;
case 88: // 3.6.29.3[TestStuff6]
case 98: // 3.6.29.20[TestStuff6]
case 104: // 3.6.29.21[TestStuff6]
case 110: // 3.6.29.22[TestStuff7]
case 117: // 3.6.29.23[TestStuff7]
case 124: // 3.6.29.24[TestStuff7]
case 140: // 3.6.29.47[TestStuff8]
case 147: // 3.6.29.49[TestStuff8]
case 156: // 3.6.30.0[TestStuff8]
case 164: // 3.6.31.0[TestStuff8]
case 185: // 3.6.31.3[TestStuff8]
case 192: // 3.6.31.5[TestStuff8]
case 199: // 3.6.31.7[TestStuff8]
case 206: // 3.6.31.12[TestStuff8]
case 213: // 3.6.31.14[TestStuff8]
case 220: // 3.6.31.22[TestStuff8]
case 227: // 3.6.31.23[TestStuff8]
case 234: // 3.6.31.31[TestStuff9]
case 241: // 3.7.0.0 RC1-3[TestStuff9]
case 248: // 3.7.0.0[TestStuff9]
case 255: // 3.7.0.2[TestStuff9]
case 262: // 3.7.0.3[TestStuff9]
case 269: // 3.7.0.4[TestStuff9]
case 276: // 3.7.0.5[TestStuff9]
case 283: // 3.7.1.0[TestStuff9]
case 290: // 3.7.1.3[TestStuff9]
case 297: // 3.7.2.1[TestStuff9]
case 304: // 3.7.2.4[TestStuff9]
case 311: // 3.7.2.8[TestStuff9]
case 318: // 3.7.2.15[TestStuff9]
case 325: // 3.7.2.20[TestStuff9]
case 332: // 3.7.2.24[TestStuff9]
case 339: // 3.7.2.25[TestStuff9]
case 346: // 3.7.2.26[TestStuff9]
case 353: // 3.7.2.27[TestStuff9]
case 360: // 3.7.2.29[TestStuff9]
case 367: // 3.7.2.31[TestStuff9]
case 374: // 3.7.2.32[TestStuff9]
case 381: // 3.7.2.33[TestStuff9]
case 388: // 3.7.2.34[TestStuff9]
case 395: // 3.7.2.36[TestStuff9]
case 402: // 3.7.2.40[TestStuff9]
case 409: // 3.7.2.42[TestStuff9]
case 416: // 3.7.2.44[TestStuff9]
case 423: // 3.7.2.45[TestStuff9]
case 430: // 3.7.2.73[TestStuff9]
case 437: // 3.7.2.74[TestStuff9]
case 444: // 3.7.2.78[TestStuff9]
case 451: // 3.7.2.82[TestStuff9]
case 458: // 3.7.2.90[TestStuff9]
case 465: // 3.7.2.91[TestStuff9]
case 472: // 3.7.2.92[TestStuff9]
case 479: // 3.7.2.95[TestStuff9]
setTestStuff6To9();
break;
case 99: // 3.6.29.20[RacingStuff-pre]
setRacingStuffPre();
break;
case 105: // 3.6.29.21[RacingStuff]
case 111: // 3.6.29.22[RacingStuff]
case 118: // 3.6.29.23[RacingStuff]
case 125: // 3.6.29.24[RacingStuff]
case 141: // 3.6.29.47[RacingStuff]
case 148: // 3.6.29.49[RacingStuff]
case 157: // 3.6.30.0[RacingStuff]
case 165: // 3.6.31.0[RacingStuff]
case 186: // 3.6.31.3[RacingStuff]
case 193: // 3.6.31.5[RacingStuff]
case 200: // 3.6.31.7[RacingStuff]
case 207: // 3.6.31.12[RacingStuff]
case 214: // 3.6.31.14[RacingStuff]
case 221: // 3.6.31.22[RacingStuff]
case 228: // 3.6.31.23[RacingStuff]
case 235: // 3.6.31.31[RacingStuff]
case 242: // 3.7.0.0 RC1-3[RacingStuff]
case 249: // 3.7.0.0[RacingStuff]
case 256: // 3.7.0.2[RacingStuff]
case 263: // 3.7.0.3[RacingStuff]
case 270: // 3.7.0.4[RacingStuff]
case 277: // 3.7.0.5[RacingStuff]
case 284: // 3.7.1.0[RacingStuff]
case 291: // 3.7.1.3[RacingStuff]
case 298: // 3.7.2.1[RacingStuff]
case 305: // 3.7.2.4[RacingStuff]
case 312: // 3.7.2.8[RacingStuff]
case 319: // 3.7.2.15[RacingStuff]
case 326: // 3.7.2.20[RacingStuff]
case 333: // 3.7.2.24[RacingStuff]
case 340: // 3.7.2.25[RacingStuff]
case 347: // 3.7.2.26[RacingStuff]
case 354: // 3.7.2.27[RacingStuff]
case 361: // 3.7.2.29[RacingStuff]
case 368: // 3.7.2.31[RacingStuff]
case 375: // 3.7.2.32[RacingStuff]
case 382: // 3.7.2.33[RacingStuff]
case 389: // 3.7.2.34[RacingStuff]
case 396: // 3.7.2.36[RacingStuff]
case 403: // 3.7.2.40[RacingStuff]
case 410: // 3.7.2.42[RacingStuff]
case 417: // 3.7.2.44[RacingStuff]
case 424: // 3.7.2.45[RacingStuff]
case 431: // 3.7.2.73[RacingStuff]
case 438: // 3.7.2.74[RacingStuff]
case 445: // 3.7.2.78[RacingStuff]
case 452: // 3.7.2.82[RacingStuff]
case 459: // 3.7.2.90[RacingStuff]
case 466: // 3.7.2.91[RacingStuff]
case 473: // 3.7.2.92[RacingStuff]
case 480: // 3.7.2.95[RacingStuff]
setRacingStuff();
break;
case 112: // 3.6.29.22[BoomRacing]
case 119: // 3.6.29.23[BoomRacing]
case 126: // 3.6.29.24[BoomRacing]
case 142: // 3.6.29.47[BoomRacing]
case 149: // 3.6.29.49[BoomRacing]
case 158: // 3.6.30.0[BoomRacing]
case 166: // 3.6.31.0[BoomRacing]
case 187: // 3.6.31.3[BoomRacing]
case 194: // 3.6.31.5[BoomRacing]
case 201: // 3.6.31.7[BoomRacing]
case 208: // 3.6.31.12[BoomRacing]
case 215: // 3.6.31.14[BoomRacing]
case 222: // 3.6.31.22[BoomRacing]
case 229: // 3.6.31.23[BoomRacing]
case 236: // 3.6.31.31[BoomRacing]
case 243: // 3.7.0.0 RC1-3[BoomRacing]
case 250: // 3.7.0.0[BoomRacing]
case 257: // 3.7.0.2[BoomRacing]
case 264: // 3.7.0.3[BoomRacing]
case 271: // 3.7.0.4[BoomRacing]
case 278: // 3.7.0.5[BoomRacing]
case 285: // 3.7.1.0[BoomRacing]
case 292: // 3.7.1.3[BoomRacing]
case 299: // 3.7.2.1[BoomRacing]
case 306: // 3.7.2.4[BoomRacing]
case 313: // 3.7.2.8[BoomRacing]
case 320: // 3.7.2.15[BoomRacing]
case 327: // 3.7.2.20[BoomRacing]
case 334: // 3.7.2.24[BoomRacing]
case 341: // 3.7.2.25[BoomRacing]
case 348: // 3.7.2.26[BoomRacing]
case 355: // 3.7.2.27[BoomRacing]
case 362: // 3.7.2.29[BoomRacing]
case 369: // 3.7.2.31[BoomRacing]
case 376: // 3.7.2.32[BoomRacing]
case 383: // 3.7.2.33[BoomRacing]
case 390: // 3.7.2.34[BoomRacing]
case 397: // 3.7.2.36[BoomRacing]
case 404: // 3.7.2.40[BoomRacing]
case 411: // 3.7.2.42[BoomRacing]
case 418: // 3.7.2.44[BoomRacing]
case 425: // 3.7.2.45[BoomRacing]
case 432: // 3.7.2.73[BoomRacing]
case 439: // 3.7.2.74[BoomRacing]
case 446: // 3.7.2.78[BoomRacing]
case 453: // 3.7.2.82[BoomRacing]
case 460: // 3.7.2.90[BoomRacing]
case 467: // 3.7.2.91[BoomRacing]
case 474: // 3.7.2.92[BoomRacing]
case 481: // 3.7.2.95[BoomRacing]
setBoomRacing();
break;
}
}
}
// ---- METHODS (PUBLIC) ---------------------------------------------------------------------------------------
/// <inheritdoc/>
public override bool Equals(object? obj) => Equals(obj as Scheme);
/// <inheritdoc/>
public bool Equals(Scheme? other)
=> other != null
&& Version == other.Version
&& HotSeatTime == other.HotSeatTime
&& RetreatTime == other.RetreatTime
&& RetreatTimeRope == other.RetreatTimeRope
&& ShowRoundTime == other.ShowRoundTime
&& Replays == other.Replays
&& FallDamage == other.FallDamage
&& ArtilleryMode == other.ArtilleryMode
&& SchemeEditor == other.SchemeEditor
&& Stockpiling == other.Stockpiling
&& WormSelect == other.WormSelect
&& SuddenDeathEvent == other.SuddenDeathEvent
&& WaterRiseRate == other.WaterRiseRate
&& WeaponCrateProb == other.WeaponCrateProb
&& DonorCards == other.DonorCards
&& HealthCrateProb == other.HealthCrateProb
&& HealthCrateEnergy == other.HealthCrateEnergy
&& UtilityCrateProb == other.UtilityCrateProb
&& ObjectTypes == other.ObjectTypes
&& ObjectCount == other.ObjectCount
&& MineDelay == other.MineDelay
&& MineDelayRandom == other.MineDelayRandom
&& DudMines == other.DudMines
&& ManualWormPlacement == other.ManualWormPlacement
&& WormEnergy == other.WormEnergy
&& TurnTime == other.TurnTime
&& TurnTimeInfinite == other.TurnTimeInfinite
&& RoundTimeMinutes == other.RoundTimeMinutes
&& RoundTimeSeconds == other.RoundTimeSeconds
&& NumberOfWins == other.NumberOfWins
&& Blood == other.Blood
&& AquaSheep == other.AquaSheep
&& SheepHeaven == other.SheepHeaven
&& GodWorms == other.GodWorms
&& IndiLand == other.IndiLand
&& UpgradeGrenade == other.UpgradeGrenade
&& UpgradeShotgun == other.UpgradeShotgun
&& UpgradeCluster == other.UpgradeCluster
&& UpgradeLongbow == other.UpgradeLongbow
&& TeamWeapons == other.TeamWeapons
&& SuperWeapons == other.SuperWeapons
&& Weapons.SequenceEqual(other.Weapons)
&& Extended.Equals(other.Extended)
&& Attachment.SequenceEqual(other.Attachment)
&& RwVersion == other.RwVersion;
/// <inheritdoc/>
public override int GetHashCode()
{
HashCode hash = new HashCode();
hash.Add(Version);
hash.Add(HotSeatTime);
hash.Add(RetreatTime);
hash.Add(RetreatTimeRope);
hash.Add(ShowRoundTime);
hash.Add(Replays);
hash.Add(FallDamage);
hash.Add(ArtilleryMode);
hash.Add(SchemeEditor);
hash.Add(Stockpiling);
hash.Add(WormSelect);
hash.Add(SuddenDeathEvent);
hash.Add(WaterRiseRate);
hash.Add(WeaponCrateProb);
hash.Add(DonorCards);
hash.Add(HealthCrateProb);
hash.Add(HealthCrateEnergy);
hash.Add(UtilityCrateProb);
hash.Add(ObjectTypes);
hash.Add(ObjectCount);
hash.Add(MineDelay);
hash.Add(MineDelayRandom);
hash.Add(DudMines);
hash.Add(ManualWormPlacement);
hash.Add(WormEnergy);
hash.Add(TurnTime);
hash.Add(TurnTimeInfinite);
hash.Add(RoundTimeMinutes);
hash.Add(RoundTimeSeconds);
hash.Add(NumberOfWins);
hash.Add(Blood);
hash.Add(AquaSheep);
hash.Add(SheepHeaven);
hash.Add(GodWorms);
hash.Add(IndiLand);
hash.Add(UpgradeGrenade);
hash.Add(UpgradeShotgun);
hash.Add(UpgradeCluster);
hash.Add(UpgradeLongbow);
hash.Add(TeamWeapons);
hash.Add(SuperWeapons);
foreach (SchemeWeapon weapon in Weapons)
hash.Add(weapon);
hash.Add(Extended);
foreach (byte attachmentByte in Attachment)
hash.Add(attachmentByte);
hash.Add(RwVersion);
return hash.ToHashCode();
}
/// <inheritdoc/>
public void Load(Stream stream)
{
using BinaryStream reader = new BinaryStream(stream, encoding: Encoding.ASCII, leaveOpen: true);
void readObjectCombo()
{
byte raw = reader.Read1Byte();
if (Version == SchemeVersion.Version1)
{
// Special handling for broken "v1" schemes setting illegal values here, bugging out old frontends.
ObjectCount = 8;
ObjectTypes = raw switch
{
0 => MapObjectType.None,
2 => MapObjectType.OilDrums,
5 => MapObjectType.Both,
_ => MapObjectType.Mines
};
}
else
{
// Actual handling as implemented in 3.6.29.0 frontend.
switch (raw)
{
case 3: case 4: raw = 1; break;
case 6: case 7: raw = 0; break;
}
// Determine object count.
int step = Math.Min(raw < 8 ? 8 : raw / 4 - 2, _objectCountLastStep);
if (step >= _objectCount10Step)
step = 100 + 10 * (step - _objectCount10Step);
else if (step >= _objectCount5Step)
step = 30 + 5 * (step - _objectCount5Step);
ObjectCount = (byte)step;
// Determine object types.
ObjectTypes = raw == 5 // old "both" value
? MapObjectType.Both
: (MapObjectType)(raw & (byte)MapObjectType.Both);
}
}
void readMineDelay()
{
byte raw = reader.Read1Byte();
MineDelayRandom = raw == 4 || raw > 127;
MineDelay = MineDelayRandom ? (byte)0 : raw;
}
void readTurnTime()
{
byte raw = reader.Read1Byte();
TurnTimeInfinite = raw > 127;
TurnTime = TurnTimeInfinite ? (byte)0 : raw;
}
void readRoundTime()
{
byte raw = reader.Read1Byte();
if (raw > 127)
{
RoundTimeMinutes = 0;
RoundTimeSeconds = (byte)(256 - raw);
}
else
{
RoundTimeMinutes = raw;
RoundTimeSeconds = 0;
}
}
// Read the header.
if (reader.ReadUInt32() != _signature)
throw new InvalidDataException("Invalid scheme file signature.");
Version = reader.ReadEnum<SchemeVersion>(true);
// Read the options.
HotSeatTime = reader.Read1Byte();
RetreatTime = reader.Read1Byte();
RetreatTimeRope = reader.Read1Byte();
ShowRoundTime = reader.ReadBoolean();
Replays = reader.ReadBoolean();
FallDamage = reader.ReadByte() * 50 % 256 * 2;
ArtilleryMode = reader.ReadBoolean();
SchemeEditor = reader.ReadEnum<SchemeEditor>(false);
Stockpiling = reader.ReadEnum<Stockpiling>(true);
WormSelect = reader.ReadEnum<WormSelect>(true);
SuddenDeathEvent = reader.ReadEnum<SuddenDeathEvent>(true);
_waterRiseIndex = reader.Read1Byte();
WeaponCrateProb = reader.ReadSByte();
DonorCards = reader.ReadBoolean();
HealthCrateProb = reader.ReadSByte();
HealthCrateEnergy = reader.Read1Byte();
UtilityCrateProb = reader.ReadSByte();
readObjectCombo();
readMineDelay();
DudMines = reader.ReadBoolean();
ManualWormPlacement = reader.ReadBoolean();
WormEnergy = reader.Read1Byte();
readTurnTime();
readRoundTime();
NumberOfWins = reader.Read1Byte();
Blood = reader.ReadBoolean();
AquaSheep = reader.ReadBoolean();
SheepHeaven = reader.ReadBoolean();
GodWorms = reader.ReadBoolean();
IndiLand = reader.ReadBoolean();
UpgradeGrenade = reader.ReadBoolean();
UpgradeShotgun = reader.ReadBoolean();
UpgradeCluster = reader.ReadBoolean();
UpgradeLongbow = reader.ReadBoolean();
TeamWeapons = reader.ReadBoolean();
SuperWeapons = reader.ReadBoolean();
// Read the weapons.
Weapons = new WeaponList();
reader.ReadStructs(Weapons.AsSpan(Version.GetWeaponCount()));
// Read available extended settings or deserialize them from RubberWorm settings encoded in probabilities.
switch (Version)
{
case SchemeVersion.Version2: LoadRubberWorm(); break;
case SchemeVersion.Version3: LoadExtendedOptions(reader); break;
}
// Read any trailing bytes (either unknown extended scheme options or unknown WWP trash).
Attachment = reader.EndOfStream
? Array.Empty<byte>()
: reader.ReadBytes((int)(reader.Length - reader.Position));
}
/// <inheritdoc/>
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"/> with a format appropriate for <see cref="Version"/>.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> to save the data to.</param>
public void Save(Stream stream) => Save(stream, (SchemeSaveFormat)((uint)Version << 16));
/// <summary>
/// Saves the data in the given file with a format appropriate for <see cref="Version"/>.
/// </summary>
/// <param name="fileName">The name of the file to save the data in.</param>
public void Save(string fileName) => Save(fileName, (SchemeSaveFormat)((uint)Version << 16));
/// <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);
}
/// <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, leaveOpen: true);
void saveObjectTypesAndCount()
{
byte raw;
if ((format == SchemeSaveFormat.Version2ObjectCount || format >= SchemeSaveFormat.Version3)
&& ObjectCount != 8)
{
// WA since 3.6.28.0 encodes object type and count in one byte.
raw = (byte)ObjectTypes;
int step = ObjectCount;
if (ObjectCount >= 100)
step = _objectCount10Step + (ObjectCount - 100) / 10;
else if (ObjectCount >= 30)
step = _objectCount5Step + (ObjectCount - 30) / 5;
raw |= (byte)(4 * step + 8);
}
else
{
// WA before 3.6.28.0 and WWP only store object type.
raw = ObjectTypes == MapObjectType.Both ? (byte)5 : (byte)ObjectTypes;
}
writer.Write(raw);
}
void saveMineDelayConfig() => writer.Write(MineDelayRandom ? (byte)4 : MineDelay);
void saveTurnTimeConfig() => writer.Write(TurnTimeInfinite ? (byte)255 : TurnTime);
void saveRoundTimeConfig() => writer.Write(RoundTimeSeconds > 0
? (byte)(256 - RoundTimeSeconds)
: RoundTimeMinutes);
// Write the header.
SchemeVersion version = (SchemeVersion)((uint)format >> 16);
writer.Write(_signature);
writer.WriteEnum(version);
// Write the options.
writer.Write(HotSeatTime);
writer.Write(RetreatTime);
writer.Write(RetreatTimeRope);
writer.Write(ShowRoundTime);
writer.Write(Replays);
writer.Write((byte)(FallDamage / 4 * 41 % 128));
writer.Write(ArtilleryMode);
writer.WriteEnum(SchemeEditor, false);
writer.WriteEnum(Stockpiling, true);
writer.WriteEnum(WormSelect, true);
writer.WriteEnum(SuddenDeathEvent, true);
writer.Write(_waterRiseIndex);
writer.Write(WeaponCrateProb);
writer.Write(DonorCards);
writer.Write(HealthCrateProb);
writer.Write(HealthCrateEnergy);
writer.Write(UtilityCrateProb);
saveObjectTypesAndCount();
saveMineDelayConfig();
writer.Write(DudMines);
writer.Write(ManualWormPlacement);
writer.Write(WormEnergy);
saveTurnTimeConfig();
saveRoundTimeConfig();
writer.Write(NumberOfWins);
writer.Write(Blood);
writer.Write(AquaSheep);
writer.Write(SheepHeaven);
writer.Write(GodWorms);
writer.Write(IndiLand);
writer.Write(UpgradeGrenade);
writer.Write(UpgradeShotgun);
writer.Write(UpgradeCluster);
writer.Write(UpgradeLongbow);
writer.Write(TeamWeapons);
writer.Write(SuperWeapons);
// Serialize RubberWorm settings encoded in weapon probabilities.
if (version == SchemeVersion.Version2)
SaveRubberWorm();
// Write the weapons.
writer.WriteStructs<SchemeWeapon>(Weapons.AsSpan(version.GetWeaponCount()));
// Clear RubberWorm probabilities again or write available extended settings.
switch (version)
{
case SchemeVersion.Version2: ClearRubberWorm(); break;
case SchemeVersion.Version3: SaveExtendedOptions(writer, Attachment.Length == 0); break;
}
// Write any trailing bytes (either unknown extended scheme options or unknown WWP trash).
writer.Write(Attachment);
}
// ---- METHODS (PRIVATE) --------------------------------------------------------------------------------------
private void ClearRubberWorm()
{
// Reset all super weapon probabilities to remove any settings made by RubberWorm.
for (Weapon superWeapon = Weapon.Freeze; superWeapon <= Weapon.Armageddon; superWeapon++)
Weapons[superWeapon].Prob = 0;
}
private void LoadExtendedOptions(BinaryStream reader)
{
// Create a copy of default options overwritten by available extended data.
Span<byte> bytes = Extended.AsSpan();
ExtendedOptions.Default.AsSpan().CopyTo(bytes);
reader.Read(bytes);
}
private void LoadRubberWorm()
{
unchecked
{
// Earthquake flags.
byte prob = (byte)Weapons[Weapon.Earthquake].Prob;
Extended.AutoReaim = prob.GetBit(0);
Extended.CircularAim = prob.GetBit(1);
Extended.AntiLockPower = prob.GetBit(2);
Extended.ShotDoesntEndTurnAll = prob.GetBit(3);
Extended.KaosMod = prob.DecodeByte(4, 4);
// Antisink.
Extended.AntiSink = Weapons[Weapon.SheepStrike].Prob != 0;
// Crate limit and rate.
prob = (byte)Weapons[Weapon.MagicBullet].Prob;
Extended.CrateLimit = prob switch
{
0 => ExtendedOptions.Default.CrateLimit,
_ => prob
};
Extended.CrateRate = (byte)Weapons[Weapon.NuclearTest].Prob;
// Mole squadron flags.
prob = (byte)Weapons[Weapon.MoleSquadron].Prob;
Extended.ShotDoesntEndTurn = prob.GetBit(0);
Extended.LoseControlDoesntEndTurn = prob.GetBit(1);
Extended.FiringPausesTimer = !prob.GetBit(2);
Extended.RopeUpgrade = prob.GetBit(3);
Extended.CrateShower = prob.GetBit(4);
Extended.WeaponsDontChange = prob.GetBit(6);
Extended.ObjectPushByExplosion = prob.GetBit(5) ? (bool?)true : null;
Extended.ExtendedFuse = prob.GetBit(7);
// Flame limit.
prob = (byte)Weapons[Weapon.ScalesOfJustice].Prob;
if (prob > 0)
Extended.FlameLimit = (ushort)(prob * 100);
// Friction.
prob = (byte)Weapons[Weapon.SalvationArmy].Prob;
if (prob > 0)
Extended.Friction = prob / 100f;
// Gravity - 8th and 7th bit control constant / proportional black hole, otherwise normal gravity.
prob = (byte)Weapons[Weapon.MailStrike].Prob;
if (prob == 0)
{
Extended.RwGravityType = RwGravityType.None;
Extended.RwGravity = ExtendedOptions.Default.RwGravity;
}
else if (prob.GetBit(7) && prob.GetBit(6))
{
Extended.RwGravityType = RwGravityType.BlackHoleLinear;
Extended.RwGravity = prob.DecodeSByte(6) * 512;
}
else if (prob.GetBit(7))
{
Extended.RwGravityType = RwGravityType.BlackHoleConstant;
Extended.RwGravity = prob.DecodeSByte(6) * 512;
}
else
{
Extended.RwGravityType = RwGravityType.Default;
Extended.RwGravity = prob.DecodeSByte(7) * 512;
}
// Rope-knocking force.
prob = (byte)Weapons[Weapon.SuperBananaBomb].Prob;
Extended.RopeKnockForce = prob switch
{
0 => null,
Byte.MaxValue => 0,
_ => prob
};
// Maximum rope speed.
prob = (byte)Weapons[Weapon.MineStrike].Prob;
Extended.RopeMaxSpeed = prob switch
{
0 => ExtendedOptions.Default.RopeMaxSpeed,
Byte.MaxValue => 0,
_ => prob
};
// Select worm any time.
Extended.WormSelectAnytime = ((byte)Weapons[Weapon.MBBomb].Prob).GetBit(0);
// Viscosity.
prob = (byte)Weapons[Weapon.ConcreteDonkey].Prob;
Extended.Viscosity = prob / 255f;
Extended.ViscosityWorms = (prob & 1) == 1;
// Wind.
prob = (byte)Weapons[Weapon.SuicideBomber].Prob;
Extended.RwWind = (byte)Weapons[Weapon.SuicideBomber].Prob / 255f;
Extended.RwWindWorms = (prob & 1) == 1;
// Worm bounce.
Extended.WormBounce = (byte)Weapons[Weapon.Armageddon].Prob / 255f;
// Version override.
RwVersion = BinaryPrimitives.ReadUInt16BigEndian(stackalloc[]
{
(byte)Weapons[Weapon.SelectWorm].Prob,
(byte)Weapons[Weapon.Freeze].Prob
});
}
// Clear abused probabilities.
ClearRubberWorm();
}
private void SaveExtendedOptions(BinaryStream writer, bool trim)
{
ReadOnlySpan<byte> bytes = Extended.AsSpan();
// Trim away default bytes.
int length = bytes.Length;
if (trim)
{
ReadOnlySpan<byte> defaultBytes = ExtendedOptions.Default.AsSpan();
for (; length > 0 && bytes[length - 1] == defaultBytes[length - 1]; length--) ;
}
writer.Write(bytes.Slice(0, length));
}
private void SaveRubberWorm()
{
unchecked
{
// Earthquake flags.
byte prob = 0;
prob = prob.SetBit(0, Extended.AutoReaim);
prob = prob.SetBit(1, Extended.CircularAim);
prob = prob.SetBit(2, Extended.AntiLockPower);
prob = prob.SetBit(3, Extended.ShotDoesntEndTurnAll);
prob = prob.Encode(Extended.KaosMod, 4, 4);
Weapons[Weapon.Earthquake].Prob = (sbyte)prob;
// Antisink
Weapons[Weapon.SheepStrike].Prob = (sbyte)(Extended.AntiSink ? 1 : 0);
// Crate limit and rate.
if (Extended.CrateLimit != ExtendedOptions.Default.CrateLimit)
Weapons[Weapon.MagicBullet].Prob = (sbyte)Math.Min(Byte.MaxValue, Extended.CrateLimit);
Weapons[Weapon.NuclearTest].Prob = (sbyte)Extended.CrateRate;
// Mole squadron flags.
prob = 0;
prob = prob.SetBit(0, Extended.ShotDoesntEndTurn);
prob = prob.SetBit(1, Extended.LoseControlDoesntEndTurn);
prob = prob.SetBit(2, !Extended.FiringPausesTimer);
prob = prob.SetBit(3, Extended.RopeUpgrade);
prob = prob.SetBit(4, Extended.CrateShower);
prob = prob.SetBit(5, Extended.ObjectPushByExplosion == true);
prob = prob.SetBit(6, Extended.WeaponsDontChange);
prob = prob.SetBit(7, Extended.ExtendedFuse);
Weapons[Weapon.MoleSquadron].Prob = (sbyte)prob;
// Flame limit.
if (Extended.FlameLimit != ExtendedOptions.Default.FlameLimit)
Weapons[Weapon.ScalesOfJustice].Prob = (sbyte)(Extended.FlameLimit / 100);
// Friction.
if (Extended.Friction != ExtendedOptions.Default.Friction)
Weapons[Weapon.SalvationArmy].Prob = (sbyte)Math.Round(Extended.Friction * 100, 0);
// Gravity - 8th and 7th bit control constant / proportional black hole, otherwise normal gravity.
prob = 0;
switch (Extended.RwGravityType)
{
case RwGravityType.BlackHoleConstant:
prob = prob.EnableBit(7);
prob = prob.Encode((sbyte)(Extended.RwGravity / 512), 6);
break;
case RwGravityType.BlackHoleLinear:
prob = prob.EnableBit(7);
prob = prob.EnableBit(6);
prob = prob.Encode((sbyte)(Extended.RwGravity / 512), 6);
break;
case RwGravityType.Default:
prob = prob.Encode((sbyte)(Extended.RwGravity / 512), 6);
break;
}
Weapons[Weapon.MailStrike].Prob = (sbyte)prob;
// Rope-knocking force.
Weapons[Weapon.SuperBananaBomb].Prob = (sbyte)(Extended.RopeKnockForce switch
{
null => 0,
0 => Byte.MaxValue,
_ => Extended.RopeKnockForce.Value
});
// Maximum rope speed.
Weapons[Weapon.MineStrike].Prob = (sbyte)(Extended.RopeMaxSpeed switch
{
16/*SchemeExtendedOptions.Default.RopeMaxSpeed*/ => 0,
0 => Byte.MaxValue,
_ => Math.Min(254, Extended.RopeMaxSpeed)
});
// Select worm any time.
prob = ((byte)Weapons[Weapon.MBBomb].Prob).SetBit(0, Extended.WormSelectAnytime);
Weapons[Weapon.MBBomb].Prob = (sbyte)prob;
// Viscosity.
prob = (byte)Math.Round(Extended.Viscosity * 255, 0);
prob.SetBit(0, Extended.ViscosityWorms);
Weapons[Weapon.ConcreteDonkey].Prob = (sbyte)prob;
// Wind.
prob = (byte)Math.Round(Extended.RwWind * 255, 0);
prob.SetBit(0, Extended.RwWindWorms);
Weapons[Weapon.SuicideBomber].Prob = (sbyte)prob;
// Worm bounce.
Weapons[Weapon.Armageddon].Prob = (sbyte)Math.Round(Extended.WormBounce * 255, 0);
// Version override.
Span<byte> versionBytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16BigEndian(versionBytes, RwVersion);
Weapons[Weapon.SelectWorm].Prob = (sbyte)versionBytes[0];
Weapons[Weapon.Freeze].Prob = (sbyte)versionBytes[1];
}
}
}
/// <summary>
/// Represents the possible variations of the options stored in a scheme file.
/// </summary>
public enum SchemeSaveFormat
{
// Values are organized as follows:
// Scheme.Version << 16 = latest SchemeSaveFormat of it
// SchemeSaveFormat >> 16 = corresponding Scheme.Version
/// <summary>Represents <see cref="SchemeVersion.Version1"/>.</summary>
Version1 = 0x00010000,
/// <summary>Represents <see cref="SchemeVersion.Version2"/>.</summary>
Version2 = 0x00020001,
/// <summary>Represents <see cref="SchemeVersion.Version2"/>, storing object count since WA 3.6.28.0.</summary>
Version2ObjectCount = 0x00020000,
/// <summary>Represents <see cref="SchemeVersion.Version3"/>.</summary>
Version3 = 0x00030000
}
}