I can't use Unreal Script (I wish I could) so any suggestions are welcome.
Thanks!
Begin Object Class=UTSeqEvent_VehicleFactory Name=UTSeqEvent_VehicleFactory_1 Archetype=UTSeqEvent_VehicleFactory'UTGame.Default__UTSeqEvent_VehicleFactory' Originator=UTVehicleFactory_Goliath'UTVehicleFactory_Goliath_1' MaxWidth=346 OutputLinks(0)=(Links=((LinkedOp=SeqAct_GetProperty'SeqAct_GetProperty_1'),(LinkedOp=SeqAct_Gate'SeqAct_Gate_1',InputLinkIdx=1)),DrawY=715) OutputLinks(1)=(DrawY=737) OutputLinks(2)=(DrawY=759) OutputLinks(3)=(DrawY=781) OutputLinks(4)=(DrawY=803) VariableLinks(0)=(LinkedVariables=(SeqVar_Object'SeqVar_Object_15'),DrawX=5060) ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=4887 ObjPosY=645 ObjName="UTVehicleFactory_Goliath_1 Vehicle Factory Event" DrawWidth=230 DrawHeight=240 Name="UTSeqEvent_VehicleFactory_1" ObjectArchetype=UTSeqEvent_VehicleFactory'UTGame.Default__UTSeqEvent_VehicleFactory' End Object Begin Object Class=UTSeqAct_HealDamage Name=UTSeqAct_HealDamage_2 Archetype=UTSeqAct_HealDamage'UTGame.Default__UTSeqAct_HealDamage' bSuperHeal=True HealAmount=300 InputLinks(0)=(DrawY=707) OutputLinks(0)=(DrawY=707) VariableLinks(0)=(LinkedVariables=(SeqVar_Object'SeqVar_Object_15'),DrawX=6054) VariableLinks(1)=(bHidden=True,DrawX=3281) VariableLinks(2)=(LinkedVariables=(SeqVar_Object'SeqVar_Object_16'),DrawX=6124) ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=6016 ObjPosY=673 DrawWidth=156 DrawHeight=61 Name="UTSeqAct_HealDamage_2" ObjectArchetype=UTSeqAct_HealDamage'UTGame.Default__UTSeqAct_HealDamage' End Object Begin Object Class=SeqAct_GetProperty Name=SeqAct_GetProperty_1 Archetype=SeqAct_GetProperty'Engine.Default__SeqAct_GetProperty' PropertyName="Health" InputLinks(0)=(DrawY=812) OutputLinks(0)=(Links=((LinkedOp=SeqCond_CompareInt'SeqCond_CompareInt_1')),DrawY=812) VariableLinks(0)=(LinkedVariables=(SeqVar_Object'SeqVar_Object_15'),DrawX=5331) VariableLinks(1)=(DrawX=5391) VariableLinks(2)=(LinkedVariables=(SeqVar_Int'SeqVar_Int_6'),DrawX=5438) VariableLinks(3)=(DrawX=5480) VariableLinks(4)=(DrawX=5532) VariableLinks(5)=(DrawX=5581) ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=5293 ObjPosY=778 DrawWidth=318 DrawHeight=61 Name="SeqAct_GetProperty_1" ObjectArchetype=SeqAct_GetProperty'Engine.Default__SeqAct_GetProperty' End Object Begin Object Class=SeqCond_CompareInt Name=SeqCond_CompareInt_1 Archetype=SeqCond_CompareInt'Engine.Default__SeqCond_CompareInt' ValueB=600 InputLinks(0)=(DrawY=650) OutputLinks(0)=(DrawY=606) OutputLinks(1)=(DrawY=628) OutputLinks(2)=(DrawY=650) OutputLinks(3)=(Links=((LinkedOp=SeqAct_Gate'SeqAct_Gate_1')),DrawY=672) OutputLinks(4)=(DrawY=694) VariableLinks(0)=(LinkedVariables=(SeqVar_Int'SeqVar_Int_6'),DrawX=5780) VariableLinks(1)=(DrawX=5805) ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=5747 ObjPosY=568 DrawWidth=91 DrawHeight=157 Name="SeqCond_CompareInt_1" ObjectArchetype=SeqCond_CompareInt'Engine.Default__SeqCond_CompareInt' End Object Begin Object Class=SeqVar_Object Name=SeqVar_Object_15 Archetype=SeqVar_Object'Engine.Default__SeqVar_Object' ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=5357 ObjPosY=1168 DrawWidth=32 DrawHeight=32 Name="SeqVar_Object_15" ObjectArchetype=SeqVar_Object'Engine.Default__SeqVar_Object' End Object Begin Object Class=SeqVar_Int Name=SeqVar_Int_6 Archetype=SeqVar_Int'Engine.Default__SeqVar_Int' IntValue=900 ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=5534 ObjPosY=1023 DrawWidth=32 DrawHeight=32 Name="SeqVar_Int_6" ObjectArchetype=SeqVar_Int'Engine.Default__SeqVar_Int' End Object Begin Object Class=SeqEvent_PlayerSpawned Name=SeqEvent_PlayerSpawned_1 Archetype=SeqEvent_PlayerSpawned'Engine.Default__SeqEvent_PlayerSpawned' MaxWidth=156 OutputLinks(0)=(DrawY=-164,bHidden=True) VariableLinks(0)=(LinkedVariables=(SeqVar_Object'SeqVar_Object_16'),DrawX=5968) VariableLinks(1)=(DrawX=6038) ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=5920 ObjPosY=962 DrawWidth=86 DrawHeight=128 Name="SeqEvent_PlayerSpawned_1" ObjectArchetype=SeqEvent_PlayerSpawned'Engine.Default__SeqEvent_PlayerSpawned' End Object Begin Object Class=SeqVar_Object Name=SeqVar_Object_16 Archetype=SeqVar_Object'Engine.Default__SeqVar_Object' ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=6244 ObjPosY=1138 DrawWidth=32 DrawHeight=32 Name="SeqVar_Object_16" ObjectArchetype=SeqVar_Object'Engine.Default__SeqVar_Object' End Object Begin Object Class=SeqAct_Gate Name=SeqAct_Gate_1 Archetype=SeqAct_Gate'Engine.Default__SeqAct_Gate' AutoCloseCount=1 InputLinks(0)=(DrawY=675) InputLinks(1)=(DrawY=697) InputLinks(2)=(DrawY=719) InputLinks(3)=(DrawY=741) OutputLinks(0)=(Links=((LinkedOp=UTSeqAct_HealDamage'UTSeqAct_HealDamage_2')),DrawY=708) ObjInstanceVersion=1 ParentSequence=Sequence'Main_Sequence' ObjPosX=5876 ObjPosY=638 DrawWidth=82 DrawHeight=117 Name="SeqAct_Gate_1" ObjectArchetype=SeqAct_Gate'Engine.Default__SeqAct_Gate' End Object
//============================================================================= // EmbeddedMutator. // *** for use as an Embedded or Conventional Mutator *** // Replaces the Player's Enforcer with the Chainsaw or other weapon // An Embedded Mutator for placing INSIDE a UT Map or // can also be used in the conventional manner. // Author: Dawn, with much assistance from Norbert Bogenrieder aka Beppo // Tested only on version 4.36 // Version 1.0 06/24/2002 10:19:56 AM //============================================================================= // To embed this Mutator in a Map: // 1. Make sure this Mutator is NOT listed in the "EditPackages" section of your // UnrealTournament.ini file! // 2. Make sure "EmbeddedMutator.u" IS located in your UnrealTournament/System // Folder. // 3. Start a fresh UED session and open your Map for editing. // 4. In the Command entry field at the bottom of the UED window type: // // obj load file=EmbeddedMutator.u package=MyLevel // // 5. Open the Actor class Browser and scroll down to find // // Actor->Info->Mutator->EmbeddedMutator // // 6. Highlight "EmbeddedMutator" in the browser and then select a surface // somewhere in your map, right-click on the surface, and in the pop-up // menu that appears, choose "Add EmbeddedMutator Here". Place only ONE // instance of this Mutator in your Map. // 7. The resulting Actor that is placed in your map can be double-clicked on // to bring up the "EmbeddedMutator Properties" browser where you can select // the Enforcer Replacement weapon, the Bot Rating, and more. // 8. Build and Save your map - the embedded Mutator code with your settings // will be saved away in the map. // // If you subsequently delete the single instance of the Mutator from your map, // and then reBuild and reSave, the Mutator code will be deleted from the map // and you will have to repeat all these steps to embed it again. //============================================================================= class EmbeddedMutator extends Mutator config(EmbeddedMutator); var bool bPreBPInitialized, bPostBPInitialized; var float BotFactor; // Relative damage that Bots inflict compared to Players var string TheReplacement; // Weapon that will replace Enforcer var DeathMatchPlus DM; enum EBotRating // Damage that Bots give Players { Poor, // quarter damage Fair, // half the damage Good, //three quarter damage EqualToPlayers // full damage }; enum ERWeaponType // Replacement Weapon type. { Chainsaw, Enforcer, DoubleEnforcer, ImpactHammer, Minigun2, PulseGun, Ripper, ShockRifle, SuperShockRifle, SniperRifle, UT_BioRifle, UT_Eightball, UT_FlakCannon, Redeemer }; // Variables which the Level designer can adjust: var() config bool bDebug; // Enable some logging to the log file var() config ERWeaponType EnforcerReplacement; // accessible to Level Designer var() config EBotRating BotRating; // accessible to Level Designer var() config bool bUseBotRatingOnMeleeWeapsOnly; // Which Weapons will Bots inflict less damage with // PreBeginPlay will be used mainly to Bootstrap load this Mutator by linking it // into the Mutator List as the 1st Mutator in a possible chain of Mutators. function PreBeginPlay() { local int i; local Mutator M, Previous, Temp; if (bDebug) { Log("**** Entered EmbeddedMutator PreBeginPlay"); Log("**** And bPreBPInitialized is "$string(bPreBPInitialized)); } if ( !bPreBPInitialized ) // older versions of UT call this function twice but we // only want to run our code once! { bPreBPInitialized = True; // Add the mutator by linking it into the Mutator List ala Beppo. // this Embedded Mutator makes sense to be 1st after the BaseMutator // in the Mutator List... // But first, check for and destroy an already loaded version for (M = Level.Game.BaseMutator; M != None; M = M.NextMutator) { if (GetItemName(string(M.class)) == GetItemName(string(Self.class))) // Found Self or a copy? { Previous.NextMutator = M.NextMutator; if (M != Self) // This is the other version { // Unlink from DamageMutator List BEFORE destroying this version. We // would need to call a similar function to UnLink from MessageMutator // and HUDMutator Lists if we were registered as them. UnLinkDamageMutator(M); // do this BEFORE M.Destroy()! M.Destroy(); } } else // this will happen at least the 1st time thru the For Loop. { Previous = M; // So Previous can now be used as the Previous Mutator // in the next pass thru the for Loop. } } // We need to Bootstrap this instance into the List Self.NextMutator = Level.Game.BaseMutator.NextMutator; // Make a place in the List Level.Game.BaseMutator.NextMutator = Self; // place it 1st after BaseMutator if (bDebug) Log("**** Mutator "$string(Self)$" is added to the list"); // Now register our self as a DamageMutator. A similar call would be made to // RegisterMessageMutator and RegisterHUDMutator if required. Level.Game.RegisterDamageMutator(Self); BotFactor = GetBotRating(); // How much will we modify Bot-inflicted damage? TheReplacement = GetRWeaponType(); //What's the Enforcer replacement weapon? DM = DeathMatchPlus(Level.Game); if (bDebug) { LogMutatorInfo(); Log("**** Leaving EmbeddedMutator PreBeginPlay"); } } } // End PreBeginPlay: Our Mutator is linked into the Mutator List // Write some informative stuff to the log function LogMutatorInfo() { local Mutator M; Log("**** Current MutatorList:"); for (M = Level.Game.BaseMutator; M != None; M = M.NextMutator) { Log("**** "$string(M.class)); } Log("**** Current DamageMutatorList:"); for (M = Level.Game.DamageMutator; M != none; M = M.NextDamageMutator) { Log("**** "$string(M.Class)); } } // Finding a copy of this Mutator and destroying it in our PreBeginPlay messes // up the DamageMutator Linked List since we're registered as a DamageMutator, // so this function removes our Mutator from the List. We would need similar // functions for UnLinking from MessageMutator and HUDMutator Lists if we were // registered as them. function UnLinkDamageMutator(Mutator M) { local bool bNotFirst; local Mutator C, Previous; for (C = Level.Game.DamageMutator; C != none; C = C.NextDamageMutator) { if (C == M) { break; } bNotFirst = True; Previous = C; } if (bNotFirst) { Previous.NextDamageMutator = C.NextDamageMutator; } else { Level.Game.DamageMutator = Level.Game.DamageMutator.NextDamageMutator; } } //============================================================================== // We already added ourselves to the Mutator List in our PreBeginPlay, but // GameInfo.InitGame() will erroneously add us again unless we intercept the // call with our own AddMutator function and prevent it. function AddMutator(Mutator M) { if (M == Self) { return; // Don't add us. } super.AddMutator(M); // keep the chain unbroken } function float GetBotRating() { switch( BotRating ) // accessible to Level Designer { case Poor: return 0.25; case Fair: return 0.5; case Good: return 0.75; case EqualToPlayers: return 1.0; default: return 0.5; } } function string GetRWeaponType() { switch( EnforcerReplacement ) // accessible to Level Designer { case Chainsaw: return "Botpack.Chainsaw"; case Enforcer: return "Botpack.Enforcer"; case DoubleEnforcer: return "Botpack.DoubleEnforcer"; case ImpactHammer: return "Botpack.ImpactHammer"; case Minigun2: return "Botpack.Minigun2"; case PulseGun: return "Botpack.PulseGun"; case Ripper: return "Botpack.Ripper"; case ShockRifle: return "Botpack.ShockRifle"; case SuperShockRifle: return "Botpack.SuperShockRifle"; case SniperRifle: return "Botpack.SniperRifle"; case UT_BioRifle: return "Botpack.UT_BioRifle"; case UT_Eightball: return "Botpack.UT_Eightball"; case UT_FlakCannon: return "Botpack.UT_FlakCannon"; case Redeemer: return "Botpack.WarheadLauncher"; default: return "Botpack.Chainsaw"; } } // Called just before PlayerPawn is Spawned. We use it to adjust a Player's // Inventory to our liking function ModifyPlayer( Pawn PlayerPawn ) { local Inventory MyEnforcer; MyEnforcer = PlayerPawn.FindInventoryType(class'Enforcer'); if (MyEnforcer != None) { MyEnforcer.DropInventory(); // Remove it from Player's Inventory MyEnforcer.Destroy(); // Remove it from the game } if ( DM == None ) // Who knows, it might happen ... return; DM.GiveWeapon(PlayerPawn,TheReplacement); // Give Replacement for Enforcer if ( NextMutator != None ) NextMutator.ModifyPlayer(PlayerPawn); } // We call MutatorTakeDamage because we have found that Bots over-excel in // Melee-only weapons maps. We want to reduce the damage Bots inflict by a // controlled amount. function MutatorTakeDamage( out int ActualDamage, Pawn Victim, Pawn InstigatedBy, out Vector HitLocation, out Vector Momentum, name DamageType) { // Check that InstigatedBy is still a valid Bot - Bots can disappear between // the time this function is called and the time it is run! if ( InstigatedBy != none && InstigatedBy.IsA('Bot') ) { if (bUseBotRatingOnMeleeWeapsOnly) { if (InstigatedBy.Weapon != None && InstigatedBy.Weapon.bMeleeWeapon) { AlterDamage(ActualDamage); if (bDebug) Log("**** Melee Only Damage adjusted for Bot's "$GetItemName(string(InstigatedBy.Weapon.class))); } } else { AlterDamage(ActualDamage); if (bDebug) Log("**** Damage adjusted for Bot's "$GetItemName(string(InstigatedBy.Weapon.class))); } } // Give any other DamageMutators in the chain their chance at it if ( NextDamageMutator != None ) { if (bDebug) Log("**** Another DamageMutator is being called!"); NextDamageMutator.MutatorTakeDamage( ActualDamage, Victim, InstigatedBy, HitLocation, Momentum, DamageType ); } } function bool AlterDamage(out int ActualDamage) { if (bDebug) Log("Potential Bot-inflicted Damage: "$ActualDamage$" "$BotFactor); ActualDamage*=BotFactor; // Adjust the Bot's Damage if (bDebug) { Log("***Actual Bot-inflicted Damage: "$ActualDamage); Log(""); } return true; } // End: Players get a Replacement for Enforcer when Spawning // and Bots inflict less Damage if desired. defaultproperties { bDebug=False BotRating=Fair // An empirically determined default for Chainsaw Matches bUseBotRatingOnMeleeWeapsOnly=True EnforcerReplacement="Botpack.Chainsaw" }
ignore the replacing flakcannon content, this isnt finished....
class EmbedMutator extends UTMutator placeable; /* //PreBeginPlay() or PostBeginPlay() function in the Mutator will be run automatically // before the game begins and can be used to perform the necessary task. //Here is sample code to accomplish this: var bool bPreBPInitialized; function PreBeginPlay() { local Mutator M, Previous, Temp; if ( !bPreBPInitialized ) // older versions of UT call this function twice but we // only want to run our code once! { bPreBPInitialized = True; // Add the mutator by linking it into the Mutator List ala Beppo. // This Embedded Mutator makes sense to be 1st after the BaseMutator // in the Mutator List... for (M = Level.Game.BaseMutator; M != None; M = M.NextMutator) { if (GetItemName(string(M.class)) == GetItemName(string(Self.class))) //Found Self or a copy? { Previous.NextMutator = M.NextMutator; // Unlink from the Mutator List if (M != Self) // Not Self - this is a copy! { // Unlink from DamageMutator List BEFORE destroying this version. We // would need to call a similar function to UnLink from MessageMutator // and HUDMutator Lists if we were registered as them. UnLinkDamageMutator(M); // do this BEFORE M.Destroy()! M.Destroy(); } } else // this will happen at least the 1st time thru the For Loop. { Previous = M; // So Previous can now be used as the Previous Mutator // in the next pass thru the for Loop. } } Self.NextMutator = Level.Game.BaseMutator.NextMutator; // Make a place in the List Level.Game.BaseMutator.NextMutator = Self; // place it 1st after BaseMutator // Here's an example that illustrates how a Mutator might also register itself as a // special Mutator such as a DamageMutator: // A similar call would be made to // RegisterMessageMutator and RegisterHUDMutator if required. Level.Game.RegisterDamageMutator(Self); } } // End PreBeginPlay. //================================================================= // Finding a copy of this Mutator and destroying it in our PreBeginPlay messes // up the DamageMutator Linked List since we're registered as a DamageMutator, // so this function removes our Mutator from the List. We would need similar // functions for UnLinking from MessageMutator and HUDMutator Lists if we were // registered as them. function UnLinkDamageMutator(Mutator M) { local bool bNotFirst; local Mutator C, Previous; for (C = Level.Game.DamageMutator; C != none; C = C.NextDamageMutator) { if (C == M) { break; } bNotFirst = True; Previous = C; } if (bNotFirst) { Previous.NextDamageMutator = C.NextDamageMutator; } else { Level.Game.DamageMutator = Level.Game.DamageMutator.NextDamageMutator; } } [2] The last two lines do all the work of inserting the embedded Mutator into the Mutator Linked List just after the built-in, or base, Mutator referred to as BaseMutator. Any other Mutators already linked into the list are shoved back to make room for the embedded Mutator. Adding this code to any Mutator will allow it to be embedded in a map and subsequently run and registered when the map is played. [2] The for loop steps through the current list of Mutators, looking for Mutators of the same class, and if any are found, a check is made to see if they are copies. GetItemName() is used to remove the Parent Class from the class name because copies of the same class can have different Parent Classes. If a copy is found, the Mutator Linked List is adjusted to unregister the copy and then the copy is destroyed with a call to Destroy(). Only the last instance of the Mutator that runs this code will survive and reregister itself. [3] The call to the new function, UnLinkDamageMutator(), must be made before the Mutator copy is destroyed because a reference to M, the copy, is passed to the new function. The Linked Lists for Damage, Message, and HUD Mutators are linked in reverse order so there is no "Base" Mutator in these lists. In this example, Level.Game.DamageMutator points to the last DamageMutator to be linked to the list instead of the first, so it's rather dynamical and the new function contains code to deal with this. MessageMutators and HUDMutators would require similar new functions to handle their own types. */ /* There is a situation where the Mutator's PreBeginPlay() will run and AddMutator() can be called on it. In that case, the Mutator would be registered by it's own PreBeginPlay() and then again by GameInfo.InitGame(). Although only one copy of the code exists, it will be registered twice and all Mutator function calls will get passed to this Mutator twice. The following function redefines AddMutator() and checks if the call is to this Mutator. If so, no registration is performed, otherwise super.AddMutator() performs the normal registration: // We already added ourselves to the Mutator List in our PreBeginPlay, but // GameInfo.InitGame() will erroneously add us again unless we intercept the // call with our own AddMutator function and prevent it. function AddMutator(Mutator M) { if (M == Self) { return; // Don't add us. } super.AddMutator(M); // keep the chain unbroken } */ function InitMutator(string Options, out string ErrorMessage) { Super.InitMutator(Options, ErrorMessage); } function bool CheckReplacement(Actor Other) { local UTWeaponPickupFactory WeaponPickup; local UTWeaponLocker Locker; local UTAmmoPickupFactory AmmoPickup; local int i; local class<UTAmmoPickupFactory> NewAmmoClass; WeaponPickup = UTWeaponPickupFactory(Other); if (WeaponPickup != None) { if (WeaponPickup.WeaponPickupClass == class'UTWeap_FlakCannon') { WeaponPickup.WeaponPickupClass = class<UTWeapon>(DynamicLoadObject("UT2004_FlakCannon.UTWeap_UT2004FlakCannon", class'Class')); WeaponPickup.InitializePickup(); Return True; } } Locker = UTWeaponLocker(Other); if (Locker != None) { for (i = 0; i < Locker.Weapons.length; i++) { if (Locker.Weapons[i].WeaponClass == class'UTWeap_FlakCannon') { Locker.ReplaceWeapon(i, class<UTWeapon>(DynamicLoadObject("UT2004_FlakCannon.UTWeap_UT2004FlakCannon", class'Class'))); return true; } } } AmmoPickup = UTAmmoPickupFactory(Other); if (AmmoPickup != None) { if(AmmoPickup.Class.Name == 'UTAmmo_FlakCannon') { NewAmmoClass = class<UTAmmoPickupFactory>(DynamicLoadObject("UT2004_FlakCannon.UT2004FlakCannonAmmo", class'Class')); AmmoPickup.TransformAmmoType(NewAmmoClass); return true; } } return true; } defaultproperties { }
[EmbeddedMutator.EmbeddedMutator] bDebug=False BotRating=Fair bUseBotRatingOnMeleeWeapsOnly=True EnforcerReplacement="Botpack.Chainsaw"
Once an embedded Mutator has been properly written, it can be compiled in the normal fashion to create a <MutatorName>.u file (replace <MutatorName> with actual file name) that can be added to a map in the following manner:
Make sure this Mutator is NOT listed in the "EditPackages" section of your UnrealTournament.ini file!
Make sure <MutatorName>.u IS located in your UnrealTournament/System Folder.
Start a fresh UnrealEd session and open your Map for editing.
In the Command entry field at the bottom of the UED window type:
obj load file= <MutatorName>.u package=MyLevel
Locate your mutator in the Actor Classes browser: Actor >> Info >> Mutator >> <MutatorName> and add it somewhere in your map. Place only ONE instance of this Mutator in your Map.
Build and Save your map – the embedded Mutator code with your settings will be saved away in the map.
If you subsequently delete the single instance of the Mutator from your map, then re-Build and re-Save, the Mutator code will be deleted from the map and you will have to repeat all these steps to embed it again. (See MyLevel and Embedding Code for more on this)
Note: For UT2003 and UT2004, in order to complete step 5 and actually add the mutator actor into your map, you need to specify the mutator as placeable. This is easily achieved by adding the command word when you define the class as below:
class MutEmbeddedTest extends Mutator placeable;
Comment