Skip to content

Commit

Permalink
RE CV: get enemy randomizer working for room 1010
Browse files Browse the repository at this point in the history
  • Loading branch information
IntelOrca committed Mar 23, 2024
1 parent 2e3dc05 commit 17db107
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 88 deletions.
248 changes: 177 additions & 71 deletions IntelOrca.Biohazard.BioRand/EnemyRandomiser.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
using System.Threading;
using IntelOrca.Biohazard.BioRand.RE1;
using IntelOrca.Biohazard.BioRand.RE2;
using IntelOrca.Biohazard.BioRand.RE3;
using IntelOrca.Biohazard.BioRand.RECV;
using IntelOrca.Biohazard.Extensions;
using IntelOrca.Biohazard.Room;
using IntelOrca.Biohazard.Script;
Expand All @@ -31,6 +33,7 @@ internal class EnemyRandomiser
private HashSet<byte> _killIdPool = new HashSet<byte>();
private Queue<byte> _killIds = new Queue<byte>();
private Dictionary<byte, EmbeddedEffect> _effects = new Dictionary<byte, EmbeddedEffect>();
private List<CvEnemyAssets> _cvEnemies = new List<CvEnemyAssets>();

public IEnemyHelper EnemyHelper => _enemyHelper;
public Dictionary<RandomizedRdt, SelectableEnemy> ChosenEnemies { get; } = new Dictionary<RandomizedRdt, SelectableEnemy>();
Expand Down Expand Up @@ -81,20 +84,43 @@ private void ReadEnemyPlacements()

private void GatherEsps()
{
foreach (var rdt in _gameData.Rdts)
if (_version == BioVersion.BiohazardCv)
{
// HarvestEnemyAssets(RdtId.Parse("1031"), ReCvEnemyIds.Zombie, 512, 2, 0, 2);
HarvestEnemyAssets(RdtId.Parse("10D0"), ReCvEnemyIds.Bat, 3, 1, 0, 1);
HarvestEnemyAssets(RdtId.Parse("10F0"), ReCvEnemyIds.Zombie, 256, 1, 0, 1);
HarvestEnemyAssets(RdtId.Parse("2000"), ReCvEnemyIds.ZombieDog, 0, 1, 0, 1);
HarvestEnemyAssets(RdtId.Parse("2060"), ReCvEnemyIds.Bandersnatch, 0, 1, 0, 1);
HarvestEnemyAssets(RdtId.Parse("20C2"), ReCvEnemyIds.Tyrant, 0, 2, 0, 2);
HarvestEnemyAssets(RdtId.Parse("80D0"), ReCvEnemyIds.Hunter, 0, 1, 0, 1);
HarvestEnemyAssets(RdtId.Parse("80D0"), ReCvEnemyIds.Hunter, 256, 3, 0, 2);
}
else
{
var embeddedEffects = GetEmbeddedEffects(rdt.RdtFile);
for (var i = 0; i < embeddedEffects.Count; i++)
foreach (var rdt in _gameData.Rdts)
{
var ee = embeddedEffects[i];
if (ee.Id != 0xFF && !_effects.ContainsKey(ee.Id))
var embeddedEffects = GetEmbeddedEffects(rdt.RdtFile);
for (var i = 0; i < embeddedEffects.Count; i++)
{
_effects[ee.Id] = ee;
var ee = embeddedEffects[i];
if (ee.Id != 0xFF && !_effects.ContainsKey(ee.Id))
{
_effects[ee.Id] = ee;
}
}
}
}
}

private void HarvestEnemyAssets(RdtId rdtId, short enemyType, short variant, int modelIndex, int motionIndex, int textureIndex)
{
var rdt = (RdtCv)_gameData.GetRdt(rdtId)!.RdtFile;
var model = rdt.Models.Pages[modelIndex];
var motion = rdt.Motions;
var texture = rdt.Textures.Groups[textureIndex];
_cvEnemies.Add(new CvEnemyAssets(enemyType, variant, model, motion, texture));
}

public byte GetNextKillId()
{
if (_killIds.Count == 0)
Expand Down Expand Up @@ -400,7 +426,7 @@ private void RandomizeRoomWithEnemy(RandomizedRdt rdt, SelectableEnemy targetEne
g_stickyEnemies.Add(rdt.RdtId, targetEnemy);
}
}
else
else if (rdt.Version != BioVersion.BiohazardCv)
{
// Mute all NPCs in the room so that we can hear enemies
foreach (var em in rdt.Enemies)
Expand Down Expand Up @@ -579,89 +605,134 @@ private bool RemoveAllEnemiesFromRoom(RandomizedRdt rdt)

private void RandomiseRoom(Rng rng, RandomizedRdt rdt, MapRoomEnemies enemySpec, SelectableEnemy targetEnemy)
{
var enemiesToChange = GetEnemiesToReplace(rdt, enemySpec);
var possibleTypes = targetEnemy.Types.Shuffle(_rng);
if (enemySpec.IncludeTypes != null)
{
var includeTypes = enemySpec.IncludeTypes.Select(x => (byte)x).ToHashSet();
possibleTypes = possibleTypes.Intersect(includeTypes).ToArray();
}
else if (enemySpec.ExcludeTypes != null)
{
var excludeTypes = enemySpec.ExcludeTypes.Select(x => (byte)x).ToHashSet();
possibleTypes = possibleTypes.Except(excludeTypes).ToArray();
}
var possibleTypes = GetPossibleEnemyTypes(enemySpec, targetEnemy);
if (possibleTypes.Length == 0)
return;

var randomEnemiesToChange = new SceEmSetOpcode[0];
if (_config.RandomEnemyPlacement && !enemySpec.KeepPositions)
if (_version == BioVersion.BiohazardCv)
{
randomEnemiesToChange = GenerateRandomEnemies(rng, rdt, enemySpec, enemiesToChange, possibleTypes[0]);
}
if (randomEnemiesToChange.Length != 0)
{
enemiesToChange = randomEnemiesToChange;
var enemyType = _rng.NextOf(possibleTypes);
var assets = _cvEnemies.FirstOrDefault(x => x.EnemyType == enemyType);
var placements = GetRandomPlacements(rdt.RdtId, rng, enemySpec, enemyType);

rdt.PostModifications.Add(() =>
{
var rdtb = ((RdtCv)rdt.RdtFile).ToBuilder();

var models = rdtb.Models.ToBuilder();
for (var i = 0; i < rdtb.Enemies.Count; i++)
{
models.Pages.RemoveAt(1 + i);
}
for (var i = 0; i < placements.Length; i++)
{
models.Pages.Insert(1, assets.Model);
}
rdtb.Models = models.ToCvModelList();

rdtb.Motions = assets.Motion;

var textures = rdtb.Textures.ToBuilder();
textures.Groups.RemoveAt(1);
textures.Groups.Insert(1, assets.Texture);
rdtb.Textures = textures.ToTextureList();

rdtb.Enemies.Clear();
foreach (var ep in placements)
{
rdtb.Enemies.Add(new RdtCv.Enemy()
{
Header = 1,
Type = enemyType,
Effect = 0,
Variant = assets.Variant,
Index = (short)rdtb.Enemies.Count,
Position = new RdtCv.VectorF(ep.X, ep.Y, ep.Z),
Rotation = new RdtCv.Vector32(0, ep.D, 0),
});
}

rdt.RdtFile = rdtb.ToRdt();
});
}
else
{
var quantity = enemiesToChange.DistinctBy(x => x.Id).Count();
possibleTypes = possibleTypes.Where(type =>
var enemiesToChange = GetEnemiesToReplace(rdt, enemySpec);
var randomEnemiesToChange = new SceEmSetOpcode[0];
if (_config.RandomEnemyPlacement && !enemySpec.KeepPositions)
{
var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty);
var maxQuantity = _enemyHelper.GetEnemyTypeLimit(_config, enemySpec.MaxDifficulty ?? _config.EnemyDifficulty, type);
return maxQuantity >= quantity;
}).ToArray();
}

if (possibleTypes.Length == 0)
return;
randomEnemiesToChange = GenerateRandomEnemies(rng, rdt, enemySpec, enemiesToChange, possibleTypes[0]);
}
if (randomEnemiesToChange.Length != 0)
{
enemiesToChange = randomEnemiesToChange;
}
else
{
var quantity = enemiesToChange.DistinctBy(x => x.Id).Count();
possibleTypes = possibleTypes.Where(type =>
{
var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty);
var maxQuantity = _enemyHelper.GetEnemyTypeLimit(_config, enemySpec.MaxDifficulty ?? _config.EnemyDifficulty, type);
return maxQuantity >= quantity;
}).ToArray();
}

var randomEnemyType = possibleTypes[0];
var ids = rdt.Enemies.Select(x => x.Id).Distinct().ToArray();
var enemyTypesId = ids.Select(x => randomEnemyType).ToArray();
if (possibleTypes.Length == 0)
return;

_enemyHelper.BeginRoom(rdt);
var randomEnemyType = possibleTypes[0];
var ids = rdt.Enemies.Select(x => x.Id).Distinct().ToArray();
var enemyTypesId = ids.Select(x => randomEnemyType).ToArray();

for (var i = 0; i < enemiesToChange.Length; i++)
{
var enemy = enemiesToChange[i];
var index = Array.IndexOf(ids, enemy.Id);
var enemyType = enemyTypesId[index];
enemy.Type = enemyType;
if (!enemySpec.KeepState)
enemy.State = 0;
if (!enemySpec.KeepAi)
enemy.Ai = 0;
enemy.Texture = 0;
if (enemySpec.Y != null)
enemy.Y = enemySpec.Y.Value;
_enemyHelper.SetEnemy(_config, rng, enemy, enemySpec, enemyType);
_enemyHelper.BeginRoom(rdt);

foreach (var dependencyType in _enemyHelper.GetEnemyDependencies(enemyType))
for (var i = 0; i < enemiesToChange.Length; i++)
{
i++;
enemy = enemiesToChange[i];
enemy.Type = dependencyType;
var enemy = enemiesToChange[i];
var index = Array.IndexOf(ids, enemy.Id);
var enemyType = enemyTypesId[index];
enemy.Type = enemyType;
if (!enemySpec.KeepState)
enemy.State = 0;
if (!enemySpec.KeepAi)
enemy.Ai = 0;
enemy.Texture = 0;
if (enemySpec.Y != null)
enemy.Y = enemySpec.Y.Value;
_enemyHelper.SetEnemy(_config, rng, enemy, enemySpec, enemyType);

foreach (var dependencyType in _enemyHelper.GetEnemyDependencies(enemyType))
{
i++;
enemy = enemiesToChange[i];
enemy.Type = dependencyType;
_enemyHelper.SetEnemy(_config, rng, enemy, enemySpec, enemyType);
}
}
}
}

private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRoomEnemies enemySpec, SceEmSetOpcode[] currentEnemies, byte enemyType)
private byte[] GetPossibleEnemyTypes(MapRoomEnemies enemySpec, SelectableEnemy targetEnemy)
{
var relevantPlacements = _enemyPositions
.Where(x => x.RdtId == rdt.RdtId)
.ToEndlessBag(_rng);
var possibleTypes = targetEnemy.Types.Shuffle(_rng);
if (enemySpec.IncludeTypes != null)
{
var includeTypes = enemySpec.IncludeTypes.Select(x => (byte)x).ToHashSet();
possibleTypes = possibleTypes.Intersect(includeTypes).ToArray();
}
else if (enemySpec.ExcludeTypes != null)
{
var excludeTypes = enemySpec.ExcludeTypes.Select(x => (byte)x).ToHashSet();
possibleTypes = possibleTypes.Except(excludeTypes).ToArray();
}

if (relevantPlacements.Count == 0)
return new SceEmSetOpcode[0];
return possibleTypes;
}

var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty);
var enemyTypeLimit = _enemyHelper.GetEnemyTypeLimit(_config, difficulty, enemyType);
var avg = 1 + _config.EnemyQuantity;
var quantity = rng.Next(1, avg * 2);
quantity = Math.Min(quantity, Math.Min(enemyTypeLimit, relevantPlacements.Count * 3));
private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRoomEnemies enemySpec, SceEmSetOpcode[] currentEnemies, byte enemyType)
{
var placements = GetRandomPlacements(rdt.RdtId, rng, enemySpec, enemyType);

foreach (var enemy in currentEnemies)
rdt.Nop(enemy.Offset);
Expand All @@ -673,9 +744,8 @@ private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRo

var enemyOpcodes = new List<OpcodeBase>();
var firstEnemyOpcodeIndex = rdt.AdditionalOpcodes.Count;
for (var i = 0; i < quantity; i++)
foreach (var ep in placements)
{
var ep = relevantPlacements.Next();
while (usedIds.Contains(enemyId))
{
enemyId++;
Expand All @@ -701,6 +771,23 @@ private SceEmSetOpcode[] GenerateRandomEnemies(Rng rng, RandomizedRdt rdt, MapRo
return enemies.ToArray();
}

private EnemyPosition[] GetRandomPlacements(RdtId rdtId, Rng rng, MapRoomEnemies enemySpec, byte enemyType)
{
var relevantPlacements = _enemyPositions
.Where(x => x.RdtId == rdtId)
.ToEndlessBag(_rng);

if (relevantPlacements.Count == 0)
return new EnemyPosition[0];

var difficulty = Math.Min(enemySpec.MaxDifficulty ?? 3, _config.EnemyDifficulty);
var enemyTypeLimit = _enemyHelper.GetEnemyTypeLimit(_config, difficulty, enemyType);
var avg = 1 + _config.EnemyQuantity;
var quantity = rng.Next(1, avg * 2);
quantity = Math.Min(quantity, Math.Min(enemyTypeLimit, relevantPlacements.Count * 3));
return relevantPlacements.Next(quantity);
}

private SceEmSetOpcode CreateEnemy(byte id, byte killId, EnemyPosition ep)
{
if (_version == BioVersion.Biohazard1)
Expand Down Expand Up @@ -866,5 +953,24 @@ public override int GetHashCode()
return (Room?.GetHashCode() ?? 0) ^ X ^ Y ^ Z ^ D ^ F;
}
}

[DebuggerDisplay("EnemyType = {EnemyType} Variant = {Variant}")]
private readonly struct CvEnemyAssets
{
public short EnemyType { get; }
public short Variant { get; }
public CvModelListPage Model { get; }
public CvMotionList Motion { get; }
public CvTextureEntryGroup Texture { get; }

public CvEnemyAssets(short enemyType, short variant, CvModelListPage model, CvMotionList motion, CvTextureEntryGroup texture)
{
EnemyType = enemyType;
Variant = variant;
Model = model;
Motion = motion;
Texture = texture;
}
}
}
}
15 changes: 9 additions & 6 deletions IntelOrca.Biohazard.BioRand/RECV/ReCvEnemyHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ public string GetEnemyName(byte type)

public int GetEnemyTypeLimit(RandoConfig config, int difficulty, byte type)
{
var limit = new byte[] { 4, 8, 12, 16 };
if (type == RECV.ReCvEnemyIds.Tyrant)
return 1;
if (type == RECV.ReCvEnemyIds.Zombie)
return 1;

var limit = new byte[] { 2, 4, 7, 10 };
var index = Math.Min(limit.Length - 1, difficulty);
return limit[index];
}
Expand All @@ -33,7 +38,8 @@ public SelectableEnemy[] GetSelectableEnemies() => new[]
new SelectableEnemy("Zombie", "LightGray", ReCvEnemyIds.Zombie),
new SelectableEnemy("Hunter", "IndianRed", ReCvEnemyIds.Hunter),
new SelectableEnemy("Bandersnatch", "Cyan", ReCvEnemyIds.Bandersnatch),
new SelectableEnemy("Zombie Dog", "Black", ReCvEnemyIds.ZombieDog)
new SelectableEnemy("Zombie Dog", "Black", ReCvEnemyIds.ZombieDog),
new SelectableEnemy("Tyrant", "Gray", ReCvEnemyIds.Tyrant)
};

public bool IsEnemy(byte type)
Expand Down Expand Up @@ -76,9 +82,6 @@ public bool SupportsEnemyType(RandoConfig config, RandomizedRdt rdt, bool hasEne

public byte[] GetEnemyDependencies(byte enemyType) => new byte[0];

public byte[] GetRequiredEsps(byte enemyType)
{
throw new NotImplementedException();
}
public byte[] GetRequiredEsps(byte enemyType) => new byte[0];
}
}
2 changes: 0 additions & 2 deletions IntelOrca.Biohazard.BioRand/RECV/ReCvRandomiser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,6 @@ public override void Generate(RandoConfig config, IRandoProgress progress, FileR
config.IncludeDocuments = false;
config.SwapCharacters = false;
config.RandomNPCs = false;
config.RandomEnemies = false;
config.RandomEnemyPlacement = false;
config.RandomCutscenes = false;
config.RandomEvents = false;
config.RandomBgm = false;
Expand Down
6 changes: 6 additions & 0 deletions IntelOrca.Biohazard.BioRand/data/recv/enemy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{ "room": "1010", "x": 8, "y": 0, "z": -11, "d": 0 },
{ "room": "1010", "x": 8, "y": 0, "z": -11, "d": 0 },
{ "room": "1010", "x": 8, "y": 0, "z": 24, "d": 0 },
{ "room": "1010", "x": 8, "y": 0, "z": 50, "d": 0 }
]
Loading

0 comments on commit 17db107

Please sign in to comment.