Skip to content

Commit

Permalink
Merge pull request #655 from TheTrackerCouncil/skip-tourian-boss-keycard
Browse files Browse the repository at this point in the history
Add option to not require the crateria boss keycard for Tourian
  • Loading branch information
MattEqualsCoder authored Jan 23, 2025
2 parents dfa41f9 + 2b649b2 commit f1e8f03
Show file tree
Hide file tree
Showing 15 changed files with 96 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public class TrackerMapSMDoor
/// <param name="item">Item identifier for the key to unlock the door</param>
/// <param name="x">X position on the map</param>
/// <param name="y">Y position on the map</param>
public TrackerMapSMDoor(string item, int x, int y)
/// <param name="skippableOnFastTourian">If the Crateria boss keycard is needed to get to Tourian</param>
public TrackerMapSMDoor(string item, int x, int y, bool skippableOnFastTourian)
{
Item = item;
X = x;
Y = y;
SkippableOnFastTourian = skippableOnFastTourian;
}

/// <summary>
Expand All @@ -32,4 +34,9 @@ public TrackerMapSMDoor(string item, int x, int y)
/// Y position on the map
/// </summary>
public int Y { get; set; }

/// <summary>
/// If the Crateria boss keycard is needed to get to Tourian
/// </summary>
public bool SkippableOnFastTourian { get; set; }
}
1 change: 1 addition & 0 deletions src/TrackerCouncil.Smz3.Data/Options/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ public class Config
public int GanonCrystalCount { get; set; } = 7;
public bool OpenPyramid { get; set; }
public int TourianBossCount { get; set; } = 4;
public bool SkipTourianBossDoor { get; set; }
public IDictionary<string, int> ItemOptions { get; set; } = new Dictionary<string, int>();
public string? RandomizerVersion { get; set; }
[System.Text.Json.Serialization.JsonIgnore, JsonIgnore]
Expand Down
5 changes: 5 additions & 0 deletions src/TrackerCouncil.Smz3.Data/Options/PlandoConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ public PlandoConfig(World world)
/// </summary>
public int TourianBossCount { get; set; } = 4;

/// <summary>
/// If in Metroid keysanity Tourian requires the Crateria Boss Keycard item
/// </summary>
public bool SkipTourianBossDoor { get; set; }

/// <summary>
/// Gets or sets the logic options that apply to the plando.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions src/TrackerCouncil.Smz3.Data/Options/RandomizerOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public Config ToConfig()
{
GameMode = GameMode.Normal,
KeysanityMode = SeedOptions.KeysanityMode,
SkipTourianBossDoor = SeedOptions.SkipTourianBossDoor,
Race = SeedOptions.Race,
ItemPlacementRule = SeedOptions.ItemPlacementRule,
DisableSpoilerLog = SeedOptions.DisableSpoilerLog,
Expand Down
1 change: 1 addition & 0 deletions src/TrackerCouncil.Smz3.Data/Options/SeedOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public class SeedOptions

public int? UniqueHintCount { get; set; }

public bool SkipTourianBossDoor { get; set; }
public int GanonsTowerCrystalCount { get; set; } = 7;
public int GanonCrystalCount { get; set; } = 7;
public bool OpenPyramid { get; set; } = false;
Expand Down
1 change: 1 addition & 0 deletions src/TrackerCouncil.Smz3.Data/ParsedRom/ParsedRomDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class ParsedRomDetails
public required int GanonsTowerCrystalCount { get; set; }
public required int GanonCrystalCount { get; set; }
public required int TourianBossCount { get; set; }
public required bool SkipTourianBossDoor { get; set; }
public required List<ParsedRomPlayer> Players { get; set; }
public required List<ParsedRomLocationDetails> Locations { get; set; }
public required List<ParsedRomBossDetails> Bosses { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public GenerationWindowViewModel GetViewModel()
_model.Basic.MsuRandomizationStyle = _options.PatchOptions.MsuRandomizationStyle;

_model.GameSettings.KeysanityMode = _options.SeedOptions.KeysanityMode;
_model.GameSettings.RequireBossKeycardForTourian = !_options.SeedOptions.SkipTourianBossDoor;
_model.GameSettings.CrystalsNeededForGT = _options.SeedOptions.GanonsTowerCrystalCount;
_model.GameSettings.CrystalsNeededForGanon = _options.SeedOptions.GanonCrystalCount;
_model.GameSettings.BossesNeededForTourian = _options.SeedOptions.TourianBossCount;
Expand Down Expand Up @@ -112,6 +113,7 @@ public void ApplyConfig(Config config, bool copySeed)
}

_model.GameSettings.KeysanityMode = config.KeysanityMode;
_model.GameSettings.RequireBossKeycardForTourian = !config.SkipTourianBossDoor;
_model.GameSettings.CrystalsNeededForGT = config.GanonsTowerCrystalCount;
_model.GameSettings.CrystalsNeededForGanon = config.GanonCrystalCount;
_model.GameSettings.BossesNeededForTourian = config.TourianBossCount;
Expand Down Expand Up @@ -176,7 +178,9 @@ public void SaveSettings()
{
_options.SeedOptions.Seed = _model.Basic.Seed;
_options.SeedOptions.KeysanityMode = _model.GameSettings.KeysanityMode;
_options.SeedOptions.SkipTourianBossDoor = !_model.GameSettings.RequireBossKeycardForTourian;
_options.SeedOptions.GanonsTowerCrystalCount = _model.GameSettings.CrystalsNeededForGT;
_options.SeedOptions.SkipTourianBossDoor = !_model.GameSettings.RequireBossKeycardForTourian;
_options.SeedOptions.GanonCrystalCount = _model.GameSettings.CrystalsNeededForGanon;
_options.SeedOptions.TourianBossCount = _model.GameSettings.BossesNeededForTourian;
_options.SeedOptions.OpenPyramid = _model.GameSettings.OpenPyramid;
Expand Down Expand Up @@ -239,6 +243,7 @@ public void LoadPlando(PlandoConfig config)
_model.PlandoConfig = config;

_model.GameSettings.KeysanityMode = _model.PlandoConfig.KeysanityMode;
_model.GameSettings.RequireBossKeycardForTourian = !_model.PlandoConfig.SkipTourianBossDoor;
_model.GameSettings.CrystalsNeededForGT = _model.PlandoConfig.GanonsTowerCrystalCount;
_model.GameSettings.CrystalsNeededForGanon = _model.PlandoConfig.GanonCrystalCount;
_model.GameSettings.OpenPyramid = _model.PlandoConfig.OpenPyramid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,21 @@ namespace TrackerCouncil.Smz3.Data.ViewModels;
public class GenerationWindowGameSettingsViewModel : ViewModelBase
{
private bool _isMultiplayer;
private KeysanityMode _keysanityMode;

[DynamicFormFieldComboBox(label: "Keysanity:", groupName: "Game Settings Top")]
public KeysanityMode KeysanityMode { get; set; }
public KeysanityMode KeysanityMode
{
get => _keysanityMode;
set
{
SetField(ref _keysanityMode, value);
OnPropertyChanged(nameof(IsMetroidKeysanity));
}
}

[DynamicFormFieldCheckBox("Require Crateria Boss Keycard for Tourian", groupName: "Game Settings Top", visibleWhenTrue: nameof(IsMetroidKeysanity))]
public bool RequireBossKeycardForTourian { get; set; }

[DynamicFormFieldSlider(0, 7, 0, 1, label: "Crystals needed for GT:", groupName: "Game Settings Top")]
public int CrystalsNeededForGT { get; set; }
Expand Down Expand Up @@ -42,6 +54,8 @@ public class GenerationWindowGameSettingsViewModel : ViewModelBase
[DynamicFormFieldCheckBox("Disable cheats", groupName: "Race Settings")]
public bool DisableCheats { get; set; }

public bool IsMetroidKeysanity => KeysanityMode is KeysanityMode.Both or KeysanityMode.SuperMetroid;

public bool IsMultiplayer
{
get => _isMultiplayer;
Expand Down
3 changes: 2 additions & 1 deletion src/TrackerCouncil.Smz3.Data/maps.json
Original file line number Diff line number Diff line change
Expand Up @@ -2269,7 +2269,8 @@
{
"Item": "Crateria Boss Keycard",
"X": 279,
"Y": 339
"Y": 339,
"SkippableOnFastTourian": true
}
]
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public override IEnumerable<GeneratedPatch> GetChanges(GetPatchesRequest data)
var plmTablePos = 0xf800;
foreach (var door in s_doorList)
{
if (door[0] == 0x99BD && data.World.Config.SkipTourianBossDoor)
{
continue;
}

var doorArgs = door[4] != KeycardPlaque.None ? doorId | door[3] : door[3];
if (door[6] == 0)
{
Expand Down Expand Up @@ -108,37 +113,69 @@ public override IEnumerable<GeneratedPatch> GetChanges(GetPatchesRequest data)
yield return new GeneratedPatch(Snes(0x8f0000 + plmTablePos), new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 });
}

public static bool GetIsMetroidKeysanity(byte[] rom)
public static bool GetIsMetroidKeysanity(byte[] rom, out bool skippedTourianBossDoor)
{
ushort plaquePLm = 0xd410;
ushort doorId = 0x0000;
var plmTablePos = 0xf800;

skippedTourianBossDoor = false;

foreach (var door in s_doorList)
{
var doorArgs = door[4] != KeycardPlaque.None ? doorId | door[3] : door[3];
if (door[6] == 0)
{
// Write dynamic door
var doorData = door[0..3].SelectMany(x => UshortBytes(x)).Concat(UshortBytes(doorArgs)).ToArray();
var romData = rom.Skip(Snes(0x8f0000 + plmTablePos)).Take(doorData.Length);
if (!romData.SequenceEqual(doorData)) return false;
var romData = rom.Skip(Snes(0x8f0000 + plmTablePos)).Take(doorData.Length).ToArray();
if (!romData.SequenceEqual(doorData))
{
if (door[0] == 0x99BD)
{
skippedTourianBossDoor = true;
}
else
{
return false;
}
}
plmTablePos += 0x08;
}
else
{
// Overwrite existing door
var doorData = door[1..3].SelectMany(x => UshortBytes(x)).Concat(UshortBytes(doorArgs)).ToArray();
var romData = rom.Skip(Snes(0x8f0000 + door[6])).Take(doorData.Length);
if (!romData.SequenceEqual(doorData)) return false;
var romData = rom.Skip(Snes(0x8f0000 + door[6])).Take(doorData.Length).ToArray();
if (!romData.SequenceEqual(doorData))
{
if (door[0] == 0x99BD)
{
skippedTourianBossDoor = true;
}
else
{
return false;
}
}
if ((door[3] == KeycardEvents.BrinstarBoss && door[0] != 0x9D9C)
|| door[3] == KeycardEvents.LowerNorfairBoss
|| door[3] == KeycardEvents.MaridiaBoss
|| door[3] == KeycardEvents.WreckedShipBoss)
{
doorData = new byte[] { 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00, 0x2F, 0xB6, 0x00, 0x00, 0x00, 0x00 };
romData = rom.Skip(Snes(0x8f0000 + door[6] + 0x06)).Take(doorData.Length);
if (!romData.SequenceEqual(doorData)) return false;
romData = rom.Skip(Snes(0x8f0000 + door[6] + 0x06)).Take(doorData.Length).ToArray();
if (!romData.SequenceEqual(doorData))
{
if (door[0] == 0x99BD)
{
skippedTourianBossDoor = true;
}
else
{
return false;
}
};
}
}

Expand All @@ -147,7 +184,7 @@ public static bool GetIsMetroidKeysanity(byte[] rom)
{
var plaqueData = UshortBytes(door[0]).Concat(UshortBytes(plaquePLm)).Concat(UshortBytes(door[5])).Concat(UshortBytes(door[4])).ToArray();
var romData = rom.Skip(Snes(0x8f0000 + plmTablePos)).Take(plaqueData.Length);
if (!romData.SequenceEqual(plaqueData)) return false;
if (!romData.SequenceEqual(plaqueData)) continue;
plmTablePos += 0x08;
}
doorId += 1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public SeedData GeneratePlandoSeed(RandomizerOptions options, PlandoConfig pland
config.GanonCrystalCount = plandoConfig.GanonCrystalCount;
config.OpenPyramid = plandoConfig.OpenPyramid;
config.TourianBossCount = plandoConfig.TourianBossCount;
config.SkipTourianBossDoor = plandoConfig.SkipTourianBossDoor;
config.LogicConfig = plandoConfig.Logic.Clone();
return plandomizer.GenerateSeed(config, CancellationToken.None);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private string GetSpoilerLog(RandomizerOptions options, SeedData seed, bool conf

if (world.Config.Keysanity)
{
log.AppendLine("Keysanity: " + world.Config.KeysanityMode.ToString());
log.AppendLine("Keysanity: " + world.Config.KeysanityMode);
}

var gtCrystals = world.Config.GanonsTowerCrystalCount;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ public ParsedRomDetails ParseRomFile(string filePath)
}).ToList();

var keysanityMode = KeysanityMode.None;
var skippedTourianBossDoor = false;
if (isKeysanityEnabled)
{
var isMetroidKeysanity = MetroidKeysanityPatch.GetIsMetroidKeysanity(rom);
var isMetroidKeysanity = MetroidKeysanityPatch.GetIsMetroidKeysanity(rom, out skippedTourianBossDoor);
var isZeldaKeysanity = ZeldaKeysanityPatch.GetIsZeldaKeysanity(rom);

if (isMetroidKeysanity && isZeldaKeysanity)
Expand Down Expand Up @@ -134,6 +135,7 @@ public ParsedRomDetails ParseRomFile(string filePath)
GanonsTowerCrystalCount = gtCrystalCount,
GanonCrystalCount = ganonCrystalCount,
TourianBossCount = tourianBossCount,
SkipTourianBossDoor = skippedTourianBossDoor,
RomGenerator = romGenerator.Value,
Players = players,
Locations = locations,
Expand Down Expand Up @@ -164,6 +166,7 @@ public SeedData GenerateSeedData(RandomizerOptions options, ParsedRomDetails par
config.GanonsTowerCrystalCount = parsedRomDetails.GanonsTowerCrystalCount;
config.GanonCrystalCount = parsedRomDetails.GanonCrystalCount;
config.TourianBossCount = parsedRomDetails.TourianBossCount;
config.SkipTourianBossDoor = parsedRomDetails.SkipTourianBossDoor;
config.LocationItems.Clear();
config.ItemOptions = parsedRomDetails.StartingItems.ToDictionary(x => $"ItemType:{x.Key}", x => x.Value);
config.LogicConfig = new LogicConfig()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,11 @@ private List<TrackerMapLocationViewModel> GetDoorLocationModels(TrackerMapRegion

foreach (var door in doors)
{
if (door.SkippableOnFastTourian && worldRegion.Config.SkipTourianBossDoor)
{
continue;
}

var model = new TrackerMapLocationViewModel(mapRegion, mapLocation, worldRegion, door);
var item = worldQueryService.FirstOrDefault(door.Item) ?? throw new InvalidOperationException();
item.UpdatedItemState += (_, _) => UpdateDoorLocationModel(model, item);
Expand Down
2 changes: 1 addition & 1 deletion src/TrackerCouncil.Smz3.UI/TrackerCouncil.Smz3.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>9.9.2</Version>
<Version>9.9.3</Version>
<AssemblyName>SMZ3CasRandomizer</AssemblyName>
<ApplicationIcon>Assets\smz3.ico</ApplicationIcon>
<RootNamespace>$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
Expand Down

0 comments on commit f1e8f03

Please sign in to comment.