Left 4 Dead 2

Left 4 Dead 2

Not enough ratings
(MSC) (Programming) How to write your own script with the MSC system.
By [FoRcE]army_guy3
This guide will explain how to write your own MSC script. What to do and what not to do.
   
Award
Favorite
Favorited
Unfavorite
Note: This guide is a work in progress
More information still needs to be added but I'm making what is written public so people can start learning how to use it.
What is the MSC?
The MSC (Master Script Controller) is an addon created by [FoRcE]army_guy3 to allow you to load multiple scripts without them conflicting with each other.

It will automatically handle scripts to work along side mutations without conflicts IF you follow guidelines mentioned in this guide. If you stray from this guide you may break functionality of other scripts or the MSC itself.

You can find it here:
https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=2203978515
Making your first MSC script
This guide won't go over basics for creating your .nut files.

It also won't cover auto loading into the MSC. You can learn to do that here:
https://steamhost.cn/steamcommunity_com/sharedfiles/filedetails/?id=2255315135

Making our MSC script is basically the same as other scripts but there is a few rules we should follow.

1. Don't modify the MSC functions. These may be changed in future updates so hijacking them may break your script or the MSC itself.
2. Don't put any other data into the "scriptedmode_addon.nut". This scope will use the same as all the other "scriptedmode_addon.nut" addon files. This is a big no no as scripts with matching variable names will conflict. You may also end up breaking back end functionality of the MSC.
3. Don't modify back end MSC Variables. Some of them are global but this does not mean they should be changed.

After we've created our script we can test it by putting:

function Main() { printl("debug: boop"); }

into our main script file.

The Main function is a place to safely run init for the execution of your code. You are not required to place it here but here is where your script is safely loaded into the MSC sub scripts.

Note that variables do not need to be placed in Main().

Example:
bIsbooped <- false; function Main() { printl("debug: boop"); bIsBooped = true; }

The MSC will warn if the Main() does not exist in the script but we can easily suppress this warning with:

Example:
MSC_ScriptInfo <- { SUPPRESS_WARNING_MAINMISSING = true }

The MSC_ScriptInfo will send data back to the MSC on load that gives it special instructions to use with that specific sub script.
Making more advanced scripts (Advanced Hook Functions)
The MSC system has been modified to work with default hook functions but in a way that allows all scripts to communicate.

Let's look at an example of the old way of blocking damage.

Normally in a mutation this is how it would look:
function AllowTakeDamage(damageTable) { return false; }

In the MSC script it will look a little different:
function MSC_AllowTakeDamage(damageTable, origDamageTable, ATD_Flags) { return MSC_ATD_ReturnType.Override_Block; }

Keep in mind that currently the "origDamageTable" is blank. I will get around to adding it in later.

This script uses the MSC_ATD_ReturnType.Override_Block type. This means that it will send data back to the MSC saying we should block this damage from happening.

This next script will force allowing the damage to take place.

function MSC_AllowTakeDamage(damageTable, origDamageTable, ATD_Flags) { return MSC_ATD_ReturnType.Override_Allow; }

Both of these are fine but what if we don't want to allow or disallow the damage? Meaning what if we want other scripts to be able to choose this.

We can use the neutral "Normal" type.

function MSC_AllowTakeDamage(damageTable, origDamageTable, ATD_Flags) { return MSC_ATD_ReturnType.Override_Normal; }

The Normal type should be used in most cases unless if your script should force that specific action. Different scripts will conflict with Override_Block and Override_Allow so you should keep this in mind based on what your script does. A conflict won't cause an error but it may cause other scripts from not working properly with yours.
Making more advanced scripts (OnGameEvent_ events)
The MSC is designed to automatically connect events to your script.

Example:

function OnGameEvent_round_start( params ) { printl("I LIKE PIE!!!"); }

This will print I LIKE PIE!!! to your console every time the round starts.
Making more advanced scripts (Chat Commands and admin hooking)
The MSC has a built in admin system for use in script hooking.

Let's do a simple example using the MSC system.

//This command hooks into the MSC system to tell it you want to check if any chat commands are yours. function EVENTMSC_ChatCommand(player, args, commandFlags) { //args[0] is set to lower case so we can compare if the strings are equal local commandString = args[0].tolower(); if (commandString=="boop") { //String is equal so lets run our boop command! boopCommand(player, args, commandFlags); //We want to return MSC_ResultType.TakeOver to tell the MSC that our script will handle the chat command. return MSC_ResultType.TakeOver; } else if (commandString=="givepie") { //String is equal so lets run our givepie command! givepieCommand(player, args, commandFlags); //We want to return MSC_ResultType.TakeOver to tell the MSC that our script will handle the chat command. return MSC_ResultType.TakeOver; } //If our chat command was not found then we want to return MSC_ResultType.NotFound to tell it to keep search scripts for this command. return MSC_ResultType.NotFound; } function boopCommand(player, args, commandFlags) { //Get the player's steamid local steamid = player.GetNetworkIDString(); //Check if the player's steamid is registered as a valid admin. if (!MSC_IsAdmin(steamid)) { //Show them a message if they don't have admin. MSC_PrintMessageToClientConsole(player, "You require admin to use this command!"); //Return so we leave this script and don't continue to run it. IMPORTANT! If this is missing it will continue to run the script even without admin! return; } MSC_PrintMessageToClientConsole(player, "BOOP!"); } function givepieCommand(player, args, commandFlags) { MSC_PrintMessageToClientConsole(player, "PIE!!!"); }

In this example we have two chat commands. "boop" and "givepie". The boop command requires admin in order to work properly. The givepie command does not. Keep this in mind for who should be allowed to use this function. For instance we only want admins to have access to kick.

This is all well and good but what if we want to target players?

The MSC has a built in system to handle this. Let's change our boop command to boop players instead of ourselves.

function EVENTMSC_ChatCommand(player, args, commandFlags) { local commandString = args[0].tolower(); if (commandString=="boop") { boopCommand(player, args, commandFlags); return MSC_ResultType.TakeOver; } return MSC_ResultType.NotFound; } function boopCommand(player, args, commandFlags) { local steamid = player.GetNetworkIDString(); if (!MSC_IsAdmin(steamid)) { MSC_PrintMessageToClientConsole(player, "You require admin to use this command!"); return; } local playerList = null; if (args.len()==1) { //In this case we only have one argument which is the command itself. In this case it will be "boop". We can assume here that we want to boop ourself. This should not be used with more harmful functions such as kick or ban. playerList.push(player); } else if (args.len()>1) { //In this case we have enough arguments to account for a player target. For this command the second argument is our player target so we use args[1]. playerList = MSC_GetPlayerMatch_Managed(player, args[1], commandFlags); } //Check to see if any players are in our PlayerList array if (playerList.len()>0) { //Loop through each victim in the playerList array foreach (victim in playerList) { //THEY SHALL BE BOOPED! THUS SAYETH TEH BOOP MONSTER! MSC_PrintMessageToClientConsole(victim, "BOOP!"); } } }

This example will boop the player found OR if you typed @All you could boop every player at once! Be careful because with great booping comes great responsiblity. o_o

The MSC can also be used with multiple player type arguments. This example pulled from MSC_AG3_AdminScript uses the teleport function. Notice how It includes two seperate playerList variables:

function EVENTMSC_ChatCommand(player, args, commandFlags) { local commandString = args[0].tolower(); if (commandString=="teleport") { teleport(player, args, commandFlags); return MSC_ResultType.TakeOver; } return MSC_ResultType.NotFound; } function teleport(player, args, commandFlags) { local steamid = player.GetNetworkIDString(); if (!MSC_IsAdmin(steamid)) { return; } local playerList = null; local playerMainPoint = null; if (args.len()<2) { MSC_PrintMessageToClientConsole(player, "teleport command missing argument!"); return; } else if (args.len()==2) { playerMainPoint = player; playerList = MSC_GetPlayerMatch_Managed(player, args[1], commandFlags); } else { playerList = MSC_GetPlayerMatch_Managed(player, args[1], commandFlags); local tempPlayerList2 = MSC_GetPlayerMatch_Managed(player, args[2], commandFlags); if (tempPlayerList2.len()==1) { playerMainPoint = tempPlayerList2[0]; } else if (tempPlayerList2.len()>1) { MSC_PrintMessageToClientConsole(player, "Only one person can be teleport point!"); return; } else if (tempPlayerList2.len()==0) { MSC_PrintMessageToClientConsole(player, "Invalid teleport point target!"); return; } } if (playerList.len()>0) { local pos = playerMainPoint.GetOrigin(); local rot = playerMainPoint.GetAngles(); local vel = playerMainPoint.GetVelocity(); foreach (victim in playerList) { victim.SetOrigin(pos); victim.SetAngles(rot); victim.SetVelocity(vel); } } }

In this example we can see the second PlayerList is being used for who two teleport the players to.

The MSC can do much more but for now I'm going to end with this.
MSC Functions
int MSC_GetSubScriptByIndex(int index)

returns the sub scope by index


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

bool MSC_IsAdmin(string steamid)

returns if the player is an admin

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

table MSC_GetAdminFlags(string steamid)

returns a table containing the players admin flags.

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

int MSC_GetAdminImmunity(string steamid)

returns the players immunity level.

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

string MSC_AdminFlagsToString(table admin_flags)

returns a string containing the admin flags

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

bool MSC_HasAdminFlag(string steamid, string flag)

returns if the player has the admin flag

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

array MSC_GetPlayerMatch_Managed(CTerrorPlayer player, string searchString, table commandFlags)

returns an array containing players that met the match conditions

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

void MSC_CreateNewFilter(string trigger, table filterScope)

Creates a new chat filter type.

More information on how this works will be added later.

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

void MSC_PrintMessageToClientConsole(CTerrorPlayer client, string message)

Prints message to client.

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

void MSC_ExecuteClientCommand(CTerrorPlayer client, string executionString)

Executes command on client. This command has heavy restrictions for what can be executed.

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

float MSC_PointDistance(Vector vector1, Vector vector2)

returns distance between two points.

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

CBaseEntity MSC_GetPlayerAimTarget(CTerrorPlayer player, bool bOnly_players, float dist, bool bRestrictWorldSpawn)

returns entity that the player is looking at.

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

table MSC_TraceLineAngles(table traceTable, float dist)

returns result of trace ray starting at start and angles and ending at distance.

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

int MSC_Convert_StringToInt(string val, int defVal=-1, table error_out=null)

returns converted value if it didn't fail in conversion process.

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

float MSC_Convert_StringToFloat(string val, float defVal=-1, table error_out=null)

returns converted value if it didn't fail in conversion process.

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

table MSC_LoadSessionSettings(string subTableName)

returns session table by name.

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

void MSC_SaveSessionSettings(string subTableName, table scriptSessionTable)

Saves the session table.

Note: This does not save to file. Once you leave the game.. It's gone.

--------------------------------
The following MSC Vanilla Game Hook functions can be used inside your MSC script
-----

MSC_AllowTakeDamage(damageTable, origDamageTable, ATD_Flags)
return MSC_ATD_ReturnType.Normal
return MSC_ATD_ReturnType.Override_Block
return MSC_ATD_ReturnType.Override_Allow

NOT YET ADDED-The origDamageTable will allow you to see what the table looked like before being modified.
-----

MSC_InterceptChat(message, player, nakedMessage, IC_Flags)
MSC_IC_ReturnType.Normal <-
MSC_IC_ReturnType.Override_Block
MSC_IC_ReturnType.Override_Allow

The nakedMessage variable will automatically remove the playername from the message.

-----

MSC_CanPickupObject(entity, CPO_Flags)
MSC_CPO_ReturnType.Override_Block
MSC_CPO_ReturnType.Override_Allow

-----

MSC_UserConsoleCommand(player, arg, UCC_Flags)

-----

MSC_BotQuery(queryflags, entity, defaultvalue, BQ_Flags)
MSC_BQ_ReturnType.Override_Block
MSC_BQ_ReturnType.Override_Allow

-----

MSC_AllowBash(basher, bashee, AB_Flags)
return MSC_AB_ReturnType.Override_Normal
return MSC_AB_ReturnType.Override_Bash_All
return MSC_AB_ReturnType.Override_Bash_None
return MSC_AB_ReturnType.Override_Bash_PushOnly

-----
1 Comments
KolyaSmirno. 11 Oct, 2020 @ 9:32pm 
Twice to put Keepo