Jump to content

Scripting with UDK


wghost81

Recommended Posts

CheckReplacement is not usable for XCOM because of all the back references. What I finally came to is inserting Mutate call before an object spawn code, pass a bunch of params to it as text (as it only accepts text) and then spawn the new object with mutator depending on these params. After that I add another check in the original code, which confirms if an object is spawned (its reference is not equal to 'none') and if not, executes vanilla code to spawn vanilla class.

Can't say I can follow that. I, too, was curious about mutators a while ago and looked through what documentation you provided about the topic, but still couldn't figure out where to start. Your initial remarks back then about CheckReplacement looked promising initially, but then seemed to go nowhere and weren't mentioned later on, so that left me scratching my head.

 

Looking at your example sources on GitHub didn't help much either. The scripts looked straightforward enough, but there was no indication how and where the MutateXYZ() methods need to be called. Documentation about the procedure is rather spotty currently, a more centralized how-to guide would be appreciated :)

Link to comment
Share on other sites

  • Replies 61
  • Created
  • Last Reply

Top Posters In This Topic

Yes, there aren't that much of tutorials on mutators, I know. :smile: I'll try to think of something, I promise. :smile:

 

I believe I mentioned it somewhere, that the basic difference between UT (the game that mutators were designed for) and XCOM is that XCOM objects have a lot of back references. UT mutators are aimed to replace stand-alone objects in the world, like weapons. When the weapon is created or picked up, CheckReplacement is called and object is re-spawned with mutator. Object's owner gets the new reference and in case of weapon it's everything it needs. Similar methods are implemented for taking damage and entering the level, for example. To make the long story short, UT mutators allow to modify specific things in specific places and those places are inappropriate for XCOM.

 

Let's take a new strategy AI mod I made for Beagle as an example as it's the most recent one. I needed to override a lot of methods in existing XGStrategyAI class and code wise the best way was to make a child class and re-write the code to keep the core things unchanged and overload some of the methods. So I did. But I couldn't just use CheckReplacement to respawn AI class as a stand-alone object in the world, because it is not stand-alone and other objects have references to it as well as it also has tons of back references. I tried it, btw, in one of my earlier attempts and it didn't work. So I ended up modifying XGStrategy.NewGame method:

    if(class'Engine'.static.GetCurrentWorldInfo().Game.BaseMutator != none)
    {
        class'Engine'.static.GetCurrentWorldInfo().Game.BaseMutator.Mutate("XGStrategy.NewGame", class'Engine'.static.GetCurrentWorldInfo().GetALocalPlayerController());
    }
    if(m_kAI == none)
    {
        m_kAI = Spawn(class'XGStrategyAI');
    }

I wrote a base XComMutator class which has Mutate method heavily modified and provides a set of functions for its child mutators to work with. For this example the relevant part of the code looks like:

function Mutate(string MutateString, PlayerController Sender)
{
...
    if (MutateString == "XGStrategy.NewGame")
    {
        MutateStrategyAI(Sender);
    }
..
}

So, if I could, I would just replace XGStrategy.NewGame code with

m_kAI = class'Engine'.static.GetCurrentWorldInfo().Game.BaseMutator.MutateStrategyAI(class'XGStrategyAI');

but since I can't get new references into existing packages, I simply can't do it.

 

MutateStrategyAI is just an empty method in XComMutator waiting to be overloaded by an actual StrategyAI mutator. Note that if mutator is not there or not supposed to mutate StrategyAI, nothing happens and vanilla code is executed.

 

Now here's my XComStrategyAIMutator class for finally replacing default StrategyAI with its modded child:

class XComStrategyAIMutator extends XComMutator;

function MutateStrategyAI(PlayerController Sender)
{
    local XGStrategy Game;
    Game = XComHeadquartersGame(class'Engine'.static.GetCurrentWorldInfo().Game).GetGameCore();
    Game.m_kAI = Spawn(class'XGStrategyAI_Mod');
}

I won't post XGStrategyAI_Mod here as it has over 1000 lines of code, but I hope you got the idea. :smile:

Edited by wghost81
Link to comment
Share on other sites

Hm, that's quite the setup you got there. I'm not sure the example you chose is a good one to demonstrate mutators with though. Since all XComStrategyAIMutator seems to be doing is spawning a strategy AI instance of a different type you could have simply done that right inside XGStrategy.NewGame(), e.g.

    if(m_kAI == none)
    {
        m_kAI = Spawn(class'XGStrategyAI_Mod');
    }

Also your XComMutator setup using a string parameter feels a bit forced, but I suppose you need a way to determine what mutator(s) to actually use when calling Mutate(). From my understanding mutators are daisy-chained, so calling Mutate() on the base mutator will in turn call Mutate() on its designated 'next' mutator and so on, thereby propagating mutation along all mutators.

 

So as Mutate() is essentially called on all mutators and since you only want specific mutators to apply in any given situation, you need to pass some extra information on to mutators that specify whether or not they should actually 'fire'.

 

I guess I'm with you on that front, but as a programmer I kinda disagree on how you structured XComMutator using a bunch of string checks passing implementation to a method stub that is supposed to be overridden with an actual implementation in a subclass. What you created is essentially an abstract type, but you're defining too many quasi-abstract methods for my taste. As I see it this makes the class very rigid as you'll have to add more cases as you need them, providing ever more cover methods.

 

There's several ways to go about that. I'd suggest to embrace the abstract type concept and have sub-classes deal with only one or two generic methods to implement at a time. Unfortunately it seems UnrealScript only allows classes to be abstract and not methods, though at least this allows you to provide default implementations for your quasi-abstract methods.

 

Suppose your XComMutator base class would look like this:

 

class XComMutator extends Mutator
	abstract;

function Mutate(string MutateString, PlayerController Sender)
{
	if (this.ShouldFire(MutateString))
	{
		this.MutateImpl(Sender);
	}
	Log("XComMutator: Current = " $ string(Name));
	if (NextMutator != none)
	{
		Log("XComMutator: Next = " $ string(NextMutator.Name));
	}
	else
	{
		Log("XComMutator: Next = None");
	}
	// never forget to call for super.Mutate from inside subclass of XComMutator class
	// if you do, you'll stop Mutate propagation along the chain of mutators
	super.Mutate(MutateString, Sender);
}

// subclasses need to override this

function bool ShouldFire(string MutateString)
{
	return true;
}

function MutateImpl(PlayerConroller Sender)
{
	// implementation goes here
}

 

Then you could provide XComMutator subclasses that only need to implement all of two methods and be done with it. Each mutator would deal with input checking on its own.

 

This way it would also be possible to decouple input checking and implementation by putting those in different classes. So, for instance, if I would like to execute all mutators of a certain family of mutator types I could provide yet another abstract intermediate class that implements ShouldFire() using a type check and leave the implementation to the corresponding type instances, like so:

 

class XComTypeMutator extends XComMutator
	abstract;
	
function bool ShouldFire(string MutateString)
{
	// we expect MutateString to be a type name
	return this.IsA(MutateString);
}

and

class XComAIMutator extends XComTypeMutator;

function MutateImpl(PlayerConroller Sender)
{
	// do AI stuff here
}

fire by calling Mutate("XComAIMutator", Sender)

 

 

 

The type-checking implementation given as an example here could also be used as the default implementation inside the base XComMutator class if you find it workable enough. But as I see you're also doing some other string processing on the inputs to extract additional parameters in XComMutator this might not be desired :smile: In this case I'd furthermore suggest to standardize input processing and relocate it to a separate method. A further modified XComMutator base class could then have the following alterations, for example:

 

function Mutate(string MutateString, PlayerController Sender)
{
	if (this.ShouldFire(MutateString))
	{
		this.MutateImpl(Sender, this.ExtractParameters(MutateString));
	}
	...
}

...

function array<Object> ExtractParameters(string MutateString)
{
	// default implementation does nothing, override as needed
	return null;
}

function MutateImpl(PlayerConroller Sender, optional array<Object> params)
{
	// implementation goes here
}

 

 

 

Again, this would allow parameter extraction to be separated from the other parts and could be handled in a unified fashion for specific type families, for example, or you could provide a default implementation that breaks apart the input string along a specific delimiter, for instance.

 

Okay, I think that's enough rambling for now, hope you don't mind the criticism :smile:

Edited by XMarksTheSpot
Link to comment
Share on other sites

I don't mind the criticism at all, but keep in mind that there are a lot of things I cannot change. :wink:

 

So, let's go deeper into this situation.

 

I can't just call spawn with a new class, because I need an import reference to class'XGStrategyAI_Mod', which is not present in the XComStrategyGame.upk package. Things look a little simpler from unrealscript side then they do from bytecode side. :smile: And this is the most basic problem with getting new classes to work: we need references that are not there.

 

Second problem is related to the fact that BaseMutator is of Mutator class which is defined in Engine.upk. So we can't just create a new child with all the new methods we need and call these methods instead of Mutate or any of the old ones, because to access new methods we need to cast BaseMutator to our child class and for that we need the damn reference which is not there.

 

I hope now you can understand my frustration. :smile: I need method to pass some params and return some values, but I don't have one and I can't create one. The only suitable method is Mutate, because I can use string param to pass some values like "XGUnit.RecordKill"$Name$kVictim.Name. And sometimes I need to invent some convoluted methods to notify the original script about mutator code being successfully executed. Spawning an object is the easiest case, as I can check for null pointer.

 

Also, the original code should be safe and should not CTD if mutator is not present.

 

Everything else looks like the matter of optimization to me. :wink: But I have a rule here: if code is not slowing down the system considerably, I have no need to care about optimization. :smile: Probably, I'm just a lazy person. :smile: The way it works now, Mutate is subsequently called for all the mutators, performs about 20 if-else checks and successfully does nothing if some mutator is not supposed to process some call. Plus it is called from like 20 different places, but, thankfully, not at the same time. :smile: To be honest, the idea is stolen from UT mutators, only difference is that instead of 20 calls for the same method it has 20 calls for 20 different methods. But mutators were never meant to be big and heavy, they meant to do one small thing just once. I tried to keep this approach, at least with latest mods.

 

BTW, original mutator class has category, relevance and active/inactive checks, so my version is surely less complicated in that respect. :smile:

Edited by wghost81
Link to comment
Share on other sites

PS I got the idea about not turning XComMutator into one big and heavy interface and I will think about it. I like it as it is now because it hides this ugly implementation and allows me to focus on the task with a child mutator. But I can see how this can be inconvenient for some users/cases.

Link to comment
Share on other sites

Amineri just discovered an interesting thing, which probably was preventing us from getting import references in: there can be no circular dependencies between game packages. So, XComGame has no references to XComStrategyGame, because XComStrategyGame is loaded after XComGame package. I tested it with UDK and it seems to be true. So, all new packages are loaded after existing ones and we are able to reference existing classes. But this makes it impossible to reference our new packages from the old ones.

Edited by wghost81
Link to comment
Share on other sites

Have a few additional ideas on how we might be able to load objects.

 

First off, when Spawning objects it's possible to pass in the return value of a function instead of a constant. A couple of examples :

 

1) Vanilla code in XGPlayer.InitBehavior :

        kUnit.m_kBehavior = Spawn(GetBehaviorClass(kUnit.GetCharacter().m_eType));

Note that GetBehaviorClass is structured as :

simulated function class<XGAIBehavior> GetBehaviorClass(XGGameData.EPawnType eAlienType)

it returns class<XGAIBehavior>, which is not metacast to a child class.

 

For the parent XGPlayer, GetBehaviorClass simply returns class'XGAIBehavior'; -- this is used when the AI controls XCOM units, for example, via mind control.

 

XGAIPlayer extends this to return AlienTypeToBehaviorClass, which is an big case statement :

 

 

 

    switch(eAlienType)
    {
        case 30:
            return class'XGAIBehavior_Sectoid';
        case 31:
            return class'XGAIBehavior_SectoidCommander';
        case 32:
            return class'XGAIBehavior_Floater';
        case 33:
            return class'XGAIBehavior_FloaterHeavy';
        case 34:
            return class'XGAIBehavior_Muton';
...

 

 

 

Note that this sort of construction spawns the correct child class of XGAIBehavior, even though the return value is of class<XGAIBehavior> (the parent class), and there is no metacasting done before being passed to Spawn(...)

 

---------------

 

2) Long War modded code in XGLoadoutMgr.ApplyLoadout :

    kItem = class'Engine'.static.GetCurrentWorldInfo().Spawn(class<XGWeapon>(class'XGItemLibrary'.static.GetItem(kLoad.Items[I])), kUnit.Owner);

The XGItemLibrary.GetItem function is similar to AlienTypeToBehaviorClass, but returns an array element as indexed by the item enum :

 

 

 

m_arrItems(1)=class'XGWeapon_LaserAssaultGun'
m_arrItems(2)=class'XGWeapon_Pistol'
m_arrItems(3)=class'XGWeapon_AssaultRifle'
m_arrItems(4)=class'XGWeapon_Shotgun'
m_arrItems(5)=class'XGWeapon_Minigun'
m_arrItems(6)=class'XGWeapon_SniperRifle'
m_arrItems(7)=class'XGWeapon_RocketLauncher'
m_arrItems(8)=class'XGWeapon_LaserPistol'
m_arrItems(9)=class'XGWeapon_LaserAssaultRifle'
m_arrItems(10)=class'XGWeapon_LaserAssaultGun'
m_arrItems(11)=class'XGWeapon_HeavyLaser'
....

 

 

 

GetItem has as return value class<XGItem>. Note that even though it is cast to class<XGWeapon> before being passed into Spawn(...), the correct child class (e.g. class<XGWeapon_MEC_FlameThrower>) is spawned. This is important in the current game because the flamethrower subclass has an important defaultproperties defined :

    m_eEquipLocation=ELocation.eSlot_Flamethrower

If the child class weren't being spawned, the item would not attach to the MEC pawn in the correct location.

 

--------------------

 

The above described how we can Spawn objects a bit dynamically based on lists of class types, but in both cases the lists of class types are already present in the package.

 

However, it is possible to dynamically load a class type. For example, in XComGameInfo.CacheMods :

    ModClass = class<XComMod>(DynamicLoadObject(ModName, class'Class'));

A similar method is used by wghost's Mutator "mutation" in Engine.upk in GameInfo.AddMutator :

    mutClass = class<Mutator>(DynamicLoadObject(mutname, class'Class'));

This code allows us to specify a class dynamically at run-time via a string reference.

 

----------------------

 

Putting this together, it should be possible to load a spawn a class dynamically with the following construction :

Spawn(class<Actor>(DynamicLoadObject("NewClassPackage.NewClassName", class'Class')), [Owner]);

The string with the Package.Class could indeed be set to be configured via an ini file instead of hard-coded into hex. Note that nothing above should prevent it from being inserted into XComGame.upk or XComStrategyGame.upk

Edited by Amineri
Link to comment
Share on other sites

The idea with DynamicLoadObject looks good. This way is it possible to do without mutators at least when we need to spawn modded child class instead of original parent class. INI thing is problematic for existing packages, though, as we'll need to re-purpose some of the existing ini variables to hold class names. And with mutator loader class we have our own fully customizable config file. ;)

 

So, there are pros and cons to both methods. Fully mutator based method allows to change hex code just once and then do all the modding with unrealscript and UDK. But the drawback of this method is indirect access to objects and functions being passes with Mutate and its string param. DynamicLoadObject allows to bypass mutators and spawn modded child class directly. However, it isn't suitable for the case when we need to insert new functionality into existing method without creating a new class, as it won't allow to dynamically load new functions. Still, we have some more options now, which no doubt is a good thing. :)

Link to comment
Share on other sites

Yes, but class can only access one specific ini which is set as class parameter. Well, actually child classes can have different ini and still access parent's ini, but still it doesn't give us direct access to MP ini variables. Unless they are static or const, if I'm not mistaken.

 

Actually, that's why I completely moved to mutators. Despite all the ugliness of the linking code in Mutate, it allows to simply clone hex part for different functions and do everything I want with unrealscipt and with no limitations. Kinda made me lazy, but allowed to create some really complex things. :smile:

Edited by wghost81
Link to comment
Share on other sites

  • Recently Browsing   0 members

    • No registered users viewing this page.

×
×
  • Create New...