From 5e8561a7502f3ffaf63ade1a90a320de67610aac Mon Sep 17 00:00:00 2001 From: lahm86 <33758420+lahm86@users.noreply.github.com> Date: Mon, 25 Apr 2022 16:44:36 +0100 Subject: [PATCH] #272 Excludable Enemies - Adds support to allow choosing which enemies are available in the randomization pool. This does work mainly on "best effort", but exclusion priority can be defined so that enemies that the randomizer has a chance to focus on excluding any that are particularly undesirable. An option is in place to show a warning message if an excluded enemy has ended up in the game. - Ensures skidoo drivers are limited if they are heavily present in any level. - Rotates an enemy in Floaters if the type is flamethrower for a better chance of killing him. --- Deps/TRGE.Core.dll | Bin 130560 -> 130560 bytes TRLevelReader/Helpers/TR2EntityUtilities.cs | 14 + TRLevelReader/Helpers/TR3EntityUtilities.cs | 14 + .../Editors/RandomizerSettings.cs | 18 ++ TRRandomizerCore/Editors/TR2RandoEditor.cs | 16 +- TRRandomizerCore/Editors/TR3RandoEditor.cs | 9 +- .../Helpers/TRRandomizationCategory.cs | 3 +- .../Helpers/TRRandomizationEventArgs.cs | 2 + TRRandomizerCore/Levels/TR2CombinedLevel.cs | 21 ++ .../Processors/AbstractLevelProcessor.cs | 8 + .../Randomizers/TR2/TR2EnemyRandomizer.cs | 255 +++++++++++++--- .../Randomizers/TR3/TR3EnemyRandomizer.cs | 193 ++++++++---- .../Environment/FLOATING.TR2-Environment.json | 22 ++ .../enemy_restrictions_special.json | 4 +- .../TR2/Restrictions/excludable_enemies.json | 44 +++ .../TR3/Restrictions/excludable_enemies.json | 41 +++ TRRandomizerCore/TRRandomizerController.cs | 28 ++ .../Utilities/LocationGenerator.cs | 2 +- .../Utilities/TR2EnemyUtilities.cs | 13 +- .../Model/BoolItemIDControlClass.cs | 19 ++ TRRandomizerView/Model/ControllerOptions.cs | 61 ++++ TRRandomizerView/TRRandomizerView.csproj | 8 + TRRandomizerView/Windows/AdvancedWindow.xaml | 65 ++++- .../Windows/AdvancedWindow.xaml.cs | 10 + TRRandomizerView/Windows/EnemyWindow.xaml | 173 +++++++++++ TRRandomizerView/Windows/EnemyWindow.xaml.cs | 274 ++++++++++++++++++ .../Windows/RandomizeProgressWindow.xaml.cs | 17 ++ 27 files changed, 1204 insertions(+), 130 deletions(-) create mode 100644 TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json create mode 100644 TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json create mode 100644 TRRandomizerView/Model/BoolItemIDControlClass.cs create mode 100644 TRRandomizerView/Windows/EnemyWindow.xaml create mode 100644 TRRandomizerView/Windows/EnemyWindow.xaml.cs diff --git a/Deps/TRGE.Core.dll b/Deps/TRGE.Core.dll index 781b77d99a6ef75a963ac6f27d7fd66ac1658c2d..c4f33fe8833746797687bdc3bb98da42db4361ed 100644 GIT binary patch delta 9511 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 delta 9463 zcmZu%3w%_?)t|X{_a@oAHm`jon`}Z@am zkkEqPhZP)j{aOlS0R;g;7X$?b`%tV<;VZR4sY)yEU_9t9XEy z2rgwA7Y*Z#2QOUaqr#IUV-XkPmf|T~gm)J|V@Mssn87_rl6}aCsp@d?3$@+q24GT> z6J+&QjG2~6jOC?BE8XHdwTQcgvovQES|lc@2F5%lC87E~_b|!p0hx9iV~TsE4rJ}e zi0ULY2g1N09&t~pb28eww5gzyr)#&sL$@|)x5jv*Hj8*1##Js2aZ8c#TOp9UK=PJ> zg7QX2{JwO&802;|RsoQ;9(D z6OD|Fs43eN{;)*=3lz+6?nlbO=-9K>^V zqQ?LDK=oBrhe`doSTM9O?NQLH2SLz@BZ$H+4iO&OWuAsvKGc z4o!^ctG`QhG~8_OL@{KxD)Q`#JiEnv4Y`H@An|2GS*rVa$&031DQ70(X)F@c8m%r= zhg?=&6>{11Fd>&+@?xBjD_8P%3(>eJZ8bou1To>h#{0SGZnBK}CCDX%ZxcQw?K)UQ zA2hZ>@?vO7vRJa!dJwuyYAQSwtsI7j;l$}C2k#UYiJF?r5-5Q8SE8X=RVdF2F}pcW z{H%HSaD;M#_d$55Rp4$hSTxvpCN&YZh*kldO9K50&?k7kIIMS-_^jEUx{@S5l5~me z(XXd&2A*btj5s%XxF~3`U55imlC3}0G4ZLAGOu4bE?z0_YjI@KRK0rEG`)b>(&FH& z#8IN>TJnMax+T8>*GQ6_$<|m=^5cR+LZ`yj%1UHMix61#KySo5mI;cIye`h%IfoQD70TwB>^S$+75lg6JniQ^pkq^tqv* z9$rm^oZJk%9nKN@CwZ|6{B^M1hC;s(Xh4t}0ninO>MUp8!xI?euG5c1_a&SlWDE@w zuip^UXS>Coak&9pd6(6P3+$4ucI{2rI6hoxm&J!`3=dpnSAowD4{Sn{tAj(Hpdeq_ zYSW#5AcR#95sOQaoGEdk#Mtra_3=5_WLqA^yH(FZwnV~HGO{gKA54!l%;y*D$J_3V zB%(V;=3;Rs#Z5-GMwEJ(m{N~piOWczoNSE@q=&ZUiSNdbkf8{o<%X8j5R`&i9U0Me z!~Bs9R!w2|!HEMWdxj(%R9G^2Ltx_$X#dG^ZPzI(TXCmOAiA*i2fO`=R!6kAllr^Y z9v3Fk+opc)w0N!fq80Z@`i)4-iMA140JKZ2xiP;1I~?{-hAp;tisK?gw3+={SG-nS zxbX*fin$GYzG-YuDjbWk)2UlbA^H78pCI}o(ep&F5-n^mnibhkj@+fr9JVBS($&Vr z`r-XE(OD4L4C`af4rlnT=$auT*0mRlRT`KD_GT)svn0DrzJub}L}$kHXba;UiaZaC&nMOzP%4UU zvSwS=Wso^uF;FCHa15G6>rMG$*-gd#vUuXA2Wz)Ly7Wrk49r!J>n@_A`=4~(NIUVKQ}vk*;Ep94DM%#1TWUAPaRe=F$Y5>@&W-5TC9<+23f zHcsnH;upk*+pR!%-R_+CHsqlG3~m4#+{+aAjgmhmk+tP08YlVRzs4vgi5LyION8`` zg{-av{~?n9&!X@SYYW`z^!r8^Mg{N>j2gXsAc#1`bY$2iX{%v!WfpJ{Ywj4u{~?au zVF~E=4?x)|jmdM-Zp`Ag0rUd>|&>X(>Tz)M%XlH9(SjA52z-CiY-@ zpOTxHF}K{+DEU8$2C-b_?nmXaG+kuT>+A+3s^$*L_$Qjtob<8&iDQ{j8XXHFe<=QE zZeII`Xo{wtG0@4NTPLMk##NkCMT37C-~2cFXk3CUj`TL zL0A>*43nox@*-BW6}Ux~f}8(iz}15X$g%GVVRx{K6p)LX=Pk=XuSg@VB`?uCZ>57` zuY^#^o@tp}h0C)kB{33KEV*lKyg}JD2HErH#2Zv!W3Yby+<1c_*BF@Zo*r*7^BRNI zcdx127Ttp(V6H03xCeg*nYubr($k_{vYvLSDo>B2-Pfmzi5PJeS7XBSpp^8@F#fA0PVLw6>dQZVq zTL($vG5R_R;rV-Ash@2h;n@bllPDYq9-`_VM;rQ~{~PesLfABrIs#eShA67qK0vht z5ybifdgDooj97EePlS0x^*xDBf%J8-z8tEF7G#r&ELplFcd~t6z z{{YU-Yy)UM2sbPq%lR9kTl4Z$;qSGVB>sEx($d-dl*sRR2kj`FqGte;c z>C2J7b2-Y75zV_F`0xYw8+j&9Mbe&uOtm274097df1rHSwPLcPBmc>0KkcA)OYcAO zpA=s{FrHrw*ZpKX54;5)R%>?05(wM*X8^V+=mlVpf^z_JqX6Fnc~OApq{9*kfX76B zRPz@A1yS%W0B2Iv?ma|s{pkqMxcJU+CR^xjqGy8TcEa6XM}I|>8g<44)R`7-k~8^9=%>dE=Pb)){raTkJSpf3jA28 zkjt6m#~J}+qFjMVrXB<)#gFv>CY9bLz@$;50+UYXFfe96&KsBvKTa8#OgdqK$)eXH zFxl|hC?Q>rAEyN(3-x}e^CCdpYM%67hytP5mM&K}LM;Ef&QqH^Fu2_)06# z-j#EJ*7~}D9`)4${n|Gl=xqONpvU~VK;QN|=rD`D4B%H1>T-l5Fk}RtONm+-&J^o( zlWa&cbgY2ROoo<^omj)*my~|a!BxiuKbtO#2LfJ*{I5VM(EQF@O#hB%`ETLttWKn+=>g2xRSUL~!bmYQX^2!g$p^15}H2mAV%))b53!VX+VZ z;-W8(51}PSb>fDu64TewOurTnb`_bviNbHh_O2gUuK>LE9(aZBfmiSzSd13%eJm;V zeQdF$nqUN2Gn@>3jQCc}@9yAN#5>)$^KV7N>Q(&P@Uhi1?DqfQSdEgTz6JTqvwehN z(fdTz=3L$vZr?nY^JekvmXF-<^U>g5ENf50BnugbcY)FYJz`n`!;lW#B(xbM_HVsS z6!he#!Y^aJb&wImdx{GiC3OM34&Zwk&>3g2zmlUy$cP7ec8lszfx&`cLTC&x5|4&f z3d^>adA<14wq0!_qUOYa|H!C-7+63j2((wRE&P()@ft8b6v5koG$%lCMm+@Mi%r{0 z#h2TMBow0BTPU163e(UAqDDm%b~pkN9>b!uZAMHT71?KKuY*Q|yF4_bRh@-%jK+u> z*=8*LtAj1k(Zlk9niM~qqD~3$&qvk zeLXNbN0>aavkty3;gs-YNKbv-r?6cyTV0xfS~2IzLFy>5Eq6-zRS`Z>`4GZP9PqGc zC&6CzgQw&jC7yn=Zw@Y!Y|xhDJ4p|gt4rYK9hDU661e0kmi}o;-a-9xYrGtO!(vRd zw~5c5vfAzOTKHIoS}#Tu1v{(RAvM+9I0PHWl#Uq|@JPAj+5 zl(aumWoR-HzDT^ctAdw{^rtJhM_m83^?Iy{Y@M#1#WknTKVVP)B(ENY>ViKRC9fWe z%AyO3M9>q_*eH#7>DRWEeLl7NAqG!)fO<1UM?uOAtJ>6Dm0S@JiD z&-X5J;VC6~ky^BOz|*AU49$YhCyAmR0+El4&S&oBRpK+C!^Et8HeM~3@2d<9M_)=0Il~1$#ag47YytbP(G91{0lWG#{~T|9EHI!LgKF`%{C@ zMh21e?3wVzXFuVbi{5Ym>NEYoJ$Cry3?De)1cR^oD_|(?BN#nF@>YxU2P!OBG15vP z!dj_+1RIfkutm&0Se%3z>5}e)qwR6yPegkhRM);PCPvhN@~{LRQ;RWk6}`!yEEjz65?a32i{y3!ycR=n_X zxb?*X&TGWoM{eb{;x|X;LF1JkoyV^ej~tyRettAre1CK%ZxAzInkR03DOtSn(oAt2 zo>ec;3^YNEj&C8ni$3^jSBS+UI>KVxMp$g*2;11tVuXnF`Y79p$wRL6*DUz(;uV15 za|*sMaND|vcJx6xJ2#5tW9@vin0~B0y$4L-xBt)%6U?|XBsLx!!gq)x$6V=HmFjd^ zdly_J??LhTvGUseg0W)wlIGaR*reNlzj@ix1&il*;1RY6Zg%xfkWIar?KE>{23hg= zsjXw+d00$6UY!F4W>8qRMqlO%4oUQyIFHj_dWbGB=w;D5(um8Jo{~ z!2ICoUJu+L*izD+Z$@1eTgG53vg=EFJw7&!c}X`F{0_1jM4%g4gSyd7kj^{2*K?M& zN6?(cUe75uA;5k~$`v|fo5}HP13FHC!B;_;;V^tFn+ymO9pviuoM6-0KGw<2Y|>4r zo6f>uX=aTPbeJp+MqT$Jp|2D~x3b?6+M9srHWv7Q8t`O24tP7eK+0#vqH-P}47!ts zznfhoT@}r6A-hC=gKtK^`-nHQ&&QzOgMd155n2!hm_+E$5M?J@%~D`OX7(DOJuump z0KrQHLu?XM&CGH}qT?<$hjbyS*JELO*h1LOjGdi{1MXq>a-4nfEVO(M&?@k&fXaLd zXfdI6P;Nx`5wcKfFR-07!a~rUWUsNyJb>94L3x_J&QuA5rcltg*j?F(0%4Da2CbHAa3G^%* z478V(W=ZTEtIJMg=h;xO`3qTIAj=e<3))oP3|ccsuWXK9HjZ9)k~{cNkQDN2po4i0 zP?a}8oKo;jWMzDG7W_4D1c4f&^%SUy0<{vqmgEl+T?dj(_Av45$z}ubj{(hO{|7XU zZRPC{`UJ^OQiRiFc^1-wDN#b_DWUfvp(GYutMUT-0SOa2Ea5bcN;r+<659VZcueKi zz&A3g`(!7sya5&UMwB!e(P4}c9d0nfF3mt0s31?@dTxKl2SZPDW0Vi zdnv{9#sKE{KIQlU<@hn>_$lRhiE_M5DSksK_Cbmg`;IEX6in2hV4{f%CYqvLg({{h zSUIzT>1Hd5*%Hf9uyQs9lXfVX*-0#)Yzh@?07Qco2N=1?NL6qKWeU!qQt<#Cu2cf8 zQAPk=q~OvlA?IZZF3nbwY$wT1lI&4%A@(b{Oa~NPh{Fm_@~DCfaa_TL_^pBqaYA`3 zONKoX{F9qsbiQH`ut6>mxiz6Ml{|x|%K407*-&23R|W4J%6|v?(hB}q+SVM@A1B&u37)LrgMybUAZU?Q z1d}ScH;dWPqL644(N}`ONXw*aWIMMfsULeYxsO3Zpi8c{! zCOU^`C(*6CyfC<@iXRRx9L8VdJwazRx2CZYN`q*#8XQy2?>}_BnuiLk?HLjrkVk>8 z=fg84wt;U4`skqtf5fZ(=94efrrzs*cU*VdD^91&b>z^|)BJEzPRWLv$KZ@&aMxhd z;J;1q{~>J(KJ+CoHn0D%N{Qv!Uv~tLe#u)h^B{io-w5OjHxyZT$szS$ c{3B^YWy+znulN GetEntityFamily(TR2Entities entity) return new List { entity }; } + public static List RemoveAliases(IEnumerable entities) + { + List ents = new List(); + foreach (TR2Entities ent in entities) + { + TR2Entities normalisedEnt = TranslateEntityAlias(ent); + if (!ents.Contains(normalisedEnt)) + { + ents.Add(normalisedEnt); + } + } + return ents; + } + public static List GetLaraTypes() { return new List diff --git a/TRLevelReader/Helpers/TR3EntityUtilities.cs b/TRLevelReader/Helpers/TR3EntityUtilities.cs index a97907d66..058155125 100644 --- a/TRLevelReader/Helpers/TR3EntityUtilities.cs +++ b/TRLevelReader/Helpers/TR3EntityUtilities.cs @@ -95,6 +95,20 @@ public static TR3Entities GetAliasForLevel(string lvl, TR3Entities entity) return entity; } + public static List RemoveAliases(IEnumerable entities) + { + List ents = new List(); + foreach (TR3Entities ent in entities) + { + TR3Entities normalisedEnt = TranslateEntityAlias(ent); + if (!ents.Contains(normalisedEnt)) + { + ents.Add(normalisedEnt); + } + } + return ents; + } + public static List GetLaraTypes() { return new List diff --git a/TRRandomizerCore/Editors/RandomizerSettings.cs b/TRRandomizerCore/Editors/RandomizerSettings.cs index 3210ae611..71e878c8f 100644 --- a/TRRandomizerCore/Editors/RandomizerSettings.cs +++ b/TRRandomizerCore/Editors/RandomizerSettings.cs @@ -3,6 +3,8 @@ using TRRandomizerCore.Helpers; using TRGE.Core; using System.Drawing; +using System.Collections.Generic; +using System.Linq; namespace TRRandomizerCore.Editors { @@ -54,6 +56,12 @@ public class RandomizerSettings public bool DocileBirdMonsters { get; set; } public RandoDifficulty RandoEnemyDifficulty { get; set; } public bool MaximiseDragonAppearance { get; set; } + public bool UseEnemyExclusions { get; set; } + public List ExcludedEnemies { get; set; } + public Dictionary ExcludableEnemies { get; set; } + public bool ShowExclusionWarnings { get; set; } + public List IncludedEnemies => ExcludableEnemies.Keys.Except(ExcludedEnemies).ToList(); + public bool OneEnemyMode => IncludedEnemies.Count == 1; public bool GlitchedSecrets { get; set; } public bool UseRewardRoomCameras { get; set; } public bool PersistOutfits { get; set; } @@ -124,6 +132,13 @@ public void ApplyConfig(Config config) DocileBirdMonsters = config.GetBool(nameof(DocileBirdMonsters)); RandoEnemyDifficulty = (RandoDifficulty)config.GetEnum(nameof(RandoEnemyDifficulty), typeof(RandoDifficulty), RandoDifficulty.Default); MaximiseDragonAppearance = config.GetBool(nameof(MaximiseDragonAppearance)); + UseEnemyExclusions = config.GetBool(nameof(UseEnemyExclusions)); + ShowExclusionWarnings = config.GetBool(nameof(ShowExclusionWarnings)); + ExcludedEnemies = config.GetString(nameof(ExcludedEnemies)) + .Split(new string[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => short.Parse(s)) + .Where(s => ExcludableEnemies.ContainsKey(s)) + .ToList(); RandomizeTextures = config.GetBool(nameof(RandomizeTextures)); TextureSeed = config.GetInt(nameof(TextureSeed), defaultSeed); @@ -219,6 +234,9 @@ public void StoreConfig(Config config) config[nameof(DocileBirdMonsters)] = DocileBirdMonsters; config[nameof(RandoEnemyDifficulty)] = RandoEnemyDifficulty; config[nameof(MaximiseDragonAppearance)] = MaximiseDragonAppearance; + config[nameof(ExcludedEnemies)] = string.Join(",", ExcludedEnemies); + config[nameof(UseEnemyExclusions)] = UseEnemyExclusions; + config[nameof(ShowExclusionWarnings)] = ShowExclusionWarnings; config[nameof(RandomizeTextures)] = RandomizeTextures; config[nameof(TextureSeed)] = TextureSeed; diff --git a/TRRandomizerCore/Editors/TR2RandoEditor.cs b/TRRandomizerCore/Editors/TR2RandoEditor.cs index 811af026b..b2831009a 100644 --- a/TRRandomizerCore/Editors/TR2RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR2RandoEditor.cs @@ -1,7 +1,10 @@ -using System.Collections.Generic; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; using System.Linq; using TRGE.Coord; using TRGE.Core; +using TRLevelReader.Helpers; using TRRandomizerCore.Processors; using TRRandomizerCore.Randomizers; using TRRandomizerCore.Textures; @@ -17,7 +20,10 @@ public TR2RandoEditor(TRDirectoryIOArgs args, TREdition edition) protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings(); + Settings = new RandomizerSettings + { + ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR2\Restrictions\excludable_enemies.json")) + }; Settings.ApplyConfig(config); } @@ -48,6 +54,12 @@ protected override void SaveImpl(AbstractTRScriptEditor scriptEditor, TRSaveMoni TR23ScriptEditor tr23ScriptEditor = scriptEditor as TR23ScriptEditor; string wipDirectory = _io.WIPOutputDirectory.FullName; + if (Settings.DevelopmentMode) + { + (tr23ScriptEditor.Script as TR23Script).LevelSelectEnabled = true; + scriptEditor.SaveScript(); + } + // Texture monitoring is needed between enemy and texture randomization // to track where imported enemies are placed. using (TR2TextureMonitorBroker textureMonitor = new TR2TextureMonitorBroker()) diff --git a/TRRandomizerCore/Editors/TR3RandoEditor.cs b/TRRandomizerCore/Editors/TR3RandoEditor.cs index 41a4a1ace..e8f94fa99 100644 --- a/TRRandomizerCore/Editors/TR3RandoEditor.cs +++ b/TRRandomizerCore/Editors/TR3RandoEditor.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System.Drawing; +using Newtonsoft.Json; +using System.Collections.Generic; using System.IO; using System.Linq; using TRGE.Coord; @@ -20,7 +20,10 @@ public TR3RandoEditor(TRDirectoryIOArgs args, TREdition edition) protected override void ApplyConfig(Config config) { - Settings = new RandomizerSettings(); + Settings = new RandomizerSettings + { + ExcludableEnemies = JsonConvert.DeserializeObject>(File.ReadAllText(@"Resources\TR3\Restrictions\excludable_enemies.json")) + }; Settings.ApplyConfig(config); } diff --git a/TRRandomizerCore/Helpers/TRRandomizationCategory.cs b/TRRandomizerCore/Helpers/TRRandomizationCategory.cs index a911be285..212ca9c4e 100644 --- a/TRRandomizerCore/Helpers/TRRandomizationCategory.cs +++ b/TRRandomizerCore/Helpers/TRRandomizationCategory.cs @@ -7,6 +7,7 @@ public enum TRRandomizationCategory PreRandomize, Randomize, Commit, - Cancel + Cancel, + Warning } } \ No newline at end of file diff --git a/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs b/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs index e21a0bbed..3883acabf 100644 --- a/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs +++ b/TRRandomizerCore/Helpers/TRRandomizationEventArgs.cs @@ -43,6 +43,8 @@ private static TRRandomizationCategory ConvertCategory(TRSaveCategory category) return TRRandomizationCategory.Cancel; // The operation has been cancelled externally case TRSaveCategory.Commit: return TRRandomizationCategory.Commit; // TRGE is commiting the changes to the original data directory + case TRSaveCategory.Warning: + return TRRandomizationCategory.Warning; // A processor wants to send a warning message default: return TRRandomizationCategory.None; } diff --git a/TRRandomizerCore/Levels/TR2CombinedLevel.cs b/TRRandomizerCore/Levels/TR2CombinedLevel.cs index 9377779b9..55c8e6b70 100644 --- a/TRRandomizerCore/Levels/TR2CombinedLevel.cs +++ b/TRRandomizerCore/Levels/TR2CombinedLevel.cs @@ -137,5 +137,26 @@ public int GetMaximumEntityLimit() return limit; } + + public int GetActualEntityCount() + { + int count = 0; + foreach (TR2Entity entity in Data.Entities) + { + switch ((TR2Entities)entity.TypeID) + { + case TR2Entities.MercSnowmobDriver: + count += 2; + break; + case TR2Entities.MarcoBartoli: + count += 7; + break; + default: + count++; + break; + } + } + return count; + } } } \ No newline at end of file diff --git a/TRRandomizerCore/Processors/AbstractLevelProcessor.cs b/TRRandomizerCore/Processors/AbstractLevelProcessor.cs index 5686a70b2..76eb9fbf6 100644 --- a/TRRandomizerCore/Processors/AbstractLevelProcessor.cs +++ b/TRRandomizerCore/Processors/AbstractLevelProcessor.cs @@ -81,6 +81,14 @@ internal void SetMessage(string text) } } + internal void SetWarning(string text) + { + lock (_monitorLock) + { + SaveMonitor.FireSaveStateChanged(category: TRSaveCategory.Warning, customDescription: text); + } + } + public void HandleException(Exception e) { lock (_monitorLock) diff --git a/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs index dd8086e31..75b1942ab 100644 --- a/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR2/TR2EnemyRandomizer.cs @@ -1,6 +1,11 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using System.Numerics; +using TRFDControl; +using TRFDControl.FDEntryTypes; +using TRFDControl.Utilities; using TRGE.Core; using TRLevelReader.Helpers; using TRLevelReader.Model; @@ -18,6 +23,8 @@ namespace TRRandomizerCore.Randomizers public class TR2EnemyRandomizer : BaseTR2Randomizer { private Dictionary> _gameEnemyTracker; + private List _excludedEnemies; + private ISet _resultantEnemies; internal int MaxPackingAttempts { get; set; } internal TR2TextureMonitorBroker TextureMonitor { get; set; } @@ -95,6 +102,12 @@ private void RandomizeEnemiesCrossLevel() // Track enemies whose counts across the game are restricted _gameEnemyTracker = TR2EnemyUtilities.PrepareEnemyGameTracker(Settings.DocileBirdMonsters, Settings.RandoEnemyDifficulty); + // #272 Selective enemy pool - convert the shorts in the settings to actual entity types + _excludedEnemies = Settings.UseEnemyExclusions ? + Settings.ExcludedEnemies.Select(s => (TR2Entities)s).ToList() : + new List(); + _resultantEnemies = new HashSet(); + SetMessage("Randomizing enemies - importing models"); foreach (EnemyProcessor processor in processors) { @@ -119,6 +132,28 @@ private void RandomizeEnemiesCrossLevel() { _processingException.Throw(); } + + // If any exclusions failed to be avoided, send a message + if (Settings.ShowExclusionWarnings) + { + VerifyExclusionStatus(); + } + } + + private void VerifyExclusionStatus() + { + List failedExclusions = _resultantEnemies.ToList().FindAll(_excludedEnemies.Contains); + if (failedExclusions.Count > 0) + { + // A little formatting + List failureNames = new List(); + foreach (TR2Entities entity in failedExclusions) + { + failureNames.Add(Settings.ExcludableEnemies[(short)entity]); + } + failureNames.Sort(); + SetWarning(string.Format("The following enemies could not be excluded entirely from the randomization pool.{0}{0}{1}", Environment.NewLine, string.Join(Environment.NewLine, failureNames))); + } } private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, int reduceEnemyCountBy = 0) @@ -139,6 +174,8 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, List chickenGuisers = TR2EnemyUtilities.GetEnemyGuisers(TR2Entities.BirdMonster); TR2Entities chickenGuiser = TR2Entities.BirdMonster; + RandoDifficulty difficulty = GetImpliedDifficulty(); + // #148 For HSH, we lock the enemies that are required for the kill counter to work outside // the gate, which means the game still has the correct target kill count, while allowing // us to randomize the ones inside the gate (except the final shotgun goon). @@ -172,30 +209,18 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, // Do we need at least one enemy that can drop? bool droppableEnemyRequired = TR2EnemyUtilities.IsDroppableEnemyRequired(level); - // Let's try to populate the list. Start by adding one water enemy - // and one droppable enemy if they are needed. + // Let's try to populate the list. Start by adding one water enemy and one droppable + // enemy if they are needed. If we want to exclude, try to select based on user priority. if (waterEnemyRequired) { List waterEnemies = TR2EntityUtilities.KillableWaterCreatures(); - TR2Entities entity; - do - { - entity = waterEnemies[_generator.Next(0, waterEnemies.Count)]; - } - while (!TR2EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(waterEnemies, level, difficulty)); } if (droppableEnemyRequired) { List droppableEnemies = TR2EntityUtilities.GetCrossLevelDroppableEnemies(!Settings.ProtectMonks); - TR2Entities entity; - do - { - entity = droppableEnemies[_generator.Next(0, droppableEnemies.Count)]; - } - while (!TR2EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(droppableEnemies, level, difficulty)); } // Are there any other types we need to retain? @@ -207,16 +232,34 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, } } - // Get all other candidate enemies and fill the list - List allEnemies = TR2EntityUtilities.GetCandidateCrossLevelEnemies(); + // Get all other candidate supported enemies + List allEnemies = TR2EntityUtilities.GetCandidateCrossLevelEnemies().FindAll(e => TR2EnemyUtilities.IsEnemySupported(level.Name, e, difficulty)); + if (Settings.OneEnemyMode || Settings.IncludedEnemies.Count < newEntities.Capacity) + { + // Marco isn't excludable in his own right because supporting a dragon-only game is impossible + allEnemies.Remove(TR2Entities.MarcoBartoli); + } + + // Remove all exclusions from the pool, and adjust the target capacity + allEnemies.RemoveAll(e => _excludedEnemies.Contains(e)); - while (newEntities.Count < newEntities.Capacity) + IEnumerable ex = allEnemies.Where(e => !newEntities.Any(TR2EntityUtilities.GetEntityFamily(e).Contains)); + List unalisedEntities = TR2EntityUtilities.RemoveAliases(ex); + while (unalisedEntities.Count < newEntities.Capacity - newEntities.Count) + { + --newEntities.Capacity; + } + + // Fill the list from the remaining candidates. Keep track of ones tested to avoid + // looping infinitely if it's not possible to fill to capacity + ISet testedEntities = new HashSet(); + while (newEntities.Count < newEntities.Capacity && testedEntities.Count < allEnemies.Count) { TR2Entities entity; // Try to enforce Marco's appearance, but only if this isn't the final packing attempt if (Settings.MaximiseDragonAppearance && !newEntities.Contains(TR2Entities.MarcoBartoli) - && TR2EnemyUtilities.IsEnemySupported(level.Name, TR2Entities.MarcoBartoli, Settings.RandoEnemyDifficulty) + && TR2EnemyUtilities.IsEnemySupported(level.Name, TR2Entities.MarcoBartoli, difficulty) && reduceEnemyCountBy == 0) { entity = TR2Entities.MarcoBartoli; @@ -226,10 +269,12 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, entity = allEnemies[_generator.Next(0, allEnemies.Count)]; } + testedEntities.Add(entity); + int adjustmentCount = TR2EnemyUtilities.GetTargetEnemyAdjustmentCount(level.Name, entity); - if (adjustmentCount != 0) + if (!Settings.OneEnemyMode && adjustmentCount != 0) { - while (newEntities.Count >= newEntities.Capacity + adjustmentCount) + while (newEntities.Count > 0 && newEntities.Count >= newEntities.Capacity + adjustmentCount) { newEntities.RemoveAt(newEntities.Count - 1); } @@ -238,33 +283,23 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, // Check if the use of this enemy triggers an overwrite of the pool, for example // the dragon in HSH. Null means nothing special has been defined. - List> restrictedCombinations = TR2EnemyUtilities.GetPermittedCombinations(level.Name, entity, Settings.RandoEnemyDifficulty); + List> restrictedCombinations = TR2EnemyUtilities.GetPermittedCombinations(level.Name, entity, difficulty); if (restrictedCombinations != null) { do { - // Pick a combination, ensuring we honour docile bird monsters if present + // Pick a combination, ensuring we honour docile bird monsters if present, + // and try to select a group that doesn't contain an excluded enemy. newEntities.Clear(); newEntities.AddRange(restrictedCombinations[_generator.Next(0, restrictedCombinations.Count)]); } - while (Settings.DocileBirdMonsters && newEntities.Contains(TR2Entities.BirdMonster) && chickenGuisers.All(g => newEntities.Contains(g))); + while (Settings.DocileBirdMonsters && newEntities.Contains(TR2Entities.BirdMonster) && chickenGuisers.All(g => newEntities.Contains(g)) + || (newEntities.Any(_excludedEnemies.Contains) && restrictedCombinations.Any(c => !c.Any(_excludedEnemies.Contains)))); break; } - // Make sure this isn't known to be unsupported in the level - if (!TR2EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)) - { - continue; - } - // If it's the chicken in HSH but we're not using docile, we don't want it ending the level - if (!Settings.DocileBirdMonsters && entity == TR2Entities.BirdMonster && level.Is(TR2LevelNames.HOME)) - { - continue; - } - - // If it's a docile chicken in Barkhang, it won't work because we can't disguise monks in this level. - if (Settings.DocileBirdMonsters && entity == TR2Entities.BirdMonster && level.Is(TR2LevelNames.MONASTERY)) + if (!Settings.DocileBirdMonsters && entity == TR2Entities.BirdMonster && level.Is(TR2LevelNames.HOME) && allEnemies.Except(newEntities).Count() > 1) { continue; } @@ -281,8 +316,13 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, } else { - // Otherwise, pick something else - continue; + // Otherwise, pick something else. If we tried to previously exclude this + // enemy and couldn't, it will slip through the net and so the appearances + // will increase. + if (allEnemies.Except(newEntities).Count() > 1) + { + continue; + } } } @@ -320,6 +360,26 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, } } + // If everything we are including is restriced by room, we need to provide at least one other enemy type + Dictionary> restrictedRoomEnemies = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.Name, difficulty); + if (restrictedRoomEnemies != null && newEntities.All(e => restrictedRoomEnemies.ContainsKey(e))) + { + List pool = TR2EntityUtilities.GetCrossLevelDroppableEnemies(!Settings.ProtectMonks); + do + { + TR2Entities fallbackEnemy; + do + { + fallbackEnemy = pool[_generator.Next(0, pool.Count)]; + } + while ((_excludedEnemies.Contains(fallbackEnemy) && pool.Any(e => !_excludedEnemies.Contains(e))) + || newEntities.Contains(fallbackEnemy) + || !TR2EnemyUtilities.IsEnemySupported(level.Name, fallbackEnemy, difficulty)); + newEntities.Add(fallbackEnemy); + } + while (newEntities.All(e => restrictedRoomEnemies.ContainsKey(e))); + } + // #144 Decide at this point who will be guising unless it has already been decided above (e.g. HSH) if (Settings.DocileBirdMonsters && newEntities.Contains(TR2Entities.BirdMonster) && chickenGuiser == TR2Entities.BirdMonster) { @@ -338,6 +398,46 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR2CombinedLevel level, }; } + private TR2Entities SelectRequiredEnemy(List pool, TR2CombinedLevel level, RandoDifficulty difficulty) + { + pool.RemoveAll(e => !TR2EnemyUtilities.IsEnemySupported(level.Name, e, difficulty)); + + TR2Entities entity; + if (pool.All(_excludedEnemies.Contains)) + { + // Select the last excluded enemy (lowest priority) + entity = _excludedEnemies.Last(e => pool.Contains(e)); + } + else + { + do + { + entity = pool[_generator.Next(0, pool.Count)]; + } + while (_excludedEnemies.Contains(entity)); + } + + return entity; + } + + private RandoDifficulty GetImpliedDifficulty() + { + if (_excludedEnemies.Count > 0 && Settings.RandoEnemyDifficulty == RandoDifficulty.Default) + { + // If every enemy in the pool has room restrictions for any level, we have to imply NoRestrictions difficulty mode + List includedEnemies = Settings.ExcludableEnemies.Keys.Except(Settings.ExcludedEnemies).Select(s => (TR2Entities)s).ToList(); + foreach (TR2ScriptedLevel level in Levels) + { + IEnumerable restrictedRoomEnemies = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.LevelFileBaseName.ToUpper(), RandoDifficulty.Default).Keys; + if (includedEnemies.All(e => restrictedRoomEnemies.Contains(e))) + { + return RandoDifficulty.NoRestrictions; + } + } + } + return Settings.RandoEnemyDifficulty; + } + private void RandomizeEnemiesNatively(TR2CombinedLevel level) { // For the assault course, nothing will be changed for the time being @@ -410,6 +510,8 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // Keep track of any new entities added (e.g. Skidoo) List newEntities = new List(); + RandoDifficulty difficulty = GetImpliedDifficulty(); + // #148 If it's HSH and we have been able to import cross-level, we will add 15 // dogs outside the gate to ensure the kill counter works. Dogs, Goon1 and // StickGoons will have been excluded from the cross-level pool for simplicity @@ -435,7 +537,7 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti } // First iterate through any enemies that are restricted by room - Dictionary> enemyRooms = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.Name, Settings.RandoEnemyDifficulty); + Dictionary> enemyRooms = TR2EnemyUtilities.GetRestrictedEnemyRooms(level.Name, difficulty); if (enemyRooms != null) { foreach (TR2Entities entity in enemyRooms.Keys) @@ -446,7 +548,7 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti } List rooms = enemyRooms[entity]; - int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(entity, Settings.RandoEnemyDifficulty); + int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(entity, difficulty); if (maxEntityCount == -1) { // We are allowed any number, but this can't be more than the number of unique rooms, @@ -609,7 +711,8 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // #278 Flamethrowers in room 29 after pulling the lever are too difficult, but if difficulty is set to unrestricted // and they do end up here, environment mods will change their positions. - if (level.Is(TR2LevelNames.FLOATER) && Settings.RandoEnemyDifficulty == RandoDifficulty.Default && (enemyIndex == 34 || enemyIndex == 35)) + int totalRestrictionCount = TR2EnemyUtilities.GetRestrictedEnemyTotalTypeCount(difficulty); + if (level.Is(TR2LevelNames.FLOATER) && difficulty == RandoDifficulty.Default && (enemyIndex == 34 || enemyIndex == 35) && enemyPool.Count > totalRestrictionCount) { while (newEntityType == TR2Entities.FlamethrowerGoon) { @@ -620,10 +723,10 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // If we are restricting count per level for this enemy and have reached that count, pick // something else. This applies when we are restricting by in-level count, but not by room // (e.g. Winston). - int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(newEntityType, Settings.RandoEnemyDifficulty); + int maxEntityCount = TR2EnemyUtilities.GetRestrictedEnemyLevelCount(newEntityType, difficulty); if (maxEntityCount != -1) { - if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount) + if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount && enemyPool.Count > totalRestrictionCount) { TR2Entities tmp = newEntityType; while (newEntityType == tmp) @@ -647,6 +750,9 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti // to the dragon, which will be handled above in defined rooms, but the check should be made // here in case this needs to be extended later. TR2EnemyUtilities.SetEntityTriggers(level.Data, currentEntity); + + // Track every enemy type across the game + _resultantEnemies.Add(newEntityType); } // MercSnowMobDriver relies on RedSnowmobile so it will be available in the model list @@ -702,9 +808,64 @@ private void RandomizeEnemies(TR2CombinedLevel level, EnemyRandomizationCollecti level.Data.NumEntities = (uint)levelEntities.Count; } + // Check in case there are too many skidoo drivers + if (difficulty == RandoDifficulty.NoRestrictions && Array.Find(level.Data.Entities, e => e.TypeID == (short)TR2Entities.MercSnowmobDriver) != null) + { + LimitSkidooEntities(level); + } + RandomizeEnemyMeshes(level, enemies); } + private void LimitSkidooEntities(TR2CombinedLevel level) + { + // Although 256 is the entity limit, any more than 250 and the level can't be saved + const int skidooLimit = 250; + if (level.GetActualEntityCount() >= skidooLimit) + { + FDControl floorData = new FDControl(); + floorData.ParseFromLevel(level.Data); + LocationGenerator locationGenerator = new LocationGenerator(); + List replacementPool = TR2EntityUtilities.GetListOfAmmoTypes(); + + TR2Entity[] skidMen; + do + { + skidMen = Array.FindAll(level.Data.Entities, e => e.TypeID == (short)TR2Entities.MercSnowmobDriver); + if (skidMen.Length == 0) + { + break; + } + + // Select a random Skidoo driver and convert him into a pickup + TR2Entity skidMan = skidMen[_generator.Next(0, skidMen.Length)]; + skidMan.TypeID = (short)replacementPool[_generator.Next(0, replacementPool.Count)]; + + // Make sure the pickup is pickupable + TRRoomSector sector = FDUtilities.GetRoomSector(skidMan.X, skidMan.Y, skidMan.Z, skidMan.Room, level.Data, floorData); + skidMan.Y = sector.Floor * 256; + if (sector.FDIndex != 0) + { + FDEntry entry = floorData.Entries[sector.FDIndex].Find(e => e is FDSlantEntry s && s.Type == FDSlantEntryType.FloorSlant); + if (entry is FDSlantEntry slant) + { + Vector4? bestMidpoint = locationGenerator.GetBestSlantMidpoint(slant); + if (bestMidpoint.HasValue) + { + skidMan.Y += (int)bestMidpoint.Value.Y; + } + } + } + + // Get rid of the enemy's triggers + FDUtilities.RemoveEntityTriggers(level.Data, Array.IndexOf(level.Data.Entities, skidMan), floorData); + } + while (level.GetActualEntityCount() >= skidooLimit); + + floorData.WriteToLevel(level.Data); + } + } + private void RandomizeEnemyMeshes(TR2CombinedLevel level, EnemyRandomizationCollection enemies) { // #314 A very primitive start to mixing-up enemy meshes - monks and yetis can take on Lara's meshes @@ -909,6 +1070,10 @@ internal void ApplyRandomization() } _outer.RandomizeEnemies(level, enemies); + if (_outer.Settings.DevelopmentMode) + { + Debug.WriteLine(level.Name + ": " + string.Join(", ", enemies.All)); + } } _outer.SaveLevel(level); diff --git a/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs b/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs index ac5868834..171ca07f1 100644 --- a/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs +++ b/TRRandomizerCore/Randomizers/TR3/TR3EnemyRandomizer.cs @@ -1,22 +1,18 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using TRRandomizerCore.Helpers; -using TRRandomizerCore.Levels; -using TRRandomizerCore.Processors; -using TRRandomizerCore.Utilities; using TRGE.Core; using TRLevelReader.Helpers; using TRLevelReader.Model; using TRLevelReader.Model.Enums; using TRModelTransporter.Transport; -using System.Diagnostics; +using TRRandomizerCore.Helpers; +using TRRandomizerCore.Levels; +using TRRandomizerCore.Processors; using TRRandomizerCore.Textures; -using Newtonsoft.Json; -using TREnvironmentEditor; -using TRFDControl.Utilities; -using TRFDControl; -using TRFDControl.FDEntryTypes; +using TRRandomizerCore.Utilities; namespace TRRandomizerCore.Randomizers { @@ -24,6 +20,8 @@ public class TR3EnemyRandomizer : BaseTR3Randomizer { private Dictionary> _gameEnemyTracker; private Dictionary> _pistolLocations; + private List _excludedEnemies; + private ISet _resultantEnemies; internal TR3TextureMonitorBroker TextureMonitor { get; set; } public ItemFactory ItemFactory { get; set; } @@ -93,6 +91,12 @@ private void RandomizeEnemiesCrossLevel() // Track enemies whose counts across the game are restricted _gameEnemyTracker = TR3EnemyUtilities.PrepareEnemyGameTracker(Settings.RandoEnemyDifficulty); + // #272 Selective enemy pool - convert the shorts in the settings to actual entity types + _excludedEnemies = Settings.UseEnemyExclusions ? + Settings.ExcludedEnemies.Select(s => (TR3Entities)s).ToList() : + new List(); + _resultantEnemies = new HashSet(); + SetMessage("Randomizing enemies - importing models"); foreach (EnemyProcessor processor in processors) { @@ -117,6 +121,28 @@ private void RandomizeEnemiesCrossLevel() { _processingException.Throw(); } + + // If any exclusions failed to be avoided, send a message + if (Settings.ShowExclusionWarnings) + { + VerifyExclusionStatus(); + } + } + + private void VerifyExclusionStatus() + { + List failedExclusions = _resultantEnemies.ToList().FindAll(_excludedEnemies.Contains); + if (failedExclusions.Count > 0) + { + // A little formatting + List failureNames = new List(); + foreach (TR3Entities entity in failedExclusions) + { + failureNames.Add(Settings.ExcludableEnemies[(short)entity]); + } + failureNames.Sort(); + SetWarning(string.Format("The following enemies could not be excluded entirely from the randomization pool.{0}{0}{1}", Environment.NewLine, string.Join(Environment.NewLine, failureNames))); + } } private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) @@ -131,12 +157,8 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) List oldEntities = GetCurrentEnemyEntities(level); // Get the list of canidadates - List allEnemies = TR3EntityUtilities.GetCandidateCrossLevelEnemies(); - if (!Settings.DocileBirdMonsters) - { - allEnemies.Remove(TR3Entities.Willie); - } - + List allEnemies = TR3EntityUtilities.GetCandidateCrossLevelEnemies().FindAll(e => TR3EnemyUtilities.IsEnemySupported(level.Name, e, Settings.RandoEnemyDifficulty)); + // Work out how many we can support int enemyCount = oldEntities.Count + TR3EnemyUtilities.GetEnemyAdjustmentCount(level.Name); List newEntities = new List(enemyCount); @@ -151,25 +173,13 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) if (waterEnemyRequired) { List waterEnemies = TR3EntityUtilities.GetKillableWaterEnemies(); - TR3Entities entity; - do - { - entity = waterEnemies[_generator.Next(0, waterEnemies.Count)]; - } - while (!TR3EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(waterEnemies, level, Settings.RandoEnemyDifficulty)); } if (droppableEnemyRequired) { List droppableEnemies = TR3EntityUtilities.FilterDroppableEnemies(allEnemies, Settings.ProtectMonks); - TR3Entities entity; - do - { - entity = droppableEnemies[_generator.Next(0, droppableEnemies.Count)]; - } - while (!TR3EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)); - newEntities.Add(entity); + newEntities.Add(SelectRequiredEnemy(droppableEnemies, level, Settings.RandoEnemyDifficulty)); } // Are there any other types we need to retain? @@ -181,10 +191,29 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) } } - // Fill the list from the remaining candidates - while (newEntities.Count < newEntities.Capacity) + if (!Settings.DocileBirdMonsters || Settings.OneEnemyMode || Settings.IncludedEnemies.Count < newEntities.Capacity) + { + // Willie isn't excludable in his own right because supporting a Willie-only game is impossible + allEnemies.Remove(TR3Entities.Willie); + } + + // Remove all exclusions from the pool, and adjust the target capacity + allEnemies.RemoveAll(e => _excludedEnemies.Contains(e)); + + IEnumerable ex = allEnemies.Where(e => !newEntities.Any(TR3EntityUtilities.GetEntityFamily(e).Contains)); + List unalisedEntities = TR3EntityUtilities.RemoveAliases(ex); + while (unalisedEntities.Count < newEntities.Capacity - newEntities.Count) + { + --newEntities.Capacity; + } + + // Fill the list from the remaining candidates. Keep track of ones tested to avoid + // looping infinitely if it's not possible to fill to capacity + ISet testedEntities = new HashSet(); + while (newEntities.Count < newEntities.Capacity && testedEntities.Count < allEnemies.Count) { TR3Entities entity = allEnemies[_generator.Next(0, allEnemies.Count)]; + testedEntities.Add(entity); // Make sure this isn't known to be unsupported in the level if (!TR3EnemyUtilities.IsEnemySupported(level.Name, entity, Settings.RandoEnemyDifficulty)) @@ -210,8 +239,13 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) } else { - // Otherwise, pick something else - continue; + // Otherwise, pick something else. If we tried to previously exclude this + // enemy and couldn't, it will slip through the net and so the appearances + // will increase. + if (allEnemies.Except(newEntities).Count() > 1) + { + continue; + } } } @@ -225,25 +259,26 @@ private EnemyTransportCollection SelectCrossLevelEnemies(TR3CombinedLevel level) } } - if (newEntities.Capacity > 1 && newEntities.Any(e => TR3EnemyUtilities.IsEnemyRestricted(level.Name, e))) + if (newEntities.Capacity > 1 && newEntities.All(e => TR3EnemyUtilities.IsEnemyRestricted(level.Name, e))) { // Make sure we have an unrestricted enemy available for the individual level conditions. This will // guarantee a "safe" enemy for the level; we avoid aliases here to avoid further complication. - TR3Entities unrestrictedEnemy; - do + bool RestrictionCheck(TR3Entities e) => + (droppableEnemyRequired && !TR3EntityUtilities.CanDropPickups(e, Settings.ProtectMonks)) + || !TR3EnemyUtilities.IsEnemySupported(level.Name, e, Settings.RandoEnemyDifficulty) + || newEntities.Contains(e) + || TR3EntityUtilities.IsWaterCreature(e) + || TR3EnemyUtilities.IsEnemyRestricted(level.Name, e) + || TR3EntityUtilities.TranslateEntityAlias(e) != e; + + List unrestrictedPool = allEnemies.FindAll(e => !RestrictionCheck(e)); + if (unrestrictedPool.Count == 0) { - unrestrictedEnemy = allEnemies[_generator.Next(0, allEnemies.Count)]; + // We are going to have to pull in the full list of candiates again, so ignoring any exclusions + unrestrictedPool = TR3EntityUtilities.GetCandidateCrossLevelEnemies().FindAll(e => !RestrictionCheck(e)); } - while - ( - (droppableEnemyRequired && !TR3EntityUtilities.CanDropPickups(unrestrictedEnemy, Settings.ProtectMonks)) - || newEntities.Contains(unrestrictedEnemy) - || TR3EntityUtilities.IsWaterCreature(unrestrictedEnemy) - || TR3EnemyUtilities.IsEnemyRestricted(level.Name, unrestrictedEnemy) - || TR3EntityUtilities.TranslateEntityAlias(unrestrictedEnemy) != unrestrictedEnemy - ); - newEntities.Add(unrestrictedEnemy); + newEntities.Add(unrestrictedPool[_generator.Next(0, unrestrictedPool.Count)]); } if (Settings.DevelopmentMode) @@ -267,6 +302,28 @@ private List GetCurrentEnemyEntities(TR3CombinedLevel level) return oldEntities; } + private TR3Entities SelectRequiredEnemy(List pool, TR3CombinedLevel level, RandoDifficulty difficulty) + { + pool.RemoveAll(e => !TR3EnemyUtilities.IsEnemySupported(level.Name, e, difficulty)); + + TR3Entities entity; + if (pool.All(_excludedEnemies.Contains)) + { + // Select the last excluded enemy (lowest priority) + entity = _excludedEnemies.Last(e => pool.Contains(e)); + } + else + { + do + { + entity = pool[_generator.Next(0, pool.Count)]; + } + while (_excludedEnemies.Contains(entity)); + } + + return entity; + } + private void RandomizeEnemiesNatively(TR3CombinedLevel level) { // For the assault course, nothing will be changed for the time being @@ -424,7 +481,7 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti int maxEntityCount = TR3EnemyUtilities.GetRestrictedEnemyLevelCount(newEntityType, Settings.RandoEnemyDifficulty); if (maxEntityCount != -1) { - if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount) + if (level.Data.Entities.ToList().FindAll(e => e.TypeID == (short)newEntityType).Count >= maxEntityCount && enemyPool.Count > maxEntityCount) { TR3Entities tmp = newEntityType; while (newEntityType == tmp || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) @@ -452,13 +509,18 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti } } } - else if (level.Is(TR3LevelNames.RXTECH) && level.IsWillardSequence && Settings.RandoEnemyDifficulty == RandoDifficulty.Default && (currentEntity.Room == 14 || currentEntity.Room == 45)) + else if (level.Is(TR3LevelNames.RXTECH) + && level.IsWillardSequence + && Settings.RandoEnemyDifficulty == RandoDifficulty.Default + && newEntityType == TR3Entities.RXTechFlameLad + && (currentEntity.Room == 14 || currentEntity.Room == 45)) { // #269 We don't want flamethrowers here because they're hostile, so getting off the minecart - // safely is too difficult. - while (newEntityType == TR3Entities.RXTechFlameLad || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) + // safely is too difficult. We can only change them if there is something else unrestricted available. + List safePool = enemyPool.FindAll(e => e != TR3Entities.RXTechFlameLad && !TR3EnemyUtilities.IsEnemyRestricted(level.Name, e)); + if (safePool.Count > 0) { - newEntityType = enemyPool[_generator.Next(0, enemyPool.Count)]; + newEntityType = safePool[_generator.Next(0, safePool.Count)]; } } else if (level.Is(TR3LevelNames.HSC)) @@ -473,24 +535,26 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti newEntityType = TR3Entities.Prisoner; } } - else if (currentEntity.Room == 78) + else if (currentEntity.Room == 78 && newEntityType == TR3Entities.Monkey) { // #286 Monkeys cannot share AI Ambush spots largely, but these are needed here to ensure the enemies // come through the gate before the timer closes them again. Just ensure no monkeys are here. - while (newEntityType == TR3Entities.Monkey || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) + List safePool = enemyPool.FindAll(e => e != TR3Entities.Monkey && !TR3EnemyUtilities.IsEnemyRestricted(level.Name, e)); + if (safePool.Count > 0) { - newEntityType = enemyPool[_generator.Next(0, enemyPool.Count)]; + newEntityType = safePool[_generator.Next(0, safePool.Count)]; + } + else + { + // Full monkey mode means we have to move them inside the gate + currentEntity.Z -= 4096; } } } - else if (level.Is(TR3LevelNames.THAMES) && (currentEntity.Room == 61 || currentEntity.Room == 62)) + else if (level.Is(TR3LevelNames.THAMES) && (currentEntity.Room == 61 || currentEntity.Room == 62) && newEntityType == TR3Entities.Monkey) { - // #286 Ban monkeys from these two rooms for now because of JP entity index differences (environment mods - // can't yet tell the difference). - while (newEntityType == TR3Entities.Monkey || TR3EnemyUtilities.IsEnemyRestricted(level.Name, newEntityType)) - { - newEntityType = enemyPool[_generator.Next(0, enemyPool.Count)]; - } + // #286 Move the monkeys away from the AI entities + currentEntity.Z -= 1024; } // Make sure to convert back to the actual type @@ -506,6 +570,9 @@ private void RandomizeEnemies(TR3CombinedLevel level, EnemyRandomizationCollecti { targetEntity.Invisible = false; } + + // Track every enemy type across the game + _resultantEnemies.Add(newEntityType); } // Add extra ammo based on this level's difficulty diff --git a/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json b/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json index 6422788e9..f8c32b0a7 100644 --- a/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json +++ b/TRRandomizerCore/Resources/TR2/Environment/FLOATING.TR2-Environment.json @@ -44,6 +44,28 @@ ] }, + { + "Condition": { + "Comments": "Similary in the cage room, rotate enemy 108 so there is a bit of time to escape after triggering him.", + "ConditionType": 0, + "EntityIndex": 108, + "EntityType": 34 + }, + "OnTrue": [ + { + "EMType": 44, + "EntityIndex": 108, + "TargetLocation": { + "X": 38400, + "Y": 4608, + "Z": 81408, + "Room": 145, + "Angle": 16384 + } + } + ] + }, + { "Condition": { "Comments": "If Marco has been added, make a new room for him and move him to it.", diff --git a/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json b/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json index 177f22c4f..9bc51e4c9 100644 --- a/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json +++ b/TRRandomizerCore/Resources/TR2/Restrictions/enemy_restrictions_special.json @@ -2943,9 +2943,7 @@ ], "1": [ [ 40, 20, 1009, 21 ], - [ 40, 20, 1009, 36 ], - [ 40, 20, 1010, 21 ], - [ 40, 20, 1010, 36 ], + [ 40, 20, 1010, 21 ], [ 40, 20, 38, 21 ], [ 40, 20, 38, 36 ], [ 40, 20, 37, 21 ], diff --git a/TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json b/TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json new file mode 100644 index 000000000..aab1248ae --- /dev/null +++ b/TRRandomizerCore/Resources/TR2/Restrictions/excludable_enemies.json @@ -0,0 +1,44 @@ +{ + "1009": "Barracuda (Tibet)", + "1008": "Barracuda (Underwater Levels)", + "1010": "Barracuda (Temple of Xian)", + "1000": "Tiger (Bengal)", + "46": "Bird Monster", + "27": "Black Moray Eel", + "38": "Crow", + "15": "Doberman", + "47": "Eagle", + "34": "Flamethrower", + "30": "Gunman 1 (Shotgun)", + "31": "Gunman 2 (Rifle)", + "19": "Knifethrower", + "16": "Masked Goon 1 (Jacket)", + "17": "Masked Goon 2 (Waistcoat)", + "18": "Masked Goon 3 (T-Shirt)", + "48": "Mercenary 1 (Grey Trousers)", + "49": "Mercenary 2 (Blue Trousers)", + "50": "Mercenary 3 (Green Trousers)", + "52": "Skidoo Driver", + "53": "Monk (Long Stick)", + "54": "Monk (Knife Stick)", + "21": "Rat", + "29": "Scuba Diver", + "25": "Shark", + "20": "Shotgun Goon", + "1001": "Snow Leopard", + "36": "Spider (Small)", + "37": "Spider (Large)", + "1003": "Stick Goon (Bandana)", + "1004": "Stick Goon (Black Jacket)", + "1005": "Stick Goon (Bodywarmer)", + "1006": "Stick Goon (Green Vest)", + "1007": "Stick Goon (White Vest)", + "33": "Stick Goon (White T-Shirt)", + "214": "T-Rex", + "1002": "Tiger (White)", + "260": "Winston", + "41": "Jade Guardian (Spear)", + "43": "Jade Guardian (Sword)", + "26": "Yellow Moray Eel", + "45": "Yeti" +} \ No newline at end of file diff --git a/TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json b/TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json new file mode 100644 index 000000000..4b8e9720b --- /dev/null +++ b/TRRandomizerCore/Resources/TR3/Restrictions/excludable_enemies.json @@ -0,0 +1,41 @@ +{ + "46": "RX Tech Claw Mutant", + "1000": "Cobra (India)", + "1001": "Cobra (Nevada)", + "34": "Compsognathus", + "42": "RX Tech Crawler Mutant", + "32": "Crocodile", + "27": "Crow", + "65": "Dam Guard", + "41": "Dog (Antarctica)", + "2000": "Dog (London)", + "2001": "Dog (Nevada)", + "25": "Killer Whale", + "35": "Lizard Man", + "56": "London Guard", + "51": "Mercenary (London)", + "37": "Mercenary (South Pacific)", + "71": "Monkey", + "61": "MP (Handgun)", + "63": "MP (MP5)", + "60": "MP (Baton)", + "62": "Prisoner", + "53": "Punk", + "288": "Raptor", + "23": "Rat", + "40": "Gunman (Meteorite Cavern)", + "39": "Gunman (Antarctica)", + "50": "Flamethrower", + "26": "Scuba Diver", + "70": "Shiva", + "28": "Tiger", + "45": "Tinnos Monster", + "44": "Tinnos Wasp", + "73": "Tony", + "20": "Tribesman (Axe)", + "21": "Tribesman (Dart)", + "287": "T-Rex", + "29": "Vulture", + "360": "Winston (Regular)", + "361": "Winston (Camoflauge)" +} \ No newline at end of file diff --git a/TRRandomizerCore/TRRandomizerController.cs b/TRRandomizerCore/TRRandomizerController.cs index 789e0fdf1..1667c475d 100644 --- a/TRRandomizerCore/TRRandomizerController.cs +++ b/TRRandomizerCore/TRRandomizerController.cs @@ -461,6 +461,34 @@ public bool MaximiseDragonAppearance set => LevelRandomizer.MaximiseDragonAppearance = value; } + public bool UseEnemyExclusions + { + get => LevelRandomizer.UseEnemyExclusions; + set => LevelRandomizer.UseEnemyExclusions = value; + } + + public bool ShowExclusionWarnings + { + get => LevelRandomizer.ShowExclusionWarnings; + set => LevelRandomizer.ShowExclusionWarnings = value; + } + + public List ExcludedEnemies + { + get => LevelRandomizer.ExcludedEnemies; + set => LevelRandomizer.ExcludedEnemies = value; + } + + public Dictionary ExcludableEnemies + { + get => LevelRandomizer.ExcludableEnemies; + } + + public List IncludedEnemies + { + get => LevelRandomizer.IncludedEnemies; + } + public bool RandomizeOutfits { get => LevelRandomizer.RandomizeOutfits; diff --git a/TRRandomizerCore/Utilities/LocationGenerator.cs b/TRRandomizerCore/Utilities/LocationGenerator.cs index c06d3cfff..fe3e41624 100644 --- a/TRRandomizerCore/Utilities/LocationGenerator.cs +++ b/TRRandomizerCore/Utilities/LocationGenerator.cs @@ -296,7 +296,7 @@ private bool IsTriggerInvalid(FDTriggerEntry trigger) ); } - private Vector4? GetBestSlantMidpoint(FDSlantEntry slant) + public Vector4? GetBestSlantMidpoint(FDSlantEntry slant) { List corners = new List { 0, 0, 0, 0 }; if (slant.XSlant > 0) diff --git a/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs b/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs index cf0bfef58..04cc8bd79 100644 --- a/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs +++ b/TRRandomizerCore/Utilities/TR2EnemyUtilities.cs @@ -177,6 +177,16 @@ public static int GetRestrictedEnemyLevelCount(TR2Entities entity, RandoDifficul return -1; } + public static int GetRestrictedEnemyTotalTypeCount(RandoDifficulty difficulty) + { + if (difficulty == RandoDifficulty.Default) + { + return _restrictedEnemyLevelCountsDefault.Count; + } + + return _restrictedEnemyLevelCountsTechnical.Count; + } + public static List> GetPermittedCombinations(string lvl, TR2Entities entity, RandoDifficulty difficulty) { if (_specialEnemyCombinations.ContainsKey(lvl) && _specialEnemyCombinations[lvl].ContainsKey(entity)) @@ -284,7 +294,6 @@ public static EnemyDifficulty GetEnemyDifficulty(List enemies) TR2Entities.Barracuda, TR2Entities.BlackMorayEel, TR2Entities.ScubaDiver, TR2Entities.Shark, TR2Entities.YellowMorayEel }, - // #192 The Barkhang/Opera House freeze appears to be caused by dead floating water creatures, so they're all banished [TR2LevelNames.MONASTERY] = new List { @@ -300,7 +309,7 @@ public static EnemyDifficulty GetEnemyDifficulty(List enemies) TR2Entities.BlackMorayEel, TR2Entities.Doberman, TR2Entities.MaskedGoon1, TR2Entities.MaskedGoon2, TR2Entities.MaskedGoon3, TR2Entities.MercSnowmobDriver, TR2Entities.MonkWithKnifeStick, TR2Entities.MonkWithLongStick, TR2Entities.StickWieldingGoon1, - TR2Entities.StickWieldingGoon2, TR2Entities.Winston, TR2Entities.YellowMorayEel + TR2Entities.StickWieldingGoon2, TR2Entities.Winston, TR2Entities.YellowMorayEel, TR2Entities.ShotgunGoon } }; diff --git a/TRRandomizerView/Model/BoolItemIDControlClass.cs b/TRRandomizerView/Model/BoolItemIDControlClass.cs new file mode 100644 index 000000000..0d7394ead --- /dev/null +++ b/TRRandomizerView/Model/BoolItemIDControlClass.cs @@ -0,0 +1,19 @@ +using System; + +namespace TRRandomizerView.Model +{ + public class BoolItemIDControlClass : BoolItemControlClass, ICloneable + { + public int ID { get; set; } + + public BoolItemIDControlClass Clone() + { + return (BoolItemIDControlClass)MemberwiseClone(); + } + + object ICloneable.Clone() + { + return Clone(); + } + } +} \ No newline at end of file diff --git a/TRRandomizerView/Model/ControllerOptions.cs b/TRRandomizerView/Model/ControllerOptions.cs index 00f945f92..200fed505 100644 --- a/TRRandomizerView/Model/ControllerOptions.cs +++ b/TRRandomizerView/Model/ControllerOptions.cs @@ -58,6 +58,8 @@ public class ControllerOptions : INotifyPropertyChanged private bool _vfxWave; private List _secretBoolItemControls, _itemBoolItemControls, _enemyBoolItemControls, _textureBoolItemControls, _audioBoolItemControls, _outfitBoolItemControls, _textBoolItemControls, _startBoolItemControls, _environmentBoolItemControls; + private List _selectableEnemies; + private bool _useEnemyExclusions, _showExclusionWarnings; private RandoDifficulty _randoEnemyDifficulty; private ItemDifficulty _randoItemDifficulty; @@ -1111,6 +1113,36 @@ public List EnemyBoolItemControls } } + public List SelectableEnemyControls + { + get => _selectableEnemies; + set + { + _selectableEnemies = value; + FirePropertyChanged(); + } + } + + public bool UseEnemyExclusions + { + get => _useEnemyExclusions; + set + { + _useEnemyExclusions = value; + FirePropertyChanged(); + } + } + + public bool ShowExclusionWarnings + { + get => _showExclusionWarnings; + set + { + _showExclusionWarnings = value; + FirePropertyChanged(); + } + } + public List TextureBoolItemControls { get => _textureBoolItemControls; @@ -1508,6 +1540,9 @@ public void Load(TRRandomizerController controller) DocileBirdMonsters.Value = _controller.DocileBirdMonsters; MaximiseDragonAppearance.Value = _controller.MaximiseDragonAppearance; RandoEnemyDifficulty = _controller.RandoEnemyDifficulty; + UseEnemyExclusions = _controller.UseEnemyExclusions; + ShowExclusionWarnings = _controller.ShowExclusionWarnings; + LoadEnemyExclusions(); RandomizeSecrets = _controller.RandomizeSecrets; SecretSeed = _controller.SecretSeed; @@ -1564,6 +1599,26 @@ public void Load(TRRandomizerController controller) FireSupportPropertiesChanged(); } + public void LoadEnemyExclusions() + { + SelectableEnemyControls = new List(); + + // Add exclusions based on priority (i.e. order) followed by the remaining included controls + _controller.ExcludedEnemies.ForEach(e => SelectableEnemyControls.Add(new BoolItemIDControlClass + { + ID = e, + Title = _controller.ExcludableEnemies[e], + Value = true + })); + + _controller.IncludedEnemies.ForEach(e => SelectableEnemyControls.Add(new BoolItemIDControlClass + { + ID = e, + Title = _controller.ExcludableEnemies[e], + Value = false + })); + } + public void RandomizeActiveSeeds() { Random rng = new Random(); @@ -1759,6 +1814,12 @@ public void Save() _controller.DocileBirdMonsters = DocileBirdMonsters.Value; _controller.MaximiseDragonAppearance = MaximiseDragonAppearance.Value; _controller.RandoEnemyDifficulty = RandoEnemyDifficulty; + _controller.UseEnemyExclusions = UseEnemyExclusions; + _controller.ShowExclusionWarnings = ShowExclusionWarnings; + + List excludedEnemies = new List(); + SelectableEnemyControls.FindAll(c => c.Value).ForEach(c => excludedEnemies.Add((short)c.ID)); + _controller.ExcludedEnemies = excludedEnemies; _controller.RandomizeSecrets = RandomizeSecrets; _controller.SecretSeed = SecretSeed; diff --git a/TRRandomizerView/TRRandomizerView.csproj b/TRRandomizerView/TRRandomizerView.csproj index 19ebf4b22..72b1afd2f 100644 --- a/TRRandomizerView/TRRandomizerView.csproj +++ b/TRRandomizerView/TRRandomizerView.csproj @@ -97,6 +97,7 @@ + @@ -107,6 +108,9 @@ AdvancedWindow.xaml + + EnemyWindow.xaml + GlobalSeedWindow.xaml @@ -165,6 +169,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/TRRandomizerView/Windows/AdvancedWindow.xaml b/TRRandomizerView/Windows/AdvancedWindow.xaml index 1f977a2bc..4e59fb15a 100644 --- a/TRRandomizerView/Windows/AdvancedWindow.xaml +++ b/TRRandomizerView/Windows/AdvancedWindow.xaml @@ -72,6 +72,8 @@ + + @@ -180,7 +182,7 @@ - @@ -239,8 +241,51 @@ + + + + + + + + + + + + + + + + + + + + + + + +