MegaGlest Forum
Archives (read only) => Glest Advanced Engine => General discussion => Topic started by: daniel.santos on 19 December 2008, 11:46:50
-
I'm chopping up the networking code now, so I wont have an 0.2.11a. Instead, I'm going to try to bring a number of things in line that I've wanted to do for some time and it's appearing as though its also the best way to achieve the desired results. I will post again when I have something to test. I'll also try to get a post out that outlines the new networking classes architecture for those technically interested.
In short, it will be hybrid client-server/peer-to-peer, support network game pause and speed change (after agreement from all players), and each network connection will have it's own thread to receive data and use IPC to notify other (interested) threads when that data is ready. Unfortunately, I'll have to have a thread for each network connection (which really isn't all that bad). It's very technically possible to do this with just one thread (::WaitForMultipleObjects() on windows and select() or pselect() on linux), but that would require me to implement pthread support in all of the Glest threading classes -- actually, that's only if I want t have a thread be able to wait on either I/O or an IPC eventm, so SCRATCH THAT! ... Take Two: I would have to modify the Socket class to be able to use select() (linux) or WaitForMultipleObjects() (windows) to support that -- hah! I'm glad I wrote this post because I overlooked that (my earlier design was going to use thread conditions for inter-thread signalling).
Either way, one thing is should provide is less lag and a better game synchronization. Also, the peer-to-peer support is optional, so if two clients can't talk to each other (i.e., firewall or routing issues), but they can both talk to the server, the server will transparently relay events as it does now. This will also set the stage for supporting the ability for other clients to take over as server should the server drop.
I already did a pretty big re-write of the network messaging layer, and that's going largely unchanged (except to add support for exchanging status data better).
EDIT: Oh yea, and one more thing. I think this game really needs UDP support. So if anybody out there is in the mood to examine how the messaging layer works and is good ad UDP-based protocols, please knock yourself out. I'm actually good at it myself, but it's a time issue. We would also probably have to modify the NetworkMessage classes to specify an importance, so some packets would need to be delivered, need to be in order and arrive within X time, while others can be out or order and get dropped and it's no big deal (obviously, corrupt is never ok and UDP's checksum is too small to be reliable, so our own checksum would need to be tagged on as well).
-
Sounds good. I'm planning on doing "Internet Protocols And Services" unit in the first semester so I could look into it when I'm finished with the GUI, if you haven't done it yourself by then.
-
-
hah! I said it was a work in progress and I already changed the states. I realized that it wont ever allow you to be in progress of changing both pause and speed states at the same time, so we don't need them separate. So here are the new states
enum State {
STATE_UNCONNECTED, // not yet connected
STATE_LISTENING, // not yet connected, but listening for connections
STATE_CONNECTED, // established a connection and sent (or sending) handshake
STATE_INTRODUCED, // handshake completed
STATE_READY, // ready to start the game
STATE_LAUNCHING, // starting game
STATE_PLAY, // game started (normal play state)
STATE_PAUSED, // game paused
STATE_QUIT, // quit game requested/initiated
STATE_END, // game terminated
STATE_COUNT
};
enum ParamChange { // T - target frame required or not
PARAM_CHANGE_NONE, // n - no pending state changes
PARAM_CHANGE_REQUESTED, // y - state change requested
PARAM_CHANGE_REQ_ACK, // y - pause request acknowledged
PARAM_CHANGE_REQ_ACCEPTED, // y - pause request accepted
PARAM_CHANGE_REQ_DENIED, // y - pause request denied
PARAM_CHANGE_COMMITED, // n - state change committed (paused, unpaused, speed changed, etc.)
PARAM_CHANGE_COUNT
};
enum GameParam {
GAME_PARAM_NONE,
GAME_PARAM_PAUSED,
GAME_PARAM_UNPAUSED,
GAME_PARAM_SPEED,
GAME_PARAM_COUNT
};So now ParamChange is used to pause, unpause and change speed where GameParam will specify which parameter of the game is being changed (new target speed specified separately). Also, this doesn't mean that there always has to be approval, for instance, when you start the game you may specify that anybody can pause the game without confirmation, but speed changes require confirmation. You may also have speed changes only take effect 10 seconds after requested so there's time to warn everybody about the impending change.
If it's not too much work, I may add a mechanism to replace dropped connections with an Ai and also to allow dropped players to re-connect, although it will probably require forcing a pause (at least, it will be easiest to implement that way. :) ) I eventually want it to be something that players in a lobby (maybe spring lobby?) can browse games and there be a game they can join at any time, even though it's already started. I'm not in a hurry there though.
-
Argh my brain just melted! :mrgreen:
Seriously though I'm sure its gonna be awesome!
p.s I'm off to make a smiley of a brain melting.
-
lol!! Sorry, it was really intended more for hailstone and other C++/OO experienced people who would be interested :)
-
:lol:
Hi,
please explain me the following situation i didn't get how it will work:
We have two clients and one server. client are called C1 and C2 server is called S.
Everyone can reach each other ( so no routing is needed )
If C1 wants to execute a command he sends it directly to S and C2.
This command mus be executed with the next keyframe( or when? ).
But what happens if the command transmission needs too long to reach C2, but it reaches S in time?
The command will be executed on S and C1 but not on C2 in the same key frame.
How does C2 know that he has to wait for a command when he reaches the keyframe?
In the current implementation C2 knows that he has to wait for command from the server because he gets a command(probably empty one) for every keyframe. So C2 knows, he has to wait when he reaches a key frame without getting a command from the server.
How will this work in the new implementation?
-
I think this is how it works.
frameUpdateBegin is called to specify which frame is being referred to, messages such as commands are sent, then frameUpdateEnd is called to say that information for a frame has ended.
Is there a distinction made between a game host (the creator of the game) and a network host?
-
hurray for titi! I spent 20 minutes typing a response and then I learned some things from your question.
So first off, hailstone has it pretty close. Actually, frameUpdateBegin() and frameUpdateEnd() (for the network interface classes) are called before and after the "world" is updated. The only reason there are two calls is to mark the beginning of the frame (tell the ClientInterface or ServerInterface what the new frame is ) and do any pre-world update calculations and then the frameUpdateEnd() to tell the network layer to send (any) data queued up while updating the world. I want it to get sent at the end of updating the world so that it will be smaller (less packets means less headers and other overhead, plus if we're compressing the data, you get better compression ratios when compressing larger amounts of data).
But the short answer to titi's question, as it stands now is that I don't care about key frames anymore and I just send commands to everybody as soon as they are issued. More precisely, C1 would send the new command to both S and C2, S would know not to relay the command to C2 and both would process the command as soon as they received it (no matter which frame it was). However, I have a better idea now.
Before I go into that, the new class NetworkMessageStatus is used to transmit status changes between clients, servers and peers and is also the base class for many other messages (intro, ready, launch, command list, etc.). It's one of those bit mangeling classes, so let me just post it's current code (which is sure to change further). Note that the term "peer" is generic and can refer to any remote relationship (server to client, client to server or client to client)class NetworkMessageStatus : public NetworkMessage {
enum DataMasks {
DATA_MASK_SOURCE = 0x0000000fu,
DATA_MASK_STATE = 0x000000f0u,
DATA_MASK_PARAM_CHANGE = 0x00000700u,
DATA_MASK_GAME_PARAM = 0x00001800u,
DATA_MASK_GAME_SPEED = 0x0000e000u,
DATA_MASK_HAS_FRAME = 0x00010000u,
DATA_MASK_HAS_TARGET_FRAME = 0x00020000u,
DATA_MASK_FRAME_IS_16_BITS = 0x00040000u
};
uint8 connections; /** bitmask of peers to whom a connection is established */
uint32 data; /** contains various data packed into 32 bits */
uint32 frame; /** (optional) the current frame at the time this message was generated */
uint32 targetFrame; /** (optional) the frame that actions specified in this packet are intended for */
public:
NetworkMessageStatus(NetworkDataBuffer &buf, NetworkMessageType type = NMT_STATUS);
NetworkMessageStatus(const Host &host, NetworkMessageType type = NMT_STATUS,
bool includeFrame = true, GameSpeed speed = GAME_SPEED_NORMAL, uint32 targetFrame = 0);
virtual ~NetworkMessageStatus();
uint8 getConnections() const {return connections;}
bool isConnected(size_t i) const {assert(i < GameConstants::maxPlayers); return connections & (1 << i);}
uint32 getData() const {return data;}
uint8 getSource() const {return static_cast<uint8> (data & DATA_MASK_SOURCE);}
State getState() const {return static_cast<State> ((data & DATA_MASK_STATE) >> 4);}
ParamChange getParamChange() const {return static_cast<ParamChange>((data & DATA_MASK_PARAM_CHANGE) >> 8);}
GameParam getGameParam() const {return static_cast<GameParam> ((data & DATA_MASK_GAME_PARAM) >> 11);}
GameSpeed getGameSpeed() const {return static_cast<GameSpeed> ((data & DATA_MASK_GAME_SPEED) >> 13) ;}
bool isResumeSaved() const {return static_cast<bool> (data & DATA_MASK_IS_RESUME);}
bool hasFrame() const {return static_cast<bool> (data & DATA_MASK_HAS_FRAME);}
bool hasTargetFrame() const {return static_cast<bool> (data & DATA_MASK_HAS_TARGET_FRAME);}
uint32 getFrame() const {return frame;}
uint32 getTargetFrame() const {return targetFrame;}
virtual size_t getNetSize() const;
virtual size_t getMaxNetSize() const;
virtual void read(NetworkDataBuffer &buf);
virtual void write(NetworkDataBuffer &buf) const;
protected:
void init(const Host &host);
void setConnection(size_t i, bool value) {
assert(i < GameConstants::maxPlayers);
uint8 mask = 1 << i;
data = value ? data | mask : data & ~mask;
}
void setConnection(size_t i) {
assert(i < GameConstants::maxPlayers);
data = data | 1 << i;
}
void setConnections(bool *values) {
connections = 0;
for(size_t i = 0; i < GameConstants::maxPlayers; ++i) {
if(values[i]) {
data = data | 1 << i;
}
}
}
void setSource(uint8 value) {
assert(value < GameConstants::maxPlayers);
data = (data & ~DATA_MASK_SOURCE) | value;
}
void setState(State value) {
assert(value < STATE_COUNT);
data = (data & ~DATA_MASK_STATE) | (value << 4);
}
void setParamChange(ParamChange value) {
assert(value < PARAM_CHANGE_COUNT);
data = (data & ~DATA_MASK_PARAM_CHANGE) | (value << 8);
}
void setGameParam(GameParam value) {
assert(value < GAME_PARAM_COUNT);
data = (data & ~DATA_MASK_GAME_PARAM) | (value << 11);
}
void setGameSpeed(GameSpeed value) {
assert(value < GAME_SPEED_COUNT);
data = (data & ~DATA_MASK_GAME_SPEED) | (value << 13);
}
void setResumeSaved(bool value) {
data = value ? data | DATA_MASK_IS_RESUME : data & ~DATA_MASK_IS_RESUME;
}
void setFrame(uint32 frame) {
data = data | DATA_MASK_HAS_FRAME;
this->frame = frame;
}
void setTargetFrame(uint32 targetFrame) {
data = data | DATA_MASK_HAS_TARGET_FRAME;
this->targetFrame = targetFrame;
}
};
So this class conveys a lot of status information and packs it down into a maxiumum of 13 bytes. Using other techniques, I can get this thing down to 5 bytes max, but this is good enough for now. The connections byte specifies a bit mask of each peer that the originator of this message is connected to. The data section uses 4 bits to specify the originator. This isn't usually necessary because that information can be learned from the socket it comes in on its self, but it may be relayed from the server, so that way a client can know the update of the status of one of it's peers that it may not be able to communicate directly with. But most importantly, there is a frame and targetFrame.
The targetFrame was originally intended to coordinate pause and speed change requests so that it could all happen at the same time on every participant in the game. It occurred to me that a game could be kept better in sync if commands were not executed immediately on the local machine. Instead, they could be given a target frame in the future and queued up locally and on the peers so that the command will (ideally) be executed at the same time on every machine. By having this behavior on the server as well, it should cancel out the "home field advantage" (i.e., being the server wont have any advantage).
This does not go for auto-commands by the way, these are always executed locally and are not transmitted at all because it's presumed that (as long as the data is the same on each machine) they can each figure these out on their own. Perhaps it would be helpful to transmit these however so the server can verify that an auto-command being executed on a client is accurate and be able to correct a client if needed. As an example, if a client thinks his unit is close enough to see an enemy unit and attack it but on the server, they are one cell off (not an unlikely condition), the server can send correcting information to update both the unit that attempted to execute the auto command and the unit it thought it saw. It may look funny because the unit would start to attack but then warp back though, but that hopefully shouldn't take long (maybe 400 milliseconds).
As a side note on the NetworkMessageStatus class, it can still be trimmed down a lot. I'm not using the DATA_MASK_FRAME_IS_16_BITS field and I can also cram part of the frame bits into the unused portion of the data field and only use extra bytes for frame. Also, I can have one message that sends the full frame as a 32 bit number but is only sent when the top 24 bits change. The rest of the time, it can transmit only the lower 8 bits of the frame number. Perhaps even better, I can use part of the data to specify how many bytes of the frame and target frame I'm sending and the upper portion will be recycled from the previous update. I wont mess with that until after all of these other issues are resolved, but there's a lot that can be done in very few bits.
Also, one thing that is changing is the way clients wait for a laggy server. At present, if a client hasn't received an update from the server for a key frame, the client stops rendering, updating sound, etc., it essentially suspends the main thread until that is received or a max wait timeout has expired. Now, it will behave as though it's paused, so rendering continues, the mouse cursor continues to move around, sounds don't stop and you can still issue commands, etc. You may have never experienced this before unless you play a game where the server is very slow (like when you compile a debug build and you're debugging it with millions of sanity checks running a second :) ), but it's a pain to deal with. I have some other ideas for ways to address this that I'll worry about once this networking rework is done.
-
ok, I think i got it now but I'm not really shure if it will work.
Every client plays its own local game, influenced by commands and updates it gets from outside. You are no longer trying to keep the games of the clients in sync.
I think this will cause jumping units with nearly every update. Even after an update command which was sent from the server to the clients the currently units will not be in sync, because the update commands delay( network ) is different for every client !
By this you will play a whole game out of sync with nearly every unit, only hold together by server updates.
A typical problem this will cause:
You see one of your units next to a line of trees in the west. Now you give a command to walk to the west and your plan is to have your units noth of this line of trees. But when your command is executed on the server the unit is a bit more south of the tree line, so it will walk south of the tree line. Now if the updates for this unit arrives your client, suddenly you see your unit in the south of the tree line, not in the north as you want it. this will be something very annoying for players, because their commands are not executed the way they wanted/planned it.
But I must admit that this asyncronous implementation is very very complicated to understand( and to debug!!) and I probably didn't get it right now.
I must say that I would still prefer an improved version of the original way it was done.
-
IDEA!!!
What about this:
- Commands are bound to key frames.
- Clients don't wait for commands which doesn't reach them
- All Clients(including the server) have a rollback system, so they can rollback and execute commands in the past which reach them too late.
More excatly:
Before executing the commands in a keyframe you copy all current unit states into a list.
Now you store the commands that will be executed with this unit list.
Unit list and command list are your state for this key Frame.
After storing the state you execute the commands of this state.
This is done for every keyframe and let's say the last 10 keyframes are stored this way.
What happens if some commands reaches the client which were meant for an old keyframe?
Add the old commands to the command lists of the keyframes for which they where meant. .
Now restore the keyframe state of the oldest command and playback the whole game up to the current frame.
Now everything should be fine again.
If you are reached by a command which is meant for a keyFrame which is no longer in the rollback list, the game is really out of sync and you can stop with an error message or something else ( full update from server for example ).
By this you will get no more "waiting for server" lag and you can play with very bad conections without getting too much trouble.
BUT: This will probably very cpu/memory intensive ?
A problem that will be there are the update commands which are send by the server. These update commands are based on the servers state, which must not be correct because he doesn't has all information yet if you don't run a full server client concept. So I would prefer this client server concept because the server is always up to date.
What do you think of this?
-
I like the rollback idea. Would need checkpoints, like you said, so it doesn't redo the whole lot. It might have performance problems - think of how many transactions go into a database for a bank which would use such a system.
-
titi, thanks for the input. The idea of being able to rollback a command sounds good in some ways, but it also sounds very tedious to code and can have fairly significant CPU overhead (probably not much memory overhead though). I say "can" have because commands aren't sent all that often, so it shouldn't be too much of a load. None the less, it will still be a LOT of code to implement.
I think a better idea may be to use the "targetFrame" but have the lead time on commands vary according to the max lag being experienced in the game. That way, if a server gets a command late, it can (maybe) fast-forward the command and send updates to peers, but re-adjust the min lead time for the targetFrame of newly issued commands.
As far as key frames, I'm completely eliminating them except for using them to force clients to wait for the server and visa-versa when a client gets too far behind (maybe 1 second or so).
So right now, every command issued will be given a targetFrame, even locally. So if I'm a client on frame 421 and I want to tell my guys to walk somewhere, I send the command and queue it up locally to occur on frame 431 (for instance), a 10 frame lead time (or 250 mS). Since I'm also sending to peers, they should get it in time as well. Then, when I reach frame 431, it comes out of the command queue.
I think that most of the units jumping around that you've seen is due to the problem with the server not sending its commands!! :O I'm also going to change updates so that instead of units instantly appearing at their new location when an update is received, they will warp there over a period of 200-ish milliseconds so it's a little easier to see what's happening. Alternately, I may figure out a way to cause them to move to their updated location more casually (very fast walking for instance).
Either way, I have a lot of re-assembling (of the code) to do. Adding a networking thread will actually improve many characteristics of network play because the thread is only there to deal with net communications and I've done a LOT of multithreaded programming in my life (13-ish years?) Also, I just added a condition/event class to make synchronization more fun in addition to a wait() function in Socket that allows you specify multiple sockets to wait on -- that way you can use non-blocking socket calls, but keep your thread asleep until there's really something to do. This reduces CPU while allowing the most optimal response times to I/O (and only requires one extra thread). In addition, when the network thread receives data, it notifies the main thread so no more polling is needed (GameInteface::onReceive()).
I'll post an updated class diagram when I get home.
-
Is it really so hard to implement this rollback system?
To create a "checkpoint" you ?simply? have to clone the list of units(deep clone).
and add the list of commands which are sent by the server (or do you need a clone of them too? ) .
Thats all you need to have a checkpoint.
Now for a rollback:
1. remove all units from the current unit list.
2. add all units from the checkpoints list
now you should have the state of the old checkpoint.
The thing I don't really know is how to fast forward gamesplay back to the current frame.
Isn't there something like this "fast forward" already implemented in glest?
Or is this the point where the things get complicated?
-
Some interesting articles on multiplayer programming:
http://www.gamasutra.com/view/feature/3 ... g_for_.php (http://www.gamasutra.com/view/feature/3230/dead_reckoning_latency_hiding_for_.php)
http://www.gamasutra.com/view/feature/3 ... r_math.php (http://www.gamasutra.com/view/feature/3221/multiplayer_math.php)
@titi:
It seems like you are talking about period saving with loading latest save when out of sync rather than a transaction style rollback system.
-
I will think about the articles, probabaly there is something useful in them I will study them...
For my idea:
I'm not talking about transactions based on every command like its possible in databases for example. In my idea the checkpoints to which you can rollback are made by hand ( and there is more than one checkpoint, I talked about 10 of them! ).
If you say "begin Transaction" in a database system a "checkpoint" is made to which you can return with a rollback. In a database system it's in fact not a copy of the whole state, but it has the same effect. Most database systems log all changes made until a commit is made, but this is useless for glest, because you cannot log all changes(movements of units and so on).
But I'm definitly NOT talking about periodical savings. I talked about some kind of rollbacks to execute commands which didn't reach you in time.
This is especially interisting when you use UDP instead of TCP for communication which doesn't guarantee that messages arrive.
-
don't know if it could be useful ... i remember the old diablo network game that has some kind of jump coz of lag (56k connection) you was walking and suddenly a monster appairs and hit you...or you suddenly jump back coz you was hitten...i think they used a kind of sync check between server and clients if it doesn't match server information overhead clients ones.
Pretty annoying with lag but worked pretty good in other cases.
-
I don't think I'll be pursuing a transactional mechanism at this time, although I do intend to enhance the "fast forward" mechanism. Currently, when a command packet arrives late or an update arrives, the unit's state can be fast forwarded, but only to the point of finishing the current cycle of the skill they are on (or that is specified in the update). I'll be expanding this so that it can complete an iteration of a skill. But for now, I believe that adding a command delay should be sufficient to solve most of these issues. The initial value will be 250 milliseconds (10 world frames), but the server will adjust this as needed based upon ping times, I'm figuring making it the average ping time plus 2 world frames (50 milliseconds). This has the added benefit of removing some of the advantage that the player on the server would otherwise have, because everybody's commands will get executed at the same time (in theory).
If a command does arrive late, I can issue the command to the unit and then tell it to fast forward execution by however late it was and that will solve most of the problems associated with late arrival. It will not solve problems of the units being located in the wrong cell however (for instance, if they were walking and you issued a stop command that arrived too late). None the less, when a late condition is detected, an update can also be sent from the server (or requested by a client) for the effected units.
I'm home now, so I'm back on Linux, hurray! I'll redo the UML class diagram with the current design tomorrow or friday and post it for your viewing pleasure. But it's coming along, but it's a lot of rework (I'm mostly finished at this point).
-
Adjustable keyframe times based on server pings are a good idea!
I think this will make the game more stable and I can play with someone from america ( pings from 300ms up to 600ms ).
I would not lower the keyframes under 250 ms. I think 250 ms is the current value and it works quite OK.
I hope it will work, but I see lots of problems with your asyncronous way of playing the game. I fear it will result in an update orgy after a while.
But probably I'm wrong, I will help you where I can (tests ... :) ).
-
Hey everybody, sorry I haven't been on the forum much lately, I've been busy with other stuff and probably will continue to be so for another week. None the less, I have been doing some work on the networking code and I decided it was appropriate to do a reassessment of the requirements and design. Upon doing so, I discovered a lot of things that will further change the design of GAE, but I haven't decided how much of this should go into 0.2.12 yet so I present it here. Hailstone, any feedback would be appreciated. I think I have a pretty clear path forward for 0.2.12, taking the middle road.
Below are the requirements and the portion I'm thinking should go in 0.2.12. Perhaps at some point (very soon) I should write a formal word document and keep it in source control, but this is good enough for now. This may read as verbose to some, but this is the nature of a requirements/specification text: it must be verbose to eliminate ambiguity. Ambiguity kills. I've done software engineering, consulting, business, software and systems analysis for over a decade, trust me on this, I know what I'm talking about. The version in green is only my current proposed target version. It's once thing to discuss ideas, but when spelling out requirements prior to building an application, completeness is important. Imagine trying to build a building with incomplete blue prints. Anyway, here they are (I'll likely revise them, so look for a "LAST UPDATED") comment at the end of this post.
.
Definitions
First some definitions for clarity's sake:
- unexpected disconnect: describes a situation where a TCP socket error occurs (socket timed out, remote peer reset connection, no route to host, etc.) or the socket remains active, but no data has been received for a specified amount of time (currently 30 seconds and will probably remain that).
- intentional disconnect: one where a network message stating that the player has chosen to leave the game is transmitted prior to any other network failure, such as reset connect, etc.
- participant: For the sake of these requirements, a "participant" will mean any participant in a network game, either a client or server (and does not describe the relationship between two participants).
.
General Networking and Lobby
- [current]: Each player slot will be numbered (starting with one) and the first slot will be assigned to the local player by default (as is now) (added 3/5/2009 for clarity)
- [current]: Server (hosting of the game) should be able to change game settings while clients are still connecting (as is now)
- [0.2.12]: Clients should be able to connect when server is in lobby but there's not an open slot set to "network"
(i.e., be in limbo for a time while host changes settings). If a client attempts to connect when there is an open network slot, they will be assigned to the first available network slot (as is current behavior). If there are none, the new client will be assigned to the "limbo queue" while the server is still in the lobby. If the server subsequently opens a network slot, the client highest in the limbo queue will be automatically assigned to that slot. (modified 3/5/2009)
- [0.2.12]: Any clients who remain in the limbo queue when the server launches the game will be disconnected. (added 3/5/2009)
- [>=0.3]: In the future, the "limbo queue" will become the "spectator list" and any clients who reside in this spectator's list when the server starts the game will be allowed to observe the game as a spectator, but may not chat (except, perhaps with other spectators). This will override the previous requirement. (added 3/5/2009)
- [0.2.12]: Removing a network slot shouldn't kick a client (as above) until game is actually started (if there are connected clients who weren't given a slot). Instead, any clients assigned to the network slot that was removed will be kicked to the next available slot (if one is open) or sent to the limbo queue. (added 3/5/2009)
- [0.2.12]: Clients should see the current game settings. Every time the server changes them, it should update on clients so they can see the changes, discuss them in chat, etc.
- [>=0.2.12]: Server should be able to kick and ban a particular client at any time (in the lobby and in game)
- [>0.2.12]: Server should be able to move clients around to different network slots (in lobby) (currently, it's first come-first server). Server should also be able to move clients to and from the limbo queue. (added 3/5/2009)
- [0.2.12]: As mentioned before, networking will become peer-peer/server instead of client server (i.e., a hybrid of peer to peer and client/server). The server still maintains control of the game and "dictates" game state to clients, but clients communicate with each other (when possible) so the server doesn't have to relay actions by client commands -- this reduces lag time.
- [>=0.2.12]: When clients aren't able to connect to each other, the server will continue to relay commands.
- [0.2.12]: The server pings all of its clients and each client the server and all connected peers once per second. Ping messages (class Game::Net::NetworkMessagePing) already contain the local machine time of the originator and when returned, they get the local machine time of the recipient attached to it. This will now be used, along with the average latency, to determine the approximate local machine time of each network participant, paving the way for more advanced and efficient game/world management (these options are explored in the last two sections).
- [0.2.12]: The state of each client's connection or lack there of is communicated to the server periodically (maybe every 8 seconds or so) along with latency statistics. The client's latency to the server its self is also communicated. If a UDP-based network layer is later established, these statistics will include packet loss (corrupted packets will always be counted as lost). This enables the server to know who needs messages forwarded to them. If a client looses a connection to a peer, the connection status update is transmitted to the server immediately.
- [>=0.3]: Shared control: Two or more clients may be assigned to the same faction, thus allowing more than one client to control the same faction. This can be very handy where duties are split between the players and can be a fun twist. For the UI, this means that the server will need to be able to have more than one client assigned per slot and that the join game client menu will need to be able to display that. (added for clarity 3/5/2009)
- [>=0.3]: DOS attack detection should be implemented as some level, even if trivial. While I have not heard of anybody attempting to exploit either Glest or GAE's networking to perform attacks, a minimal level of DOS detection should be in order. While I'm saying 0.3 or later for this requirement, safe-guards for buffer overrun exploits are always a constant must and after 0.2.12, it wouldn't hurt to carefully review the networking code and make sure it's safe.
.
Disconnects, Reconnects and New Game-Time Connections
The following items target 0.2.12 specifically:
- [0.2.12]: When a client disconnects (either intentionally or unexpectedly), the disconnected player's faction is left uncontrolled. This is the current behavior for the most part.
- [0.2.12]: When a client disconnects intentionally, a message should be sent to server and all peers (or relayed by server if peers weren't connected) so that the cause of disconnect is known. This should not cause an auto-save.
- [0.2.12]: When a client disconnects unexpectedly, the game should auto-save on server and all peers (as it does currently). If this brings the number of clients connected to the server to zero, then the game should pause and a message box should ask the server if they want to quit or not (this is also current behavior). If there is at least one other client connected, game play should not be interrupted (the disconnected client's faction will be left uncontrolled as previously described).
The following overrides the 0.2.12 specifications above and target a release >0.2.12 (maybe even the 0.3 branch for some requirements).
- [>0.2.12]: If a client disconnects for any reason (intentionally, unexpectedly or kicked), the server should be prompted (message box) and be able to replace that player's faction with an AI of their choice or uncontrolled. This message box should have the escape key linked to choose the "uncontrolled" option so they may quickly exit the message box if need be. (FIXME: this requirement mixes user interface design and functional requirements)
- [>0.2.12]: The server should be able to change any non-player-controlled (i.e., AI or uncontrolled) faction's controller with another non-player controller at game time, including making the faction uncontrolled. These changes should always be broadcast to clients in a way that is clear (like a message to the console and a "beep" type of sound) but un-obtrusive.
- [>0.2.12]: When a client disconnects unexpectedly, the game pauses on all peers and everybody is notified. More specifically, the server shall transmit a mandatory pause command to all clients effective in approximately average_latency milliseconds. The client should be able to reconnect while the game is still in progress, without everybody else having to leave the game. The server should be able to choose a course of action from the following:a) remain paused to see if the client re-connects, b) resume game leaving the disconnected player's faction un-controlled or c) resume game replacing the disconnected player's faction with an AI of the server's choice. Again, each client should clearly be notified.
- [>0.2.12]: When a server disconnects, either intentionally or unexpectedly:
- If the disconnect was unexpected, each client will auto-save the game (this is current behavior).
- If there is only one client, then the client is converted to a server, paused and given the same options a server is given when a client disconnects (see above, identical behavior)
- If there is more than one client, a lottery is performed to determine who will be promoted to the server, after which behavior will be the same as if a client had disconnected (described above).
- [>0.2.12]: glestadv.ini shall have a new setting NetAllowClientReconnect with the values of "none", "prompt" and "auto". If set to "none", clients who were disconnected (intentionally or not) shall not be allowed to reconnect to the server while game is still in progress. If set to "prompt" and the client was not kicked, the server will be prompted and asked if the client should be allowed to reconnect and resume control of their faction. If set to "auto" and the client was not kicked, the client will automatically resume control of their faction when they attempt to reconnect.
- [>0.2.12]: glestadv.ini shall have a new boolean setting NetAllowLateClients. If true and the a client attempts to connect to the server while a game is already in progress, then the server is prompted and may choose to a) drop the connection, b) replace an AI with the new client or c) add the client to an existing player-controlled faction. In the case of option c, the client controlling the target faction must be prompted and give approval. If set to false, clients who connect at game time who were not previously disconnected are told to bugger off.
- [>0.2.12]: If NetAllowClientReconnect is set to "none" and NetAllowLateClients is false, the server will stop listening for new connections to reduce possible exposure. If NetAllowClientReconnect is set to "prompt" or "auto" and NetAllowLateClients is false, then the server will stop listening for new connections when the game starts, but begin listening again if a client drops.
.
Game settings, speed and pause
- [0.2.12]: Some in glestadv.ini will be transmitted from the server to each client in a network game and these will be used for game play (overriding their local settings). This includes the following glestadv.ini values:
- GsAutoRepairEnabled - rather or not auto-repair is allowed *
- GsAutoReturnEnabled - rather or not units will automatically return from auto-commands like auto-attack, auto-repair, etc. *
- GsDayTime - how long a day lasts
- GsFogOfWarEnabled
- GsRandStartLocs
- NetPauseAllowed
- NetChangeSpeedAllowed
- GsSpeedFastest - applicable only if NetChangeSpeedAllowed is true
- GsSpeedSlowest - applicable only if NetChangeSpeedAllowed is true
- GsWorldUpdateFps
* Indicates that the server dictates rather or not the setting may be enabled by the client, but does not turn it on unless the client already has it enabled in their glesetadv.ini (a.k.a., Game::Config object). (added 3/5/2009)
- [0.2.12]: If NetPauseAllowed is true, any game participant may pause the game. When a pause is requested, a target execution time is sent (probably 2x the max average latency) so that all participants pause on the same frame. The same is true for speed changes if NetChangeSpeedAllowed. The rules for this should probably be more flexible, but this is the current 0.2.12 implementation plan.
- [>0.2.12]: Future NetPauseAllowed and NetChangeSpeedAllowed may need more options besides true and false. Instead, it may need to be something like "not allowed", "allowed", "allowed only with vote" and have a "min pause lead time" and a "min speed change lead time" to keep players from changing game speed when it suits them without consent from other players. (More exploration and details of this requirement are needed)
- [>0.2.12]: The New Game UI will need to be able to change these and display changes the server makes on each client in the lobby.
- [>=0.2.12]: In a network game, weather events should determined by the server and dictated to clients. At current (both GAE and original Glest) there can exist different weather conditions on two different participants.
.
I've reached the maximum message size for this forum, so I'll resume this in the next post...
EDIT: 2009-01-28: Added weather sync requirement
EDIT: 2009-03-05: Made modifications (in blue) consistient with hailstone's feedback here (https://forum.megaglest.org/index.php?topic=4015.msg22077#msg22077). Outstanding issues:- Synching tech trees and/or sharing content like tileset, maps, etc.
- password-protected games
-
Ok, here's the rest of the requirements...
World updates and rendering
Some of these requirements have been mentioned in other places and some are not network-related, but important enough to mention here since they are closely tied to the way game play will function.
- [0.2.12]: Begin tracking game time in milliseconds since start, excepting time paused. This mechanism will eventually replace world frames. For network games, this time is dictated by the server and each client figures out the correct offset from their local machine time using timing information from ping latency calculations. Server will dictate the offset from its own local machine time and that will keep all clients in sync when pausing and speed changes occur.
- [>0.2.12, perhaps 0.3.x]: Entirely eliminate "world frame" concept and replace it with a sophisticated scheduling mechanism that runs the appropriate updates when needed and no more. (Implementation Note: The base classes for this functionality are already coded and used for other timing mechanisms in 0.2.12)
- [>=0.2.12]: Entirely remove updates to animation progress (data member Game::Unit::animProgress) from world update and replace with a time-based (millisecond) system. When a skill starts the start time is recorded (expressed in "game time" milliseconds). When rendering begins, the animation progress will be calculated based upon the current game time and animation start time -- thus, regardless of world updates, animations will always play out at the same speed. Currently, if world updates aren't happening 40 times per seconds, there are a multitude of problems that can result, including projectiles not being fired! (this happens in original Glest engine as well). Thus, this requirement will change the mechanism that determines when projectiles will fire (see ProjectileParticleSystem, AttackParticleSystem and ParticleObserver). When the complex scheduling is implemented, it will simply be scheduled (so this requirement may wait until that is implemented, I'm not sure yet). This requirement is the primary implement to enabling a schedule-based world, lessoning the burden of needing the world updates as frequently, will result in smoother animations and eliminate MANY wasted CPU cycles that occur in both GAE and original Glest.
- [>0.2.12]: Although not network related, the above requirement is also integral in plans to optimize the way mesh morphing (a.k.a., "interpolation") is performed to reduce CPU cycles as well as improve animation smoothness and overall (rendering) frame rate. This is part of the proposal to implement an "interpolation thread pool" (containing one or more threads) that will begin asynchronously interpolating mesh data for the upcoming rendering frame using current rendering FPS statistics to predict when the next rendering frame will probably occur. Errors in such prediction will probably be ignored since the next frame rate calculation will generally make up the difference for the next rendering cycle and the variance should be so small as to be unnoticable. Plus, if rendering begins before interpolation for the target rendering frame has completed, it can update the Interpolator's (for lack of a better name) target rendering time with the actual so that the remaining animations will be accurate.
- [>0.3]: As a final addendum to the above two requirements, a new setting in glestadv.ini RenderAnimationSmoothness will eventually be introduced. The valid values will be something like 0.125 to 1.0 and specify the ratio of mesh morphing to frame rendering. At 1.0, all meshes are morphed every rendering frame. At 0.5, meshes are morphed every other frame and at 0.125, every eighth. This will add one more variable to control performance vs appearance. (Currently, it happens at every rendering frame, but the Unit::animProgress only updates a max of 40 times per second, so at 60 rendering frames per second, 20 of those are wasted re-interpolating mesh data when Unit::animProgress hasn't changed!)
.
Network game play and World Updates
- [0.2.12]: As mentioned in the "General Networking and Lobby" section, the server will track the average latency for each connection its clients, its clients connection to the server and each client connection to its peers (or lack thereof). Thus, the server will be able to predict with reasonable accuracy how long it takes for a message to be transferred from one participant to another. This value will be used to set a "command lead time" that is broadcast periodically to each client by the server, may change depending upon network conditions and will generally be something like 1.5x to 2.0x the average_latency time of the slowest connection in the game (and ideally less than 500 milliseconds -- and such a hard limit may be set). When any game participant issues a command to their units, it is given a scheduled execution time (in 0.2.12 it will be expressed as a target frame, but that will change to "game time", expressed in milliseconds, in subsequent implementations). This scheduled execution time is transmitted over the network along with the command(s) and the command(s) are queued locally instead of being immediately issued. Once the scheduled execution time or frame arrives, the commands will be executed locally with the anticipation that each game participant will execute them at the same time, or at least the same game time or frame as the server will always be running the game approximately half_average_latency (equal to average_latency / 2) milliseconds ahead of clients. This should not only be a good mechanism to keep games in sync, it will also eliminate most of the advantage that playing on the server has over clients.
- [>=0.2.12]: If the commands arrive later than the scheduled, the recipient will issue the command and then fast-forward the commanded units by the appropriate number of frames (for initial implementation) or milliseconds (in future implementation). If the commands arrived late to a client, the client will send an update request to the server for each commanded unit. This will cause the server to send a full update for each of those units (updates & update requests have been in place for several releases now).
- [>=0.4-ish]: To fully eliminate any advantage the server has over the clients, its possible to cause rendering on the server to represent game state half_average_latency milliseconds later than current. This will require a fairly large "insulation" layer between game state and rendering, however, and may not be worth it.
.
I still want to cover the new messaging, so I'll do that with yet another post. I'll probably draw up a formal sequence diagram, because I think that works well.
-
grr, I can't get either Argouml nor Umbrello to make descent sequence diagrams for me so I'll spell it out with text >:(
So each message will have a source and destination (just like a real sequence diagram). For brievity, I'll use S for Server, C1 for Client1 and C2 for Client2. This diagram (psudo-diagram) illustrates a network conversation for a 3- player network game. Note that sequence IDs with alpha suffix attempts to denote async operations. When this sequence completes, a new game will have started.
New Network Game Sequence Diagram (network communications)
1 S : listen
2 S <- C1 : connect
3 S -> C1 : accept
4 S -> C1 : handshake Game::Net::NetworkMessageHandshake (formerly
NetworkMessageIntro) contains version info (of server software)
and the id and uid the server is assigning to the new client.
5 S <- C1 : handshake Client responds sending it's version, and echoing the newly
assigned id and uid.
6 S <- C1 : player info Sends a NetworkMessagePlayerInfo object which encapsulates a
Game::PlayerInfo object and status (NetworkPlayerStatus) info.
PlayerInfo contains player name, relevant config preferences and
network info (Game::Net::NetworkInfo) from the perspective of
the remote client. Thus, their percieved IP address may be a
VPN address. The PlayerInfo object's networkInfo.localHostName
will contain the host name as the remote host percieves it.
7 S -> C1 : game info NetworkMessageGameInfo contains GameSettings and
Game::PlayerInfo + status for all players.
8 C1 : listen C1 is now considered to be fully connected and will now start
listening for peer connections
9 S <------- C2 : connect
10 S -------> C2 : accept
11 S -------> C2 : handshake
12 S <------- C2 : handshake
13 S <------- C2 : player info
14a S -> C1 : game info
14b S -------> C2 : game info
15 C2 : listen
16 C1 <- C2 : connect Onus is on the newly connected client to attempt to contact peers
17 C1 -> C2 : accept
18 C1 -> C2 : handshake UID of C1 sent to C2 along with handshake
19 C1 <- C2 : handshake UID of C2 sent to C1 as well
20a S <- C1 : status update clients must report that they have established a connection to
20b S <------- C2 : status update each other
21a S -> C1 : game info from this point forward, the server will no longer relay
21b S -------> C2 : game info commands from C1 to C2 or visa-versa
22 S : host uses UI to change game settings
23a S -> C1 : game info
23b S -------> C2 : game info
24 S : host chooses "launch game" from UI
25a S -> C1 : launch
25b S -------> C2 : launch
26a S : loads Loads all game data (map, tileset and faction tree) and builds a
26b C1 : loads Shared::Util::Checksums object
26c C2 : loads
27a S : wait until all clients are ready
28a S <- C1 : report ready Game::Net::NetworkMessageReady sent to server with Checksums
28b S <------- C2 : report ready object. If it doesn't match the server, then the server pukes
on them.
29a S -> C1 : begin game at x time
29b S -------> C2 : begin game at x time
30a S : starts game
30b C1 : starts game
30c C2 : starts gameNote that at various points in this sequence, alternate routes may occur for items 3 and 10 if a client is banned by the server (by IP address) and also for 18 and 19 if the UID, IP address and player name provided by each client does not match the values sent by the server in the game info message.
Saved Games
When restoring a saved network game, the saved game file is sent from the server to all clients at some point in this sequence prior to step 25. Probably, it should not be sent with the GameInfo necessarily. Perhaps there should be a delay of about 15 seconds between the time the server selects the saved game file and it decides to transmit it to clients. The reason for this is that we don't want to send every saved game file as they scroll through them, perhaps trying to find the appropriate saved game to resume. At every change, a new NetworkMessageGameInfo should be sent immediately however (or within 1000 milliseconds) so that each client has an updated view of what they are about to play. This message (without the entire saved game) is much smaller and can be sent more frequently. Of course, if less than 15 seconds have passed since the host/server selected the saved game to restore and they hit the launch button, it should force the saved game file to be transmitted immediately.
EDIT: Correction in sequence (there was also two step 5s)
EDIT: 2009-03-26: Updated sequence, changed numbering (fixed numbering problems), added more details in sequence, added information for how saved games should be handled.
EDIT: 2009-04-14: Moved assignment of player ID and uid from the game info message to the handshake and clarified how checksums are managed.
-
Looks good.
OT:
but this is the nature of a requirements/specification text
Apparently this is a specific language. I read this about the C++ Standard by James Kanze at http://groups.google.com/group/comp.lang.c++/browse_thread/thread/28b14a1308974070:
"The standard is written in a variant of English sometimes called standardese. It's designed (or at least intended) to be
absolutely precise and unambiguous, even at the cost of understandability. It doesn't always succeed with its intent,
but in all cases, precision and a lack of ambiguity have precedence over readability."
Anyway back on topic now.
[0.2.12]: Clients should be able to connect when server is in lobby but there's not an open slot set to "network" (i.e., be in limbo for a time while host changes settings)
Will the server want the first people in to automatically be assigned to a slot? and it doesn't explain what happens when there are two people in limbo and a slot is opened.
I propose that a que (with a max amount of items) be created where players are sent to when they join. The server can then add or remove the desired network or CPU players to or from a slot (the position of which is moveable by the server). Empty slots should be visual and numerical.
Perhaps before clients connect server can adjust settings then press a listen button to allow clients to enter the que (something useful for a master server so games don't appear immediately).
How will specators be dealt with?
# GsAutoRepairEnabled - rather or not auto-repair is allowed
# GsAutoReturnEnabled - rather or not units will automatically return from auto-commands like auto-attack, auto-repair, etc.
I don't know if I agree with GsAutoRepairEnabled and GsAutoReturnEnabled being forced on clients. It seems like personal preference to me.
Also here are some things that you didn't mention: syncing tech trees or downloading maps, password protected joining.
-
By syncing, do you mean the ability to play on a map or tileset that one player does not have? If so, that is a must. Not sure how that'll cope with a tileset though, seeing the size of that.
Why can't we have each player choose his or her own tileset? The as long as the walkable/unwalkable parts are the same, it shouldn't change gameplay. (most tilesets have the same items as walkable/unwalkable anyway).
Lookin' good. I never could understand network code. That's the one thing worse than AI. (AI isn't that bad actually, unless we are talking about RTS AI, which I imagine is the hardest).
-
Do you have any news for us here? Is there any progress or trouble?
One little wish (if you are still on it), please keep in mind that we once want a master server to setup/find games.
We need no implementations yet, just keep it in mind when you design protocols.
-
A master server... <3 *sigh*
heaven (on earth) ;D
-
Do you have any news for us here? Is there any progress or trouble?
One little wish (if you are still on it), please keep in mind that we once want a master server to setup/find games.
We need no implementations yet, just keep it in mind when you design protocols.
Hey buddy, sorry for the late response!!
Yea, lots of progress, but lots of work. I've also been fiddling with a few other projects, Lincity-NG, KDevelop and gdb recently. I might try to start working on gdb's Project Archer (http://sourceware.org/gdb/wiki/ProjectArcher) and if I like it, I might try to get a paying job working on gdb even! I've also have had a lot of other work to take care of over the past few months, but progress indeed is being made. No real problems. I posted a minor enum quandary recently, but nothing I'm worried about, I'm using the copy & paste mechanism for now because it's simple.
OT: On the topic of languages and enums (but off of this topic :) ), I'm not even happy with Java 5's enums :( But that's ok, it's better (in most ways) than how we did enums before in Java (and I still have projects that used old-school Java enums -- basically it enforces it's behavior via private & protected constructors & such). In fact, I have a Java project that uses polymorphic Java enums, that one's a trip! It takes a lot of rules to enforce it, but they have come out cleanly thus far and work great over network (RMI over IIOP) and other serialization situations -- compact on the wire, yet safe for ensuring the same bytecode is using it on each end. I rarely use the default Java object serialization across the wire, because I think it's too fat (even compressed). All of this while handing a variable inheritance tree is tricky, but it served the specific application very nicely. Oh yea, they were also annotated for use in Java 5 Persistence (via Hibernate) and that's where they were mostly used. But, I'm WAAAY off topic! (sorry guys! :) )
So since I'm doing the networking from the ground up, I'm just trying to make sure the quality is top notch. I've refactored and reexamined the design a number of times now (sometimes on paper, UML or text/word doc, other times in code). I'm getting happier with the design, I just want to make sure it's nice and clean. :) If I find a UML editor (preferably one that at least does descent C++ reverse engineering) I'll try to spit out another class diagram and maybe convert the text I posted earlier into a real visual sequence diagram (although I think the text at least serves it's purpose). I want to make sequence diagrams for each major networking operations during game play as well (what I covered is just in the lobby). Most of this is already in place, it's just getting altered.
Also, I've added this Printable C++ class (an abstract interface) and an ObjectPrinter class to print Printable objects and I quite like it. Having this in place will make debugging the networking code very nice. Example output:
361750: sent to peer[0]: NetworkMessagePing {
NetworkMessage {
size = 22
type = NMT_PING
writing = false
simRxTime = 292632639351
}
id = 213
time = 292632534356
timeRcvd = 0
pong = false
}
The idea here is to cut out as many possible ways to be confounded as reasonably possible. Silly bugs can be hard to trace down once they go across the wire, is the problem on the client or the server? Well with all messages logged, it's easier to tell.
Looks good.
Thanks!
OT:
but this is the nature of a requirements/specification text
Apparently this is a specific language. I read this about the C++ Standard by James Kanze at http://groups.google.com/group/comp.lang.c++/browse_thread/thread/28b14a1308974070:
"The standard is written in a variant of English sometimes called standardese. It's designed (or at least intended) to be
absolutely precise and unambiguous, even at the cost of understandability. It doesn't always succeed with its intent,
but in all cases, precision and a lack of ambiguity have precedence over readability."
Hah! no doubt, it definitely begins to become a language of it's own. But if I don't specify things like this, then people die. Well.... not actually at the moment, but if I were working on code for life support systems, heavy machinery, etc., people could die from ambiguity. Certainly projects, careers and businesses literally die, right and left as a result of these types of mistakes (it's scary!). But I would rather be on a low-paying project with a high chance of success than a six figure project that's doomed (I've been there, it's scary as hell and not worth the pay!).
So anyway...
[0.2.12]: Clients should be able to connect when server is in lobby but there's not an open slot set to "network" (i.e., be in limbo for a time while host changes settings)
Will the server want the first people in to automatically be assigned to a slot? and it doesn't explain what happens when there are two people in limbo and a slot is opened.
I propose that a que (with a max amount of items) be created where players are sent to when they join. The server can then add or remove the desired network or CPU players to or from a slot (the position of which is moveable by the server). Empty slots should be visual and numerical.
Perhaps before clients connect server can adjust settings then press a listen button to allow clients to enter the que (something useful for a master server so games don't appear immediately).
How will specators be dealt with?
Spectators! What a wonderful idea! (why didn't I think of that? :) So I think that sounds like a lovely idea and very apropos. So perhaps when a client initially connects, they automatically go into the observer pool as a substitute for "limbo". The person on the server then has to select who they want in which slot. Alternately, people can be automatically sucked out of the observer pool when the server sets a slot to be network controlled and kicked back to the observer pool when they close it or set it to AI, plus giving them the option to manually move them around. I dunno how to best do that in the UI at this point, but I know how to best implement it in the supporting classes that the UI will interact with.
However, spectators will require a new enum entry for the NetworkRole enum (I've already added them :) and some other various code here and there to manage it. Because I want to allow two players to control the same faction (optionally of course) as well as allow players to control more than one faction on their team, this presents its self at a good time. If coded correctly, it can actually simplify things more than complicate it. Each player basically gets a list of factions and teams they can control as well as factions and teams they can observe. This is important for good AI when scenarios involving compound teams to best simulate the story line (i.e., having two AIs, each configured differently, both controlling the same faction can produce some interesting, if not confusing outcomes).
# GsAutoRepairEnabled - rather or not auto-repair is allowed
# GsAutoReturnEnabled - rather or not units will automatically return from auto-commands like auto-attack, auto-repair, etc.
I don't know if I agree with GsAutoRepairEnabled and GsAutoReturnEnabled being forced on clients. It seems like personal preference to me.
Also here are some things that you didn't mention: syncing tech trees or downloading maps, password protected joining.
hmm... Some may see these as a bit of cheating because the computer is doing something for you, so I feel that it's probably important to allow the server to define rather or not they can be enabled in the game rules. I hadn't considered that in a network game, the host allows them, but one of the players may not wish to use them, so I agree, there should be a way to disable it for the client that doesn't want it. So perhaps it's only enabled if it's enabled in the local game settings and also the network GameSettings? We can worry about a pretty way to present that in the UI later (as long as the actual rules make sense for now).
Thanks for your feedback hailstone!
By syncing, do you mean the ability to play on a map or tileset that one player does not have? If so, that is a must. Not sure how that'll cope with a tileset though, seeing the size of that.
No, I'm referring to the synchronization of units in game. I'm not opposed to a feature that allows you to transfer content files while in the game lobby, there is already full support for transferring files in the code (that's now network games are resumed, the server sends each client the saved game). However, I just want a nice well-thought out paradigm. My primary concern is that whatever functionality that allows people to share content needs to be absolutely safe from exploitation. That's the only reason I don't quickly throw in a few lines of code to allow you to sync tile sets, maps and tech trees with others from inside the game (I want it done right basically). My real preference is to have a system similar to Wesnoth where there are update servers and you can download mods, tilesets, etc. from there. This gives some ability for files to be monitored and checked for exploits should any surface. Then, if two people wanted to play but one player didn't have the files, the client could sent the file data (summary) to the other client and they could then retrieve it from the main server (from in game). All of this is functionality I don't plan on personally pursuing very soon.
Why can't we have each player choose his or her own tileset? The as long as the walkable/unwalkable parts are the same, it shouldn't change gameplay. (most tilesets have the same items as walkable/unwalkable anyway).
Currently, it can be raining on one network game and clear in another. That's a bug (in bug database) and will be fixed soon in >=0.2.12. Because tilesets define weather and can potentially effect visibility, I'm not sure that it would be appro0priate to allow different tile sets. Either way, that would also require hacking up the game further and I'm not sure I'm in favor of that particular idea.
Lookin' good. I never could understand network code. That's the one thing worse than AI. (AI isn't that bad actually, unless we are talking about RTS AI, which I imagine is the hardest).
Thanks! OT: No, RTS AI isn't the hardest at all! RPG AI (of the type we haven't seen yet) is the hardest IMHO. I started designing a personality engine to simulate real interactions with NPCs a few years ago just to see how closely we could make a video game character behave like a real living organism. I came up with some interesting ideas, but decided that the end result (after a lot of work) probably wouldn't benefit man kind because it could become too engrossing and video games (especially MMORPGS) are already far too immersive as it is! I had the help of a friend who's studying psychology and already has a degree in biochemistry. This was valuable because the first level of the design was the simulating the reptilian brain (you can't simulate a personality until you understand the layers and instincts that make up a personality, animal, human, imaginary species, or otherwise). You have to run a simulation for each new character based upon events in their lives that form their persona, coupled with their genetics, environment and social interactions. Once you do that, you can use the persona data to have an RPG character that interacts with you in a fashion based upon their (simulated) life experiences. Further yet, memory continues to accumulate and the process never stops, so the next time you interact with them, they will respond according to how previous interactions went. This was originally an extension of Everquest's crappy "faction system" when I was doing some RPG hacking long ago. -- But again, WAAAAYY off topic!
-
hailstone, sorry for taking so long to address the issues you bought up. I've updated the specification addressing your issues except for the last 3, tech tree synching, sharing maps and password-protected games. I've gone through a number of designs and re-designs and I'm feeling pretty happy with what I have now. I'm going to get everything working up to the exchange of player info & game info between the client and server as that will give me enough to really validate my design in practice and then I'll be ready to have you join in. I regret that I didn't have this ready for you sooner, but such is life. I think that I've been able to simplify things enough and separate out the various concepts like the aught to be.
The main differences is that each RemoteInterface object now performs most of the functions involved with handling the messages it receives. It performs the processing it needs to do and then calls an onReceive() function of the GameInterface derived class (which are ClientInterface and ServerInterface), where again, the GameInterface base class handles what is generic enough and then passes control to the derived class in a sort of Chain of Responsibility pattern (but not truly so). I'm using the NVI Idiom (Non-Virtual Interface) in the GameInterface to perform what functionality is common and then pass control to the deriving class via a private virtual function (which surprisingly enough, can be overridden by a deriving class). This is actually a new technique that I recently learned to help clean up a variety issues that I was having otherwise.
So here's a brief text-based class diagram for summary: Host
|
+--------------------------------+
| |
V V
GameInterface RemoteInterface
| |
+----+----+ +----------+-------+
V V | | |
ClientInterface ServerInterface V | |
RemotePeerInterface V |
RemoteClientInterface V
RemoteServerInterface
RemoteInterface no longer uses multiple inheritance to derive from NetworkStatus (and I've renamed NetworkStatus to NetworkStatistics). Instead, it has a "NetworkStatistics stats" object as a data member. As before, each RemoteInterface object has a "GameInterface &owner" data member named which refers it back to whomever "owns" it (so the RemoteInterface class hierarchy is analogous to the previous ConnectionSlot class) and the ClientInterface class has a "RemoteServerInterface server" data member since it is always associated with a single server. RemoteInterface objects are not necessarily connected by virtue of their existence any more (as the ConnectionSlot class used to function). There is also still just one network thread (instead of one for each connection as in a much earlier design) and it calls GameInterface::execute() which contains the main network thread loop. Since I have implemented a Socket::wait function that will wait on multiple sockets, this loop is pretty CPU-friendly now, so there's no polling anymore, it pretty much only runs when there's really something to do. I also managed to get the DEBUG_NETWORK_DELAY functionality cleanly woven into the design (finally! I rather agonized over this part until I found the right approach). So now I've gotten the number of #if blocks for this reduced to a bare minimum and nicely contained in just a few places (in NetworkMessage and RemoteInterface classes only now).
So that's the basic run down on what's changed and I hope to have it ready for you to start working on some of it soon!
-
I had a read of the NVI idiom. It looks really good.
I hope to have it ready for you to start working on some of it soon!
That's good. I have a couple of issues though. I see interface in most of the names, is this just for naming or is it an actual interface class? I assume it's not an interface class since it is being instantiated.
RemoteInterface objects are not necessarily connected by virtue of their existence any more
Does this mean that by creating a game it won't listen for connections without further action?
-
I had a read of the NVI idiom. It looks really good.
Yea, it makes sense where there is stuff that should always be done, but you want to allow the core of some function to be overriden. I'm glad I've been learning a lot of new things over the last several months. (I picked that one up from Scott Meyer's book as well)
... I have a couple of issues though. I see interface in most of the names, is this just for naming or is it an actual interface class? I assume it's not an interface class since it is being instantiated.
Yea, I agree that it's a bit confusing and I'm certain open to alternate naming! Truth be told, I really like Java's solution to multiplie inheritence, that there are the concept of interfaces which are separate from the concept of a class and that you can only derived from one class but that you may implement as many interfaces as you like. I'm not using any of the nomenclature/naming-conventions that distinguishes classes from interfaces (like m$'s CMyClass and IMyInterface style), so in the networking code, "Interface" is referring to a logical "network game interface". Also, "GameInterface" could also be named "LocalInterface". GameInterface and it's derivitives don't actually encapsulate a socket where data is transmitted or received, only a server socket that is capible of accepting new connections. The RemoteInterface classes do encapsulate sockets that send and receive data. Each RemoteInterface object is dedicated to interfacing with a single remote host.
RemoteInterface objects are not necessarily connected by virtue of their existence any more
Does this mean that by creating a game it won't listen for connections without further action?
No, it's just an internal thing because when the ClientInterface object is created it has a "RemoteServerInterface server" object that's a data member. Thus, the RemoteServerInterface object is alive, but is not actually connected to anything. The client will create and destroy RemotePeerInterface objects as needed, as will the ServerInterface create & destroy RemoteClientInterface objects as needed. But the ClientInterface always has a RemoteServerInterface object that's valid, but not necessarily connected to anything. After you look at it, if you think it's a bad design, please say so.
For instance, the ClientInterface object could change it's "server" data member to type "RemoteServerInterface *" that's NULL whenever there is no connection attempt in progress. However, all of the RemoteInterface classes will exist through many states which aren't necessarily usable (i.e., STATE_CONNTECTING, STATE_NEGOTIATED, etc.), so care must be taken throughout that just because an object exists, that it doesn't mean that we have a valid connection to a remote host.
Also, after examining it further, I'm wrong. The old ConnectionSlot class did previously have instances before they were actually connected to anything.
-
You could make the limbo queue just be a slot that can contain multiple people (or slots created as the people are connecting), and when the game is started, the people in the slot/slots basically act as players with nothing to do but watch (If this is what you're planning, sorry, I only read the first page. :-\). And btw, why shouldn't the spectators be able to talk to players?
-
GameInterface and it's derivitives don't actually encapsulate a socket where data is transmitted or received, only a server socket that is capible of accepting new connections. The RemoteInterface classes do encapsulate sockets that send and receive data. Each RemoteInterface object is dedicated to interfacing with a single remote host.
This reminds me of Remote Procedure Calls a little. http://msdn.microsoft.com/en-us/library/aa373935.aspx
-
Sorry again for the delayed response.
This reminds me of Remote Procedure Calls a little. http://msdn.microsoft.com/en-us/library/aa373935.aspx
OT: Yes, and RMI is the Java equivalent of m$'s RPCs. RPC is a microsoft-specific technology, and RMI can share the criticism that it is Java-specific. However, RMI offers a IIOP layer so that your RMI objects are accessible just like any other CORBA object (thus, the open standard). When I've used RMI, I always use the IIOP layer, even when I'm only interfacing with other Java VMs.
But anyway, yes, it is similar, although nowhere near the level of thoroughness or completeness as either of the afore mentioned technologies. A RemoteClientInterface talks across the wire to RemoteServerInterface on the other side and RemotePeerInterface objects talk to each other in turn. The GameInterface derived objects (either ClientInterface of ServerInterface) glue them all together into a complete "network game interface" mechanism that is exposed to the rest of the executable. If C++ had a way to make classes "package private" like java (i.e., private within a namespace) then all of then RemoteInterface and all of it's derived classes would be package private. Now that's that part of the concept of the design.
Now, in an ideal world, since RemoteInterface objects take care of most of their own affairs on their own, they should generally run themsevles as well (i.e., have their own threads). This is where performance considerations overstep prettiness and we do stuff like the WaitOnMultipleOjbects/muliti-select and expose their internal socket data to manage all of their I/O from a single thread (and to remove polling).
You could make the limbo queue just be a slot that can contain multiple people (or slots created as the people are connecting), and when the game is started, the people in the slot/slots basically act as players with nothing to do but watch (If this is what you're planning, sorry, I only read the first page. :-\). And btw, why shouldn't the spectators be able to talk to players?
Well, maybe spectators/observers should be able to chat globally only. If we allowed them to spectate on everybody and then chat privately to individual teams and/or players, then they could act as spies, creating an unfair advantage for another player. But if we made all of their chat global, it would eliminate that problem because the person they were spying on would see their chat as well and be able to complain and have them kicked by the game host. :)
So maybe we'll just call the "limbo queue" the Spectators and have them appear at either the above or below the list of factions. I haven't seen hailstone's new GUIs yet, so however hailstone wants to figure that in is fine with me. And actually, the spectators was hailstone's idea, I wasn't even thinking about it, but I totally agree with it. Also, remember that we're planning on supporting multiple players controlling a faction in the future, so each of these faction rows in the GUI will eventually need to support more than one actual player being connected to it with a primary player (who's config rules will be applied to the faction -- specifically the auto-repair and auto-return preferences). So, the "Spectators" row can work similarly. But this is only proposed, I encourage you to decide for yourself how the UI should look as UI has never been my area of expertise.
-
Just a note: RPC is NOT something Microsoft invented. They only implemented it much later than others and of course with incompatible extentions to assure their monopol. ( like they do with every standard they use )
I thought it was SUN Microsystems who invented it, but I was wrong, they only made a very popular implemention.
More here :
http://en.wikipedia.org/wiki/Remote_procedure_call
-
Very OT!!!
cool, ty for that! :) Yes, the practice m$ uses is called "Embrace, Extend, Extinguish". I know that SOAP or other XML-based messaging protocols are the hot thing these days and I'm not up on those techs. Something inside of me just cringes at the thought of bloated texty messages squirming around on the internet. I know that we have jillions of times the bandwidth that we used to have, but I guess it's my own issue to deal with. (can't they at least gzip them? :) ). But back to m$, if you want a good read on how these mechanisms work (the monopoly, propaganda tricks, etc.) check out the Halloween Documents (http://en.wikipedia.org/wiki/Halloween_Documents), particularly the first one with annotations by Eric Raymond -- I found this exceedingly enlightening (link: http://catb.org/~esr/halloween/halloween1.html (http://catb.org/~esr/halloween/halloween1.html) -- this used to be on the opensource.org web site, but I guess this is where it lives now). Also note that you don't necessarily have to "extinguish" as in "remove" a technology to accomplish your mission. I've heard native american activists say that you can commit genocide if you can kill a people's language, traditions and culture -- they wont know who they are any more, the threat is extinguished. Similarly with technology, if you can cause a technology to become irrelevant, then you have accomplished your mission.
But back to the topic!
I'm checking in my code now. the "svn diff" was 7603 lines long, so I have a lot of explaining to do (I always examine the diff to make sure I know what I'm checking in). Hopefully, this will get done today because I'll have to leave in an hour. The sources compile (on Linux) but I don't expect them to work (haven't actually tried to run it! :) ). They are still very messy (lots of commented out code & such). All-in-all, I'm hoping that it's worth it in the end. I've gotten a lot of very nice mechanisms in and I'm hoping that stuff like the ObjectPrinter and Printable class (interface) makes future development, debugging and troubleshooting much easier, especially in the case of users reporting errors that aren't up to debugging. I like the way the core mechanisms have turned out, so it's just a matter of beating everything else into shape.
-
"svn diff" was 7603 lines
I assume this will take a while to get it stable and working .....
-
Sorry, you're right. It's a radical departure in the networking code. On the bright side, I've been able to implement or design hooks into this layer to support most of the future functionality we have planned that the networking code needs to support. So we shouldn't have to "break" the networking code much in the future. This also affects the GameSettings and other such items and will support all of the ideas we've discussed about UI changes for starting new games, joining network games and restoring saved network games. In addition, I've addressed in the GameSettings all of the ideas we've discussed about enhancing the AI (in a generic fashion), so this code shouldn't need to be changed a significant amount when we get to implementing those features. I've done a lot of pen and paper design and re-design work to try to make sure I'm doing this part as well as possible.
-
The network re-write is really starting to shape up! The lobby is now fully functional for the most part (for new games only). Now, whenever the server changes the game settings, they are correctly updated on the client and the "join game" screen now displays (crudely) the game information so you can see what settings you are about to play with and they are updated as the server changes them. I don't plan on getting very fancy in 0.2.x with this, just a text display will be good enough for now.
Peer-to-peer functionality will also remain un-implemented in 0.2.x, although most of the mechanisms are in place, just disabled. This is because I wanted the new networking code built to support it, but we also want to keep our goals reasonably attainable and that's just not needed to meet the minimal requirements for 0.2.12.
All of the functionality for multiple players per faction is also in place at the networking code level, but inaccessible through the GUI screens. When we get to 0.3, that's where we're going to finish this.
So here is a snapshot of the "New Game" menu and the corresponding "Join Game" screen on the client. All data is updated in real-time as the server changes it:
Server UI
(http://glest.codemonger.org/files/gae_0.2.12-wip_new_game_snapshot.png)
Client UI:
(http://glest.codemonger.org/files/gae_0.2.12-wip_join_game_snapshot.png)
As a final note on this, the status for each player will be displayed in this format:
Player Name: last_ping (average_ping) Tx/Rx bytes_sent_per_second/bytes_received_per_second
Here is what each of these fields are more precisely:
- last_ping - the most recent ping time in milliseconds (pings occur every 1 second)
- average_ping - the average ping time over the last 10 seconds
- bytes_sent_per_second - average data transmitted (in bytes) over the last 5 seconds
- bytes_received_per_second - average data received (in bytes) over the last 5 seconds
The duration of the history used for each of these items can be easily changed in the code if desired (in case you care, it's in the Game::Net::RemoteInterface constructor, you change the parameters passed to the construction of the "stats" data member which is of type Game::Net::NetworkStatistics). But I figured these were good values to show the type of connection health info that we usually care about. Packet loss is not shown since that cannot be determined when using TCP (it hides that from you).
So hailstone,
I'm finally totally ready for your help!! :) I've checked in all of my code and I think it's been de-uglified enough that I won't be completely embarrassed to have somebody else look at it! ;D There are still some things that I didn't think out well enough in the design, but this should be good enough for now. (I still need to gen the API docs again and upload them as a reference.)
Also, if you want to check in your change for the 0.3 branch, I'll start working on a merge (if you're ready for me to do that). The ugliest part will be the UI screens because I've changed the guts of how they work, but not much at all of the actual UI components. I've tried to leave the UI in charge of making it's demands of the network interface objects and polling them during their update() functions, rather than have the network interface objects drive the UI components (i.e., the network interface objects don't know about the UI at all).
If you start two instances of glestadv, you can go into the "new game" screen of one and the "join game" screen of the other and try stuff out. It's not uncommon at all for a failed assertion to happen (SIGABRT) as there's still lots of bugs.
Here are a few outstanding issues as far as the game lobby is concerned:
Most network failures no longer cause the game to crash. Game::Net::GameInterface::execute() is the main network thread function. When it calls the RemoteInterface::update() function (game/network/game_interface.cpp:214) that function should never throw anything. Nothing that RemoteInterface::update() calls should throw anything except a GlestException or it's sub-classes (PosixException, ProtocolException, etc.) -- at least this is the way it's supposed to be, there's still more to do on that front. Most of the networking code is executed there, however. So RemoteInterface::update()'s primary duty is to retrieve new messages and call RemoteInterface::dispatch(). The dispatch() function is still rather clumsy. :( I recall somewhere wanting to keep some type of NetworkMessage object, so it has this nasty "should I delete this message when you are done" mechanism that is ugly and should eventually get fixed (sorry).
So when a GlestException is thrown, the RemoteInterface::update() catches it and calls GameInterface::onError() passing it the exception which is stored (poorly) in a variable of GameInterface. The UI classes should check this on each update and actually display the error message to the user or at least tell them that there was a network error. The current behavior is that the information is written to the network log and the peer is disconnected.
Next, I've gotten rid of most of the old network messages that can be summarized as a simple change of state. These are instead replaced with NetworkMessageStatus which contains the remote peer's state. The networking code doesn't yet check for all of the state changes that signal something needs to be done, like the server going from STATE_INITIALIZED to STATE_LAUNCHING (the replacement for the old NetworkMessageLaunch message). Once the server is done launching, but before game play actually starts, it will have created a Checksums object with all of the checksums for each file it loaded. Each client will create these too and they should transmit them to the server once they have completed the launch sequence (also before starting game play). This is sent in the NetworkMessageReady message. Once the server receives them all and (if NetConsistencyChecks is enabled in the glestadv.ini) verifies that all files match, then the server should set a game start time, probably 5 or 10 seconds in the future. The cool thing about this is that the NetworkStatistics class, through the data exchanged in pings, can provide the approximate time on a remote host using the ping history to account for latency. Thus, the server can transmit transition to STATE_READY and set a start time, specifying it in the remote host's local time (so differences in the system clocks of different computers don't matter). Then the UI should ideally show a count down timer so the player knows when the game start is coming.
So for now maybe you can just check out the code and post any questions you have and you let me know what part you want to work on first. I'll check in frequently and until we have a commit mailer daemon, I'll post every time I check in. How does that sound?
-
It's good you're making progress.
I've tried to compile r307 but getting this error (using "jam" without any args):
game/network/network_message.cpp: In member function ‘void Game::Net::NetworkWriteableXmlDoc::uncompress()’:
game/network/network_message.cpp:437: error: call of overloaded ‘toStr(uLongf&)’ is ambiguous
./shared_lib/include/util/conversion.h:125: note: candidates are: static const std::string& Shared::Util::Conversion::toStr(bool)
./shared_lib/include/util/conversion.h:129: note: static std::string Shared::Util::Conversion::toStr(int)
./shared_lib/include/util/conversion.h:135: note: static std::string Shared::Util::Conversion::toStr(unsigned int)
./shared_lib/include/util/conversion.h:141: note: static std::string Shared::Util::Conversion::toStr(Shared::Platform::int64)
./shared_lib/include/util/conversion.h:147: note: static std::string Shared::Util::Conversion::toStr(Shared::Platform::uint64)
./shared_lib/include/util/conversion.h:177: note: static std::string Shared::Util::Conversion::toStr(float)
g++ -c -o ./build/i686-pc-linux-gnu/optimize/game/network/network_message.o -D_XOPEN_SOURCE=600 -DPACKAGE_NAME="gae" -DPACKAGE_TARNAME="gae" -DPACKAGE_VERSION="0.2.12-wip" -DPACKAGE_STRING="gae 0.2.12-wip" -DPACKAGE_BUGREPORT="http://bugs.codemonger.org" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DUSE_POSIX_SOCKETS= -DX11_AVAILABLE=1 -DHAVE_GLOB_H=1 -DHAVE_SYS_IOCTL_H=1 -DHAVE_SYS_TIME_H=1 -DHAVE_BYTESWAP_H=1 -DUSE_SDL= -DHAVE_PTHREAD=1 -DHAVE_LIBZ=1 -I. -I/usr/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT -pthread -pthread -I./game/. -I./game/ai -I./game/facilities -I./game/game -I./game/global -I./game/graphics -I./game/gui -I./game/main -I./game/menu -I./game/network -I./game/sound -I./game/type_instances -I./game/types -I./game/world -I./shared_lib/include -I./shared_lib/include/platform -I./shared_lib/include/platform/sdl -I./shared_lib/include/util -I./shared_lib/include/graphics -I./shared_lib/include/graphics/gl -I./shared_lib/include/sound -I./shared_lib/include/sound/openal -I./shared_lib/include/xml -Wall -W -Wno-unused -Wno-sign-compare -fno-strict-aliasing -O3 -g3 -DNDEBUG -ffast-math -ftree-vectorize -Winit-self -Wmissing-include-dirs -Wwrite-strings -Wextra -Winvalid-pch game/network/network_message.cpp
...failed C++ ./build/i686-pc-linux-gnu/optimize/game/network/network_message.o
For the 0.3 GUI code, it mostly changes menu files. The project files will need to be modified (Visual Studio instructions here (https://forum.megaglest.org/index.php?topic=4180.0)). I haven't tried CEGUI in Linux yet.
I've installed Xubuntu 8.10 in a VirtualBox VM hoping the 3D acceleration support would work for Glest. I installed 3.2.1 and got it to work partially without shadows but no models are showing - only rain, terrain and some GUI. I suspect the VirtualBox Guest Additions Graphics Driver doesn't include some or all of the required OpenGL extensions. And the VM just closed itself ???
I should be able to work on the GUI code in the VM, at least until I get a non-virtual installation of Linux (the computer I was going to use decided to die).
I'm all for the frequent check ins.
-
Thanks! I looked this one up and it appears that the type uLongf (a type that zlib defines and uses) is defined in a very platform specific fashion, so I definitely need to cast this.
As for the OpenGL, I'm amazed they have a VM graphics driver that supports any of it! Until I can build a new windows machine, I'm going to try and set up a build under wine. For now, I'll get this fix checked in as soon as I can, which will probably be tomorrow.
-
I'm able to compile and connect to another instance with the loopback address (anything else locks it up). Compiler still says it is ambiguous. Changing long to int works. Perhaps another overloaded method for unsigned long would be better?
The errors are much nicer.
off topic:
On the codemonger website it says, "For linux, you must download glestadv-data separately and extract." but there is no archive of this except on the sourceforge downloads page. Or there is and it's not clear which one.
-
For the errors, I think we'll eventually want to have the long description go to the log file and display a more succinct message in-game, but this is good enough for now.
As for the ambiguity error, I think I see the problem! :) I only implemented toStr for a small handful of types. I'll add a fatter catch-all that uses a stringstream and accepts a templatized type. This will result in more code being inlined than the way the others are done because we have to create and destroy a stringstream object, but it'll work for the odd cases.
// ugly (i.e., fat) catch-all
template<typename T>
static string toStr(T v) {
stringstream str;
str << v;
return str.str();
}
Unfortunately, the box I was going to convert into a windows machine is now missing a power supply, so I'll have to wait a bit longer before I can install windows again and begin testing & debugging there. I guess we're both getting a taste of computers deciding to die. :o
Thanks for letting me know about the messed up glestadv-data thing, I'll check that out. I think that it's outdated now since we're including all of those files with the compiled binaries.