Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modifying CampaignBehaviors #110

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
* [Modding Gauntlet UIs Without C#](docs/_tutorials/modding-gauntlet-without-csharp.md) (Easy)
* [Packing your Mods for Vortex](docs/_tutorials/packing_mods_for_vortex.md) (Easy)
* [Modifying/Adding Settlements](docs/_tutorials/new_settlements.md) (Easy)

* [Modifying CampaignBehaviors - GameModels](docs/_tutorials/altering_existing_behavior_via_gamemodels.md) (Easy)
* [Modifying CampaignBehaviors - Reregister Event](docs/_tutorials/altering_existing_behavior_via_reregistering_events.md) (Medicore)
## [C# API Documentation](docs/_csharp-api/)

* [CampaignSystem](docs/_csharp-api/campaignsystem/)
Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
* [Modding Gauntlet UIs Without C#](_tutorials/modding-gauntlet-without-csharp.md) (Easy)
* [Packing your Mods for Vortex](_tutorials/packing_mods_for_vortex.md) (Easy)
* [Modifying/Adding Settlements](_tutorials/new_settlements.md) (Easy)
* [Modifying CampaignBehaviors - GameModels](_tutorials/altering_existing_behavior_via_gamemodels.md) (Easy)
* [Modifying CampaignBehaviors - Reregister Event](_tutorials/altering_existing_behavior_via_reregistering_events.md) (Medicore)

## [C# API Documentation](_csharp-api/)

Expand Down
80 changes: 80 additions & 0 deletions docs/_tutorials/altering_existing_behavior_via_gamemodels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
Sometimes you might want to alter some of the existing behaviors. Here we try to do this with 2 different aproaches.

### 1. With GameModels
[GameModels](../_csharp-api/core/gamemodel.md) that are derived from the `TaleWorlds.Core.GameModel` class.
There is a naming convention like `*Something*Model` (when it's an abstract class derived from the GameModels) and `*DefaultSomething*Model` (when it's derived from `Something*Model`).

Some behaviors have related game models which include key factors for behaviors. You need to check whether your need them in model (if there are some) or not.

**Note:** Before we start these are what I could do with my knowledge, there is alot way to do this (probably better ways) but once you get the idea you can do as you need.
For this tutorial I want to edit `SettlementLoyaltyModel` and do the followings:
1. Add +2 Loyality Buff for Settlement belongs to Vlandian Kingdom, lets say as Culturel Buff.
2. Change the Owner Culture Debuff from -3 to -1.
3. Change the `DailyNotableRelationBonus` from 1 to 2 and the threshold for this to happen from 75 to 55.

First, extend the model:
```csharp
class ModifiedDefaultSettlementLoyaltyModel : DefaultSettlementLoyaltyModel
```

Then add the new model via the `gameStarter` in `InitializeGameStarter`:
```csharp
protected override void InitializeGameStarter(Game game, IGameStarter starterObject)
{
if (starterObject is CampaignGameStarter campaignGameStarter)
{
campaignGameStarter.AddModel(new ModifiedDefaultSettlementLoyaltyModel());
}
}
```
Note: It does remove (doesn't duplicate) the original `DefaultSettlementLoyaltyModel` when the `GameModelManager` is creating the models in `AddGameModelsManager()` method in `TaleWorlds.Core.Game` class. If someone else figure it out please edit here.

Lets override the things we want to edit:
```csharp
public override int DailyNotableRelationBonus => 2;
public override float ThresholdForNotableRelationBonus => 55f;

public override ExplainedNumber CalculateLoyaltyChange(Town town, bool includeDescriptions = false)
{
var myResult = base.CalculateLoyaltyChange(town, includeDescriptions);

EditLoyalityFactors(ref myResult, "Owner Culture", -1f, includeDescriptions);
VlandianLoyalityBuff(town, ref myResult);
return myResult;
}
```

Here the function to edit an existing factor (most of the parts are private in the models so we can't override the method that calculates the factor, instead we create another `ExplaineNumber` and change it while copying from the original model)
```csharp
public void EditLoyalityFactors(ref ExplainedNumber result,string descriptionOfFactor,float newValue,bool includeDescriptions)
{
var listOfFactors = result.GetLines();
var temp = new ExplainedNumber(0f, includeDescriptions, null);
foreach (var (name, value) in listOfFactors)
{
if (name.Equals(descriptionOfFactor))
{
temp.Add(newValue,new TextObject(name, null), null);
continue;
}
temp.Add(value,new TextObject(name, null), null);
}

result = temp;
}
```

And finally adding the new factor:
```csharp
public void VlandianLoyalityBuff(Town town, ref ExplainedNumber explainedNumber)
{
if(town.OwnerClan != null && town.OwnerClan.Kingdom != null && town.OwnerClan.Kingdom.Name.ToString().Equals("Vlandia"))
{
var VlandianCultureLoyalityBuffTO = new TextObject("Vlandian culture buff", null);
explainedNumber.Add(2f, VlandianCultureLoyalityBuffTO, null);
}
}
```

### 2. Altering Behavior Class
[Modify Campaign Behavior With ReRegistering Event](./altering_existing_behavior_via_reregistering_events.md)
202 changes: 202 additions & 0 deletions docs/_tutorials/altering_existing_behavior_via_reregistering_events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
### Adding new location to settlement
Sometimes what you wanna add/modify not be in model or there is no model,it can be even hardcoded in private methods/fields.Editing existing things hard but adding generally easy.
In this tutorial I want to walkthrough adding new interior scenes/districts to villages by playing around PlayerTownVisitCampaignBehavior

First of all let me try to explain how the settlement interactions work as far as I know.

When you go to settlement `TaleWorlds.CampaignSystem.PlayerEncounter` class creates `TaleWorlds.CampaignSystem.LocationEncounter` base on settlement you just visit.There is 3 type of encounter Town,Village,Castle and they derived from TaleWorlds.CampaignSystem.LocationEncounter
Important part here they override `CreateAndOpenMissionController()` method which creates [mission](../_csharp-api/mountandblade/mission.md)/scene.This is first thing we need to modify.These are happens in PlayerEncounter.EnterSettlement() method afterwards which calls `EnterSettlementAction`
and next part come in.

`PlayerTownVisitCampaignBehavior` class is the where most of the interactions defined.Its derived from [CampaignBehaviorBase](../_csharp-api/campaignsystem/campaignbehaviorbase.md) like other behaviors.Here we need to know about [GameMenus](../_csharp-api/campaignsystem/gamemenu.md) and [GameMenuOptions](../_csharp-api/campaignsystem/gamemenu.md).
When you enter settlement first `on_init` method of current GameMenu called then `on_condition` of method of GameMenuOptions called.Let say you click the take a walk around town center option it will call `on_consuqence` method of that option which going to call
`TownEncounter.CreateAndOpenMissionController()` and it will create town_center scene.Let say you walk around town and went to tavern door when you use door `SandBox.Source.Objects.SettlementObjects.PassageUsePoint.OnUse()` will be called and it will assign `Campaign.Current.GameMenuManager.NextLocation` to its toLocation field which you can edit with editor(modding tools) and calls `Mission.Current.EndMission()`.
Then we again start from town menu's on_init method and it will check `Campaign.Current.GameMenuManager.NextLocation` if its not null it will create another mission.For villages there is no check like this and `VillageEncouter.CreateAndOpenMissionController()` will return null for locations different than **"village_center"**.


#1.Create Scene(s)

Make sure you have passage point with correct id(location id on xml file) on toLocation parameter for both inside/outside of scene.Save them inside your [SceneObj](../_intro/folder-structure.md) folder

#2.Override settlement.xml and location_complex_templates.xml

Just copy and paste both xml to your [ModuleData](../_intro/folder-structure.md) folder.
I will just add tavern for village_complex and will name it new_complex here how it looks
```xml
<LocationComplexTemplate id="new_complex">
<Location id="village_center"
name="{=25pNV9E3}Village Center"
scene_name="scn_new_interior_village"
indoor="false"
max_prosperity="40"
ai_can_enter="CanAlways"
ai_can_exit="CanAlways"
player_can_enter="CanAlways"
player_can_see="CanAlways" />
<Location id="tavern"
name="{=DO7Vk4iw}Tavern test text"
scene_name="scn_new_interior_tavern"
indoor="true"
max_prosperity="30"
ai_can_enter="CanIfGrownUpMaleOrHero"
ai_can_exit="CanAlways"
player_can_enter="CanAlways"
player_can_see="CanAlways" />
<Passages>
<Passage location_1="village_center"
location_2="tavern" />
</Passages>
</LocationComplexTemplate>
```
for village I choose Polisia since its close to starting point as empire culture,we just need to edit Locations part here how it looks
```xml
<Locations complex_template="LocationComplexTemplate.new_complex">
<Location id="village_center" scene_name="scn_new_interior_village" />
<Location id="tavern" scene_name="scn_new_interior_tavern" />
</Locations>
```
you need to add these 2 xmls to `SubModule.xml` and also need to add .xslt to override native xmls.[See here](http://docs.modding.bannerlord.com/bestpractices/xslt_usage_tutorial/)

#3.
Modify Village Encounter
```csharp
class ModifiedVillageEncouter : VillageEncouter
{
public override IMission CreateAndOpenMissionController(Location nextLocation, Location previousLocation = null, CharacterObject talkToChar = null, string playerSpecialSpawnTag = null)
{

IMission result = null;
if (nextLocation.StringId == "village_center")
{
result = CampaignMission.OpenVillageMission(nextLocation.GetSceneName(1), nextLocation, talkToChar);
}
else if (nextLocation.StringId == "tavern")
{

result = CampaignMission.OpenIndoorMission(nextLocation.GetSceneName(1), 1, nextLocation, talkToChar);
}
return result;
}
}
```
#4.
Lets write modified behavior
```csharp
class ModifiedPlayerTownVisitCampaignBehavior : PlayerTownVisitCampaignBehavior
{
public ModifiedPlayerTownVisitCampaignBehavior(IGameStarter gameStarter)
{
//Removing original Behavior
CampaignBehaviorBase behaviorInstance = (gameStarter as CampaignGameStarter).CampaignBehaviors.ToList<CampaignBehaviorBase>().Find(x => x.GetType()==typeof(PlayerTownVisitCampaignBehavior));
(gameStarter as CampaignGameStarter).RemoveBehavior<PlayerTownVisitCampaignBehavior>(behaviorInstance as PlayerTownVisitCampaignBehavior);
}
}
```
#5.
Lets add [GameMenuOption](../_csharp-api/campaignsystem/gamemenu.md) and its delegates,here you can add another GameMenu as distirct and stuff I will keep it simple.Since GameMenuOption's on_condition called after GameMenu's on_init I will use it with same manner.Its really up to you.
```csharp
protected new void AddGameMenus(CampaignGameStarter campaignGameSystemStarter)
{
campaignGameSystemStarter.AddGameMenuOption("village", "tavern", "{=l9sFJawW}Visit the local inn!!",
new GameMenuOption.OnConditionDelegate(this.game_menu_village_village_inn_on_condition),
new GameMenuOption.OnConsequenceDelegate(this.game_menu_village_village_inn_on_consequence), false, 0);
}

public bool game_menu_village_village_inn_on_condition(MenuCallbackArgs args)
{
//check if player try to go an interior scene
//you can just just return false and option will not be visible also you can enable it for specific settlement etc. here like:
//if(!Settlement.CurrentSettlement.LocationComplex.GetListOfLocations().Any((Location x) => x.StringId == "tavern")) return false
if (this.CheckAndOpenNextLocation(args))
{
return false;
}
//these part I believe possible quest and helper icons inside scene when you press ALT
bool shouldBeDisabled;
TextObject disabledText;
bool canPlayerDo = Campaign.Current.Models.SettlementAccessModel.CanMainHeroAccessLocation(Settlement.CurrentSettlement, "tavern", out shouldBeDisabled, out disabledText);
List<Location> currentLocations = Settlement.CurrentSettlement.LocationComplex.FindAll((string x) => x == "tavern").ToList<Location>();
args.OptionIssueType = Campaign.Current.IssueManager.CheckIssueForMenuLocations(currentLocations);
args.OptionQuestStatus = Campaign.Current.QuestManager.CheckQuestForMenuLocations(currentLocations);
args.optionLeaveType = GameMenuOption.LeaveType.Submenu;

return MenuHelper.SetOptionProperties(args, canPlayerDo, shouldBeDisabled, disabledText);
}

private void game_menu_village_village_inn_on_consequence(MenuCallbackArgs args)
{
//it doesnt work setting next location and call CheckAndOpenNextLocation due to MapState basicially same code in CheckAndOpenNextLocation probably can be reduced to 1 method
Settlement currentSettlement = PlayerEncounter.EncounterSettlement;
PlayerEncounter.LocationEncounter = new ModifiedVillageEncouter(currentSettlement);//here we use our modified encounter
PlayerEncounter.LocationEncounter.CreateAndOpenMissionController(LocationComplex.Current.GetLocationWithId("tavern"),
LocationComplex.Current.GetLocationWithId("village_center"), null, null);

Campaign.Current.GameMenuManager.NextLocation = null;
Campaign.Current.GameMenuManager.PreviousLocation = null;
}
```
#6.
Lets add CheckAndOpenNextLocation method, the methods have same naming convention(if not same) with parent class to when you check it you can recognize.
```csharp
public bool CheckAndOpenNextLocation(MenuCallbackArgs args)
{
/check if player try to go an interior scene
if (Campaign.Current.GameMenuManager.NextLocation != null && GameStateManager.Current.ActiveState is MapState)
{
Settlement currentSettlement = PlayerEncounter.EncounterSettlement;
string stringId = Campaign.Current.GameMenuManager.NextLocation.StringId;


PlayerEncounter.LocationEncounter = new ModifiedVillageEncouter(currentSettlement);

PlayerEncounter.LocationEncounter.CreateAndOpenMissionController(Campaign.Current.GameMenuManager.NextLocation,
Campaign.Current.GameMenuManager.PreviousLocation, null, null);
//its important to set these 2 to null because after mission end game switch to menu and if Next location not null it will loop here(passage points(Doors) does assign next points)
Campaign.Current.GameMenuManager.NextLocation = null;
Campaign.Current.GameMenuManager.PreviousLocation = null;

//this is which menu will be game showing after mission end(using passage points(door) also ends mission)
//hard coded it will go village menu every time when player leave area
Campaign.Current.GameMenuManager.SetNextMenu("village");

return true;
}
return false;
}
```
There is some stuffs in parent class for Game Menu indexes etc.

#7.
Finally this part is important,modifying existing behaviors you need to understand how it works partially if not whole but at the end you can do this for every one of them(if the event did not occur before here)
Till here all we did add a menu option and edit VillageEncounter here we'll first call parent's register event then delete listeners on the function we edited(AddGameMenu in this situation) and add listener again.
```csharp
public new void OnAfterNewGameCreated(CampaignGameStarter campaignGameStarter)
{
base.OnAfterNewGameCreated(campaignGameStarter);//Parent class call add game menu here
this.AddGameMenus(campaignGameStarter);
}
public override void RegisterEvents()
{
//ReRegister original events
base.RegisterEvents();
//Get behavior, it is this class now(removed on constructor)
CampaignBehaviorBase baseToModify = Campaign.Current.CampaignBehaviorManager.GetBehavior<ModifiedPlayerTownVisitCampaignBehavior>();
//Remove listener on modified method
CampaignEvents.OnNewGameCreatedEvent.ClearListeners(baseToModify);
CampaignEvents.OnGameLoadedEvent.ClearListeners(baseToModify);
//Add listener back with new method
CampaignEvents.OnNewGameCreatedEvent.AddNonSerializedListener(this, new Action<CampaignGameStarter>(this.OnAfterNewGameCreated));
CampaignEvents.OnGameLoadedEvent.AddNonSerializedListener(this, new Action<CampaignGameStarter>(this.OnAfterNewGameCreated));
}
```
#8.
Add modified behavior to gameStarter
```csharp
public class VillageInteriorScene : MBSubModuleBase
{
protected override void InitializeGameStarter(Game game, IGameStarter starterObject)
{
(starterObject as CampaignGameStarter).AddBehavior(new ModifiedPlayerTownVisitCampaignBehavior(starterObject));
}
}
```