PDA

View Full Version : How to pass data besides deliverMessage()?



Brenn
November 14th, 2008, 14:28
I know how I could do this through deliverMessage(), but quite frankly it's a bit clunky.

I have a table that represents a pool roll in my ruleset, is there a way to pass the table from client to host and vice versa, without me having to parse it through the ChatManager?

I'm thinking you could do this through the database, but haven't thought that angle out. I'd rather not do it that way either, since this roll data doesn't really need to be in there. I think I might be missing something simple here and therefore am asking the question.

EDIT: edited the title to be a bit more clear ;)

Foen
November 14th, 2008, 16:14
There isn't an easy way to do this, the two methods I have used are using chatmanager (this is built into the foundation ruleset) or adding sub-nodes to a client-owned database node, which has an onChildUpdate handler on the host side. Both are clunky.

Foen

Brenn
November 15th, 2008, 01:24
Thanks Foen, I thought I'd check before I clunked something together.

I can parse through ChatManager, and not touch the database. However the database is easier. Is there a drawback to using the database? Syncing issues or anything?

EDIT:
More Info might be better. I'm separating the one rolls from the chat and putting them into a tracker because of the manipulation of the rolls that occurs in the progress of the game. I need to push the rolls to all clients. Any good ideas conceptually? I don't particularly need code, just ideas.

Attatched is a screenshot, the tracker is on the left side. Also note I've done absolutely nothing with skinning. This is obviously not a d20 game despite the logo ;)

Foen
November 15th, 2008, 07:29
The problem with using the database is that shared ownership of database nodes isn't possible. A client session can 'own' a node (have write access) or 'hold' it (have read-only access) with the restriction that only one client can be the owner but mulitple clients can be holders. So if you want to pass info to the host using the database, each client needs to have its own different node and the host has to watch each of them and retrieve the data.

Furthermore (as if that wasn't difficult enough) there isn't an atomic operation which creates or copies a fully-populated node, so the host needs to know when a client has finished building the node before it tries to retrieve it. And you thought using the database would be easier ;)

The problem with using the chatmanager is that the comms is easier from client to host than it is from host to client. Normally that is alright, because once the host has received a request from a client the host has pretty much full access to the client anyway and can push results directly into the client database.

For information, I use the chat-based approach in the Foundation and Call of Cthulhu rulesets and the database approach in the forthcoming Rolemaster ruleset. The chat-based approach works as follows:

Chatmanager receives incoming messages from clients and checks if they match a particular pattern.
If they don't match then normal chat processing is used.
If they match the pattern, then the chatmanager assumes they are a command from the client and invokes the host-side command handler.
Host-side code can dynamically register a handler function for any command name, which will be invoked using the syntax myhandler(command,text,sender). The command parameter is the command name registered by the host, the text parameter is a string sent by the client and the sender is the User.getUsername() of the client sending the command.

This sounds complex, but here is an extract of the code needed to send a string from the client to the host using this mechanism:



In a host script module:
function onInit()
if User.isHost() then
--[[ only allow hosts to handle incoming greetings ]]
ChatManager.registerCommandHandler("greeting",greet);
end
end

function greet(command,text,sender)
--[[ handle the incoming greeting from a client,
in this case just print it to the chat window ]]
local msg = {};
msg.font = "systemfont";
msg.text = text .. " from " .. sender;
ChatManager.addMessage(msg);
end

In the client script block:
function sendGreeting(greet)
if not User.IsHost() then
--[[ only allow clients to invoke the send ]]
ChatManager.sendCommand("greeting","hello world");
end
end


The first function registers the command handler (in this case, a function called 'greet') which will be used to deal with any incoming command with the 'greeting' command name.

The second function handles commands received from clients that have the 'greeting' command name. In this example it just puts a message on the host chat window, a bit like a whisper.

The third function sends the command from the client to the host. It specifies which command name should be used ('greeting' in this case) and what string parameter to send. Anything which can be encoded as a string can be passed to the host, including die rolls etc.

Behind the scenes, this is supported by some added functionality in the chatmanager script, which you can find in the Foundation ruleset.

Hope that helps

Foen

Brenn
November 15th, 2008, 11:01
Behind the scenes, this is supported by some added functionality in the chatmanager script, which you can find in the Foundation ruleset.

Hope that helps

Foen


100% with you. I'm going to use the chat. LOL yeah I just thought the DB would be easier. I'd love just to be able to pass tables, but truth be told doing it through chat isn't so bad except if I later expand the structure of the object representing the oneRoll.

Ahh, heck no, this is lua-


for k, v in pairs

to the rescue ;)

One last thing- is there a limit on the length of a string you can send with deliverMessage()?

I'll check out the Foundation ruleset over coffee right now.

Thanks Foen, you really went above and beyond there ;)

Foen
November 15th, 2008, 12:30
I'm not aware of a limit, but I wouldn't send anything too big. Plan B would be to construct the structure as a temporary node in the DB and once it is complete, send the node path to the host using the chat interface. The host could then delete the temporary node when it has finished with it.

Foen

Dachannien
November 15th, 2008, 19:04
Maybe SmiteWorks should add in a sort of Remote Procedure Call for FG:

A client calls a function, passing in a function-identifying code and some data. On the host side, the default handler just tosses it in the bit bucket, but if there are any handler functions registered for the RPC handler, then the handler functions are called. Each handler function should check to see if the function identifier matches, and then it could operate on the data provided to it, which would arrive as the same Lua data that was given to the function on the client (regardless of data type, length, etc.).

Brenn
November 15th, 2008, 23:51
Well, I've got it nailed going through chat. This will send any table you pass to it (I think, I've tried a few different types) through chat. There's recursion both in the construction of the string and the reconstruction of the table, so I would imagine that if the table were too large or too heavily nested that you might see stack overflow problems.

Anyway if any are interested here's what I did:

first of in onInit for Chat Manager:

controlChars = {["tableEsc"] = string.char(5),
["tableStartKey"] = string.char(15),
["tableEndKey"] = string.char(14),
["indexKey"] = string.char(6),
["stringKey"] = string.char(17),
["numberKey"] = string.char(18),
["booleanKey"] = string.char(19)};
controlPatternSet = "";
controlDecode = {};
for k, v in pairs(controlChars) do
controlPatternSet = controlPatternSet .. "%" .. v;
controlDecode[v] = k;
end
notControlPatternSet = "[^" .. controlPatternSet;
notControlPatternSet = notControlPatternSet .. "]";
controlPatternSet = "[" .. controlPatternSet;
controlPatternSet = controlPatternSet .. "]";

The control pattern characters can be changed here if necessary.

then in chat_chat

function onReceiveMessage(messagedata)
local tableEsc = ChatManager.controlChars["tableEsc"];
if string.sub(messagedata.text, 1, string.len(tableEsc)) == tableEsc then
-- Strip the table escape character before passing it on to ChatManager.
ChatManager.onTableReceived(string.sub(messagedata .text, (string.len(tableEsc)+1)));
return true;
end
end

and back to ChatManager

function deliverTable(aTable, recipientList)
if type(aTable) == "table" then
local tableStr = constructTableString("", aTable);
local msg = {};
msg.font = "systemfont";
msg.text = tableStr;
msg.dice = {};
msg.diemodifier = 0;
msg.dicesecret = false;
if User.isHost() then
msg.sender = GmIdentityManager.getCurrent();
else
msg.sender = User.getIdentityLabel();
end
if recipientList ~= nil then
deliverMessage(msg, recipientList);
else
deliverMessage(msg);
end
end
end

function constructTableString(currStr, aTable)
local msg = currStr;
local idxType = "";
if msg == "" then
msg = controlChars["tableEsc"] .. controlChars["tableStartKey"];
end
for k, v in pairs(aTable) do
if type(k) == "string" then
idxType = controlChars["stringKey"];
elseif type(k) == "number" then
idxType = controlChars["numberKey"];
elseif type(k) == "boolean" then
idxType = controlChars["booleanKey"];
end
msg = msg .. controlChars["indexKey"] .. idxType .. k;
if type(v) == "table" then
msg = msg .. controlChars["tableStartKey"];
msg = constructTableString(msg, v);
elseif type(v) == "string" then
msg = msg .. controlChars["stringKey"] .. v;
elseif type(v) == "number" then
msg = msg .. controlChars["numberKey"] .. tostring(v);
elseif type(v) == "boolean" then
msg = msg .. controlChars["booleanKey"] .. tostring(v);
end
end
msg = msg .. controlChars["tableEndKey"];
return msg;
end

function onTableReceived(tableStr)
local buildTable = {};
local finalTable = {};
local function builder(cont, val) table.insert(buildTable, {control = cont, value = val}) end;
builder(string.gsub(tableStr, "("..controlPatternSet..")".."("..notControlPatternSet.."*)", builder));
-- remove final null record. It appends the length of the string at the end... Commented out for now.
--table.remove(buildTable,table.maxn(buildTable));
if controlDecode[buildTable[1].control] == "tableStartKey" then
--strip the initial table start key for the initial loop of reconstructTable.
table.remove(buildTable, 1);
finalTable = reconstructTable(buildTable);
if finalTable.tableType == "oneroll" then --process the table.
OneRollEngine.onOneRollReceived(finalTable);
else
print("ChatManager: Unknown table type delivered.");
end
else
print("ChatManager: Invalid table construction string delivered.");
end
end

function reconstructTable(buildTable)
local reconTable = {};
local subBuildTable = {};
local subTable = {};
local haveSubTable = false;
local currIndex = "";
local currIndexType = "";
local haveIndex = false;
local currVal = "";
local currValType = "";
local haveVal = false;
local outerDone = false;
local i = 1;
while not outerDone do
if controlDecode[buildTable[i].control] == "tableStartKey" then
local j = i + 1;
local k = 1;
local done = false;
local tableStartKeyCount = 0; --to track nested tables.
while not done do
subBuildTable[k] = table.remove(buildTable, j);
if controlDecode[subBuildTable[k].control] == "tableStartKey" then
tableStartKeyCount = tableStartKeyCount + 1;
elseif controlDecode[subBuildTable[k].control] == "tableEndKey" then
if tableStartKeyCount == 0 then
--no nested tables.
done = true;
else
--nested tables present.
tableStartKeyCount = tableStartKeyCount - 1;
end;
end
if j > table.maxn(buildTable) then
done = true;
end;
k = k + 1;
end
subTable = reconstructTable(subBuildTable);
haveSubTable = true;
subBuildTable = {};
k = 1;
elseif controlDecode[buildTable[i].control] == "tableEndKey" then
return reconTable;
elseif controlDecode[buildTable[i].control] == "indexKey" then
i = i + 1; --the index key will immediately be followed by a key to indicate the type of index.
currIndexType = controlDecode[buildTable[i].control];
if currIndexType == "stringKey" then
currIndex = buildTable[i].value;
elseif currIndexType == "numberKey" then
currIndex = tonumber(buildTable[i].value);
elseif currIndexType == "booleanKey" then
if buildTable[i].value == "true" then
currIndex = true;
else
currIndex = false;
end
end
haveIndex = true;
elseif controlDecode[buildTable[i].control] == "stringKey" then
currVal = buildTable[i].value;
currValType = "string";
haveVal = true;
elseif controlDecode[buildTable[i].control] == "numberKey" then
currVal = buildTable[i].value;
currValType = "number";
haveVal = true;
elseif controlDecode[buildTable[i].control] == "booleanKey" then
currVal = buildTable[i].value;
currValType = "boolean";
haveVal = true;
else
print("ChatManager: Invalid control character during table reconstruction.");
end
if haveIndex then
if haveSubTable then
reconTable[currIndex] = subTable;
haveSubTable = false;
haveIndex = false;
elseif haveVal then
if currValType == "string" then
reconTable[currIndex] = currVal;
elseif currValType == "number" then
reconTable[currIndex] = tonumber(currVal);
elseif currValType == "boolean" then
if currVal == "true" then
reconTable[currIndex] = true;
else
reconTable[currIndex] = false;
end
end
haveVal = false;
haveIndex = false;
end
end
i = i + 1;
if i > table.maxn(buildTable) then
outerDone = true;
end
end
end

Just call ChatManager.deliverTable and pass it the table you want to send. I haven't done extremely robust testing, but it appears to be working ok so far- at least for what I'm doing with it.

I'm an extremely casual programmer so there are more than likely ways this can be improved or done better. Plus troubleshooting recursion problems makes my head hurt ;)