Modding Scripts in The Witcher 3

Modifying scripts in the Witcher 3 is the primary way to modify game behavior, though you should know the basics behind programming before you try to jump in. Script mods, unlike other mods, do not need to be cooked, packaged, or bundled, and sit as loose files (in the same directory structure as the vanilla scripts) in your mod folder's  directory. When the game is started, all mod scripts are loaded and any scripts matching a vanilla script (i.e. same file name in the same directory structure) replace the vanilla script and are loaded instead. You should be aware of the limitations as well as the workarounds that exist for script modding.

Do No Harm (and mind your whitespace edits!)
While modding, you should keep in mind that every change that you make can affect other mods that a user installs, so you should do your best to keep your changes self-contained and with as few side-effects as you can. You should also try to avoid changing things like whitespace in sections of code that you are not modifying, because these changes can make it difficult for a user to merge in your mod.

Comment Tags
You should clearly mark sections of code that are modified or added by your mod. If you are changing a single line, you could use a simple comment tacked on to the end of the line like this:

default useNativeTargeting = false; //modEnhancedTargeting

or if you're modifying a larger section of code, you should have a begin and end comment, something like this:

//modEnhancedTargeting BEGIN MyModdedCode; MyModdedCode2; MyModdedCode3; MyModdedCode4; //modEnhancedTargeting END

Adding comment tags like this make it obvious which sections of code that you've modified so that users installing your mod can easily merge your mod in.

Use Script Studio (even if you don't use it to edit script)
Script Studio can be an incredible tool if you know how to use it and its limitations. Don't forget that you can use it to trigger a script recompile without launching the game.

Use the scriptslog
Instead of using the combat log or sounds to help debug your program, consider using the logging functions built into the game. An article on how to enable the scriptslog and how to use it is available here. This has the positive side effect of not being needed to comment your debug code out before you package your mod and put it up for download, and giving another way of having users diagnose problems with you—just have them turn on the scriptslog and send you the log!

Use Commented 1.21 Scripts
In version 1.21 scripts were extensively commented, which can be really helpfull when working with them.

You can download them from nexus here http://www.nexusmods.com/witcher3/mods/1685/?

Game Facts System (Saving/Loading Data)
Facts are used to store data in saves. (when the game is saved) They can be used to save and load type int directly.

Saving
function FactsAdd( ID : string, optional value : int, optional validFor : int  )

Fact by ID passed as first argument is created (if it didn't exist), adds value (also to existing fact) passed as second argument, valid for value of type int passed as third argument(but I'm not sure how validFor works, probably it's pointing to some predefined game time value) if you want your fact to be valid forever, as a third argument pass - 1 or just don't pass third argument.

function FactsSet(ID : string, val : int, optional validFor : int ) Sets fact by ID passed as first argument to value passed as second argument, optionally sets its validFor to value passed as third argument. Fact is automatically created if it didn't exist before.

function FactsSubstract(ID : string, optional val : int) From a fact by ID passed as first argument, substracts value passed as second argument, val : int is optional, if you only pass fact ID to the function, it will substract 1 from the fact total value.

Loading
function FactsQuerySum( ID : string ) : int Returns value of fact by ID passed to it as argument. function FactsDoesExist( ID : string ) : bool Returns true if fact by ID passed as an argument exists, false if it doesn't.

Other
function FactsRemove( ID : string ) : bool Removes fact by ID passed as argument.

Functions not tested
function FactsQuerySumSince( ID : string, sinceTime : EngineTime ) : int function FactsQueryLatestValue( ID : string ) : int See scripts/game/facts.ws, there are also "GameplayFacts", only difference seems to be that they can be set to be valid for exact time value instead of predefined values like "normal" facts, but you will have to test it.

Other Method
There is another method for saving data with scripts, not related to game saves, using user.settings. See Menus in The Witcher 3/Scripting

Executing Code On Game Load And Location Change
When you want your code to be executed on Game Load or Location Change, you can use CR4Game event OnAfterLoadingScreenGameStart. (scripts\game\r4Game.ws)

Or W3PlayerWitcher event OnSpawned. (scripts\game\player\playerWitcher.ws)

Simply call your function or anything you want there.

Executing Code On Closing Main Menu
When you want to execute code on closing menu, for example to update scripts with new values of variables modified in menu, you can use CR4CommonIngameMenu event OnClosingMenu (scripts\game\gui\main_menu\commonIngameMenu.ws)

Simply call your function at the end of it. Now it will be called each time you close main menu after save is loaded.

Execute Code When Item Is Equipped/Unequipped
Use W3PlayerWitcher events. (scripts\game\player\playerWitcher.ws)

On Equipping Item
Use event OnEquipItemRequested

On Unequipping Item
Use event OnUnequipItemRequested

Fast Travel And Teleporting Methods
You will use mostly CCommonGame methods.

Fast Travel To Location With Specified Position
Use method ScheduleWorldChangeToPosition. import final function ScheduleWorldChangeToPosition( worldPath : string, position : Vector, rotation : EulerAngles ); You need to provide:

World path, which you can get using CWorld method GetDepotPath import final function GetDepotPath : string; Position, which you can get using CNode method GetWorldPosition import final function GetWorldPosition : Vector; Rotation, which you can get using CCameraDirector method GetCameraRotation or set to zero using global function EulerSetZeros import final function GetCameraRotation : EulerAngles;

function EulerSetZeros( out eulerAngles : EulerAngles ) { 	eulerAngles.Yaw		= 0.0f; eulerAngles.Pitch	= 0.0f; eulerAngles.Roll	= 0.0f; }

Example
Example class with methods for saving worldPath/CameraRotation and traveling to location. class Example { 	var worldPathSaved : string; var playerPositionSavedSaved : Vector; var cameraRotationSavedSaved : EulerAngles; var rotationSet : bool; default rotationSet = false; \\to check if rotation was set public function SaveworldPath { 		worldPathSaved = theGame.GetWorld.GetDepotPath; } 	public function SavePlayerPosition { 		playerPositionSavedSaved = thePlayer.GetWorldPosition; } 	public function SaveCameraRotation { 		cameraRotationSaved = theGame.GetWorld.GetDepotPath; rotationSet = true; } 	public function TravelToWorld { 		if (!rotationSet) \\if rotation wasn't set, set it to zeros EulerSetZeros(cameraRotationSaved); theGame.ScheduleWorldChangeToPosition(worldPathSaved, playerPositionSaved, cameraRotationSaved); } }

Fast Travel To Location With Specified Travel Point
import final function ScheduleWorldChangeToMapPin( worldPath : string, mapPinName : name );

Fast Travel To Custom Map Waypoint
There is DebugTeleportToPin method in CPlayer (player.ws) that I modified and used it for mod Custom Fast Travel, it allows you there travelling to custom map waypoint (the green one you can set on map)

I added it as an method of W3PlayerWitcher (playerWitcher.ws) in my mod. function TravelToPin { 		var mapManager 		: CCommonMapManager = theGame.GetCommonMapManager; var rootMenu		: CR4Menu; var mapMenu			: CR4MapMenu; var currWorld		: CWorld = theGame.GetWorld; var destWorldPath	: string; var id, area, type				: int; var position		: Vector; var rotation 		: EulerAngles; var goToCurrent		: Bool = false; var pinNum	: int; var waterDepth	: float; pinNum = FactsQuerySum("TravelPinId"); rootMenu = (CR4Menu)theGame.GetGuiManager.GetRootMenu; mapManager.GetUserMapPinByIndex( pinNum, id, area, position.X, position.Y, type ); destWorldPath = mapManager.GetWorldPathFromAreaType( area ); if (destWorldPath == currWorld.GetPath ) goToCurrent = true; if ( goToCurrent && FactsDoesExist("TravelPinSet")) { 			currWorld.NavigationComputeZ(position, -500.f, 500.f, position.Z); if (currWorld.GetWaterLevel(position, true) > position.Z) 				position.Z = currWorld.GetWaterLevel(position, true); currWorld.NavigationFindSafeSpot(position, 0.5f, 20.f, position); Teleport( position ); FactsRemove("TravelPinSet"); FactsRemove("TravelPinId"); waterDepth = currWorld.GetWaterDepth( position ); if( waterDepth == 10000 ) waterDepth = 0; if (!currWorld.NavigationComputeZ(position, -500.f, 500.f, position.Z) && waterDepth <= 0) AddTimer( 'DebugWaitForNavigableTerrain', 1.f, true ); } 		else if (destWorldPath == "" || !FactsDoesExist("TravelPinSet")) theGame.GetGuiManager.ShowNotification("Map pin not set"); else { 			theGame.ScheduleWorldChangeToPosition( destWorldPath, position, rotation ); AddTimer( 'DebugWaitForNavigableTerrain', 1.f, true,, , true ); FactsRemove("TravelPinSet"); FactsRemove("TravelPinId"); } 	} Thx to NiNaka on Nexus who added some code that teleports the player to the water surface when you travel to map pin set on water. But it only works when you travel to the same location, for traveling to another location, you'll have to use event OnAfterLoadingScreenGameStart for example.

Note that it uses facts system to make sure you only travel to "green" map pin and not the others. So you will have to set "TravelPinSet" and "TravelPinID" facts somewhere, preferably using event  OnUserMapPinSet in CR4MapMenu (mapMenu.ws) so they will be set when you add this pin to the map. event OnUserMapPinSet( posX : float, posY : float, type : int, fromSelectionPanel : bool ) { 		var manager	: CCommonMapManager = theGame.GetCommonMapManager; var worldPath : string; var realShownArea : EAreaName; var area : int; var position : Vector; var idToAdd, idToRemove, indexToAdd : int; var realType : int; idToAdd = 0; idToRemove = 0; if ( m_currentArea == m_shownArea ) { 			worldPath = theGame.GetWorld.GetDepotPath; realShownArea = manager.GetAreaFromWorldPath( worldPath, true ); } 		else { 			realShownArea = m_shownArea; } 		position.X = posX; position.Y = posY; position.Z = 0; realType = type; if ( fromSelectionPanel ) { 			realType += 1; } 		if ( !manager.ToggleUserMapPin( (int)realShownArea, position, realType, fromSelectionPanel, idToAdd, idToRemove ) ) { 			showNotification( GetLocStringByKeyExt("panel_hud_message_actionnotallowed") ); } 		if ( idToRemove != 0 && !fromSelectionPanel) { 			m_fxRemoveUserMapPin.InvokeSelfOneArg( FlashArgUInt( idToRemove ) ); //here FactsRemove("TravelPinId"); FactsRemove("TravelPinSet"); //here } 		else { 			m_fxRemoveUserMapPin.InvokeSelfOneArg( FlashArgUInt( idToRemove ) ); } 		if ( idToAdd != 0 ) { 			indexToAdd = manager.GetUserMapPinIndexById( idToAdd ); if ( indexToAdd >= 0 ) { 				UpdateDataWithSingleUserMapPin( indexToAdd ); theSound.SoundEvent("gui_hubmap_mark_pin"); //here if ( fromSelectionPanel && !FactsDoesExist("TravelPinSet")) { 				FactsRemove("TravelPinId"); } 			else if ( !fromSelectionPanel ) { 				FactsAdd("TravelPinId", indexToAdd, -1); FactsAdd("TravelPinSet"); } 			//here } 	}

Teleport To Position In The Same location
Use CEntity Teleport method. import final function Teleport( position : Vector ); Example: thePlayer.Teleport(6,6,6);

Enabling Fast Travel From Anywhere
Use CCommonMapManager DBG_AllowFT method. In popular mod Fast Travel To Anywhere it's handled in different way, but this more "ellegant". function DBG_AllowFT( allow : bool ) { 		m_dbgAllowFT = allow; } Example: theGame.GetCommonMapManager.DBG_AllowFT( true );

Useful Exec Functions
You can call them from console.

Show current position in HUD notification and save it in log. Witcher 3 Scripts Logging exec function Position { 	var position : string; position = VecToString(thePlayer.GetWorldPosition); thePlayer.ShowHudNotification(position); LogChannel('SAVEDPOSITION', position ); } Show current world path in HUD notification and save it in log. exec function WorldPath { 	var worldPath : string; worldPath = theGame.GetWorld.GetDepotPath; thePlayer.ShowHudNotification(worldPath); LogChannel('SAVEDWORLDPATH', worldPath ); }

List Of World Paths
So you won't have to get them manually.
 * "levels\wyzima_castle\wyzima_castle.w2w" - Castle In Vizima
 * "levels\novigrad\novigrad.w2w" - Novigrad/Velen/Oxenfurt
 * "levels\skellige\skellige.w2w" - Skellige
 * "levels\kaer_morhen\kaer_morhen.w2w" - Kaer Morhen
 * "levels\prolog_village\prolog_village.w2w" - White Orchard
 * "dlc\bob\data\levels\bob\bob.w2w" - Toussaint
 * "levels\island_of_mist\island_of_mist.w2w" - Island Of Mist
 * "levels\the_spiral\spiral.w2w" - World from "Through Time And Space" quest.

Items
You will use mostly CInventoryComponent methods. To access it you will probably have to use CGameplayEntity method GetInventory.

Spawn Item In Player Inventory
Use AddAnItem method. public final function AddAnItem(item : name, optional quantity : int, optional dontInformGui : bool, optional dontMarkAsNew : bool, optional showAsRewardInUIHax : bool) : array { 		var arr : array; var i : int; var isReadableItem : bool; if( theGame.GetDefinitionsManager.IsItemSingletonItem(item) && GetEntity == thePlayer) { 			if(GetItemQuantityByName(item) > 0) { 				arr = GetItemsIds(item); } 			else { 				arr.PushBack(AddSingleItem(item, !dontInformGui, !dontMarkAsNew)); } 			quantity = 1; } 		else { 			if(quantity < 2 ) { 				arr.PushBack(AddSingleItem(item, !dontInformGui, !dontMarkAsNew)); } 			else { 				arr = AddMultiItem(item, quantity, !dontInformGui, !dontMarkAsNew); } 		} 		if(this == thePlayer.GetInventory) { 			if(ItemHasTag(arr[0],'ReadableItem')) UpdateInitialReadState(arr[0]); if(showAsRewardInUIHax || ItemHasTag(arr[0],'GwintCard')) thePlayer.DisplayItemRewardNotification(GetItemName(arr[0]), quantity ); } 		return arr; } Example, adding 1 Clearing Potion: thePlayer.GetInventory.AddAnItem('Clearing Potion');

Check If Player Has Item In Inventory
Use HasItem method to search for item by name.

Example, checking if player has Clearing Potion: thePlayer.GetInventory.HasItem('Clearing Potion'); This methods looks for an item by name, there are other methods to search by id or tag, see CInventoryComponent article.

Check If item Is Equipped In Slot By Item Name
Use CInventoryComponent method GetItemEquippedOnSlot. public function GetItemEquippedOnSlot(slot : EEquipmentSlots, out item : SItemUniqueId) : bool { 		var player : W3PlayerWitcher; player = ((W3PlayerWitcher)GetEntity); if(player) { 			return player.GetItemEquippedOnSlot(slot, item); } 		else { 			return false; } 	} As you can see you need to pass it a slot (EEquipmentSlots, see enums) you want to check as first argument, and item id (SItemUniqueId) will be stored in variable passed as second argument.

Example, check if player has equipped Wolf School steel sword in EES_SteelSword slot and return bool value: function Example : bool { 	var sword : SItemUniqueId; var swordName : name; GetInventory.GetItemEquippedOnSlot(EES_SteelSword, sword); swordName = GetItemName(sword); if (swordName == 'Wolf School steel sword') return true; else return false; } Note that you have to use GetItemName method to get item name by SItemUniqueId.

See CInventoryComponent for more methods used to manipulate items in game.