Skip to content
This repository has been archived by the owner on Feb 5, 2018. It is now read-only.

External Mod Module Support

Ashley Muncaster edited this page Aug 21, 2017 · 13 revisions

The main branch contains functionality to allow mod modules to provide command response directly from within their own module code, without having to write a command response class in TP:KTaNE. There are currently two implementation options available for providing the interaction: a simple way and a more complex way.

Easy/Simple Implementation

This option is useful for issuing a simple sequence of KMSelectable object interactions, like in Keypad. The method should have the exact signature declared below (can be either public or non-public, but must be non-static):

KMSelectable[] ProcessTwitchCommand(string command)
{
}

Depending on the value of the command, return back either:

  • an array of KMSelectable objects to interact with, in the order they should be interacted with
  • null, to indicate that nothing should happen (i.e. ignore the command)

If at any point a KMSelectable interaction will cause a strike, TP:KTaNE will stop execution of the given KMSelectable objects mid-way automatically; you don't have to worry about this in your implementation. Also note that this method will not be invoked if the module is solved either.

Do not implement this option if:

  • You have to hold down on an interaction (like in The Button)
  • Buttons have to be pressed in a time-critical manner (like Light Cycle, Crazy Talk, Color Flash)
  • The module has a delay between the KMSelectable interaction and a strike/solve being awarded (like Two Bits, Cheap Checkout, Simon Screams)

Advanced Implementation

This option allows for full flexibility of interaction, by asking you to implement a coroutine-like method. If you don't know how to write coroutines in Unity, I recommend reading up on the Unity documentation to better understand how coroutines work. The method should have the exact signature declared below (can be either public or non-public, but must be non-static):

IEnumerator ProcessTwitchCommand(string command)
{
}

This implementation option will allow you to do the following:

  • yield break; without yield return-ing something, to denote that nothing should happen (i.e. ignore the command)
  • Wait for seconds using yield return new WaitForSeconds(__);
  • Wait for the next frame using yield return null;
  • A KMSelectable object to interact with; Twitch Plays will hold down the interaction of this object until the same KMSelectable object is yielded again, either by this command or a subsequent command
  • A KMSelectable [] array. This causes ALL of the buttons to be pressed unless one of the ones pressed causes a strike, in which case, the remainder of the sequence is aborted.
  • A string to denote special cases like the following:
    • "strike" indicates that this command will cause a strike at some later point; all this does is tell Twitch Plays to attribute the strike to the author of this command
    • "solve" indicates that this command will solve the module at some later point; all this does is tell Twitch Plays to attribute the solve to the author of this command
    • "trycancel" indicates that this command is allowed to be cancelled at the given time of the yield. Just know that you won't be able to clean up if you do your cancel this way, and there is a pending !cancel or !stop.
    • "cancelled" indicates that you have stopped processing the command in response to the TwitchShouldCancelCommand bool being set to true.
    • "sendtochat {message}" Send a message directly to twitch chat.
    • "multiple strikes" Indicates that the issued command is going to cause more than one strike, so should disable the internal strike tracker in order to avoid flooding the chat with "VoteNay Module {id} got a strike! +1 strike to {Nickname}" for as many strikes as will be awarded.
    • "award strikes {count}" Used after "multiple strikes" to actually award the strikes to the author of the command. Be sure this is done AFTER the command that is going to cause multiple strikes is processed, before returning control to twitch plays.
  • A Quaternion object to change the orientation of the bomb; this is only useful for very specific scenarios that require re-orientation of the bomb to inspect the module (e.g. Perspective Pegs, Rubik's Cube)

You could also interact directly with your own KMSelectable objects without passing them through to TP:KTaNE.

Be aware that the coroutine should yield return something if the command given is valid and should interact with the module in some way; doing nothing may cause TP:KTaNE to misinterpret the response as an ignore command response. You can yield return anything you want to achieve this, so long as it isn't a special case like above.

Also, be aware that TP:KTaNE plays the commands out in a single-queue and doesn't dispatch the actions simultaneously. To this fact, ensure that your command response doesn't halt the coroutine excessively, unless the module's design explicitly infers this style of behaviour (like releasing a button on a specific condition).

Additional Implementation Tasks

All of the tasks mentioned below are purely optional, but allow for a richer user-to-module interaction. Again, these can be public or non-public as before, but must still be non-static.

bool TwitchShouldCancelCommand;

Declaring this field is a way for the Advanced Implementation to be notified that it should cancel command processing. If you define this and see in the code that the value of the field is set to true, then stop processing the command, clean up, then do a yield return "cancelled" to acknowledge the cancel.

string TwitchManualCode;

Declaring this field allows you to specify the manual that is looked up on The Manual Repository when !{id} manual is entered into chat.

string TwitchHelpMessage;

Declaring this field allows you to specify the help message displaying examples of how to input the command, when !{id} help is entered into chat. Use the {0} string token to denote where the module's ID should be inserted into the help text.

Examples

Easy/Simple Option

Semaphore test code:

public string TwitchHelpMessage = "Move to the next flag with !{0} move right or !{0} press right. Move to previous flag with !{0} move left or !{0} press left.  Submit with !{0} press ok.";
public KMSelectable[] ProcessTwitchCommand(string command)
{
    if (command.Equals("press left", StringComparison.InvariantCultureIgnoreCase) ||
        command.Equals("move left", StringComparison.InvariantCultureIgnoreCase))
    {
        return new KMSelectable[] { PreviousButton };
    }
    else if (command.Equals("press right", StringComparison.InvariantCultureIgnoreCase) ||
        command.Equals("move right", StringComparison.InvariantCultureIgnoreCase))
    {
        return new KMSelectable[] { NextButton };
    }
    else if (command.Equals("press ok", StringComparison.InvariantCultureIgnoreCase))
    {
        return new KMSelectable[] { OKButton };
    }

    return null;
}

Advanced Option

Colour Flash test code:

public string TwitchManualCode = "Color Flash";
public string TwitchHelpMessage = "Submit the correct response with !{0} press yes 3, or !{0} press no 5.";
public IEnumerator ProcessTwitchCommand(string command)
{
    Match modulesMatch = Regex.Match(command, "^press (yes|no|y|n) ([1-8]|any)$", RegexOptions.IgnoreCase);
    if (!modulesMatch.Success)
    {
        yield break;
    }

    KMSelectable buttonSelectable = null;

    string buttonName = modulesMatch.Groups[1].Value;
    if (buttonName.Equals("yes", StringComparison.InvariantCultureIgnoreCase) || buttonName.Equals("y", StringComparison.InvariantCultureIgnoreCase))
    {
        buttonSelectable = ButtonYes.KMSelectable;
    }
    else if (buttonName.Equals("no", StringComparison.InvariantCultureIgnoreCase) || buttonName.Equals("n", StringComparison.InvariantCultureIgnoreCase))
    {
        buttonSelectable = ButtonNo.KMSelectable;
    }

    if (buttonSelectable == null)
    {
        yield break;
    }

    string position = modulesMatch.Groups[2].Value;
    int positionIndex = int.MinValue;

    if (int.TryParse(position, out positionIndex))
    {
        positionIndex--;
        while (positionIndex != _currentColourSequenceIndex)
        {
            yield return new WaitForSeconds(0.1f);
        }

        yield return buttonSelectable;
        yield return new WaitForSeconds(0.1f);
        yield return buttonSelectable;
    }
    else if (position.Equals("any", StringComparison.InvariantCultureIgnoreCase))
    {
        yield return buttonSelectable;
        yield return new WaitForSeconds(0.1f);
        yield return buttonSelectable;
    }
}

Monsplde, Fight! Test code:

//In the header portion of your project, include this item, then hook it up in the prefab.
public KMBombInfo Info;

//The following line was present in the void start() routine.
    Info.OnBombExploded += BombExploded;

bool exploded;  //Used to detect when the bomb has exploded.
int strikesToExplosion;  //Used to count how many strikes it took for "BOOM" to explode the bomb.
void BombExploded()
{
    exploded = true;
}

//The following lines were present in the void OnPress(int btnID) routine to deal with the multi-strike explosion
    if (MD.specials[moveIDs[buttonID]] == "BOOM" && CD.specials[crID] != "DOC")
    {
        while (!exploded)
        {
            Module.HandleStrike();
            strikesToExplosion++;
        }
        //BOOM!
        Debug.LogFormat("[MonsplodeFight #{0}] Pressed BOOM!", _moduleId);
    }

//Now the actual ProcessTwitchCommand routine. 
IEnumerator ProcessTwitchCommand(string command)
{
    int btn = -1;
    command = command.ToLowerInvariant().Trim();

    //position based
    if (Regex.IsMatch(command, @"^press [a-zA-Z]+$"))
    {
        command = command.Substring(6).Trim();
        switch (command)
        {
            case "tl": case "lt": case "topleft": case "lefttop": btn = 0; break;
            case "tr": case "rt": case "topright": case "righttop": btn = 1; break;
            case "bl": case "lb": case "buttomleft": case "leftbuttom": btn = 2; break;
            case "br": case "rb": case "bottomright": case "rightbottom": btn = 3; break;
            default: yield break;
        }
    }
    else
    { 
        //direct name with "use"
        if (Regex.IsMatch(command, @"^use [a-z ]+$"))
        {
            command = command.Substring(4).Trim();
        }

        //direct name without "use"
        if (command == MD.names[moveIDs[0]].Replace('\n', ' ').ToLowerInvariant()) btn = 0;
        else if (command == MD.names[moveIDs[1]].Replace('\n', ' ').ToLowerInvariant()) btn = 1;
        else if (command == MD.names[moveIDs[2]].Replace('\n', ' ').ToLowerInvariant()) btn = 2;
        else if (command == MD.names[moveIDs[3]].Replace('\n', ' ').ToLowerInvariant()) btn = 3;
        else yield break;
    }
    if (btn == -1) yield break;

    yield return null;

    //Special case to catch when the chat command orders "BOOM" to be pressed against anyone other than DocSplode.
    if (MD.specials[moveIDs[btn]] == "BOOM" && CD.specials[crID] != "DOC")
    {
        yield return "multiple strikes";
        OnPress(btn);
        yield return string.Format("award strikes {0}",strikesToExplosion);
    }
    else
    {
        OnPress(btn);
    }
}