Search in sources :

Example 1 with SOCGameElements

use of soc.message.SOCGameElements in project JSettlers2 by jdmonin.

the class SOCGameHandler method joinGame.

/**
 * Client has been approved to join game; send JOINGAMEAUTH and the entire state of the game to client.
 * Unless <tt>isTakingOver</tt>, announce {@link SOCJoinGame} client join event to other players.
 *<P>
 * Does not add the client to the game's or server's list of players,
 * that should be done before calling this method.
 *<P>
 * Assumes NEWGAME (or NEWGAMEWITHOPTIONS) has already been sent out.
 * The game's first message<B>*</B> sent to the connecting client is JOINGAMEAUTH, unless isReset.
 *<P>
 * Among other messages, player names are sent via SITDOWN, and pieces on board
 * sent by PUTPIECE.  See comments here for further details.
 * If <tt>isTakingOver</tt>, some details are sent by calling
 * {@link #sitDown_sendPrivateInfo(SOCGame, Connection, int)}.
 * The group of messages sent here ends with GAMEMEMBERS, SETTURN and GAMESTATE.
 * Then, the entire game is sent a JOINGAME for the new game member.
 *<P>
 * *<B>I18N:</B> If the game has a {@link SOCScenario} and the client needs scenario info or localized strings
 * for the scenario name and description, {@link SOCScenarioInfo} or {@link SOCLocalizedStrings} is
 * sent before JOINGAMEAUTH.  This handles i18n and scenarios added or changed between the client's
 * and server's versions.
 *
 * @param gameData Game to join
 * @param c        The connection of joining client
 * @param isReset  Game is a board-reset of an existing game.  This is always false when
 *                 called from SOCServer instead of from inside the SOCGameHandler.
 * @param isTakingOver  Client is re-joining; this connection replaces an earlier one which
 *                      is defunct because of a network problem.
 *                      If <tt>isTakingOver</tt>, don't send anything to other players.
 *
 * @see SOCServer#connectToGame(Connection, String, Map)
 * @see SOCServer#createOrJoinGameIfUserOK(Connection, String, String, String, Map)
 */
@SuppressWarnings("unchecked")
public void joinGame(SOCGame gameData, Connection c, final boolean isReset, final boolean isTakingOver) {
    // If game's already started, true if any bot is seated (can be taken over)
    boolean hasRobot = false;
    String gameName = gameData.getName();
    final String cliName = c.getData();
    if (!isReset) {
        // First, send updated scenario info or localized strings if needed
        // (SOCScenarioInfo or SOCLocalizedStrings); checks c.getVersion(), scd.scenariosInfoSent etc.
        final String gameScen = gameData.getGameOptionStringValue("SC");
        if (gameScen != null)
            srv.sendGameScenarioInfo(gameScen, null, c, false);
        // Now, join game
        c.put(SOCJoinGameAuth.toCmd(gameName));
        c.put(SOCStatusMessage.toCmd(SOCStatusMessage.SV_OK, // "Welcome to Java Settlers of Catan!"
        c.getLocalized("member.welcome")));
    }
    // c.put(SOCGameState.toCmd(gameName, gameData.getGameState()));
    for (int i = 0; i < gameData.maxPlayers; i++) {
        /**
         * send them the already-seated player information;
         * if isReset, don't send, because sitDown will
         * be sent from resetBoardAndNotify.
         */
        if (!isReset) {
            SOCPlayer pl = gameData.getPlayer(i);
            String plName = pl.getName();
            if ((plName != null) && !gameData.isSeatVacant(i)) {
                final boolean isRobot = pl.isRobot();
                if (isRobot)
                    hasRobot = true;
                c.put(SOCSitDown.toCmd(gameName, plName, i, isRobot));
            }
        }
        /**
         * send the seat lock information
         */
        final SOCGame.SeatLockState sl = gameData.getSeatLock(i);
        if ((sl != SOCGame.SeatLockState.CLEAR_ON_RESET) || (c.getVersion() >= 2000))
            srv.messageToPlayer(c, new SOCSetSeatLock(gameName, i, sl));
        else
            // old client
            srv.messageToPlayer(c, new SOCSetSeatLock(gameName, i, SOCGame.SeatLockState.LOCKED));
    }
    c.put(getBoardLayoutMessage(gameData).toCmd());
    /**
     * if game hasn't started yet, each player's potentialSettlements are
     * identical, so send that info once for all players.
     * Otherwise send each player's unique potential settlement list,
     * to populate legal sets before sending any of their PutPieces.
     */
    if ((gameData.getGameState() < SOCGame.START1A) && (c.getVersion() >= SOCPotentialSettlements.VERSION_FOR_PLAYERNUM_ALL)) {
        final HashSet<Integer> psList = gameData.getPlayer(0).getPotentialSettlements();
        // Some boards may have multiple land areas.
        // See also below, and startGame which has very similar code.
        final HashSet<Integer>[] lan;
        final int pan;
        boolean addedPsList = false;
        if (gameData.hasSeaBoard) {
            final SOCBoardLarge bl = (SOCBoardLarge) gameData.getBoard();
            lan = bl.getLandAreasLegalNodes();
            pan = bl.getStartingLandArea();
            if ((lan != null) && (pan != 0) && !lan[pan].equals(psList)) {
                // If potentials != legals[startingLandArea], send as legals[0]
                lan[0] = psList;
                addedPsList = true;
            }
        } else {
            lan = null;
            pan = 0;
        }
        if (lan == null) {
            c.put(SOCPotentialSettlements.toCmd(gameName, -1, new ArrayList<Integer>(psList)));
        } else {
            c.put(SOCPotentialSettlements.toCmd(gameName, -1, pan, lan, SOCBoardAtServer.getLegalSeaEdges(gameData, -1)));
        }
        if (addedPsList)
            // Undo change to game's copy of landAreasLegalNodes
            lan[0] = null;
        if (gameData.isGameOptionSet(SOCGameOption.K_SC_CLVI))
            c.put(SOCPlayerElement.toCmd(gameName, -1, SOCPlayerElement.SET, SOCPlayerElement.SCENARIO_CLOTH_COUNT, ((SOCBoardLarge) (gameData.getBoard())).getCloth()));
    // individual villages' cloth counts are sent soon below
    } else {
        for (int pn = 0; pn < gameData.maxPlayers; ++pn) {
            final SOCPlayer pl = gameData.getPlayer(pn);
            final HashSet<Integer> psList = pl.getPotentialSettlements();
            // Some boards may have multiple land areas.
            // See also above, and startGame which has very similar code.
            final HashSet<Integer>[] lan;
            if (gameData.hasSeaBoard && (pn == 0)) {
                // send this info once, not per-player:
                // Note: Assumes all players have same legal nodes.
                final SOCBoardLarge bl = (SOCBoardLarge) gameData.getBoard();
                lan = bl.getLandAreasLegalNodes();
                if (lan != null)
                    lan[0] = psList;
            } else {
                lan = null;
            }
            if (lan == null) {
                c.put(SOCPotentialSettlements.toCmd(gameName, pn, new ArrayList<Integer>(psList)));
            } else {
                c.put(SOCPotentialSettlements.toCmd(gameName, pn, 0, lan, SOCBoardAtServer.getLegalSeaEdges(gameData, pn)));
                // Undo change to game's copy of landAreasLegalNodes
                lan[0] = null;
            }
        }
    }
    /**
     * If normal game play has started:
     * _SC_CLVI: Send updated Cloth counts for any changed villages.
     * _SC_FTRI: Send any changed Special Edges.
     */
    if (gameData.hasSeaBoard && (gameData.getGameState() >= SOCGame.ROLL_OR_CARD)) {
        final SOCBoardLarge bl = (SOCBoardLarge) gameData.getBoard();
        // SC_CLVI:
        final HashMap<Integer, SOCVillage> villages = bl.getVillages();
        if (villages != null)
            for (final SOCVillage vi : villages.values()) {
                final int cl = vi.getCloth();
                if (cl != SOCVillage.STARTING_CLOTH)
                    srv.messageToGame(gameName, new SOCPieceValue(gameName, vi.getCoordinates(), cl, 0));
            }
        // SC_FTRI:
        boolean sendEdgeChanges = bl.hasSpecialEdges();
        if (!sendEdgeChanges) {
            // check the board for any Special Edge layout part
            for (String ap : SOCBoardLarge.SPECIAL_EDGE_LAYOUT_PARTS) {
                if (bl.getAddedLayoutPart(ap) != null) {
                    sendEdgeChanges = true;
                    break;
                }
            }
        }
        if (sendEdgeChanges)
            joinGame_sendBoardSpecialEdgeChanges(gameData, bl, c);
    }
    /**
     * Send the current player number.
     * Before v2.0.00, this wasn't sent so early; was sent
     * just before SOCGameState and the "joined the game" text.
     * This earlier send has been tested against 1.1.07 (released 2009-10-31).
     */
    if (c.getVersion() >= SOCGameElements.MIN_VERSION)
        c.put(new SOCGameElements(gameName, SOCGameElements.CURRENT_PLAYER, gameData.getCurrentPlayerNumber()).toCmd());
    else
        c.put(SOCSetTurn.toCmd(gameName, gameData.getCurrentPlayerNumber()));
    /**
     * Send the game's Special Item info, if any, if game has started:
     */
    final String[] gameSITypes;
    if (gameData.getGameState() >= SOCGame.START1A) {
        Set<String> ty = gameData.getSpecialItemTypes();
        gameSITypes = (ty != null) ? ty.toArray(new String[ty.size()]) : null;
    } else {
        gameSITypes = null;
    }
    /**
     * Holds any special items shared between game and player. Those must be sent just once, not twice,
     * when per-game and then per-player special item info is sent. Per-player loop should check
     * {@code gameSItoPlayer.get(typeKey)[playerNumber].get(itemIndex)}; unused per-player lists
     * and typeKeys are null, so check each dereference; also check itemIndex versus list length.
     */
    final HashMap<String, ArrayList<SOCSpecialItem>[]> gameSItoPlayer;
    if (gameSITypes == null) {
        gameSItoPlayer = null;
    } else {
        gameSItoPlayer = new HashMap<String, ArrayList<SOCSpecialItem>[]>();
        for (int i = 0; i < gameSITypes.length; ++i) {
            final String tkey = gameSITypes[i];
            ArrayList<SOCSpecialItem> gsi = gameData.getSpecialItems(tkey);
            if (gsi == null)
                // shouldn't happen
                continue;
            final int L = gsi.size();
            for (// use this loop type to avoid ConcurrentModificationException if locking bug
            int gi = 0; // use this loop type to avoid ConcurrentModificationException if locking bug
            gi < L; // use this loop type to avoid ConcurrentModificationException if locking bug
            ++gi) {
                final SOCSpecialItem si = gsi.get(gi);
                if (si == null) {
                    c.put(new SOCSetSpecialItem(gameName, SOCSetSpecialItem.OP_CLEAR, tkey, gi, -1, -1).toCmd());
                    continue;
                }
                // player index, or -1: if pl != null, must search pl's items for a match
                int pi = -1;
                final SOCPlayer pl = si.getPlayer();
                if (pl != null) {
                    ArrayList<SOCSpecialItem> iList = pl.getSpecialItems(tkey);
                    if (iList != null) {
                        for (int k = 0; k < iList.size(); ++k) {
                            if (si == iList.get(k)) {
                                pi = k;
                                break;
                            }
                        }
                    }
                }
                c.put(new SOCSetSpecialItem(gameData, SOCSetSpecialItem.OP_SET, tkey, gi, pi, si).toCmd());
                if (pi != -1) {
                    // remember for use when sending per-player info
                    ArrayList<SOCSpecialItem>[] toAllPl = gameSItoPlayer.get(tkey);
                    if (toAllPl == null) {
                        toAllPl = new ArrayList[gameData.maxPlayers];
                        gameSItoPlayer.put(tkey, toAllPl);
                    }
                    ArrayList<SOCSpecialItem> iList = toAllPl[pl.getPlayerNumber()];
                    if (iList == null) {
                        iList = new ArrayList<SOCSpecialItem>();
                        toAllPl[pl.getPlayerNumber()] = iList;
                    }
                    int iLL = iList.size();
                    while (iLL <= pi) {
                        iList.add(null);
                        ++iLL;
                    }
                    iList.set(pi, si);
                }
            }
        }
    }
    /**
     * send the per-player information
     */
    for (int i = 0; i < gameData.maxPlayers; i++) {
        SOCPlayer pl = gameData.getPlayer(i);
        /**
         * send scenario info before any putpiece, so they know their
         * starting land areas and scenario events
         */
        int itm = pl.getSpecialVP();
        if (itm != 0) {
            srv.messageToPlayer(c, new SOCPlayerElement(gameName, i, SOCPlayerElement.SET, SOCPlayerElement.SCENARIO_SVP, itm));
            ArrayList<SOCPlayer.SpecialVPInfo> svpis = pl.getSpecialVPInfo();
            if (svpis != null)
                for (SOCPlayer.SpecialVPInfo svpi : svpis) srv.messageToPlayer(c, new SOCSVPTextMessage(gameName, i, svpi.svp, c.getLocalized(svpi.desc)));
        }
        itm = pl.getScenarioPlayerEvents();
        if (itm != 0)
            srv.messageToPlayer(c, new SOCPlayerElement(gameName, i, SOCPlayerElement.SET, SOCPlayerElement.SCENARIO_PLAYEREVENTS_BITMASK, itm));
        itm = pl.getScenarioSVPLandAreas();
        if (itm != 0)
            srv.messageToPlayer(c, new SOCPlayerElement(gameName, i, SOCPlayerElement.SET, SOCPlayerElement.SCENARIO_SVP_LANDAREAS_BITMASK, itm));
        itm = pl.getStartingLandAreasEncoded();
        if (itm != 0)
            srv.messageToPlayer(c, new SOCPlayerElement(gameName, i, SOCPlayerElement.SET, SOCPlayerElement.STARTING_LANDAREAS, itm));
        itm = pl.getCloth();
        if (itm != 0)
            srv.messageToPlayer(c, new SOCPlayerElement(gameName, i, SOCPlayerElement.SET, SOCPlayerElement.SCENARIO_CLOTH_COUNT, itm));
        // Send piece info even if player has left the game (pl.getName() == null).
        // This lets them see "their" pieces before srv.sitDown(), if they rejoin at same position.
        Enumeration<SOCPlayingPiece> piecesEnum = pl.getPieces().elements();
        while (piecesEnum.hasMoreElements()) {
            SOCPlayingPiece piece = piecesEnum.nextElement();
            if (piece.getType() == SOCPlayingPiece.CITY)
                c.put(SOCPutPiece.toCmd(gameName, i, SOCPlayingPiece.SETTLEMENT, piece.getCoordinates()));
            c.put(SOCPutPiece.toCmd(gameName, i, piece.getType(), piece.getCoordinates()));
        }
        // _SC_PIRI: special-case piece not part of getPieces
        {
            final SOCFortress piece = pl.getFortress();
            if (piece != null) {
                final int coord = piece.getCoordinates(), str = piece.getStrength();
                c.put(SOCPutPiece.toCmd(gameName, i, piece.getType(), coord));
                if (str != SOCFortress.STARTING_STRENGTH)
                    c.put(SOCPieceValue.toCmd(gameName, coord, str, 0));
            }
        }
        // _SC_PIRI: for display, send count of warships only after SOCShip pieces are sent
        itm = pl.getNumWarships();
        if (itm != 0)
            srv.messageToPlayer(c, new SOCPlayerElement(gameName, i, SOCPlayerElement.SET, SOCPlayerElement.SCENARIO_WARSHIP_COUNT, itm));
        /**
         * send node coord of the last settlement, resources,
         * knight cards played, number of playing pieces in hand
         */
        final int[] counts = new int[(gameData.hasSeaBoard) ? 7 : 6];
        counts[0] = pl.getLastSettlementCoord();
        // will send with SOCPlayerElement.UNKNOWN
        counts[1] = pl.getResources().getTotal();
        counts[2] = pl.getNumKnights();
        counts[3] = pl.getNumPieces(SOCPlayingPiece.ROAD);
        counts[4] = pl.getNumPieces(SOCPlayingPiece.SETTLEMENT);
        counts[5] = pl.getNumPieces(SOCPlayingPiece.CITY);
        if (gameData.hasSeaBoard)
            counts[6] = pl.getNumPieces(SOCPlayingPiece.SHIP);
        if (c.getVersion() >= SOCPlayerElements.MIN_VERSION) {
            c.put(new SOCPlayerElements(gameName, i, SOCPlayerElement.SET, (gameData.hasSeaBoard) ? ELEM_JOINGAME_WITH_PIECETYPES_SEA : ELEM_JOINGAME_WITH_PIECETYPES_CLASSIC, counts).toCmd());
        } else {
            c.put(SOCLastSettlement.toCmd(gameName, i, counts[0]));
            // client too old for SOCPlayerElement.LAST_SETTLEMENT_NODE
            for (int j = 1; j < counts.length; ++j) c.put(SOCPlayerElement.toCmd(gameName, i, SOCPlayerElement.SET, ELEM_JOINGAME_WITH_PIECETYPES_CLASSIC[j], counts[j]));
        }
        final int numDevCards = pl.getInventory().getTotal();
        final int unknownType;
        if (c.getVersion() >= SOCDevCardConstants.VERSION_FOR_NEW_TYPES)
            unknownType = SOCDevCardConstants.UNKNOWN;
        else
            unknownType = SOCDevCardConstants.UNKNOWN_FOR_VERS_1_X;
        final String cardUnknownCmd = SOCDevCardAction.toCmd(gameName, i, SOCDevCardAction.ADD_OLD, unknownType);
        for (int j = 0; j < numDevCards; j++) {
            c.put(cardUnknownCmd);
        }
        if (gameSITypes != null) {
            for (int j = 0; j < gameSITypes.length; ++j) {
                final String tkey = gameSITypes[j];
                ArrayList<SOCSpecialItem> plsi = pl.getSpecialItems(tkey);
                if (plsi == null)
                    // shouldn't happen
                    continue;
                // pi loop body checks gameSItoPlayer to see if already sent (object shared with game)
                final ArrayList<SOCSpecialItem>[] toAllPl = gameSItoPlayer.get(tkey);
                final ArrayList<SOCSpecialItem> iList = (toAllPl != null) ? toAllPl[i] : null;
                final int L = plsi.size();
                for (// use this loop type to avoid ConcurrentModificationException
                int pi = 0; // use this loop type to avoid ConcurrentModificationException
                pi < L; // use this loop type to avoid ConcurrentModificationException
                ++pi) {
                    final SOCSpecialItem si = plsi.get(pi);
                    if (si == null) {
                        c.put(new SOCSetSpecialItem(gameName, SOCSetSpecialItem.OP_CLEAR, tkey, -1, pi, i).toCmd());
                        continue;
                    }
                    if ((iList != null) && (iList.size() > pi) && (iList.get(pi) == si))
                        // already sent (shared with game)
                        continue;
                    c.put(new SOCSetSpecialItem(gameData, SOCSetSpecialItem.OP_SET, tkey, -1, pi, si).toCmd());
                }
            }
        }
        if ((i == 0) && (c.getVersion() < SOCGameElements.MIN_VERSION)) {
            // per-game data, send once; send here only if client is
            // too old to send together with other game elements,
            // otherwise send soon with longest road / largest army
            c.put(SOCFirstPlayer.toCmd(gameName, gameData.getFirstPlayer()));
            c.put(SOCDevCardCount.toCmd(gameName, gameData.getNumDevCards()));
        }
        c.put(SOCChangeFace.toCmd(gameName, i, pl.getFaceId()));
        if (i == 0) {
            // per-game data, send once
            c.put(SOCDiceResult.toCmd(gameName, gameData.getCurrentDice()));
        }
    }
    // /
    // / send dev card count, rounds count, first player, who has longest road and largest army
    // /
    final SOCPlayer lrPlayer = gameData.getPlayerWithLongestRoad(), laPlayer = gameData.getPlayerWithLargestArmy();
    final int lrPlayerNum = (lrPlayer != null) ? lrPlayer.getPlayerNumber() : -1, laPlayerNum = (laPlayer != null) ? laPlayer.getPlayerNumber() : -1;
    if (c.getVersion() < SOCGameElements.MIN_VERSION) {
        c.put(SOCLongestRoad.toCmd(gameName, lrPlayerNum));
        c.put(SOCLargestArmy.toCmd(gameName, laPlayerNum));
    } else {
        c.put(new SOCGameElements(gameName, ELEM_JOINGAME_DEVCARDS_ROUNDS_PLNUMS_FIRST_LONGEST_LARGEST, new int[] { gameData.getNumDevCards(), gameData.getRoundCount(), gameData.getFirstPlayer(), lrPlayerNum, laPlayerNum }).toCmd());
    }
    /**
     * If we're rejoining and taking over a seat after a network problem,
     * send our resource and hand information.
     */
    if (isTakingOver) {
        SOCPlayer cliPl = gameData.getPlayer(cliName);
        if (cliPl != null) {
            int pn = cliPl.getPlayerNumber();
            if ((pn != -1) && !gameData.isSeatVacant(pn))
                sitDown_sendPrivateInfo(gameData, c, pn);
        }
    }
    String membersCommand = null;
    srv.gameList.takeMonitorForGame(gameName);
    /**
     * Almost done; send GAMEMEMBERS as a hint to client that we're almost ready for its input.
     * There's no new data in GAMEMEMBERS, because player names have already been sent by
     * the SITDOWN messages above.
     */
    try {
        Vector<Connection> gameMembers = srv.gameList.getMembers(gameName);
        membersCommand = SOCGameMembers.toCmd(gameName, gameMembers);
    } catch (Exception e) {
        D.ebugPrintln("Exception in SGH.joinGame (gameMembers) - " + e);
    }
    srv.gameList.releaseMonitorForGame(gameName);
    if (membersCommand != null)
        c.put(membersCommand);
    // before v2.0.00, current player number (SETTURN) was sent here,
    // between membersCommand and GAMESTATE.
    c.put(SOCGameState.toCmd(gameName, gameData.getGameState()));
    if (D.ebugOn)
        D.ebugPrintln("*** " + cliName + " joined the game " + gameName + " at " + DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date()));
    /**
     * Let everyone else know about the change
     */
    if (isTakingOver) {
        return;
    }
    srv.messageToGame(gameName, new SOCJoinGame(cliName, "", "dummyhost", gameName));
    if ((!isReset) && gameData.getGameState() >= SOCGame.START2A) {
        srv.messageToPlayerKeyed(c, gameName, // "This game has started. To play, take over a robot."
        (hasRobot) ? // "This game has started. To play, take over a robot."
        "member.join.game.started.bots" : // "This game has started; no new players can sit down."
        "member.join.game.started");
    }
}
Also used : SOCSVPTextMessage(soc.message.SOCSVPTextMessage) SOCJoinGame(soc.message.SOCJoinGame) ArrayList(java.util.ArrayList) SOCPlayerElement(soc.message.SOCPlayerElement) HashSet(java.util.HashSet) SOCPieceValue(soc.message.SOCPieceValue) Connection(soc.server.genericServer.Connection) NoSuchElementException(java.util.NoSuchElementException) Date(java.util.Date) SOCSetSeatLock(soc.message.SOCSetSeatLock) SOCPlayerElements(soc.message.SOCPlayerElements) SOCSetSpecialItem(soc.message.SOCSetSpecialItem) SOCGameElements(soc.message.SOCGameElements)

Example 2 with SOCGameElements

use of soc.message.SOCGameElements in project JSettlers2 by jdmonin.

the class SOCGameHandler method sendGameState.

/**
 * Send all game members the current state of the game with a message.
 * May also send other messages to the current player.
 * Note that the current (or new) player number is not sent here.
 * If game is now OVER, sends appropriate messages.
 *<P>
 * State {@link SOCGame#WAITING_FOR_DISCARDS}:
 * If a 7 is rolled, will also say who must discard (in a GAMETEXTMSG).
 *<P>
 * State {@link SOCGame#WAITING_FOR_ROB_CHOOSE_PLAYER}:
 * If current player must choose which player to rob,
 * will also prompt their client to choose (in a CHOOSEPLAYERREQUEST).
 *<P>
 * State {@link SOCGame#STARTS_WAITING_FOR_PICK_GOLD_RESOURCE}:
 * To announce the player must pick a resource to gain from the gold hex initial placement,
 * please call {@link #sendGameState_sendGoldPickAnnounceText(SOCGame, String, Connection, SOCGame.RollResult)}.
 *<P>
 * State {@link SOCGame#WAITING_FOR_PICK_GOLD_RESOURCE}:
 * If a gold hex is rolled, does not say who
 * must pick resources to gain (because of timing).  Please call
 * {@link #sendGameState_sendGoldPickAnnounceText(SOCGame, String, Connection, SOCGame.RollResult)}
 * after sending the resource gain text ("x gets 1 sheep").
 *<P>
 * <b>Note:</b> If game is now {@link SOCGame#OVER OVER} and the {@link SOCGame#isBotsOnly} flag is set,
 * {@link #sendGameStateOVER(SOCGame)} will call {@link SOCServer#destroyGameAndBroadcast(String, String)}.
 * Be sure that callers to {@code sendGameState} don't assume the game will still exist after calling this method.
 * Also, {@code destroyGame} might create more {@link SOCGame#isBotsOnly} games, depending on server settings.
 *<P>
 * <b>Locks:</b> Does not hold {@link SOCGameList#takeMonitor()} or
 * {@link SOCGameList#takeMonitorForGame}<tt>(gaName)</tt> when called.
 * Some callers call {@link SOCGame#takeMonitor()} before calling; not important here.
 *
 * @see #sendTurn(SOCGame, boolean)
 * @see #sendGameState(SOCGame)
 * @see #sendGameStateOVER(SOCGame)
 *
 * @param ga  the game
 * @param omitGameStateMessage  if true, don't send the {@link SOCGameState} message itself
 *    but do send any other messages as described above. For use just after sending a message which
 *    includes a Game State field.
 * @param sendRollPrompt  If true, and if we send a text message to prompt
 *    the player to roll, send a RollDicePrompt data message.
 *    If the client is too old (1.0.6), it will ignore the prompt.
 *
 * @return    did we send a text message to prompt the player to roll?
 *    If so, sendTurn can also send a RollDicePrompt data message.
 * @since 1.1.00
 */
boolean sendGameState(SOCGame ga, final boolean omitGameStateMessage, final boolean sendRollPrompt) {
    if (ga == null)
        return false;
    final int gaState = ga.getGameState();
    final int cpn = ga.getCurrentPlayerNumber();
    final String gname = ga.getName();
    boolean promptedRoll = false;
    if (gaState == SOCGame.OVER) {
        /**
         * Before sending state "OVER", enforce current player number.
         * This helps the client's copy of game recognize winning condition.
         */
        srv.messageToGame(gname, (ga.clientVersionLowest >= SOCGameElements.MIN_VERSION) ? new SOCGameElements(gname, SOCGameElements.CURRENT_PLAYER, cpn) : new SOCSetTurn(gname, cpn));
    }
    if (!omitGameStateMessage)
        srv.messageToGame(gname, new SOCGameState(gname, gaState));
    SOCPlayer player = null;
    if (cpn != -1)
        player = ga.getPlayer(cpn);
    switch(gaState) {
        case SOCGame.START1A:
        case SOCGame.START2A:
        case SOCGame.START3A:
            // "It's Joe's turn to build a settlement."
            srv.messageToGameKeyed(ga, true, "prompt.turn.to.build.stlmt", player.getName());
            if ((gaState >= SOCGame.START2A) && ga.isGameOptionSet(SOCGameOption.K_SC_3IP)) {
                // reminder to player before their 2nd, 3rd settlements
                Connection con = srv.getConnection(player.getName());
                if (con != null) {
                    srv.messageToPlayerKeyed(con, gname, "prompt.gameopt._SC_3IP.part1");
                    // "This game gives you 3 initial settlements and roads."
                    srv.messageToPlayerKeyed(con, gname, "prompt.gameopt._SC_3IP.part2");
                // "Your free resources will be from the third settlement."
                }
            }
            break;
        case SOCGame.START1B:
        case SOCGame.START2B:
        case SOCGame.START3B:
            srv.messageToGameKeyed(ga, true, (// "It's Joe's turn to build a road or ship."
            (ga.hasSeaBoard) ? // "It's Joe's turn to build a road or ship."
            "prompt.turn.to.build.road.or.ship" : "prompt.turn.to.build.road"), player.getName());
            break;
        case SOCGame.ROLL_OR_CARD:
            // "It's Joe's turn to roll the dice."
            srv.messageToGameKeyed(ga, true, "prompt.turn.to.roll.dice", player.getName());
            promptedRoll = true;
            if (sendRollPrompt)
                srv.messageToGame(gname, new SOCRollDicePrompt(gname, player.getPlayerNumber()));
            break;
        case SOCGame.WAITING_FOR_DISCARDS:
            {
                ArrayList<String> names = new ArrayList<String>();
                for (int i = 0; i < ga.maxPlayers; i++) if (ga.getPlayer(i).getNeedToDiscard())
                    names.add(ga.getPlayer(i).getName());
                if (names.size() == 1)
                    // "Joe needs to discard"
                    srv.messageToGameKeyed(ga, true, "prompt.discard.1", names.get(0));
                else
                    // "Joe and Ed need to discard"
                    srv.messageToGameKeyedSpecial(ga, true, "prompt.discard.n", names);
            }
            break;
        case SOCGame.WAITING_FOR_ROBBER_OR_PIRATE:
            // "{0} must choose to move the robber or the pirate."
            srv.messageToGameKeyed(ga, true, "robber.willmove.choose", player.getName());
            break;
        case SOCGame.PLACING_ROBBER:
            // "{0} will move the robber."
            srv.messageToGameKeyed(ga, true, "robber.willmove", player.getName());
            break;
        case SOCGame.PLACING_PIRATE:
            // "{0} will move the pirate ship."
            srv.messageToGameKeyed(ga, true, "robber.willmove.pirate", player.getName());
            break;
        case SOCGame.WAITING_FOR_ROB_CHOOSE_PLAYER:
            /**
             * get the choices from the game
             */
            final boolean canStealNone = ga.isGameOptionSet(SOCGameOption.K_SC_PIRI);
            boolean[] choices = new boolean[ga.maxPlayers + (canStealNone ? 1 : 0)];
            Arrays.fill(choices, false);
            if (canStealNone)
                choices[ga.maxPlayers] = true;
            for (SOCPlayer pl : ga.getPossibleVictims()) choices[pl.getPlayerNumber()] = true;
            /**
             * ask the current player to choose a player to steal from
             */
            Connection con = srv.getConnection(ga.getPlayer(cpn).getName());
            if (con != null) {
                con.put(SOCChoosePlayerRequest.toCmd(gname, choices));
            }
            break;
        case SOCGame.OVER:
            sendGameStateOVER(ga);
            break;
    }
    return promptedRoll;
}
Also used : SOCGameState(soc.message.SOCGameState) Connection(soc.server.genericServer.Connection) ArrayList(java.util.ArrayList) SOCRollDicePrompt(soc.message.SOCRollDicePrompt) SOCGameElements(soc.message.SOCGameElements) SOCSetTurn(soc.message.SOCSetTurn)

Example 3 with SOCGameElements

use of soc.message.SOCGameElements in project JSettlers2 by jdmonin.

the class SOCGameHandler method startGame.

// javadoc inherited from GameHandler
/**
 * {@inheritDoc}
 *<P>
 * If {@link SOCGame#hasSeaBoard}: Once the board is made, send the updated
 * {@link SOCPotentialSettlements potential settlements}.
 *<P>
 * If this code changes, must also update {@link soctest.TestBoardLayouts#testSingleLayout(SOCScenario, int)}.
 */
public void startGame(SOCGame ga) {
    if (ga == null)
        return;
    final String gaName = ga.getName();
    // TODO once multiple handler threads, encapsulate this
    srv.numberOfGamesStarted++;
    /**
     * start the game, place any initial pieces.
     * If anything is added to this game object setup code,
     * update soctest.TestBoardLayouts.testSingleLayout(..).
     */
    // for playerEvent, gameEvent callbacks (since 2.0.00)
    ga.setScenarioEventListener(this);
    ga.startGame();
    // used on sea board; if null, all are legal
    final int[][] legalSeaEdges;
    if (ga.hasSeaBoard) {
        legalSeaEdges = SOCBoardAtServer.getLegalSeaEdges(ga, -1);
        if (legalSeaEdges != null)
            for (int pn = 0; pn < ga.maxPlayers; ++pn) ga.getPlayer(pn).setRestrictedLegalShips(legalSeaEdges[pn]);
        if (ga.isGameOptionSet(SOCGameOption.K_SC_FTRI) || ga.isGameOptionSet(SOCGameOption.K_SC_PIRI)) {
            // scenario has initial pieces
            ((SOCBoardAtServer) (ga.getBoard())).startGame_putInitPieces(ga);
        }
    } else {
        legalSeaEdges = null;
    }
    srv.gameList.takeMonitorForGame(gaName);
    try {
        /**
         * send the board layout
         */
        try {
            srv.messageToGameWithMon(gaName, getBoardLayoutMessage(ga));
        // For scenario option _SC_CLVI, the board layout message
        // includes villages and the general supply cloth count.
        // For _SC_PIRI, it includes the Pirate Path (additional layout part "PP").
        } catch (IllegalArgumentException e) {
            System.err.println("startGame: Cannot send board for " + gaName + ": " + e.getMessage());
            // the enclosing try-finally will releaseMonitorForGame(gaName) before returning
            return;
        }
        if (ga.hasSeaBoard) {
            // See also joinGame which has very similar code.
            // Send the updated Potential/Legal Settlement node list
            // Note: Assumes all players have same potential settlements
            // (sends with playerNumber -1 == all)
            final HashSet<Integer> psList = ga.getPlayer(0).getPotentialSettlements();
            // Some boards may have multiple land areas.
            final HashSet<Integer>[] lan;
            final int pan;
            boolean addedPsList = false;
            final SOCBoardLarge bl = (SOCBoardLarge) ga.getBoard();
            lan = bl.getLandAreasLegalNodes();
            pan = bl.getStartingLandArea();
            if ((lan != null) && (pan != 0) && !lan[pan].equals(psList)) {
                // If potentials != legals[startingLandArea], send as legals[0]
                lan[0] = psList;
                addedPsList = true;
            }
            if (lan == null)
                srv.messageToGameWithMon(gaName, new SOCPotentialSettlements(gaName, -1, new ArrayList<Integer>(psList)));
            else
                srv.messageToGameWithMon(gaName, new SOCPotentialSettlements(gaName, -1, pan, lan, legalSeaEdges));
            if (addedPsList)
                // Undo change to game's copy of landAreasLegalNodes
                lan[0] = null;
        }
        /**
         * send the player info
         */
        boolean sentInitPiecesState = false;
        for (int i = 0; i < ga.maxPlayers; i++) {
            if (ga.isSeatVacant(i))
                continue;
            final SOCPlayer pl = ga.getPlayer(i);
            final int[] counts = new int[(ga.hasSeaBoard) ? 4 : 3];
            counts[0] = pl.getNumPieces(SOCPlayingPiece.ROAD);
            counts[1] = pl.getNumPieces(SOCPlayingPiece.SETTLEMENT);
            counts[2] = pl.getNumPieces(SOCPlayingPiece.CITY);
            if (ga.hasSeaBoard) {
                // Some scenarios like SC_PIRI may place initial pieces at fixed locations.
                // Usually, pieces will be empty.
                final Vector<SOCPlayingPiece> pieces = pl.getPieces();
                if (!pieces.isEmpty()) {
                    if (!sentInitPiecesState) {
                        // Temporary state change, to avoid initial-piece placement actions.
                        // The actual game state will be sent soon.
                        srv.messageToGameWithMon(gaName, new SOCGameState(gaName, SOCGame.READY));
                        sentInitPiecesState = true;
                    }
                    for (SOCPlayingPiece pp : pieces) srv.messageToGameWithMon(gaName, new SOCPutPiece(gaName, i, pp.getType(), pp.getCoordinates()));
                    SOCPlayingPiece pp = pl.getFortress();
                    if (pp != null)
                        srv.messageToGameWithMon(gaName, new SOCPutPiece(gaName, i, pp.getType(), pp.getCoordinates()));
                }
                counts[3] = pl.getNumPieces(SOCPlayingPiece.SHIP);
            }
            if (ga.clientVersionLowest >= SOCPlayerElements.MIN_VERSION)
                srv.messageToGameWithMon(gaName, new SOCPlayerElements(gaName, i, SOCPlayerElement.SET, (ga.hasSeaBoard) ? ELEM_PIECETYPES_SEA : ELEM_PIECETYPES_CLASSIC, counts));
            else
                for (int j = 0; j < counts.length; ++j) srv.messageToGameWithMon(gaName, new SOCPlayerElement(gaName, i, SOCPlayerElement.SET, ELEM_PIECETYPES_SEA[j], counts[j]));
            if (ga.clientVersionLowest < SOCPlayerElement.VERSION_FOR_CARD_ELEMENTS)
                srv.messageToGameWithMon(gaName, new SOCSetPlayedDevCard(gaName, i, false));
        }
        if (ga.clientVersionLowest >= SOCPlayerElement.VERSION_FOR_CARD_ELEMENTS)
            srv.messageToGameWithMon(gaName, new SOCPlayerElement(gaName, -1, SOCPlayerElement.SET, SOCPlayerElement.PLAYED_DEV_CARD_FLAG, 0));
        /**
         * send the number of dev cards
         */
        srv.messageToGameWithMon(gaName, (ga.clientVersionLowest >= SOCGameElements.MIN_VERSION) ? new SOCGameElements(gaName, SOCGameElements.DEV_CARD_COUNT, ga.getNumDevCards()) : new SOCDevCardCount(gaName, ga.getNumDevCards()));
        /**
         * ga.startGame() picks who goes first, but feedback is nice
         */
        srv.messageToGameKeyed(ga, false, // "Randomly picking a starting player..."
        "start.picking.random.starting.player");
    } finally {
        srv.gameList.releaseMonitorForGame(gaName);
    }
    /**
     * send the game state and start the game.
     * send game state and whose turn it is.
     */
    if (ga.clientVersionLowest >= SOCGameState.VERSION_FOR_GAME_STATE_AS_FIELD) {
        srv.messageToGame(gaName, new SOCStartGame(gaName, ga.getGameState()));
        sendTurn(ga, false);
    } else {
        final int cpn = ga.getCurrentPlayerNumber();
        final boolean sendRoll = sendGameState(ga, false, false);
        srv.messageToGame(gaName, new SOCStartGame(gaName, 0));
        srv.messageToGame(gaName, new SOCTurn(gaName, cpn, 0));
        if (sendRoll)
            srv.messageToGame(gaName, new SOCRollDicePrompt(gaName, cpn));
    }
}
Also used : SOCGameState(soc.message.SOCGameState) SOCPotentialSettlements(soc.message.SOCPotentialSettlements) SOCTurn(soc.message.SOCTurn) SOCRollDicePrompt(soc.message.SOCRollDicePrompt) SOCPlayerElement(soc.message.SOCPlayerElement) SOCSetPlayedDevCard(soc.message.SOCSetPlayedDevCard) HashSet(java.util.HashSet) SOCStartGame(soc.message.SOCStartGame) SOCPlayerElements(soc.message.SOCPlayerElements) SOCPutPiece(soc.message.SOCPutPiece) SOCGameElements(soc.message.SOCGameElements) SOCDevCardCount(soc.message.SOCDevCardCount)

Example 4 with SOCGameElements

use of soc.message.SOCGameElements in project JSettlers2 by jdmonin.

the class SOCGameHandler method forceEndGameTurn.

/**
 * Try to force-end the current player's turn in this game.
 * Alter game state and send messages to players.
 * Will call {@link #endGameTurn(SOCGame, SOCPlayer, boolean)} if appropriate.
 * Will send gameState and current player (turn) to clients.
 *<P>
 * If the current player has lost connection, send the {@link SOCLeaveGame LEAVEGAME}
 * message out <b>before</b> calling this method.
 *<P>
 * Assumes, as {@link #endGameTurn(SOCGame, SOCPlayer, boolean)} does:
 * <UL>
 * <LI> ga.canEndTurn already called, returned false
 * <LI> ga.takeMonitor already called (not the same as {@link SOCGameList#takeMonitorForGame(String)})
 * <LI> gamelist.takeMonitorForGame is NOT called, we do NOT have that monitor
 * </UL>
 * @param ga Game to force end turn
 * @param plName Current player's name. Needed because if they have been disconnected by
 *               {@link #leaveGame(SOCGame, Connection)},
 *               their name within game object is already null.
 * @return true if the turn was ended and game is still active;
 *          false if we find that all players have left and
 *          the gamestate has been changed here to {@link SOCGame#OVER}.
 *
 * @see #endGameTurnOrForce(SOCGame, int, String, Connection, boolean)
 * @see SOCGame#forceEndTurn()
 */
private final boolean forceEndGameTurn(SOCGame ga, final String plName) {
    final String gaName = ga.getName();
    final int cpn = ga.getCurrentPlayerNumber();
    final int endFromGameState = ga.getGameState();
    SOCPlayer cp = ga.getPlayer(cpn);
    if (cp.hasAskedSpecialBuild()) {
        cp.setAskedSpecialBuild(false);
        srv.messageToGame(gaName, new SOCPlayerElement(gaName, cpn, SOCPlayerElement.SET, SOCPlayerElement.ASK_SPECIAL_BUILD, 0));
    }
    final SOCForceEndTurnResult res = ga.forceEndTurn();
    // also could be initial placement (START1A or START2A or START3A).
    if (SOCGame.OVER == ga.getGameState())
        // <--- Early return: All players have left ---
        return false;
    /**
     * Report any resources lost or gained.
     * See also forceGamePlayerDiscardOrGain for same reporting code.
     */
    SOCResourceSet resGainLoss = res.getResourcesGainedLost();
    if (resGainLoss != null) {
        /**
         * If gold hex or returning resources to player (not discarding), report actual types/amounts.
         * For discard, tell the discarding player's client that they discarded the resources,
         * tell everyone else that the player discarded unknown resources.
         */
        if (!res.isLoss()) {
            if ((endFromGameState == SOCGame.WAITING_FOR_PICK_GOLD_RESOURCE) || (endFromGameState == SOCGame.STARTS_WAITING_FOR_PICK_GOLD_RESOURCE)) {
                // Send SOCPlayerElement messages, "gains" text
                reportRsrcGainGold(ga, cp, cpn, resGainLoss, true, false);
            } else {
                // Send SOCPlayerElement messages
                reportRsrcGainLoss(gaName, resGainLoss, false, false, cpn, -1, null, null);
            }
        } else {
            Connection c = srv.getConnection(plName);
            if ((c != null) && c.isConnected())
                reportRsrcGainLoss(gaName, resGainLoss, true, true, cpn, -1, null, c);
            int totalRes = resGainLoss.getTotal();
            srv.messageToGameExcept(gaName, c, new SOCPlayerElement(gaName, cpn, SOCPlayerElement.LOSE, SOCPlayerElement.UNKNOWN, totalRes, true), true);
            // "{0} discarded {1} resources."
            srv.messageToGameKeyed(ga, true, "action.discarded", plName, totalRes);
        }
    }
    /**
     * report any dev-card or item returned to player's hand
     */
    final SOCInventoryItem itemCard = res.getReturnedInvItem();
    SOCInventoryItemAction retItemActionMsg = null;
    if (itemCard != null) {
        Connection c = srv.getConnection(plName);
        if ((c != null) && c.isConnected()) {
            if (itemCard instanceof SOCDevCard) {
                int card = itemCard.itype;
                if ((card == SOCDevCardConstants.KNIGHT) && (c.getVersion() < SOCDevCardConstants.VERSION_FOR_NEW_TYPES))
                    card = SOCDevCardConstants.KNIGHT_FOR_VERS_1_X;
                srv.messageToPlayer(c, new SOCDevCardAction(gaName, cpn, SOCDevCardAction.ADD_OLD, card));
            } else {
                retItemActionMsg = new SOCInventoryItemAction(gaName, cpn, (itemCard.isPlayable() ? SOCInventoryItemAction.ADD_PLAYABLE : SOCInventoryItemAction.ADD_OTHER), itemCard.itype, itemCard.isKept(), itemCard.isVPItem(), itemCard.canCancelPlay);
                srv.messageToPlayer(c, retItemActionMsg);
            }
        }
        // Announce item to game with same retItemActionMsg sent to player?
        boolean announceAsInvItemAction = false;
        // Announce this item to game as an unknown dev card type?
        boolean announceAsUnknown = true;
        if (!(itemCard instanceof SOCDevCard)) {
            // SOCInventoryItem: Add any new kinds here, to announce to all players.
            // If it needs a special message, do so and set announceAsUnknown = false
            // If it's private and doesn't need a special message, set handled = true and let it announce as unknown
            boolean handled = false;
            if (ga.isGameOptionSet(SOCGameOption.K_SC_FTRI)) {
                // endFromGameState is PLACING_INV_ITEM.
                // "Gift port" item details are public, send return message to whole game:
                handled = true;
                announceAsInvItemAction = true;
                announceAsUnknown = false;
            }
            // Fallback:
            if (!handled)
                System.err.println("forceEndGameTurn: Unhandled inventory item type " + itemCard.itype + " class " + itemCard.getClass());
        }
        if (announceAsInvItemAction) {
            srv.messageToGameExcept(gaName, c, retItemActionMsg, true);
        } else if (announceAsUnknown) {
            if (ga.clientVersionLowest >= SOCDevCardConstants.VERSION_FOR_NEW_TYPES) {
                srv.messageToGameExcept(gaName, c, new SOCDevCardAction(gaName, cpn, SOCDevCardAction.ADD_OLD, SOCDevCardConstants.UNKNOWN), true);
            } else {
                srv.messageToGameForVersionsExcept(ga, -1, SOCDevCardConstants.VERSION_FOR_NEW_TYPES - 1, c, new SOCDevCardAction(gaName, cpn, SOCDevCardAction.ADD_OLD, SOCDevCardConstants.UNKNOWN_FOR_VERS_1_X), true);
                srv.messageToGameForVersionsExcept(ga, SOCDevCardConstants.VERSION_FOR_NEW_TYPES, Integer.MAX_VALUE, c, new SOCDevCardAction(gaName, cpn, SOCDevCardAction.ADD_OLD, SOCDevCardConstants.UNKNOWN), true);
            }
            srv.messageToGameKeyed(ga, true, "forceend.devcard.returned", plName);
        // "{0}''s just-played development card was returned."
        }
    }
    /**
     * For initial placements, we don't end turns as normal.
     * (Player number may go forward or backwards, new state isn't ROLL_OR_CARD, etc.)
     * Update clients' gamestate, but don't call endGameTurn.
     */
    final int forceRes = res.getResult();
    if ((forceRes == SOCForceEndTurnResult.FORCE_ENDTURN_SKIP_START_ADV) || (forceRes == SOCForceEndTurnResult.FORCE_ENDTURN_SKIP_START_ADVBACK)) {
        if (res.didUpdateFP() || res.didUpdateLP()) {
            final int fpn = ga.getFirstPlayer();
            final SOCMessage msg = (ga.clientVersionLowest >= SOCGameElements.MIN_VERSION) ? new SOCGameElements(gaName, SOCGameElements.FIRST_PLAYER, fpn) : new SOCFirstPlayer(gaName, fpn);
            // will cause clients to recalculate lastPlayer too
            srv.messageToGame(gaName, msg);
        }
        sendTurn(ga, false);
        // <--- Early return ---
        return true;
    }
    /**
     * If the turn can now end, proceed as if player requested it.
     * Otherwise, send current gamestate.  We'll all wait for other
     * players to send discard messages, and afterwards this turn can end.
     */
    if (ga.canEndTurn(cpn))
        // could force gamestate to OVER, if a client leaves
        endGameTurn(ga, null, true);
    else
        sendGameState(ga, false, false);
    return (ga.getGameState() != SOCGame.OVER);
}
Also used : SOCMessage(soc.message.SOCMessage) SOCInventoryItemAction(soc.message.SOCInventoryItemAction) Connection(soc.server.genericServer.Connection) SOCPlayerElement(soc.message.SOCPlayerElement) SOCGameElements(soc.message.SOCGameElements) SOCFirstPlayer(soc.message.SOCFirstPlayer) SOCDevCardAction(soc.message.SOCDevCardAction)

Aggregations

SOCGameElements (soc.message.SOCGameElements)4 SOCPlayerElement (soc.message.SOCPlayerElement)3 Connection (soc.server.genericServer.Connection)3 ArrayList (java.util.ArrayList)2 HashSet (java.util.HashSet)2 SOCGameState (soc.message.SOCGameState)2 SOCPlayerElements (soc.message.SOCPlayerElements)2 SOCRollDicePrompt (soc.message.SOCRollDicePrompt)2 Date (java.util.Date)1 NoSuchElementException (java.util.NoSuchElementException)1 SOCDevCardAction (soc.message.SOCDevCardAction)1 SOCDevCardCount (soc.message.SOCDevCardCount)1 SOCFirstPlayer (soc.message.SOCFirstPlayer)1 SOCInventoryItemAction (soc.message.SOCInventoryItemAction)1 SOCJoinGame (soc.message.SOCJoinGame)1 SOCMessage (soc.message.SOCMessage)1 SOCPieceValue (soc.message.SOCPieceValue)1 SOCPotentialSettlements (soc.message.SOCPotentialSettlements)1 SOCPutPiece (soc.message.SOCPutPiece)1 SOCSVPTextMessage (soc.message.SOCSVPTextMessage)1