PDA

View Full Version : Windowlist entries only on client?



Moon Wizard
February 27th, 2009, 21:03
OK, I've come across a particular behavior with shared database nodes that I was hoping to confirm my understanding.

Background
I've been building some code to be able to drag effects from a character to the combat tracker. The idea is that you can drop an effect on a combat tracker entry, and the effect will automatically be added to the effect list for that entry.

Behavior
When a client drags an effect to a combat tracker entry, the client attempts to find an empty window or create a new window to contain the effect that was dropped. The client is able to populate the fields on an empty window, but any created windows only exist on the client, not the host.

I assume that this occurs because the owner of the combat tracker is the host (unlike the character sheets). Given that the client should only have read access to the combat tracker, it seems strange that the client can edit the value of existing nodes, and even create nodes which are not propagated to the main database.

Questions
* Is my understanding of the situation correct?
* Is there any way to enable clients to synchronously add nodes to a parent node owned by the host?

Thanks,
JPG

Foen
February 27th, 2009, 22:20
I'm keen to understand the same mechanics and have similar issues.

It is not clear to me how the FG-handled sharing (shareas and playercontrol tags) and the programatic sharing (addHolder etc) interact, and my guess is that programmatic sharing cannot replicate all the features of FG-handled sharing :cry:

Whenever I've built complex shared resources and tried to permit client-side control, I've run into problems. The only way I've solved these is by passing messages from client to host and getting the host to create the new entries in the shared lists.

At times this has become very unwieldy (the most extreme case involved the client creating a complex database structure in its own database space, and passing the path of that structure to the host, which then copied the structure across to a shared space).

A further guess is that the DB-sharing facilities in FG were designed primarily for character sheet data, and that our increased reliance on complex sharing (such as combat trackers) is pushing the underlying mechanisms to their limit, and beyond.

Stuart

Moon Wizard
March 6th, 2009, 08:15
Bump.

I have a feeling that this may potentially lead me to an understanding of why my rulesets have encountered the infinite loop issue on nested windowlists that are shared with the client.

Any information from the SmiteWorks team would be very helpful.

Thanks,
JPG

Moon Wizard
March 12th, 2009, 01:05
Bump. Looking for some guidance from SmiteWorks team.

Thanks,
JPG

Goblin-King
March 12th, 2009, 11:04
This issue is quite complex, and to completely understand what you're doing, I'd need to know a bit more about the data base structure, node ownerships and exactly what modifications you're trying to make. I'll try to make good guesses, but correct me if I'm wrong...

Just to clarify the ownership issue, the host is not actually considered an owner of a node at any time. The host can modify all nodes, and nodes that no player controls would be considered ownerless, which means only the host can access them.

There are indeed some UI design legacy issues here, mostly they would be aimed at protecting the host data base from unauthorized access. A client should only be able to create nodes under those it owns, and I think there's a bug here regarding that issue (the proper function in such cases should be failure, i.e. windowlist.createWindow and databasenode.createChild returning nil).

Basically, your view seems to be correct in that the created data only exists on the client. Nodes created under a parent node that a client owns are allowed, and should be properly created, as is the case with character sheets.

At this time, the only workaround I can suggest would be a separate node that the client owns, under which the data is placed. For example:



combattracker
id-00001 (targeted NPC)
effects
id-00001 (an effect created by the GM in the usual style)
name [string]
id-00002 (PC creating the effect)
targetedeffects
id-00001 (now referring to the NPC)
id-00001 (the first effect from the PC affecting the given NPC)
name [string]

If you gave ownership of a PC combat tracker node to the respective client, they could create these through scripting, and the combat tracker would need to display effects from multiple sources under the NPC's effect list.

(What a nice feature... a nice submission to the foundation maybe? ;) )

The Interface.requestNewClientWindow function was made for this purpose, i.e. to allow the client to request a node that it owns, and is used for notes in foundation, for example. Another, similar function, under the DB package would probably be the best way to create nodes for a purpose like this. There would still probably need to be some kind of a mechanism to prevent the client from overwriting the host database, which is not trivial.


It is not clear to me how the FG-handled sharing (shareas and playercontrol tags) and the programatic sharing (addHolder etc) interact, and my guess is that programmatic sharing cannot replicate all the features of FG-handled sharing
The playercontrol tag allows the creation of windows as a result of using the Interface.requestNewClientWindow function, as well as modifying the way the window is handled on the client, such as allowing it to be closed through the radial, or to be shared to others.

Implementing a windowinstance.shareAs function is a good feature request...

The addHolder function does not share the window, it just sends the data to the client(s). It's basically useful for preloading, making sure the user has access to the data on the next session, or simply for passing a block of data to the client.

Tenian
March 12th, 2009, 11:36
I did some work on one specific instance of this and eventually came up with the work around used in 1.4 (it might have changed in 1.4.1 or beyond) .

Here is the a subset of the effect code that created the problem. In this case the each combattracker entry has a single effects node under it:



function addEffect(eff_name, eff_expire, eff_savemod, eff_init, eff_applied_by)
-- Parameter validation
if not eff_expire then
eff_expire = "";
end

-- Break out the details of the new effect
local new_effectslist = CombatCommon.parseEffect(eff_name);

-- First, check for duplicate effects to overwrite
-- Or exit if the new effect is weaker

<code removed>

-- Next check for blank effects to fill in
if not target_effect then
for k, v in pairs(getWindows()) do
if v.label.getValue() == "" then
target_effect = v;
break;
end
end
end

-- Finally, if we haven't found a target, then create a window to hold the effect
if not target_effect then
target_effect = createWindow();
end

-- Add the effect details
target_effect.label.setValue(eff_name);
<code removed>

end


This code would function if there was a blank effect. If there was not a blank effect then target_effect = createWindow(); would fire. As the host this created a list entry that both the client and the host could see. As a client this would create an entry that only the client could see.

Since createWindow() did not have the desired effect, I tried rewriting that block of code to act directly on the database. Specifically I changed the blank effect check to:



if target_effect then
target_node = target_effect.getDatabaseNode()
else
if getDatabaseNode() then
print(getDatabaseNode().getNodeName());
else
print("bar");
end
target_node = getDatabaseNode().createChild();
print(target_node.getNodeName());
end
target_node.createChild("label","string").setValue(eff_name);


Originally I thought maybe the client list was somehow not bound to the database and getDatabaseNode() might not be returning anything. However I never get the "bar" result.

So I added the getNodeName() to see what was going on. Both the host and client output:
combattracker.id-00017-.effects.id-0000# where number varies depending on how many effects I've added.

The rub is the client created nodes do not show up on the host, nor do they appear in the db.xml. The client created nodes do not persist if FGII is restarted (not surprising since they aren't in the database). However they appear (via the print statements) to function as you would expect.

As a work around I ended up adding an onListRearranged handler to the effect list and creating a blank entry (if User.isHost) when no entry exists.

It seems odd that the client can manipulate existing nodes, but not create their own. Also the "phantom" nodes created when the client calls getDatabaseNode().createChild() are very confusing, everything appears as though this function executed correctly, even getNodeName() returns the expected path, however these nodes do not exist for anyone other than the client.

Moon Wizard
March 14th, 2009, 01:58
Thank you very much for your time, Goblin-King! Your clarification of ownership does help quite a bit.

There are 2 areas that I would like to discuss, and get any thoughts you might have.

First, let's talk about the shared combat tracker features that need write access. The combat tracker in the d20_JPG and 4E_JPG rulesets is linked to an ownerless node (combattracker) to which each client is added as a holder when they login.

Specifically, I've added the ability to drop attack rolls, damage rolls and effects onto the combat tracker by either the client or the host. The damage rolls will automatically adjust the hit points fields (wounds, temphp) accordingly; and the effects will automatically add the given effect to the combat tracker entry.

My experience so far is:
* Hit point field adjustments appear to work without any problems.
* Effect list items added by the client are not visible to the host, nor stored in the database.
* Existing effect list items can be edited by the client (label, duration string, save modifier, initiative count)

This leads me to believe that:
* Creation of non-leaf nodes by the client is not allowed, and will create client-only ghost nodes.
* Even though the client is only a holder, the holders can apparently edit leaf node field values (string, number).

I'm not sure if this is how it was designed, but how it appears to work. The problem with the requestNewClientWindow approach is that the window creation is asynchronous, which does not allow data to be set.

Questions/Comments
* Given your statements, it seems that depending on the current behavior is questionable.
* The only method I can think for reliably editing the combat tracker nodes through client actions is to create some sort of kludgy hack, where I use deliverMessage to pass a coded message to the host, which is then decoded and processed, similar to what Foen has suggested in other threads.
* Any other suggestions for how to edit ownerless window fields and lists from a client?
* Any other suggestions for how to share windows in a manner similar to what I am trying to do?

Second, I am seeing situations within the rulesets where windowlists within child windows of another windowlist appear to sometimes end up in an infinite loop. I believe that this is tied to how shared windows behave.

This happens both within the ownerless combat tracker and the client-owned player character sheet. In the combat tracker, there is an effects list for each combat tracker entry, just like foundation ruleset. In the player sheet, there is a list of power types which contain a list of powers, and each power has a list of attack lines and a list of effects in addition to their leaf fields.

In the combattracker_effects.lua in the foundation ruleset, there is a check to add a list entry when a list is empty. This check was originally implemented in the lists that were in the shared windows, but appeared to cause the infinite looping issue almost immediately. I assumed at the time that this was because the onInit calls on the host and each client were essentially in a race condition, and that they were each creating a node with the same ID. At that time, I could almost consistently reproduce the infinite looping issue by simply adding and removing a combat tracker entry.

I have since removed that check from the onInit function of all shared windowlists, and it only triggers on the machine when the toggle to show that list is pressed.

However, the infinite loop issue still happen in both the combat tracker and the player sheet. Unfortunately, the issue is essentially random, and does not occur for everyone; and I have yet to find a series of steps to reproduce the issue consistently. In the combat tracker, the window shows ghost combat tracker entries which are not clickable and do not have a menu on either host or client. In the player sheet, the window shows a ghost power which is not clickable or editable. I know it is in a loop of some kind, because the onInit function is firing continuously (based on debug output). As soon as the client which is causing the loop exits, then the ghost list item becomes a real list item which is editable and clickable.

Questions/Comments
* I have tried multiple ways to debug this issue, but the debugging is limited to simple print outputs. Also, even the os.time() function is disabled, so I can't even accurately compare execution patterns when control passes between client/host. Any other debugging options I should try?
* It appears that the issue only occurs in shared windows with nested windowlists. Is there any code in FG that you can see that may trigger a loop in the given scenarios?
* Any other suggestions?

Thanks,
JPG

Moon Wizard
March 14th, 2009, 02:05
Sample combat tracker entry:



<combattracker>
<holder name="BOB" />
<holder name="keaver" />
<holder name="Galeb" />
<holder name="Pitcher" />
<holder name="Hades" />
<holder name="DARTH" />
<id-00002>
<holder name="BOB" />
<holder name="keaver" />
<holder name="Galeb" />
<holder name="Pitcher" />
<holder name="Hades" />
<holder name="DARTH" />
<ac type="number">21</ac>
<ac_lock type="number">1</ac_lock>
<actionpoints type="number">1</actionpoints>
<actionpoints_lock type="number">1</actionpoints_lock>
<active type="number">0</active>
<apused type="number">0</apused>
<effects idcounter="4">
<holder name="BOB" />
<holder name="keaver" />
<holder name="Galeb" />
<holder name="Pitcher" />
<holder name="Hades" />
<holder name="DARTH" />
<id-00001>
<holder name="BOB" />
<holder name="keaver" />
<holder name="Galeb" />
<holder name="Pitcher" />
<holder name="Hades" />
<holder name="DARTH" />
<effectinit type="number">17</effectinit>
<effectsavemod type="number">0</effectsavemod>
<expiration type="string">encounter</expiration>
<label type="string">Marked</label>
</id-00001>
<id-00002>
<holder name="BOB" />
<holder name="keaver" />
<holder name="Galeb" />
<holder name="Pitcher" />
<holder name="Hades" />
<holder name="DARTH" />
<effectinit type="number">14</effectinit>
<effectsavemod type="number">0</effectsavemod>
<expiration type="string">endnext</expiration>
<label type="string">Dazed</label>
</id-00002>
<id-00003>
<holder name="BOB" />
<holder name="keaver" />
<holder name="Galeb" />
<holder name="Pitcher" />
<holder name="Hades" />
<holder name="DARTH" />
<effectinit type="number">17</effectinit>
<effectsavemod type="number">0</effectsavemod>
</id-00003>
</effects>
<fortitude type="number">16</fortitude>
<fortitude_lock type="number">1</fortitude_lock>
<friendfoe type="string">friend</friendfoe>
<healsurgeremaining type="number">4</healsurgeremaining>
<healsurgesmax type="number">8</healsurgesmax>
<healsurgesmax_lock type="number">1</healsurgesmax_lock>
<healsurgesused type="number">4</healsurgesused>
<healsurgesused_lock type="number">1</healsurgesused_lock>
<hp type="number">52</hp>
<hp_lock type="number">1</hp_lock>
<hptemp type="number">0</hptemp>
<hptemp_lock type="number">1</hptemp_lock>
<immediate_check type="number">0</immediate_check>
<init type="number">13</init>
<init_lock type="number">1</init_lock>
<initresult type="number">17</initresult>
<link type="windowreference">
<class>charsheet</class>
<recordname>charsheet.id-00001</recordname>
</link>
<name type="string">Sloan Titus</name>
<name_lock type="number">1</name_lock>
<reach type="number">1</reach>
<reflex type="number">19</reflex>
<reflex_lock type="number">1</reflex_lock>
<save type="number">0</save>
<save_lock type="number">1</save_lock>
<show_npc type="number">0</show_npc>
<space type="number">1</space>
<specialdef type="string">Save +5 vs. charm</specialdef>
<specialdef_lock type="number">1</specialdef_lock>
<speed type="number">6</speed>
<speed_lock type="number">1</speed_lock>
<status type="string">Light</status>
<token type="token">tokens/shared/Custom/Christian_70.png</token>
<type type="string">pc</type>
<will type="number">19</will>
<will_lock type="number">1</will_lock>
<wounds type="number">6</wounds>
<wounds_lock type="number">1</wounds_lock>
</id-00002>



Thanks,
JPG

Moon Wizard
March 16th, 2009, 21:22
Based on the information so far, I have been implementing a OOB message passing mechanism to allow clients to send special requests to the host using the deliverMessage function. It appears that by using deliverMessage, I am getting synchronous execution (i.e. client waits for deliverMessage to complete on host before continuing). I'm not depending on the behavior, but it appears to work that way.

Also, I have been attempting to strip all createNode, createWindow and createChild functions from running on the client anywhere other than in the character sheet. That's why I built the message passing mechanism to allow the host to make any changes needed by client actions.

The only creation functions I haven't been able to address directly are the ones that are tied to custom controls, which are dependent on fields they create internally in the database. (such as checkbox, checkcounter, etc.) The only way I can think to address the creation issue on the custom controls is to remove all fields from the custom controls, make them explicit in the windowclass, and have the custom control reference the explicit fields. This seems like it defeats the purpose of building encapsulated custom controls, but I can't think of another way to do it right now.

Any suggestions?

Thanks,
JPG

scytale2
March 16th, 2009, 22:54
Just a note to Goblin King:

The groups I run have switched back to an old version of the 4E ruleset now, due to slowdown, potentially as a result of this looping issue and thus we don't have this "effect placing" functionality any more. The facility of placing effects on the tracker (or for the DM on the token itself) significant speeds up gameplay, especially for those of us with laptops and a small screen and makes the game vastly more enjoyable to play.

So, can I lend some weight to try to resolve this issue? It seems that the development of the ruleset post 1.3 will be halted until it is resolved, as the slowdown from 1.4 onwards causes serious slowdown and potential damage to both my PCs.

Foen
March 16th, 2009, 23:53
JPG

The deliverMessage mechanism is asynchronous in that you cannot pass a result back to the client. So creating a window bound to a new database node (for example) doesn't pass a window handle back to the calling client. You cannot use Interface.requestNewClientWindow, as that doesn't return a window handle either.

As regards custom controls, I tend to use dynamic control creation only for unbound controls (not on principle, it just seems to end up that way) so haven't run into the problem you are citing. For static controls, creating the underlying database node is often good enough, as the control will automatically bind to it anyway.

On a separate, but related, issue I find that the lack of a grouping construct (other than subwindows) makes fully functional encapsulated control a little elusive. An issue I'd love to see solved.

Stuart

Bidmaron
April 16th, 2009, 02:29
I'd like to understand this issue better. Can someone direct me to a codeblock showing this messaging solution in action? I don't see why in the effects case the client can't just send a message with the client-owned node with the effects data and then just let the host create the entry on the CT.

Foen
April 17th, 2009, 05:42
The Labyrinth Lord ruleset (on the FG Wiki) uses message passing for the shared Notes feature. The basic message passing has three features implemented in chatmanager.lua:

- registerHandler() allows code on the host to register a Lua function to handle a given request type. I implemented it this way to allow extensions etc to register command handlers dynamically, rather than having them hard coded.
- sendCommand() allows code on the client to send a remote command to the host.
- executeCommand() is called from onReceiveMessage (in chat_chat.lua) to check if the incoming message is a remote request, and then passes it to a registered command handler. If the incoming message isn't a remote request, it returns nil and hence normal message processing occurs.

The Notes facility (in notemanager.lua) uses remote requests to create new notes on the client and to force the host to re-share the existing notes (I have found that client-side data can become stale).

Hope that helps

Stuart

Tenian
April 17th, 2009, 11:00
I believe TheBox is another pretty common example of message pass :)

Bidmaron
April 17th, 2009, 13:09
Thanks for the examples.

Bidmaron
April 18th, 2009, 14:22
As a workaround to preclude all this message-passing, why couldn't the onInit code of the CT (on the host only with isHost() check) just create a node (or check it already existing) for each player at the root level and then just have the client CT code make its effects as sub-nodes of the client-owned top-level node. I don't know enough yet to know how you'd trap the log-in of other players to set up their master nodes, but it seems like this kludge would work.

Moon Wizard
April 18th, 2009, 21:45
Bidmaron,

The challenge is that the client does not have ownership of the combat tracker node. Therefore the client is not allowed to create any data nodes within the combat tracker framework.

When a game is running and a client drops an effect on the combat tracker entry, the goal is to create a new effect data entry for that combat tracker entry.

There are a few other limitations as well:
* Only one client can own a node at a time.
* Only the host can assign an owner to a node.
* There is no function that the client can call to request ownership of a node.

In my rulesets, I ended up developing my own message passing system to handle this situation. In fact, it ended up being so useful, that I used the message passing for several features.
* Player whispers
* The Box rolls
* Apply effect to combat tracker entry
* Apply damage to combat tracker entry
* Client can end their own turn in the combat tracker
* Apply rest changes to combat tracker data when a character rests

Cheers,
JPG

Bidmaron
April 19th, 2009, 21:42
Thanks for the explanation, Moon_wizard. I'm going to have to study your code to see how you're doing it.