1. We have added a Gift Upgrades feature that allows you to gift an account upgrade to another member, just in time for the holiday season. You can see the gift option when going to the Account Upgrades screen, or on any user profile screen.
    Dismiss Notice

CyGame::getActivePlayer() runs Out of Sync

Discussion in 'Civ4 - Creation & Customization' started by Caesium, Jun 19, 2006.

  1. Caesium

    Caesium Radiant!

    Joined:
    Jan 14, 2006
    Messages:
    526
    Is there any other command that could replace the gc.getActivePlayer() command?
    Because the gc.getActivePlayer() command runs into OOS.
     
  2. TheLopez

    TheLopez Deity

    Joined:
    Jan 16, 2006
    Messages:
    2,525
    Location:
    Oregon
    Caesium, here is the entry from: http://civilization4.net/files/modding/PythonAPI_v160/

    for the gc.getActivePlayer() method:
    Code:
    PlayerType getActivePlayer()
    Returns the index of the human player currently active and using the local 
    machine. In a single-player game this returns the human player, in a Hotseat 
    game returns the currently active human player, in a network/PBEM game 
    returns the player on whose machine this code is run. This is useful for 
    running player-specific code in MP games.
    
    Bad Example (makes MP games go out of synch, do NOT use this in MP):
    
    if gc.getGame().getActivePlayer() == gc.getPlayer(0):
            gc.getPlayer(0).setGold(1000000)
    
    Above code gives 1 million gold to player 0, if the machine this code is 
    run on is used by this player. For other machines in an MP game this code 
    is not run -- they are not aware player 0 got 1 million gold -- so the game 
    goes out of synch, generating OOS errors. So never use the 
    getActivePlayer() method to change the gamestate in MP! It's main use is 
    for UI events:
    
    Good Example (displays the message 'You are player 0' to player 0 and 
    only player 0):
    
    if gc.getGame().getActivePlayer() == gc.getPlayer(0):
            CyInterface().addMessage(0, False, 0, "You are player 0", "", 0, "", CyGame().getReplayMessageColor(0), 0, 0, False, False)
    
    So you need to use gc.getGame().getActivePlayer() instead of gc.getActivePlayer()
     
  3. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    Actually, I think gc.getGame().getActivePlayer() does the same thing as gc.getActivePlayer(), in the context of OOS problems. The code for CyGlobalContext::getActivePlayer:

    Code:
    CyPlayer* CyGlobalContext::getCyActivePlayer()
    {
    	PlayerTypes pt = GC.getGameINLINE().getActivePlayer();
    	return pt != NO_PLAYER ? getCyPlayer(pt) : NULL;
    }
    
    Edit: Well, gc.getActivePlayer will return a CyPlayer object, where as gc.getGame().getActivePlayer() will return the player's id. However, for the sake of OOS problems, I think they will do the same thing. gc.getActivePlayer() is just a shortcut for gc.getPlayer(gc.getGame().getActivePlayer())


    Rather, it matters with how you're using it. If you do something that changes the game state after the determining the player, you'll probably go out of sync. The example they give:

    Code:
    if gc.getGame().getActivePlayer() == gc.getPlayer(0):
            gc.getPlayer(0).setGold(1000000)
    
    doesn't work because only on one computer will the players gold change. However, the second example:

    Code:
    if gc.getGame().getActivePlayer() == gc.getPlayer(0):
            CyInterface().addMessage(0, False, 0, "You are player 0", "", 0, "", CyGame().getReplayMessageColor(0), 0, 0, False, False)
    
    ..DOES work, because while it's only run on one computer, that's what it's SUPPOSED to do. It's not supposed to show that message on other computers. Otherwise, you could click F5 and the military advisor screen pops up on EVERYONE's computer.

    The problem you run into a lot with OOS is that this "only run on one computer" is desired, you only want the screen to show on one persons screen, but then they do something that you want to affect everyone. However, you put the code that affects the game state in the same one-computer-only-code (such as the handleAction function for the screen) and it won't work.

    For example, the Civics screen only pops up on one persons computer, so whenever there are any events to be handled on that screen, only computer with the screen up will be running the python code to handle the events of this screen.

    CvCivicsScreen does a great example of how to use getActivePlayer effectively:

    Code:
    def Revolution(self, inputClass):
    
    		activePlayer = gc.getPlayer(self.iActivePlayer)
    
    		if (inputClass.getNotifyCode() == NotifyCode.NOTIFY_CLICKED) :
    			if (activePlayer.canRevolution(0)):
    				messageControl = CyMessageControl()
    				messageControl.sendUpdateCivics(self.m_paeDisplayCivics)			
    			screen = self.getScreen()
    			screen.hideScreen()
    
    Note that they use the CyMessageControl() class to force the message to go to all computers. This code will only be run on the machine of the user that's messing around with their civics, so using the following wouldn't work, because the civics change would only go to the local computer, and thus the game goes OOS...

    Code:
    def Revolution(self, inputClass):
    
    		activePlayer = gc.getPlayer(self.iActivePlayer)
    
    		if (inputClass.getNotifyCode() == NotifyCode.NOTIFY_CLICKED) :
    			if (activePlayer.canRevolution(0)):
    				[b]gc.getPlayer(activePlayer).setCivics([i]Whatever the arguments are[/i])[/b]
    			screen = self.getScreen()
    			screen.hideScreen()
    
    Of course, setCivics wouldn't be the right method even in a one-player game because it skips the anarchy, it's a "force the civics" method. But, forgetting that, the problem with this code is that it is only done on the current machine, aka the player who has the screen up. Since the code isn't run on other players in the game on seperate computers, the game goes OOS.

    Edit: So, to answer the original question, it depends on what you're using it for. Don't get the player who's turn it currently is, because it will probably mess up simultaneous turns. I can't vouch for that, but I think if you're trying to change the gamestate, using the pUnit.getOwner(), pCity.getOwner(), pPlot.getOwner() is a better way to go. I'm sure there's SOME way to use one of those (or other getOwner()) methods in context.
     
  4. The Great Apple

    The Great Apple Big Cheese

    Joined:
    Mar 24, 2002
    Messages:
    3,361
    Location:
    Oxford, England
    Just as a confirmation, in case I'm missing something, this code would not cause the game to OOS as it would run on all machines, right?
    Code:
    gc.getPlayer(0).setGold(1000000)
    
     
  5. Caesium

    Caesium Radiant!

    Joined:
    Jan 14, 2006
    Messages:
    526
    Thanks Gerikes. If I'd give you a link to my mod, would you be so kind to check, why it runs into OOS? The Mod works quite good, but it always runs into OOS when I gice it away for testing.

    If I understand him correctly, you understand him right :)
     
  6. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    That's correct. Using just that code alone, since it will run on all the computers in the game, player 0 on every computer will get the gold. The local player is not going to always be player 0. Normally the host is 0, the next player to enter is 1, etc.

    But in the example BAD code, it doesn't work.

    Code:
    if gc.getGame().getActivePlayer() == gc.getPlayer(0):
            gc.getPlayer(0).setGold(1000000)
    
    Imagine this run on a 1v1 game, where there are two players who both currently have 0 gold. Suddenly, an event happens, and the bad example python code is run...

    ----------------------------
    Player 0 (Host's Computer)
    ----------------------------

    On player 0 (the host), the line of code:

    Code:
    if gc.getGame().getActivePlayer() == gc.getPlayer(0):
    
    evaluates to..

    Code:
    if (object of player [b]0[/b]) == (object of player 0):
    
    Edit: I just realized this while looking through, but saying that the above code returns true isn't right. The objects used might be different addresses, which is what's compared. So, think of gc.getActivePlayer() instead, and then compare the ID's, not the objects.

    This returns true, thus on the host's computer, the statements inside the if block is run:

    Code:
            gc.getPlayer(0).setGold(1000000)
    
    Thus, player 0 (the host) now has a ton of gold.

    ---------------------------
    Player 1 (Client's Computer):
    ---------------------------

    The first line of code evaluates to:

    Code:
    if (object of player [b]1[/b]) ==  (object of player 0)
    
    Edit: See my edit above.

    This is FALSE, so on the clients computer, the setGold function is not run, and thus the result is that on the client, both players still have zero gold. The host's player 0 has a ton, the client's player 0 still have none, thus OOS.

    Edit: Caesium, sure I'll take a look. I think getting multiplayer to work correctly is so interesting. Maybe I'll start freelancing myself to mods so I can just work on those problems :lol:
     
  7. Gaurav

    Gaurav Prince

    Joined:
    May 14, 2006
    Messages:
    402
    But how do you go about testing multiplayer mods? You can't cheat!
     
  8. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    You can always make mods that allow for cheats (either cheat utilities you made or try taking out all of the multiplayer checks in the original cheat utilities).
     
  9. Jeckel

    Jeckel Great Reverend

    Joined:
    Nov 16, 2005
    Messages:
    1,637
    Location:
    Peoria, IL
    Nice explanation, but I got a couple questions to salitfy this in my mind.

    Say you got a 1v1 MP game.
    Player 0 is the host.
    Player 1 is the client.

    Durning Player 0's turn on the host computer, getActivePlayer would give you playerID 0.

    Durning Player 0's turn on the client computer, would getActivePlayer give you playerID 0 or 1?

    Basicly does it give you the playerID of player takeing their turn or the playerID of the player's computer?
     
  10. The Great Apple

    The Great Apple Big Cheese

    Joined:
    Mar 24, 2002
    Messages:
    3,361
    Location:
    Oxford, England
    It would return 1. It returns the ID of the player playing on that machine at the present time.
     
  11. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    Exactly correct.

    One more example, because I've recently seen this a bunch in some mods...

    I think that a lot of people think that getActivePlayer() returns the player whose current turn it is, but that's not exactly true. That might be why there are a lot of problems when people who do something like this when trying to show a message to just one player...

    Code:
    CyInterface().addMessage([b]CyGame().getActivePlayer()[/b], True, 10, message, None, 2, None, ColorTypes(8), 0, 0, False, False)
    
    This works for SP, hotseat (assuming you want to send it to just the player whose current turn it is), and (I believe) pbem, but in other multiplayer it will send the message to ALL the players, not just one. Say you want to show this message on the screen of the player who just walked into a plot. You have the unit object, pUnit. A more correct way (as I know so far) would be:

    Code:
    [b]ePlayer = pUnit.getOwner()[/b]
    [b]if ( ePlayer == CyGame().getActivePlayer() ):[/b]
            CyInterface().addMessage([b]ePlayer[/b], True, 10, message, None, 2, None, ColorTypes(8), 0, 0, False, False)
    
     
  12. Jeckel

    Jeckel Great Reverend

    Joined:
    Nov 16, 2005
    Messages:
    1,637
    Location:
    Peoria, IL
    Ahh, thankyou for clearing that up. I had assumed that was how it worked, but never knew for sure.

    Is there a function to get the ID of player who's turn it it?
     
  13. Teg_Navanis

    Teg_Navanis King

    Joined:
    Jan 21, 2006
    Messages:
    737
    I guess you'll have to work with the function's argsList. Normally, you can easily find out whose turn it is (sometimes the current player is an argument of the function (iPlayer), in most other cases it is the owner of the unit that is moving/attacking/... or the city that is being upgraded). I don't think there is a function though.
     
  14. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    Yeah, that's pretty much your only option. I think if they made a "getPlayerTurn" then people would use it, and then problems would come up during simultaneous mode. Just my guess though.
     
  15. Jeckel

    Jeckel Great Reverend

    Joined:
    Nov 16, 2005
    Messages:
    1,637
    Location:
    Peoria, IL
    Nods, thanx guys, I was just wondering, but I see your point about the simultanious move problems.
     
  16. Gaurav

    Gaurav Prince

    Joined:
    May 14, 2006
    Messages:
    402
    I was wondering if you could finish this example and explain how to give the player who say, just hit ALT-G or whatever, 1,000,000 gold in multiplayer, without giving it to the other players, without causing an out of synch error. I am of course assuming this is possible without the SDK.

    I am using this example because I figure it is relatively simple, even if (almost) useless. But if you have something similar around already implemented, that would basically work along the same line and show me what to do, feel free to use that.
     
  17. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    Well, I have another example here from Civcraft, but I'm not sure how helpful it will be. It shows how to have code that's only being run on one computer propagate it's message to the others. However, it involves pushing orders to a unit, which is a little bit easier since there is a function that pretty much does it for you. I'll put some extra comments for other things at the end, but as I haven't had the need to do things like what you asked yet, I'm hesitant to put up code since OOS problems are sometimes tough to find and debug, so I don't want to throw down that sort of code. The code I do use I'm pretty sure works, since I've been testing with it all along.

    The Example

    I have a screen that will only pop up on the screen of the player that selected a certain style of attack (since attacking a plot will allow you in Civcraft to choose the unit to attack, rather than the defender getting the best one).

    The actual code to "popup" the screen is done whenever a user makes a certain unit do a mission. I used a screen rather than a pop-up because you seem to have much more control over how a screen looks, even if it takes more code to make it.

    When I order a unit to run the mission that will start the screen, the message is passed to all computers that this unit is trying to do that mission. The pushMission function (which is where I have code to start the screen), then, will call the python function that starts up the screen on each computer. In the init code, however, I have a check to make sure that the screen only pops up for the owner of the unit who made the call.

    Code:
    ##########################################
        # Interface Screen
        ##########################################
        def interfaceScreen(self, eMission, iPlotX, iPlotY, playerToPopFor):
            # If the player has no selected unit, it couldn't of been them that
            # popped up the screen.
            if (CyInterface().getHeadSelectedUnit() == None):
                return
    
            self.pSelectedGroup = CyInterface().getHeadSelectedUnit().getGroup()
    
            # Multiplayer check
            if (self.pSelectedGroup.getOwner() != playerToPopFor):
                return
    
    
    Thus, the screen is only created on that one player's monitor, and for everyone else the code stops executing through this function. This means that I now have to be careful, as anything in the rest of the code (including all the functions that handle input) will only affect that one player, and a wrong move can throw the game OOS.

    Eventually, the player will select the graphic icon from the screen of the unit they want to attack. Here's the code that handles input:

    Code:
    if (inputClass.getNotifyCode() == NotifyCode.NOTIFY_CLICKED):
                if (inputClass.getFlags() & MouseFlags.MOUSE_RBUTTONUP):
                    # Cancel unit targetting on right-click
                    self.pScreen.hideScreen()
                elif (inputClass.getFlags() & MouseFlags.MOUSE_LBUTTONUP):
                    # Turn the Page
                    if (inputClass.getFunctionName() == self.szTurnPageFunctionName):
                        self.page = (self.page + 1) % self.iTotalPages
                        self.pScreen.setState(self.szTurnPageFunctionName, False)
                        self.updateView()
    
                    # Get a target
                    if (inputClass.getFunctionName() == self.szUnitChooserPrefix):
                        unitPicked = self.plotUnits[self.page][inputClass.getID()]
                        # What a hack. Assume that the mission that needs to be called when targetting is done is one before the mission called to target.
                        # This is always the case at the time of this writing, but still a pretty big hack. Could be fixed by
                        # allowing xml tags in the missioninfos be able to see other missions.
                        [b]gc.getGame().selectionListGameNetMessage(int(GameMessageTypes.GAMEMESSAGE_PUSH_MISSION), self.eMissionInput - 1, unitPicked.getOwner(), unitPicked.getID(), gSC_ATTACK_MISSION_TARGET_DATA_INCLUDED_FLAG, False, False)[/b]
                        self.pScreen.hideScreen()
    
    
    (Ignore the comment about the hack. That has to do with the mission I want to run can't be determined from the old mission, since I can't add an entry in the XML file that says "when this mission is run, run this other mission". So, the "hack" is just to put the mission I do want to run one before the other in the XML, and then just subtract one from the eMissionInput variable to get the correct mission. This has nothing to do with what I'm talking about now, however)

    The emboldened line is the line that is important. At first, I just had a "self.pSelectedGroup.pushMission(blah blah...)", but that wouldn't work since it only pushed the mission on the local computer running the code. It worked fine in single player, but multiplayer would be thrown out of whack. (This was especially hard to detect since the mission I used doesn't affect any of the pieces of data that an OOS checksum checks for (specifically, a unit's hit points), and thus the game was OOS, but there was no message being displayed).

    So, instead I used the selectionListGameNetMessage. This function is used to do message-passing (where a "message" is a way to pass information between computers that only originates on one computer, such as the click on my unit-chooser screen), and in vanilla Civ is only used in the SDK to handle actions (where the "selection list" is the row of buttons on the bottom of your screen where you can do things). Specifically, it orders around the selected unit.

    Code:
    Arguments:
    
    
    VOID selectionListGameNetMessage(INT eMessage, INT iData2, INT iData3, INT iData4, INT iFlags, BOOL bAlt, BOOL bShift)
    
    The first argument uses the GameMessageType enumerated type for what to actually do. So, most of the times, you can get away with using this function for doing stuff. For example, I used it for GameMessageTypes.GAMEMESSAGE_PUSH_MISSION, then filled in the rest of the data for that mission. Note, however, that the mission pushed using selectionListGameNetMessage will affect the currently selected unit (more specifically, the currently selected unit's selection group), so trying to send a message to all computers to have some other group push a mission won't work (unless you change the selected unit, then change back, but that's a bit hackish if you ask me). You can also use "selectedCitiesGameNetMessage" to do orders with specific cities. This is how you can make a certain action



    Other Thoughts
    That should help you do many of the things you might want to do. However, you still are rather limited, it seems. First off, it just so happens that push mission was on the list, but "change gold" wasn't. So, how do you do things on the list? Also, there is the aformentioned item that the selected unit or city will be involved.


    Also, the mission pushed (or whatever message you're using) can only affect the selected unit (or, in the case of selectedCitiesGameNetMessage, the selected cities). These two combined restrictions will eliminate the ability for doing many things.

    So, what's the generic alternative? The CyMessageControl() function is probably the key. Like I said, I haven't had experience doing it, but I THINK that calling CyMessageControl().sendApplyEvent(...) using it's proper arguments could get you to run a custom event (which you would add to the self.Events variable in your CvEventManager class). I'm not sure if that's right, but it's a good alley to start checking out if that's the desired outcome.

    Hopefully this has been helpful. I'm reading over it now and I start to think that many parts could be worded better. Maybe at some point, if I ever finally check out what the sendApplyEvent function does, I'll put all the MP and OOS tidbits of info I have and put it into a well-crafted tutorial, so I can stop insulting myself with attempts of explanations like this :p
     
  18. Gaurav

    Gaurav Prince

    Joined:
    May 14, 2006
    Messages:
    402
    Thanks so much for your help, this clears things up a lot. Since there are only a few limited GameMessageTypes, figuring out how to work sendApplyEvent will probably be useful at some point. Does anyone have an example of sendApplyEvent() used in Python?

    For my mod the set() function called most often is CyUnit.setName(). However I am not sure yet that is where my OOS errors come from, because they were in there before I ever added any of Ruff's unit naming mods. So I still have to learn how to track them down in the first place.

    Needless to say, I figure many of my users are confused as to why an unaltered gameplay mod could possibly cause an OOS in the first place.

    Since I only have a single PC, I may only be able to use the simultaneous turns PBEM to test, which is far too tedious. Do you have any other ideas on how to test with a single PC? Perhaps if I turn down the graphics to the lowest settings, do you think it is possible to run two Civ4 clients on a single PC? Would that work any differently from the regular LAN configuration?
     
  19. Gerikes

    Gerikes User of Run-on Sentences.

    Joined:
    Jul 26, 2005
    Messages:
    1,753
    Location:
    Massachusetts
    Keep in mind that there are multiple things that will cause a game to go out of sync, but that there are only a limited number of game values that are checked when the game checks for OOS.

    I'm working right now on making a script that, when OOS is detected, it will write all these values that are important to a file. If run on both computers at the same spot in the game, the output of the files could be compared to see where exactly one computer differs from the other, which should help in tracking down where things went wrong (much moreso than just a simple number).

    One thing that might be quick and dirty is to see if the soren random number generator calls are different. By enabling the right combination of logging in the .ini file, you should be spawning a log (MPlog.txt, I think the name is) that gives you all of the movements that are happening, as well as a log of all the soren random number calls. If one computer makes a call to getSorenRandNum and another doesn't, then the seeds will go out of sync, and cause OOS. This might be why a unaltered gameplay mod may go out of sync, since either side isn't changing gameplay, but one may be making use of the soren random number generator, while the other one isn't.

    As for testing on one computer, not too sure how that would work. I'm lucky enough to have two computers in my room, so I can do testing that way. I don't know if there's a way to get Civ to run with two instances on one computer, and even with play by e-mail and hotseat things won't completely recreate a true multiplayer game, mostly since the getActivePlayer calls work differently. Although if you were to somehow get two instances up at the same time, I'm sure that would be enough to recreate a true multiplayer w/ multicomputer setting. Any bugs that would be missed by having a latency of zero would probably be out of your reach to fix anyway. Perhaps you should try looking for some Virtual Machine software like VMWare or Virtual PC (both cost money but have trials, I believe). I've also heard of a free microsoft product called Virtual Server 2005. Never used it, though I hear it doesn't work with Windows XP Home.
     
  20. Gaurav

    Gaurav Prince

    Joined:
    May 14, 2006
    Messages:
    402
    Should I replace all calls from the mod to getSorenRandNum() with an alternative random number generator? There are plenty of those to choose from for python.

    Since my mod doesn't alter gameplay, I don't see why I would need to use it at all. For example, who cares if after a reload you get a different unit name?

    Edit: On second thought, it may be important that the other players generate the same random unit name.
     

Share This Page