• 0

    posted a message on embedding ace libs vs. version control
    Thanks that's very helpful -- exactly what I needed to know!
    Posted in: Lua Code Discussion
  • 0

    posted a message on embedding ace libs vs. version control
    Sorry for the long rambly post... complex topics are hard for me to condense...

    I am wondering what are the "best practices" for using the ace libraries as embedded libs in my addon, in terms of getting, using, and redistributing the appropriate version of Ace with my addon's embedded /Libs/ folder.

    I got an error report today from one of the user's of my addon (SpeakinSpell) which revealed to me that Ackis Recipe List is using a significantly newer version of the Ace GUI libs than the one I embedded in SpeakinSpell. So that newer version is getting loaded instead of the one I tested/debugged against, and it appears to cause a conflict in my code (which I'll debug when I get home this evening)

    Having seen some Lua errors pop up in Ace libs since 3.3.5 (in other addons which have since been updated to make those errors go away, and I don't know if they fixed the problem by changing how they use Ace, or by updated Ace itself) I've been keeping my eye out for a new release of Ace3. Even though the SVN logs show ongoing work on Ace3, the last release tag was 7 months ago, and that's the version I'm using, embedding, and redistributing in my addon. But it's not the latest available.

    I'm pretty experienced with SVN from using it at work, but the one thing I have never messed with in SVN are externals. I hope I'm using the right word for that. I mean where you reference an outside SVN repository for code you share within your own project, like Ace3. i.e. to create a subdirectory in my code that would be pulled in from an external SVN repository.

    As it is, without using the function of SVN externals, I took the last public release of Ace3, selected the individual components that I actually use, and COPIED them into my SVN tree. Changes to Ace3 are completely independant of changes to my own addon, and my process for the past year and a half has been to take newer versions of Ace3 as I discover them, and copy them into my Libs folder, and check them into my SVN repository, as completely parallel source code control history.


    So I have a few questions that are all tangled up with each other...

    1) Should I stop doing it that way, and switch over to always pulling the HEAD revision off the Ace3 SVN repository (possibly through use of SVN externals), since the Ace team isn't tagging releases?

    2) Is it kosher to use and redistribute the HEAD revision of Ace3 code even though the Ace3 team hasn't tagged it as a release?

    3) Am I wrong about the Ace3 team not tagging releases? I am looking for release versions here:
    http://wow.curse.com/downloads/wow-addons/details/ace3.aspx
    and here:
    http://www.wowace.com/addons/ace3/

    On those pages, I can see that file changes were committed to the Ace3 SVN repository within the past 2 hours, but a release hasn't been tagged in 7 months.

    Are the individual libraries being tagged instead? That is, should I be looking for Ace-GUI specifically, instead of Ace3 as a whole? Or does the Ace team just not tag releases, and if so, am I expected to use the HEAD revision?

    Or is there an Ace4 or something that has replaced Ace3??

    4) Is there another way to identify the latest stable version of Ace3 libs, other than SVN tags, or what I find embedded in other authors' addons? (like the newer version I found in Ackis Recipe List today) I'm hesitant to follow a practice of always pulling the HEAD revision, because that's essentially the alpha quality code, and I don't know if the Ace team wants me to redistribute that or not.

    I don't know if the Ace team is consistent about updating the version numbers that drive the LibStub logic to load only the latest version. If I pull from the HEAD revision, and some other author pulls from the HEAD revision, are we safe from pulling two versions of code that are actually different, even though they both (Accidentally) specify a version=12 (or whatever number)? Does the Ace team have that automated by a script, or just consistent enough about remembering to update it every time you commit a change to each code file?


    I've been making a living as a software dev for over 10 years, and I have seen this kind of thing handled dozens of different ways. Every company and every dev team works their source code control and update distribution processes differently. This is just another incarnation of what C++ Windows programmers call "DLL Hell" and I took the Microsoft approach to give up on sharing and copy all the libs I use into my folder and use my local copies... except that embedded libs like this use the nice LibStub method to only load the newest available version, which is what put me back in "DLL Hell" today and prompted this post.

    I just want to know how I can work with the Ace3 team instead of against them, to use a version of those libraries (which are so very helpful to me!) without introducing conflicts or redistributing bad versions of their libs that they'd rather I not redistribute.

    Should I just pull the HEAD revision off the Ace3 SVN? Or is there a better way to identify the best redistributable version that I should be using?

    Should I continue to copy my embedded version into my own folders, or should I set that up to point directly at the Ace3 SVN instead of using my own copy? I never modify the Ace3 source code other than to overwrite it with a newer version.
    Posted in: Lua Code Discussion
  • 0

    posted a message on JustSTFU AddOn, I'm fairly new >.>
    You have a very big conditional statement that must pass before it will SendChatMessage. If it doesn't SendChatMessage, then obviously the condition is evaluating to false... but it tests too many things to be able to see it clearly... break it up into one condition at a time to debug how far it gets


    if (event == "CHAT_MSG_WHISPER") then
     print("CHAT_MSG_WHISPER")
     if list then -- i have to question where this list comes from, it's suspicious
      print("list is not nil")
      if type(source)=="string" then
       print("source is a string")
       if source~="" then
        print("source is a non-empty string:"..source)
        if type(list[source])=="table" then -- this condition is the most suspicious to me, probably expecting type "string" here??
         local name = source --this is implied by your intent, but missing from your code
         local reason = list[name] -- again I'm guessing at your data structure
         SendChatMessage("This user is ignoring you. Reason:"..reason,"WHISPER",nil,name)
        end
       end
      end
     end
    end
    
    Posted in: Lua Code Discussion
  • 0

    posted a message on AceComm/CTL how to cancel a failing transfer?
    Thanks for the helpful recommendations everyone.

    I reworked my hack to CTL (posted above) into a stand-alone lib, as follows...

    any comments, criticism, suggestions welcome :)

    -------------------------------------------------------------------------------
    -- SilentSendAddonMessage.lua
    --
    -- A simple hook into the SendAddonMessage API
    -- to keep it from generating error messages for bad channels
    -- such as "you are not in a guild" and "no such player as 'player' is online"
    --
    -- This is intended to hide the issue of interrupted data transfers
    -- when sending a queue of data in whispers to a player who goes offline.
    -- or when leaving guild during a similar bulk data transfer
    --
    -------------------------------------------------------------------------------
    
    
    -------------------------------------------------------------------------------
    -- INIT
    -------------------------------------------------------------------------------
    
    local _G = _G
    local VERSION = 1 -- Dec 3, 2009
    
    -- only load once
    if _G.SilentSendAddonMessage then
    	return
    end
    
    -- create
    _G.SilentSendAddonMessage = {}
    _G.SilentSendAddonMessage.version = VERSION -- for future use
    
    
    -------------------------------------------------------------------------------
    -- THE SILENCER FUNCTION
    -------------------------------------------------------------------------------
    
    
    _G.SilentSendAddonMessage.func = function ( prefix, text, chattype, target )
    	if chattype == "WHISPER" and not UnitIsConnected( target) then
    		--print("SilentSendAddonMessage: player is not online:"..target)
    		return
    	elseif chattype == "GUILD" and not IsInGuild() then
    		--print("SilentSendAddonMessage: you're not in a guild")
    		return
    	else
    		return SilentSendAddonMessage.Orig_SendAddonMessage( prefix, text, chattype, target )
    	end
    end
    
    
    -------------------------------------------------------------------------------
    -- REPLACE global SendAddonMessage with our silent version
    -------------------------------------------------------------------------------
    
    -- hooksecurefunc would call our function AFTER the original function
    -- but we need to be called first, so we can abort the original call
    
    _G.SilentSendAddonMessage.Orig_SendAddonMessage = _G.SendAddonMessage
    _G.SendAddonMessage = SilentSendAddonMessage.func
    
    
    
    -------------------------------------------------------------------------------
    -- FOR FUTURE USE
    -------------------------------------------------------------------------------
    
    -- sample code and notes 
    -- for reference purposes in case blizzard changes it on us
    
    -- Currently (WoW 3.2.2) the party, raid, and battleground channels 
    -- do not generate error messages from the blizzard API, 
    -- even if they are invalid channels, i.e. because you left the group
    -- so we don't bother to validate those channels in the simpler function above
    
    -- the table-driven function below seemed slightly slower 
    -- and it's unnecessary since the group channels don't show error messages
    
    --[[
    
    
    local SAFETYCHECKS = {
    
    ["WHISPER"]		= function( t ) 
    	--NOTE: no handling here for error from attempting cross-faction, 
    	-- but that's not something that will change in the middle of processing a queue
    	return UnitIsConnected( t ) 
    end,
    
    ["GUILD"]		= function( t ) 
    	return IsInGuild() 
    end,
    
    ["PARTY"]		= function( t ) 
    	-- GetNumRealPartyMembers() > 1
    	-- WoW 3.2.2 is silent on this error, so we can just allow it
    	return true 
    end,
    
    ["RAID"]		= function( t ) 
    	-- GetNumRealRaidMembers() > 1
    	-- WoW 3.2.2 is silent on this error, so we can just allow it
    	return true 
    end,
    
    ["BATTLEGROUND"]= function( t ) 
    	-- this check would require a loop to correctly detect prep time
    	-- WoW 3.2.2 is silent on this error, so we can just allow it
    	return true
    end,
    
    } -- end SAFETYCHECKS
    
    
    _G.SilentSendAddonMessage.func = function ( prefix, text, chattype, target )
        if not SAFETYCHECKS[ chattype ]( target ) then
            print("SilentSendAddonMessage: bad target: "..tostring(target or chattype) )
            return
        end
        return SilentSendAddonMessage.Orig_SendAddonMessage( prefix, text, chattype, target )
    end
    
    
    --]]
    
    Posted in: Lua Code Discussion
  • 0

    posted a message on AceComm/CTL how to cancel a failing transfer?
    Thanks!

    UnitIsFriend seems to work long distance, but maybe like you said that could have been because my test subject was in my party.

    I like the idea of UnitIsConnected. I didn't know about that API - nice!

    My only concern about that would be retaining a check that I'm not trying to whisper from alliance to horde or vice-versa, which is part of what UnitIsFriend gives me. Maybe both should be used? I'll experiment with it more. I don't want it to spit out errors if I try to whisper invisible addon data to a mob or cross-faction.

    I can't find any info on those APIs for GetRealNum... what's the difference between those and the equivalent GetNum functions?
    Posted in: Lua Code Discussion
  • 0

    posted a message on AceComm/CTL how to cancel a failing transfer?
    I must admit I've never thought about the problem of people dropping out of parties. This causes error messages also?
    After working with it a bit tonight I can safely say that party, raid, and BG channels don't complain, but guild and whisper do.

    I tried this change in CTL, and it works good enough for me for silencing errors from failing to send to guild or 'no such player.'

    local SAFETYCHECKS = {
        ["WHISPER"]		= function( target ) return ( UnitIsFriend( "player", target )			) end,
        ["GUILD"]		= function( target ) return ( GetGuildInfo( UnitName("player") ) ~= nil ) end,
        ["PARTY"]		= function( target ) return ( true --[[doesn't show errors--]]			) end,
        ["RAID"]		= function( target ) return ( true --[[doesn't show errors--]]			) end,
        ["BATTLEGROUND"]= function( target ) return ( true --[[doesn't show errors--]]			) end,
    }
    
    function CTL_SafeSendAddonMessage( prefix, text, chattype, target )
        if not SAFETYCHECKS[ chattype ]( target ) then
            --print("CTL_SafeSendAddonMessage: bad target: "..tostring(target or chattype) )
            return
        end
        return _G.SendAddonMessage( prefix, text, chattype, target )
    end
    
    function ChatThrottleLib:SendAddonMessage(prio, prefix, text, chattype, target, queueName, callbackFn, callbackArg)
    
    -- replace the 2 uses of _G.SendAddonMessage in this function with CTL_SafeSendAddonMessage
    
    


    I realize this is just a limited hack. AceComm and SpeakinSpell above this in the software stack both think the transfers went through. The callbackFn doesn't relay the failure. Also CTL mistakenly thinks it used the bandwidth, which it could recover. /shrug. That doesn't bother me very much. The error messages are masked, which is basically all I cared about.

    I don't want to release a rogue CTL_VERSION=22 into the wild though...
    Posted in: Lua Code Discussion
  • 0

    posted a message on AceComm/CTL how to cancel a failing transfer?
    SpeakinSpell allows users to write their own custom speeches for announcing when they cast spells or encounter other game events. It's for glorified powerful macros, except they trigger automatically from the API event notifications instead of writing actual macros that do "/cast some spell \n /s say something about it". SpeakinSpell detects the UNIT_SPELLCAST_SENT (and many other event hooks) and picks a random speech from your list to say something into the chat.

    The data I'm sharing is mostly the list of macro speeches = user-created content that they want to share with each other. Those tables can be very large lists of hundreds of text strings.
    Posted in: Lua Code Discussion
  • 0

    posted a message on AceComm/CTL how to cancel a failing transfer?
    Regardless of how much I compress my data, if I shrink 100 chunks down to 30, and my target user goes offline after the first chunk... instead of seeing 99 errors that no such player is online, now I only see 29... either way my chat frame is flooded and it looks like an equally bad problem. I want to see 0 errors, or at worst 1 error.

    I already check for valid channels(and whisper targets) before sending the whole table through AceComm.

    • UnitIsFriend( "player", target ) - checks if a target player is a valid whisper target... I have not actually tried this yet to check for a player who recently DC'd, but I doubt the API caches the player's friend faction status to return true for an offline player (does it??)
    • GetNumPartyMembers() > 1 - to check if PARTY is a valid channel
    • GetNumRaidMembers() > 1 - to check if RAID is a valid channel
    • GetGuildInfo( UnitName("player") ) is not nil - checks if GUILD is a valid channel
    • SpeakinSpell:IsInBG() - performs a slightly more sophisticated check whether BATTLEGROUND is a valid channel (I copied this function call out of somewhere else in my code and I forget off-hand why it's not a raw blizzard API call)
    (that should cover everything supported by SendAddonMessage)

    Why not just put that logic immediately before the call to the raw blizz API for SendAddonMessage inside CTL? It wouldn't have to wipe the queue - just skip that one transfer. I don't think AceComm would even need to know - for all it cares it could consider it a completed transfer.

    I'll probably try that...

    What's the worst that would happen there? The far end reconnects and gets the last few chunks of the table, which fails to Deserialize? So I could ignore it and move on, or send back a fresh sync request to start over.

    I just dont want my users to see a flood of error messages in the chat for something that's supposed to be a silent background task...
    Posted in: Lua Code Discussion
  • 0

    posted a message on AceComm/CTL how to cancel a failing transfer?
    I just started using AceComm-3.0, which is based on ChatThrottleLib (CTL) which is based on SendAddonMessage, to add some data sharing to my addon using invisible chat channels.

    It's coming together nicely and I have it mostly working. While I'm aware of some general issues I have with it, ways it could be more optimized, and better automation around these features, there is only one problem that I ran into so far that I'm not sure how to solve.

    If I'm in the middle of sending a large data transfer to another player using the WHISPER addon channel, and that player goes offline during the transfer... CTL keeps trying to send the rest of the message chunks (I'm inclined to call them packets, but AceComm and CTL code comments both call them chunks), which results in very many error messages in the chat saying "no player named 'playername' is online"

    I'd like to make it detect this and stop trying to send the rest of the large data table I was trying to send. The table I'm sending is about 70kb, so it takes about 100 chunks, which can result in a lot of errors... I know I could make the table smaller, but regardless of the total amount of data, this is still a problem.

    So I started using the callback function option of AceComm/CTL so that it calls my function after each chunk. This let me debug it a little more with print statements to show progress on the transfer which has also helped me understand a lot better about how this works.

    I have taken that callback function as an opportunity to double check that the target player is still online, and/or check for similar cases like if I'm sending a bulk of data to the PARTY channel but I dropped out of the party.

    But from what i can tell, AceComm and CTL don't check for these things, and they don't check the return value from my callback function either, so I'm not seeing any way to cancel the data transfer when I can see that the rest of it will fail.

    I am considering the following options:
    • find out if there's a way to use AceComm more correctly to fix this problem? maybe the feature is there but I don't see how to use it?
    • hack AceComm / CTL code to fix this issue by changing this third-party library (I'm a little hesitant to do this even though I have all the code)
    • find an updated version of AceComm that fixes this problem
    • look for another library that doesn't have this problem

    Any suggestions?
    Posted in: Lua Code Discussion
  • 0

    posted a message on General Practise: named arguments, optional arguments
    I'm intrigued. What's oUF?
    Posted in: Lua Code Discussion
  • 0

    posted a message on wowace/curse sync not happening?
    It's understandable. As long as it's not just me :)
    Posted in: General Chat
  • 0

    posted a message on wowace/curse sync not happening?
    I tagged a release in SVN today about 4-5 hours ago. It showed up on wowace.com right away, but still does not appear on curse.com. Is anyone else seeing this issue? I've seen it take a while before, and I've read that the packager runs on a fixed schedule so it's not always immediate, but it's never taken this long before. How long should I wait before trying to retag it?
    Posted in: General Chat
  • 0

    posted a message on General Practise: named arguments, optional arguments
    The purpose of my addon is to detect when a game event happens (originally when you cast a spell with UNIT_SPELLCAST_SENT, but enhanced over the past year to include additional events) and announce that spell casting or other event in the chat, like "/p <caster> cast <spellname> on <target>"


    My DefaultEvent object is actually defined once in SpeakinSpell.DEFAULTS.DetectedEvent. I don't create it as a local in OnSpeechEvent as I showed there - I was oversimplifying my code above to show how I was passing function parameters.

    I didn't set it up that way for GC because I didn't know that GCing a table was such a big deal. I did it that way so I only had to define the default values once in a single place that's easy to find and change - it makes sense for default values to be global)

    As I understand it, the only issue with passing a table as a function parameter is not the way you pass the data to the function call - it's just that the table must be GCed eventually. If I created a table without passing it to a function, it would be the same issue, right? We're not talking about a problem that occurs from pushing the table pointer onto the stack when making the function call (unless I'm missing something?)

    I assume you would raise an equal complaint about some of the other functions I've written that use a local table of functions to achieve something like a switch-case statement, instead of a series of if-else conditions... GCing that table costs CPU cycles that I would not lose from refactoring that code into a series of if-else conditions.

    On a side note, a function that can take 100+ optional args? Ouch. I'd refactor that bitch into a handful of more specific functions. It'll be a hell of a lot easier to maintain too.


    The 100+ optional args mostly drive a single generic subroutine that does string replacements out of a table using string.gsub.

    For each Table.keyname = value, I replace substrings to change bracketed "<keyname>" pattern matches to the corresponding "value". Each call to OnSpeechEvent can specify custom string substitutions by setting DetectedEventStub.keyname = value. This includes string substitutions like <target> <caster> <spellname> <rank> etc. Info about the event that you can use in a string (a speech announcing the event in chat, ex. "/p <caster> cast <spellname> on <target>")

    Many of those <substitutions> strings are things I want to guarantee have some value defined for any speech for any event, so I set up default values for those in SpeakinSpell.DEFAULTS.DetectedEvent. But if the blizzard API event notification provides more accurate info, I use that instead of the defaults. The blizz API event parameters vary a lot, which is the main reason I set up my string replacement code to operate on a table like this.

    I don't have much other functional logic that looks at those optional values, like if-else conditions, so it doesn't make sense to split it into separate functions.

    As I enhance it to be able to announce more kinds of game events, I use this optional parameters table to drive customizing the string substitutions for each event, with maximum shared code.

    For example the most recent one I added was a handler for COMBAT_LOG_EVENT_UNFILTERED with SPELL_DAMAGE to detect when you deal a critical strike. My design made it easy to add DetectedEventStub.damage to support a speech like "/p My <spellname> just crit for <damage> damage - woot!". All I had to do was define DetectedEventStub.damage = damage, and no additional code was needed to implement the <damage> substitution because I built my framework to enable me to do that easily.

    Plus, more importantly I think to the topic of the OP, I didn't have to go back and change any of the other event hooks that I already created to deal with my new damage parameter, because I set it up as an optional parameter using a table similar to the method described in the OP.

    What I hear you saying about GC is that it's a bad performance hit on the CPU to create and delete a table - there's some overhead there. So I could optimize the performance of this design somewhat by reusing some of these tables more instead of making copies in a few places. I've never noticed this performance hit yet, but I assume it will become more evident as my addon grows to support more functions, so as defensive as I may be about my design, I do appreciate the criticism.
    Posted in: Lua Code Discussion
  • 0

    posted a message on General Practise: named arguments, optional arguments
    OK that makes sense now, thanks :)
    Posted in: Lua Code Discussion
  • 0

    posted a message on General Practise: named arguments, optional arguments
    I use this approach for code maintenance purposes. My addon, SpeakinSpell, has a main function called OnSpeechEvent which I call in close to 100 different places, and that function supports upwards of 100 optional arguments. inspired by my experience with kwargs in python, I architected my code to use named parameters with optional values using a table as described above (though I don't use that syntax style - I write it as two lines)

    I used to use a named argument list and pass nil values where I wanted it to be unspecified / use a default (before I figured out how to use tables to achieve something like python's kwargs). But as I added more features to my addon, that meant adding more calls to that function, and more optional arguments... and that led me down a path where maintaining the older code to search/replace/update all the calls to OnSpeechEvent to add more parameters to a named parameter list in OnSpeechEvent( arg1, arg2, arg3, etc ) turned into a nightmare.

    So I architected my code to use tables for passing optional named arguments like kwargs in python in order to make my code self-maintaining and easier to enhance. Now I use a lot of code fragments like the following...

    I don't understand why you're saying this is such a bad idea, because I've never noticed a performance hit from doing this. But if I should be doing this a better way, please explain, because of course I'd always love to do better at this if I can...

    
    
    -- this ValidateObject function provides a nice readable way to implement default values
    -- if any key is missing from obj, it will be imported from DefaultObject
    function SpeakinSpell:ValidateObject( obj, DefaultObject )
    	for key,val in pairs(DefaultObject) do
    		if obj[key] == nil then -- NOTE: don't replace "false" values
    			if type( DefaultObject[key] ) == "table" then
    				obj[key] = self:CopyTable(DefaultObject[key])
    			else
    				obj[key] = DefaultObject[key]
    			end
    		end
    	end
    end
    
    
    -- This is the function I call in 100 places with different parameters
    function SpeakinSpell:OnSpeechEvent( DetectedEventStub )
    	local DefaultEvent = {
    		name = "UNKNOWN",
    		type = "EVENT",
    		rank = "",
    		target = UnitName("target"),
    		caster = UnitName("player"),
    -- this is pseudo-code.  There are more default parameters than this in my real code, and many are not validated with defaults like this - they just get passed down into subroutines and used in a string.gsub() function
    	}
    
    	local DetectedEvent = SpeakinSpell:ValidateObject( DetectedEventStub, DefaultEvent )
    	
    	-- do stuff with the DetectedEvent object here
    end
    
    
    -- The rest of these functions are RegisterEvent event handler functions
    -- Note the variety of parameter lists I want to pass into OnSpeechEvent
    -- and the flexibility that tables (kwargs) have given me this way
    
    function SpeakinSpell:UNIT_SPELLCAST(event, caster, target, spellname, spellrank)
    
    	-- process the spell
    	local DetectedEventStub = {
    		-- event descriptors
    		type = event,
    		name = spellname,
    		rank = spellrank,
    		-- event-specific data for substitutions
    		caster = caster,
    		target = target,
    		spellid = self:GetSpellID(spellname, spellrank),
    	}
    	self:OnSpeechEvent( DetectedEventStub )
    end
    
    function SpeakinSpell:ACHIEVEMENT_EARNED(event, AchievementID)
    	local IDNumber, Name, Points, Completed, Month, Day, Year, Description, Flags, Image, RewardText = GetAchievementInfo( AchievementID )
    	local DetectedEventStub = {
    		type = "ACHIEVEMENT",
    		name = L["me"], -- DisplayName = "Achievement Earned by <name>"
    		-- NOTE: we don't want the name of this achievement to be in the type or name of this stub, defined above, 
    		--		because we want ALL differently-named achievements to share the same speech event settings
    		--		so we'll use an event-specific custom data name to support substitution the name of this achievement in the speech
    		achievement = Name,
    		-- and the achievement link also relays the achievement name, via the standard <spelllink> substitution
    		spelllink = GetAchievementLink( AchievementID ),
    		-- and standard meaning for target and caster OF THE EVENT
    		target = UnitName("player"),
    		caster = UnitName("player"),
    		-- and let's include (most of) the rest of the achievement info for substitutions, why not?
    		points = Points,
    		desc = Description,
    		reward = RewardText,
    	}
    	self:OnSpeechEvent( DetectedEventStub )
    end
    
    function SpeakinSpell:BARBER_SHOP_OPEN()
    	local DetectedEventStub = {
    		type = "NPC",
    		name = L["Enter Barber Chair"],
    	}
    	self:OnSpeechEvent( DetectedEventStub )
    end
    
    
    Posted in: Lua Code Discussion
  • To post a comment, please or register a new account.