-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Instead of writing all notes and thoughts in commit message (I know it is a bad practice), I want to set up a wiki to explain some in my code. Just in case someone wants to do something about the code work.
It should be noted, or have already been widely accepted, that Westwood Studios does NOT make a perfect rulesmd.ini (and other ini files). There are many pitfalls that may have or have not discovered by generations of modders. But still, I offer my sincere gratitude and respect to long-gone Westwood Studios.
You already knew what I will say to Electronic Arts, so I skip this part.
Let’s cut the craps and focus on the work.
You may skip this part if you have already been experienced in RA2 modding.
By a glance into rulesmd.ini, we will discover that, this file is organized by sections, like this:
[General]
UIName=Name:General
Name=Red Alert 2 Yuri's Revenge -- Official Rules of Engagement
; veteran factors ;gs checked up on these since Armor was broken. Now they are all multipliers.
VeteranRatio=3.0 ; must destroy this multiple of self-value to become a veteran [per level]
VeteranCombat=1.1 ; multiplier to damage
VeteranSpeed=1.2 ; multiplier to max speed
VeteranSight=0.0 ; multiplier to sight !!!going past ten is a hard code Vegas crash!!!
VeteranArmor=1.5 ; For armor, think of it as max strength being multiplied by the number (in reality, damage is divided by this
VeteranROF=0.6 ; ROF delay multiplier
VeteranCap=2 ; maximum veteran level that can be obtained
InitialVeteran=no ; Do initial forces start as veterans?
Unless mentioned otherwise, I will call General as tag and fields like VeteranRatio as attributes; and of course, anything after semicolon will be comments.
Additionally, some sections are not conventional since they look like this:
[InfantryTypes]
1=E1
2=E2
3=SHK
4=ENGINEER
5=JUMPJET
(I presume) they work as registration list, i.e., all infantries will register in this section and the same applies to other registration list. There are many registration lists, Countries, InfantryTypes, VehicleTypes, AircraftTypes, BuildingTypes, TerrainTypes, SmudgeTypes, OverlayTypes, Animations, VoxelAnims, Particles, ParticleSystems, SuperWeaponTypes, Warheads, AIGenerals and VariableNames, total of 16.
Most of registration list starts from index 1, while some of them starts from index 0. Weapons and Projectiles do not require registration in original YR game; however, certain third-party framework like Ares, may require so. The registration list in original rulesmd.ini is not fully inclusive, i.e., it does not contain all elements. One obvious example is the warhead, [GRIZAPE] of [105mmE] which used as elite primary weapon by Grizzly Tank, or [MTNK]. The warhead is not included in warhead registration list.
And there may exist some mismatches and mistakes in rulesmd.ini. Kirov Airship uses [BlimpBombE] as elite primary weapon, with warhead of [KTSTLEXP]. However, this naming is colliding with an entry in [Animations] and missing from [Warheads]. V3 rocket and dreadnought fire rockets, they use non-conventional logic to do so.
Last, some registration lists need to be present in the file by special order, i.e., they are hard-coded, and their entries are non-modifiable and non-interchangeable. One example is [Colors]; there are many more, but I will not exhaust them here.
This part shows some implementation details for EXAMPLE; actual implementation (in C#) may change by time. For anyone with basic knowledge of C-style programming languages and data structure, codes should be very easy to understand. And of course, I only explain some key details, error checking and string treatments are omitted.
Data is read into following structure:
List<Dictionary<string, Hashtable>> dataSets = new List<Dictionary<string, Hashtable>>();
In above line, dataSets is the data structure holds all information in rulesmd.ini. It can have as many Dictionary<string, Hashtable> as we want, and the number of elements in the List<> is determined by the MAX of a enum type, GlobalProperty.SublistIndex, which represents different categories:
public class GlobalProperty
{
public enum SublistIndex
{
BuildingTypes, // 0, must be at the beginning
InfantryTypes, // 1
VehicleTypes, // 2
AircraftTypes, // 3
Warheads, // 4
DummyTags, // 5
Projectiles, // 6
Weapons, // 7
Uncategorized, // 8, it must be at the end
MAX // 9
}
}
The naming is self-explained, and I will discuss the last several tags, starting from DummyTags.
DummyTags, in short, represents the tag that we do not wish to, or have no ability to modify them by status quo. It includes some non-modifiable registration list, and some lists that I choose to omit them for now. Tags that are categorized as dummy, will not be read into dataSets; instead, these lines will be put into here:
List<string> dummyLinesToWrite = new List<string>();
These lines will be written back to saved file without any change, except comments are removed.
Another fundamental data structure is:
Dictionary<string, GlobalProperty.SublistIndex> tagCategorizedList = new();
This Dictionary established a fundamental category for different tags. It is linked with its corresponding SublistIndex, and this table should always be updated with we add, change, remove certain tag for avoiding unwanted NullPointerException.
The first step is to determine which file we want to read. There are many ways to do so. For debugging convenience, we can define a path inside the program or feed as command line argument; but in actual world, we prefer to use a Open File dialog to achieve that.
After purging all unwanted whitespaces and comments from a line in rules.ini, the immediate step is to detect if it start with [; usually this marks the line contains a tag, and we need do some special treatment. Generally, two situations may occur after the tag line:
- The tag is a registration list and all the lines following (until another tag or EOF is read) can extract a tag, most likely.
- The following lines are attributes list.
Situation 1 is little bit more complicated to deal: we only need to start hashtableCreationMode and start to read tags in this registration list, register them in tagCategorizedList, create entry for it in corresponding Dictionary<> of dataSets.
Situation 2 is considerably easy to deal: find its SublistIndex in tagCategorizedList and start to write attributes; if did not find, create a new entry under SublistIndex.Uncategorized.
As you may have realized or read from the comments in the code, there is a potential bug. This special algorithm will require that all registration lists in rulesmd.ini occurs before its child-tag. But such requirement is unnecessary for rulesmd.ini. This bug can be fixed by introducing more examination when we enter hashtableCreationMode and read from registration list, but at the cost of longer reading time.
Above fix is not introduced in Version 1.2.0 since only some registration lists are actually read; many more registration lists are treated as dummy.
Post-processing of these tags is to correctly categorize weapons and projectiles since they did not require to be registered and will be categorized as Uncategorized. A simple way to detect whether the tag is a weapon or not, is to check if it has Warhead. All weapons will have the attribute otherwise it wont function as expected. When we are checking every item in Uncategorized` list, we will do such inspection.
But Westwood did make some mistakes and tricky implementations inside rulesmd.ini. Some weapons` warheads are missing from warhead registration list, and we need to do some extra work by putting them in the right tags. Also, we want to omit some warhead entry due to certain reason (will document them later).
Here is the least familiar part to me since I have literally none to little experience on GUI programming and WPF.
Three windows are created in this program, MainWindow, CategoryUnitListWindow and AttributesModWindow. MainWindow serves as the primary and entry window for this program. All data belongs to this window. CategoryUnitListWindow will accept a paramenter, SublistIndex from MainWindow, then display the items in this index of dataSets. When double-click an entry here, AttributesModWindow will get a string serve as tag, from its parent and display all atrributes` name and value.
Write-back can be divided in five steps:
- Write all dummy lines back into file.
- Traverse all registration lists that were read, write the registration list first; attributes of entries of registration lists will be put into a separate
List<string> appendixList. This step ensures registration lists occur earlier than everything else. - For registration-free lists, like weapons and projectiles, put into
appendixList. - Write
Uncategorizedentries intoappendixList. - Write
appendixListto file.
appendixList is a place that we store all lines that "will be there, just not now". Traversing List<Dictionary<>> is time-consuming, and we avoid the procedure as much as we can, at a cost of higher memory usage, although CPU performance, RAM size and I/O speed should not be major concern as we are not dealing with mountains of data (original rulesmd.ini only has 31,062 lines).
And well, it means these tricks can be done in more elegant ways.
Whitespaces in original rulesmd.ini occurs in many places:
[CASTL05H]
UIName=Name:CASTL05
Name=Busch Stadium H
TechLevel=-1
Strength=600
Insignificant=yes
Nominal=yes
;RadarInvisible=yes
Points=5
Armor=concrete
Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60
MaxDebris=15
MinDebris=5
DebrisAnim=Dbris3sm,Dbris7sm,Dbris8sm,Dbris10sm,Dbris9lg,Dbris2sm,Dbris3lg
;Selectable=no
;IsBase=no
BaseNormal=no ;psst....IsBase isn't a Rules flag
Sight=6 ; UC base values
ClickRepairable=no
LeaveRubble=yes
After removing comments and skipping empty lies, above section looks like:
[CASTL05H]
UIName=Name:CASTL05
Name=Busch Stadium H
TechLevel=-1
Strength=600
Insignificant=yes
Nominal=yes
Points=5
Armor=concrete
Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60
MaxDebris=15
MinDebris=5
DebrisAnim=Dbris3sm,Dbris7sm,Dbris8sm,Dbris10sm,Dbris9lg,Dbris2sm,Dbris3lg
BaseNormal=no
Sight=6
ClickRepairable=no
LeaveRubble=yes
Pay close attention, there are whitespaces at the end of line in BaseNormal and Sight. This white space will create very unwanted result and must be purged from the string when we write attributes` value, otherwise we may have many unwanted results.
As updated in version 1.2.1, we have a separate List<string> for buildings. Although all buildings are still preserved in dataSets. However, there is an issue that, buildings must be in exact order as it is related with triggers.
Pity is that, Dictionary<> in C#, as documented by MS, does not guarantee to maintain insertion order, as I quote from MSDN:
For purposes of enumeration, each item in the dictionary is treated as a
KeyValuePair<TKey,TValue>structure representing a value and its key. The order in which the items are returned is undefined.
So, there is a need that we have to use a List<string>, since it will guarantee enumeration order:
List<string> buildingList_ordered = new();
Given this change, we have changed the order of SublistIndex, that moves BuildingTypes to the very first one with integer value of 0 as we could avoid some complexity in writing back. Also it may bring more complexity when we try to add or remove entry from buildings' list as we have to change all three places, tagCategorizedList, dataSets and this list.
Some tags, although have been categorized as Uncategorized; however, they represent certain things. I choose to list them here as a reminder.
It looks like a discarded unit from Tiberium Sun.
[UFO]
Name=Scrin Ship
TechLevel=1
Strength=1000
Insignificant=yes
Nominal=yes
;RadarInvisible=yes
Points=5
Armor=concrete
Explosion=TWLT070,S_BANG48,S_BRNL58,S_CLSN58,S_TUMU60
MaxDebris=8
PlaceAnywhere=yes
;IsBase=no
BaseNormal=no ;psst....IsBase isn't a Rules flag
A projectile type, not used by any weapon. It may originally use by [Sapper], the C4 bomb of SEAL and Tanya, however it is replaced by Invisible.
[Sapper]
Damage=2500 ; a boatload (get it?)
ROF=100
Range=1.5
CellRangefinding=yes
Projectile=Invisible;Invisible5
;AntiNaval=yes
;AntiUnderwater=yes
;AntiOrganic=no;to make exception for squid and dolphin
Warhead=Mechanical;gs please do not use the warhead marked "do not use" Super
Report=SealPlaceBomb
SabotageCursor=yes ;gs instead of normal fire cursor to avoid confusion
[Invisible5]
Inviso=yes
Image=none
AA=no
AG=no
;AN=yes
A projectile, presume to be used by Dolphin's Primary and ElitePrimay, but got replaced by Sonic.
[DLPH]
UIName=Name:DLPH
Name=Dolphin
NotHuman=yes
Prerequisite=GAYARD,GATECH
Primary=SonicZap
[SonicZap]
Damage=4
AmbientDamage=10
ROF=120
Range=6
;Projectile=Null
Projectile=Sonic
Speed=100
Warhead=SonicWarhead
Report=DolphinAttack
IsSonic=Yes
DecloakToFire=no
;AntiUnderwater=yes
[Null]
Inviso=yes
Arm=9999999
Image=none
A projectile, not used by any weapon.
[BlimpBombPE]
ShrapnelWeapon=SuperCometFragment
ShrapnelCount=8
Image=ZBOMB
Arm=10
Shadow=no
Acceleration=1
Vertical=yes
DetonationAltitude=20000
A projectile, like AAHeatSeeker2 but with Proximity=yes and SubjectToWalls=yes; not used by any weapon.
[AAHeatSeeker3]
Arm=2
Shadow=no
Proximity=yes
Ranged=yes
AA=yes
AG=yes
Image=DRAGON
ROT=16
SubjectToCliffs=no
SubjectToElevation=no
SubjectToWalls=yes
A warhead, not sued by any weapon. Note: with Mamamia's modified rulesmd.ini, it has already registered as warhead.
[SANoBuilding]
Verses=100%,80%,80%,50%,25%,25%,1%,1%,1%,80%,100%
InfDeath=1
AnimList=PIFFPIFF,PIFFPIFF
Bullets=yes
ProneDamage=70%
A warhead, not used by any weapon.
[OldPsychGasCreate]
CellSpread=1
PercentAtMax=1
Verses=1%,1%,1%,1%,1%,1%,1%,1%,1%,1%,1%
InfDeath=1
Particle=PsychCloudSys
A warhead, although not used by any weapon, but it is present as FlameDamage2=Fire2 under [CombatDamage].
; napalm and fire in general, that doesn't set other things on fire (weird, but necessary)
[Fire2]
CellSpread=.5
PercentAtMax=.5
Wood=yes
Verses=600%,500%,200%,60%,30%,5%,150%,100%,2%,200%,100%
;Verses=600%,148%,59%,6%,2%
InfDeath=4
Sparky=no
Fire=no
ProneDamage=600%
Aapparently it is obsolete since it belongs to Tiberium Sun.
[VEINHOLE]
Name=Veinhole Monster
LegalTarget=yes
RadarColor=92,92,0
Land=Rock
IsVeinholeMonster=true
IsVeins=true
NoUseTileLandType=true