Starfinder Playlist
  1. #1

    "Implementation" for adding character language to /mood

    I'm posting here to give an idea to other modders before I forget about what work I have put into it and it is lost.

    ---

    In the title "implementation" is in quotes because I am a JavaScript engineer. I don't know LUA or other languages related to Unity. I'm hoping that work on the regular expression (pattern) and basic business logic comes through and is helpful.

    Here is a simple, super rough implementation:
    (it basically uses and replaces `RegExp.prototype.exec()`
    https://developer.mozilla.org/en-US/...ts/RegExp/exec

    Code:
    function emptyStringAsUndefined(input) {
        if(input === "") {
            return undefined
        }
        return input
    }
    function parseMood(text) {
        const moodRegExp =/^\/mood\u0020+(?:(?<moodDefinition>\(\u0020*(?:(?<language>.*);\u0020*)?(?<mood>.*)\u0020*\)\u0020*))?(?<message>.*)/
    
        const result = moodRegExp.exec(text)
        if (result == null) {
            // use `null` for both null and undefined
            return null
        }
        const { groups } = result
        const moodDefinition = emptyStringAsUndefined(groups.moodDefinition)
        const language = emptyStringAsUndefined(groups.language)
        let mood = emptyStringAsUndefined(groups.mood)
        let message = emptyStringAsUndefined(groups.message)
    
        if (
            typeof language === 'undefined' && 
            typeof mood === 'undefined' && 
            typeof moodDefinition === 'string' && 
            moodDefinition.trim() !== ""
        ) {
            // Mood can be parenthesis consisting only of whitespace
            mood = moodDefinition.trimEnd()
        }
        
        if (
            typeof message === 'undefined' ||
            (typeof message === 'string' && message.trim() === '')
        ) {
            // consider messages made up of only whitespace to not be a match. It
            // can not be parsed or processed any further
            return null
        }
    
        if (typeof language === 'undefined' && typeof mood === 'undefined') {
            if (typeof message === 'string') {
                const trimmedMessage = message.trimStart()
                const firstSpaceIndex = trimmedMessage.indexOf(" ")
                if(firstSpaceIndex === -1) {
                    // if there is no message after assuming the first word is the
                    // mood, then consider it not to be a match
                    return null
                }
                const newMood = trimmedMessage.slice(0, firstSpaceIndex)
                const newMessage = trimmedMessage.slice(firstSpaceIndex + 1)
                if (newMessage === '') {
                    // if the resulting message is empty, then it is considered not
                    // to be a match
                    return null
                }
                mood = newMood
                message = newMessage
            }
        }
        return {
            groups: {
                ...groups,
    
                // Consider an empty string to be the same as undefined to simplify
                // consuming code.
                language,
                message,
                mood,
                moodDefinition
            }
        }
    }
    I imagine a real implementation parses a command first then sends the rest of the line to another parser based on what it finds. The use of null and undefined could be more consistent. There are a number of ways this can be improved.

    Code:
    /mood ([language]; [mood]) <message>


    More to come as I have exceeded the character limit for a single post.

    ---

    I have seen Zacchaeus's post regarding Feature Requests.
    https://www.fantasygrounds.com/forum...ature-Requests
    Idea informer appears to lack the formatting capabilities that I need to communicate what I want. I generally don't like having multiple credentials for the same community, even though I can appreciate it as a lower barrier to entry and better than nothing.

    I'm tired because of searching for a new job, fighting a bad mouse infestation, and a few other things going on. Once things get better I can see about properly using idea informer or looking at the tutorials.

    I can see that there are some tutorials in this forum. I am waiting for the caffeine to kick in. I have not yet found the part describing how to create new commands or modify existing ones. I'll keep digging after this post.

  2. #2
    Below is a super bare bones test runner I wrote in the same scratch file because I was too tired to set up proper Jest/Mocha tests.


    Code:
    function testParseMood(text, shouldMatch, expectedLanguage, expectedMood, expectedMessage){
        if (typeof text !== 'string') {
            throw new TypeError("Input must be a string of text characters.")
        }
        const result = parseMood(text)
    
        if (result != null && shouldMatch === false) {
            throw new SyntaxError("Expected input NOT to match")
        }
        if (result == null && shouldMatch === true) {
            throw new SyntaxError("Expected input to match")
        }
        if (result != null) {
            const { groups } = result
            const { language, mood, message } = groups
    
            const languageResult = typeof language === undefined 
              ? "no matches" 
              : language === "" 
                ? "an empty string"
                : `"${language}"`
            if (typeof expectedLanguage === 'undefined' && typeof language !== 'undefined') {
                throw new Error(`Expected no matches for "language" but received: ${languageResult}`)
            } else if (typeof expectedLanguage === 'string' && language !== expectedLanguage) {
                throw new Error(`Expected language to match "${expectedLanguage}" but received: ${languageResult}`)
            }
    
            const moodResult = typeof mood === undefined 
              ? "no matches" 
              : mood === "" 
                ? "an empty string"
                : `"${mood}"`
            if (typeof expectedMood === 'undefined' && typeof mood !== 'undefined') {
                throw new Error(`Expected no matches for "mood" but received: ${moodResult}`)
            } else if (typeof expectedMood === 'string' && mood !== expectedMood) {
                throw new Error(`Expected mood to match "${expectedMood}" but received: ${moodResult}`)
            }
    
            const messageResult = typeof message === undefined 
              ? "no matches" 
              : message === "" 
                ? "an empty string"
                : `"${message}"`
            if (typeof expectedMessage === 'undefined' && typeof message !== 'undefined') {
                throw new Error(`Expected no matches for "message" but received: ${messageResult}`)
            } else if (typeof expectedMessage === 'string' && message !== expectedMessage) {
                throw new Error(`Expected message to match "${expectedMessage}" but received: ${messageResult}`)
            }
        }
    }
    function testAll(testParams) {
        testParams.forEach(function testEachParams(testParameters, testIndex){
            const testNumber = testIndex +1
            const [
                testShouldMatch,
                testExpectedLanguage,
                testExpectedMood,
                testExpectedMessage,
                testText
            ] = testParameters
            try {
                testParseMood(
                    testText,
                    testShouldMatch,
                    testExpectedLanguage,
                    testExpectedMood,
                    testExpectedMessage
                )
                console.log(`PASS [TEST ${testNumber}]`)
            } catch (testException) {
                console.log(`FAIL [TEST ${testNumber}]: ${testException.message}\n${testException.stack}`)
            }
        })
    }
    const allTestParams = [
        // Both language and mood
        /* 1*/[true, "Ancient Dwarven", "Very Angry", "I'll not party with an Elf!", "/mood (Ancient Dwarven; Very Angry) I'll not party with an Elf!"],
    
        // Mood only
        /* 2*/[true, undefined, "Very Angry", "I'll not party with an Elf!", "/mood (Very Angry) I'll not party with an Elf!"],
    
        // Language Only - indicated by trailing semi-colon
        /* 3*/[true, "Ancient Dwarven", undefined, "I'll not party with an Elf!", "/mood (Ancient Dwarven;) I'll not party with an Elf!"],
    
        // No Parenthesis - everything before the first space is the mood
        /* 4*/[true, undefined, "I'll", "not party with an Elf!", "/mood I'll not party with an Elf!"],
        /* 5*/[true, undefined, "Angry", "I'll not party with an Elf!", "/mood Angry I'll not party with an Elf!"],
        /* 6*/[true, undefined, "Very-Angry", "I'll not party with an Elf!", "/mood Very-Angry I'll not party with an Elf!"],
    
        // If a mood is provided, but no message, then it is not a match
        /* 7*/[false, undefined, "I'll", undefined, "/mood I'll"],
        /* 8*/[false, undefined, "Very Angry", undefined, "/mood (Very Angry)"],
    
        // Empty parenthesis
        /* 9*/[true, undefined, "()", "I'll not party with an Elf!", "/mood () I'll not party with an Elf!"],
        /*10*/[true, undefined, "()", "I'll not party with an Elf!", "/mood () I'll not party with an Elf!"],
    
        // Mood parenthesis with only whitespace
        /*11*/[true, undefined, "(   )", "I'll not party with an Elf!", "/mood (   ) I'll not party with an Elf!"],
    
        // No space between parenthesis and message
        /*12*/[true, undefined, "()", "I'll not party with an Elf!", "/mood ()I'll not party with an Elf!"],
    
        // No space between language and mood
        /*13*/[true, "Ancient Dwarven", "Very Angry", "I'll not party with an Elf!", "/mood (Ancient Dwarven;Very Angry) I'll not party with an Elf!"],
        
        // Leading spaces before language or mood
        /*14*/[true, "Ancient Dwarven", "Very Angry", "I'll not party with an Elf!", "/mood (   Ancient Dwarven;Very Angry) I'll not party with an Elf!"],
        /*15*/[true, undefined, "Very Angry", "I'll not party with an Elf!", "/mood (   Very Angry) I'll not party with an Elf!"],
    
        // Semi-colon present, but no language
        /*16*/[true, undefined, "Very Angry", "I'll not party with an Elf!", "/mood (;Very Angry) I'll not party with an Elf!"],
        /*17*/[true, undefined, "Very Angry", "I'll not party with an Elf!", "/mood (   ; Very Angry) I'll not party with an Elf!"],
        /*18*/[true, undefined, "Very Angry", "I'll not party with an Elf!", "/mood (   ;Very Angry) I'll not party with an Elf!"],
    
        // no leading command
        /*19*/[false, undefined, undefined, undefined, "(Ancient Dwarven; Very Angry) I'll not party with an Elf!"],
    
        // wrong leading command
        /*20*/[false, undefined, undefined, undefined, "/badmood (Ancient Dwarven; Very Angry) I'll not party with an Elf!"],
    ]
    testAll(allTestParams)
    Open about: blank in a web browser, open the developer console, paste the code above into it and it should just work. Well, anyone comfortable with JavaScript should. If you aren't, you should never paste foreign code into the developer console as it comes with risks.

    I imagine a hypothetical /say or /speak command could be implemented similar to mood. Maybe a keyword or special value could be used to indicate that the command should inherit the language currently selected in the chat window.
    Last edited by willrune; February 12th, 2024 at 17:17.

  3. #3
    I may have been a bit naïve. I'll have to use Idea Informer when I am feeling better, but I can at least point it to this thread from Idea Informer for more formatted information.

    XML on its own isn't too scary, especially given its relationship to HTML4/5, SVG, and XHTML. LUA, at first blush looks similar enough to JavaScript that I can wrap my head around it, but I imagine the global context/window is very different from one in Node.js or a Web Browser.

    It looks like modders won't be able to do this for a few reasons. One being the underlying Comm (Program Communications) class will probably need to be updated.

    https://fantasygroundsunity.atlassia...996644567/Comm

    A simple semi-colon and specific order of parameters might not be the right solution if we want to establish a pattern for commands that can be used elsewhere or be consistent with an existing pattern.

  4. #4
    Brainstorming out loud

    slash commands are a way of calling methods with certain parameters/arguments as parsed from a string of text (such as from the chat window, developer console, or a command line terminal).



    Is the command bound to the active character?

    The "/mood" command appears to be, but I don't know enough about the underlying class method to be sure.

    What happens if the active character does not know the language provided?

    - No Output (silent failure)
    - Chat Window Error Message (user only)
    - Chat Window Error Message (user and DM)
    - Console Error Message (user only)
    - Console Error Message (user and DM)
    - Fallback to Common (probably undesirable if the intent is to use a specific language)
    - Fallback to null (probably undesirable if the intent is to use a specific language)
    - ...something else?

    What if the language doesn't exist?

    The same thing that happens if a language is added to a character sheet that has not been added to the game and associated with font by the GM.

    It will follow the same rules of case sensitivity that are already in place.

    What if the text is for someone other than the active character?

    Then "/mood" is the wrong interface. The underlying communications class may need new business logic.

    What if the text is not for a character or spoken word?

    Again, we are no longer talking "/mood" at this point.

    Like something written in a letter or inscribed on an item? I don't think that is supported yet. At that point the language system would need to support traits related to the senses. Sight and Sound more commonly. Rarely "Touch" for something like brail (sp?). Its a fantasy world, maybe you have an unusual race communicating through smell or taste or some custom sense.

    Either way this is probably out of scope for a first pass if all you are looking to do is enable slash commands or the chat bubble UI / text type. At this point you may need a new inline or block level text type and new UI elements (or updated existing ones) that understand that type to display text in a specific language without giving away the underlying content to characters or users that don't have access to that language.

    The UI should not identify what the language is unless the character knows it or is under the effect of something like the "tongues" or "comprehend languages" spells.

    If the content has been translated by a third party, there may be a toggle similar to identified/unidentified items that only the GM can toggle. (does this restriction still hold if it is the user that created the language text block to begin with? this could get murky)

    This quickly becomes a large, complicated task.
    Last edited by willrune; February 12th, 2024 at 19:00.

Thread Information

Users Browsing this Thread

There are currently 1 users browsing this thread. (0 members and 1 guests)

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  
STAR TREK 2d20

Log in

Log in