From a4df87e52f647effe33be2b8635715c06da8149a Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Fri, 23 Jun 2006 17:58:11 +0000 Subject: [PATCH] Behold Vilya, Ring of Air and repository for our game and virtual worldly extensions to the distributed environment provided by Narya. git-svn-id: svn+ssh://src.earth.threerings.net/vilya/trunk@1 c613c5cb-e716-0410-b11b-feb51c14d237 --- LICENSE | 458 ++++++ README | 65 + build.xml | 294 ++++ docs/stylesheet.css | 52 + lib/LIBS | 27 + lib/README | 22 + lib/SOURCE_HEADER | 21 + src/java/com/threerings/micasa/Log.java | 72 + .../threerings/micasa/client/ChatPanel.java | 336 +++++ .../micasa/client/ClientController.java | 140 ++ .../threerings/micasa/client/LogonPanel.java | 234 +++ .../threerings/micasa/client/MiCasaApp.java | 115 ++ .../micasa/client/MiCasaApplet.java | 124 ++ .../micasa/client/MiCasaClient.java | 232 +++ .../threerings/micasa/client/MiCasaFrame.java | 89 ++ .../micasa/client/OccupantList.java | 101 ++ .../micasa/data/MiCasaBootstrapData.java | 34 + .../com/threerings/micasa/lobby/Lobby.java | 59 + .../threerings/micasa/lobby/LobbyConfig.java | 108 ++ .../micasa/lobby/LobbyController.java | 121 ++ .../micasa/lobby/LobbyDispatcher.java | 78 + .../threerings/micasa/lobby/LobbyManager.java | 91 ++ .../micasa/lobby/LobbyMarshaller.java | 132 ++ .../threerings/micasa/lobby/LobbyObject.java | 33 + .../threerings/micasa/lobby/LobbyPanel.java | 114 ++ .../micasa/lobby/LobbyProvider.java | 47 + .../micasa/lobby/LobbyRegistry.java | 272 ++++ .../micasa/lobby/LobbySelector.java | 219 +++ .../threerings/micasa/lobby/LobbyService.java | 83 + .../micasa/lobby/table/TableItem.java | 252 ++++ .../micasa/lobby/table/TableListView.java | 305 ++++ .../micasa/lobby/table/TableLobbyConfig.java | 45 + .../micasa/lobby/table/TableLobbyManager.java | 59 + .../micasa/lobby/table/TableLobbyObject.java | 111 ++ .../micasa/server/MiCasaClient.java | 50 + .../micasa/server/MiCasaConfig.java | 33 + .../micasa/server/MiCasaServer.java | 74 + .../micasa/simulator/client/SimpleClient.java | 197 +++ .../micasa/simulator/client/SimpleFrame.java | 51 + .../micasa/simulator/client/SimulatorApp.java | 227 +++ .../simulator/client/SimulatorClient.java | 29 + .../simulator/client/SimulatorController.java | 134 ++ .../simulator/client/SimulatorFrame.java | 52 + .../simulator/client/SimulatorService.java | 44 + .../micasa/simulator/data/SimulatorInfo.java | 40 + .../simulator/data/SimulatorMarshaller.java | 51 + .../micasa/simulator/server/SimpleServer.java | 50 + .../micasa/simulator/server/Simulant.java | 88 ++ .../simulator/server/SimulatorDispatcher.java | 71 + .../simulator/server/SimulatorManager.java | 237 +++ .../simulator/server/SimulatorProvider.java | 62 + .../simulator/server/SimulatorServer.java | 49 + .../simulator/util/SimulatorContext.java | 44 + .../threerings/micasa/util/MiCasaContext.java | 46 + src/java/com/threerings/parlor/Log.java | 56 + src/java/com/threerings/parlor/card/Log.java | 56 + .../card/client/CardGameController.java | 122 ++ .../parlor/card/client/CardGameDecoder.java | 101 ++ .../parlor/card/client/CardGameReceiver.java | 71 + .../parlor/card/client/CardPanel.java | 1149 ++++++++++++++ .../parlor/card/client/CardSprite.java | 252 ++++ .../card/client/CardSpriteObserver.java | 65 + .../parlor/card/client/MicroCardSprite.java | 59 + .../com/threerings/parlor/card/data/Card.java | 244 +++ .../parlor/card/data/CardCodes.java | 60 + .../parlor/card/data/CardGameObject.java | 31 + .../com/threerings/parlor/card/data/Deck.java | 121 ++ .../com/threerings/parlor/card/data/Hand.java | 90 ++ .../parlor/card/data/PlayerCard.java | 54 + .../parlor/card/server/CardGameManager.java | 268 ++++ .../parlor/card/server/CardGameSender.java | 85 ++ .../TrickCardGameControllerDelegate.java | 49 + .../trick/client/TrickCardGameService.java | 41 + .../card/trick/data/TrickCardCodes.java | 42 + .../trick/data/TrickCardGameMarshaller.java | 73 + .../card/trick/data/TrickCardGameObject.java | 210 +++ .../trick/server/TrickCardGameDispatcher.java | 84 ++ .../server/TrickCardGameManagerDelegate.java | 643 ++++++++ .../trick/server/TrickCardGameProvider.java | 50 + .../card/trick/util/TrickCardGameUtil.java | 195 +++ .../client/DefaultSwingTableConfigurator.java | 129 ++ .../parlor/client/GameReadyObserver.java | 43 + .../threerings/parlor/client/Invitation.java | 211 +++ .../parlor/client/InvitationHandler.java | 45 + .../client/InvitationResponseObserver.java | 61 + .../parlor/client/ParlorDecoder.java | 101 ++ .../parlor/client/ParlorDirector.java | 250 ++++ .../parlor/client/ParlorReceiver.java | 84 ++ .../parlor/client/ParlorService.java | 164 ++ .../parlor/client/SeatednessObserver.java | 38 + .../parlor/client/TableConfigurator.java | 120 ++ .../parlor/client/TableDirector.java | 363 +++++ .../parlor/client/TableObserver.java | 48 + .../threerings/parlor/data/ParlorCodes.java | 62 + .../parlor/data/ParlorMarshaller.java | 200 +++ .../com/threerings/parlor/data/Table.java | 402 +++++ .../threerings/parlor/data/TableConfig.java | 49 + .../parlor/data/TableLobbyObject.java | 56 + .../parlor/game/client/GameConfigurator.java | 103 ++ .../parlor/game/client/GameController.java | 331 ++++ .../game/client/GameControllerDelegate.java | 74 + .../parlor/game/client/GameService.java | 38 + .../game/client/SwingGameConfigurator.java | 92 ++ .../threerings/parlor/game/data/GameAI.java | 34 + .../parlor/game/data/GameCodes.java | 44 + .../parlor/game/data/GameConfig.java | 152 ++ .../parlor/game/data/GameMarshaller.java | 50 + .../parlor/game/data/GameObject.java | 448 ++++++ .../parlor/game/data/PartyGameConfig.java | 50 + .../parlor/game/server/AIGameTicker.java | 109 ++ .../parlor/game/server/GameDispatcher.java | 69 + .../parlor/game/server/GameManager.java | 1329 +++++++++++++++++ .../game/server/GameManagerDelegate.java | 108 ++ .../parlor/game/server/GameProvider.java | 39 + .../parlor/game/server/GameWatcher.java | 76 + .../parlor/game/server/TeamGameManager.java | 36 + .../parlor/media/ScoreAnimation.java | 86 ++ .../parlor/server/ParlorDispatcher.java | 116 ++ .../parlor/server/ParlorManager.java | 274 ++++ .../parlor/server/ParlorProvider.java | 233 +++ .../parlor/server/ParlorSender.java | 85 ++ .../parlor/server/TableManager.java | 379 +++++ .../parlor/server/TableManagerProvider.java | 37 + .../parlor/turn/client/TurnDisplay.java | 217 +++ .../turn/client/TurnGameController.java | 44 + .../client/TurnGameControllerDelegate.java | 149 ++ .../parlor/turn/data/TurnGameObject.java | 64 + .../parlor/turn/server/TurnGameManager.java | 92 ++ .../turn/server/TurnGameManagerDelegate.java | 246 +++ .../threerings/parlor/util/ParlorContext.java | 37 + .../threerings/parlor/util/RobotPlayer.java | 128 ++ src/java/com/threerings/puzzle/Log.java | 56 + .../puzzle/client/PlayerStatusView.java | 124 ++ .../puzzle/client/PuzzleAnimationWaiter.java | 74 + .../puzzle/client/PuzzleBoardView.java | 420 ++++++ .../puzzle/client/PuzzleController.java | 978 ++++++++++++ .../client/PuzzleControllerDelegate.java | 153 ++ .../puzzle/client/PuzzleGameService.java | 49 + .../threerings/puzzle/client/PuzzlePanel.java | 319 ++++ .../threerings/puzzle/client/PuzzlePrefs.java | 35 + .../com/threerings/puzzle/data/Board.java | 205 +++ .../threerings/puzzle/data/BoardSummary.java | 77 + .../threerings/puzzle/data/PuzzleCodes.java | 43 + .../threerings/puzzle/data/PuzzleConfig.java | 51 + .../puzzle/data/PuzzleGameCodes.java | 32 + .../puzzle/data/PuzzleGameMarshaller.java | 62 + .../threerings/puzzle/data/PuzzleObject.java | 144 ++ .../puzzle/drop/client/DropBlockSprite.java | 273 ++++ .../puzzle/drop/client/DropBoardView.java | 585 ++++++++ .../drop/client/DropControllerDelegate.java | 1213 +++++++++++++++ .../puzzle/drop/client/DropPanel.java | 41 + .../puzzle/drop/client/DropSprite.java | 584 ++++++++ .../drop/client/DropSpriteObserver.java | 34 + .../puzzle/drop/client/NextBlockView.java | 108 ++ .../drop/client/PieceGroupAnimation.java | 111 ++ .../puzzle/drop/data/DropBoard.java | 887 +++++++++++ .../puzzle/drop/data/DropBoardSummary.java | 88 ++ .../puzzle/drop/data/DropCodes.java | 39 + .../puzzle/drop/data/DropConfig.java | 35 + .../puzzle/drop/data/DropLogic.java | 41 + .../puzzle/drop/data/DropPieceCodes.java | 37 + .../puzzle/drop/data/SegmentInfo.java | 60 + .../drop/server/DropManagerDelegate.java | 174 +++ .../puzzle/drop/util/DropBoardUtil.java | 62 + .../puzzle/drop/util/DropGameUtil.java | 71 + .../puzzle/drop/util/PieceDestroyer.java | 181 +++ .../puzzle/drop/util/PieceDropLogic.java | 80 + .../puzzle/drop/util/PieceDropper.java | 203 +++ .../puzzle/server/PuzzleGameDispatcher.java | 78 + .../puzzle/server/PuzzleGameProvider.java | 45 + .../puzzle/server/PuzzleManager.java | 632 ++++++++ .../puzzle/server/PuzzleManagerDelegate.java | 42 + .../com/threerings/puzzle/util/PointSet.java | 245 +++ .../threerings/puzzle/util/PuzzleContext.java | 62 + .../puzzle/util/PuzzleGameUtil.java | 51 + src/java/com/threerings/stage/Log.java | 38 + .../stage/client/SceneColorizer.java | 130 ++ .../stage/client/StageSceneController.java | 57 + .../stage/client/StageScenePanel.java | 659 ++++++++ .../stage/client/StageSceneService.java | 27 + .../stage/data/DefaultColorUpdate.java | 39 + .../stage/data/ModifyObjectsUpdate.java | 60 + .../com/threerings/stage/data/StageCodes.java | 56 + .../threerings/stage/data/StageLocation.java | 133 ++ .../stage/data/StageMisoSceneModel.java | 64 + .../stage/data/StageOccupantInfo.java | 52 + .../com/threerings/stage/data/StageScene.java | 211 +++ .../stage/data/StageSceneConfig.java | 27 + .../stage/data/StageSceneMarshaller.java | 67 + .../stage/data/StageSceneModel.java | 85 ++ .../stage/data/StageSceneObject.java | 83 + .../stage/server/StageSceneDispatcher.java | 79 + .../stage/server/StageSceneManager.java | 847 +++++++++++ .../stage/server/StageSceneProvider.java | 48 + .../threerings/stage/server/StageServer.java | 67 + .../stage/tools/editor/DirectionButton.java | 192 +++ .../stage/tools/editor/EditorApp.java | 351 +++++ .../stage/tools/editor/EditorConfig.java | 34 + .../stage/tools/editor/EditorFrame.java | 563 +++++++ .../stage/tools/editor/EditorModel.java | 262 ++++ .../tools/editor/EditorModelListener.java | 24 + .../stage/tools/editor/EditorScenePanel.java | 1145 ++++++++++++++ .../stage/tools/editor/EditorScrollBox.java | 174 +++ .../tools/editor/EditorToolBarPanel.java | 110 ++ .../threerings/stage/tools/editor/Log.java | 38 + .../tools/editor/ObjectEditorDialog.java | 245 +++ .../stage/tools/editor/PortalDialog.java | 164 ++ .../stage/tools/editor/PortalTool.java | 165 ++ .../stage/tools/editor/PreferencesDialog.java | 103 ++ .../stage/tools/editor/SceneInfoPanel.java | 289 ++++ .../stage/tools/editor/TestTileLoader.java | 173 +++ .../stage/tools/editor/TileInfoPanel.java | 707 +++++++++ .../tools/editor/util/EditorContext.java | 31 + .../tools/editor/util/EditorDialogUtil.java | 100 ++ .../tools/editor/util/ExtrasPainter.java | 14 + .../stage/tools/editor/util/TileSetUtil.java | 30 + .../stage/tools/viewer/ViewerApp.java | 215 +++ .../stage/tools/viewer/ViewerFrame.java | 170 +++ .../stage/tools/viewer/ViewerScenePanel.java | 240 +++ .../tools/xml/StageMisoSceneRuleSet.java | 21 + .../stage/tools/xml/StageSceneParser.java | 74 + .../stage/tools/xml/StageSceneRuleSet.java | 21 + .../stage/tools/xml/StageSceneWriter.java | 67 + .../stage/util/PlacementConstraints.java | 559 +++++++ .../threerings/stage/util/StageContext.java | 76 + .../threerings/stage/util/StageSceneUtil.java | 413 +++++ src/java/com/threerings/whirled/Log.java | 63 + .../whirled/client/SceneController.java | 91 ++ .../whirled/client/SceneDecoder.java | 69 + .../whirled/client/SceneDirector.java | 532 +++++++ .../whirled/client/SceneReceiver.java | 39 + .../whirled/client/SceneService.java | 86 ++ .../client/persist/SceneRepository.java | 65 + .../com/threerings/whirled/data/AuxModel.java | 36 + .../whirled/data/DefaultSceneConfig.java | 44 + .../com/threerings/whirled/data/Scene.java | 83 + .../threerings/whirled/data/SceneCodes.java | 33 + .../threerings/whirled/data/SceneImpl.java | 127 ++ .../whirled/data/SceneMarshaller.java | 124 ++ .../threerings/whirled/data/SceneModel.java | 94 ++ .../threerings/whirled/data/SceneObject.java | 28 + .../threerings/whirled/data/SceneUpdate.java | 201 +++ .../whirled/data/ScenedBodyObject.java | 39 + .../whirled/server/SceneDispatcher.java | 73 + .../whirled/server/SceneManager.java | 194 +++ .../whirled/server/SceneProvider.java | 184 +++ .../whirled/server/SceneRegistry.java | 354 +++++ .../whirled/server/SceneSender.java | 47 + .../whirled/server/WhirledClient.java | 40 + .../whirled/server/WhirledServer.java | 91 ++ .../server/persist/DummySceneRepository.java | 60 + .../server/persist/SceneRepository.java | 73 + src/java/com/threerings/whirled/spot/Log.java | 56 + .../spot/client/SpotSceneController.java | 36 + .../spot/client/SpotSceneDirector.java | 492 ++++++ .../whirled/spot/client/SpotService.java | 83 + .../threerings/whirled/spot/data/Cluster.java | 77 + .../whirled/spot/data/ClusterObject.java | 74 + .../spot/data/ClusteredBodyObject.java | 47 + .../whirled/spot/data/Location.java | 34 + .../threerings/whirled/spot/data/Portal.java | 112 ++ .../whirled/spot/data/SceneLocation.java | 90 ++ .../whirled/spot/data/SpotCodes.java | 57 + .../whirled/spot/data/SpotMarshaller.java | 93 ++ .../whirled/spot/data/SpotScene.java | 80 + .../whirled/spot/data/SpotSceneImpl.java | 157 ++ .../whirled/spot/data/SpotSceneModel.java | 96 ++ .../whirled/spot/data/SpotSceneObject.java | 140 ++ .../threerings/whirled/spot/images/scene.dia | Bin 0 -> 2000 bytes .../threerings/whirled/spot/images/scene.png | Bin 0 -> 12994 bytes .../com/threerings/whirled/spot/package.html | 108 ++ .../whirled/spot/server/SpotClient.java | 34 + .../whirled/spot/server/SpotDispatcher.java | 95 ++ .../whirled/spot/server/SpotProvider.java | 324 ++++ .../whirled/spot/server/SpotSceneManager.java | 626 ++++++++ .../whirled/spot/tools/EditablePortal.java | 42 + .../spot/tools/xml/SpotSceneRuleSet.java | 164 ++ .../spot/tools/xml/SpotSceneWriter.java | 113 ++ .../whirled/tools/xml/SceneParser.java | 113 ++ .../whirled/tools/xml/SceneRuleSet.java | 60 + .../whirled/tools/xml/SceneWriter.java | 121 ++ .../whirled/util/NoSuchSceneException.java | 42 + .../threerings/whirled/util/SceneFactory.java | 39 + .../threerings/whirled/util/UpdateList.java | 107 ++ .../whirled/util/WhirledContext.java | 37 + src/java/com/threerings/whirled/zone/Log.java | 56 + .../whirled/zone/client/ZoneAdapter.java | 51 + .../whirled/zone/client/ZoneDecoder.java | 69 + .../whirled/zone/client/ZoneDirector.java | 315 ++++ .../whirled/zone/client/ZoneObserver.java | 58 + .../whirled/zone/client/ZoneReceiver.java | 40 + .../whirled/zone/client/ZoneService.java | 67 + .../whirled/zone/data/SceneSummary.java | 58 + .../whirled/zone/data/ZoneCodes.java | 35 + .../whirled/zone/data/ZoneMarshaller.java | 125 ++ .../whirled/zone/data/ZoneSummary.java | 53 + .../whirled/zone/data/ZonedBodyObject.java | 41 + .../whirled/zone/server/ZoneDispatcher.java | 74 + .../whirled/zone/server/ZoneManager.java | 104 ++ .../whirled/zone/server/ZoneProvider.java | 303 ++++ .../whirled/zone/server/ZoneRegistry.java | 88 ++ .../whirled/zone/server/ZoneSender.java | 47 + .../whirled/zone/util/ZoneUtil.java | 66 + tests/build.xml | 115 ++ tests/rsrc/whirled/spot/tools/xml/scene.xml | 9 + tests/rsrc/whirled/tools/xml/scene.xml | 8 + .../com/threerings/parlor/TestClient.java | 127 ++ .../com/threerings/parlor/TestConfig.java | 57 + .../com/threerings/parlor/TestController.java | 35 + .../com/threerings/parlor/TestManager.java | 29 + .../com/threerings/parlor/TestServer.java | 61 + .../whirled/DummyClientSceneRepository.java | 59 + .../com/threerings/whirled/TestClient.java | 115 ++ .../com/threerings/whirled/TestConfig.java | 38 + .../threerings/whirled/TestController.java | 34 + .../spot/tools/xml/SpotSceneParserTest.java | 74 + .../whirled/tools/xml/SceneParserTest.java | 65 + 317 files changed, 45818 insertions(+) create mode 100644 LICENSE create mode 100644 README create mode 100644 build.xml create mode 100644 docs/stylesheet.css create mode 100644 lib/LIBS create mode 100644 lib/README create mode 100644 lib/SOURCE_HEADER create mode 100644 src/java/com/threerings/micasa/Log.java create mode 100644 src/java/com/threerings/micasa/client/ChatPanel.java create mode 100644 src/java/com/threerings/micasa/client/ClientController.java create mode 100644 src/java/com/threerings/micasa/client/LogonPanel.java create mode 100644 src/java/com/threerings/micasa/client/MiCasaApp.java create mode 100644 src/java/com/threerings/micasa/client/MiCasaApplet.java create mode 100644 src/java/com/threerings/micasa/client/MiCasaClient.java create mode 100644 src/java/com/threerings/micasa/client/MiCasaFrame.java create mode 100644 src/java/com/threerings/micasa/client/OccupantList.java create mode 100644 src/java/com/threerings/micasa/data/MiCasaBootstrapData.java create mode 100644 src/java/com/threerings/micasa/lobby/Lobby.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyConfig.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyController.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyDispatcher.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyManager.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyMarshaller.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyObject.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyPanel.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyProvider.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyRegistry.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbySelector.java create mode 100644 src/java/com/threerings/micasa/lobby/LobbyService.java create mode 100644 src/java/com/threerings/micasa/lobby/table/TableItem.java create mode 100644 src/java/com/threerings/micasa/lobby/table/TableListView.java create mode 100644 src/java/com/threerings/micasa/lobby/table/TableLobbyConfig.java create mode 100644 src/java/com/threerings/micasa/lobby/table/TableLobbyManager.java create mode 100644 src/java/com/threerings/micasa/lobby/table/TableLobbyObject.java create mode 100644 src/java/com/threerings/micasa/server/MiCasaClient.java create mode 100644 src/java/com/threerings/micasa/server/MiCasaConfig.java create mode 100644 src/java/com/threerings/micasa/server/MiCasaServer.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimpleClient.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimpleFrame.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimulatorApp.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimulatorClient.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimulatorController.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimulatorFrame.java create mode 100644 src/java/com/threerings/micasa/simulator/client/SimulatorService.java create mode 100644 src/java/com/threerings/micasa/simulator/data/SimulatorInfo.java create mode 100644 src/java/com/threerings/micasa/simulator/data/SimulatorMarshaller.java create mode 100644 src/java/com/threerings/micasa/simulator/server/SimpleServer.java create mode 100644 src/java/com/threerings/micasa/simulator/server/Simulant.java create mode 100644 src/java/com/threerings/micasa/simulator/server/SimulatorDispatcher.java create mode 100644 src/java/com/threerings/micasa/simulator/server/SimulatorManager.java create mode 100644 src/java/com/threerings/micasa/simulator/server/SimulatorProvider.java create mode 100644 src/java/com/threerings/micasa/simulator/server/SimulatorServer.java create mode 100644 src/java/com/threerings/micasa/simulator/util/SimulatorContext.java create mode 100644 src/java/com/threerings/micasa/util/MiCasaContext.java create mode 100644 src/java/com/threerings/parlor/Log.java create mode 100644 src/java/com/threerings/parlor/card/Log.java create mode 100644 src/java/com/threerings/parlor/card/client/CardGameController.java create mode 100644 src/java/com/threerings/parlor/card/client/CardGameDecoder.java create mode 100644 src/java/com/threerings/parlor/card/client/CardGameReceiver.java create mode 100644 src/java/com/threerings/parlor/card/client/CardPanel.java create mode 100644 src/java/com/threerings/parlor/card/client/CardSprite.java create mode 100644 src/java/com/threerings/parlor/card/client/CardSpriteObserver.java create mode 100644 src/java/com/threerings/parlor/card/client/MicroCardSprite.java create mode 100644 src/java/com/threerings/parlor/card/data/Card.java create mode 100644 src/java/com/threerings/parlor/card/data/CardCodes.java create mode 100644 src/java/com/threerings/parlor/card/data/CardGameObject.java create mode 100644 src/java/com/threerings/parlor/card/data/Deck.java create mode 100644 src/java/com/threerings/parlor/card/data/Hand.java create mode 100644 src/java/com/threerings/parlor/card/data/PlayerCard.java create mode 100644 src/java/com/threerings/parlor/card/server/CardGameManager.java create mode 100644 src/java/com/threerings/parlor/card/server/CardGameSender.java create mode 100644 src/java/com/threerings/parlor/card/trick/client/TrickCardGameControllerDelegate.java create mode 100644 src/java/com/threerings/parlor/card/trick/client/TrickCardGameService.java create mode 100644 src/java/com/threerings/parlor/card/trick/data/TrickCardCodes.java create mode 100644 src/java/com/threerings/parlor/card/trick/data/TrickCardGameMarshaller.java create mode 100644 src/java/com/threerings/parlor/card/trick/data/TrickCardGameObject.java create mode 100644 src/java/com/threerings/parlor/card/trick/server/TrickCardGameDispatcher.java create mode 100644 src/java/com/threerings/parlor/card/trick/server/TrickCardGameManagerDelegate.java create mode 100644 src/java/com/threerings/parlor/card/trick/server/TrickCardGameProvider.java create mode 100644 src/java/com/threerings/parlor/card/trick/util/TrickCardGameUtil.java create mode 100644 src/java/com/threerings/parlor/client/DefaultSwingTableConfigurator.java create mode 100644 src/java/com/threerings/parlor/client/GameReadyObserver.java create mode 100644 src/java/com/threerings/parlor/client/Invitation.java create mode 100644 src/java/com/threerings/parlor/client/InvitationHandler.java create mode 100644 src/java/com/threerings/parlor/client/InvitationResponseObserver.java create mode 100644 src/java/com/threerings/parlor/client/ParlorDecoder.java create mode 100644 src/java/com/threerings/parlor/client/ParlorDirector.java create mode 100644 src/java/com/threerings/parlor/client/ParlorReceiver.java create mode 100644 src/java/com/threerings/parlor/client/ParlorService.java create mode 100644 src/java/com/threerings/parlor/client/SeatednessObserver.java create mode 100644 src/java/com/threerings/parlor/client/TableConfigurator.java create mode 100644 src/java/com/threerings/parlor/client/TableDirector.java create mode 100644 src/java/com/threerings/parlor/client/TableObserver.java create mode 100644 src/java/com/threerings/parlor/data/ParlorCodes.java create mode 100644 src/java/com/threerings/parlor/data/ParlorMarshaller.java create mode 100644 src/java/com/threerings/parlor/data/Table.java create mode 100644 src/java/com/threerings/parlor/data/TableConfig.java create mode 100644 src/java/com/threerings/parlor/data/TableLobbyObject.java create mode 100644 src/java/com/threerings/parlor/game/client/GameConfigurator.java create mode 100644 src/java/com/threerings/parlor/game/client/GameController.java create mode 100644 src/java/com/threerings/parlor/game/client/GameControllerDelegate.java create mode 100644 src/java/com/threerings/parlor/game/client/GameService.java create mode 100644 src/java/com/threerings/parlor/game/client/SwingGameConfigurator.java create mode 100644 src/java/com/threerings/parlor/game/data/GameAI.java create mode 100644 src/java/com/threerings/parlor/game/data/GameCodes.java create mode 100644 src/java/com/threerings/parlor/game/data/GameConfig.java create mode 100644 src/java/com/threerings/parlor/game/data/GameMarshaller.java create mode 100644 src/java/com/threerings/parlor/game/data/GameObject.java create mode 100644 src/java/com/threerings/parlor/game/data/PartyGameConfig.java create mode 100644 src/java/com/threerings/parlor/game/server/AIGameTicker.java create mode 100644 src/java/com/threerings/parlor/game/server/GameDispatcher.java create mode 100644 src/java/com/threerings/parlor/game/server/GameManager.java create mode 100644 src/java/com/threerings/parlor/game/server/GameManagerDelegate.java create mode 100644 src/java/com/threerings/parlor/game/server/GameProvider.java create mode 100644 src/java/com/threerings/parlor/game/server/GameWatcher.java create mode 100644 src/java/com/threerings/parlor/game/server/TeamGameManager.java create mode 100644 src/java/com/threerings/parlor/media/ScoreAnimation.java create mode 100644 src/java/com/threerings/parlor/server/ParlorDispatcher.java create mode 100644 src/java/com/threerings/parlor/server/ParlorManager.java create mode 100644 src/java/com/threerings/parlor/server/ParlorProvider.java create mode 100644 src/java/com/threerings/parlor/server/ParlorSender.java create mode 100644 src/java/com/threerings/parlor/server/TableManager.java create mode 100644 src/java/com/threerings/parlor/server/TableManagerProvider.java create mode 100644 src/java/com/threerings/parlor/turn/client/TurnDisplay.java create mode 100644 src/java/com/threerings/parlor/turn/client/TurnGameController.java create mode 100644 src/java/com/threerings/parlor/turn/client/TurnGameControllerDelegate.java create mode 100644 src/java/com/threerings/parlor/turn/data/TurnGameObject.java create mode 100644 src/java/com/threerings/parlor/turn/server/TurnGameManager.java create mode 100644 src/java/com/threerings/parlor/turn/server/TurnGameManagerDelegate.java create mode 100644 src/java/com/threerings/parlor/util/ParlorContext.java create mode 100644 src/java/com/threerings/parlor/util/RobotPlayer.java create mode 100644 src/java/com/threerings/puzzle/Log.java create mode 100644 src/java/com/threerings/puzzle/client/PlayerStatusView.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleBoardView.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleController.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzleGameService.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzlePanel.java create mode 100644 src/java/com/threerings/puzzle/client/PuzzlePrefs.java create mode 100644 src/java/com/threerings/puzzle/data/Board.java create mode 100644 src/java/com/threerings/puzzle/data/BoardSummary.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleCodes.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleConfig.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleGameCodes.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java create mode 100644 src/java/com/threerings/puzzle/data/PuzzleObject.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropBoardView.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropPanel.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropSprite.java create mode 100644 src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java create mode 100644 src/java/com/threerings/puzzle/drop/client/NextBlockView.java create mode 100644 src/java/com/threerings/puzzle/drop/client/PieceGroupAnimation.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropBoard.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropCodes.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropConfig.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropLogic.java create mode 100644 src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java create mode 100644 src/java/com/threerings/puzzle/drop/data/SegmentInfo.java create mode 100644 src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java create mode 100644 src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java create mode 100644 src/java/com/threerings/puzzle/drop/util/DropGameUtil.java create mode 100644 src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java create mode 100644 src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java create mode 100644 src/java/com/threerings/puzzle/drop/util/PieceDropper.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleGameProvider.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleManager.java create mode 100644 src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java create mode 100644 src/java/com/threerings/puzzle/util/PointSet.java create mode 100644 src/java/com/threerings/puzzle/util/PuzzleContext.java create mode 100644 src/java/com/threerings/puzzle/util/PuzzleGameUtil.java create mode 100644 src/java/com/threerings/stage/Log.java create mode 100644 src/java/com/threerings/stage/client/SceneColorizer.java create mode 100644 src/java/com/threerings/stage/client/StageSceneController.java create mode 100644 src/java/com/threerings/stage/client/StageScenePanel.java create mode 100644 src/java/com/threerings/stage/client/StageSceneService.java create mode 100644 src/java/com/threerings/stage/data/DefaultColorUpdate.java create mode 100644 src/java/com/threerings/stage/data/ModifyObjectsUpdate.java create mode 100644 src/java/com/threerings/stage/data/StageCodes.java create mode 100644 src/java/com/threerings/stage/data/StageLocation.java create mode 100644 src/java/com/threerings/stage/data/StageMisoSceneModel.java create mode 100644 src/java/com/threerings/stage/data/StageOccupantInfo.java create mode 100644 src/java/com/threerings/stage/data/StageScene.java create mode 100644 src/java/com/threerings/stage/data/StageSceneConfig.java create mode 100644 src/java/com/threerings/stage/data/StageSceneMarshaller.java create mode 100644 src/java/com/threerings/stage/data/StageSceneModel.java create mode 100644 src/java/com/threerings/stage/data/StageSceneObject.java create mode 100644 src/java/com/threerings/stage/server/StageSceneDispatcher.java create mode 100644 src/java/com/threerings/stage/server/StageSceneManager.java create mode 100644 src/java/com/threerings/stage/server/StageSceneProvider.java create mode 100644 src/java/com/threerings/stage/server/StageServer.java create mode 100644 src/java/com/threerings/stage/tools/editor/DirectionButton.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorApp.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorConfig.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorFrame.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorModel.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorModelListener.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorScenePanel.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorScrollBox.java create mode 100644 src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java create mode 100644 src/java/com/threerings/stage/tools/editor/Log.java create mode 100644 src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java create mode 100644 src/java/com/threerings/stage/tools/editor/PortalDialog.java create mode 100644 src/java/com/threerings/stage/tools/editor/PortalTool.java create mode 100644 src/java/com/threerings/stage/tools/editor/PreferencesDialog.java create mode 100644 src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java create mode 100644 src/java/com/threerings/stage/tools/editor/TestTileLoader.java create mode 100644 src/java/com/threerings/stage/tools/editor/TileInfoPanel.java create mode 100644 src/java/com/threerings/stage/tools/editor/util/EditorContext.java create mode 100644 src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java create mode 100644 src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java create mode 100644 src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java create mode 100644 src/java/com/threerings/stage/tools/viewer/ViewerApp.java create mode 100644 src/java/com/threerings/stage/tools/viewer/ViewerFrame.java create mode 100644 src/java/com/threerings/stage/tools/viewer/ViewerScenePanel.java create mode 100644 src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java create mode 100644 src/java/com/threerings/stage/tools/xml/StageSceneParser.java create mode 100644 src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java create mode 100644 src/java/com/threerings/stage/tools/xml/StageSceneWriter.java create mode 100644 src/java/com/threerings/stage/util/PlacementConstraints.java create mode 100644 src/java/com/threerings/stage/util/StageContext.java create mode 100644 src/java/com/threerings/stage/util/StageSceneUtil.java create mode 100644 src/java/com/threerings/whirled/Log.java create mode 100644 src/java/com/threerings/whirled/client/SceneController.java create mode 100644 src/java/com/threerings/whirled/client/SceneDecoder.java create mode 100644 src/java/com/threerings/whirled/client/SceneDirector.java create mode 100644 src/java/com/threerings/whirled/client/SceneReceiver.java create mode 100644 src/java/com/threerings/whirled/client/SceneService.java create mode 100644 src/java/com/threerings/whirled/client/persist/SceneRepository.java create mode 100644 src/java/com/threerings/whirled/data/AuxModel.java create mode 100644 src/java/com/threerings/whirled/data/DefaultSceneConfig.java create mode 100644 src/java/com/threerings/whirled/data/Scene.java create mode 100644 src/java/com/threerings/whirled/data/SceneCodes.java create mode 100644 src/java/com/threerings/whirled/data/SceneImpl.java create mode 100644 src/java/com/threerings/whirled/data/SceneMarshaller.java create mode 100644 src/java/com/threerings/whirled/data/SceneModel.java create mode 100644 src/java/com/threerings/whirled/data/SceneObject.java create mode 100644 src/java/com/threerings/whirled/data/SceneUpdate.java create mode 100644 src/java/com/threerings/whirled/data/ScenedBodyObject.java create mode 100644 src/java/com/threerings/whirled/server/SceneDispatcher.java create mode 100644 src/java/com/threerings/whirled/server/SceneManager.java create mode 100644 src/java/com/threerings/whirled/server/SceneProvider.java create mode 100644 src/java/com/threerings/whirled/server/SceneRegistry.java create mode 100644 src/java/com/threerings/whirled/server/SceneSender.java create mode 100644 src/java/com/threerings/whirled/server/WhirledClient.java create mode 100644 src/java/com/threerings/whirled/server/WhirledServer.java create mode 100644 src/java/com/threerings/whirled/server/persist/DummySceneRepository.java create mode 100644 src/java/com/threerings/whirled/server/persist/SceneRepository.java create mode 100644 src/java/com/threerings/whirled/spot/Log.java create mode 100644 src/java/com/threerings/whirled/spot/client/SpotSceneController.java create mode 100644 src/java/com/threerings/whirled/spot/client/SpotSceneDirector.java create mode 100644 src/java/com/threerings/whirled/spot/client/SpotService.java create mode 100644 src/java/com/threerings/whirled/spot/data/Cluster.java create mode 100644 src/java/com/threerings/whirled/spot/data/ClusterObject.java create mode 100644 src/java/com/threerings/whirled/spot/data/ClusteredBodyObject.java create mode 100644 src/java/com/threerings/whirled/spot/data/Location.java create mode 100644 src/java/com/threerings/whirled/spot/data/Portal.java create mode 100644 src/java/com/threerings/whirled/spot/data/SceneLocation.java create mode 100644 src/java/com/threerings/whirled/spot/data/SpotCodes.java create mode 100644 src/java/com/threerings/whirled/spot/data/SpotMarshaller.java create mode 100644 src/java/com/threerings/whirled/spot/data/SpotScene.java create mode 100644 src/java/com/threerings/whirled/spot/data/SpotSceneImpl.java create mode 100644 src/java/com/threerings/whirled/spot/data/SpotSceneModel.java create mode 100644 src/java/com/threerings/whirled/spot/data/SpotSceneObject.java create mode 100644 src/java/com/threerings/whirled/spot/images/scene.dia create mode 100644 src/java/com/threerings/whirled/spot/images/scene.png create mode 100644 src/java/com/threerings/whirled/spot/package.html create mode 100644 src/java/com/threerings/whirled/spot/server/SpotClient.java create mode 100644 src/java/com/threerings/whirled/spot/server/SpotDispatcher.java create mode 100644 src/java/com/threerings/whirled/spot/server/SpotProvider.java create mode 100644 src/java/com/threerings/whirled/spot/server/SpotSceneManager.java create mode 100644 src/java/com/threerings/whirled/spot/tools/EditablePortal.java create mode 100644 src/java/com/threerings/whirled/spot/tools/xml/SpotSceneRuleSet.java create mode 100644 src/java/com/threerings/whirled/spot/tools/xml/SpotSceneWriter.java create mode 100644 src/java/com/threerings/whirled/tools/xml/SceneParser.java create mode 100644 src/java/com/threerings/whirled/tools/xml/SceneRuleSet.java create mode 100644 src/java/com/threerings/whirled/tools/xml/SceneWriter.java create mode 100644 src/java/com/threerings/whirled/util/NoSuchSceneException.java create mode 100644 src/java/com/threerings/whirled/util/SceneFactory.java create mode 100644 src/java/com/threerings/whirled/util/UpdateList.java create mode 100644 src/java/com/threerings/whirled/util/WhirledContext.java create mode 100644 src/java/com/threerings/whirled/zone/Log.java create mode 100644 src/java/com/threerings/whirled/zone/client/ZoneAdapter.java create mode 100644 src/java/com/threerings/whirled/zone/client/ZoneDecoder.java create mode 100644 src/java/com/threerings/whirled/zone/client/ZoneDirector.java create mode 100644 src/java/com/threerings/whirled/zone/client/ZoneObserver.java create mode 100644 src/java/com/threerings/whirled/zone/client/ZoneReceiver.java create mode 100644 src/java/com/threerings/whirled/zone/client/ZoneService.java create mode 100644 src/java/com/threerings/whirled/zone/data/SceneSummary.java create mode 100644 src/java/com/threerings/whirled/zone/data/ZoneCodes.java create mode 100644 src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java create mode 100644 src/java/com/threerings/whirled/zone/data/ZoneSummary.java create mode 100644 src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java create mode 100644 src/java/com/threerings/whirled/zone/server/ZoneDispatcher.java create mode 100644 src/java/com/threerings/whirled/zone/server/ZoneManager.java create mode 100644 src/java/com/threerings/whirled/zone/server/ZoneProvider.java create mode 100644 src/java/com/threerings/whirled/zone/server/ZoneRegistry.java create mode 100644 src/java/com/threerings/whirled/zone/server/ZoneSender.java create mode 100644 src/java/com/threerings/whirled/zone/util/ZoneUtil.java create mode 100644 tests/build.xml create mode 100644 tests/rsrc/whirled/spot/tools/xml/scene.xml create mode 100644 tests/rsrc/whirled/tools/xml/scene.xml create mode 100644 tests/src/java/com/threerings/parlor/TestClient.java create mode 100644 tests/src/java/com/threerings/parlor/TestConfig.java create mode 100644 tests/src/java/com/threerings/parlor/TestController.java create mode 100644 tests/src/java/com/threerings/parlor/TestManager.java create mode 100644 tests/src/java/com/threerings/parlor/TestServer.java create mode 100644 tests/src/java/com/threerings/whirled/DummyClientSceneRepository.java create mode 100644 tests/src/java/com/threerings/whirled/TestClient.java create mode 100644 tests/src/java/com/threerings/whirled/TestConfig.java create mode 100644 tests/src/java/com/threerings/whirled/TestController.java create mode 100644 tests/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneParserTest.java create mode 100644 tests/src/java/com/threerings/whirled/tools/xml/SceneParserTest.java diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..3b204400 --- /dev/null +++ b/LICENSE @@ -0,0 +1,458 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README b/README new file mode 100644 index 00000000..1446bb32 --- /dev/null +++ b/README @@ -0,0 +1,65 @@ +The Vilya library +----------------- + +The Vilya library provides various facilities for making networked multiplayer +games. It's various packages include: + + * whirled - builds on the crowd framework and defines a scene graph with + portals to move between scenes and provides hooks for distributing and + updating scene data (for example isometric rendering information) over + the network + * parlor - builds upon the crowd framework to create the notion of a + game with players and provides tools for making turn based games + * puzzle - builds on the parlor and media frameworks to provide tools + for implementing puzzle games in a networked environment + * micasa - builds on the parlor framework to provide lobbies and + matchmaking for multiplayer games + +Documentation is somewhat sparse at the moment, but inspection of the code in +the tests/ directory shows examples of use of many features of the library. + +Building +-------- + +Building the library is very simple. First ensure that the necessary third +party jar files are available in the lib/ directory. See lib/README for a list +of the necessary third party jar files and how to get them. + +The library is built using Ant, a modern build tool written in and for Java. If +you aren't already using Ant for other projects, it can be found here: + + http://ant.apache.org/ + +Invoke ant with any of the following targets: + + all: builds the distribution files and javadoc documentation + compile: builds only the class files (dist/classes) + javadoc: builds only the javadoc documentation (dist/docs) + dist: builds the distribution jar files (dist/*.jar) + +Distribution +------------ + +The Vilya library is released under the LGPL. The most recent version of the +library is available here: + + http://www.threerings.net/code/vilya/ + +Contribution +------------ + +Contributions to Vilya are welcome. Read-only access to the subversion +repository is available at: + +svn://code.threerings.net/vilya/trunk + +Patches can be mailed to Michael Bayne . + +Contact Information +------------------- + +Vilya is actively developed by the scurvy dogs at Three Rings Design, Inc. +Contact Michael Bayne with questions, comments and other +wordly endeavors. + +$Id$ diff --git a/build.xml b/build.xml new file mode 100644 index 00000000..4f8b5590 --- /dev/null +++ b/build.xml @@ -0,0 +1,294 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/stylesheet.css b/docs/stylesheet.css new file mode 100644 index 00000000..3f0592b8 --- /dev/null +++ b/docs/stylesheet.css @@ -0,0 +1,52 @@ +/* Javadoc style sheet */ + +/* Page background color */ +body { + background-color: #FFFFFF; + font-family: Arial, Helvetica, sans-serif; +} + +pre { + font-family: Courier New, monospace +} + +code { + font-family: Courier New, monospace +} + +/* Headings */ +h1 { font-size: 145% } + +/* Table colors */ +.TableHeadingColor { background: #CCCCFF } /* Dark mauve */ +.TableSubHeadingColor { background: #EEEEFF } /* Light mauve */ +.TableRowColor { background: #FFFFFF } /* White */ + +/* Font used in left-hand frame lists */ +.FrameTitleFont { font-size: 100%; font-family: Helvetica, Arial, sans-serif } +.FrameHeadingFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } +.FrameItemFont { font-size: 90%; font-family: Helvetica, Arial, sans-serif } + +/* Navigation bar fonts and colors */ +.NavBarCell1 { background-color:#EEEEFF;} /* Light mauve */ +.NavBarCell1Rev { background-color:#00008B;} /* Dark Blue */ +.NavBarFont1 { font-family: Arial, Helvetica, sans-serif; color:#000000;} +.NavBarFont1Rev { font-family: Arial, Helvetica, sans-serif; color:#FFFFFF;} + +.NavBarCell2 { + font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; +} +.NavBarCell3 { + font-family: Arial, Helvetica, sans-serif; background-color:#FFFFFF; +} + +.example { + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + margin-left: 20px; + margin-right: 20px; + border: 1px solid black; + background-color: #FFFF99 +} diff --git a/lib/LIBS b/lib/LIBS new file mode 100644 index 00000000..a423b0cd --- /dev/null +++ b/lib/LIBS @@ -0,0 +1,27 @@ +ant.jar +commons-beanutils.jar +commons-collections.jar +commons-digester.jar +commons-io.jar +commons-lang.jar +commons-logging.jar +jme-awt.jar +jme-bui.jar +jme-effects.jar +jme.jar +jme-model.jar +jme-sound.jar +junit-3.7.jar +lwjgl.jar +lwjgl_util.jar +narya-base.jar +narya-distrib.jar +narya-tests.jar +nenya-cast.jar +nenya-media.jar +nenya-miso.jar +nenya-rsrc.jar +retroweaver-all-1.2.2.jar +samskivert.jar +velocity-1.5-dev.jar +xml-writer.jar diff --git a/lib/README b/lib/README new file mode 100644 index 00000000..c84ceaba --- /dev/null +++ b/lib/README @@ -0,0 +1,22 @@ +Required external libraries +--------------------------- + +The Narya code requires the following external Java libraries (JAR files) +to be placed (or symlinked) here in the lib/ directory or in the shared +library directory specified in the build.xml file (java.libraries). + +* The samskivert library: + http://samskivert.com/code/samskivert/ + +* Apache Commons libraries: collections, digester, io, and lang + http://jakarta.apache.org/commons/ + +* Megginson Technologies' XMLWriter 0.2 library: + http://www.megginson.com/Software/xml-writer-0.2.zip + +Once the jar files are in place (regardless of what they are named), the +build system will automatically include them in the compile-time +classpath. + +-- +$Id: README 3105 2004-08-27 18:38:55Z mdb $ diff --git a/lib/SOURCE_HEADER b/lib/SOURCE_HEADER new file mode 100644 index 00000000..c86bdcba --- /dev/null +++ b/lib/SOURCE_HEADER @@ -0,0 +1,21 @@ +// +// $Id$ +// +// Vilya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/vilya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + diff --git a/src/java/com/threerings/micasa/Log.java b/src/java/com/threerings/micasa/Log.java new file mode 100644 index 00000000..1d73f14e --- /dev/null +++ b/src/java/com/threerings/micasa/Log.java @@ -0,0 +1,72 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa; + +/** + * A placeholder class that contains a reference to the log object used by + * the MiCasa package. This is a useful pattern to use when using the + * samskivert logging facilities. One creates a top-level class like this + * one that instantiates a log object with an name that identifies log + * messages from that package and then provides static methods that + * generate log messages using that instance. Then, classes in that + * package need only import the log wrapper class and can easily use it to + * generate log messages. For example: + * + *
+ * import com.threerings.micasa.Log;
+ * // ...
+ * Log.warning("All hell is breaking loose!");
+ * // ...
+ * 
+ * + * @see com.samskivert.util.Log + */ +public class Log +{ + /** The static log instance configured for use by this package. */ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("micasa"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/micasa/client/ChatPanel.java b/src/java/com/threerings/micasa/client/ChatPanel.java new file mode 100644 index 00000000..a36ba5d8 --- /dev/null +++ b/src/java/com/threerings/micasa/client/ChatPanel.java @@ -0,0 +1,336 @@ +// +// $Id: ChatPanel.java 3741 2005-10-25 11:13:51Z elizabeth $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import java.util.StringTokenizer; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.JTextPane; + +import javax.swing.event.AncestorEvent; +import javax.swing.event.AncestorListener; + +import javax.swing.text.BadLocationException; +import javax.swing.text.Document; +import javax.swing.text.Style; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; +import com.samskivert.swing.event.AncestorAdapter; + +import com.threerings.util.Name; + +import com.threerings.crowd.chat.client.ChatDirector; +import com.threerings.crowd.chat.client.ChatDisplay; +import com.threerings.crowd.chat.data.ChatCodes; +import com.threerings.crowd.chat.data.ChatMessage; +import com.threerings.crowd.chat.data.SystemMessage; +import com.threerings.crowd.chat.data.UserMessage; + +import com.threerings.crowd.client.OccupantObserver; +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.OccupantInfo; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.micasa.Log; + +public class ChatPanel + extends JPanel + implements ActionListener, ChatDisplay, OccupantObserver, PlaceView +{ + public ChatPanel (CrowdContext ctx) + { + // keep this around for later + _ctx = ctx; + + // create our chat director and register ourselves with it + _chatdtr = new ChatDirector(_ctx, null, null); + // XXX - the line above royally borks things because it sends + // null, null downstream. + _chatdtr.addChatDisplay(this); + + // register as an occupant observer + _ctx.getOccupantDirector().addOccupantObserver(this); + + GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + setLayout(gl); + + // create our scrolling chat text display + _text = new JTextPane(); + _text.setEditable(false); + add(new JScrollPane(_text)); + + // create our styles and add those to the text pane + createStyles(_text); + + // add a label for the text entry stuff + add(new JLabel("Type here to chat:"), GroupLayout.FIXED); + + // create a horizontal group for the text entry bar + gl = new HGroupLayout(GroupLayout.STRETCH); + JPanel epanel = new JPanel(gl); + epanel.add(_entry = new JTextField()); + _entry.setActionCommand("send"); + _entry.addActionListener(this); + _entry.setEnabled(false); + + _send = new JButton("Send"); + _send.setEnabled(false); + _send.addActionListener(this); + _send.setActionCommand("send"); + epanel.add(_send, GroupLayout.FIXED); + add(epanel, GroupLayout.FIXED); + + // listen to ancestor events to request focus when added + addAncestorListener(new AncestorAdapter() { + public void ancestorAdded (AncestorEvent e) { + if (_focus) { + _entry.requestFocusInWindow(); + } + } + }); + } + + /** + * For applications where the chat box has extremely limited space, + * the send button can be removed to leave more space for the text + * input box. + */ + public void removeSendButton () + { + if (_send.isVisible()) { + // _send.getParent().remove(_send); + _send.setVisible(false); + } + } + + /** + * Sets whether the chat box text entry field requests the keyboard + * focus when the panel receives {@link + * AncestorListener#ancestorAdded} or {@link PlaceView#willEnterPlace} + * events. + */ + public void setRequestFocus (boolean focus) + { + _focus = focus; + } + + protected void createStyles (JTextPane text) + { + StyleContext sctx = StyleContext.getDefaultStyleContext(); + Style defstyle = sctx.getStyle(StyleContext.DEFAULT_STYLE); + + _nameStyle = text.addStyle("name", defstyle); + StyleConstants.setForeground(_nameStyle, Color.blue); + + _msgStyle = text.addStyle("msg", defstyle); + StyleConstants.setForeground(_msgStyle, Color.black); + + _errStyle = text.addStyle("err", defstyle); + StyleConstants.setForeground(_errStyle, Color.red); + + _noticeStyle = text.addStyle("notice", defstyle); + StyleConstants.setForeground(_noticeStyle, Color.magenta); + } + + // documentation inherited + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + if (cmd.equals("send")) { + sendText(); + + } else { + System.out.println("Unknown action event: " + cmd); + } + } + + // documentation inherited + public void occupantEntered (OccupantInfo info) + { + displayOccupantMessage("*** " + info.username + " entered."); + } + + // documentation inherited + public void occupantLeft (OccupantInfo info) + { + displayOccupantMessage("*** " + info.username + " left."); + } + + // documentation inherited + public void occupantUpdated (OccupantInfo oinfo, OccupantInfo info) + { + } + + protected void displayOccupantMessage (String message) + { + append(message + "\n", _noticeStyle); + } + + protected void sendText () + { + String text = _entry.getText(); + + // if the message to send begins with /tell then parse it and + // generate a tell request rather than a speak request + if (text.startsWith("/tell")) { + StringTokenizer tok = new StringTokenizer(text); + // there should be at least three tokens: '/tell target word' + if (tok.countTokens() < 3) { + displayError("Usage: /tell username message"); + return; + } + + // skip the /tell and grab the username + tok.nextToken(); + String username = tok.nextToken(); + + // now strip off everything up to the username to get the + // message + int uidx = text.indexOf(username); + String message = text.substring(uidx + username.length()).trim(); + + // request to send this text as a tell message + _chatdtr.requestTell(new Name(username), message, null); + + } else if (text.startsWith("/clear")) { + // clear the chat box + _chatdtr.clearDisplays(); + + } else { + // request to send this text as a chat message + _chatdtr.requestSpeak(null, text, ChatCodes.DEFAULT_MODE); + } + + // clear out the input because we sent a request + _entry.setText(""); + } + + // documentation inherited from interface ChatDisplay + public void clear () + { + _text.setText(""); + } + + // documentation inherited from interface ChatDisplay + public void displayMessage (ChatMessage message) + { + if (message instanceof UserMessage) { + UserMessage msg = (UserMessage) message; + if (msg.localtype == ChatCodes.USER_CHAT_TYPE) { + append("[" + msg.speaker + " whispers] ", _nameStyle); + append(msg.message + "\n", _msgStyle); + } else { + append("<" + msg.speaker + "> ", _nameStyle); + append(msg.message + "\n", _msgStyle); + } + + } else if (message instanceof SystemMessage) { + append(message.message + "\n", _noticeStyle); + + } else { + Log.warning("Received unknown message type [message=" + + message + "]."); + } + } + + protected void displayError (String message) + { + append(message + "\n", _errStyle); + } + + /** + * Append the specified text in the specified style. + */ + protected void append (String text, Style style) + { + Document doc = _text.getDocument(); + try { + doc.insertString(doc.getLength(), text, style); + } catch (BadLocationException ble) { + Log.warning("Unable to insert text!? [error=" + ble + "]."); + } + } + + public void willEnterPlace (PlaceObject place) + { + // enable our chat input elements since we're now somewhere that + // we can chat + _entry.setEnabled(true); + _send.setEnabled(true); + if (_focus) { + _entry.requestFocusInWindow(); + } + } + + // documentation inherited + public void didLeavePlace (PlaceObject place) + { + // nothing doing + } + + // documentation inherited + public Dimension getPreferredSize () + { + Dimension size = super.getPreferredSize(); + // always prefer a sensible but not overly large width. this also + // prevents us from inheriting a foolishly large preferred width + // from the JTextPane which sometimes decides it wants to be as + // wide as its widest line of text rather than wrap that line of + // text. + size.width = PREFERRED_WIDTH; + return size; + } + + protected CrowdContext _ctx; + protected ChatDirector _chatdtr; + + protected boolean _focus = true; + + protected JComboBox _roombox; + protected JTextPane _text; + protected JButton _send; + protected JTextField _entry; + + protected Style _nameStyle; + protected Style _msgStyle; + protected Style _errStyle; + protected Style _noticeStyle; + + /** A width that isn't so skinny that the text is teeny. */ + protected static final int PREFERRED_WIDTH = 200; +} diff --git a/src/java/com/threerings/micasa/client/ClientController.java b/src/java/com/threerings/micasa/client/ClientController.java new file mode 100644 index 00000000..659aafb3 --- /dev/null +++ b/src/java/com/threerings/micasa/client/ClientController.java @@ -0,0 +1,140 @@ +// +// $Id: ClientController.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.awt.event.ActionEvent; +import com.samskivert.swing.Controller; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.SessionObserver; + +import com.threerings.crowd.data.BodyObject; + +import com.threerings.micasa.Log; +import com.threerings.micasa.data.MiCasaBootstrapData; +import com.threerings.micasa.util.MiCasaContext; + +/** + * Responsible for top-level control of the client user interface. + */ +public class ClientController extends Controller + implements SessionObserver +{ + /** + * Creates a new client controller. The controller will set everything + * up in preparation for logging on. + */ + public ClientController (MiCasaContext ctx, MiCasaFrame frame) + { + // we'll want to keep these around + _ctx = ctx; + _frame = frame; + + // we want to know about logon/logoff + _ctx.getClient().addClientObserver(this); + + // create the logon panel and display it + _logonPanel = new LogonPanel(_ctx); + _frame.setPanel(_logonPanel); + } + + // documentation inherited + public boolean handleAction (ActionEvent action) + { + String cmd = action.getActionCommand(); + + if (cmd.equals("logoff")) { + // request that we logoff + _ctx.getClient().logoff(true); + return true; + } + + Log.info("Unhandled action: " + action); + return false; + } + + // documentation inherited + public void clientDidLogon (Client client) + { + Log.info("Client did logon [client=" + client + "]."); + + // keep the body object around for stuff + _body = (BodyObject)client.getClientObject(); + + // figure out where to go + int moveOid = -1; + + // hacky hack + String jumpOidStr = null; + try { + jumpOidStr = System.getProperty("jumpoid"); + } catch (SecurityException se) { + Log.info("Not checking for jumpOid as we're in an applet."); + } + + if (jumpOidStr != null) { + try { + moveOid = Integer.parseInt(jumpOidStr); + } catch (NumberFormatException nfe) { + Log.warning("Invalid jump oid [oid=" + jumpOidStr + + ", err=" + nfe + "]."); + } + + } else if (_body.location != -1) { + // if we were already in a location, go there + moveOid = _body.location; + + } else { + // otherwise head to the default lobby to start things off + MiCasaBootstrapData data = (MiCasaBootstrapData) + client.getBootstrapData(); + moveOid = data.defLobbyOid; + } + + if (moveOid > 0) { + _ctx.getLocationDirector().moveTo(moveOid); + } + } + + // documentation inherited + public void clientObjectDidChange (Client client) + { + // regrab our body object + _body = (BodyObject)client.getClientObject(); + } + + // documentation inherited + public void clientDidLogoff (Client client) + { + Log.info("Client did logoff [client=" + client + "]."); + + // reinstate the logon panel + _frame.setPanel(_logonPanel); + } + + protected MiCasaContext _ctx; + protected MiCasaFrame _frame; + protected BodyObject _body; + + // our panels + protected LogonPanel _logonPanel; +} diff --git a/src/java/com/threerings/micasa/client/LogonPanel.java b/src/java/com/threerings/micasa/client/LogonPanel.java new file mode 100644 index 00000000..573c5e12 --- /dev/null +++ b/src/java/com/threerings/micasa/client/LogonPanel.java @@ -0,0 +1,234 @@ +// +// $Id: LogonPanel.java 4158 2006-05-30 22:12:15Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.awt.Dimension; +import java.awt.Font; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPasswordField; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; + +import com.threerings.util.MessageBundle; +import com.threerings.util.Name; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.ClientObserver; +import com.threerings.presents.client.LogonException; +import com.threerings.presents.net.Credentials; +import com.threerings.presents.net.UsernamePasswordCreds; + +import com.threerings.micasa.util.MiCasaContext; + +public class LogonPanel extends JPanel + implements ActionListener, ClientObserver +{ + public LogonPanel (MiCasaContext ctx) + { + // keep these around for later + _ctx = ctx; + _msgs = _ctx.getMessageManager().getBundle("micasa"); + + setLayout(new VGroupLayout()); + + // stick the logon components into a panel that will stretch them + // to a sensible width + JPanel box = new JPanel( + new VGroupLayout(VGroupLayout.NONE, VGroupLayout.STRETCH, + 5, VGroupLayout.CENTER)) { + public Dimension getPreferredSize () { + Dimension psize = super.getPreferredSize(); + psize.width = Math.max(psize.width, 300); + return psize; + } + }; + add(box); + + // try obtaining our title text from a system property + String tstr = null; + try { + tstr = System.getProperty("logon.title"); + } catch (Throwable t) { + } + if (tstr == null) { + tstr = "Mi Casa!"; + } + + // create a big fat label + JLabel title = new JLabel(tstr, JLabel.CENTER); + title.setFont(new Font("Helvetica", Font.BOLD, 24)); + box.add(title); + + // create the username bar + JPanel bar = new JPanel(new HGroupLayout(GroupLayout.STRETCH)); + bar.add(new JLabel(_msgs.get("m.username")), GroupLayout.FIXED); + _username = new JTextField(); + _username.setActionCommand("skipToPassword"); + _username.addActionListener(this); + bar.add(_username); + box.add(bar); + + // create the password bar + bar = new JPanel(new HGroupLayout(GroupLayout.STRETCH)); + bar.add(new JLabel(_msgs.get("m.password")), GroupLayout.FIXED); + _password = new JPasswordField(); + _password.setActionCommand("logon"); + _password.addActionListener(this); + bar.add(_password); + box.add(bar); + + // create the logon button bar + HGroupLayout gl = new HGroupLayout(GroupLayout.NONE); + gl.setJustification(GroupLayout.RIGHT); + bar = new JPanel(gl); + _logon = new JButton(_msgs.get("m.logon")); + _logon.setActionCommand("logon"); + _logon.addActionListener(this); + bar.add(_logon); + box.add(bar); + + box.add(new JLabel(_msgs.get("m.status"))); + _status = new JTextArea() { + public Dimension getPreferredScrollableViewportSize () + { + return new Dimension(10, 100); + } + }; + _status.setEditable(false); + JScrollPane scroller = new JScrollPane(_status); + box.add(scroller); + + // we'll want to listen for logon failure + _ctx.getClient().addClientObserver(this); + + // start with focus in the username field + _username.requestFocusInWindow(); + } + + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + if (cmd.equals("skipToPassword")) { + _password.requestFocusInWindow(); + + } else if (cmd.equals("logon")) { + logon(); + + } else { + System.out.println("Unknown action event: " + cmd); + } + } + + // documentation inherited from interface + public void clientDidLogon (Client client) + { + _status.append(_msgs.get("m.logon_success") + "\n"); + } + + // documentation inherited from interface + public void clientDidLogoff (Client client) + { + _status.append(_msgs.get("m.logged_off") + "\n"); + setLogonEnabled(true); + } + + // documentation inherited from interface + public void clientFailedToLogon (Client client, Exception cause) + { + String msg; + if (cause instanceof LogonException) { + msg = MessageBundle.compose("m.logon_failed", cause.getMessage()); + } else { + msg = MessageBundle.tcompose("m.logon_failed", cause.getMessage()); + } + _status.append(_msgs.xlate(msg) + "\n"); + setLogonEnabled(true); + } + + // documentation inherited from interface + public void clientObjectDidChange (Client client) + { + // nothing we can do here... + } + + // documentation inherited from interface + public void clientConnectionFailed (Client client, Exception cause) + { + String msg = MessageBundle.tcompose("m.connection_failed", + cause.getMessage()); + _status.append(_msgs.xlate(msg) + "\n"); + setLogonEnabled(true); + } + + // documentation inherited from interface + public boolean clientWillLogoff (Client client) + { + // no vetoing here + return true; + } + + protected void logon () + { + // disable further logon attempts until we hear back + setLogonEnabled(false); + + Name username = new Name(_username.getText().trim()); + String password = new String(_password.getPassword()).trim(); + + String server = _ctx.getClient().getHostname(); + int port = _ctx.getClient().getPorts()[0]; + String msg = MessageBundle.tcompose("m.logging_on", + server, String.valueOf(port)); + _status.append(_msgs.xlate(msg) + "\n"); + + // configure the client with some credentials and logon + Credentials creds = new UsernamePasswordCreds(username, password); + Client client = _ctx.getClient(); + client.setCredentials(creds); + client.logon(); + } + + protected void setLogonEnabled (boolean enabled) + { + _username.setEnabled(enabled); + _password.setEnabled(enabled); + _logon.setEnabled(enabled); + } + + protected MiCasaContext _ctx; + protected MessageBundle _msgs; + + protected JTextField _username; + protected JPasswordField _password; + protected JButton _logon; + protected JTextArea _status; +} diff --git a/src/java/com/threerings/micasa/client/MiCasaApp.java b/src/java/com/threerings/micasa/client/MiCasaApp.java new file mode 100644 index 00000000..2b9db597 --- /dev/null +++ b/src/java/com/threerings/micasa/client/MiCasaApp.java @@ -0,0 +1,115 @@ +// +// $Id: MiCasaApp.java 4158 2006-05-30 22:12:15Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.io.IOException; + +import com.samskivert.swing.util.SwingUtil; +import com.threerings.util.Name; + +import com.threerings.presents.client.Client; +import com.threerings.presents.net.UsernamePasswordCreds; + +import com.threerings.micasa.Log; + +/** + * The micasa app is the main point of entry for the MiCasa client + * application. It creates and initializes the myriad components of the + * client and sets all the proper wheels in motion. + */ +public class MiCasaApp +{ + public void init () + throws IOException + { + // create a frame + _frame = new MiCasaFrame(); + + // create our client instance + String cclass = null; + try { + cclass = System.getProperty("client"); + } catch (Throwable t) { + // security manager in effect, no problem + } + if (cclass == null) { + cclass = MiCasaClient.class.getName(); + } + + try { + _client = (MiCasaClient)Class.forName(cclass).newInstance(); + } catch (Exception e) { + Log.warning("Unable to instantiate client class " + + "[cclass=" + cclass + "]."); + Log.logStackTrace(e); + } + + // initialize our client instance + _client.init(_frame); + } + + public void run (String server, String username, String password) + { + // position everything and show the frame + _frame.setSize(800, 600); + SwingUtil.centerWindow(_frame); + _frame.setVisible(true); + + Client client = _client.getContext().getClient(); + + Log.info("Using [server=" + server + "."); + client.setServer(server, Client.DEFAULT_SERVER_PORTS); + + // configure the client with some credentials and logon + if (username != null && password != null) { + // create and set our credentials + client.setCredentials( + new UsernamePasswordCreds(new Name(username), password)); + client.logon(); + } + } + + public static void main (String[] args) + { + String server = "localhost"; + if (args.length > 0) { + server = args[0]; + } + String username = (args.length > 1) ? args[1] : null; + String password = (args.length > 2) ? args[2] : null; + + MiCasaApp app = new MiCasaApp(); + try { + // initialize the app + app.init(); + } catch (IOException ioe) { + Log.warning("Error initializing application."); + Log.logStackTrace(ioe); + } + + // and run it + app.run(server, username, password); + } + + protected MiCasaClient _client; + protected MiCasaFrame _frame; +} diff --git a/src/java/com/threerings/micasa/client/MiCasaApplet.java b/src/java/com/threerings/micasa/client/MiCasaApplet.java new file mode 100644 index 00000000..941be0c9 --- /dev/null +++ b/src/java/com/threerings/micasa/client/MiCasaApplet.java @@ -0,0 +1,124 @@ +// +// $Id: MiCasaApplet.java 4158 2006-05-30 22:12:15Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.applet.Applet; +import java.io.IOException; + +import com.samskivert.swing.util.SwingUtil; +import com.threerings.util.Name; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.ClientAdapter; +import com.threerings.presents.net.UsernamePasswordCreds; + +import com.threerings.micasa.Log; + +/** + * The MiCasa applet is used to make MiCasa games available via the web + * browser. + */ +public class MiCasaApplet extends Applet +{ + /** + * Create the client instance and set things up. + */ + public void init () + { + try { + // create a frame + _frame = new MiCasaFrame(); + + // create our client instance + _client = new MiCasaClient(); + _client.init(_frame); + + Name username = new Name(requireParameter("username")); + String authkey = requireParameter("authkey"); + String server = getCodeBase().getHost(); + + Client client = _client.getContext().getClient(); + + // indicate which server to which we should connect + client.setServer(server, Client.DEFAULT_SERVER_PORTS); + + // create and set our credentials + client.setCredentials( + new UsernamePasswordCreds(username, authkey)); + + // we want to hide the client frame when we logoff + client.addClientObserver(new ClientAdapter() { + public void clientDidLogoff (Client c) + { + _frame.setVisible(false); + } + }); + + } catch (IOException ioe) { + Log.warning("Unable to create client."); + Log.logStackTrace(ioe); + } + } + + protected String requireParameter (String name) + throws IOException + { + String value = getParameter(name); + if (value == null) { + throw new IOException("Applet missing '" + name + "' parameter."); + } + return value; + } + + /** + * Display the client frame and really get things going. + */ + public void start () + { + if (_client != null) { + // show the frame + _frame.setSize(800, 600); + SwingUtil.centerWindow(_frame); + _frame.setVisible(true); + // and log on + _client.getContext().getClient().logon(); + } + } + + /** + * Log off and shut on down. + */ + public void stop () + { + if (_client != null) { + // hide the frame and log off + _frame.setVisible(false); + Client client = _client.getContext().getClient(); + if (client.isLoggedOn()) { + client.logoff(false); + } + } + } + + protected MiCasaClient _client; + protected MiCasaFrame _frame; +} diff --git a/src/java/com/threerings/micasa/client/MiCasaClient.java b/src/java/com/threerings/micasa/client/MiCasaClient.java new file mode 100644 index 00000000..e10c2844 --- /dev/null +++ b/src/java/com/threerings/micasa/client/MiCasaClient.java @@ -0,0 +1,232 @@ +// +// $Id: MiCasaClient.java 4158 2006-05-30 22:12:15Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.awt.EventQueue; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.io.IOException; +import javax.swing.JPanel; + +import com.samskivert.util.Config; +import com.samskivert.util.RunQueue; +import com.threerings.util.MessageManager; + +import com.threerings.presents.client.Client; +import com.threerings.presents.dobj.DObjectManager; + +import com.threerings.crowd.client.LocationDirector; +import com.threerings.crowd.client.OccupantDirector; +import com.threerings.crowd.client.PlaceView; + +import com.threerings.crowd.chat.client.ChatDirector; + +import com.threerings.parlor.client.ParlorDirector; + +import com.threerings.micasa.util.MiCasaContext; + +/** + * The MiCasa client takes care of instantiating all of the proper + * managers and loading up all of the necessary configuration and getting + * the client bootstrapped. It can be extended by games that require an + * extended context implementation. + */ +public class MiCasaClient + implements RunQueue +{ + /** + * Initializes a new client and provides it with a frame in which to + * display everything. + */ + public void init (MiCasaFrame frame) + throws IOException + { + // create our context + _ctx = createContextImpl(); + + // create the directors/managers/etc. provided by the context + createContextServices(); + + // for test purposes, hardcode the server info + _client.setServer("localhost", Client.DEFAULT_SERVER_PORTS); + + // keep this for later + _frame = frame; + + // log off when they close the window + _frame.addWindowListener(new WindowAdapter() { + public void windowClosing (WindowEvent evt) { + // if we're logged on, log off + if (_client.isLoggedOn()) { + _client.logoff(true); + } else { + // otherwise get the heck out + System.exit(0); + } + } + }); + + // create our client controller and stick it in the frame + _frame.setController(new ClientController(_ctx, _frame)); + } + + /** + * Returns a reference to the context in effect for this client. This + * reference is valid for the lifetime of the application. + */ + public MiCasaContext getContext () + { + return _ctx; + } + + /** + * Creates the {@link MiCasaContext} implementation that will be + * passed around to all of the client code. Derived classes may wish + * to override this and create some extended context implementation. + */ + protected MiCasaContext createContextImpl () + { + return new MiCasaContextImpl(); + } + + /** + * Creates and initializes the various services that are provided by + * the context. Derived classes that provide an extended context + * should override this method and create their own extended + * services. They should be sure to call + * super.createContextServices. + */ + protected void createContextServices () + throws IOException + { + // create the handles on our various services + _client = new Client(null, this); + + // create our managers and directors + _locdir = new LocationDirector(_ctx); + _occdir = new OccupantDirector(_ctx); + _pardtr = new ParlorDirector(_ctx); + _msgmgr = new MessageManager(MESSAGE_MANAGER_PREFIX); + _chatdir = new ChatDirector(_ctx, _msgmgr, null); + } + + // documentation inherited from interface RunQueue + public void postRunnable (Runnable run) + { + // queue it on up on the awt thread + EventQueue.invokeLater(run); + } + + // documentation inherited from interface RunQueue + public boolean isDispatchThread () + { + return EventQueue.isDispatchThread(); + } + + /** + * The context implementation. This provides access to all of the + * objects and services that are needed by the operating client. + */ + protected class MiCasaContextImpl implements MiCasaContext + { + /** + * Apparently the default constructor has default access, rather + * than protected access, even though this class is declared to be + * protected. Why, I don't know, but we need to be able to extend + * this class elsewhere, so we need this. + */ + protected MiCasaContextImpl () + { + } + + public Client getClient () + { + return _client; + } + + public DObjectManager getDObjectManager () + { + return _client.getDObjectManager(); + } + + public Config getConfig () + { + return _config; + } + + public LocationDirector getLocationDirector () + { + return _locdir; + } + + public OccupantDirector getOccupantDirector () + { + return _occdir; + } + + public ChatDirector getChatDirector () + { + return _chatdir; + } + + public ParlorDirector getParlorDirector () + { + return _pardtr; + } + + public void setPlaceView (PlaceView view) + { + // stick the place view into our frame + _frame.setPanel((JPanel)view); + } + + public void clearPlaceView (PlaceView view) + { + // we'll just let the next place view replace our old one + } + + public MiCasaFrame getFrame () + { + return _frame; + } + + public MessageManager getMessageManager () + { + return _msgmgr; + } + } + + protected MiCasaContext _ctx; + protected MiCasaFrame _frame; + protected Config _config = new Config("micasa"); + + protected Client _client; + protected LocationDirector _locdir; + protected OccupantDirector _occdir; + protected ChatDirector _chatdir; + protected ParlorDirector _pardtr; + protected MessageManager _msgmgr; + + /** The prefix prepended to localization bundle names before looking + * them up in the classpath. */ + protected static final String MESSAGE_MANAGER_PREFIX = "rsrc.i18n"; +} diff --git a/src/java/com/threerings/micasa/client/MiCasaFrame.java b/src/java/com/threerings/micasa/client/MiCasaFrame.java new file mode 100644 index 00000000..15e12692 --- /dev/null +++ b/src/java/com/threerings/micasa/client/MiCasaFrame.java @@ -0,0 +1,89 @@ +// +// $Id: MiCasaFrame.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.awt.BorderLayout; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.ControllerProvider; + +/** + * Contains the user interface for the MiCasa client application. + */ +public class MiCasaFrame + extends JFrame implements ControllerProvider +{ + /** + * Constructs the top-level MiCasa client frame. + */ + public MiCasaFrame () + { + this("MiCasa Client"); + } + + /** + * Constructs the top-level MiCasa client frame with the specified + * window title. + */ + public MiCasaFrame (String title) + { + super(title); + + // we'll handle shutting things down ourselves + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + } + + /** + * Sets the panel that makes up the entire client display. + */ + public void setPanel (JPanel panel) + { + // remove the old panel + getContentPane().removeAll(); + // add the new one + getContentPane().add(panel, BorderLayout.CENTER); + // swing doesn't properly repaint after adding/removing children + panel.revalidate(); + repaint(); + } + + /** + * Sets the controller for the outermost scope. This controller will + * handle all actions that aren't handled by controllers of tigher + * scope. + */ + public void setController (Controller controller) + { + _controller = controller; + } + + // documentation inherited + public Controller getController () + { + return _controller; + } + + protected Controller _controller; +} diff --git a/src/java/com/threerings/micasa/client/OccupantList.java b/src/java/com/threerings/micasa/client/OccupantList.java new file mode 100644 index 00000000..5222eb19 --- /dev/null +++ b/src/java/com/threerings/micasa/client/OccupantList.java @@ -0,0 +1,101 @@ +// +// $Id: OccupantList.java 3406 2005-03-15 02:12:03Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.client; + +import java.util.Iterator; +import javax.swing.DefaultListModel; +import javax.swing.JList; + +import com.threerings.crowd.client.OccupantObserver; +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.OccupantInfo; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +/** + * The occupant list displays the list of users that are in a particular + * place. + */ +public class OccupantList + extends JList implements PlaceView, OccupantObserver +{ + /** + * Constructs an occupant list with the supplied context which it will + * use to register itself with the necessary managers. + */ + public OccupantList (CrowdContext ctx) + { + // set up our list model + _model = new DefaultListModel(); + setModel(_model); + + // keep our context around for later + _ctx = ctx; + + // register ourselves as an occupant observer + _ctx.getOccupantDirector().addOccupantObserver(this); + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + // add all of the occupants of the place to our list + Iterator users = plobj.occupantInfo.iterator(); + while (users.hasNext()) { + OccupantInfo info = (OccupantInfo)users.next(); + _model.addElement(info.username); + } + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + // clear out our occupant entries + _model.clear(); + } + + // documentation inherited + public void occupantEntered (OccupantInfo info) + { + // simply add this user to our list + _model.addElement(info.username); + } + + // documentation inherited + public void occupantLeft (OccupantInfo info) + { + // remove this occupant from our list + _model.removeElement(info.username); + } + + // documentation inherited + public void occupantUpdated (OccupantInfo oinfo, OccupantInfo info) + { + // nothing doing + } + + /** Our client context. */ + protected CrowdContext _ctx; + + /** A list model that provides a vector interface. */ + protected DefaultListModel _model; +} diff --git a/src/java/com/threerings/micasa/data/MiCasaBootstrapData.java b/src/java/com/threerings/micasa/data/MiCasaBootstrapData.java new file mode 100644 index 00000000..4edc8495 --- /dev/null +++ b/src/java/com/threerings/micasa/data/MiCasaBootstrapData.java @@ -0,0 +1,34 @@ +// +// $Id: MiCasaBootstrapData.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.data; + +import com.threerings.presents.net.BootstrapData; + +/** + * Extends the basic Presents bootstrap data and provides some bootstrap + * information specific to the MiCasa services. + */ +public class MiCasaBootstrapData extends BootstrapData +{ + /** The oid of the default lobby. */ + public int defLobbyOid; +} diff --git a/src/java/com/threerings/micasa/lobby/Lobby.java b/src/java/com/threerings/micasa/lobby/Lobby.java new file mode 100644 index 00000000..59e5b2ca --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/Lobby.java @@ -0,0 +1,59 @@ +// +// $Id: Lobby.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import com.threerings.io.SimpleStreamableObject; + +/** + * A simple class for keeping track of information for each lobby in + * operation on the server. + */ +public class Lobby extends SimpleStreamableObject +{ + /** The object id of the lobby place object. */ + public int placeOid; + + /** The universal game identifier string for the game matchmade by + * this lobby. */ + public String gameIdent; + + /** The human readable name of the lobby. */ + public String name; + + /** + * Constructs a lobby record and initializes it with the specified + * values. + */ + public Lobby (int placeOid, String gameIdent, String name) + { + this.placeOid = placeOid; + this.gameIdent = gameIdent; + this.name = name; + } + + /** + * Constructs a blank lobby record suitable for unserialization. + */ + public Lobby () + { + } +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyConfig.java b/src/java/com/threerings/micasa/lobby/LobbyConfig.java new file mode 100644 index 00000000..264e7a8f --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyConfig.java @@ -0,0 +1,108 @@ +// +// $Id: LobbyConfig.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import javax.swing.JComponent; +import javax.swing.JLabel; + +import java.util.Properties; +import com.samskivert.util.StringUtil; + +import com.threerings.crowd.client.PlaceController; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.micasa.util.MiCasaContext; +import com.threerings.parlor.game.data.GameConfig; + +public class LobbyConfig extends PlaceConfig +{ + // documentation inherited + public PlaceController createController () + { + return new LobbyController(); + } + + // documentation inherited + public String getManagerClassName () + { + return "com.threerings.micasa.lobby.LobbyManager"; + } + + /** + * Derived classes override this function and create the appropriate + * matchmaking user interface component. + */ + public JComponent createMatchMakingView (MiCasaContext ctx) + { + return new JLabel("Match-making view goes here."); + } + + /** + * Instantiates and returns a game config instance using the game + * config classname provided by the lobby configuration parameters. + * + * @exception Exception thrown if a problem occurs loading or + * instantiating the class. + */ + public GameConfig getGameConfig () + throws Exception + { + return (GameConfig)Class.forName(_gameConfigClass).newInstance(); + } + + /** + * Initializes this lobby config object with the properties that are + * used to configure the lobby. This is called on the server when the + * lobby is loaded. + */ + public void init (Properties config) + throws Exception + { + _gameConfigClass = getConfigValue(config, "game_config"); + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + if (buf.length() > 1) { + buf.append(", "); + } + buf.append("game_config=").append(_gameConfigClass); + } + + /** Looks up a configuration property in the supplied properties + * object and throws an exception if it's not found. */ + protected String getConfigValue (Properties config, String key) + throws Exception + { + String value = config.getProperty(key); + if (StringUtil.isBlank(value)) { + throw new Exception("Missing '" + key + "' definition in " + + "lobby configuration."); + } + return value; + } + + /** The name of the game config class that represents the type of game + * we are matchmaking for in this lobby. */ + protected String _gameConfigClass; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyController.java b/src/java/com/threerings/micasa/lobby/LobbyController.java new file mode 100644 index 00000000..a56efd79 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyController.java @@ -0,0 +1,121 @@ +// +// $Id: LobbyController.java 3440 2005-03-30 01:09:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import com.threerings.util.Name; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.client.PlaceController; +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.parlor.client.*; +import com.threerings.parlor.game.data.GameConfig; + +import com.threerings.micasa.Log; +import com.threerings.micasa.util.MiCasaContext; + +public class LobbyController extends PlaceController + implements InvitationHandler, InvitationResponseObserver +{ + public void init (CrowdContext ctx, PlaceConfig config) + { + // cast our context reference + _ctx = (MiCasaContext)ctx; + _config = (LobbyConfig)config; + + super.init(ctx, config); + + // register ourselves as the invitation handler + _ctx.getParlorDirector().setInvitationHandler(this); + } + + protected PlaceView createPlaceView (CrowdContext ctx) + { + return new LobbyPanel(_ctx, _config); + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + super.willEnterPlace(plobj); + + try { + // if a system property has been specified requesting that we + // invite another player, do so now + String invitee = System.getProperty("invitee"); + if (invitee != null) { + // create a game config object + try { + GameConfig config = _config.getGameConfig(); + _ctx.getParlorDirector().invite( + new Name(invitee), config, this); + } catch (Exception e) { + Log.warning("Error instantiating game config."); + Log.logStackTrace(e); + } + } + + } catch (SecurityException se) { + // nothing to see here, move it along... + } + } + + // documentation inherited from interface + public void invitationReceived (Invitation invite) + { + Log.info("Invitation received [invite=" + invite + "]."); + + // accept the invitation. we're game... + invite.accept(); + } + + // documentation inherited from interface + public void invitationCancelled (Invitation invite) + { + Log.info("Invitation cancelled " + invite + "."); + } + + // documentation inherited from interface + public void invitationAccepted (Invitation invite) + { + Log.info("Invitation accepted " + invite + "."); + } + + // documentation inherited from interface + public void invitationRefused (Invitation invite, String message) + { + Log.info("Invitation refused [invite=" + invite + + ", message=" + message + "]."); + } + + // documentation inherited from interface + public void invitationCountered (Invitation invite, GameConfig config) + { + Log.info("Invitation countered [invite=" + invite + + ", config=" + config + "]."); + } + + protected MiCasaContext _ctx; + protected LobbyConfig _config; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyDispatcher.java b/src/java/com/threerings/micasa/lobby/LobbyDispatcher.java new file mode 100644 index 00000000..058a356f --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyDispatcher.java @@ -0,0 +1,78 @@ +// +// $Id: LobbyDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import com.threerings.micasa.lobby.LobbyMarshaller; +import com.threerings.micasa.lobby.LobbyService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import java.util.List; + +/** + * Dispatches requests to the {@link LobbyProvider}. + */ +public class LobbyDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public LobbyDispatcher (LobbyProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new LobbyMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case LobbyMarshaller.GET_CATEGORIES: + ((LobbyProvider)provider).getCategories( + source, + (LobbyService.CategoriesListener)args[0] + ); + return; + + case LobbyMarshaller.GET_LOBBIES: + ((LobbyProvider)provider).getLobbies( + source, + (String)args[0], (LobbyService.LobbiesListener)args[1] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyManager.java b/src/java/com/threerings/micasa/lobby/LobbyManager.java new file mode 100644 index 00000000..ed4e32b6 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyManager.java @@ -0,0 +1,91 @@ +// +// $Id: LobbyManager.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import java.util.Properties; +import com.samskivert.util.StringUtil; + +import com.threerings.crowd.server.PlaceManager; +import com.threerings.micasa.Log; + +/** + * Takes care of the server side of a particular lobby. + */ +public class LobbyManager extends PlaceManager +{ + /** + * Initializes this lobby manager with its configuration properties. + * + * @exception Exception thrown if a configuration error is detected. + */ + public void init (LobbyRegistry lobreg, Properties config) + throws Exception + { + // look up some configuration parameters + _gameIdent = getConfigValue(config, "ugi"); + _name = getConfigValue(config, "name"); + + // keep this for later + _lobreg = lobreg; + + Log.info("Lobby manager initialized [ident=" + _gameIdent + + ", name=" + _name + "]."); + } + + /** Looks up a configuration property in the supplied properties + * object and throws an exception if it's not found. */ + protected String getConfigValue (Properties config, String key) + throws Exception + { + String value = config.getProperty(key); + if (StringUtil.isBlank(value)) { + throw new Exception("Missing '" + key + "' definition in " + + "lobby configuration."); + } + return value; + } + + // documentation inherited + protected Class getPlaceObjectClass () + { + return LobbyObject.class; + } + + // documentation inherited + protected void didStartup () + { + super.didStartup(); + + // let the lobby registry know that we're up and running + _lobreg.lobbyReady(_plobj.getOid(), _gameIdent, _name); + } + + /** The universal game identifier for the game matchmade by this + * lobby. */ + protected String _gameIdent; + + /** The human readable name of this lobby. */ + protected String _name; + + /** A reference to the lobby registry. */ + protected LobbyRegistry _lobreg; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyMarshaller.java b/src/java/com/threerings/micasa/lobby/LobbyMarshaller.java new file mode 100644 index 00000000..efbf229c --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyMarshaller.java @@ -0,0 +1,132 @@ +// +// $Id: LobbyMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import com.threerings.micasa.lobby.LobbyService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import java.util.List; + +/** + * Provides the implementation of the {@link LobbyService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class LobbyMarshaller extends InvocationMarshaller + implements LobbyService +{ + // documentation inherited + public static class CategoriesMarshaller extends ListenerMarshaller + implements CategoriesListener + { + /** The method id used to dispatch {@link #gotCategories} + * responses. */ + public static final int GOT_CATEGORIES = 1; + + // documentation inherited from interface + public void gotCategories (String[] arg1) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, GOT_CATEGORIES, + new Object[] { arg1 })); + } + + // documentation inherited + public void dispatchResponse (int methodId, Object[] args) + { + switch (methodId) { + case GOT_CATEGORIES: + ((CategoriesListener)listener).gotCategories( + (String[])args[0]); + return; + + default: + super.dispatchResponse(methodId, args); + return; + } + } + } + + // documentation inherited + public static class LobbiesMarshaller extends ListenerMarshaller + implements LobbiesListener + { + /** The method id used to dispatch {@link #gotLobbies} + * responses. */ + public static final int GOT_LOBBIES = 1; + + // documentation inherited from interface + public void gotLobbies (List arg1) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, GOT_LOBBIES, + new Object[] { arg1 })); + } + + // documentation inherited + public void dispatchResponse (int methodId, Object[] args) + { + switch (methodId) { + case GOT_LOBBIES: + ((LobbiesListener)listener).gotLobbies( + (List)args[0]); + return; + + default: + super.dispatchResponse(methodId, args); + return; + } + } + } + + /** The method id used to dispatch {@link #getCategories} requests. */ + public static final int GET_CATEGORIES = 1; + + // documentation inherited from interface + public void getCategories (Client arg1, LobbyService.CategoriesListener arg2) + { + LobbyMarshaller.CategoriesMarshaller listener2 = new LobbyMarshaller.CategoriesMarshaller(); + listener2.listener = arg2; + sendRequest(arg1, GET_CATEGORIES, new Object[] { + listener2 + }); + } + + /** The method id used to dispatch {@link #getLobbies} requests. */ + public static final int GET_LOBBIES = 2; + + // documentation inherited from interface + public void getLobbies (Client arg1, String arg2, LobbyService.LobbiesListener arg3) + { + LobbyMarshaller.LobbiesMarshaller listener3 = new LobbyMarshaller.LobbiesMarshaller(); + listener3.listener = arg3; + sendRequest(arg1, GET_LOBBIES, new Object[] { + arg2, listener3 + }); + } + +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyObject.java b/src/java/com/threerings/micasa/lobby/LobbyObject.java new file mode 100644 index 00000000..1389f3c0 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyObject.java @@ -0,0 +1,33 @@ +// +// $Id: LobbyObject.java 3218 2004-11-17 23:16:56Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import com.threerings.crowd.data.PlaceObject; + +/** + * Presently the lobby object contains nothing specific, but the class + * acts as a placeholder in case lobby-wide fields are needed in the + * future. + */ +public class LobbyObject extends PlaceObject +{ +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyPanel.java b/src/java/com/threerings/micasa/lobby/LobbyPanel.java new file mode 100644 index 00000000..ab15f0f4 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyPanel.java @@ -0,0 +1,114 @@ +// +// $Id: LobbyPanel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import javax.swing.*; +import com.samskivert.swing.*; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.micasa.client.*; +import com.threerings.micasa.util.MiCasaContext; + +/** + * Used to display the interface for the lobbies. It contains a lobby + * selection mechanism, a chat interface and a user interface for whatever + * match-making mechanism is appropriate for this particular lobby. + */ +public class LobbyPanel + extends JPanel implements PlaceView +{ + /** + * Constructs a new lobby panel and the associated user interface + * elements. + */ + public LobbyPanel (MiCasaContext ctx, LobbyConfig config) + { + // we want a five pixel border around everything + setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // create our primary layout which divides the display in two + // horizontally + GroupLayout gl = new HGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + gl.setJustification(GroupLayout.RIGHT); + setLayout(gl); + + // create our main panel + gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + _main = new JPanel(gl); + + // create our match-making view + _main.add(config.createMatchMakingView(ctx)); + + // create a chat box and stick that in as well + _main.add(new ChatPanel(ctx)); + + // now add the main panel into the mix + add(_main); + + // create our sidebar panel + gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + JPanel sidePanel = new JPanel(gl); + + // the sidebar contains a lobby info display... + + // ...a lobby selector... + JLabel label = new JLabel("Select a lobby..."); + sidePanel.add(label, GroupLayout.FIXED); + LobbySelector selector = new LobbySelector(ctx); + sidePanel.add(selector); + + // and an occupants list + label = new JLabel("People in lobby"); + sidePanel.add(label, GroupLayout.FIXED); + _occupants = new OccupantList(ctx); + sidePanel.add(_occupants); + + JButton logoff = new JButton("Logoff"); + logoff.addActionListener(Controller.DISPATCHER); + logoff.setActionCommand("logoff"); + sidePanel.add(logoff, GroupLayout.FIXED); + + // add our sidebar panel into the mix + add(sidePanel, GroupLayout.FIXED); + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + } + + /** Contains the match-making view and the chatbox. */ + protected JPanel _main; + + /** Our occupant list display. */ + protected OccupantList _occupants; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyProvider.java b/src/java/com/threerings/micasa/lobby/LobbyProvider.java new file mode 100644 index 00000000..8e268475 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyProvider.java @@ -0,0 +1,47 @@ +// +// $Id: LobbyProvider.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import com.threerings.micasa.lobby.LobbyService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; +import java.util.List; + +/** + * Defines the server-side of the {@link LobbyService}. + */ +public interface LobbyProvider extends InvocationProvider +{ + /** + * Handles a {@link LobbyService#getCategories} request. + */ + public void getCategories (ClientObject caller, LobbyService.CategoriesListener arg1) + throws InvocationException; + + /** + * Handles a {@link LobbyService#getLobbies} request. + */ + public void getLobbies (ClientObject caller, String arg1, LobbyService.LobbiesListener arg2) + throws InvocationException; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyRegistry.java b/src/java/com/threerings/micasa/lobby/LobbyRegistry.java new file mode 100644 index 00000000..2e2fa85e --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyRegistry.java @@ -0,0 +1,272 @@ +// +// $Id: LobbyRegistry.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import java.util.*; +import com.samskivert.util.*; +import com.threerings.util.StreamableArrayList; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationManager; + +import com.threerings.crowd.data.BodyObject; + +import com.threerings.micasa.Log; +import com.threerings.micasa.lobby.LobbyService.CategoriesListener; +import com.threerings.micasa.lobby.LobbyService.LobbiesListener; +import com.threerings.micasa.server.MiCasaConfig; +import com.threerings.micasa.server.MiCasaServer; + +/** + * The lobby registry is the primary class that coordinates the lobby + * services on the client. It sets up the necessary invocation services + * and keeps track of the lobbies in operation on the server. Only one + * lobby registry should be created on a server. + * + *

Presently, the lobby registry is configured with lobbies via the + * server configuration. An example configuration follows: + * + *

+ * lobby_ids = foolobby, barlobby, bazlobby
+ *
+ * foolobby.mgrclass = com.threerings.micasa.lobby.LobbyManager
+ * foolobby.ugi = 
+ * foolobby.name = 
+ * foolobby.config1 = some config value
+ * foolobby.config2 = some other config value
+ *
+ * barlobby.mgrclass = com.threerings.micasa.lobby.LobbyManager
+ * barlobby.ugi = 
+ * barlobby.name = 
+ * ...
+ * 
+ * + * This information will be loaded from the MiCasa server configuration + * which means that it should live in + * rsrc/config/micasa/server.properties somwhere in the + * classpath where it will override the default MiCasa server properties + * file. + * + *

The UGI or universal game identifier is a string that + * is used to uniquely identify every type of game and also to classify it + * according to meaningful keywords. It is best described with a few + * examples: + * + *

+ * backgammon,board,strategy
+ * spades,card,partner
+ * yahtzee,dice
+ * 
+ * + * As you can see, a UGI should start with an identifier uniquely + * identifying the type of game and can be followed by a list of keywords + * that classify it as a member of a particular category of games + * (eg. board, card, dice, partner game, strategy game). A game can belong + * to multiple categories. + * + *

As long as the UGIs in use by a particular server make some kind of + * sense, the client will be able to use them to search for lobbies + * containing games of similar types using the provided facilities. + */ +public class LobbyRegistry + implements LobbyProvider +{ + /** + * Initializes the registry. It will use the supplied configuration + * instance to determine which lobbies to load, etc. + * + * @param invmgr a reference to the server's invocation manager. + */ + public void init (InvocationManager invmgr) + { + // register ourselves as an invocation service handler + invmgr.registerDispatcher(new LobbyDispatcher(this), true); + + // create our lobby managers + String[] lmgrs = null; + lmgrs = MiCasaConfig.config.getValue(LOBIDS_KEY, lmgrs); + if (lmgrs == null || lmgrs.length == 0) { + Log.warning("No lobbies specified in config file (via '" + + LOBIDS_KEY + "' parameter)."); + + } else { + for (int i = 0; i < lmgrs.length; i++) { + loadLobby(lmgrs[i]); + } + } + } + + /** + * Returns the oid of the default lobby. + */ + public int getDefaultLobbyOid () + { + return _defLobbyOid; + } + + /** + * Extracts the properties for a lobby from the server config and + * creates and initializes the lobby manager. + */ + protected void loadLobby (String lobbyId) + { + try { + // extract the properties for this lobby + Properties props = MiCasaConfig.config.getSubProperties(lobbyId); + + // get the lobby manager class and UGI + String cfgClass = props.getProperty("config"); + if (StringUtil.isBlank(cfgClass)) { + throw new Exception("Missing 'config' definition in " + + "lobby configuration."); + } + + // create and initialize the lobby config object + LobbyConfig config = (LobbyConfig) + Class.forName(cfgClass).newInstance(); + config.init(props); + + // create and initialize the lobby manager. it will call + // lobbyReady() when it has obtained a reference to its lobby + // object and is ready to roll + LobbyManager lobmgr = (LobbyManager) + MiCasaServer.plreg.createPlace(config, null); + lobmgr.init(this, props); + + } catch (Exception e) { + Log.warning("Unable to create lobby manager " + + "[lobbyId=" + lobbyId + ", error=" + e + "]."); + } + } + + /** + * Returns information about all lobbies hosting games in the + * specified category. + * + * @param requester the body object of the client requesting the lobby + * list (which can be used to filter the list based on their + * capabilities). + * @param category the category of game for which the lobbies are + * desired. + * @param target the list into which the matching lobbies will be + * deposited. + */ + public void getLobbies (BodyObject requester, String category, + List target) + { + ArrayList list = (ArrayList)_lobbies.get(category); + if (list != null) { + target.addAll(list); + } + } + + /** + * Returns an array containing the category identifiers of all the + * categories in which lobbies have been registered with the registry. + * + * @param requester the body object of the client requesting the + * cateogory list (which can be used to filter the list based on their + * capabilities). + */ + public String[] getCategories (BodyObject requester) + { + // might want to cache this some day so that we don't recreate it + // every time someone wants it. we can cache the array until the + // category count changes + String[] cats = new String[_lobbies.size()]; + Iterator iter = _lobbies.keySet().iterator(); + for (int i = 0; iter.hasNext(); i++) { + cats[i] = (String)iter.next(); + } + return cats; + } + + /** + * Processes a request by the client to obtain a list of the lobby + * categories available on this server. + */ + public void getCategories (ClientObject caller, CategoriesListener listener) + { + listener.gotCategories(getCategories((BodyObject)caller)); + } + + /** + * Processes a request by the client to obtain a list of lobbies + * matching the supplied category string. + */ + public void getLobbies (ClientObject caller, String category, + LobbiesListener listener) + { + StreamableArrayList target = new StreamableArrayList(); + ArrayList list = (ArrayList)_lobbies.get(category); + if (list != null) { + target.addAll(list); + } + listener.gotLobbies(target); + } + + /** + * Called by our lobby managers once they have started up and are + * ready to do their lobby duties. + */ + protected void lobbyReady (int placeOid, String gameIdent, String name) + { + // create a lobby record + Lobby record = new Lobby(placeOid, gameIdent, name); + + // if we don't already have a default lobby, this one is the big + // winner + if (_defLobbyOid == -1) { + _defLobbyOid = placeOid; + } + + // and register it in all the right lobby tables + StringTokenizer tok = new StringTokenizer(gameIdent, ","); + while (tok.hasMoreTokens()) { + String category = tok.nextToken(); + registerLobby(category, record); + } + } + + /** Registers the supplied lobby in the specified category table. */ + protected void registerLobby (String category, Lobby record) + { + ArrayList catlist = (ArrayList)_lobbies.get(category); + if (catlist == null) { + catlist = new ArrayList(); + _lobbies.put(category, catlist); + } + catlist.add(record); + Log.info("Registered lobby [cat=" + category + + ", record=" + record + "]."); + } + + /** A table containing references to all of our lobby records (in the + * form of category lists. */ + protected HashMap _lobbies = new HashMap(); + + /** The oid of the default lobby. */ + protected int _defLobbyOid = -1; + + /** The configuration key for the lobby managers list. */ + protected static final String LOBIDS_KEY = "lobby_ids"; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbySelector.java b/src/java/com/threerings/micasa/lobby/LobbySelector.java new file mode 100644 index 00000000..94aa9823 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbySelector.java @@ -0,0 +1,219 @@ +// +// $Id: LobbySelector.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.*; + +import java.util.*; + +import javax.swing.*; + +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.micasa.Log; +import com.threerings.micasa.util.MiCasaContext; + +/** + * The lobby selector displays a drop-down box listing the categories of + * lobbies available on this server and when a category is selected, it + * displays a list of the lobbies that are available in that category. If + * a lobby is double-clicked, it moves the client into that lobby room. + */ +public class LobbySelector extends JPanel + implements ActionListener, LobbyService.CategoriesListener, + LobbyService.LobbiesListener +{ + /** + * Constructs a new lobby selector component. It will wait until it is + * visible before issuing a get categories request to download a list + * of categories. + */ + public LobbySelector (MiCasaContext ctx) + { + setLayout(new BorderLayout()); + + // keep this around for later + _ctx = ctx; + + // create our drop-down box with a bogus entry at the moment + String[] options = new String[] { CAT_FIRST_ITEM }; + _combo = new JComboBox(options); + _combo.addActionListener(this); + add(_combo, BorderLayout.NORTH); + + // and create our empty lobby list + _loblist = new JList(); + _loblist.setCellRenderer(new LobbyCellRenderer()); + // add a mouse listener that tells us about double clicks + MouseListener ml = new MouseAdapter() { + public void mouseClicked (MouseEvent e) { + if (e.getClickCount() == 2) { + int index = _loblist.locationToIndex(e.getPoint()); + Object item = _loblist.getSelectedValue(); + enterLobby((Lobby)item); + } + } + }; + _loblist.addMouseListener(ml); + add(_loblist, BorderLayout.CENTER); + } + + // documentation inherited + public void addNotify () + { + super.addNotify(); + + // get a handle on our lobby service instance + _lservice = (LobbyService) + _ctx.getClient().requireService(LobbyService.class); + // and use them to look up the lobby categories + _lservice.getCategories(_ctx.getClient(), this); + } + + /** + * Called when the user selects a category or double-clicks on a + * lobby. + */ + public void actionPerformed (ActionEvent evt) + { + if (evt.getSource() == _combo) { + String selcat = (String)_combo.getSelectedItem(); + if (!selcat.equals(CAT_FIRST_ITEM)) { + selectCategory(selcat); + } + } + } + + // documentation inherited from interface + public void gotCategories (String[] categories) + { + // append these to our "unselected" item + for (int i = 0; i < categories.length; i++) { + _combo.addItem(categories[i]); + } + } + + // documentation inherited from interface + public void gotLobbies (List lobbies) + { + // create a list model for this category + DefaultListModel model = new DefaultListModel(); + + // populate it with the lobby info + Iterator iter = lobbies.iterator(); + while (iter.hasNext()) { + model.addElement(iter.next()); + } + + // stick it in the table + _catlists.put(_pendingCategory, model); + + // finally tell the lobby list to update the display (which we do + // by setting the combo box to this category in case the luser + // decided to try to select some new category while we weren't + // looking) + _combo.setSelectedItem(_pendingCategory); + + // clear out our pending category indicator + _pendingCategory = null; + } + + // documentation inherited from interface + public void requestFailed (String reason) + { + Log.info("Request failed [reason=" + reason + "]."); + + // clear out our pending category indicator in case this was a + // failed getLobbies() request + _pendingCategory = null; + } + + /** + * Fetches the list of lobbies available in a particular category (if + * they haven't already been fetched) and displays them in the lobby + * list. + */ + protected void selectCategory (String category) + { + DefaultListModel model = (DefaultListModel)_catlists.get(category); + if (model != null) { + _loblist.setModel(model); + + } else if (_pendingCategory == null) { + // make a note that we're loading up this category + _pendingCategory = category; + // issue a request to load up the lobbies in this category + _lservice.getLobbies(_ctx.getClient(), category, this); + + } else { + Log.info("Ignoring category select request because " + + "one is outstanding [pcat=" + _pendingCategory + + ", newcat=" + category + "]."); + } + } + + /** Called when the user selects a lobby from the lobby list. */ + protected void enterLobby (Lobby lobby) + { + // make sure we're not already in this lobby + PlaceObject plobj = _ctx.getLocationDirector().getPlaceObject(); + if (plobj != null && plobj.getOid() == lobby.placeOid) { + return; + } + + // otherwise request that we go there + _ctx.getLocationDirector().moveTo(lobby.placeOid); + + Log.info("Entering lobby " + lobby + "."); + + } + + /** Used to render Lobby instances in our lobby list. */ + protected static class LobbyCellRenderer + extends DefaultListCellRenderer + { + public Component getListCellRendererComponent( + JList list, + Object value, + int index, + boolean isSelected, + boolean cellHasFocus) { + // use the lobby's name rather than the value of toString() + value = ((Lobby)value).name; + return super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + } + } + + protected MiCasaContext _ctx; + protected LobbyService _lservice; + + protected JComboBox _combo; + protected JList _loblist; + + protected HashMap _catlists = new HashMap(); + protected String _pendingCategory; + + protected static final String CAT_FIRST_ITEM = ""; +} diff --git a/src/java/com/threerings/micasa/lobby/LobbyService.java b/src/java/com/threerings/micasa/lobby/LobbyService.java new file mode 100644 index 00000000..96bb239b --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/LobbyService.java @@ -0,0 +1,83 @@ +// +// $Id: LobbyService.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby; + +import java.util.List; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +/** + * Provides an interface to the various parlor services that are directly + * invokable by the client (by means of the invocation services). + */ +public interface LobbyService extends InvocationService +{ + /** + * Used to communicate the results of a {@link #getCategories} + * request. + */ + public static interface CategoriesListener extends InvocationListener + { + /** + * Supplies the listener with the results of a {@link + * #getCategories} request. + */ + public void gotCategories (String[] categories); + } + + /** + * Used to communicate the results of a {@link #getLobbies} + * request. + */ + public static interface LobbiesListener extends InvocationListener + { + /** + * Supplies the listener with the results of a {@link + * #getLobbies} request. + */ + public void gotLobbies (List lobbies); + } + + /** + * Requests the list of lobby cateogories that are available on this + * server. + * + * @param client a connected, operational client instance. + * @param listener the listener that will receive and process the + * response. + */ + public void getCategories (Client client, CategoriesListener listener); + + /** + * Requests information on all active lobbies that match the specified + * category. + * + * @param client a connected, operational client instance. + * @param category the category of game for which a list of lobbies is + * desired. + * @param listener the listener that will receive and process the + * response. + */ + public void getLobbies (Client client, String category, + LobbiesListener listener); +} diff --git a/src/java/com/threerings/micasa/lobby/table/TableItem.java b/src/java/com/threerings/micasa/lobby/table/TableItem.java new file mode 100644 index 00000000..02580a53 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/table/TableItem.java @@ -0,0 +1,252 @@ +// +// $Id: TableItem.java 3758 2005-11-10 23:18:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby.table; + +import java.awt.Color; +import java.awt.GridBagLayout; +import java.awt.GridBagConstraints; +import java.awt.Insets; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.threerings.util.Name; + +import com.threerings.crowd.data.BodyObject; + +import com.threerings.parlor.client.TableDirector; +import com.threerings.parlor.client.SeatednessObserver; +import com.threerings.parlor.data.Table; + +import com.threerings.micasa.Log; +import com.threerings.micasa.util.MiCasaContext; + +/** + * A table item displays the user interface for a single table (whether it + * be in-play or still being matchmade). + */ +public class TableItem + extends JPanel + implements ActionListener, SeatednessObserver +{ + /** A reference to the table we are displaying. */ + public Table table; + + /** + * Creates a new table item to display and interact with the supplied + * table. + */ + public TableItem (MiCasaContext ctx, TableDirector tdtr, Table table) + { + // keep track of these + _tdtr = tdtr; + _ctx = ctx; + + // add ourselves as a seatedness observer + _tdtr.addSeatednessObserver(this); + + // figure out who we are + _self = ((BodyObject)ctx.getClient().getClientObject()).getVisibleName(); + + // now create our user interface + setBorder(BorderFactory.createLineBorder(Color.black)); + setLayout(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + + // create a label for the table + JLabel tlabel = new JLabel("Table " + table.tableId); + gbc.gridwidth = GridBagConstraints.REMAINDER; + gbc.insets = new Insets(2, 0, 0, 0); + add(tlabel, gbc); + + // we have one button for every "seat" at the table + int bcount = table.occupants.length; + + // create blank buttons for now and then we'll update everything + // with the current state when we're done + gbc.weightx = 1.0; + gbc.insets = new Insets(2, 0, 2, 0); + _seats = new JButton[bcount]; + for (int i = 0; i < bcount; i++) { + // create the button + _seats[i] = new JButton(JOIN_LABEL); + _seats[i].addActionListener(this); + + // if we're on the left + if (i % 2 == 0) { + // if we're the last seat, then we've got an odd number + // and need to center this final seat + if (i == bcount-1) { + gbc.gridwidth = GridBagConstraints.REMAINDER; + } else { + gbc.gridwidth = 1; + } + + } else { + gbc.gridwidth = GridBagConstraints.REMAINDER; + } + + // adjust the insets of our last element + if (i == bcount-1) { + gbc.insets = new Insets(2, 0, 4, 0); + } + + // and add the button with the configured constraints + add(_seats[i], gbc); + + // if we just added the first button, add the "go" button + // right after it + if (i == 0) { + _goButton = new JButton("Go"); + _goButton.setActionCommand("go"); + _goButton.addActionListener(this); + add(_goButton, gbc); + } + } + + // and update ourselves based on the contents of the occupants + // list + tableUpdated(table); + } + + /** + * Called when our table has been updated and we need to update the UI + * to reflect the new information. + */ + public void tableUpdated (Table table) + { + // grab this new table reference + this.table = table; + + // first look to see if we're already sitting at a table + boolean isSeated = _tdtr.isSeated(); + + // now enable and label the buttons accordingly + int slength = _seats.length; + for (int i = 0; i < slength; i++) { + if (table.occupants[i] == null) { + _seats[i].setText(JOIN_LABEL); + _seats[i].setEnabled(!isSeated); + _seats[i].setActionCommand("join"); + + } else if (table.occupants[i].equals(_self) && + !table.inPlay()) { + _seats[i].setText(LEAVE_LABEL); + _seats[i].setEnabled(true); + _seats[i].setActionCommand("leave"); + + } else { + _seats[i].setText(table.occupants[i].toString()); + _seats[i].setEnabled(false); + } + } + + // show or hide our "go" button appropriately + _goButton.setVisible(table.gameOid != -1); + } + + /** + * Called by the table list view prior to removing us. Here we clean + * up. + */ + public void tableRemoved () + { + // no more observy + _tdtr.removeSeatednessObserver(this); + } + + // documentation inherited + public void actionPerformed (ActionEvent event) + { + String cmd = event.getActionCommand(); + if (cmd.equals("join")) { + // figure out what position this button is in + int position = -1; + for (int i = 0; i < _seats.length; i++) { + if (_seats[i] == event.getSource()) { + position = i; + break; + } + } + + // sanity check + if (position == -1) { + Log.warning("Unable to figure out what position a " + + "click came from [event=" + event + "]."); + } else { + // otherwise, request to join the table at this position + _tdtr.joinTable(table.getTableId(), position); + } + + } else if (cmd.equals("leave")) { + // if we're not joining, we're leaving + _tdtr.leaveTable(table.getTableId()); + + } else if (cmd.equals("go")) { + // they want to see the game... so go there + _ctx.getLocationDirector().moveTo(table.gameOid); + + } else { + Log.warning("Received unknown action [event=" + event + "]."); + } + } + + // documentation inherited + public void seatednessDidChange (boolean isSeated) + { + // just update ourselves + tableUpdated(table); + + // enable or disable the go button based on our seatedness + if (_goButton.isVisible()) { + _goButton.setEnabled(!isSeated); + } + } + + /** A reference to our context. */ + protected MiCasaContext _ctx; + + /** Our username. */ + protected Name _self; + + /** A reference to our table director. */ + protected TableDirector _tdtr; + + /** We have a button for each "seat" at the table. */ + protected JButton[] _seats; + + /** We have a button for going to games that are already in + * progress. */ + protected JButton _goButton; + + /** The text shown for seats at which the user can join. */ + protected static final String JOIN_LABEL = ""; + + /** The text shown for the seat in which this user occupies and which + * lets her/him know that they can leave that seat by clicking. */ + protected static final String LEAVE_LABEL = ""; +} diff --git a/src/java/com/threerings/micasa/lobby/table/TableListView.java b/src/java/com/threerings/micasa/lobby/table/TableListView.java new file mode 100644 index 00000000..fbd2961a --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/table/TableListView.java @@ -0,0 +1,305 @@ +// +// $Id: TableListView.java 3593 2005-06-10 18:10:49Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby.table; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Iterator; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JScrollPane; + +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.micasa.Log; +import com.threerings.micasa.lobby.LobbyConfig; +import com.threerings.micasa.util.MiCasaContext; + +import com.threerings.parlor.client.SeatednessObserver; +import com.threerings.parlor.client.TableConfigurator; +import com.threerings.parlor.client.TableDirector; +import com.threerings.parlor.client.TableObserver; +import com.threerings.parlor.data.Table; +import com.threerings.parlor.game.client.GameConfigurator; +import com.threerings.parlor.game.client.SwingGameConfigurator; +import com.threerings.parlor.game.data.GameConfig; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; + +/** + * A view that displays the tables in a table lobby. It displays two + * separate lists, one of tables being matchmade and another of games in + * progress. These tables are updated dynamically as they proceed through + * the matchmaking process. UI mechanisms for creating and joining tables + * are also provided. + */ +public class TableListView extends JPanel + implements PlaceView, TableObserver, ActionListener, SeatednessObserver +{ + /** + * Creates a new table list view, suitable for providing the user + * interface for table-style matchmaking in a table lobby. + */ + public TableListView (MiCasaContext ctx, LobbyConfig config) + { + // keep track of these + _config = config; + _ctx = ctx; + + // create our table director + _tdtr = new TableDirector(ctx, TableLobbyObject.TABLE_SET, this); + + // add ourselves as a seatedness observer + _tdtr.addSeatednessObserver(this); + + // set up a layout manager + HGroupLayout gl = new HGroupLayout(HGroupLayout.STRETCH); + gl.setOffAxisPolicy(HGroupLayout.STRETCH); + setLayout(gl); + + // we have two lists of tables, one of tables being matchmade... + VGroupLayout pgl = new VGroupLayout(VGroupLayout.STRETCH); + pgl.setOffAxisPolicy(VGroupLayout.STRETCH); + JPanel panel = new JPanel(pgl); + panel.add(new JLabel("Pending tables"), VGroupLayout.FIXED); + + VGroupLayout mgl = new VGroupLayout(VGroupLayout.NONE); + mgl.setOffAxisPolicy(VGroupLayout.STRETCH); + mgl.setJustification(VGroupLayout.TOP); + _matchList = new JPanel(mgl); + _matchList.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + panel.add(new JScrollPane(_matchList)); + + // create and initialize the configurator interface + GameConfig gconfig = null; + try { + gconfig = config.getGameConfig(); + + _tableFigger = gconfig.createTableConfigurator(); + if (_tableFigger == null) { + Log.warning("Game config has not been set up to work with " + + "tables: it needs to return non-null from " + + "createTableConfigurator()."); + // let's just wait until we throw an NPE below + } + + _figger = gconfig.createConfigurator(); + _tableFigger.init(_ctx, _figger); + if (_figger != null) { + _figger.init(_ctx); + _figger.setGameConfig(gconfig); + panel.add(((SwingGameConfigurator) _figger).getPanel(), + VGroupLayout.FIXED); + } + + _create = new JButton("Create table"); + _create.addActionListener(this); + panel.add(_create, VGroupLayout.FIXED); + + } catch (Exception e) { + Log.warning("Unable to create configurator interface " + + "[config=" + gconfig + "]."); + Log.logStackTrace(e); + + // stick something in the UI to let them know we're hosed + panel.add(new JLabel("Aiya! Can't create tables. " + + "Configuration borked."), VGroupLayout.FIXED); + } + + add(panel); + + // ...and one of games in progress + panel = new JPanel(pgl); + panel.add(new JLabel("Games in progress"), VGroupLayout.FIXED); + + _playList = new JPanel(mgl); + _playList.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + panel.add(new JScrollPane(_playList)); + + add(panel); + } + + // documentation inherited + public void willEnterPlace (PlaceObject place) + { + // pass the good word on to our table director + _tdtr.willEnterPlace(place); + + // iterate over the tables already active in this lobby and put + // them in their respective lists + TableLobbyObject tlobj = (TableLobbyObject)place; + Iterator iter = tlobj.tableSet.iterator(); + while (iter.hasNext()) { + tableAdded((Table)iter.next()); + } + } + + // documentation inherited + public void didLeavePlace (PlaceObject place) + { + // pass the good word on to our table director + _tdtr.didLeavePlace(place); + + // clear out our table lists + _matchList.removeAll(); + _playList.removeAll(); + } + + // documentation inherited + public void tableAdded (Table table) + { + Log.info("Table added [table=" + table + "]."); + + // create a table item for this table and insert it into the + // appropriate list + JPanel panel = table.inPlay() ? _playList : _matchList; + panel.add(new TableItem(_ctx, _tdtr, table)); + SwingUtil.refresh(panel); + } + + // documentation inherited + public void tableUpdated (Table table) + { + Log.info("Table updated [table=" + table + "]."); + + // locate the table item associated with this table + TableItem item = getTableItem(table.getTableId()); + if (item == null) { + Log.warning("Received table updated notification for " + + "unknown table [table=" + table + "]."); + return; + } + + // let the item perform any updates it finds necessary + item.tableUpdated(table); + + // and we may need to move the item from the match to the in-play + // list if it just transitioned + if (table.gameOid != -1 && item.getParent() == _matchList) { + _matchList.remove(item); + SwingUtil.refresh(_matchList); + _playList.add(item); + SwingUtil.refresh(_playList); + } + } + + // documentation inherited + public void tableRemoved (int tableId) + { + Log.info("Table removed [tableId=" + tableId + "]."); + + // locate the table item associated with this table + TableItem item = getTableItem(tableId); + if (item == null) { + Log.warning("Received table removed notification for " + + "unknown table [tableId=" + tableId + "]."); + return; + } + + // remove this item from the user interface + JPanel panel = (JPanel)item.getParent(); + panel.remove(item); + SwingUtil.refresh(panel); + + // let the little fellow know that we gave him the boot + item.tableRemoved(); + } + + // documentation inherited + public void actionPerformed (ActionEvent event) + { + // the create table button was clicked. use the game config as + // configured by the configurator to create a table + _tdtr.createTable(_tableFigger.getTableConfig(), + _figger.getGameConfig()); + } + + // documentation inherited + public void seatednessDidChange (boolean isSeated) + { + // update the create table button + _create.setEnabled(!isSeated); + } + + /** + * Fetches the table item component associated with the specified + * table id. + */ + protected TableItem getTableItem (int tableId) + { + // first check the match list + int ccount = _matchList.getComponentCount(); + for (int i = 0; i < ccount; i++) { + TableItem child = (TableItem)_matchList.getComponent(i); + if (child.table.getTableId() == tableId) { + return child; + } + } + + // then the inplay list + ccount = _playList.getComponentCount(); + for (int i = 0; i < ccount; i++) { + TableItem child = (TableItem)_playList.getComponent(i); + if (child.table.getTableId() == tableId) { + return child; + } + } + + // sorry charlie + return null; + } + + /** A reference to the client context. */ + protected MiCasaContext _ctx; + + /** A reference to the lobby config for the lobby in which we are + * doing table-style matchmaking. */ + protected LobbyConfig _config; + + /** A reference to our table director. */ + protected TableDirector _tdtr; + + /** The list of tables currently being matchmade. */ + protected JPanel _matchList; + + /** The list of tables that are in play. */ + protected JPanel _playList; + + /** The interface used to configure the table for a game. */ + protected TableConfigurator _tableFigger; + + /** The interface used to configure a game before creating it. */ + protected GameConfigurator _figger; + + /** Our create table button. */ + protected JButton _create; + + /** Our number of players indicator. */ + protected JLabel _pcount; +} diff --git a/src/java/com/threerings/micasa/lobby/table/TableLobbyConfig.java b/src/java/com/threerings/micasa/lobby/table/TableLobbyConfig.java new file mode 100644 index 00000000..d13915c4 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/table/TableLobbyConfig.java @@ -0,0 +1,45 @@ +// +// $Id: TableLobbyConfig.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby.table; + +import javax.swing.JComponent; +import com.threerings.micasa.lobby.LobbyConfig; +import com.threerings.micasa.util.MiCasaContext; + +/** + * Instructs the lobby services to use a {@link TableListView} as the + * matchmaking component. + */ +public class TableLobbyConfig extends LobbyConfig +{ + // documentation inherited + public String getManagerClassName () + { + return "com.threerings.micasa.lobby.table.TableLobbyManager"; + } + + // documentation inherited + public JComponent createMatchMakingView (MiCasaContext ctx) + { + return new TableListView(ctx, this); + } +} diff --git a/src/java/com/threerings/micasa/lobby/table/TableLobbyManager.java b/src/java/com/threerings/micasa/lobby/table/TableLobbyManager.java new file mode 100644 index 00000000..517483cf --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/table/TableLobbyManager.java @@ -0,0 +1,59 @@ +// +// $Id: TableLobbyManager.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby.table; + +import com.threerings.parlor.server.TableManager; +import com.threerings.parlor.server.TableManagerProvider; +import com.threerings.micasa.lobby.LobbyManager; + +/** + * Extends lobby manager only to ensure that a table lobby object is used + * for table lobbies. + */ +public class TableLobbyManager + extends LobbyManager implements TableManagerProvider +{ + // documentation inherited + protected void didStartup () + { + super.didStartup(); + + // now that we have our place object, we can create our table + // manager + _tmgr = new TableManager(this); + } + + // documentation inherited + protected Class getPlaceObjectClass () + { + return TableLobbyObject.class; + } + + // documentation inherited + public TableManager getTableManager () + { + return _tmgr; + } + + /** A reference to our table manager. */ + protected TableManager _tmgr; +} diff --git a/src/java/com/threerings/micasa/lobby/table/TableLobbyObject.java b/src/java/com/threerings/micasa/lobby/table/TableLobbyObject.java new file mode 100644 index 00000000..0858d290 --- /dev/null +++ b/src/java/com/threerings/micasa/lobby/table/TableLobbyObject.java @@ -0,0 +1,111 @@ +// +// $Id: TableLobbyObject.java 4166 2006-05-31 04:16:57Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.lobby.table; + +import com.threerings.presents.dobj.DSet; +import com.threerings.parlor.data.Table; +import com.threerings.micasa.lobby.LobbyObject; + +public class TableLobbyObject + extends LobbyObject + implements com.threerings.parlor.data.TableLobbyObject +{ + // AUTO-GENERATED: FIELDS START + /** The field name of the tableSet field. */ + public static final String TABLE_SET = "tableSet"; + // AUTO-GENERATED: FIELDS END + + /** A set containing all of the tables being managed by this lobby. */ + public DSet tableSet = new DSet(); + + // documentation inherited + public DSet getTables () + { + return tableSet; + } + + // documentation inherited from interface + public void addToTables (Table table) + { + addToTableSet(table); + } + + // documentation inherited from interface + public void updateTables (Table table) + { + updateTableSet(table); + } + + // documentation inherited from interface + public void removeFromTables (Comparable key) + { + removeFromTableSet(key); + } + + // AUTO-GENERATED: METHODS START + /** + * Requests that the specified entry be added to the + * tableSet set. The set will not change until the event is + * actually propagated through the system. + */ + public void addToTableSet (DSet.Entry elem) + { + requestEntryAdd(TABLE_SET, tableSet, elem); + } + + /** + * Requests that the entry matching the supplied key be removed from + * the tableSet set. The set will not change until the + * event is actually propagated through the system. + */ + public void removeFromTableSet (Comparable key) + { + requestEntryRemove(TABLE_SET, tableSet, key); + } + + /** + * Requests that the specified entry be updated in the + * tableSet set. The set will not change until the event is + * actually propagated through the system. + */ + public void updateTableSet (DSet.Entry elem) + { + requestEntryUpdate(TABLE_SET, tableSet, elem); + } + + /** + * Requests that the tableSet field be set to the + * specified value. Generally one only adds, updates and removes + * entries of a distributed set, but certain situations call for a + * complete replacement of the set value. The local value will be + * updated immediately and an event will be propagated through the + * system to notify all listeners that the attribute did + * change. Proxied copies of this object (on clients) will apply the + * value change when they received the attribute changed notification. + */ + public void setTableSet (DSet value) + { + requestAttributeChange(TABLE_SET, value, this.tableSet); + this.tableSet = (value == null) ? null : value.typedClone(); + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/micasa/server/MiCasaClient.java b/src/java/com/threerings/micasa/server/MiCasaClient.java new file mode 100644 index 00000000..b6e7af59 --- /dev/null +++ b/src/java/com/threerings/micasa/server/MiCasaClient.java @@ -0,0 +1,50 @@ +// +// $Id: MiCasaClient.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.server; + +import com.threerings.presents.net.BootstrapData; +import com.threerings.crowd.server.CrowdClient; + +import com.threerings.micasa.data.MiCasaBootstrapData; + +/** + * Extends the Crowd client and provides bootstrap data specific to the + * MiCasa services. + */ +public class MiCasaClient extends CrowdClient +{ + // documentation inherited + protected BootstrapData createBootstrapData () + { + return new MiCasaBootstrapData(); + } + + // documentation inherited + protected void populateBootstrapData (BootstrapData data) + { + super.populateBootstrapData(data); + + // let the client know their default lobby oid + ((MiCasaBootstrapData)data).defLobbyOid = + MiCasaServer.lobreg.getDefaultLobbyOid(); + } +} diff --git a/src/java/com/threerings/micasa/server/MiCasaConfig.java b/src/java/com/threerings/micasa/server/MiCasaConfig.java new file mode 100644 index 00000000..9ff1cfe2 --- /dev/null +++ b/src/java/com/threerings/micasa/server/MiCasaConfig.java @@ -0,0 +1,33 @@ +// +// $Id: MiCasaConfig.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.server; + +import com.samskivert.util.Config; + +/** + * Provides access to the MiCasa server configuration. + */ +public class MiCasaConfig +{ + /** Provides access to configuration data for this package. */ + public static Config config = new Config("rsrc/config/micasa/server"); +} diff --git a/src/java/com/threerings/micasa/server/MiCasaServer.java b/src/java/com/threerings/micasa/server/MiCasaServer.java new file mode 100644 index 00000000..281cb188 --- /dev/null +++ b/src/java/com/threerings/micasa/server/MiCasaServer.java @@ -0,0 +1,74 @@ +// +// $Id: MiCasaServer.java 4158 2006-05-30 22:12:15Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.server; + +import com.threerings.crowd.server.CrowdServer; +import com.threerings.parlor.server.ParlorManager; + +import com.threerings.micasa.Log; +import com.threerings.micasa.lobby.LobbyRegistry; + +/** + * This class is the main entry point and general organizer of everything + * that goes on in the MiCasa game server process. + */ +public class MiCasaServer extends CrowdServer +{ + /** The parlor manager in operation on this server. */ + public static ParlorManager parmgr = new ParlorManager(); + + /** The lobby registry operating on this server. */ + public static LobbyRegistry lobreg = new LobbyRegistry(); + + /** + * Initializes all of the server services and prepares for operation. + */ + public void init () + throws Exception + { + // do the base server initialization + super.init(); + + // configure the client manager to use our client class + clmgr.setClientClass(MiCasaClient.class); + + // initialize our parlor manager + parmgr.init(invmgr, plreg); + + // initialize the lobby registry + lobreg.init(invmgr); + + Log.info("MiCasa server initialized."); + } + + public static void main (String[] args) + { + MiCasaServer server = new MiCasaServer(); + try { + server.init(); + server.run(); + } catch (Exception e) { + Log.warning("Unable to initialize server."); + Log.logStackTrace(e); + } + } +} diff --git a/src/java/com/threerings/micasa/simulator/client/SimpleClient.java b/src/java/com/threerings/micasa/simulator/client/SimpleClient.java new file mode 100644 index 00000000..f34858bc --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimpleClient.java @@ -0,0 +1,197 @@ +// +// $Id: SimpleClient.java 3283 2004-12-22 19:23:00Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import java.awt.EventQueue; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import java.io.IOException; + +import javax.swing.JPanel; + +import com.samskivert.util.Config; +import com.samskivert.util.RunQueue; +import com.threerings.util.MessageManager; + +import com.threerings.presents.client.Client; +import com.threerings.presents.dobj.DObjectManager; + +import com.threerings.crowd.client.LocationDirector; +import com.threerings.crowd.client.OccupantDirector; +import com.threerings.crowd.client.PlaceView; + +import com.threerings.crowd.chat.client.ChatDirector; + +import com.threerings.parlor.client.ParlorDirector; +import com.threerings.parlor.util.ParlorContext; + +import com.threerings.micasa.client.MiCasaFrame; +import com.threerings.micasa.util.MiCasaContext; + +public class SimpleClient + implements RunQueue, SimulatorClient +{ + public SimpleClient (SimulatorFrame frame) + throws IOException + { + // create our context + _ctx = createContext(); + + // create the handles on our various services + _client = new Client(null, this); + + // create our managers and directors + _msgmgr = new MessageManager(getMessageManagerPrefix()); + _locdir = new LocationDirector(_ctx); + _occdir = new OccupantDirector(_ctx); + _pardtr = new ParlorDirector(_ctx); + _chatdir = new ChatDirector(_ctx, _msgmgr, null); + + // keep this for later + _frame = frame; + + // log off when they close the window + _frame.getFrame().addWindowListener(new WindowAdapter() { + public void windowClosing (WindowEvent evt) { + // if we're logged on, log off + if (_client.isLoggedOn()) { + _client.logoff(true); + } + } + }); + } + + /** + * Creates our context reference. + */ + protected MiCasaContext createContext () + { + return new MiCasaContextImpl(); + } + + /** + * Returns the prefix used by the message manager when looking for + * translation properties files. + */ + protected String getMessageManagerPrefix () + { + return "rsrc"; + } + + /** + * Returns a reference to the context in effect for this client. This + * reference is valid for the lifetime of the application. + */ + public ParlorContext getParlorContext () + { + return _ctx; + } + + // documentation inherited from interface RunQueue + public void postRunnable (Runnable run) + { + // queue it on up on the awt thread + EventQueue.invokeLater(run); + } + + // documentation inherited from interface RunQueue + public boolean isDispatchThread () + { + return EventQueue.isDispatchThread(); + } + + /** + * The context implementation. This provides access to all of the + * objects and services that are needed by the operating client. + */ + protected class MiCasaContextImpl implements MiCasaContext + { + public Config getConfig () + { + return _config; + } + + public Client getClient () + { + return _client; + } + + public DObjectManager getDObjectManager () + { + return _client.getDObjectManager(); + } + + public LocationDirector getLocationDirector () + { + return _locdir; + } + + public OccupantDirector getOccupantDirector () + { + return _occdir; + } + + public ParlorDirector getParlorDirector () + { + return _pardtr; + } + + public ChatDirector getChatDirector () + { + return _chatdir; + } + + public void setPlaceView (PlaceView view) + { + // stick the place view into our frame + _frame.setPanel((JPanel)view); + } + + public void clearPlaceView (PlaceView view) + { + // we'll just let the next view replace the old one + } + + public MiCasaFrame getFrame () + { + return (MiCasaFrame)_frame; + } + + public MessageManager getMessageManager () + { + return _msgmgr; + } + } + + protected MiCasaContext _ctx; + protected SimulatorFrame _frame; + protected MessageManager _msgmgr; + + protected Config _config = new Config("micasa"); + protected Client _client; + protected LocationDirector _locdir; + protected OccupantDirector _occdir; + protected ParlorDirector _pardtr; + protected ChatDirector _chatdir; +} + diff --git a/src/java/com/threerings/micasa/simulator/client/SimpleFrame.java b/src/java/com/threerings/micasa/simulator/client/SimpleFrame.java new file mode 100644 index 00000000..dbb533f4 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimpleFrame.java @@ -0,0 +1,51 @@ +// +// $Id: SimpleFrame.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import javax.swing.JFrame; + +import com.samskivert.swing.Controller; + +import com.threerings.micasa.client.MiCasaFrame; + +/** + * Contains the user interface for the Simulator client application. + */ +public class SimpleFrame extends MiCasaFrame + implements SimulatorFrame +{ + /** + * Constructs the top-level Simulator client frame. + */ + public SimpleFrame () + { + super("Simulator"); + } + + // documentation inherited + public JFrame getFrame () + { + return this; + } + + protected Controller _controller; +} diff --git a/src/java/com/threerings/micasa/simulator/client/SimulatorApp.java b/src/java/com/threerings/micasa/simulator/client/SimulatorApp.java new file mode 100644 index 00000000..416cd948 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimulatorApp.java @@ -0,0 +1,227 @@ +// +// $Id: SimulatorApp.java 4158 2006-05-30 22:12:15Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import javax.swing.JFrame; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.Interval; +import com.samskivert.util.ResultListener; + +import com.threerings.util.Name; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.ClientAdapter; +import com.threerings.presents.net.UsernamePasswordCreds; + +import com.threerings.micasa.Log; +import com.threerings.micasa.simulator.data.SimulatorInfo; +import com.threerings.micasa.simulator.server.SimpleServer; +import com.threerings.micasa.simulator.server.SimulatorServer; + +/** + * The simulator application is a test harness to facilitate development + * and debugging of games. + */ +public class SimulatorApp +{ + public void start (final String[] args) throws Exception + { + // create a frame + _frame = createSimulatorFrame(); + + // create the simulator info object + SimulatorInfo siminfo = new SimulatorInfo(); + siminfo.gameConfigClass = args[0]; + siminfo.simClass = args[1]; + siminfo.playerCount = getInt( + System.getProperty("playercount"), DEFAULT_PLAYER_COUNT); + + // create our client instance + _client = createSimulatorClient(_frame); + + // set up the top-level client controller + Controller ctrl = createController(siminfo); + _frame.setController(ctrl); + + // create the server + SimulatorServer server = createSimulatorServer(); + server.init(new ResultListener() { + public void requestCompleted (Object result) { + try { + run(); + } catch (Exception e) { + Log.warning("Simulator initialization failed " + + "[e=" + e + "]."); + } + } + public void requestFailed (Exception e) { + Log.warning("Simulator initialization failed [e=" + e + "]."); + } + }); + + // run the server on a separate thread + _serverThread = new ServerThread(server); + // start up the server so that we can be notified when + // initialization is complete + _serverThread.start(); + } + + protected SimulatorServer createSimulatorServer () + { + return new SimpleServer(); + } + + protected SimulatorFrame createSimulatorFrame () + { + return new SimpleFrame(); + } + + protected SimulatorClient createSimulatorClient (SimulatorFrame frame) + throws Exception + { + return new SimpleClient(_frame); + } + + protected SimulatorController createController (SimulatorInfo siminfo) + { + return new SimulatorController( + _client.getParlorContext(), _frame, siminfo); + } + + public void run () + { + // configure and display the main frame + JFrame frame = _frame.getFrame(); + frame.setSize(800, 600); + SwingUtil.centerWindow(frame); + frame.setVisible(true); + + // start up the client + Client client = _client.getParlorContext().getClient(); + Log.info("Connecting to localhost."); + client.setServer("localhost", Client.DEFAULT_SERVER_PORTS); + + // we want to exit when we logged off or failed to log on + client.addClientObserver(new ClientAdapter() { + public void clientFailedToLogon (Client c, Exception cause) { + Log.info("Client failed to logon: " + cause); + System.exit(0); + } + public void clientDidLogoff (Client c) { + System.exit(0); + } + }); + + // configure the client with some credentials and logon + String username = System.getProperty("username"); + if (username == null) { + username = + "bob" + ((int)(Math.random() * Integer.MAX_VALUE) % 500); + } + String password = System.getProperty("password"); + if (password == null) { + password = "test"; + } + + // create and set our credentials + client.setCredentials( + new UsernamePasswordCreds(new Name(username), password)); + + // this is a bit of a hack, but we need to give the server long + // enough to fully initialize and start listening on its socket + // before we try to logon; there's no good way for this otherwise + // wholly independent thread to wait for the server to be ready as + // in normal circumstances they are entirely different processes; + // so we just wait half a second which does the job + new Interval() { + public void expired () { + _client.getParlorContext().getClient().logon(); + } + }.schedule(500L); + } + + public static void main (String[] args) + { + if (!checkArgs(args)) { + return; + } + + SimulatorApp app = new SimulatorApp(); + try { + app.start(args); + } catch (Exception e) { + Log.warning("Error starting up application."); + Log.logStackTrace(e); + } + } + + protected static boolean checkArgs (String[] args) + { + if (args.length < 2) { + String msg = "Usage:\n" + + " java com.threerings.simulator.SimulatorApp " + + " \n" + + "Optional properties:\n" + + " -Dusername=\n" + + " -Dplayercount=\n" + + " -Dwidth=\n" + + " -Dheight="; + System.out.println(msg); + return false; + } + + return true; + } + + protected int getInt (String value, int defval) + { + try { + return Integer.parseInt(value); + } catch (NumberFormatException nfe) { + return defval; + } + } + + protected static class ServerThread extends Thread + { + public ServerThread (SimulatorServer server) + { + _server = server; + } + + public void run () + { + _server.run(); + } + + protected SimulatorServer _server; + } + + protected SimulatorClient _client; + protected SimulatorFrame _frame; + protected ServerThread _serverThread; + + /** The default number of players in the game. */ + protected static final int DEFAULT_PLAYER_COUNT = 2; +} diff --git a/src/java/com/threerings/micasa/simulator/client/SimulatorClient.java b/src/java/com/threerings/micasa/simulator/client/SimulatorClient.java new file mode 100644 index 00000000..7171872e --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimulatorClient.java @@ -0,0 +1,29 @@ +// +// $Id: SimulatorClient.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import com.threerings.parlor.util.ParlorContext; + +public interface SimulatorClient +{ + public ParlorContext getParlorContext (); +} diff --git a/src/java/com/threerings/micasa/simulator/client/SimulatorController.java b/src/java/com/threerings/micasa/simulator/client/SimulatorController.java new file mode 100644 index 00000000..8144e9a5 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimulatorController.java @@ -0,0 +1,134 @@ +// +// $Id: SimulatorController.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import java.awt.event.ActionEvent; +import com.samskivert.swing.Controller; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.SessionObserver; + +import com.threerings.crowd.data.BodyObject; + +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.util.ParlorContext; + +import com.threerings.micasa.Log; +import com.threerings.micasa.simulator.data.SimulatorInfo; + +/** + * Responsible for top-level control of the simulator client user interface. + */ +public class SimulatorController extends Controller + implements SessionObserver +{ + /** Command constant used to logoff the client. */ + public static final String LOGOFF = "logoff"; + +// 577-2028 + + /** + * Creates a new simulator controller. The controller will set + * everything up in preparation for logging on. + */ + public SimulatorController (ParlorContext ctx, SimulatorFrame frame, + SimulatorInfo info) + { + // we'll want to keep these around + _ctx = ctx; + _frame = frame; + _info = info; + + // we want to know about logon/logoff + _ctx.getClient().addClientObserver(this); + } + + // documentation inherited + public boolean handleAction (ActionEvent action) + { + String cmd = action.getActionCommand(); + + if (cmd.equals(LOGOFF)) { + // request that we logoff + _ctx.getClient().logoff(true); + return true; + } + + Log.info("Unhandled action: " + action); + return false; + } + + // documentation inherited + public void clientDidLogon (Client client) + { + Log.info("Client did logon [client=" + client + "]."); + + // keep the body object around for stuff + _body = (BodyObject)client.getClientObject(); + + // have at it + createGame(client); + } + + public void createGame (Client client) + { + GameConfig config = null; + try { + // create the game config object + config = (GameConfig) + Class.forName(_info.gameConfigClass).newInstance(); + + // get the simulator service and use it to request that our + // game be created + SimulatorService sservice = (SimulatorService) + client.requireService(SimulatorService.class); + sservice.createGame( + client, config, _info.simClass, _info.playerCount); + + // our work here is done, as the location manager will move us + // into the game room straightaway + + } catch (Exception e) { + Log.warning("Failed to instantiate game config " + + "[class=" + _info.gameConfigClass + + ", error=" + e + "]."); + } + } + + // documentation inherited + public void clientObjectDidChange (Client client) + { + // regrab our body object + _body = (BodyObject)client.getClientObject(); + } + + // documentation inherited + public void clientDidLogoff (Client client) + { + Log.info("Client did logoff [client=" + client + "]."); + } + + protected ParlorContext _ctx; + protected SimulatorFrame _frame; + protected SimulatorInfo _info; + protected BodyObject _body; +} diff --git a/src/java/com/threerings/micasa/simulator/client/SimulatorFrame.java b/src/java/com/threerings/micasa/simulator/client/SimulatorFrame.java new file mode 100644 index 00000000..c88ab3a3 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimulatorFrame.java @@ -0,0 +1,52 @@ +// +// $Id: SimulatorFrame.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import javax.swing.JFrame; +import javax.swing.JPanel; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.ControllerProvider; + +/** + * Contains the user interface for the Simulator client application. + */ +public interface SimulatorFrame extends ControllerProvider +{ + /** + * Returns a reference to the top-level frame that the simulator will + * use to display everything. + */ + public JFrame getFrame (); + + /** + * Sets the panel that makes up the entire client display. + */ + public void setPanel (JPanel panel); + + /** + * Sets the controller for the outermost scope. This controller will + * handle all actions that aren't handled by controllers of higher + * scope. + */ + public void setController (Controller controller); +} diff --git a/src/java/com/threerings/micasa/simulator/client/SimulatorService.java b/src/java/com/threerings/micasa/simulator/client/SimulatorService.java new file mode 100644 index 00000000..00d856e0 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/client/SimulatorService.java @@ -0,0 +1,44 @@ +// +// $Id: SimulatorService.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.parlor.game.data.GameConfig; + +/** + * Provides access to simulator invocation services. + */ +public interface SimulatorService extends InvocationService +{ + /** + * Requests that a new game be created. + * + * @param client a connected, operational client instance. + * @param config the game config for the game to be created. + * @param simClass the class name of the simulant to create. + * @param playerCount the number of players in the game. + */ + public void createGame (Client client, GameConfig config, + String simClass, int playerCount); +} diff --git a/src/java/com/threerings/micasa/simulator/data/SimulatorInfo.java b/src/java/com/threerings/micasa/simulator/data/SimulatorInfo.java new file mode 100644 index 00000000..bcfb248a --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/data/SimulatorInfo.java @@ -0,0 +1,40 @@ +// +// $Id: SimulatorInfo.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.data; + +public class SimulatorInfo +{ + /** The game config classname. */ + public String gameConfigClass; + + /** The simulant classname. */ + public String simClass; + + /** The number of players in the game. */ + public int playerCount; + + public String toString () + { + return "[gameConfigClass=" + gameConfigClass + + ", simClass=" + simClass + ", playerCount=" + playerCount + "]"; + } +} diff --git a/src/java/com/threerings/micasa/simulator/data/SimulatorMarshaller.java b/src/java/com/threerings/micasa/simulator/data/SimulatorMarshaller.java new file mode 100644 index 00000000..60a49177 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/data/SimulatorMarshaller.java @@ -0,0 +1,51 @@ +// +// $Id: SimulatorMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.data; + +import com.threerings.micasa.simulator.client.SimulatorService; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; + +/** + * Provides the implementation of the {@link SimulatorService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class SimulatorMarshaller extends InvocationMarshaller + implements SimulatorService +{ + /** The method id used to dispatch {@link #createGame} requests. */ + public static final int CREATE_GAME = 1; + + // documentation inherited from interface + public void createGame (Client arg1, GameConfig arg2, String arg3, int arg4) + { + sendRequest(arg1, CREATE_GAME, new Object[] { + arg2, arg3, Integer.valueOf(arg4) + }); + } + +} diff --git a/src/java/com/threerings/micasa/simulator/server/SimpleServer.java b/src/java/com/threerings/micasa/simulator/server/SimpleServer.java new file mode 100644 index 00000000..7f585015 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/server/SimpleServer.java @@ -0,0 +1,50 @@ +// +// $Id: SimpleServer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.server; + +import com.samskivert.util.ResultListener; + +import com.threerings.micasa.server.MiCasaServer; + +/** + * A simple simulator server implementation that extends the MiCasa server + * and provides no special functionality. + */ +public class SimpleServer extends MiCasaServer + implements SimulatorServer +{ + // documentation inherited + public void init (ResultListener obs) + throws Exception + { + super.init(); + + // create the simulator manager + SimulatorManager simmgr = new SimulatorManager(); + simmgr.init(invmgr, plreg, clmgr, omgr, this); + + if (obs != null) { + // let the initialization observer know that we've started up + obs.requestCompleted(this); + } + } +} diff --git a/src/java/com/threerings/micasa/simulator/server/Simulant.java b/src/java/com/threerings/micasa/simulator/server/Simulant.java new file mode 100644 index 00000000..10645c6f --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/server/Simulant.java @@ -0,0 +1,88 @@ +// +// $Id: Simulant.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.server; + +import com.threerings.presents.dobj.DObjectManager; +import com.threerings.presents.dobj.MessageEvent; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.server.GameManager; + +public abstract class Simulant +{ + /** + * Initializes the simulant with a body object and the game config for + * the game they'll be engaged in. + */ + public void init (BodyObject self, GameConfig config, + GameManager gmgr, DObjectManager omgr) + { + _self = self; + _config = config; + _gmgr = gmgr; + _omgr = omgr; + } + + /** + * Called when the simulant is about to enter the room in which it + * will be doing all of its business. Default implementation + * immediately notifies the game manager that the simulant is ready to + * play. Sub-classes may wish to override this to do things like + * subscribe to the game object, but should be sure to call this + * method when they're finished to give the game manager the go-ahead + * to proceed. + */ + public void willEnterPlace (PlaceObject plobj) + { + // let the game manager know that the simulant's ready + _gmgr.playerReady(_self); + } + + /** + * Posts the given message event to the server. Since the simulant + * resides within the server itself, it has no available client + * distributed object manager and so we must set up the source oid + * ourselves before sending it on its merry way. Sub-classes should + * accordingly be sure to make use of this method to send any + * messages. + */ + protected void postEvent (MessageEvent mevt) + { + mevt.setSourceOid(_self.getOid()); + _omgr.postEvent(mevt); + } + + /** The game config object. */ + protected GameConfig _config; + + /** The game manager for the game we're playing. */ + protected GameManager _gmgr; + + /** Our body object. */ + protected BodyObject _self; + + /** The object manager with which we're interacting. */ + protected DObjectManager _omgr; +} diff --git a/src/java/com/threerings/micasa/simulator/server/SimulatorDispatcher.java b/src/java/com/threerings/micasa/simulator/server/SimulatorDispatcher.java new file mode 100644 index 00000000..ff338599 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/server/SimulatorDispatcher.java @@ -0,0 +1,71 @@ +// +// $Id: SimulatorDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.server; + +import com.threerings.micasa.simulator.client.SimulatorService; +import com.threerings.micasa.simulator.data.SimulatorMarshaller; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; + +/** + * Dispatches requests to the {@link SimulatorProvider}. + */ +public class SimulatorDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public SimulatorDispatcher (SimulatorProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new SimulatorMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case SimulatorMarshaller.CREATE_GAME: + ((SimulatorProvider)provider).createGame( + source, + (GameConfig)args[0], (String)args[1], ((Integer)args[2]).intValue() + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/micasa/simulator/server/SimulatorManager.java b/src/java/com/threerings/micasa/simulator/server/SimulatorManager.java new file mode 100644 index 00000000..b64bed69 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/server/SimulatorManager.java @@ -0,0 +1,237 @@ +// +// $Id: SimulatorManager.java 3758 2005-11-10 23:18:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.server; + +import java.util.ArrayList; + +import com.threerings.util.Name; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.dobj.RootDObjectManager; +import com.threerings.presents.server.ClientManager; +import com.threerings.presents.server.ClientResolutionListener; +import com.threerings.presents.server.InvocationManager; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.server.PlaceManager; +import com.threerings.crowd.server.PlaceRegistry.CreationObserver; +import com.threerings.crowd.server.PlaceRegistry; + +import com.threerings.parlor.game.data.GameAI; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.game.server.GameManager; + +import com.threerings.micasa.Log; + +/** + * The simulator manager is responsible for handling the simulator + * services on the server side. + */ +public class SimulatorManager +{ + /** + * Initializes the simulator manager manager. This should be called by + * the server that is making use of the simulator services on the + * single instance of simulator manager that it has created. + * + * @param invmgr a reference to the invocation manager in use by this + * server. + */ + public void init (InvocationManager invmgr, PlaceRegistry plreg, + ClientManager clmgr, RootDObjectManager omgr, + SimulatorServer simserv) + { + // register our simulator provider + SimulatorProvider sprov = new SimulatorProvider(this); + invmgr.registerDispatcher(new SimulatorDispatcher(sprov), true); + + // keep these for later + _plreg = plreg; + _clmgr = clmgr; + _omgr = omgr; + _simserv = simserv; + } + + /** + * Creates a game along with the specified number of simulant players + * and forcibly moves all players into the game room. + */ + public void createGame ( + BodyObject source, GameConfig config, String simClass, int playerCount) + { + new CreateGameTask(source, config, simClass, playerCount); + } + + public class CreateGameTask implements CreationObserver + { + public CreateGameTask ( + BodyObject source, GameConfig config, String simClass, + int playerCount) + { + // save off game request info + _source = source; + _config = config; + _simClass = simClass; + _playerCount = playerCount; + + try { + // create the game manager and begin its initialization + // process. the game manager will take care of notifying + // the players that the game has been created once it has + // been started up (which is done by the place registry + // once the game object creation has completed) + + // configure the game config with the player names + config.players = new Name[_playerCount]; + config.players[0] = _source.getVisibleName(); + for (int ii = 1; ii < _playerCount; ii++) { + config.players[ii] = new Name("simulant" + ii); + } + + // we needn't hang around and wait for game object + // creation if it's just us + CreationObserver obs = (_playerCount == 1) ? null : this; + _gmgr = (GameManager)_plreg.createPlace(config, obs); + + } catch (Exception e) { + Log.warning("Unable to create game manager [e=" + e + "]."); + Log.logStackTrace(e); + } + } + + // documentation inherited + public void placeCreated (PlaceObject place, PlaceManager pmgr) + { + // cast the place to the game object for the game we're creating + _gobj = (GameObject)place; + + // determine the AI player skill level + byte skill; + try { + skill = Byte.parseByte(System.getProperty("skill")); + } catch (NumberFormatException nfe) { + skill = DEFAULT_SKILL; + } + + for (int ii = 1; ii < _playerCount; ii++) { + // mark all simulants as AI players + _gmgr.setAI(ii, new GameAI(0, skill)); + } + + // resolve the simulant body objects + ClientResolutionListener listener = new ClientResolutionListener() + { + public void clientResolved (Name username, ClientObject clobj) + { + // hold onto the body object for later game creation + _sims.add(clobj); + + // create the game if we've received all body objects + if (_sims.size() == (_playerCount - 1)) { + createSimulants(); + } + } + + public void resolutionFailed (Name username, Exception cause) + { + Log.warning("Unable to create simulant body object " + + "[error=" + cause + "]."); + } + }; + + // resolve client objects for all of our simulants + for (int ii = 1; ii < _playerCount; ii++) { + Name username = new Name("simulant" + ii); + _clmgr.resolveClientObject(username, listener); + } + } + + /** + * Called when all simulant body objects are present and the + * simulants are ready to be created. + */ + protected void createSimulants () + { + // finish setting up the simulants + for (int ii = 1; ii < _playerCount; ii++) { + // create the simulant object + Simulant sim; + try { + sim = (Simulant)Class.forName(_simClass).newInstance(); + } catch (Exception e) { + Log.warning("Unable to create simulant " + + "[class=" + _simClass + "]."); + return; + } + + // give the simulant its body + BodyObject bobj = (BodyObject)_sims.get(ii - 1); + sim.init(bobj, _config, _gmgr, _omgr); + + // give the simulant a chance to engage in place antics + sim.willEnterPlace(_gobj); + + // move the simulant into the game room since they have no + // location director to move them automagically + try { + _plreg.locprov.moveTo(bobj, _gobj.getOid()); + } catch (Exception e) { + Log.warning("Failed to move simulant into room " + + "[e=" + e + "]."); + return; + } + } + } + + /** The simulant body objects. */ + protected ArrayList _sims = new ArrayList(); + + /** The game object for the game being created. */ + protected GameObject _gobj; + + /** The game manager for the game being created. */ + protected GameManager _gmgr; + + /** The number of players in the game. */ + protected int _playerCount; + + /** The simulant class instantiated on game creation. */ + protected String _simClass; + + /** The game config object. */ + protected GameConfig _config; + + /** The body object of the player requesting the game creation. */ + protected BodyObject _source; + } + + // needed for general operation + protected PlaceRegistry _plreg; + protected ClientManager _clmgr; + protected RootDObjectManager _omgr; + protected SimulatorServer _simserv; + + /** The default skill level for AI players. */ + protected static final byte DEFAULT_SKILL = 50; +} diff --git a/src/java/com/threerings/micasa/simulator/server/SimulatorProvider.java b/src/java/com/threerings/micasa/simulator/server/SimulatorProvider.java new file mode 100644 index 00000000..ba497293 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/server/SimulatorProvider.java @@ -0,0 +1,62 @@ +// +// $Id: SimulatorProvider.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.server; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationProvider; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.parlor.game.data.GameConfig; + +import com.threerings.micasa.Log; + +/** + * The simulator provider handles game creation requests on the server + * side, passing them off to the {@link SimulatorManager}. + */ +public class SimulatorProvider + implements InvocationProvider +{ + /** + * Constructs a simulator provider. + */ + public SimulatorProvider (SimulatorManager simmgr) + { + _simmgr = simmgr; + } + + /** + * Processes a request from the client to create a new game. + */ + public void createGame (ClientObject caller, GameConfig config, + String simClass, int playerCount) + { + Log.info("handleCreateGameRequest [caller=" + caller.who() + + ", config=" + config + ", simClass=" + simClass + + ", playerCount=" + playerCount + "]."); + + _simmgr.createGame((BodyObject)caller, config, simClass, playerCount); + } + + /** The simulator manager. */ + protected SimulatorManager _simmgr; +} diff --git a/src/java/com/threerings/micasa/simulator/server/SimulatorServer.java b/src/java/com/threerings/micasa/simulator/server/SimulatorServer.java new file mode 100644 index 00000000..959fe47a --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/server/SimulatorServer.java @@ -0,0 +1,49 @@ +// +// $Id: SimulatorServer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.server; + +import com.samskivert.util.ResultListener; + +/** + * The simulator manager needs a mechanism for faking body object + * registrations, which is provided by implementations of this interface. + */ +public interface SimulatorServer +{ + /** + * Called to initialize this server instance. + * + * @param obs the observer to notify when the server has finished + * starting up, or null if no notification is desired. + * + * @exception Exception thrown if anything goes wrong initializing the + * server. + */ + public void init (ResultListener obs) throws Exception; + + /** + * Called to perform the main body of server processing. This is + * called from the server thread and should do the simulator server's + * primary business. + */ + public void run (); +} diff --git a/src/java/com/threerings/micasa/simulator/util/SimulatorContext.java b/src/java/com/threerings/micasa/simulator/util/SimulatorContext.java new file mode 100644 index 00000000..1c076775 --- /dev/null +++ b/src/java/com/threerings/micasa/simulator/util/SimulatorContext.java @@ -0,0 +1,44 @@ +// +// $Id: SimulatorContext.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.simulator.util; + +import com.threerings.parlor.util.ParlorContext; + +import com.threerings.micasa.simulator.client.SimulatorFrame; +import com.threerings.micasa.simulator.data.SimulatorInfo; + +/** + * The simulator context encapsulates the contexts of all of the services + * that are used by the simulator client so that we can pass around one + * single context implementation that provides all of the necessary + * components to all of the services in use. + */ +public interface SimulatorContext + extends ParlorContext +{ + /** Returns a reference to the primary user interface frame. */ + public SimulatorFrame getFrame (); + + /** Returns a reference to the simulator info describing the game and + * other details of the simulation. */ + public SimulatorInfo getSimulatorInfo (); +} diff --git a/src/java/com/threerings/micasa/util/MiCasaContext.java b/src/java/com/threerings/micasa/util/MiCasaContext.java new file mode 100644 index 00000000..6ec36b15 --- /dev/null +++ b/src/java/com/threerings/micasa/util/MiCasaContext.java @@ -0,0 +1,46 @@ +// +// $Id: MiCasaContext.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.micasa.util; + +import com.threerings.util.MessageManager; +import com.threerings.parlor.util.ParlorContext; +import com.threerings.micasa.client.MiCasaFrame; + +/** + * The micasa context encapsulates the contexts of all of the services + * that are used by the micasa client so that we can pass around one + * single context implementation that provides all of the necessary + * components to all of the services in use. + */ +public interface MiCasaContext + extends ParlorContext +{ + /** Returns a reference to the primary user interface frame. This can + * be used to set the top-level panel when we enter a game, etc. */ + public MiCasaFrame getFrame (); + + /** + * Returns a reference to the message manager used by the client to + * generate localized messages. + */ + public MessageManager getMessageManager (); +} diff --git a/src/java/com/threerings/parlor/Log.java b/src/java/com/threerings/parlor/Log.java new file mode 100644 index 00000000..76411fe6 --- /dev/null +++ b/src/java/com/threerings/parlor/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor; + +/** + * A placeholder class that contains a reference to the log object used by + * the Parlor services. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("parlor"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/parlor/card/Log.java b/src/java/com/threerings/parlor/card/Log.java new file mode 100644 index 00000000..b70dadf8 --- /dev/null +++ b/src/java/com/threerings/parlor/card/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card; + +/** + * A placeholder class that contains a reference to the log object used by + * the Card services. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("card"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/parlor/card/client/CardGameController.java b/src/java/com/threerings/parlor/card/client/CardGameController.java new file mode 100644 index 00000000..43fa8827 --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/CardGameController.java @@ -0,0 +1,122 @@ +// +// $Id: CardGameController.java 4053 2006-04-25 00:49:58Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import com.threerings.util.Name; + +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.card.Log; +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.CardCodes; +import com.threerings.parlor.card.data.Hand; +import com.threerings.parlor.game.client.GameController; +import com.threerings.parlor.turn.client.TurnGameController; + +/** + * A controller class for card games. Handles common functions like + * accepting dealt hands. + */ +public abstract class CardGameController extends GameController + implements TurnGameController, CardCodes, CardGameReceiver +{ + // Documentation inherited. + public void willEnterPlace (PlaceObject plobj) + { + super.willEnterPlace(plobj); + + if (_ctx.getClient().getClientObject().receivers.containsKey( + CardGameDecoder.RECEIVER_CODE)) { + Log.warning("Yuh oh, we already have a card game receiver " + + "registered and are trying for another...!"); + Thread.dumpStack(); + } + + _ctx.getClient().getInvocationDirector().registerReceiver( + new CardGameDecoder(this)); + } + + // Documentation inherited. + public void didLeavePlace (PlaceObject plobj) + { + super.didLeavePlace(plobj); + + _ctx.getClient().getInvocationDirector().unregisterReceiver( + CardGameDecoder.RECEIVER_CODE); + } + + // Documentation inherited. + public void turnDidChange (Name turnHolder) + {} + + /** + * Called by our sender to notify us of a received hand. + */ + public final void receivedHand (int oid, Hand hand) + { + if (oid == _gobj.getOid()) { + receivedHand(hand); + } + } + + /** + * Called when the server deals the client a new hand of cards. Default + * implementation does nothing. + * + * @param hand the hand dealt to the user + */ + public void receivedHand (Hand hand) + {} + + /** + * Dispatched to the client when it has received a set of cards + * from another player. Default implementation does nothing. + * + * @param plidx the index of the player providing the cards + * @param cards the cards received + */ + public void receivedCardsFromPlayer (int plidx, Card[] cards) + {} + + /** + * Dispatched to the client when the server has forced it to send + * a set of cards to another player. Default implementation does + * nothing. + * + * @param plidx the index of the player to which the cards were sent + * @param cards the cards sent + */ + public void sentCardsToPlayer (int plidx, Card[] cards) + {} + + /** + * Dispatched to the client when a set of cards is transferred between + * two other players in the game. Default implementation does nothing. + * + * @param fromidx the index of the player sending the cards + * @param toidx the index of the player receiving the cards + * @param cards the number of cards transferred + */ + public void cardsTransferredBetweenPlayers (int fromidx, int toidx, + int cards) + {} +} diff --git a/src/java/com/threerings/parlor/card/client/CardGameDecoder.java b/src/java/com/threerings/parlor/card/client/CardGameDecoder.java new file mode 100644 index 00000000..ab05fcfe --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/CardGameDecoder.java @@ -0,0 +1,101 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import com.threerings.parlor.card.client.CardGameReceiver; +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.Hand; +import com.threerings.presents.client.InvocationDecoder; + +/** + * Dispatches calls to a {@link CardGameReceiver} instance. + */ +public class CardGameDecoder extends InvocationDecoder +{ + /** The generated hash code used to identify this receiver class. */ + public static final String RECEIVER_CODE = "0718199d459e31d8d673744c71b0e788"; + + /** The method id used to dispatch {@link CardGameReceiver#cardsTransferredBetweenPlayers} + * notifications. */ + public static final int CARDS_TRANSFERRED_BETWEEN_PLAYERS = 1; + + /** The method id used to dispatch {@link CardGameReceiver#receivedCardsFromPlayer} + * notifications. */ + public static final int RECEIVED_CARDS_FROM_PLAYER = 2; + + /** The method id used to dispatch {@link CardGameReceiver#receivedHand} + * notifications. */ + public static final int RECEIVED_HAND = 3; + + /** The method id used to dispatch {@link CardGameReceiver#sentCardsToPlayer} + * notifications. */ + public static final int SENT_CARDS_TO_PLAYER = 4; + + /** + * Creates a decoder that may be registered to dispatch invocation + * service notifications to the specified receiver. + */ + public CardGameDecoder (CardGameReceiver receiver) + { + this.receiver = receiver; + } + + // documentation inherited + public String getReceiverCode () + { + return RECEIVER_CODE; + } + + // documentation inherited + public void dispatchNotification (int methodId, Object[] args) + { + switch (methodId) { + case CARDS_TRANSFERRED_BETWEEN_PLAYERS: + ((CardGameReceiver)receiver).cardsTransferredBetweenPlayers( + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), ((Integer)args[2]).intValue() + ); + return; + + case RECEIVED_CARDS_FROM_PLAYER: + ((CardGameReceiver)receiver).receivedCardsFromPlayer( + ((Integer)args[0]).intValue(), (Card[])args[1] + ); + return; + + case RECEIVED_HAND: + ((CardGameReceiver)receiver).receivedHand( + ((Integer)args[0]).intValue(), (Hand)args[1] + ); + return; + + case SENT_CARDS_TO_PLAYER: + ((CardGameReceiver)receiver).sentCardsToPlayer( + ((Integer)args[0]).intValue(), (Card[])args[1] + ); + return; + + default: + super.dispatchNotification(methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/parlor/card/client/CardGameReceiver.java b/src/java/com/threerings/parlor/card/client/CardGameReceiver.java new file mode 100644 index 00000000..f3ebbf70 --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/CardGameReceiver.java @@ -0,0 +1,71 @@ +// +// $Id: ParlorReceiver.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.Hand; + +import com.threerings.presents.client.InvocationReceiver; + +/** + * Defines, for the card game services, a set of notifications delivered + * asynchronously by the server to the client. + */ +public interface CardGameReceiver extends InvocationReceiver +{ + /** + * Dispatched to the client when it has received a hand of cards. + * + * @param oid the oid of the game for which this hand applies + * @param hand the received hand + */ + public void receivedHand (int oid, Hand hand); + + /** + * Dispatched to the client when it has received a set of cards + * from another player. + * + * @param plidx the index of the player providing the cards + * @param cards the cards received + */ + public void receivedCardsFromPlayer (int plidx, Card[] cards); + + /** + * Dispatched to the client when the server has forced it to send + * a set of cards to another player. + * + * @param plidx the index of the player to which the cards were sent + * @param cards the cards sent + */ + public void sentCardsToPlayer (int plidx, Card[] cards); + + /** + * Dispatched to the client when a set of cards is transferred between + * two other players in the game. + * + * @param fromidx the index of the player sending the cards + * @param toidx the index of the player receiving the cards + * @param cards the number of cards transferred + */ + public void cardsTransferredBetweenPlayers (int fromidx, int toidx, + int cards); +} diff --git a/src/java/com/threerings/parlor/card/client/CardPanel.java b/src/java/com/threerings/parlor/card/client/CardPanel.java new file mode 100644 index 00000000..213111ad --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/CardPanel.java @@ -0,0 +1,1149 @@ +// +// $Id: CardPanel.java 3778 2005-12-08 19:41:12Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; + +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.event.MouseInputAdapter; + +import com.samskivert.util.ObserverList; +import com.samskivert.util.QuickSort; + +import com.threerings.media.FrameManager; +import com.threerings.media.VirtualMediaPanel; +import com.threerings.media.image.Mirage; +import com.threerings.media.sprite.PathAdapter; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.util.LinePath; +import com.threerings.media.util.Path; +import com.threerings.media.util.PathSequence; +import com.threerings.media.util.Pathable; + +import com.threerings.parlor.card.Log; +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.CardCodes; +import com.threerings.parlor.card.data.Deck; +import com.threerings.parlor.card.data.Hand; + +/** + * Extends VirtualMediaPanel to provide services specific to rendering + * and manipulating playing cards. + */ +public abstract class CardPanel extends VirtualMediaPanel + implements CardCodes +{ + /** The selection mode in which cards are not selectable. */ + public static final int NONE = 0; + + /** The selection mode in which the user can select a single card. */ + public static final int SINGLE = 1; + + /** The selection mode in which the user can select multiple cards. */ + public static final int MULTIPLE = 2; + + /** + * A predicate class for {@link CardSprite}s. Provides control over which + * cards are selectable, playable, etc. + */ + public static interface CardSpritePredicate + { + /** + * Evaluates the specified sprite. + */ + public boolean evaluate (CardSprite sprite); + } + + /** + * A listener for card selection/deselection. + */ + public static interface CardSelectionObserver + { + /** + * Called when a card has been selected. + */ + public void cardSpriteSelected (CardSprite sprite); + + /** + * Called when a card has been deselected. + */ + public void cardSpriteDeselected (CardSprite sprite); + } + + /** + * Constructor. + * + * @param frameManager the frame manager + */ + public CardPanel (FrameManager frameManager) + { + super(frameManager); + + // add a listener for mouse events + CardListener cl = new CardListener(); + addMouseListener(cl); + addMouseMotionListener(cl); + } + + /** + * Returns the full-sized image for the back of a playing card. + */ + public abstract Mirage getCardBackImage (); + + /** + * Returns the full-sized image for the front of the specified card. + */ + public abstract Mirage getCardImage (Card card); + + /** + * Returns the small-sized image for the back of a playing card. + */ + public abstract Mirage getMicroCardBackImage (); + + /** + * Returns the small-sized image for the front of the specified card. + */ + public abstract Mirage getMicroCardImage (Card card); + + /** + * Sets the location of the hand (the location of the center of the hand's + * upper edge). + */ + public void setHandLocation (int x, int y) + { + _handLocation.setLocation(x, y); + } + + /** + * Sets the horizontal spacing between cards in the hand. + */ + public void setHandSpacing (int spacing) + { + _handSpacing = spacing; + } + + /** + * Sets the vertical distance to offset cards that are selectable or + * playable. + */ + public void setSelectableCardOffset (int offset) + { + _selectableCardOffset = offset; + } + + /** + * Sets the vertical distance to offset cards that are selected. + */ + public void setSelectedCardOffset (int offset) + { + _selectedCardOffset = offset; + } + + /** + * Sets the selection mode for the hand (NONE, PLAY_SINGLE, SINGLE, + * or MULTIPLE). Changing the selection mode does not change the + * current selection. + */ + public void setHandSelectionMode (int mode) + { + _handSelectionMode = mode; + + // update the offsets of all cards in the hand + updateHandOffsets(); + } + + /** + * Sets the selection predicate that determines which cards from the hand + * may be selected (if null, all cards may be selected). Changing the + * predicate does not change the current selection. + */ + public void setHandSelectionPredicate (CardSpritePredicate pred) + { + _handSelectionPredicate = pred; + + // update the offsets of all cards in the hand + updateHandOffsets(); + } + + /** + * Returns the currently selected hand sprite (null if no sprites are + * selected, the first sprite if multiple sprites are selected). + */ + public CardSprite getSelectedHandSprite () + { + return _selectedHandSprites.size() == 0 ? + null : (CardSprite)_selectedHandSprites.get(0); + } + + /** + * Returns an array containing the currently selected hand sprites + * (returns an empty array if no sprites are selected). + */ + public CardSprite[] getSelectedHandSprites () + { + return (CardSprite[])_selectedHandSprites.toArray( + new CardSprite[_selectedHandSprites.size()]); + } + + /** + * Programmatically selects a sprite in the hand. + */ + public void selectHandSprite (final CardSprite sprite) + { + // make sure it's not already selected + if (_selectedHandSprites.contains(sprite)) { + return; + } + + // if in single card mode and there's another card selected, + // deselect it + if (_handSelectionMode == SINGLE) { + CardSprite oldSprite = getSelectedHandSprite(); + if (oldSprite != null) { + deselectHandSprite(oldSprite); + } + } + + // add to list and update offset + _selectedHandSprites.add(sprite); + sprite.setLocation(sprite.getX(), getHandY(sprite)); + + // notify the observers + ObserverList.ObserverOp op = new ObserverList.ObserverOp() { + public boolean apply (Object obs) { + ((CardSelectionObserver)obs).cardSpriteSelected(sprite); + return true; + } + }; + _handSelectionObservers.apply(op); + } + + /** + * Programmatically deselects a sprite in the hand. + */ + public void deselectHandSprite (final CardSprite sprite) + { + // make sure it's selected + if (!_selectedHandSprites.contains(sprite)) { + return; + } + + // remove from list and update offset + _selectedHandSprites.remove(sprite); + sprite.setLocation(sprite.getX(), getHandY(sprite)); + + // notify the observers + ObserverList.ObserverOp op = new ObserverList.ObserverOp() { + public boolean apply (Object obs) { + ((CardSelectionObserver)obs).cardSpriteDeselected(sprite); + return true; + } + }; + _handSelectionObservers.apply(op); + } + + /** + * Clears any existing hand sprite selection. + */ + public void clearHandSelection () + { + CardSprite[] sprites = getSelectedHandSprites(); + for (int i = 0; i < sprites.length; i++) { + deselectHandSprite(sprites[i]); + } + } + + /** + * Adds an object to the list of observers to notify when cards in the + * hand are selected/deselected. + */ + public void addHandSelectionObserver (CardSelectionObserver obs) + { + _handSelectionObservers.add(obs); + } + + /** + * Removes an object from the hand selection observer list. + */ + public void removeHandSelectionObserver (CardSelectionObserver obs) + { + _handSelectionObservers.remove(obs); + } + + /** + * Fades a hand of cards in. + * + * @param hand the hand of cards + * @param fadeDuration the amount of time to spend fading in + * the entire hand + */ + public void setHand (Hand hand, long fadeDuration) + { + // make sure no cards are hanging around + clearHand(); + + // create the sprites + int size = hand.size(); + for (int i = 0; i < size; i++) { + CardSprite cs = new CardSprite(this, (Card)hand.get(i)); + _handSprites.add(cs); + } + + // sort them + if (shouldSortHand()) { + QuickSort.sort(_handSprites); + } + + // fade them in at proper locations and layers + long cardDuration = fadeDuration / size; + for (int i = 0; i < size; i++) { + CardSprite cs = (CardSprite)_handSprites.get(i); + cs.setLocation(getHandX(size, i), _handLocation.y); + cs.setRenderOrder(i); + cs.addSpriteObserver(_handSpriteObserver); + addSprite(cs); + cs.fadeIn(i * cardDuration, cardDuration); + } + + // make sure we have the right card sprite active + updateActiveCardSprite(); + } + + /** + * Fades a hand of cards in face-down. + * + * @param size the size of the hand + * @param fadeDuration the amount of time to spend fading in + * each card + */ + public void setHand (int size, long fadeDuration) + { + // fill hand will null entries to signify unknown cards + Hand hand = new Hand(); + for (int i = 0; i < size; i++) { + hand.add(null); + } + setHand(hand, fadeDuration); + } + + /** + * Shows a hand that was previous set face-down. + * + * @param hand the hand of cards + */ + public void showHand (Hand hand) + { + // sort the hand + if (shouldSortHand()) { + QuickSort.sort(hand); + } + + // set the sprites + int len = Math.min(_handSprites.size(), hand.size()); + for (int i = 0; i < len; i++) { + CardSprite cs = (CardSprite)_handSprites.get(i); + cs.setCard((Card)hand.get(i)); + } + } + + /** + * Returns the first sprite in the hand that corresponds to the + * specified card, or null if the card is not in the hand. + */ + public CardSprite getHandSprite (Card card) + { + return getCardSprite(_handSprites, card); + } + + /** + * Clears all cards from the hand. + */ + public void clearHand () + { + clearHandSelection(); + clearSprites(_handSprites); + } + + /** + * Clears all cards from the board. + */ + public void clearBoard () + { + clearSprites(_boardSprites); + } + + /** + * Flies a set of cards from the hand into the ether. Clears any selected + * cards. + * + * @param cards the card sprites to remove from the hand + * @param dest the point to fly the cards to + * @param flightDuration the duration of the cards' flight + * @param fadePortion the amount of time to spend fading out + * as a proportion of the flight duration + */ + public void flyFromHand (CardSprite[] cards, Point dest, + long flightDuration, float fadePortion) + { + // fly each sprite over, removing it from the hand immediately and + // from the board when it finishes its path + for (int i = 0; i < cards.length; i++) { + removeFromHand(cards[i]); + LinePath flight = new LinePath(dest, flightDuration); + cards[i].addSpriteObserver(_pathEndRemover); + cards[i].moveAndFadeOut(flight, flightDuration, fadePortion); + } + + // adjust the hand to cover the hole + adjustHand(flightDuration, false); + } + + /** + * Flies a set of cards from the ether into the hand. Clears any selected + * cards. The cards will first fly to the selected card offset, pause for + * the specified duration, and then drop into the hand. + * + * @param cards the cards to add to the hand + * @param src the point to fly the cards from + * @param flightDuration the duration of the cards' flight + * @param pauseDuration the duration of the pause before dropping into the + * hand + * @param dropDuration the duration of the cards' drop into the + * hand + * @param fadePortion the amount of time to spend fading in + * as a proportion of the flight duration + */ + public void flyIntoHand (Card[] cards, Point src, long flightDuration, + long pauseDuration, long dropDuration, float fadePortion) + { + // first create the sprites and add them to the list + CardSprite[] sprites = new CardSprite[cards.length]; + for (int i = 0; i < cards.length; i++) { + sprites[i] = new CardSprite(this, cards[i]); + _handSprites.add(sprites[i]); + } + + // settle the hand + adjustHand(flightDuration, true); + + // then set the layers and fly the cards in + int size = _handSprites.size(); + for (int i = 0; i < sprites.length; i++) { + int idx = _handSprites.indexOf(sprites[i]); + sprites[i].setLocation(src.x, src.y); + sprites[i].setRenderOrder(idx); + sprites[i].addSpriteObserver(_handSpriteObserver); + addSprite(sprites[i]); + + // create a path sequence containing flight, pause, and drop + ArrayList paths = new ArrayList(); + Point hp2 = new Point(getHandX(size, idx), _handLocation.y), + hp1 = new Point(hp2.x, hp2.y - _selectedCardOffset); + paths.add(new LinePath(hp1, flightDuration)); + paths.add(new LinePath(hp1, pauseDuration)); + paths.add(new LinePath(hp2, dropDuration)); + sprites[i].moveAndFadeIn(new PathSequence(paths), flightDuration + + pauseDuration + dropDuration, fadePortion); + } + } + + /** + * Flies a set of cards from the ether into the ether. + * + * @param cards the cards to fly across + * @param src the point to fly the cards from + * @param dest the point to fly the cards to + * @param flightDuration the duration of the cards' flight + * @param cardDelay the amount of time to wait between cards + * @param fadePortion the amount of time to spend fading in and out + * as a proportion of the flight duration + */ + public void flyAcross (Card[] cards, Point src, Point dest, + long flightDuration, long cardDelay, float fadePortion) + { + for (int i = 0; i < cards.length; i++) { + // add on top of all board sprites + CardSprite cs = new CardSprite(this, cards[i]); + cs.setRenderOrder(getHighestBoardLayer() + 1 + i); + cs.setLocation(src.x, src.y); + addSprite(cs); + + // prepend an initial delay to all cards after the first + Path path; + long pathDuration; + LinePath flight = new LinePath(dest, flightDuration); + if (i > 0) { + long delayDuration = cardDelay * i; + LinePath delay = new LinePath(src, delayDuration); + path = new PathSequence(delay, flight); + pathDuration = delayDuration + flightDuration; + + } else { + path = flight; + pathDuration = flightDuration; + } + cs.addSpriteObserver(_pathEndRemover); + cs.moveAndFadeInAndOut(path, pathDuration, fadePortion); + } + } + + /** + * Flies a set of cards from the ether into the ether face-down. + * + * @param number the number of cards to fly across + * @param src the point to fly the cards from + * @param dest the point to fly the cards to + * @param flightDuration the duration of the cards' flight + * @param cardDelay the amount of time to wait between cards + * @param fadePortion the amount of time to spend fading in and out + * as a proportion of the flight duration + */ + public void flyAcross (int number, Point src, Point dest, + long flightDuration, long cardDelay, float fadePortion) + { + // use null values to signify unknown cards + flyAcross(new Card[number], src, dest, flightDuration, + cardDelay, fadePortion); + } + + /** + * Flies a card from the hand onto the board. Clears any cards selected. + * + * @param card the sprite to remove from the hand + * @param dest the point to fly the card to + * @param flightDuration the duration of the card's flight + */ + public void flyFromHandToBoard (CardSprite card, Point dest, + long flightDuration) + { + // fly it over + LinePath flight = new LinePath(dest, flightDuration); + card.move(flight); + + // lower the board so that the card from hand is on top + lowerBoardSprites(card.getRenderOrder() - 1); + + // move from one list to the other + removeFromHand(card); + _boardSprites.add(card); + + // adjust the hand to cover the hole + adjustHand(flightDuration, false); + } + + /** + * Flies a card from the ether onto the board. + * + * @param card the card to add to the board + * @param src the point to fly the card from + * @param dest the point to fly the card to + * @param flightDuration the duration of the card's flight + * @param fadePortion the amount of time to spend fading in as a + * proportion of the flight duration + */ + public void flyToBoard (Card card, Point src, Point dest, + long flightDuration, float fadePortion) + { + // add it on top of the existing cards + CardSprite cs = new CardSprite(this, card); + cs.setRenderOrder(getHighestBoardLayer() + 1); + cs.setLocation(src.x, src.y); + addSprite(cs); + _boardSprites.add(cs); + + // and fly it over + LinePath flight = new LinePath(dest, flightDuration); + cs.moveAndFadeIn(flight, flightDuration, fadePortion); + } + + /** + * Adds a card to the board immediately. + * + * @param card the card to add to the board + * @param dest the point at which to add the card + */ + public void addToBoard (Card card, Point dest) + { + CardSprite cs = new CardSprite(this, card); + cs.setRenderOrder(getHighestBoardLayer() + 1); + cs.setLocation(dest.x, dest.y); + addSprite(cs); + _boardSprites.add(cs); + } + + /** + * Flies a set of cards from the board into the ether. + * + * @param cards the cards to remove from the board + * @param dest the point to fly the cards to + * @param flightDuration the duration of the cards' flight + * @param fadePortion the amount of time to spend fading out as a + * proportion of the flight duration + */ + public void flyFromBoard (CardSprite[] cards, Point dest, + long flightDuration, float fadePortion) + { + for (int i = 0; i < cards.length; i++) { + LinePath flight = new LinePath(dest, flightDuration); + cards[i].addSpriteObserver(_pathEndRemover); + cards[i].moveAndFadeOut(flight, flightDuration, fadePortion); + _boardSprites.remove(cards[i]); + } + } + + // documentation inherited + protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect) + { + gfx.setColor(DEFAULT_BACKGROUND); + gfx.fill(dirtyRect); + super.paintBehind(gfx, dirtyRect); + } + + /** + * Flies a set of cards from the board into the ether through an + * intermediate point. + * + * @param cards the cards to remove from the board + * @param dest1 the first point to fly the cards to + * @param dest2 the final destination of the cards + * @param flightDuration the duration of the cards' flight + * @param fadePortion the amount of time to spend fading out as a + * proportion of the flight duration + */ + public void flyFromBoard (CardSprite[] cards, Point dest1, Point dest2, + long flightDuration, float fadePortion) + { + for (int i = 0; i < cards.length; i++) { + PathSequence flight = new PathSequence( + new LinePath(dest1, flightDuration/2), + new LinePath(dest1, dest2, flightDuration/2)); + cards[i].addSpriteObserver(_pathEndRemover); + cards[i].moveAndFadeOut(flight, flightDuration, fadePortion); + _boardSprites.remove(cards[i]); + } + } + + /** + * Returns the first card sprite in the specified list that represents + * the specified card, or null if there is no such sprite in the list. + */ + protected CardSprite getCardSprite (ArrayList list, Card card) + { + for (int i = 0; i < list.size(); i++) { + CardSprite cs = (CardSprite)list.get(i); + if (card.equals(cs.getCard())) { + return cs; + } + } + return null; + } + + /** + * Returns whether the user's hand should be sorted when displayed. By + * default, hands are sorted. + */ + protected boolean shouldSortHand () + { + return true; + } + + /** + * Expands or collapses the hand to accommodate new cards or cover the + * space left by removed cards. Skips unmanaged sprites. Clears out + * any selected cards. + * + * @param adjustDuration the amount of time to spend settling the cards + * into their new locations + * @param updateLayers whether or not to update the layers of the cards + */ + protected void adjustHand (long adjustDuration, boolean updateLayers) + { + // clear out selected cards + clearHandSelection(); + + // Sort the hand + if (shouldSortHand()) { + QuickSort.sort(_handSprites); + } + + // Move each card to its proper position (and, optionally, layer) + int size = _handSprites.size(); + for (int i = 0; i < size; i++) { + CardSprite cs = (CardSprite)_handSprites.get(i); + if (!isManaged(cs)) { + continue; + } + if (updateLayers) { + cs.setRenderOrder(i); + } + LinePath adjust = new LinePath(new Point(getHandX(size, i), + _handLocation.y), adjustDuration); + cs.move(adjust); + } + } + + /** + * Removes a card from the hand. + */ + protected void removeFromHand (CardSprite card) + { + _selectedHandSprites.remove(card); + _handSprites.remove(card); + } + + /** + * Updates the offsets of all the cards in the hand. If there is only + * one selectable card, that card will always be raised slightly. + */ + protected void updateHandOffsets () + { + // make active card sprite is up-to-date + updateActiveCardSprite(); + + int size = _handSprites.size(); + for (int i = 0; i < size; i++) { + CardSprite cs = (CardSprite)_handSprites.get(i); + if (!cs.isMoving()) { + cs.setLocation(cs.getX(), getHandY(cs)); + } + } + } + + /** + * Given the location and spacing of the hand, returns the x location of + * the card at the specified index within a hand of the specified size. + */ + protected int getHandX (int size, int idx) + { + // get the card width from the image if not yet known + if (_cardWidth == 0) { + _cardWidth = getCardBackImage().getWidth(); + } + // first compute the width of the entire hand, then use that to + // determine the centered location + int width = (size - 1) * _handSpacing + _cardWidth; + return (_handLocation.x - width/2) + idx * _handSpacing; + } + + /** + * Determines the y location of the specified card sprite, given its + * selection state. + */ + protected int getHandY (CardSprite sprite) + { + if (_selectedHandSprites.contains(sprite)) { + return _handLocation.y - _selectedCardOffset; + + } else if (isSelectable(sprite) && + (sprite == _activeCardSprite || isOnlySelectable(sprite))) { + return _handLocation.y - _selectableCardOffset; + + } else { + return _handLocation.y; + } + } + + /** + * Given the current selection mode and predicate, determines if the + * specified sprite is selectable. + */ + protected boolean isSelectable (CardSprite sprite) + { + return _handSelectionMode != NONE && + (_handSelectionPredicate == null || + _handSelectionPredicate.evaluate(sprite)); + } + + /** + * Determines whether the specified sprite is the only selectable sprite + * in the hand according to the selection predicate. + */ + protected boolean isOnlySelectable (CardSprite sprite) + { + // if there's no predicate, last remaining card is only selectable + if (_handSelectionPredicate == null) { + return _handSprites.size() == 1 && _handSprites.contains(sprite); + } + + // otherwise, look for a sprite that fits the predicate and isn't the + // parameter + int size = _handSprites.size(); + for (int i = 0; i < size; i++) { + CardSprite cs = (CardSprite)_handSprites.get(i); + if (cs != sprite && _handSelectionPredicate.evaluate(cs)) { + return false; + } + } + return true; + } + + /** + * Lowers all board sprites so that they are rendered at or below the + * specified layer. + */ + protected void lowerBoardSprites (int layer) + { + // see if they're already lower + int highest = getHighestBoardLayer(); + if (highest <= layer) { + return; + } + + // lower them just enough + int size = _boardSprites.size(), adjustment = layer - highest; + for (int i = 0; i < size; i++) { + CardSprite cs = (CardSprite)_boardSprites.get(i); + cs.setRenderOrder(cs.getRenderOrder() + adjustment); + } + } + + /** + * Returns the highest render order of any sprite on the board. + */ + protected int getHighestBoardLayer () + { + // must be at least zero, because that's the lowest number we can push + // the sprites down to (the layer of the first card in the hand) + int size = _boardSprites.size(), highest = 0; + for (int i = 0; i < size; i++) { + highest = Math.max(highest, + ((CardSprite)_boardSprites.get(i)).getRenderOrder()); + } + return highest; + } + + /** + * Clears an array of sprites from the specified list and from the panel. + */ + protected void clearSprites (ArrayList sprites) + { + for (Iterator it = sprites.iterator(); it.hasNext(); ) { + removeSprite((CardSprite)it.next()); + it.remove(); + } + } + + /** + * Updates the active card sprite based on the location of the mouse + * pointer. + */ + protected void updateActiveCardSprite () + { + // can't do anything if we don't know where the mouse pointer is + if (_mouseEvent == null) { + return; + } + + Sprite newHighestHit = _spritemgr.getHighestHitSprite( + _mouseEvent.getX(), _mouseEvent.getY()); + + CardSprite newActiveCardSprite = + (newHighestHit instanceof CardSprite ? + (CardSprite)newHighestHit : null); + + if (_activeCardSprite != newActiveCardSprite) { + if (_activeCardSprite != null && + isManaged(_activeCardSprite)) { + _activeCardSprite.queueNotification( + new CardSpriteExitedOp(_activeCardSprite, + _mouseEvent) + ); + } + _activeCardSprite = newActiveCardSprite; + if (_activeCardSprite != null) { + _activeCardSprite.queueNotification( + new CardSpriteEnteredOp(_activeCardSprite, + _mouseEvent) + ); + } + } + } + + /** The width of the playing cards. */ + protected int _cardWidth; + + /** The last motion/entrance/exit event received from the mouse. */ + protected MouseEvent _mouseEvent; + + /** The currently active card sprite (the one that the mouse is over). */ + protected CardSprite _activeCardSprite; + + /** The sprites for cards within the hand. */ + protected ArrayList _handSprites = new ArrayList(); + + /** The sprites for cards within the hand that have been selected. */ + protected ArrayList _selectedHandSprites = new ArrayList(); + + /** The current selection mode for the hand. */ + protected int _handSelectionMode; + + /** The predicate that determines which cards are selectable (if null, all + * cards are selectable). */ + protected CardSpritePredicate _handSelectionPredicate; + + /** Observers of hand card selection/deselection. */ + protected ObserverList _handSelectionObservers = new ObserverList( + ObserverList.FAST_UNSAFE_NOTIFY); + + /** The location of the center of the hand's upper edge. */ + protected Point _handLocation = new Point(); + + /** The horizontal distance between cards in the hand. */ + protected int _handSpacing; + + /** The vertical distance to offset cards that are selectable. */ + protected int _selectableCardOffset; + + /** The vertical distance to offset cards that are selected. */ + protected int _selectedCardOffset; + + /** The sprites for cards on the board. */ + protected ArrayList _boardSprites = new ArrayList(); + + /** The hand sprite observer instance. */ + protected HandSpriteObserver _handSpriteObserver = + new HandSpriteObserver(); + + /** A path observer that removes the sprite at the end of its path. */ + protected PathAdapter _pathEndRemover = new PathAdapter() { + public void pathCompleted (Sprite sprite, Path path, long when) { + removeSprite(sprite); + } + }; + + /** Listens for interactions with cards in hand. */ + protected class HandSpriteObserver extends PathAdapter + implements CardSpriteObserver + { + public void pathCompleted (Sprite sprite, Path path, long when) + { + updateActiveCardSprite(); + maybeUpdateOffset((CardSprite)sprite); + } + + public void cardSpriteClicked (CardSprite sprite, MouseEvent me) + { + // select, deselect, or play card in hand + if (_selectedHandSprites.contains(sprite) && + _handSelectionMode != NONE) { + deselectHandSprite(sprite); + + } else if (_handSprites.contains(sprite) && isSelectable(sprite)) { + selectHandSprite(sprite); + } + } + + public void cardSpriteEntered (CardSprite sprite, MouseEvent me) + { + maybeUpdateOffset(sprite); + } + + public void cardSpriteExited (CardSprite sprite, MouseEvent me) + { + maybeUpdateOffset(sprite); + } + + public void cardSpriteDragged (CardSprite sprite, MouseEvent me) + {} + + protected void maybeUpdateOffset (CardSprite sprite) + { + // update the offset if it's in the hand and isn't moving + if (_handSprites.contains(sprite) && !sprite.isMoving()) { + sprite.setLocation(sprite.getX(), getHandY(sprite)); + } + } + }; + + /** Listens for mouse interactions with cards. */ + protected class CardListener extends MouseInputAdapter + { + public void mousePressed (MouseEvent me) + { + if (_activeCardSprite != null && + isManaged(_activeCardSprite)) { + _handleX = _activeCardSprite.getX() - me.getX(); + _handleY = _activeCardSprite.getY() - me.getY(); + _hasBeenDragged = false; + } + } + + public void mouseReleased (MouseEvent me) + { + if (_activeCardSprite != null && + isManaged(_activeCardSprite) && + _hasBeenDragged) { + _activeCardSprite.queueNotification( + new CardSpriteDraggedOp(_activeCardSprite, me) + ); + } + } + + public void mouseClicked (MouseEvent me) + { + if (_activeCardSprite != null && + isManaged(_activeCardSprite)) { + _activeCardSprite.queueNotification( + new CardSpriteClickedOp(_activeCardSprite, me) + ); + } + } + + public void mouseMoved (MouseEvent me) + { + _mouseEvent = me; + + updateActiveCardSprite(); + } + + public void mouseDragged (MouseEvent me) + { + _mouseEvent = me; + + if (_activeCardSprite != null && + isManaged(_activeCardSprite) && + _activeCardSprite.isDraggable()) { + _activeCardSprite.setLocation( + me.getX() + _handleX, + me.getY() + _handleY + ); + _hasBeenDragged = true; + + } else { + updateActiveCardSprite(); + } + } + + public void mouseEntered (MouseEvent me) + { + _mouseEvent = me; + } + + public void mouseExited (MouseEvent me) + { + _mouseEvent = me; + } + + protected int _handleX, _handleY; + protected boolean _hasBeenDragged; + } + + /** Calls CardSpriteObserver.cardSpriteClicked. */ + protected static class CardSpriteClickedOp implements + ObserverList.ObserverOp + { + public CardSpriteClickedOp (CardSprite sprite, MouseEvent me) + { + _sprite = sprite; + _me = me; + } + + public boolean apply (Object observer) + { + if (observer instanceof CardSpriteObserver) { + ((CardSpriteObserver)observer).cardSpriteClicked(_sprite, + _me); + } + return true; + } + + protected CardSprite _sprite; + protected MouseEvent _me; + } + + /** Calls CardSpriteObserver.cardSpriteEntered. */ + protected static class CardSpriteEnteredOp implements + ObserverList.ObserverOp + { + public CardSpriteEnteredOp (CardSprite sprite, MouseEvent me) + { + _sprite = sprite; + _me = me; + } + + public boolean apply (Object observer) + { + if (observer instanceof CardSpriteObserver) { + ((CardSpriteObserver)observer).cardSpriteEntered(_sprite, + _me); + } + return true; + } + + protected CardSprite _sprite; + protected MouseEvent _me; + } + + /** Calls CardSpriteObserver.cardSpriteExited. */ + protected static class CardSpriteExitedOp implements + ObserverList.ObserverOp + { + public CardSpriteExitedOp (CardSprite sprite, MouseEvent me) + { + _sprite = sprite; + _me = me; + } + + public boolean apply (Object observer) + { + if (observer instanceof CardSpriteObserver) { + ((CardSpriteObserver)observer).cardSpriteExited(_sprite, _me); + } + return true; + } + + protected CardSprite _sprite; + protected MouseEvent _me; + } + + /** Calls CardSpriteObserver.cardSpriteDragged. */ + protected static class CardSpriteDraggedOp implements + ObserverList.ObserverOp + { + public CardSpriteDraggedOp (CardSprite sprite, MouseEvent me) + { + _sprite = sprite; + _me = me; + } + + public boolean apply (Object observer) + { + if (observer instanceof CardSpriteObserver) { + ((CardSpriteObserver)observer).cardSpriteDragged(_sprite, + _me); + } + return true; + } + + protected CardSprite _sprite; + protected MouseEvent _me; + } + + /** A nice default green card table background color. */ + protected static Color DEFAULT_BACKGROUND = new Color(0x326D36); +} diff --git a/src/java/com/threerings/parlor/card/client/CardSprite.java b/src/java/com/threerings/parlor/card/client/CardSprite.java new file mode 100644 index 00000000..2e9e81fa --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/CardSprite.java @@ -0,0 +1,252 @@ +// +// $Id: CardSprite.java 3819 2006-01-24 19:46:34Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import java.awt.Graphics2D; +import java.awt.geom.AffineTransform; + +import com.threerings.media.image.Mirage; +import com.threerings.media.sprite.FadableImageSprite; +import com.threerings.media.util.Path; + +import com.threerings.parlor.card.data.Card; + +/** + * A sprite representing a playing card. + */ +public class CardSprite extends FadableImageSprite + implements Comparable +{ + /** + * Creates a new upward-facing card sprite. + * + * @param panel the panel responsible for the sprite + * @param card the card to depict (can be null, in which case the + * card back will be shown) + */ + public CardSprite (CardPanel panel, Card card) + { + _panel = panel; + _card = card; + _facingUp = true; + + updateMirage(); + } + + /** + * Creates a new card sprite. + * + * @param panel the panel responsible for the sprite + * @param card the card to depict + * @param facingUp whether or not the card should be facing up + */ + public CardSprite (CardPanel panel, Card card, boolean facingUp) + { + _panel = panel; + _card = card; + _facingUp = facingUp; + + updateMirage(); + } + + /** + * Sets the card to depict. + * + * @param card the new card + */ + public void setCard (Card card) + { + _card = card; + + updateMirage(); + } + + /** + * Returns the card being depicted. + * + * @return the current card + */ + public Card getCard () + { + return _card; + } + + /** + * Turns this card up or down. + * + * @param facingUp whether or not the card should be facing up + */ + public void setFacingUp (boolean facingUp) + { + _facingUp = facingUp; + + updateMirage(); + } + + /** + * Checks whether this card is facing up or down. + * + * @return true if the card is facing up, false if facing down + */ + public boolean isFacingUp () + { + return _facingUp; + } + + /** + * Sets whether or not the user can drag this card around the board. + * + * @param draggable whether or not the user can drag the card + */ + public void setDraggable (boolean draggable) + { + _draggable = draggable; + } + + /** + * Checks whether or not the user can drag this card. + * + * @return true if the user can drag the card, false if not + */ + public boolean isDraggable () + { + return _draggable; + } + + /** + * Flip the card from its current displayed card to the specified card. + */ + public void flip (Card newCard, long duration) + { + _flipStamp = 0; + _flipDuration = duration; + _flipCard = newCard; + + _scaleFactor = 1.0; + } + + // Documentation inherited. + public void tick (long tickStamp) + { + super.tick(tickStamp); + + // Take care of any flipping we might be doing. + if (_flipDuration != -1) { + if (_flipStamp == 0) { + _flipStamp = tickStamp; + } + + long diff = tickStamp - _flipStamp; + + // Set the new scale while we're flipping + if (diff < _flipDuration/2) { + _scaleFactor = 1.0 - ((float)diff*2)/_flipDuration; + } else { + // Switch the image to the card we're flipping to. + if (_flipCard != null) { + setCard(_flipCard); + _flipCard = null; + } + _scaleFactor = ((float)diff*2)/_flipDuration - 1.0; + } + + // If we're done, stop flipping. + if (_scaleFactor > 1.0) { + _scaleFactor = 1.0; + _flipDuration = -1; + } + + // Make sure we flag our location as needing redrawing + if (_mgr != null) { + _mgr.getRegionManager().invalidateRegion(_bounds); + } + } + + } + + // Documentation inherited. + public void paint (Graphics2D gfx) + { + if (_scaleFactor <= 0) { + return; + } + // If we are flipping the card, scale it horizontally. + AffineTransform otrans = gfx.getTransform(); + if (_scaleFactor < 1.0) { + int xtrans = getX() + getWidth()/2; + gfx.translate(xtrans, 0); + gfx.scale(_scaleFactor, 1.0); + gfx.translate(-xtrans, 0); + } + + super.paint(gfx); + + gfx.setTransform(otrans); + } + + /** + * Compares this to another card sprite based on their cards. + */ + public int compareTo (Object other) + { + CardSprite cs = (CardSprite)other; + if (_card == null || cs._card == null) { + return 0; + + } else { + return _card.compareTo(cs._card); + } + } + + /** + * Updates the mirage according to the current state. + */ + protected void updateMirage () + { + setMirage((_card != null && _facingUp ) ? + _panel.getCardImage(_card) : _panel.getCardBackImage()); + } + + /** The panel responsible for the sprite. */ + protected CardPanel _panel; + + /** The depicted card. */ + protected Card _card; + + /** Whether or not the card is facing up. */ + protected boolean _facingUp; + + /** Whether or not the user can drag the card around the board. */ + protected boolean _draggable; + + /** The horizontal scale factor used while flipping the card. */ + protected double _scaleFactor = 1.0; + + /** If flipping, how long the current flip should take (otherwise -1). */ + protected long _flipDuration; + + /** The timestamp for when we started flipping the card. */ + protected long _flipStamp; + + /** The card which will be revealed when we're done flipping. */ + protected Card _flipCard; +} diff --git a/src/java/com/threerings/parlor/card/client/CardSpriteObserver.java b/src/java/com/threerings/parlor/card/client/CardSpriteObserver.java new file mode 100644 index 00000000..f9f855f8 --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/CardSpriteObserver.java @@ -0,0 +1,65 @@ +// +// $Id: CardSpriteObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import java.awt.event.MouseEvent; + +/** + * Observer interface for (draggable) card sprites. + */ +public interface CardSpriteObserver +{ + /** + * Notifies the observer that the user clicked a card sprite. + * + * @param sprite the dragged sprite + * @param me the mouse event associated with the drag + */ + public void cardSpriteClicked (CardSprite sprite, MouseEvent me); + + /** + * Notifies the observer that the user moved the mouse pointer onto + * a card sprite. + * + * @param sprite the entered sprite + * @param me the mouse event associated with the entrance + */ + public void cardSpriteEntered (CardSprite sprite, MouseEvent me); + + /** + * Notifies the observer that the user moved the mouse pointer off of + * a card sprite. + * + * @param sprite the exited the sprite + * @param me the mouse event associated with the exit + */ + public void cardSpriteExited (CardSprite sprite, MouseEvent me); + + /** + * Notifies the observer that the user dragged a card sprite to a new + * location. + * + * @param sprite the dragged sprite + * @param me the mouse event associated with the drag + */ + public void cardSpriteDragged (CardSprite sprite, MouseEvent me); +} diff --git a/src/java/com/threerings/parlor/card/client/MicroCardSprite.java b/src/java/com/threerings/parlor/card/client/MicroCardSprite.java new file mode 100644 index 00000000..31efba05 --- /dev/null +++ b/src/java/com/threerings/parlor/card/client/MicroCardSprite.java @@ -0,0 +1,59 @@ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.client; + +import com.threerings.parlor.card.data.Card; + +public class MicroCardSprite extends CardSprite +{ + /** + * Creates a new upward-facing micro-card sprite. + * + * @param panel the panel responsible for the sprite + * @param card the card to depict (can be null, in which case the + * card back will be shown) + */ + public MicroCardSprite (CardPanel panel, Card card) + { + super(panel, card); + } + + /** + * Creates a new micro-card sprite. + * + * @param panel the panel responsible for the sprite + * @param card the card to depict + * @param facingUp whether or not the card should be facing up + */ + public MicroCardSprite (CardPanel panel, Card card, boolean facingUp) + { + super(panel, card, facingUp); + } + + /** + * Updates the mirage according to the current state. + */ + protected void updateMirage () + { + setMirage((_card != null && _facingUp ) ? + _panel.getMicroCardImage(_card) : _panel.getMicroCardBackImage()); + } + +} diff --git a/src/java/com/threerings/parlor/card/data/Card.java b/src/java/com/threerings/parlor/card/data/Card.java new file mode 100644 index 00000000..159f5a89 --- /dev/null +++ b/src/java/com/threerings/parlor/card/data/Card.java @@ -0,0 +1,244 @@ +// +// $Id: Card.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.data; + +import java.io.IOException; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; + +import com.threerings.presents.dobj.DSet; + +/** + * Instances of this class represent individual playing cards. + */ +public class Card implements DSet.Entry, Comparable, CardCodes +{ + /** + * No-arg constructor for deserialization. + */ + public Card () + {} + + /** + * Creates a new card. + * + * @param number the number of the card + * @param suit the suit of the card + */ + public Card (int number, int suit) + { + _value = (byte)((suit << 5) | number); + } + + /** + * Returns the value of the card, either from 2 to 11 or + * KING, QUEEN, JACK, ACE, RED_JOKER, or BLACK_JOKER. + * + * @return the value of the card + */ + public int getNumber () + { + return (_value & 0x1F); + } + + /** + * Returns the suit of the card: SPADES, HEARTS, DIAMONDS, or + * CLUBS. If the card is the joker, the suit is undefined. + * + * @return the suit of the card + */ + public int getSuit () + { + return (_value >> 5); + } + + /** + * Checks whether the card is a number card (2 to 10). + * + * @return true if the card is a number card, false otherwise + */ + public boolean isNumber () + { + int number = getNumber(); + + return number >= 2 && number <= 10; + } + + /** + * Checks whether the card is a face card (KING, QUEEN, or JACK). + * + * @return true if the card is a face card, false otherwise + */ + public boolean isFace () + { + int number = getNumber(); + + return number == KING || number == QUEEN || number == JACK; + } + + /** + * Checks whether the card is an ace. + * + * @return true if the card is an ace, false otherwise + */ + public boolean isAce () + { + return getNumber() == ACE; + } + + /** + * Checks whether the card is a joker. + * + * @return true if the card is a joker, false otherwise + */ + public boolean isJoker () + { + int number = getNumber(); + + return number == RED_JOKER || number == BLACK_JOKER; + } + + /** + * Checks whether or not this card is valid. The no-arg public + * constructor for deserialization creates an invalid card. + * + * @return true if this card is valid, false if not + */ + public boolean isValid () + { + int number = getNumber(), suit = getSuit(); + + return number == RED_JOKER || number == BLACK_JOKER || + (number >= 2 && number <= ACE && + suit >= SPADES && suit <= DIAMONDS); + } + + // Documentation inherited. + public Comparable getKey () + { + if (_key == null) { + _key = Byte.valueOf(_value); + } + + return _key; + } + + /** + * Returns a hash code for this card. + * + * @return this card's hash code + */ + public int hashCode () + { + return _value; + } + + /** + * Checks this card for equality with another. + * + * @param other the other card to compare + * @return true if the cards are equal, false otherwise + */ + public boolean equals (Object other) + { + if (other instanceof Card) { + return _value == ((Card)other)._value; + } + else { + return false; + } + } + + /** + * Compares this card to another. The card order is the same as the + * initial deck ordering: two through ten, jack, queen, king, ace for + * spades, hearts, clubs, and diamonds, then the red joker and the + * black joker. + * + * @param other the other card to compare this to + * @return -1, 0, or +1, depending on whether this card is less than, + * equal to, or greater than the other card + */ + public int compareTo (Object other) + { + int otherValue = ((Card)other)._value; + + if (_value > otherValue) { + return +1; + } else if(_value < otherValue) { + return -1; + } else { + return 0; + } + } + + /** + * Returns a string representation of this card. + * + * @return a description of this card + */ + public String toString () + { + int number = getNumber(); + + if (number == RED_JOKER) { + return "RJ"; + } + else if (number == BLACK_JOKER) { + return "BJ"; + } + else { + StringBuilder sb = new StringBuilder(); + + if (number >= 2 && number <= 9) { + sb.append(Integer.toString(number)); + } + else { + switch (number) { + case 10: sb.append('T'); break; + case JACK: sb.append('J'); break; + case QUEEN: sb.append('Q'); break; + case KING: sb.append('K'); break; + case ACE: sb.append('A'); break; + default: sb.append('?'); break; + } + } + + switch (getSuit()) { + case SPADES: sb.append('s'); break; + case HEARTS: sb.append('h'); break; + case CLUBS: sb.append('c'); break; + case DIAMONDS: sb.append('d'); break; + default: sb.append('?'); break; + } + + return sb.toString(); + } + } + + /** The number of the card. */ + protected byte _value; + + /** The comparison key. */ + protected transient Byte _key; +} diff --git a/src/java/com/threerings/parlor/card/data/CardCodes.java b/src/java/com/threerings/parlor/card/data/CardCodes.java new file mode 100644 index 00000000..c19ac5fc --- /dev/null +++ b/src/java/com/threerings/parlor/card/data/CardCodes.java @@ -0,0 +1,60 @@ +// +// $Id: CardCodes.java 3224 2004-11-19 19:04:56Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.data; + +import com.threerings.presents.data.InvocationCodes; + +/** + * Constants relating to the card services. + */ +public interface CardCodes extends InvocationCodes +{ + /** The suit of spades. */ + public static final int SPADES = 0; + + /** The suit of hearts. */ + public static final int HEARTS = 1; + + /** The suit of clubs. */ + public static final int CLUBS = 2; + + /** The suit of diamonds. */ + public static final int DIAMONDS = 3; + + /** The number of the jack. */ + public static final int JACK = 11; + + /** The number of the queen. */ + public static final int QUEEN = 12; + + /** The number of the king. */ + public static final int KING = 13; + + /** The number of the ace. */ + public static final int ACE = 14; + + /** The number of the red joker. */ + public static final int RED_JOKER = 15; + + /** The number of the black joker. */ + public static final int BLACK_JOKER = 16; +} diff --git a/src/java/com/threerings/parlor/card/data/CardGameObject.java b/src/java/com/threerings/parlor/card/data/CardGameObject.java new file mode 100644 index 00000000..14cd594f --- /dev/null +++ b/src/java/com/threerings/parlor/card/data/CardGameObject.java @@ -0,0 +1,31 @@ +// +// $Id: TurnGameObject.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.data; + +import com.threerings.parlor.game.data.GameObject; + +/** + * Game object class for card games. + */ +public class CardGameObject extends GameObject +{ +} diff --git a/src/java/com/threerings/parlor/card/data/Deck.java b/src/java/com/threerings/parlor/card/data/Deck.java new file mode 100644 index 00000000..4e8dd800 --- /dev/null +++ b/src/java/com/threerings/parlor/card/data/Deck.java @@ -0,0 +1,121 @@ +// +// $Id: Deck.java 3713 2005-09-27 21:58:10Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.data; + +import java.util.Collections; +import java.util.List; + +import com.threerings.util.StreamableArrayList; + +/** + * Instances of this class represent decks of cards. + */ +public class Deck extends StreamableArrayList + implements CardCodes +{ + /** + * Default constructor creates an unshuffled deck of cards without + * jokers. + */ + public Deck () + { + reset(false); + } + + /** + * Constructor. + * + * @param includeJokers whether or not to include the two jokers + * in the deck + */ + public Deck (boolean includeJokers) + { + reset(includeJokers); + } + + /** + * Resets the deck to its initial state: an unshuffled deck of + * 52 or 54 cards, depending on whether the jokers are included. + * + * @param includeJokers whether or not to include the two jokers + * in the deck + */ + public void reset (boolean includeJokers) + { + clear(); + + for (int i = SPADES; i <= DIAMONDS; i++) { + for (int j = 2; j <= ACE; j++) { + add(new Card(j, i)); + } + } + + if (includeJokers) { + add(new Card(RED_JOKER, 3)); + add(new Card(BLACK_JOKER, 3)); + } + } + + /** + * Shuffles the deck. + */ + public void shuffle () + { + Collections.shuffle(this); + } + + /** + * Deals a hand of cards from the deck. + * + * @param size the size of the hand to deal + * @return the newly created and populated hand, or null + * if there are not enough cards in the deck to deal the hand + */ + public Hand dealHand (int size) + { + int dsize = size(); + if (dsize < size) { + return null; + + } else { + Hand hand = new Hand(); + + // use a sublist view to manipulate the top of the deck + List sublist = subList(dsize - size, dsize); + hand.addAll(sublist); + sublist.clear(); + + return hand; + } + } + + /** + * Returns a hand of cards to the deck. + * + * @param hand the hand of cards to return + */ + public void returnHand (Hand hand) + { + addAll(hand); + hand.clear(); + } +} diff --git a/src/java/com/threerings/parlor/card/data/Hand.java b/src/java/com/threerings/parlor/card/data/Hand.java new file mode 100644 index 00000000..305571ca --- /dev/null +++ b/src/java/com/threerings/parlor/card/data/Hand.java @@ -0,0 +1,90 @@ +// +// $Id: Hand.java 3813 2006-01-19 21:50:53Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.data; + +import com.threerings.util.StreamableArrayList; + +/** + * Instances of this class represent hands of cards. + */ +public class Hand extends StreamableArrayList +{ + /** + * Adds all of the specified cards to this hand. + */ + public void addAll (Card[] cards) + { + for (int i = 0; i < cards.length; i++) { + add(cards[i]); + } + } + + /** + * Removes all of the specified cards from this hand. + */ + public void removeAll (Card[] cards) + { + for (int i = 0; i < cards.length; i++) { + remove(cards[i]); + } + } + + /** + * Checks whether this hand contains all of the specified cards. + */ + public boolean containsAll (Card[] cards) + { + for (int i = 0; i < cards.length; i++) { + if (!contains(cards[i])) { + return false; + } + } + return true; + } + + /** + * Counts the members of a particular suit within this hand. + * + * @param suit the suit of interest + * @return the number of cards in the specified suit + */ + public int getSuitMemberCount (int suit) + { + int len = size(), members = 0; + for (int i = 0; i < len; i++) { + if (((Card)get(i)).getSuit() == suit) { + members++; + } + } + return members; + } + + /** + * Get an array of the cards in this hand. + */ + public Card[] getCards () + { + Card[] cards = new Card[size()]; + toArray(cards); + return cards; + } +} diff --git a/src/java/com/threerings/parlor/card/data/PlayerCard.java b/src/java/com/threerings/parlor/card/data/PlayerCard.java new file mode 100644 index 00000000..75abebed --- /dev/null +++ b/src/java/com/threerings/parlor/card/data/PlayerCard.java @@ -0,0 +1,54 @@ +// +// $Id: TrickCardGameObject.java 3382 2005-03-03 19:55:35Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.data; + +import com.threerings.io.Streamable; + +/** + * Pairs a player index with the card that the player played in the trick. + */ +public class PlayerCard implements Streamable +{ + /** The index of the player. */ + public int pidx; + + /** The card that the player played. */ + public Card card; + + /** + * No-argument constructor for deserialization. + */ + public PlayerCard () + {} + + /** + * Creates a new player card. + * + * @param pidx the index of the player + * @param card the card played + */ + public PlayerCard (int pidx, Card card) + { + this.pidx = pidx; + this.card = card; + } +} diff --git a/src/java/com/threerings/parlor/card/server/CardGameManager.java b/src/java/com/threerings/parlor/card/server/CardGameManager.java new file mode 100644 index 00000000..d2f0c261 --- /dev/null +++ b/src/java/com/threerings/parlor/card/server/CardGameManager.java @@ -0,0 +1,268 @@ +// +// $Id: CardGameManager.java 3829 2006-02-03 19:11:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.server; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.OccupantInfo; +import com.threerings.crowd.server.OccupantOp; + +import com.threerings.parlor.card.Log; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.CardCodes; +import com.threerings.parlor.card.data.CardGameObject; +import com.threerings.parlor.card.data.Deck; +import com.threerings.parlor.card.data.Hand; +import com.threerings.parlor.game.server.GameManager; +import com.threerings.parlor.turn.server.TurnGameManager; + +import com.threerings.presents.client.InvocationService.ConfirmListener; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.dobj.MessageEvent; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.PresentsServer; + +/** + * A manager class for card games. Handles common functions like dealing + * hands of cards to all players. + */ +public class CardGameManager extends GameManager + implements TurnGameManager, CardCodes +{ + // Documentation inherited. + protected void didStartup () + { + super.didStartup(); + _cardgameobj = (CardGameObject)_gameobj; + } + + // Documentation inherited. + public void turnWillStart () + {} + + // Documentation inherited. + public void turnDidStart () + {} + + // Documentation inherited. + public void turnDidEnd () + {} + + /** + * This should be called to start a rematched game. It just starts the + * current game anew, but provides a mechanism for derived classes to + * do special things when there is a rematch. + */ + public void rematchGame () + { + if (gameWillRematch()) { + startGame(); + } + } + + /** + * Derived classes can override this method and take any action needed + * prior to a game rematch. If the rematch needs to be vetoed for any + * reason, they can return false from this method and the rematch will + * be aborted. + */ + protected boolean gameWillRematch () + { + return true; + } + + /** + * Deals a hand of cards to the player at the specified index from + * the given Deck. + * + * @param deck the deck from which to deal + * @param size the size of the hand to deal + * @param playerIndex the index of the target player + * @return the hand dealt to the player, or null if the deal + * was canceled because the deck did not contain enough cards + */ + public Hand dealHand (Deck deck, int size, int playerIndex) + { + if (deck.size() < size) { + return null; + + } else { + Hand hand = deck.dealHand(size); + if (!isAI(playerIndex)) { + ClientObject clobj = (ClientObject) + PresentsServer.omgr.getObject(_playerOids[playerIndex]); + if (clobj != null) { + CardGameSender.sendHand(clobj, _cardgameobj.getOid(), hand); + } + } + return hand; + } + } + + /** + * Deals a hand of cards to each player from the specified + * Deck. + * + * @param deck the deck from which to deal + * @param size the size of the hands to deal + * @return the array of hands dealt to each player, or null if + * the deal was canceled because the deck did not contain enough + * cards + */ + public Hand[] dealHands (Deck deck, int size) + { + if (deck.size() < size * _playerCount) { + return null; + + } else { + Hand[] hands = new Hand[_playerCount]; + + for (int i=0;i<_playerCount;i++) { + hands[i] = dealHand(deck, size, i); + } + + return hands; + } + } + + /** + * Gets the player index of the specified client object, or -1 + * if the client object does not represent a player. + */ + public int getPlayerIndex (ClientObject client) + { + int oid = client.getOid(); + for (int i=0;i<_playerOids.length;i++) { + if (_playerOids[i] == oid) { + return i; + } + } + return -1; + } + + /** + * Returns the client object corresponding to the specified player index, + * or null if the position is not occupied by a player. + */ + public ClientObject getClientObject (int pidx) + { + if (_playerOids[pidx] != 0) { + return (ClientObject)PresentsServer.omgr.getObject( + _playerOids[pidx]); + + } else { + return null; + } + } + + /** + * Sends a set of cards from one player to another. + * + * @param fromPlayerIdx the index of the player sending the cards + * @param toPlayerIdx the index of the player receiving the cards + * @param cards the cards to be exchanged + */ + public void transferCardsBetweenPlayers (int fromPlayerIdx, + int toPlayerIdx, Card[] cards) + { + // Notify the sender that the cards have been taken + ClientObject fromClient = getClientObject(fromPlayerIdx); + if (fromClient != null) { + CardGameSender.sentCardsToPlayer(fromClient, toPlayerIdx, cards); + } + + // Notify the receiver with the cards + ClientObject toClient = getClientObject(toPlayerIdx); + if (toClient != null) { + CardGameSender.sendCardsFromPlayer(toClient, fromPlayerIdx, + cards); + } + + // and everybody else in the room other than the sender and the + // receiver with the number of cards sent + notifyCardsTransferred(fromPlayerIdx, toPlayerIdx, cards.length); + } + + /** + * Sends sets of cards between players simultaneously. Each player is + * guaranteed to receive the notification of cards received after the + * notification of cards sent. The length of the arrays passed must + * be equal to the player count. + * + * @param toPlayerIndices for each player, the index of the player to + * transfer cards to + * @param cards for each player, the cards to transfer + */ + public void transferCardsBetweenPlayers (int[] toPlayerIndices, + Card[][] cards) + { + // Send all removal notices + for (int i = 0; i < _playerCount; i++) { + ClientObject fromClient = getClientObject(i); + if (fromClient != null) { + CardGameSender.sentCardsToPlayer(fromClient, + toPlayerIndices[i], cards[i]); + } + } + + // Send all addition notices and notify everyone else + for (int i = 0; i < _playerCount; i++) { + ClientObject toClient = getClientObject(toPlayerIndices[i]); + if (toClient != null) { + CardGameSender.sendCardsFromPlayer(toClient, i, cards[i]); + } + notifyCardsTransferred(i, toPlayerIndices[i], cards[i].length); + } + } + + /** + * Notifies everyone in the room (other than the sender and the receiver) + * that a set of cards have been transferred. + * + * @param fromPlayerIdx the index of the player sending the cards + * @param toPlayerIdx the index of the player receiving the cards + * @param cards the number of cards sent + */ + protected void notifyCardsTransferred (final int fromPlayerIdx, + final int toPlayerIdx, final int cards) + { + final int senderOid = _playerOids[fromPlayerIdx], + receiverOid = _playerOids[toPlayerIdx]; + OccupantOp op = new OccupantOp() { + public void apply (OccupantInfo info) { + int oid = info.getBodyOid(); + if (oid != senderOid && oid != receiverOid) { + ClientObject client = + (ClientObject)PresentsServer.omgr.getObject(oid); + if (client != null) { + CardGameSender.cardsTransferredBetweenPlayers(client, + fromPlayerIdx, toPlayerIdx, cards); + } + } + } + }; + applyToOccupants(op); + } + + /** The card game object. */ + protected CardGameObject _cardgameobj; +} diff --git a/src/java/com/threerings/parlor/card/server/CardGameSender.java b/src/java/com/threerings/parlor/card/server/CardGameSender.java new file mode 100644 index 00000000..6ad718b6 --- /dev/null +++ b/src/java/com/threerings/parlor/card/server/CardGameSender.java @@ -0,0 +1,85 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.server; + +import com.threerings.parlor.card.client.CardGameDecoder; +import com.threerings.parlor.card.client.CardGameReceiver; +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.Hand; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationSender; + +/** + * Used to issue notifications to a {@link CardGameReceiver} instance on a + * client. + */ +public class CardGameSender extends InvocationSender +{ + /** + * Issues a notification that will result in a call to {@link + * CardGameReceiver#cardsTransferredBetweenPlayers} on a client. + */ + public static void cardsTransferredBetweenPlayers ( + ClientObject target, int arg1, int arg2, int arg3) + { + sendNotification( + target, CardGameDecoder.RECEIVER_CODE, CardGameDecoder.CARDS_TRANSFERRED_BETWEEN_PLAYERS, + new Object[] { Integer.valueOf(arg1), Integer.valueOf(arg2), Integer.valueOf(arg3) }); + } + + /** + * Issues a notification that will result in a call to {@link + * CardGameReceiver#receivedCardsFromPlayer} on a client. + */ + public static void sendCardsFromPlayer ( + ClientObject target, int arg1, Card[] arg2) + { + sendNotification( + target, CardGameDecoder.RECEIVER_CODE, CardGameDecoder.RECEIVED_CARDS_FROM_PLAYER, + new Object[] { Integer.valueOf(arg1), arg2 }); + } + + /** + * Issues a notification that will result in a call to {@link + * CardGameReceiver#receivedHand} on a client. + */ + public static void sendHand ( + ClientObject target, int arg1, Hand arg2) + { + sendNotification( + target, CardGameDecoder.RECEIVER_CODE, CardGameDecoder.RECEIVED_HAND, + new Object[] { Integer.valueOf(arg1), arg2 }); + } + + /** + * Issues a notification that will result in a call to {@link + * CardGameReceiver#sentCardsToPlayer} on a client. + */ + public static void sentCardsToPlayer ( + ClientObject target, int arg1, Card[] arg2) + { + sendNotification( + target, CardGameDecoder.RECEIVER_CODE, CardGameDecoder.SENT_CARDS_TO_PLAYER, + new Object[] { Integer.valueOf(arg1), arg2 }); + } + +} diff --git a/src/java/com/threerings/parlor/card/trick/client/TrickCardGameControllerDelegate.java b/src/java/com/threerings/parlor/card/trick/client/TrickCardGameControllerDelegate.java new file mode 100644 index 00000000..b948460c --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/client/TrickCardGameControllerDelegate.java @@ -0,0 +1,49 @@ +// +// $Id: TrickCardGameControllerDelegate.java 3465 2005-04-12 02:50:17Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.client; + +import com.threerings.parlor.turn.client.TurnGameControllerDelegate; + +import com.threerings.parlor.card.client.CardGameController; + +/** + * A card game controller delegate for trick-based card games, such as + * Spades and Hearts. + */ +public class TrickCardGameControllerDelegate + extends TurnGameControllerDelegate +{ + /** + * Constructor. + * + * @param controller the game controller + */ + public TrickCardGameControllerDelegate (CardGameController + controller) + { + super(controller); + _cgctrl = controller; + } + + /** The card game controller. */ + protected CardGameController _cgctrl; +} diff --git a/src/java/com/threerings/parlor/card/trick/client/TrickCardGameService.java b/src/java/com/threerings/parlor/card/trick/client/TrickCardGameService.java new file mode 100644 index 00000000..1cc9a758 --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/client/TrickCardGameService.java @@ -0,0 +1,41 @@ +// +// $Id: RoisterService.java 17829 2004-11-12 20:24:43Z mdb $ + +package com.threerings.parlor.card.trick.client; + +import com.threerings.parlor.card.data.Card; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +/** + * Service calls related to trick card games. + */ +public interface TrickCardGameService extends InvocationService +{ + /** + * Sends a group of cards to the player at the specified index. + * + * @param client the client object + * @param toidx the index of the player to send the cards to + * @param cards the cards to send + */ + public void sendCardsToPlayer (Client client, int toidx, Card[] cards); + + /** + * Plays a card in the trick. + * + * @param client the client object + * @param card the card to play + * @param handSize the size of the player's hand, which is used to verify + * that the request is for the current trick + */ + public void playCard (Client client, Card card, int handSize); + + /** + * A request for a rematch. + * + * @param client the client object + */ + public void requestRematch (Client client); +} diff --git a/src/java/com/threerings/parlor/card/trick/data/TrickCardCodes.java b/src/java/com/threerings/parlor/card/trick/data/TrickCardCodes.java new file mode 100644 index 00000000..66535ecf --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/data/TrickCardCodes.java @@ -0,0 +1,42 @@ +// +// $Id: CardCodes.java 3224 2004-11-19 19:04:56Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.data; + +import com.threerings.parlor.card.data.CardCodes; + +/** + * Constants relating to trick-based card games. + */ +public interface TrickCardCodes extends CardCodes +{ + /** For four-player games, the bottom (own) player. */ + public static final int BOTTOM = 0; + + /** For four-player games, the player on the left. */ + public static final int LEFT = 1; + + /** For four-player games, the top (opposite) player. */ + public static final int TOP = 2; + + /** For four-player games, the player on the right. */ + public static final int RIGHT = 3; +} diff --git a/src/java/com/threerings/parlor/card/trick/data/TrickCardGameMarshaller.java b/src/java/com/threerings/parlor/card/trick/data/TrickCardGameMarshaller.java new file mode 100644 index 00000000..d03a8fed --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/data/TrickCardGameMarshaller.java @@ -0,0 +1,73 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.data; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.trick.client.TrickCardGameService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; + +/** + * Provides the implementation of the {@link TrickCardGameService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class TrickCardGameMarshaller extends InvocationMarshaller + implements TrickCardGameService +{ + /** The method id used to dispatch {@link #playCard} requests. */ + public static final int PLAY_CARD = 1; + + // documentation inherited from interface + public void playCard (Client arg1, Card arg2, int arg3) + { + sendRequest(arg1, PLAY_CARD, new Object[] { + arg2, Integer.valueOf(arg3) + }); + } + + /** The method id used to dispatch {@link #requestRematch} requests. */ + public static final int REQUEST_REMATCH = 2; + + // documentation inherited from interface + public void requestRematch (Client arg1) + { + sendRequest(arg1, REQUEST_REMATCH, new Object[] { + + }); + } + + /** The method id used to dispatch {@link #sendCardsToPlayer} requests. */ + public static final int SEND_CARDS_TO_PLAYER = 3; + + // documentation inherited from interface + public void sendCardsToPlayer (Client arg1, int arg2, Card[] arg3) + { + sendRequest(arg1, SEND_CARDS_TO_PLAYER, new Object[] { + Integer.valueOf(arg2), arg3 + }); + } + +} diff --git a/src/java/com/threerings/parlor/card/trick/data/TrickCardGameObject.java b/src/java/com/threerings/parlor/card/trick/data/TrickCardGameObject.java new file mode 100644 index 00000000..32e865b6 --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/data/TrickCardGameObject.java @@ -0,0 +1,210 @@ +// +// $Id: TrickCardGameObject.java 3516 2005-04-21 00:47:17Z andrzej $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.data; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.Hand; +import com.threerings.parlor.card.data.PlayerCard; +import com.threerings.parlor.turn.data.TurnGameObject; + +/** + * Game objects for trick-based card games must implement this interface. + */ +public interface TrickCardGameObject extends TurnGameObject +{ + /** The state that indicates the game is currently between hands. */ + public static final int BETWEEN_HANDS = 0; + + /** The state that indicates the game is currently playing a hand. */ + public static final int PLAYING_HAND = 1; + + /** The state that indicates the game is currently playing a trick. */ + public static final int PLAYING_TRICK = 2; + + /** The number of states defined for the base trick card game object. */ + public static final int TRICK_STATE_COUNT = 3; + + /** Indicates that the player has not requested or accepted a rematch. */ + public static final int NO_REQUEST = 0; + + /** Indicates that the player has requested a rematch. */ + public static final int REQUESTS_REMATCH = 1; + + /** Indicates that the player has accepted the rematch request. */ + public static final int ACCEPTS_REMATCH = 2; + + /** + * Returns a reference to the trick card game service used to make + * requests to the server. + * + * @return a reference to the trick card game service + */ + public TrickCardGameMarshaller getTrickCardGameService (); + + /** + * Sets the reference to the trick card game service. + * + * @param trickCardGameService the trick card game service + */ + public void setTrickCardGameService (TrickCardGameMarshaller + trickCardGameService); + + /** + * Returns the name of the field that contains the trick state: between + * hands, playing a hand, or playing a trick. + * + * @return the name of the trickState field + */ + public String getTrickStateFieldName (); + + /** + * Returns the trick state: between hands, playing a hand, or playing a + * trick. + * + * @return the trick state + */ + public int getTrickState (); + + /** + * Sets the trick state. + * + * @param trickState the trick state + */ + public void setTrickState (int trickState); + + /** + * Returns an array containing the turn duration scales for each player. + * Turn duration scales decrease each time players time out. + * + * @return the array of turn duration scales + */ + public float[] getTurnDurationScales (); + + /** + * Sets the array of turn duration scales. + * + * @param turnDurationScales the array of turn duration scales + */ + public void setTurnDurationScales (float[] turnDurationScales); + + /** + * Sets an element of the array of turn duration scales. + * + * @param turnDurationScale the turn duration scale + * @param index the index of the turn duration scale + */ + public void setTurnDurationScalesAt (float turnDurationScale, int index); + + /** + * Returns the duration of the current turn, which may depend on the state + * of the game as well as the duration scale of the active player. + */ + public long getTurnDuration (); + + /** + * Returns the name of the field that contains the history of the trick + * in terms of the cards played by each player. + * + * @return the name of the cardsPlayed field + */ + public String getCardsPlayedFieldName (); + + /** + * Returns an array containing the history of the trick in terms of the + * cards played by each player. + * + * @return the cards played so far in the trick + */ + public PlayerCard[] getCardsPlayed (); + + /** + * Sets the array of cards played by each player. + * + * @param cardsPlayed the array of cards played + */ + public void setCardsPlayed (PlayerCard[] cardsPlayed); + + /** + * Returns the name of the field that contains the history of the last + * trick in terms of the cards played by each player. + * + * @return the name of the lastCardsPlayed field + */ + public String getLastCardsPlayedFieldName (); + + /** + * Returns an array containing the history of the last trick in terms of + * the cards played by each player. + * + * @return the cards played in the last trick + */ + public PlayerCard[] getLastCardsPlayed (); + + /** + * Sets the last array of cards played by each player. + * + * @param lastCardsPlayed the last array of cards played + */ + public void setLastCardsPlayed (PlayerCard[] lastCardsPlayed); + + /** + * Returns the name of the field that contains the rematch requests. + * + * @return the name of the rematchRequests field + */ + public String getRematchRequestsFieldName (); + + /** + * Returns the array of rematch requests. + * + * @return the array of rematch requests + */ + public int[] getRematchRequests (); + + /** + * Sets the array of rematch requests. + * + * @param rematchRequests the array of rematch requests + */ + public void setRematchRequests (int[] rematchRequests); + + /** + * Sets an element of the rematch request array. + * + * @param rematchRequest the rematch request value + * @param index the index at which to set the value + */ + public void setRematchRequestsAt (int rematchRequest, int index); + + /** + * Checks whether a user can play the specified card at this time. + * + * @param hand the player's hand + * @param card the card that the user would like to play + */ + public boolean isCardPlayable (Hand hand, Card card); + + /** + * Returns the card of the player who took the current trick. + */ + public PlayerCard getTrickTaker (); +} diff --git a/src/java/com/threerings/parlor/card/trick/server/TrickCardGameDispatcher.java b/src/java/com/threerings/parlor/card/trick/server/TrickCardGameDispatcher.java new file mode 100644 index 00000000..17655c83 --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/server/TrickCardGameDispatcher.java @@ -0,0 +1,84 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.server; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.trick.client.TrickCardGameService; +import com.threerings.parlor.card.trick.data.TrickCardGameMarshaller; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; + +/** + * Dispatches requests to the {@link TrickCardGameProvider}. + */ +public class TrickCardGameDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public TrickCardGameDispatcher (TrickCardGameProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new TrickCardGameMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case TrickCardGameMarshaller.PLAY_CARD: + ((TrickCardGameProvider)provider).playCard( + source, + (Card)args[0], ((Integer)args[1]).intValue() + ); + return; + + case TrickCardGameMarshaller.REQUEST_REMATCH: + ((TrickCardGameProvider)provider).requestRematch( + source + ); + return; + + case TrickCardGameMarshaller.SEND_CARDS_TO_PLAYER: + ((TrickCardGameProvider)provider).sendCardsToPlayer( + source, + ((Integer)args[0]).intValue(), (Card[])args[1] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/parlor/card/trick/server/TrickCardGameManagerDelegate.java b/src/java/com/threerings/parlor/card/trick/server/TrickCardGameManagerDelegate.java new file mode 100644 index 00000000..724f58fb --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/server/TrickCardGameManagerDelegate.java @@ -0,0 +1,643 @@ +// +// $Id: TrickCardGameManagerDelegate.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.server; + +import java.util.ArrayList; +import java.util.Arrays; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.Interval; +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.util.Name; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.server.PresentsServer; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.parlor.game.server.GameManager; +import com.threerings.parlor.turn.server.TurnGameManagerDelegate; + +import com.threerings.parlor.card.Log; +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.CardGameObject; +import com.threerings.parlor.card.data.Deck; +import com.threerings.parlor.card.data.Hand; +import com.threerings.parlor.card.data.PlayerCard; +import com.threerings.parlor.card.server.CardGameManager; +import com.threerings.parlor.card.trick.data.TrickCardGameMarshaller; +import com.threerings.parlor.card.trick.data.TrickCardGameObject; + +/** + * A card game manager delegate for trick-based card games, such as + * Spades and Hearts. + */ +public class TrickCardGameManagerDelegate extends TurnGameManagerDelegate + implements TrickCardGameProvider +{ + /** + * Constructor. + * + * @param manager the game manager + */ + public TrickCardGameManagerDelegate (CardGameManager manager) + { + super(manager); + + _cgmgr = manager; + _deck = new Deck(); + } + + // Documentation inherited. + public void didStartup (PlaceObject plobj) + { + super.didStartup(plobj); + + _trickCardGame = (TrickCardGameObject)plobj; + + _cardGame = (CardGameObject)plobj; + + _trickCardGame.setTrickCardGameService( + (TrickCardGameMarshaller)PresentsServer.invmgr.registerDispatcher( + new TrickCardGameDispatcher(this), false)); + } + + // Documentation inherited. + public void didShutdown () + { + super.didShutdown(); + + PresentsServer.invmgr.clearDispatcher( + _trickCardGame.getTrickCardGameService()); + } + + // Documentation inherited. + public void gameWillStart () + { + super.gameWillStart(); + + // clear out the last cards played + _trickCardGame.setLastCardsPlayed(null); + + // initialize the turn duration scales + float[] scales = new float[_cardGame.getPlayerCount()]; + Arrays.fill(scales, 1.0f); + _trickCardGame.setTurnDurationScales(scales); + } + + /** + * Called when the game has started. Default implementation starts the + * first hand. + */ + public void gameDidStart () + { + super.gameDidStart(); + + // start the first hand + startHand(); + } + + // Documentation inherited. + public void gameDidEnd () + { + super.gameDidEnd(); + + // make sure all intervals are cancelled + _turnTimeoutInterval.cancel(); + _endTrickInterval.cancel(); + + // make sure trick state is back to between hands + if (_trickCardGame.getTrickState() != + TrickCardGameObject.BETWEEN_HANDS) { + _trickCardGame.setTrickState(TrickCardGameObject.BETWEEN_HANDS); + } + + // initialize the array of rematch requests + _trickCardGame.setRematchRequests( + new int[_cardGame.getPlayerCount()]); + } + + // Documentation inherited. + public void startTurn () + { + super.startTurn(); + + // initialize the timeout flag and schedule the timeout interval + _turnTimedOut = false; + _turnTimeoutInterval.schedule(_trickCardGame.getTurnDuration()); + } + + // Documentation inherited. + public void endTurn () + { + // cancel the timeout interval + _turnTimeoutInterval.cancel(); + + // reduce or increase the turn duration scale + if (_turnTimedOut) { + reduceTurnDurationScale(_turnIdx); + + } else { + increaseTurnDurationScale(_turnIdx); + } + + super.endTurn(); + } + + /** + * Starts a hand of cards. Calls {@link #handWillStart}, sets the trick + * state to PLAYING_HAND, and calls {@link #handDidStart}. + */ + public void startHand () + { + handWillStart(); + _trickCardGame.setTrickState(TrickCardGameObject.PLAYING_HAND); + handDidStart(); + } + + /** + * Ends the hand of cards. Calls {@link #handWillEnd}, sets the trick + * state to BETWEEN_HANDS, and calls {@link #handDidEnd}. + */ + public void endHand () + { + handWillEnd(); + _trickCardGame.setTrickState(TrickCardGameObject.BETWEEN_HANDS); + handDidEnd(); + } + + /** + * Starts a trick. Calls {@link #trickWillStart}, sets the trick + * state to PLAYING_TRICK, and calls {@link #trickDidStart}. + */ + public void startTrick () + { + trickWillStart(); + _trickCardGame.setTrickState(TrickCardGameObject.PLAYING_TRICK); + trickDidStart(); + } + + /** + * Ends the trick. Calls {@link #trickWillEnd}, sets the trick + * state to PLAYING_HAND, and calls {@link #trickDidEnd}. + */ + public void endTrick () + { + trickWillEnd(); + _trickCardGame.setTrickState(TrickCardGameObject.PLAYING_HAND); + trickDidEnd(); + } + + /** + * Processes a request to transfer a group of cards between players. + * Default implementation verifies that the user's hand contains the + * specified cards, then calls {@link #sendCardsToPlayer(int, int, + * Card[])}. + */ + public void sendCardsToPlayer (ClientObject client, int toidx, + Card[] cards) + { + // make sure they're actually a player + int fromidx = _cgmgr.getPlayerIndex(client); + if (fromidx == -1) { + Log.warning("Send request from non-player [username=" + + ((BodyObject)client).who() + ", cards=" + + StringUtil.toString(cards) + "]."); + return; + } + + // make sure they have the cards + if (!_hands[fromidx].containsAll(cards)) { + Log.warning("Tried to send cards not held [username=" + + ((BodyObject)client).who() + ", cards=" + + StringUtil.toString(cards) + "]."); + return; + } + + // send the cards + sendCardsToPlayer(fromidx, toidx, cards); + } + + /** + * Sends cards between players without error checking. Default + * implementation transfers the cards between hands and notifies + * everyone of the transfer using {@link + * CardGameManager#transferCardsBetweenPlayers(int, int, Card[])}. + */ + protected void sendCardsToPlayer (int fromidx, int toidx, Card[] cards) + { + // remove from sending player's hand + _hands[fromidx].removeAll(cards); + + // add to receiving player's hand + _hands[toidx].addAll(cards); + + // notify everyone of the transfer + _cgmgr.transferCardsBetweenPlayers(fromidx, toidx, cards); + } + + // Documentation inherited. + public void playCard (ClientObject client, Card card, int handSize) + { + // make sure we're playing a trick + if (_trickCardGame.getTrickState() != + TrickCardGameObject.PLAYING_TRICK) { + return; // silently ignore play attempts after timeouts + } + + // make sure it's their turn + Name username = ((BodyObject)client).getVisibleName(); + if (!username.equals(_trickCardGame.getTurnHolder())) { + return; + } + + // make sure they're on the right trick + int pidx = _cardGame.getPlayerIndex(username); + if (_hands[pidx].size() != handSize) { + return; + } + + // make sure their hand contains the specified card + if (!_hands[pidx].contains(card)) { + Log.warning("Tried to play card not held [username=" + username + + ", card=" + card + "]."); + return; + } + + // make sure the card is legal to play + if (!_trickCardGame.isCardPlayable(_hands[pidx], card)) { + Log.warning("Tried to play illegal card [username=" + username + + ", card=" + card + "]."); + return; + } + + // play the card + playCard(pidx, card); + } + + /** + * Plays a card for a player without error checking. + */ + protected void playCard (int pidx, Card card) + { + ((DObject) _trickCardGame).startTransaction(); + try { + // play the card by removing it from the hand and adding it + // to the end of the cards played array + _hands[pidx].remove(card); + PlayerCard[] cards = (PlayerCard[])ArrayUtil.append( + _trickCardGame.getCardsPlayed(), new PlayerCard(pidx, card)); + _trickCardGame.setCardsPlayed(cards); + + // end the user's turn + endTurn(); + + // end the trick if everyone has played a card + if (_turnIdx == -1) { + if (_endTrickDelay == 0) { + endTrick(); + + } else { + _endTrickInterval.schedule(_endTrickDelay); + } + } + } finally { + ((DObject) _trickCardGame).commitTransaction(); + } + } + + // Documentation inherited. + public void requestRematch (ClientObject client) + { + // make sure the game is over + if (_cardGame.state != CardGameObject.GAME_OVER) { + Log.warning("Tried to request rematch when game wasn't over " + + "[username=" + ((BodyObject)client).who() + "]."); + return; + } + + // make sure the requester is one of the players + int pidx = _cgmgr.getPlayerIndex(client); + if (pidx == -1) { + Log.warning("Rematch request from non-player [username=" + + ((BodyObject)client).who() + "]."); + return; + } + + // make sure the player hasn't already requested + if (_trickCardGame.getRematchRequests()[pidx] != + TrickCardGameObject.NO_REQUEST) { + Log.warning("Repeated rematch request [username=" + + ((BodyObject)client).who() + "]."); + return; + } + + // if player is first requesting, set to request; else set + // to accept + int req = (getRematchRequestCount() == 0 ? + TrickCardGameObject.REQUESTS_REMATCH : + TrickCardGameObject.ACCEPTS_REMATCH); + _trickCardGame.setRematchRequestsAt(req, pidx); + + // if all players accept the rematch, restart the game + if (getRematchRequestCount() == _cardGame.getPlayerCount()) { + _cgmgr.rematchGame(); + } + } + + /** + * Returns the number of players currently requesting or accepting + * a rematch. + */ + protected int getRematchRequestCount () + { + int[] rematchRequests = _trickCardGame.getRematchRequests(); + int count = 0; + for (int i = 0; i < rematchRequests.length; i++) { + if (rematchRequests[i] != TrickCardGameObject.NO_REQUEST) { + count++; + } + } + return count; + } + + /** + * Checks whether the trick is complete--that is, whether each player has + * played a card. + */ + protected boolean isTrickComplete () + { + return _trickCardGame.getCardsPlayed().length == + _cardGame.getPlayerCount(); + } + + // Documentation inherited. + protected void setFirstTurnHolder () + { + if (_trickCardGame.getTrickState() == + TrickCardGameObject.PLAYING_TRICK) { + super.setFirstTurnHolder(); + + } else { + _turnIdx = -1; + } + } + + // Documentation inherited. + protected void setNextTurnHolder () + { + if (_trickCardGame.getTrickState() == + TrickCardGameObject.PLAYING_TRICK && + !isTrickComplete()) { + super.setNextTurnHolder(); + + } else { + _turnIdx = -1; + } + } + + /** + * Called when the current turn times out. Default implementation + * plays a random playable card if in the trick-playing state. + */ + protected void turnTimedOut () + { + if (_trickCardGame.getTrickState() == + TrickCardGameObject.PLAYING_TRICK) { + playCard(_turnIdx, pickRandomPlayableCard(_hands[_turnIdx])); + } + } + + /** + * Reduces the specified player's turn duration due to a time-out. + */ + protected void reduceTurnDurationScale (int pidx) + { + float oldScale = _trickCardGame.getTurnDurationScales()[pidx], + newScale = Math.max(oldScale - TURN_DURATION_SCALE_REDUCTION, + MINIMUM_TURN_DURATION_SCALE); + if (newScale != oldScale) { + _trickCardGame.setTurnDurationScalesAt(newScale, pidx); + } + } + + /** + * Increases the specified player's turn duration due to avoiding a + * time-out. + */ + protected void increaseTurnDurationScale (int pidx) + { + float oldScale = _trickCardGame.getTurnDurationScales()[pidx], + newScale = Math.min(oldScale + TURN_DURATION_SCALE_INCREASE, + 1.0f); + if (newScale != oldScale) { + _trickCardGame.setTurnDurationScalesAt(newScale, pidx); + } + } + + /** + * Returns a random playable card from the specified hand. + */ + protected Card pickRandomPlayableCard (Hand hand) + { + ArrayList playableCards = new ArrayList(); + for (int i = 0; i < hand.size(); i++) { + Card card = (Card)hand.get(i); + if (_trickCardGame.isCardPlayable(hand, card)) { + playableCards.add(card); + } + } + return (Card)RandomUtil.pickRandom(playableCards); + } + + /** + * Notifies the object that a new hand is about to start. + */ + protected void handWillStart () + {} + + /** + * Notifies the object that a new hand has just started. Default + * implementation prepares the deck, deals the hands, and starts the + * first trick. + */ + protected void handDidStart () + { + // prepare the deck + prepareDeck(); + + // deal cards to players + dealHands(); + + // start the first trick + startTrick(); + } + + /** + * Prepares the deck for a new hand of cards. Default implementation + * resets to a full deck without jokers and shuffles. + */ + protected void prepareDeck () + { + _deck.reset(false); + _deck.shuffle(); + } + + /** + * Deals hands to the players. Default implementation deals the entire + * deck to the players in equal-sized hands. + */ + protected void dealHands () + { + _hands = _cgmgr.dealHands(_deck, _deck.size() / + _cardGame.getPlayerCount()); + } + + /** + * Notifies the object that the hand is about to end. + */ + protected void handWillEnd () + {} + + /** + * Notifies the object that the hand has ended. Default implementation + * starts the next hand. + */ + protected void handDidEnd () + { + startHand(); + } + + /** + * Notifies the object that a new trick is about to start. Default + * implementation resets the array of cards played. + */ + protected void trickWillStart () + { + _trickCardGame.setCardsPlayed(new PlayerCard[0]); + } + + /** + * Notifies the object that a new trick has started. Default + * implementation sets the first turn holder and starts the + * turn. + */ + protected void trickDidStart () + { + setFirstTurnHolder(); + startTurn(); + } + + /** + * Notifies the object that the trick is about to end. + */ + protected void trickWillEnd () + {} + + /** + * Notifies the object that the trick has ended. Default implementation + * records the last cards played and starts the next trick, unless a + * player has run out of cards, in which case it ends the hand. + */ + protected void trickDidEnd () + { + // store the trick results for late-joiners + _trickCardGame.setLastCardsPlayed(_trickCardGame.getCardsPlayed()); + + // clear out the cards played in the trick + _trickCardGame.setCardsPlayed(null); + + // verify that each player has at least one card + if (anyHandsEmpty()) { + endHand(); + return; + } + + // everyone has cards; let's play another trick + startTrick(); + } + + /** + * Checks whether any hands are empty. + */ + protected boolean anyHandsEmpty () + { + for (int i = 0; i < _hands.length; i++) { + if (_hands[i].isEmpty()) { + return true; + } + } + return false; + } + + /** The card game manager. */ + protected CardGameManager _cgmgr; + + /** The game object as trick card game. */ + protected TrickCardGameObject _trickCardGame; + + /** The game object as card game. */ + protected CardGameObject _cardGame; + + /** The amount of time to wait before ending the trick. */ + protected long _endTrickDelay; + + /** The deck from which cards are dealt. */ + protected Deck _deck; + + /** The hands of each player. */ + protected Hand[] _hands; + + /** Whether or not the turn timed out. */ + protected boolean _turnTimedOut; + + /** The all-purpose turn timeout interval. */ + protected Interval _turnTimeoutInterval = + new Interval(PresentsServer.omgr) { + public void expired () { + _turnTimedOut = true; + turnTimedOut(); + } + }; + + /** Calls {@link #endTrick} upon expiration. */ + protected Interval _endTrickInterval = new Interval(PresentsServer.omgr) { + public void expired () { + endTrick(); + } + }; + + /** Reduce turn duration scales by this amount each time the player times + * out. */ + protected static final float TURN_DURATION_SCALE_REDUCTION = 0.25f; + + /** + * Increase turn duration scales by this amount each time the player + * manages not to time out. */ + protected static final float TURN_DURATION_SCALE_INCREASE = 0.5f; + + /** Don't let turn duration scales get below this level. */ + protected static final float MINIMUM_TURN_DURATION_SCALE = 0.1f; +} diff --git a/src/java/com/threerings/parlor/card/trick/server/TrickCardGameProvider.java b/src/java/com/threerings/parlor/card/trick/server/TrickCardGameProvider.java new file mode 100644 index 00000000..783f8ccc --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/server/TrickCardGameProvider.java @@ -0,0 +1,50 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.server; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.trick.client.TrickCardGameService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +/** + * Defines the server-side of the {@link TrickCardGameService}. + */ +public interface TrickCardGameProvider extends InvocationProvider +{ + /** + * Handles a {@link TrickCardGameService#playCard} request. + */ + public void playCard (ClientObject caller, Card arg1, int arg2); + + /** + * Handles a {@link TrickCardGameService#requestRematch} request. + */ + public void requestRematch (ClientObject caller); + + /** + * Handles a {@link TrickCardGameService#sendCardsToPlayer} request. + */ + public void sendCardsToPlayer (ClientObject caller, int arg1, Card[] arg2); +} diff --git a/src/java/com/threerings/parlor/card/trick/util/TrickCardGameUtil.java b/src/java/com/threerings/parlor/card/trick/util/TrickCardGameUtil.java new file mode 100644 index 00000000..962bad50 --- /dev/null +++ b/src/java/com/threerings/parlor/card/trick/util/TrickCardGameUtil.java @@ -0,0 +1,195 @@ +// +// $Id: TrickCardGameObject.java 3382 2005-03-03 19:55:35Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.card.trick.util; + +import com.threerings.parlor.card.data.Card; +import com.threerings.parlor.card.data.Hand; +import com.threerings.parlor.card.data.PlayerCard; +import com.threerings.parlor.card.trick.data.TrickCardCodes; + +/** + * Methods of general utility to trick-taking card games. + */ +public class TrickCardGameUtil + implements TrickCardCodes +{ + /** + * For four-player games with fixed partnerships, this returns the index + * of the player's team. + * + * @param pidx the player index + */ + public static int getTeamIndex (int pidx) + { + return pidx & 1; + } + + /** + * For four-player games with fixed partnerships, this returns the index + * of the other team. + * + * @param tidx the index of the team + */ + public static int getOtherTeamIndex (int tidx) + { + return tidx ^ 1; + } + + /** + * For four-player games with fixed partnerships, this returns the index + * of the player's partner. + */ + public static int getPartnerIndex (int pidx) + { + return pidx ^ 2; + } + + /** + * For four-player games with fixed partnerships, this returns the index + * of one of the members of a team. + * + * @param tidx the index of the team + * @param midx the index of the player within the team + */ + public static int getTeamMemberIndex (int tidx, int midx) + { + return (midx << 1) | tidx; + } + + /** + * For four-player games, this returns the index of the player after the + * specified player going clockwise around the table. + */ + public static int getNextInClockwiseSequence (int pidx) + { + // 2 + // 1 3 + // 0 + return (pidx + 1) & 3; + } + + /** + * For four-player games with fixed partnerships, this returns the + * relative location of one player from the point of view of another. + * + * @param pidx1 the index of the player to whom the location is relative + * @param pidx2 the index of the player whose location is desired + * @return the relative location (TOP, BOTTOM, LEFT, or RIGHT) + */ + public static int getRelativeLocation (int pidx1, int pidx2) + { + return (pidx2 - pidx1) & 3; + } + + /** + * For four-player games, returns the index of the player to the left of + * the specified player. + */ + public static int getLeftIndex (int pidx) + { + return (pidx + 1) & 3; + } + + /** + * For four-player games, returns the index of the player to the right of + * the specified player. + */ + public static int getRightIndex (int pidx) + { + return (pidx + 3) & 3; + } + + /** + * For four-player games, returns the index of the player across from the + * specified player. + */ + public static int getOppositeIndex (int pidx) + { + return pidx ^ 2; + } + + /** + * Checks whether the player can follow the suit lead with the hand given. + */ + public static boolean canFollowSuit (PlayerCard[] cardsPlayed, Hand hand) + { + return hand.getSuitMemberCount(cardsPlayed[0].card.getSuit()) > 0; + } + + /** + * Checks whether the specified array contains the given card. + */ + public static boolean containsCard (PlayerCard[] cards, Card card) + { + for (int i = 0; i < cards.length; i++) { + if (cards[i].card.equals(card)) { + return true; + } + } + return false; + } + + /** + * Determines the number of cards that belong to the specified suit within + * the array given. + */ + public static int countSuitMembers (PlayerCard[] cards, int suit) + { + int count = 0; + for (int i = 0; i < cards.length; i++) { + if (cards[i].card.getSuit() == suit) { + count++; + } + } + return count; + } + + /** + * Checks whether the proposed card follows the suit lead. + */ + public static boolean followsSuit (PlayerCard[] cardsPlayed, Card card) + { + return cardsPlayed[0].card.getSuit() == card.getSuit(); + } + + /** + * Returns the highest card (according to the standard A,K,...,2 ordering) + * in the suit lead, with an optional trump suit. + * + * @param trumpSuit the trump suit, or -1 for none + */ + public static PlayerCard getHighestInLeadSuit (PlayerCard[] cardsPlayed, + int trumpSuit) + { + PlayerCard highest = cardsPlayed[0]; + for (int i = 1; i < cardsPlayed.length; i++) { + PlayerCard other = cardsPlayed[i]; + if ((other.card.getSuit() == highest.card.getSuit() && + other.card.compareTo(highest.card) > 0) || + (other.card.getSuit() == trumpSuit && + highest.card.getSuit() != trumpSuit)) { + highest = other; + } + } + return highest; + } +} diff --git a/src/java/com/threerings/parlor/client/DefaultSwingTableConfigurator.java b/src/java/com/threerings/parlor/client/DefaultSwingTableConfigurator.java new file mode 100644 index 00000000..408eff86 --- /dev/null +++ b/src/java/com/threerings/parlor/client/DefaultSwingTableConfigurator.java @@ -0,0 +1,129 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import javax.swing.JCheckBox; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.samskivert.swing.SimpleSlider; +import com.samskivert.swing.VGroupLayout; + +import com.threerings.parlor.data.TableConfig; +import com.threerings.parlor.util.ParlorContext; + +import com.threerings.parlor.game.client.SwingGameConfigurator; +import com.threerings.parlor.game.data.GameConfig; + +/** + * Provides a default implementation of a TableConfigurator for + * a Swing interface. + */ +public class DefaultSwingTableConfigurator extends TableConfigurator +{ + /** + * Create a TableConfigurator that allows only the specified number + * of players and lets the configuring user enable private games + * only if the number of players is greater than 2. + */ + public DefaultSwingTableConfigurator (int players) + { + this(players, (players > 2)); + } + + /** + * Create a TableConfigurator that allows only the specified number + * of players and lets the user configure a private table, or not. + */ + public DefaultSwingTableConfigurator (int players, boolean allowPrivate) + { + this(players, players, players, allowPrivate); + } + + /** + * Create a TableConfigurator that allows for the specified configuration + * parameters. + */ + public DefaultSwingTableConfigurator (int minPlayers, + int desiredPlayers, int maxPlayers, boolean allowPrivate) + { + _config.minimumPlayerCount = minPlayers; + + // create a slider for players, if applicable + if (minPlayers != maxPlayers) { + _playerSlider = new SimpleSlider( + "", minPlayers, maxPlayers, desiredPlayers); + + } else { + _config.desiredPlayerCount = desiredPlayers; + } + + // create up the checkbox for private games, if applicable + if (allowPrivate) { + _privateCheck = new JCheckBox(); + } + } + + // documentation inherited + protected void createConfigInterface () + { + super.createConfigInterface(); + + SwingGameConfigurator gconf = (SwingGameConfigurator) _gameConfigurator; + + if (_playerSlider != null) { + // TODO: proper translation + gconf.addControl(new JLabel("Players:"), _playerSlider); + } + + if (_privateCheck != null) { + // TODO: proper translation + gconf.addControl(new JLabel("Private:"), _privateCheck); + } + } + + // documentation inherited + public boolean isEmpty () + { + return (_playerSlider == null) && (_privateCheck == null); + } + + // documentation inherited + protected void flushTableConfig() + { + super.flushTableConfig(); + + if (_playerSlider != null) { + _config.desiredPlayerCount = _playerSlider.getValue(); + } + if (_privateCheck != null) { + _config.privateTable = _privateCheck.isSelected(); + } + } + + /** A slider for configuring the number of players at the table. */ + protected SimpleSlider _playerSlider; + + /** A checkbox to allow the table creator to specify if the table is + * private. */ + protected JCheckBox _privateCheck; +} diff --git a/src/java/com/threerings/parlor/client/GameReadyObserver.java b/src/java/com/threerings/parlor/client/GameReadyObserver.java new file mode 100644 index 00000000..63bf298b --- /dev/null +++ b/src/java/com/threerings/parlor/client/GameReadyObserver.java @@ -0,0 +1,43 @@ +// +// $Id: GameReadyObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +/** + * Used to inform interested parties when the {@link ParlorDirector} + * receives a game ready notification. The observers can ratify the + * decision to head directly into the game or can take responsibility + * themselves for doing so. + */ +public interface GameReadyObserver +{ + /** + * Called when a game ready notification is received. + * + * @param gameOid the place oid of the ready game. + * + * @return if the observer returns true from this method, the parlor + * director assumes they will take care of entering the game room + * after performing processing of their own. If all observers return + * false, the director will enter the game room automatically. + */ + public boolean receivedGameReady (int gameOid); +} diff --git a/src/java/com/threerings/parlor/client/Invitation.java b/src/java/com/threerings/parlor/client/Invitation.java new file mode 100644 index 00000000..3dadbe95 --- /dev/null +++ b/src/java/com/threerings/parlor/client/Invitation.java @@ -0,0 +1,211 @@ +// +// $Id: Invitation.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.util.Name; + +import com.threerings.parlor.Log; +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.util.ParlorContext; + +/** + * The invitation class is used to track information related to + * outstanding invitations generated by or targeted to this client. + */ +public class Invitation + implements ParlorCodes, ParlorService.InviteListener +{ + /** The unique id for this invitation (as assigned by the + * server). This is -1 until we receive an acknowledgement from + * the server that our invitation was delivered. */ + public int inviteId = -1; + + /** The name of the other user involved in this invitation. */ + public Name opponent; + + /** The configuration of the game to be created. */ + public GameConfig config; + + /** Constructs a new invitation record. */ + public Invitation (ParlorContext ctx, ParlorService pservice, + Name opponent, GameConfig config, + InvitationResponseObserver observer) + { + _ctx = ctx; + _pservice = pservice; + _observer = observer; + this.opponent = opponent; + this.config = config; + } + + /** + * Accepts this invitation. + */ + public void accept () + { + // generate the invocation service request + _pservice.respond(_ctx.getClient(), inviteId, + INVITATION_ACCEPTED, null, this); + } + + /** + * Refuses this invitation. + * + * @param message the message to deliver to the inviting user + * explaining the reason for the refusal or null if no message is to + * be provided. + */ + public void refuse (String message) + { + // generate the invocation service request + _pservice.respond(_ctx.getClient(), inviteId, + INVITATION_REFUSED, message, this); + } + + /** + * Cancels this invitation. + */ + public void cancel () + { + // if the invitation has not yet been acknowleged by the + // server, we make a note that it should be cancelled when we + // do receive the acknowlegement + if (inviteId == -1) { + _cancelled = true; + + } else { + // otherwise, generate the invocation service request + _pservice.cancel(_ctx.getClient(), inviteId, this); + // and remove it from the pending table + _ctx.getParlorDirector().clearInvitation(this); + } + } + + /** + * Counters this invitation with an invitation with different game + * configuration parameters. + * + * @param config the updated game configuration. + * @param observer the entity that will be notified if this + * counter-invitation is accepted, refused or countered. + */ + public void counter (GameConfig config, InvitationResponseObserver observer) + { + // update our observer (who will eventually be hearing back from + // the other client about their counter-invitation) + _observer = observer; + + // generate the invocation service request + _pservice.respond(_ctx.getClient(), inviteId, + INVITATION_COUNTERED, config, this); + } + + // documentation inherited from interface + public void inviteReceived (int inviteId) + { + // fill in our invitation id + this.inviteId = inviteId; + + // if the invitation was cancelled before we heard back about + // it, we need to send off a cancellation request now + if (_cancelled) { + _pservice.cancel(_ctx.getClient(), inviteId, this); + } else { + // otherwise, put it in the pending invites table + _ctx.getParlorDirector().registerInvitation(this); + } + } + + // documentation inherited from interface + public void requestFailed (String reason) + { + // let the observer know what's up + _observer.invitationRefused(this, reason); + } + + /** + * Called by the parlor director when we receive a response to an + * invitation initiated by this client. + */ + protected void receivedResponse (int code, Object arg) + { + // make sure we have an observer to notify + if (_observer == null) { + Log.warning("No observer registered for invitation " + + this + "."); + return; + } + + // notify the observer + try { + switch (code) { + case INVITATION_ACCEPTED: + _observer.invitationAccepted(this); + break; + + case INVITATION_REFUSED: + _observer.invitationRefused(this, (String)arg); + break; + + case INVITATION_COUNTERED: + _observer.invitationCountered(this, (GameConfig)arg); + break; + } + + } catch (Exception e) { + Log.warning("Invitation response observer choked on response " + + "[code=" + code + ", arg=" + arg + + ", invite=" + this + "]."); + Log.logStackTrace(e); + } + + // unless the invitation was countered, we can remove it from the + // pending table because it's resolved + if (code != INVITATION_COUNTERED) { + _ctx.getParlorDirector().clearInvitation(this); + } + } + + /** Returns a string representation of this invitation record. */ + public String toString () + { + return "[inviteId=" + inviteId + ", opponent=" + opponent + + ", config=" + config + ", observer=" + _observer + + ", cancelled=" + _cancelled + "]"; + } + + /** Provides access to client services. */ + protected ParlorContext _ctx; + + /** Provides access to parlor services. */ + protected ParlorService _pservice; + + /** The entity to notify when we receive a response for this + * invitation. */ + protected InvitationResponseObserver _observer; + + /** A flag indicating that we were requested to cancel this + * invitation before we even heard back with an acknowledgement + * that it was received by the server. */ + protected boolean _cancelled = false; +} diff --git a/src/java/com/threerings/parlor/client/InvitationHandler.java b/src/java/com/threerings/parlor/client/InvitationHandler.java new file mode 100644 index 00000000..b34918f7 --- /dev/null +++ b/src/java/com/threerings/parlor/client/InvitationHandler.java @@ -0,0 +1,45 @@ +// +// $Id: InvitationHandler.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +/** + * A client entity that wishes to handle invitations received by other + * clients should implement this interface and register itself with the + * parlor director. It will subsequently be notified of any incoming + * invitations. It is also responsible for handling cancelled invitations. + */ +public interface InvitationHandler +{ + /** + * Called when an invitation is received from another player. + * + * @param invite the received invitation. + */ + public void invitationReceived (Invitation invite); + + /** + * Called when an invitation is cancelled by the inviting player. + * + * @param invite the cancelled invitation. + */ + public void invitationCancelled (Invitation invite); +} diff --git a/src/java/com/threerings/parlor/client/InvitationResponseObserver.java b/src/java/com/threerings/parlor/client/InvitationResponseObserver.java new file mode 100644 index 00000000..ea9ece46 --- /dev/null +++ b/src/java/com/threerings/parlor/client/InvitationResponseObserver.java @@ -0,0 +1,61 @@ +// +// $Id: InvitationResponseObserver.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.parlor.game.data.GameConfig; + +/** + * A client entity that wishes to generate invitations for games must + * implement this interface. An invitation can be accepted, refused or + * countered. A countered invitation is one where the game configuration + * is adjusted by the invited player and proposed back to the inviting + * player. + */ +public interface InvitationResponseObserver +{ + /** + * Called if the invitation was accepted. + * + * @param invite the invitation for which we received a response. + */ + public void invitationAccepted (Invitation invite); + + /** + * Called if the invitation was refused. + * + * @param invite the invitation for which we received a response. + * @param message a message provided by the invited user explaining + * the reason for their refusal, or the empty string if no message was + * provided. + */ + public void invitationRefused (Invitation invite, String message); + + /** + * Called if the invitation was countered with an alternate game + * configuration. + * + * @param invite the invitation for which we received a response. + * @param config the game configuration proposed by the invited + * player. + */ + public void invitationCountered (Invitation invite, GameConfig config); +} diff --git a/src/java/com/threerings/parlor/client/ParlorDecoder.java b/src/java/com/threerings/parlor/client/ParlorDecoder.java new file mode 100644 index 00000000..093c716e --- /dev/null +++ b/src/java/com/threerings/parlor/client/ParlorDecoder.java @@ -0,0 +1,101 @@ +// +// $Id: ParlorDecoder.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.parlor.client.ParlorReceiver; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.presents.client.InvocationDecoder; +import com.threerings.util.Name; + +/** + * Dispatches calls to a {@link ParlorReceiver} instance. + */ +public class ParlorDecoder extends InvocationDecoder +{ + /** The generated hash code used to identify this receiver class. */ + public static final String RECEIVER_CODE = "5ef9ee0d359c42a9024498ee9aad119a"; + + /** The method id used to dispatch {@link ParlorReceiver#gameIsReady} + * notifications. */ + public static final int GAME_IS_READY = 1; + + /** The method id used to dispatch {@link ParlorReceiver#receivedInvite} + * notifications. */ + public static final int RECEIVED_INVITE = 2; + + /** The method id used to dispatch {@link ParlorReceiver#receivedInviteCancellation} + * notifications. */ + public static final int RECEIVED_INVITE_CANCELLATION = 3; + + /** The method id used to dispatch {@link ParlorReceiver#receivedInviteResponse} + * notifications. */ + public static final int RECEIVED_INVITE_RESPONSE = 4; + + /** + * Creates a decoder that may be registered to dispatch invocation + * service notifications to the specified receiver. + */ + public ParlorDecoder (ParlorReceiver receiver) + { + this.receiver = receiver; + } + + // documentation inherited + public String getReceiverCode () + { + return RECEIVER_CODE; + } + + // documentation inherited + public void dispatchNotification (int methodId, Object[] args) + { + switch (methodId) { + case GAME_IS_READY: + ((ParlorReceiver)receiver).gameIsReady( + ((Integer)args[0]).intValue() + ); + return; + + case RECEIVED_INVITE: + ((ParlorReceiver)receiver).receivedInvite( + ((Integer)args[0]).intValue(), (Name)args[1], (GameConfig)args[2] + ); + return; + + case RECEIVED_INVITE_CANCELLATION: + ((ParlorReceiver)receiver).receivedInviteCancellation( + ((Integer)args[0]).intValue() + ); + return; + + case RECEIVED_INVITE_RESPONSE: + ((ParlorReceiver)receiver).receivedInviteResponse( + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), (Object)args[2] + ); + return; + + default: + super.dispatchNotification(methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/parlor/client/ParlorDirector.java b/src/java/com/threerings/parlor/client/ParlorDirector.java new file mode 100644 index 00000000..53c0f3b6 --- /dev/null +++ b/src/java/com/threerings/parlor/client/ParlorDirector.java @@ -0,0 +1,250 @@ +// +// $Id: ParlorDirector.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import java.util.ArrayList; + +import com.samskivert.util.HashIntMap; +import com.threerings.util.Name; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.parlor.Log; +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.util.ParlorContext; + +/** + * The parlor director manages the client side of the game configuration + * and matchmaking processes. It is also the entity that is listening for + * game start notifications which it then dispatches the client entity + * that will actually create and display the user interface for the game + * that started. + */ +public class ParlorDirector extends BasicDirector + implements ParlorCodes, ParlorReceiver +{ + /** + * Constructs a parlor director and provides it with the parlor + * context that it can use to access the client services that it needs + * to provide its own services. Only one parlor director should be + * active in the client at one time and it should be made available + * via the parlor context. + * + * @param ctx the parlor context in use by the client. + */ + public ParlorDirector (ParlorContext ctx) + { + super(ctx); + _ctx = ctx; + + // register ourselves with the invocation director as a parlor + // notification receiver + _ctx.getClient().getInvocationDirector().registerReceiver( + new ParlorDecoder(this)); + } + + /** + * Sets the invitation handler, which is the entity that will be + * notified when we receive incoming invitation notifications and when + * invitations have been cancelled. + * + * @param handler our new invitation handler. + */ + public void setInvitationHandler (InvitationHandler handler) + { + _handler = handler; + } + + /** + * Adds the specified observer to the list of entities that are + * notified when we receive a game ready notification. + */ + public void addGameReadyObserver (GameReadyObserver observer) + { + _grobs.add(observer); + } + + /** + * Removes the specified observer from the list of entities that are + * notified when we receive a game ready notification. + */ + public void removeGameReadyObserver (GameReadyObserver observer) + { + _grobs.remove(observer); + } + + /** + * Requests that the named user be invited to a game described by the + * supplied game config. + * + * @param invitee the user to invite. + * @param config the configuration of the game to which the user is + * being invited. + * @param observer the entity that will be notified if this invitation + * is accepted, refused or countered. + * + * @return an invitation object that can be used to manage the + * outstanding invitation. + */ + public Invitation invite (Name invitee, GameConfig config, + InvitationResponseObserver observer) + { + // create the invitation record + Invitation invite = new Invitation( + _ctx, _pservice, invitee, config, observer); + // submit the invitation request to the server + _pservice.invite(_ctx.getClient(), invitee, config, invite); + // and return the invitation to the caller + return invite; + } + + /** + * Requests that the specified single player game be started. + * + * @param config the configuration of the single player game to be + * started. + * @param listener a listener to be informed of failure if the game + * cannot be started. + */ + public void startSolitaire ( + GameConfig config, InvocationService.ConfirmListener listener) + { + _pservice.startSolitaire(_ctx.getClient(), config, listener); + } + + // documentation inherited + public void clientDidLogoff (Client client) + { + super.clientDidLogoff(client); + _pservice = null; + _pendingInvites.clear(); + } + + // documentation inherited + protected void fetchServices (Client client) + { + // get a handle on our parlor services + _pservice = (ParlorService)client.requireService(ParlorService.class); + } + + // documentation inherited from interface + public void gameIsReady (int gameOid) + { + Log.info("Handling game ready [goid=" + gameOid + "]."); + + // see what our observers have to say about it + boolean handled = false; + for (int i = 0; i < _grobs.size(); i++) { + GameReadyObserver grob = (GameReadyObserver)_grobs.get(i); + handled = grob.receivedGameReady(gameOid) || handled; + } + + // if none of the observers took matters into their own hands, + // then we'll head on over to the game room ourselves + if (!handled) { + _ctx.getLocationDirector().moveTo(gameOid); + } + } + + // documentation inherited from interface + public void receivedInvite (int remoteId, Name inviter, GameConfig config) + { + // create an invitation record for this invitation + Invitation invite = new Invitation( + _ctx, _pservice, inviter, config, null); + invite.inviteId = remoteId; + + // put it in the pending invitations table + _pendingInvites.put(remoteId, invite); + + try { + // notify the invitation handler of the incoming invitation + _handler.invitationReceived(invite); + + } catch (Exception e) { + Log.warning("Invitation handler choked on invite " + + "notification " + invite + "."); + Log.logStackTrace(e); + } + } + + // documentation inherited from interface + public void receivedInviteResponse ( + int remoteId, int code, Object arg) + { + // look up the invitation record for this invitation + Invitation invite = (Invitation)_pendingInvites.get(remoteId); + if (invite == null) { + Log.warning("Have no record of invitation for which we " + + "received a response?! [remoteId=" + remoteId + + ", code=" + code + ", arg=" + arg + "]."); + + } else { + invite.receivedResponse(code, arg); + } + } + + // documentation inherited from interface + public void receivedInviteCancellation (int remoteId) + { + // TBD + } + + /** + * Register a new invitation in our pending invitations table. The + * invitation will call this when it knows its invitation id. + */ + protected void registerInvitation (Invitation invite) + { + _pendingInvites.put(invite.inviteId, invite); + } + + /** + * Called by an invitation when it knows it is no longer and can be + * cleared from the pending invitations table. + */ + protected void clearInvitation (Invitation invite) + { + _pendingInvites.remove(invite.inviteId); + } + + /** An active parlor context. */ + protected ParlorContext _ctx; + + /** Provides access to parlor server side services. */ + protected ParlorService _pservice; + + /** The entity that has registered itself to handle incoming + * invitation notifications. */ + protected InvitationHandler _handler; + + /** A table of acknowledged (but not yet accepted or refused) + * invitation requests, keyed on invitation id. */ + protected HashIntMap _pendingInvites = new HashIntMap(); + + /** We notify the entities on this list when we get a game ready + * notification. */ + protected ArrayList _grobs = new ArrayList(); +} diff --git a/src/java/com/threerings/parlor/client/ParlorReceiver.java b/src/java/com/threerings/parlor/client/ParlorReceiver.java new file mode 100644 index 00000000..0e05a9dd --- /dev/null +++ b/src/java/com/threerings/parlor/client/ParlorReceiver.java @@ -0,0 +1,84 @@ +// +// $Id: ParlorReceiver.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.util.Name; + +import com.threerings.presents.client.InvocationReceiver; + +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.game.data.GameConfig; + +/** + * Defines, for the parlor services, a set of notifications delivered + * asynchronously by the server to the client. These are handled by the + * {@link ParlorDirector}. + */ +public interface ParlorReceiver extends InvocationReceiver +{ + /** + * Dispatched to the client when a game in which they are a + * participant is ready for play. The client will then enter the game + * room which will trigger the loading of the appropriate game UI code + * and generally get things started. + * + * @param gameOid the object id of the game object. + */ + public void gameIsReady (int gameOid); + + /** + * Called by the invocation services when another user has invited us + * to play a game. + * + * @param remoteId the unique indentifier for this invitation (used + * when countering or responding). + * @param inviter the username of the inviting user. + * @param config the configuration information for the game to which + * we've been invited. + */ + public void receivedInvite (int remoteId, Name inviter, GameConfig config); + + /** + * Called by the invocation services when another user has responded + * to our invitation by either accepting, refusing or countering it. + * + * @param remoteId the indentifier for the invitation on question. + * @param code the response code, either {@link + * ParlorCodes#INVITATION_ACCEPTED} or {@link + * ParlorCodes#INVITATION_REFUSED} or {@link + * ParlorCodes#INVITATION_COUNTERED}. + * @param arg in the case of a refused invitation, a string + * containing a message provided by the invited user explaining the + * reason for refusal (the empty string if no explanation was + * provided). In the case of a countered invitation, a new game config + * object with the modified game configuration. + */ + public void receivedInviteResponse (int remoteId, int code, Object arg); + + /** + * Called by the invocation services when an outstanding invitation + * has been cancelled by the inviting user. + * + * @param remoteId the indentifier of the cancelled invitation. + */ + public void receivedInviteCancellation (int remoteId); +} diff --git a/src/java/com/threerings/parlor/client/ParlorService.java b/src/java/com/threerings/parlor/client/ParlorService.java new file mode 100644 index 00000000..4cab0964 --- /dev/null +++ b/src/java/com/threerings/parlor/client/ParlorService.java @@ -0,0 +1,164 @@ +// +// $Id: ParlorService.java 3525 2005-04-26 02:38:42Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.util.Name; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.parlor.data.TableConfig; + +import com.threerings.parlor.game.data.GameConfig; + +/** + * Provides an interface to the various parlor invocation services. + * Presently these services are limited to the various matchmaking + * mechanisms. It is unlikely that client code will want to make direct + * use of this class, instead they would make use of the programmatic + * interface provided by the {@link ParlorDirector}. + */ +public interface ParlorService extends InvocationService +{ + /** + * Used to communicate responses to {@link #invite} requests. + */ + public static interface InviteListener extends InvocationListener + { + /** + * Called in response to a successful {@link #invite} request. + */ + public void inviteReceived (int inviteId); + } + + /** + * You probably don't want to call this directly, but want to generate + * your invitation request via {@link ParlorDirector#invite}. Requests + * that an invitation be delivered to the named user, requesting that + * they join the inviting user in a game, the details of which are + * specified in the supplied game config object. + * + * @param client a connected, operational client instance. + * @param invitee the username of the user to be invited. + * @param config a game config object detailing the type and + * configuration of the game to be created. + * @param listener will receive and process the response. + */ + public void invite (Client client, Name invitee, GameConfig config, + InviteListener listener); + + /** + * You probably don't want to call this directly, but want to call one + * of {@link Invitation#accept}, {@link Invitation#refuse}, or {@link + * Invitation#counter}. Requests that an invitation response be + * delivered with the specified parameters. + * + * @param client a connected, operational client instance. + * @param inviteId the unique id previously assigned by the server to + * this invitation. + * @param code the response code to use in responding to the + * invitation. + * @param arg the argument associated with the response (a string + * message from the player explaining why the response was refused in + * the case of an invitation refusal or an updated game configuration + * object in the case of a counter-invitation, or null in the case of + * an accepted invitation). + * @param listener will receive and process the response. + */ + public void respond (Client client, int inviteId, int code, Object arg, + InvocationListener listener); + + /** + * You probably don't want to call this directly, but want to call + * {@link Invitation#cancel}. Requests that an outstanding + * invitation be cancelled. + * + * @param client a connected, operational client instance. + * @param inviteId the unique id previously assigned by the server to + * this invitation. + * @param listener will receive and process the response. + */ + public void cancel (Client client, int inviteId, + InvocationListener listener); + + /** + * Used to communicate responses to {@link #createTable}, {@link + * #joinTable}, and {@link #leaveTable} requests. + */ + public static interface TableListener extends InvocationListener + { + public void tableCreated (int tableId); + } + + /** + * You probably don't want to call this directly, but want to call + * {@link TableDirector#createTable}. Requests that a new table be + * created. + * + * @param client a connected, operational client instance. + * @param lobbyOid the oid of the lobby that will contain the newly + * created table. + * @param tableConfig the table configuration parameters. + * @param config the game config for the game to be matchmade by the + * table. + * @param listener will receive and process the response. + */ + public void createTable (Client client, int lobbyOid, + TableConfig tableConfig, GameConfig config, TableListener listener); + + /** + * You probably don't want to call this directly, but want to call + * {@link TableDirector#joinTable}. Requests that the current user + * be added to the specified table at the specified position. + * + * @param client a connected, operational client instance. + * @param lobbyOid the oid of the lobby that contains the table. + * @param tableId the unique id of the table to which this user wishes + * to be added. + * @param position the position at the table to which this user desires + * to be added. + * @param listener will receive and process the response. + */ + public void joinTable (Client client, int lobbyOid, int tableId, + int position, InvocationListener listener); + + /** + * You probably don't want to call this directly, but want to call + * {@link TableDirector#leaveTable}. Requests that the current user be + * removed from the specified table. + * + * @param client a connected, operational client instance. + * @param lobbyOid the oid of the lobby that contains the table. + * @param tableId the unique id of the table from which this user + * wishes to be removed. + * @param listener will receive and process the response. + */ + public void leaveTable (Client client, int lobbyOid, int tableId, + InvocationListener listener); + + /** + * Requests to start a single player game with the specified game + * configuration. + */ + public void startSolitaire (Client client, GameConfig config, + ConfirmListener listener); +} diff --git a/src/java/com/threerings/parlor/client/SeatednessObserver.java b/src/java/com/threerings/parlor/client/SeatednessObserver.java new file mode 100644 index 00000000..11fb100f --- /dev/null +++ b/src/java/com/threerings/parlor/client/SeatednessObserver.java @@ -0,0 +1,38 @@ +// +// $Id: SeatednessObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +/** + * Entites that wish to hear about when we sit down at a table or stand up + * from a table can implement this interface and register themselves with + * the {@link TableDirector}. + */ +public interface SeatednessObserver +{ + /** + * Called when this client sits down at or stands up from a table. + * + * @param isSeated true if the client is now seated at a table, false + * if they are now no longer seated at a table. + */ + public void seatednessDidChange (boolean isSeated); +} diff --git a/src/java/com/threerings/parlor/client/TableConfigurator.java b/src/java/com/threerings/parlor/client/TableConfigurator.java new file mode 100644 index 00000000..c1259d87 --- /dev/null +++ b/src/java/com/threerings/parlor/client/TableConfigurator.java @@ -0,0 +1,120 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.parlor.util.ParlorContext; + +import com.threerings.parlor.data.TableConfig; + +import com.threerings.parlor.game.client.GameConfigurator; + +/** + * This should be implemented some user-interface element that allows + * the user to configure whichever TableConfig options are relevant. + */ +public abstract class TableConfigurator +{ + /** + * Create a TableConfigurator. + */ + public TableConfigurator () + { + _config = createTableConfig(); + } + + /** + * Optionally, you can set a pre-configured TableConfig + * that will be used to initialize the display parameters (if possible). + * This should be called prior to init(). + */ + public void setTableConfig (TableConfig config) + { + if (config != null) { + _config = config; + } + } + + /** + * Initialize the TableConfigurator. + * + * At the time this is called, the GameConfigurator should have + * already been initialized. + */ + public void init (ParlorContext ctx, GameConfigurator gameConfigurator) + { + _ctx = ctx; + _gameConfigurator = gameConfigurator; + createConfigInterface(); + } + + /** + * Create the table config object that will be used. + */ + protected TableConfig createTableConfig () + { + return new TableConfig(); + } + + /** + * Create the config interface. + */ + protected void createConfigInterface () + { + // nothing by default + } + + /** + * If true, the TableConfigurator is empty, which doesn't mean that + * it will not return a TableConfig object (for it must), but rather + * that there are no user-editable options being presented in the + * config interface. + */ + public abstract boolean isEmpty (); + + /** + * Return the fully configured table config according to the currently + * configured user interface elements. + */ + public TableConfig getTableConfig () + { + flushTableConfig(); + return _config; + } + + /** + * Derived classes will want to override this method, flushing + * values from the user interface to the table config object. + */ + protected void flushTableConfig () + { + // nothing by default + } + + /** Provides access to client services. */ + protected ParlorContext _ctx; + + /** The config we're configurating. */ + protected TableConfig _config; + + /** The game configurator, which we may wish to reference. */ + protected GameConfigurator _gameConfigurator; +} diff --git a/src/java/com/threerings/parlor/client/TableDirector.java b/src/java/com/threerings/parlor/client/TableDirector.java new file mode 100644 index 00000000..8f55203e --- /dev/null +++ b/src/java/com/threerings/parlor/client/TableDirector.java @@ -0,0 +1,363 @@ +// +// $Id: TableDirector.java 3758 2005-11-10 23:18:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import java.util.ArrayList; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; + +import com.threerings.presents.dobj.EntryAddedEvent; +import com.threerings.presents.dobj.EntryRemovedEvent; +import com.threerings.presents.dobj.EntryUpdatedEvent; +import com.threerings.presents.dobj.SetListener; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.Log; +import com.threerings.parlor.data.Table; +import com.threerings.parlor.data.TableConfig; +import com.threerings.parlor.data.TableLobbyObject; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.util.ParlorContext; + +/** + * As tables are created and managed within the scope of a place (a + * lobby), we want to fold the table management functionality into the + * standard hierarchy of place controllers that deal with place-related + * functionality on the client. Thus, instead of forcing places that + * expect to have tables to extend a TableLobbyController or + * something similar, we instead provide the table director which can be + * instantiated by the place controller (or specific table related views) + * to handle the table matchmaking services. + * + *

Entites that do so, will need to implement the {@link + * TableObserver} interface so that the table director can notify them + * when table related things happen. + * + *

The table services expect that the place object being used as a + * lobby in which the table matchmaking takes place implements the {@link + * TableLobbyObject} interface. + */ +public class TableDirector extends BasicDirector + implements SetListener, ParlorService.TableListener +{ + /** + * Creates a new table director to manage tables with the specified + * observer which will receive callbacks when interesting table + * related things happen. + * + * @param ctx the parlor context in use by the client. + * @param tableField the field name of the distributed set that + * contains the tables we will be managing. + * @param observer the entity that will receive callbacks when things + * happen to the tables. + */ + public TableDirector ( + ParlorContext ctx, String tableField, TableObserver observer) + { + super(ctx); + + // keep track of this stuff + _ctx = ctx; + _tableField = tableField; + _observer = observer; + } + + /** + * This must be called by the entity that uses the table director when + * the using entity prepares to enter and display a place. It is + * assumed that the client is already subscribed to the provided place + * object. + */ + public void willEnterPlace (PlaceObject place) + { + // add ourselves as a listener to the place object + place.addListener(this); + + // and remember this for later + _lobby = place; + } + + /** + * This must be called by the entity that uses the table director when + * the using entity has left and is done displaying a place. + */ + public void didLeavePlace (PlaceObject place) + { + // remove our listenership + place.removeListener(this); + + // clear out our lobby reference + _lobby = null; + } + + /** + * Requests that the specified observer be added to the list of + * observers that are notified when this client sits down at or stands + * up from a table. + */ + public void addSeatednessObserver (SeatednessObserver observer) + { + _seatedObservers.add(observer); + } + + /** + * Requests that the specified observer be removed from to the list of + * observers that are notified when this client sits down at or stands + * up from a table. + */ + public void removeSeatednessObserver (SeatednessObserver observer) + { + _seatedObservers.remove(observer); + } + + /** + * Returns true if this client is currently seated at a table, false + * if they are not. + */ + public boolean isSeated () + { + return (_ourTable != null); + } + + /** + * Sends a request to create a table with the specified game + * configuration. This user will become the owner of this table and + * will be added to the first position in the table. The response will + * be communicated via the {@link TableObserver} interface. + */ + public void createTable (TableConfig tableConfig, GameConfig config) + { + // if we're already in a table, refuse the request + if (_ourTable != null) { + Log.warning("Ignoring request to create table as we're " + + "already in a table [table=" + _ourTable + "]."); + return; + } + + // make sure we're currently in a place + if (_lobby == null) { + Log.warning("Requested to create a table but we're not " + + "currently in a place [config=" + config + "]."); + return; + } + + // go ahead and issue the create request + _pservice.createTable(_ctx.getClient(), _lobby.getOid(), tableConfig, + config, this); + } + + /** + * Sends a request to join the specified table at the specified + * position. The response will be communicated via the {@link + * TableObserver} interface. + */ + public void joinTable (int tableId, int position) + { + // if we're already in a table, refuse the request + if (_ourTable != null) { + Log.warning("Ignoring request to join table as we're " + + "already in a table [table=" + _ourTable + "]."); + return; + } + + // make sure we're currently in a place + if (_lobby == null) { + Log.warning("Requested to join a table but we're not " + + "currently in a place [tableId=" + tableId + "]."); + return; + } + + // issue the join request + _pservice.joinTable(_ctx.getClient(), _lobby.getOid(), tableId, + position, this); + } + + /** + * Sends a request to leave the specified table at which we are + * presumably seated. The response will be communicated via the {@link + * TableObserver} interface. + */ + public void leaveTable (int tableId) + { + // make sure we're currently in a place + if (_lobby == null) { + Log.warning("Requested to leave a table but we're not " + + "currently in a place [tableId=" + tableId + "]."); + return; + } + + // issue the leave request + _pservice.leaveTable(_ctx.getClient(), _lobby.getOid(), tableId, this); + } + + // documentation inherited + public void clientDidLogoff (Client client) + { + super.clientDidLogoff(client); + _pservice = null; + _lobby = null; + _ourTable = null; + } + + // documentation inherited + protected void fetchServices (Client client) + { + // get a handle on our parlor services + _pservice = (ParlorService)client.requireService(ParlorService.class); + } + + // documentation inherited + public void entryAdded (EntryAddedEvent event) + { + if (event.getName().equals(_tableField)) { + Table table = (Table)event.getEntry(); + + // check to see if we just joined a table + checkSeatedness(table); + + // now let the observer know what's up + _observer.tableAdded(table); + } + } + + // documentation inherited + public void entryUpdated (EntryUpdatedEvent event) + { + if (event.getName().equals(_tableField)) { + Table table = (Table)event.getEntry(); + + // check to see if we just joined or left a table + checkSeatedness(table); + + // now let the observer know what's up + _observer.tableUpdated(table); + } + } + + // documentation inherited + public void entryRemoved (EntryRemovedEvent event) + { + if (event.getName().equals(_tableField)) { + Integer tableId = (Integer)event.getKey(); + + // check to see if our table just disappeared + if (_ourTable != null && tableId.equals(_ourTable.tableId)) { + _ourTable = null; + notifySeatedness(false); + } + + // now let the observer know what's up + _observer.tableRemoved(tableId.intValue()); + } + } + + // documentation inherited from interface + public void tableCreated (int tableId) + { + // nothing much to do here + Log.info("Table creation succeeded [tableId=" + tableId + "]."); + } + + // documentation inherited from interface + public void requestFailed (String reason) + { + Log.warning("Table creation failed [reason=" + reason + "]."); + } + + /** + * Checks to see if we're a member of this table and notes it as our + * table, if so. + */ + protected void checkSeatedness (Table table) + { + Table oldTable = _ourTable; + + // if this is the same table as our table, clear out our table + // reference and allow it to be added back if we are still in the + // table + if (table.equals(_ourTable)) { + _ourTable = null; + } + + // look for our username in the occupants array + BodyObject self = (BodyObject)_ctx.getClient().getClientObject(); + for (int i = 0; i < table.occupants.length; i++) { + if (self.getVisibleName().equals(table.occupants[i])) { + _ourTable = table; + break; + } + } + + // if nothing changed, bail now + if (oldTable == _ourTable || + (oldTable != null && oldTable.equals(_ourTable))) { + return; + } + + // otherwise notify the observers + notifySeatedness(_ourTable != null); + } + + /** + * Notifies the seatedness observers of a seatedness change. + */ + protected void notifySeatedness (boolean isSeated) + { + int slength = _seatedObservers.size(); + for (int i = 0; i < slength; i++) { + SeatednessObserver observer = (SeatednessObserver) + _seatedObservers.get(i); + try { + observer.seatednessDidChange(isSeated); + } catch (Exception e) { + Log.warning("Observer choked in seatednessDidChange() " + + "[observer=" + observer + "]."); + Log.logStackTrace(e); + } + } + } + + /** A context by which we can access necessary client services. */ + protected ParlorContext _ctx; + + /** Parlor server-side services. */ + protected ParlorService _pservice; + + /** The place object in which we're currently managing tables. */ + protected PlaceObject _lobby; + + /** The field name of the distributed set that contains our tables. */ + protected String _tableField; + + /** The entity that we talk to when table stuff happens. */ + protected TableObserver _observer; + + /** The table of which we are a member if any. */ + protected Table _ourTable; + + /** An array of entities that want to hear about when we stand up or + * sit down. */ + protected ArrayList _seatedObservers = new ArrayList(); +} diff --git a/src/java/com/threerings/parlor/client/TableObserver.java b/src/java/com/threerings/parlor/client/TableObserver.java new file mode 100644 index 00000000..c49991c9 --- /dev/null +++ b/src/java/com/threerings/parlor/client/TableObserver.java @@ -0,0 +1,48 @@ +// +// $Id: TableObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.client; + +import com.threerings.parlor.data.Table; + +/** + * The {@link TableDirector} converts distributed object events into + * higher level callbacks to implementers of this interface, which are + * expected to render these events sensically in a user interface. + */ +public interface TableObserver +{ + /** + * Called when a new table is created. + */ + public void tableAdded (Table table); + + /** + * Called when something has changed about a table (occupant list + * updated, state changed from matchmaking to in-play, etc.). + */ + public void tableUpdated (Table table); + + /** + * Called when a table goes away. + */ + public void tableRemoved (int tableId); +} diff --git a/src/java/com/threerings/parlor/data/ParlorCodes.java b/src/java/com/threerings/parlor/data/ParlorCodes.java new file mode 100644 index 00000000..f7e0def8 --- /dev/null +++ b/src/java/com/threerings/parlor/data/ParlorCodes.java @@ -0,0 +1,62 @@ +// +// $Id: ParlorCodes.java 3359 2005-02-19 22:38:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.data; + +import com.threerings.presents.data.InvocationCodes; + +/** + * Contains codes used by the parlor invocation services. + */ +public interface ParlorCodes extends InvocationCodes +{ + /** The response code for an accepted invitation. */ + public static final int INVITATION_ACCEPTED = 0; + + /** The response code for a refused invitation. */ + public static final int INVITATION_REFUSED = 1; + + /** The response code for a countered invitation. */ + public static final int INVITATION_COUNTERED = 2; + + /** An error code explaining that an invitation was rejected because + * the invited user was not online at the time the invitation was + * received. */ + public static final String INVITEE_NOT_ONLINE = "m.invitee_not_online"; + + /** An error code returned when a user requests to join a table that + * doesn't exist. */ + public static final String NO_SUCH_TABLE = "m.no_such_table"; + + /** An error code returned when a user requests to join a table at a + * position that is not valid. */ + public static final String INVALID_TABLE_POSITION = + "m.invalid_table_position"; + + /** An error code returned when a user requests to join a table in a + * position that is already occupied. */ + public static final String TABLE_POSITION_OCCUPIED = + "m.table_position_occupied"; + + /** An error code returned when a user requests to leave a table that + * they were not sitting at in the first place. */ + public static final String NOT_AT_TABLE = "m.not_at_table"; +} diff --git a/src/java/com/threerings/parlor/data/ParlorMarshaller.java b/src/java/com/threerings/parlor/data/ParlorMarshaller.java new file mode 100644 index 00000000..29c87e08 --- /dev/null +++ b/src/java/com/threerings/parlor/data/ParlorMarshaller.java @@ -0,0 +1,200 @@ +// +// $Id: ParlorMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.data; + +import com.threerings.parlor.client.ParlorService; +import com.threerings.parlor.data.TableConfig; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import com.threerings.util.Name; + +/** + * Provides the implementation of the {@link ParlorService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class ParlorMarshaller extends InvocationMarshaller + implements ParlorService +{ + // documentation inherited + public static class InviteMarshaller extends ListenerMarshaller + implements InviteListener + { + /** The method id used to dispatch {@link #inviteReceived} + * responses. */ + public static final int INVITE_RECEIVED = 1; + + // documentation inherited from interface + public void inviteReceived (int arg1) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, INVITE_RECEIVED, + new Object[] { Integer.valueOf(arg1) })); + } + + // documentation inherited + public void dispatchResponse (int methodId, Object[] args) + { + switch (methodId) { + case INVITE_RECEIVED: + ((InviteListener)listener).inviteReceived( + ((Integer)args[0]).intValue()); + return; + + default: + super.dispatchResponse(methodId, args); + return; + } + } + } + + // documentation inherited + public static class TableMarshaller extends ListenerMarshaller + implements TableListener + { + /** The method id used to dispatch {@link #tableCreated} + * responses. */ + public static final int TABLE_CREATED = 1; + + // documentation inherited from interface + public void tableCreated (int arg1) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, TABLE_CREATED, + new Object[] { Integer.valueOf(arg1) })); + } + + // documentation inherited + public void dispatchResponse (int methodId, Object[] args) + { + switch (methodId) { + case TABLE_CREATED: + ((TableListener)listener).tableCreated( + ((Integer)args[0]).intValue()); + return; + + default: + super.dispatchResponse(methodId, args); + return; + } + } + } + + /** The method id used to dispatch {@link #cancel} requests. */ + public static final int CANCEL = 1; + + // documentation inherited from interface + public void cancel (Client arg1, int arg2, InvocationService.InvocationListener arg3) + { + ListenerMarshaller listener3 = new ListenerMarshaller(); + listener3.listener = arg3; + sendRequest(arg1, CANCEL, new Object[] { + Integer.valueOf(arg2), listener3 + }); + } + + /** The method id used to dispatch {@link #createTable} requests. */ + public static final int CREATE_TABLE = 2; + + // documentation inherited from interface + public void createTable (Client arg1, int arg2, TableConfig arg3, GameConfig arg4, ParlorService.TableListener arg5) + { + ParlorMarshaller.TableMarshaller listener5 = new ParlorMarshaller.TableMarshaller(); + listener5.listener = arg5; + sendRequest(arg1, CREATE_TABLE, new Object[] { + Integer.valueOf(arg2), arg3, arg4, listener5 + }); + } + + /** The method id used to dispatch {@link #invite} requests. */ + public static final int INVITE = 3; + + // documentation inherited from interface + public void invite (Client arg1, Name arg2, GameConfig arg3, ParlorService.InviteListener arg4) + { + ParlorMarshaller.InviteMarshaller listener4 = new ParlorMarshaller.InviteMarshaller(); + listener4.listener = arg4; + sendRequest(arg1, INVITE, new Object[] { + arg2, arg3, listener4 + }); + } + + /** The method id used to dispatch {@link #joinTable} requests. */ + public static final int JOIN_TABLE = 4; + + // documentation inherited from interface + public void joinTable (Client arg1, int arg2, int arg3, int arg4, InvocationService.InvocationListener arg5) + { + ListenerMarshaller listener5 = new ListenerMarshaller(); + listener5.listener = arg5; + sendRequest(arg1, JOIN_TABLE, new Object[] { + Integer.valueOf(arg2), Integer.valueOf(arg3), Integer.valueOf(arg4), listener5 + }); + } + + /** The method id used to dispatch {@link #leaveTable} requests. */ + public static final int LEAVE_TABLE = 5; + + // documentation inherited from interface + public void leaveTable (Client arg1, int arg2, int arg3, InvocationService.InvocationListener arg4) + { + ListenerMarshaller listener4 = new ListenerMarshaller(); + listener4.listener = arg4; + sendRequest(arg1, LEAVE_TABLE, new Object[] { + Integer.valueOf(arg2), Integer.valueOf(arg3), listener4 + }); + } + + /** The method id used to dispatch {@link #respond} requests. */ + public static final int RESPOND = 6; + + // documentation inherited from interface + public void respond (Client arg1, int arg2, int arg3, Object arg4, InvocationService.InvocationListener arg5) + { + ListenerMarshaller listener5 = new ListenerMarshaller(); + listener5.listener = arg5; + sendRequest(arg1, RESPOND, new Object[] { + Integer.valueOf(arg2), Integer.valueOf(arg3), arg4, listener5 + }); + } + + /** The method id used to dispatch {@link #startSolitaire} requests. */ + public static final int START_SOLITAIRE = 7; + + // documentation inherited from interface + public void startSolitaire (Client arg1, GameConfig arg2, InvocationService.ConfirmListener arg3) + { + InvocationMarshaller.ConfirmMarshaller listener3 = new InvocationMarshaller.ConfirmMarshaller(); + listener3.listener = arg3; + sendRequest(arg1, START_SOLITAIRE, new Object[] { + arg2, listener3 + }); + } + +} diff --git a/src/java/com/threerings/parlor/data/Table.java b/src/java/com/threerings/parlor/data/Table.java new file mode 100644 index 00000000..85a3c471 --- /dev/null +++ b/src/java/com/threerings/parlor/data/Table.java @@ -0,0 +1,402 @@ +// +// $Id: Table.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.data; + +import com.samskivert.util.ArrayIntSet; +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.util.Name; + +import com.threerings.presents.dobj.DSet; + +import com.threerings.crowd.data.BodyObject; + +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.data.PartyGameConfig; + +/** + * This class represents a table that is being used to matchmake a game by + * the Parlor services. + */ +public class Table + implements DSet.Entry, ParlorCodes +{ + /** The unique identifier for this table. */ + public Integer tableId; + + /** The object id of the lobby object with which this table is + * associated. */ + public int lobbyOid; + + /** The oid of the game that was created from this table or -1 if the + * table is still in matchmaking mode. */ + public int gameOid = -1; + + /** An array of the usernames of the occupants of this table (some + * slots may not be filled), or null if a party game. */ + public Name[] occupants; + + /** The body oids of the occupants of this table, or null if a party game. + * (This is not propagated to remote instances.) */ + public transient int[] bodyOids; + + /** The game config for the game that is being matchmade. */ + public GameConfig config; + + /** The table configuration object. */ + public TableConfig tconfig; + + /** + * Creates a new table instance, and assigns it the next monotonically + * increasing table id. + * + * @param lobbyOid the object id of the lobby in which this table is + * to live. + * @param tconfig the table configuration for this table. + * @param config the configuration of the game being matchmade by this + * table. + */ + public Table (int lobbyOid, TableConfig tconfig, GameConfig config) + { + // assign a unique table id + tableId = Integer.valueOf(++_tableIdCounter); + + // keep track of our lobby oid + this.lobbyOid = lobbyOid; + + // keep a casted reference around + this.tconfig = tconfig; + this.config = config; + + // make room for the maximum number of players + if (tconfig.desiredPlayerCount != -1) { + occupants = new Name[tconfig.desiredPlayerCount]; + bodyOids = new int[occupants.length]; + + // fill in information on the AIs + int acount = (config.ais == null) ? 0 : config.ais.length; + for (int ii = 0; ii < acount; ii++) { + // TODO: handle this naming business better + occupants[ii] = new Name("AI " + (ii+1)); + } + } + } + + /** + * Constructs a blank table instance, suitable for unserialization. + */ + public Table () + { + } + + /** + * A convenience function for accessing the table id as an int. + */ + public int getTableId () + { + return tableId.intValue(); + } + + /** + * Returns true if there is no one sitting at this table. + */ + public boolean isEmpty () + { + for (int i = 0; i < bodyOids.length; i++) { + if (bodyOids[i] != 0) { + return false; + } + } + return true; + } + + /** + * Count the number of players currently occupying this table. + */ + public int getOccupiedCount () + { + int count = 0; + for (int ii = 0; ii < occupants.length; ii++) { + if (occupants[ii] != null) { + count++; + } + } + return count; + } + + /** + * Once a table is ready to play (see {@link #mayBeStarted} and {@link + * #shouldBeStarted}), the players array can be fetched using this + * method. It will return an array containing the usernames of all of + * the players in the game, sized properly and with each player in the + * appropriate position. + */ + public Name[] getPlayers () + { + if (isPartyGame()) { + return occupants; + } + + // create and populate the players array + Name[] players = new Name[getOccupiedCount()]; + for (int ii = 0, dex = 0; ii < occupants.length; ii++) { + if (occupants[ii] != null) { + players[dex++] = occupants[ii]; + } + } + + return players; + } + + /** + * For a team game, get the team member indices of the compressed + * players array returned by getPlayers(). + */ + public int[][] getTeamMemberIndices () + { + int[][] teams = tconfig.teamMemberIndices; + if (teams == null) { + return null; + } + + // compress the team indexes down + ArrayIntSet set = new ArrayIntSet(); + int[][] newTeams = new int[teams.length][]; + Name[] players = getPlayers(); + for (int ii=0; ii < teams.length; ii++) { + set.clear(); + for (int jj=0; jj < teams[ii].length; jj++) { + Name occ = occupants[teams[ii][jj]]; + if (occ != null) { + set.add(ListUtil.indexOf(players, occ)); + } + } + newTeams[ii] = set.toIntArray(); + } + + return newTeams; + } + + /** + * Return true if the game is a party game. + */ + public boolean isPartyGame () + { + return (PartyGameConfig.NOT_PARTY_GAME != getPartyGameType()); + } + + /** + * Get the type of party game being played at this table, or + * PartyGameConfig.NOT_PARTY_GAME. + */ + public byte getPartyGameType () + { + if (config instanceof PartyGameConfig) { + return ((PartyGameConfig) config).getPartyGameType(); + + } else { + return PartyGameConfig.NOT_PARTY_GAME; + } + } + + /** + * Requests to seat the specified user at the specified position in + * this table. + * + * @param position the position in which to seat the user. + * @param occupant the occupant to set. + * + * @return null if the user was successfully seated, a string error + * code explaining the failure if the user was not able to be seated + * at that position. + */ + public String setOccupant (int position, BodyObject occupant) + { + // make sure the requested position is a valid one + if (position >= tconfig.desiredPlayerCount || position < 0) { + return INVALID_TABLE_POSITION; + } + + // make sure the requested position is not already occupied + if (occupants[position] != null) { + return TABLE_POSITION_OCCUPIED; + } + + // otherwise all is well, stick 'em in + setOccupantPos(position, occupant); + return null; + } + + /** + * This method is used for party games, it does no bounds checking + * or verification of the player's ability to join, if you are unsure + * you should call 'setOccupant'. + */ + public void setOccupantPos (int position, BodyObject occupant) + { + occupants[position] = occupant.getVisibleName(); + bodyOids[position] = occupant.getOid(); + } + + /** + * Requests that the specified user be removed from their seat at this + * table. + * + * @return true if the user was seated at the table and has now been + * removed, false if the user was never seated at the table in the + * first place. + */ + public boolean clearOccupant (Name username) + { + for (int i = 0; i < occupants.length; i++) { + if (username.equals(occupants[i])) { + clearOccupantPos(i); + return true; + } + } + return false; + } + + /** + * Requests that the user identified by the specified body object id + * be removed from their seat at this table. + * + * @return true if the user was seated at the table and has now been + * removed, false if the user was never seated at the table in the + * first place. + */ + public boolean clearOccupant (int bodyOid) + { + for (int i = 0; i < bodyOids.length; i++) { + if (bodyOid == bodyOids[i]) { + clearOccupantPos(i); + return true; + } + } + return false; + } + + /** + * Called to clear an occupant at the specified position. + * Only call this method if you know what you're doing. + */ + public void clearOccupantPos (int position) + { + occupants[position] = null; + bodyOids[position] = 0; + } + + /** + * Returns true if this table has a sufficient number of occupants + * that the game can be started. + */ + public boolean mayBeStarted () + { + if (tconfig.teamMemberIndices == null) { + // for a normal game, just check to see if we're past the minimum + return tconfig.minimumPlayerCount <= getOccupiedCount(); + + } else { + // for a team game, make sure each team has the minimum players + int[][] teams = tconfig.teamMemberIndices; + for (int ii=0; ii < teams.length; ii++) { + int teamCount = 0; + for (int jj=0; jj < teams[ii].length; jj++) { + if (occupants[teams[ii][jj]] != null) { + teamCount++; + } + } + if (teamCount < tconfig.minimumPlayerCount) { + return false; + } + } + return true; + } + } + + /** + * Returns true if sufficient seats are occupied that the game should + * be automatically started. + */ + public boolean shouldBeStarted () + { + return tconfig.desiredPlayerCount <= getOccupiedCount(); + } + + /** + * Returns true if this table is in play, false if it is still being + * matchmade. + */ + public boolean inPlay () + { + return gameOid != -1; + } + + // documentation inherited + public Comparable getKey () + { + return tableId; + } + + // documentation inherited + public boolean equals (Object other) + { + return (other instanceof Table) && + tableId.equals(((Table) other).tableId); + } + + // documentation inherited + public int hashCode () + { + return tableId.intValue(); + } + + /** + * Generates a string representation of this table instance. + */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append(StringUtil.shortClassName(this)); + buf.append(" ["); + toString(buf); + buf.append("]"); + return buf.toString(); + } + + /** + * Helper method for toString, ripe for overrideability. + */ + protected void toString (StringBuilder buf) + { + buf.append("tableId=").append(tableId); + buf.append(", lobbyOid=").append(lobbyOid); + buf.append(", gameOid=").append(gameOid); + buf.append(", occupants=").append(StringUtil.toString(occupants)); + buf.append(", config=").append(config); + } + + /** A counter for assigning table ids. */ + protected static int _tableIdCounter = 0; +} diff --git a/src/java/com/threerings/parlor/data/TableConfig.java b/src/java/com/threerings/parlor/data/TableConfig.java new file mode 100644 index 00000000..6dd56496 --- /dev/null +++ b/src/java/com/threerings/parlor/data/TableConfig.java @@ -0,0 +1,49 @@ +// +// $Id: TableConfig.java 3604 2005-06-17 20:25:28Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.data; + +import com.threerings.io.SimpleStreamableObject; + +/** + * Table configuration parameters for a game that is to be matchmade + * using the table services. + */ +public class TableConfig extends SimpleStreamableObject +{ + /** The total number of players that are desired for the table, + * or -1 for a party game. For team games, this should be set to the + * total number of players overall, as teams may be unequal. */ + public int desiredPlayerCount; + + /** The minimum number of players needed overall (or per-team if a + * team-based game) for the game to start at the creator's discretion. */ + public int minimumPlayerCount; + + /** If non-null, indicates that this is a team-based game and contains + * the team assignments for each player. For example, a game with + * three players in two teams- players 0 and 2 versus player 1- would + * have { {0, 2}, {1} }; */ + public int[][] teamMemberIndices; + + /** Whether the table is "private". */ + public boolean privateTable; +} diff --git a/src/java/com/threerings/parlor/data/TableLobbyObject.java b/src/java/com/threerings/parlor/data/TableLobbyObject.java new file mode 100644 index 00000000..a5bc4c80 --- /dev/null +++ b/src/java/com/threerings/parlor/data/TableLobbyObject.java @@ -0,0 +1,56 @@ +// +// $Id: TableLobbyObject.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.data; + +import com.threerings.presents.dobj.DSet; + +/** + * This interface must be implemented by the place object used by a lobby + * that wishes to make use of the table services. + */ +public interface TableLobbyObject +{ + /** + * Returns a reference to the distributed set instance that will be + * holding the tables. + */ + public DSet getTables (); + + /** + * Adds the supplied table instance to the tables set (using the + * appropriate distributed object mechanisms). + */ + public void addToTables (Table table); + + /** + * Updates the value of the specified table instance in the tables + * distributed set (using the appropriate distributed object + * mechanisms). + */ + public void updateTables (Table table); + + /** + * Removes the table instance that matches the specified key from the + * tables set (using the appropriate distributed object mechanisms). + */ + public void removeFromTables (Comparable key); +} diff --git a/src/java/com/threerings/parlor/game/client/GameConfigurator.java b/src/java/com/threerings/parlor/game/client/GameConfigurator.java new file mode 100644 index 00000000..4936b9f6 --- /dev/null +++ b/src/java/com/threerings/parlor/game/client/GameConfigurator.java @@ -0,0 +1,103 @@ +// +// $Id: GameConfigurator.java 3590 2005-06-08 23:20:18Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.client; + +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.util.ParlorContext; + +/** + * Provides the base from which interfaces can be built to configure games + * prior to starting them. Derived classes would extend the base + * configurator adding interface elements and wiring them up properly to + * allow the user to configure an instance of their game. + * + *

Clients that use the game configurator will want to instantiate one + * based on the class returned from the {@link GameConfig} and then + * initialize it with a call to {@link #init}. + */ +public abstract class GameConfigurator +{ + /** + * Initializes this game configurator, creates its user interface + * elements and prepares it for display. + */ + public void init (ParlorContext ctx) + { + // save this for later + _ctx = ctx; + + // create our interface elements + createConfigInterface(); + } + + /** + * The default implementation creates nothing. + */ + protected void createConfigInterface () + { + } + + /** + * Provides this configurator with its configuration. It should set up + * all of its user interface elements to reflect the configuration. + */ + public void setGameConfig (GameConfig config) + { + _config = config; + // set up the user interface + gotGameConfig(); + } + + /** + * Derived classes will likely want to override this method and + * configure their user interface elements accordingly. + */ + protected void gotGameConfig () + { + } + + /** + * Obtains a configured game configuration. + */ + public GameConfig getGameConfig () + { + // flush our changes to the config object + flushGameConfig(); + return _config; + } + + /** + * Derived classes will want to override this method, flushing values + * from the user interface to the game config object so that it is + * properly configured prior to being returned to the {@link + * #getGameConfig} caller. + */ + protected void flushGameConfig () + { + } + + /** Provides access to client services. */ + protected ParlorContext _ctx; + + /** Our game configuration. */ + protected GameConfig _config; +} diff --git a/src/java/com/threerings/parlor/game/client/GameController.java b/src/java/com/threerings/parlor/game/client/GameController.java new file mode 100644 index 00000000..901b249e --- /dev/null +++ b/src/java/com/threerings/parlor/game/client/GameController.java @@ -0,0 +1,331 @@ +// +// $Id: GameController.java 3804 2006-01-13 01:52:36Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.client; + +import java.awt.event.ActionEvent; + +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; + +import com.threerings.crowd.client.PlaceController; +import com.threerings.crowd.client.PlaceControllerDelegate; +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.parlor.Log; +import com.threerings.parlor.game.data.GameCodes; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.game.data.PartyGameConfig; +import com.threerings.parlor.util.ParlorContext; + +/** + * The game controller manages the flow and control of a game on the + * client side. This class serves as the root of a hierarchy of controller + * classes that aim to provide functionality shared between various + * similar games. The base controller provides functionality for starting + * and ending the game and for calculating ratings adjustements when a + * game ends normally. It also handles the basic house keeping like + * subscription to the game object and dispatch of commands and + * distributed object events. + */ +public abstract class GameController extends PlaceController + implements AttributeChangeListener +{ + /** + * Initializes this game controller with the game configuration that + * was established during the match making process. Derived classes + * may want to override this method to initialize themselves with + * game-specific configuration parameters but they should be sure to + * call super.init in such cases. + * + * @param ctx the client context. + * @param config the configuration of the game we are intended to + * control. + */ + public void init (CrowdContext ctx, PlaceConfig config) + { + // cast our references before we call super.init() so that when + // super.init() calls createPlaceView(), we have our casted + // references already in place + _ctx = (ParlorContext)ctx; + _config = (GameConfig)config; + + super.init(ctx, config); + } + + /** + * Adds this controller as a listener to the game object (thus derived + * classes need not do so) and lets the game manager know that we are + * now ready to go. + */ + public void willEnterPlace (PlaceObject plobj) + { + super.willEnterPlace(plobj); + + // obtain a casted reference + _gobj = (GameObject)plobj; + + // if this place object is not our current location we'll need to + // add it as an auxiliary chat source + BodyObject bobj = (BodyObject)_ctx.getClient().getClientObject(); + if (bobj.location != plobj.getOid()) { + _ctx.getChatDirector().addAuxiliarySource( + _gobj, GameCodes.GAME_CHAT_TYPE); + } + + // and add ourselves as a listener + _gobj.addListener(this); + + // we don't want to claim to be finished until any derived classes + // that overrode this method have executed, so we'll queue up a + // runnable here that will let the game manager know that we're + // ready on the next pass through the distributed event loop + Log.info("Entering game " + _gobj.which() + "."); + if (_gobj.getPlayerIndex(bobj.getVisibleName()) != -1) { + _ctx.getClient().getRunQueue().postRunnable(new Runnable() { + public void run () { + // finally let the game manager know that we're ready + // to roll + playerReady(); + } + }); + } + } + + /** + * Removes our listener registration from the game object and cleans + * house. + */ + public void didLeavePlace (PlaceObject plobj) + { + super.didLeavePlace(plobj); + + _ctx.getChatDirector().removeAuxiliarySource(_gobj); + + // unlisten to the game object + _gobj.removeListener(this); + _gobj = null; + } + + /** + * Returns whether the game is over. + */ + public boolean isGameOver () + { + boolean gameOver = + ((_gobj != null) ? (_gobj.state != GameObject.IN_PLAY) : true); + return (_gameOver || gameOver); + } + + /** + * Sets the client game over override. This is used in situations + * where we determine that the game is over before the server has + * informed us of such. + */ + public void setGameOver (boolean gameOver) + { + _gameOver = gameOver; + } + + /** + * Calls {@link #gameWillReset}, ends the current game (locally, it + * does not tell the server to end the game), and waits to receive a + * reset notification (which is simply an event setting the game state + * to IN_PLAY even though it's already set to + * IN_PLAY) from the server which will start up a new + * game. Derived classes should override {@link #gameWillReset} to + * perform any game-specific animations. + */ + public void resetGame () + { + // let derived classes do their thing + gameWillReset(); + + // end the game until we receive a new board + setGameOver(true); + } + + /** + * Returns the unique round identifier for the current round. + */ + public int getRoundId () + { + return (_gobj == null) ? -1 : _gobj.roundId; + } + + /** + * Handles basic game controller action events. Derived classes should + * be sure to call super.handleAction for events they + * don't specifically handle. + */ + public boolean handleAction (ActionEvent action) + { + return super.handleAction(action); + } + + /** + * A way for controllers to display a game-related system message. + */ + public void systemMessage (String bundle, String msg) + { + _ctx.getChatDirector().displayInfo( + bundle, msg, GameCodes.GAME_CHAT_TYPE); + } + + // documentation inherited + public void attributeChanged (AttributeChangedEvent event) + { + // deal with game state changes + if (event.getName().equals(GameObject.STATE)) { + if (!stateDidChange(event.getIntValue())) { + Log.warning("Game transitioned to unknown state " + + "[gobj=" + _gobj + + ", state=" + event.getIntValue() + "]."); + } + } + } + + /** + * Derived classes can override this method if they add additional game + * states and should handle transitions to those states, returning true to + * indicate they were handled and calling super for the normal game states. + */ + protected boolean stateDidChange (int state) + { + switch (state) { + case GameObject.IN_PLAY: + gameDidStart(); + return true; + case GameObject.GAME_OVER: + gameDidEnd(); + return true; + case GameObject.CANCELLED: + gameWasCancelled(); + return true; + } + return false; + } + + /** + * Called after we've entered the game and everything has initialized + * to notify the server that we, as a player, are ready to play. + */ + protected void playerReady () + { + Log.info("Reporting ready " + _gobj.which() + "."); + _gobj.gameService.playerReady(_ctx.getClient()); + } + + /** + * Called when the game transitions to the IN_PLAY + * state. This happens when all of the players have arrived and the + * server starts the game. + */ + protected void gameDidStart () + { + if (_gobj == null) { + Log.info("Received gameDidStart() after leaving game room."); + return; + } + + // clear out our game over flag + setGameOver(false); + + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((GameControllerDelegate)delegate).gameDidStart(); + } + }); + } + + /** + * Called when the game transitions to the GAME_OVER + * state. This happens when the game reaches some end condition by + * normal means (is not cancelled or aborted). + */ + protected void gameDidEnd () + { + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((GameControllerDelegate)delegate).gameDidEnd(); + } + }); + } + + /** + * Called when the game was cancelled for some reason. + */ + protected void gameWasCancelled () + { + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((GameControllerDelegate)delegate).gameWasCancelled(); + } + }); + } + + /** + * Called to give derived classes a chance to display animations, send + * a final packet, or do any other business they care to do when the + * game is about to reset. + */ + protected void gameWillReset () + { + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((GameControllerDelegate)delegate).gameWillReset(); + } + }); + } + + /** + * Used to determine if this game is a party game. + */ + protected boolean isPartyGame () + { + return (_config instanceof PartyGameConfig) && + (((PartyGameConfig)_config).getPartyGameType() != + PartyGameConfig.NOT_PARTY_GAME); + } + + /** A reference to the active parlor context. */ + protected ParlorContext _ctx; + + /** Our game configuration information. */ + protected GameConfig _config; + + /** A reference to the game object for the game that we're + * controlling. */ + protected GameObject _gobj; + + /** A local flag overriding the game over state for situations where + * the client knows the game is over before the server has + * transitioned the game object accordingly. */ + protected boolean _gameOver; +} diff --git a/src/java/com/threerings/parlor/game/client/GameControllerDelegate.java b/src/java/com/threerings/parlor/game/client/GameControllerDelegate.java new file mode 100644 index 00000000..42cfacfa --- /dev/null +++ b/src/java/com/threerings/parlor/game/client/GameControllerDelegate.java @@ -0,0 +1,74 @@ +// +// $Id: GameControllerDelegate.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.client; + +import com.threerings.crowd.client.PlaceControllerDelegate; + +/** + * Extends the {@link PlaceControllerDelegate} mechanism with game + * controller specific methods. + */ +public class GameControllerDelegate extends PlaceControllerDelegate +{ + /** + * Provides the delegate with a reference to the game controller for + * which it is delegating. + */ + public GameControllerDelegate (GameController ctrl) + { + super(ctrl); + } + + /** + * Called when the game transitions to the IN_PLAY + * state. This happens when all of the players have arrived and the + * server starts the game. + */ + public void gameDidStart () + { + } + + /** + * Called when the game transitions to the GAME_OVER + * state. This happens when the game reaches some end condition by + * normal means (is not cancelled or aborted). + */ + public void gameDidEnd () + { + } + + /** + * Called when the game was cancelled for some reason. + */ + public void gameWasCancelled () + { + } + + /** + * Called to give derived classes a chance to display animations, send + * a final packet, or do any other business they care to do when the + * game is about to reset. + */ + public void gameWillReset () + { + } +} diff --git a/src/java/com/threerings/parlor/game/client/GameService.java b/src/java/com/threerings/parlor/game/client/GameService.java new file mode 100644 index 00000000..15ec7c8a --- /dev/null +++ b/src/java/com/threerings/parlor/game/client/GameService.java @@ -0,0 +1,38 @@ +// +// $Id: GameService.java 3600 2005-06-15 23:46:31Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +/** + * Provides services used by game clients to request that actions be taken + * by the game manager. + */ +public interface GameService extends InvocationService +{ + /** + * Lets the game manager know that the calling player is in the game + * room and ready to play. + */ + public void playerReady (Client client); +} diff --git a/src/java/com/threerings/parlor/game/client/SwingGameConfigurator.java b/src/java/com/threerings/parlor/game/client/SwingGameConfigurator.java new file mode 100644 index 00000000..ba10e40e --- /dev/null +++ b/src/java/com/threerings/parlor/game/client/SwingGameConfigurator.java @@ -0,0 +1,92 @@ +// +// $Id: GameConfigurator.java 3590 2005-06-08 23:20:18Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.client; + +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; + +import javax.swing.JComponent; +import javax.swing.JPanel; + +import com.samskivert.swing.VGroupLayout; + +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.util.ParlorContext; + +/** + * Provides the base from which interfaces can be built to configure games + * prior to starting them. Derived classes would extend the base + * configurator adding interface elements and wiring them up properly to + * allow the user to configure an instance of their game. + * + *

Clients that use the game configurator will want to instantiate one + * based on the class returned from the {@link GameConfig} and then + * initialize it with a call to {@link #init}. + */ +public abstract class SwingGameConfigurator extends GameConfigurator +{ + /** + * Get the Swing JPanel that contains the UI elements for this configurator. + */ + public JPanel getPanel () + { + return _panel; + } + + /** + * Add a control to the interface. This should be the standard way + * that configurator controls are added, but note also that external + * entities may add their own controls that are related to the game, + * but do not directly alter the game config, so that all the controls + * are added in a uniform manner and are well aligned. + */ + public void addControl (JComponent label, JComponent control) + { + // Set up the constraints. There's really no point in saving + // these somewhere, as they're cloned anyway with every component + // insertion. + GridBagConstraints lc = new GridBagConstraints(); + lc.gridx = 0; + lc.anchor = GridBagConstraints.NORTHWEST; + lc.insets = _labelInsets; + + GridBagConstraints cc = new GridBagConstraints(); + cc.gridx = 1; + cc.anchor = GridBagConstraints.NORTHWEST; + cc.insets = _controlInsets; + cc.gridwidth = GridBagConstraints.REMAINDER; + + // add the components + _panel.add(label, lc); + _panel.add(control, cc); + } + + /** The panel on which the config options are placed. */ + protected JPanel _panel = new JPanel(new GridBagLayout()); + + /** Insets for configuration labels. */ + protected Insets _labelInsets = new Insets(1, 0, 1, 4); + + /** Insets for configuration controls. */ + protected Insets _controlInsets = new Insets(1, 4, 1, 0); +} diff --git a/src/java/com/threerings/parlor/game/data/GameAI.java b/src/java/com/threerings/parlor/game/data/GameAI.java new file mode 100644 index 00000000..02b9ea92 --- /dev/null +++ b/src/java/com/threerings/parlor/game/data/GameAI.java @@ -0,0 +1,34 @@ +// +// $Id: GameAI.java 3399 2005-03-12 07:37:34Z mdb $ + +package com.threerings.parlor.game.data; + +import com.threerings.io.SimpleStreamableObject; + +/** + * Represents attributes of an AI player. + */ +public class GameAI extends SimpleStreamableObject +{ + /** The "personality" of the AI, which can be interpreted by + * each puzzle. */ + public int personality; + + /** The skill level of the AI. */ + public int skill; + + /** A blank constructor for serialization. */ + public GameAI () + { + } + + /** + * Constructs an AI with the specified (game-interpreted) skill and + * personality. + */ + public GameAI (int personality, int skill) + { + this.personality = personality; + this.skill = skill; + } +} diff --git a/src/java/com/threerings/parlor/game/data/GameCodes.java b/src/java/com/threerings/parlor/game/data/GameCodes.java new file mode 100644 index 00000000..e137455e --- /dev/null +++ b/src/java/com/threerings/parlor/game/data/GameCodes.java @@ -0,0 +1,44 @@ +// +// $Id: PuzzleCodes.java 3184 2004-10-28 19:20:27Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.data; + +import com.threerings.presents.data.InvocationCodes; + +/** + * Constants relating to the game services. + */ +public interface GameCodes extends InvocationCodes +{ + /** The message bundle identifier for general game messages. */ + public static final String GAME_MESSAGE_BUNDLE = "game.general"; + + /** The name of the message event to a placeObject that a player + * was knocked out of a puzzle. */ + public static final String PLAYER_KNOCKED_OUT = "playerKnocked"; + + /** The name of the message event to a placeObject that reports + * the winners and losers of a game. */ + public static final String WINNERS_AND_LOSERS = "winnersAndLosers"; + + /** A chat type for chatting on the game object. */ + public static final String GAME_CHAT_TYPE = "gameChat"; +} diff --git a/src/java/com/threerings/parlor/game/data/GameConfig.java b/src/java/com/threerings/parlor/game/data/GameConfig.java new file mode 100644 index 00000000..274c10ed --- /dev/null +++ b/src/java/com/threerings/parlor/game/data/GameConfig.java @@ -0,0 +1,152 @@ +// +// $Id: GameConfig.java 4026 2006-04-18 01:32:41Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.data; + +import java.util.ArrayList; +import java.util.List; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.util.Name; + +import com.threerings.parlor.client.TableConfigurator; +import com.threerings.parlor.game.client.GameConfigurator; + +/** + * The game config class encapsulates the configuration information for a + * particular type of game. The hierarchy of game config objects mimics the + * hierarchy of game managers and controllers. Both the game manager and game + * controller are provided with the game config object when the game is + * created. + * + *

The game config object is also the mechanism used to instantiate the + * appropriate game manager and controller. Every game must have an associated + * game config derived class that overrides {@link #createController} and + * {@link #getManagerClassName}, returning the appropriate game controller and + * manager class for that game. Thus the entire chain of events that causes a + * particular game to be created is the construction of the appropriate game + * config instance which is provided to the server as part of an invitation or + * via some other matchmaking mechanism. + */ +public abstract class GameConfig extends PlaceConfig implements Cloneable +{ + /** The usernames of the players involved in this game, or an empty + * array if such information is not needed by this particular game. */ + public Name[] players = new Name[0]; + + /** Indicates whether or not this game is rated. */ + public boolean rated = true; + + /** Configurations for AIs to be used in this game. Slots with real + * players should be null and slots with AIs should contain + * configuration for those AIs. A null array indicates no use of AIs + * at all. */ + public GameAI[] ais = new GameAI[0]; + + /** + * Returns the message bundle identifier for the bundle that should be + * used to translate the translatable strings used to describe the + * game config parameters. + */ + public abstract String getBundleName (); + + /** + * Creates a configurator that can be used to create a user interface + * for configuring this instance prior to starting the game. If no + * configuration is necessary, this method should return null. + */ + public abstract GameConfigurator createConfigurator (); + + /** + * Creates a table configurator for initializing 'table' properties + * of the game. The default implementation returns null. + */ + public TableConfigurator createTableConfigurator () + { + return null; + } + + /** + * Returns a translatable label describing this game. + */ + public String getGameName () + { + // the whole getRatingTypeId(), getGameName(), getBundleName() + // business should be cleaned up. we should have getGameIdent() + // and everything should have a default implementation using that + return "m." + getBundleName(); + } + + /** + * Returns the game rating type, if the system uses such things. + */ + public byte getRatingTypeId () + { + return (byte)-1; + } + + /** + * Returns a List of strings that describe the configuration of this + * game. Default implementation returns an empty list. + */ + public List getDescription () + { + return new ArrayList(); // nothing by default + } + + /** + * Returns true if this game config object is equal to the supplied + * object (meaning it is also a game config object and its + * configuration settings are the same as ours). + */ + public boolean equals (Object other) + { + // make sure they're of the same class + if (other.getClass() == this.getClass()) { + GameConfig that = (GameConfig) other; + return this.rated == that.rated; + + } else { + return false; + } + } + + /** + * Computes a hashcode for this game config object that supports our + * {@link #equals} implementation. Objects that are equal should have + * the same hashcode. + */ + public int hashCode () + { + // look ma, it's so sophisticated! + return getClass().hashCode() + (rated ? 1 : 0); + } + + // documentation inherited + public Object clone () + { + try { + return super.clone(); + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException("clone() failed: " + cnse); + } + } +} diff --git a/src/java/com/threerings/parlor/game/data/GameMarshaller.java b/src/java/com/threerings/parlor/game/data/GameMarshaller.java new file mode 100644 index 00000000..00fdffe8 --- /dev/null +++ b/src/java/com/threerings/parlor/game/data/GameMarshaller.java @@ -0,0 +1,50 @@ +// +// $Id: GameMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.data; + +import com.threerings.parlor.game.client.GameService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; + +/** + * Provides the implementation of the {@link GameService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class GameMarshaller extends InvocationMarshaller + implements GameService +{ + /** The method id used to dispatch {@link #playerReady} requests. */ + public static final int PLAYER_READY = 1; + + // documentation inherited from interface + public void playerReady (Client arg1) + { + sendRequest(arg1, PLAYER_READY, new Object[] { + + }); + } + +} diff --git a/src/java/com/threerings/parlor/game/data/GameObject.java b/src/java/com/threerings/parlor/game/data/GameObject.java new file mode 100644 index 00000000..81c9b8ce --- /dev/null +++ b/src/java/com/threerings/parlor/game/data/GameObject.java @@ -0,0 +1,448 @@ +// +// $Id: GameObject.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.data; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; +import com.threerings.util.Name; + +import com.threerings.crowd.data.PlaceObject; + +/** + * A game object hosts the shared data associated with a game played by + * one or more players. The game object extends the place object so that + * the game can act as a place where players actually go when playing the + * game. Only very basic information is maintained in the base game + * object. It serves as the base for a hierarchy of game object + * derivatives that handle basic gameplay for a suite of different game + * types (ie. turn based games, party games, board games, card games, + * etc.). + */ +public class GameObject extends PlaceObject +{ + // AUTO-GENERATED: FIELDS START + /** The field name of the gameService field. */ + public static final String GAME_SERVICE = "gameService"; + + /** The field name of the state field. */ + public static final String STATE = "state"; + + /** The field name of the isRated field. */ + public static final String IS_RATED = "isRated"; + + /** The field name of the isPrivate field. */ + public static final String IS_PRIVATE = "isPrivate"; + + /** The field name of the players field. */ + public static final String PLAYERS = "players"; + + /** The field name of the winners field. */ + public static final String WINNERS = "winners"; + + /** The field name of the roundId field. */ + public static final String ROUND_ID = "roundId"; + + /** The field name of the playerStatus field. */ + public static final String PLAYER_STATUS = "playerStatus"; + // AUTO-GENERATED: FIELDS END + + /** A game state constant indicating that the game has not yet started + * and is still awaiting the arrival of all of the players. */ + public static final int PRE_GAME = 0; + + /** A game state constant indicating that the game is in play. */ + public static final int IN_PLAY = 1; + + /** A game state constant indicating that the game ended normally. */ + public static final int GAME_OVER = 2; + + /** A game state constant indicating that the game was cancelled. */ + public static final int CANCELLED = 3; + + /** The player status constant for a player whose game is in play. */ + public static final int PLAYER_IN_PLAY = 0; + + /** The player status constant for a player whose has been knocked out + * of the game. NOTE: This can include a player choosing to leave a + * game prematurely. */ + public static final int PLAYER_LEFT_GAME = 1; + + /** Provides general game invocation services. */ + public GameMarshaller gameService; + + /** The game state, one of {@link #PRE_GAME}, {@link #IN_PLAY}, + * {@link #GAME_OVER}, or {@link #CANCELLED}. */ + public int state = PRE_GAME; + + /** Indicates whether or not this game is rated. */ + public boolean isRated; + + /** Indicates whether the game is "private". */ + public boolean isPrivate; + + /** The usernames of the players involved in this game. */ + public Name[] players; + + /** Whether each player in the game is a winner, or null + * if the game is not yet over. */ + public boolean[] winners; + + /** The unique round identifier for the current round. */ + public int roundId; + + /** If null, indicates that all present players are active, or for + * more complex games can be non-null to indicate the current status + * of each player in the game. The status value is one of + * {@link #PLAYER_LEFT_GAME} or {@link #PLAYER_IN_PLAY}. */ + public int[] playerStatus; + + /** + * Returns the number of players in the game. + */ + public int getPlayerCount () + { + int count = 0; + int size = players.length; + for (int ii = 0; ii < size; ii++) { + if (players[ii] != null) { + count++; + } + } + return count; + } + + /** + * Returns the number of active players in the game. + */ + public int getActivePlayerCount () + { + int count = 0; + int size = players.length; + for (int ii = 0; ii < size; ii++) { + if (isActivePlayer(ii)) { + count++; + } + } + return count; + } + + /** + * Returns whether the given player is still an active player in + * the game. (Ie. whether or not they are still participating.) + */ + public boolean isActivePlayer (int pidx) + { + return isOccupiedPlayer(pidx) && + (playerStatus == null || isActivePlayerStatus(playerStatus[pidx])); + } + + + /** + * Returns the player index of the given user in the game, or + * -1 if the player is not involved in the game. + */ + public int getPlayerIndex (Name username) + { + int size = (players == null) ? 0 : players.length; + for (int ii = 0; ii < size; ii++) { + if (players[ii] != null && players[ii].equals(username)) { + return ii; + } + } + return -1; + } + + /** + * Returns whether the game is in play. A game that is not in play + * could either be awaiting players, ended, or cancelled. + */ + public boolean isInPlay () + { + return (state == IN_PLAY); + } + + /** + * Returns whether the given player index in the game is occupied. + */ + public boolean isOccupiedPlayer (int pidx) + { + return (pidx >= 0 && pidx < players.length) && (players[pidx] != null); + } + + /** + * Returns whether the given player index is a winner, or false if the + * winners are not yet assigned. + */ + public boolean isWinner (int pidx) + { + return (winners == null) ? false : winners[pidx]; + } + + /** + * Returns the number of winners for this game, or 0 if + * the winners array is not populated, e.g., the game is not yet over. + */ + public int getWinnerCount () + { + int count = 0; + int size = (winners == null) ? 0 : winners.length; + for (int ii = 0; ii < size; ii++) { + if (winners[ii]) { + count++; + } + } + return count; + } + + /** + * Returns true if the game is ended in a draw. + */ + public boolean isDraw () + { + return getWinnerCount() == getPlayerCount(); + } + + /** + * Returns the winner index of the first winning player for this game, + * or -1 if there are no winners or the winners array is + * not yet assigned. This is only likely to be useful for games that + * are known to have a single winner. + */ + public int getWinnerIndex () + { + int size = (winners == null) ? 0 : winners.length; + for (int ii = 0; ii < size; ii++) { + if (winners[ii]) { + return ii; + } + } + return -1; + } + + /** + * Returns the type of party game being played or NOT_PARTY_GAME. + * {@link PartyGameConfig}. + */ + public byte getPartyGameType () + { + return PartyGameConfig.NOT_PARTY_GAME; + } + + /** + * Used by {@link #isActivePlayer} to determine if the supplied status is + * associated with an active player (one that has not resigned from the + * game and/or left the game room). + */ + protected boolean isActivePlayerStatus (int playerStatus) + { + return playerStatus == PLAYER_IN_PLAY; + } + + // documentation inherited + protected void which (StringBuilder buf) + { + super.which(buf); + StringUtil.toString(buf, players); + buf.append(":").append(state); + } + + // AUTO-GENERATED: METHODS START + /** + * Requests that the gameService field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setGameService (GameMarshaller value) + { + GameMarshaller ovalue = this.gameService; + requestAttributeChange( + GAME_SERVICE, value, ovalue); + this.gameService = value; + } + + /** + * Requests that the state field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setState (int value) + { + int ovalue = this.state; + requestAttributeChange( + STATE, Integer.valueOf(value), Integer.valueOf(ovalue)); + this.state = value; + } + + /** + * Requests that the isRated field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setIsRated (boolean value) + { + boolean ovalue = this.isRated; + requestAttributeChange( + IS_RATED, Boolean.valueOf(value), Boolean.valueOf(ovalue)); + this.isRated = value; + } + + /** + * Requests that the isPrivate field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setIsPrivate (boolean value) + { + boolean ovalue = this.isPrivate; + requestAttributeChange( + IS_PRIVATE, Boolean.valueOf(value), Boolean.valueOf(ovalue)); + this.isPrivate = value; + } + + /** + * Requests that the players field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setPlayers (Name[] value) + { + Name[] ovalue = this.players; + requestAttributeChange( + PLAYERS, value, ovalue); + this.players = (value == null) ? null : (Name[])value.clone(); + } + + /** + * Requests that the indexth element of + * players field be set to the specified value. + * The local value will be updated immediately and an event will be + * propagated through the system to notify all listeners that the + * attribute did change. Proxied copies of this object (on clients) + * will apply the value change when they received the attribute + * changed notification. + */ + public void setPlayersAt (Name value, int index) + { + Name ovalue = this.players[index]; + requestElementUpdate( + PLAYERS, index, value, ovalue); + this.players[index] = value; + } + + /** + * Requests that the winners field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setWinners (boolean[] value) + { + boolean[] ovalue = this.winners; + requestAttributeChange( + WINNERS, value, ovalue); + this.winners = (value == null) ? null : (boolean[])value.clone(); + } + + /** + * Requests that the indexth element of + * winners field be set to the specified value. + * The local value will be updated immediately and an event will be + * propagated through the system to notify all listeners that the + * attribute did change. Proxied copies of this object (on clients) + * will apply the value change when they received the attribute + * changed notification. + */ + public void setWinnersAt (boolean value, int index) + { + boolean ovalue = this.winners[index]; + requestElementUpdate( + WINNERS, index, Boolean.valueOf(value), Boolean.valueOf(ovalue)); + this.winners[index] = value; + } + + /** + * Requests that the roundId field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setRoundId (int value) + { + int ovalue = this.roundId; + requestAttributeChange( + ROUND_ID, Integer.valueOf(value), Integer.valueOf(ovalue)); + this.roundId = value; + } + + /** + * Requests that the playerStatus field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setPlayerStatus (int[] value) + { + int[] ovalue = this.playerStatus; + requestAttributeChange( + PLAYER_STATUS, value, ovalue); + this.playerStatus = (value == null) ? null : (int[])value.clone(); + } + + /** + * Requests that the indexth element of + * playerStatus field be set to the specified value. + * The local value will be updated immediately and an event will be + * propagated through the system to notify all listeners that the + * attribute did change. Proxied copies of this object (on clients) + * will apply the value change when they received the attribute + * changed notification. + */ + public void setPlayerStatusAt (int value, int index) + { + int ovalue = this.playerStatus[index]; + requestElementUpdate( + PLAYER_STATUS, index, Integer.valueOf(value), Integer.valueOf(ovalue)); + this.playerStatus[index] = value; + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/parlor/game/data/PartyGameConfig.java b/src/java/com/threerings/parlor/game/data/PartyGameConfig.java new file mode 100644 index 00000000..752c81b6 --- /dev/null +++ b/src/java/com/threerings/parlor/game/data/PartyGameConfig.java @@ -0,0 +1,50 @@ +// +// $Id: PartyGameConfig.java 3804 2006-01-13 01:52:36Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.data; + +/** + * Provides additional information for party games. + * A party game is a game in which the players are not set prior to starting, + * but players can come and go at will during the normal progression of + * the game. + */ +public interface PartyGameConfig +{ + /** Party game constant indicating that this game, while it does + * implement PartyGameConfig, is not currently being played in party + * mode. */ + public static final byte NOT_PARTY_GAME = 0; + + /** Party game constant indicating that we're in a party game in which + * players must sit at an available seat to play, otherwise they're + * an observer. */ + public static final byte SEATED_PARTY_GAME = 1; + + /** Party game constant indicating that everyone in the game place is + * a "player", meaning that they do not need to claim a seat to play. */ + public static final byte FREE_FOR_ALL_PARTY_GAME = 2; + + /** + * Get the type of party game being played. + */ + public byte getPartyGameType (); +} diff --git a/src/java/com/threerings/parlor/game/server/AIGameTicker.java b/src/java/com/threerings/parlor/game/server/AIGameTicker.java new file mode 100644 index 00000000..139b513b --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/AIGameTicker.java @@ -0,0 +1,109 @@ +// +// $Id: AIGameTicker.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +import java.util.Iterator; +import java.util.HashSet; + +import com.samskivert.util.Interval; + +import com.threerings.crowd.server.CrowdServer; + +/** + * Handles ticking all the GameManagers that are hosting games with AIs. + */ +public class AIGameTicker extends Interval +{ + /** The frequency with which we dispatch AI game ticks. */ + public static final long TICK_FREQUENCY = 3333L; // every 3 1/3 seconds + + /** + * Register the specified GameManager to receive AI ticks. + */ + public static synchronized void registerAIGame (GameManager mgr) + { + if (_ticker == null) { + _ticker = new AIGameTicker(); + } + _ticker.addAIGame(mgr); + } + + /** + * Take the specified GameManager off the AI tick list. + */ + public static synchronized void unregisterAIGame (GameManager mgr) + { + if (_ticker != null) { + _ticker.removeAIGame(mgr); + } + } + + /** + * Construct an AIGameTicker and start it ticking. + */ + private AIGameTicker () + { + super(CrowdServer.omgr); + _games = new HashSet(); + + schedule(TICK_FREQUENCY, true); + } + + /** + * Add the specified manager to the list of those receiving AI ticks. + */ + protected void addAIGame (GameManager mgr) + { + _games.add(mgr); + } + + /** + * Remove the specified manager from receiving AI ticks. + */ + protected void removeAIGame (GameManager mgr) + { + _games.remove(mgr); + + // if there aren't any AIs, let's stop running + if (_games.isEmpty()) { + cancel(); + _ticker = null; + } + } + + /** + * Tick all the game AIs while on the dobj thread. + */ + public void expired () + { + Iterator iter = _games.iterator(); + while (iter.hasNext()) { + ((GameManager) iter.next()).tickAIs(); + } + } + + /** Our set of ai games. */ + protected HashSet _games; + + /** Our single ticker for all AI games. */ + protected static AIGameTicker _ticker; +} diff --git a/src/java/com/threerings/parlor/game/server/GameDispatcher.java b/src/java/com/threerings/parlor/game/server/GameDispatcher.java new file mode 100644 index 00000000..f438b178 --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/GameDispatcher.java @@ -0,0 +1,69 @@ +// +// $Id: GameDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +import com.threerings.parlor.game.client.GameService; +import com.threerings.parlor.game.data.GameMarshaller; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; + +/** + * Dispatches requests to the {@link GameProvider}. + */ +public class GameDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public GameDispatcher (GameProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new GameMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case GameMarshaller.PLAYER_READY: + ((GameProvider)provider).playerReady( + source + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/parlor/game/server/GameManager.java b/src/java/com/threerings/parlor/game/server/GameManager.java new file mode 100644 index 00000000..f14607a4 --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/GameManager.java @@ -0,0 +1,1329 @@ +// +// $Id: GameManager.java 4175 2006-06-06 15:51:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; + +import com.samskivert.util.ArrayIntSet; +import com.samskivert.util.IntListUtil; +import com.samskivert.util.Interval; +import com.samskivert.util.RepeatCallTracker; +import com.samskivert.util.StringUtil; +import com.samskivert.util.Tuple; +import com.threerings.util.Name; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; +import com.threerings.presents.dobj.DObject; + +import com.threerings.crowd.chat.server.SpeakProvider; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.server.CrowdServer; +import com.threerings.crowd.server.PlaceManager; +import com.threerings.crowd.server.PlaceManagerDelegate; + +import com.threerings.parlor.Log; +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.game.data.GameAI; +import com.threerings.parlor.game.data.GameCodes; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.data.GameMarshaller; +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.game.data.PartyGameConfig; +import com.threerings.parlor.server.ParlorSender; + +import com.threerings.util.MessageBundle; + +/** + * The game manager handles the server side management of a game. It + * manipulates the game state in accordance with the logic of the game + * flow and generally manages the whole game playing process. + * + *

The game manager extends the place manager because games are + * implicitly played in a location, the players of the game implicitly + * bodies in that location. + */ +public class GameManager extends PlaceManager + implements ParlorCodes, GameCodes, GameProvider, AttributeChangeListener +{ + // documentation inherited + protected void didInit () + { + super.didInit(); + + // save off a casted reference to our config + _gameconfig = (GameConfig)_config; + + // register this game manager + _managers.add(this); + + // and start up a tick interval if we've not already got one + if (_tickInterval == null) { + _tickInterval = new Interval(CrowdServer.omgr) { + public void expired () { + tickAllGames(); + } + }; + _tickInterval.schedule(TICK_DELAY, true); + } + + // configure our AIs + for (int ii = 0; ii < _gameconfig.ais.length; ii++) { + if (_gameconfig.ais[ii] != null) { + setAI(ii, _gameconfig.ais[ii]); + } + } + } + + /** + * Returns the configuration object for the game being managed by this + * manager. + */ + public GameConfig getGameConfig () + { + return _gameconfig; + } + + /** + * Used to determine if this game is a party game. + */ + public boolean isPartyGame () + { + return (_gameconfig instanceof PartyGameConfig) && + (((PartyGameConfig)_gameconfig).getPartyGameType() != + PartyGameConfig.NOT_PARTY_GAME); + } + + /** + * Adds the given player to the game at the first available player + * index. This should only be called before the game is started, and + * is most likely to be used to add players to party games. + * + * @param player the username of the player to add to this game. + * @return the player index at which the player was added, or + * -1 if the player could not be added to the game. + */ + public int addPlayer (Name player) + { + // determine the first available player index + int pidx = -1; + for (int ii = 0; ii < getPlayerSlots(); ii++) { + if (!_gameobj.isOccupiedPlayer(ii)) { + pidx = ii; + break; + } + } + + // sanity-check the player index + if (pidx == -1) { + Log.warning("Couldn't find free player index for player " + + "[game=" + _gameobj.which() + ", player=" + player + + ", players=" + StringUtil.toString(_gameobj.players) + + "]."); + return -1; + } + + // proceed with the rest of the adding business + return (!addPlayerAt(player, pidx)) ? -1 : pidx; + } + + /** + * Adds the given player to the game at the specified player index. + * This should only be called before the game is started, and is most + * likely to be used to add players to party games. + * + * @param player the username of the player to add to this game. + * @param pidx the player index at which the player is to be added. + * @return true if the player was added successfully, false if not. + */ + public boolean addPlayerAt (Name player, int pidx) + { + // make sure the specified player index is valid + if (pidx < 0 || pidx >= getPlayerSlots()) { + Log.warning("Attempt to add player at an invalid index " + + "[game=" + _gameobj.which() + ", player=" + player + + ", pidx=" + pidx + "]."); + return false; + } + + // make sure the player index is available + if (_gameobj.players[pidx] != null) { + Log.warning("Attempt to add player at occupied index " + + "[game=" + _gameobj.which() + ", player=" + player + + ", pidx=" + pidx + "]."); + return false; + } + + // make sure the player isn't already somehow a part of the game + // to avoid any potential badness that might ensue if we added + // them more than once + if (_gameobj.getPlayerIndex(player) != -1) { + Log.warning("Attempt to add player to game that they're already " + + "playing [game=" + _gameobj.which() + + ", player=" + player + "]."); + return false; + } + + // get the player's body object + BodyObject bobj = CrowdServer.lookupBody(player); + if (bobj == null) { + Log.warning("Unable to get body object while adding player " + + "[game=" + _gameobj.which() + + ", player=" + player + "]."); + return false; + } + + // fill in the player's information + _gameobj.setPlayersAt(player, pidx); + + // increment the number of players in the game + _playerCount++; + + // save off their oid + _playerOids[pidx] = bobj.getOid(); + + // let derived classes do what they like + playerWasAdded(player, pidx); + + return true; + } + + /** + * Called when a player was added to the game. Derived classes may + * override this method to perform any game-specific actions they + * desire, but should be sure to call + * super.playerWasAdded(). + * + * @param player the username of the player added to the game. + * @param pidx the player index of the player added to the game. + */ + protected void playerWasAdded (Name player, int pidx) + { + } + + /** + * Removes the given player from the game. This is most likely to be + * used to allow players involved in a party game to leave the game + * early-on if they realize they'd rather not play for some reason. + * + * @param player the username of the player to remove from this game. + * @return true if the player was successfully removed, false if not. + */ + public boolean removePlayer (Name player) + { + // get the player's index in the player list + int pidx = _gameobj.getPlayerIndex(player); + + // sanity-check the player index + if (pidx == -1) { + Log.warning("Attempt to remove non-player from players list " + + "[game=" + _gameobj.which() + + ", player=" + player + + ", players=" + StringUtil.toString(_gameobj.players) + + "]."); + return false; + } + + // remove the player from the players list + _gameobj.setPlayersAt(null, pidx); + + // clear out the player's entry in the player oid list + _playerOids[pidx] = 0; + + if (_AIs != null) { + // clear out the player's entry in the AI list + _AIs[pidx] = null; + } + + // decrement the number of players in the game + _playerCount--; + + // let derived classes do what they like + playerWasRemoved(player, pidx); + + return true; + } + + /** + * Called when a player was removed from the game. Derived classes + * may override this method to perform any game-specific actions they + * desire, but should be sure to call + * super.playerWasRemoved(). + * + * @param player the username of the player removed from the game. + * @param pidx the player index of the player before they were removed + * from the game. + */ + protected void playerWasRemoved (Name player, int pidx) + { + } + + /** + * Replaces the player at the specified index and calls {@link + * #playerWasReplaced} to let derived classes and delegates know + * what's going on. + */ + public void replacePlayer (final int pidx, final Name player) + { + final Name oplayer = _gameobj.players[pidx]; + _gameobj.setPlayersAt(player, pidx); + + // allow derived classes to respond + playerWasReplaced(pidx, oplayer, player); + + // notify our delegates + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).playerWasReplaced( + pidx, oplayer, player); + } + }); + } + + /** + * Called when a player has been replaced via a call to {@link + * #replacePlayer}. + */ + protected void playerWasReplaced (int pidx, Name oldPlayer, Name newPlayer) + { + } + + /** + * Returns the user object for the player with the specified index or + * null if the player at that index is not online. + */ + public BodyObject getPlayer (int playerIdx) + { + // if we have their oid, use that + int ploid = _playerOids[playerIdx]; + if (ploid > 0) { + return (BodyObject)CrowdServer.omgr.getObject(ploid); + } + // otherwise look them up by name + Name name = getPlayerName(playerIdx); + return (name == null) ? null : CrowdServer.lookupBody(name); + } + + /** + * Sets the specified player as an AI with the specified + * configuration. It is assumed that this will be set soon after the + * player names for all AIs present in the game. (It should be done + * before human players start trickling into the game.) + * + * @param pidx the player index of the AI. + * @param ai the AI configuration. + */ + public void setAI (final int pidx, final GameAI ai) + { + if (_AIs == null) { + // create and initialize the AI configuration array + _AIs = new GameAI[getPlayerSlots()]; + // set up a delegate op for AI ticking + _tickAIOp = new TickAIDelegateOp(); + } + + // save off the AI's configuration + _AIs[pidx] = ai; + + // let the delegates know that the player's been made an AI + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).setAI(pidx, ai); + } + }); + } + + /** + * Returns the name of the player with the specified index or null if + * no player exists at that index. + */ + public Name getPlayerName (int index) + { + return (_gameobj == null) ? null : _gameobj.players[index]; + } + + /** + * Returns the player index of the given user in the game, or + * -1 if the player is not involved in the game. + */ + public int getPlayerIndex (Name username) + { + return (_gameobj == null) ? -1 : _gameobj.getPlayerIndex(username); + } + + /** + * Returns the user object oid of the player with the specified index. + */ + public int getPlayerOid (int index) + { + return (_playerOids == null) ? -1 : _playerOids[index]; + } + + /** + * Returns the number of players in the game. + */ + public int getPlayerCount () + { + return _playerCount; + } + + /** + * Returns the number of players allowed in this game. + */ + public int getPlayerSlots () + { + return _gameconfig.players.length; + } + + /** + * Returns whether the player at the specified player index is an AI. + */ + public boolean isAI (int pidx) + { + return (_AIs != null && _AIs[pidx] != null); + } + + /** + * Returns whether the player at the specified player index is actively + * playing the game + */ + public boolean isActivePlayer (int pidx) + { + return _gameobj.isActivePlayer(pidx) && + (getPlayerOid(pidx) > 0 || isAI(pidx)); + } + + /** + * Returns the unique round identifier for the current round. + */ + public int getRoundId () + { + return _gameobj.roundId; + } + + /** + * Sends a system message to the players in the game room. + */ + public void systemMessage (String msgbundle, String msg) + { + systemMessage(msgbundle, msg, false); + } + + /** + * Sends a system message to the players in the game room. + * + * @param waitForStart if true, the message will not be sent until the + * game has started. + */ + public void systemMessage ( + String msgbundle, String msg, boolean waitForStart) + { + if (waitForStart && + ((_gameobj == null) || (_gameobj.state == GameObject.PRE_GAME))) { + // queue up the message. + if (_startmsgs == null) { + _startmsgs = new ArrayList>(); + } + _startmsgs.add(new Tuple(msgbundle, msg)); + return; + } + + // otherwise, just deliver the message + SpeakProvider.sendInfo(_gameobj, msgbundle, msg); + } + + /** + * Report to the knocked-out player's room that they were knocked out. + */ + protected void reportPlayerKnockedOut (int pidx) + { + BodyObject user = getPlayer(pidx); + if (user == null) { + // body object can be null for ai players + return; + } + + DObject place = CrowdServer.omgr.getObject(user.location); + if (place != null) { + place.postMessage(PLAYER_KNOCKED_OUT, + new Object[] { new int[] { user.getOid() } }); + } + } + + // documentation inherited + protected Class getPlaceObjectClass () + { + return GameObject.class; + } + + // documentation inherited + protected void didStartup () + { + // obtain a casted reference to our game object + _gameobj = (GameObject)_plobj; + + // stick the players into the game object + _gameobj.setPlayers(_gameconfig.players); + + // set up an initial player status array + _gameobj.setPlayerStatus(new int[getPlayerSlots()]); + + // save off the number of players so that we needn't repeatedly + // iterate through the player name array server-side unnecessarily + _playerCount = _gameobj.getPlayerCount(); + + // instantiate a player oid array which we'll fill in later + _playerOids = new int[getPlayerSlots()]; + + // create and fill in our game service object + GameMarshaller service = (GameMarshaller) + _invmgr.registerDispatcher(new GameDispatcher(this), false); + _gameobj.setGameService(service); + + // give delegates a chance to do their thing + super.didStartup(); + + // let the players of this game know that we're ready to roll (if + // we have a specific set of players) + for (int ii = 0; ii < getPlayerSlots(); ii++) { + // skip non-existent players and AIs + if (!_gameobj.isOccupiedPlayer(ii) || isAI(ii)) { + continue; + } + + BodyObject bobj = CrowdServer.lookupBody(_gameobj.players[ii]); + if (bobj == null) { + Log.warning("Unable to deliver game ready to non-existent " + + "player [game=" + _gameobj.which() + + ", player=" + _gameobj.players[ii] + "]."); + continue; + } + + // deliver a game ready notification to the player + ParlorSender.gameIsReady(bobj, _gameobj.getOid()); + } + + // start up a no-show timer if needed + if (needsNoShowTimer()) { + new Interval(CrowdServer.omgr) { + public void expired () { + checkForNoShows(); + } + }.schedule(NOSHOW_DELAY); + } + } + + /** + * Returns true if this game requires a no-show timer. The default + * implementation returns true for non-party games and false for party + * games. Derived classes may wish to change or augment this behavior. + */ + protected boolean needsNoShowTimer () + { + return !isPartyGame(); + } + + /** + * Derived classes that need their AIs to be ticked periodically + * should override this method and return true. Many AIs can act + * entirely in reaction to game state changes and need no periodic + * ticking which is why ticking is disabled by default. + * + * @see #tickAIs + */ + protected boolean needsAITick () + { + return false; + } + + // documentation inherited + protected void didShutdown () + { + super.didShutdown(); + + // unregister this game manager + _managers.remove(this); + + // remove the tick interval if there are no remaining managers + if (_managers.size() == 0) { + _tickInterval.cancel(); + _tickInterval = null; + } + + // clear out our service registration + _invmgr.clearDispatcher(_gameobj.gameService); + } + + // documentation inherited + protected void bodyLeft (int bodyOid) + { + // first resign the player from the game + int pidx = IntListUtil.indexOf(_playerOids, bodyOid); + if (pidx != -1 && _gameobj.isInPlay() && + _gameobj.isActivePlayer(pidx)) { + // end the player's game if they bail on an in-progress game + endPlayerGame(pidx); + } + + // then complete the bodyLeft() processing which may result in a call + // to placeBecameEmpty() which will shut the game down + super.bodyLeft(bodyOid); + } + + /** + * When a game room becomes empty, we cancel the game if it's still in + * progress and close down the game room. + */ + protected void placeBecameEmpty () + { +// Log.info("Game room empty. Going away. " + +// "[game=" + _gameobj.which() + "]."); + + // if we're in play then move to game over + if (_gameobj.state != GameObject.PRE_GAME && + _gameobj.state != GameObject.GAME_OVER && + _gameobj.state != GameObject.CANCELLED) { + _gameobj.setState(GameObject.GAME_OVER); + // and shutdown directly + shutdown(); + + // cancel the game; which will shut us down + } else if (!cancelGame()) { + // or shut down directly if the game is already over + shutdown(); + } + } + + /** + * Called when all players have arrived in the game room. By default, + * this starts up the game, but a manager may wish to override this + * and start the game according to different criterion. + */ + protected void playersAllHere () + { + // start up the game if we're not a party game and if we haven't + // already done so + if (!isPartyGame() && _gameobj.state == GameObject.PRE_GAME) { + startGame(); + } + } + + /** + * Called after the no-show delay has expired following the delivery + * of notifications to all players that the game is ready. + * Note: this is not called for party games. Those games have + * a human who decides when to start the game. + */ + protected void checkForNoShows () + { + // nothing to worry about if we're already started + if (_gameobj.state != GameObject.PRE_GAME) { + return; + } + + // if there's no one in the room, go ahead and clear it out + if (_plobj.occupants.size() == 0) { + Log.info("Cancelling total no-show " + + "[game=" + _gameobj.which() + + ", players=" + StringUtil.toString(_gameobj.players) + + ", poids=" + StringUtil.toString(_playerOids) + "]."); + placeBecameEmpty(); + + } else { + // do the right thing if we have any no-show players + for (int ii = 0; ii < getPlayerSlots(); ii++) { + if (!playerIsReady(ii)) { + handlePartialNoShow(); + return; + } + } + } + } + + /** + * This is called when some, but not all, players failed to show up + * for a game. The default implementation simply cancels the game. + */ + protected void handlePartialNoShow () + { + // mark the no-show players; this will cause allPlayersReady() to + // think that everyone has arrived, but still allow us to tell who + // has not shown up in gameDidStart() + int humansHere = 0; + for (int ii = 0; ii < _playerOids.length; ii++) { + if (_playerOids[ii] == 0) { + _playerOids[ii] = -1; + } else if (!isAI(ii)) { + humansHere++; + } + } + + if ((humansHere == 0) && !startWithoutHumans()) { + // if there are no human players in the game, just cancel it + Log.info("Canceling no-show game [game=" + _gameobj.which() + + ", players=" + StringUtil.toString(_playerOids) + "]."); + cancelGame(); + + } else { + // go ahead and report that everyone is ready (which will start the + // game); gameDidStart() will take care of giving the boot to + // anyone who isn't around + Log.info("Forcing start of partial no-show game " + + "[game=" + _gameobj.which() + + ", poids=" + StringUtil.toString(_playerOids) + "]."); + playersAllHere(); + } + } + + /** + * This is called when the game is ready to start (all players + * involved have delivered their "am ready" notifications). It calls + * {@link #gameWillStart}, sets the necessary wheels in motion and + * then calls {@link #gameDidStart}. Derived classes should override + * one or both of the calldown functions (rather than this function) + * if they need to do things before or after the game starts. + * + * @return true if the game was started, false if it could not be + * started because it was already in play or because all players have + * not yet reported in. + */ + public boolean startGame () + { + // complain if we're already started + if (_gameobj.state == GameObject.IN_PLAY) { + Log.warning("Requested to start an already in-play game " + + "[game=" + _gameobj.which() + "]."); + Thread.dumpStack(); + return false; + } + + // TEMP: clear out our game end tracker + _gameEndTracker.clear(); + + // make sure everyone has turned up + if (!allPlayersReady()) { + Log.warning("Requested to start a game that is still " + + "awaiting players [game=" + _gameobj.which() + + ", pnames=" + StringUtil.toString(_gameobj.players) + + ", poids=" + StringUtil.toString(_playerOids) + "]."); + return false; + } + + // if we're still waiting for a call to endGame() to propagate, + // queue up a runnable to start the game which will allow the + // endGame() to propagate before we start things up + if (_committedState == GameObject.IN_PLAY) { + Log.info("Postponing start of still-ending game " + + "[which=" + _gameobj.which() + "]."); + CrowdServer.omgr.postRunnable(new Runnable() { + public void run () { + startGame(); + } + }); + return true; + } + + // let the derived class do its pre-start stuff + gameWillStart(); + + // transition the game to started + _gameobj.setState(GameObject.IN_PLAY); + + // when our events are applied, we'll call gameDidStart() + return true; + } + + /** + * @return true if we should start the game even without any humans. + * Default implementation always returns false. + */ + protected boolean startWithoutHumans () + { + return false; + } + + /** + * Called when the game is about to start, but before the game start + * notification has been delivered to the players. Derived classes + * should override this if they need to perform some pre-start + * activities, but should be sure to call + * super.gameWillStart(). + */ + protected void gameWillStart () + { + // increment the round identifier + _gameobj.setRoundId(_gameobj.roundId + 1); + + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).gameWillStart(); + } + }); + } + + /** + * Called when the game state changes. This happens after the attribute + * change event has propagated. + * + * @param state the new game state. + * @param oldState the previous game state. + */ + protected void stateDidChange (int state, int oldState) + { + switch (state) { + case GameObject.IN_PLAY: + gameDidStart(); + break; + + case GameObject.GAME_OVER: + // we do some jiggery pokery to allow derived game objects to have + // different notions of what it means to be in play + _gameobj.state = oldState; + boolean wasInPlay = _gameobj.isInPlay(); + _gameobj.state = state; + + // now call gameDidEnd() only if the game was previously in play + if (wasInPlay) { + gameDidEnd(); + } + break; + + case GameObject.CANCELLED: + // let the manager do anything it cares to + gameWasCancelled(); + + // and shutdown if there's no one here + if (_plobj.occupants.size() == 0) { + shutdown(); + } + break; + } + } + + /** + * Called after the game start notification was dispatched. Derived + * classes can override this to put whatever wheels they might need + * into motion now that the game is started (if anything other than + * transitioning the game to {@link GameObject#IN_PLAY} is necessary), + * but should be sure to call super.gameDidStart(). + */ + protected void gameDidStart () + { + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).gameDidStart(); + } + }); + + // inform the players of any pending messages. + if (_startmsgs != null) { + for (Tuple mtup : _startmsgs) { + systemMessage(mtup.left, // bundle + mtup.right); // message + } + _startmsgs = null; + } + + // and register ourselves to receive AI ticks + if (_AIs != null && needsAITick()) { + AIGameTicker.registerAIGame(this); + } + + // any players who have not claimed that they are ready should now + // be given le boote royale + for (int ii = 0; ii < _playerOids.length; ii++) { + if (_playerOids[ii] == -1) { + Log.info("Booting no-show player [game=" + _gameobj.which() + + ", player=" + getPlayerName(ii) + "]."); + _playerOids[ii] = 0; // unfiddle the blank oid + endPlayerGame(ii); + } + } + } + + /** + * Called by the {@link AIGameTicker} if we're registered as an AI + * game. + */ + protected void tickAIs () + { + for (int ii = 0; ii < _AIs.length; ii++) { + if (_AIs[ii] != null) { + tickAI(ii, _AIs[ii]); + } + } + } + + /** + * Called by tickAIs to tick each AI in the game. + */ + protected void tickAI (int pidx, GameAI ai) + { + _tickAIOp.setAI(pidx, ai); + applyToDelegates(_tickAIOp); + } + + /** + * Ends the game for the given player. + */ + public void endPlayerGame (int pidx) + { + // go for a little transactional efficiency + _gameobj.startTransaction(); + try { + // end the player's game + if (_gameobj.playerStatus != null) { + _gameobj.setPlayerStatusAt(GameObject.PLAYER_LEFT_GAME, pidx); + } + + // let derived classes do some business + playerGameDidEnd(pidx); + + announcePlayerGameOver(pidx); + + } finally { + _gameobj.commitTransaction(); + } + + // if it's time to end the game, then do so + if (shouldEndGame()) { + endGame(); + } else { + // otherwise report that the player was knocked out to other + // people in his/her room + reportPlayerKnockedOut(pidx); + } + } + + /** + * Announce to everyone in the game that a player's game has ended. + */ + protected void announcePlayerGameOver (int pidx) + { + String message = MessageBundle.tcompose( + "m.player_game_over", getPlayerName(pidx)); + systemMessage(GAME_MESSAGE_BUNDLE, message); + } + + /** + * Called when a player has been marked as knocked out but before the + * knock-out status update has been sent to the players. Any status + * information that needs be updated in light of the knocked out + * player can be updated here. + */ + protected void playerGameDidEnd (int pidx) + { + } + + /** + * Called when a player leaves the game in order to determine whether + * the game should be ended based on its current state, which will + * include updated player status for the player in question. The + * default implementation returns true if the game is in play and + * there is only one player left. Derived classes may wish to + * override this method in order to customize the required end-game + * conditions. + */ + protected boolean shouldEndGame () + { + return (_gameobj.isInPlay() && _gameobj.getActivePlayerCount() == 1); + } + + /** + * Called when the game is known to be over. This will call some + * calldown functions to determine the winner of the game and then + * transition the game to the {@link GameObject#GAME_OVER} state. + */ + public void endGame () + { + // TEMP: debug pending rating repeat bug + if (_gameEndTracker.checkCall( + "Requested to end already ended game " + + "[game=" + _gameobj.which() + "].")) { + return; + } + // END TEMP + + if (!_gameobj.isInPlay()) { + Log.info("Refusing to end game that was not in play " + + "[game=" + _gameobj.which() + "]."); + return; + } + + _gameobj.startTransaction(); + try { + // let the derived class do its pre-end stuff + gameWillEnd(); + + // determine winners and set them in the game object + boolean[] winners = new boolean[getPlayerSlots()]; + assignWinners(winners); + _gameobj.setWinners(winners); + + // transition to the game over state + _gameobj.setState(GameObject.GAME_OVER); + + } finally { + _gameobj.commitTransaction(); + } + + // wait until we hear the game state transition on the game object + // to invoke our game over code so that we can be sure that any + // final events dispatched on the game object prior to the call to + // endGame() have been dispatched + } + + /** + * Sets the state of the game to {@link GameObject#CANCELLED}. + * + * @return true if the game was cancelled, false if it was already over or + * cancelled. + */ + public boolean cancelGame () + { + if (_gameobj.state != GameObject.GAME_OVER && + _gameobj.state != GameObject.CANCELLED) { + _gameobj.setState(GameObject.CANCELLED); + return true; + } + return false; + } + + /** + * Assigns the final winning status for each player to their respect + * player index in the supplied array. This will be called by {@link + * #endGame} when the game is over. The default implementation marks + * no players as winners. Derived classes should override this method + * in order to customize the winning conditions. + */ + protected void assignWinners (boolean[] winners) + { + Arrays.fill(winners, false); + } + + /** + * Returns whether game conclusion antics such as rating updates + * should be performed when an in-play game is ended. Derived classes + * may wish to override this method to customize the conditions under + * which the game is concluded. + */ + public boolean shouldConcludeGame () + { + return (_gameobj.state == GameObject.GAME_OVER); + } + + /** + * Called when the game is about to end, but before the game end + * notification has been delivered to the players. Derived classes + * should override this if they need to perform some pre-end + * activities, but should be sure to call + * super.gameWillEnd(). + */ + protected void gameWillEnd () + { + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).gameWillEnd(); + } + }); + } + + /** + * Called after the game has transitioned to the {@link + * GameObject#GAME_OVER} state. Derived classes should override this + * to perform any post-game activities, but should be sure to call + * super.gameDidEnd(). + */ + protected void gameDidEnd () + { + // remove ourselves from the AI ticker, if applicable + if (_AIs != null && needsAITick()) { + AIGameTicker.unregisterAIGame(this); + } + + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).gameDidEnd(); + } + }); + + // report the winners and losers if appropriate + int winnerCount = _gameobj.getWinnerCount(); + if (shouldConcludeGame() && winnerCount > 0 && !_gameobj.isDraw()) { + reportWinnersAndLosers(); + } + + // calculate ratings and all that... + } + + /** + * Called to let the manager know that the game was cancelled (and may be + * about to be shutdown if there's no one in the room). In the base + * framework a game will only be canceled if no one shows up, so {@link + * #gameWillStart}, etc. will never have been called and thus {@link + * #gameWillEnd}, etc. will not be called. However, if a game chooses to + * cancel itself for whatever reason, no effort will be made to call {@link + * #endGame} and the game ending call backs so that game can override this + * method to do anything it needs. Note that {@link #didShutdown} will be + * called in every case and that's generally the best place to free + * resources so this method may not be needed. + */ + protected void gameWasCancelled () + { + // nothing to do by default + } + + /** + * Report winner and loser oids to each room that any of the + * winners/losers is in. + */ + protected void reportWinnersAndLosers () + { + int numPlayers = _playerOids.length; + + // set up 3 sets that will not need internal expanding + ArrayIntSet winners = new ArrayIntSet(numPlayers); + ArrayIntSet losers = new ArrayIntSet(numPlayers); + ArrayIntSet places = new ArrayIntSet(numPlayers); + + for (int ii=0; ii < numPlayers; ii++) { + BodyObject user = getPlayer(ii); + if (user != null) { + places.add(user.location); + (_gameobj.isWinner(ii) ? winners : losers).add(user.getOid()); + } + } + + Object[] args = + new Object[] { winners.toIntArray(), losers.toIntArray() }; + + // now send a message event to each room + for (int ii=0, nn = places.size(); ii < nn; ii++) { + DObject place = CrowdServer.omgr.getObject(places.get(ii)); + if (place != null) { + place.postMessage(WINNERS_AND_LOSERS, args); + } + } + } + + /** + * Called when the game is to be reset to its starting state in + * preparation for a new game without actually ending the current + * game. It calls {@link #gameWillReset} followed by the standard game + * start processing ({@link #gameWillStart} and {@link + * #gameDidStart}). Derived classes should override these calldown + * functions (rather than this function) if they need to do things + * before or after the game resets. + */ + public void resetGame () + { + // let the derived class do its pre-reset stuff + gameWillReset(); + // do the standard game start processing + gameWillStart(); + // transition to in-play which will trigger a call to gameDidStart() + _gameobj.setState(GameObject.IN_PLAY); + } + + /** + * Called when the game is about to reset, but before the board has + * been re-initialized or any other clearing out of game data has + * taken place. Derived classes should override this if they need to + * perform some pre-reset activities. + */ + protected void gameWillReset () + { + // reinitialize the player status + _gameobj.setPlayerStatus(new int[getPlayerSlots()]); + + // let our delegates do their business + applyToDelegates(new DelegateOp() { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate)delegate).gameWillReset(); + } + }); + } + + // documentation inherited from interface + public void playerReady (ClientObject caller) + { + BodyObject plobj = (BodyObject)caller; + + // get the user's player index + int pidx = _gameobj.getPlayerIndex(plobj.getVisibleName()); + if (pidx == -1) { + // only complain if this is not a party game, since it's + // perfectly normal to receive a player ready notification + // from a user entering a party game in which they're not yet + // a participant + if (!isPartyGame()) { + Log.warning("Received playerReady() from non-player? " + + "[caller=" + caller + "]."); + } + return; + } + + // make a note of this player's oid + _playerOids[pidx] = plobj.getOid(); + + // if everyone is now ready to go, get things underway + if (allPlayersReady()) { + playersAllHere(); + } + } + + /** + * Returns true if all (non-AI) players have delivered their {@link + * #playerReady} notifications, false if they have not. + */ + public boolean allPlayersReady () + { + for (int ii = 0; ii < getPlayerSlots(); ii++) { + if (!playerIsReady(ii)) { + return false; + } + } + return true; + } + + /** + * Returns true if the player at the specified slot is ready (or if + * there is meant to be no player in that slot), false if there is + * meant to be a player in the specified slot and they have not yet + * reported that they are ready. + */ + public boolean playerIsReady (int pidx) + { + return (!_gameobj.isOccupiedPlayer(pidx) || // unoccupied slot + _playerOids[pidx] != 0 || // player is ready + isAI(pidx)); // player is AI + } + + /** + * Gives game managers an opportunity to perform periodic processing + * that is not driven by events generated by the player. + */ + protected void tick (long tickStamp) + { + // nothing for now + } + + // documentation inherited + public void attributeChanged (AttributeChangedEvent event) + { + if (event.getName().equals(GameObject.STATE)) { + stateDidChange(_committedState = event.getIntValue(), + ((Integer)event.getOldValue()).intValue()); + } + } + + /** + * Called periodically to call {@link #tick} on all registered game + * managers. + */ + protected static void tickAllGames () + { + long now = System.currentTimeMillis(); + int size = _managers.size(); + for (int ii = 0; ii < size; ii++) { + GameManager gmgr = _managers.get(ii); + try { + gmgr.tick(now); + } catch (Exception e) { + Log.warning("Game manager choked during tick " + + "[gmgr=" + gmgr + "]."); + Log.logStackTrace(e); + } + } + } + + /** + * A helper operation to distribute AI ticks to our delegates. + */ + protected class TickAIDelegateOp implements DelegateOp + { + public void apply (PlaceManagerDelegate delegate) { + ((GameManagerDelegate) delegate).tickAI(_pidx, _ai); + } + + public void setAI (int pidx, GameAI ai) + { + _pidx = pidx; + _ai = ai; + } + + protected int _pidx; + protected GameAI _ai; + } + + /** A reference to our game config. */ + protected GameConfig _gameconfig; + + /** A reference to our game object. */ + protected GameObject _gameobj; + + /** The number of players in the game. */ + protected int _playerCount; + + /** The oids of our player and AI body objects. */ + protected int[] _playerOids; + + /** If AIs are present, contains their configuration, or null at human + * player indexes. */ + protected GameAI[] _AIs; + + /** If non-null, contains bundles and messages that should be sent as + * system messages once the game has started. */ + protected ArrayList> _startmsgs; + + /** Our delegate operator to tick AIs. */ + protected TickAIDelegateOp _tickAIOp; + + /** The state of the game that has been propagated to our + * subscribers. */ + protected int _committedState; + + /** TEMP: debugging the pending rating double release bug. */ + protected RepeatCallTracker _gameEndTracker = new RepeatCallTracker(); + + /** A list of all currently active game managers. */ + protected static ArrayList _managers = + new ArrayList(); + + /** The interval for the game manager tick. */ + protected static Interval _tickInterval; + + /** We give players 30 seconds to turn up in a game; after that, + * they're considered a no show. */ + protected static final long NOSHOW_DELAY = 30 * 1000L; + + /** The delay in milliseconds between ticking of all game managers. */ + protected static final long TICK_DELAY = 5L * 1000L; +} diff --git a/src/java/com/threerings/parlor/game/server/GameManagerDelegate.java b/src/java/com/threerings/parlor/game/server/GameManagerDelegate.java new file mode 100644 index 00000000..60e393c1 --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/GameManagerDelegate.java @@ -0,0 +1,108 @@ +// +// $Id: GameManagerDelegate.java 3667 2005-08-03 07:46:54Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +import com.threerings.util.Name; + +import com.threerings.crowd.server.PlaceManagerDelegate; + +import com.threerings.parlor.game.data.GameAI; + +/** + * Extends the {@link PlaceManagerDelegate} mechanism with game manager + * specific methods. + */ +public class GameManagerDelegate extends PlaceManagerDelegate +{ + /** + * Provides the delegate with a reference to the game manager for + * which it is delegating. + */ + public GameManagerDelegate (GameManager gmgr) + { + super(gmgr); + } + + /** + * Called by the game manager when the game is about to start. + */ + public void gameWillStart () + { + } + + /** + * Called by the game manager after the game was started. + */ + public void gameDidStart () + { + } + + /** + * Called when a player in the game has been replaced by a call to + * {@link GameManager#replacePlayer}. + */ + public void playerWasReplaced (int pidx, Name oldPlayer, Name newPlayer) + { + } + + /** + * Called by the manager when we should do some AI. Only called while + * the game is IN_PLAY. + * + * @param pidx the player index to fake some gameplay for. + * @param ai a record indicating the AI's configuration. + */ + public void tickAI (int pidx, GameAI ai) + { + } + + /** + * Called by the game manager when the game is about to end. + */ + public void gameWillEnd () + { + } + + /** + * Called by the game manager after the game ended. + */ + public void gameDidEnd () + { + } + + /** + * Called when the game is about to reset, but before any other + * clearing out of game data has taken place. Derived classes should + * override this if they need to perform some pre-reset activities. + */ + public void gameWillReset () + { + } + + /** + * Called when the specified player has been set as an AI with the + * supplied AI configuration. + */ + public void setAI (int pidx, GameAI ai) + { + } +} diff --git a/src/java/com/threerings/parlor/game/server/GameProvider.java b/src/java/com/threerings/parlor/game/server/GameProvider.java new file mode 100644 index 00000000..13f7f90c --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/GameProvider.java @@ -0,0 +1,39 @@ +// +// $Id: GameProvider.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +import com.threerings.parlor.game.client.GameService; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +/** + * Defines the server-side of the {@link GameService}. + */ +public interface GameProvider extends InvocationProvider +{ + /** + * Handles a {@link GameService#playerReady} request. + */ + public void playerReady (ClientObject caller); +} diff --git a/src/java/com/threerings/parlor/game/server/GameWatcher.java b/src/java/com/threerings/parlor/game/server/GameWatcher.java new file mode 100644 index 00000000..ce5e86d7 --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/GameWatcher.java @@ -0,0 +1,76 @@ +// +// $Id: GameWatcher.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; + +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.server.PlaceManager; +import com.threerings.crowd.server.PlaceRegistry; + +import com.threerings.parlor.game.data.GameObject; + +/** + * An abstract convenience class used server-side to keep an eye on a game + * and perform a one-time game-over activity when the game ends. Classes + * that care to make use of the game watcher should create an instance, + * implement {@link #gameDidEnd}, and pass the instance to the place + * registry in the call to {@link PlaceRegistry#createPlace} when the + * puzzle is created. + */ +public abstract class GameWatcher + implements PlaceRegistry.CreationObserver, AttributeChangeListener +{ + // documentation inherited + public void placeCreated (PlaceObject place, PlaceManager pmgr) + { + _gameobj = (GameObject)place; + _gameobj.addListener(this); + } + + // documentation inherited + public void attributeChanged (AttributeChangedEvent event) + { + if (event.getName().equals(GameObject.STATE)) { + // if we transitioned to a non-in-play state, the game has + // completed + if (!_gameobj.isInPlay()) { + try { + gameDidEnd(_gameobj); + } finally { + _gameobj.removeListener(this); + _gameobj = null; + } + } + } + } + + /** + * Called when the game ends to give derived classes a chance to + * engage in their game-over antics. + */ + protected abstract void gameDidEnd (GameObject gameobj); + + /** The game object we're observing. */ + protected GameObject _gameobj; +} diff --git a/src/java/com/threerings/parlor/game/server/TeamGameManager.java b/src/java/com/threerings/parlor/game/server/TeamGameManager.java new file mode 100644 index 00000000..91edbe7d --- /dev/null +++ b/src/java/com/threerings/parlor/game/server/TeamGameManager.java @@ -0,0 +1,36 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.game.server; + +/** + * An interface to be implemented by a game if it wants to hear about + * the team indices that were configured during matchmaking. + */ +public interface TeamGameManager +{ + /** + * Set the team member indices. For example, a game with + * three players in two teams- players 0 and 2 versus player 1- would + * have { {0, 2}, {1} } set. + */ + public void setTeamMemberIndices (int[][] teams); +} diff --git a/src/java/com/threerings/parlor/media/ScoreAnimation.java b/src/java/com/threerings/parlor/media/ScoreAnimation.java new file mode 100644 index 00000000..de2ca8fa --- /dev/null +++ b/src/java/com/threerings/parlor/media/ScoreAnimation.java @@ -0,0 +1,86 @@ +// +// $Id: ScoreAnimation.java 3479 2005-04-13 19:06:33Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.media; + +import java.awt.Color; +import java.awt.Font; + +import com.samskivert.swing.Label; + +import com.threerings.media.MediaPanel; +import com.threerings.media.animation.FloatingTextAnimation; + +public class ScoreAnimation extends FloatingTextAnimation +{ + { // initializer, run automatically with every constructor + setRenderOrder(Integer.MAX_VALUE); + } + + /** + * Constructs a score animation for the given score value centered at + * the given coordinates. + */ + public ScoreAnimation (Label label, int x, int y) + { + super(label, x, y); + } + + /** + * Constructs a score animation for the given score value centered at + * the given coordinates. The animation will float up the screen for + * 30 pixels. + */ + public ScoreAnimation (Label label, int x, int y, long floatPeriod) + { + super(label, x, y, floatPeriod); + } + + /** + * Constructs a score animation for the given score value starting at + * the given coordinates and floating toward the specified + * coordinates. + */ + public ScoreAnimation (Label label, int sx, int sy, + int destx, int desty, long floatPeriod) + { + super(label, sx, sy, destx, desty, floatPeriod); + } + + /** + * Create and configure a Label suitable for a ScoreAnimation with + * all the most common options. + */ + public static Label createLabel (String score, Color c, Font font, + MediaPanel host) + { + Label label = new Label(score); + label.setTargetWidth(host.getWidth()); + label.setStyle(Label.OUTLINE); + label.setTextColor(c); + label.setAlternateColor(Color.BLACK); + label.setFont(font); + label.setAlignment(Label.CENTER); + label.layout(host); + + return label; + } +} diff --git a/src/java/com/threerings/parlor/server/ParlorDispatcher.java b/src/java/com/threerings/parlor/server/ParlorDispatcher.java new file mode 100644 index 00000000..28e42006 --- /dev/null +++ b/src/java/com/threerings/parlor/server/ParlorDispatcher.java @@ -0,0 +1,116 @@ +// +// $Id: ParlorDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.server; + +import com.threerings.parlor.client.ParlorService; +import com.threerings.parlor.data.ParlorMarshaller; +import com.threerings.parlor.data.TableConfig; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import com.threerings.util.Name; + +/** + * Dispatches requests to the {@link ParlorProvider}. + */ +public class ParlorDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public ParlorDispatcher (ParlorProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new ParlorMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case ParlorMarshaller.CANCEL: + ((ParlorProvider)provider).cancel( + source, + ((Integer)args[0]).intValue(), (InvocationService.InvocationListener)args[1] + ); + return; + + case ParlorMarshaller.CREATE_TABLE: + ((ParlorProvider)provider).createTable( + source, + ((Integer)args[0]).intValue(), (TableConfig)args[1], (GameConfig)args[2], (ParlorService.TableListener)args[3] + ); + return; + + case ParlorMarshaller.INVITE: + ((ParlorProvider)provider).invite( + source, + (Name)args[0], (GameConfig)args[1], (ParlorService.InviteListener)args[2] + ); + return; + + case ParlorMarshaller.JOIN_TABLE: + ((ParlorProvider)provider).joinTable( + source, + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), ((Integer)args[2]).intValue(), (InvocationService.InvocationListener)args[3] + ); + return; + + case ParlorMarshaller.LEAVE_TABLE: + ((ParlorProvider)provider).leaveTable( + source, + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), (InvocationService.InvocationListener)args[2] + ); + return; + + case ParlorMarshaller.RESPOND: + ((ParlorProvider)provider).respond( + source, + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), (Object)args[2], (InvocationService.InvocationListener)args[3] + ); + return; + + case ParlorMarshaller.START_SOLITAIRE: + ((ParlorProvider)provider).startSolitaire( + source, + (GameConfig)args[0], (InvocationService.ConfirmListener)args[1] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/parlor/server/ParlorManager.java b/src/java/com/threerings/parlor/server/ParlorManager.java new file mode 100644 index 00000000..0722e3ec --- /dev/null +++ b/src/java/com/threerings/parlor/server/ParlorManager.java @@ -0,0 +1,274 @@ +// +// $Id: ParlorManager.java 3758 2005-11-10 23:18:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.server; + +import com.samskivert.util.HashIntMap; +import com.threerings.util.Name; + +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationManager; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.server.PlaceRegistry; + +import com.threerings.parlor.Log; +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.server.GameManager; + +/** + * The parlor manager is responsible for the parlor services in + * aggregate. This includes maintaining the registry of active games, + * handling the necessary coordination for the matchmaking services and + * anything else that falls outside the scope of an actual in-progress + * game. + */ +public class ParlorManager + implements ParlorCodes +{ + /** Provides the server-side implementation of the parlor services. */ + public ParlorProvider parprov; + + /** + * Initializes the parlor manager. This should be called by the server + * that is making use of the parlor services on the single instance of + * parlor manager that it has created. + * + * @param invmgr a reference to the invocation manager in use by this + * server. + * @param plreg a reference to the place registry to be used by the + * parlor manager when creating game places. + */ + public void init (InvocationManager invmgr, PlaceRegistry plreg) + { + // create and register our invocation provider + parprov = new ParlorProvider(this); + invmgr.registerDispatcher(new ParlorDispatcher(parprov), true); + + // keep this for later + _plreg = plreg; + } + + /** + * Issues an invitation from the inviter to the + * invitee for a game as described by the supplied config + * object. + * + * @param inviter the player initiating the invitation. + * @param invitee the player being invited. + * @param config the configuration of the game being proposed. + * + * @return the invitation identifier for the newly created invitation + * record. + * + * @exception InvocationException thrown if the invitation was not + * able to be processed for some reason (like the invited player has + * requested not to be disturbed). The explanation will be provided in + * the message data of the exception. + */ + public int invite (BodyObject inviter, BodyObject invitee, + GameConfig config) + throws InvocationException + { +// Log.info("Received invitation request [inviter=" + inviter + +// ", invitee=" + invitee + ", config=" + config + "]."); + + // here we should check to make sure the invitee hasn't muted the + // inviter, and that the inviter isn't shunned and all that other + // access control type stuff + + // create a new invitation record for this invitation + Invitation invite = new Invitation(inviter, invitee, config); + + // stick it in the pending invites table + _invites.put(invite.inviteId, invite); + + // deliver an invite notification to the invitee + ParlorSender.sendInvite( + invitee, invite.inviteId, inviter.getVisibleName(), config); + + // and let the caller know the invite id we assigned + return invite.inviteId; + } + + /** + * Effects a response to an invitation (accept, refuse or counter), + * made by the specified source user with the specified arguments. + * + * @param source the body object of the user that is issuing this + * response. + * @param inviteId the identifier of the invitation to which we are + * responding. + * @param code the response code (either {@link + * #INVITATION_ACCEPTED}, {@link #INVITATION_REFUSED} or {@link + * #INVITATION_COUNTERED}). + * @param arg the argument that goes along with the response: an + * explanatory message in the case of a refusal (the empty string, not + * null, if no message was provided) or the new game configuration in + * the case of a counter-invitation. + */ + public void respondToInvite (BodyObject source, int inviteId, int code, + Object arg) + { + // look up the invitation + Invitation invite = (Invitation)_invites.get(inviteId); + if (invite == null) { + Log.warning("Requested to respond to non-existent invitation " + + "[source=" + source + ", inviteId=" + inviteId + + ", code=" + code + ", arg=" + arg + "]."); + return; + } + + // make sure this response came from the proper person + if (source != invite.invitee) { + Log.warning("Got response from non-invitee [source=" + source + + ", invite=" + invite + ", code=" + code + + ", arg=" + arg + "]."); + return; + } + + // let the other user know that a response was made to this + // invitation + ParlorSender.sendInviteResponse( + invite.inviter, invite.inviteId, code, arg); + + switch (code) { + case INVITATION_ACCEPTED: + // the invitation was accepted, so we'll need to start up the + // game and get the necessary balls rolling + processAcceptedInvitation(invite); + // and remove the invitation from the pending table + _invites.remove(inviteId); + break; + + case INVITATION_REFUSED: + // remove the invitation record from the pending table as it + // is no longer pending + _invites.remove(inviteId); + break; + + case INVITATION_COUNTERED: + // swap control of the invitation to the invitee + invite.swapControl(); + break; + + default: + Log.warning("Requested to respond to invitation with " + + "unknown response code [source=" + source + + ", invite=" + invite + ", code=" + code + + ", arg=" + arg + "]."); + break; + } + } + + /** + * Requests that an outstanding invitation be cancelled. + * + * @param source the body object of the user that is making the + * request. + * @param inviteId the unique id of the invitation to be cancelled. + */ + public void cancelInvite (BodyObject source, int inviteId) + { + // TBD + } + + /** + * Starts up and configures the game manager for an accepted + * invitation. + */ + protected void processAcceptedInvitation (Invitation invite) + { + try { + Log.info("Creating game manager [invite=" + invite + "]."); + + // configure the game config with the player info + invite.config.players = new Name[] { + invite.invitee.username, invite.inviter.username }; + + // create the game manager and begin it's initialization + // process. the game manager will take care of notifying the + // players that the game has been created once it has been + // started up (which is done by the place registry once the + // game object creation has completed) + GameManager gmgr = (GameManager) + _plreg.createPlace(invite.config, null); + + } catch (Exception e) { + Log.warning("Unable to create game manager [invite=" + invite + + ", error=" + e + "]."); + Log.logStackTrace(e); + } + } + + /** + * The invitation record is used by the parlor manager to keep track + * of pending invitations. + */ + protected static class Invitation + { + /** The unique identifier for this invitation. */ + public int inviteId = _nextInviteId++; + + /** The person proposing the invitation. */ + public BodyObject inviter; + + /** The person to whom the invitation is proposed. */ + public BodyObject invitee; + + /** The configuration of the game being proposed. */ + public GameConfig config; + + /** + * Constructs a new invitation with the specified participants and + * configuration. + */ + public Invitation (BodyObject inviter, BodyObject invitee, + GameConfig config) + { + this.inviter = inviter; + this.invitee = invitee; + this.config = config; + } + + /** + * Swaps the inviter and invitee which is necessary when the + * invitee responds with a counter-invitation. + */ + public void swapControl () + { + BodyObject tmp = inviter; + inviter = invitee; + invitee = tmp; + } + } + + /** The place registry with which we operate. */ + protected PlaceRegistry _plreg; + + /** The table of pending invitations. */ + protected HashIntMap _invites = new HashIntMap(); + + /** A counter used to generate unique identifiers for invitation + * records. */ + protected static int _nextInviteId = 0; +} diff --git a/src/java/com/threerings/parlor/server/ParlorProvider.java b/src/java/com/threerings/parlor/server/ParlorProvider.java new file mode 100644 index 00000000..83e2ec12 --- /dev/null +++ b/src/java/com/threerings/parlor/server/ParlorProvider.java @@ -0,0 +1,233 @@ +// +// $Id: ParlorProvider.java 3758 2005-11-10 23:18:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.server; + +import com.threerings.util.Name; + +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.client.InvocationService.InvocationListener; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.server.CrowdServer; +import com.threerings.crowd.server.PlaceManager; + +import com.threerings.parlor.Log; +import com.threerings.parlor.client.ParlorService; +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.data.TableConfig; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.server.GameManager; + +/** + * The parlor provider handles the server side of the various Parlor + * services that are made available for direct invocation by the client. + * Primarily these are the matchmaking mechanisms. + */ +public class ParlorProvider + implements InvocationProvider, ParlorCodes +{ + /** + * Constructs a parlor provider instance which will be used to handle + * all parlor-related invocation service requests. This is + * automatically taken care of by the parlor manager, so no other + * entity need instantiate and register a parlor provider. + * + * @param pmgr a reference to the parlor manager active in this + * server. + */ + public ParlorProvider (ParlorManager pmgr) + { + _pmgr = pmgr; + } + + /** + * Processes a request from the client to invite another user to play + * a game. + */ + public void invite (ClientObject caller, Name invitee, GameConfig config, + ParlorService.InviteListener listener) + throws InvocationException + { +// Log.info("Handling invite request [source=" + source + +// ", invitee=" + invitee + ", config=" + config + "]."); + + BodyObject source = (BodyObject)caller; + String rsp = null; + + // ensure that the invitee is online at present + BodyObject target = CrowdServer.lookupBody(invitee); + if (target == null) { + throw new InvocationException(INVITEE_NOT_ONLINE); + } + + // submit the invite request to the parlor manager + int inviteId = _pmgr.invite(source, target, config); + listener.inviteReceived(inviteId); + } + + /** + * Processes a request from the client to respond to an outstanding + * invitation by accepting, refusing, or countering it. + */ + public void respond (ClientObject caller, int inviteId, int code, + Object arg, InvocationListener listener) + { + // pass this on to the parlor manager + _pmgr.respondToInvite((BodyObject)caller, inviteId, code, arg); + } + + /** + * Processes a request from the client to cancel an outstanding + * invitation. + */ + public void cancel (ClientObject caller, int inviteId, + InvocationListener listener) + { + // pass this on to the parlor manager + _pmgr.cancelInvite((BodyObject)caller, inviteId); + } + + /** + * Processes a request from the client to create a new table. + */ + public void createTable ( + ClientObject caller, int lobbyOid, TableConfig tableConfig, + GameConfig config, ParlorService.TableListener listener) + throws InvocationException + { + Log.info("Handling create table request [caller=" + caller.who() + + ", lobbyOid=" + lobbyOid + ", config=" + config + "]."); + + // pass the creation request on to the table manager + TableManager tmgr = getTableManager(lobbyOid); + int tableId = tmgr.createTable((BodyObject)caller, tableConfig, config); + listener.tableCreated(tableId); + } + + /** + * Processes a request from the client to join an existing table. + */ + public void joinTable (ClientObject caller, int lobbyOid, int tableId, + int position, InvocationListener listener) + throws InvocationException + { + Log.info("Handling join table request [caller=" + caller.who() + + ", lobbyOid=" + lobbyOid + ", tableId=" + tableId + + ", position=" + position + "]."); + + // pass the join request on to the table manager + TableManager tmgr = getTableManager(lobbyOid); + tmgr.joinTable((BodyObject)caller, tableId, position); + + // there is normally no success response. the client will see + // themselves show up in the table that they joined + } + + /** + * Processes a request from the client to leave an existing table. + */ + public void leaveTable (ClientObject caller, int lobbyOid, int tableId, + InvocationListener listener) + throws InvocationException + { + Log.info("Handling leave table request [caller=" + caller.who() + + ", lobbyOid=" + lobbyOid + ", tableId=" + tableId + "]."); + + // pass the join request on to the table manager + TableManager tmgr = getTableManager(lobbyOid); + tmgr.leaveTable((BodyObject)caller, tableId); + + // there is normally no success response. the client will see + // themselves removed from the table they just left + } + + /** + * Handles a {@link ParlorService#startSolitaire} request. + */ + public void startSolitaire (ClientObject caller, GameConfig config, + InvocationService.ConfirmListener listener) + throws InvocationException + { + BodyObject user = (BodyObject)caller; + + Log.debug("Processing start puzzle [caller=" + user.who() + + ", config=" + config + "]."); + + try { + // just this fellow will be playing + if (config.players == null || config.players.length == 0) { + config.players = new Name[] { user.getVisibleName() }; + } + + // create the game manager and begin its initialization + // process + GameManager gmgr = (GameManager) + CrowdServer.plreg.createPlace(config, null); + + // the game manager will take care of notifying the player + // that the game has been created once it has been started up; + // but we let the caller know that we processed their request + listener.requestProcessed(); + + } catch (InstantiationException ie) { + Log.warning("Error instantiating game manager " + + "[for=" + caller.who() + ", config=" + config + "]."); + Log.logStackTrace(ie); + throw new InvocationException(INTERNAL_ERROR); + } + } + + /** + * Looks up the place manager associated with the supplied lobby oid, + * casts it to a table lobby manager and obtains the associated table + * manager reference. + * + * @exception InvocationException thrown if something goes wrong + * along the way like no place manager exists or the place manager + * that does exist doesn't implement table lobby manager. + */ + protected TableManager getTableManager (int lobbyOid) + throws InvocationException + { + PlaceManager plmgr = CrowdServer.plreg.getPlaceManager(lobbyOid); + if (plmgr == null) { + Log.warning("No place manager exists from which to obtain " + + "table manager reference [ploid=" + lobbyOid + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // sanity check + if (!(plmgr instanceof TableManagerProvider)) { + Log.warning("Place manager not a table lobby manager " + + "[plmgr=" + plmgr + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + return ((TableManagerProvider)plmgr).getTableManager(); + } + + /** A reference to the parlor manager we're working with. */ + protected ParlorManager _pmgr; +} diff --git a/src/java/com/threerings/parlor/server/ParlorSender.java b/src/java/com/threerings/parlor/server/ParlorSender.java new file mode 100644 index 00000000..9af4655b --- /dev/null +++ b/src/java/com/threerings/parlor/server/ParlorSender.java @@ -0,0 +1,85 @@ +// +// $Id: ParlorSender.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.server; + +import com.threerings.parlor.client.ParlorDecoder; +import com.threerings.parlor.client.ParlorReceiver; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationSender; +import com.threerings.util.Name; + +/** + * Used to issue notifications to a {@link ParlorReceiver} instance on a + * client. + */ +public class ParlorSender extends InvocationSender +{ + /** + * Issues a notification that will result in a call to {@link + * ParlorReceiver#gameIsReady} on a client. + */ + public static void gameIsReady ( + ClientObject target, int arg1) + { + sendNotification( + target, ParlorDecoder.RECEIVER_CODE, ParlorDecoder.GAME_IS_READY, + new Object[] { Integer.valueOf(arg1) }); + } + + /** + * Issues a notification that will result in a call to {@link + * ParlorReceiver#receivedInvite} on a client. + */ + public static void sendInvite ( + ClientObject target, int arg1, Name arg2, GameConfig arg3) + { + sendNotification( + target, ParlorDecoder.RECEIVER_CODE, ParlorDecoder.RECEIVED_INVITE, + new Object[] { Integer.valueOf(arg1), arg2, arg3 }); + } + + /** + * Issues a notification that will result in a call to {@link + * ParlorReceiver#receivedInviteCancellation} on a client. + */ + public static void sendInviteCancellation ( + ClientObject target, int arg1) + { + sendNotification( + target, ParlorDecoder.RECEIVER_CODE, ParlorDecoder.RECEIVED_INVITE_CANCELLATION, + new Object[] { Integer.valueOf(arg1) }); + } + + /** + * Issues a notification that will result in a call to {@link + * ParlorReceiver#receivedInviteResponse} on a client. + */ + public static void sendInviteResponse ( + ClientObject target, int arg1, int arg2, Object arg3) + { + sendNotification( + target, ParlorDecoder.RECEIVER_CODE, ParlorDecoder.RECEIVED_INVITE_RESPONSE, + new Object[] { Integer.valueOf(arg1), Integer.valueOf(arg2), arg3 }); + } + +} diff --git a/src/java/com/threerings/parlor/server/TableManager.java b/src/java/com/threerings/parlor/server/TableManager.java new file mode 100644 index 00000000..c8339262 --- /dev/null +++ b/src/java/com/threerings/parlor/server/TableManager.java @@ -0,0 +1,379 @@ +// +// $Id: TableManager.java 3804 2006-01-13 01:52:36Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.server; + +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.StringUtil; +import com.threerings.util.Name; + +import com.threerings.presents.dobj.ChangeListener; +import com.threerings.presents.dobj.ObjectAddedEvent; +import com.threerings.presents.dobj.ObjectDeathListener; +import com.threerings.presents.dobj.ObjectDestroyedEvent; +import com.threerings.presents.dobj.ObjectRemovedEvent; +import com.threerings.presents.dobj.OidListListener; +import com.threerings.presents.server.InvocationException; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.server.CrowdServer; +import com.threerings.crowd.server.PlaceManager; +import com.threerings.crowd.server.PlaceRegistry; + +import com.threerings.parlor.Log; +import com.threerings.parlor.data.ParlorCodes; +import com.threerings.parlor.data.Table; +import com.threerings.parlor.data.TableConfig; +import com.threerings.parlor.data.TableLobbyObject; +import com.threerings.parlor.game.data.GameConfig; +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.game.server.GameManager; + +/** + * A table manager can be used by a place manager to take care of the + * management of a table matchmaking service in the place managed by the + * place manager. The place manager need instantiate a table manager and + * implement the {@link TableManagerProvider} interface to provide access + * to the table manager for the associated invocation services. + */ +public class TableManager + implements ParlorCodes, OidListListener +{ + /** + * Creates a table manager that will work in tandem with the specified + * place manager to manage a table matchmaking service in this place. + */ + public TableManager (PlaceManager plmgr) + { + // get a reference to our place object + _plobj = plmgr.getPlaceObject(); + + // add ourselves as an oidlist listener to this lobby so that we + // can tell if a user leaves the lobby without leaving their table + _plobj.addListener(this); + + // make sure it implements table lobby object + _tlobj = (TableLobbyObject)_plobj; + } + + /** + * Requests that a new table be created to matchmake the game + * described by the supplied game config instance. The config instance + * provided must implement the {@link TableConfig} interface so that + * the parlor services can determine how to configure the table that + * will be created. + * + * @param creator the body object that will own the new table. + * @param tableConfig the configuration parameters for the table. + * @param config the configuration of the game to be created. + * + * @return the id of the newly created table. + * + * @exception InvocationException thrown if the table creation was + * not able processed for some reason. The explanation will be + * provided in the message data of the exception. + */ + public int createTable (BodyObject creator, TableConfig tableConfig, + GameConfig config) + throws InvocationException + { + // make sure the creator is an occupant of the lobby in which + // they are requesting to create a table + if (!_plobj.occupants.contains(creator.getOid())) { + Log.warning("Requested to create a table in a lobby not " + + "occupied by the creator [creator=" + creator + + ", loboid=" + _plobj.getOid() + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // create a brand spanking new table + Table table = new Table(_plobj.getOid(), tableConfig, config); + + // stick the creator into the first non-AI position + int cpos = (config.ais == null) ? 0 : config.ais.length; + String error = table.setOccupant(cpos, creator); + if (error != null) { + Log.warning("Unable to add creator to position zero of " + + "table!? [table=" + table + ", creator=" + creator + + ", error=" + error + "]."); + // bail out now and abort the table creation process + throw new InvocationException(error); + } + + // stick the table into the table lobby object + _tlobj.addToTables(table); + + // make a mapping from the creator to this table + _boidMap.put(creator.getOid(), table); + + // also stick it into our tables tables + _tables.put(table.getTableId(), table); + + // if the table has only one seat, start the game immediately + if (table.shouldBeStarted()) { + createGame(table); + } + + // finally let the caller know what the new table id is + return table.getTableId(); + } + + /** + * Requests that the specified user be added to the specified table at + * the specified position. If the user successfully joins the table, + * the function will return normally. If they are not able to join for + * some reason (the slot is already full, etc.), a {@link + * InvocationException} will be thrown with a message code that + * describes the reason for failure. If the user does successfully + * join, they will be added to the table entry in the tables set in + * the place object that is hosting the table. + * + * @param joiner the body object of the user that wishes to join the + * table. + * @param tableId the id of the table to be joined. + * @param position the position at which to join the table. + * + * @exception InvocationException thrown if the joining was not able + * processed for some reason. The explanation will be provided in the + * message data of the exception. + */ + public void joinTable (BodyObject joiner, int tableId, int position) + throws InvocationException + { + // look the table up + Table table = (Table)_tables.get(tableId); + if (table == null) { + throw new InvocationException(NO_SUCH_TABLE); + } + + // request that the user be added to the table at that position + String error = table.setOccupant(position, joiner); + // if that failed, report the error + if (error != null) { + throw new InvocationException(error); + } + + // if the table is sufficiently full, start the game automatically + if (table.shouldBeStarted()) { + createGame(table); + } else { + // make a mapping from this occupant to this table + _boidMap.put(joiner.getOid(), table); + } + + // update the table in the lobby + _tlobj.updateTables(table); + } + + /** + * Requests that the specified user be removed from the specified + * table. If the user successfully leaves the table, the function will + * return normally. If they are not able to leave for some reason + * (they aren't sitting at the table, etc.), a {@link + * InvocationException} will be thrown with a message code that + * describes the reason for failure. + * + * @param leaver the body object of the user that wishes to leave the + * table. + * @param tableId the id of the table to be left. + * + * @exception InvocationException thrown if the leaving was not able + * processed for some reason. The explanation will be provided in the + * message data of the exception. + */ + public void leaveTable (BodyObject leaver, int tableId) + throws InvocationException + { + // look the table up + Table table = (Table)_tables.get(tableId); + if (table == null) { + throw new InvocationException(NO_SUCH_TABLE); + } + + // request that the user be removed from the table + if (!table.clearOccupant(leaver.getVisibleName())) { + throw new InvocationException(NOT_AT_TABLE); + } + + // remove the mapping from this user to the table + if (_boidMap.remove(leaver.getOid()) == null) { + Log.warning("No body to table mapping to clear? " + + "[leaver=" + leaver + ", table=" + table + "]."); + } + + // either update or delete the table depending on whether or not + // we just removed the last occupant + if (table.isEmpty()) { + purgeTable(table); + } else { + _tlobj.updateTables(table); + } + } + + /** + * Removes the table from all of our internal tables and from its + * lobby's distributed object. + */ + protected void purgeTable (Table table) + { + // remove the table from our tables table + _tables.remove(table.getTableId()); + + // clear out all matching entries in the boid map + for (int i = 0; i < table.bodyOids.length; i++) { + _boidMap.remove(table.bodyOids[i]); + } + + // remove the table from the lobby object + _tlobj.removeFromTables(table.tableId); + } + + /** + * Called when we're ready to create a game (either an invitation has + * been accepted or a table is ready to start. If there is a problem + * creating the game manager, it should be reported in the logs. + */ + protected void createGame (final Table table) + throws InvocationException + { + // fill the players array into the game config + table.config.players = table.getPlayers(); + + PlaceRegistry.CreationObserver obs = + new PlaceRegistry.CreationObserver() { + public void placeCreated (PlaceObject plobj, PlaceManager pmgr) { + gameCreated(table, plobj); + } + }; + try { + CrowdServer.plreg.createPlace(table.config, obs); + } catch (Throwable t) { + Log.warning("Failed to create manager for game " + + "[config=" + table.config + "]: " + t); + throw new InvocationException(INTERNAL_ERROR); + } + } + + /** + * Called when our game has been created, we take this opportunity to + * clean up the table and transition it to "in play" mode. + */ + protected void gameCreated (Table table, PlaceObject plobj) + { + // update the table with the newly created game object + table.gameOid = plobj.getOid(); + + // configure the privacy of the game + ((GameObject) plobj).setIsPrivate(table.tconfig.privateTable); + + // clear the occupant to table mappings as this game is underway + for (int i = 0; i < table.bodyOids.length; i++) { + _boidMap.remove(table.bodyOids[i]); + } + + // add an object death listener to unmap the table when the game + // finally goes away + plobj.addListener(_gameDeathListener); + + // and then update the lobby object that contains the table + _tlobj.updateTables(table); + } + + /** + * Called when a game created from a table managed by this table + * manager was destroyed. We remove the associated table. + */ + protected void unmapTable (int gameOid) + { + // look through our tables table for a table with a matching game + Iterator iter = _tables.values().iterator(); + while (iter.hasNext()) { + Table table = (Table)iter.next(); + if (table.gameOid == gameOid) { + purgeTable(table); + return; // all done + } + } + + Log.warning("Requested to unmap table that wasn't mapped " + + "[gameOid=" + gameOid + "]."); + } + + // documentation inherited + public void objectAdded (ObjectAddedEvent event) + { + // nothing doing + } + + // documentation inherited + public void objectRemoved (ObjectRemovedEvent event) + { + // if an occupant departed, see if they are in a pending table + if (!event.getName().equals(PlaceObject.OCCUPANTS)) { + return; + } + + // look up the table to which this occupant is mapped + int bodyOid = event.getOid(); + Table pender = (Table)_boidMap.remove(bodyOid); + if (pender == null) { + return; + } + + // remove this occupant from the table + if (!pender.clearOccupant(bodyOid)) { + Log.warning("Attempt to remove body from mapped table failed " + + "[table=" + pender + ", bodyOid=" + bodyOid + "]."); + return; + } + + // either update or delete the table depending on whether or not + // we just removed the last occupant + if (pender.isEmpty()) { + purgeTable(pender); + } else { + _tlobj.updateTables(pender); + } + } + + /** A reference to the place object in which we're managing tables. */ + protected PlaceObject _plobj; + + /** A reference to our place object casted to a table lobby object. */ + protected TableLobbyObject _tlobj; + + /** The table of pending tables. */ + protected HashIntMap _tables = new HashIntMap(); + + /** A mapping from body oid to table. */ + protected HashIntMap _boidMap = new HashIntMap(); + + /** A listener that prunes tables after the game dies. */ + protected ChangeListener _gameDeathListener = new ObjectDeathListener() { + public void objectDestroyed (ObjectDestroyedEvent event) { + unmapTable(event.getTargetOid()); + } + }; +} diff --git a/src/java/com/threerings/parlor/server/TableManagerProvider.java b/src/java/com/threerings/parlor/server/TableManagerProvider.java new file mode 100644 index 00000000..39d8ebbd --- /dev/null +++ b/src/java/com/threerings/parlor/server/TableManagerProvider.java @@ -0,0 +1,37 @@ +// +// $Id: TableManagerProvider.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.server; + +/** + * A place manager that wishes to provide table matchmaking services in + * its place needs to create a table manager and make it available by + * implementing this interface. The table invocation services and the + * table manager will take care of the rest. + */ +public interface TableManagerProvider +{ + /** + * Returns a reference to the table manager that is responsible for + * table management in this lobby. + */ + public TableManager getTableManager (); +} diff --git a/src/java/com/threerings/parlor/turn/client/TurnDisplay.java b/src/java/com/threerings/parlor/turn/client/TurnDisplay.java new file mode 100644 index 00000000..2e9297c9 --- /dev/null +++ b/src/java/com/threerings/parlor/turn/client/TurnDisplay.java @@ -0,0 +1,217 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.turn.client; + +import java.awt.Color; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; + +import java.util.HashMap; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.Icon; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.VGroupLayout; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.util.Name; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.turn.data.TurnGameObject; + +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; +import com.threerings.presents.dobj.ElementUpdateListener; +import com.threerings.presents.dobj.ElementUpdatedEvent; +import com.threerings.presents.dobj.DObject; + +/** + * Automatically display a list of players and turn change information + * in a turn-based game. + */ +// TODO +// - adapt this to be able to display scores in some generic way as well. +// - allow configuring of turn / winner labels from prototypes, +// rather than forcing one to be an icon, the other a string, and +// examine the prototype to determine how to highlight the turnholder. +public class TurnDisplay extends JPanel + implements PlaceView, AttributeChangeListener, ElementUpdateListener +{ + /** + * Create a TurnDisplay. + */ + public TurnDisplay () + { + } + + /** + * Create a TurnDisplay for a game using the specified Icon to denote + * whose turn it is. + */ + public TurnDisplay (Icon turnIcon) + { + setTurnIcon(turnIcon); + } + + /** + * Set the icon to use. + */ + public void setTurnIcon (Icon turnIcon) + { + _turnIcon = turnIcon; + if (_turnObj != null) { + createList(); + } + } + + /** + * Set the text to be displayed next to the winner's name. + */ + public void setWinnerText (String winnerText) + { + _winnerText = winnerText; + } + + /** + * Set optional icons to use for identifying each player in the game. + */ + public void setPlayerIcons (Icon[] icons) + { + _playerIcons = icons; + if (_turnObj != null) { + createList(); + } + } + + /** + * Create the list of names and highlight as appropriate. + */ + protected void createList () + { + removeAll(); + _labels.clear(); + + GridBagLayout gridbag = new GridBagLayout(); + setLayout(gridbag); + + GridBagConstraints iconC = new GridBagConstraints(); + GridBagConstraints labelC = new GridBagConstraints(); + iconC.fill = labelC.fill = GridBagConstraints.BOTH; + labelC.weightx = 1.0; + labelC.insets.left = 10; + labelC.gridwidth = GridBagConstraints.REMAINDER; + + Name[] names = _turnObj.getPlayers(); + boolean[] winners = ((GameObject) _turnObj).winners; + Name holder = _turnObj.getTurnHolder(); + for (int ii=0, jj=0; ii < names.length; ii++, jj++) { + if (names[ii] == null) continue; + + JLabel iconLabel = new JLabel(); + if (winners == null) { + if (names[ii].equals(holder)) { + iconLabel.setIcon(_turnIcon); + } + } else if (winners[ii]) { + iconLabel.setText(_winnerText); + iconLabel.setForeground(Color.GREEN); + } + _labels.put(names[ii], iconLabel); + add(iconLabel, iconC); + + JLabel label = new JLabel(names[ii].toString()); + if (_playerIcons != null) { + label.setIcon(_playerIcons[jj]); + } + add(label, labelC); + } + + SwingUtil.refresh(this); + } + + // documentation inherited from interface PlaceView + public void willEnterPlace (PlaceObject plobj) + { + _turnObj = (TurnGameObject) plobj; + plobj.addListener(this); + createList(); + } + + // documentation inherited from interface PlaceView + public void didLeavePlace (PlaceObject plobj) + { + plobj.removeListener(this); + _turnObj = null; + removeAll(); + } + + // documentation inherited from interface AttributeChangeListener + public void attributeChanged (AttributeChangedEvent event) + { + String name = event.getName(); + if (name.equals(_turnObj.getTurnHolderFieldName())) { + JLabel oldLabel = (JLabel) _labels.get((Name) event.getOldValue()); + if (oldLabel != null) { + oldLabel.setIcon(null); + } + JLabel newLabel = (JLabel) _labels.get((Name) event.getValue()); + if (newLabel != null) { + newLabel.setIcon(_turnIcon); + } + + } else if (name.equals(GameObject.PLAYERS)) { + createList(); + + } else if (name.equals(GameObject.WINNERS)) { + createList(); + } + } + + // documentation inherited from interface ElementUpdateListener + public void elementUpdated (ElementUpdatedEvent event) + { + String name = event.getName(); + if (name.equals(GameObject.PLAYERS)) { + createList(); + } + } + + /** The TurnGameObject we're displaying. */ + protected TurnGameObject _turnObj; + + /** A mapping of the labels currently associated with each player. */ + protected HashMap _labels = new HashMap(); + + /** The game-specified player icons. */ + protected Icon[] _playerIcons; + + /** The text to display next to a winner's name. */ + protected String _winnerText; + + /** The Icon we use for indicating the turn. */ + protected Icon _turnIcon; +} diff --git a/src/java/com/threerings/parlor/turn/client/TurnGameController.java b/src/java/com/threerings/parlor/turn/client/TurnGameController.java new file mode 100644 index 00000000..e6859834 --- /dev/null +++ b/src/java/com/threerings/parlor/turn/client/TurnGameController.java @@ -0,0 +1,44 @@ +// +// $Id: TurnGameController.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.turn.client; + +import com.threerings.util.Name; + +import com.threerings.parlor.game.client.GameController; + +/** + * Games that wish to make use of the turn game services should have their + * controller implement this interface and create an instance of {@link + * TurnGameControllerDelegate} which should be passed to {@link + * GameController#addDelegate}. + */ +public interface TurnGameController +{ + /** + * Called when the turn changed. This indicates the start of a turn + * and the user interface should adjust itself accordingly (activating + * controls if it is our turn and deactivating them if it is not). + * + * @param turnHolder the username of the new holder of the turn. + */ + public void turnDidChange (Name turnHolder); +} diff --git a/src/java/com/threerings/parlor/turn/client/TurnGameControllerDelegate.java b/src/java/com/threerings/parlor/turn/client/TurnGameControllerDelegate.java new file mode 100644 index 00000000..108cc717 --- /dev/null +++ b/src/java/com/threerings/parlor/turn/client/TurnGameControllerDelegate.java @@ -0,0 +1,149 @@ +// +// $Id: TurnGameControllerDelegate.java 3758 2005-11-10 23:18:58Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.turn.client; + +import com.threerings.util.Name; + +import com.threerings.presents.dobj.AttributeChangedEvent; +import com.threerings.presents.dobj.AttributeChangeListener; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.parlor.game.client.GameController; +import com.threerings.parlor.game.client.GameControllerDelegate; +import com.threerings.parlor.game.data.GameObject; + +import com.threerings.parlor.turn.data.TurnGameObject; + +/** + * Performs the client-side processing for a turn-based game. Games which + * wish to make use of these services must construct a delegate and call + * out to it at the appropriate times (see the method documentation for + * which methods should be called when). The game's controller must also + * implement the {@link TurnGameController} interface so that it can be + * notified when turn-based game events take place. + */ +public class TurnGameControllerDelegate extends GameControllerDelegate + implements AttributeChangeListener +{ + /** + * Constructs a delegate which will call back to the supplied {@link + * TurnGameController} implementation wen turn-based game related + * things happen. + */ + public TurnGameControllerDelegate (TurnGameController tgctrl) + { + super((GameController)tgctrl); + + // keep this around for later + _tgctrl = tgctrl; + } + + /** + * Returns true if the game is in progress and it is our turn; false + * otherwise. + */ + public boolean isOurTurn () + { + BodyObject self = (BodyObject)_ctx.getClient().getClientObject(); + return (_gameObj.state == GameObject.IN_PLAY && + self.getVisibleName().equals(_turnGame.getTurnHolder())); + } + + /** + * Returns the index of the current turn holder as configured in the + * game object. + * + * @return the index into the players array of the current turn holder + * or -1 if there is no current turn holder. + */ + public int getTurnHolderIndex () + { + return _gameObj.getPlayerIndex(_turnGame.getTurnHolder()); + } + + // documentation inherited + public void init (CrowdContext ctx, PlaceConfig config) + { + _ctx = ctx; + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + // get a casted reference to the object + _gameObj = (GameObject)plobj; + _turnGame = (TurnGameObject)plobj; + _thfield = _turnGame.getTurnHolderFieldName(); + + // and add ourselves as a listener + plobj.addListener(this); + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + // remove our listenership + plobj.removeListener(this); + + // clean up + _turnGame = null; + } + + // documentation inherited + public void attributeChanged (AttributeChangedEvent event) + { + // handle turn changes + if (event.getName().equals(_thfield)) { + Name name = (Name)event.getValue(); + Name oname = (Name)event.getOldValue(); + if (TurnGameObject.TURN_HOLDER_REPLACED.equals(name) || + TurnGameObject.TURN_HOLDER_REPLACED.equals(oname)) { + // small hackery: ignore the turn holder being set to + // TURN_HOLDER_REPLACED as it means that we're replacing + // the current turn holder rather than switching turns; + // also ignore the new turn holder when we switch from THR + // to a real name again + } else { + _tgctrl.turnDidChange(name); + } + } + } + + /** The turn game controller for whom we are delegating. */ + protected TurnGameController _tgctrl; + + /** A reference to our client context. */ + protected CrowdContext _ctx; + + /** A reference to our game object. */ + protected GameObject _gameObj; + + /** A casted reference to our game object as a turn game. */ + protected TurnGameObject _turnGame; + + /** The name of the turn holder field. */ + protected String _thfield; +} diff --git a/src/java/com/threerings/parlor/turn/data/TurnGameObject.java b/src/java/com/threerings/parlor/turn/data/TurnGameObject.java new file mode 100644 index 00000000..d1812e50 --- /dev/null +++ b/src/java/com/threerings/parlor/turn/data/TurnGameObject.java @@ -0,0 +1,64 @@ +// +// $Id: TurnGameObject.java 3667 2005-08-03 07:46:54Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.turn.data; + +import com.threerings.util.Name; + +import com.threerings.parlor.game.data.GameObject; + +/** + * Games that wish to support turn-based play must implement this + * interface with their {@link GameObject}. + */ +public interface TurnGameObject +{ + /** A special value used to communicate to the client that the current + * turn holder was replaced (perhaps due to disconnection or departure + * and being replaced by an AI). */ + public static final Name TURN_HOLDER_REPLACED = + new Name("__TURN_HOLDER_REPLACED__"); + + /** + * Returns the distributed object field name of the + * turnHolder field in the object that implements this + * interface. + */ + public String getTurnHolderFieldName (); + + /** + * Returns the username of the player who is currently taking their + * turn in this turn-based game or null if no user + * currently holds the turn. + */ + public Name getTurnHolder (); + + /** + * Requests that the turnHolder field be set to the specified + * value. + */ + public void setTurnHolder (Name turnHolder); + + /** + * Returns the array of player names involved in the game. + */ + public Name[] getPlayers (); +} diff --git a/src/java/com/threerings/parlor/turn/server/TurnGameManager.java b/src/java/com/threerings/parlor/turn/server/TurnGameManager.java new file mode 100644 index 00000000..197382ae --- /dev/null +++ b/src/java/com/threerings/parlor/turn/server/TurnGameManager.java @@ -0,0 +1,92 @@ +// +// $Id: TurnGameManager.java 3390 2005-03-10 00:13:25Z tedv $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.turn.server; + +import com.threerings.util.Name; + +import com.threerings.parlor.game.server.GameManager; + +/** + * A game manager that wishes to make use of the turn game services should + * implement this interface and create a {@link TurnGameManagerDelegate} + * which will perform the basic turn game processing and call back to the + * main manager via this interface. + * + *

The basic flow of a turn-based game is as follows: + *

+ * GameManager.gameWillStart()
+ * GameManager.gameDidStart()
+ *   TurnGameManagerDelegate.setFirstTurnHolder()
+ *   TurnGameManagerDelegate.startTurn()
+ *     TurnGameManager.turnWillStart()
+ *   TurnGameManagerDelegate.endTurn()
+ *     TurnGameManager.turnDidEnd()
+ *   TurnGameManagerDelegate.setNextTurnHolder()
+ *   TurnGameManagerDelegate.startTurn()
+ *     ...
+ * GameManager.endGame()
+ * 
+ */ +public interface TurnGameManager +{ + /** + * Extending {@link GameManager} should automatically handle + * implementing this method. + */ + public Name getPlayerName (int index); + + /** + * Extending {@link GameManager} should automatically handle + * implementing this method. + */ + public int getPlayerIndex (Name username); + + /** + * Extending {@link GameManager} should automatically handle + * implementing this method. + */ + public int getPlayerCount (); + + /** + * Extending {@link GameManager} should automatically handle + * implementing this method. + */ + public boolean isActivePlayer (int pidx); + + /** + * Called when we are about to start the next turn. Implementations + * can do whatever pre-start turn activities need to be done. + */ + public void turnWillStart (); + + /** + * Called when we have started the next turn. Implementations can do + * whatever post-start turn activities need to be done. + */ + public void turnDidStart (); + + /** + * Called when the turn was ended. Implementations can perform any + * post-turn processing (like updating scores, etc.). + */ + public void turnDidEnd (); +} diff --git a/src/java/com/threerings/parlor/turn/server/TurnGameManagerDelegate.java b/src/java/com/threerings/parlor/turn/server/TurnGameManagerDelegate.java new file mode 100644 index 00000000..b6c95dec --- /dev/null +++ b/src/java/com/threerings/parlor/turn/server/TurnGameManagerDelegate.java @@ -0,0 +1,246 @@ +// +// $Id: TurnGameManagerDelegate.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.turn.server; + +import com.samskivert.util.RandomUtil; +import com.threerings.util.Name; + +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.Log; +import com.threerings.parlor.game.server.GameManager; +import com.threerings.parlor.game.server.GameManagerDelegate; + +import com.threerings.parlor.turn.data.TurnGameObject; + +/** + * Performs the server-side turn-based game processing for a turn based + * game. Game managers which wish to make use of the turn services must + * implement {@link TurnGameManager} and either create an instance of this + * class, or an instance of a derivation which customizes the behavior, + * either of which would be passed to {@link GameManager#addDelegate} to + * be activated. + */ +public class TurnGameManagerDelegate extends GameManagerDelegate +{ + /** + * Constructs a delegate that will manage the turn game state and call + * back to the supplied {@link TurnGameManager} implementation to let + * it in on the progression of the game. + */ + public TurnGameManagerDelegate (TurnGameManager tgmgr) + { + super((GameManager)tgmgr); + _tgmgr = tgmgr; + } + + /** + * Returns the index of the current turn holder as configured in the + * game object. + * + * @return the index into the players array of the current turn holder + * or -1 if there is no current turn holder. + */ + public int getTurnHolderIndex () + { + return _tgmgr.getPlayerIndex(_turnGame.getTurnHolder()); + } + + /** Test if it's the inputted player's turn. */ + public boolean isPlayersTurn (int playerIndex) + { + // Don't accidently match a visitor's id of -1 with the "no one's + // turn" state of turn -1. + int turnHolder = getTurnHolderIndex(); + if (turnHolder < 0) { + return false; + } + + // It's this player's turn if the ids match + return (turnHolder == playerIndex); + } + + /** + * Called to start the next turn. It calls {@link + * TurnGameManager#turnWillStart} to allow our owning manager to + * perform any pre-start turn processing, sets the turn holder that + * was configured either when the game started or when finishing up + * the last turn, and then calls {@link TurnGameManager#turnDidStart} + * to allow the manager to perform any post-start turn + * processing. This assumes that a valid turn holder has been + * assigned. If some pre-game preparation needs to take place in a + * non-turn-based manner, this function should not be called until it + * is time to start the first turn. + */ + public void startTurn () + { + // sanity check + if (_turnIdx < 0 || _turnIdx >= _turnGame.getPlayers().length) { + Log.warning("startTurn() called with invalid turn index " + + "[game=" + where() + ", turnIdx=" + _turnIdx + "]."); + // abort, abort + return; + } + + // get the player name and sanity-check again + Name name = _tgmgr.getPlayerName(_turnIdx); + if (name == null) { + Log.warning("startTurn() called with invalid player " + + "[game=" + where() + ", turnIdx=" + _turnIdx + "]."); + return; + } + + // do pre-start processing + _tgmgr.turnWillStart(); + + // and set the turn indicator accordingly + _turnGame.setTurnHolder(name); + + // do post-start processing + _tgmgr.turnDidStart(); + } + + /** + * Called to end the turn. Whatever indication a game manager has that + * the turn has ended (probably the submission of a valid move of some + * sort by the turn holding player), it should call this function to + * cause this turn to end and the next to begin. + * + *

If the next turn should not be started immediately after this + * turn, the game manager should arrange for {@link + * #setNextTurnHolder} to set the {@link #_turnIdx} field to + * -1 which will cause us not to start the next turn. It + * can then call {@link GameManager#endGame} if the game is over or do + * whatever else it needs to do outside the context of the turn flow. + * To start things back up again it would set {@link #_turnIdx} to the + * next turn holder and call {@link #startTurn} itself. + */ + public void endTurn () + { + // let the manager know that the turn is over + _tgmgr.turnDidEnd(); + + // figure out who's up next + setNextTurnHolder(); + + // and start the next turn if desired + if (_turnIdx != -1) { + startTurn(); + + } else { + // otherwise, clear out the turn holder + _turnGame.setTurnHolder(null); + } + } + + // documentation inherited + public void didStartup (PlaceObject plobj) + { + _turnGame = (TurnGameObject)plobj; + } + + // documentation inherited + public void playerWasReplaced (int pidx, Name oplayer, Name nplayer) + { + // we need to update the turn holder if the current turn holder + // was the player that was replaced and we need to do so in a way + // that doesn't make everyone think that the turn just changed + if (oplayer != null && oplayer.equals(_turnGame.getTurnHolder())) { + // small hackery: this will indicate to the client that we are + // replacing the turn holder rather than changing the turn + _turnGame.setTurnHolder(TurnGameObject.TURN_HOLDER_REPLACED); + _turnGame.setTurnHolder(nplayer); + } + } + + /** + * This should be called from {@link GameManager#gameDidStart} to let + * the turn delegate perform start of game processing. + */ + public void gameDidStart () + { + // figure out who will be first + setFirstTurnHolder(); + + // and start the first turn if we should apparently do so + if (_turnIdx != -1) { + startTurn(); + } + } + + /** + * This is called to determine which player will take the first + * turn. The default implementation chooses a player at random. + */ + protected void setFirstTurnHolder () + { + int size = _turnGame.getPlayers().length; + int firstPick = _turnIdx = RandomUtil.getInt(size); + while (!_tgmgr.isActivePlayer(_turnIdx)) { + _turnIdx = (_turnIdx + 1) % size; + if (_turnIdx == firstPick) { + Log.warning("No players eligible for first turn. Choking. " + + "[game=" + where() + "]."); + return; + } + } + } + + /** + * This is called to determine which player will next hold the turn. + * The default implementation simply rotates through the players in + * order, but some games may need to mess with the turn from time to + * time. This should update the _turnIdx field, not set + * the turn holder field in the game object directly. + */ + protected void setNextTurnHolder () + { + // stick with the current player if they're the only participant + if (_tgmgr.getPlayerCount() == 1) { + return; + } + + // find the next occupied active player slot + int size = _turnGame.getPlayers().length; + int oturnIdx = _turnIdx; + do { + _turnIdx = (_turnIdx + 1) % size; + if (_turnIdx == oturnIdx) { + // if we've wrapped all the way around, stop where we are + // even if the current player is not active. + Log.warning("1 or less active players. Unable to properly " + + "change turn. [game=" + where() + "]."); + break; + } + } while (!_tgmgr.isActivePlayer(_turnIdx)); + } + + /** The game manager for which we are delegating. */ + protected TurnGameManager _tgmgr; + + /** A reference to our game object. */ + protected TurnGameObject _turnGame; + + /** The player index of the current turn holder or -1 if + * it's no one's turn. */ + protected int _turnIdx = -1; +} diff --git a/src/java/com/threerings/parlor/util/ParlorContext.java b/src/java/com/threerings/parlor/util/ParlorContext.java new file mode 100644 index 00000000..f1606cc0 --- /dev/null +++ b/src/java/com/threerings/parlor/util/ParlorContext.java @@ -0,0 +1,37 @@ +// +// $Id: ParlorContext.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.util; + +import com.threerings.crowd.util.CrowdContext; +import com.threerings.parlor.client.ParlorDirector; + +/** + * The parlor context provides access to the various managers, etc. that + * are needed by the parlor client code. + */ +public interface ParlorContext extends CrowdContext +{ + /** + * Returns a reference to the parlor director. + */ + public ParlorDirector getParlorDirector (); +} diff --git a/src/java/com/threerings/parlor/util/RobotPlayer.java b/src/java/com/threerings/parlor/util/RobotPlayer.java new file mode 100644 index 00000000..72219a4e --- /dev/null +++ b/src/java/com/threerings/parlor/util/RobotPlayer.java @@ -0,0 +1,128 @@ +// +// $Id: RobotPlayer.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor.util; + +import java.awt.Component; +import java.util.ArrayList; + +import com.samskivert.swing.Controller; +import com.samskivert.util.CollectionUtil; +import com.samskivert.util.Interval; +import com.samskivert.util.RandomUtil; + +import com.threerings.util.KeyTranslator; + +/** + * The robot player is a computer player with truly rudimentary artificial + * intelligence that periodically posts random commands selected from the + * available key press and release commands to the target component. + * + * Note that {@link java.awt.Robot} could have been used to post key + * events to the target component rather than commands, but not all key + * events can be simulated in that fashion (e.g., a right shift key + * press), and this seemed somehow more proper in any case. + */ +public class RobotPlayer extends Interval +{ + /** + * Constructs a robot player. + */ + public RobotPlayer (Component target, KeyTranslator xlate) + { + // save off references + _target = target; + _xlate = xlate; + + // build the list of available commands + CollectionUtil.addAll(_press, _xlate.enumeratePressCommands()); + CollectionUtil.addAll(_release, _xlate.enumerateReleaseCommands()); + } + + /** + * Sets whether the robot player is actively posting action commands. + */ + public void setActive (boolean active) + { + if (active != _active) { + if (active) { + schedule(_robotDelay, true); + } else { + cancel(); // stop the robot player + } + _active = active; + } + } + + /** + * Sets the delay in milliseconds between posting each action command. + */ + public void setRobotDelay (long delay) + { + _robotDelay = delay; + + // if the robot is active, reset it with the new delay time + if (isActive()) { + setActive(false); + setActive(true); + } + } + + /** + * Returns whether the robot is currently active and periodically + * posting action commands. + */ + public boolean isActive () + { + return _active; + } + + // documentation inherited + public void expired () + { + // post a random key press command + int idx = RandomUtil.getInt(_press.size()); + String command = (String)_press.get(idx); + // Log.info("Posting artificial command [cmd=" + command + "]."); + Controller.postAction(_target, command); + } + + /** The default robot delay. */ + protected static final long DEFAULT_ROBOT_DELAY = 500L; + + /** Whether the robot is active or not. */ + protected boolean _active = false; + + /** The milliseconds between posting each action command. */ + protected long _robotDelay = DEFAULT_ROBOT_DELAY; + + /** The list of available key press action commands. */ + protected ArrayList _press = new ArrayList(); + + /** The list of available key release action commands. */ + protected ArrayList _release = new ArrayList(); + + /** The key translator that describes available keys and commands. */ + protected KeyTranslator _xlate; + + /** The target component associated with game action commands. */ + protected Component _target; +} diff --git a/src/java/com/threerings/puzzle/Log.java b/src/java/com/threerings/puzzle/Log.java new file mode 100644 index 00000000..10488fbe --- /dev/null +++ b/src/java/com/threerings/puzzle/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("puzzle"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/puzzle/client/PlayerStatusView.java b/src/java/com/threerings/puzzle/client/PlayerStatusView.java new file mode 100644 index 00000000..8107d06e --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PlayerStatusView.java @@ -0,0 +1,124 @@ +// +// $Id: PlayerStatusView.java 3664 2005-07-28 21:10:11Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import javax.swing.JPanel; + +import com.threerings.util.Name; + +import com.threerings.parlor.game.data.GameObject; + +import com.threerings.puzzle.data.BoardSummary; +import com.threerings.puzzle.data.PuzzleConfig; +import com.threerings.puzzle.data.PuzzleObject; + +/** + * The player status view displays a player's current status in the game. + */ +public class PlayerStatusView extends JPanel +{ + /** + * Constructs a player status view. + */ + public PlayerStatusView (GameObject gameobj, int pidx) + { + // save off references + _gameobj = gameobj; + _username = _gameobj.players[pidx]; + _pidx = pidx; + + // configure the panel + setOpaque(false); + } + + /** + * Initializes the player status view with the puzzle config. + */ + public void init (PuzzleConfig config) + { + // nothing for now + } + + /** + * Get the player index of the player represented by this view. + */ + public int getPlayerIndex () + { + return _pidx; + } + + /** + * Sets the player board summary. + */ + public void setBoardSummary (BoardSummary summary) + { + _summary = summary; + repaint(); + } + + /** + * Sets the player status. + */ + public void setStatus (int status) + { + if (_status != status) { + _status = status; + repaint(); + } + } + + /** + * Sets whether to highlight the player status display when rendered. + */ + public void setHighlighted (boolean highlight) + { + if (_highlight != highlight) { + _highlight = highlight; + repaint(); + } + } + + /** Returns a string representation of this instance. */ + public String toString () + { + return "[user=" + _username + ", pidx=" + _pidx + + ", status=" + _status + "]"; + } + + /** The game object associated with this view. */ + protected GameObject _gameobj; + + /** The player name. */ + protected Name _username; + + /** The player index. */ + protected int _pidx; + + /** Whether this display is highlighted. */ + protected boolean _highlight; + + /** The player board summary. */ + protected BoardSummary _summary; + + /** The player game status. */ + protected int _status = PuzzleObject.PLAYER_IN_PLAY; +} diff --git a/src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java b/src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java new file mode 100644 index 00000000..68ec4826 --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzleAnimationWaiter.java @@ -0,0 +1,74 @@ +// +// $Id: PuzzleAnimationWaiter.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import com.threerings.media.animation.AnimationWaiter; + +import com.threerings.puzzle.data.PuzzleObject; + +/** + * An animation waiter to be used with puzzles that want to modify the + * game object or board in some way after the animations end, and would + * like to do so in a safe fashion such that their changes aren't + * unwittingly performed on game data for a subsequent round of the + * puzzle. + */ +public abstract class PuzzleAnimationWaiter extends AnimationWaiter +{ + /** + * Constructs a puzzle animation waiter. + */ + public PuzzleAnimationWaiter (PuzzleObject puzobj) + { + _puzobj = puzobj; + _roundId = puzobj.roundId; + } + + /** + * Returns whether the puzzle associated with this puzzle animation + * waiter is still valid. + */ + public boolean puzzleStillValid () + { + return (_puzobj.isInPlay() && (_roundId == _puzobj.roundId)); + } + + // documentation inherited + protected final void allAnimationsFinished () + { + allAnimationsFinished(puzzleStillValid()); + } + + /** + * Replacement for {@link AnimationWaiter#allAnimationsFinished} that + * also reports whether the puzzle associated with this animation + * waiter is still valid. + */ + protected abstract void allAnimationsFinished (boolean puzStillValid); + + /** The initial round id. */ + protected int _roundId; + + /** The puzzle object that the animations we're observering want to + * modify. */ + protected PuzzleObject _puzobj; +} diff --git a/src/java/com/threerings/puzzle/client/PuzzleBoardView.java b/src/java/com/threerings/puzzle/client/PuzzleBoardView.java new file mode 100644 index 00000000..059bc7d8 --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzleBoardView.java @@ -0,0 +1,420 @@ +// +// $Id: PuzzleBoardView.java 3854 2006-02-14 22:17:52Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.geom.AffineTransform; +import java.util.ArrayList; + +import javax.swing.UIManager; + +import com.samskivert.swing.Label; +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.media.VirtualMediaPanel; +import com.threerings.media.animation.Animation; +import com.threerings.media.animation.AnimationAdapter; +import com.threerings.media.animation.AnimationArranger; +import com.threerings.media.image.Mirage; +import com.threerings.media.sprite.Sprite; + +import com.threerings.parlor.media.ScoreAnimation; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleCodes; +import com.threerings.puzzle.data.PuzzleConfig; +import com.threerings.puzzle.util.PuzzleContext; + +/** + * The puzzle board view displays a view of a puzzle game. + */ +public abstract class PuzzleBoardView extends VirtualMediaPanel +{ + /** + * Constructs a puzzle board view. + */ + public PuzzleBoardView (PuzzleContext ctx) + { + super(ctx.getFrameManager()); + + // keep this for later + _ctx = ctx; + } + + /** + * Initializes the board with the board dimensions. + */ + public void init (PuzzleConfig config) + { + // save off our bounds + Dimension bounds = getPreferredSize(); + _bounds = new Rectangle(0, 0, bounds.width, bounds.height); + } + + /** + * Sets the board to be displayed. + */ + public void setBoard (Board board) + { + _board = board; + } + + /** + * Provides the board view with a reference to its controller so that + * it may communicate directly rather than by posting actions up the + * interface hierarchy which sometimes fails if the puzzle board view + * is hidden before we get a chance to post our actions. + */ + public void setController (PuzzleController pctrl) + { + _pctrl = pctrl; + } + + /** + * Sets the background image displayed by the board view. + */ + public void setBackgroundImage (Mirage image) + { + _background = image; + } + + /** + * Set whether this puzzle is paused or not. + * If paused, a label will be displayed with the component's font, + * which may be set with setFont(). + */ + public void setPaused (boolean paused) + { + if (paused) { + String pmsg = _pctrl.getPauseString(); + pmsg = _ctx.getMessageManager().getBundle( + PuzzleCodes.PUZZLE_MESSAGE_BUNDLE).xlate(pmsg); + // create a label using our component's standard font + _pauseLabel = new Label(pmsg, Label.BOLD | Label.OUTLINE, + Color.WHITE, Color.BLACK, + getFont()); + _pauseLabel.setTargetWidth(_bounds.width); + _pauseLabel.layout(this); + } else { + _pauseLabel = null; + } + repaint(); + } + + /** + * Adds the given animation to the set of animations currently present + * on the board. The animation will be added to a list of action + * animations whose count can be queried with {@link + * #getActionAnimationCount}. The animation will automatically be + * removed from the action list when it completes. + */ + public void addActionAnimation (Animation anim) + { + super.addAnimation(anim); + + // remember the animation's existence + _actionAnims.add(anim); + + // and listen for it to finish so that we can clear it out + anim.addAnimationObserver(_actionAnimObs); + } + + // documentation inherited + public void abortAnimation (Animation anim) + { + super.abortAnimation(anim); + + // always check to see if it was action-y + animationFinished(anim); + } + + /** + * Called when a potential action animation is finished. + */ + protected void animationFinished (Animation anim) + { + if (DEBUG_ACTION) { + Log.info("Animation cleared " + StringUtil.shortClassName(anim) + + ":" + _actionAnims.contains(anim)); + } + + // if it WAS an action animation, check for a clear + if (_actionAnims.remove(anim)) { + maybeFireCleared(); + } + } + + /** + * Adds the given sprite to the set of sprites currently present on + * the board. The sprite will be added to a list of action sprites + * whose count can be queried with {@link #getActionSpriteCount}. Callers + * should be sure to remove the sprite when their work with it is done + * via {@link #removeSprite}. + */ + public void addActionSprite (Sprite sprite) + { + // add the piece to the sprite manager + addSprite(sprite); + + // note that this piece is interesting + _actionSprites.add(sprite); + } + + /** + * Removes the given sprite from the board. + */ + public void removeSprite (Sprite sprite) + { + super.removeSprite(sprite); + + if (DEBUG_ACTION) { + Log.info("Sprite cleared " + StringUtil.shortClassName(sprite) + + ":" + _actionSprites.contains(sprite)); + } + + // we just always check to see if it was action-y + if (_actionSprites.remove(sprite)) { + maybeFireCleared(); + } + } + + // documentation inherited + public void clearSprites () + { + super.clearSprites(); + _actionSprites.clear(); + } + + // documentation inherited + public void clearAnimations () + { + super.clearAnimations(); + _actionAnims.clear(); + } + + /** + * Returns the number of action animations on the board. + */ + public int getActionAnimationCount () + { + return _actionAnims.size(); + } + + /** + * Returns the number of action sprites on the board. + */ + public int getActionSpriteCount () + { + return _actionSprites.size(); + } + + /** + * Returns the count of action sprites and animations on the board. + */ + public int getActionCount () + { + return _actionSprites.size() + _actionAnims.size(); + } + + /** + * Dumps to the logs, a list of interesting sprites and animations + * currently active on the puzzle board. + */ + public void dumpActors () + { + StringUtil.Formatter fmt = new StringUtil.Formatter() { + public String toString (Object obj) { + return StringUtil.shortClassName(obj); + } + }; + Log.info("Board contents [board=" + StringUtil.shortClassName(this) + + ", sprites=" + StringUtil.listToString(_actionSprites, fmt) + + ", anims=" + StringUtil.listToString(_actionAnims, fmt) + + "]."); + } + + /** + * Creates and returns an animation displaying the given string with + * the specified parameters, floating it a short distance up the view. + * + * @param score the score text to display. + * @param color the color of the text. + * @param font the font whith which to create the score animation. + * @param x the x-position at which the score is to be placed. + * @param y the y-position at which the score is to be placed. + */ + public ScoreAnimation createScoreAnimation ( + String score, Color color, Font font, int x, int y) + { + return createScoreAnimation( + ScoreAnimation.createLabel(score, color, font, this), x, y); + } + + /** + * Creates a score animation, allowing derived classes to use custom + * animations that are customized following a call to + * {@link #createScoreAnimation}. + */ + protected ScoreAnimation createScoreAnimation (Label label, int x, int y) + { + return new ScoreAnimation(label, x, y); + } + + /** + * Positions the supplied animation so as to avoid any active + * animations previously registered with this method, and adds the + * animation to the list of animations to be avoided by any future + * avoid animations. + */ + public void trackAvoidAnimation (Animation anim) + { + // lazy init the arranger + if (_avoidArranger == null) { + _avoidArranger = new AnimationArranger(); + } + _avoidArranger.positionAvoidAnimation(anim, _vbounds); + } + + // documentation inherited + public void paintBehind (Graphics2D gfx, Rectangle dirty) + { + super.paintBehind(gfx, dirty); + + // render the background + renderBackground(gfx, dirty); + } + + /** + * Fills the background of the board with the background color. + */ + protected void renderBackground (Graphics2D gfx, Rectangle dirty) + { + gfx.setColor(getBackground()); + gfx.fill(dirty); + } + + // documentation inherited + public void paintBetween (Graphics2D gfx, Rectangle dirty) + { + super.paintBetween(gfx, dirty); +// PerformanceMonitor.tick(this, "paint"); + renderBoard(gfx, dirty); + } + + // documentation inherited + protected void paintInFront (Graphics2D gfx, Rectangle dirty) + { + super.paintInFront(gfx, dirty); + + // if the action is paused, indicate as much + if (_pauseLabel != null) { + Dimension d = _pauseLabel.getSize(); + _pauseLabel.render(gfx, + _vbounds.x + (_vbounds.width - d.width) / 2, + _vbounds.y + (_vbounds.height - d.height) / 2); + } + } + + /** + * Fires a {@link #ACTION_CLEARED} command iff we have no remaining + * interesting sprites or animations. + */ + protected void maybeFireCleared () + { + if (DEBUG_ACTION) { + Log.info("Maybe firing cleared " + + getActionCount() + ":" + isShowing()); + } + if (getActionCount() == 0) { + // we're probably in the middle of a tick() in an + // animationDidFinish() call and we want everyone to finish + // processing their business before we go clearing the action, + // so we queue this up to be run after the tick is complete + _ctx.getClient().getRunQueue().postRunnable(new Runnable() { + public void run () { + _pctrl.boardActionCleared(); + } + }); + } + } + + /** + * Renders the board contents to the given graphics context. + * Sub-classes should implement this method to draw all of their + * game-specific business. + */ + protected abstract void renderBoard (Graphics2D gfx, Rectangle dirty); + + /** Our client context. */ + protected PuzzleContext _ctx; + + /** Our puzzle controller. */ + protected PuzzleController _pctrl; + + /** The board data to be displayed. */ + protected Board _board; + + /** The board's bounding rectangle. */ + protected Rectangle _bounds; + + /** The action animations on the board. */ + protected ArrayList _actionAnims = new ArrayList(); + + /** The action sprites on the board. */ + protected ArrayList _actionSprites = new ArrayList(); + + /** Prevents certain animations from overlapping others. */ + protected AnimationArranger _avoidArranger; + + /** Our background image. */ + protected Mirage _background; + + /** A label to show when the puzzle is paused. */ + protected Label _pauseLabel; + + /** The distance in pixels that score animations float. */ + protected int _scoreDist = DEFAULT_SCORE_DISTANCE; + + /** Listens to our action animations and clears them when they're done. */ + protected AnimationAdapter _actionAnimObs = new AnimationAdapter() { + public void animationCompleted (Animation anim, long when) { + animationFinished(anim); + } + }; + + /** Temporary action debugging. */ + protected static boolean DEBUG_ACTION = false; + + // action state constants + protected static final int ACTION_GOING = 0; + protected static final int CLEAR_PENDING = 1; + protected static final int ACTION_CLEARED = 2; + + /** The default vertical distance to float score animations. */ + protected static final int DEFAULT_SCORE_DISTANCE = 30; +} diff --git a/src/java/com/threerings/puzzle/client/PuzzleController.java b/src/java/com/threerings/puzzle/client/PuzzleController.java new file mode 100644 index 00000000..5fd571de --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzleController.java @@ -0,0 +1,978 @@ +// +// $Id: PuzzleController.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.KeyAdapter; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +import java.util.ArrayList; + +import com.samskivert.swing.util.MouseHijacker; + +import com.samskivert.util.CollectionUtil; +import com.samskivert.util.ObserverList; +import com.samskivert.util.StringUtil; + +import com.threerings.media.FrameParticipant; + +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; +import com.threerings.presents.dobj.ElementUpdateListener; +import com.threerings.presents.dobj.ElementUpdatedEvent; + +import com.threerings.crowd.client.PlaceControllerDelegate; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.game.client.GameController; +import com.threerings.parlor.game.data.GameObject; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleCodes; +import com.threerings.puzzle.data.PuzzleConfig; +import com.threerings.puzzle.data.PuzzleObject; +import com.threerings.puzzle.util.PuzzleContext; + +/** + * The puzzle game controller handles logical actions for a puzzle game. + */ +public abstract class PuzzleController extends GameController + implements PuzzleCodes +{ + /** The action command to toggle chatting mode. */ + public static final String TOGGLE_CHATTING = "toggle_chat"; + + /** Used by {@link PuzzleController#fireWhenActionCleared}. */ + public static interface ClearPender + { + /** {@link #actionCleared} return code. */ + public static final int RESTART_ACTION = -1; + + /** {@link #actionCleared} return code. */ + public static final int CARE_NOT = 0; + + /** {@link #actionCleared} return code. */ + public static final int NO_RESTART_ACTION = 1; + + /** + * Called when the action is fully cleared. + * + * @return One of {@link #RESTART_ACTION}, {@link #CARE_NOT} or + * {@link #NO_RESTART_ACTION}. + */ + public int actionCleared (); + } + + // documentation inherited + protected void didInit () + { + super.didInit(); + + _panel = (PuzzlePanel)_view; + _pctx = (PuzzleContext)_ctx; + + // initialize the puzzle panel + _puzconfig = (PuzzleConfig)_config; + _panel.init(_puzconfig); + + // initialize the board view + _pview = _panel.getBoardView(); + _pview.setController(this); + } + + /** + * Creates and returns a new board model. + */ + protected abstract Board newBoard (); + + /** + * Returns the board associated with the puzzle. + */ + public Board getBoard () + { + return _pboard; + } + + /** + * Returns the player's index in the list of players for the game. + */ + public int getPlayerIndex () + { + return _pidx; + } + + // documentation inherited + public void setGameOver (boolean gameOver) + { + super.setGameOver(gameOver); + + // clear the action if we're informed that the game is over early + // by the client + if (gameOver) { + clearAction(); + } + } + + /** + * Returns true if the puzzle has action, false if the action is + * cleared or it is suspended. + */ + public boolean hasAction () + { + return (_astate == ACTION_GOING); + } + + /** + * Sets whether we're focusing on the chat window rather than the puzzle. + */ + public void setChatting (boolean chatting) + { + // ignore the request if we're already there + if ((isChatting() == chatting) || + // ..or if we want to initiate chatting and.. + // we either can't right now or we don't have action + (chatting && (!canStartChatting() || !hasAction()))) { + return; + } + + // update the panel + _panel.setPuzzleGrabsKeys(!chatting); + + // if we're moving focus to chat.. + if (chatting) { + if (_unpauser != null) { + Log.warning("Huh? Already have a mouse unpauser?"); + _unpauser.release(); + } + _unpauser = new Unpauser(_panel); + + } else { + if (_unpauser != null) { + _unpauser.release(); + _unpauser = null; + } + } + + // update the chatting state + _chatting = chatting; + + // dispatch the change to our delegates + applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((PuzzleControllerDelegate)delegate).setChatting(_chatting); + } + }); + + // and check if we should be suspending the action during this pause + if (supportsActionPause()) { + // clear the action if we're pausing, resume it if we're + // unpausing + if (chatting) { + clearAction(); + } else { + safeStartAction(); + } + _pview.setPaused(chatting); + } + } + + /** + * Get the (untranslated) string to display when the puzzle is paused. + */ + public String getPauseString () + { + return "m.paused"; + } + + /** + * Derived classes should override this and return false if their + * action should not be paused when the user switches control to the + * chat area. + */ + protected boolean supportsActionPause () + { + return true; + } + + /** + * Can we start chatting at this juncture? + */ + protected boolean canStartChatting () + { + // check with the delegates + final boolean[] canChatNow = new boolean[] { true }; + applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + canChatNow[0] = + ((PuzzleControllerDelegate)delegate).canStartChatting() && + canChatNow[0]; + } + }); + return canChatNow[0]; + } + + /** + * Returns true if the puzzle has been defocused because the player + * is doing some chatting. + */ + public boolean isChatting () + { + return _chatting; + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + super.willEnterPlace(plobj); + + // get a casted reference to our puzzle object + _puzobj = (PuzzleObject)plobj; + _puzobj.addListener(_kolist); + _puzobj.addListener(_mlist); + + // listen to key events.. + _pctx.getKeyDispatcher().addGlobalKeyListener(_globalKeyListener); + + // save off our player index + _pidx = _puzobj.getPlayerIndex(_pctx.getUsername()); + + // generate the starting board + generateNewBoard(); + + // if the game is already in play, start up the action + if (_puzobj.isInPlay() && _puzobj.isActivePlayer(_pidx)) { + startAction(); + } + } + + // documentation inherited + public void mayLeavePlace (PlaceObject plobj) + { + super.mayLeavePlace(plobj); + + // flush any pending progress events + sendProgressUpdate(); + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + super.didLeavePlace(plobj); + + // clean up and clear out + clearAction(); + + // stop listening to key events.. + _pctx.getKeyDispatcher().removeGlobalKeyListener(_globalKeyListener); + + // clear out the puzzle object + if (_puzobj != null) { + _puzobj.removeListener(_mlist); + _puzobj.removeListener(_kolist); + _puzobj = null; + } + } + + /** + * Puzzles that do not have "action" that starts and stops (via {@link + * #startAction} and {@link #clearAction}) when the puzzle starts and + * stops can override this method and return false. + */ + protected boolean isActionPuzzle () + { + return true; + } + + /** + * Indicates whether the action should start immediately as a result + * of {@link #gameDidStart} being called. If a puzzle wishes to do + * some beginning of the game fun stuff, like display a tutorial + * screen, they can veto the action start and then start it themselves + * later. + */ + protected boolean startActionImmediately () + { + return true; + } + + // documentation inherited + public void attributeChanged (AttributeChangedEvent event) + { + String name = event.getName(); + + // deal with game state changes + if (name.equals(PuzzleObject.STATE)) { + switch (event.getIntValue()) { + case PuzzleObject.IN_PLAY: + // we have to postpone all game starting activity until the + // current action has ended; only after all the animations have + // been completed will everything be in a state fit for + // starting back up again + fireWhenActionCleared(new ClearPender() { + public int actionCleared () { + // do the standard game did start business + gameDidStart(); + // we don't always start the action immediately + return startActionImmediately() ? + RESTART_ACTION : NO_RESTART_ACTION; + } + }); + break; + + case PuzzleObject.GAME_OVER: + // similarly we haev to postpone game ending activity until + // the current action has ended + // clean up and clear out + clearAction(); + // wait until the action is cleared before we roll down to our + // delegates and do all that business + fireWhenActionCleared(new ClearPender() { + public int actionCleared () { + gameDidEnd(); + return CARE_NOT; + } + }); + break; + + default: + super.attributeChanged(event); + break; + } + + } else if (name.equals(PuzzleObject.ROUND_ID)) { + // Need to clear out stale events. If we don't, we could send + // events that claim to be from the new round that are actually + // from the old round. + _events.clear(); + } + } + + // documentation inherited + protected void gameWillReset () + { + super.gameWillReset(); + + // stop the old action + clearAction(); + + // when the server gets around to resetting the game, we'll get a + // 'state => IN_PLAY' message which will result in gameDidStart() + // being called and starting the action back up + } + + /** + * Called when a new board is set. + */ + public void setBoard (Board board) + { + // we don't need to do anything by default + } + + /** + * Derived classes should override this method and do whatever is + * necessary to start up the action for their puzzle. This could be + * called when the user is already in the "room" and the game starts, + * or immediately upon entering the room if the game is already + * started (for example if they disconnected and reconnected to a game + * already in progress). + */ + protected void startAction () + { + // do nothing if we're not an action puzzle + if (!isActionPuzzle()) { + return; + } + + // refuse to start the action if our puzzle view is hidden + if (_pidx != -1 && !_panel.getBoardView().isShowing()) { + Log.warning("Refusing to start action on hidden puzzle."); + Thread.dumpStack(); + return; + } + + // refuse to start the action if it's already going + if (_astate != ACTION_CLEARED) { + Log.warning("Action state inappropriate for startAction() " + + "[astate=" + _astate + "]."); + Thread.dumpStack(); + return; + } + + if (isChatting() && supportsActionPause()) { + Log.info("Not starting action, player is chatting in a puzzle " + + "that supports pausing the action."); + return; + } + + Log.debug("Starting puzzle action."); + + // register the game progress updater; it may already be updated + // because we can cycle through clearing the action and starting + // it again before the updater gets a chance to unregister itself + if (!_pctx.getFrameManager().isRegisteredFrameParticipant(_updater)) { + _pctx.getFrameManager().registerFrameParticipant(_updater); + } + + // make a note that we've started the action + _astate = ACTION_GOING; + + // let our panel know what's up + _panel.startAction(); + + // and if we're not currently chatting, set the puzzle to grab + // keys and for the chatbox to look disabled + if (!isChatting()) { + _panel.setPuzzleGrabsKeys(true); + } + + // let our delegates do their business + applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((PuzzleControllerDelegate)delegate).startAction(); + } + }); + } + + /** + * If it is not known whether the puzzle board view has finished + * animating its final bits after a previous call to {@link + * #clearAction}, this method should be used instead of {@link + * #startAction} as it will wait until the action is confirmedly over + * before starting it anew. + */ + protected void safeStartAction () + { + // do nothing if we're not an action puzzle + if (!isActionPuzzle()) { + return; + } + + fireWhenActionCleared(new ClearPender() { + public int actionCleared () { + return RESTART_ACTION; + } + }); + } + + /** + * Called when the game has ended or when it is going to reset and + * when the client leaves the game "room". This method does not always + * immediately clear the action, but may mark the clear as pending if + * the action cannot yet be cleared (as indicated by {@link + * #canClearAction}). The action will eventually be cleared which will + * result in a call to {@link #actuallyClearAction} which is what + * derived classes should override to do their action clearing + * business. + */ + protected void clearAction () + { + // do nothing if we're not an action puzzle + if (!isActionPuzzle()) { + return; + } + + // no need to clear if we're already cleared or clearing + if (_astate == CLEAR_PENDING || _astate == ACTION_CLEARED) { + return; + } + + Log.debug("Attempting to clear puzzle action."); + + // put ourselves into a pending clear state and attempt to clear + // the action + _astate = CLEAR_PENDING; + maybeClearAction(); + } + + /** + * This method is called by the {@link PuzzleBoardView} when all + * action on the board has finished. + */ + protected void boardActionCleared () + { + // if we have a clear pending, this could be the trigger that + // allows us to clear our action + maybeClearAction(); + } + + /** + * Queues up code to be invoked when the action is completely cleared + * (including all remaining interesting sprites and animations on the + * puzzle board). + */ + protected void fireWhenActionCleared (ClearPender pender) + { + // if the action is already ended, fire this pender immediately + if (_astate == ACTION_CLEARED) { + if (pender.actionCleared() == ClearPender.RESTART_ACTION) { + Log.debug("Restarting action at behest of pender " + + pender + "."); + startAction(); + } + + } else { + Log.debug("Queueing action pender " + pender + "."); + _clearPenders.add(pender); + } + } + + /** + * Returns whether or not it is safe to clear the action. The default + * behavior is to not allow the action to be cleared until all + * interesting sprites and animations in the board view have finished. + * If derived classes or delegates wish to postpone the clearing of + * the action, they can return false from this method, but they must + * then be sure to call {@link #maybeClearAction} when whatever + * condition that caused them to desire to postpone action clearing + * has finally been satisfied. + */ + protected boolean canClearAction () + { + final boolean[] canClear = new boolean[1]; + canClear[0] = (_pview.getActionCount() == 0); +// if (!canClear[0]) { +// _pview.dumpActors(); +// PuzzleBoardView.DEBUG_ACTION = true; +// } + + // let our delegates do their business + applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + canClear[0] = canClear[0] && + ((PuzzleControllerDelegate)delegate).canClearAction(); + } + }); + + return canClear[0]; + } + + /** + * Called to effect the actual clearing of our action if we've + * received some asynchronous trigger that indicates that it may well + * be safe now to clear the action. + */ + protected void maybeClearAction () + { + if (_astate == CLEAR_PENDING && canClearAction()) { + actuallyClearAction(); +// } else { +// Log.info("Not clearing action [astate=" + _astate + +// ", canClear=" + canClearAction() + "]."); + } + } + + /** + * Performs the actual process of clearing the action for this puzzle. + * This is only called after it is known to be safe to clear the + * action. Derived classes can override this method and clear out + * anything that is not needed while the puzzle's "action" is not + * going (timers, etc.). Anything that is cleared out here should be + * recreated in {@link #startAction}. + */ + protected void actuallyClearAction () + { + Log.debug("Actually clearing action."); + + // make a note that we've cleared the action + _astate = ACTION_CLEARED; +// PuzzleBoardView.DEBUG_ACTION = false; + + // let our delegates do their business + applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((PuzzleControllerDelegate)delegate).clearAction(); + } + }); + + // let our panel know what's up + _panel.clearAction(); + _panel.setPuzzleGrabsKeys(false); // let the user chat + + // deliver one final update to the server + sendProgressUpdate(); + + // let derived classes do things + try { + actionWasCleared(); + } catch (Exception e) { + Log.warning("Choked in actionWasCleared"); + Log.logStackTrace(e); + } + + // notify any penders that the action has cleared + final int[] results = new int[2]; + _clearPenders.apply(new ObserverList.ObserverOp() { + public boolean apply (Object observer) { + switch (((ClearPender)observer).actionCleared()) { + case ClearPender.RESTART_ACTION: results[0]++; break; + case ClearPender.NO_RESTART_ACTION: results[1]++; break; + } + return true; + } + }); + _clearPenders.clear(); + + // if there are no refusals and at least one restart request, go + // ahead and restart the action now + if (results[1] == 0 && results[0] > 0) { + startAction(); + } + } + + /** + * Called when the action was actually cleared, but before the action + * obsevers are notified. + */ + protected void actionWasCleared () + { + } + + // documentation inherited + public boolean handleAction (ActionEvent action) + { + String cmd = action.getActionCommand(); + if (cmd.equals(TOGGLE_CHATTING)) { + setChatting(!isChatting()); + + } else { + return super.handleAction(action); + } + + return true; + } + + /** + * Returns the delay in milliseconds between sending each progress + * update event to the server. Derived classes may wish to override + * this to send their progress updates more or less frequently than + * the default. + */ + protected long getProgressInterval () + { + return DEFAULT_PROGRESS_INTERVAL; + } + + /** + * Signal the game to generate and distribute a new board. + */ + protected void generateNewBoard () + { + // wait for any animations or sprites in the board to finish their + // business before setting the board into place + fireWhenActionCleared(new ClearPender() { + public int actionCleared () { + // update the player board + _pboard = newBoard(); + if (_puzobj.seed != 0) { + _pboard.initializeSeed(_puzobj.seed); + } + setBoard(_pboard); + _pview.setBoard(_pboard); + + // and repaint things + _pview.repaint(); + + // let our delegates do their business + DelegateOp dop = new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((PuzzleControllerDelegate)delegate).setBoard(_pboard); + } + }; + applyToDelegates(PuzzleControllerDelegate.class, dop); + + return CARE_NOT; + } + }); + } + + /** + * Returns the number of progress events currently queued up for + * sending to the server with the next progress update. + */ + public int getEventCount () + { + return _events.size(); + } + + /** + * Are we syncing boards for this puzzle? + * By default, we defer to the PuzzlePanel and its runtime config. + */ + protected boolean isSyncingBoards () + { + return PuzzlePanel.isSyncingBoards(); + } + + /** + * Adds the given progress event and a snapshot of the supplied board + * state to the set of progress events and associated board states for + * later transmission to the server. + */ + public void addProgressEvent (int event, Board board) + { + // make sure they don't queue things up at strange times + if (_puzobj.state != PuzzleObject.IN_PLAY) { + Log.warning("Rejecting progress event; game not in play " + + "[puzobj=" + _puzobj.which() + + ", event=" + event + "]."); + return; + } + + _events.add(Integer.valueOf(event)); + if (isSyncingBoards()) { + _states.add((board == null) ? null : board.clone()); + if (board == null) { + Log.warning("Added progress event with no associated board " + + "state, server will not be able to ensure " + + "board state synchronization."); + } + } + } + + /** + * Sends the server a game progress update with the list of events, as + * well as board states if {@link PuzzlePanel#isSyncingBoards} is true. + */ + public void sendProgressUpdate () + { + // make sure we have our puzzle object and events to send + int size = _events.size(); + if (size == 0 || _puzobj == null) { + return; + } + + // create an array of the events we're sending to the server + int[] events = CollectionUtil.toIntArray(_events); + _events.clear(); + +// Log.info("Sending progress [round=" + _puzobj.roundId + +// ", events=" + StringUtil.toString(events) + "]."); + + // create an array of the board states that correspond with those + // events (if state syncing is enabled) + int numStates = _states.size(); + if (numStates == size) { // ie, if we have a board to match every event + Board[] states = new Board[numStates]; + _states.toArray(states); + _states.clear(); + + // send the update progress request + _puzobj.puzzleGameService.updateProgressSync( + _ctx.getClient(), _puzobj.roundId, events, states); + + } else { + // send the update progress request + _puzobj.puzzleGameService.updateProgress( + _ctx.getClient(), _puzobj.roundId, events); + } + } + + /** + * Called when a player is knocked out of the game to give the puzzle + * a chance to perform any post-knockout actions that may be desired. + * Derived classes may wish to override this method but should be sure + * to call super.playerKnockedOut(). + */ + protected void playerKnockedOut (final int pidx) + { + // dispatch this to our delegates + applyToDelegates(PuzzleControllerDelegate.class, new DelegateOp() { + public void apply (PlaceControllerDelegate delegate) { + ((PuzzleControllerDelegate)delegate).playerKnockedOut(pidx); + } + }); + } + + /** + * Catches clicks an unpauses, without passing the click through + * to the puzzle. + */ + class Unpauser extends MouseHijacker + { + public Unpauser (PuzzlePanel panel) + { + super(panel.getBoardView()); + _panel = panel; + panel.addMouseListener(_clicker); + panel.getBoardView().addMouseListener(_clicker); + } + + public Component release () + { + _panel.removeMouseListener(_clicker); + _panel.getBoardView().removeMouseListener(_clicker); + return super.release(); + } + + protected MouseAdapter _clicker = new MouseAdapter() { + public void mousePressed (MouseEvent event) { + setChatting(false); // this will call release + } + }; + + protected PuzzlePanel _panel; + } + + /** A special frame participant that handles the sending of puzzle + * progress updates. We can't just + * register an interval for this because sometimes the clock goes + * backwards in time in windows and our intervals don't get called for + * a long period of time which causes the server to think the client + * is disconnected or cheating and resign them from the puzzle. God + * bless you, Microsoft. */ + protected class Updater implements FrameParticipant + { + public void tick (long tickStamp) { + if (_astate == ACTION_CLEARED) { + // remove ourselves as the action is now cleared; we can't + // do this in actuallyClearAction() because that might get + // called during the PuzzlePanel's frame tick and it's + // only safe to remove yourself during a tick(), not + // another frame participant + _pctx.getFrameManager().removeFrameParticipant(_updater); + + } else if (tickStamp - _lastProgressTick > getProgressInterval()) { + _lastProgressTick = tickStamp; + sendProgressUpdate(); + } + } + + public boolean needsPaint () { + return false; + } + + public Component getComponent () { + return null; + } + + public long _lastProgressTick; + } + + /** + * Create the updater to be used in this puzzle. + */ + protected Updater createUpdater () + { + return new Updater(); + } + + /** The mouse jockey for unpausing our puzzles. */ + protected Unpauser _unpauser; + + /** Handles the sending of puzzle progress updates. */ + protected Updater _updater = createUpdater(); + + /** Listens for players being knocked out. */ + protected ElementUpdateListener _kolist = new ElementUpdateListener() { + public void elementUpdated (ElementUpdatedEvent event) { + String name = event.getName(); + if (name.equals(PuzzleObject.PLAYER_STATUS)) { + if (event.getIntValue() == GameObject.PLAYER_LEFT_GAME) { + playerKnockedOut(event.getIndex()); + } + } + } + }; + + /** Listens for various attribute changes. */ + protected AttributeChangeListener _mlist = new AttributeChangeListener() { + public void attributeChanged (AttributeChangedEvent event) { + String name = event.getName(); + if (name.equals(PuzzleObject.SEED)) { + generateNewBoard(); + } + } + }; + + /** A casted reference to the client context. */ + protected PuzzleContext _pctx; + + /** Our player index in the game. */ + protected int _pidx; + + /** The puzzle panel. */ + protected PuzzlePanel _panel; + + /** The puzzle config. */ + protected PuzzleConfig _puzconfig; + + /** A reference to our puzzle game object. */ + protected PuzzleObject _puzobj; + + /** The puzzle board view. */ + protected PuzzleBoardView _pview; + + /** The puzzle board data. */ + protected Board _pboard; + + /** The list of relevant game events since the last progress update. */ + protected ArrayList _events = new ArrayList(); + + /** Board snapshots that correspond to our board state after each of + * our events has been applied. */ + protected ArrayList _states = new ArrayList(); + + /** A flag indicating that we're in chatting mode. */ + protected boolean _chatting = false; + + /** The current action state of the puzzle. */ + protected int _astate = ACTION_CLEARED; + + /** The action cleared penders. */ + protected ObserverList _clearPenders = new ObserverList( + ObserverList.SAFE_IN_ORDER_NOTIFY); + + /** A key listener that currently just toggles pause in the puzzle. */ + protected KeyListener _globalKeyListener = new KeyAdapter() { + public void keyReleased (KeyEvent e) + { + int keycode = e.getKeyCode(); + // toggle chatting (pause) + if (keycode == KeyEvent.VK_ESCAPE || keycode == KeyEvent.VK_PAUSE) { + setChatting(!isChatting()); + + // pressing P also to pause (but not unpause), + // and only if it has not been reassigned + } else if (keycode == KeyEvent.VK_P && !isChatting() && + !_panel._xlate.hasCommand(KeyEvent.VK_P)) { + setChatting(true); + } + } + }; + + /** The delay in milliseconds between progress update intervals. */ + protected static final long DEFAULT_PROGRESS_INTERVAL = 6000L; + + /** A {@link #_astate} constant. */ + protected static final int ACTION_CLEARED = 0; + + /** A {@link #_astate} constant. */ + protected static final int CLEAR_PENDING = 1; + + /** A {@link #_astate} constant. */ + protected static final int ACTION_GOING = 2; +} diff --git a/src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java b/src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java new file mode 100644 index 00000000..3f29a3c9 --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzleControllerDelegate.java @@ -0,0 +1,153 @@ +// +// $Id: PuzzleControllerDelegate.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.game.client.GameControllerDelegate; + +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleCodes; +import com.threerings.puzzle.data.PuzzleGameCodes; +import com.threerings.puzzle.data.PuzzleObject; + +/** + * A base class for puzzle controller delegates. Provides access to some + * delegated puzzle controller methods ({@link #startAction}, {@link + * #clearAction}, etc.) and provides a casted reference to the puzzle + * object. + */ +public class PuzzleControllerDelegate extends GameControllerDelegate + implements PuzzleCodes, PuzzleGameCodes +{ + /** + * Constructs a puzzle controller delegate. + */ + public PuzzleControllerDelegate (PuzzleController ctrl) + { + super(ctrl); + + // keep around a casted reference to our controller + _ctrl = ctrl; + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + super.willEnterPlace(plobj); + + // get a casted reference to our game object + _puzobj = (PuzzleObject)plobj; + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + super.didLeavePlace(plobj); + + _puzobj = null; + } + + /** + * Called when a player is knocked out of the game. + */ + public void playerKnockedOut (int pidx) + { + } + + /** + * Called when the user toggles chatting mode. + */ + public void setChatting (boolean chatting) + { + } + + /** + * Can we start chatting at the instant that this method is called? + */ + protected boolean canStartChatting () + { + return true; + } + + /** + * Derived classes should override this method and do whatever is + * necessary to start up the action for their puzzle. This could be + * called when the user is already in the "room" and the game starts, + * or immediately upon entering the room if the game is already + * started (for example if they disconnected and reconnected to a game + * already in progress). + */ + protected void startAction () + { + } + + /** + * Delegates that wish to postpone action clearing can override this + * method to return false until such time as the action can be + * cleared. They must, however, call {@link #maybeClearAction} when + * conditions become such that they would once again allow action to + * be cleared. + */ + protected boolean canClearAction () + { + return true; + } + + /** + * Calls {@link PuzzleController#maybeClearAction}, preserving its + * protected access but making the method available to all + * PuzzleControllerDelegate derivations. + */ + protected void maybeClearAction () + { + _ctrl.maybeClearAction(); + } + + /** + * Puzzles should override this method and clear out any action on the + * board and generally clean up anything that was going on because the + * game was in play. This is called when the game has ended or when it + * is going to reset and when the client leaves the game "room". + * Anything that is cleared out here should be recreated in {@link + * #startAction}. + */ + protected void clearAction () + { + } + + /** + * Called when the puzzle controller sets up a new board for the + * player. + * + * @param board the newly initialized and ready-to-go board. + */ + public void setBoard (Board board) + { + } + + /** Our puzzle controller. */ + protected PuzzleController _ctrl; + + /** The puzzle distributed object. */ + protected PuzzleObject _puzobj; +} diff --git a/src/java/com/threerings/puzzle/client/PuzzleGameService.java b/src/java/com/threerings/puzzle/client/PuzzleGameService.java new file mode 100644 index 00000000..925e7e07 --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzleGameService.java @@ -0,0 +1,49 @@ +// +// $Id: PuzzleGameService.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleCodes; + +/** + * Provides services used by puzzle game clients to request that actions + * be taken by the puzzle manager. + */ +public interface PuzzleGameService extends InvocationService, PuzzleCodes +{ + /** + * Asks the puzzle manager to apply the supplied progress events for + * the specified puzzle round to the player's state. + */ + public void updateProgress (Client client, int roundId, int[] events); + + /** + * Debug variant of {@link #updateProgress} that is only used when + * {@link PuzzlePanel#isSyncingBoards} is true and which includes the + * board states associated with each event. + */ + public void updateProgressSync ( + Client client, int roundId, int[] events, Board[] states); +} diff --git a/src/java/com/threerings/puzzle/client/PuzzlePanel.java b/src/java/com/threerings/puzzle/client/PuzzlePanel.java new file mode 100644 index 00000000..80fa1430 --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzlePanel.java @@ -0,0 +1,319 @@ +// +// $Id: PuzzlePanel.java 4007 2006-04-10 08:59:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import java.awt.BorderLayout; +import javax.swing.JPanel; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.ControllerProvider; +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.util.KeyTranslator; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.parlor.util.RobotPlayer; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.data.PuzzleCodes; +import com.threerings.puzzle.data.PuzzleConfig; +import com.threerings.puzzle.data.PuzzleGameCodes; +import com.threerings.puzzle.util.PuzzleContext; + +/** + * The puzzle panel class should be extended by classes that provide a + * view for a puzzle game. The {@link PuzzleController} calls these + * methods as necessary to perform its duties in managing the logical + * actions of a puzzle game. + */ +public abstract class PuzzlePanel extends JPanel + implements PlaceView, ControllerProvider, PuzzleCodes, PuzzleGameCodes +{ + /** + * Constructs a puzzle panel. + */ + public PuzzlePanel (PuzzleContext ctx, PuzzleController controller) + { + _ctx = ctx; + _controller = controller; + + // set up the keyboard manager + _xlate = getKeyTranslator(); + _ctx.getKeyboardManager().setTarget(this, _xlate); + + // configure the puzzle panel + setLayout(new BorderLayout()); + + // create the puzzle board view + _bview = createBoardView(ctx); + + // create the puzzle board panel + _bpanel = createBoardPanel(ctx); + + // add the board panel + add(_bpanel, BorderLayout.CENTER); + } + + // documentation inherited + public void addNotify () + { + super.addNotify(); + + // leave the keyboard manager disabled to start, and set things up + // for chatting + setPuzzleGrabsKeys(false); + } + + // documentation inherited + public void removeNotify () + { + super.removeNotify(); + + // don't ever leave with the keys grabbed + setPuzzleGrabsKeys(false); + } + + /** + * Temporarily replaces the puzzle board display with the supplied + * overlay panel. The panel can be removed and the board display + * restored by calling {@link #popOverlayPanel}. + * + * @return true if the specified panel will be displayed, false if it + * was not able to be pushed because another overlay panel is already + * pushed onto the primary panel. + */ + public boolean pushOverlayPanel (JPanel opanel) + { + // bail if we've already got an overlay + if (_opanel != null) { + Log.info("Refusing to push overlay panel, we've already got one " + + "[opanel=" + _opanel + ", npanel=" + opanel + "]."); + return false; + } + + // swap in the overlay panel + _opanel = opanel; + remove(_bpanel); + add(_opanel); + + // make sure the UI updates + SwingUtil.refresh(this); + + return true; + } + + /** + * Pops the overlay panel off of the main puzzle board display. + */ + public void popOverlayPanel () + { + if (_opanel != null) { + remove(_opanel); + _opanel = null; + add(_bpanel); + + // make sure the UI updates + SwingUtil.refresh(this); + } + } + + /** + * Returns true if an overlay panel is currently being displayed. + */ + public boolean hasOverlay () + { + return _opanel != null; + } + + /** + * Initializes the puzzle panel with the puzzle config of the puzzle + * whose user interface is being displayed by the panel + */ + public void init (PuzzleConfig config) + { + _config = config; + _bview.init(config); + } + + /** + * Sets whether this panel receives events periodically from a robot + * player. + */ + public void setRobotPlayer (boolean isrobot) + { + // create a robot player if necessary + if (_robot == null) { + _robot = createRobotPlayer(); + } + setPuzzleGrabsKeys(!isrobot); + _robot.setRobotDelay(200L); + _robot.setActive(isrobot); + } + + /** + * Sets whether the puzzle grabs keys or if they should go to the chat + * window. + */ + public void setPuzzleGrabsKeys (boolean puzgrabs) + { + // enable or disable the key manager appropriately + _ctx.getKeyboardManager().setEnabled(puzgrabs); + if (puzgrabs) { + getBoardView().requestFocusInWindow(); + } + } + + /** + * Called by the controller when the action starts. + */ + public void startAction () + { + // make the first player a robot player + + if (isRobotTesting() && _controller.getPlayerIndex() == 0) { + setRobotPlayer(true); + } + } + + /** + * Called by the controller when the action stops. + */ + public void clearAction () + { + // deactivate the robot player + if (isRobotTesting() && _controller.getPlayerIndex() == 0) { + setRobotPlayer(false); + } + } + + /** + * Creates a robot player using the default RobotPlayer. Derived + * classes can override to make use of more advanced robots adapted to + * specific puzzles. + */ + protected RobotPlayer createRobotPlayer () + { + return new RobotPlayer(this, _xlate); + } + + /** + * Creates the puzzle board view that will be used to display the main + * puzzle interface. This is called when the puzzle panel is + * constructed. The derived panel will still be responsible for adding + * the board to the interface hierarchy. + */ + protected abstract PuzzleBoardView createBoardView (PuzzleContext ctx); + + /** + * Creates the main panel used to display the puzzle and its various + * in-game accoutrements (next block views, player status displays, + * etc.) This is called when the puzzle panel is constructed. The + * derived panel is responsible for making sure that the board view is + * present in the board panel. + */ + protected abstract JPanel createBoardPanel (PuzzleContext ctx); + + /** + * Returns a key translator with the desired key to controller command + * mappings desired for this puzzle. + */ + protected abstract KeyTranslator getKeyTranslator (); + + // documentation inherited from interface + public void willEnterPlace (PlaceObject plobj) + { + } + + // documentation inherited from interface + public void didLeavePlace (PlaceObject plobj) + { + // disable the keyboard manager when we leave + _ctx.getKeyboardManager().reset(); + } + + /** + * Returns a reference to the {@link PuzzleBoardView} in use. + */ + public PuzzleBoardView getBoardView () + { + return _bview; + } + + // documentation inherited + public Controller getController () + { + return _controller; + } + + /** Returns true if the robot tester is activated. */ + public static boolean isRobotTesting () + { + return _robotTest.getValue(); + } + + /** Returns true if board syncing is activated. */ + public static boolean isSyncingBoards () + { + return _syncBoardState.getValue(); + } + + /** Our puzzle context. */ + protected PuzzleContext _ctx; + + /** The board view on which the primary puzzle interface is displayed. */ + protected PuzzleBoardView _bview; + + /** The board panel displayed while the puzzle is in progress. */ + protected JPanel _bpanel; + + /** The board overlay panel displayed as requested. */ + protected JPanel _opanel; + + /** The puzzle config. */ + protected PuzzleConfig _config; + + /** The robot player. */ + protected RobotPlayer _robot; + + /** Our key translations. */ + protected KeyTranslator _xlate; + + /** The puzzle game controller. */ + protected PuzzleController _controller; + + /** A debug hook that toggles activation of the robot test player. */ + protected static RuntimeAdjust.BooleanAdjust _robotTest = + new RuntimeAdjust.BooleanAdjust( + "Activates the robot test player which will make random moves " + + "in any activated puzzle.", "narya.puzzle.robot_tester", + PuzzlePrefs.config, false); + + /** A debug hook that toggles activation of board syncing. */ + protected static RuntimeAdjust.BooleanAdjust _syncBoardState = + new RuntimeAdjust.BooleanAdjust( + "Sends a snapshot of the puzzle board with every event to aid " + + "in debugging.", "narya.puzzle.sync_board_state", + PuzzlePrefs.config, false); +} diff --git a/src/java/com/threerings/puzzle/client/PuzzlePrefs.java b/src/java/com/threerings/puzzle/client/PuzzlePrefs.java new file mode 100644 index 00000000..dbfe598b --- /dev/null +++ b/src/java/com/threerings/puzzle/client/PuzzlePrefs.java @@ -0,0 +1,35 @@ +// +// $Id: PuzzlePrefs.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.client; + +import com.samskivert.util.Config; + +/** + * Provides access to runtime configuration parameters for this package + * and its subpackages. + */ +public class PuzzlePrefs +{ + /** Used to load our preferences from a properties file and map them + * to the persistent Java preferences repository. */ + public static Config config = new Config("rsrc/config/puzzle"); +} diff --git a/src/java/com/threerings/puzzle/data/Board.java b/src/java/com/threerings/puzzle/data/Board.java new file mode 100644 index 00000000..ebbdd816 --- /dev/null +++ b/src/java/com/threerings/puzzle/data/Board.java @@ -0,0 +1,205 @@ +// +// $Id: Board.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +import java.util.Random; + +import com.threerings.io.Streamable; + +/** + * An abstract base class for generating and storing puzzle board data. + */ +public abstract class Board + implements Cloneable, Streamable +{ + /** + * Outputs a string representation of the board contents. + */ + public abstract void dump (); + + /** + * Outputs a string representation of the board contents, interlaced + * with the supplied comparison board. + */ + public abstract void dumpAndCompare (Board other); + + /** + * Returns whether this board is equal to the given comparison board. + */ + public abstract boolean equals (Board other); + + // documentation inherited + public Object clone () + { + try { + Board board = (Board)super.clone(); + board._rando = (BoardRandom)_rando.clone(); + return board; + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException("All is wrong with universe."); + } + } + + /** + * Sets the seed in the board's random number generator and calls + * {@link #populate}. + */ + public void initializeSeed (long seed) + { + _rando = new BoardRandom(seed); + populate(); + } + + /** + * Returns the random number generator used by the board to generate + * random numbers for our puzzles. + */ + public Random getRandom () + { + return _rando; + } + + /** + * Called after the seed is set in the board to give derived classes a + * chance to do things like populating the board with random pieces. + */ + protected void populate () + { + } + + /** Used to generate random numbers. */ + protected static class BoardRandom extends Random + implements Cloneable + { + public BoardRandom (long seed) + { + super(0L); + setSeed(seed); + } + + // documentation inherited + public synchronized void setSeed (long seed) + { + _seed = (seed ^ multiplier) & mask; + } + + // documentation inherited + synchronized protected int next (int bits) + { + long nextseed = (_seed * multiplier + addend) & mask; + _seed = nextseed; + return (int)(nextseed >>> (48 - bits)); + } + + // documentation inherited + public void nextBytes (byte[] bytes) + { + unimplemented(); + } + + // not overridden: (They seemed innocent enough) + // nextInt() + // nextLong() + // nextBoolean() + // nextFloat() + + // documentation inherited + public int nextInt (int n) + { + if (n <= 0) { + throw new IllegalArgumentException("n must be positive"); + } + + if ((n & -n) == n) { // i.e., n is a power of 2 + return (int)((n * (long)next(31)) >> 31); + } + + int bits, val; + do { + bits = next(31); + val = bits % n; + } while (bits - val + (n-1) < 0); + return val; + } + + // documentation inherited + public double nextDouble () + { + long l = ((long)(next(26)) << 27) + next(27); + return l / (double)(1L << 53); + } + + // documentation inherited + public synchronized double nextGaussian () + { + if (_haveNextNextGaussian) { + _haveNextNextGaussian = false; + return _nextNextGaussian; + + } else { + double v1, v2, s; + do { + v1 = 2 * nextDouble() - 1; + v2 = 2 * nextDouble() - 1; + s = v1 * v1 + v2 * v2; + } while (s >= 1 || s == 0); + double multiplier = Math.sqrt(-2 * Math.log(s)/s); + _nextNextGaussian = v2 * multiplier; + _haveNextNextGaussian = true; + return v1 * multiplier; + } + } + + // documentation inherited + public Object clone () + { + try { + return super.clone(); + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException("All is wrong with universe."); + } + } + + /** + * I suppose I could copy all the methods from Random, then we + * wouldn't need this.. + */ + private final void unimplemented () + { + throw new RuntimeException( + "The Random method you attempted to call " + + "has not been implemented by BoardRandom."); + } + + /** The internal state related to generating random numbers. */ + protected long _seed; + protected double _nextNextGaussian; + protected boolean _haveNextNextGaussian = false; + + private final static long multiplier = 0x5DEECE66DL; + private final static long addend = 0xBL; + private final static long mask = (1L << 48) - 1; + } + + /** The object we use to generate our random numbers. */ + protected transient BoardRandom _rando; +} diff --git a/src/java/com/threerings/puzzle/data/BoardSummary.java b/src/java/com/threerings/puzzle/data/BoardSummary.java new file mode 100644 index 00000000..406ac45f --- /dev/null +++ b/src/java/com/threerings/puzzle/data/BoardSummary.java @@ -0,0 +1,77 @@ +// +// $Id: BoardSummary.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +import com.threerings.io.SimpleStreamableObject; + +/** + * Provides summarized data representing a player's board in a puzzle + * game. Board summaries are maintained by the puzzle server and are + * periodically sent to the clients to give them a view into how well + * their opponent(s) are doing. The data required to marshal a board + * summary object should be notably smaller in size than what would be + * required to marshal the entire associated {@link Board}. + * + *

Note all non-transient members of this and derived classes will + * automatically be serialized when the summary is sent over the wire. + */ +public abstract class BoardSummary extends SimpleStreamableObject +{ + /** + * Constructs an empty board summary for use when un-serializing. + */ + public BoardSummary () + { + // nothing for now + } + + /** + * Constructs a board summary that retrieves full board information + * from the supplied board when summarizing. + */ + public BoardSummary (Board board) + { + setBoard(board); + } + + /** + * Sets the board associated with this board summary, causing + * an immediate update to this summary. + */ + public void setBoard (Board board) + { + _board = board; + summarize(); // immediately summarize the new board + } + + /** + * Called by the {@link + * com.threerings.puzzle.server.PuzzleManager} to refresh the + * board summary information by studying the associated board + * contents. + */ + public abstract void summarize (); + + /** The board that we're summarizing. This is only valid on the + * server, and on the client only for the actual player's board. */ + protected transient Board _board; +} diff --git a/src/java/com/threerings/puzzle/data/PuzzleCodes.java b/src/java/com/threerings/puzzle/data/PuzzleCodes.java new file mode 100644 index 00000000..9ceee3ac --- /dev/null +++ b/src/java/com/threerings/puzzle/data/PuzzleCodes.java @@ -0,0 +1,43 @@ +// +// $Id: PuzzleCodes.java 3640 2005-07-01 23:56:54Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +import com.threerings.presents.data.InvocationCodes; + +/** + * Constants relating to the puzzle services. + */ +public interface PuzzleCodes extends InvocationCodes +{ + /** The message bundle identifier for general puzzle messages. */ + public static final String PUZZLE_MESSAGE_BUNDLE = "puzzle.general"; + + /** The default puzzle difficulty level. */ + public static final int DEFAULT_DIFFICULTY = 2; + + /** Whether to enable debug logging and assertions for puzzles. Note + * that enabling this may result in the server or client exiting + * unexpectedly if certain error conditions arise in order to + * facilitate debugging, and so this should never be enabled in any + * environment even remotely resembling production. */ + public static final boolean DEBUG_PUZZLE = false; +} diff --git a/src/java/com/threerings/puzzle/data/PuzzleConfig.java b/src/java/com/threerings/puzzle/data/PuzzleConfig.java new file mode 100644 index 00000000..f79d18f7 --- /dev/null +++ b/src/java/com/threerings/puzzle/data/PuzzleConfig.java @@ -0,0 +1,51 @@ +// +// $Id: PuzzleConfig.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +import com.threerings.parlor.game.data.GameConfig; + +/** + * Encapsulates the basic configuration information for a puzzle game. + */ +public abstract class PuzzleConfig extends GameConfig + implements Cloneable +{ + /** + * Constructs a blank puzzle config. + */ + public PuzzleConfig () + { + } + + /** + * Returns the message bundle identifier for the bundle that should be + * used to translate the translatable strings used to describe the + * puzzle config parameters. The default implementation returns the + * base puzzle message bundle, but puzzles that have their own message + * bundle should override this method and return their puzzle-specific + * bundle identifier. + */ + public String getBundleName () + { + return PuzzleCodes.PUZZLE_MESSAGE_BUNDLE; + } +} diff --git a/src/java/com/threerings/puzzle/data/PuzzleGameCodes.java b/src/java/com/threerings/puzzle/data/PuzzleGameCodes.java new file mode 100644 index 00000000..3ffb4936 --- /dev/null +++ b/src/java/com/threerings/puzzle/data/PuzzleGameCodes.java @@ -0,0 +1,32 @@ +// +// $Id: PuzzleGameCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +/** + * Contains codes used by the puzzle game client implementations. This + * differs from {@link PuzzleCodes} as that is related to the puzzle + * services which span the client and the server. + */ +public interface PuzzleGameCodes +{ + // Nothing at the moment. +} diff --git a/src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java b/src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java new file mode 100644 index 00000000..42b04e73 --- /dev/null +++ b/src/java/com/threerings/puzzle/data/PuzzleGameMarshaller.java @@ -0,0 +1,62 @@ +// +// $Id: PuzzleGameMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import com.threerings.puzzle.client.PuzzleGameService; +import com.threerings.puzzle.data.Board; + +/** + * Provides the implementation of the {@link PuzzleGameService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class PuzzleGameMarshaller extends InvocationMarshaller + implements PuzzleGameService +{ + /** The method id used to dispatch {@link #updateProgress} requests. */ + public static final int UPDATE_PROGRESS = 1; + + // documentation inherited from interface + public void updateProgress (Client arg1, int arg2, int[] arg3) + { + sendRequest(arg1, UPDATE_PROGRESS, new Object[] { + Integer.valueOf(arg2), arg3 + }); + } + + /** The method id used to dispatch {@link #updateProgressSync} requests. */ + public static final int UPDATE_PROGRESS_SYNC = 2; + + // documentation inherited from interface + public void updateProgressSync (Client arg1, int arg2, int[] arg3, Board[] arg4) + { + sendRequest(arg1, UPDATE_PROGRESS_SYNC, new Object[] { + Integer.valueOf(arg2), arg3, arg4 + }); + } + +} diff --git a/src/java/com/threerings/puzzle/data/PuzzleObject.java b/src/java/com/threerings/puzzle/data/PuzzleObject.java new file mode 100644 index 00000000..3bd2b534 --- /dev/null +++ b/src/java/com/threerings/puzzle/data/PuzzleObject.java @@ -0,0 +1,144 @@ +// +// $Id: PuzzleObject.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.data; + +import com.threerings.parlor.game.data.GameObject; + +/** + * Extends the basic {@link GameObject} to add individual player + * status. Puzzle games typically contain numerous players that may be + * knocked out of the game while the overall game continues on, thereby + * necessitating this second level of game status. + */ +public class PuzzleObject extends GameObject + implements PuzzleCodes +{ + // AUTO-GENERATED: FIELDS START + /** The field name of the puzzleGameService field. */ + public static final String PUZZLE_GAME_SERVICE = "puzzleGameService"; + + /** The field name of the difficulty field. */ + public static final String DIFFICULTY = "difficulty"; + + /** The field name of the summaries field. */ + public static final String SUMMARIES = "summaries"; + + /** The field name of the seed field. */ + public static final String SEED = "seed"; + // AUTO-GENERATED: FIELDS END + + /** Provides general puzzle game invocation services. */ + public PuzzleGameMarshaller puzzleGameService; + + /** The puzzle difficulty level. */ + public int difficulty; + + /** Summaries of the boards of all players in this puzzle (may be null + * if the puzzle doesn't support individual player boards). */ + public BoardSummary[] summaries; + + /** The seed used to germinate the boards. */ + public long seed; + + // AUTO-GENERATED: METHODS START + /** + * Requests that the puzzleGameService field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setPuzzleGameService (PuzzleGameMarshaller value) + { + PuzzleGameMarshaller ovalue = this.puzzleGameService; + requestAttributeChange( + PUZZLE_GAME_SERVICE, value, ovalue); + this.puzzleGameService = value; + } + + /** + * Requests that the difficulty field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setDifficulty (int value) + { + int ovalue = this.difficulty; + requestAttributeChange( + DIFFICULTY, Integer.valueOf(value), Integer.valueOf(ovalue)); + this.difficulty = value; + } + + /** + * Requests that the summaries field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setSummaries (BoardSummary[] value) + { + BoardSummary[] ovalue = this.summaries; + requestAttributeChange( + SUMMARIES, value, ovalue); + this.summaries = (value == null) ? null : (BoardSummary[])value.clone(); + } + + /** + * Requests that the indexth element of + * summaries field be set to the specified value. + * The local value will be updated immediately and an event will be + * propagated through the system to notify all listeners that the + * attribute did change. Proxied copies of this object (on clients) + * will apply the value change when they received the attribute + * changed notification. + */ + public void setSummariesAt (BoardSummary value, int index) + { + BoardSummary ovalue = this.summaries[index]; + requestElementUpdate( + SUMMARIES, index, value, ovalue); + this.summaries[index] = value; + } + + /** + * Requests that the seed field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setSeed (long value) + { + long ovalue = this.seed; + requestAttributeChange( + SEED, Long.valueOf(value), Long.valueOf(ovalue)); + this.seed = value; + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java b/src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java new file mode 100644 index 00000000..9466cdaf --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/DropBlockSprite.java @@ -0,0 +1,273 @@ +// +// $Id: DropBlockSprite.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import java.awt.Rectangle; + +/** + * The drop block sprite represents a block of multiple pieces that can be + * rotated to any of the four cardinal compass directions. As such, it + * may span multiple columns or rows depending on its orientation. The + * block has a "central" piece around which it rotates, with the other + * pieces referred to as "external" pieces. + */ +public class DropBlockSprite extends DropSprite +{ + /** + * Constructs a drop block sprite and starts it dropping. + * + * @param view the board view upon which this sprite will be displayed. + * @param col the column of the central piece. + * @param row the row of the central piece. + * @param orient the orientation of the sprite. + * @param pieces the pieces displayed by the sprite. + */ + public DropBlockSprite ( + DropBoardView view, int col, int row, int orient, int[] pieces) + { + super(view, col, row, pieces, 0); + + _orient = orient; + } + + /** + * Constructs a drop block sprite and starts it dropping. + * + * @param view the board view upon which this sprite will be displayed. + * @param col the column of the central piece. + * @param row the row of the central piece. + * @param orient the orientation of the sprite. + * @param pieces the pieces displayed by the sprite. + * @param renderOrder the rendering order of the sprite. + */ + public DropBlockSprite ( + DropBoardView view, int col, int row, int orient, int[] pieces, + int renderOrder) + { + super(view, col, row, pieces, 0, renderOrder); + + _orient = orient; + } + + // documentation inherited + protected void init () + { + super.init(); + + setOrientation(_orient); + } + + /** + * Returns an array of the row numbers containing the block pieces. + * The first index is the row of the central piece. The array is + * cached and re-used internally and so the caller should make their + * own copy if they care to modify it. + */ + public int[] getRows () + { + return _rows; + } + + /** + * Returns an array of the column numbers containing the block pieces. + * The first index is the column of the central piece. The array is + * cached and re-used internally and so the caller should make their + * own copy if they care to modify it. + */ + public int[] getColumns () + { + return _cols; + } + + /** + * Returns the bounds of the block in board piece coordinates. The + * bounds rectangle is cached and re-used internally and so the caller + * should make their own copy if they care to modify it. + */ + public Rectangle getBoardBounds () + { + return _dbounds; + } + + /** + * Returns the row the external piece is located in. + */ + public int getExternalRow () + { + return _erow; + } + + /** + * Returns the column the external piece is located in. + */ + public int getExternalColumn () + { + return _ecol; + } + + // documentation inherited + public void setColumn (int col) + { + super.setColumn(col); + updateDropInfo(); + } + + // documentation inherited + public void setRow (int row) + { + super.setRow(row); + updateDropInfo(); + } + + // documentation inherited + public void setBoardLocation (int row, int col) + { + super.setBoardLocation(row, col); + updateDropInfo(); + } + + /** + * Updates the sprite image offset to reflect the direction in which + * the external piece is hanging. + */ + public void setOrientation (int orient) + { + super.setOrientation(orient); + + int edx = 0, edy = 0; + if (orient == NORTH) { + edy = -1; + } else if (orient == WEST) { + edx = -1; + } + + // update the sprite image offset + setRowOffset(edy); + setColumnOffset(edx); + + // update the external piece position and drop block bounds + updateDropInfo(); + } + + // documentation inherited + public void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", erow=").append(_erow); + buf.append(", ecol=").append(_ecol); + } + + /** + * Can this sprite pop-up a row on a forgiving rotation? + */ + public boolean canPopup () + { + return (_popups > 0); + } + + /** + * Called if we pop up to decrement the remaining popups we have. + */ + public void didPopup () + { + _popups--; + } + + /** + * Re-calculates the external piece position and bounds of the drop + * block. + */ + protected void updateDropInfo () + { + // update the external piece location + _erow = calculateExternalRow(); + _ecol = calculateExternalColumn(); + + // update the piece row and column arrays + _rows[0] = _row; + _rows[1] = _erow; + _cols[0] = _col; + _cols[1] = _ecol; + + // calculate the drop block board bounds + int maxrow = Math.max(_row, _erow); + int mincol = Math.min(_col, _ecol); + + int bpwid, bphei; + if (_orient == NORTH || _orient == SOUTH) { + bpwid = 1; + bphei = 2; + } else { + bpwid = 2; + bphei = 1; + } + + // create the bounds rectangle if necessary + _dbounds.setBounds(mincol, maxrow, bpwid, bphei); + } + + /** + * Returns the row the external piece is located in based on the + * current central piece location and sprite orientation. + */ + protected int calculateExternalRow () + { + if (_orient == NORTH) { + return (_row - 1); + } else if (_orient == SOUTH) { + return (_row + 1); + } else { + return _row; + } + } + + /** + * Returns the column the external piece is located in based on the + * current central piece location and sprite orientation. + */ + protected int calculateExternalColumn () + { + if (_orient == WEST) { + return (_col - 1); + } else if (_orient == EAST) { + return (_col + 1); + } else { + return _col; + } + } + + /** How many times this sprite can be popped-up a row in a forgiving + * rotation. */ + protected byte _popups = 2; + + /** The drop block bounds in board coordinates. */ + protected Rectangle _dbounds = new Rectangle(); + + /** The drop block piece rows. */ + protected int[] _rows = new int[2]; + + /** The drop block piece columns. */ + protected int[] _cols = new int[2]; + + /** The external piece location in board coordinates. */ + protected int _ecol, _erow; +} diff --git a/src/java/com/threerings/puzzle/drop/client/DropBoardView.java b/src/java/com/threerings/puzzle/drop/client/DropBoardView.java new file mode 100644 index 00000000..a371747a --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/DropBoardView.java @@ -0,0 +1,585 @@ +// +// $Id: DropBoardView.java 3527 2005-04-28 23:17:29Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; + +import java.util.Iterator; + +import com.threerings.media.image.Mirage; +import com.threerings.media.sprite.ImageSprite; +import com.threerings.media.sprite.PathAdapter; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.util.LinePath; +import com.threerings.media.util.Path; + +import com.threerings.parlor.media.ScoreAnimation; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.client.PuzzleBoardView; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleConfig; +import com.threerings.puzzle.util.PuzzleContext; + +import com.threerings.puzzle.drop.data.DropBoard; +import com.threerings.puzzle.drop.data.DropConfig; +import com.threerings.puzzle.drop.data.DropPieceCodes; + +/** + * The drop board view displays a drop puzzle game in progress for a + * single player. + */ +public abstract class DropBoardView extends PuzzleBoardView + implements DropPieceCodes +{ + /** The color used to render normal scoring text. */ + public static final Color SCORE_COLOR = Color.white; + + /** The color used to render chain reward scoring text. */ + public static final Color CHAIN_COLOR = Color.yellow; + + /** + * Constructs a drop board view. + */ + public DropBoardView (PuzzleContext ctx, int pwid, int phei) + { + super(ctx); + + // save off piece dimensions + _pwid = pwid; + _phei = phei; + + // determine distance to float score animations + _scoreDist = 2 * _phei; + } + + /** + * Initializes the board with the board dimensions. + */ + public void init (PuzzleConfig config) + { + DropConfig dconfig = (DropConfig)config; + + // save off the board dimensions in pieces + _bwid = dconfig.getBoardWidth(); + _bhei = dconfig.getBoardHeight(); + + super.init(config); + } + + /** + * Returns the width in pixels of a single board piece. + */ + public int getPieceWidth () + { + return _pwid; + } + + /** + * Returns the height in pixels of a single board piece. + */ + public int getPieceHeight () + { + return _phei; + } + + /** + * Called by the {@link DropSprite} to populate pos with + * the screen coordinates in pixels at which a piece at (col, + * row) in the board should be drawn. Derived classes may wish + * to override this method to allow specialised positioning of + * sprites. + */ + public void getPiecePosition (int col, int row, Point pos) + { + pos.setLocation(col * _pwid, (row * _phei) - _roff); + } + + /** + * Called by the {@link DropSprite} to get the dimensions of the area + * that will be occupied by rendering a piece segment of the given + * orientation and length whose bottom-leftmost corner is at + * (col, row). + */ + public Dimension getPieceSegmentSize (int col, int row, int orient, int len) + { + if (orient == NORTH || orient == SOUTH) { + return new Dimension(_pwid, len * _phei); + } else { + return new Dimension(len * _pwid, _phei); + } + } + + /** + * Creates a new piece sprite and places it directly in it's correct + * position. + */ + public void createPiece (int piece, int sx, int sy) + { + if (sx < 0 || sy < 0 || sx >= _bwid || sy >= _bhei) { + Log.warning("Requested to create piece in invalid location " + + "[sx=" + sx + ", sy=" + sy + "]."); + Thread.dumpStack(); + return; + } + createPiece(piece, sx, sy, sx, sy, 0L); + } + + /** + * Refreshes the piece sprite at the specified location, if no sprite + * exists at the location, one will be created. Note: this + * method assumes the default {@link ImageSprite} is being used to + * display pieces. If {@link #createPieceSprite} is overridden to + * return a non-ImageSprite, this method must also be customized. + */ + public void updatePiece (int sx, int sy) + { + updatePiece(_dboard.getPiece(sx, sy), sx, sy); + } + + /** + * Updates the piece sprite at the specified location, if no sprite + * exists at the location, one will be created. Note: this + * method assumes the default {@link ImageSprite} is being used to + * display pieces. If {@link #createPieceSprite} is overridden to + * return a non-ImageSprite, this method must also be customized. + */ + public void updatePiece (int piece, int sx, int sy) + { + if (sx < 0 || sy < 0 || sx >= _bwid || sy >= _bhei) { + Log.warning("Requested to update piece in invalid location " + + "[sx=" + sx + ", sy=" + sy + "]."); + Thread.dumpStack(); + return; + } + int spos = sy * _bwid + sx; + if (_pieces[spos] != null) { + ((ImageSprite)_pieces[spos]).setMirage( + getPieceImage(piece, sx, sy, NORTH)); + } else { + createPiece(piece, sx, sy); + } + } + + /** + * Creates a new piece sprite and moves it into position on the board. + */ + public void createPiece (int piece, int sx, int sy, int tx, int ty, + long duration) + { + if (tx < 0 || ty < 0 || tx >= _bwid || ty >= _bhei) { + Log.warning("Requested to create and move piece to invalid " + + "location [tx=" + tx + ", ty=" + ty + "]."); + Thread.dumpStack(); + return; + } + Sprite sprite = createPieceSprite(piece, sx, sy); + if (sprite != null) { + // position the piece properly to start + Point start = new Point(); + getPiecePosition(sx, sy, start); + sprite.setLocation(start.x, start.y); + // now add it to the view + addSprite(sprite); + // and potentially move it into place + movePiece(sprite, sx, sy, tx, ty, duration); + } + } + + /** + * Instructs the view to move the piece at the specified starting + * position to the specified destination position. There must be a + * sprite at the starting position, if there is a sprite at the + * destination position, it must also be moved immediately following + * this call (as in the case of a swap) to avoid badness. + * + * @return the piece sprite that is being moved. + */ + public Sprite movePiece (int sx, int sy, int tx, int ty, long duration) + { + int spos = sy * _bwid + sx; + Sprite piece = _pieces[spos]; + if (piece == null) { + Log.warning("Missing source sprite for drop [sx=" + sx + + ", sy=" + sy + ", tx=" + tx + ", ty=" + ty + "]."); + return null; + } + _pieces[spos] = null; + movePiece(piece, sx, sy, tx, ty, duration); + return piece; + } + + /** + * A helper function for moving pieces into place. + */ + protected void movePiece (Sprite piece, final int sx, final int sy, + final int tx, final int ty, long duration) + { + final Exception where = new Exception(); + + // if the sprite needn't move, then just position it and be done + Point start = new Point(); + getPiecePosition(sx, sy, start); + if (sx == tx && sy == ty) { + int tpos = ty * _bwid + tx; + if (_pieces[tpos] != null) { + Log.warning("Zoiks! Asked to add a piece where we already " + + "have one [sx=" + sx + ", sy=" + sy + + ", tx=" + tx + ", ty=" + ty + "]."); + Log.logStackTrace(where); + return; + } + _pieces[tpos] = piece; + piece.setLocation(start.x, start.y); + return; + } + + // note that this piece is moving toward its destination + final int tpos = ty * _bwid + tx; + _moving[tpos] = true; + + // then create a path and do some bits + Point end = new Point(); + getPiecePosition(tx, ty, end); + piece.addSpriteObserver(new PathAdapter() { + public void pathCompleted (Sprite sprite, Path path, long when) { + sprite.removeSpriteObserver(this); + if (_pieces[tpos] != null) { + Log.warning("Oh god, we're dropping onto another piece " + + "[sx=" + sx + ", sy=" + sy + + ", tx=" + tx + ", ty=" + ty + "]."); + Log.logStackTrace(where); + return; + } + _pieces[tpos] = sprite; + if (_actionSprites.remove(sprite)) { + maybeFireCleared(); + } + pieceArrived(when, sprite, tx, ty); + } + }); + _actionSprites.add(piece); + piece.move(new LinePath(start, end, duration)); + } + + /** + * Called when a piece is finished moving into its requested position. + * Derived classes may wish to take this opportunity to play a sound + * or whatnot. + */ + protected void pieceArrived (long tickStamp, Sprite sprite, int px, int py) + { + _moving[py * _bwid + px] = false; + } + + /** + * Returns true if the piece that is supposed to be at the specified + * coordinates is not yet there but is actually en route to that + * location (due to a call to {@link #movePiece}). + */ + protected boolean isMoving (int px, int py) + { + return _moving[py * _bwid + px]; + } + + /** + * Returns the image used to display the given piece at coordinates + * (0, 0) with an orientation of {@link #NORTH}. This + * serves as a convenience routine for those puzzles that don't bother + * rendering their pieces differently when placed at different board + * coordinates or in different orientations. + */ + public Mirage getPieceImage (int piece) + { + return getPieceImage(piece, 0, 0, NORTH); + } + + /** + * Returns the image used to display the given piece at the specified + * column and row with the given orientation. + */ + public abstract Mirage getPieceImage ( + int piece, int col, int row, int orient); + + // documentation inherited + public void setBoard (Board board) + { + // when a new board arrives, we want to remove all drop sprites + // so that they don't modify the new board with their old ideas + for (Iterator iter = _actionSprites.iterator(); iter.hasNext(); ) { + Sprite s = (Sprite) iter.next(); + if (s instanceof DropSprite) { + // remove it from _sprites safely + iter.remove(); + // but then use the standard removal method + removeSprite(s); + } + } + + // remove all of this board's piece sprites + int pcount = (_pieces == null) ? 0 : _pieces.length; + for (int ii = 0; ii < pcount; ii++) { + if (_pieces[ii] != null) { + removeSprite(_pieces[ii]); + } + } + + super.setBoard(board); + + _dboard = (DropBoard)board; + + // create the pieces for the new board + Point spos = new Point(); + int width = _dboard.getWidth(), height = _dboard.getHeight(); + _pieces = new Sprite[width * height]; + for (int yy = 0; yy < height; yy++) { + for (int xx = 0; xx < width; xx++) { + Sprite piece = createPieceSprite( + _dboard.getPiece(xx, yy), xx, yy); + if (piece != null) { + int ppos = yy * width + xx; + getPiecePosition(xx, yy, spos); + piece.setLocation(spos.x, spos.y); + addSprite(piece); + _pieces[ppos] = piece; + } + } + } + + // we use this to track when pieces are animating toward their new + // position and are not yet in place + _moving = new boolean[width * height]; + } + + /** + * Returns the piece sprite at the specified location. + */ + public Sprite getPieceSprite (int xx, int yy) + { + return _pieces[yy * _dboard.getWidth() + xx]; + } + + /** + * Clears the specified piece from the board. + */ + public void clearPieceSprite (int xx, int yy) + { + int ppos = yy * _dboard.getWidth() + xx; + if (_pieces[ppos] != null) { + removeSprite(_pieces[ppos]); + _pieces[ppos] = null; + } + } + + /** + * Clears out a piece from the board along with its piece sprite. + */ + public void clearPiece (int xx, int yy) + { + _dboard.setPiece(xx, yy, PIECE_NONE); + clearPieceSprite(xx, yy); + } + + /** + * Creates a new drop sprite used to animate the given pieces falling + * in the specified column. + */ + public DropSprite createPieces (int col, int row, int[] pieces, int dist) + { + return new DropSprite(this, col, row, pieces, dist); + } + + /** + * Dirties the rectangle encompassing the segment with the given + * direction and length whose bottom-leftmost corner is at (col, + * row). + */ + public void dirtySegment (int dir, int col, int row, int len) + { + int x = _pwid * col, y = (_phei * row) - _roff; + int wid = (dir == VERTICAL) ? _pwid : len * _pwid; + int hei = (dir == VERTICAL) ? _phei * len : _phei; + _remgr.invalidateRegion(x, y, wid, hei); + } + + /** + * Creates and returns an animation showing the specified score + * floating up the view, with the label initially centered within the + * view. + * + * @param score the score text to display. + * @param color the color of the text. + * @param font the font. + */ + public ScoreAnimation createScoreAnimation ( + String score, Color color, Font font) + { + return createScoreAnimation( + score, color, font, 0, _bhei - 1, _bwid, _bhei); + } + + /** + * Creates and returns an animation showing the specified score + * floating up the view. + * + * @param score the score text to display. + * @param color the color of the text. + * @param font the font to use. + * @param x the left coordinate in board coordinates of the rectangle + * within which the score is to be centered. + * @param y the bottom coordinate in board coordinates of the + * rectangle within which the score is to be centered. + * @param width the width in board coordinates of the rectangle within + * which the score is to be centered. + * @param height the height in board coordinates of the rectangle + * within which the score is to be centered. + */ + public ScoreAnimation createScoreAnimation (String score, Color color, + Font font, int x, int y, + int width, int height) + { + // create the score animation + ScoreAnimation anim = + createScoreAnimation(score, color, font, x, y); + + // position the label within the specified rectangle + Dimension lsize = anim.getLabel().getSize(); + Point pos = new Point(); + centerRectInBoardRect( + x, y, width, height, lsize.width, lsize.height, pos); + anim.setLocation(pos.x, pos.y); + + return anim; + } + + /** + * Creates the sprite that is used to display the specified piece. If + * the piece represents no piece, this method should return null. + */ + protected Sprite createPieceSprite (int piece, int px, int py) + { + if (piece == PIECE_NONE) { + return null; + } + ImageSprite sprite = new ImageSprite( + getPieceImage(piece, px, py, NORTH)); + sprite.setRenderOrder(-1); + return sprite; + } + + /** + * Populates pos with the most appropriate screen + * coordinates to center a rectangle of the given width and height (in + * pixels) within the specified rectangle (in board coordinates). + * + * @param bx the bounding rectangle's left board coordinate. + * @param by the bounding rectangle's bottom board coordinate. + * @param bwid the bounding rectangle's width in board coordinates. + * @param bhei the bounding rectangle's height in board coordinates. + * @param rwid the width of the rectangle to position in pixels. + * @param rhei the height of the rectangle to position in pixels. + * @param pos the point to populate with the rectangle's final + * position. + */ + protected void centerRectInBoardRect ( + int bx, int by, int bwid, int bhei, int rwid, int rhei, Point pos) + { + getPiecePosition(bx, by + 1, pos); + pos.x += (((bwid * _pwid) - rwid) / 2); + pos.y -= ((((bhei * _phei) - rhei) / 2) + rhei); + + // constrain to fit wholly within the board bounds + pos.x = Math.max(Math.min(pos.x, _bounds.width - rwid), 0); + } + + /** + * Rotates the given drop block sprite to the specified orientation, + * updating the image as necessary. Derived classes that make use of + * block dropping functionality should override this method to do the + * right thing. + */ + public void rotateDropBlock (DropBlockSprite sprite, int orient) + { + // nothing for now + } + + // documentation inherited + public void paintBetween (Graphics2D gfx, Rectangle dirtyRect) + { + gfx.translate(0, -_roff); + renderBoard(gfx, dirtyRect); + renderRisingPieces(gfx, dirtyRect); + gfx.translate(0, _roff); + } + + // documentation inherited + public Dimension getPreferredSize () + { + int wid = _bwid * _pwid; + int hei = _bhei * _phei; + return new Dimension(wid, hei); + } + + /** + * Renders the row of rising pieces to the given graphics context. + * Sub-classes that make use of board rising functionality should + * override this method to draw the rising piece row. + */ + protected void renderRisingPieces (Graphics2D gfx, Rectangle dirtyRect) + { + // nothing for now + } + + /** + * Sets the board rising offset to the given y-position. + */ + protected void setRiseOffset (int y) + { + if (y != _roff) { + _roff = y; + _remgr.invalidateRegion(_bounds); + } + } + + /** The drop board. */ + protected DropBoard _dboard; + + /** A sprite for every piece displayed in the drop board. */ + protected Sprite[] _pieces; + + /** Indicates whether a piece is en route to its destination. */ + protected boolean[] _moving; + + /** The piece dimensions in pixels. */ + protected int _pwid, _phei; + + /** The board rising offset. */ + protected int _roff; + + /** The board dimensions in pieces. */ + protected int _bwid, _bhei; +} diff --git a/src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java b/src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java new file mode 100644 index 00000000..1ef90ae8 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/DropControllerDelegate.java @@ -0,0 +1,1213 @@ +// +// $Id: DropControllerDelegate.java 3772 2005-12-01 23:54:00Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import java.awt.Component; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.ActionEvent; + +import java.util.List; + +import com.samskivert.util.IntListUtil; + +import com.threerings.media.FrameParticipant; +import com.threerings.media.animation.Animation; +import com.threerings.media.animation.AnimationAdapter; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.puzzle.util.PuzzleContext; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.client.PuzzleController; +import com.threerings.puzzle.client.PuzzleControllerDelegate; +import com.threerings.puzzle.client.PuzzlePanel; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.BoardSummary; + +import com.threerings.puzzle.drop.data.DropBoard; +import com.threerings.puzzle.drop.data.DropCodes; +import com.threerings.puzzle.drop.data.DropConfig; +import com.threerings.puzzle.drop.data.DropLogic; +import com.threerings.puzzle.drop.data.DropPieceCodes; +import com.threerings.puzzle.drop.util.PieceDropLogic; +import com.threerings.puzzle.drop.util.PieceDropper.PieceDropInfo; +import com.threerings.puzzle.drop.util.PieceDropper; + +/** + * Games that wish to make use of the drop puzzle services will need to + * create an extension of this delegate class, customizing it for their + * particular game and then adding it via {@link + * PuzzleController#addDelegate}. + * + *

It handles logical actions for a puzzle game that generally + * consists of a two-dimensional board containing pieces, with new pieces + * either falling into the board as a "drop block", or rising into the + * bottom of the board in new piece rows. + * + *

Derived classes must implement {@link #getPieceVelocity} and {@link + * #evolveBoard}. + * + *

Block-dropping puzzles will likely want to override {@link + * #createNextBlock}, {@link #blockDidLand}, and {@link + * #getPieceDropLogic}. + * + *

Board-rising puzzles will likely want to override {@link + * #getRiseVelocity}, {@link #getRiseDistance}, {@link + * #getPieceDropLogic}, and {@link #boardDidRise}. + */ +public abstract class DropControllerDelegate extends PuzzleControllerDelegate + implements DropCodes, DropPieceCodes, FrameParticipant +{ + /** The action command for moving the block to the left. */ + public static final String MOVE_BLOCK_LEFT = "move_block_left"; + + /** The action command for moving the block to the right. */ + public static final String MOVE_BLOCK_RIGHT = "move_block_right"; + + /** The action command for rotating the block counter-clockwise. */ + public static final String ROTATE_BLOCK_CCW = "rotate_block_ccw"; + + /** The action command for rotating the block clockwise. */ + public static final String ROTATE_BLOCK_CW = "rotate_block_cw"; + + /** The action command for starting to dropping the block. */ + public static final String START_DROP_BLOCK = "start_drop_block"; + + /** The action command for ending dropping the block. */ + public static final String END_DROP_BLOCK = "end_drop_block"; + + /** The action command for raising the next rising row. */ + public static final String RAISE_ROW = "raise_row"; + + /** + * Creates a delegate with the specified drop game logic and + * controller. + */ + public DropControllerDelegate (PuzzleController ctrl, DropLogic logic) + { + super(ctrl); + + // keep this for later + _ctrl = ctrl; + + // obtain the drop logic parameters + DropLogic dlogic = (DropLogic)logic; + _usedrop = dlogic.useBlockDropping(); + _userise = dlogic.useBoardRising(); + + if (_userise) { + // prepare for board rising + _risevel = getRiseVelocity(); + _risedist = getRiseDistance(); + } + } + + // documentation inherited + public void init (CrowdContext ctx, PlaceConfig config) + { + super.init(ctx, config); + + // save things off + PuzzlePanel panel = (PuzzlePanel)_ctrl.getPlaceView(); + _ctx = (PuzzleContext)ctx; + _dview = (DropBoardView)panel.getBoardView(); + _dpanel = (DropPanel)panel; + _dboard = (DropBoard)_ctrl.getBoard(); + + // obtain the board dimensions + DropConfig dconfig = (DropConfig)config; + _bwid = dconfig.getBoardWidth(); + _bhei = dconfig.getBoardHeight(); + + // create the piece dropper if appropriate + PieceDropLogic pdl = getPieceDropLogic(); + if (pdl != null) { + _dropper = new PieceDropper(pdl); + } + } + + /** + * Returns the speed with which the next board row should rise into + * place, in pixels per millisecond. + */ + protected float getRiseVelocity () + { + return DEFAULT_RISE_VELOCITY; + } + + /** + * Returns the distance in pixels that each board row will traverse + * when rising into place. + */ + protected int getRiseDistance () + { + return DEFAULT_RISE_DISTANCE; + } + + /** + * Starts up the action; tries evolving the board to get things going. + */ + protected void startAction () + { + super.startAction(); + +// Log.info("Starting drop action"); + + // add ourselves as a frame participant + _ctx.getFrameManager().registerFrameParticipant(this); + +// if (_userise) { +// // make sure the board has its next row of pieces +// advanceRisingPieces(); +// // we'll set up the risestamp on the first rise tick +// } + + // if we've a drop block left over from our previous action, set + // it on its merry way once more + if (_blocksprite != null) { + long delta = _dview.getTimeStamp() - _blockStamp; + Log.info("Restarting drop sprite [delta=" + delta + "]."); + _blocksprite.fastForward(delta); + _blockStamp = 0L; + _dview.addSprite(_blocksprite); + + // if we cleared the action while the drop sprite was + // bouncing, we need to land the block to get things going + // again + if (_blocksprite.isBouncing()) { + Log.info("Ended on a bounce, landing the block and " + + "starting things up."); + checkBlockLanded("bounced", true, true); + } + } + + // evolve the board to kick-start the game into action + unstabilizeBoard(); + } + + // documentation inherited + protected boolean canClearAction () + { + if (!_stable) { + Log.info("Rejecting canClear() request because not stable."); + } + return _stable && super.canClearAction(); + } + + /** + * Clears out all of the action in the board; removes any drop block + * sprites, any pieces rising in the board, and resets the animation + * timestamps. + */ + protected void clearAction () + { + super.clearAction(); + +// Log.info("Clearing drop action."); + + // do away with the bounce interval + _bounceStamp = 0; + _bounceRow = Integer.MIN_VALUE; + + // kill any active drop block + if (_blocksprite != null) { + _dview.removeSprite(_blocksprite); + _blockStamp = _dview.getTimeStamp(); + } + + // reset intermediate rising timestamps + _rpstamp = 0; + _zipstamp = 0; + _fastDrop = false; + + // remove ourselves as a frame participant + _ctx.getFrameManager().removeFrameParticipant(this); + } + + // documentation inherited + public void gameDidEnd () + { + super.gameDidEnd(); + + // clear out the drop block sprite + _blocksprite = null; + + // reset ourselves back to pre-game conditions + _risestamp = 0; +// _dview.setRisingPieces(null); + } + + // documentation inherited + public boolean handleAction (ActionEvent action) + { + // handle any block-related movement actions + if (handleBlockAction(action)) { + return true; + } + + String cmd = action.getActionCommand(); + if (cmd.equals(START_DROP_BLOCK)) { + handleDropBlock(true); + + } else if (cmd.equals(END_DROP_BLOCK)) { + handleDropBlock(false); + + } else { + return super.handleAction(action); + } + + return true; + } + + // documentation inherited + public void setBoard (Board board) + { + super.setBoard(board); + + // update the casted board reference + _dboard = (DropBoard)board; + // and update our self-summary + updateSelfSummary(); + } + + // documentation inherited + protected boolean handleBlockAction (ActionEvent action) + { + String cmd = action.getActionCommand(); + boolean handled = false; + + if (cmd.equals(MOVE_BLOCK_LEFT)) { + handleMoveBlock(LEFT); + handled = true; + + } else if (cmd.equals(MOVE_BLOCK_RIGHT)) { + handleMoveBlock(RIGHT); + handled = true; + + } else if (cmd.equals(ROTATE_BLOCK_CCW)) { + handleRotateBlock(CCW); + handled = true; + + } else if (cmd.equals(ROTATE_BLOCK_CW)) { + handleRotateBlock(CW); + handled = true; + } + + if (handled && _blocksprite != null) { + // land the block if it's been placed on something solid as a + // result of one of the above actions + String source = "fiddled [cmd=" + cmd + "]"; + if (checkBlockLanded(source, false, false)) { + startBounceTimer(source); + } + } + + return handled; + } + + /** + * Handles block moved events. + */ + protected void handleMoveBlock (int dir) + { + if (_blocksprite == null) { + return; + } + + // gather information regarding the attempted move + Rectangle bb = _blocksprite.getBoardBounds(); + int row = _blocksprite.getRow(), col = _blocksprite.getColumn(); + int dx = (dir == LEFT) ? -1 : 1; + + // if the sprite has made it to the bottom of the board then we + // don't want to allow it to "virtually" fall any further because + // of the bounce interval + float pctdone = (row >= (_bhei - 1)) ? 0 : + _blocksprite.getPercentDone(_dview.getTimeStamp()); + + // get the drop block position resulting from the move + Point pos = _dboard.getForgivingMove( + bb.x, bb.y, bb.width, bb.height, dx, 0, pctdone); + if (pos != null) { + int frow = row + (pos.y - bb.y); + int fcol = col + (pos.x - bb.x); + // Log.info("Valid move [row=" + frow + ", col=" + col + "]."); + _blocksprite.setBoardLocation(frow, fcol); + } + } + + /** + * Handles block rotation events. + */ + protected void handleRotateBlock (int dir) + { + if (_blocksprite == null) { + return; + } + + // gather information regarding the attempted rotation + int[] rows = _blocksprite.getRows(); + int[] cols = _blocksprite.getColumns(); + // if the sprite has made it to the bottom of the board then we + // don't want to allow it to "virtually" fall any further because + // of the bounce interval + float pctdone = (rows[0] >= (_bhei - 1)) ? 0 : + _blocksprite.getPercentDone(_dview.getTimeStamp()); + + // get the drop block position resulting from the rotation + int[] info = _dboard.getForgivingRotation( + rows, cols, _blocksprite.getOrientation(), dir, + getRotationType(), pctdone, _blocksprite.canPopup()); + if (info != null) { +// Log.info("Found valid rotation " + +// "[orient=" + DirectionUtil.toShortString(info[0]) + +// ", col=" + info[1] + ", row=" + info[2] + +// ", blocksprite=" + _blocksprite + "]."); + + // update the piece image + _dview.rotateDropBlock(_blocksprite, info[0]); + + // place the block in its newly rotated location + _blocksprite.setBoardLocation(info[2], info[1]); + + if (info[3] != 0) { + _blocksprite.didPopup(); + } + + // let derived classes do what they will + blockDidRotate(dir); + } + } + + /** + * Called when the drop block has rotated in the specified direction + * to allow derived classes to engage in any game-specific antics. + */ + protected void blockDidRotate (int dir) + { + } + + /** + * Returns the rotation type used by this drop game. Either {@link + * DropBoard#RADIAL_ROTATION} or {@link DropBoard#INPLACE_ROTATION}. + */ + protected int getRotationType () + { + return DropBoard.RADIAL_ROTATION; + } + + /** + * Handles drop block events. + */ + protected void handleDropBlock (boolean fast) + { + _fastDrop = fast; + + // only allow changing the piece velocity if we're not bouncing + if (_blocksprite != null && _bounceStamp == 0) { + // Log.info("Updating drop block velocity [fast=" + fast + "]."); + _blocksprite.setVelocity(getPieceVelocity(fast)); + } + } + + /** + * Returns the drop sprite velocity to assign to a new drop sprite. + */ + protected abstract float getPieceVelocity (boolean fast); + + /** + * Handles creation and dropping of the next dropping block. + */ + protected void dropNextBlock () + { + if (_blocksprite != null || !_ctrl.hasAction()) { + Log.info("Not dropping block [bs=" + (_blocksprite != null) + + ", action=" + _ctrl.hasAction() + "]."); + return; + } + + // determine whether or not the game should be ended because we + // can't drop the next block + if (checkDropEndsGame()) { + return; + } + + int pidx = _ctrl.getPlayerIndex(); + if (pidx != -1 && !_ctrl.isGameOver() && _puzobj.isActivePlayer(pidx)) { + // create the next block + _blocksprite = createNextBlock(); + if (_blocksprite != null) { + // reset the drop block fast-drop state + _fastDrop = false; + + // configure and add the drop block sprite + _blocksprite.setVelocity(getPieceVelocity(_fastDrop)); + _blocksprite.addSpriteObserver(_dropMovedHandler); + _dview.addSprite(_blocksprite); + + // update the next block display + _dpanel.setNextBlock(peekNextPieces()); + + // make sure the block has somewhere to go + if (checkBlockLanded("next-block", false, true)) { + startBounceTimer("next-block"); + } + } + + // clear out our last bounce row + _bounceRow = Integer.MIN_VALUE; + } + } + + /** + * Called by {@link #dropNextBlock} to determine whether the game + * should be ended rather than dropping the next block because the + * board is filled and a new block cannot enter. If true is returned, + * the drop controller assumes that the derived class will have ended + * or reset the game as appropriate and will simply abandon its + * attempt to drop the next block. + */ + protected boolean checkDropEndsGame () + { + return false; + } + + /** + * Called only for block-dropping puzzles when it's time to create the + * next drop block. Returns the drop block sprite if it was + * successfully created, or null if it was not. + */ + protected DropBlockSprite createNextBlock () + { + // nothing for now + return null; + } + + /** + * Take a peek at the next pieces. + */ + protected int[] peekNextPieces () + { + return null; + } + + /** + * Called when a drop sprite posts a piece moved event. + */ + protected void handleDropSpriteMoved ( + DropSprite sprite, long when, int col, int row) + { + if (sprite instanceof DropBlockSprite) { + if (checkBlockLanded("piece-moved", false, true)) { + startBounceTimer("piece-moved"); + } + // keep dropping the drop block + sprite.drop(); + return; + } + + if (sprite.getDistance() > 0) { + sprite.drop(); + + } else { + // remove the sprite + _dview.removeSprite(sprite); + + // apply the pieces to the board + applyDropSprite(sprite, col, row); + + // perform any new destruction and falling + unstabilizeBoard(); + } + } + + /** + * Applies the pieces in the given sprite to the specified column and + * row in the board. Called when a drop sprite has finished + * traversing its entire distance. + */ + protected void applyDropSprite (DropSprite sprite, int col, int row) + { + // set the pieces in the board + int[] pieces = sprite.getPieces(); + _dboard.setSegment(VERTICAL, col, row, pieces); + // create the updated board pieces + for (int dy = 0; dy < pieces.length; dy++) { + // pieces outside the board are discarded + if (row - dy >= 0) { + // note: vertical segments are applied counting downwards + // from the starting row toward row zero + _dview.createPiece(pieces[dy], col, row - dy); + } + } + } + + /** + * Called to determine whether it is safe to evolve the board. The + * default implementation does not allow board evolution if there are + * sprites or animations active on the board. + */ + protected boolean canEvolveBoard () + { + return (_dview.getActionCount() == 0); + } + + /** + * Evolves the board to an unchanging state. If the board is in a + * state where pieces should react with one another to cause changes + * to the board state (such as piece dropping via {@link #dropPieces}, + * piece destruction, and/or piece joining), this is where that + * process should be effected. + * + *

When no further evolution is possible and the board has + * stabilized this method should return false to indicate that such + * action should be taken. That will result in a follow-up call to + * {@link #boardDidStabilize} (assuming that the action was not + * cleared prior to the final stabilization of the board). + */ + protected abstract boolean evolveBoard (); + + /** + * Derived classes should call this method whenever they change some + * board state that will require board evolution to restabilize the + * board. + */ + protected void unstabilizeBoard () + { + _stable = false; + } + + /** + * Called when the board has been fully evolved and is once again + * stable. The default implementation updates the player's local board + * summary and drops the next block into the board, but derived + * classes may wish to perform custom actions if they don't use drop + * blocks or have other requirements. + */ + protected void boardDidStabilize () + { + updateSelfSummary(); + dropNextBlock(); + } + + /** + * Updates the player's own local board summary to reflect the local + * copy of the player's board which is likely to be more up to date + * than the server-side board from which all other player board + * summaries originate. + */ + public void updateSelfSummary () + { + int pidx = _ctrl.getPlayerIndex(); + if (pidx != -1 && _puzobj != null && _puzobj.summaries != null) { + BoardSummary bsum = _puzobj.summaries[pidx]; + bsum.setBoard(_dboard); + bsum.summarize(); + _dpanel.setSummary(pidx, bsum); + } + } + + /** + * Called when an animation finishes doing its business. Derived + * classes may wish to override this method but should be sure to call + * super.animationDidFinish(). + */ + protected void animationDidFinish (Animation anim) + { + unstabilizeBoard(); + } + + /** + * Checks whether the drop block can continue dropping and lands its + * pieces if not. Returns whether at least one piece of the block has + * landed; note that the other piece may need subsequent dropping. + * + * @param commit if true, the block landing is committed, if false, it + * is only checked, not committed. + * @param atTop whether the block sprite is to be treated as being at + * the top of its current row. + */ + protected boolean checkBlockLanded ( + String source, boolean commit, boolean atTop) + { + if (_blocksprite == null) { + return true; + } + + // check to see that both pieces can continue dropping + int[] rows = _blocksprite.getRows(); + int[] cols = _blocksprite.getColumns(); + + // TODO: we may need to limit pctdone here to account for landing + // on the bottom of the board. + float pctdone = (atTop) ? 0.0f : + _blocksprite.getPercentDone(_dview.getTimeStamp()); + +// Log.info("Checking landed [source=" + source + +// ", bounceRow=" + _bounceRow + +// ", rows=" + StringUtil.toString(rows) + +// ", cols=" + StringUtil.toString(cols) + +// ", orient=" + DirectionUtil.toShortString( +// _blocksprite.getOrientation()) + +// ", commit=" + commit + ", pctdone=" + pctdone + "]."); + + if (_dboard.isValidDrop(rows, cols, pctdone)) { + if (commit) { + Log.info("Not valid drop [source=" + source + + ", commit=" + commit + ", atTop=" + atTop + + ", pctdone=" + pctdone + "]."); + } + return false; + } + + // if we're committing the landing, remove the sprite and update + // the board and all that + if (commit) { + // give sub-classes a chance to do any pre-landing business + blockWillLand(); + + // stamp the pieces into the board + int[] pieces = _blocksprite.getPieces(); + boolean error = false; + for (int ii = 0; ii < pieces.length; ii++) { + if (rows[ii] >= 0) { + int col = cols[ii], row = rows[ii]; + + // sanity-check the block to make sure it's located in + // a valid position, and that we aren't somehow + // overwriting an existing piece + if (col < 0 || col >= _bwid || row >= _bhei) { + Log.warning("Placing drop block piece outside board " + + "bounds!? [x=" + col + ", y=" + row + + ", pidx=" + ii + + ", blocksprite=" + _blocksprite + "]."); + error = true; + + } else { + int cpiece = _dboard.getPiece(col, row); + if (cpiece != PIECE_NONE) { + Log.warning("Placing drop block piece onto " + + "occupied board position!? [x=" + col + + ", y=" + row + ", pidx=" + ii + + ", blocksprite=" + _blocksprite + "]."); + error = true; + } + } + + if (!error) { + // stuff the piece into the board + _dboard.setPiece(col, row, pieces[ii]); + _dview.createPiece(pieces[ii], col, row); + } + } + + if (DEBUG_PUZZLE && error) { + _dboard.dump(); + Log.warning("Bailing out in a flaming pyre of glory."); + System.exit(0); + } + } + + // remove the drop block sprite + _dview.removeSprite(_blocksprite); + _blocksprite = null; + + // give sub-classes a chance to do any post-landing business + blockDidLand(); + } + + return true; + } + + /** + * Called only for block-dropping puzzles when the drop block is about + * to land on something. Derived classes may wish to override this + * method to perform game-specific actions such as queueing up a + * "block placed" progress event. + */ + protected void blockWillLand () + { + // nothing for now + } + + /** + * Called only for block-dropping puzzles when the drop block lands on + * something. Derived classes may wish to override this method to + * perform any game-specific actions. + */ + protected void blockDidLand () + { + // nothing for now + } + + /** + * Called when a block lands. We give the user a smidgen of time to + * continue to fiddle with the block before we actually land it. If + * the block is still landed when the bounce timer expires, we commit + * the landing, otherwise we let the block keep falling. + */ + protected void startBounceTimer (String source) + { + int bounceRow = IntListUtil.getMaxValue(_blocksprite.getRows()); +// Log.info("startBounceTimer [source=" + source + +// ", bounceStamp=" + _bounceStamp + +// ", time=" + _dview.getTimeStamp() + +// ", bounceRow=" + _bounceRow + +// ", nbounceRow=" + bounceRow + "]."); + + // forcibly land the block if we bounce twice at the same row + if (_bounceStamp == 0 && _bounceRow == bounceRow) { + if (checkBlockLanded("double-bounced", true, true)) { + unstabilizeBoard(); + } + return; + } + + // if the bounce "timer" is already started, the user probably did + // something like rotate the piece while it was bouncing (which is + // why we give them the bounce interval), so we don't reset + if (_bounceStamp == 0) { + // slow the piece down so that it doesn't fly past the + // coordinates at which it's potentially landing; we have to + // do this before we tell the sprite that it's bouncing + // because changing the velocity fiddles with the rowstamp and + // we're going to reset the rowstamp when we tell the sprite + // that it's bouncing + _blocksprite.setVelocity(getPieceVelocity(false)); + + // set up our bounce interval (it depends on the current piece + // velocity and so must be set at the time we bounce) + _bounceInterval = (int) + ((_dview.getPieceHeight() * BOUNCE_FRACTION) / + getPieceVelocity(false)); +// Log.info("bounceInterval=" + _bounceInterval + +// ", phei=" + _dview.getPieceHeight() + +// ", vel=" + getPieceVelocity(false)); + + // make a note of the time we started bouncing + _bounceStamp = _dview.getTimeStamp(); + + // and the row at which we're bouncing + _bounceRow = bounceRow; + + // put the block sprite into bouncing mode + _blocksprite.setBouncing(true); + } + } + + /** + * Called when the bounce timer expires. Herein we either commit the + * landing of a block if it is still landed or let it keep falling if + * it is no longer landed. + */ + protected void bounceTimerExpired () + { +// Log.info("bounceTimerExpired [bounceStamp=" + _bounceStamp + +// ", time=" + _dview.getTimeStamp() + +// ", bounceRow=" + _bounceRow + "]."); + + // make sure we weren't cancelled for some reason + if (_bounceStamp != 0) { + if (checkBlockLanded("bounced", true, true)) { + unstabilizeBoard(); + + } else if (_blocksprite != null) { + // take the block sprite out of bouncing mode + _blocksprite.setBouncing(false); + } + _bounceStamp = 0; + } + } + + /** + * Drops any pieces that need dropping and returns whether any pieces + * were dropped. Derived classes that would like to drop their pieces + * should include a call to this method in their {@link #evolveBoard} + * implementation, and must also override {@link #getPieceDropLogic} + * to provide their game-specific piece dropper implementation. + */ + protected boolean dropPieces () + { + PieceDropper.DropObserver drobs = new PieceDropper.DropObserver() { + public void pieceDropped ( + int piece, int sx, int sy, int dx, int dy) { + float vel = getPieceVelocity(true) * 1.5f; + long duration = (long)(_dview.getPieceHeight() * + Math.abs(dy-sy) / vel); + if (sy < 0) { + _dview.createPiece(piece, sx, sy, dx, dy, duration); + } else { + _dview.movePiece(sx, sy, dx, dy, duration); + } + } + }; + return (_dropper.dropPieces(_dboard, drobs) > 0); + } + + /** + * Returns the piece dropper used to drop any pieces that need + * dropping in the board. Derived classes that intend to make use of + * {@link #dropPieces} must implement this method and return a + * reference to their game-specific piece dropper implementation. + */ + protected PieceDropLogic getPieceDropLogic () + { + return null; + } + + // documentation inherited + public void tick (long tickStamp) + { +// if (_userise && (tickStamp >= _risesent + RISE_INTERVAL)) { +// _risesent += RISE_INTERVAL; +// raiseBoard(tickStamp); +// } + + // check the bounce timer + if ((_bounceStamp != 0) && + ((tickStamp - _bounceStamp) >= _bounceInterval)) { + bounceTimerExpired(); + } + + // if we can't evolve the board because it doesn't need evolving + // or things are going on, we stop here + if (_stable || !canEvolveBoard()) { + return; + } + + // if we do not evolve the board in any way, let the derived class + // know that the board stabilized so that they can drop in a new + // piece if they like or take whatever other action is appropriate + boolean evolving = evolveBoard(); + boolean debug = false; + if (debug) { + Log.info("Evolved board [evolving=" + evolving + "]."); + } + + // if we're no longer evolving and the action has not ended, go + // ahead and let our derived class know that the board has + // stabilized so that it can drop in the next piece or somesuch + if (!evolving) { + // no evolving again until someone destabilizes the board + _stable = true; + + // this will trigger further puzzle activity + if (debug) { + Log.info("Board did stabilize"); + } + boardDidStabilize(); + + // ensure that if we have been postponing action due to board + // evolution, that it will now be cleared + if (!_ctrl.hasAction()) { + if (debug) { + Log.info("Maybe clearing action."); + } + maybeClearAction(); + } + } + } + + // documentation inherited + public Component getComponent () + { + return null; + } + + // documentation inherited + public boolean needsPaint () + { + return false; + } + + /** + * Sets whether the board rising is paused. + */ + public void setRisingPaused (boolean paused) + { + if (paused && _rpstamp == 0) { + // pause the board + _rpstamp = _dview.getTimeStamp(); + + } else if (!paused && _rpstamp != 0) { + // un-pause the board + long delta = _dview.getTimeStamp() - _rpstamp; + _risestamp += delta; + if (_zipstamp != 0) { + _zipstamp += delta; + } + _rpstamp = 0; + } + } + + /** + * Causes the board to zip quickly to the next row. + */ + public void zipToNextRow () + { + // don't overwrite an existing zip + if (_zipstamp == 0) { + // if we're paused, inherit the pause time, otherwise use the + // current time + if (_rpstamp != 0) { + _zipstamp = _rpstamp; + } else { + _zipstamp = _dview.getTimeStamp(); + } + } + } + + /** + * Called periodically on the frame tick. Raises the board row based + * on the time since the current row traversal began. + */ + /* + protected void raiseBoard (long tickStamp) + { + // don't raise if rising is paused or the action is cleared + if (_rpstamp != 0 || !_ctrl.hasAction()) { + return; + } + + // initialize the rise stamp the first time we're risen + if (_risestamp == 0) { + _risestamp = tickStamp; + _risesent = _risestamp; + } + + // determine how far we've risen + long msecs = tickStamp - _risestamp; + float travpix = msecs * _risevel; + + // account for any zipping effect + long zipsecs = 0; + if (_zipstamp > 0) { + zipsecs = tickStamp - _zipstamp; + // make sure we don't zip past the top + float zippix = (zipsecs * _risevel * 15); + if (travpix < _risedist) { + travpix += zippix; + travpix = Math.min(travpix, _risedist); + } + } + + float pctdone = travpix / _risedist; + + boolean rose = false; + if (pctdone >= 1.0f) { + rose = true; + if (_zipstamp > 0) { + // clear out any zip stamp + _zipstamp = 0; + _risestamp = tickStamp; + pctdone = 1f; + + } else { + long used = (long)(_risedist / _risevel); + _risestamp += used; + } + + // give sub-classes a chance to do their thing + boardWillRise(); + } + + // update the board display + int ypos = ((int)(_risedist * pctdone)) % _risedist; + _dview.setRiseOffset(ypos); + + if (rose) { + // check to see if this means doom and defeat (even though the + // game might be over, we still want to advance the piece + // packet one last time and do the last rise so that the + // server can tell that we kicked the proverbial bucket) + boolean canRise = checkCanRise(); + + // apply the rising row pieces to the board + int[] pieces = _dview.getRisingPieces(); + _dboard.applyRisingPieces(pieces); + + // set up the next row of rising pieces + _dview.setRisingPieces(null); + advanceRisingPieces(); + + // give sub-classes a chance to do their thing + boardDidRise(); + + if (canRise) { + // evolve the board + unstabilizeBoard(); + + } else { + Log.debug("Sticking fork in it [risers=" + + StringUtil.toString(pieces) + "."); + + // let the controller know that we're done for + _ctrl.resetGame(); + } + } + +// Log.info("Board rise [msecs=" + msecs + ", roff=" + ypos + +// ", pctdone=" + pctdone + ", zipsecs=" + zipsecs + "]."); + } + */ + + /** + * Called to determine whether or not rising a new row into the board + * is legal. The default implementation will return false if the top + * row of the board contains any pieces. + */ + protected boolean checkCanRise () + { + return !_dboard.rowContainsPieces(0, PIECE_NONE); + } + + /** + * Called only for board-rising puzzles before effecting the rising of + * the board by one row. Derived classes may wish to override this + * method to add any desired behaviour, but should be sure to call + * super.boardWillRise(). + */ + protected void boardWillRise () + { + // nothing for now + } + + /** + * Called only for board-rising puzzles when the board has finished + * rising one row. Derived classes may wish to override this method + * to add any desired behaviour, but should be sure to call + * super.boardDidRise(). + */ + protected void boardDidRise () + { + // nothing for now + } + + /** The yohoho context. */ + protected PuzzleContext _ctx; + + /** Our puzzle controller. */ + protected PuzzleController _ctrl; + + /** The drop panel. */ + protected DropPanel _dpanel; + + /** The drop board view. */ + protected DropBoardView _dview; + + /** The drop board. */ + protected DropBoard _dboard; + + /** Whether the game is using drop block functionality. */ + protected boolean _usedrop; + + /** Whether the game is using board rising functionality. */ + protected boolean _userise; + + /** Whether or not the board is currently stable. */ + protected boolean _stable; + + /** The board dimensions in pieces. */ + protected int _bwid, _bhei; + + /** The distance the board row travels in pixels. */ + protected int _risedist; + + /** The speed with which the board rises in pixels per millisecond. */ + protected float _risevel; + + /** The drop block sprite associated with the landing block, if any. */ + protected DropBlockSprite _blocksprite; + + /** The piece dropper used to drop pieces in the board if the puzzle + * chooses to make use of piece dropping functionality. */ + protected PieceDropper _dropper; + + /** The time at which the board rise was paused. */ + protected long _rpstamp; + + /** The time at which the last board rise began. */ + protected long _risestamp; + + /** The time at which we last fired off a board rising event. */ + protected long _risesent; + + /** The time at which we were requested to start zipping. */ + protected long _zipstamp; + + /** The duration of the bounce interval. */ + protected int _bounceInterval; + + /** The time at which we last started bouncing, or 0. */ + protected long _bounceStamp; + + /** The row at which we last bounced, or {@link Integer#MIN_VALUE}. */ + protected int _bounceRow; + + /** The timestamp used to keep track of when the drop block was + * removed so that we can fast-forward it when restored. */ + protected long _blockStamp; + + /** Whether the drop blocks are currently dropping quickly. */ + protected boolean _fastDrop; + + /** Used to evolve the board following the completion of animations. */ + protected AnimationAdapter _evolveObserver = new AnimationAdapter() { + public void animationCompleted (Animation anim, long when) { + animationDidFinish(anim); + } + }; + + /** Used to listen to drop sprites and react to their move events. */ + protected DropSpriteObserver _dropMovedHandler = new DropSpriteObserver() { + public void pieceMoved ( + DropSprite sprite, long when, int col, int row) { + handleDropSpriteMoved(sprite, when, col, row); + } + }; + + /** A piece operation that will update piece sprites as board + * positions are updated. */ + protected DropBoard.PieceOperation _updateBoardOp = + new DropBoard.PieceOperation() { + public boolean execute (DropBoard board, int col, int row) { + _dview.updatePiece(col, row); + return true; + } + }; + + /** The default board row rising velocity. */ + protected static final float DEFAULT_RISE_VELOCITY = 100f / 1000f; + + /** The default board row rising distance. */ + protected static final int DEFAULT_RISE_DISTANCE = 20; + + /** The delay in milliseconds between board rising intervals. */ + protected static final long RISE_INTERVAL = 50L; + + /** Defines the distance of a piece that we allow to bounce before we + * land it. */ + protected static final float BOUNCE_FRACTION = 0.125f; +} diff --git a/src/java/com/threerings/puzzle/drop/client/DropPanel.java b/src/java/com/threerings/puzzle/drop/client/DropPanel.java new file mode 100644 index 00000000..5b54aa6e --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/DropPanel.java @@ -0,0 +1,41 @@ +// +// $Id: DropPanel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import com.threerings.puzzle.data.BoardSummary; + +/** + * Puzzles using the drop services need implement this interface to + * display drop puzzle related information. + */ +public interface DropPanel +{ + /** + * Sets the next block to be displayed. + */ + public void setNextBlock (int[] pieces); + + /** + * Updates the board summary display for the given player. + */ + public void setSummary (int pidx, BoardSummary summary); +} diff --git a/src/java/com/threerings/puzzle/drop/client/DropSprite.java b/src/java/com/threerings/puzzle/drop/client/DropSprite.java new file mode 100644 index 00000000..a60b4eb3 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/DropSprite.java @@ -0,0 +1,584 @@ +// +// $Id: DropSprite.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Shape; + +import com.samskivert.util.ObserverList; +import com.threerings.util.DirectionUtil; + +import com.threerings.media.image.Mirage; +import com.threerings.media.sprite.Sprite; + +import com.threerings.puzzle.Log; + +/** + * The drop sprite is a sprite that displays one or more pieces falling + * toward the bottom of the board. + */ +public class DropSprite extends Sprite +{ + /** + * Constructs a drop sprite and starts it dropping. + * + * @param view the board view upon which this sprite will be displayed. + * @param col the column of the sprite. + * @param row the row of the bottom-most piece. + * @param pieces the pieces displayed by the sprite. + * @param dist the distance the sprite is to drop in rows. + */ + public DropSprite ( + DropBoardView view, int col, int row, int[] pieces, int dist) + { + this(view, col, row, pieces, dist, -1); + } + + /** + * Constructs a drop sprite and starts it dropping. + * + * @param view the board view upon which this sprite will be displayed. + * @param col the column of the sprite. + * @param row the row of the bottom-most piece. + * @param pieces the pieces displayed by the sprite. + * @param dist the distance the sprite is to drop in rows. + * @param renderOrder the render order. + */ + public DropSprite ( + DropBoardView view, int col, int row, int[] pieces, int dist, + int renderOrder) + { + _view = view; + _col = col; + _row = row; + _pieces = pieces; + _dist = (dist == 0) ? 1 : dist; + _orient = NORTH; + _unit = _view.getPieceHeight(); + setRenderOrder(renderOrder); + } + + // documentation inherited + protected void init () + { + super.init(); + + // size the bounds to fit our pieces + updateBounds(); + // set up the piece location + setBoardLocation(_row, _col); + // calculate vertical render offset based on the number of pieces + setRowOffset(-(_pieces.length - 1)); + } + + /** + * Returns the remaining number of columns to drop. + */ + public int getDistance () + { + return _dist; + } + + /** + * Returns the column the piece is located in. + */ + public int getColumn () + { + return _col; + } + + /** + * Returns the row the piece is located in. + */ + public int getRow () + { + return _row; + } + + /** + * Returns the pieces the sprite is displaying. + */ + public int[] getPieces () + { + return _pieces; + } + + /** + * Returns the velocity of this sprite. + */ + public float getVelocity () + { + return _vel; + } + + /** + * Sets the row and column the piece is located in. + */ + public void setBoardLocation (int row, int col) + { + _row = row; + _col = col; + updatePosition(); + } + + /** + * Sets the column the piece is located in. + */ + public void setColumn (int col) + { + _col = col; + updatePosition(); + } + + /** + * Set the row the piece is located in. + */ + public void setRow (int row) + { + _row = row; + updatePosition(); + } + + /** + * Sets the column offset of the sprite image. + */ + public void setColumnOffset (int count) + { + _offx = count; + updateRenderOffset(); + updateRenderOrigin(); + } + + /** + * Sets the row offset of the sprite image. + */ + public void setRowOffset (int count) + { + _offy = count; + updateRenderOffset(); + updateRenderOrigin(); + } + + /** + * Sets the pieces the sprite is displaying. + */ + public void setPieces (int[] pieces) + { + _pieces = pieces; + } + + /** + * Sets the velocity of this sprite. The time at which the current + * row was entered is modified so that the sprite position will remain + * the same when calculated using the new velocity since the piece + * sprite may have its velocity modified in the middle of a row + * traversal. + */ + public void setVelocity (float velocity) + { + // bail if we've already got the requested velocity + if (_vel == velocity) { + return; + } + + if (_rowstamp > 0) { + // get our current distance along the row + long now = _view.getTimeStamp(); + float pctdone = getPercentDone(now); + + // revise the current row entry time to account for the new velocity + float travpix = pctdone * _unit; + long msecs = (long)(travpix / velocity); + _rowstamp = now - msecs; + } + + // update the velocity + _vel = velocity; + } + + /** + * Starts the piece dropping toward the next row. + */ + public void drop () + { + // Log.info("Dropping piece [piece=" + this + "]."); + + // drop one row by default + if (_dist <= 0) { + _dist = 1; + } + + if (_stopstamp > 0) { + // we're dropping from a stand-still + long delta = _view.getTimeStamp() - _stopstamp; + _rowstamp += delta; + _stopstamp = 0; + + } else { + // we're continuing a previous drop, so make use of any + // previously existing time + _rowstamp = _endstamp; + } + } + + /** + * Returns true if this drop sprite is dropping, false if it has been + * {@link #stop}ped or has not yet been {@link #drop}ped. + */ + public boolean isDropping () + { + return (_stopstamp == 0) && (_rowstamp != 0); + } + + /** + * Stops the piece from dropping. + */ + public void stop () + { + if (_stopstamp == 0) { + _stopstamp = _view.getTimeStamp(); + // Log.info("Stopped piece [piece=" + this + "]."); + } + } + + /** + * Puts the drop sprite into (or takes it out of) bouncing + * mode. Bouncing mode is used to put the sprite into limbo after it + * lands but before we commit the landing, giving the user a last + * moment change move or rotate the piece. While the sprite is + * "bouncing" it will be rendered one pixel below it's at rest state. + */ + public void setBouncing (boolean bouncing) + { + if (_bouncing = bouncing) { + // if we've activated bouncing, shift the sprite slightly to + // illustrate its new state + shiftForBounce(); + + // to prevent funny business in the event that we were a long + // ways past the end of the row when we landed, we warp the + // sprite back to the exact point of landing for the purposes + // of the bounce and any subsequent antics + _endstamp = _rowstamp = _view.getTimeStamp(); + +// Log.info("Adjusted rowstap due to bounce " + +// "[time=" + _endstamp + "]."); + } + } + + /** + * Returns true if this sprite is bouncing. + */ + public boolean isBouncing () + { + return _bouncing; + } + + /** + * Updates the sprite's location to illustrate that it is currently in + * the "bouncing" state. + */ + protected void shiftForBounce () + { + setLocation(_ox, _srcPos.y+1); + } + + // documentation inherited + public boolean inside (Shape shape) + { + return shape.contains(_bounds); + } + + /** + * Returns a value between 0.0 and 1.0 + * representing how far the piece has moved toward the next row + * as of the given time stamp. + */ + public float getPercentDone (long timestamp) + { + // if we've never been ticked and so haven't yet initialized our + // row start timestamp, just let the caller know that we've not + // traversed our row at all + if (_rowstamp == 0) { + return 0.0f; + } + + long msecs = Math.max(0, timestamp - _rowstamp); + float travpix = msecs * _vel; + float pctdone = (travpix / _unit); + +// Log.info("getPercentDone [timestamp=" + timestamp + +// ", rowstamp=" + _rowstamp + ", msecs=" + msecs + +// ", travpix=" + travpix + ", pctdone=" + pctdone + +// ", vel=" + _vel + "]."); + + return pctdone; + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // get the column and row increment based on the sprite's orientation + int oidx = _orient/2; + int incx = ORIENT_DX[oidx]; + int incy = ORIENT_DY[oidx]; + + // determine offset from the start of each actual row and column + int dx = _ox - _srcPos.x, dy = _oy - _srcPos.y; + + int pcol = _col, prow = _row; + for (int ii = 0; ii < _pieces.length; ii++) { + // ask the board for the render position of this piece + _view.getPiecePosition(pcol, prow, _renderPos); + // draw the piece image + paintPieceImage(gfx, ii, pcol, prow, _orient, + _renderPos.x + dx, _renderPos.y + dy); + // increment the target column and row + pcol += incx; + prow += incy; + } + } + + /** + * Paints the specified piece with the supplied parameters. + */ + protected void paintPieceImage (Graphics2D gfx, int pieceidx, + int col, int row, int orient, int x, int y) + { + Mirage image = _view.getPieceImage(_pieces[pieceidx], col, row, orient); + image.paint(gfx, x, y); + } + + // documentation inherited + public void tick (long timestamp) + { + super.tick(timestamp); + + // initialize our rowstamp if we haven't done so already + if (_rowstamp == 0) { + _rowstamp = timestamp; + } + + // if we're bouncing or paused, do nothing here + if (_bouncing || _stopstamp > 0) { + return; + } + + PieceMovedOp pmop = null; + + // figure out how far along the current board coordinate we should be + float pctdone = getPercentDone(timestamp); + if (pctdone >= 1.0f) { + // note that we've reached the next row + advancePosition(); + + // update remaining drop distance + _dist--; + + // calculate any remaining time to be used + long used = (long)(_unit / _vel); + _endstamp = _rowstamp + used; + _rowstamp = _endstamp; + + // update our percent done because we've moved down a row + pctdone -= 1.0; + + // inform observers that we've reached our destination + pmop = new PieceMovedOp(this, timestamp, _col, _row); + } + + // constrain the sprite's position to the destination row + pctdone = Math.min(pctdone, 1.0f); + + // calculate the latest sprite position + int nx = _srcPos.x + (int)((_destPos.x - _srcPos.x) * pctdone); + int ny = _srcPos.y + (int)((_destPos.y - _srcPos.y) * pctdone); + +// Log.info("Drop sprite tick [dist=" + _dist + ", pctdone=" + pctdone + +// ", row=" + _row + ", col=" + _col + +// ", nx=" + nx + ", ny=" + ny + "]."); + + // only update the sprite's location if it actually moved + if (_ox != nx || _oy != ny) { + setLocation(nx, ny); + } + + // lastly notify our observers if we made it to the next row + if (pmop != null) { + _observers.apply(pmop); + } + } + + /** + * Called when the sprite has finished traversing its current row to + * advance its board coordinates to the next row. + */ + protected void advancePosition () + { + setRow(_row + 1); + // Log.info("Moved to row " + _row); + } + + // documentation inherited + public void fastForward (long timeDelta) + { + if (_rowstamp > 0) { + _rowstamp += timeDelta; + } + } + + // documentation inherited + public void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", orient=").append(DirectionUtil.toShortString(_orient)); + buf.append(", row=").append(_row); + buf.append(", col=").append(_col); + buf.append(", offx=").append(_offx); + buf.append(", offy=").append(_offy); + buf.append(", dist=").append(_dist); + } + + /** + * Updates internal pixel coordinates used when the piece is moving. + */ + protected void updatePosition () + { + _view.getPiecePosition(_col, _row, _srcPos); + _view.getPiecePosition(_col, _row+1, _destPos); + setLocation(_srcPos.x, _srcPos.y); + } + + // documentation inherited + public void setOrientation (int orient) + { + invalidate(); + super.setOrientation(orient); + updateBounds(); + invalidate(); + } + + /** + * Updates the bounds for this sprite based on the sprite display + * dimensions in the view. + */ + protected void updateBounds () + { + Dimension size = _view.getPieceSegmentSize( + _col, _row, _orient, _pieces.length); + _bounds.width = size.width; + _bounds.height = size.height; + } + + /** + * Adjusts our render origin such that our location is not in the + * upper left of the sprite's rendered image but is in fact offset by + * some number of rows and columns. + */ + protected void updateRenderOffset () + { + _oxoff = -(_view.getPieceWidth() * _offx); + _oyoff = -(_view.getPieceHeight() * _offy); + } + + /** Used to dispatch {@link DropSpriteObserver#pieceMoved}. */ + protected static class PieceMovedOp implements ObserverList.ObserverOp + { + public PieceMovedOp (DropSprite sprite, long when, int col, int row) + { + _sprite = sprite; + _when = when; + _col = col; + _row = row; + } + + public boolean apply (Object observer) + { + if (observer instanceof DropSpriteObserver) { + ((DropSpriteObserver)observer).pieceMoved( + _sprite, _when, _col, _row); + } + return true; + } + + protected DropSprite _sprite; + protected long _when; + protected int _col, _row; + } + + /** The default piece velocity. */ + protected static final float DEFAULT_VELOCITY = 30f/1000f; + + /** The time at which we started the current row. */ + protected long _rowstamp; + + /** The time at which we reached the end of the previous row. */ + protected long _endstamp; + + /** The time at which we were stopped en route to our next row. */ + protected long _stopstamp; + + /** The board view upon which this sprite is displayed. */ + protected DropBoardView _view; + + /** The unit distance the sprite moves to reach the next row. */ + protected int _unit; + + /** The screen coordinates of the top-left of the row currently + * occupied by the sprite. */ + protected Point _srcPos = new Point(); + + /** The screen coordinates of the top-left of the row toward which the + * sprite is falling. */ + protected Point _destPos = new Point(); + + /** The piece render position; used as working data when determining + * where to render each piece in the sprite. */ + protected Point _renderPos = new Point(); + + /** The number of rows remaining to drop. */ + protected int _dist; + + /** The piece velocity. */ + protected float _vel = DEFAULT_VELOCITY; + + /** The offsets in columns or rows at which the piece is rendered. */ + protected int _offx, _offy; + + /** The current piece location in the board. */ + protected int _row, _col; + + /** The pieces this sprite is displaying. */ + protected int[] _pieces; + + /** Indicates that the drop sprite is bouncing; see {@link + * #setBouncing}. */ + protected boolean _bouncing; + + // used to compute the column and row increment while rendering the + // sprite's pieces based on its orientation + // W N E S + protected static final int[] ORIENT_DX = { -1, 0, 1, 0 }; + protected static final int[] ORIENT_DY = { 0, -1, 0, 1 }; +} diff --git a/src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java b/src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java new file mode 100644 index 00000000..67814dc0 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/DropSpriteObserver.java @@ -0,0 +1,34 @@ +// +// $Id: DropSpriteObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +/** + * Provides notifications for drop puzzle specific stuff. + */ +public interface DropSpriteObserver +{ + /** + * Called when the drop sprite has moved completely to the specified + * board coordinates. + */ + public void pieceMoved (DropSprite sprite, long when, int col, int row); +} diff --git a/src/java/com/threerings/puzzle/drop/client/NextBlockView.java b/src/java/com/threerings/puzzle/drop/client/NextBlockView.java new file mode 100644 index 00000000..27205269 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/NextBlockView.java @@ -0,0 +1,108 @@ +// +// $Id: NextBlockView.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import javax.swing.JComponent; + +import com.threerings.media.image.Mirage; +import com.threerings.util.DirectionCodes; + +/** + * The next block view displays an image representing the next drop block + * to appear in the game. + */ +public class NextBlockView extends JComponent + implements DirectionCodes +{ + /** + * Constructs a next block view. + */ + public NextBlockView (DropBoardView view, int pwid, int phei, int orient) + { + // save things off + _view = view; + _pwid = pwid; + _phei = phei; + _orient = orient; + + // configure the component + setOpaque(false); + } + + /** + * Sets the pieces displayed by the view. + */ + public void setPieces (int[] pieces) + { + _pieces = pieces; + repaint(); + } + + // documentation inherited + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + // draw the pieces + Graphics2D gfx = (Graphics2D)g; + if (_pieces != null) { + Dimension size = getSize(); + int xpos = (_orient == VERTICAL) ? 0 : (size.width - _pwid); + int ypos = (_orient == VERTICAL) ? (size.height - _phei) : 0; + + for (int ii = 0; ii < _pieces.length; ii++) { + Mirage image = _view.getPieceImage(_pieces[ii]); + image.paint(gfx, xpos, ypos); + if (_orient == VERTICAL) { + ypos -= _phei; + } else { + xpos -= _pwid; + } + } + } + } + + // documentation inherited + public Dimension getPreferredSize () + { + int wid = (_orient == VERTICAL) ? _pwid : (2 * _pwid); + int hei = (_orient == VERTICAL) ? (2 * _phei) : _phei; + return new Dimension(wid, hei); + } + + /** The drop board view from which we obtain piece images. */ + protected DropBoardView _view; + + /** The pieces displayed by this view. */ + protected int[] _pieces; + + /** The piece dimensions in pixels. */ + protected int _pwid, _phei; + + /** The view orientation; one of {@link #HORIZONTAL} or {@link + * #VERTICAL}. */ + protected int _orient; +} diff --git a/src/java/com/threerings/puzzle/drop/client/PieceGroupAnimation.java b/src/java/com/threerings/puzzle/drop/client/PieceGroupAnimation.java new file mode 100644 index 00000000..3e63aa8b --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/client/PieceGroupAnimation.java @@ -0,0 +1,111 @@ +// +// $Id: PieceGroupAnimation.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.client; + +import java.awt.Graphics2D; +import java.awt.Rectangle; + +import com.threerings.media.animation.Animation; +import com.threerings.media.sprite.ImageSprite; +import com.threerings.media.sprite.PathObserver; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.util.Path; + +import com.threerings.puzzle.drop.data.DropBoard; +import com.threerings.puzzle.drop.data.DropPieceCodes; + +/** + * Animates all the pieces on a puzzle board doing some sort of global + * effect like all flying into place or out into the ether. + */ +public abstract class PieceGroupAnimation extends Animation + implements PathObserver +{ + /** + * Creates a piece group animation which must be initialized with a + * subsequent call to {@link #init}. + */ + public PieceGroupAnimation (DropBoardView view, DropBoard board) + { + super(new Rectangle(0, 0, 0, 0)); // we don't render ourselves + _view = view; + _board = board; + } + + // documentation inherited + public void tick (long tickStamp) + { + // nothing doing + } + + // documentation inherited + public void paint (Graphics2D gfx) + { + // nothing doing + } + + // documentation inherited from interface + public void pathCancelled (Sprite sprite, Path path) + { + _finished = (--_penders == 0); + } + + // documentation inherited from interface + public void pathCompleted (Sprite sprite, Path path, long when) + { + _finished = (--_penders == 0); + } + + // documentation inherited + protected void willStart (long tickStamp) + { + super.willStart(tickStamp); + + // create an image sprite for every piece on the board and set + // them on their paths + int width = _board.getWidth(), height = _board.getHeight(); + _sprites = new Sprite[width * height]; + for (int yy = 0; yy < height; yy++) { + for (int xx = 0; xx < width; xx++) { + int spos = yy*width+xx; + _sprites[spos] = _view.getPieceSprite(xx, yy); + if (_sprites[spos] != null) { + configureSprite(_sprites[spos], xx, yy); + _sprites[spos].addSpriteObserver(this); + _penders++; + } + } + } + } + + /** + * An animation must override this method to configure each sprite + * with a path, potentially a render order, and whatever other + * configurations are needed. + */ + protected abstract void configureSprite (Sprite sprite, int xx, int yy); + + protected DropBoardView _view; + protected DropBoard _board; + protected Sprite[] _sprites; + protected int _penders; +} diff --git a/src/java/com/threerings/puzzle/drop/data/DropBoard.java b/src/java/com/threerings/puzzle/drop/data/DropBoard.java new file mode 100644 index 00000000..2660cebf --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/DropBoard.java @@ -0,0 +1,887 @@ +// +// $Id: DropBoard.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.Arrays; + +import org.apache.commons.lang.StringUtils; + +import com.threerings.util.DirectionUtil; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.drop.client.DropControllerDelegate; +import com.threerings.puzzle.drop.util.DropBoardUtil; + +/** + * A class that provides for various useful logical operations to be + * enacted on a two-dimensional board and provides an easier mechanism for + * referencing pieces by position. + */ +public class DropBoard extends Board + implements DropPieceCodes +{ + /** The rotation constant for rotation around a central piece. */ + public static final int RADIAL_ROTATION = 0; + + /** The rotation constant for rotation wherein the block occupies the + * same columns when rotating. */ + public static final int INPLACE_ROTATION = 1; + + /** An operation that does naught but clear pieces, which proves to be + * generally useful. */ + public static final PieceOperation CLEAR_OP = new PieceOperation () { + public boolean execute (DropBoard board, int col, int row) { + board.setPiece(col, row, PIECE_NONE); + return true; + } + }; + + /** + * An interface to be implemented by classes that would like to apply + * some operation to each piece in a column or row segment in the + * board. + */ + public interface PieceOperation + { + /** + * Called for each piece in the board segment the operation is + * being applied to. + * + * @return true if the operation should continue to be applied if + * being applied to multiple pieces, or false if it should + * terminate after this application. + */ + public boolean execute (DropBoard board, int col, int row); + } + + /** + * Constructs an empty drop board for use when unserializing. + */ + public DropBoard () + { + this(null, 0, 0); + } + + /** + * Constructs a drop board of the given dimensions with its + * pieces initialized to PIECE_NONE. + */ + public DropBoard (int bwid, int bhei) + { + this(new int[bwid*bhei], bwid, bhei); + fill(PIECE_NONE); + } + + /** + * Constructs a drop board of the given dimensions with its + * pieces initialized to the given piece. + */ + public DropBoard (int bwid, int bhei, int piece) + { + this(new int[bwid*bhei], bwid, bhei); + fill(piece); + } + + /** + * Constructs a drop board with the given board and dimensions. + */ + public DropBoard (int[] board, int bwid, int bhei) + { + _board = board; + _bwid = bwid; + _bhei = bhei; + } + + /** + * Returns the width of the board in columns. + */ + public int getWidth() + { + return _bwid; + } + + /** + * Returns the height of the board in rows. + */ + public int getHeight() + { + return _bhei; + } + + /** + * Returns the piece at the given column and row in the board. + */ + public int getPiece (int col, int row) + { + try { + return _board[(row*_bwid) + col]; + } catch (Exception e) { + Log.warning("Failed getting piece [col=" + col + + ", row=" + row + ", error=" + e + "]."); + Log.logStackTrace(e); + return -1; + } + } + + /** + * For boards that are always filled, this method is called to obtain + * pieces to fill the board. + */ + public int getNextPiece () + { + return PIECE_NONE; + } + + /** + * Returns the distance the piece at the given column and row can drop + * until it hits a non-empty piece (defined as {@link #PIECE_NONE}). + */ + public int getDropDistance (int col, int row) + { + int dist = 0; + for (int yy = row + 1; yy < _bhei; yy++) { + if (getPiece(col, yy) != PIECE_NONE) { + return dist; + } + dist++; + } + return dist; + } + + /** + * Returns whether the given row in the board is empty. + */ + public boolean isRowEmpty (int row) + { + for (int col = 0; col < _bwid; col++) { + if (getPiece(col, row) != PIECE_NONE) { + return false; + } + } + return true; + } + + /** + * Returns whether all of the pieces at the given coordinates can be + * dropped one row. + */ + public boolean isValidDrop (int[] rows, int[] cols, float pctdone) + { + int bottom = _bhei - 1; + for (int ii = 0; ii < rows.length; ii++) { + // pieces at bottom can't be dropped + if (rows[ii] >= bottom) { + return false; + } + + // pieces with pieces below them can't be dropped + int row = rows[ii] + 1; + if (row >= 0 && getPiece(cols[ii], row) != PIECE_NONE) { + return false; + } + } + + return true; + } + + /** + * Returns true if the specified coordinate is within the bounds of + * the board, false if it is not. + */ + public boolean inBounds (int col, int row) + { + return (col >= 0 && row >= 0 && col < getWidth() && row < getHeight()); + } + + /** + * Returns whether the specified block in the board is empty. The + * block is allowed to occupy space off the top of the board as long + * as it is within the horizontal board bounds. + * + * @param col the left coordinate of the block. + * @param row the bottom coordinate of the block. + * @param wid the width of the block. + * @param hei the height of the block. + */ + public boolean isBlockEmpty (int col, int row, int wid, int hei) + { + for (int ypos = row; ypos > (row - hei); ypos--) { + for (int xpos = col; xpos < (col + wid); xpos++) { + // only allow movement off the top of the board that's + // within the horizontal screen bounds and in a column + // that's not topped out + if (ypos < 0) { + if ((xpos < 0 || xpos >= _bwid) || + (getPiece(xpos, 0) != PIECE_NONE)) { + return false; + } else { + continue; + } + } + + // don't allow movement outside the side or bottom bounds + if (xpos < 0 || + xpos >= _bwid || + ypos >= _bhei) { + return false; + } + + // make sure no piece is present + if (getPiece(xpos, ypos) != PIECE_NONE) { + return false; + } + } + } + + return true; + } + + /** + * Rotates the given block in the given direction and returns its + * final state as (orient, col, row, popped), where + * orient is the final orientation of the drop block; + * col and row are the final column and row + * coordinates, respectively, of the central drop block piece. + * popped will be set to 1 if the piece was popped up, 0 + * otherwise. + */ + public int[] getForgivingRotation ( + int[] rows, int[] cols, int orient, int dir, int rtype, float pctdone, + boolean canPopup) + { + int px = cols[0], py = rows[0]; + +// Log.info("Starting rotation [px=" + px + ", py=" + py + +// ", orient=" + orient + ", pctdone=" + pctdone + "]."); + + // try rotating the block in the given direction through all four + // possible orientations + for (int ii = 0; ii < 4; ii++) { + int oidx = orient/2; + + // adjust the position of the central piece + px += ROTATE_DX[rtype][dir][oidx]; + py += ROTATE_DY[rtype][dir][oidx]; + + // update the orientation + orient = DropBoardUtil.getRotatedOrientation(orient, dir); + oidx = orient/2; + + // because isBlockEmpty() always assumes the origin of the + // block is in the lower-left, we need to adjust the + // coordinates of the drop block's "central" piece accordingly + int ox = px + ORIENT_ORIGIN_DX[oidx]; + int oy = py + ORIENT_ORIGIN_DY[oidx]; + + // if we're less than 50 percent through with our fall, we + // want to check our current coordinates for validity; if + // we're more, we want to check the row below our current + // coordinates + if (pctdone > 0.5) { + oy += 1; + } + + // try each of three coercions: nothing, one left, one right + for (int c = 0; c < COERCE_DX.length; c++) { + int cx = COERCE_DX[c]; + // check if our hypothetical new coordinates are empty + if (isBlockEmpty(ox + cx, oy, + ORIENT_WIDTHS[oidx], ORIENT_HEIGHTS[oidx])) { +// Log.info( +// "Block is empty [ox=" + ox + ", cx=" + cx + +// ", oy=" + oy + ", oidx=" + oidx + +// ", orient=" + DirectionUtil.toShortString(orient) + +// ", owid=" + ORIENT_WIDTHS[oidx] + +// ", ohei=" + ORIENT_HEIGHTS[oidx] + "]."); + return new int[] { orient, px + cx, py, 0 }; + } + } + + // if our piece is facing south and we're using radial + // rotation then we need to try popping the piece up a row to + // check for a fit + if (canPopup && rtype == RADIAL_ROTATION && orient == SOUTH) { + // check if our hypothetical new coordinates are empty + if (isBlockEmpty(ox, oy - 1, + ORIENT_WIDTHS[oidx], ORIENT_HEIGHTS[oidx])) { +// Log.info( +// "Popped-up block is empty [ox=" + ox + +// ", oy=" + (oy - 1) + ", oidx=" + oidx + +// ", orient=" + DirectionUtil.toShortString(orient) + +// ", owid=" + ORIENT_WIDTHS[oidx] + +// ", ohei=" + ORIENT_HEIGHTS[oidx] + +// ", bhei=" + _bhei + "]."); + return new int[] { orient, px, py - 1, 1 }; + } + } + } + + // this should never happen since even in the most tightly + // constrained case where the block is entirely surrounded by + // other pieces there are always two valid orientations. + Log.warning("**** We're horked and couldn't rotate at all!"); +// System.exit(0); + return null; + } + + /** + * Returns a {@link Point} object containing the coordinates to place + * the bottom-left of the given block at after moving it the given + * distance on the x- and y-axes, or null if the move is + * not valid. Note that only the final block position is checked. + * + * @param col the leftmost column of the block. + * @param row the bottommost row of the block. + * @param wid the width of the block. + * @param hei the height of the block. + * @param dx the distance to move the block in columns. + * @param dy the distance to move the block in rows. + * @param pctdone the percentage of the inter-block distance that the + * piece has fallen thus far. + */ + public Point getForgivingMove ( + int col, int row, int wid, int hei, int dx, int dy, float pctdone) + { + // try placing the block in the desired position and, failing + // that, at the same horizontal position but one row farther down + int xpos = col + dx, ypos = row + dy; + + // if we're above the halfway mark, we check our current neighbors + // to see if we can move there; if we're below the halfway mark we + // check the next row down + if (pctdone >= 0.5) { + ypos += 1; + } + + // if the block we wish to occupy is empty, we're all good + return (isBlockEmpty(xpos, ypos, wid, hei)) ? + new Point(xpos, row + dy) : null; + } + + /** + * Populates the given array with the column levels for this board. + */ + public void getColumnLevels (byte[] columns) + { + int bwid = getWidth(), bhei = getHeight(); + for (int col = 0; col < bwid; col++) { + int dist = getDropDistance(col, -1); + columns[col] = (byte)(bhei - dist); + } + } + + /** + * Called by the {@link DropControllerDelegate} when it's time to + * apply a rising row of pieces to the board. Shifts all of the + * pieces in the given board up one row and places the given row of + * pieces at the bottom of the board. + */ + public void applyRisingPieces (int[] pieces) + { + // shift all pieces up one row + int end = _bhei - 1; + for (int yy = 0; yy < end; yy++) { + for (int xx = 0; xx < _bwid; xx++) { + setPiece(xx, yy, getPiece(xx, yy + 1)); + } + } + + // apply the row pieces to the board + int ypos = _bhei - 1; + for (int xx = 0; xx < _bwid; xx++) { + setPiece(xx, ypos, pieces[xx]); + } + } + + /** + * Returns true if the specified row (which count down, with zero at + * the top of the board) contains any pieces. + * + * @param row the row to check for pieces. + * @param blankPiece the blank piece value, non-instances of which + * will be sought. + */ + public boolean rowContainsPieces (int row, int blankPiece) + { + for (int x = 0; x < _bwid; x++) { + if (getPiece(x, row) != blankPiece) { + return true; + } + } + return false; + } + + /** + * Fills the board contents with the given piece. + */ + public void fill (int piece) + { + Arrays.fill(_board, (int)piece); + } + + /** + * Sets the piece at the given coordinates. + * + * @return true if the piece was set, false if it was invalid. + */ + public boolean setPiece (int col, int row, int piece) + { + if (col >= 0 && row >= 0 && col < _bwid && row < _bhei) { + _board[(row*_bwid) + col] = (int)piece; + return true; + + } else { + Log.warning("Attempt to set piece outside board bounds " + + "[col=" + col + ", row=" + row + ", p=" + piece + "]."); + return false; + } + } + + /** + * Sets the pieces within the specified rectangle to the given piece. + */ + public void setRect (int x, int y, int width, int height, int piece) + { + for (int yy = y; yy > (y - height); yy--) { + for (int xx = x; xx < (x + width); xx++) { + setPiece(xx, yy, piece); + } + } + } + + /** + * Sets the pieces in the given board segment to the specified piece. + * + * @param dir the direction of the segment; one of {@link #HORIZONTAL} + * or {@link #VERTICAL}. + * @param col the starting column of the segment. + * @param row the starting row of the segment. + * @param len the length of the segment in pieces. + * @param piece the piece to set in the segment. + * + * @return false if the segment was only partially applied because + * some pieces were outside the bounds of the board, true if it was + * completely applied. + */ + public boolean setSegment (int dir, int col, int row, int len, int piece) + { + _setPieceOp.init(piece); + applyOp(dir, col, row, len, _setPieceOp); + return !_setPieceOp.getError(); + } + + /** + * Sets the pieces in the given board segment to the specified pieces. + * + * @param dir the direction of the segment; one of {@link #HORIZONTAL} + * or {@link #VERTICAL}. + * @param col the starting column of the segment. + * @param row the starting row of the segment. + * @param pieces the pieces to set in the segment. + */ + public void setSegment (int dir, int col, int row, int[] pieces) + { + _setSegmentOp.init(dir, pieces); + applyOp(dir, col, row, pieces.length, _setSegmentOp); + } + + /** + * Applies a specified {@link PieceOperation} to all pieces in the + * specified row or column starting at the specified coordinates and + * spanning the remainder of the row or column (depending on the + * application direction) in the board. + * + * @param dir the direction to iterate in; one of {@link #HORIZONTAL} + * or {@link #VERTICAL}. + * @param col the starting column of the segment. + * @param row the starting row of the segment. + * @param op the piece operation to apply to each piece. + */ + public void applyOp (int dir, int col, int row, PieceOperation op) + { + int len = (dir == HORIZONTAL) ? _bwid - col : row + 1; + applyOp(dir, col, row, len, op); + } + + /** + * Applies a specified {@link PieceOperation} to all pieces in a row + * or column segment starting at the specified coordinates and of the + * specified length in the board. + * + * @param dir the direction to iterate in; one of {@link #HORIZONTAL} + * or {@link #VERTICAL}. + * @param col the starting leftmost column of the segment. + * @param row the starting bottommost row of the segment. + * @param len the number of pieces in the segment. + * @param op the piece operation to apply to each piece. + */ + public void applyOp (int dir, int col, int row, int len, PieceOperation op) + { + if (dir == HORIZONTAL) { + int end = Math.min(col + len, _bwid); + for (int ii = col; ii < end; ii++) { + if (!op.execute(this, ii, row)) { + break; + } + } + + } else { + int end = Math.max(row - len, -1); + for (int ii = row; ii > end; ii--) { + if (!op.execute(this, col, ii)) { + break; + } + } + } + } + + /** + * Applies a specified {@link PieceOperation} to the specified piece + * in the board. + * + * @param col the column of the piece. + * @param row the row of the piece. + * @param op the piece operation to apply to the piece. + */ + public void applyOp (int col, int row, PieceOperation op) + { + op.execute(this, col, row); + } + + // documentation inherited from interface + public void dump () + { + dumpAndCompare(null); + } + + // documentation inherited from interface + public void dumpAndCompare (Board other) + { + if (other != null && !(other instanceof DropBoard)) { + throw new IllegalArgumentException( + "Can't compare drop board to non-drop-board."); + } + + DropBoard dother = (DropBoard)other; + int padwid = getPadWidth(); + if (other != null) { + // padwid = (padwid * 2) + 1; + padwid *= 2; + } + + for (int y = 0; y < _bhei; y++) { + StringBuilder buf = new StringBuilder(); + for (int x = 0; x < _bwid; x++) { + int piece = getPiece(x, y); + String str = formatPiece(piece); + if (dother != null) { + int opiece = dother.getPiece(x, y); + if (opiece != piece) { + str += "|" + formatPiece(opiece); + } + } + buf.append(StringUtils.rightPad(str, padwid)); + } + System.err.println(buf.toString()); + } + } + + /** Returns a string representation of this instance. */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append("[wid=").append(_bwid); + buf.append(", hei=").append(_bhei); + return buf.append("]").toString(); + } + + // documentation inherited from interface + public boolean equals (Board other) + { + // make sure we're comparing the same class type + if (!this.getClass().getName().equals(other.getClass().getName())) { + throw new IllegalArgumentException( + "Can't compare board of different class types " + + "[src=" + this.getClass().getName() + + ", other=" + other.getClass().getName() + "]."); + } + + // we're certainly not equal if our dimensions differ + DropBoard dother = (DropBoard)other; + if (dother.getWidth() != _bwid || + dother.getHeight() != _bhei) { + return false; + } + + // check each board piece + for (int xx = 0; xx < _bwid; xx++) { + for (int yy = 0; yy < _bhei; yy++) { + if (getPiece(xx, yy) != dother.getPiece(xx, yy)) { + return false; + } + } + } + + // we're equal + return true; + } + + /** + * Returns whether the given coordinates are within the board bounds. + */ + public boolean isValidPosition (int x, int y) + { + return (x >= 0 && + y >= 0 && + x < _bwid && + y < _bhei); + } + + /** + * Returns the bounds of this board. Note that a single rectangle is + * re-used internally and so the caller should not modify the + * returned rectangle. + */ + public Rectangle getBounds () + { + if (_bounds == null) { + _bounds = new Rectangle(0, 0, _bwid, _bhei); + } + return _bounds; + } + + /** + * Returns the size of the board in pieces. + */ + public int size () + { + return (_bwid*_bhei); + } + + /** + * Copies the contents of this board directly into the supplied board, + * overwriting the destination board in its entirety. + */ + public void copyInto (DropBoard board) + { + // make sure the target board is a valid target + if (board.getWidth() != _bwid || board.getHeight() != _bhei) { + Log.warning("Can't copy board into destination board with " + + "different dimensions [src=" + this + + ", dest=" + board + "]."); + return; + } + + // copy our pieces directly into the board, avoiding any unsightly + // object allocation which is largely the point of this method, + // after all. + int[] dest = ((DropBoard)board).getBoard(); + System.arraycopy(_board, 0, dest, 0, (_bwid*_bhei)); + } + + /** + * Returns the raw board data associated with this board. One + * shouldn't fiddle about with this unless one knows what one is + * doing. + */ + public int[] getBoard () + { + return _board; + } + + /** + * Sets the board data and board dimensions. + */ + public void setBoard (int[] board, int bwid, int bhei) + { + _board = board; + _bwid = bwid; + _bhei = bhei; + } + + /** + * Sets the board pieces. + */ + public void setBoard (int[] board) + { + int size = (_bwid*_bhei); + if (board.length < size) { + Log.warning("Attempt to set board with invalid data size " + + "[len=" + board.length + ", expected=" + size + "]."); + return; + } + + _board = board; + } + + // documentation inherited + public Object clone () + { + DropBoard board = (DropBoard)super.clone(); + board._board = (int[])_board.clone(); + return board; + } + + /** + * Returns the number of characters to which a single piece should be + * padded when dumping the board for debugging purposes. + */ + protected int getPadWidth () + { + return DEFAULT_PAD_WIDTH; + } + + /** + * Returns a string representation of the given piece for use when + * dumping the board. + */ + protected String formatPiece (int piece) + { + return (piece == PIECE_NONE) ? "." : String.valueOf(piece); + } + + /** An operation that sets the pieces in a board segment to a + * specified array of pieces. */ + protected static class SetSegmentOperation implements PieceOperation + { + /** + * Sets the array of pieces to be placed in the board segment. + */ + public void init (int dir, int[] pieces) + { + _dir = dir; + _pieces = pieces; + _idx = (dir == HORIZONTAL) ? _pieces.length - 1 : 0; + } + + // documentation inherited + public boolean execute (DropBoard board, int col, int row) + { + if (_dir == HORIZONTAL) { + board.setPiece(col, row, _pieces[_idx--]); + } else { + board.setPiece(col, row, _pieces[_idx++]); + } + return true; + } + + /** The orientation in which the pieces are to be placed. */ + protected int _dir; + + /** The current piece index. */ + protected int _idx; + + /** The pieces to set in the board. */ + protected int[] _pieces; + } + + /** An operation that sets all pieces to a specified piece. */ + protected static class SetPieceOperation implements PieceOperation + { + /** + * Sets the piece to be placed in the board segment. + */ + public void init (int piece) + { + _piece = piece; + _error = false; + } + + /** + * Returns true if we attempted to set a piece outside the bounds + * of the board during the course of our operation. + */ + public boolean getError () + { + return _error; + } + + // documentation inherited + public boolean execute (DropBoard board, int col, int row) + { + if (!board.setPiece(col, row, _piece)) { + _error = true; + } + return true; + } + + /** The piece to set in the board. */ + protected int _piece; + + /** Set to true if an error occurred setting a piece. */ + protected boolean _error; + } + + /** The board data. */ + protected int[] _board; + + /** The board dimensions in pieces. */ + protected int _bwid, _bhei; + + /** The bounds of this board. */ + protected transient Rectangle _bounds; + + // used to reconfigure the block when rotating it + protected static final int[][][] ROTATE_DX = { + // W N E S W N E S + {{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }}, // RADIAL + {{ -1, 1, 0, 0 }, { -1, 0, 0, 1 }}, // INPLACE + // CCW CW + }; + + // used to reconfigure the block when rotating it + protected static final int[][][] ROTATE_DY = { + // W N E S W N E S + {{ 0, 0, 0, 0 }, { 0, 0, 0, 0 }}, // RADIAL + {{ -1, 0, 0, 1 }, { 0, 0, -1, 1 }}, // INPLACE + // CCW CW + }; + + // used to compute the bounds of the isBlockEmpty() block based on the + // drop block's orientation and "root" block position + protected static final int[] ORIENT_WIDTHS = { 2, 1, 2, 1 }; + protected static final int[] ORIENT_HEIGHTS = { 1, 2, 1, 2 }; + + // used to compute the origin of the isBlockEmpty() block based on the + // drop block's orientation and "root" block position + protected static final int[] ORIENT_ORIGIN_DX = { -1, 0, 0, 0 }; + protected static final int[] ORIENT_ORIGIN_DY = { 0, 0, 0, 1 }; + + // used to coerce the block when rotating either a space to the left + // or right (or not at all) + protected static final int[] COERCE_DX = { 0, 1, -1 }; + + /** The operation used to set the pieces in a board segment. */ + protected static final SetSegmentOperation _setSegmentOp = + new SetSegmentOperation(); + + /** The operation used to set a piece in a board segment. */ + protected static final SetPieceOperation _setPieceOp = + new SetPieceOperation(); + + /** The number of characters to which each board piece should be + * padded when outputting for debug purposes. */ + protected static final int DEFAULT_PAD_WIDTH = 3; +} diff --git a/src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java b/src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java new file mode 100644 index 00000000..97b77bc2 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/DropBoardSummary.java @@ -0,0 +1,88 @@ +// +// $Id: DropBoardSummary.java 3495 2005-04-15 21:44:36Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.BoardSummary; + +/** + * Provides a summary of a {@link DropBoard}. + */ +public class DropBoardSummary extends BoardSummary +{ + /** The row levels for each column. */ + public byte[] columns; + + /** + * Constructs an empty drop board summary for use when un-serializing. + */ + public DropBoardSummary () + { + // nothing for now + } + + /** + * Constructs a drop board summary that retrieves board information + * from the supplied board when summarizing. + */ + public DropBoardSummary (Board board) + { + super(board); + } + + /** + * Returns the column number of the column within the given column + * range that contains the most pieces. + */ + public int getHighestColumn (int startx, int endx) + { + byte value = columns[startx]; + int idx = startx; + for (int xx = startx + 1; xx <= endx; xx++) { + if (columns[xx] > value) { + value = columns[xx]; + idx = xx; + } + } + return idx; + } + + // documentation inherited + public void setBoard (Board board) + { + _dboard = (DropBoard)board; + // create the columns array + columns = new byte[_dboard.getWidth()]; + + super.setBoard(board); + } + + // documentation inherited + public void summarize () + { + // update the board column levels + _dboard.getColumnLevels(columns); + } + + /** The drop board we're summarizing. */ + protected transient DropBoard _dboard; +} diff --git a/src/java/com/threerings/puzzle/drop/data/DropCodes.java b/src/java/com/threerings/puzzle/drop/data/DropCodes.java new file mode 100644 index 00000000..0ffd0670 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/DropCodes.java @@ -0,0 +1,39 @@ +// +// $Id: DropCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +import com.threerings.puzzle.data.PuzzleGameCodes; + +/** + * Contains codes used by the drop game services. + */ +public interface DropCodes extends PuzzleGameCodes +{ + /** The message bundle identifier for drop puzzle messages. */ + public static final String DROP_MESSAGE_BUNDLE = "puzzle.drop"; + + /** The name of the control stream that provides drop pieces. */ + public static final String DROP_STREAM = "drop"; + + /** The name of the control stream that provides rise pieces. */ + public static final String RISE_STREAM = "rise"; +} diff --git a/src/java/com/threerings/puzzle/drop/data/DropConfig.java b/src/java/com/threerings/puzzle/drop/data/DropConfig.java new file mode 100644 index 00000000..43036402 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/DropConfig.java @@ -0,0 +1,35 @@ +// +// $Id: DropConfig.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +/** + * Provides access to the configuration information for a drop puzzle + * game. + */ +public interface DropConfig +{ + /** Returns the board width in pieces. */ + public int getBoardWidth (); + + /** Returns the board height in pieces. */ + public int getBoardHeight (); +} diff --git a/src/java/com/threerings/puzzle/drop/data/DropLogic.java b/src/java/com/threerings/puzzle/drop/data/DropLogic.java new file mode 100644 index 00000000..677a4690 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/DropLogic.java @@ -0,0 +1,41 @@ +// +// $Id: DropLogic.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +/** + * Describes the features and configuration desired for a given drop + * puzzle game. + */ +public interface DropLogic +{ + /** + * Returns whether the puzzle game would like to make use of the + * manipulable block dropping functionality. + */ + public boolean useBlockDropping (); + + /** + * Returns whether the puzzle game would like to make use of the + * rising board functionality. + */ + public boolean useBoardRising (); +} diff --git a/src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java b/src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java new file mode 100644 index 00000000..3ac73ff5 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/DropPieceCodes.java @@ -0,0 +1,37 @@ +// +// $Id: DropPieceCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +import com.threerings.util.DirectionCodes; + +/** + * The drop piece codes interface contains constants common to the drop + * game package. + */ +public interface DropPieceCodes extends DirectionCodes +{ + /** The piece constant denoting an empty board piece. */ + public static final byte PIECE_NONE = -1; + + /** The number of pieces in a drop block. */ + public static final int DROP_BLOCK_PIECE_COUNT = 2; +} diff --git a/src/java/com/threerings/puzzle/drop/data/SegmentInfo.java b/src/java/com/threerings/puzzle/drop/data/SegmentInfo.java new file mode 100644 index 00000000..94b019eb --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/data/SegmentInfo.java @@ -0,0 +1,60 @@ +// +// $Id: SegmentInfo.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.data; + +import com.samskivert.util.StringUtil; +import com.threerings.util.DirectionCodes; + +/** + * Describes a segment of pieces in a {@link DropBoard}. + */ +public class SegmentInfo +{ + /** The segment's direction; one of {@link DirectionCodes#HORIZONTAL} + * or {@link DirectionCodes#VERTICAL}. */ + public int dir; + + /** The segment's lower-left board coordinates. */ + public int x, y; + + /** The segment's length in pieces. */ + public int len; + + /** + * Constructs a segment info object. + */ + public SegmentInfo (int dir, int x, int y, int len) + { + this.dir = dir; + this.x = x; + this.y = y; + this.len = len; + } + + /** + * Returns a string representation of this instance. + */ + public String toString () + { + return StringUtil.fieldsToString(this); + } +} diff --git a/src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java b/src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java new file mode 100644 index 00000000..e4bfa197 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/server/DropManagerDelegate.java @@ -0,0 +1,174 @@ +// +// $Id: DropManagerDelegate.java 3777 2005-12-07 19:19:11Z mjohnson $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.server; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleCodes; +import com.threerings.puzzle.server.PuzzleManager; +import com.threerings.puzzle.server.PuzzleManagerDelegate; + +import com.threerings.puzzle.drop.data.DropBoard; +import com.threerings.puzzle.drop.data.DropCodes; +import com.threerings.puzzle.drop.data.DropConfig; +import com.threerings.puzzle.drop.data.DropLogic; +import com.threerings.puzzle.drop.util.PieceDropLogic; +import com.threerings.puzzle.drop.util.PieceDropper; + +/** + * Provides the necessary support for a puzzle game that involves a + * two-dimensional board containing pieces, with new pieces either falling + * into the board as a "drop block", or rising into the bottom of the + * board in new piece rows, groups of blocks can be "broken" and garbage + * can be sent to other players' boards as a result. This is implemented + * as a delegate so that the natural hierarchy need not be twisted to + * differentiate between puzzles that use piece dropping and those that + * don't. Because we have need to structure our hierarchy around things + * like whether a puzzle is a duty puzzle, this becomes necessary. + * + *

A puzzle game using these services will then need to extend this + * delegate, implementing the necessary methods to customize it for the + * particulars of their game and then register it with their game manager + * via {@link PuzzleManager#addDelegate}. + * + *

It also keeps track of, for each player, board level information, + * and player game status. Miscellaneous utility routines are provided + * for checking things like whether the game is over, whether a player is + * still active in the game, and so forth. + * + *

Derived classes are likely to want to override {@link + * #getPieceDropLogic}. + */ +public abstract class DropManagerDelegate extends PuzzleManagerDelegate + implements PuzzleCodes, DropCodes +{ + /** + * Provides the delegate with a reference to the manager for which it + * is delegating as well as the logic object that it uses to determine + * how to manage the drop puzzle. + */ + public DropManagerDelegate (PuzzleManager puzmgr, DropLogic logic) + { + super(puzmgr); + + // configure the game-specific settings + _usedrop = logic.useBlockDropping(); + _userise = logic.useBoardRising(); + if (_usedrop && _userise) { + Log.warning("Can't use dropping blocks and board rising "+ + "functionality simultaneously in a drop puzzle game! " + + "Falling back to straight dropping."); + _userise = false; + } + } + + // documentation inherited + public void didInit (PlaceConfig config) + { + _dconfig = (DropConfig)config; + + // save things off + _bwid = _dconfig.getBoardWidth(); + _bhei = _dconfig.getBoardHeight(); + + super.didInit(config); + } + + // documentation inherited + public void didStartup (PlaceObject plobj) + { + super.didStartup(plobj); + + // initialize the drop board array + _dboards = new DropBoard[_puzmgr.getPlayerCount()]; + + // create the piece dropper if appropriate + PieceDropLogic pdl = getPieceDropLogic(); + if (pdl != null) { + _dropper = new PieceDropper(pdl); + } + } + + // documentation inherited + public void gameWillStart () + { + super.gameWillStart(); + + // get casted references to all player drop boards + Board[] board = _puzmgr.getBoards(); + for (int ii = 0; ii < _puzmgr.getPlayerCount(); ii++) { + _dboards[ii] = (DropBoard)board[ii]; + } + } + + /** + * Drops any pieces that need dropping on the given player's board and + * returns whether any pieces were dropped. + */ + protected boolean dropPieces (DropBoard board) + { + return (_dropper.dropPieces(board, null) > 0); + } + + /** + * Returns the piece drop logic used to drop any pieces that need + * dropping in the board. + */ + protected PieceDropLogic getPieceDropLogic () + { + return null; + } + + /** + * This method should be called by derived classes whenever the player + * successfully places a drop block. + */ + protected void placedBlock (int pidx) + { + } + + /** The drop game board for each player. */ + protected DropBoard[] _dboards; + + /** The drop game config object. */ + protected DropConfig _dconfig; + + /** Whether the game is using drop block functionality. */ + protected boolean _usedrop; + + /** Whether the game is using board rising functionality. */ + protected boolean _userise; + + /** The board dimensions in pieces. */ + protected int _bwid, _bhei; + + /** The piece dropper used to drop pieces in the board if the puzzle + * chooses to make use of piece dropping functionality. */ + protected PieceDropper _dropper; + + /** Used to limit the maximum number of board update loops permitted + * before assuming something's gone horribly awry and aborting. */ + protected static final int MAX_UPDATE_LOOPS = 100; +} diff --git a/src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java b/src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java new file mode 100644 index 00000000..8dbcabfb --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/util/DropBoardUtil.java @@ -0,0 +1,62 @@ +// +// $Id: DropBoardUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.util; + +import com.threerings.util.DirectionCodes; + +public class DropBoardUtil + implements DirectionCodes +{ + /** + * Returns the orientation resulting from rotating the block in the + * given direction the specified number of times. + * + * @param orient the current orientation. + * @param dir the direction to rotate in; one of CW or + * CCW. + * @param count the number of rotations to perform. + * + * @return the rotated orientation. + */ + public static int getRotatedOrientation (int orient, int dir, int count) + { + for (int ii = 0; ii < (count % 4); ii++) { + orient = getRotatedOrientation(orient, dir); + } + return orient; + } + + /** + * Returns the orientation resulting from rotating the block in + * the given direction. + * + * @param orient the current orientation. + * @param dir the direction to rotate in; one of CW or + * CCW. + * + * @return the rotated orientation. + */ + public static int getRotatedOrientation (int orient, int dir) + { + return (orient + ((dir == CW) ? 2 : 6)) % DIRECTION_COUNT; + } +} diff --git a/src/java/com/threerings/puzzle/drop/util/DropGameUtil.java b/src/java/com/threerings/puzzle/drop/util/DropGameUtil.java new file mode 100644 index 00000000..35d17632 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/util/DropGameUtil.java @@ -0,0 +1,71 @@ +// +// $Id: DropGameUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.util; + +import java.awt.event.KeyEvent; + +import com.threerings.util.KeyTranslatorImpl; + +import com.threerings.puzzle.drop.client.DropControllerDelegate; +import com.threerings.puzzle.util.PuzzleGameUtil; + +/** + * Drop puzzle game related utilities. + */ +public class DropGameUtil +{ + /** + * Returns a key translator configured with mappings suitable for a + * drop puzzle game. + */ + public static KeyTranslatorImpl getKeyTranslator () + { + // start with the standard puzzle key mappings + KeyTranslatorImpl xlate = PuzzleGameUtil.getKeyTranslator(); + + // add all press key mappings + xlate.addPressCommand(KeyEvent.VK_LEFT, + DropControllerDelegate.MOVE_BLOCK_LEFT, + MOVE_RATE, MOVE_DELAY); + xlate.addPressCommand(KeyEvent.VK_RIGHT, + DropControllerDelegate.MOVE_BLOCK_RIGHT, + MOVE_RATE, MOVE_DELAY); + xlate.addPressCommand(KeyEvent.VK_UP, + DropControllerDelegate.ROTATE_BLOCK_CCW, 0); + xlate.addPressCommand(KeyEvent.VK_DOWN, + DropControllerDelegate.ROTATE_BLOCK_CW, 0); + xlate.addPressCommand(KeyEvent.VK_SPACE, + DropControllerDelegate.START_DROP_BLOCK, 0); + + // add all release key mappings + xlate.addReleaseCommand(KeyEvent.VK_SPACE, + DropControllerDelegate.END_DROP_BLOCK); + + return xlate; + } + + /** The move key repeat rate in moves per second. */ + protected static final int MOVE_RATE = 7; + + /** The delay in milliseconds before the move keys begin to repeat. */ + protected static final long MOVE_DELAY = 300L; +} diff --git a/src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java b/src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java new file mode 100644 index 00000000..a894c22e --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/util/PieceDestroyer.java @@ -0,0 +1,181 @@ +// +// $Id: PieceDestroyer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.util; + +import java.util.ArrayList; +import java.util.List; + +import com.threerings.puzzle.drop.data.DropBoard; +import com.threerings.puzzle.drop.data.DropBoard.PieceOperation; +import com.threerings.puzzle.drop.data.DropPieceCodes; +import com.threerings.puzzle.drop.data.SegmentInfo; + +/** + * Handles destroying contiguous piece segments in a drop board. + */ +public class PieceDestroyer + implements DropPieceCodes +{ + /** + * An interface to be implemented by specific puzzles to detail the + * parameters and methodology by which pieces are destroyed in the + * puzzle board. + */ + public interface DestroyLogic + { + /** + * Returns the minimum length of a contiguously piece segment that + * should be destroyed. + */ + public int getMinimumLength (); + + /** + * Returns whether piece a is equivalent to piece + * b for the purposes of including it in a contiguous + * piece segment to be destroyed. + */ + public boolean isEquivalent (int a, int b); + } + + /** + * Constructs a piece destroyer that destroys pieces as specified by + * the supplied destroy logic. + */ + public PieceDestroyer (DestroyLogic logic) + { + _logic = logic; + } + + /** + * Destroys all pieces in the given board that are in contiguous rows + * or columns of pieces, returning a list of {@link SegmentInfo} + * objects detailing the destroyed piece segments. Note that a single + * list is used internally to gather the segment info, and so callers + * that care to modify the list should create their own copy; also, + * the pieces in the segments may overlap, i.e., two segments may + * contain the same piece. + */ + public List destroyPieces (DropBoard board, PieceOperation destroyOp) + { + // find all horizontally-oriented destroyed segments + int bwid = board.getWidth(), bhei = board.getHeight(); + _destroyed.clear(); + int end = bwid - _logic.getMinimumLength() + 1; + for (int yy = (bhei - 1); yy >= 0; yy--) { + int xx = 0; + while (xx < end) { + xx += findSegment(board, HORIZONTAL, xx, yy); + } + } + + // find all vertically-oriented destroyed segments + end = _logic.getMinimumLength() - 2; + for (int xx = 0; xx < bwid; xx++) { + int yy = bhei - 1; + while (yy > end) { + yy -= findSegment(board, VERTICAL, xx, yy); + } + } + + // destroy the pieces + int size = _destroyed.size(); + for (int ii = 0; ii < size; ii++) { + SegmentInfo si = (SegmentInfo)_destroyed.get(ii); + board.applyOp(si.dir, si.x, si.y, si.len, destroyOp); + } + + return _destroyed; + } + + /** + * Searches for a contiguously colored piece segment with the + * specified orientation and root coordinates in the supplied board + * and returns the length of the segment traversed. + */ + protected int findSegment (DropBoard board, int dir, int x, int y) + { + _lengthOp.reset(); + board.applyOp(dir, x, y, _lengthOp); + int len = _lengthOp.getLength(); + if (len >= _logic.getMinimumLength()) { + _destroyed.add(new SegmentInfo(dir, x, y, len)); + } + return len; + } + + /** + * A piece operation that calculates the length of the contiguous + * piece segment to which it is applied. + */ + protected class SegmentLengthOperation + implements PieceOperation + { + /** + * Resets the operation for application to a new piece segment. + */ + public void reset () + { + _len = 0; + } + + /** + * Returns the length of the contiguous piece segment. + */ + public int getLength () + { + return _len; + } + + // documentation inherited + public boolean execute (DropBoard board, int col, int row) + { + int piece = board.getPiece(col, row); + if (_len == 0) { + _len = 1; + _piece = piece; + return (piece != PIECE_NONE); + + } else if (_logic.isEquivalent(piece, _piece)) { + _len++; + return true; + + } else { + return false; + } + } + + /** The root segment piece. */ + protected int _piece; + + /** The segment length in pieces. */ + protected int _len; + } + + /** The puzzle-specific destroy logic with which we do our business. */ + protected DestroyLogic _logic; + + /** The piece operation used to determine segment length. */ + protected SegmentLengthOperation _lengthOp = new SegmentLengthOperation(); + + /** The list of destroyed piece segments. */ + protected ArrayList _destroyed = new ArrayList(); +} diff --git a/src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java b/src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java new file mode 100644 index 00000000..3a80af39 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/util/PieceDropLogic.java @@ -0,0 +1,80 @@ +// +// $Id: PieceDropLogic.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.util; + +import com.threerings.util.DirectionCodes; + +import com.threerings.puzzle.drop.data.DropBoard; + +/** + * An interface to be implemented by games that would like to be able to + * drop their pieces during game play. + */ +public interface PieceDropLogic +{ + /** + * Should the board always be filled? + * + * @return false for normal behavior. + */ + public boolean boardAlwaysFilled (); + + /** + * Returns whether the given piece is potentially droppable. + */ + public boolean isDroppablePiece (int piece); + + /** + * Returns whether the given piece has constraints upon it that + * impact its droppability. + */ + public boolean isConstrainedPiece (int piece); + + /** + * Returns whether the given piece terminates a column climb when + * determining the height of a piece column to be dropped. + * + * @param allowConst whether to allow dropping constrained pieces + * (though only in the first encountered constrained block.) + * @param piece the piece to consider. + * @param pre whether the climbability check is being performed + * before the height is incremented, or after. + */ + public boolean isClimbablePiece ( + boolean allowConst, int piece, boolean pre); + + /** + * Returns the x-axis coordinate of the specified edge of the + * given constrained piece. + * + *

TODO: This should go away once the sword and sail games + * have standardized on WEST/EAST or BLOCK_LEFT/BLOCK_RIGHT to + * reference block edges. + * + * @param board the board to search. + * @param col the column of the constrained piece. + * @param row the row of the constrained piece. + * @param dir the edge direction to find; one of {@link + * DirectionCodes#LEFT} or {@link DirectionCodes#RIGHT}. + */ + public int getConstrainedEdge (DropBoard board, int col, int row, int dir); +} diff --git a/src/java/com/threerings/puzzle/drop/util/PieceDropper.java b/src/java/com/threerings/puzzle/drop/util/PieceDropper.java new file mode 100644 index 00000000..5d008a55 --- /dev/null +++ b/src/java/com/threerings/puzzle/drop/util/PieceDropper.java @@ -0,0 +1,203 @@ +// +// $Id: PieceDropper.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.drop.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.samskivert.util.StringUtil; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.drop.data.DropBoard; +import com.threerings.puzzle.drop.data.DropPieceCodes; + +/** + * Handles dropping pieces in a board. + */ +public class PieceDropper + implements DropPieceCodes +{ + /** + * A class to hold information detailing the pieces to be dropped + * in a particular column. + */ + public static class PieceDropInfo + { + /** The starting row of the bottom piece being dropped. */ + public int row; + + /** The column number. */ + public int col; + + /** The distance to drop the pieces. */ + public int dist; + + /** The pieces to be dropped. */ + public int[] pieces; + + /** + * Constructs a piece drop info object. + */ + public PieceDropInfo (int col, int row, int dist) + { + this.col = col; + this.row = row; + this.dist = dist; + } + + /** Returns a string representation of this instance. */ + public String toString () + { + return StringUtil.fieldsToString(this); + } + } + + /** + * Called to inform a drop observer that a piece has been dropped. + */ + public static interface DropObserver + { + /** Indicates that the specified piece was dropped. */ + public void pieceDropped (int piece, int sx, int sy, int dx, int dy); + } + + /** + * Constructs a piece dropper that uses the supplied piece drop logic + * to specialise itself for a particular puzzle. + */ + public PieceDropper (PieceDropLogic logic) + { + _logic = logic; + } + + /** + * Effects any drops possible on the supplied board (modifying the + * board in the progress) and notifying the supplied drop observer of + * those drops. + * + * @return the number of pieces dropped. + */ + public int dropPieces (DropBoard board, DropObserver drobs) + { + int dropped = 0, bhei = board.getHeight(), bwid = board.getWidth(); + for (int yy = bhei - 1; yy >= 0; yy--) { + for (int xx = 0; xx < bwid; xx++) { + dropped += dropPieces(board, xx, yy, drobs); + } + } + + // if the board wants pieces to be dropped in to fill the gaps, do + // that now + if (_logic.boardAlwaysFilled()) { + for (int xx = 0; xx < bwid; xx++) { + int dist = board.getDropDistance(xx, -1); + for (int ii = 0; ii < dist; ii++) { + int yy = (-1 - ii); + int piece = board.getNextPiece(); + if (piece != PIECE_NONE) { + drop(board, piece, xx, yy, yy + dist, drobs); + dropped++; + } + } + } + } + + return dropped; + } + + /** + * Computes and effects the drop for the specified piece and any + * associated attached pieces. The supplied observer is notified of + * all drops. + */ + protected int dropPieces ( + DropBoard board, int xx, int yy, DropObserver drobs) + { + // skip empty or fixed pieces + int piece = board.getPiece(xx, yy); + if (!_logic.isDroppablePiece(piece)) { + return 0; + } + + int dropped = 0; + if (_logic.isConstrainedPiece(piece)) { + // find out where this constrained block starts and ends + int start = _logic.getConstrainedEdge(board, xx, yy, LEFT); + int end = _logic.getConstrainedEdge(board, xx, yy, RIGHT); + int bwid = board.getWidth(); + if (start < 0 || end >= bwid) { + Log.warning("Board reported bogus constrained edge " + + "[x=" + xx + ", y=" + yy + + ", start=" + start + ", end=" + end + "]."); + board.dump(); + start = Math.max(start, 0); + end = Math.min(end, bwid); + } + + // get the smallest drop distance across all of the block columns + int dist = board.getHeight() - 1; + for (int xpos = start; xpos <= end; xpos++) { + dist = Math.min(dist, board.getDropDistance(xpos, yy)); + } + if (dist == 0) { + return 0; + } + + // scoot along the bottom edge of the block, noting the drop + // for each column + for (int xpos = start; xpos <= end; xpos++) { + piece = board.getPiece(xpos, yy); + drop(board, piece, xpos, yy, yy + dist, drobs); + dropped++; + } + + } else { + // get the distance to drop the pieces + int dist = board.getDropDistance(xx, yy); + if (dist == 0) { + return 0; + } + drop(board, piece, xx, yy, yy + dist, drobs); + dropped++; + } + + return dropped; + } + + /** Helpy helper function. */ + protected final void drop (DropBoard board, int piece, + int xx, int yy, int ty, DropObserver drobs) + { + // don't try to clear things out if we're filling in from off-board + if (yy >= 0) { + board.setPiece(xx, yy, PIECE_NONE); + } + board.setPiece(xx, ty, piece); + if (drobs != null) { + drobs.pieceDropped(piece, xx, yy, xx, ty); + } + } + + /** Allows puzzle-specific customizations. */ + protected PieceDropLogic _logic; +} diff --git a/src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java b/src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java new file mode 100644 index 00000000..d44a6a31 --- /dev/null +++ b/src/java/com/threerings/puzzle/server/PuzzleGameDispatcher.java @@ -0,0 +1,78 @@ +// +// $Id: PuzzleGameDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.server; + +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import com.threerings.puzzle.client.PuzzleGameService; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.PuzzleGameMarshaller; + +/** + * Dispatches requests to the {@link PuzzleGameProvider}. + */ +public class PuzzleGameDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public PuzzleGameDispatcher (PuzzleGameProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new PuzzleGameMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case PuzzleGameMarshaller.UPDATE_PROGRESS: + ((PuzzleGameProvider)provider).updateProgress( + source, + ((Integer)args[0]).intValue(), (int[])args[1] + ); + return; + + case PuzzleGameMarshaller.UPDATE_PROGRESS_SYNC: + ((PuzzleGameProvider)provider).updateProgressSync( + source, + ((Integer)args[0]).intValue(), (int[])args[1], (Board[])args[2] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/puzzle/server/PuzzleGameProvider.java b/src/java/com/threerings/puzzle/server/PuzzleGameProvider.java new file mode 100644 index 00000000..bb25da0e --- /dev/null +++ b/src/java/com/threerings/puzzle/server/PuzzleGameProvider.java @@ -0,0 +1,45 @@ +// +// $Id: PuzzleGameProvider.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.server; + +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; +import com.threerings.puzzle.client.PuzzleGameService; +import com.threerings.puzzle.data.Board; + +/** + * Defines the server-side of the {@link PuzzleGameService}. + */ +public interface PuzzleGameProvider extends InvocationProvider +{ + /** + * Handles a {@link PuzzleGameService#updateProgress} request. + */ + public void updateProgress (ClientObject caller, int arg1, int[] arg2); + + /** + * Handles a {@link PuzzleGameService#updateProgressSync} request. + */ + public void updateProgressSync (ClientObject caller, int arg1, int[] arg2, Board[] arg3); +} diff --git a/src/java/com/threerings/puzzle/server/PuzzleManager.java b/src/java/com/threerings/puzzle/server/PuzzleManager.java new file mode 100644 index 00000000..7e7a9531 --- /dev/null +++ b/src/java/com/threerings/puzzle/server/PuzzleManager.java @@ -0,0 +1,632 @@ +// +// $Id: PuzzleManager.java 4188 2006-06-13 18:03:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.server; + +import java.util.Arrays; + +import com.samskivert.util.IntListUtil; +import com.samskivert.util.Interval; +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.OidList; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.server.CrowdServer; + +import com.threerings.parlor.game.data.GameObject; +import com.threerings.parlor.game.server.GameManager; + +import com.threerings.util.MessageBundle; +import com.threerings.util.Name; + +import com.threerings.puzzle.Log; +import com.threerings.puzzle.data.Board; +import com.threerings.puzzle.data.BoardSummary; +import com.threerings.puzzle.data.PuzzleCodes; +import com.threerings.puzzle.data.PuzzleConfig; +import com.threerings.puzzle.data.PuzzleGameMarshaller; +import com.threerings.puzzle.data.PuzzleObject; + +/** + * Extends the {@link GameManager} with facilities for the puzzle games + * that are used in Yohoho. Only features generic to all of our games are + * in this base class and additional features are supported both through + * the inheritance hierarchy and through delegating helpers (because Java + * conveniently doesn't support multiple inheritance). + */ +public abstract class PuzzleManager extends GameManager + implements PuzzleCodes, PuzzleGameProvider +{ + /** + * Returns the boards for all players. + */ + public Board[] getBoards () + { + return _boards; + } + + /** + * Returns the board summary for the given player index. + */ + public BoardSummary getBoardSummary (int pidx) + { + return (_puzobj == null || _puzobj.summaries == null) ? null : + _puzobj.summaries[pidx]; + } + + /** + * Returns whether this puzzle cares to make use of per-player board + * summaries that are sent periodically to all users in the puzzle via + * {@link #sendStatusUpdate}. The default implementation returns + * false. + */ + public boolean needsBoardSummaries () + { + return false; + } + + /** + * Returns whether this puzzle compares board states before it applies + * progress events, or after. The default implementation returns + * true. + */ + protected boolean compareBeforeApply () + { + return true; + } + + /** + * Handles the server and client states being out of sync when in + * debug mode. The default implementation halts the server. + */ + protected void handleBoardNotEqual () + { + // bail out so that we know something's royally borked + System.exit(0); + } + + /** + * Calls {@link BoardSummary#summarize} on the given player's board + * summary to refresh the summary information in preparation for + * sending along to the client(s). + * + * @param pidx the player index of the player whose board is to be + * summarized. + */ + public void updateBoardSummary (int pidx) + { + if (_puzobj.summaries != null && _puzobj.summaries[pidx] != null) { + _puzobj.summaries[pidx].summarize(); + } + } + + /** + * Applies updateBoardSummary on all the players' boards. AI board + * summaries should be updated by the AI logic. + */ + public void updateBoardSummaries () + { + if (_puzobj.summaries != null) { + for (int ii = 0; ii < _puzobj.summaries.length; ii++) { + if (!isAI(ii) || summarizeAIBoard()) { + updateBoardSummary(ii); + } + } + } + } + + // documentation inherited + protected void playerGameDidEnd (int pidx) + { + super.playerGameDidEnd(pidx); + + updateSummaryOnDeath(pidx); + } + + /** + * Updates the board summary for a player who has been eliminated and + * performs an update to communicate this change. + */ + protected void updateSummaryOnDeath (int pidx) + { + if (!isAI(pidx)) { + // update the board summary with the player's final board + updateBoardSummary(pidx); + } + + // force a status update + updateStatus(); + } + + /** + * Override to have board summaries for AIs automatically generated. + */ + protected boolean summarizeAIBoard () + { + return false; + } + + // documentation inherited + protected Class getPlaceObjectClass () + { + return PuzzleObject.class; + } + + // documentation inherited + protected void didInit () + { + super.didInit(); + + // save off a casted reference to our puzzle config + _puzconfig = (PuzzleConfig)_config; + } + + // documentation inherited + protected void didStartup () + { + super.didStartup(); + + // grab the puzzle object + _puzobj = (PuzzleObject)_gameobj; + + // create and fill in our game service object + PuzzleGameMarshaller service = (PuzzleGameMarshaller) + _invmgr.registerDispatcher(new PuzzleGameDispatcher(this), false); + _puzobj.setPuzzleGameService(service); + } + + // documentation inherited + protected void gameWillStart () + { + int size = getPlayerSlots(); + if (_boards == null) { + // create our arrays + _boards = new Board[size]; + _lastProgress = new long[size]; + } else { + Arrays.fill(_boards, null); + } + + // start everyone out with reasonable last progress stamps + Arrays.fill(_lastProgress, System.currentTimeMillis()); + + // compute the starting difficulty (this has to happen before we + // set the seed because that triggers the generation of the boards + // on the client) + _puzobj.setDifficulty(computeDifficulty()); + + // initialize the seed that goes out with this round + _puzobj.setSeed(RandomUtil.rand.nextLong()); + + // initialize the player boards + initBoards(); + + // let the game manager start up its business + super.gameWillStart(); + + // send along an initial status update before we start up the + // status update interval + sendStatusUpdate(); + + long statusInterval = getStatusInterval(); + if (_statusInterval == null && statusInterval > 0) { + // register the status update interval to address subsequent + // periodic updates + _statusInterval = new Interval(CrowdServer.omgr) { + public void expired () { + sendStatusUpdate(); + } + }; + _statusInterval.schedule(statusInterval, true); + } + } + + /** + * Returns the frequency with which puzzle status updates are + * broadcast to the players (which is accomplished via a call to + * {@link #sendStatusUpdate} which in turn calls {@link #updateStatus} + * wherein derived classes can participate in the status update). + * Returning O (the default) indicates that a periodic + * status update is not desired. + */ + protected long getStatusInterval () + { + return 0L; + } + + /** + * When a puzzle game starts, the manager is given the opportunity to + * configure the puzzle difficulty based on information known about + * the player. Additionally, when the game resets due to the player + * clearing the board, etc. this will be called again, so the + * difficulty can be ramped up as the player progresses. In situations + * where ratings and experience are tracked, the difficulty can be + * seeded based on the players prior performance. + */ + protected int computeDifficulty () + { + return DEFAULT_DIFFICULTY; + } + + // documentation inherited + protected void gameDidStart () + { + super.gameDidStart(); + + // log the AI skill levels for games involving AIs as it's useful + // when tuning AI algorithms + if (_AIs != null) { + Log.info("AIs on the job [game=" + _puzobj.which() + + ", skillz=" + StringUtil.toString(_AIs) + "]."); + } + } + + /** + * Updates (in one puzzle object transaction) all periodically updated + * status information. + */ + protected void sendStatusUpdate () + { + _puzobj.startTransaction(); + try { + // Log.info("Updating status [game=" + _puzobj.which() + "]."); + updateStatus(); + } finally { + _puzobj.commitTransaction(); + } + } + + /** + * A puzzle periodically (default of once every 5 seconds but + * configurable by puzzle) updates status information that is visible + * to the user. Derived classes can override this method and effect + * their updates by generating events on the puzzle object and they + * will be packaged into the update transaction. + */ + protected void updateStatus () + { + // if we're a board summary updating kind of puzzle, do that + if (needsBoardSummaries()) { + // generate the latest summaries + updateBoardSummaries(); + // then broadcast them to the clients + _puzobj.setSummaries(_puzobj.summaries); + } + } + + /** + * Send a system message with the puzzle bundle. + */ + protected void systemMessage (String msg) + { + systemMessage(msg, false); + } + + /** + * Send a system message with the puzzle bundle. + * + * @param waitForStart if true, the message will not be sent until the + * game has started. + */ + protected void systemMessage (String msg, boolean waitForStart) + { + systemMessage(PUZZLE_MESSAGE_BUNDLE, msg, waitForStart); + } + + /** + * Creates and initializes boards and board summaries (if desired per + * {@link #needsBoardSummaries}) for each player. + */ + protected void initBoards () + { + long seed = _puzobj.seed; + BoardSummary[] summaries = needsBoardSummaries() ? + new BoardSummary[getPlayerSlots()] : null; + + // set up game information for each player + for (int ii = 0, nn = getPlayerSlots(); ii < nn; ii++) { + boolean needsPlayerBoard = needsPlayerBoard(ii); + if (needsPlayerBoard) { + // create the game board + _boards[ii] = newBoard(ii); + _boards[ii].initializeSeed(seed); + if (summaries != null) { + summaries[ii] = newBoardSummary(_boards[ii]); + } + } + } + + _puzobj.setSummaries(summaries); + } + + /** + * Returns whether this puzzle needs a board for the given player + * index. The default implementation only creates boards for occupied + * player slots. Derived classes may wish to override this method if + * they have specialized board needs, e.g., they need only a single + * board for all players. + */ + protected boolean needsPlayerBoard (int pidx) + { + return (_puzobj.isOccupiedPlayer(pidx)); + } + + // documentation inherited + protected void gameDidEnd () + { + if (_statusInterval != null) { + // remove the client update interval + _statusInterval.cancel(); + _statusInterval = null; + } + + // send along one final status update + sendStatusUpdate(); + + super.gameDidEnd(); + } + + // documentation inherited + protected void didShutdown () + { + super.didShutdown(); + + // make sure our update interval is unregistered + if (_statusInterval != null) { + // remove the client update interval + _statusInterval.cancel(); + _statusInterval = null; + } + + // clear out our service registration + _invmgr.clearDispatcher(_puzobj.puzzleGameService); + } + + /** + * Applies progress updates received from the client. If puzzle + * debugging is enabled, this also compares the client board dumps + * provided along with each puzzle event. + */ + protected void applyProgressEvents (int pidx, int[] gevents, Board[] states) + { + int size = gevents.length; + boolean before = compareBeforeApply(); + + for (int ii = 0, pos = 0; ii < size; ii++) { + int gevent = gevents[ii]; + Board cboard = (states == null) ? null : states[ii]; + + // if we have state syncing enabled, make sure the board is + // correct before applying the event + if (before && (cboard != null)) { + compareBoards(pidx, cboard, gevent, before); + } + + // apply the event to the player's board + if (!applyProgressEvent(pidx, gevent, cboard)) { + Log.warning("Unknown event [puzzle=" + where() + + ", pidx=" + pidx + ", event=" + gevent + "]."); + } + + // maybe we are comparing boards afterwards + if (!before && (cboard != null)) { + compareBoards(pidx, cboard, gevent, before); + } + } + } + + /** + * Compare our server board to the specified sent-back user board. + */ + protected void compareBoards (int pidx, Board boardstate, + int gevent, boolean before) + { + if (DEBUG_PUZZLE) { + Log.info((before ? "About to apply " : "Just applied ") + + "[game=" + _puzobj.which() + ", pidx=" + pidx + + ", event=" + gevent + "]."); + } + if (boardstate == null) { + if (DEBUG_PUZZLE) { + Log.info("No board state provided. Can't compare."); + } + return; + } + boolean equal = _boards[pidx].equals(boardstate); + if (!equal) { + Log.warning("Client and server board states not equal! " + + "[game=" + _puzobj.which() + + ", type=" + _puzobj.getClass().getName() + "]."); + } + if (DEBUG_PUZZLE) { + // if we're debugging, dump the board state every time + // we're about to apply an event + _boards[pidx].dumpAndCompare(boardstate); + } + if (!equal) { + if (DEBUG_PUZZLE) { + handleBoardNotEqual(); + } else { + // dump the board state since we're not debugging and + // didn't just do it above + _boards[pidx].dumpAndCompare(boardstate); + } + } + } + + /** + * Called by {@link #updateProgress} to give the server a chance to + * apply each game event received from the client to the respective + * player's server-side board and, someday, confirm their validity. + * Derived classes that make use of the progress updating + * functionality should be sure to override this method to perform + * their game-specific event application antics. They should first + * perform a call to super() to see if the event is handled there. + * + * @param pidx the player index that submitted the progress event. + * @param gevent the progress event itself. + * @param cboard a snapshot of the board on the client iff the client has + * board syncing enabled (which is only enabled when debugging). + * + * @return true to indicate that the event was handled. + */ + protected boolean applyProgressEvent (int pidx, int gevent, Board cboard) + { + return false; + } + + /** + * Overrides the game manager implementation to mark all active + * players as winners. Derived classes may wish to override this + * method in order to customize the winning conditions. + */ + protected void assignWinners (boolean[] winners) + { + for (int ii = 0; ii < winners.length; ii++) { + winners[ii] = _puzobj.isActivePlayer(ii); + } + } + + /** + * Creates and returns a new starting board for the given player. + */ + protected abstract Board newBoard (int pidx); + + /** + * Creates and returns a new board summary for the given board. + * Puzzles that do not make use of board summaries should implement + * this method and return null. + */ + protected abstract BoardSummary newBoardSummary (Board board); + + // documentation inherited from interface PuzzleGameProvider + public void updateProgress (ClientObject caller, int roundId, int[] events) + { + updateProgressSync(caller, roundId, events, null); + } + + /** + * Called when the puzzle manager receives a progress update. It + * checks to make sure that the progress update is valid and the + * puzzle is still in play and then applies the updates via {@link + * #applyProgressEvents}. + */ + public void updateProgressSync ( + ClientObject caller, int roundId, int[] events, Board[] states) + { + // determine the caller's player index in the game + int pidx = IntListUtil.indexOf(_playerOids, caller.getOid()); + if (pidx == -1) { + Log.warning("Received progress update for non-player?! " + + "[game=" + _puzobj.which() + ", who=" + caller.who() + + ", ploids=" + StringUtil.toString(_playerOids) + "]."); + return; + } + + // bail if the progress update isn't for the current round + if (roundId != _puzobj.roundId) { + // only warn if this isn't a straggling update from the + // previous round + if (roundId != _puzobj.roundId-1) { + Log.warning("Received progress update for invalid round, " + + "not applying [game=" + _puzobj.which() + + ", invalidRoundId=" + roundId + + ", roundId=" + _puzobj.roundId + "]."); + } + return; + } + + // if the game is over, we wing straggling updates + if (!_puzobj.isInPlay()) { + Log.debug("Ignoring straggling events " + + "[game=" + _puzobj.which() + + ", user=" + getPlayerName(pidx) + + ", events=" + StringUtil.toString(events) + "]."); + return; + } + +// Log.info("Handling progress events [game=" + _puzobj.which() + +// ", pidx=" + pidx + ", roundId=" + roundId + +// ", count=" + events.length + "]."); + + // note that we received a progress update from this player + _lastProgress[pidx] = System.currentTimeMillis(); + + // apply the progress events to the player's puzzle state + applyProgressEvents(pidx, events, states); + } + + // documentation inherited + protected void tick (long tickStamp) + { + super.tick(tickStamp); + + // every five seconds, we call the inactivity checking code + if (_puzobj != null && _puzobj.isInPlay() && checkForInactivity()) { + int pcount = getPlayerSlots(); + for (int ii = 0; ii < pcount && _puzobj.isInPlay(); ii++) { + if (!isAI(ii)) { + checkPlayerActivity(tickStamp, ii); + } + } + } + } + + /** + * Returns whether {@link #checkPlayerActivity} should be called + * periodically while the game is in play to make sure players are + * still active. + */ + protected boolean checkForInactivity () + { + return false; + } + + /** + * Called periodically for each human player to give puzzles a chance + * to make sure all such players are engaging in reasonable levels of + * activity. The default implementation does naught. + */ + protected void checkPlayerActivity (long tickStamp, int pidx) + { + // nothing for now + } + + /** A casted reference to our puzzle config object. */ + protected PuzzleConfig _puzconfig; + + /** A casted reference to our puzzle game object. */ + protected PuzzleObject _puzobj; + + /** The player boards. */ + protected Board[] _boards; + + /** The client update interval. */ + protected Interval _statusInterval; + + /** Used to track the last time we received a progress event from each + * player in this puzzle. */ + protected long[] _lastProgress; +} diff --git a/src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java b/src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java new file mode 100644 index 00000000..f40e157e --- /dev/null +++ b/src/java/com/threerings/puzzle/server/PuzzleManagerDelegate.java @@ -0,0 +1,42 @@ +// +// $Id: PuzzleManagerDelegate.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.server; + +import com.threerings.parlor.game.server.GameManagerDelegate; + +/** + * Extends the {@link GameManagerDelegate} mechanism with puzzle manager + * specific methods (of which there are currently none). + */ +public class PuzzleManagerDelegate extends GameManagerDelegate +{ + /** + * Constructs a puzzle manager delegate. + */ + public PuzzleManagerDelegate (PuzzleManager puzmgr) + { + super(puzmgr); + _puzmgr = puzmgr; + } + + protected PuzzleManager _puzmgr; +} diff --git a/src/java/com/threerings/puzzle/util/PointSet.java b/src/java/com/threerings/puzzle/util/PointSet.java new file mode 100644 index 00000000..70f0679a --- /dev/null +++ b/src/java/com/threerings/puzzle/util/PointSet.java @@ -0,0 +1,245 @@ +// +// $Id: PointSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.util; + +import java.awt.Point; +import java.util.Iterator; + +import com.threerings.puzzle.Log; + +/** + * The point set class provides an efficient implementation of a set + * containing two-dimensional point values as (x, y). + */ +public class PointSet +{ + /** + * Creates a point set that can contain points within the given range + * of values. + * + * @param rangeX the maximum x-axis range. + * @param rangeY the maximum y-axis range. + */ + public PointSet (int rangeX, int rangeY) + { + _rangeX = rangeX; + _rangeY = rangeY; + _points = new boolean[rangeX][rangeY]; + } + + /** + * Adds a point to the set and returns whether the point was already + * present in the set. + * + * @param x the point x-coordinate. + * @param y the point y-coordinate. + * + * @return true if the point was already present, false if not. + */ + public boolean add (int x, int y) + { + boolean present = _points[x][y]; + _points[x][y] = true; + if (!present) { + _count++; + } + return present; + } + + /** + * Adds all points in the given set to this set. + * + * @param set the set containing points to add. + */ + public void addAll (PointSet set) + { + Iterator iter = set.iterator(); + Point pt; + while ((pt = (Point)iter.next()) != null) { + add(pt.x, pt.y); + } + } + + /** + * Clears all points from this set. + */ + public void clear () + { + if (_count == 0) { + // no need to clear anything + return; + } + + for (int xx = 0; xx < _rangeX; xx++) { + for (int yy = 0; yy < _rangeY; yy++) { + _points[xx][yy] = false; + } + } + _count = 0; + } + + /** + * Returns whether this set contains the given point. + * + * @param x the point x-coordinate. + * @param y the point y-coordinate. + * + * @return true if the set contains the point, false if not. + */ + public boolean contains (int x, int y) + { + return (_points[x][y]); + } + + /** + * Returns whether this set is empty. + * + * @return true if the set is empty, false if not. + */ + public boolean isEmpty () + { + return (_count == 0); + } + + /** + * Returns an iterator that iterates over the points in this set, + * returning them as {@link Point} objects. Note that the iterator + * uses a single point object internally, and so callers should create + * their own copy of the point if they plan to do something fancy with + * it. + * + * @return the iterator over the set's points. + */ + public Iterator iterator () + { + return new PointIterator(); + } + + /** + * Removes the given point from the set and returns whether the point + * was present in the set. + * + * @param x the point x-coordinate. + * @param y the point y-coordinate. + * + * @return true if the point was present, false if not. + */ + public boolean remove (int x, int y) + { + boolean present = _points[x][y]; + _points[x][y] = false; + if (present) { + _count--; + } + return present; + } + + /** + * Returns the number of points in the set. + * + * @return the number of points. + */ + public int size () + { + return _count; + } + + /** + * Returns a string representation of the point set. + */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append("["); + Iterator iter = iterator(); + Point val; + while ((val = (Point)iter.next()) != null) { + buf.append("(").append(val.x); + buf.append(",").append(val.y); + buf.append(")"); + + if (iter.hasNext()) { + buf.append(", "); + } + } + return buf.append("]").toString(); + } + + protected class PointIterator implements Iterator + { + public boolean hasNext () + { + return (_curCount < _count); + } + + public Object next () + { + if (_curCount == _count) { + return null; + } + + while (!_points[_curX][_curY]) { + advance(); + } + + _curCount++; + _point.setLocation(_curX, _curY); + + if (_curCount < _count) { + advance(); + } + + return _point; + } + + public void remove () + { + throw new UnsupportedOperationException(); + } + + protected void advance () + { + if ((++_curX) >= _rangeX) { + _curX = 0; + _curY++; + } + + if (_curY >= _rangeY) { + Log.warning("Advanced past point range."); + _curY = 0; + } + } + + protected int _curCount = 0; + protected int _curX = 0, _curY = 0; + protected Point _point = new Point(); + } + + /** The dimensions of the point array. */ + protected int _rangeX, _rangeY; + + /** The points in the set. */ + protected boolean _points[][]; + + /** The number of points in the set. */ + protected int _count; +} diff --git a/src/java/com/threerings/puzzle/util/PuzzleContext.java b/src/java/com/threerings/puzzle/util/PuzzleContext.java new file mode 100644 index 00000000..3f02f32d --- /dev/null +++ b/src/java/com/threerings/puzzle/util/PuzzleContext.java @@ -0,0 +1,62 @@ +// +// $Id: PuzzleContext.java 3524 2005-04-25 21:08:52Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.util; + +import com.threerings.util.KeyDispatcher; +import com.threerings.util.KeyboardManager; +import com.threerings.util.MessageManager; +import com.threerings.util.Name; + +import com.threerings.media.FrameManager; + +import com.threerings.parlor.util.ParlorContext; + +/** + * Provides access to entities needed by the puzzle services. + */ +public interface PuzzleContext extends ParlorContext +{ + /** + * Returns the username of the local user. + */ + public Name getUsername (); + + /** + * Returns a reference to the message manager used by the client. + */ + public MessageManager getMessageManager (); + + /** + * Provides access to the frame manager. + */ + public FrameManager getFrameManager (); + + /** + * Provides access to the keyboard manager. + */ + public KeyboardManager getKeyboardManager (); + + /** + * Provides access to the key dispatcher. + */ + public KeyDispatcher getKeyDispatcher (); +} diff --git a/src/java/com/threerings/puzzle/util/PuzzleGameUtil.java b/src/java/com/threerings/puzzle/util/PuzzleGameUtil.java new file mode 100644 index 00000000..699ae208 --- /dev/null +++ b/src/java/com/threerings/puzzle/util/PuzzleGameUtil.java @@ -0,0 +1,51 @@ +// +// $Id: PuzzleGameUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.puzzle.util; + +import java.awt.event.KeyEvent; + +import com.threerings.util.KeyTranslatorImpl; +import com.threerings.puzzle.client.PuzzleController; +import com.threerings.puzzle.client.PuzzlePanel; + +/** + * Puzzle game related utilities. + */ +public class PuzzleGameUtil +{ + /** + * Returns a key translator configured with basic puzzle game + * mappings. + */ + public static KeyTranslatorImpl getKeyTranslator () + { + KeyTranslatorImpl xlate = new KeyTranslatorImpl(); + + if (!PuzzlePanel.isRobotTesting()) { + // add the standard pause keys + xlate.addPressCommand( + KeyEvent.VK_P, PuzzleController.TOGGLE_CHATTING); + } + + return xlate; + } +} diff --git a/src/java/com/threerings/stage/Log.java b/src/java/com/threerings/stage/Log.java new file mode 100644 index 00000000..81a155a0 --- /dev/null +++ b/src/java/com/threerings/stage/Log.java @@ -0,0 +1,38 @@ +// +// $Id: Log.java 49 2001-08-09 00:32:53Z mdb $ + +package com.threerings.stage; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("stage"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/stage/client/SceneColorizer.java b/src/java/com/threerings/stage/client/SceneColorizer.java new file mode 100644 index 00000000..c5223386 --- /dev/null +++ b/src/java/com/threerings/stage/client/SceneColorizer.java @@ -0,0 +1,130 @@ +// +// $Id: SceneColorizer.java 17027 2004-09-10 00:10:46Z ray $ + +package com.threerings.stage.client; + +import java.util.HashMap; +import java.util.Iterator; + +import com.threerings.media.image.ColorPository; +import com.threerings.media.image.Colorization; +import com.threerings.media.tile.TileSet; + +import com.threerings.miso.data.ObjectInfo; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageScene; + +/** + * Handles colorization of object tiles in a scene. + */ +public class SceneColorizer implements TileSet.Colorizer +{ + /** + * Creates a scene colorizer for the supplied scene. + */ + public SceneColorizer (ColorPository cpos, StageScene scene) + { + _cpos = cpos; + _scene = scene; + + // enumerate the color ids for all possible colorization classes + for (Iterator iter = _cpos.enumerateClasses(); iter.hasNext(); ) { + String cname = ((ColorPository.ClassRecord)iter.next()).name; + _cids.put(cname, _cpos.enumerateColorIds(cname)); + } + } + + /** + * Set an auxiliary colorizer that overrides our colorizations. + */ + public void setAuxiliary (TileSet.Colorizer aux) + { + _aux = aux; + } + + /** + * Obtains a colorizer for the supplied scene object. + */ + public TileSet.Colorizer getColorizer (final ObjectInfo oinfo) + { + // if the object has no custom colorizations, return the default + // colorizer + if (oinfo.zations == 0) { + return this; + } + + // otherwise create a custom colorizer that returns this object's + // custom colorization assignments + return new TileSet.Colorizer() { + public Colorization getColorization (int index, String zation) { + int colorId = 0; + switch (index) { + case 0: colorId = oinfo.getPrimaryZation(); break; + case 1: colorId = oinfo.getSecondaryZation(); break; + } + if (colorId == 0) { + return SceneColorizer.this.getColorization(index, zation); + } else { + return _cpos.getColorization(zation, colorId); + } + } + }; + } + + // documentation inherited from interface TileSet.Colorizer + public Colorization getColorization (int index, String zation) + { + // This method is called when an object in the scene has no colorization + // of its own defined for a particular color class. + if (_aux != null) { + Colorization c = _aux.getColorization(index, zation); + if (c != null) { + return c; + } + } + return _cpos.getColorization(zation, getColorId(zation)); + } + + /** + * Get the colorId to use for the specified colorization. + */ + public int getColorId (String zation) + { + // 1. We see if the scene contains a default color we should use. + ColorPository.ClassRecord rec = _cpos.getClassRecord(zation); + int colorId = _scene.getDefaultColor(rec.classId); + if (colorId == -1) { + // 2. If the scene does not contain a color, see if a default + // is defined for that color class. + ColorPository.ColorRecord def = rec.getDefault(); + if (def != null) { + return def.colorId; + } + + // 3. If there are no defaults whatsoever, just hash on the sceneId. + int[] cids = (int[])_cids.get(zation); + if (cids == null) { + Log.warning("Zoiks, have no colorizations for '" + + zation + "'."); + return -1; + } else { + colorId = cids[_scene.getZoneId() % cids.length]; + } + } + return colorId; + } + + /** An auxiliary colorizer which may temporarily return + * non-standard colorizations. */ + protected TileSet.Colorizer _aux; + + /** The entity from which we obtain colorization info. */ + protected ColorPository _cpos; + + /** The scene for which we're providing zations. */ + protected StageScene _scene; + + /** Contains our colorization class information. */ + protected HashMap _cids = new HashMap(); +} diff --git a/src/java/com/threerings/stage/client/StageSceneController.java b/src/java/com/threerings/stage/client/StageSceneController.java new file mode 100644 index 00000000..227be915 --- /dev/null +++ b/src/java/com/threerings/stage/client/StageSceneController.java @@ -0,0 +1,57 @@ +// +// $Id: WorldSceneController.java 9625 2003-06-11 04:17:18Z mdb $ + +package com.threerings.stage.client; + +import com.samskivert.util.Tuple; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.spot.client.SpotSceneController; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.util.StageContext; + +/** + * Extends the {@link SpotSceneController} with functionality specific to + * displaying Stage scenes. + */ +public class StageSceneController extends SpotSceneController +{ + /** + * Called when the user clicks on a location within the scene. + */ + public void handleLocationClicked (Object source, StageLocation loc) + { + Log.warning("handleLocationClicked(" + source + ", " + loc + ")"); + } + + /** + * Handles a cluster clicked event. + * + * @param tuple a Tuple containing (Cluster, Point) with the Cluster + * that was clicked and the Point being the screen coords of the click. + */ + public void handleClusterClicked (Object source, Tuple tuple) + { + Log.warning("handleClusterClicked(" + source + ", " + tuple + ")"); + } + + // documentation inherited + protected PlaceView createPlaceView (CrowdContext ctx) + { + return new StageScenePanel((StageContext)ctx, this); + } + + // documentation inherited + protected void sceneUpdated (SceneUpdate update) + { + super.sceneUpdated(update); + + // let the scene panel know to rethink everything + ((StageScenePanel)_view).sceneUpdated(update); + } +} diff --git a/src/java/com/threerings/stage/client/StageScenePanel.java b/src/java/com/threerings/stage/client/StageScenePanel.java new file mode 100644 index 00000000..3f4b1d4e --- /dev/null +++ b/src/java/com/threerings/stage/client/StageScenePanel.java @@ -0,0 +1,659 @@ +// +// $Id: WorldScenePanel.java 18366 2004-12-15 22:56:58Z ray $ + +package com.threerings.stage.client; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.geom.Ellipse2D; + +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.ControllerProvider; +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.swing.util.SwingUtil; + +import com.samskivert.util.Tuple; +import com.threerings.util.StreamableArrayList; + +import com.threerings.media.tile.ObjectTile; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.UniformTileSet; + +import com.threerings.miso.client.MisoScenePanel; +import com.threerings.miso.client.SceneObject; +import com.threerings.miso.client.SceneObjectTip; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.util.MisoUtil; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.whirled.data.SceneUpdate; + +import com.threerings.whirled.spot.data.Cluster; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SceneLocation; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageMisoSceneModel; +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneModel; +import com.threerings.stage.util.StageContext; +import com.threerings.stage.util.StageSceneUtil; + +/** + * Extends the basic Miso scene panel with Stage fun stuff like portals, + * clusters and locations. + */ +public class StageScenePanel extends MisoScenePanel + implements ControllerProvider, KeyListener, PlaceView +{ + /** An action command generated when the user clicks on a location + * within the scene. */ + public static final String LOCATION_CLICKED = "LocationClicked"; + + /** An action command generated when a cluster is clicked. */ + public static final String CLUSTER_CLICKED = "ClusterClicked"; + + /** Show flag that indicates we should show all clusters. */ + public static final int SHOW_CLUSTERS = (1 << 1); + + /** Show flag that indicates we should render known land plots + * (expensive, don't turn this on willy nilly). */ + public static final int SHOW_PLOTS = (1 << 2); + + /** + * Constructs a stage scene view panel. + */ + public StageScenePanel (StageContext ctx, Controller ctrl) + { + super(ctx, StageSceneUtil.getMetrics()); + + // keep these around for later + _ctx = ctx; + _ctrl = ctrl; + _ctrl.setControlledPanel(this); + + // no layout manager + setLayout(null); + } + + /** + * Get the tileset colorizer in use in this scene. + */ + public SceneColorizer getColorizer () + { + return _rizer; + } + + // documentation inherited + protected TileSet.Colorizer getColorizer (ObjectInfo oinfo) + { + return _rizer.getColorizer(oinfo); + } + + /** + * Returns the scene being displayed by this panel. Do not modify it. + */ + public StageScene getScene () + { + return _scene; + } + + /** + * Sets the scene managed by the panel. + */ + public void setScene (StageScene scene) + { + _scene = scene; + if (_scene != null) { + _rizer = new SceneColorizer(_ctx.getColorPository(), scene); + recomputePortals(); + setSceneModel(StageMisoSceneModel.getSceneModel( + scene.getSceneModel())); + } else { + Log.warning("Zoiks! We can't display a null scene!"); + // TODO: display something to the user letting them know that + // we're so hosed that we don't even know what time it is + } + } + + /** + * Called when we have received a scene update from the server. + */ + public void sceneUpdated (SceneUpdate update) + { + // recompute the portals as those may well have changed + recomputePortals(); + + // we go aheand and completely replace our scene model which will + // reload the whole good goddamned business; it is a little + // shocking to the user, but it's guaranteed to work + refreshScene(); + } + + /** + * Computes a set of display objects for the portals in this scene. + */ + protected void recomputePortals () + { + // create scene objects for our portals + UniformTileSet ots = loadPortalTileSet(); + + _portobjs.clear(); + for (Iterator iter = _scene.getPortals(); iter.hasNext(); ) { + Portal portal = (Portal) iter.next(); + StageLocation loc = (StageLocation) portal.loc; + Point p = getScreenCoords(loc.x, loc.y); + int tx = MisoUtil.fullToTile(loc.x); + int ty = MisoUtil.fullToTile(loc.y); + Point ts = MisoUtil.tileToScreen(_metrics, tx, ty, new Point()); + +// Log.info("Added portal " + portal + +// " [screen=" + StringUtil.toString(p) + +// ", tile=" + StringUtil.coordsToString(tx, ty) + +// ", tscreen=" + StringUtil.toString(ts) + "]."); + + ObjectInfo info = new ObjectInfo(0, tx, ty); + info.action = "portal:" + portal.portalId; + + // TODO: cache me + ObjectTile tile = new PortalObjectTile( + ts.x + _metrics.tilehwid - p.x + (PORTAL_ICON_WIDTH / 2), + ts.y + _metrics.tilehei - p.y + (PORTAL_ICON_HEIGHT / 2)); + tile.setImage(ots.getTileMirage(loc.orient)); + + _portobjs.add(new SceneObject(this, info, tile) { + public boolean setHovered (boolean hovered) { + ((PortalObjectTile)this.tile).hovered = hovered; + return isResponsive(); + } + }); + } + } + + // documentation inherited + protected void recomputeVisible () + { + super.recomputeVisible(); + + // add our visible portal objects to the list of visible objects + for (int ii = 0, ll = _portobjs.size(); ii < ll; ii++) { + SceneObject pobj = (SceneObject)_portobjs.get(ii); + if (pobj.bounds != null && _vbounds.intersects(pobj.bounds)) { + _vizobjs.add(pobj); + } + } + } + + // documentation inherited from interface ControllerProvider + public Controller getController () + { + return _ctrl; + } + + // documentation inherited from interface KeyListener + public void keyPressed (KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ALT) { + // display all tooltips + setShowFlags(SHOW_TIPS, true); + } + } + + // documentation inherited from interface KeyListener + public void keyReleased (KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ALT) { + // stop displaying all tooltips + setShowFlags(SHOW_TIPS, defaultShowTips()); + } + } + + // documentation inherited from interface PlaceView + public void willEnterPlace (PlaceObject plobj) + { + } + + // documentation inherited from interface PlaceView + public void didLeavePlace (PlaceObject plobj) + { + } + + /** + * Returns true if we should always show the object tooltips by + * default, false if they should only be shown while the 'Alt' key is + * depressed. + */ + protected boolean defaultShowTips () + { + return false; + } + + // documentation inherited + public void keyTyped (KeyEvent e) + { + // nothing + } + + // documentation inherited + protected boolean handleMousePressed (Object hobject, MouseEvent event) + { + // let our parent have a crack at the old mouse press + if (super.handleMousePressed(hobject, event)) { + return true; + } + + // if the hover object is a cluster, we clicked it! + if (event.getButton() == MouseEvent.BUTTON1) { + if (hobject instanceof Cluster) { + Object actarg = new Tuple(hobject, event.getPoint()); + Controller.postAction(this, CLUSTER_CLICKED, actarg); + } else { + // post an action indicating that we've clicked on a location + Point lc = MisoUtil.screenToFull( + _metrics, event.getX(), event.getY(), new Point()); + Controller.postAction(this, LOCATION_CLICKED, + new StageLocation(lc.x, lc.y, (byte)0)); + } + return true; + } + return false; + } + + /** + * Called when our show flags have changed. + */ + protected void showFlagsDidChange (int oldflags) + { + super.showFlagsDidChange(oldflags); + + if ((oldflags & SHOW_CLUSTERS) != (_showFlags & SHOW_CLUSTERS)) { + // dirty every cluster rectangle + Iterator iter = _clusters.values().iterator(); + while (iter.hasNext()) { + dirtyCluster((Shape)iter.next()); + } + } + } + + /** + * Called when a real cluster is created or updated in the scene. + */ + protected void clusterUpdated (Cluster cluster) + { + // compute a screen rectangle that contains all possible "spots" + // in this cluster + ArrayList spots = StageSceneUtil.getClusterLocs(cluster); + Rectangle cbounds = null; + for (int ii = 0, ll = spots.size(); ii < ll; ii++) { + StageLocation loc = ((StageLocation) spots.get(ii).loc); + Point sp = getScreenCoords(loc.x, loc.y); + if (cbounds == null) { + cbounds = new Rectangle(sp.x, sp.y, 0, 0); + } else { + cbounds.add(sp.x, sp.y); + } + } + + if (cbounds == null) { + // if we found no one actually in this cluster, nix it + removeCluster(cluster.clusterOid); + } else { + // otherwise have the view update the cluster + updateCluster(cluster, cbounds); + } + } + + /** + * Adds or updates the specified cluster in the view. Metrics will be + * created that allow the cluster to be rendered and hovered over. + * + * @param cluster the cluster record to be added. + * @param bounds the screen coordinates that bound the occupants of + * the cluster. + */ + public void updateCluster (Cluster cluster, Rectangle bounds) + { + // dirty any old bounds + dirtyCluster(cluster); + + // compute the screen coordinate bounds of this cluster + Shape shape = new Ellipse2D.Float( + bounds.x, bounds.y, bounds.width, bounds.height); + _clusters.put(cluster, shape); + + // if the mouse is inside these bounds, we highlight this cluster + Shape mshape = new Ellipse2D.Float( + bounds.x-CLUSTER_SLOP, bounds.y-CLUSTER_SLOP, + bounds.width+2*CLUSTER_SLOP, bounds.height+2*CLUSTER_SLOP); + _clusterWells.put(cluster, mshape); + + // dirty our new bounds + dirtyCluster(shape); + } + + /** + * Removes the specified cluster from the view. + * + * @return true if such a cluster existed and was removed. + */ + public boolean removeCluster (int clusterOid) + { + Cluster key = new Cluster(); + key.clusterOid = clusterOid; + _clusterWells.remove(key); + Shape shape = (Shape)_clusters.remove(key); + if (shape == null) { + return false; + } + + dirtyCluster(shape); + // clear out the hover object if this cluster was it + if (_hobject instanceof Cluster && + ((Cluster)_hobject).clusterOid == clusterOid) { + _hobject = null; + } + return true; + } + + /** + * A place for subclasses to react to the hover object changing. + * One of the supplied arguments may be null. + */ + protected void hoverObjectChanged (Object oldHover, Object newHover) + { + super.hoverObjectChanged(oldHover, newHover); + + if (oldHover instanceof Cluster) { + dirtyCluster((Cluster)oldHover); + } + if (newHover instanceof Cluster) { + dirtyCluster((Cluster)newHover); + } + } + + /** + * Gives derived classes a chance to compute a hover object that takes + * precedence over sprites and actionable objects. If this method + * returns non-null, no sprite or object hover calculations will be + * performed and the object returned will become the new hover object. + */ + protected Object computeOverHover (int mx, int my) + { + return null; + } + + /** + * Gives derived classes a chance to compute a hover object that is + * used if the mouse is not hovering over a sprite or actionable + * object. If this method is called, it means that there are no + * sprites or objects under the mouse. Thus if it returns non-null, + * the object returned will become the new hover object. + */ + protected Object computeUnderHover (int mx, int my) + { + if (!isResponsive()) { + return null; + } + + // if the current hover object is a cluster, see if we're still in + // that cluster + if (_hobject instanceof Cluster) { + Cluster cluster = (Cluster)_hobject; + if (containsPoint(cluster, mx, my)) { + return cluster; + } + } + + // otherwise, check to see if the mouse is in some new cluster + Iterator iter = _clusters.keySet().iterator(); + while (iter.hasNext()) { + Cluster cclust = (Cluster)iter.next(); + if (containsPoint(cclust, mx, my)) { + return cclust; + } + } + + return null; + } + + /** + * Returns true if the specified cluster contains the supplied screen + * coordinate. + */ + protected boolean containsPoint (Cluster cluster, int mx, int my) + { + Shape shape = (Shape)_clusterWells.get(cluster); + return (shape == null) ? false : shape.contains(mx, my); + } + + /** + * Dirties the supplied cluster. + */ + protected void dirtyCluster (Cluster cluster) + { + if (cluster != null) { + dirtyCluster((Shape)_clusters.get(cluster)); + } + } + + /** + * Dirties the supplied cluster rectangle. + */ + protected void dirtyCluster (Shape shape) + { + if (shape != null) { + Rectangle r = shape.getBounds(); + _remgr.invalidateRegion( + r.x - (CLUSTER_PAD / 2), + r.y - (CLUSTER_PAD / 2), + r.width + (CLUSTER_PAD * 3 / 2), + r.height + (CLUSTER_PAD * 3 / 2)); + } + } + + /** + * Returns the portal at the specified full coordinates or null if no + * portal exists at said coordinates. + */ + public Portal getPortal (int fullX, int fullY) + { + Iterator iter = _scene.getPortals(); + while (iter.hasNext()) { + Portal portal = (Portal)iter.next(); + StageLocation loc = (StageLocation) portal.loc; + if (loc.x == fullX && loc.y == fullY) { + return portal; + } + } + return null; + } + + // documentation inherited + protected void paintBaseDecorations (Graphics2D gfx, Rectangle clip) + { + super.paintBaseDecorations(gfx, clip); + + paintClusters(gfx, clip); + } + + /** + * Paints any visible clusters. + */ + protected void paintClusters (Graphics2D gfx, Rectangle clip) + { + // remember how daddy's things were arranged + Object oalias = SwingUtil.activateAntiAliasing(gfx); + Composite ocomp = gfx.getComposite(); + Stroke ostroke = gfx.getStroke(); + + // get ready to draw clusters + gfx.setStroke(CLUSTER_STROKE); + gfx.setColor(CLUSTER_COLOR); + + if (checkShowFlag(SHOW_CLUSTERS) + /* || // _alwaysShowClusters.getValue() */) { + // draw all clusters + Iterator iter = _clusters.keySet().iterator(); + while (iter.hasNext()) { + drawCluster(gfx, clip, (Cluster)iter.next()); + } + + } else if (_hobject instanceof Cluster) { + // or just draw the active cluster + drawCluster(gfx, clip, (Cluster)_hobject); + } + + // put back daddy's things + gfx.setComposite(ocomp); + gfx.setStroke(ostroke); + SwingUtil.restoreAntiAliasing(gfx, oalias); + } + + /** + * Draw the cluster specified by the rectangle. + */ + protected void drawCluster (Graphics2D gfx, Rectangle clip, Cluster cluster) + { + Shape shape = (Shape)_clusters.get(cluster); + if ((shape != null) && shape.intersects(clip)) { + if (_hobject == cluster) { + gfx.setComposite(HIGHLIGHT_ALPHA); + } else { + gfx.setComposite(SHOWN_ALPHA); + } + gfx.draw(shape); + } + } + + /** + * Returns true if the specified location is associated with a portal. + */ + protected boolean isPortal (Location loc) + { + if (_scene == null) { + return false; + } + Iterator iter = _scene.getPortals(); + while (iter.hasNext()) { + Portal p = (Portal)iter.next(); + if (loc.equals(p.loc)) { + return true; + } + } + return false; + } + + /** + * Loads up the tileset used to render the portal arrows. + */ + protected UniformTileSet loadPortalTileSet () + { + return _ctx.getTileManager().loadTileSet( + "media/stage/portal_arrows.png", + PORTAL_ICON_WIDTH, PORTAL_ICON_HEIGHT); + } + + /** Used to render portals as objects in a scene. */ + protected class PortalObjectTile extends ObjectTile + { + public boolean hovered = false; + + public PortalObjectTile (int ox, int oy) + { + setOrigin(ox, oy); + } + + public void paint (Graphics2D gfx, int x, int y) + { + Composite ocomp = gfx.getComposite(); + if (!isResponsive() || !hovered) { + gfx.setComposite(INACTIVE_PORTAL_ALPHA); + } + super.paint(gfx, x, y); + gfx.setComposite(ocomp); + } + } + + /** A reference to our client context. */ + protected StageContext _ctx; + + /** The controller with which we work in tandem. */ + protected Controller _ctrl; + + /** Our currently displayed scene. */ + protected StageScene _scene; + + /** Contains scene objects for our portals. */ + protected ArrayList _portobjs = new ArrayList(); + + /** Shapes describing the clusters, indexed by cluster. */ + protected HashMap _clusters = new HashMap(); + + /** Shapes describing the clusters, indexed by cluster. */ + protected HashMap _clusterWells = new HashMap(); + + /** Handles scene object colorization. */ + protected SceneColorizer _rizer; + +// /** A debug hook that toggles always-on rendering of clusters. */ +// protected static RuntimeAdjust.BooleanAdjust _alwaysShowClusters = +// new RuntimeAdjust.BooleanAdjust( +// "Causes all clusters to always be rendered.", +// "yohoho.miso.always_show_clusters", +// ClientPrefs.config, false); + + /** The width of the portal icons. */ + protected static final int PORTAL_ICON_WIDTH = 48; + + /** The height of the portal icons. */ + protected static final int PORTAL_ICON_HEIGHT = 48; + + /** The distance within which the mouse must be from a location + * in order to highlight it. */ + protected static final int MAX_LOCATION_DIST = 25; + + /** The amount the stroke a cluster. */ + protected static final int CLUSTER_PAD = 4; + + /** The width with which to draw the cluster. */ + protected static final Stroke CLUSTER_STROKE = new BasicStroke(CLUSTER_PAD); + + /** The color used to render clusters. */ + protected static final Color CLUSTER_COLOR = Color.ORANGE; + + /** Alpha level used to hightlight locations or clusters. */ + protected static final Composite HIGHLIGHT_ALPHA = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f); + + /** Alpha level used to render clusters when they're not selected. */ + protected static final Composite SHOWN_ALPHA = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.15f); + + /** The alpha with which to render inactive portals. */ + protected static final Composite INACTIVE_PORTAL_ALPHA = HIGHLIGHT_ALPHA; + + /** The number of pixels outside a cluster when we assume the mouse is + * "over" that cluster. */ + protected static final int CLUSTER_SLOP = 25; +} diff --git a/src/java/com/threerings/stage/client/StageSceneService.java b/src/java/com/threerings/stage/client/StageSceneService.java new file mode 100644 index 00000000..8516d086 --- /dev/null +++ b/src/java/com/threerings/stage/client/StageSceneService.java @@ -0,0 +1,27 @@ +// +// $Id: WorldSceneService.java 14426 2004-03-12 12:12:32Z mdb $ + +package com.threerings.stage.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.miso.data.ObjectInfo; + +/** + * Provides services relating to Stage scenes. + */ +public interface StageSceneService extends InvocationService +{ + /** + * Requests to add the supplied object to the current scene. + */ + public void addObject (Client client, ObjectInfo info, + ConfirmListener listener); + + /** + * Requests to remove the supplied objects from the current scene. + */ + public void removeObjects (Client client, ObjectInfo[] info, + ConfirmListener listener); +} diff --git a/src/java/com/threerings/stage/data/DefaultColorUpdate.java b/src/java/com/threerings/stage/data/DefaultColorUpdate.java new file mode 100644 index 00000000..81bd91b9 --- /dev/null +++ b/src/java/com/threerings/stage/data/DefaultColorUpdate.java @@ -0,0 +1,39 @@ +// +// $Id: DefaultColorUpdate.java 16551 2004-07-27 20:53:28Z ray $ + +package com.threerings.stage.data; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; + +/** + * Update to change the default colorization for objects in a scene which + * do not define their own colorization. + */ +public class DefaultColorUpdate extends SceneUpdate +{ + /** The class id of the colorization we're changing. */ + public int classId; + + /** The color id to set as the new default, or -1 to remove the default. */ + public int colorId; + + /** + * Initializes this update. + */ + public void init (int targetId, int targetVersion, int classId, int colorId) + { + init(targetId, targetVersion); + this.classId = classId; + this.colorId = colorId; + } + + // documentation inherited + public void apply (SceneModel model) + { + super.apply(model); + + StageSceneModel smodel = (StageSceneModel)model; + smodel.setDefaultColor(classId, colorId); + } +} diff --git a/src/java/com/threerings/stage/data/ModifyObjectsUpdate.java b/src/java/com/threerings/stage/data/ModifyObjectsUpdate.java new file mode 100644 index 00000000..861a414e --- /dev/null +++ b/src/java/com/threerings/stage/data/ModifyObjectsUpdate.java @@ -0,0 +1,60 @@ +// +// $Id: AddObjectUpdate.java 15953 2004-06-11 23:40:47Z ray $ + +package com.threerings.stage.data; + +import com.threerings.miso.data.ObjectInfo; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; + +/** + * A scene update that is broadcast when objects have been added to or removed + * from the scene. + */ +public class ModifyObjectsUpdate extends SceneUpdate +{ + /** The objects added to the scene (or null for none). */ + public ObjectInfo[] added; + + /** The objects removed from the scene (or null for none). */ + public ObjectInfo[] removed; + + /** + * Initializes this update with all necessary data. + * + * @param added the objects added to the scene, or null for + * none + * @param removed the objects removed from the scene, or null + * for none + */ + public void init (int targetId, int targetVersion, ObjectInfo[] added, + ObjectInfo[] removed) + { + init(targetId, targetVersion); + this.added = added; + this.removed = removed; + } + + // documentation inherited + public void apply (SceneModel model) + { + super.apply(model); + + StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(model); + + // wipe out the objects that need to go + if (removed != null) { + for (int ii = 0; ii < removed.length; ii++) { + mmodel.removeObject(removed[ii]); + } + } + + // add the new objects + if (added != null) { + for (int ii = 0; ii < added.length; ii++) { + mmodel.addObject(added[ii]); + } + } + } +} diff --git a/src/java/com/threerings/stage/data/StageCodes.java b/src/java/com/threerings/stage/data/StageCodes.java new file mode 100644 index 00000000..cb1320fb --- /dev/null +++ b/src/java/com/threerings/stage/data/StageCodes.java @@ -0,0 +1,56 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.data; + +import com.threerings.util.MessageBundle; + +import com.threerings.presents.data.InvocationCodes; + +import com.threerings.crowd.data.BodyObject; + +/** + * Codes and constants relating to the Stage system. + */ +public interface StageCodes extends InvocationCodes +{ + /** The i18n bundle identifier for the Stage system. */ + public static final String STAGE_MESSAGE_BUNDLE = "stage.general"; + + /** The resource set that contains our tileset bundles. */ + public static final String TILESET_RSRC_SET = "tilesets"; + + /** The access control identifier for scene modification privileges. + * See {@link BodyObject#checkAccess}. */ + public static final String MODIFY_SCENE_ACCESS = "stage.modify_scene"; + + /** The access control identifier for potentially damaging scene + * modification privileges. See {@link BodyObject#checkAccess}. */ + public static final String MUTILATE_SCENE_ACCESS = "stage.mutilate_scene"; + + /** An error delivered when adding objects to scenes. */ + public static final String ERR_NO_OVERLAP = MessageBundle.qualify( + STAGE_MESSAGE_BUNDLE, "m.addobj_no_overlap"); + + /** An error delivered when adding objects to scenes. */ + public static final String ERR_CANNOT_CLUSTER = MessageBundle.qualify( + STAGE_MESSAGE_BUNDLE, "m.cannot_cluster"); +} diff --git a/src/java/com/threerings/stage/data/StageLocation.java b/src/java/com/threerings/stage/data/StageLocation.java new file mode 100644 index 00000000..5c7c6d52 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageLocation.java @@ -0,0 +1,133 @@ +// +// $Id: Location.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.data; + +import com.threerings.io.SimpleStreamableObject; + +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.whirled.spot.data.Location; + +/** + * Contains information on a scene occupant's position and orientation. + */ +public class StageLocation extends SimpleStreamableObject + implements Location +{ + /** The user's x position (interpreted by the display system). */ + public int x; + + /** The user's y position (interpreted by the display system). */ + public int y; + + /** The user's orientation (defined by {@link DirectionCodes}). */ + public byte orient; + + /** {@link #toString} helper function. */ + public String orientToString () + { + return DirectionUtil.toShortString(orient); + } + + /** + * A zero-argument constructor used when unserializing instances. + */ + public StageLocation () + { + } + + /** + * Constructs a location with the specified coordinates and + * orientation. + */ + public StageLocation (int x, int y, byte orient) + { + this.x = x; + this.y = y; + this.orient = orient; + + if (orient == -1) { + Thread.dumpStack(); + } + } + + // documentation inherited from interface Location + public Location getOpposite () + { + StageLocation opp = (StageLocation) clone(); + opp.orient = (byte) DirectionUtil.getOpposite(orient); + return opp; + } + + /** + * Location equivalence means that the coordinates and orientation are + * the same. + */ + public boolean equivalent (Location oloc) + { + return equals(oloc) && (orient == ((StageLocation) oloc).orient); + } + + /** + * Location equality is determined by coordinates. + */ + public boolean equals (Object other) + { + // TEMP + if (other instanceof com.threerings.whirled.spot.data.SceneLocation) { + com.threerings.stage.Log.warning( + "Illegal compare of SceneLocation and Location!!!"); + Thread.dumpStack(); // this will help us find logic errors, + // as a SceneLocation and a Location shouldn't be compared.. + } + // END: temp + + if (other instanceof StageLocation) { + StageLocation that = (StageLocation) other; + return (this.x == that.x) && (this.y == that.y); + + } else { + return false; + } + } + + /** + * Computes a reasonable hashcode for location instances. + */ + public int hashCode () + { + return x ^ y; + } + + /** + * Creates a clone of this instance. + */ + public Object clone () + { + try { + return (StageLocation)super.clone(); + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException("StageLocation.clone() failed " + cnse); + } + } +} diff --git a/src/java/com/threerings/stage/data/StageMisoSceneModel.java b/src/java/com/threerings/stage/data/StageMisoSceneModel.java new file mode 100644 index 00000000..f6ea348e --- /dev/null +++ b/src/java/com/threerings/stage/data/StageMisoSceneModel.java @@ -0,0 +1,64 @@ +// +// $Id: YoMisoSceneModel.java 9680 2003-06-13 02:16:00Z mdb $ + +package com.threerings.stage.data; + +import com.threerings.miso.data.SparseMisoSceneModel; + +import com.threerings.whirled.data.AuxModel; +import com.threerings.whirled.data.SceneModel; + +/** + * Extends the {@link SparseMisoSceneModel} with the necessary interface + * to wire it up to the Whirled auxiliary model system. + */ +public class StageMisoSceneModel extends SparseMisoSceneModel + implements AuxModel +{ + /** The width (in tiles) of a scene section. */ + public static final int SECTION_WIDTH = 9; + + /** The height (in tiles) of a scene section. */ + public static final int SECTION_HEIGHT = 9; + + /** + * Creates a completely uninitialized scene model. + */ + public StageMisoSceneModel () + { + super(SECTION_WIDTH, SECTION_HEIGHT); + } + + /** + * Locates and returns the {@link StageMisoSceneModel} among the + * auxiliary scene models associated with the supplied scene model. + * null is returned if no miso scene model could be + * found. + */ + public static StageMisoSceneModel getSceneModel (SceneModel model) + { + for (int ii = 0; ii < model.auxModels.length; ii++) { + if (model.auxModels[ii] instanceof StageMisoSceneModel) { + return (StageMisoSceneModel)model.auxModels[ii]; + } + } + return null; + } + + /** + * Returns the section key for the specified tile coordinate. + */ + public int getSectionKey (int x, int y) + { + return key(x, y); + } + + /** + * Returns the section identified by the specified key, or null if no + * section exists for that key. + */ + public Section getSection (int key) + { + return (Section)_sections.get(key); + } +} diff --git a/src/java/com/threerings/stage/data/StageOccupantInfo.java b/src/java/com/threerings/stage/data/StageOccupantInfo.java new file mode 100644 index 00000000..c2c5d986 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageOccupantInfo.java @@ -0,0 +1,52 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.data; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.OccupantInfo; + +/** + * Extends the standard occupant info with stage specific business. + */ +public class StageOccupantInfo extends OccupantInfo +{ + /** Creates a new instance for the specified body. */ + public StageOccupantInfo (BodyObject body) + { + super(body); + } + + /** Creates a blank instance for unserialization. */ + public StageOccupantInfo () + { + } + + /** + * Should return true if the occupant in question is available to be + * clustered with, false if they are "busy". This means that a user + * can click on them and start a chat circle. + */ + public boolean isClusterable () + { + return true; + } +} diff --git a/src/java/com/threerings/stage/data/StageScene.java b/src/java/com/threerings/stage/data/StageScene.java new file mode 100644 index 00000000..c4ff4de5 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageScene.java @@ -0,0 +1,211 @@ +// +// $Id: YoScene.java 17013 2004-09-08 02:39:09Z ray $ + +package com.threerings.stage.data; + +import java.util.ArrayList; +import java.util.Iterator; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.util.MisoUtil; + +import com.threerings.crowd.data.PlaceConfig; + +import com.threerings.whirled.data.Scene; +import com.threerings.whirled.data.SceneImpl; +import com.threerings.whirled.data.SceneUpdate; + +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SpotScene; +import com.threerings.whirled.spot.data.SpotSceneImpl; +import com.threerings.whirled.spot.data.SpotSceneModel; + +import com.threerings.stage.Log; + +/** + * The implementation of the Stage scene interface. + */ +public class StageScene extends SceneImpl + implements Scene, SpotScene, Cloneable +{ + /** + * Creates an instance that will obtain data from the supplied scene + * model and place config. + */ + public StageScene (StageSceneModel model, PlaceConfig config) + { + super(model, config); + _model = model; + _sdelegate = new SpotSceneImpl(SpotSceneModel.getSceneModel(_model)); + readInterestingObjects(); + } + + /** + * Returns the scene type (e.g. "world", "port", "bank", etc.). + */ + public String getType () + { + return _model.type; + } + + /** + * Returns the zone id to which this scene belongs. + */ + public int getZoneId () + { + return _model.zoneId; + } + + /** + * Sets the type of this scene. + */ + public void setType (String type) + { + _model.type = type; + } + + /** + * Get the default color id to use for the specified colorization class, + * or -1 if no default is set. + */ + public int getDefaultColor (int classId) + { + return _model.getDefaultColor(classId); + } + + /** + * Set the default color to use for the specified colorization class id. + * Setting the colorId to -1 disables the default. + */ + public void setDefaultColor (int classId, int colorId) + { + _model.setDefaultColor(classId, colorId); + } + + /** + * Iterates over all of the interesting objects in this scene. + */ + public Iterator enumerateObjects () + { + return _objects.iterator(); + } + + /** + * Adds a new object to this scene. + */ + public void addObject (ObjectInfo info) + { + _objects.add(info); + + // add it to the underlying scene model + StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(_model); + if (mmodel != null) { + if (!mmodel.addObject(info)) { + Log.warning("Scene model rejected object add " + + "[scene=" + this + ", object=" + info + "]."); + } + } + } + + /** + * Removes an object from this scene. + */ + public boolean removeObject (ObjectInfo info) + { + boolean removed = _objects.remove(info); + + // remove it from the underlying scene model + StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(_model); + if (mmodel != null) { + removed = mmodel.removeObject(info) || removed; + } + + return removed; + } + + // documentation inherited + public void updateReceived (SceneUpdate update) + { + super.updateReceived(update); + + // update our spot scene delegate + _sdelegate.updateReceived(); + + // re-read our interesting objects + readInterestingObjects(); + } + + // documentation inherited + public Object clone () + throws CloneNotSupportedException + { + // create a new scene with a clone of our model + return new StageScene((StageSceneModel)_model.clone(), _config); + } + + // documentation inherited from interface + public Portal getPortal (int portalId) + { + return _sdelegate.getPortal(portalId); + } + + // documentation inherited from interface + public int getPortalCount () + { + return _sdelegate.getPortalCount(); + } + + // documentation inherited from interface + public Iterator getPortals () + { + return _sdelegate.getPortals(); + } + + // documentation inherited from interface + public short getNextPortalId () + { + return _sdelegate.getNextPortalId(); + } + + // documentation inherited from interface + public Portal getDefaultEntrance () + { + return _sdelegate.getDefaultEntrance(); + } + + // documentation inherited from interface + public void addPortal (Portal portal) + { + _sdelegate.addPortal(portal); + } + + // documentation inherited from interface + public void removePortal (Portal portal) + { + _sdelegate.removePortal(portal); + } + + // documentation inherited from interface + public void setDefaultEntrance (Portal portal) + { + _sdelegate.setDefaultEntrance(portal); + } + + protected void readInterestingObjects () + { + _objects.clear(); + StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(_model); + if (mmodel != null) { + mmodel.getInterestingObjects(_objects); + } + } + + /** A reference to our scene model. */ + protected StageSceneModel _model; + + /** Our spot scene delegate. */ + protected SpotSceneImpl _sdelegate; + + /** A list of all interesting scene objects. */ + protected ArrayList _objects = new ArrayList(); +} diff --git a/src/java/com/threerings/stage/data/StageSceneConfig.java b/src/java/com/threerings/stage/data/StageSceneConfig.java new file mode 100644 index 00000000..8dbc4793 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageSceneConfig.java @@ -0,0 +1,27 @@ +// +// $Id: StageSceneConfig.java 8478 2003-05-07 05:12:26Z mdb $ + +package com.threerings.stage.data; + +import com.threerings.crowd.client.PlaceController; +import com.threerings.crowd.data.PlaceConfig; + +import com.threerings.stage.client.StageSceneController; + +/** + * Place configuration for the main isometric scenes in Stage. + */ +public class StageSceneConfig extends PlaceConfig +{ + // documentation inherited + public PlaceController createController () + { + return new StageSceneController(); + } + + // documentation inherited + public String getManagerClassName () + { + return "com.threerings.stage.server.StageSceneManager"; + } +} diff --git a/src/java/com/threerings/stage/data/StageSceneMarshaller.java b/src/java/com/threerings/stage/data/StageSceneMarshaller.java new file mode 100644 index 00000000..5e85b9a6 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageSceneMarshaller.java @@ -0,0 +1,67 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.data; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import com.threerings.stage.client.StageSceneService; + +/** + * Provides the implementation of the {@link StageSceneService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class StageSceneMarshaller extends InvocationMarshaller + implements StageSceneService +{ + /** The method id used to dispatch {@link #addObject} requests. */ + public static final int ADD_OBJECT = 1; + + // documentation inherited from interface + public void addObject (Client arg1, ObjectInfo arg2, InvocationService.ConfirmListener arg3) + { + InvocationMarshaller.ConfirmMarshaller listener3 = new InvocationMarshaller.ConfirmMarshaller(); + listener3.listener = arg3; + sendRequest(arg1, ADD_OBJECT, new Object[] { + arg2, listener3 + }); + } + + /** The method id used to dispatch {@link #removeObjects} requests. */ + public static final int REMOVE_OBJECTS = 2; + + // documentation inherited from interface + public void removeObjects (Client arg1, ObjectInfo[] arg2, InvocationService.ConfirmListener arg3) + { + InvocationMarshaller.ConfirmMarshaller listener3 = new InvocationMarshaller.ConfirmMarshaller(); + listener3.listener = arg3; + sendRequest(arg1, REMOVE_OBJECTS, new Object[] { + arg2, listener3 + }); + } + +} diff --git a/src/java/com/threerings/stage/data/StageSceneModel.java b/src/java/com/threerings/stage/data/StageSceneModel.java new file mode 100644 index 00000000..12f3c6b8 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageSceneModel.java @@ -0,0 +1,85 @@ +// +// $Id: YoSceneModel.java 17643 2004-10-28 22:58:30Z mdb $ + +package com.threerings.stage.data; + +import com.threerings.util.StreamableArrayList; +import com.threerings.util.StreamableIntIntMap; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.spot.data.SpotSceneModel; + +/** + * Extends the basic scene model with the notion of a scene type and + * incorporates the necessary auxiliary models used by the Stage system. + */ +public class StageSceneModel extends SceneModel +{ + /** A scene type code. */ + public static final String WORLD = "world"; + + /** This scene's type which is a string identifier used to later + * construct a specific controller to handle this scene. */ + public String type; + + /** The zone id to which this scene belongs. */ + public int zoneId; + + /** If non-null, contains default colorizations to use for objects + * that do not have colorizations defined. */ + public StreamableIntIntMap defaultColors; + + /** + * Get the default color to use for the specified colorization + * classId, or -1 if no default is set for that colorization. + */ + public int getDefaultColor (int classId) + { + if (defaultColors != null) { + return defaultColors.get(classId); + } + return -1; + } + + /** + * Set the default colorId to use for a specified colorization + * classId, or -1 to clear the default for that colorization. + */ + public void setDefaultColor (int classId, int colorId) + { + if (colorId == -1) { + if (defaultColors != null) { + defaultColors.remove(classId); + if (defaultColors.size() == 0) { + defaultColors = null; + } + } + + } else { + if (defaultColors == null) { + defaultColors = new StreamableIntIntMap(); + } + defaultColors.put(classId, colorId); + } + } + + /** + * Creates and returns a blank scene model. + */ + public static StageSceneModel blankStageSceneModel () + { + StageSceneModel model = new StageSceneModel(); + populateBlankStageSceneModel(model); + return model; + } + + /** + * Populates a blank scene model with blank values. + */ + protected static void populateBlankStageSceneModel (StageSceneModel model) + { + populateBlankSceneModel(model); + model.addAuxModel(new SpotSceneModel()); + model.addAuxModel(new StageMisoSceneModel()); + } +} diff --git a/src/java/com/threerings/stage/data/StageSceneObject.java b/src/java/com/threerings/stage/data/StageSceneObject.java new file mode 100644 index 00000000..45056486 --- /dev/null +++ b/src/java/com/threerings/stage/data/StageSceneObject.java @@ -0,0 +1,83 @@ +// +// $Id: StageSceneObject.java 18473 2004-12-28 03:52:57Z mdb $ + +package com.threerings.stage.data; + +import com.threerings.whirled.spot.data.SpotSceneObject; + +/** + * Extends the basic {@link SpotSceneObject} with data and services + * specific to isometric stage scenes. + */ +public class StageSceneObject extends SpotSceneObject +{ + // AUTO-GENERATED: FIELDS START + /** The field name of the stageSceneService field. */ + public static final String STAGE_SCENE_SERVICE = "stageSceneService"; + + /** The field name of the lightLevel field. */ + public static final String LIGHT_LEVEL = "lightLevel"; + + /** The field name of the lightShade field. */ + public static final String LIGHT_SHADE = "lightShade"; + // AUTO-GENERATED: FIELDS END + + /** Provides stage scene services. */ + public StageSceneMarshaller stageSceneService; + + /** The light level in this scene. 0f being fully on, 1f fully shaded. */ + public float lightLevel = 0f; + + /** The color of the light. */ + public int lightShade = 0xFFFFFF; + + // AUTO-GENERATED: METHODS START + /** + * Requests that the stageSceneService field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setStageSceneService (StageSceneMarshaller value) + { + StageSceneMarshaller ovalue = this.stageSceneService; + requestAttributeChange( + STAGE_SCENE_SERVICE, value, ovalue); + this.stageSceneService = value; + } + + /** + * Requests that the lightLevel field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setLightLevel (float value) + { + float ovalue = this.lightLevel; + requestAttributeChange( + LIGHT_LEVEL, Float.valueOf(value), Float.valueOf(ovalue)); + this.lightLevel = value; + } + + /** + * Requests that the lightShade field be set to the + * specified value. The local value will be updated immediately and an + * event will be propagated through the system to notify all listeners + * that the attribute did change. Proxied copies of this object (on + * clients) will apply the value change when they received the + * attribute changed notification. + */ + public void setLightShade (int value) + { + int ovalue = this.lightShade; + requestAttributeChange( + LIGHT_SHADE, Integer.valueOf(value), Integer.valueOf(ovalue)); + this.lightShade = value; + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/stage/server/StageSceneDispatcher.java b/src/java/com/threerings/stage/server/StageSceneDispatcher.java new file mode 100644 index 00000000..bfe10799 --- /dev/null +++ b/src/java/com/threerings/stage/server/StageSceneDispatcher.java @@ -0,0 +1,79 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.server; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import com.threerings.stage.client.StageSceneService; +import com.threerings.stage.data.StageSceneMarshaller; + +/** + * Dispatches requests to the {@link StageSceneProvider}. + */ +public class StageSceneDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public StageSceneDispatcher (StageSceneProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new StageSceneMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case StageSceneMarshaller.ADD_OBJECT: + ((StageSceneProvider)provider).addObject( + source, + (ObjectInfo)args[0], (InvocationService.ConfirmListener)args[1] + ); + return; + + case StageSceneMarshaller.REMOVE_OBJECTS: + ((StageSceneProvider)provider).removeObjects( + source, + (ObjectInfo[])args[0], (InvocationService.ConfirmListener)args[1] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/stage/server/StageSceneManager.java b/src/java/com/threerings/stage/server/StageSceneManager.java new file mode 100644 index 00000000..fb36ad65 --- /dev/null +++ b/src/java/com/threerings/stage/server/StageSceneManager.java @@ -0,0 +1,847 @@ +// +// $Id: WorldSceneManager.java 19769 2005-03-17 07:38:31Z mdb $ + +package com.threerings.stage.server; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; + +import com.samskivert.io.PersistenceException; +import com.samskivert.util.HashIntMap; +import com.samskivert.util.Invoker; +import com.samskivert.util.StringUtil; + +import com.threerings.media.util.AStarPathUtil; +import com.threerings.media.util.MathUtil; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.util.MisoSceneMetrics; +import com.threerings.miso.util.MisoUtil; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; + +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.spot.data.Cluster; +import com.threerings.whirled.spot.data.ClusterObject; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SceneLocation; +import com.threerings.whirled.spot.server.SpotSceneManager; + +import com.threerings.stage.Log; +import com.threerings.stage.client.StageSceneService; +import com.threerings.stage.data.DefaultColorUpdate; +import com.threerings.stage.data.ModifyObjectsUpdate; +import com.threerings.stage.data.StageCodes; +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageMisoSceneModel; +import com.threerings.stage.data.StageOccupantInfo; +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneMarshaller; +import com.threerings.stage.data.StageSceneModel; +import com.threerings.stage.data.StageSceneObject; +import com.threerings.stage.util.StageSceneUtil; + +/** + * Defines extensions to the basic Stage scene manager specific to + * displaying isometric "stage" scenes (these may be indoor, outdoor or + * aboard a vessel). + */ +public class StageSceneManager extends SpotSceneManager + implements StageSceneProvider +{ + /** + * Returns a traversal predicate for use with {@link + * StageSceneUtil#findStandingSpot} that validates whether a player can + * stand in the searched spots. + */ + public AStarPathUtil.TraversalPred getCanStandPred () + { + // this checks to see if we can stand at a particular tile coord + return new AStarPathUtil.TraversalPred() { + public boolean canTraverse (Object traverser, int x, int y) { + _tloc.x = MisoUtil.toFull(x, 2); + _tloc.y = MisoUtil.toFull(y, 2); + return mayStandAtLocation((BodyObject)traverser, _tloc); + } + protected StageLocation _tloc = new StageLocation(); + }; + } + + /** + * Adds the supplied object to this scene. A persistent update is + * generated and broadcast to all scene occupants. The update is + * stored in the repository for communication to future occupants of + * the scene and then entire complex process of changing our virtual + * game world is effected at the simple call of this single method. + * + * @param killOverlap if true, overlapping object will be removed, and + * the allowOverlap argument will be ignored. + * @param allowOverlap if true, overlapping objects will be allowed + * but one must be *very* careful to ensure that they know what they + * are doing (ie. the objects have render priorities that correctly + * handle the overlap). + * + * @return true if the object was added, false if the add was rejected + * because the object overlaps an existing scene object. + */ + public boolean addObject (ObjectInfo info, boolean killOverlap, + boolean allowOverlap) + { + // determine whether or not any object overlap our footprint + Rectangle foot = StageSceneUtil.getObjectFootprint( + StageServer.tilemgr, info.tileId, info.x, info.y); + if (foot == null) { + Log.warning("Aiya! Unable to compute object footprint! " + + "[where=" + where() + ", info=" + info + "]."); + return false; + } + + ObjectInfo[] lappers = StageSceneUtil.getIntersectedObjects( + StageServer.tilemgr, (StageSceneModel)_sscene.getSceneModel(), + foot); + if (!killOverlap && lappers.length > 0 && !allowOverlap) { + // no overlapping allowed + return false; + } + + // create our scene update which will be stored in the database + // and used to efficiently update clients + final ModifyObjectsUpdate update = new ModifyObjectsUpdate(); + update.init(_sscene.getId(), _sscene.getVersion(), + new ObjectInfo[] { info }, killOverlap ? lappers : null); + + Log.info("Modifying objects '" + update + "."); + recordUpdate(update, true); + + return true; + } + + /** + * Changes the default colorization for the specified color class. + * A persistent update is generated and broadcast to all scene + * occupants. The update is stored in the repository for communication + * to future occupants of the scene and then entire complex process + * of changing our virtual game world is effected at the simple call + * of this single method. + */ + public void setColor (int classId, int colorId) + { + DefaultColorUpdate update = new DefaultColorUpdate(); + update.init(_sscene.getId(), _sscene.getVersion(), classId, colorId); + recordUpdate(update, false); + } + + /** + * Returns true if the specified tile coordinate is passable (the base + * tile is passable and it is not in the footprint of an object). + */ + public boolean isPassable (int tx, int ty) + { + return (StageSceneUtil.isPassable( + StageServer.tilemgr, _mmodel.getBaseTileId(tx, ty)) && + !checkContains(_footprints.iterator(), tx, ty)); + } + + /** + * Called by NPPs to determine whether or not they can stand at the + * specified location. + */ + public boolean mayStandAtLocation (BodyObject source, StageLocation loc) + { + return validateLocation(source, loc, false); + } + + // documentation inherited from interface + public void addObject (ClientObject caller, ObjectInfo info, + StageSceneService.ConfirmListener listener) + throws InvocationException + { + BodyObject user = (BodyObject)caller; + String err = user.checkAccess(StageCodes.MODIFY_SCENE_ACCESS, _sscene); + if (err != null) { + throw new InvocationException(err); + } + + if (addObject(info, false, true)) { + listener.requestProcessed(); + } else { + listener.requestFailed(StageCodes.ERR_NO_OVERLAP); + } + } + + // documentation inherited from interface + public void removeObjects (ClientObject caller, ObjectInfo[] info, + StageSceneService.ConfirmListener listener) + throws InvocationException + { + BodyObject user = (BodyObject)caller; + String err = user.checkAccess(StageCodes.MODIFY_SCENE_ACCESS, _sscene); + if (err != null) { + throw new InvocationException(err); + } + + // create our scene update which will be stored in the database + // and used to efficiently update clients + ModifyObjectsUpdate update = new ModifyObjectsUpdate(); + update.init(_sscene.getId(), _sscene.getVersion(), null, info); + + Log.info("Modifying objects '" + update + "."); + recordUpdate(update, true); + + listener.requestProcessed(); + } + + // documentation inherited + protected void gotSceneData () + { + super.gotSceneData(); + + // cast some scene related bits + _sscene = (StageScene)_scene; + _mmodel = StageMisoSceneModel.getSceneModel(_scene.getSceneModel()); + + // note the footprints of all objects and portals in this scene + computeFootprints(); + } + + /** + * Applies the supplied scene update to our runtime scene and then + * fires off an invocation unit to apply the update to the scene + * database. Finally the update is "recorded" which broadcasts the + * update to all occupants of the scene and stores the update so that + * it can be provided as a patch to future scene visitors. + */ + protected void recordUpdate (SceneUpdate update, boolean tilesModified) + { + // do the standard update processing + recordUpdate(update); + + // then recompute some internal structures if need be + if (tilesModified) { + sceneTilesModified(); + } + } + + /** + * Called when any change is made to a scene's base or object tiles + * (resulting in a change in the way the scene looks). If any sneaky + * business need be done to deal with said addition, it can be handled + * here. + */ + protected void sceneTilesModified () + { + // compute the object footprints of all objects in this scene + computeFootprints(); + } + + // documentation inherited + protected void didStartup () + { + super.didStartup(); + _ssobj = (StageSceneObject)_plobj; + + // register and fill in our stage scene service + StageSceneMarshaller service = (StageSceneMarshaller) + StageServer.invmgr.registerDispatcher( + new StageSceneDispatcher(this), false); + _ssobj.setStageSceneService(service); + } + + // documentation inherited + protected void didShutdown () + { + super.didShutdown(); + StageServer.invmgr.clearDispatcher(_ssobj.stageSceneService); + } + + // documentation inherited + protected Class getPlaceObjectClass () + { + return StageSceneObject.class; + } + + // documentation inherited + protected void bodyLeft (int bodyOid) + { + super.bodyLeft(bodyOid); + + // out ye go! + _loners.remove(bodyOid); + } + + // documentation inherited + protected void updateLocation (BodyObject source, Location loc) + { + super.updateLocation(source, loc); + + // keep a rectangle around for each un-clustered occupant + StageLocation sloc = (StageLocation) loc; + int tx = MisoUtil.fullToTile(sloc.x), ty = MisoUtil.fullToTile(sloc.y); + _loners.put(source.getOid(), new Rectangle(tx, ty, 1, 1)); + } + + /** + * Computes the footprints of all objects and portals in this scene. + * This is done when we are first resolved and following any + * modifications to the scene's tile data. + */ + protected void computeFootprints () + { + _footprints.clear(); + _mmodel.visitObjects(new StageMisoSceneModel.ObjectVisitor() { + public void visit (ObjectInfo info) { + Rectangle foot = StageSceneUtil.getObjectFootprint( + StageServer.tilemgr, info.tileId, info.x, info.y); + _footprints.add(foot); + } + }); + +// Log.info("Computed footprints [where=" + where () + +// ", rects=" + StringUtil.toString(_footprints) + "]."); + + // place the tile coordinates of our portals into a set for + // efficient comparison with location coordinates + _plocs.clear(); + for (Iterator iter = _sscene.getPortals(); iter.hasNext(); ) { + Portal port = (Portal)iter.next(); + StageLocation loc = (StageLocation) port.loc; + _plocs.add(new Point(MisoUtil.fullToTile(loc.x), + MisoUtil.fullToTile(loc.y))); + } + } + + /** + * Helper function for {@link #mayStandAtLocation} and {@link + * #validateLocation(BodyObject,Location)}. + */ + protected boolean validateLocation (BodyObject source, StageLocation loc, + boolean allowPortals) + { + int tx = MisoUtil.fullToTile(loc.x), ty = MisoUtil.fullToTile(loc.y); + + // make sure the tile at that location is passable + if (!StageSceneUtil.isPassable(StageServer.tilemgr, + _mmodel.getBaseTileId(tx, ty))) { +// Log.info("Rejecting non-passable loc [who=" + source.who() + +// ", loc=" + loc + "]."); + return false; + } + + // if they're moving to stand on a portal, let them do it + if (allowPortals && _plocs.contains(new Point(tx, ty))) { + return true; + } + + // if they are already standing on this tile, allow it + SceneLocation cloc = (SceneLocation) + _ssobj.occupantLocs.get(Integer.valueOf(source.getOid())); + if (cloc != null) { + StageLocation sloc = (StageLocation) cloc.loc; + if (MisoUtil.fullToTile(sloc.x) == tx && + MisoUtil.fullToTile(sloc.y) == ty) { +// Log.info("Allowing adjust [who=" + source.who () + +// ", cloc=" + cloc + ", nloc=" + loc + "]."); + return true; + } + } + + // make sure they're not standing in a cluster footprint, an + // object footprint, or in the same tile as another scene occupant + if (checkContains(_ssobj.clusters.iterator(), tx, ty) || + checkContains(_footprints.iterator(), tx, ty) || + checkContains(_loners.values().iterator(), tx, ty)) { +// Log.info("Rejecting loc [who=" + source.who() + +// ", loc=" + loc + ", inCluster=" + +// checkContains(_ssobj.clusters.iterator(), tx, ty) + +// ", inFootprint=" + +// checkContains(_footprints.iterator(), tx, ty) + +// ", onLoner=" + +// checkContains(_loners.values().iterator(), tx, ty) + +// "]."); + return false; + } + + return true; + } + + /** Helper function for {@link #validateLocation}. */ + protected boolean checkContains (Iterator iter, int tx, int ty) + { + while (iter.hasNext()) { + Rectangle rect = (Rectangle)iter.next(); + if (rect.contains(tx, ty)) { +// Log.info(StringUtil.toString(rect) + " contains " + +// StringUtil.coordsToString(tx, ty) + "."); + return true; + } + } + return false; + } + + // documentation inherited + protected SceneLocation computeEnteringLocation ( + BodyObject body, Portal entry) + { + // sanity check + if (entry == null) { + Log.warning("Requested to compute entering location for " + + "non-existent portal [where=" + where() + + ", who=" + body.who() + "]."); + entry = _sscene.getDefaultEntrance(); + } + + MisoSceneMetrics metrics = StageSceneUtil.getMetrics(); + SceneLocation loc = new SceneLocation( + entry.getOppLocation(), body.getOid()); + StageLocation sloc = (StageLocation) loc.loc; + int tx = MisoUtil.fullToTile(sloc.x), + ty = MisoUtil.fullToTile(sloc.y); + int oidx = sloc.orient/2; + int lidx = (oidx+3)%4; // rotate to the left + int ridx = (oidx+1)%4; // rotate to the right + + // start in the row in front of the portal and search forward, + // checking the center, then the spot(s) to the left, then the + // spot(s) to the right (fanning out by one tile each time) + final int MAX_FAN = 4; + LOC_SEARCH: + for (int fan = 1; fan < MAX_FAN; fan++) { + tx += PORTAL_DX[oidx]; + ty += PORTAL_DY[oidx]; + + // look in the center column + if (checkEntry(metrics, body, tx, ty, sloc)) { + break LOC_SEARCH; + } + + // look to the left + for (int lx = tx, ly = ty, ff = 0; ff < fan; ff++) { + lx += PORTAL_DX[lidx]; + ly += PORTAL_DY[lidx]; + if (checkEntry(metrics, body, lx, ly, sloc)) { + break LOC_SEARCH; + } + } + + // look to the right + for (int rx = tx, ry = ty, ff = 0; ff < fan; ff++) { + rx += PORTAL_DX[ridx]; + ry += PORTAL_DY[ridx]; + if (checkEntry(metrics, body, rx, ry, sloc)) { + break LOC_SEARCH; + } + } + + // if this is our last pass and we didn't find anything, + // revert back to the portal location + if (fan == MAX_FAN-1) { + sloc = (StageLocation) entry.getOppLocation(); + } + } + + tx = MisoUtil.fullToTile(sloc.x); + ty = MisoUtil.fullToTile(sloc.y); + _loners.put(body.getOid(), new Rectangle(tx, ty, 1, 1)); + + loc.loc = sloc; + return loc; + } + + /** Helper function for {@link #computeEnteringLocation}. */ + protected boolean checkEntry (MisoSceneMetrics metrics, BodyObject body, + int tx, int ty, StageLocation loc) + { + loc.x = MisoUtil.toFull(tx, metrics.finegran/2); + loc.y = MisoUtil.toFull(ty, metrics.finegran/2); + return validateLocation(body, loc, false); + } + + // documentation inherited + protected boolean validateLocation (BodyObject source, Location loc) + { + // TODO: make sure the user isn't warping to hell and gone (and if + // they are, make sure they're an admin) + return validateLocation(source, (StageLocation)loc, true); + } + + // documentation inherited + protected boolean canAddBody (ClusterRecord clrec, BodyObject body) + { + // make sure we have a setting for clusters of this size + if (clrec.size() >= TARGET_SIZE.length-2) { + return false; + } + + // if this is a brand new cluster, just make it 2x2 and be done + // with it + Cluster cl = clrec.getCluster(); + if (cl.width == 0) { + cl.width = 2; + cl.height = 2; +// Log.info("Created new cluster for " + body.who() + +// " (" + cl + ")."); + return true; + } + +// Log.info("Maybe adding " + body.who() + " to " + cl + "."); + + // if the cluster is already big enough, we're groovy + int target = TARGET_SIZE[clrec.size()+1]; + if (cl.width >= target) { + return true; + } + + // make an expanded footprint and try to fit it somewhere + int expand = target-cl.width; + Rectangle rect = null; + for (int ii = 0; ii < X_OFF.length; ii++) { + rect = new Rectangle(cl.x + expand*X_OFF[ii], + cl.y + expand*Y_OFF[ii], + cl.width+expand, cl.height+expand); + + // if this rect overlaps objects, other clusters, portals or + // impassable tiles, it's no good + if (checkIntersects(_ssobj.clusters.iterator(), rect, cl) || + checkIntersects(_footprints.iterator(), rect, cl) || + checkPortals(rect) || checkViolatesPassability(rect)) { + rect = null; + } else { + break; + } + } + + // if we couldn't expand in any direction, it's no go + if (rect == null) { +// Log.info("Couldn't expand cluster " + cl + "."); + return false; + } + + // now look to see if we just expanded our cluster over top of any + // unsuspecting standers by and if so, attempt to subsume them + for (Iterator iter = _loners.keySet().iterator(); + iter.hasNext(); ) { + int bodyOid = ((Integer)iter.next()).intValue(); + // skip ourselves + if (bodyOid == body.getOid()) { + continue; + } + + // do the right thing with a person standing right on the + // cluster (they're the person we're clustering with) + Rectangle trect = (Rectangle)_loners.get(bodyOid); + if (trect.equals(cl)) { + continue; + } + + if (trect.intersects(rect)) { + // make sure this person isn't already in our cluster + ClusterObject clobj = clrec.getClusterObject(); + if (clobj != null && clobj.occupants.contains(bodyOid)) { + Log.warning("Ignoring stale occupant [where=" + where() + + ", cluster=" + cl + ", occ=" + bodyOid + "]."); + continue; + } + + // make sure the subsumee exists + final BodyObject bobj = (BodyObject) + StageServer.omgr.getObject(bodyOid); + if (bobj == null) { + Log.warning("Can't subsume disappeared body " + + "[where=" + where() + ", cluster=" + cl + + ", boid=" + bodyOid + "]."); + continue; + } + + // we subsume nearby pirates by queueing up their addition + // to our cluster to be processed after we finish adding + // ourselves to this cluster + final ClusterRecord fclrec = clrec; + StageServer.omgr.postRunnable(new Runnable() { + public void run () { + try { + Log.info("Subsuming " + bobj.who() + + " into " + fclrec.getCluster() + "."); + fclrec.addBody(bobj); + + } catch (InvocationException ie) { + Log.info("Unable to subsume neighbor " + + "[cluster=" + fclrec.getCluster() + + ", neighbor=" + bobj.who() + + ", cause=" + ie.getMessage() + "]."); + } + } + }); + } + } + +// Log.info("Expanding cluster [cluster=" + cl + +// ", rect=" + rect + "]."); + cl.setBounds(rect); + + return true; + } + + /** Helper function for {@link #canAddBody}. */ + protected boolean checkIntersects (Iterator iter, Rectangle rect, + Rectangle ignore) + { + while (iter.hasNext()) { + Rectangle trect = (Rectangle)iter.next(); + if (ignore != null && trect.equals(ignore)) { + continue; + } + if (trect.intersects(rect)) { + return true; + } + } + return false; + } + + /** Helper function for {@link #canAddBody}. */ + protected boolean checkPortals (Rectangle rect) + { + for (Iterator iter = _plocs.iterator(); iter.hasNext(); ) { + Point ppoint = (Point)iter.next(); + if (rect.contains(ppoint)) { + return true; + } + } + return false; + } + + /** Helper function for {@link #canAddBody}. */ + protected boolean checkViolatesPassability (Rectangle rect) + { + // we just check the whole damned thing, but it's not really that + // expensive, so we're not going to worry about it + // Check the bounds + 1 in each direction, so we can see if a fringer + // blocks us. + for (int xx = rect.x-1, ex = rect.x+rect.width+1; xx < ex; xx++) { + for (int yy = rect.y-1, ey = rect.y+rect.height+1; yy < ey; yy++) { + int btid = _mmodel.getBaseTileId(xx, yy); + if ((btid == 0) && + ((xx == rect.x-1) || (xx == rect.x + rect.width) || + (yy == rect.y-1) || (yy == rect.y + rect.height))) { + // it's ok to have an unspecified base tile in the outer + // border + continue; + + } else if (!StageSceneUtil.isPassable( + StageServer.tilemgr, btid)) { +// Log.info("Cluster impassable " + +// "[rect=" + StringUtil.toString(rect) + +// ", spot=" + StringUtil.coordsToString(xx, yy) + +// "]."); + return true; + } + } + } + return false; + } + + // documentation inherited + protected void bodyAdded (ClusterRecord clrec, BodyObject body) + { + super.bodyAdded(clrec, body); + +// Log.info(body.who() + " added to " + clrec + "."); + + // remove them from the loners map if they were in it + int bodyOid = body.getOid(); + _loners.remove(bodyOid); + + Cluster cl = clrec.getCluster(); + if (clrec.size() == 1) { + // if we're adding our first player, assign initial dimensions + // to the cluster + cl.width = 1; cl.height = 1; + SceneLocation loc = locationForBody(bodyOid); + if (loc == null) { + Log.warning("Foreign body added to cluster [clrec=" + clrec + + ", body=" + body.who() + "]."); + cl.x = 10; cl.y = 10; + } else { + StageLocation sloc = (StageLocation) loc.loc; + cl.x = MisoUtil.fullToTile(sloc.x); + cl.y = MisoUtil.fullToTile(sloc.y); + } + // we'll do everything else when occupant two shows up + return; + } + + // generate a list of all valid locations for this cluster + ArrayList locs = StageSceneUtil.getClusterLocs(cl); + +// Log.info("Positioning " + clrec.size() + " bodies in " + +// StringUtil.toString(locs) + " for " + cl + "."); + + // make sure everyone is in their proper position + for (Iterator iter = clrec.keySet().iterator(); iter.hasNext(); ) { + int tbodyOid = ((Integer)iter.next()).intValue(); + // leave the newly added player to last + if (tbodyOid != bodyOid) { + positionBody(cl, tbodyOid, locs); + } + } + + // finally position the new guy + positionBody(cl, bodyOid, locs); + } + + /** Helper function for {@link #bodyAdded}. */ + protected void positionBody (Cluster cl, int bodyOid, ArrayList locs) + { + SceneLocation sloc = (SceneLocation) + _ssobj.occupantLocs.get(Integer.valueOf(bodyOid)); + if (sloc == null) { + BodyObject user = (BodyObject)StageServer.omgr.getObject(bodyOid); + String who = (user == null) ? ("" + bodyOid) : user.who(); + Log.warning("Can't position locationless user " + + "[where=" + where() + ", cluster=" + cl + + ", boid=" + who + "]."); + return; + } + + SceneLocation cloc = getClosestLoc(locs, sloc); +// Log.info("Maybe moving " + bodyOid + " from " + sloc + +// " to " + cloc + +// " (avail=" + StringUtil.toString(locs) + ")."); + if (cloc != null && !cloc.loc.equivalent(sloc.loc)) { + cloc.bodyOid = bodyOid; +// Log.info("Moving " + bodyOid + " to " + cloc + +// " for " + cl + "."); + _ssobj.updateOccupantLocs(cloc); + } + } + + // documentation inherited + protected void bodyRemoved (ClusterRecord clrec, BodyObject body) + { + super.bodyRemoved(clrec, body); + + // if we've been reduced to a zero person cluster, we can skip all + // this because the cluster will be destroyed when we return + if (clrec.size() < 1) { + return; + } + + // reduce the bounds of the cluster if necessary + int target = TARGET_SIZE[clrec.size()]; + Cluster cl = clrec.getCluster(); + // allow a cluster to remain one size larger than it needs to be + // as long as there's more than one person in it + if (cl.width <= (target + ((clrec.size() > 1) ? 1 : 0))) { + return; + } + + // otherwise shrink the cluster + cl.width = target; + cl.height = target; + + // generate a list of all valid locations for this cluster + ArrayList locs = StageSceneUtil.getClusterLocs(cl); + + // make sure everyone is in their proper position + for (Iterator iter = clrec.keySet().iterator(); iter.hasNext(); ) { + int bodyOid = ((Integer)iter.next()).intValue(); + // leave the newly added player to last + if (bodyOid != body.getOid()) { + positionBody(cl, bodyOid, locs); + } + } + } + + // documentation inherited + protected void checkCanCluster (BodyObject initiator, BodyObject target) + throws InvocationException + { + StageOccupantInfo info = (StageOccupantInfo)_ssobj.occupantInfo.get( + Integer.valueOf(target.getOid())); + if (info == null) { + Log.warning("Have no occinfo for cluster target " + + "[where=" + where() + ", init=" + initiator.who() + + ", target=" + target.who() + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + if (!info.isClusterable()) { + throw new InvocationException(StageCodes.ERR_CANNOT_CLUSTER); + } + } + + /** + * Locates and removes the location in the list closest to the + * supplied location. + */ + protected static SceneLocation getClosestLoc ( + ArrayList locs, SceneLocation optimalLocation) + { + StageLocation loc = (StageLocation) optimalLocation.loc; + SceneLocation cloc = null; + float cdist = Integer.MAX_VALUE; + int cidx = -1; + for (int ii = 0, ll = locs.size(); ii < ll; ii++) { + SceneLocation tloc = (SceneLocation)locs.get(ii); + StageLocation sl = (StageLocation) tloc.loc; + float tdist = MathUtil.distance(loc.x, loc.y, sl.x, sl.y); + if (tdist < cdist) { + cloc = tloc; + cdist = tdist; + cidx = ii; + } + } + if (cidx != -1) { + locs.remove(cidx); + } + return cloc; + } + + /** A casted reference to our scene object. */ + protected StageSceneObject _ssobj; + + /** A casted reference to our scene data. */ + protected StageScene _sscene; + + /** Our miso scene data extracted for convenience and efficiency. */ + protected StageMisoSceneModel _mmodel; + + /** Rectangles describing the footprints (in tile coordinates) of all + * of our scene objects. */ + protected ArrayList _footprints = new ArrayList(); + + /** Rectangles containing a "footprint" for the users that aren't in + * any clusters. */ + protected HashIntMap _loners = new HashIntMap(); + + /** Contains the (tile) coordinates of all of our portals. */ + protected HashSet _plocs = new HashSet(); + + /** The dimensions of a cluster with the specified number of + * occupants. */ + protected static final int[] TARGET_SIZE = { + 1, 2, // one person cluster + 3, 3, 3, // two to four person cluster + 4, 4, 4, 4, // five to eight person cluster + 5, 5, 5, 5, // nine to twelve person cluster + 6, 6, 6, 6, // thirteen to sixteen person cluster + 7, 7, 7, 7, 7, 7, 7, 7, // seventeen to twenty four person cluster + 8 // needed though we'll never expand to this size + }; + + /** Used by {@link #canAddBody}. */ + protected static final int[] X_OFF = { 0, -1, 0, -1 }; + + /** Used by {@link #canAddBody}. */ + protected static final int[] Y_OFF = { 0, 0, -1, -1 }; + + /** Used by {@link #computeEnteringLocation}. */ + protected static final int[] PORTAL_DX = { 0, -1, 0, 1 }; // W N E S + + /** Used by {@link #computeEnteringLocation}. */ + protected static final int[] PORTAL_DY = { 1, 0, -1, 0 }; // W N E S +} diff --git a/src/java/com/threerings/stage/server/StageSceneProvider.java b/src/java/com/threerings/stage/server/StageSceneProvider.java new file mode 100644 index 00000000..4e4cc6b1 --- /dev/null +++ b/src/java/com/threerings/stage/server/StageSceneProvider.java @@ -0,0 +1,48 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.server; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; +import com.threerings.stage.client.StageSceneService; + +/** + * Defines the server-side of the {@link StageSceneService}. + */ +public interface StageSceneProvider extends InvocationProvider +{ + /** + * Handles a {@link StageSceneService#addObject} request. + */ + public void addObject (ClientObject caller, ObjectInfo arg1, InvocationService.ConfirmListener arg2) + throws InvocationException; + + /** + * Handles a {@link StageSceneService#removeObjects} request. + */ + public void removeObjects (ClientObject caller, ObjectInfo[] arg1, InvocationService.ConfirmListener arg2) + throws InvocationException; +} diff --git a/src/java/com/threerings/stage/server/StageServer.java b/src/java/com/threerings/stage/server/StageServer.java new file mode 100644 index 00000000..f7596d0b --- /dev/null +++ b/src/java/com/threerings/stage/server/StageServer.java @@ -0,0 +1,67 @@ +// +// $Id$ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.stage.server; + +import com.threerings.resource.ResourceManager; + +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.bundle.BundledTileSetRepository; + +import com.threerings.whirled.server.WhirledServer; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageCodes; + +/** + * Extends the Whirled server to provide services needed by the Stage + * system. + */ +public abstract class StageServer extends WhirledServer +{ + /** A resource manager with which we can load resources in the same + * manner that the client does (for resources that are used on both + * the server and client). */ + public ResourceManager rsrcmgr; + + /** Provides access to our tile repository. */ + public static TileManager tilemgr; + + // documentation inherited + public void init () + throws Exception + { + // do the base server initialization + super.init(); + + // create the resource manager + rsrcmgr = new ResourceManager("rsrc"); + rsrcmgr.initBundles(null, "config/resource/manager.properties", null); + + // create our tile manager and repository + tilemgr = new TileManager(null); + tilemgr.setTileSetRepository( + new BundledTileSetRepository(rsrcmgr, null, + StageCodes.TILESET_RSRC_SET)); + + Log.info("Stage server initialized."); + } +} diff --git a/src/java/com/threerings/stage/tools/editor/DirectionButton.java b/src/java/com/threerings/stage/tools/editor/DirectionButton.java new file mode 100644 index 00000000..42ffd7dd --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/DirectionButton.java @@ -0,0 +1,192 @@ +// +// $Id: DirectionButton.java 5784 2003-01-08 04:17:40Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import javax.swing.AbstractButton; +import javax.swing.DefaultButtonModel; + +import com.threerings.media.image.ColorUtil; + +import com.threerings.util.DirectionCodes; + +/** + * A button that allows for selection from the compass directions. + */ +public class DirectionButton extends AbstractButton +{ + /** + * Construct a 41x41 DirectionButton. + */ + public DirectionButton () + { + this(41); + } + + /** + * Construct a DirectionButton with the specified preferred diameter. + */ + public DirectionButton (int preferredDiameter) + { + _prefdia = preferredDiameter; + configSize(_prefdia); + + setModel(new DefaultButtonModel()); + + addMouseListener(new MouseAdapter() { + public void mousePressed (MouseEvent event) + { + if (isEnabled()) { + _armed = getDirection(event.getX(), event.getY()); + repaint(); + } + } + + public void mouseReleased (MouseEvent event) + { + if ((_armed != -1) && + isEnabled() && + (_armed == getDirection(event.getX(), event.getY())) && + (_direction != _armed)) { + + _direction = _armed; + fireStateChanged(); + } + _armed = -1; + repaint(); + } + + + }); + } + + /** + * Paint this component and the selected direction, + * dimmed if we're inactive. + */ + public void paintComponent (Graphics g) + { + super.paintComponent(g); + + // draw the selected direction circle + g.setColor(isEnabled() ? Color.red + : ColorUtil.blend(Color.red, getBackground())); + g.fillOval(_coords[_direction][0], _coords[_direction][1], + _cdia, _cdia); + + if (_armed != -1) { + g.setColor(ColorUtil.blend(Color.black, getBackground())); + g.fillOval(_coords[_armed][0], _coords[_armed][1], + _cdia, _cdia); + } + + // draw a circle around the various direction circles + g.setColor(isEnabled() ? Color.black + : ColorUtil.blend(Color.black, getBackground())); + g.drawOval(0, 0, _dia, _dia); + + // draw each direction circle + for (int ii=DirectionCodes.SOUTHWEST; + ii < DirectionCodes.DIRECTION_COUNT; + ii++) { + + g.drawOval(_coords[ii][0], _coords[ii][1], _cdia, _cdia); + } + } + + /** + * Get the direction that is currently selected. + */ + public int getDirection () + { + return _direction; + } + + /** + * Set the currently displayed direction + */ + public void setDirection (int direction) + { + if (direction != _direction) { + _direction = direction; + fireStateChanged(); + repaint(); + } + } + + /** + * Get the direction specified by the mouse coordinates + */ + protected int getDirection (int x, int y) + { + for (int ii=0; ii < DirectionCodes.DIRECTION_COUNT; ii++) { + if ((x > _coords[ii][0]) && (x < (_coords[ii][0] + _cdia)) && + (y > _coords[ii][1]) && (y < (_coords[ii][1] + _cdia))) { + return ii; + } + } + return -1; + } + + // documentation inherited + public void setSize (Dimension d) + { + super.setSize(d); + + configSize(Math.min(d.width, d.height)); + } + + /** + * Reconfigure the way we look for a new size + */ + protected void configSize (int diameter) + { + // recompute our sizes + _dia = diameter - 1; + _cdia = _dia / 4; + + int mid = (_dia - _cdia) / 2; + + int num = DirectionCodes.DIRECTION_COUNT; // oh we all know it's 8. + + for (int ii=0; ii < num; ii++) { + double rads = Math.PI * 2 * ii / num; + + // 0 radians specifies EAST, so we offset + int dir = (ii + DirectionCodes.EAST) % num; + + _coords[dir][0] = mid + ((int) (mid * Math.cos(rads))); + _coords[dir][1] = mid + ((int) (mid * Math.sin(rads))); + } + } + + // documentation inherited + public Dimension getPreferredSize () + { + return new Dimension(_prefdia, _prefdia); + } + + /** The current direction displayed by the button. */ + protected int _direction = DirectionCodes.SOUTH; + + // if we're "armed" for a possible state change, which direction is armed? + // (-1 for unarmed) + protected int _armed = -1; + + /** Diameter of the drawn enclosing circle. */ + protected int _dia; + + /** Diameter of selection circles. */ + protected int _cdia; + + /** Coordinates of each of the selection circles. */ + protected int[][] _coords = new int[DirectionCodes.DIRECTION_COUNT][2]; + + /** Our preferred diameter. */ + protected int _prefdia; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorApp.java b/src/java/com/threerings/stage/tools/editor/EditorApp.java new file mode 100644 index 00000000..5f4536f3 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorApp.java @@ -0,0 +1,351 @@ +// +// $Id: EditorApp.java 19661 2005-03-09 02:40:29Z andrzej $ + +package com.threerings.stage.tools.editor; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.DisplayMode; +import java.awt.EventQueue; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; + +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JProgressBar; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.util.List; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.DebugChords; +import com.samskivert.util.StringUtil; + +import com.threerings.util.KeyDispatcher; +import com.threerings.util.KeyboardManager; +import com.threerings.util.MessageBundle; +import com.threerings.util.MessageManager; + +import com.threerings.resource.ResourceManager; + +import com.threerings.media.FrameManager; +import com.threerings.media.image.ColorPository; +import com.threerings.media.image.ImageManager; +import com.threerings.media.util.ModeUtil; + +import com.threerings.media.tile.TileSetRepository; +import com.threerings.media.tile.bundle.BundledTileSetRepository; + +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.bundle.BundledComponentRepository; +import com.threerings.miso.tile.MisoTileManager; + +import com.threerings.stage.data.StageSceneModel; +import com.threerings.stage.tools.editor.util.EditorContext; + +/** + * A scene editor application that provides facilities for viewing, + * editing, and saving the scene templates that comprise a game. + */ +public class EditorApp implements Runnable +{ + /** + * Construct and initialize the EditorApp object. + */ + public EditorApp (String[] args) + throws IOException + { + final String target = (args.length > 0) ? args[0] : null; + + if (System.getProperty("no_log_redir") != null) { + Log.info("Logging to console only."); + + } else { + String dlog = localDataDir("editor.log"); + try { + PrintStream logOut = new PrintStream( + new BufferedOutputStream(new FileOutputStream(dlog)), true); + System.setOut(logOut); + System.setErr(logOut); + Log.info("Opened debug log '" + dlog + "'."); + + } catch (IOException ioe) { + Log.warning("Failed to open debug log [path=" + dlog + + ", error=" + ioe + "]."); + } + } + + // we need to use heavy-weight popup menus so that they can overly + // our non-lightweight Miso view + JPopupMenu.setDefaultLightWeightPopupEnabled(false); + + // create and size the main application frame + _frame = createEditorFrame(); + + // create our frame manager + _framemgr = FrameManager.newInstance(_frame, _frame); + + // create our myriad managers, repositories, etc. + _rmgr = new ResourceManager("rsrc"); + + // build up a simple ui for displaying progress + JPanel progressPanel = new JPanel(new BorderLayout()); + final JLabel progressLabel = new JLabel(); + final JProgressBar progress = new JProgressBar(0,100); + final JPanel spacer = new JPanel(); + + progressPanel.add(progressLabel, BorderLayout.CENTER); + progressPanel.add(progress, BorderLayout.SOUTH); + progressPanel.setPreferredSize(new Dimension(300,80)); + spacer.add(progressPanel); + + _frame.getContentPane().add(spacer); + + final EditorFrame frameRef = _frame; + + ResourceManager.InitObserver obs = new ResourceManager.InitObserver() { + public void progress (int percent, long remaining) { + String msg = "Unpacking..."; + if (remaining >= 0) { + msg += " " + remaining + " seconds remaining."; + } + progressLabel.setText(msg); + progress.setValue(percent); + + if (percent >= 100) { + frameRef.getContentPane().remove(spacer); + EditorApp.this.finishInit(target); + } + } + + public void initializationFailed (Exception e) { + Log.warning("Failed unpacking bundles [e=" + e + "]."); + Log.logStackTrace(e); + } + }; + // we want our methods called on the AWT thread + obs = new ResourceManager.AWTInitObserver(obs); + _rmgr.initBundles(null, "config/resource/editor.properties", obs); + } + + public void finishInit (String target) + { + _msgmgr = new MessageManager("rsrc.i18n"); + _imgr = new ImageManager(_rmgr, _frame); + _tilemgr = new MisoTileManager(_rmgr, _imgr); + + try { + _tsrepo = new BundledTileSetRepository(_rmgr, _imgr, "tilesets"); + _tilemgr.setTileSetRepository(_tsrepo); + _crepo = new BundledComponentRepository( + _rmgr, _imgr, "components"); + } catch (IOException e) { + Log.warning("Exception loading tilesets and and icon manager " + + "[Exception=" + e + "]."); + return; + } + + _colpos = ColorPository.loadColorPository(_rmgr); + _kbdmgr = new KeyboardManager(); + _keydisp = new KeyDispatcher(_frame); + + _ctx = new EditorContextImpl(); + + // initialize the frame with the now-prepared context + _frame.init(_ctx, target); + + // wire up our runtime adjustment editor + DebugChords.activate(); + + // if we have a target file, load it up + if (target != null) { + _frame.openScene(target); + } + } + + /** + * Given a subdirectory name (that should correspond to the calling + * service), returns a file path that can be used to store local data. + */ + public static String localDataDir (String subdir) + { + String appdir = System.getProperty("appdir"); + if (StringUtil.isBlank(appdir)) { + appdir = ".narya-editor"; + String home = System.getProperty("user.home"); + if (!StringUtil.isBlank(home)) { + appdir = home + File.separator + appdir; + } + } + return appdir + File.separator + subdir; + } + + /** + * Run the application. + */ + public void run () + { + // enter full-screen exclusive mode if available and if we have + // the right display mode + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice gd = env.getDefaultScreenDevice(); + DisplayMode pmode = null; + try { + DisplayMode cmode = gd.getDisplayMode(); + pmode = ModeUtil.getDisplayMode( + gd, cmode.getWidth(), cmode.getHeight(), 16, 15); + } catch (Throwable t) { + // Win98 seems to choke on it's own vomit when we attempt to + // enumerate the available display modes; yay! + Log.warning("Failed to probe display mode."); + Log.logStackTrace(t); + } + + if (_viewFullScreen.getValue() && gd.isFullScreenSupported() && + pmode != null) { + Log.info("Switching to screen mode " + + "[mode=" + ModeUtil.toString(pmode) + "]."); + // set the frame to undecorated, full-screen + _frame.setUndecorated(true); + gd.setFullScreenWindow(_frame); + // switch to our happy custom display mode + gd.setDisplayMode(pmode); + // lay the frame out properly (we can't do this before making + // it full screen because packing causes the window to become + // displayable which apparently prevents the window from + // subsequently being made a full-screen window) + _frame.pack(); + + } else { + _frame.setSize(1024, 768); + SwingUtil.centerWindow(_frame); + _frame.setVisible(true); + } + _framemgr.start(); + } + + protected EditorFrame createEditorFrame () + { + return new EditorFrame(); + } + + /** + * Derived classes can override this method and add additional scene + * types. + */ + protected void enumerateSceneTypes (List types) + { + types.add(StageSceneModel.WORLD); + } + + /** + * Instantiate the application object and start it running. + */ + public static void main (String[] args) + { + try { + EditorApp app = new EditorApp(args); + // start up the UI on the AWT thread to avoid deadlocks + EventQueue.invokeLater(app); + + } catch (IOException ioe) { + Log.warning("Unable to initialize editor."); + Log.logStackTrace(ioe); + } + } + + /** + * The implementation of the EditorContext interface that provides + * handles to the config and manager objects that offer commonly + * used services. + */ + protected class EditorContextImpl implements EditorContext + { + public MisoTileManager getTileManager () { + return _tilemgr; + } + + public FrameManager getFrameManager () { + return _framemgr; + } + + public ResourceManager getResourceManager () { + return _rmgr; + } + + public ImageManager getImageManager () { + return _imgr; + } + + public MessageManager getMessageManager () { + return _msgmgr; + } + + public KeyboardManager getKeyboardManager() { + return _kbdmgr; + } + + public ComponentRepository getComponentRepository () { + return _crepo; + } + + public KeyDispatcher getKeyDispatcher () { + return _keydisp; + } + + public String xlate (String message) { + return xlate("stage.editor", message); + } + + public String xlate (String bundle, String message) { + MessageBundle mbundle = _msgmgr.getBundle(bundle); + if (mbundle == null) { + Log.warning("Requested to translate message with " + + "non-existent bundle [bundle=" + bundle + + ", message=" + message + "]."); + return message; + } else { + return mbundle.xlate(message); + } + } + + public TileSetRepository getTileSetRepository () { + return _tsrepo; + } + + public ColorPository getColorPository () { + return _colpos; + } + + public void enumerateSceneTypes (List types) { + EditorApp.this.enumerateSceneTypes(types); + } + } + + protected EditorContext _ctx; + protected EditorFrame _frame; + protected FrameManager _framemgr; + + protected ResourceManager _rmgr; + protected ImageManager _imgr; + protected MisoTileManager _tilemgr; + protected TileSetRepository _tsrepo; + protected ColorPository _colpos; + protected MessageManager _msgmgr; + protected KeyboardManager _kbdmgr; + protected BundledComponentRepository _crepo; + protected KeyDispatcher _keydisp; + + /** A debug hook that toggles debug rendering of traversable tiles. */ + protected static RuntimeAdjust.BooleanAdjust _viewFullScreen = + new RuntimeAdjust.BooleanAdjust( + "Toggles whether or not the scene editor uses full screen mode.", + "stage.editor.full_screen", EditorConfig.config, false); +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorConfig.java b/src/java/com/threerings/stage/tools/editor/EditorConfig.java new file mode 100644 index 00000000..44e06dbf --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorConfig.java @@ -0,0 +1,34 @@ +// +// $Id: EditorConfig.java 1352 2002-03-28 23:55:21Z ray $ + +package com.threerings.stage.tools.editor; + +import com.samskivert.util.Config; + +/** + * Provides access to configuration data for the editor. + */ +public class EditorConfig +{ + /** Provides access to config data for this package. */ + public static Config config = new Config("rsrc/config/stage/tools/editor"); + + /** + * Accessor method for getting the test tile directory. + */ + public static String getTestTileDirectory () + { + return config.getValue(TESTTILE_KEY, TESTTILE_DEF); + } + + /** + * Accessor method for setting the test tile directory. + */ + public static void setTestTileDirectory (String newvalue) + { + config.setValue(TESTTILE_KEY, newvalue); + } + + private static final String TESTTILE_KEY = "testtiledir"; + private static final String TESTTILE_DEF = "."; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorFrame.java b/src/java/com/threerings/stage/tools/editor/EditorFrame.java new file mode 100644 index 00000000..a6a58814 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorFrame.java @@ -0,0 +1,563 @@ +// +// $Id: EditorFrame.java 19411 2005-02-19 00:28:49Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.BorderLayout; +import java.awt.EventQueue; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.MouseWheelEvent; +import java.awt.event.MouseWheelListener; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; + +import java.io.File; + +import javax.swing.JComponent; +import javax.swing.JFileChooser; +import javax.swing.JInternalFrame; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollBar; +import javax.swing.KeyStroke; +import javax.swing.filechooser.FileFilter; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.util.MenuUtil; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.media.ManagedJFrame; + +import com.threerings.miso.tile.BaseTileSet; +import com.threerings.miso.util.MisoSceneMetrics; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneModel; + +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; +import com.threerings.stage.tools.xml.StageSceneParser; +import com.threerings.stage.tools.xml.StageSceneWriter; + +public class EditorFrame extends ManagedJFrame +{ + public EditorFrame () + { + // treat a closing window as a request to quit + addWindowListener(new WindowAdapter () { + public void windowClosing (WindowEvent e) { + handleQuit(null); + } + }); + + // set our initial window title + setFilePath(null); + } + + public void init (EditorContext ctx, String target) + { + _ctx = ctx; + + // create the editor data model for reference by the panels + _model = new EditorModel(_ctx.getTileManager()); + + // create the file chooser used for saving and loading scenes + if (target == null) { + target = EditorConfig.config.getValue( + "editor.last_dir", System.getProperty("user.dir")); + } + _chooser = (target == null) ? + new JFileChooser() : new JFileChooser(target); + _chooser.setFileFilter(new FileFilter () { + public boolean accept (File f) { + return (f.isDirectory() || f.getName().endsWith(".xml")); + } + public String getDescription () { + return "XML Files"; + } + }); + + // instead of using a popup, we slip the file chooser into the + // main interface (so the editor can run full-screen); thus we + // can't use the standard business and have to add an action + // listen to our file chooser + _chooser.addActionListener(_openListener); + + // set up the menu bar + createMenuBar(); + + // create a top-level panel to manage everything + JPanel top = new JPanel(new BorderLayout()); + + // create a sub-panel to contain the toolbar and scene view + _main = new JPanel(new BorderLayout()); + + // set up the scene view panel with a default scene + _svpanel = new EditorScenePanel(_ctx, this, _model); + _main.add(_svpanel, BorderLayout.CENTER); + + // create a toolbar for action selection and other options + JPanel upper = new JPanel( + new HGroupLayout(GroupLayout.NONE, GroupLayout.LEFT)); + upper.add(new EditorToolBarPanel(_ctx.getTileManager(), _model), + GroupLayout.FIXED); + _sceneInfoPanel = new SceneInfoPanel(_ctx, _model, _svpanel); + upper.add(_sceneInfoPanel, GroupLayout.FIXED); + _main.add(upper, BorderLayout.NORTH); + + // create a couple of scroll bars + JScrollBar horiz = new JScrollBar(JScrollBar.HORIZONTAL); + horiz.setBlockIncrement(500); + horiz.setModel(_svpanel.getHorizModel()); + _main.add(horiz, BorderLayout.SOUTH); + JScrollBar vert = new JScrollBar(JScrollBar.VERTICAL); + vert.setBlockIncrement(500); + vert.setModel(_svpanel.getVertModel()); + _main.add(vert, BorderLayout.EAST); + + // add the scene view and toolbar to the top-level panel + top.add(_main, BorderLayout.CENTER); + + // set up our left sidebar panel + JPanel west = GroupLayout.makeVStretchBox(5); + _tpanel = new TileInfoPanel(_ctx, _model); + west.add(_tpanel); + + JPanel boxPane = GroupLayout.makeHBox(); + boxPane.add(_scrollBox = new EditorScrollBox(_svpanel)); + west.add(boxPane, GroupLayout.FIXED); + + // add the sub-panel to the top panel + top.add(west, BorderLayout.WEST); + + // now add our top-level panel + getContentPane().add(top, BorderLayout.CENTER); + + // observe mouse-wheel events in the scene view panel to allow + // scrolling the wheel to select the tile to be placed + _svpanel.addMouseWheelListener(new MouseWheelListener() { + public void mouseWheelMoved (MouseWheelEvent e) { + if (e.getWheelRotation() < 0) { + _tpanel.selectPreviousTile(); + } else { + _tpanel.selectNextTile(); + } + } + }); + + // finally set a default scene + newScene(); + } + + /** + * Create the menu bar and menu items and add them to the frame. + */ + public void createMenuBar () + { + // create the "File" menu + JMenu menuFile = new JMenu("File"); + createFileMenu(menuFile); + + // create the "Edit" menu +// JMenu menuEdit = new JMenu("Edit"); +// MenuUtil.addMenuItem(menuEdit, "Undo", this, "handleUndo"); +// MenuUtil.addMenuItem(menuEdit, "Cut", this, "handleCut"); +// MenuUtil.addMenuItem(menuEdit, "Copy", this, "handleCopy"); +// MenuUtil.addMenuItem(menuEdit, "Paste", this, "handlePaste"); +// MenuUtil.addMenuItem(menuEdit, "Clear", this, "handleClear"); +// MenuUtil.addMenuItem(menuEdit, "Select All", this, "handleSelectAll"); +// menuEdit.setMnemonic(KeyEvent.VK_E); + + // create the "Actions" menu + JMenu menuActions = new JMenu("Actions"); + createActionsMenu(menuActions); + + // create the "Settings" menu + KeyStroke accel = null; + JMenu menuSettings = new JMenu("Settings"); + menuSettings.setMnemonic(KeyEvent.VK_S); + accel = KeyStroke.getKeyStroke( + KeyEvent.VK_SEMICOLON, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuSettings, "Preferences...", KeyEvent.VK_P, + accel, this, "handlePreferences"); + + // create the menu bar + JMenuBar bar = new JMenuBar(); + bar.add(menuFile); + // bar.add(menuEdit); + bar.add(menuActions); + bar.add(menuSettings); + + // add the menu bar to the frame + setJMenuBar(bar); + } + + protected void createFileMenu (JMenu menuFile) + { + KeyStroke accel = null; + menuFile.setMnemonic(KeyEvent.VK_F); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "New", KeyEvent.VK_N, accel, + this, "handleNew"); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "Open", KeyEvent.VK_O, accel, + this, "handleOpen"); + //addMenuItem(menuFile, "Close", KeyEvent.VK_C); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "Save", KeyEvent.VK_S, accel, + this, "handleSave"); + MenuUtil.addMenuItem(menuFile, "Save As", KeyEvent.VK_A, null, + this, "handleSaveAs"); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuFile, "Quit", KeyEvent.VK_Q, accel, + this, "handleQuit"); + } + + protected void createActionsMenu (JMenu menuActions) + { + KeyStroke accel = null; + MenuUtil.addMenuItem(menuActions, "Load (reload) test tiles", this, + "handleTestTiles"); + menuActions.setMnemonic(KeyEvent.VK_A); + + accel = KeyStroke.getKeyStroke(KeyEvent.VK_D, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuActions, "Make default base tile", + KeyEvent.VK_M, accel, this, "handleSetDefBase"); + + accel = KeyStroke.getKeyStroke(KeyEvent.VK_M, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuActions, "Update mini view", + KeyEvent.VK_M, accel, this, "updateMiniView"); + } + + protected void setScene (StageScene scene) + { + // save off the scene objects + _scene = scene; + + // display the name of the scene + _sceneInfoPanel.setScene(_scene); + + // set up the scene view panel with the new scene + _svpanel.setScene(_scene); + _svpanel.repaint(); + } + + protected void newScene () + { + try { + MisoSceneMetrics metrics = _svpanel.getSceneMetrics(); + StageSceneModel model = StageSceneModel.blankStageSceneModel(); + model.type = StageSceneModel.WORLD; + setScene(new StageScene(model, null)); + + } catch (Exception e) { + Log.warning("Unable to set blank scene."); + Log.logStackTrace(e); + } + } + + /** + * Creates a blank scene and configures the editor to begin editing + * it. Eventually this should make sure the user has a chance to save + * any scene for which editing is currently in progress. + */ + public void handleNew (ActionEvent evt) + { + // clear out the fiel path so that "Save" pops up "Save as" rather + // than overwriting whatever you were editing before + setFilePath(null); + + newScene(); + } + + /** + * Presents the user with an open file dialog and loads the scenes + * from the selected file. + */ + public void handleOpen (ActionEvent evt) + { + _chooser.setDialogType(JFileChooser.OPEN_DIALOG); + _main.remove(_svpanel); + _main.add(_chooser, BorderLayout.CENTER); + SwingUtil.refresh(_main); + } + + /** + * Loads the scene from the specified path. + */ + public void openScene (String path) + { + String errmsg = null; + + // attempt loading and installation of the scene + try { + StageSceneModel model = (StageSceneModel)_parser.parseScene(path); + if (model == null) { + errmsg = "No scene data found"; + } else { + setScene(new StageScene(model, null)); + // keep track of the path for later saving + setFilePath(path); + } + + } catch (Exception e) { + errmsg = "Parse error: " + e; + Log.logStackTrace(e); + } + + if (errmsg != null) { + errmsg = "Unable to load scene from " + path + ":\n" + errmsg; + JOptionPane.showMessageDialog( + this, errmsg, "Load error", JOptionPane.ERROR_MESSAGE); + } + } + + /** + * Save the scenes to the file they were last associated with. + */ + public void handleSave (ActionEvent evt) + { + if (_filepath == null) { + handleSaveAs(evt); + return; + } + + if (!checkSaveOk()) { + return; + } + + try { + // we don't write directly to the file in question, in case + // something craps out during the writing process + File tmpfile = new File(_filepath + ".tmp"); + _writer.writeScene(tmpfile, _scene.getSceneModel()); + + // now that we've successfully written the new file, delete + // the old file + File sfile = new File(_filepath); + if (!sfile.delete()) { + Log.warning("Aiya! Not able to remove " + _filepath + + " so that we can replace it with " + + tmpfile.getPath() + "."); + } + + // now rename the new save file into place + if (!tmpfile.renameTo(sfile)) { + Log.warning("Fork! Not able to rename " + tmpfile.getPath() + + " to " + _filepath + "."); + } + + } catch (Exception e) { + String errmsg = "Unable to save scene to " + _filepath + ":\n" + e; + JOptionPane.showMessageDialog( + this, errmsg, "Save error", JOptionPane.ERROR_MESSAGE); + Log.warning("Error writing scene " + + "[fname=" + _filepath + ", e=" + e + "]."); + Log.logStackTrace(e); + } + } + + /** + * Check to see if the save can proceed, and pop up errors if it + * can't. + */ + protected boolean checkSaveOk () + { + String name = _scene.getName(); + String type = _scene.getType(); + + String err = ""; + if (name.equals("") || name.indexOf("<") != -1 || + name.indexOf(">") != -1) { + err += "Invalid scene name.\n"; + } + + if (type.equals("")) { + err += "No scene type specified.\n"; + } + + if (_svpanel.getEntrance() == null) { + err += "No default entrance portal.\n"; + } + + if ("".equals(err)) { + return true; + } else { + JOptionPane.showMessageDialog( + this, err + "\nPlease fix and try again.", + "Won't save: scene is broken!", + JOptionPane.ERROR_MESSAGE); + return false; + } + } + + /** + * Present the user with a save file dialog and save the scenes to + * the selected file. + */ + public void handleSaveAs (ActionEvent evt) + { + _chooser.setDialogType(JFileChooser.SAVE_DIALOG); + _main.remove(_svpanel); + _main.add(_chooser, BorderLayout.CENTER); + SwingUtil.refresh(_main); + } + + /** + * Keeps our file path around and conveys that information in the + * window title. + */ + protected void setFilePath (String filepath) + { + _filepath = filepath; + setTitle("Narya Scene Editor: " + + (_filepath == null ? "" : _filepath)); + } + + /** + * Handles a request to quit. Presently this just quits, but + * eventually it should give the user the chance to save any edits in + * progress. + */ + public void handleQuit (ActionEvent evt) + { + System.exit(0); + } + + /** + * Handles a request to reload the test tiles. + */ + public void handleTestTiles (ActionEvent evt) + { + // don't instantiate a testLoader until we actually need one + if (_testLoader == null) { + _testLoader = new TestTileLoader(); + } + + _tpanel.insertTestTiles(_testLoader.loadTestTiles()); + } + + /** + * Update the mini view in the scrollbox. + */ + public void updateMiniView (ActionEvent evt) + { + _scrollBox.updateView(); + } + + /** + * Handles a request to open the preferences dialog. + */ + public void handlePreferences (ActionEvent evt) + { + PreferencesDialog pd = new PreferencesDialog(); + EditorDialogUtil.display(this, pd); + } + + /** + * Make the currently selected base tile into the scene's default + * tile. + */ + public void handleSetDefBase (ActionEvent evt) + { + if (_model.getTileSet() instanceof BaseTileSet) { + _svpanel.updateDefaultTileSet(_model.getTileSetId()); + } else { + Log.warning("Not making non-base tileset into default " + + _model.getTileSet() + "."); + } + } + + /** Handles JFileChooser responses when opening files. */ + protected ActionListener _openListener = new ActionListener() { + public void actionPerformed (ActionEvent event) { + String cmd = event.getActionCommand(); + + // restore the scene view + _main.remove(_chooser); + _main.add(_svpanel); + SwingUtil.refresh(_main); + + // load up the scene if they selected one + if (cmd.equals(JFileChooser.APPROVE_SELECTION)) { + File filescene = _chooser.getSelectedFile(); + switch (_chooser.getDialogType()) { + case JFileChooser.OPEN_DIALOG: + openScene(filescene.getPath()); + EditorConfig.config.setValue( + "editor.last_dir", filescene.getParent()); + break; + + case JFileChooser.SAVE_DIALOG: + setFilePath(filescene.getPath()); + handleSave(null); + break; + + default: + Log.warning("Wha? Weird dialog type " + + _chooser.getDialogType() + "."); + break; + } + } + + // oh god the hackery; on linux at least, the focus seems + // to be hosed after we hide the chooser and we can't just + // request to move it somewhere useful because it just + // ignores us; so instead we have to wait for the current + // event queue to flush before we can transfer focus + EventQueue.invokeLater(new Runnable() { + public void run () { + _sceneInfoPanel.requestFocusInWindow(); + } + }); + } + }; + + /** The scene currently undergoing edit. */ + protected StageScene _scene; + + /** The file last associated with the current scene. */ + protected String _filepath; + + /** Contains the scene view panel or other fun stuff. */ + protected JPanel _main; + + /** Used for displaying dialogs. */ + protected JInternalFrame _dialog; + + /** The file chooser used for loading and saving scenes. */ + protected JFileChooser _chooser; + + /** The panel that displays the scene view. */ + protected EditorScenePanel _svpanel; + + /** The panel that displays tile info. */ + protected TileInfoPanel _tpanel; + + /** The panel that displays scene info. */ + protected SceneInfoPanel _sceneInfoPanel; + + /** The scrollbox used to display the view position within the scene. */ + protected EditorScrollBox _scrollBox; + + /** The editor data model. */ + protected EditorModel _model; + + /** The editor context. */ + protected EditorContext _ctx; + + /** We use this to load scenes. */ + protected StageSceneParser _parser = new StageSceneParser(); + + /** We use this to save scenes. */ + protected StageSceneWriter _writer = new StageSceneWriter(); + + /** The test tileset loader. */ + protected TestTileLoader _testLoader; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorModel.java b/src/java/com/threerings/stage/tools/editor/EditorModel.java new file mode 100644 index 00000000..8936e7c5 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorModel.java @@ -0,0 +1,262 @@ +// +// $Id: EditorModel.java 17780 2004-11-10 23:00:07Z ray $ + +package com.threerings.stage.tools.editor; + +import java.util.ArrayList; + +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.util.DirectionCodes; + +/** + * The EditorModel class provides a holding place for storing, + * modifying and retrieving data that is shared across the Editor + * application and its myriad UI components. + */ +public class EditorModel +{ + /** Action mode constants. */ + public static final int ACTION_PLACE_TILE = 0; + public static final int ACTION_EDIT_TILE = 1; + public static final int ACTION_PLACE_PORTAL = 2; + + /** The number of actions. */ + public static final int NUM_ACTIONS = 3; + + /** Miso layer constants. */ + public static final int BASE_LAYER = 0; + public static final int OBJECT_LAYER = 1; + public static final String[] LAYER_NAMES = { "Base", "Object" }; + + /** Prose descriptions of actions suitable for tool tip display. */ + public static final String[] TIP_ACTIONS = { + "Place/Delete tiles.", "Edit object tiles.", + "Create/Edit/Delete portal." + }; + + /** String translations for action identifiers. */ + public static final String[] CMD_ACTIONS = { + "place_tile", "edit_tile", "place_portal" + }; + + public EditorModel (TileManager tilemgr) + { + _tilemgr = tilemgr; + _tileSetId = _tileIndex = -1; + _lnum = _mode = 0; + } + + /** + * Add an editor model listener. + * + * @param l the listener. + */ + public void addListener (EditorModelListener l) + { + if (!_listeners.contains(l)) { + _listeners.add(l); + } + } + + /** + * Notify all model listeners that the editor model has changed. + */ + protected void notifyListeners (int event) + { + int size = _listeners.size(); + for (int ii = 0; ii < size; ii++) { + ((EditorModelListener)_listeners.get(ii)).modelChanged(event); + } + } + + /** + * Returns whether the currently selected tile is valid. + */ + public boolean isTileValid () + { + return (_tile != null); + } + + /** + * Returns the current editor action mode. + */ + public int getActionMode () + { + return _mode; + } + + /** + * Returns the current scene layer index undergoing edit. + */ + public int getLayerIndex () + { + return _lnum; + } + + /** + * Returns the currently selected tile set. + */ + public TileSet getTileSet () + { + return _tileSet; + } + + /** + * Returns the currently selected tile set id. + */ + public int getTileSetId () + { + return _tileSetId; + } + + /** + * Returns the currently selected tile id within the selected tile + * set. + */ + public int getTileId () + { + return _tileIndex; + } + + /** + * Returns the currently selected tile for placement within the + * scene. + */ + public Tile getTile () + { + return _tile; + } + + /** + * Returns the fully qualified tile id of the currently selected tile. + */ + public int getFQTileId () + { + return _fqTileId; + } + + /** + * Marks the currently selected tile as invalid such that editing + * when no tiles are available can be properly effected. + */ + public void clearTile () + { + _tileSet = null; + _tileSetId = -1; + _tileIndex = -1; + _tile = null; + } + + /** + * Sets the editor action mode. The specified mode should be one + * of CMD_ACTIONS. + */ + public void setActionMode (String cmd) + { + for (int ii = 0; ii < CMD_ACTIONS.length; ii++) { + if (CMD_ACTIONS[ii].equals(cmd)) { + _mode = ii; + notifyListeners(EditorModelListener.ACTION_MODE_CHANGED); + return; + } + } + + Log.warning("Attempt to set to unknown mode [cmd=" + cmd + "]."); + } + + /** + * Sets the scene layer index undergoing edit. + */ + public void setLayerIndex (int lnum) + { + if (lnum != _lnum) { + _lnum = lnum; + notifyListeners(EditorModelListener.LAYER_INDEX_CHANGED); + } + } + + /** + * Sets the selected tile for placement within the scene. + */ + public void setTile (TileSet set, int tileSetId, int tileIndex) + { + _tile = set.getTile(tileIndex); + _tileSet = set; + _tileSetId = tileSetId; + _tileIndex = tileIndex; + _fqTileId = TileUtil.getFQTileId(tileSetId, tileIndex); + notifyListeners(EditorModelListener.TILE_CHANGED); + } + + /** + * Sets the tile id of the tile selected for placement within the + * scene. + */ + public void setTileId (int tileIndex) + { + setTile(_tileSet, _tileSetId, tileIndex); + } + + /** + * Sets the direction in which we should grip objects. + */ + public void setObjectGripDirection (int direction) + { + _objectGrip = direction; + } + + /** + * Gets the direction in which we should grip objects. + */ + public int getObjectGripDirection () + { + return _objectGrip; + } + + /** + * Returns a string representation of the editor model. + */ + public String toString () + { + StringBuilder buf = new StringBuilder(); + buf.append("[set=").append(_tileSet); + buf.append(", tid=").append(_tileIndex); + buf.append(", lnum=").append(_lnum); + buf.append(", tile=").append(_tile); + return buf.append("]").toString(); + } + + /** The currently selected action mode. */ + protected int _mode; + + /** The currently selected tileset. */ + protected TileSet _tileSet; + + /** The currently selected tileset id. */ + protected int _tileSetId; + + /** The currently selected tile id. */ + protected int _tileIndex; + + /** The fully qualified tile id of the currently selected tile. */ + protected int _fqTileId; + + /** The currently selected layer number. */ + protected int _lnum; + + /** The currently selected tile for placement in the scene. */ + protected Tile _tile; + + /** The model listeners. */ + protected ArrayList _listeners = new ArrayList(); + + /** The tile manager. */ + protected TileManager _tilemgr; + + /** Direction (which corner) we grip an object by. */ + protected int _objectGrip = DirectionCodes.SOUTH; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorModelListener.java b/src/java/com/threerings/stage/tools/editor/EditorModelListener.java new file mode 100644 index 00000000..3fc7cd34 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorModelListener.java @@ -0,0 +1,24 @@ +// +// $Id: EditorModelListener.java 217 2001-11-29 00:29:31Z mdb $ + +package com.threerings.stage.tools.editor; + +/** + * The editor model listener interface should be implemented by + * classes that would like to be notified when the editor model is + * changed. + * + * @see EditorModel + */ +public interface EditorModelListener +{ + /** + * Called by the {@link EditorModel} when the model is changed. + */ + public void modelChanged (int event); + + /** Notification event constants. */ + public static final int ACTION_MODE_CHANGED = 0; + public static final int LAYER_INDEX_CHANGED = 1; + public static final int TILE_CHANGED = 2; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorScenePanel.java b/src/java/com/threerings/stage/tools/editor/EditorScenePanel.java new file mode 100644 index 00000000..72dada34 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorScenePanel.java @@ -0,0 +1,1145 @@ +// +// $Id: EditorScenePanel.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.Stroke; + +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.awt.event.MouseEvent; +import java.awt.geom.Ellipse2D; + +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.BoundedRangeModel; +import javax.swing.DefaultBoundedRangeModel; +import javax.swing.JFrame; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import com.samskivert.swing.Controller; +import com.samskivert.swing.TGraphics2D; +import com.samskivert.swing.util.SwingUtil; +import com.samskivert.util.RandomUtil; +import com.samskivert.util.StringUtil; +import com.threerings.util.DirectionCodes; + +import com.threerings.media.tile.ObjectTile; +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileUtil; + +import com.threerings.whirled.spot.tools.EditablePortal; +import com.threerings.whirled.spot.data.Portal; + +import com.threerings.miso.client.ObjectActionHandler; +import com.threerings.miso.client.SceneBlock; +import com.threerings.miso.client.SceneObject; +import com.threerings.miso.data.MisoSceneModel; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.util.MisoUtil; + +import com.threerings.stage.client.StageScenePanel; +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageMisoSceneModel; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; +import com.threerings.stage.tools.editor.util.ExtrasPainter; + +/** + * Displays the scene view and handles UI events on the scene. Various + * actions may be performed on the scene depending on the selected action + * mode, including placing and deleting tiles or locations and creating + * portals. + */ +public class EditorScenePanel extends StageScenePanel + implements EditorModelListener, KeyListener, ChangeListener +{ + /** + * Constructs the editor scene view panel. + */ + public EditorScenePanel (EditorContext ctx, JFrame frame, EditorModel model) + { + super(ctx, new Controller() { + }); + + // keep these around for later + _ctx = ctx; + _frame = frame; + + // save off references to our objects + _emodel = model; + _emodel.addListener(this); + + // listen for alt keys + ctx.getKeyDispatcher().addGlobalKeyListener(this); + + // listen to our range models and scroll our badself + _horizRange.addChangeListener(this); + _vertRange.addChangeListener(this); + } + + /** + * Returns a range model that controls the scrollability of the scene + * in the horizontal direction. + */ + public BoundedRangeModel getHorizModel () + { + return _horizRange; + } + + /** + * Returns a range model that controls the scrollability of the scene + * in the vertical direction. + */ + public BoundedRangeModel getVertModel () + { + return _vertRange; + } + + // documentation inherited from interface + public void stateChanged (ChangeEvent e) + { + setViewLocation(_horizRange.getValue(), _vertRange.getValue()); + _refreshBox = true; + } + + // documentation inherited + public void setSceneModel (MisoSceneModel model) + { + super.setSceneModel(model); + + // compute the "area" in which we'll allow the view to scroll + computeScrollArea(); + } + + /** + * Updates the default tileset in the currently edited scene. + */ + public void updateDefaultTileSet (int tileSetId) + { + _model.setDefaultBaseTileSet(tileSetId); + _blocks.clear(); + rethink(); + } + + // documentation inherited + public void setBounds (int x, int y, int width, int height) + { + super.setBounds(x, y, width, height); + + updateScrollArea(_horizRange.getValue(), _vertRange.getValue()); + } + + /** + * Computes the area in which the view is allowed to scroll. + */ + protected void computeScrollArea () + { + StageMisoSceneModel ysmodel = (StageMisoSceneModel)_model; + _area = null; + for (Iterator iter = ysmodel.getSections(); iter.hasNext(); ) { + StageMisoSceneModel.Section sect = + (StageMisoSceneModel.Section)iter.next(); + Rectangle sbounds = MisoUtil.getFootprintPolygon( + _metrics, sect.x, sect.y, + ysmodel.swidth, ysmodel.sheight).getBounds(); + if (_area == null) { + _area = sbounds; + } else { + _area.add(sbounds); + } + } + + // if we have no blocks, fake something up to start + if (_area == null) { + _area = new Rectangle(-250, -250, 500, 500); + } + + updateScrollArea(_horizRange.getValue(), _vertRange.getValue()); + } + + /** + * Updates our bounded range models to reflect potential changes in + * the viewable area and the scrollable area. + */ + protected void updateScrollArea (int hval, int vval) + { + int hmax = _area.x+_area.width; + hval = Math.min(hval, hmax-_vbounds.width); + _horizRange.setRangeProperties( + hval, _vbounds.width, _area.x, hmax, false); + + int vmax = _area.y+_area.height; + vval = Math.min(vval, vmax-_vbounds.height); + _vertRange.setRangeProperties( + vval, _vbounds.height, _area.y, vmax, false); + +// Log.info("Updated extents area:" + StringUtil.toString(_area) + +// " vb:" + StringUtil.toString(_vbounds) + "."); + // update the dimensions of the scrollbox + + // possibly refresh the dimensions of the box + // and queue a repaint for it + SwingUtil.refresh(_box); + _refreshBox = true; + } + + /** + * Handle placing the currently selected tile at the given screen + * coordinates in the scene. + */ + protected void placeTile (int x, int y) + { + Rectangle drag = clearTileSelectRegion(x, y); + + // sanity check + if (!_emodel.isTileValid()) { + return; + } + + switch (_emodel.getLayerIndex()) { + case EditorModel.BASE_LAYER: + updateBaseTiles(x, y, drag, _emodel.getFQTileId(), + _emodel.getTileSetId(), + _emodel.getTileSet().getTileCount()); + break; + + case EditorModel.OBJECT_LAYER: + addObject((ObjectTile)_emodel.getTile(), + _emodel.getFQTileId(), x, y); + break; + } + + // potentially update our scrollable area + computeScrollArea(); + } + + /** + * Handle deleting the tile at the given screen coordinates from + * the scene. + */ + protected void deleteTile (int x, int y) + { + Rectangle drag = clearTileSelectRegion(x, y); + Log.info("Deleting " + drag); + + switch (_emodel.getLayerIndex()) { + case EditorModel.BASE_LAYER: + updateBaseTiles(x, y, drag, 0, 0, 1); + break; + + case EditorModel.OBJECT_LAYER: + if (drag != null) { + // locate any object that intersects this rectangle + ArrayList hits = new ArrayList(); + for (Iterator iter = _vizobjs.iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + if (scobj.objectFootprintOverlaps(drag)) { + hits.add(scobj); + } + } + // and delete 'em + for (int ii = 0; ii < hits.size(); ii++) { + deleteObject((SceneObject)hits.get(ii)); + } + + } else { + // delete the object tile over which the mouse is hovering + if (_hobject instanceof SceneObject) { + deleteObject((SceneObject)_hobject); + } + } + break; + } + } + + /** + * Used to place or delete base tiles. + */ + protected void updateBaseTiles (int x, int y, Rectangle drag, + int fqTileId, int tileSetId, int tileCount) + { + if (drag == null) { + setBaseTile(fqTileId, x, y); + } else { + setBaseTiles(drag, tileSetId, tileCount); + } + } + + /** + * Handle editing the tile at the given screen coordinates from the + * scene. If the tile is not an object tile, we don't do anything. + */ + protected void editTile (int x, int y) + { + // bail if we're not hovering over a scene object + if (_hobject == null || !(_hobject instanceof SceneObject)) { + return; + } + + // create our object editor dialog if we haven't yet + if (_objEditor == null) { + _objEditor = new ObjectEditorDialog(_ctx, this); + } + + // prepare and display our object editor dialog + _eobject = (SceneObject)_hobject; + _objEditor.prepare(_eobject); + EditorDialogUtil.display(_frame, _objEditor); + } + + /** + * Called by the {@link ObjectEditorDialog} when it is dismissed. + */ + protected void objectEditorDismissed () + { + recomputeVisible(); + _model.updateObject(_eobject.info); + _eobject = null; + } + + /** + * Pop up the portal dialog for the specified location. + */ + protected void editPortal (EditablePortal portal) + { + // create our portal dialog if we haven't yet + if (_dialogPortal == null) { + _dialogPortal = new PortalDialog(); + } + + // pass location information on to the dialog + _dialogPortal.prepare((StageScene)_scene, portal); + + // allow the user to edit the info + EditorDialogUtil.display(_frame, _dialogPortal); + + // this gets called when a portal is added, so go ahead and + // recompute them little buggers + recomputePortals(); + recomputeVisible(); + } + + // documentation inherited + public void modelChanged (int event) + { + switch (event) { + case ACTION_MODE_CHANGED: + switch (_emodel.getActionMode()) { + case EditorModel.ACTION_PLACE_TILE: + enableCoordHighlighting(false); + if (_emodel.isTileValid()) { + setPlacingTile(_emodel.getTile()); + } + break; + + case EditorModel.ACTION_EDIT_TILE: + setPlacingTile(null); + enableCoordHighlighting(false); + break; + + case EditorModel.ACTION_PLACE_PORTAL: + setPlacingTile(null); + enableCoordHighlighting(true); + break; + } + break; + + case TILE_CHANGED: + if (_emodel.isTileValid() && + _emodel.getActionMode() == EditorModel.ACTION_PLACE_TILE) { + setPlacingTile(_emodel.getTile()); + } + break; + } + + repaint(); + } + + // documentation inherited from interface + public void mousePressed (MouseEvent event) + { + int mx = event.getX(), my = event.getY(); + switch (_emodel.getActionMode()) { + case EditorModel.ACTION_PLACE_TILE: +// if (_emodel.getLayerIndex() == EditorModel.BASE_LAYER) { + setTileSelectRegion(getTileCoords(mx, my)); +// } + break; + + case EditorModel.ACTION_PLACE_PORTAL: + Point fcoords = getFullCoords(mx, my); + // mouse button three is delete + if (event.getButton() == MouseEvent.BUTTON3) { + deletePortal(fcoords.x, fcoords.y); + } else { + // if they clicked on an existing portal... + EditablePortal portal = (EditablePortal) + getPortal(fcoords.x, fcoords.y); + if (portal != null) { + // ...edit it... + editPortal(portal); + } else { + // ...otherwise create a new one + new PortalTool().init(this, mx, my); + } + } + break; + + default: + super.mousePressed(event); + break; + } + } + + // documentation inherited + protected boolean handleMousePressed (Object hobject, MouseEvent event) + { + // don't do the standard cluster and location stuff here + return false; + } + + // documentation inherited + public void mouseReleased (MouseEvent e) + { + super.mouseReleased(e); + + Point tc = getTileCoords(e.getX(), e.getY()); + switch (_emodel.getActionMode()) { + case EditorModel.ACTION_PLACE_TILE: + switch (e.getButton()) { + case MouseEvent.BUTTON1: + placeTile(tc.x, tc.y); + _refreshBox = true; + break; + + case MouseEvent.BUTTON2: + editTile(tc.x, tc.y); + break; + + case MouseEvent.BUTTON3: + deleteTile(tc.x, tc.y); + _refreshBox = true; + break; + } + break; + + case EditorModel.ACTION_EDIT_TILE: + editTile(tc.x, tc.y); + break; + + case EditorModel.ACTION_PLACE_PORTAL: + // nothing to do here; the portal tool handles all + break; + } + + repaint(); + } + + // documentation inherited + public void mouseMoved (MouseEvent e) + { + super.mouseMoved(e); + int x = e.getX(), y = e.getY(); + boolean repaint = false; + + // update the potential tile placement + if (_ptile != null) { + boolean changed; + + if (_ptile instanceof ObjectTile) { + if (changed = updateObjectTileCoords( + x, y, _ppos, (ObjectTile)_ptile)) { + _pscobj.relocateObject(_metrics, _ppos.x, _ppos.y); + } + } else { + changed = updateTileCoords(x, y, _ppos); + } + + if (changed) { + _validPlacement = + isTilePlacementValid(_ppos.x, _ppos.y, _ptile); + repaint = true; + } + } + + // update the highlighted portal's fine coordinates + if (_coordHighlighting) { + repaint = (updateCoordPos(x, y, _hfull) || repaint); + } + + // TODO: dirty things with a finer grain + if (repaint) { + repaint(); + } + } + + // documentation inherited + public void mouseDragged (MouseEvent e) + { + super.mouseDragged(e); + // do the same thing as when we move + mouseMoved(e); + } + + // documentation inherited + public void mouseExited (MouseEvent e) + { + super.mouseExited(e); + + // remove any highlighted tiles and placing tile + _ppos.setLocation(Integer.MIN_VALUE, 0); + _hfull.setLocation(Integer.MIN_VALUE, 0); + } + + // documentation inherited + public void keyPressed (KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ALT) { + // enable scene view tooltips + setShowFlags(SHOW_TIPS, true); + } + } + + // documentation inherited + public void keyReleased (KeyEvent e) + { + if (e.getKeyCode() == KeyEvent.VK_ALT) { + // disable scene view tooltips + setShowFlags(SHOW_TIPS, false); + } + } + + // documentation inherited + public void keyTyped (KeyEvent e) + { + // nothing + } + + // documentation inherited + protected void fireObjectAction ( + ObjectActionHandler handler, SceneObject scobj, ActionEvent event) + { + // do nothing in the editor thanksverymuch + } + + /** + * A place for subclasses to react to the hover object changing. + * One of the supplied arguments may be null. + */ + protected void hoverObjectChanged (Object oldHover, Object newHover) + { + super.hoverObjectChanged(oldHover, newHover); + + // we always repaint our objects when the hover changes + if (oldHover instanceof SceneObject) { + SceneObject oldhov = (SceneObject)oldHover; + _remgr.invalidateRegion(oldhov.getObjectFootprint().getBounds()); + } + if (newHover instanceof SceneObject) { + SceneObject newhov = (SceneObject)newHover; + _remgr.invalidateRegion(newhov.getObjectFootprint().getBounds()); + } + } + + /** + * Sets a base tile at the specified position in the scene (in tile + * coordinates). + */ + public void setBaseTile (int fqTileId, int x, int y) + { + if (!_model.setBaseTile(fqTileId, x, y)) { + return; + } + getBlock(x, y).updateBaseTile(fqTileId, x, y); + + // and recompute any surrounding fringe + for (int fx = x-1, xn = x+1; fx <= xn; fx++) { + for (int fy = y-1, yn = y+1; fy <= yn; fy++) { + getBlock(fx, fy).updateFringe(fx, fy); + } + } + } + + /** + * Set a region of tiles to a random selection from the supplied + * tileset. + */ + public void setBaseTiles (Rectangle r, int setId, int tileCount) + { + for (int x = r.x; x < r.x + r.width; x++) { + for (int y = r.y; y < r.y + r.height; y++) { + int index = RandomUtil.getInt(tileCount); + int fqTileId = TileUtil.getFQTileId(setId, index); + setBaseTile(fqTileId, x, y); + } + } + } + + /** + * Sets an object tile at the specified position in the scene (in tile + * coordinates). + */ + public void addObject (ObjectTile tile, int fqTileId, int x, int y) + { + Point p = new Point(x, y); + adjustObjectCoordsAccordingToGrip(p, tile); + + ObjectInfo oinfo = new ObjectInfo(fqTileId, x, y); + + // first attempt to add it to the appropriate scene block; this + // will fail if there's already a copy of the same object at this + // coordinate + if (getBlock(x, y).addObject(oinfo)) { + // create an object info and add it to the scene model + _model.addObject(oinfo); + // recompute our visible object set + recomputeVisible(); + } + } + + /** + * Deletes the object tile at the specified tile coordinates. + */ + public void deleteObject (SceneObject scobj) + { + // remove it from the scene model + if (_model.removeObject(scobj.info)) { + // clear the object out of its block + getBlock(scobj.info.x, scobj.info.y).deleteObject(scobj.info); + } else { + Log.warning("Requested to remove unknown object " + scobj + "."); + } + + // recompute our visible object set + recomputeVisible(); + + // make sure we clear the hover if that's what we're deleting + if (_hobject == scobj) { + _hobject = null; + } + } + + /** + * Sets the tile that is currently being placed. It will not be + * rendered until after a call to {@link #updateTileCoords} on the + * placing tile (which happens automatically when the mouse moves). + */ + public void setPlacingTile (Tile tile) + { + _ptile = tile; + + // if this is an object tile, create a temporary scene object we + // can use to perform calculations with the object while placing + if (_ptile instanceof ObjectTile) { + _pscobj = new SceneObject(this, new ObjectInfo(0, _ppos.x, _ppos.y), + (ObjectTile)tile); + } else { + _pscobj = null; + } + } + + /** + * Sets the start (in tile coords) of a mouse drag when placing + * a rectangular area of base tiles. + */ + public void setTileSelectRegion (Point drag) + { + _drag = drag; + } + + /** + * Clear and return the drag rectangle for selecting a rectangular + * region. + * + * @return null if the drag is the same as the supplied tile + * coordinates, a rectangle containing the selected region if it was + * different. + */ + public Rectangle clearTileSelectRegion (int x, int y) + { + Rectangle drect = null; + if (_drag != null && (x != _drag.x || y != _drag.y)) { + int w = 1 + ((x > _drag.x) ? (x - _drag.x) : (_drag.x - x)); + int h = 1 + ((y > _drag.y) ? (y - _drag.y) : (_drag.y - y)); + drect = new Rectangle( + Math.min(x, _drag.x), Math.min(y, _drag.y), w, h); + } + _drag = null; + return drect; + } + + /** + * Enables or disables highlighting of the tile over which the mouse + * is currently positioned. + */ + public void enableCoordHighlighting (boolean enabled) + { + _coordHighlighting = enabled; + } + + /** + * Deletes the portal at the specified full coordinates. + */ + public void deletePortal (int x, int y) + { + Portal port = getPortal(x, y); + if (port != null) { + _scene.removePortal(port); + recomputePortals(); + recomputeVisible(); + } + } + + /** + * Returns the portal that serves as the default entrance to this + * scene or null if no default is set. + */ + public Portal getEntrance () + { + return _scene.getDefaultEntrance(); + } + + /** + * Makes the specified portal the default entrance to this scene. + */ + public void setEntrance (Portal port) + { + _scene.setDefaultEntrance(port); + } + + // documentation inherited + protected void recomputeVisible () + { + super.recomputeVisible(); + + // see if any of our visible objects overlap and mark them as bad + // monkeys; we love N^2 algorithms + for (Iterator iter = _vizobjs.iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + scobj.setWarning(overlaps(scobj)); + } + } + + // documentation inherited + protected void warnVisible (SceneBlock block, Rectangle sbounds) + { + // nothing doing + } + + /** Helper function for {@link #recomputeVisible}. */ + protected boolean overlaps (SceneObject tobj) + { + for (Iterator iter = _vizobjs.iterator(); iter.hasNext(); ) { + SceneObject scobj = (SceneObject)iter.next(); + if (scobj != tobj && tobj.objectFootprintOverlaps(scobj) && + tobj.getPriority() == scobj.getPriority()) { + return true; + } + } + return false; + } + + // documentation inherited + protected void paintHighlights (Graphics2D gfx, Rectangle dirty) + { + Polygon hpoly = null; + + if (_hobject != null && _hobject instanceof SceneObject) { + SceneObject scobj = (SceneObject)_hobject; + hpoly = scobj.getObjectFootprint(); + + } + + if (_emodel.getActionMode() == EditorModel.ACTION_PLACE_TILE && + (hpoly == null || + _emodel.getLayerIndex() == EditorModel.BASE_LAYER)) { + hpoly = MisoUtil.getTilePolygon(_metrics, _hcoords.x, _hcoords.y); + } + + if (hpoly != null) { + gfx.setColor(Color.green); + gfx.draw(hpoly); + } + + // paint the highlighted full coordinate + if (_coordHighlighting && _hfull.x != Integer.MIN_VALUE) { + Point spos = new Point(); + MisoUtil.fullToScreen(_metrics, _hfull.x, _hfull.y, spos); + + // set the desired stroke and color + Stroke ostroke = gfx.getStroke(); + gfx.setStroke(HIGHLIGHT_STROKE); + + // draw a red circle at the coordinate + gfx.setColor(Color.red); + gfx.draw(new Ellipse2D.Float(spos.x - 1, spos.y - 1, 3, 3)); + + // restore the original stroke + gfx.setStroke(ostroke); + } + } + + // documentation inherited + protected void paintExtras (Graphics2D gfx, Rectangle dirty) + { + super.paintExtras(gfx, dirty); + + // we don't want to paint the 'extras' stuff to the copy.. + if (gfx instanceof TGraphics2D) { + gfx = ((TGraphics2D) gfx).getPrimary(); + } + + paintPortals(gfx); + paintHighlights(gfx, dirty); + paintPlacingTile(gfx); + + // and call into any extras painters + for (int ii = 0; ii < _extras.size(); ii++) { + ((ExtrasPainter)_extras.get(ii)).paintExtras(gfx); + } + } + + /** + * Add an extras painter. + */ + protected void addExtrasPainter (ExtrasPainter painter) + { + _extras.add(painter); + } + + /** + * Remove the specified extras painter. + */ + protected void removeExtrasPainter (ExtrasPainter painter) + { + _extras.remove(painter); + } + + /** + * Paints a transparent image of the tile being placed and draws a + * highlight around the bounds of the tile's current prospective + * position. The highlight is drawn in green if the tile placement is + * valid, or red if not. + * + * @param gfx the graphics context. + */ + protected void paintPlacingTile (Graphics2D gfx) + { + // bail if we've no placing tile + if (_ptile == null || _ppos.x == Integer.MIN_VALUE) { + return; + } + + // draw a transparent rendition of the placing tile image + Composite ocomp = gfx.getComposite(); + gfx.setComposite(ALPHA_PLACING); + Shape bpoly; + if (_pscobj != null) { + _pscobj.paint(gfx); + bpoly = _pscobj.getObjectFootprint(); + + } else { + bpoly = MisoUtil.getTilePolygon(_metrics, _ppos.x, _ppos.y); + Rectangle bounds = bpoly.getBounds(); + _ptile.paint(gfx, bounds.x, bounds.y); + } + gfx.setComposite(ocomp); + + // if we're dragging, grab that footprint + if (_drag != null) { + bpoly = MisoUtil.getMultiTilePolygon(_metrics, _ppos, _drag); + } + + // draw an outline around the tile footprint + gfx.setColor(_validPlacement ? Color.blue : Color.red); + gfx.draw(bpoly); + } + + /** + * Paint demarcations at all portals in the scene. + * + * @param gfx the graphics context. + */ + protected void paintPortals (Graphics2D gfx) + { + Iterator iter = _scene.getPortals(); + while (iter.hasNext()) { + paintPortal(gfx, (EditablePortal)iter.next()); + } + } + + /** + * Paint the specified portal. + */ + protected void paintPortal (Graphics2D gfx, EditablePortal port) + { + // get the portal's center coordinate + StageLocation loc = (StageLocation) port.loc; + Point spos = new Point(); + MisoUtil.fullToScreen(_metrics, loc.x, loc.y, spos); + int cx = spos.x, cy = spos.y; + + // translate the origin to center on the portal + gfx.translate(cx, cy); + + // rotate to reflect the portal orientation + double rot = (Math.PI / 4.0f) * loc.orient; + gfx.rotate(rot); + + // draw the triangle + gfx.setColor(Color.blue); + gfx.fill(_locTri); + + // outline the triangle in black + gfx.setColor(Color.black); + gfx.draw(_locTri); + + // draw the rectangle + gfx.setColor(Color.red); + gfx.fillRect(-1, 2, 3, 3); + + // restore the original transform + gfx.rotate(-rot); + gfx.translate(-cx, -cy); + + // highlight the portal if it's the default entrance + if (port.equals(_scene.getDefaultEntrance())) { + gfx.setColor(Color.cyan); + gfx.drawRect(spos.x - 5, spos.y - 5, 10, 10); + } + } + + /** + * Updates the coordinate position and returns true if it has changed. + */ + public boolean updateCoordPos (int x, int y, Point cpos) + { + Point npos = MisoUtil.screenToFull(_metrics, x, y, new Point()); + if (!cpos.equals(npos)) { + cpos.setLocation(npos.x, npos.y); + return true; + } else { + return false; + } + } + + /** + * Returns whether placing a tile at the given coordinates in the + * scene is valid. Makes sure placing an object fits within the scene + * and doesn't overlap any other objects. + */ + protected boolean isTilePlacementValid (int x, int y, Tile tile) + { + if (tile instanceof ObjectTile) { + // create a temporary scene object for this tile + SceneObject nobj = new SceneObject(this, new ObjectInfo(0, x, y), + (ObjectTile)tile); + // report invalidity if overlaps any existing objects + int ocount = _vizobjs.size(); + for (int ii = 0; ii < ocount; ii++) { + SceneObject scobj = (SceneObject)_vizobjs.get(ii); + if (scobj.objectFootprintOverlaps(nobj)) { + return false; + } + } + } + + return true; + } + + // documentation inherited + protected boolean skipHitObject (SceneObject scobj) + { + return false; // skip nothing + } + + /** + * Converts the supplied screen coordinates into tile coordinates for + * an object tile. (See {@link #updateTileCoords}.) + * + * @return true if the tile coordinates have changed. + */ + protected boolean updateObjectTileCoords (int sx, int sy, Point tpos, + ObjectTile otile) + { + Point npos = new Point(); + MisoUtil.screenToTile(_metrics, sx, sy, npos); + adjustObjectCoordsAccordingToGrip(npos, otile); + if (!tpos.equals(npos)) { + tpos.setLocation(npos.x, npos.y); + return true; + } else { + return false; + } + } + + /** + * Alter the position of the object according to which corner + * we are holding it by. + */ + protected void adjustObjectCoordsAccordingToGrip (Point p, ObjectTile tile) + { + int dy = Math.max(3, (tile.getHeight() / _metrics.tilehei)); + int dx = Math.max(3, (tile.getWidth() / _metrics.tilewid)); + + switch (_emodel.getObjectGripDirection()) { + case DirectionCodes.NORTH: + p.x += dy; + p.y += dy; + break; + + case DirectionCodes.WEST: + p.x += dx; + break; + + case DirectionCodes.EAST: + p.y += dx; + break; + + case DirectionCodes.NORTHWEST: + p.x += dy + (dx / 2); + p.y += dy - (dx / 2); + break; + + case DirectionCodes.NORTHEAST: + p.x += dy - (dx / 2); + p.y += dy + (dx / 2); + break; + + case DirectionCodes.SOUTHWEST: + p.x += (dx / 2); + p.y -= (dx / 2); + break; + + case DirectionCodes.SOUTHEAST: + p.x -= (dx / 2); + p.y += (dx / 2); + break; + } + } + + /** + * Sets the editor model. + */ + protected void setEditorModel (EditorModel model) + { + _emodel = model; + } + + /** + * Set the scroll box that tracks our view. + */ + public void setEditorScrollBox (EditorScrollBox box) + { + _box = box; + } + + // documentation inherited + protected void paint (Graphics2D gfx, Rectangle[] dirty) + { + // if we need to refresh the box and we have all the scene data, do it + if (_refreshBox && _visiBlocks.isEmpty()) { + _refreshBox = false; + Graphics2D mini = _box.getMiniGraphics(); + Graphics2D t = new TGraphics2D(gfx, mini); + super.paint(t, dirty); + mini.dispose(); + _box.repaint(); + + } else { + // otherwise, just do a normal fast paint + super.paint(gfx, dirty); + } + } + + /** Provides access to stuff. */ + protected EditorContext _ctx; + + /** Our editor model. */ + protected EditorModel _emodel; + + /** The scrollbox that tracks our view. */ + protected EditorScrollBox _box; + + /** Do we need to refresh the image being displayed in our scrollbox? */ + protected boolean _refreshBox; + + /** We need this to create our dialogs when they are needed. */ + protected JFrame _frame; + + /** Allows scrolling horizontally. */ + protected BoundedRangeModel _horizRange = new DefaultBoundedRangeModel(); + + /** Allows scrolling vertically. */ + protected BoundedRangeModel _vertRange = new DefaultBoundedRangeModel(); + + /** The virtual screen rectangle around which we scroll. */ + protected Rectangle _area; + + /** Whether or not coordinate highlighting is enabled. */ + protected boolean _coordHighlighting; + + /** The currently highlighted full coordinate. */ + protected Point _hfull = new Point(Integer.MIN_VALUE, 0); + + /** The location of the start of a tile drag in tile coords. */ + protected Point _drag = null; + + /** The position of the tile currently being placed. */ + protected Point _ppos = new Point(Integer.MIN_VALUE, 0); + + /** Used to track whether or not the current "placing" tile is in a + * valid position. */ + protected boolean _validPlacement = false; + + /** The tile currently being placed. */ + protected Tile _ptile; + + /** Metrics for the tile currently being placed if it is an object + * tile. */ + protected SceneObject _pscobj; + + /** A list of things that will do some extra painting for us. */ + protected ArrayList _extras = new ArrayList(); + + /** The dialog providing portal edit functionality. */ + protected PortalDialog _dialogPortal; + + /** The dialog providing object edit functionality. */ + protected ObjectEditorDialog _objEditor; + + /** The object currently being edited by the object editor dialog. */ + protected SceneObject _eobject; + + /** The triangle used to render a portal on-screen. */ + protected static Polygon _locTri; + + static { + _locTri = new Polygon(); + _locTri.addPoint(-3, -3); + _locTri.addPoint(3, -3); + _locTri.addPoint(0, 3); + }; + + /** The stroke object used to draw highlighted tiles and coordinates. */ + protected static final Stroke HIGHLIGHT_STROKE = new BasicStroke(2); + + /** Alpha level used to render transparent placing tile image. */ + protected static final Composite ALPHA_PLACING = + AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f); +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorScrollBox.java b/src/java/com/threerings/stage/tools/editor/EditorScrollBox.java new file mode 100644 index 00000000..36ddeabd --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorScrollBox.java @@ -0,0 +1,174 @@ +// +// $Id: EditorScrollBox.java 19363 2005-02-17 21:36:41Z ray $ + +package com.threerings.stage.tools.editor; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Graphics; +import java.awt.Graphics2D; + +import java.awt.event.MouseEvent; + +import java.awt.geom.AffineTransform; + +import java.awt.image.BufferedImage; + +import java.awt.event.ComponentAdapter; +import java.awt.event.ComponentEvent; + +import com.samskivert.swing.ScrollBox; +import com.samskivert.swing.util.SwingUtil; + +/** + * A scrollbox that shows our position in the editor. + */ +public class EditorScrollBox extends ScrollBox +{ + /** + * Create the tightly-coupled EditorScrollBox. + */ + public EditorScrollBox (EditorScenePanel panel) + { + super(panel.getHorizModel(), panel.getVertModel()); + + _panel = panel; + _panel.setEditorScrollBox(this); + _panel.addComponentListener(new ComponentAdapter() { + public void componentResized (ComponentEvent e) + { + SwingUtil.refresh(EditorScrollBox.this); + } + }); + + createMiniMap(1, 1); + } + + /** + * Update the view of the editor map. + */ + public void updateView () + { + _updatingView = true; + _horz.setValue(_horz.getMinimum()); + _vert.setValue(_vert.getMinimum()); + } + + /** + * Get the graphics to draw into when rendering onto the minimap. + */ + public Graphics2D getMiniGraphics () + { + Graphics2D gfx = (Graphics2D) _miniMap.getGraphics(); + gfx.transform(AffineTransform.getTranslateInstance(_box.x, _box.y)); + gfx.transform(AffineTransform.getScaleInstance( + _hFactor, _vFactor)); + + if (_updatingView) { + EventQueue.invokeLater(new Runnable() { + public void run () + { + doNextPieceOfUpdate(); + } + }); + } + + return gfx; + } + + /** + * Do the next piece of the update of the view. + */ + protected void doNextPieceOfUpdate () + { + int x = _horz.getValue(); + int y = _vert.getValue(); + int lastX = _horz.getMaximum() - _horz.getExtent(); + if (x != lastX) { + x = Math.min(x + _horz.getExtent(), lastX); + } else { + x = _horz.getMinimum(); + + int lastY = _vert.getMaximum() - _vert.getExtent(); + if (y != lastY) { + y = Math.min(y + _vert.getExtent(), lastY); + } else { + // we're done + _updatingView = false; + return; + } + } + + _horz.setValue(x); + _vert.setValue(y); + } + + // documentation inherited + public Dimension getPreferredSize () + { + int horz = _horz.getMaximum() - _horz.getMinimum(); + int vert = _vert.getMaximum() - _vert.getMinimum(); + int height = MAX_HEIGHT; + int width = (int) Math.round(horz * (((float) height) / vert)); + + int maxwidth = getParent().getWidth(); + if (maxwidth > 0 && width > maxwidth) { + height = (int) Math.round(height * (((float) maxwidth) / width)); + width = maxwidth; + } + + return new Dimension(width, height); + } + + // documentation inherited + public void setBounds (int x, int y, int w, int h) + { + super.setBounds(x, y, w, h); + + if ((w != _miniMap.getWidth()) || (h != _miniMap.getHeight())) { + createMiniMap(w, h); + } + } + + // documentation inherited + protected void paintBackground (Graphics g) + { + g.drawImage(_miniMap, 0, 0, null); + } + + /** + * Create a mini map image of the correct dimensions. + */ + protected void createMiniMap (int w, int h) + { + BufferedImage newMini = new BufferedImage( + w, h, BufferedImage.TYPE_INT_ARGB); + + // TODO: copy stuff over? + _miniMap = newMini; + Graphics g = _miniMap.getGraphics(); + g.setColor(Color.black); + g.fillRect(0, 0, w, h); + g.dispose(); + } + + // documentation inherited + protected boolean isActiveButton (MouseEvent e) + { + // all buttons are ok. + return true; + } + + /** The minimap image. */ + protected BufferedImage _miniMap; + + /** The panel we box for. */ + protected EditorScenePanel _panel; + + /** Whether or not we're updating our little view. */ + protected boolean _updatingView; + + /** Our maximum height. */ + protected static final int MAX_HEIGHT = 200; +} diff --git a/src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java b/src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java new file mode 100644 index 00000000..6eae8ef4 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/EditorToolBarPanel.java @@ -0,0 +1,110 @@ +// +// $Id: EditorToolBarPanel.java 17780 2004-11-10 23:00:07Z ray $ + +package com.threerings.stage.tools.editor; + +import java.awt.*; +import java.awt.event.*; +import java.util.ArrayList; +import javax.swing.*; +import com.samskivert.swing.*; + +import com.threerings.media.tile.Tile; +import com.threerings.media.tile.TileIcon; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.UniformTileSet; + +public class EditorToolBarPanel extends JPanel implements ActionListener +{ + public EditorToolBarPanel (TileManager tilemgr, EditorModel model) + { + _model = model; + + // use of flowlayout positions the toolbar and floats properly + setLayout(new FlowLayout(FlowLayout.LEFT)); + + // get our toolbar icons + UniformTileSet tbset = tilemgr.loadTileSet(ICONS_PATH, 40, 40); + + // create the toolbar + JToolBar toolbar = new JToolBar(); + + // add all of the toolbar buttons + _buttons = new ArrayList(); + for (int ii = 0; ii < EditorModel.NUM_ACTIONS; ii++) { + // get the button icon images + Tile tile = tbset.getTile(ii); + if (tile != null) { + String cmd = EditorModel.CMD_ACTIONS[ii]; + String tip = EditorModel.TIP_ACTIONS[ii]; + + // create the button + JButton b = addButton(toolbar, cmd, tip, new TileIcon(tile)); + + // add it to the set of buttons we're managing + _buttons.add(b); + + } else { + Log.warning("Unable to load toolbar icon " + + "[index=" + ii + "]."); + } + } + + // default to the first button + setSelectedButton((JButton)_buttons.get(0)); + + // add the toolbar + add(toolbar); + } + + protected JButton addButton (JToolBar toolbar, String cmd, String tip, + TileIcon icon) + { + // create the button and configure accordingly + JButton button = new JButton(new DimmedIcon(icon)); + button.setSelectedIcon(icon); + button.addActionListener(this); + button.setActionCommand("tbar_" + cmd); + button.setToolTipText(tip); + + // add the button to the toolbar + toolbar.add(button); + + return button; + } + + protected void setSelectedButton (JButton button) + { + for (int ii = 0; ii < _buttons.size(); ii++) { + JButton tb = (JButton)_buttons.get(ii); + tb.setSelected(tb == button); + } + } + + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.startsWith("tbar")) { + + // select the chosen mode in the toolbar + setSelectedButton((JButton)e.getSource()); + + // update the active mode in the model, stripping the + // "tbar_" prefix from the command string + _model.setActionMode(cmd.substring(5)); + + } else { + Log.warning("Unknown action command [cmd=" + cmd + "]."); + } + } + + /** The buttons in the tool bar. */ + protected ArrayList _buttons; + + /** The editor data model. */ + protected EditorModel _model; + + protected static final String ICONS_PATH = + "media/stage/tools/editor/toolbar_icons.png"; +} diff --git a/src/java/com/threerings/stage/tools/editor/Log.java b/src/java/com/threerings/stage/tools/editor/Log.java new file mode 100644 index 00000000..dcfd0ae7 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/Log.java @@ -0,0 +1,38 @@ +// +// $Id: Log.java 8859 2003-05-16 20:05:45Z mdb $ + +package com.threerings.stage.tools.editor; + +/** + * A placeholder class that contains a reference to the log object used by + * this package. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("stage.tools.editor"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java b/src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java new file mode 100644 index 00000000..79e4144b --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/ObjectEditorDialog.java @@ -0,0 +1,245 @@ + +// $Id: ObjectEditorDialog.java 16959 2004-08-30 22:09:53Z ray $ + +package com.threerings.stage.tools.editor; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.Arrays; + +import javax.swing.BorderFactory; +import javax.swing.JComboBox; +import javax.swing.JInternalFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JSlider; +import javax.swing.JTextField; + +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; +import com.samskivert.util.StringUtil; + +import com.threerings.media.image.ColorPository.ColorRecord; +import com.threerings.media.image.ColorPository; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.RecolorableTileSet; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.miso.client.SceneObject; + +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; + +/** + * Used to edit object attributes. + */ +public class ObjectEditorDialog extends JInternalFrame + implements ActionListener +{ + public ObjectEditorDialog (EditorContext ctx, EditorScenePanel panel) + { + super("Edit object attributes", true); + _ctx = ctx; + _panel = panel; + + // get a handle on the top-level panel + JPanel top = (JPanel)getContentPane(); + + // set up a layout manager for the panel + VGroupLayout gl = new VGroupLayout( + VGroupLayout.STRETCH, VGroupLayout.STRETCH, 5, VGroupLayout.CENTER); + top.setLayout(gl); + top.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // object action editor elements + JPanel sub = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + sub.add(new JLabel("Object action command:"), HGroupLayout.FIXED); + sub.add(_action = new JTextField()); + _action.addActionListener(this); + _action.setActionCommand("ok"); + top.add(sub); + + // create the priority slider + sub = new JPanel(new HGroupLayout(HGroupLayout.STRETCH)); + sub.add(new JLabel("Render priority:"), HGroupLayout.FIXED); + sub.add(_priority = new JSlider(-5, 5)); + _priority.setMajorTickSpacing(5); + _priority.setMinorTickSpacing(1); + _priority.setPaintTicks(true); + top.add(sub); + + // create colorization selectors + JPanel zations = HGroupLayout.makeButtonBox(HGroupLayout.LEFT); + zations.add(new JLabel("Colorizations:")); + zations.add(_primary = new JComboBox(NO_CHOICES)); + zations.add(_secondary = new JComboBox(NO_CHOICES)); + top.add(zations); + + // create our OK/Cancel buttons + sub = HGroupLayout.makeButtonBox(HGroupLayout.CENTER); + EditorDialogUtil.addButton(this, sub, "OK", "ok"); + EditorDialogUtil.addButton(this, sub, "Cancel", "cancel"); + top.add(sub); + + pack(); + } + + /** + * Prepare the dialog for display. This method should be called + * before display() is called. + * + * @param scobj the object to edit. + */ + public void prepare (SceneObject scobj) + { + _scobj = scobj; + + // set our title to the name of the tileset and the tile index + String title; + int tsid = TileUtil.getTileSetId(scobj.info.tileId); + int tidx = TileUtil.getTileIndex(scobj.info.tileId); + TileSet tset = null; + try { + tset = _ctx.getTileManager().getTileSet(tsid); + title = tset.getName() + ": " + tidx; + } catch (NoSuchTileSetException nstse) { + title = "Error(" + tsid + "): " + tidx; + } + title += " (" + StringUtil.coordsToString( + _scobj.info.x, _scobj.info.y) + ")"; + setTitle(title); + + // configure our elements + String atext = (scobj.info.action == null ? "" : scobj.info.action); + _action.setText(atext); + _priority.setValue(scobj.getPriority()); + + // if the object supports colorizations, configure those + boolean haveZations = false; + Object[] pzations = null; + Object[] szations = null; + if (tset != null) { + String[] zations = null; + if (tset instanceof RecolorableTileSet) { + zations = ((RecolorableTileSet)tset).getColorizations(); + } + if (zations != null) { + pzations = computeZations(zations, 0); + szations = computeZations(zations, 1); + } + } + configureZations(_primary, pzations, _scobj.info.getPrimaryZation()); + configureZations(_secondary, szations, + _scobj.info.getSecondaryZation()); + + // select the text edit field and focus it + _action.setCaretPosition(0); + _action.moveCaretPosition(atext.length()); + _action.requestFocusInWindow(); + } + + protected Object[] computeZations (String[] zations, int index) + { + if (zations.length <= index || StringUtil.isBlank(zations[index])) { + return null; + } + ColorPository cpos = _ctx.getColorPository(); + ColorRecord[] crecs = cpos.enumerateColors(zations[index]); + if (crecs == null) { + return null; + } + Object[] czations = new Object[crecs.length+1]; + czations[0] = new ZationChoice(0, "none"); + for (int ii = 0; ii < crecs.length; ii++) { + czations[ii+1] = + new ZationChoice(crecs[ii].colorId, crecs[ii].name); + } + Arrays.sort(czations); + return czations; + } + + protected void configureZations ( + JComboBox combo, Object[] zations, int colorId) + { + int selidx = 0; + combo.setEnabled(zations != null); + if (zations != null) { + combo.removeAllItems(); + for (int ii = 0; ii < zations.length; ii++) { + combo.addItem(zations[ii]); + if (((ZationChoice)zations[ii]).colorId == colorId) { + selidx = ii; + } + } + } + combo.setSelectedIndex(selidx); + } + + // documentation inherited from interface + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.equals("ok")) { + _scobj.info.action = _action.getText(); + byte prio = (byte)_priority.getValue(); + if (prio != _scobj.getPriority()) { + _scobj.setPriority(prio); + } + + int ozations = _scobj.info.zations; + ZationChoice pchoice = (ZationChoice)_primary.getSelectedItem(); + ZationChoice schoice = (ZationChoice)_secondary.getSelectedItem(); + _scobj.info.setZations(pchoice.colorId, schoice.colorId); + if (ozations != _scobj.info.zations) { + _scobj.refreshObjectTile(_panel); + } + + _panel.objectEditorDismissed(); + + } else if (cmd.equals("cancel")) { + // do nothing except hide the dialog + _panel.objectEditorDismissed(); + + } else { + System.err.println("Received unknown action: " + e); + return; + } + + // hide the dialog + EditorDialogUtil.dismiss(this); + } + + /** Used to display colorization choices. */ + protected static class ZationChoice + implements Comparable + { + public short colorId; + public String name; + + public ZationChoice (int colorId, String name) { + this.colorId = (short)colorId; + this.name = name; + } + + public int compareTo (Object other) { + return colorId - ((ZationChoice)other).colorId; + } + + public String toString () { + return name; + } + } + + protected EditorContext _ctx; + protected EditorScenePanel _panel; + protected JTextField _action; + protected JSlider _priority; + protected SceneObject _scobj; + protected JComboBox _primary, _secondary; + + protected static final ZationChoice[] NO_CHOICES = new ZationChoice[] { + new ZationChoice(0, "none") }; +} diff --git a/src/java/com/threerings/stage/tools/editor/PortalDialog.java b/src/java/com/threerings/stage/tools/editor/PortalDialog.java new file mode 100644 index 00000000..357f3018 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/PortalDialog.java @@ -0,0 +1,164 @@ +// +// $Id: PortalDialog.java 9371 2003-06-02 20:28:21Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.*; +import java.awt.event.*; +import javax.swing.*; + +import com.samskivert.swing.*; + +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.tools.EditablePortal; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.tools.editor.util.EditorDialogUtil; + +/** + * The PortalDialog is used to present the user with a dialog + * allowing them to enter the information associated with an + * EditablePortal. The dialog is used both to set up a new + * portal and to edit an existing portal. + */ +public class PortalDialog extends JInternalFrame + implements ActionListener +{ + /** + * Constructs the portal dialog. + */ + public PortalDialog () + { + super("Edit Portal", true); + + // get a handle on the top-level panel + JPanel top = (JPanel)getContentPane(); + + // set up a layout manager for the panel + GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + top.setLayout(gl); + top.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // add the dialog instruction text + top.add(new JLabel("Enter settings for this portal:")); + + // create a panel to contain the portal name info + JPanel sub = new JPanel(new HGroupLayout(GroupLayout.STRETCH)); + sub.add(new JLabel("Portal name:", SwingConstants.RIGHT)); + + // create and add the portal name text entry field + sub.add(_portalText = new JTextField()); + + // add the portal name info to the top-level panel + top.add(sub); + + // create a check box to allow making this the default + // entrance portal + _entrance = new JCheckBox("Default Entrance"); + _entrance.addActionListener(this); + _entrance.setActionCommand("entrance"); + top.add(_entrance); + + // create a panel to contain the OK/Cancel buttons + sub = new JPanel(new HGroupLayout()); + EditorDialogUtil.addButton(this, sub, "OK", "ok"); + + // add the buttons to the top-level panel + top.add(sub); + + pack(); + } + + /** + * Prepare the dialog for display. This method should be called + * before display() is called. + * + * @param port the portal to edit. + */ + public void prepare (StageScene scene, EditablePortal port) + { + _port = port; + _scene = scene; + + // if the location is already a portal, fill the text entry field + // with the current scene name, else clear it + String text = port.name; + _portalText.setText(text); + + // select the text edit field + _portalText.setCaretPosition(0); + _portalText.moveCaretPosition(text.length()); + + // select the default entrance check box appropriately + Portal entry = _scene.getDefaultEntrance(); + _entrance.setSelected(entry == null || + entry.portalId == _port.portalId); + + // request the keyboard focus so that the destination scene + // name can be typed immediately + _portalText.requestFocusInWindow(); + } + + /** + * Handle action events on the dialog user interface elements. + */ + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.equals("entrance")) { + _entrance.setSelected(_entrance.isSelected()); + + } else if (cmd.equals("ok")) { + handleSubmit(); + + } else { + Log.warning("Unknown action command [cmd=" + cmd + "]."); + } + } + + /** + * Handles the user submitting the dialog via the "OK" button. + */ + protected void handleSubmit () + { + // get the destination scene name + _port.name = _portalText.getText(); + + // update the scene's default entrance + if (_entrance.isSelected()) { + _scene.setDefaultEntrance(_port); + + } else if (_scene.getDefaultEntrance() == _port) { + _scene.setDefaultEntrance(null); + } + + // hide the dialog + EditorDialogUtil.dismiss(this); + } + + // documentation inherited + protected void processKeyEvent (KeyEvent e) + { + switch (e.getKeyCode()) { + case KeyEvent.VK_ENTER: handleSubmit(); break; + case KeyEvent.VK_ESCAPE: setVisible(false); break; + } + } + + /** The scene. */ + protected StageScene _scene; + + /** The portal name text entry field. */ + protected JTextField _portalText; + + /** The portal default entrance check box. */ + protected JCheckBox _entrance; + + /** The location object denoting the portal location. */ + protected EditablePortal _port; + + /** The combo box listing the direction orientations. */ + protected JComboBox _orientcombo; +} diff --git a/src/java/com/threerings/stage/tools/editor/PortalTool.java b/src/java/com/threerings/stage/tools/editor/PortalTool.java new file mode 100644 index 00000000..ce87e623 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/PortalTool.java @@ -0,0 +1,165 @@ +// +// $Id: PortalTool.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.event.MouseEvent; +import javax.swing.event.MouseInputAdapter; + +import java.util.Iterator; + +import com.threerings.miso.util.MisoUtil; +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.tools.EditablePortal; + +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageScene; +import com.threerings.stage.tools.editor.util.ExtrasPainter; + +/** + * A tool for setting a portal. + */ +public class PortalTool extends MouseInputAdapter + implements ExtrasPainter +{ + public void init (EditorScenePanel panel, int centerx, int centery) + { + _panel = panel; + _scene = (StageScene)_panel.getScene(); + + // adjust the center screen coordinate so that it is positioned at + // the closest full coordinate where a portal may be placed + Point p = _panel.getFullCoords(centerx, centery); + + // configure the portal with these full coordinates + _portal = new EditablePortal(); + _portal.loc = new StageLocation(p.x, p.y, (byte)DirectionCodes.NORTH); + + // we want to listen to drags and clicks + _panel.addMouseListener(this); + _panel.addMouseMotionListener(this); + _panel.addExtrasPainter(this); + + // now map that back to a screen coordinate + _center = _panel.getScreenCoords(p.x, p.y); + + calculateOrientation(centerx, centery); + } + + /** + * When the mouse is dragged we update the current portal. + */ + public void mouseDragged (MouseEvent event) + { + calculateOrientation(event.getX(), event.getY()); + } + + /** + * If button1 is released, we store the new portal, if button3: we + * cancel. + */ + public void mouseReleased (MouseEvent event) + { + switch (event.getButton()) { + case MouseEvent.BUTTON1: + savePortal(); + // fall through to BUTTON3 + + case MouseEvent.BUTTON3: + dispose(); + break; + } + } + + /** + * Paint what the clusters would look like if we were to add them to + * the scene. + */ + public void paintExtras (Graphics2D gfx) + { + _panel.paintPortal(gfx, _portal); + } + + /** + * Updates the orientation of the portal based on the current mouse + * position and potentially repaints. + */ + protected void calculateOrientation (int x, int y) + { + // get the orientation towards the center from the current x/y + int norient = MisoUtil.getProjectedIsoDirection( + x, y, _center.x, _center.y); + norient = DirectionUtil.getOpposite(norient); + if (norient == DirectionCodes.NONE) { + norient = DirectionCodes.NORTH; + } + + // if our orientation changed, repaint + StageLocation loc = (StageLocation) _portal.loc; + if (norient != loc.orient) { + loc.orient = (byte)norient; + _panel.repaint(); + } + } + + /** + * Adds our portal to the scene. + */ + protected void savePortal () + { + // try to come up with a reasonable name + byte orient = ((StageLocation) _portal.loc).orient; + String dirname = DirectionUtil.toString(orient).toLowerCase(); + String name = dirname; + for (int ii=1; portalNameExists(name); ii++) { + name = dirname + ii; + } + _portal.name = name; + + // add the portal to the scene and pop up the editor dialog + _portal.portalId = _scene.getNextPortalId(); + _scene.addPortal(_portal); + _panel.editPortal(_portal); + } + + /** + * See if the portal name already exists. + */ + protected boolean portalNameExists (String name) + { + Iterator iter = _scene.getPortals(); + while (iter.hasNext()) { + if (((EditablePortal)iter.next()).name.equals(name)) { + return true; + } + } + return false; + } + + /** + * Get rid of ourselves. + */ + protected void dispose () + { + _panel.removeMouseListener(this); + _panel.removeMouseMotionListener(this); + _panel.removeExtrasPainter(this); + } + + /** The panel. */ + protected EditorScenePanel _panel; + + /** The scene. */ + protected StageScene _scene; + + /** Center coordinates. */ + protected Point _center; + + /** Our newly created portal. */ + protected EditablePortal _portal; +} diff --git a/src/java/com/threerings/stage/tools/editor/PreferencesDialog.java b/src/java/com/threerings/stage/tools/editor/PreferencesDialog.java new file mode 100644 index 00000000..5b6a11a0 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/PreferencesDialog.java @@ -0,0 +1,103 @@ +// +// $Id: PreferencesDialog.java 9938 2003-06-20 03:55:58Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JInternalFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.swing.VGroupLayout; + +import com.threerings.stage.tools.editor.util.EditorDialogUtil; + +/** + * A dialog for editing preferences. + */ +public class PreferencesDialog extends JInternalFrame + implements ActionListener +{ + /** + * Creates a preferences dialog. + */ + public PreferencesDialog () + { + super("Editor Preferences", true); + + // set up a layout manager for the panel + JPanel top = (JPanel)getContentPane(); + GroupLayout gl = new VGroupLayout(GroupLayout.STRETCH); + gl.setOffAxisPolicy(GroupLayout.STRETCH); + top.setLayout(gl); + top.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); + + // create the display for the test tile directory pref + JPanel sub = new JPanel(new HGroupLayout(GroupLayout.STRETCH)); + sub.add(new JLabel("Directory to search for test tiles:"), + GroupLayout.FIXED); + EditorDialogUtil.addButton(this, sub, + EditorConfig.getTestTileDirectory(), "testtiledir"); + top.add(sub); + + sub = new JPanel(new HGroupLayout()); + EditorDialogUtil.addButton(this, sub, "OK", "ok"); + top.add(sub); + + pack(); + } + + /** + * Handle action events. + */ + public void actionPerformed (ActionEvent e) + { + String cmd = e.getActionCommand(); + + if (cmd.equals("testtiledir")) { + changeTestTileDir((JButton) e.getSource()); + + } else if (cmd.equals("ok")) { + EditorDialogUtil.dispose(this); + + } else { + Log.warning("Unknown action command [cmd=" + cmd + "]."); + } + } + + /** + * Pop up a file selection box for specifying the directory to look + * in for test tiles. + */ + protected void changeTestTileDir (JButton button) + { + JFileChooser chooser; + + // figure out which + File f = new File(button.getText()); + if (!f.exists()) { + chooser = new JFileChooser(); + } else { + chooser = new JFileChooser(f); + } + + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setSelectedFile(f); + + int result = chooser.showDialog(this, "Select"); + if (JFileChooser.APPROVE_OPTION == result) { + f = chooser.getSelectedFile(); + String newdir = f.getPath(); + button.setText(newdir); + EditorConfig.setTestTileDirectory(newdir); + } + } +} diff --git a/src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java b/src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java new file mode 100644 index 00000000..35ffb606 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/SceneInfoPanel.java @@ -0,0 +1,289 @@ +// +// $Id: SceneInfoPanel.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.FocusAdapter; +import java.awt.event.FocusEvent; + +import java.util.HashSet; +import java.util.Iterator; + +import javax.swing.DefaultComboBoxModel; +import javax.swing.JComboBox; +import javax.swing.JComponent; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; + +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; + +import com.samskivert.swing.GroupLayout; +import com.samskivert.swing.HGroupLayout; +import com.samskivert.util.CollectionUtil; +import com.samskivert.util.Collections; +import com.samskivert.util.ComparableArrayList; + +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.data.SparseMisoSceneModel.ObjectVisitor; + +import com.threerings.media.image.ColorPository; + +import com.threerings.media.tile.NoSuchTileSetException; +import com.threerings.media.tile.RecolorableTileSet; +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileUtil; + +import com.threerings.stage.data.StageMisoSceneModel; +import com.threerings.stage.data.StageScene; + +import com.threerings.stage.tools.editor.util.EditorContext; + +/** + * The scene info panel presents the user with options to select the + * scene layer to edit, and whether to display tile coordinates and + * locations in the scene view. + */ +public class SceneInfoPanel extends JPanel + implements ActionListener +{ + /** + * Constructs the scene info panel. + */ + public SceneInfoPanel (EditorContext ctx, EditorModel model, + EditorScenePanel svpanel) + { + _ctx = ctx; + _svpanel = svpanel; + + // configure the panel + GroupLayout gl = new HGroupLayout(); + gl.setGap(12); + setLayout(gl); + + JPanel vert = GroupLayout.makeVStretchBox(5); + + JPanel hbox = GroupLayout.makeHBox(); + + // create a panel for the name label + hbox.add(createLabel("Scene name:", _scenename = new JTextField(10))); + _scenename.addActionListener(this); + _scenename.addFocusListener(new FocusAdapter() { + public void focusLost (FocusEvent e) { + _scenename.postActionEvent(); + } + }); + + // create a drop-down for selecting the scene type + ComparableArrayList types = new ComparableArrayList(); + ctx.enumerateSceneTypes(types); + types.sort(); + types.add(0, ""); + hbox.add(createLabel("Scene type:", + _scenetype = new JComboBox(types.toArray()))); + _scenetype.addActionListener(this); + + vert.add(hbox); + + // set up the scene colorization stuff + hbox = GroupLayout.makeButtonBox(GroupLayout.LEFT); + hbox.add(_colorClasses = new JComboBox()); + hbox.add(_colorIds = new JComboBox()); + vert.add(createLabel("Colorizations:", hbox), GroupLayout.FIXED); + _colorClasses.addActionListener(this); + _colorIds.addActionListener(this); + _colorClasses.addPopupMenuListener(new PopupMenuListener() { + public void popupMenuCanceled (PopupMenuEvent e) { } + public void popupMenuWillBecomeInvisible (PopupMenuEvent e) { } + public void popupMenuWillBecomeVisible (PopupMenuEvent e) { + recomputeColorClasses(); + } + }); + + add(vert, GroupLayout.FIXED); + } + + protected JPanel createLabel (String label, JComponent comp) + { + JPanel panel = GroupLayout.makeButtonBox(GroupLayout.CENTER); + panel.add(new JLabel(label), GroupLayout.FIXED); + panel.add(comp); + return panel; + } + + /** + * Called when the scene in the editor changes. + */ + public void setScene (StageScene scene) + { + _scene = scene; + _scenename.setText(scene.getName()); + _scenetype.setSelectedItem(scene.getType()); + + recomputeColorClasses(); + } + + // documentation inherited from interface ActionListener + public void actionPerformed (ActionEvent e) + { + Object src = e.getSource(); + if (src == _scenename) { + _scene.setName(_scenename.getText().trim()); + + } else if (src == _scenetype) { + _scene.setType((String) _scenetype.getSelectedItem()); + + } else if (src == _colorClasses) { + String cclass = (String) _colorClasses.getSelectedItem(); + configureColorIds(cclass); + + } else if (src == _colorIds) { + setNewDefaultColor(); + } + } + + /** + * Prior to the color classes popup popping up, recompute the possible + * values. + */ + protected void recomputeColorClasses () + { + // add all possible colorization names to the list + final TileManager tilemgr = _ctx.getTileManager(); + final HashSet set = new HashSet(); + StageMisoSceneModel msmodel = StageMisoSceneModel.getSceneModel( + _scene.getSceneModel()); + msmodel.visitObjects(new ObjectVisitor() { + public void visit (ObjectInfo info) { + int tsid = TileUtil.getTileSetId(info.tileId); + TileSet tset; + try { + tset = tilemgr.getTileSet(tsid); + } catch (NoSuchTileSetException nstse) { + return; + } + String[] zations; + if (tset instanceof RecolorableTileSet) { + zations = ((RecolorableTileSet) tset).getColorizations(); + } else { + return; + } + if (zations != null) { + for (int ii=0; ii < zations.length; ii++) { + set.add(zations[ii]); + } + } + } + }); + + Object selected = _colorClasses.getSelectedItem(); + DefaultComboBoxModel model = + (DefaultComboBoxModel) _colorClasses.getModel(); + model.removeAllElements(); + for (Iterator itr = Collections.getSortedIterator(set); + itr.hasNext(); ) { + model.addElement(itr.next()); + } + if (selected != null) { + _colorClasses.setSelectedItem(selected); + } + } + + /** + * Show which color Ids are available for the currently selected + * colorization class, and select the selected one. + */ + protected void configureColorIds (String cclass) + { + _colorIds.removeActionListener(this); + try { + DefaultComboBoxModel model = + (DefaultComboBoxModel) _colorIds.getModel(); + model.removeAllElements(); + if (cclass == null) { + return; + } + + ColorPository cpos = _ctx.getColorPository(); + + String noChoice = ""; + String choice = noChoice; + + ColorPository.ClassRecord classRec = cpos.getClassRecord(cclass); + int pick = _scene.getDefaultColor(classRec.classId); + + ColorPository.ColorRecord[] colors = cpos.enumerateColors(cclass); + ComparableArrayList list = new ComparableArrayList(); + for (int ii=0; ii < colors.length; ii++) { + list.insertSorted(colors[ii].name); + if (colors[ii].colorId == pick) { + choice = colors[ii].name; + } + } + + model.addElement(noChoice); + for (int ii=0; ii < list.size(); ii++) { + model.addElement(list.get(ii)); + } + _colorIds.setSelectedItem(choice); + + } finally { + _colorIds.addActionListener(this); + } + } + + /** + * Called when a _colorIds color is selected. + */ + protected void setNewDefaultColor () + { + String cclass = (String) _colorClasses.getSelectedItem(); + if (cclass == null) { + return; + } + + ColorPository cpos = _ctx.getColorPository(); + ColorPository.ClassRecord classRec = cpos.getClassRecord(cclass); + ColorPository.ColorRecord[] colors = cpos.enumerateColors(cclass); + Object selected = _colorIds.getSelectedItem(); + int pick = -1; + + for (int ii=0; ii < colors.length; ii++) { + if (colors[ii].name.equals(selected)) { + pick = colors[ii].colorId; + break; + } + } + + // only update the scene if our selection has actually changed + if (_scene.getDefaultColor(classRec.classId) != pick) { + _scene.setDefaultColor(classRec.classId, pick); + _svpanel.setScene(_scene); + _svpanel.repaint(); + } + } + + /** The giver of life. */ + protected EditorContext _ctx; + + /** The scene we're controlling. */ + protected StageScene _scene; + + /** The scene name entry field. */ + protected JTextField _scenename; + + /** The scene type selector. */ + protected JComboBox _scenetype, _colorClasses, _colorIds; + + /** The scene panel, which we hackily repaint when default colors change. */ + protected EditorScenePanel _svpanel; + + /** The object grip direction button. */ + protected DirectionButton _dirbutton; +} diff --git a/src/java/com/threerings/stage/tools/editor/TestTileLoader.java b/src/java/com/threerings/stage/tools/editor/TestTileLoader.java new file mode 100644 index 00000000..6c076ef3 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/TestTileLoader.java @@ -0,0 +1,173 @@ +// +// $Id: TestTileLoader.java 9938 2003-06-20 03:55:58Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileFilter; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.HashMap; +import java.util.Iterator; +import javax.imageio.ImageIO; + +import com.samskivert.util.HashIntMap; + +import com.threerings.media.tile.ImageProvider; +import com.threerings.media.tile.SimpleCachingImageProvider; +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetIDBroker; + +import com.threerings.miso.tile.tools.xml.BaseTileSetRuleSet; +import com.threerings.media.tile.tools.xml.ObjectTileSetRuleSet; +import com.threerings.media.tile.tools.xml.SwissArmyTileSetRuleSet; +import com.threerings.media.tile.tools.xml.XMLTileSetParser; + +/** + * The TestTileLoader handles test tiles. Test tiles are tiles that an + * artist can load in on-the-fly to see how things look in the scene editor. + */ +public class TestTileLoader implements TileSetIDBroker +{ + /** + * Construct the TestTileLoader. + */ + public TestTileLoader () + { + // our xml parser + _parser = new XMLTileSetParser(); + // add some rulesets + _parser.addRuleSet("bundle/base", new BaseTileSetRuleSet()); + _parser.addRuleSet("bundle/object", new ObjectTileSetRuleSet()); + + // we used to parse fringes, but we don't anymore + //_parser.addRuleSet("bundle/fringe", new SwissArmyTileSetRuleSet()); + } + + /** + * Check the specified directory and all its subdirectories for xml files. + * Each directory should contain at most one xml file, each xml file + * should specify at most one tileset. That tileset specification + * will be used to create tilesets for all the .png files in the same + * directory. + * + * @return a HashIntMap containing a TileSetId -> TileSet mapping for + * all the tilesets we create. + */ + public HashIntMap loadTestTiles () + { + String directory = EditorConfig.getTestTileDirectory(); + HashIntMap map = new HashIntMap(); + + // recurse test directory, making a tileset from the xml file inside + // and cloning it for each image we find in there. + File testdir = new File(directory); + // make sure it's a directory + if (!testdir.isDirectory()) { + Log.warning("Test tileset directory is not actually a directory: " + + directory); + return map; + } + + // recursively load all the test tiles + loadTestTilesFromDir(testdir, map); + + return map; + } + + /** + * Load xml tile sets from a directory. + */ + protected void loadTestTilesFromDir (File directory, + HashIntMap sets) + { + // first recurse + File[] subdirs = directory.listFiles(new FileFilter() { + public boolean accept (File f) { + return f.isDirectory(); + } + }); + for (int ii=0; ii < subdirs.length; ii++) { + loadTestTilesFromDir(subdirs[ii], sets); + } + + // now look for the xml file + String[] xml = directory.list(new FilenameFilter() { + public boolean accept (File dir, String name) { + return name.endsWith(".xml"); + } + }); + + for (int ii=0; ii < xml.length; ii++) { + File xmlfile = new File(directory, xml[ii]); + + HashMap tiles = new HashMap(); + try { + _parser.loadTileSets(xmlfile, tiles); + } catch (IOException ioe) { + Log.warning("Error while parsing " + xmlfile.getPath()); + Log.logStackTrace(ioe); + continue; + } + + Iterator iter = tiles.values().iterator(); + while (iter.hasNext()) { + TileSet ts = (TileSet) iter.next(); + String path = new File(directory, ts.getImagePath()).getPath(); + + // before we insert, make sure we can load the image + if (null != _improv.getTileSetImage(path, null)) { + ts.setImageProvider(_improv); + ts.setImagePath(path); + sets.put(getTileSetID(path), ts); + } + } + } + } + + /** + * Generate unique and completely fake tileset IDs that will be stable + * even after a reload of test tiles. + */ + public int getTileSetID (String tileSetPath) + { + Integer id = (Integer) _idmap.get(tileSetPath); + if (null == id) { + id = Integer.valueOf(_fakeID--); + _idmap.put(tileSetPath, id); + } + return id.intValue(); + } + + // documentation inherited + public boolean tileSetMapped (String tilesetPath) + { + return _idmap.containsKey(tilesetPath); + } + + /** + * Since we're just testing, we don't save these crazy IDs. + */ + public void commit () + { + // this method does nothing. perhaps it should be called "committee". + } + + /** The value of the next fakeID we'll hand out. */ + protected int _fakeID = -1; + + /** A mapping of pathname -> tileset id. */ + protected HashMap _idmap = new HashMap(); + + /** Our xml parser. */ + protected XMLTileSetParser _parser; + + /** Our image provider. */ + protected ImageProvider _improv = new SimpleCachingImageProvider() { + protected BufferedImage loadImage (String path) + throws IOException { + return ImageIO.read(new File(path)); + } + }; +} diff --git a/src/java/com/threerings/stage/tools/editor/TileInfoPanel.java b/src/java/com/threerings/stage/tools/editor/TileInfoPanel.java new file mode 100644 index 00000000..1a98f795 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/TileInfoPanel.java @@ -0,0 +1,707 @@ +// +// $Id: TileInfoPanel.java 17625 2004-10-28 17:50:03Z mdb $ + +package com.threerings.stage.tools.editor; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Image; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import java.util.ArrayList; +import java.util.Iterator; + +import javax.swing.BorderFactory; +import javax.swing.DefaultListCellRenderer; +import javax.swing.DefaultListModel; +import javax.swing.ImageIcon; +import javax.swing.JList; +import javax.swing.JSplitPane; +import javax.swing.JTable; +import javax.swing.JTree; +import javax.swing.ListSelectionModel; +import javax.swing.border.Border; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableColumn; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.QuickSort; +import com.samskivert.util.StringUtil; + +import com.threerings.media.SafeScrollPane; + +import com.threerings.media.tile.TileSet; +import com.threerings.media.tile.TileSetRepository; + +import com.threerings.stage.tools.editor.util.EditorContext; +import com.threerings.stage.tools.editor.util.TileSetUtil; + +/** + * The tile info panel presents the user with options to select the + * tile to be applied to the scene. + */ +public class TileInfoPanel extends JSplitPane + implements ListSelectionListener, TreeSelectionListener +{ + /** + * Constructs the tile info panel. + */ + public TileInfoPanel (EditorContext ctx, EditorModel model) + { + TileSetRepository tsrepo = ctx.getTileSetRepository(); + + // set up our key observers + registerKeyListener(ctx); + + _model = model; + + // we're going to sort all of the available tilesets into those + // which are applicable to each layer + try { + _layerSets = new ArrayList[2]; + _layerLengths = new int[2]; + for (int ii=0; ii < 2; ii++) { + _layerSets[ii] = new ArrayList(); + } + + Iterator tsids = tsrepo.enumerateTileSetIds(); + while (tsids.hasNext()) { + Integer tsid = (Integer)tsids.next(); + TileSet set = tsrepo.getTileSet(tsid.intValue()); + + // determine which layer to which this tileset applies + int lidx = TileSetUtil.getLayerIndex(set); + if (lidx != -1) { + _layerSets[lidx].add( + new TileSetRecord(lidx, tsid.intValue(), set)); + } + } + + for (int ii=0; ii < 2; ii++) { + _layerLengths[ii] = _layerSets[ii].size(); + } + + } catch (Exception e) { + Log.warning("Error enumerating tilesets."); + Log.logStackTrace(e); + } + + // set up a border denoting our contents + Border border = BorderFactory.createEtchedBorder(); + setBorder(BorderFactory.createTitledBorder(border, "Tile Info")); + + // create a tree for selecting tileset + DefaultMutableTreeNode root = new DefaultMutableTreeNode(""); + _tsettree = new JTree(root); + + // don't draw any funny little icons in the tree + DefaultTreeCellRenderer cellrend = + (DefaultTreeCellRenderer) _tsettree.getCellRenderer(); + cellrend.setLeafIcon(null); + cellrend.setOpenIcon(null); + cellrend.setClosedIcon(null); + + // tree- only let one thing be selected, and let us know when it haps + _tsettree.getSelectionModel().setSelectionMode( + TreeSelectionModel.SINGLE_TREE_SELECTION); + _tsettree.addTreeSelectionListener(this); + + // create a scrollpane to hold the tree + SafeScrollPane scrolly = new SafeScrollPane(_tsettree); + scrolly.setVerticalScrollBarPolicy( + SafeScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + + DefaultListModel qmodel = new DefaultListModel(); + for (int ii=0; ii < 10; ii++) { + qmodel.addElement(""); + } + _quickList = new JList(qmodel); + _quickList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + _quickList.addListSelectionListener(this); + _quickList.setCellRenderer(new DefaultListCellRenderer() { + public Component getListCellRendererComponent ( + JList list, Object value, int index, boolean isSelected, + boolean cellHasFocus) + { + // put the key number in front of each element + Component result = super.getListCellRendererComponent( + list, value, index, isSelected, cellHasFocus); + setText("" + ((index + 11) % 10) + ". " + getText()); + return result; + } + }); + JSplitPane leftSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + leftSplit.setTopComponent(new SafeScrollPane(_quickList)); + leftSplit.setBottomComponent(scrolly); + + // add the west side + setLeftComponent(leftSplit); + + // create a table to display the tiles in the selected tileset + _tiletable = new JTable(_tablemodel = new TileTableModel()); + _tiletable.getSelectionModel().addListSelectionListener(this); + _tiletable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + updateTileTable(); + + // wrap the table in a scrollpane for lengthy tilesets + _scroller = new SafeScrollPane(_tiletable); + _scroller.setVerticalScrollBarPolicy( + SafeScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + + // add the tile table as the entire east side + setRightComponent(_scroller); + + // add the tilesets and select the starting tile set + updateTileSetTree(); + + // The damn splitpane freaks out unless we do this + setDividerLocation(230); + } + + /** + * Register key listeners to do things with the quick list. + */ + protected void registerKeyListener (EditorContext ctx) + { + ctx.getKeyDispatcher().addGlobalKeyListener(new KeyAdapter() { + public void keyTyped (KeyEvent e) + { + char keychar = e.getKeyChar(); + if ((keychar < '0') || (keychar > '9')) { + return; + } + + // turn 1 into 0, and 0 into 9 + int index = (keychar - '0' + 9) % 10; + if (e.isControlDown() || e.isAltDown()) { + // add + if (_curTrec == null) { + return; + } + _quickList.clearSelection(); + DefaultListModel model = + (DefaultListModel) _quickList.getModel(); + int olddex = model.indexOf(_curTrec); + if (olddex != -1) { + model.set(olddex, ""); + } + model.set(index, _curTrec); + _quickList.setSelectedIndex(index); + + } else { + // select + _quickList.setSelectedIndex(index); + } + } + }); + } + + /** + * Selects the previous tile in the list of available tiles. + */ + public void selectPreviousTile () + { + int row = _tiletable.getSelectedRow(); + if (--row >= 0) { + _tiletable.setRowSelectionInterval(row, row); + } + } + + /** + * Selects the next tile in the list of available tiles. + */ + public void selectNextTile () + { + int row = _tiletable.getSelectedRow(); + if (++row < _tiletable.getRowCount()) { + _tiletable.setRowSelectionInterval(row, row); + } + } + + // documentation inherited + public Dimension getPreferredSize () + { + return new Dimension(WIDTH, HEIGHT); + } + + /** + * Display the tiles in a tileset when it is selected. + */ + public void valueChanged (TreeSelectionEvent e) + { + DefaultMutableTreeNode node = + (DefaultMutableTreeNode) _tsettree.getLastSelectedPathComponent(); + + // we only care when a leaf is selected + if ((node != null) && (node.isLeaf())) { + + _selected = node; + + Object uobj = node.getUserObject(); + if (!(uobj instanceof TileSetRecord)) { + Log.info("Eh? Non-TileSetRecord leaf [obj=" + uobj + + ", class=" + StringUtil.shortClassName(uobj) + "]."); + return; + } + + tileSetSelected((TileSetRecord) uobj); + _quickList.clearSelection(); + } + } + + /** + * Called when a tileset is selected, either via the tree + * or the recent list. + */ + protected void tileSetSelected (TileSetRecord trec) + { + // if they've selected something new, update our tile display + if (_model.getTileSet() != trec.tileSet) { + _curTrec = trec; + _model.setLayerIndex(trec.layer); + + // update the model to reflect new tile set and select tile + // zero by default + _model.setTile(trec.tileSet, trec.tileSetId, 0); + + // update the tile table to reflect the new tileset + updateTileTable(); + +// _quickList.removeListSelectionListener(this); +// // add it to the recent list +// DefaultListModel recentModel = (DefaultListModel) +// _quickList.getModel(); +// recentModel.removeElement(trec); +// recentModel.add(0, trec); +// _quickList.setSelectedIndex(0); +// _quickList.addListSelectionListener(this); + } + } + + /** + * Remove previous test tiles and insert the new batch. + */ + protected void insertTestTiles (HashIntMap tests) + { + // trim the tilesets back to remove any previous test tiles + for (int ii=0; ii < 2; ii++) { + for (int jj=_layerSets[ii].size() - 1; jj >= _layerLengths[ii]; + jj--) { + _layerSets[ii].remove(jj); + } + } + + // insert the new test tiles + Iterator iter = tests.keys(); + while (iter.hasNext()) { + Integer tsid = (Integer) iter.next(); + + TileSet set = (TileSet) tests.get(tsid); + + // determine which layer to which this tileset applies + int lidx = TileSetUtil.getLayerIndex(set); + if (lidx != -1) { + // make up a negative number to refer to this temporary tileset + _layerSets[lidx].add( + new TileSetRecord(lidx, tsid.intValue(), set)); + } + } + + updateTileSetTree(); + } + + /** + * The layer has changed, update the tree to reflect the tilesets + * now available. + */ + public void updateTileSetTree () + { + // first clear out the tree + DefaultTreeModel model = (DefaultTreeModel) _tsettree.getModel(); + DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot(); + root.removeAllChildren(); + + ArrayList expand = new ArrayList(); + + // add all the elements in the base layer + DefaultMutableTreeNode base = new DefaultMutableTreeNode("Base Layer"); + root.add(base); + addNodes(base, getSortedTileSets(EditorModel.BASE_LAYER), + "", 0, expand); + + // add all the elements in the object layer + DefaultMutableTreeNode obj = new DefaultMutableTreeNode("Object Layer"); + root.add(obj); + addNodes(obj, getSortedTileSets(EditorModel.OBJECT_LAYER), + "", 0, expand); + + // notify the JTree that we've put some brand new branches on it. + model.reload(); + + // expand our container categories + for (Iterator iter = expand.iterator(); iter.hasNext(); ) { + _tsettree.expandPath((TreePath)iter.next()); + } + + // now select the previously selected item, or the first... + if (_selected == null) { + _selected = (DefaultMutableTreeNode) root.getFirstLeaf(); + } + _tsettree.setSelectionPath(new TreePath(_selected.getPath())); + } + + /** + * Populate the tree with the available tilesets for the selected layer. + */ + protected TileSetRecord[] getSortedTileSets (int layer) + { + // get the list of tilesets we now want to show + ArrayList sets = _layerSets[layer]; + + // we don't want to sort the actual array since we have + // kept the test tiles at the end + TileSetRecord[] sorted = new TileSetRecord[sets.size()]; + sets.toArray(sorted); + QuickSort.sort(sorted); + + return sorted; + } + + /** + * Recursively add tilesets to the tree. + * + * @param prefix The portion of the full tileset name that we've + * already parsed, it corresponds to the node we're adding to. + * @param position The position in the array from whence to start adding. + * @return the number of elements added to 'node' from 'list'. + */ + protected int addNodes (DefaultMutableTreeNode node, + TileSetRecord[] list, String prefix, int position, + ArrayList expand) + { + int prefixlen = prefix.length(); + + for (int ii = position; ii < list.length; ) { + String name = list[ii].fullname(); + + // if the next name on the list doesn't start with the prefix, + // we have no business adding it to this node. + if (!name.startsWith(prefix)) { + return ii - position; + } + + // is there another category name? + int dex = name.indexOf('/', prefixlen); + if (dex == -1) { + // nope, just add this item to the node. + DefaultMutableTreeNode item = new DefaultMutableTreeNode( + list[ii]); + node.add(item); + + // oh, we're so sneaky! + // if the item we're adding has the same TileSetRecord + // as the previously selected item, we're going to want to + // select it.. + if ((_selected != null) && + (list[ii].equals(_selected.getUserObject()))) { + _selected = item; + } + + ii++; + + } else { + // new category! + String catname = name.substring(prefixlen, dex); + DefaultMutableTreeNode category = + new DefaultMutableTreeNode(catname); + node.add(category); + + // if we have further categories below, start expanded + if (name.indexOf('/', dex+1) != -1) { + expand.add(new TreePath(category.getPath())); + } + + // recurse.. + ii += addNodes(category, list, name.substring(0, dex + 1), + ii, expand); + } + } + + return list.length - position; + } + + /** + * Update the tile table to reflect the currently selected tile set. + */ + protected void updateTileTable () + { + // get the table width before we update the table model since + // updating the model seems to reset the table width to an + // incorrect default + TableColumn tcol = _tiletable.getColumnModel().getColumn(0); + _tablewid = tcol.getWidth() - (2 * EDGE_TILE_H); + + // clear out the old selection because we're going to change + // tilesets + _tiletable.clearSelection(); + + // update the table model with the new tile set tiles + _tablemodel.updateTileSet(); + + // if there are no tiles in the current tile set, we're done + if (!_model.isTileValid()) { + return; + } + + // set row heights to match the scaled tile image heights + int numTiles = getTileCount(); + TileSet set = _model.getTileSet(); + for (int ii = 0; ii < numTiles; ii++) { + Image img = set.getRawTileImage(ii); + int hei = getScaledTileImageHeight(img); + _tiletable.setRowHeight(ii, hei + (2 * EDGE_TILE_V)); + } + + // select the selected tile + int tid = _model.getTileId(); + _tiletable.setRowSelectionInterval(tid, tid); + + if (_scroller != null) { + // scroll to the selected tile + Rectangle r = _tiletable.getCellRect(tid, 0, true); + _scroller.getViewport().setViewPosition(new Point(r.x, r.y)); + } + } + + /** + * Handle tile table selections. + */ + public void valueChanged (ListSelectionEvent e) + { + // ignore extra messages + if (e.getValueIsAdjusting()) { + return; + } + + Object src = e.getSource(); + if (src == _quickList) { + Object o = _quickList.getSelectedValue(); + if (o instanceof TileSetRecord) { + tileSetSelected((TileSetRecord) o); + if (o != _selected.getUserObject()) { + _tsettree.clearSelection(); + } + } + } else { + // otherwise they clicked on the tile table. + ListSelectionModel lsm = (ListSelectionModel) src; + if (!lsm.isSelectionEmpty()) { + _model.setTileId(lsm.getMinSelectionIndex()); + } + } + } + + /** + * Returns the number of tiles in the currently selected tileset. + */ + protected int getTileCount () + { + if (!_model.isTileValid()) { + return 0; + + } else { + TileSet set = _model.getTileSet(); + return (set == null) ? 0 : set.getTileCount(); + } + } + + /** + * Returns the height of the given tile image after scaling to fit + * within the width of the tile table. + */ + protected int getScaledTileImageHeight (Image img) + { + int wid = img.getWidth(null), hei = img.getHeight(null); + if (wid > _tablewid) { + float frac = (float)wid / (float)_tablewid; + return (int)(hei / frac); + } + + return hei; + } + + /** + * Extends the {@link AbstractTableModel} to encapsulate the table + * layout and display options required when displaying the tiles in + * the currently selected tileset. + */ + protected class TileTableModel extends AbstractTableModel + { + /** + * Called when the tile set associated with the table has been + * changed. Clears the cached image icons used to display each + * cell and updates the number of rows in the table to properly + * deal with tile sets of varying sizes. + */ + public synchronized void updateTileSet () + { + int numTiles = getTileCount(); + _icons = new ImageIcon[numTiles]; + fireTableRowsInserted(0, numTiles); + } + + // documentation inherited + public int getColumnCount () + { + return 1; + } + + // documentation inherited + public String getColumnName (int columnIndex) + { + return null; + } + + // documentation inherited + public int getRowCount () + { + return getTileCount(); + } + + // documentation inherited + public Object getValueAt (int row, int col) + { + // return the icon immediately if it's already cached + if (_icons[row] != null) { + return _icons[row]; + } + + // generate and save off the tile image scaled to fit the table + TileSet set = _model.getTileSet(); + Image img = set.getRawTileImage(row); + int hei = getScaledTileImageHeight(img); + + if (hei != img.getHeight(null)) { + img = img.getScaledInstance( + _tablewid, hei, Image.SCALE_SMOOTH); + } + + return (_icons[row] = new ImageIcon(img)); + } + + // documentation inherited + public Class getColumnClass (int c) + { + // return the object associated with the column to force + // rendering of our icon images rather than straight text + return getValueAt(0, c).getClass(); + } + + /** The image icons used to display the table cell contents. */ + protected ImageIcon _icons[]; + } + + /** + * Used to manage tilesets in the tileset selection combobox. + */ + protected static class TileSetRecord implements Comparable + { + public int layer; + public int tileSetId; + public TileSet tileSet; + public String shortname; + + public TileSetRecord (int layer, int tileSetId, TileSet tileSet) + { + this.layer = layer; + this.tileSetId = tileSetId; + this.tileSet = tileSet; + + shortname = fullname(); + + // cut everything before the last slash for our shortname + int lastdex = shortname.lastIndexOf('/'); + if (lastdex != -1) { + shortname = shortname.substring(lastdex + 1); + } + } + + public String fullname () + { + return tileSet.getName(); + } + + public String toString () + { + return shortname; + } + + public int compareTo (Object o) + { + return fullname().compareToIgnoreCase( + ((TileSetRecord) o).fullname()); + } + + public boolean equals (Object o) + { + if (o instanceof TileSetRecord) { + TileSetRecord tsr = (TileSetRecord) o; + return ((tsr.layer == layer) && (tsr.tileSetId == tileSetId)); + } + return false; + } + } + + /** Default desired panel dimensions. */ + protected static final int WIDTH = 400; + protected static final int HEIGHT = 300; + + /** Buffer space surrounding each tile in the tile table. */ + protected static final int EDGE_TILE_H = 4; + protected static final int EDGE_TILE_V = 4; + + /** An ArrayList of TileSetRecords for each layer. */ + protected ArrayList[] _layerSets; + + /** The original number of TileSetRecords for each layer. */ + protected int[] _layerLengths; + + /** The tree listing available tilesets. */ + protected JTree _tsettree; + + /** The selected tree node. */ + protected DefaultMutableTreeNode _selected; + + /** The table listing all tiles in the selected tileset. */ + protected JTable _tiletable; + + /** The list of quickly-selectable tilesets. */ + protected JList _quickList; + + /** The currently selected tileset record. */ + protected TileSetRecord _curTrec; + + /** The width of the tile table column in pixels. */ + protected int _tablewid; + + /** The scroll pane containing the tile table. */ + protected SafeScrollPane _scroller; + + /** The editor model. */ + protected EditorModel _model; + + /** The tile table data model. */ + protected TileTableModel _tablemodel; +} diff --git a/src/java/com/threerings/stage/tools/editor/util/EditorContext.java b/src/java/com/threerings/stage/tools/editor/util/EditorContext.java new file mode 100644 index 00000000..e19de94f --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/EditorContext.java @@ -0,0 +1,31 @@ +// +// $Id: EditorContext.java 7429 2003-04-01 02:19:34Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import java.util.List; + +import com.threerings.media.image.ColorPository; +import com.threerings.media.tile.TileSetRepository; + +import com.threerings.stage.util.StageContext; + +public interface EditorContext extends StageContext +{ + /** + * Return a reference to the tile set repository in use by the tile + * manager. This reference is valid for the lifetime of the + * application. + */ + public TileSetRepository getTileSetRepository (); + + /** + * Returns a colorization repository for use by the editor. + */ + public ColorPository getColorPository (); + + /** + * Inserts all known scene types into the supplied list. + */ + public void enumerateSceneTypes (List types); +} diff --git a/src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java b/src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java new file mode 100644 index 00000000..beb35d43 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/EditorDialogUtil.java @@ -0,0 +1,100 @@ +// +// $Id: EditorDialogUtil.java 9223 2003-05-27 02:35:00Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import java.awt.Container; +import java.awt.Dimension; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JComboBox; +import javax.swing.JFrame; +import javax.swing.JInternalFrame; +import javax.swing.JLayeredPane; + +import com.threerings.util.DirectionUtil; + +public class EditorDialogUtil +{ + /** + * Add a button to a container with the given parameters and + * action listener. + * + * @param l the listener. + * @param container the container. + * @param name the button name. + * @param cmd the action command. + */ + public static void addButton (ActionListener l, Container container, + String name, String cmd) + { + JButton button = new JButton(name); + button.addActionListener(l); + button.setActionCommand(cmd); + container.add(button); + } + + /** + * Create and return a combo box seeded with the various possible + * orientation direction names. + * + * @param l the listener. + * + * @return the combo box. + */ + public static JComboBox getOrientationComboBox (ActionListener l) + { + JComboBox box = new JComboBox(DirectionUtil.getDirectionNames()); + box.addActionListener(l); + box.setActionCommand("orient"); + return box; + } + + /** + * Centers the supplied dialog in its parent's bounds. + */ + public static void center (JFrame parent, JInternalFrame dialog) + { + Dimension psize = parent.getSize(); + Dimension dsize = dialog.getSize(); + dialog.setLocation((psize.width-dsize.width)/2, + (psize.height-dsize.height)/2); + } + + /** + * Display a dialog centered within the given frame. + * + * @param parent the parent frame. + * @param dialog the dialog. + */ + public static void display (JFrame parent, JInternalFrame dialog) + { + center(parent, dialog); + parent.getLayeredPane().add(dialog, JLayeredPane.POPUP_LAYER); + dialog.setVisible(true); + } + + /** + * Removes the supplied dialog from its parent container, but does not + * dispose it. + */ + public static void dismiss (JInternalFrame dialog) + { + Container parent = dialog.getParent(); + if (parent != null) { + parent.remove(dialog); + parent.repaint(); + } + dialog.setVisible(false); + } + + /** + * Handles safely dismissing and disposing of the supplied dialog. + */ + public static void dispose (JInternalFrame dialog) + { + dismiss(dialog); + dialog.dispose(); + } +} diff --git a/src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java b/src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java new file mode 100644 index 00000000..ffcc4eb3 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/ExtrasPainter.java @@ -0,0 +1,14 @@ +// +// $Id: ExtrasPainter.java 7998 2003-04-18 18:34:41Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import java.awt.Graphics2D; + +/** + * An interface for painting extra stuff. + */ +public interface ExtrasPainter +{ + public void paintExtras (Graphics2D gfx); +} diff --git a/src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java b/src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java new file mode 100644 index 00000000..bafe1a32 --- /dev/null +++ b/src/java/com/threerings/stage/tools/editor/util/TileSetUtil.java @@ -0,0 +1,30 @@ +// +// $Id: TileSetUtil.java 9371 2003-06-02 20:28:21Z mdb $ + +package com.threerings.stage.tools.editor.util; + +import com.threerings.media.tile.TileSet; +import com.threerings.miso.tile.BaseTileSet; + +import com.threerings.stage.tools.editor.Log; +import com.threerings.stage.tools.editor.EditorModel; + +/** + * Miscellaneous useful routines for working with lists of {@link TileSet} + * and {@link BaseTileSet} objects. + */ +public class TileSetUtil +{ + /** + * Returns the layer index of the layer for which this tileset + * provides tiles. + */ + public static int getLayerIndex (TileSet set) + { + if (set instanceof BaseTileSet) { + return EditorModel.BASE_LAYER; + } else { + return EditorModel.OBJECT_LAYER; + } + } +} diff --git a/src/java/com/threerings/stage/tools/viewer/ViewerApp.java b/src/java/com/threerings/stage/tools/viewer/ViewerApp.java new file mode 100644 index 00000000..e8b11e82 --- /dev/null +++ b/src/java/com/threerings/stage/tools/viewer/ViewerApp.java @@ -0,0 +1,215 @@ +// +// $Id: ViewerApp.java 19661 2005-03-09 02:40:29Z andrzej $ + +package com.threerings.stage.tools.viewer; + +import java.awt.DisplayMode; +import java.awt.EventQueue; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.GraphicsEnvironment; + +import java.io.File; +import java.io.IOException; + +import com.samskivert.swing.RuntimeAdjust; +import com.samskivert.swing.util.SwingUtil; + +import com.threerings.resource.ResourceManager; +import com.threerings.util.KeyDispatcher; +import com.threerings.util.KeyboardManager; +import com.threerings.util.MessageManager; + +import com.threerings.media.FrameManager; +import com.threerings.media.IconManager; +import com.threerings.media.image.ColorPository; +import com.threerings.media.image.ImageManager; +import com.threerings.media.tile.bundle.BundledTileSetRepository; + +import com.threerings.cast.CharacterManager; +import com.threerings.cast.bundle.BundledComponentRepository; +import com.threerings.cast.ComponentRepository; + +import com.threerings.miso.tile.MisoTileManager; + +import com.threerings.stage.Log; +import com.threerings.stage.util.StageContext; + +/** + * The ViewerApp is a scene viewing application that allows for trying out + * Stage scenes in a pseudo-runtime environment. + */ +public class ViewerApp +{ + /** + * Construct and initialize the ViewerApp object. + */ + public ViewerApp (String[] args) + throws IOException + { + // get the graphics environment + GraphicsEnvironment env = + GraphicsEnvironment.getLocalGraphicsEnvironment(); + + // get the target graphics device + GraphicsDevice gd = env.getDefaultScreenDevice(); + Log.info("Graphics device [dev=" + gd + + ", mem=" + gd.getAvailableAcceleratedMemory() + + ", displayChange=" + gd.isDisplayChangeSupported() + + ", fullScreen=" + gd.isFullScreenSupported() + "]."); + + // get the graphics configuration and display mode information + GraphicsConfiguration gc = gd.getDefaultConfiguration(); + DisplayMode dm = gd.getDisplayMode(); + Log.info("Display mode [bits=" + dm.getBitDepth() + + ", wid=" + dm.getWidth() + ", hei=" + dm.getHeight() + + ", refresh=" + dm.getRefreshRate() + "]."); + + _rmgr = new ResourceManager("rsrc"); + _rmgr.initBundles(null, "config/resource/manager.properties", null); + _imgr = new ImageManager(_rmgr, _frame); + _tilemgr = new MisoTileManager(_rmgr, _imgr); + _tilemgr.setTileSetRepository( + new BundledTileSetRepository(_rmgr, _imgr, "tilesets")); + _colpos = ColorPository.loadColorPository(_rmgr); + _crepo = new BundledComponentRepository(_rmgr, _imgr, "components"); + _mesgmgr = new MessageManager("rsrc.i18n"); + + _frame = new ViewerFrame(gc); + _framemgr = FrameManager.newInstance(_frame, _frame); + + StageContext ctx = new ContextImpl(); + _frame.init(ctx, new CharacterManager(_imgr, _crepo)); + + // grab our argument + _target = (args.length > 0) ? args[0] : null; + + // size and position the window, entering full-screen exclusive + // mode if available and desired + if (gd.isFullScreenSupported() /* && _viewFullScreen.getValue() */) { + Log.info("Entering full-screen exclusive mode."); + gd.setFullScreenWindow(_frame); + } else { + _frame.setSize(640, 575); + SwingUtil.centerWindow(_frame); + } + } + + /** + * The implementation of the {@link StageContext} interface that + * provides handles to the config and manager objects that offer + * commonly used services. + */ + protected class ContextImpl implements StageContext + { + public FrameManager getFrameManager () { + return _framemgr; + } + + public MisoTileManager getTileManager () { + return _tilemgr; + } + + // documentation inherited from interface + public ResourceManager getResourceManager () { + return _rmgr; + } + + // documentation inherited from interface + public ImageManager getImageManager () { + return _imgr; + } + + // documentation inherited from interface + public MessageManager getMessageManager () { + return _mesgmgr; + } + + // documentation inherited from interface + public IconManager getIconManager () { + return null; + } + + // documentation inherited from interface + public KeyboardManager getKeyboardManager() { + return null; + } + + // documentation inherited from interface + public ComponentRepository getComponentRepository () { + return _crepo; + } + + // documentation inherited from interface + public ColorPository getColorPository () { + return _colpos; + } + + // documentation inherited from interface + public KeyDispatcher getKeyDispatcher () { + return null; + } + + // documentation inherited from interface + public String xlate (String message) { + return message; + } + + // documentation inherited from interface + public String xlate (String bundle, String message) { + return message; + } + } + + /** + * Run the application. + */ + public void run () + { + // show the window + _frame.setVisible(true); + _framemgr.start(); + + // load up anything specified on the command line + EventQueue.invokeLater(new Runnable() { + public void run () { + if (_target != null) { + _frame.loadScene(_target); + } else { + _frame.openScene(null); + } + } + }); + } + + /** + * Instantiate the application object and start it running. + */ + public static void main (String[] args) + { + try { + ViewerApp app = new ViewerApp(args); + app.run(); + } catch (IOException ioe) { + System.err.println("Error initializing viewer app."); + ioe.printStackTrace(); + } + } + + protected ResourceManager _rmgr; + protected MisoTileManager _tilemgr; + protected ImageManager _imgr; + protected BundledComponentRepository _crepo; + protected ColorPository _colpos; + protected MessageManager _mesgmgr; + + protected FrameManager _framemgr; + protected ViewerFrame _frame; + protected String _target; + +// /** A debug hook that toggles debug rendering of traversable tiles. */ +// protected static RuntimeAdjust.BooleanAdjust _viewFullScreen = +// new RuntimeAdjust.BooleanAdjust( +// "Toggles whether or not the scene viewer uses full screen mode.", +// "stage.viewer.full_screen", ToolPrefs.config, false); +} diff --git a/src/java/com/threerings/stage/tools/viewer/ViewerFrame.java b/src/java/com/threerings/stage/tools/viewer/ViewerFrame.java new file mode 100644 index 00000000..0094613a --- /dev/null +++ b/src/java/com/threerings/stage/tools/viewer/ViewerFrame.java @@ -0,0 +1,170 @@ +// +// $Id: ViewerFrame.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.viewer; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.GraphicsConfiguration; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; + +import java.io.File; + +import javax.swing.JFileChooser; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JOptionPane; +import javax.swing.KeyStroke; +import javax.swing.filechooser.FileFilter; + +import com.samskivert.swing.util.MenuUtil; + +import com.threerings.cast.CharacterManager; +import com.threerings.media.ManagedJFrame; + +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SpotSceneModel; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneModel; +import com.threerings.stage.tools.xml.StageSceneParser; +import com.threerings.stage.util.StageContext; + +/** + * The viewer frame is the main application window. + */ +public class ViewerFrame extends ManagedJFrame +{ + /** + * Creates a frame in which the viewer application can operate. + */ + public ViewerFrame (GraphicsConfiguration gc) + { + super(gc); + + // set up the frame options + setTitle("Scene Viewer"); + setDefaultCloseOperation(EXIT_ON_CLOSE); + + // set the frame and content panel background to black + setBackground(Color.black); + getContentPane().setBackground(Color.black); + + // create the "File" menu + KeyStroke accel = null; + JMenu menuSettings = new JMenu("File"); + + // open... + accel = KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuSettings, "Open scene...", KeyEvent.VK_O, + accel, this, "openScene"); + + // decoys! + MenuUtil.addMenuItem(menuSettings, "Decoys!", this, "getJiggy"); + + // quit + menuSettings.addSeparator(); + accel = KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK); + MenuUtil.addMenuItem(menuSettings, "Quit", KeyEvent.VK_Q, + accel, this, "handleQuit"); + + // create the menu bar + JMenuBar bar = new JMenuBar(); + bar.add(menuSettings); + + // add the menu bar to the frame + setJMenuBar(bar); + } + + /** + * Instructs us to create our scene panel. + */ + public void init (StageContext ctx, CharacterManager charmgr) + { + _panel = new ViewerScenePanel(ctx, charmgr); + getContentPane().add(_panel, BorderLayout.CENTER); + } + + /** + * Callback for opening a new scene. + */ + public void openScene (ActionEvent event) + { +// String where = ToolPrefs.config.getValue( +// "viewer.last_dir", System.getProperty("user.dir")); + String where = System.getProperty("user.dir"); + JFileChooser chooser = new JFileChooser(where); + chooser.setFileFilter(new FileFilter () { + public boolean accept (File f) { + return (f.isDirectory() || f.getName().endsWith(".xml")); + } + public String getDescription () { + return "XML Files"; + } + }); + int result = chooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + File filescene = chooser.getSelectedFile(); + loadScene(filescene.getPath()); +// ToolPrefs.config.setValue("viewer.last_dir", filescene.getParent()); + } + } + + public void getJiggy (ActionEvent event) + { + _panel.createDecoys(); + } + + /** + * Handles a request to get the fuck out of dodge. + */ + public void handleQuit (ActionEvent evt) + { + System.exit(0); + } + + public void loadScene (String path) + { + String errmsg = null; + + try { + StageSceneParser parser = new StageSceneParser(); + StageSceneModel model = (StageSceneModel)parser.parseScene(path); + if (model == null) { + errmsg = "No scene found in scene file '" + path + "'."; + + } else { + SpotSceneModel ssmodel = SpotSceneModel.getSceneModel(model); + Location defloc = null; + // find the default entrance to this scene + for (int ii = 0; ii < ssmodel.portals.length; ii++) { + Portal port = ssmodel.portals[ii]; + if (port.portalId == ssmodel.defaultEntranceId) { + defloc = port.getOppLocation(); + break; + } + } + if (defloc == null) { + Log.warning("Scene has no def. entrance '" + path + "'."); + } + + _panel.setScene(new StageScene(model, null), defloc); + } + + } catch (Exception e) { + errmsg = "Error parsing scene file '" + path + "'."; + Log.logStackTrace(e); + } + + if (errmsg != null) { + JOptionPane.showMessageDialog( + this, errmsg, "Load error", JOptionPane.ERROR_MESSAGE); + } + } + + protected ViewerScenePanel _panel; +} diff --git a/src/java/com/threerings/stage/tools/viewer/ViewerScenePanel.java b/src/java/com/threerings/stage/tools/viewer/ViewerScenePanel.java new file mode 100644 index 00000000..cff786c2 --- /dev/null +++ b/src/java/com/threerings/stage/tools/viewer/ViewerScenePanel.java @@ -0,0 +1,240 @@ +// +// $Id: ViewerScenePanel.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.viewer; + +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.event.MouseEvent; + +import com.samskivert.swing.Controller; +import com.samskivert.util.RandomUtil; + +import com.threerings.media.util.PerformanceMonitor; +import com.threerings.media.util.PerformanceObserver; + +import com.threerings.cast.CharacterDescriptor; +import com.threerings.cast.CharacterManager; +import com.threerings.cast.CharacterSprite; +import com.threerings.cast.ComponentRepository; +import com.threerings.cast.util.CastUtil; + +import com.threerings.media.sprite.PathObserver; +import com.threerings.media.sprite.Sprite; +import com.threerings.media.sprite.SpriteManager; +import com.threerings.media.util.LineSegmentPath; +import com.threerings.media.util.Path; + +import com.threerings.stage.client.StageScenePanel; +import com.threerings.whirled.spot.data.Location; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageScene; +import com.threerings.stage.util.StageContext; + +public class ViewerScenePanel extends StageScenePanel + implements PerformanceObserver, PathObserver +{ + /** + * Construct the panel and initialize it with a context. + */ + public ViewerScenePanel (StageContext ctx, CharacterManager charmgr) + { + super(ctx, new Controller() { + }); + + _charmgr = charmgr; + + // create the character descriptors + _descUser = CastUtil.getRandomDescriptor( + "female", ctx.getComponentRepository()); + _descDecoy = CastUtil.getRandomDescriptor( + "male", ctx.getComponentRepository()); + + // create the manipulable sprite + _sprite = createSprite(_descUser); + setFollowsPathable(_sprite, CENTER_ON_PATHABLE); + + PerformanceMonitor.register(this, "paint", 1000); + } + + // documentation inherited + public void setScene (StageScene scene, Location defloc) + { + setScene(scene); + + // move all of our sprites to the default entrance + _defloc = (StageLocation) defloc; + Point defpos = getScreenCoords(_defloc.x, _defloc.y); + _sprite.setLocation(defpos.x, defpos.y); + + if (_decoys != null) { + for (int ii = 0; ii < _decoys.length; ii++) { + _decoys[ii].setLocation(defpos.x, defpos.y); + } + createDecoyPaths(); + } + } + + /** + * Creates a new sprite. + */ + protected CharacterSprite createSprite (CharacterDescriptor desc) + { + CharacterSprite s = _charmgr.getCharacter(desc); + if (s != null) { + // start 'em out standing + s.setActionSequence(CharacterSprite.STANDING); + if (_defloc != null) { + Point defpos = getScreenCoords(_defloc.x, _defloc.y); + s.setLocation(defpos.x, defpos.y); + } else { + s.setLocation(300, 300); + } + s.addSpriteObserver(this); + _spritemgr.addSprite(s); + } + + return s; + } + + /** + * Creates the decoy sprites. + */ + public void createDecoys () + { + int decoys = DEFAULT_NUM_DECOYS; + try { + decoys = Integer.parseInt(System.getProperty("decoys")); + } catch (Exception e) { + } + + if (_decoys == null) { + _decoys = new CharacterSprite[decoys]; + for (int ii = 0; ii < decoys; ii++) { + _decoys[ii] = createSprite(_descDecoy); + } + } + + // if we have a scene, create paths for our decoys + createDecoyPaths(); + } + + /** + * Creates paths for the decoy sprites. + */ + protected void createDecoyPaths () + { + if (_decoys != null) { + for (int ii = 0; ii < _decoys.length; ii++) { + if (_decoys[ii] != null) { + createRandomPath(_decoys[ii]); + } + } + } + } + + // documentation inherited + public void paint (Graphics g) + { + super.paint(g); + PerformanceMonitor.tick(this, "paint"); + } + + // documentation inherited + public void checkpoint (String name, int ticks) + { + Log.info(name + " [ticks=" + ticks + "]."); + } + + /** MouseListener interface methods */ + public void mousePressed (MouseEvent e) + { + int x = e.getX(), y = e.getY(); + + switch (e.getModifiers()) { + case MouseEvent.BUTTON1_MASK: + createPath(_sprite, x, y); + break; + + case MouseEvent.BUTTON2_MASK: + if (_decoys != null) { + for (int ii = 0; ii < _decoys.length; ii++) { + createPath(_decoys[ii], x, y); + } + } + break; + } + } + + /** + * Assigns the sprite a path leading to the given destination + * screen coordinates. Returns whether a path was successfully + * assigned. + */ + protected boolean createPath (CharacterSprite s, int x, int y) + { + // get the path from here to there + LineSegmentPath path = (LineSegmentPath)getPath(s, x, y, false); + if (path == null) { + s.cancelMove(); + return false; + } + + // start the sprite moving along the path + path.setVelocity(100f/1000f); + s.move(path); + return true; + } + + /** + * Assigns a new random path to the given sprite. + */ + protected void createRandomPath (CharacterSprite s) + { + int x, y; + do { + x = _vbounds.x + RandomUtil.getInt(_vbounds.width); + y = _vbounds.y + RandomUtil.getInt(_vbounds.height); + } while (!createPath(s, x, y)); + } + + // documentation inherited from interface + public void pathCancelled (Sprite sprite, Path path) + { + // nothing doing + } + + // documentation inherited from interface + public void pathCompleted (Sprite sprite, Path path, long when) + { + if (sprite != _sprite) { + // move the decoy to a new random location + createRandomPath((CharacterSprite)sprite); + } + } + + /** The number of decoy characters milling about. */ + protected static final int DEFAULT_NUM_DECOYS = 10; + + /** The current scene's default entrance. */ + protected StageLocation _defloc; + + /** Provides character sprite data. */ + protected CharacterManager _charmgr; + + /** The character descriptor for the user character. */ + protected CharacterDescriptor _descUser; + + /** The character descriptor for the decoy characters. */ + protected CharacterDescriptor _descDecoy; + + /** The sprite we're manipulating within the view. */ + protected CharacterSprite _sprite; + + /** The test sprites that meander about aimlessly. */ + protected CharacterSprite[] _decoys; +} diff --git a/src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java b/src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java new file mode 100644 index 00000000..8c3c0c0e --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageMisoSceneRuleSet.java @@ -0,0 +1,21 @@ +// +// $Id: StageMisoSceneRuleSet.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.xml; + +import com.threerings.miso.data.SparseMisoSceneModel; +import com.threerings.miso.tools.xml.SparseMisoSceneRuleSet; + +import com.threerings.stage.data.StageMisoSceneModel; + +/** + * Customizes the miso scene rule set such that it creates {@link + * StageMisoSceneModel} instances. + */ +public class StageMisoSceneRuleSet extends SparseMisoSceneRuleSet +{ + protected SparseMisoSceneModel createMisoSceneModel () + { + return new StageMisoSceneModel(); + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageSceneParser.java b/src/java/com/threerings/stage/tools/xml/StageSceneParser.java new file mode 100644 index 00000000..54fed826 --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageSceneParser.java @@ -0,0 +1,74 @@ +// +// $Id: StageSceneParser.java 16546 2004-07-27 20:49:56Z ray $ + +package com.threerings.stage.tools.xml; + +import org.apache.commons.digester.Rule; + +import org.xml.sax.Attributes; + +import com.threerings.whirled.spot.tools.xml.SpotSceneRuleSet; +import com.threerings.whirled.tools.xml.SceneParser; +import com.threerings.whirled.tools.xml.SceneRuleSet; + +import com.threerings.whirled.spot.data.Location; + +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageSceneModel; + +/** + * Parses {@link StageSceneModel} instances from an XML description file. + */ +public class StageSceneParser extends SceneParser +{ + /** + * Constructs a parser that can be used to parse Stage scene models. + */ + public StageSceneParser () + { + super(""); + + // add a rule to parse scene colorizations + _digester.addRule("scene/zations/zation", new Rule() { + public void begin (String namespace, String name, + Attributes attrs) throws Exception { + StageSceneModel yoscene = (StageSceneModel) digester.peek(); + int classId = Integer.parseInt(attrs.getValue("classId")); + int colorId = Integer.parseInt(attrs.getValue("colorId")); + yoscene.setDefaultColor(classId, colorId); + } + }); + + // add rule sets for our aux scene models + registerAuxRuleSet(new SpotSceneRuleSet() { + protected Location createLocation () { + return new StageLocation(); + } + }); + registerAuxRuleSet(new StageMisoSceneRuleSet()); + } + + // documentation inherited from interface + protected SceneRuleSet createSceneRuleSet () + { + return new StageSceneRuleSet(); + } + + /** + * A simple hook for parsing a single scene from the command line. + */ + public static void main (String[] args) + { + if (args.length < 1) { + System.err.println("Usage: StageSceneParser scene.xml"); + System.exit(-1); + } + + try { + System.out.println( + "Parsed " + new StageSceneParser().parseScene(args[0])); + } catch (Exception e) { + e.printStackTrace(System.err); + } + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java b/src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java new file mode 100644 index 00000000..965cd7e8 --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageSceneRuleSet.java @@ -0,0 +1,21 @@ +// +// $Id: StageSceneRuleSet.java 6370 2003-02-12 07:32:00Z mdb $ + +package com.threerings.stage.tools.xml; + +import com.threerings.whirled.tools.xml.SceneRuleSet; + +import com.threerings.stage.data.StageScene; +import com.threerings.stage.data.StageSceneModel; + +/** + * Used to parse an {@link StageScene} from XML. + */ +public class StageSceneRuleSet extends SceneRuleSet +{ + // documentation inherited + protected Class getSceneClass () + { + return StageSceneModel.class; + } +} diff --git a/src/java/com/threerings/stage/tools/xml/StageSceneWriter.java b/src/java/com/threerings/stage/tools/xml/StageSceneWriter.java new file mode 100644 index 00000000..be74e534 --- /dev/null +++ b/src/java/com/threerings/stage/tools/xml/StageSceneWriter.java @@ -0,0 +1,67 @@ +// +// $Id: StageSceneWriter.java 20143 2005-03-30 01:12:48Z mdb $ + +package com.threerings.stage.tools.xml; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import com.megginson.sax.DataWriter; + +import com.threerings.miso.tools.xml.SparseMisoSceneWriter; + +import com.threerings.whirled.spot.data.SpotSceneModel; +import com.threerings.whirled.spot.tools.xml.SpotSceneWriter; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.tools.xml.SceneWriter; + +import com.threerings.stage.data.StageMisoSceneModel; + +import com.threerings.stage.data.StageSceneModel; + +/** + * Generates an XML representation of a {@link StageSceneModel}. + */ +public class StageSceneWriter extends SceneWriter +{ + public StageSceneWriter () + { + // register our auxiliary model writers + registerAuxWriter(SpotSceneModel.class, new SpotSceneWriter()); + registerAuxWriter(StageMisoSceneModel.class, + new SparseMisoSceneWriter()); + } + + // documentation inherited + protected void addSceneAttributes (SceneModel scene, AttributesImpl attrs) + { + super.addSceneAttributes(scene, attrs); + StageSceneModel sscene = (StageSceneModel)scene; + attrs.addAttribute("", "type", "", "", sscene.type); + } + + // documentation inherited + protected void writeSceneData (SceneModel scene, DataWriter writer) + throws SAXException + { + // write out any default colorizations + StageSceneModel sscene = (StageSceneModel)scene; + if (sscene.defaultColors != null) { + writer.startElement("zations"); + int[] keys = sscene.defaultColors.getKeys(); + for (int ii=0, nn=keys.length; ii < nn; ii++) { + int value = sscene.defaultColors.get(keys[ii]); + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "classId", "", "", + String.valueOf(keys[ii])); + attrs.addAttribute("", "colorId", "", "", + String.valueOf(value)); + writer.emptyElement("", "zation", "", attrs); + } + writer.endElement("zations"); + } + + super.writeSceneData(scene, writer); + } +} diff --git a/src/java/com/threerings/stage/util/PlacementConstraints.java b/src/java/com/threerings/stage/util/PlacementConstraints.java new file mode 100644 index 00000000..8e611528 --- /dev/null +++ b/src/java/com/threerings/stage/util/PlacementConstraints.java @@ -0,0 +1,559 @@ +// +// $Id: YoSceneUtil.java 19769 2005-03-17 07:38:31Z mdb $ + +package com.threerings.stage.util; + +import java.awt.Rectangle; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; +import com.threerings.util.MessageBundle; + +import com.threerings.media.tile.ObjectTile; +import com.threerings.media.tile.ObjectTileSet; +import com.threerings.media.tile.TileManager; + +import com.threerings.miso.data.ObjectInfo; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageCodes; +import com.threerings.stage.data.StageMisoSceneModel; +import com.threerings.stage.data.StageScene; + +/** + * Maintains extra information on objects in a scene and checks proposed + * placement operations for constraint violations. When the constraints + * object is in use, all placement operations (object additions and removals) + * must go through the constraints object so that the object's internal state + * remains consistent. + */ +public class PlacementConstraints + implements DirectionCodes, StageCodes +{ + /** + * Default constructor. + */ + public PlacementConstraints (TileManager tilemgr, StageScene scene) + { + _tilemgr = tilemgr; + _scene = scene; + _mmodel = StageMisoSceneModel.getSceneModel(scene.getSceneModel()); + + // add all the objects in the scene + StageMisoSceneModel.ObjectVisitor visitor = + new StageMisoSceneModel.ObjectVisitor() { + public void visit (ObjectInfo info) { + ObjectData data = createObjectData(info); + if (data != null) { + // clone the map key, as the visit method reuses a + // single ObjectInfo instance for uninteresting objects + // in a section + _objectData.put((ObjectInfo)info.clone(), data); + } + } + }; + _mmodel.visitObjects(visitor); + } + + /** + * Determines whether the constraints allow the specified object to be + * added to the scene. + * + * @return null if the constraints allow the operation, + * otherwise a translatable string explaining why the object can't be + * added + */ + public String allowAddObject (ObjectInfo info) + { + return allowModifyObjects(new ObjectInfo[] { info }, + new ObjectInfo[0]); + } + + /** + * Adds the specified object through the constraints. + */ + public void addObject (ObjectInfo info) + { + ObjectData data = createObjectData(info); + if (data != null) { + _scene.addObject(info); + _objectData.put(info, data); + } + } + + /** + * Determines whether the constraints allow the specified object to be + * removed from the scene. + * + * @return null if the constraints allow the operation, + * otherwise a translatable string explaining why the object can't be + * removed + */ + public String allowRemoveObject (ObjectInfo info) + { + return allowModifyObjects(new ObjectInfo[0], + new ObjectInfo[] { info }); + } + + /** + * Removes the specified object through the constraints. + */ + public void removeObject (ObjectInfo info) + { + _scene.removeObject(info); + _objectData.remove(info); + } + + /** + * Determines whether the constraints allow the specified objects to be + * added and removed simultaneously. + * + * @return null if the constraints allow the operation, + * otherwise a translatable string explaining why the objects can't be + * modified + */ + public String allowModifyObjects (ObjectInfo[] added, + ObjectInfo[] removed) + { + ObjectData[] addedData = new ObjectData[added.length]; + for (int i = 0; i < added.length; i++) { + addedData[i] = createObjectData(added[i]); + if (addedData[i] == null) { + return INTERNAL_ERROR; + } + } + + ObjectData[] removedData = getObjectDataFromInfo(removed); + if (removedData == null) { + return INTERNAL_ERROR; + } + + return allowModifyObjects(addedData, removedData); + } + + /** + * Returns an ObjectData array that corresponds to the supplied + * ObjectInfo array. Returns null on error. + */ + protected ObjectData[] getObjectDataFromInfo (ObjectInfo[] info) + { + if (info == null) { + return null; + } + ObjectData[] data = new ObjectData[info.length]; + for (int ii = 0; ii < info.length; ii++) { + data[ii] = (ObjectData)_objectData.get(info[ii]); + if (data[ii] == null) { + Log.warning("Couldn't match object info up to data [info=" + + info[ii] + "]."); + return null; + } + } + return data; + } + + /** + * Determines whether the constraints allow the specified objects to be + * added and removed simultaneously. Subclasses that wish to define + * additional constraints should override this method. + * + * @return null if the constraints allow the operation, + * otherwise a qualified translatable string explaining why the objects + * can't be modified + */ + protected String allowModifyObjects (ObjectData[] added, + ObjectData[] removed) + { + DirectionHeight dirheight = new DirectionHeight(); + + for (int i = 0; i < added.length; i++) { + if (added[i].tile.hasConstraint(ObjectTileSet.ON_SURFACE) && + !isOnSurface(added[i], added, removed)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.not_on_surface"); + } + + int dir = getConstraintDirection(added[i], ObjectTileSet.ON_WALL); + if (dir != NONE && !isOnWall(added[i], added, removed, dir)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.not_on_wall"); + } + + if (getConstraintDirectionHeight(added[i], ObjectTileSet.ATTACH, + dirheight) && !isAttached(added[i], added, removed, + dirheight.dir, dirheight.low)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.not_attached"); + } + + dir = getConstraintDirection(added[i], ObjectTileSet.SPACE); + if (dir != NONE && !hasSpace(added[i], added, removed, dir)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.no_space"); + } + + if (hasSpaceConstrainedAdjacent(added[i], added, removed)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.no_space_adj"); + } + } + + for (int i = 0; i < removed.length; i++) { + if (removed[i].tile.hasConstraint(ObjectTileSet.SURFACE) && + hasOnSurface(removed[i], added, removed)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.has_on_surface"); + } + + int dir = getConstraintDirection(removed[i], ObjectTileSet.WALL); + if (dir != NONE) { + if (hasOnWall(removed[i], added, removed, dir)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.has_on_wall"); + + } else if (hasAttached(removed[i], added, removed, dir)) { + return MessageBundle.qualify(STAGE_MESSAGE_BUNDLE, + "m.has_attached"); + } + } + } + + return null; + } + + /** + * Determines whether the specified surface has anything on it that won't + * be held up if the surface is removed. + */ + protected boolean hasOnSurface (ObjectData data, ObjectData[] added, + ObjectData[] removed) + { + ArrayList objects = getObjectData(data.bounds, added, removed); + for (int i = 0, size = objects.size(); i < size; i++) { + ObjectData odata = (ObjectData)objects.get(i); + if (odata.tile.hasConstraint(ObjectTileSet.ON_SURFACE) && + !isOnSurface(odata, added, removed)) { + return true; + } + } + return false; + } + + /** + * Determines whether the specified wall has anything on it that won't be + * held up if the wall is removed. + */ + protected boolean hasOnWall (ObjectData data, ObjectData[] added, + ObjectData[] removed, int dir) + { + ArrayList objects = getObjectData(data.bounds, added, removed); + for (int i = 0, size = objects.size(); i < size; i++) { + ObjectData odata = (ObjectData)objects.get(i); + if (getConstraintDirection(odata, ObjectTileSet.ON_WALL) == dir && + !isOnWall(odata, added, removed, dir)) { + return true; + } + } + return false; + } + + /** + * Determines whether the specified wall has anything attached to it that + * won't be held up if the wall is removed. + */ + protected boolean hasAttached (ObjectData data, ObjectData[] added, + ObjectData[] removed, int dir) + { + DirectionHeight dirheight = new DirectionHeight(); + + ArrayList objects = getObjectData(getAdjacentEdge(data.bounds, + DirectionUtil.getOpposite(dir)), added, removed); + for (int i = 0, size = objects.size(); i < size; i++) { + ObjectData odata = (ObjectData)objects.get(i); + if (getConstraintDirectionHeight(odata, ObjectTileSet.ATTACH, + dirheight) && !isAttached(odata, added, removed, + dirheight.dir, dirheight.low)) { + return true; + } + } + return false; + } + + /** + * Verifies that the objects adjacent to the given object will still have + * their space constraints met if the object is added. + */ + protected boolean hasSpaceConstrainedAdjacent (ObjectData data, + ObjectData[] added, ObjectData[] removed) + { + Rectangle r = data.bounds; + // grow the ObjectData bounds 1 square in each direction + _constrainRect.setBounds(r.x - 1, r.y - 1, r.width + 2, r.height + 2); + + ArrayList objects = getObjectData(_constrainRect, added, removed); + for (int i = 0, size = objects.size(); i < size; i++) { + ObjectData odata = (ObjectData)objects.get(i); + int dir = getConstraintDirection(odata, ObjectTileSet.SPACE); + if (dir != NONE && !hasSpace(odata, added, removed, dir)) { + return true; + } + } + return false; + } + + /** + * Determines whether the specified object has empty space in the specified + * direction. + */ + protected boolean hasSpace (ObjectData data, ObjectData[] added, + ObjectData[] removed, int dir) + { + return getObjectData(getAdjacentEdge(data.bounds, dir), added, + removed).size() == 0; + } + + /** + * Determines whether the specified object is on a surface. + */ + protected boolean isOnSurface (ObjectData data, ObjectData[] added, + ObjectData[] removed) + { + return isCovered(data.bounds, added, removed, ObjectTileSet.SURFACE, + null); + } + + /** + * Determines whether the specified object is on a wall facing the + * specified direction. + */ + protected boolean isOnWall (ObjectData data, ObjectData[] added, + ObjectData[] removed, int dir) + { + return isCovered(data.bounds, added, removed, + getDirectionalConstraint(ObjectTileSet.WALL, dir), null); + } + + /** + * Determines whether the specified object is attached to another object in + * the specified direction and at the specified height. + */ + protected boolean isAttached (ObjectData data, ObjectData[] added, + ObjectData[] removed, int dir, boolean low) + { + return isCovered(getAdjacentEdge(data.bounds, dir), added, removed, + getDirectionalConstraint(ObjectTileSet.WALL, dir), low ? + getDirectionalConstraint(ObjectTileSet.WALL, dir, true) : null); + } + + /** + * Given a rectangle, determines whether all of the tiles within + * the rectangle intersect an object. If the constraint parameter is + * non-null, the intersected objects must have that constraint (or the + * alternate constraint, if specified). + */ + protected boolean isCovered (Rectangle rect, ObjectData[] added, + ObjectData[] removed, String constraint, String altstraint) + { + ArrayList objects = getObjectData(rect, added, removed); + for (int y = rect.y, ymax = rect.y + rect.height; y < ymax; y++) { + for (int x = rect.x, xmax = rect.x + rect.width; x < xmax; x++) { + boolean covered = false; + for (int i = 0, size = objects.size(); i < size; i++) { + ObjectData data = (ObjectData)objects.get(i); + if (data.bounds.contains(x, y) && (constraint == null || + data.tile.hasConstraint(constraint) || + (altstraint != null && + data.tile.hasConstraint(altstraint)))) { + covered = true; + break; + } + } + if (!covered) { + return false; + } + } + } + return true; + } + + /** + * Creates and returns a rectangle that covers the specified rectangle's + * adjacent edge (the squares one tile beyond the bounds) in the specified + * direction. + */ + protected Rectangle getAdjacentEdge (Rectangle rect, int dir) + { + switch (dir) { + case NORTH: + return new Rectangle(rect.x - 1, rect.y, 1, rect.height); + + case EAST: + return new Rectangle(rect.x, rect.y - 1, rect.width, 1); + + case SOUTH: + return new Rectangle(rect.x + rect.width, rect.y, 1, + rect.height); + + case WEST: + return new Rectangle(rect.x, rect.y + rect.height, + rect.width, 1); + + default: + return null; + } + } + + /** + * Returns the direction in which the specified object is constrained by + * appending "[NESW]" to the given constraint prefix. Returns + * NONE if there is no such directional constraint. + */ + protected int getConstraintDirection (ObjectData data, String prefix) + { + DirectionHeight dirheight = new DirectionHeight(); + return getConstraintDirectionHeight(data, prefix, dirheight) ? + dirheight.dir : NONE; + } + + /** + * Populates the supplied {@link DirectionHeight} object with the direction + * and height of the constraint identified by the given prefix. + * + * @return true if the object was successfully populated, false if there is + * no such constraint + */ + protected boolean getConstraintDirectionHeight (ObjectData data, + String prefix, DirectionHeight dirheight) + { + String[] constraints = data.tile.getConstraints(); + if (constraints == null) { + return false; + } + + for (int i = 0; i < constraints.length; i++) { + if (constraints[i].startsWith(prefix)) { + int fromidx = prefix.length(), + toidx = constraints[i].indexOf('_', fromidx); + dirheight.dir = DirectionUtil.fromShortString(toidx == -1 ? + constraints[i].substring(fromidx) : + constraints[i].substring(fromidx, toidx)); + dirheight.low = constraints[i].endsWith(ObjectTileSet.LOW); + return true; + } + } + return false; + } + + /** + * Given a constraint prefix and a direction, returns the directional + * constraint. + */ + protected String getDirectionalConstraint (String prefix, int dir) + { + return getDirectionalConstraint(prefix, dir, false); + } + + /** + * Given a constraint prefix, direction, and height, returns the + * directional constraint. + */ + protected String getDirectionalConstraint (String prefix, int dir, + boolean low) + { + return prefix + DirectionUtil.toShortString(dir) + + (low ? ObjectTileSet.LOW : ""); + } + + /** + * Finds all objects whose bounds intersect the given rectangle and + * returns a list containing their {@link ObjectData} elements. + * + * @param added an array of objects to add to the search + * @param removed an array of objects to exclude from the search + */ + protected ArrayList getObjectData (Rectangle rect, ObjectData[] added, + ObjectData[] removed) + { + ArrayList list = new ArrayList(); + + for (Iterator it = _objectData.values().iterator(); it.hasNext(); ) { + ObjectData data = (ObjectData)it.next(); + if (rect.intersects(data.bounds) && !ListUtil.contains(removed, + data)) { + list.add(data); + } + } + + for (int i = 0; i < added.length; i++) { + if (rect.intersects(added[i].bounds)) { + list.add(added[i]); + } + } + + return list; + } + + /** + * Using the tile manager, computes and returns the specified object's + * data. + */ + protected ObjectData createObjectData (ObjectInfo info) + { + try { + ObjectTile tile = (ObjectTile)_tilemgr.getTile(info.tileId); + Rectangle bounds = new Rectangle(info.x, info.y, tile.getBaseWidth(), + tile.getBaseHeight()); + bounds.translate(1 - bounds.width, 1 - bounds.height); + return new ObjectData(bounds, tile); + + } catch (Exception e) { + Log.warning("Error retrieving tile for object [info=" + + info + ", error=" + e + "]."); + Log.logStackTrace(e); + return null; + } + } + + /** Contains information about an object used in checking constraints. */ + protected class ObjectData + { + public Rectangle bounds; + public ObjectTile tile; + + public ObjectData (Rectangle bounds, ObjectTile tile) + { + this.bounds = bounds; + this.tile = tile; + } + } + + /** Contains the direction and height of a constraint. */ + protected class DirectionHeight + { + public int dir; + public boolean low; + } + + /** The tile manager to use for object dimensions and constraints. */ + protected TileManager _tilemgr; + + /** The scene being checked for constraints. */ + protected StageScene _scene; + + /** The Miso scene model. */ + protected StageMisoSceneModel _mmodel; + + /** For all objects in the scene, maps {@link ObjectInfo}s to + * {@link ObjectData}s. */ + protected HashMap _objectData = new HashMap(); + + /** One rectangle we'll re-use for all constraints ops. */ + protected static final Rectangle _constrainRect = new Rectangle(); +} diff --git a/src/java/com/threerings/stage/util/StageContext.java b/src/java/com/threerings/stage/util/StageContext.java new file mode 100644 index 00000000..725510bf --- /dev/null +++ b/src/java/com/threerings/stage/util/StageContext.java @@ -0,0 +1,76 @@ +// +// $Id: BasicYoContext.java 19661 2005-03-09 02:40:29Z andrzej $ + +package com.threerings.stage.util; + +import com.threerings.resource.ResourceManager; +import com.threerings.util.KeyDispatcher; +import com.threerings.util.KeyboardManager; +import com.threerings.util.MessageManager; + +import com.threerings.media.FrameManager; +import com.threerings.media.image.ColorPository; +import com.threerings.media.image.ImageManager; + +import com.threerings.cast.CharacterManager; +import com.threerings.cast.ComponentRepository; +import com.threerings.miso.util.MisoContext; + +/** + * A context that provides for the myriad requirements of the Stage + * system. + */ +public interface StageContext + extends MisoContext +{ + /** + * Returns the frame manager driving our interface. + */ + public FrameManager getFrameManager (); + + /** + * Returns the resource manager via which all client resources are + * loaded. + */ + public ResourceManager getResourceManager (); + + /** + * Access to the image manager. + */ + public ImageManager getImageManager (); + + /** + * Provides access to the key dispatcher. + */ + public KeyDispatcher getKeyDispatcher (); + + /** + * Returns a reference to the message manager used by the client. + */ + public MessageManager getMessageManager (); + + /** + * Returns a reference to the keyboard manager. + */ + public KeyboardManager getKeyboardManager(); + + /** + * Returns the component repository in use by this client. + */ + public ComponentRepository getComponentRepository (); + + /** + * Returns a reference to the colorization repository. + */ + public ColorPository getColorPository (); + + /** + * Translates the specified message using the default bundle. + */ + public String xlate (String message); + + /** + * Translates the specified message using the specified bundle. + */ + public String xlate (String bundle, String message); +} diff --git a/src/java/com/threerings/stage/util/StageSceneUtil.java b/src/java/com/threerings/stage/util/StageSceneUtil.java new file mode 100644 index 00000000..79ef83e0 --- /dev/null +++ b/src/java/com/threerings/stage/util/StageSceneUtil.java @@ -0,0 +1,413 @@ +// +// $Id: YoSceneUtil.java 19769 2005-03-17 07:38:31Z mdb $ + +package com.threerings.stage.util; + +import java.awt.Point; +import java.awt.Rectangle; +import java.util.ArrayList; +import java.util.Comparator; + +import com.samskivert.util.SortableArrayList; +import com.samskivert.util.StringUtil; +import com.threerings.util.DirectionCodes; +import com.threerings.util.DirectionUtil; + +import com.threerings.media.tile.TileManager; +import com.threerings.media.tile.TileUtil; +import com.threerings.media.tile.TrimmedObjectTileSet; +import com.threerings.media.util.AStarPathUtil; +import com.threerings.media.util.MathUtil; + +import com.threerings.miso.MisoConfig; +import com.threerings.miso.data.ObjectInfo; +import com.threerings.miso.tile.BaseTileSet; +import com.threerings.miso.util.MisoSceneMetrics; +import com.threerings.miso.util.MisoUtil; +import com.threerings.miso.util.ObjectSet; + +import com.threerings.whirled.spot.data.Cluster; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.SceneLocation; + +import com.threerings.stage.Log; +import com.threerings.stage.data.StageLocation; +import com.threerings.stage.data.StageMisoSceneModel; +import com.threerings.stage.data.StageSceneModel; + +/** + * Provides scene related utility functions. + */ +public class StageSceneUtil +{ + /** + * Returns the scene metrics we use to do our calculations. + */ + public static MisoSceneMetrics getMetrics () + { + return _metrics; + } + + /** + * Does the necessary jiggery pokery to figure out where the specified + * object's associated location is. + */ + public static StageLocation locationForObject ( + TileManager tilemgr, ObjectInfo info) + { + return locationForObject(tilemgr, info.tileId, info.x, info.y); + } + + /** + * Does the necessary jiggery pokery to figure out where the specified + * object's associated location is. + * + * @param tilemgr a tile manager that can be used to look up the tile + * information. + * @param tileId the fully qualified tile id of the object tile. + * @param tx the object's x tile coordinate. + * @param ty the object's y tile coordinate. + */ + public static StageLocation locationForObject ( + TileManager tilemgr, int tileId, int tx, int ty) + { + try { + int tsid = TileUtil.getTileSetId(tileId); + int tidx = TileUtil.getTileIndex(tileId); + TrimmedObjectTileSet tset = (TrimmedObjectTileSet) + tilemgr.getTileSet(tsid); + if (tset == null || tset.getSpotOrient(tidx) < 0) { + return null; + } + + Point opos = MisoUtil.tilePlusFineToFull( + _metrics, tx, ty, tset.getXSpot(tidx), tset.getYSpot(tidx), + new Point()); + +// Log.info("Computed location [set=" + tset.getName() + +// ", tidx=" + tidx + ", tx=" + tx + ", ty=" + ty + +// ", sx=" + tset.getXSpot(tidx) + +// ", sy=" + tset.getYSpot(tidx) + +// ", lx=" + opos.x + ", ly=" + opos.y + +// ", fg=" + _metrics.finegran + "]."); + return new StageLocation(opos.x, opos.y, + (byte)tset.getSpotOrient(tidx)); + + } catch (Exception e) { + Log.warning("Unable to look up object tile for scene object " + + "[tileId=" + tileId + ", error=" + e + "]."); + } + return null; + } + + /** + * Converts full coordinates to Cartesian coordinates. + */ + public static void locationToCoords (int lx, int ly, Point coords) + { + int tx = MisoUtil.fullToTile(lx), fx = MisoUtil.fullToFine(lx); + int ty = MisoUtil.fullToTile(ly), fy = MisoUtil.fullToFine(ly); + coords.x = tx*_metrics.finegran+fx; + coords.y = ty*_metrics.finegran+fy; + } + + /** + * Converts Cartesian coordinates back to full coordinates. + */ + public static void coordsToLocation (int cx, int cy, Point loc) + { + loc.x = MisoUtil.toFull(cx/_metrics.finegran, cx%_metrics.finegran); + loc.y = MisoUtil.toFull(cy/_metrics.finegran, cy%_metrics.finegran); + } + + /** + * Returns the footprint, in absolute tile coordinates, for the + * specified object with origin as specified. + */ + public static Rectangle getObjectFootprint ( + TileManager tilemgr, int tileId, int ox, int oy) + { + Rectangle foot = new Rectangle(); + getObjectFootprint(tilemgr, tileId, ox, oy, foot); + return foot; + } + + /** + * Fills in the footprint, in absolute tile coordinates, for the + * specified object with origin as specified. + * + * @return true if the object was successfully looked up and the + * footprint filled in, false if an error occurred trying to look up + * the associated object tile. + */ + public static boolean getObjectFootprint ( + TileManager tilemgr, int tileId, int ox, int oy, Rectangle foot) + { + try { + int tsid = TileUtil.getTileSetId(tileId); + int tidx = TileUtil.getTileIndex(tileId); + TrimmedObjectTileSet tset = (TrimmedObjectTileSet) + tilemgr.getTileSet(tsid); + if (tset == null) { + return false; + } + + int bwidth = tset.getBaseWidth(tidx); + int bheight = tset.getBaseHeight(tidx); + foot.setBounds(ox - bwidth + 1, oy - bheight + 1, bwidth, bheight); + return true; + + } catch (Exception e) { + Log.warning("Unable to look up object tile for scene object " + + "[tileId=" + tileId + ", error=" + e + "]."); + return false; + } + } + + /** + * Looks up the base tile set for the specified fully qualified tile + * identifier and returns true if the associated tile is passable. + */ + public static boolean isPassable (TileManager tilemgr, int tileId) + { + // non-existent tiles are not passable + if (tileId <= 0) { + return false; + } + + try { + int tsid = TileUtil.getTileSetId(tileId); + int tidx = TileUtil.getTileIndex(tileId); + BaseTileSet tset = (BaseTileSet)tilemgr.getTileSet(tsid); + return tset.getPassability()[tidx]; + + } catch (Exception e) { + Log.warning("Unable to look up base tile [tileId=" + tileId + + ", error=" + e + "]."); + return true; + } + } + + /** + * Computes a list of the valid locations in this cluster. + */ + public static ArrayList getClusterLocs (Cluster cluster) + { + ArrayList list = new ArrayList(); + + // convert our tile coordinates into a cartesian coordinate system + // with units equal to one fine coordinate in size + int fx = cluster.x*_metrics.finegran+1, + fy = cluster.y*_metrics.finegran+1; + int fwid = cluster.width*_metrics.finegran-2, + fhei = cluster.height*_metrics.finegran-2; + int cx = fx + fwid/2, cy = fy + fhei/2; + + // if it's a 1x1 cluster, return one location in the center of the + // cluster + if (cluster.width == 1) { + StageLocation loc = new StageLocation( + MisoUtil.toFull(cluster.x, 2), MisoUtil.toFull(cluster.y, 2), + (byte)DirectionCodes.SOUTHWEST); + list.add(new SceneLocation(loc, 0)); + return list; + } + + double radius = (double)fwid/2; + int clidx = cluster.width-2; + if (clidx >= CLUSTER_METRICS.length/2 || clidx < 0) { + Log.warning("Requested locs from invalid cluster " + cluster + "."); + Thread.dumpStack(); + return list; + } + + for (double angle = CLUSTER_METRICS[clidx*2]; angle < Math.PI*2; + angle += CLUSTER_METRICS[clidx*2+1]) { + int sx = cx + (int)Math.round(Math.cos(angle) * radius); + int sy = cy + (int)Math.round(Math.sin(angle) * radius); + + // obtain the orientation facing toward the center + int orient = 2*(int)(Math.round(angle/(Math.PI/4))%8); + orient = DirectionUtil.rotateCW(DirectionCodes.SOUTH, orient); + orient = DirectionUtil.getOpposite(orient); + + // convert them back to full coordinates for the location + int tx = MathUtil.floorDiv(sx, _metrics.finegran); + sx = MisoUtil.toFull(tx, sx-(tx*_metrics.finegran)); + int ty = MathUtil.floorDiv(sy, _metrics.finegran); + sy = MisoUtil.toFull(ty, sy-(ty*_metrics.finegran)); + StageLocation loc = new StageLocation(sx, sy, (byte) orient); + list.add(new SceneLocation(loc, 0)); + } + + return list; + } + +// /** +// * Returns true if this user is available to be clustered with. +// */ +// public static boolean isClusterable (YoOccupantInfo info) +// { +// switch (info.activity) { +// case ActivityCodes.NONE: +// case ActivityCodes.READING: +// case ActivityCodes.IDLE: +// case ActivityCodes.DISCONNECTED: +// return true; +// default: +// return false; +// } +// } + + /** + * Locates a spot to stand near the supplied rectangular footprint. + * First a spot will be sought in a tile immediately next to the + * footprint, then one tile removed, then two, up to the maximum + * distance specified by dist. + * + * @param foot the tile coordinate footprint around which we are + * attempting to stand. + * @param dist the maximum number of tiles away from the footprint to + * search before giving up. + * @param pred a predicate that will be used to determine whether a + * particular spot can be stood upon (we're hijacking the meaning of + * "traverse" in this case, but the interface is otherwise so nice). + * @param traverser the object that will be passed to the traversal + * predicate. + * @param nearto a point (in tile coordinates) which will be used to + * select from among the valid standing spots, the one nearest to the + * supplied point will be returned. + * @param orient if not {@link DirectionCodes#NONE} this orientation + * will be used to override the "natural" orientation of the spot + * which is facing toward the footprint. + * + * @return the closest spot to the + */ + public static StageLocation findStandingSpot ( + Rectangle foot, int dist, AStarPathUtil.TraversalPred pred, + Object traverser, final Point nearto, int orient) + { + // generate a list of the tile coordinates of all squares around + // this footprint + SortableArrayList spots = new SortableArrayList(); + + for (int dd = 1; dd <= dist; dd++) { + int yy1 = foot.y-dd, yy2 = foot.y+foot.height+dd-1; + int xx1 = foot.x-dd, xx2 = foot.x+foot.width+dd-1; + + // get the corners + spots.add( + new StageLocation(xx1, yy1, (byte)DirectionCodes.SOUTHWEST)); + spots.add( + new StageLocation(xx1, yy2, (byte)DirectionCodes.SOUTHEAST)); + spots.add( + new StageLocation(xx2, yy1, (byte)DirectionCodes.NORTHWEST)); + spots.add( + new StageLocation(xx2, yy2, (byte)DirectionCodes.NORTHEAST)); + + // then the sides + for (int xx = xx1+1; xx < xx2; xx++) { + spots.add( + new StageLocation(xx, yy1, (byte)DirectionCodes.WEST)); + spots.add( + new StageLocation(xx, yy2, (byte)DirectionCodes.EAST)); + } + for (int yy = yy1+1; yy < yy2; yy++) { + spots.add( + new StageLocation(xx1, yy, (byte)DirectionCodes.SOUTH)); + spots.add( + new StageLocation(xx2, yy, (byte)DirectionCodes.NORTH)); + } + + // sort them in order of closeness to the players current + // coordinate + spots.sort(new Comparator() { + public int compare (Object o1, Object o2) { + return dist((StageLocation)o1) - dist((StageLocation)o2); + } + private final int dist (StageLocation l) { + return Math.round(100*MathUtil.distance( + l.x, l.y, nearto.x, nearto.y)); + } + }); + + // return the first spot that can be "traversed" which we're + // taking to mean "stood upon" + for (int ii = 0, ll = spots.size(); ii < ll; ii++) { + StageLocation loc = (StageLocation)spots.get(ii); + if (pred.canTraverse(traverser, loc.x, loc.y)) { + // convert to full coordinates + loc.x = MisoUtil.toFull(loc.x, 2); + loc.y = MisoUtil.toFull(loc.y, 2); + + // see if we need to override the orientation + if (DirectionCodes.NONE != orient) { + loc.orient = (byte) orient; + } + return loc; + } + } + + // clear this list and try one further out + spots.clear(); + } + + return null; + } + + /** + * Returns an array of the objects intersected by the supplied tile + * coordinate rectangle. + */ + public static ObjectInfo[] getIntersectedObjects ( + TileManager tmgr, StageSceneModel model, Rectangle rect) + { + // first get all objects whose origin is in an expanded version of + // our intersection rect, any object that is *so* large that its + // origin falls outside of this rectangle but that still + // intersects this rectangle can go to hell; it's either this or + // we iterate over every object in the whole goddamned scene which + // is so hairily inefficient i can't even bear to contemplate it + ObjectSet objs = new ObjectSet(); + Rectangle orect = new Rectangle(rect); + orect.grow(MAX_OBJECT_SIZE, MAX_OBJECT_SIZE); + StageMisoSceneModel mmodel = StageMisoSceneModel.getSceneModel(model); + mmodel.getObjects(orect, objs); + + // now prune from this set any and all objects that don't actually + // overlap the specified rectangle + Rectangle foot = new Rectangle(); + for (int ii = 0; ii < objs.size(); ii++) { + ObjectInfo info = objs.get(ii); + if (getObjectFootprint(tmgr, info.tileId, info.x, info.y, foot)) { + if (!foot.intersects(rect)) { + objs.remove(ii--); + } + } else { + Log.warning("Unknown potentially intersecting object?! " + + "[scene=" + model.name + " (" + model.sceneId + + "), info=" + info + "]."); + } + } + + return objs.toArray(); + } + + /** Our default scene metrics. */ + protected static MisoSceneMetrics _metrics = MisoConfig.getSceneMetrics(); + + /** Contains the starting offset from zero radians for the first + * occupant and the radial distance between occupants. */ + protected static final double[] CLUSTER_METRICS = { + Math.PI/4, Math.PI/2, // 2x + Math.PI/4, Math.PI/2, // 3x + 0, Math.PI/4, // 4x + Math.PI/12, Math.PI/6, // 5x + 0, Math.PI/8, // 6x + Math.PI/24, Math.PI/12, // 7x + }; + + /** The maximum footprint width or height for which we will account in + * {@link #getIntersectedObjects}. */ + protected static final int MAX_OBJECT_SIZE = 15; +} diff --git a/src/java/com/threerings/whirled/Log.java b/src/java/com/threerings/whirled/Log.java new file mode 100644 index 00000000..de1e57fe --- /dev/null +++ b/src/java/com/threerings/whirled/Log.java @@ -0,0 +1,63 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled; + +/** + * A placeholder class that contains a reference to the log object used by + * the Whirled services. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("whirled"); + + /** Convenience function. */ + public static boolean debug () + { + return (com.samskivert.util.Log.getLevel() == + com.samskivert.util.Log.DEBUG); + } + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/whirled/client/SceneController.java b/src/java/com/threerings/whirled/client/SceneController.java new file mode 100644 index 00000000..661d82c6 --- /dev/null +++ b/src/java/com/threerings/whirled/client/SceneController.java @@ -0,0 +1,91 @@ +// +// $Id: SceneController.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.client; + +import com.threerings.presents.dobj.MessageEvent; +import com.threerings.presents.dobj.MessageListener; + +import com.threerings.crowd.client.PlaceController; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.whirled.data.SceneCodes; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.util.WhirledContext; + +/** + * The base scene controller class. It is expected that users of the + * Whirled services will extend this controller class when creating + * specialized controllers for their scenes. + */ +public abstract class SceneController extends PlaceController +{ + // documentation inherited + public void init (CrowdContext ctx, PlaceConfig config) + { + super.init(ctx, config); + _wctx = (WhirledContext)ctx; + } + + // documentation inherited + public void willEnterPlace (PlaceObject plobj) + { + super.willEnterPlace(plobj); + plobj.addListener(_updateListener); + } + + // documentation inherited + public void didLeavePlace (PlaceObject plobj) + { + super.didLeavePlace(plobj); + plobj.removeListener(_updateListener); + } + + /** + * This method is called if a scene update is recorded while we + * currently occupy a scene. The default implementation will update + * our local scene and scene model, but derived classes will likely + * want to ensure that the update is properly displayed. + */ + protected void sceneUpdated (SceneUpdate update) + { + // apply the update to the scene + _wctx.getSceneDirector().getScene().updateReceived(update); + + // we don't persistify these updates in this circumstance, but + // next time we come to this scene we'll redownload the update and + // apply it to the repository; as the updates are meant to be very + // small, this shouldn't be horribly less efficient + } + + /** Used to listen for scene updates. */ + protected MessageListener _updateListener = new MessageListener() { + public void messageReceived (MessageEvent event) { + if (event.getName().equals(SceneCodes.SCENE_UPDATE)) { + sceneUpdated((SceneUpdate)event.getArgs()[0]); + } + } + }; + + protected WhirledContext _wctx; +} diff --git a/src/java/com/threerings/whirled/client/SceneDecoder.java b/src/java/com/threerings/whirled/client/SceneDecoder.java new file mode 100644 index 00000000..7e4c65e0 --- /dev/null +++ b/src/java/com/threerings/whirled/client/SceneDecoder.java @@ -0,0 +1,69 @@ +// +// $Id: SceneDecoder.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.client; + +import com.threerings.presents.client.InvocationDecoder; +import com.threerings.whirled.client.SceneReceiver; + +/** + * Dispatches calls to a {@link SceneReceiver} instance. + */ +public class SceneDecoder extends InvocationDecoder +{ + /** The generated hash code used to identify this receiver class. */ + public static final String RECEIVER_CODE = "c4d0cf66b81a6e83d119b2d607725651"; + + /** The method id used to dispatch {@link SceneReceiver#forcedMove} + * notifications. */ + public static final int FORCED_MOVE = 1; + + /** + * Creates a decoder that may be registered to dispatch invocation + * service notifications to the specified receiver. + */ + public SceneDecoder (SceneReceiver receiver) + { + this.receiver = receiver; + } + + // documentation inherited + public String getReceiverCode () + { + return RECEIVER_CODE; + } + + // documentation inherited + public void dispatchNotification (int methodId, Object[] args) + { + switch (methodId) { + case FORCED_MOVE: + ((SceneReceiver)receiver).forcedMove( + ((Integer)args[0]).intValue() + ); + return; + + default: + super.dispatchNotification(methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/whirled/client/SceneDirector.java b/src/java/com/threerings/whirled/client/SceneDirector.java new file mode 100644 index 00000000..4e00fed1 --- /dev/null +++ b/src/java/com/threerings/whirled/client/SceneDirector.java @@ -0,0 +1,532 @@ +// +// $Id: SceneDirector.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.client; + +import java.io.IOException; + +import com.samskivert.util.LRUHashMap; +import com.samskivert.util.ResultListener; +import com.samskivert.util.StringUtil; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; + +import com.threerings.crowd.client.LocationDirector; +import com.threerings.crowd.client.LocationObserver; +import com.threerings.crowd.data.PlaceConfig; + +import com.threerings.whirled.Log; +import com.threerings.whirled.client.persist.SceneRepository; +import com.threerings.whirled.data.Scene; +import com.threerings.whirled.data.SceneCodes; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.util.NoSuchSceneException; +import com.threerings.whirled.util.SceneFactory; +import com.threerings.whirled.util.WhirledContext; +import com.threerings.whirled.data.SceneUpdate; + +/** + * The scene director is the client's interface to all things scene + * related. It interfaces with the scene repository to ensure that scene + * objects are available when the client enters a particular scene. It + * handles moving from scene to scene (it coordinates with the {@link + * LocationDirector} in order to do this). + * + *

Note that when the scene director is in use instead of the location + * director, scene ids instead of place oids will be supplied to {@link + * LocationObserver#locationMayChange} and {@link + * LocationObserver#locationChangeFailed}. + */ +public class SceneDirector extends BasicDirector + implements SceneCodes, LocationDirector.FailureHandler, + SceneReceiver, SceneService.SceneMoveListener +{ + /** + * Used to recover from a problem after a completed moveTo. + */ + public static interface MoveHandler + { + /** + * Should instruct the client to move the last known working + * location (as well as clean up after the failed moveTo request). + */ + public void recoverMoveTo (int sceneId); + } + + /** + * Creates a new scene director with the specified context. + * + * @param ctx the active client context. + * @param locdir the location director in use on the client, with + * which the scene director will coordinate when changing location. + * @param screp the entity from which the scene director will load + * scene data from the local client scene storage. This may be null + * when the SceneDirector is constructed, but it should be + * supplied via {@link #setSceneRepository} prior to really using + * this director. + * @param fact the factory that knows which derivation of {@link + * Scene} to create for the current system. + */ + public SceneDirector (WhirledContext ctx, LocationDirector locdir, + SceneRepository screp, SceneFactory fact) + { + super(ctx); + + // we'll need these for later + _ctx = ctx; + _locdir = locdir; + setSceneRepository(screp); + _fact = fact; + + // set ourselves up as a failure handler with the location + // director because we need to do special processing + _locdir.setFailureHandler(this); + + // register for scene notifications + _ctx.getClient().getInvocationDirector().registerReceiver( + new SceneDecoder(this)); + } + + /** + * Set the scene repository. + */ + public void setSceneRepository (SceneRepository screp) + { + _screp = screp; + _scache.clear(); + } + + /** + * Returns the display scene object associated with the scene we + * currently occupy or null if we currently occupy no scene. + */ + public Scene getScene () + { + return _scene; + } + + /** + * Requests that this client move the specified scene. A request will + * be made and when the response is received, the location observers + * will be notified of success or failure. + * + * @return true if the move to request was issued, false if it was + * rejected by a location observer or because we have another request + * outstanding. + */ + public boolean moveTo (int sceneId) + { + // make sure the sceneId is valid + if (sceneId < 0) { + Log.warning("Refusing moveTo(): invalid sceneId " + sceneId + "."); + return false; + } + + // sanity-check the destination scene id + if (sceneId == _sceneId) { + Log.warning("Refusing request to move to the same scene " + + "[sceneId=" + sceneId + "]."); + return false; + } + + // prepare to move to this scene (sets up pending data) + if (!prepareMoveTo(sceneId, null)) { + return false; + } + + // check the version of our cached copy of the scene to which + // we're requesting to move; if we were unable to load it, assume + // a cached version of zero + int sceneVers = 0; + if (_pendingModel != null) { + sceneVers = _pendingModel.version; + } + + // issue a moveTo request + Log.info("Issuing moveTo(" + sceneId + ", " + sceneVers + ")."); + _sservice.moveTo(_ctx.getClient(), sceneId, sceneVers, this); + return true; + } + + /** + * Prepares to move to the requested scene. The location observers are + * asked to ratify the move and our pending scene mode is loaded from + * the scene repository. This can be called by cooperating directors + * that need to coopt the moveTo process. + */ + public boolean prepareMoveTo (int sceneId, ResultListener rl) + { + // first check to see if our observers are happy with this move + // request + if (!_locdir.mayMoveTo(sceneId, rl)) { + return false; + } + + // we need to call this both to mark that we're issuing a move + // request and to check to see if the last issued request should + // be considered stale + boolean refuse = _locdir.checkRepeatMove(); + + // complain if we're over-writing a pending request + if (_pendingSceneId != -1) { + if (refuse) { + Log.warning("Refusing moveTo; We have a request outstanding " + + "[psid=" + _pendingSceneId + + ", nsid=" + sceneId + "]."); + return false; + + } else { + Log.warning("Overriding stale moveTo request " + + "[psid=" + _pendingSceneId + + ", nsid=" + sceneId + "]."); + } + } + + // load up the pending scene so that we can communicate it's most + // recent version to the server + _pendingModel = loadSceneModel(sceneId); + + // make a note of our pending scene id + _pendingSceneId = sceneId; + + // all systems go + return true; + } + + /** + * Returns the model loaded in preparation for a scene + * transition. This is made available only for cooperating directors + * which may need to coopt the scene transition process. The pending + * model is only valid immediately following a call to {@link + * #prepareMoveTo}. + */ + public SceneModel getPendingModel () + { + return _pendingModel; + } + + // documentation inherited from interface + public void moveSucceeded (int placeId, PlaceConfig config) + { + // our move request was successful, deal with subscribing to our + // new place object + _locdir.didMoveTo(placeId, config); + + // since we're committed to moving to the new scene, we'll + // parallelize and go ahead and load up the new scene now rather + // than wait until subscription to our place object succeeds + + // keep track of our previous scene info + _previousSceneId = _sceneId; + + // clear out the old info + clearScene(); + + // make the pending scene the active scene + _sceneId = _pendingSceneId; + _pendingSceneId = -1; + + // load the new scene model + _model = loadSceneModel(_sceneId); + + // complain if we didn't find a scene + if (_model == null) { + Log.warning("Aiya! Unable to load scene [sid=" + _sceneId + + ", plid=" + placeId + "]."); + return; + } + + // and finally create a display scene instance with the model and + // the place config + _scene = _fact.createScene(_model, config); + } + + // documentation inherited from interface + public void moveSucceededWithUpdates ( + int placeId, PlaceConfig config, SceneUpdate[] updates) + { + Log.info("Got updates [placeId=" + placeId + ", config=" + config + + ", updates=" + StringUtil.toString(updates) + "]."); + + // apply the updates to our cached scene + SceneModel model = loadSceneModel(_pendingSceneId); + boolean failure = false; + for (int ii = 0; ii < updates.length; ii++) { + try { + updates[ii].validate(model); + } catch (IllegalStateException ise) { + Log.warning("Scene update failed validation [model=" + model + + ", update=" + updates[ii] + + ", error=" + ise.getMessage() + "]."); + failure = true; + break; + } + + try { + updates[ii].apply(model); + } catch (Exception e) { + Log.warning("Failure applying scene update [model=" + model + + ", update=" + updates[ii] + "]."); + Log.logStackTrace(e); + failure = true; + break; + } + } + + if (failure) { + // delete the now half-booched scene model from the repository + try { + _screp.deleteSceneModel(_pendingSceneId); + } catch (IOException ioe) { + Log.warning("Failure removing booched scene model " + + "[sceneId=" + _pendingSceneId + "]."); + Log.logStackTrace(ioe); + } + + // act as if the scene move failed, though we'll be in a funny + // state because the server thinks we've changed scenes, but + // the client can try again without its booched scene model + requestFailed(INTERNAL_ERROR); + return; + } + + // store the updated scene in the repository + try { + _screp.storeSceneModel(model); + } catch (IOException ioe) { + Log.warning("Failed to update repository with updated scene " + + "[sceneId=" + model.sceneId + "]."); + Log.logStackTrace(ioe); + } + + // finally pass through to the normal success handler + moveSucceeded(placeId, config); + } + + // documentation inherited from interface + public void moveSucceededWithScene ( + int placeId, PlaceConfig config, SceneModel model) + { + Log.info("Got updated scene model [placeId=" + placeId + + ", config=" + config + ", scene=" + model.sceneId + "/" + + model.name + "/" + model.version + "]."); + + // update the model in the repository + try { + _screp.storeSceneModel(model); + } catch (IOException ioe) { + Log.warning("Failed to update repository with new version " + + "[sceneId=" + model.sceneId + + ", nvers=" + model.version + "]."); + Log.logStackTrace(ioe); + } + + // update our scene cache + _scache.put(Integer.valueOf(model.sceneId), model); + + // and pass through to the normal move succeeded handler + moveSucceeded(placeId, config); + } + + // documentation inherited from interface + public void requestFailed (String reason) + { + // clear out our pending request oid + int sceneId = _pendingSceneId; + _pendingSceneId = -1; + + // let our observers know that something has gone horribly awry + _locdir.failedToMoveTo(sceneId, reason); + } + + /** + * Called to clean up our place and scene state information when we + * leave a scene. + */ + public void didLeaveScene () + { + // let the location director know what's up + _locdir.didLeavePlace(); + + // clear out our own scene state + clearScene(); + } + + // documentation inherited from interface + public void forcedMove (int sceneId) + { + Log.info("Moving at request of server [sceneId=" + sceneId + "]."); + + // clear out our old scene and place data + didLeaveScene(); + + // move to the new scene + moveTo(sceneId); + } + + /** + * Sets the moveHandler for use in recoverFailedMove. + */ + public void setMoveHandler (MoveHandler handler) + { + if (_moveHandler != null) { + Log.warning("Requested to set move handler, but we've " + + "already got one. The conflicting entities will " + + "likely need to perform more sophisticated " + + "coordination to deal with failures. " + + "[old=" + _moveHandler + ", new=" + handler + "]."); + + } else { + _moveHandler = handler; + } + } + + /** + * Called when something breaks down in the process of performing a + * moveTo request. + */ + public void recoverFailedMove (int placeId) + { + // we'll need this momentarily + int sceneId = _sceneId; + + // clear out our now bogus scene tracking info + clearScene(); + + // if we were previously somewhere (and that somewhere isn't where + // we just tried to go), try going back to that happy place + if (_previousSceneId != -1 && _previousSceneId != sceneId) { + // if we have a move handler use that + if (_moveHandler != null) { + _moveHandler.recoverMoveTo(_previousSceneId); + + } else { + moveTo(_previousSceneId); + } + } + } + + /** + * Clears out our current scene information and releases the scene + * model for the loaded scene back to the cache. + */ + protected void clearScene () + { + // clear out our scene id info + _sceneId = -1; + + // clear out our references + _model = null; + _scene = null; + } + + /** + * Loads a scene from the repository. If the scene is cached, it will + * be returned from the cache instead. + */ + protected SceneModel loadSceneModel (int sceneId) + { + // first look in the model cache + Integer key = Integer.valueOf(sceneId); + SceneModel model = (SceneModel)_scache.get(key); + + // load from the repository if it's not cached + if (model == null) { + try { + model = _screp.loadSceneModel(sceneId); + _scache.put(key, model); + + } catch (NoSuchSceneException nsse) { + // nothing special here, just fall through and return null + + } catch (IOException ioe) { + // complain first, then return null + Log.warning("Error loading scene [scid=" + sceneId + + ", error=" + ioe + "]."); + } + } + + return model; + } + + // documentation inherited from interface + public void clientDidLogoff (Client client) + { + super.clientDidLogoff(client); + + // clear out our business + clearScene(); + _scache.clear(); + _pendingSceneId = -1; + _pendingModel = null; + _previousSceneId = -1; + _sservice = null; + } + + // documentation inherited from interface + protected void fetchServices (Client client) + { + // get a handle on our scene service + _sservice = (SceneService)client.requireService(SceneService.class); + } + + /** Access to general client services. */ + protected WhirledContext _ctx; + + /** Access to our scene services. */ + protected SceneService _sservice; + + /** The client's active location director. */ + protected LocationDirector _locdir; + + /** The entity via which we load scene data. */ + protected SceneRepository _screp; + + /** The entity we use to create scenes from scene models. */ + protected SceneFactory _fact; + + /** A cache of scene model information. */ + protected LRUHashMap _scache = new LRUHashMap(5); + + /** The display scene object for the scene we currently occupy. */ + protected Scene _scene; + + /** The scene model for the scene we currently occupy. */ + protected SceneModel _model; + + /** The id of the scene we currently occupy. */ + protected int _sceneId = -1; + + /** Our most recent copy of the scene model for the scene we're about + * to enter. */ + protected SceneModel _pendingModel; + + /** The id of the scene for which we have an outstanding moveTo + * request, or -1 if we have no outstanding request. */ + protected int _pendingSceneId = -1; + + /** The id of the scene we previously occupied. */ + protected int _previousSceneId = -1; + + /** Reference to our move handler. */ + protected MoveHandler _moveHandler = null; +} diff --git a/src/java/com/threerings/whirled/client/SceneReceiver.java b/src/java/com/threerings/whirled/client/SceneReceiver.java new file mode 100644 index 00000000..09f328cf --- /dev/null +++ b/src/java/com/threerings/whirled/client/SceneReceiver.java @@ -0,0 +1,39 @@ +// +// $Id: SceneReceiver.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.client; + +import com.threerings.presents.client.InvocationReceiver; + +/** + * Defines, for the scene services, a set of notifications delivered + * asynchronously by the server to the client. + */ +public interface SceneReceiver extends InvocationReceiver +{ + /** + * Used to communicate a required move notification to the client. The + * server will have removed the client from their existing scene + * and the client is then responsible for generating a {@link + * SceneService#moveTo} request to move to the new scene. + */ + public void forcedMove (int sceneId); +} diff --git a/src/java/com/threerings/whirled/client/SceneService.java b/src/java/com/threerings/whirled/client/SceneService.java new file mode 100644 index 00000000..dd8b15cb --- /dev/null +++ b/src/java/com/threerings/whirled/client/SceneService.java @@ -0,0 +1,86 @@ +// +// $Id: SceneService.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.crowd.data.PlaceConfig; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; + +/** + * The scene service class provides the client interface to the scene + * related invocation services (e.g. moving from scene to scene). + */ +public interface SceneService extends InvocationService +{ + /** + * Used to communicate the response to a {@link #moveTo} request. + */ + public static interface SceneMoveListener extends InvocationListener + { + /** + * Indicates that a move succeeded. + * + * @param placeId the place object id of the newly occupied scene. + * @param config metadata related to the newly occupied scene. + */ + public void moveSucceeded (int placeId, PlaceConfig config); + + /** + * Indicates that a move succeeded and that the client's cached + * scene information should be updated with the supplied data. + * + * @param placeId the place object id of the newly occupied scene. + * @param config metadata related to the newly occupied scene. + * @param updates updates that must be applied to the client's + * copy of a scene model to bring it up to date. + */ + public void moveSucceededWithUpdates (int placeId, PlaceConfig config, + SceneUpdate[] updates); + + /** + * Indicates that a move succeeded and that the client's cached + * scene information should be updated with the supplied data. + * + * @param placeId the place object id of the newly occupied scene. + * @param config metadata related to the newly occupied scene. + * @param model a fresh copy of the most recent scene data for the + * newly occupied scene. + */ + public void moveSucceededWithScene (int placeId, PlaceConfig config, + SceneModel model); + } + + /** + * Requests that that this client's body be moved to the specified + * scene. + * + * @param sceneId the scene id to which we want to move. + * @param version the version number of the scene object that we have + * in our local repository. + */ + public void moveTo (Client client, int sceneId, int version, + SceneMoveListener listener); +} diff --git a/src/java/com/threerings/whirled/client/persist/SceneRepository.java b/src/java/com/threerings/whirled/client/persist/SceneRepository.java new file mode 100644 index 00000000..e98814dc --- /dev/null +++ b/src/java/com/threerings/whirled/client/persist/SceneRepository.java @@ -0,0 +1,65 @@ +// +// $Id: SceneRepository.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.client.persist; + +import java.io.IOException; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.util.NoSuchSceneException; + +/** + * The scene repository provides access to a persistent repository of + * scene information. + * + * @see SceneModel + */ +public interface SceneRepository +{ + /** + * Fetches the model for the scene with the specified id. + * + * @exception IOException thrown if an error occurs attempting to load + * the scene data. + * @exception NoSuchSceneException thrown if no scene exists with the + * specified scene id. + */ + public SceneModel loadSceneModel (int sceneId) + throws IOException, NoSuchSceneException; + + /** + * Updates or inserts this scene model as appropriate. + * + * @exception IOException thrown if an error occurs attempting to + * access the repository. + */ + public void storeSceneModel (SceneModel model) + throws IOException; + + /** + * Deletes the specified scene model from the repository. + * + * @exception IOException thrown if an error occurs attempting to + * access the repository. + */ + public void deleteSceneModel (int sceneId) + throws IOException; +} diff --git a/src/java/com/threerings/whirled/data/AuxModel.java b/src/java/com/threerings/whirled/data/AuxModel.java new file mode 100644 index 00000000..b855ce07 --- /dev/null +++ b/src/java/com/threerings/whirled/data/AuxModel.java @@ -0,0 +1,36 @@ +// +// $Id: AuxModel.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.io.Streamable; + +/** + * An interface that must be implemented by auxiliary scene models. + */ +public interface AuxModel extends Streamable, Cloneable +{ + /** + * Creates a clone of this auxiliary model. + */ + public Object clone () + throws CloneNotSupportedException; +} diff --git a/src/java/com/threerings/whirled/data/DefaultSceneConfig.java b/src/java/com/threerings/whirled/data/DefaultSceneConfig.java new file mode 100644 index 00000000..db2d864e --- /dev/null +++ b/src/java/com/threerings/whirled/data/DefaultSceneConfig.java @@ -0,0 +1,44 @@ +// +// $Id: DefaultSceneConfig.java 4026 2006-04-18 01:32:41Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.whirled.client.SceneController; + +/** + * The default scene config simply causes the default scene manager and + * controller to be created. A user of the Whirled services would most + * likely extend the default scene config. + * + *

Note that this place config won't even work on the client side + * because it instantiates a {@link SceneController} which is an abstract + * class. It is used only for testing the server side and as a placeholder + * in case standard scene configuration information is one day needed. + */ +public class DefaultSceneConfig extends PlaceConfig +{ + // documentation inherited + public String getManagerClassName () + { + return "com.threerings.whirled.server.SceneManager"; + } +} diff --git a/src/java/com/threerings/whirled/data/Scene.java b/src/java/com/threerings/whirled/data/Scene.java new file mode 100644 index 00000000..a0b8505f --- /dev/null +++ b/src/java/com/threerings/whirled/data/Scene.java @@ -0,0 +1,83 @@ +// +// $Id: Scene.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.crowd.data.PlaceConfig; + +/** + * This interface makes available basic scene information. At this basic + * level, not much information is available, but extensions to this + * interface begin to create a more comprehensive picture of a scene in a + * system built from the Whirled services. + */ +public interface Scene +{ + /** + * Returns the unique identifier for this scene. + */ + public int getId (); + + /** + * Returns the human readable name of this scene. + */ + public String getName (); + + /** + * Returns the version number of this scene. + */ + public int getVersion (); + + /** + * Returns the place config that can be used to determine which place + * controller instance should be used to display this scene as well as + * to obtain runtime configuration information. + */ + public PlaceConfig getPlaceConfig (); + + /** + * Sets this scene's unique identifier. + */ + public void setId (int sceneId); + + /** + * Sets the human readable name of this scene. + */ + public void setName (String name); + + /** + * Sets this scene's version number. + */ + public void setVersion (int version); + + /** + * Called to inform the scene that an update has been received while + * the scene was resolved and active. The update should be applied to + * the underlying scene model and any derivative data should be + * appropriately updated. + */ + public void updateReceived (SceneUpdate update); + + /** + * Returns the scene model from which this scene was created. + */ + public SceneModel getSceneModel (); +} diff --git a/src/java/com/threerings/whirled/data/SceneCodes.java b/src/java/com/threerings/whirled/data/SceneCodes.java new file mode 100644 index 00000000..0d3e4520 --- /dev/null +++ b/src/java/com/threerings/whirled/data/SceneCodes.java @@ -0,0 +1,33 @@ +// +// $Id: SceneCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.crowd.data.LocationCodes; + +/** + * Contains codes used by the scene invocation services. + */ +public interface SceneCodes extends LocationCodes +{ + /** The message identifier for scene update messages. */ + public static final String SCENE_UPDATE = "scene_update"; +} diff --git a/src/java/com/threerings/whirled/data/SceneImpl.java b/src/java/com/threerings/whirled/data/SceneImpl.java new file mode 100644 index 00000000..85658b4b --- /dev/null +++ b/src/java/com/threerings/whirled/data/SceneImpl.java @@ -0,0 +1,127 @@ +// +// $Id: SceneImpl.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.crowd.data.PlaceConfig; + +import com.threerings.whirled.Log; + +/** + * An implementation of the {@link Scene} interface. + */ +public class SceneImpl implements Scene +{ + /** + * Creates an instance that will obtain data from the supplied scene + * model and place config. + */ + public SceneImpl (SceneModel model, PlaceConfig config) + { + _model = model; + _config = config; + } + + /** + * Instantiates a blank scene implementation. No place config will be + * associated with this scene. + */ + public SceneImpl () + { + _model = SceneModel.blankSceneModel(); + } + + // documentation inherited + public int getId () + { + return _model.sceneId; + } + + // documentation inherited + public String getName () + { + return _model.name; + } + + // documentation inherited + public int getVersion () + { + return _model.version; + } + + // documentation inherited + public PlaceConfig getPlaceConfig () + { + return _config; + } + + // documentation inherited from interface + public void setId (int sceneId) + { + _model.sceneId = sceneId; + } + + // documentation inherited from interface + public void setName (String name) + { + _model.name = name; + } + + // documentation inherited from interface + public void setVersion (int version) + { + _model.version = version; + } + + // documentation inherited from interface + public void updateReceived (SceneUpdate update) + { + try { + // validate and apply the update + update.validate(_model); + update.apply(_model); + } catch (Exception e) { + Log.warning("Error applying update [scene=" + this + + ", update=" + update + "]."); + Log.logStackTrace(e); + } + } + + // documentation inherited from interface + public SceneModel getSceneModel () + { + return _model; + } + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + return "[model=" + _model + ", config=" + _config + "]"; + } + + /** A reference to our scene model. */ + protected SceneModel _model; + + /** A reference to our place configuration. */ + protected PlaceConfig _config; +} diff --git a/src/java/com/threerings/whirled/data/SceneMarshaller.java b/src/java/com/threerings/whirled/data/SceneMarshaller.java new file mode 100644 index 00000000..adfa85e6 --- /dev/null +++ b/src/java/com/threerings/whirled/data/SceneMarshaller.java @@ -0,0 +1,124 @@ +// +// $Id: SceneMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import com.threerings.whirled.client.SceneService; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; + +/** + * Provides the implementation of the {@link SceneService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class SceneMarshaller extends InvocationMarshaller + implements SceneService +{ + // documentation inherited + public static class SceneMoveMarshaller extends ListenerMarshaller + implements SceneMoveListener + { + /** The method id used to dispatch {@link #moveSucceeded} + * responses. */ + public static final int MOVE_SUCCEEDED = 1; + + // documentation inherited from interface + public void moveSucceeded (int arg1, PlaceConfig arg2) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, MOVE_SUCCEEDED, + new Object[] { Integer.valueOf(arg1), arg2 })); + } + + /** The method id used to dispatch {@link #moveSucceededWithScene} + * responses. */ + public static final int MOVE_SUCCEEDED_WITH_SCENE = 2; + + // documentation inherited from interface + public void moveSucceededWithScene (int arg1, PlaceConfig arg2, SceneModel arg3) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, MOVE_SUCCEEDED_WITH_SCENE, + new Object[] { Integer.valueOf(arg1), arg2, arg3 })); + } + + /** The method id used to dispatch {@link #moveSucceededWithUpdates} + * responses. */ + public static final int MOVE_SUCCEEDED_WITH_UPDATES = 3; + + // documentation inherited from interface + public void moveSucceededWithUpdates (int arg1, PlaceConfig arg2, SceneUpdate[] arg3) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, MOVE_SUCCEEDED_WITH_UPDATES, + new Object[] { Integer.valueOf(arg1), arg2, arg3 })); + } + + // documentation inherited + public void dispatchResponse (int methodId, Object[] args) + { + switch (methodId) { + case MOVE_SUCCEEDED: + ((SceneMoveListener)listener).moveSucceeded( + ((Integer)args[0]).intValue(), (PlaceConfig)args[1]); + return; + + case MOVE_SUCCEEDED_WITH_SCENE: + ((SceneMoveListener)listener).moveSucceededWithScene( + ((Integer)args[0]).intValue(), (PlaceConfig)args[1], (SceneModel)args[2]); + return; + + case MOVE_SUCCEEDED_WITH_UPDATES: + ((SceneMoveListener)listener).moveSucceededWithUpdates( + ((Integer)args[0]).intValue(), (PlaceConfig)args[1], (SceneUpdate[])args[2]); + return; + + default: + super.dispatchResponse(methodId, args); + return; + } + } + } + + /** The method id used to dispatch {@link #moveTo} requests. */ + public static final int MOVE_TO = 1; + + // documentation inherited from interface + public void moveTo (Client arg1, int arg2, int arg3, SceneService.SceneMoveListener arg4) + { + SceneMarshaller.SceneMoveMarshaller listener4 = new SceneMarshaller.SceneMoveMarshaller(); + listener4.listener = arg4; + sendRequest(arg1, MOVE_TO, new Object[] { + Integer.valueOf(arg2), Integer.valueOf(arg3), listener4 + }); + } + +} diff --git a/src/java/com/threerings/whirled/data/SceneModel.java b/src/java/com/threerings/whirled/data/SceneModel.java new file mode 100644 index 00000000..f0e32d44 --- /dev/null +++ b/src/java/com/threerings/whirled/data/SceneModel.java @@ -0,0 +1,94 @@ +// +// $Id: SceneModel.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.samskivert.util.ArrayUtil; + +import com.threerings.io.SimpleStreamableObject; + +/** + * The scene model is the bare bones representation of the data for a + * scene in the Whirled system. From the scene model, one would create an + * instance of {@link Scene}. + * + *

The scene model is what is loaded from the scene repositories and + * what is transmitted over the wire when communicating scenes from the + * server to the client. + */ +public class SceneModel extends SimpleStreamableObject + implements Cloneable +{ + /** This scene's unique identifier. */ + public int sceneId; + + /** The human readable name of this scene. */ + public String name; + + /** The version number of this scene. Versions are incremented + * whenever modifications are made to a scene so that clients can + * determine whether or not they have the latest version of a + * scene. */ + public int version; + + /** Auxiliary scene model information. */ + public AuxModel[] auxModels = new AuxModel[0]; + + /** + * Adds the specified auxiliary model to this scene model. + */ + public void addAuxModel (AuxModel auxModel) + { + auxModels = (AuxModel[])ArrayUtil.append(auxModels, auxModel); + } + + // documentation inherited + public Object clone () + throws CloneNotSupportedException + { + SceneModel model = (SceneModel)super.clone(); + model.auxModels = new AuxModel[auxModels.length]; + for (int ii = 0; ii < auxModels.length; ii++) { + model.auxModels[ii] = (AuxModel)auxModels[ii].clone(); + } + return model; + } + + /** + * Creates and returns a blank scene model. + */ + public static SceneModel blankSceneModel () + { + SceneModel model = new SceneModel(); + populateBlankSceneModel(model); + return model; + } + + /** + * Populates a blank scene model with blank values. + */ + protected static void populateBlankSceneModel (SceneModel model) + { + model.sceneId = -1; + model.name = ""; + model.version = 0; + } +} diff --git a/src/java/com/threerings/whirled/data/SceneObject.java b/src/java/com/threerings/whirled/data/SceneObject.java new file mode 100644 index 00000000..63a47272 --- /dev/null +++ b/src/java/com/threerings/whirled/data/SceneObject.java @@ -0,0 +1,28 @@ +// +// $Id: SceneObject.java 3288 2004-12-28 03:51:29Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import com.threerings.crowd.data.PlaceObject; + +public class SceneObject extends PlaceObject +{ +} diff --git a/src/java/com/threerings/whirled/data/SceneUpdate.java b/src/java/com/threerings/whirled/data/SceneUpdate.java new file mode 100644 index 00000000..125b1b0b --- /dev/null +++ b/src/java/com/threerings/whirled/data/SceneUpdate.java @@ -0,0 +1,201 @@ +// +// $Id: SceneUpdate.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +import java.io.IOException; + +import com.samskivert.util.StringUtil; + +import com.threerings.io.ObjectInputStream; +import com.threerings.io.ObjectOutputStream; +import com.threerings.io.Streamable; + +import com.threerings.whirled.Log; + +/** + * Used to encapsulate updates to scenes in such a manner that updates can + * be stored persistently and sent to clients to update their own local + * copies of scenes. + */ +public class SceneUpdate + implements Streamable, Cloneable +{ + /** + * Initializes this scene update such that it will operate on a scene + * with the specified target scene and version number. + * + * @param targetId the id of the scene on which we are to operate. + * @param targetVersion the version of the scene on which we are to + * operate. + */ + public void init (int targetId, int targetVersion) + { + _targetId = targetId; + _targetVersion = targetVersion; + } + + /** + * Returns the scene id for which this update is appropriate. + */ + public int getSceneId () + { + return _targetId; + } + + /** + * Returns the scene version for which this update is appropriate. + */ + public int getSceneVersion () + { + return _targetVersion; + } + + /** + * Called to ensure that the scene is in the appropriate state prior + * to applying the update. + * + * @exception IllegalStateException thrown if the update cannot be + * applied to the scene because it is not in a valid state + * (appropriate previous updates were not applied, it's the wrong kind + * of scene, etc.). + */ + public void validate (SceneModel model) + throws IllegalStateException + { + if (model.sceneId != _targetId) { + String errmsg = "Wrong target scene, expected id " + + _targetId + " got id " + model.sceneId; + throw new IllegalStateException(errmsg); + } + if (model.version != _targetVersion) { + String errmsg = "Target scene not proper version, expected " + + _targetVersion + " got " + model.version; + throw new IllegalStateException(errmsg); + } + } + + /** + * Applies this update to the specified scene model. Derived classes + * will want to override this method and apply updates of their own, + * being sure to call super.apply. + */ + public void apply (SceneModel model) + { + // increment the version; disallowing integer overflow + model.version = Math.max(_targetVersion + 1, model.version); + + // sanity check for the amazing two billion updates + if (model.version == _targetVersion) { + Log.warning("Egads! This scene has been updated two billion " + + "times [model=" + model + ", update=" + this + "]."); + } + } + + /** + * Writes our custom streamable fields. + */ + public void writeObject (ObjectOutputStream out) + throws IOException + { + if (null == _dbSer.get()) { + out.writeInt(_targetId); + out.writeInt(_targetVersion); + } + out.defaultWriteObject(); + } + + /** + * Reads our custom streamable fields. + */ + public void readObject (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + if (null == _dbSer.get()) { + _targetId = in.readInt(); + _targetVersion = in.readInt(); + } + in.defaultReadObject(); + } + + /** + * Serializes the bare representation of this instance without the scene id + * and version fields. Useful when storing updates in a database where the + * scene id and version fields are stored in separate columns and the rest + * if the representation is contained in an opaque blob. + */ + public void persistTo (ObjectOutputStream out) + throws IOException + { + _dbSer.set(Boolean.TRUE); + try { + out.writeBareObject(this); + } finally { + _dbSer.set(null); + } + } + + /** + * Unserializes this instance from the bare representation created by + * {@link #persistTo}. + */ + public void unpersistFrom (ObjectInputStream in) + throws IOException, ClassNotFoundException + { + _dbSer.set(Boolean.TRUE); + try { + in.readBareObject(this); + } finally { + _dbSer.set(null); + } + } + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + StringBuilder buf = new StringBuilder("["); + toString(buf); + return buf.append("]").toString(); + } + + /** + * An extensible mechanism for generating a string representation of + * this instance. + */ + protected void toString (StringBuilder buf) + { + buf.append("sceneId=").append(_targetId); + buf.append(", version=").append(_targetVersion); + buf.append(", "); + StringUtil.fieldsToString(buf, this); + } + + /** The version number of the scene on which we operate. */ + protected transient int _targetId; + + /** The version number of the scene on which we operate. */ + protected transient int _targetVersion; + + /** Used when serializing this update for storage in the database. */ + protected static ThreadLocal _dbSer = new ThreadLocal(); +} diff --git a/src/java/com/threerings/whirled/data/ScenedBodyObject.java b/src/java/com/threerings/whirled/data/ScenedBodyObject.java new file mode 100644 index 00000000..09d49cce --- /dev/null +++ b/src/java/com/threerings/whirled/data/ScenedBodyObject.java @@ -0,0 +1,39 @@ +// +// $Id: ScenedBodyObject.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.data; + +/** + * A system that uses the whirled services must provide a body object + * extension that implements this interface. + */ +public interface ScenedBodyObject +{ + /** + * Returns the scene id currently occupied by this body. + */ + public int getSceneId (); + + /** + * Sets the scene id currently occupied by this body. + */ + public void setSceneId (int sceneId); +} diff --git a/src/java/com/threerings/whirled/server/SceneDispatcher.java b/src/java/com/threerings/whirled/server/SceneDispatcher.java new file mode 100644 index 00000000..2b4902c8 --- /dev/null +++ b/src/java/com/threerings/whirled/server/SceneDispatcher.java @@ -0,0 +1,73 @@ +// +// $Id: SceneDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import com.threerings.whirled.client.SceneService; +import com.threerings.whirled.data.SceneMarshaller; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; + +/** + * Dispatches requests to the {@link SceneProvider}. + */ +public class SceneDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public SceneDispatcher (SceneProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new SceneMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case SceneMarshaller.MOVE_TO: + ((SceneProvider)provider).moveTo( + source, + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), (SceneService.SceneMoveListener)args[2] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/whirled/server/SceneManager.java b/src/java/com/threerings/whirled/server/SceneManager.java new file mode 100644 index 00000000..daef19e7 --- /dev/null +++ b/src/java/com/threerings/whirled/server/SceneManager.java @@ -0,0 +1,194 @@ +// +// $Id: SceneManager.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import com.samskivert.io.PersistenceException; +import com.samskivert.util.Invoker; + +import com.threerings.crowd.server.PlaceManager; +import com.threerings.presents.server.PresentsServer; + +import com.threerings.whirled.Log; +import com.threerings.whirled.data.Scene; +import com.threerings.whirled.data.SceneCodes; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.server.WhirledServer; +import com.threerings.whirled.util.UpdateList; + +/** + * The scene manager extends the place manager and takes care of basic + * scene services. Presently that is little more than registering the + * scene manager with the scene registry so that the manager can be looked + * up by scene id in addition to place object id. + */ +public class SceneManager extends PlaceManager +{ + /** + * Returns the scene object (not the scene distributed object) being + * managed by this scene manager. + */ + public Scene getScene () + { + return _scene; + } + + /** + * Returns {@link UpdateList#getUpdates} for this scene's updates. + */ + public SceneUpdate[] getUpdates (int fromVersion) + { + return _updates.getUpdates(fromVersion); + } + + /** + * Called by the scene registry once the scene manager has been + * created (and initialized), but before it is started up. + */ + protected void setSceneData (Scene scene, UpdateList updates, + SceneRegistry screg) + { + _scene = scene; + _screg = screg; + _updates = updates; + + // make sure the list and our version of the scene are in + // accordance + if (!_updates.validate(scene.getVersion())) { + Log.warning("Provided with invalid updates; flushing " + + "[where=" + where() + + ", sceneId=" + scene.getId() + "]."); + // clear out the update list as it will not allow us to bring + // clients up to date with our current scene version; instead + // they'll have to download the whole thing + _updates = new UpdateList(); + } + + // let derived classes react to the receipt of scene data + gotSceneData(); + } + + /** + * A method that can be overridden by derived classes to perform + * initialization processing after we receive our scene information + * but before we're started up (and hence registered as an active + * place). + */ + protected void gotSceneData () + { + } + + /** + * We're fully ready to go, so now we register ourselves with the + * scene registry which will make us available to the clients and + * system at large. + */ + protected void didStartup () + { + super.didStartup(); + + // Wait until us and all of our subclasses have completely finished + // running didStartup prior to registering the scene as being ready. + PresentsServer.omgr.postRunnable(new Runnable() { + public void run () { + _screg.sceneManagerDidStart(SceneManager.this); + } + }); + } + + /** + * Called when we have shutdown. + */ + protected void didShutdown () + { + super.didShutdown(); + + // unregister ourselves with the scene registry + _screg.unmapSceneManager(this); + } + + /** + * When a modification is made to a scene, the scene manager should + * create a SceneUpdate instance and pass it to this method which will + * update the in-memory scene, and apply and record the update in the + * scene repository. + * + *

This update will be stored persistently and provided (along + * with any other accumulated updates) to clients that later request + * to enter the scene with an old version of the scene data. Updates + * are not stored forever, but a sizable number of recent updates are + * stored so that moderately current clients can apply incremental + * patches to their scenes rather than redownloading entire scenes + * when they change. + */ + protected void recordUpdate (final SceneUpdate update) + { + // instruct our in-memory copy of the scene to apply the update + _scene.updateReceived(update); + + // add it to our in memory update list + _updates.addUpdate(update); + + // and apply and store it in the repository + WhirledServer.invoker.postUnit(new Invoker.Unit() { + public boolean invoke () { + try { + _screg.getSceneRepository().applyAndRecordUpdate( + _scene.getSceneModel(), update); + } catch (PersistenceException pe) { + Log.warning("Failed to apply scene update " + update + "."); + Log.logStackTrace(pe); + } + return false; + } + }); + + // broadcast the update to all occupants of the scene + _plobj.postMessage(SceneCodes.SCENE_UPDATE, new Object[] { update }); + } + + // documentation inherited + public String where () + { + return _scene.getName() + " (" + super.where() + ":" + + _scene.getId() + ")"; + } + + // documentation inherited + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", scene=").append(_scene); + } + + /** A reference to our scene implementation which provides a + * meaningful interpretation of the data in the scene model. */ + protected Scene _scene; + + /** A list of the updates tracked for this scene. These will be used + * to attempt to bring clients up to date efficiently if they request + * to enter our scene with old scene model data. */ + protected UpdateList _updates; + + /** A reference to the scene registry so that we can call back to it + * when we're fully initialized. */ + protected SceneRegistry _screg; +} diff --git a/src/java/com/threerings/whirled/server/SceneProvider.java b/src/java/com/threerings/whirled/server/SceneProvider.java new file mode 100644 index 00000000..d300a226 --- /dev/null +++ b/src/java/com/threerings/whirled/server/SceneProvider.java @@ -0,0 +1,184 @@ +// +// $Id: SceneProvider.java 3832 2006-02-04 03:49:53Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.server.LocationProvider; + +import com.threerings.whirled.Log; +import com.threerings.whirled.client.SceneService.SceneMoveListener; +import com.threerings.whirled.data.SceneCodes; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.data.ScenedBodyObject; + +/** + * The scene provider handles the server side of the scene related + * invocation services (e.g. moving from scene to scene). + */ +public class SceneProvider + implements InvocationProvider, SceneCodes +{ + /** + * Constructs a scene provider that will interact with the supplied + * scene registry. + */ + public SceneProvider (LocationProvider locprov, SceneRegistry screg) + { + _locprov = locprov; + _screg = screg; + } + + /** + * Processes a request from a client to move to a new scene. + */ + public void moveTo (ClientObject caller, int sceneId, + final int sceneVer, final SceneMoveListener listener) + { + final BodyObject source = (BodyObject)caller; + + // create a callback object that will handle the resolution or + // failed resolution of the scene + SceneRegistry.ResolutionListener rl = null; + rl = new SceneRegistry.ResolutionListener() { + public void sceneWasResolved (SceneManager scmgr) { + // make sure our caller is still around; under heavy load, + // clients might end their session while the scene is + // resolving + if (!source.isActive()) { + Log.info("Abandoning scene move, client gone " + + "[who=" + source.who() + + ", dest=" + scmgr.where() + "]."); + InvocationMarshaller.setNoResponse(listener); + return; + } + finishMoveToRequest(source, scmgr, sceneVer, listener); + } + + public void sceneFailedToResolve (int rsceneId, Exception reason) { + Log.warning("Unable to resolve scene [sceneid=" + rsceneId + + ", reason=" + reason + "]."); + // pretend like the scene doesn't exist to the client + listener.requestFailed(NO_SUCH_PLACE); + } + }; + + // make sure the scene they are headed to is actually loaded into + // the server + _screg.resolveScene(sceneId, rl); + } + + /** + * This is called after the scene to which we are moving is guaranteed + * to have been loaded into the server. + */ + protected void finishMoveToRequest ( + BodyObject source, SceneManager scmgr, int sceneVersion, + SceneMoveListener listener) + { + try { + effectSceneMove(source, scmgr, sceneVersion, listener); + + } catch (InvocationException sfe) { + listener.requestFailed(sfe.getMessage()); + + } catch (RuntimeException re) { + Log.logStackTrace(re); + listener.requestFailed(INTERNAL_ERROR); + } + } + + /** + * Moves the supplied body into the supplied (already resolved) scene + * and informs the supplied listener if the move is successful. + * + * @exception InvocationException thrown if a failure occurs + * attempting to move the user into the place associated with the + * scene. + */ + public void effectSceneMove (BodyObject source, SceneManager scmgr, + int sceneVersion, SceneMoveListener listener) + throws InvocationException + { + // move to the place object associated with this scene + int ploid = scmgr.getPlaceObject().getOid(); + PlaceConfig config = _locprov.moveTo(source, ploid); + + // now that we've finally moved, we can update the user object + // with the new scene id + ((ScenedBodyObject)source).setSceneId(scmgr.getScene().getId()); + + // check to see if they need a newer version of the scene data + SceneModel model = scmgr.getScene().getSceneModel(); + if (sceneVersion < model.version) { + // try getting updates + SceneUpdate[] updates = scmgr.getUpdates(sceneVersion); + if (updates != null) { + listener.moveSucceededWithUpdates(ploid, config, updates); + } else { + listener.moveSucceededWithScene(ploid, config, model); + } + } else { + listener.moveSucceeded(ploid, config); + } + } + + /** + * Ejects the specified body from their current scene and sends them a + * request to move to the specified new scene. This is the + * scene-equivalent to {@link LocationProvider#moveBody}. + */ + public void moveBody (BodyObject source, int sceneId) + { + // first remove them from their old place + _locprov.leaveOccupiedPlace(source); + + // then send a forced move notification + SceneSender.forcedMove(source, sceneId); + } + + /** + * Ejects the specified body from their current scene and zone. This + * is the zone equivalent to {@link + * LocationProvider#leaveOccupiedPlace}. + */ + public void leaveOccupiedScene (ScenedBodyObject source) + { + // remove them from their occupied place + _locprov.leaveOccupiedPlace((BodyObject)source); + + // and clear out their scene information + source.setSceneId(-1); + } + + /** The location provider we use to handle low-level location stuff. */ + protected LocationProvider _locprov; + + /** The scene registry with which we interact. */ + protected SceneRegistry _screg; +} diff --git a/src/java/com/threerings/whirled/server/SceneRegistry.java b/src/java/com/threerings/whirled/server/SceneRegistry.java new file mode 100644 index 00000000..b2e79404 --- /dev/null +++ b/src/java/com/threerings/whirled/server/SceneRegistry.java @@ -0,0 +1,354 @@ +// +// $Id: SceneRegistry.java 3679 2005-08-11 22:30:40Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import java.util.ArrayList; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.Invoker; + +import com.threerings.presents.server.InvocationManager; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.server.CrowdServer; + +import com.threerings.whirled.Log; +import com.threerings.whirled.data.Scene; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.server.persist.SceneRepository; +import com.threerings.whirled.util.SceneFactory; +import com.threerings.whirled.util.UpdateList; + +/** + * The scene registry is responsible for the management of all scenes. It + * handles interaction with the scene repository and ensures that scenes + * are loaded into memory when needed and flushed from memory when not + * needed. + * + *

The scene repository also takes care of bridging from the blocking, + * synchronous world of the scene repository to the non-blocking + * asynchronous world of the distributed object event queue. Thus its + * interfaces for accessing scenes are structured so as to not block the + * dobjmgr thread while waiting for scenes to be read from or written to + * the repository. + * + *

Note: All access to the scene registry should take place + * from the dobjmgr thread. + */ +public class SceneRegistry +{ + /** + * Used to create {@link PlaceConfig} instances for scenes. + */ + public static interface ConfigFactory + { + /** + * Creates the place config instance appropriate to the specified + * scene. + */ + PlaceConfig createPlaceConfig (SceneModel model); + } + + /** Used to provide scene-related server-side services. */ + public SceneProvider sceneprov; + + /** + * Constructs a scene registry, instructing it to load and store + * scenes using the supplied scene repository. + */ + public SceneRegistry (InvocationManager invmgr, SceneRepository screp, + SceneFactory scfact, ConfigFactory confact) + { + _screp = screp; + _scfact = scfact; + _confact = confact; + + // create/register a scene provider with the invocation services + sceneprov = new SceneProvider(CrowdServer.plreg.locprov, this); + invmgr.registerDispatcher(new SceneDispatcher(sceneprov), true); + } + + /** + * Fetches the scene manager assosciated with the specified scene. + * + * @return the scene manager for the specified scene or null if no + * scene manager is loaded for that scene. + */ + public SceneManager getSceneManager (int sceneId) + { + return (SceneManager)_scenemgrs.get(sceneId); + } + + /** + * Returns a reference to the scene repository in use by this + * registry. + */ + public SceneRepository getSceneRepository () + { + return _screp; + } + + /** + * Returns {@link SceneManager#where} for the specified scene or + * null:sceneId if no scene manager exists for that + * scene. + */ + public String where (int sceneId) + { + SceneManager scmgr = getSceneManager(sceneId); + return (scmgr == null) ? ("null:" + sceneId) : scmgr.where(); + } + + /** + * Because scenes must be loaded from the scene repository and this + * must not be done on the dobjmgr thread, the interface for resolving + * scenes requires that the entity that wishes for a scene to be + * resolved implement this callback interface so that it can be + * notified when a scene has been loaded and initialized. + */ + public static interface ResolutionListener + { + /** + * Called when the scene has been successfully resolved. The scene + * manager instance provided can be used to obtain a reference to + * the scene, or the scene distributed object. + */ + public void sceneWasResolved (SceneManager scmgr); + + /** + * Called if some failure occurred in the scene resolution + * process. + */ + public void sceneFailedToResolve (int sceneId, Exception reason); + } + + /** + * Requests that the specified scene be resolved, which means loaded + * into the server and initialized if the scene is not currently + * active. The supplied callback instance will be notified, on the + * dobjmgr thread, when the scene has been resolved. If the scene is + * already active, it will be notified immediately (before the call to + * {@link #resolveScene} returns). + * + * @param sceneId the id of the scene to resolve. + * @param target a reference to a callback instance that will be + * notified when the scene has been resolved (which may be immediately + * if the scene is already active). + */ + public void resolveScene (int sceneId, ResolutionListener target) + { + SceneManager mgr = (SceneManager)_scenemgrs.get(sceneId); + if (mgr != null) { + // if the scene is already resolved, we're ready to roll + target.sceneWasResolved(mgr); + return; + } + + if (Log.debug()) { + Log.debug("Resolving scene [id=" + sceneId + "]."); + } + + // otherwise we've got to resolve the scene and call them back + // later; we can manipulate the penders table with impunity here + // because we only do so on the dobjmgr thread + ArrayList penders = (ArrayList)_penders.get(sceneId); + + // if we're already in the process of resolving this scene, just + // add these guys to the list to be notified when it finally is + // resolved + if (penders != null) { + penders.add(target); + + } else { + // otherwise we've got to initiate the resolution process. + // first we create the penders list + _penders.put(sceneId, penders = new ArrayList()); + penders.add(target); + + // i don't like cluttering up method declarations with final + // keywords... + final int fsceneId = sceneId; + + if (Log.debug()) { + Log.debug("Invoking scene lookup [id=" + sceneId + "]."); + } + + // then we queue up an execution unit that'll load the scene + // and initialize it and all that + WhirledServer.invoker.postUnit(new Invoker.Unit() { + // this is run on the invoker thread + public boolean invoke () + { + try { + _model = _screp.loadSceneModel(fsceneId); + _updates = _screp.loadUpdates(fsceneId); + } catch (Exception e) { + _cause = e; + } + return true; + } + + // this is run on the dobjmgr thread + public void handleResult () + { + if (_model != null) { + processSuccessfulResolution(_model, _updates); + } else if (_cause != null) { + processFailedResolution(fsceneId, _cause); + } else { + Log.warning("Scene loading unit finished with " + + "neither a scene nor a reason for " + + "failure!?"); + } + } + + public String toString () + { + return "SceneRegistry.SceneLoader " + + (_model == null ? "" : _model.name) + + "(" + fsceneId + ")"; + } + + protected SceneModel _model; + protected UpdateList _updates; + protected Exception _cause; + }); + } + } + + /** + * Called when the scene resolution has completed successfully. + */ + protected void processSuccessfulResolution ( + SceneModel model, UpdateList updates) + { + // now that the scene is loaded, we can create a scene manager for + // it. that will be initialized by the place registry and when + // that is finally complete, then we can let our penders know + // what's up + + try { + // first create our scene instance + Scene scene = _scfact.createScene( + model, _confact.createPlaceConfig(model)); + + // now create our scene manager + SceneManager scmgr = (SceneManager) + CrowdServer.plreg.createPlace(scene.getPlaceConfig(), null); + scmgr.setSceneData(scene, updates, this); + + // when the scene manager completes its startup procedings, it + // will call back to the scene registry and let us know that + // we can turn the penders loose + + } catch (Exception e) { + // so close, but no cigar + processFailedResolution(model.sceneId, e); + } + } + + /** + * Called if resolving the scene fails for some reason. + */ + protected void processFailedResolution (int sceneId, Exception cause) + { + Log.info("Failed to resolve scene [sceneId=" + sceneId + + ", cause=" + cause + "]."); + Log.logStackTrace(cause); + + // alas things didn't work out, notify our penders + ArrayList penders = (ArrayList)_penders.remove(sceneId); + if (penders != null) { + for (int i = 0; i < penders.size(); i++) { + ResolutionListener rl = (ResolutionListener)penders.get(i); + try { + rl.sceneFailedToResolve(sceneId, cause); + } catch (Exception e) { + Log.warning("Resolution listener choked."); + Log.logStackTrace(e); + } + } + } + } + + /** + * Called by the scene manager once it has started up (meaning that it + * has its place object and is ready to roll). + */ + protected void sceneManagerDidStart (SceneManager scmgr) + { + // register this scene manager in our table + int sceneId = scmgr.getScene().getId(); + _scenemgrs.put(sceneId, scmgr); + + if (Log.debug()) { + Log.debug("Registering scene manager [scid=" + sceneId + + ", scmgr=" + scmgr + "]."); + } + + // now notify any penders + ArrayList penders = (ArrayList)_penders.remove(sceneId); + if (penders != null) { + for (int i = 0; i < penders.size(); i++) { + ResolutionListener rl = (ResolutionListener)penders.get(i); + try { + rl.sceneWasResolved(scmgr); + } catch (Exception e) { + Log.warning("Resolution listener choked."); + Log.logStackTrace(e); + } + } + } + } + + /** + * Called by the scene manager when it is shut down. + */ + protected void unmapSceneManager (SceneManager scmgr) + { + if (_scenemgrs.remove(scmgr.getScene().getId()) == null) { + Log.warning("Requested to unmap unmapped scene manager " + + "[scmgr=" + scmgr + "]."); + return; + } + + if (Log.debug()) { + Log.debug("Unmapped scene manager " + scmgr + "."); + } + } + + /** The entity from which we load scene models. */ + protected SceneRepository _screp; + + /** Used to generate place configs for our scenes. */ + protected ConfigFactory _confact; + + /** The entity via which we create scene instances from scene + * models. */ + protected SceneFactory _scfact; + + /** A mapping from scene ids to scene managers. */ + protected HashIntMap _scenemgrs = new HashIntMap(); + + /** The table of pending resolution listeners. */ + protected HashIntMap _penders = new HashIntMap(); +} diff --git a/src/java/com/threerings/whirled/server/SceneSender.java b/src/java/com/threerings/whirled/server/SceneSender.java new file mode 100644 index 00000000..5132c732 --- /dev/null +++ b/src/java/com/threerings/whirled/server/SceneSender.java @@ -0,0 +1,47 @@ +// +// $Id: SceneSender.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationSender; +import com.threerings.whirled.client.SceneDecoder; +import com.threerings.whirled.client.SceneReceiver; + +/** + * Used to issue notifications to a {@link SceneReceiver} instance on a + * client. + */ +public class SceneSender extends InvocationSender +{ + /** + * Issues a notification that will result in a call to {@link + * SceneReceiver#forcedMove} on a client. + */ + public static void forcedMove ( + ClientObject target, int arg1) + { + sendNotification( + target, SceneDecoder.RECEIVER_CODE, SceneDecoder.FORCED_MOVE, + new Object[] { Integer.valueOf(arg1) }); + } + +} diff --git a/src/java/com/threerings/whirled/server/WhirledClient.java b/src/java/com/threerings/whirled/server/WhirledClient.java new file mode 100644 index 00000000..80c72b3b --- /dev/null +++ b/src/java/com/threerings/whirled/server/WhirledClient.java @@ -0,0 +1,40 @@ +// +// $Id: WhirledClient.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.server.CrowdClient; + +import com.threerings.whirled.data.ScenedBodyObject; + +/** + * The client object used by client management on the Whirled server. + */ +public class WhirledClient extends CrowdClient +{ + // documentation inherited from interface + protected void clearLocation (BodyObject bobj) + { + WhirledServer.screg.sceneprov.leaveOccupiedScene( + (ScenedBodyObject)bobj); + } +} diff --git a/src/java/com/threerings/whirled/server/WhirledServer.java b/src/java/com/threerings/whirled/server/WhirledServer.java new file mode 100644 index 00000000..6f4fe38d --- /dev/null +++ b/src/java/com/threerings/whirled/server/WhirledServer.java @@ -0,0 +1,91 @@ +// +// $Id: WhirledServer.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server; + +import com.threerings.crowd.server.CrowdServer; + +import com.threerings.whirled.Log; +import com.threerings.whirled.server.persist.SceneRepository; +import com.threerings.whirled.util.SceneFactory; + +/** + * The whirled server extends the {@link CrowdServer} and provides access + * to managers and the like that are needed by the Whirled serviecs. + */ +public abstract class WhirledServer extends CrowdServer +{ + /** The scene registry. */ + public static SceneRegistry screg; + + /** + * Initializes all of the server services and prepares for operation. + */ + public void init () + throws Exception + { + // do the base server initialization + super.init(); + + // configure the client to use our whirled client + clmgr.setClientClass(WhirledClient.class); + + // create the scene repository + _screp = createSceneRepository(); + + // create our scene registry + screg = new SceneRegistry(invmgr, _screp, createSceneFactory(), + createConfigFactory()); + + Log.info("Whirled server initialized."); + } + + /** + * Creates the scene repository that will be used by this server. + * + * @exception Exception thrown if any error occurs while instantiating + * or initializing the scene repository. + */ + protected abstract SceneRepository createSceneRepository () + throws Exception; + + /** + * Creates the scene factory that will be used by our scene registry. + * + * @exception Exception thrown if any error occurs while instantiating + * or initializing the scene repository. + */ + protected abstract SceneFactory createSceneFactory () + throws Exception; + + /** + * Creates the place config factory that will be used our scene + * registry. + * + * @exception Exception thrown if any error occurs while instantiating + * or initializing the scene repository. + */ + protected abstract SceneRegistry.ConfigFactory createConfigFactory () + throws Exception; + + /** The scene repository in use by this server. */ + protected SceneRepository _screp; +} diff --git a/src/java/com/threerings/whirled/server/persist/DummySceneRepository.java b/src/java/com/threerings/whirled/server/persist/DummySceneRepository.java new file mode 100644 index 00000000..8fd8fe88 --- /dev/null +++ b/src/java/com/threerings/whirled/server/persist/DummySceneRepository.java @@ -0,0 +1,60 @@ +// +// $Id: DummySceneRepository.java 3438 2005-03-29 23:18:59Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server.persist; + +import com.samskivert.io.PersistenceException; + +import com.threerings.whirled.Log; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.util.NoSuchSceneException; +import com.threerings.whirled.util.UpdateList; + +/** + * The dummy scene repository just pretends to load and store scenes, but + * in fact it just creates new blank scenes when requested to load a scene + * and does nothing when requested to save one. + */ +public class DummySceneRepository implements SceneRepository +{ + // documentation inherited + public SceneModel loadSceneModel (int sceneId) + throws PersistenceException, NoSuchSceneException + { + Log.info("Creating dummy scene [id=" + sceneId + "]."); + return SceneModel.blankSceneModel(); + } + + // documentation inherited from interface + public UpdateList loadUpdates (int sceneId) + throws PersistenceException, NoSuchSceneException + { + return new UpdateList(); + } + + // documentation inherited from interface + public void applyAndRecordUpdate (SceneModel model, SceneUpdate update) + throws PersistenceException + { + // nothing doing + } +} diff --git a/src/java/com/threerings/whirled/server/persist/SceneRepository.java b/src/java/com/threerings/whirled/server/persist/SceneRepository.java new file mode 100644 index 00000000..bdc70763 --- /dev/null +++ b/src/java/com/threerings/whirled/server/persist/SceneRepository.java @@ -0,0 +1,73 @@ +// +// $Id: SceneRepository.java 3438 2005-03-29 23:18:59Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.server.persist; + +import com.samskivert.io.PersistenceException; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.util.NoSuchSceneException; +import com.threerings.whirled.util.UpdateList; + +/** + * The scene repository provides the basic interface for loading and + * updating scene data. It is used by the scene registry and though more + * scene related persistence services may be needed in a full-fledged + * application, the scene repository only encapsulates those needed by the + * scene registry and other services provided by the Whirled framework. + */ +public interface SceneRepository +{ + /** + * Fetches the model for the scene with the specified scene id. + * + * @exception PersistenceException thrown if an error occurs + * attempting to load the scene data. + * @exception NoSuchSceneException thrown if no scene exists with the + * specified scene id. + */ + public SceneModel loadSceneModel (int sceneId) + throws PersistenceException, NoSuchSceneException; + + /** + * Fetches the set of updates associated with the specified scene. + * + * @exception PersistenceException thrown if an error occurs + * attempting to load the scene updates. + * @exception NoSuchSceneException thrown if no scene exists with the + * specified scene id. + */ + public UpdateList loadUpdates (int sceneId) + throws PersistenceException, NoSuchSceneException; + + /** + * Applise the supplied scene update to persistent representation of + * its associated scene, then stores the update persistently for + * future invocations of the server to load. Note: the scene + * update will have already been applied to the supplied scene model. + * + * @exception PersistenceException thrown if an error occurs + * attempting to apply the scene update. + */ + public void applyAndRecordUpdate (SceneModel model, SceneUpdate update) + throws PersistenceException; +} diff --git a/src/java/com/threerings/whirled/spot/Log.java b/src/java/com/threerings/whirled/spot/Log.java new file mode 100644 index 00000000..4887faef --- /dev/null +++ b/src/java/com/threerings/whirled/spot/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot; + +/** + * A placeholder class that contains a reference to the log object used by + * the Whirled Spot services. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("whirled.spot"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/whirled/spot/client/SpotSceneController.java b/src/java/com/threerings/whirled/spot/client/SpotSceneController.java new file mode 100644 index 00000000..f8a306c6 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/client/SpotSceneController.java @@ -0,0 +1,36 @@ +// +// $Id: SpotSceneController.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.client; + +import com.threerings.whirled.client.SceneController; + +/** + * The base spot scene controller class. It is expected that users of the + * Whirled Spot services will extend this controller class when creating + * specialized controllers for their scenes. Presently there are no basic + * scene services provided by this controller, but its existence affords + * the addition of such services should they become necessary in the + * future. + */ +public abstract class SpotSceneController extends SceneController +{ +} diff --git a/src/java/com/threerings/whirled/spot/client/SpotSceneDirector.java b/src/java/com/threerings/whirled/spot/client/SpotSceneDirector.java new file mode 100644 index 00000000..693aff8c --- /dev/null +++ b/src/java/com/threerings/whirled/spot/client/SpotSceneDirector.java @@ -0,0 +1,492 @@ +// +// $Id: SpotSceneDirector.java 4142 2006-05-23 23:16:14Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.client; + +import com.samskivert.util.ResultListener; +import com.samskivert.util.StringUtil; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService.ConfirmListener; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.dobj.AttributeChangeListener; +import com.threerings.presents.dobj.AttributeChangedEvent; +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.DObjectManager; +import com.threerings.presents.dobj.ObjectAccessException; +import com.threerings.presents.dobj.Subscriber; + +import com.threerings.crowd.chat.client.ChatDirector; +import com.threerings.crowd.chat.data.ChatCodes; +import com.threerings.crowd.client.LocationAdapter; +import com.threerings.crowd.client.LocationDirector; +import com.threerings.crowd.data.PlaceObject; + +import com.threerings.whirled.client.SceneDirector; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.ScenedBodyObject; +import com.threerings.whirled.util.WhirledContext; + +import com.threerings.whirled.spot.Log; +import com.threerings.whirled.spot.data.ClusteredBodyObject; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SpotCodes; +import com.threerings.whirled.spot.data.SpotScene; + +/** + * Extends the standard scene director with facilities to move between + * locations within a scene. + */ +public class SpotSceneDirector extends BasicDirector + implements SpotCodes, Subscriber, AttributeChangeListener +{ + /** + * Creates a new spot scene director with the specified context and + * which will cooperate with the supplied scene director. + * + * @param ctx the active client context. + * @param locdir the location director with which we will be + * cooperating. + * @param scdir the scene director with which we will be cooperating. + */ + public SpotSceneDirector (WhirledContext ctx, LocationDirector locdir, + SceneDirector scdir) + { + super(ctx); + + _ctx = ctx; + _scdir = scdir; + + // wire ourselves up to hear about leave place notifications + locdir.addLocationObserver(new LocationAdapter() { + public void locationDidChange (PlaceObject place) { + // we need to clear some things out when we leave a place + handleDeparture(); + } + }); + } + + /** + * Configures this spot scene director with a chat director, with + * which it will coordinate to implement cluster chatting. + */ + public void setChatDirector (ChatDirector chatdir) + { + _chatdir = chatdir; + } + + /** + * Returns our current location unless we have a location change + * pending, in which case our pending location is returned. + */ + public Location getIntendedLocation () + { + return (_pendingLoc != null) ? _pendingLoc : _location; + } + + /** + * Requests that this client move to the location specified by the + * supplied portal id. A request will be made and when the response is + * received, the location observers will be notified of success or + * failure. + * + * @return true if the request was issued, false if it was rejected by + * a location observer or because we have another request outstanding. + */ + public boolean traversePortal (int portalId) + { + return traversePortal(portalId, null); + } + + /** + * Requests that this client move to the location specified by the + * supplied portal id. A request will be made and when the response is + * received, the location observers will be notified of success or + * failure. + */ + public boolean traversePortal (int portalId, ResultListener rl) + { + // look up the destination scene and location + SpotScene scene = (SpotScene)_scdir.getScene(); + if (scene == null) { + Log.warning("Requested to traverse portal when we have " + + "no scene [portalId=" + portalId + "]."); + return false; + } + + // sanity check the server's notion of what scene we're in with + // our notion of it + int sceneId = _scdir.getScene().getId(); + ScenedBodyObject sbobj = (ScenedBodyObject) + _ctx.getClient().getClientObject(); + if (sceneId != sbobj.getSceneId()) { + Log.warning("Client and server differ in opinion of what scene " + + "we're in [sSceneId=" + sbobj.getSceneId() + + ", cSceneId=" + sceneId + "]."); + return false; + } + + // find the portal they're talking about + Portal dest = scene.getPortal(portalId); + if (dest == null) { + Log.warning("Requested to traverse non-existent portal " + + "[portalId=" + portalId + ", portals=" + + StringUtil.toString(scene.getPortals()) + "]."); + return false; + } + + // prepare to move to this scene (sets up pending data) + if (!_scdir.prepareMoveTo(dest.targetSceneId, rl)) { + Log.info("Portal traversal vetoed by scene director " + + "[portalId=" + portalId + "]."); + return false; + } + + // check the version of our cached copy of the scene to which + // we're requesting to move; if we were unable to load it, assume + // a cached version of zero + int sceneVer = 0; + SceneModel pendingModel = _scdir.getPendingModel(); + if (pendingModel != null) { + sceneVer = pendingModel.version; + } + + // issue a traversePortal request + Log.info("Issuing traversePortal(" + + sceneId + ", " + dest + ", " + sceneVer + ")."); + _sservice.traversePortal( + _ctx.getClient(), sceneId, portalId, sceneVer, _scdir); + return true; + } + + /** + * Issues a request to change our location within the scene to the + * specified location. + * + * @param loc the new location to which to move. + * @param listener will be notified of success or failure. Most client + * entities find out about location changes via changes to the + * occupant info data, but the initiator of a location change request + * can be notified of its success or failure, primarily so that it can + * act in anticipation of a successful location change (like by + * starting a sprite moving toward the new location), but backtrack if + * it finds out that the location change failed. + */ + public void changeLocation (Location loc, final ResultListener listener) + { + // refuse if there's a pending location change or if we're already + // at the specified location + if (loc.equivalent(_location)) { + Log.info("Not going to " + loc + "; we're at " + _location + + " and we're headed to " + _pendingLoc + "."); + if (listener != null) { + // This isn't really a failure, it's just a no-op. + listener.requestCompleted(_location); + } + return; + } + + if (_pendingLoc != null) { + Log.info("Not going to " + loc + "; we're at " + _location + + " and we're headed to " + _pendingLoc + "."); + if (listener != null) { + // Already moving, best thing to do is ignore it. + listener.requestCompleted(_pendingLoc); + } + return; + } + + SpotScene scene = (SpotScene)_scdir.getScene(); + if (scene == null) { + Log.warning("Requested to change locations, but we're not " + + "currently in any scene [loc=" + loc + "]."); + if (listener != null) { + listener.requestFailed(new Exception("m.cant_get_there")); + } + return; + } + + int sceneId = _scdir.getScene().getId(); + Log.info("Sending changeLocation request [scid=" + sceneId + + ", loc=" + loc + "]."); + + _pendingLoc = (Location)loc.clone(); + ConfirmListener clist = new ConfirmListener() { + public void requestProcessed () { + _location = _pendingLoc; + _pendingLoc = null; + if (listener != null) { + listener.requestCompleted(_location); + } + } + + public void requestFailed (String reason) { + _pendingLoc = null; + if (listener != null) { + listener.requestFailed(new Exception(reason)); + } + } + }; + _sservice.changeLocation(_ctx.getClient(), sceneId, loc, clist); + } + + /** + * Issues a request to join the cluster associated with the specified + * user (starting one if necessary). + * + * @param froid the bodyOid of another user; the calling user will + * be made to join the target user's cluster. + * @param listener will be notified of success or failure. + */ + public void joinCluster (int froid, final ResultListener listener) + { + SpotScene scene = (SpotScene)_scdir.getScene(); + if (scene == null) { + Log.warning("Requested to join cluster, but we're not " + + "currently in any scene [froid=" + froid + "]."); + if (listener != null) { + listener.requestFailed(new Exception("m.cant_get_there")); + } + return; + } + + Log.info("Joining cluster [friend=" + froid + "]."); + + _sservice.joinCluster(_ctx.getClient(), froid, new ConfirmListener() { + public void requestProcessed () { + if (listener != null) { + listener.requestCompleted(null); + } + } + + public void requestFailed (String reason) { + if (listener != null) { + listener.requestFailed(new Exception(reason)); + } + } + }); + } + + /** + * Sends a chat message to the other users in the cluster to which the + * location that we currently occupy belongs. + * + * @return true if a cluster speak message was delivered, false if we + * are not in a valid cluster and refused to deliver the request. + */ + public boolean requestClusterSpeak (String message) + { + return requestClusterSpeak(message, ChatCodes.DEFAULT_MODE); + } + + /** + * Sends a chat message to the other users in the cluster to which the + * location that we currently occupy belongs. + * + * @return true if a cluster speak message was delivered, false if we + * are not in a valid cluster and refused to deliver the request. + */ + public boolean requestClusterSpeak (String message, byte mode) + { + // make sure we're currently in a scene + SpotScene scene = (SpotScene)_scdir.getScene(); + if (scene == null) { + Log.warning("Requested to speak to cluster, but we're not " + + "currently in any scene [message=" + message + "]."); + return false; + } + + // make sure we're part of a cluster + if (_self.getClusterOid() <= 0) { + Log.info("Ignoring cluster speak as we're not in a cluster " + + "[cloid=" + _self.getClusterOid() + "]."); + return false; + } + + message = _chatdir.filter(message, null, true); + if (message != null) { + _sservice.clusterSpeak(_ctx.getClient(), message, mode); + } + return true; + } + + // documentation inherited from interface + public void objectAvailable (DObject object) + { + clearCluster(false); + int oid = object.getOid(); + if (oid != _self.getClusterOid()) { + // we got it too late, just unsubscribe + DObjectManager omgr = _ctx.getDObjectManager(); + omgr.unsubscribeFromObject(oid, this); + } else { + // it's our new cluster! + _clobj = object; + if (_chatdir != null) { + _chatdir.addAuxiliarySource(object, CLUSTER_CHAT_TYPE); + } + } + } + + // documentation inherited from interface + public void requestFailed (int oid, ObjectAccessException cause) + { + Log.warning("Unable to subscribe to cluster chat object " + + "[oid=" + oid + ", cause=" + cause + "]."); + } + + // documentation inherited from interface + public void attributeChanged (AttributeChangedEvent event) + { + if (event.getName().equals(_self.getClusterField()) && + !event.getValue().equals(event.getOldValue())) { + maybeUpdateCluster(); + } + } + + // documentation inherited + public void clientDidLogon (Client client) + { + super.clientDidLogon(client); + + ClientObject clientObj = client.getClientObject(); + if (clientObj instanceof ClusteredBodyObject) { + // listen to the client object + clientObj.addListener(this); + _self = (ClusteredBodyObject) clientObj; + + // we may need to subscribe to a cluster due to session resumption + maybeUpdateCluster(); + } + } + + // documentation inherited + public void clientObjectDidChange (Client client) + { + super.clientObjectDidChange(client); + + // listen to the client object + ClientObject clientObj = client.getClientObject(); + clientObj.addListener(this); + _self = (ClusteredBodyObject) clientObj; + } + + // documentation inherited + public void clientDidLogoff (Client client) + { + super.clientDidLogoff(client); + + // clear out our business + _location = null; + _pendingLoc = null; + _sservice = null; + clearCluster(true); + + // stop listening to the client object + client.getClientObject().removeListener(this); + _self = null; + } + + // documentation inherited + protected void fetchServices (Client client) + { + _sservice = (SpotService)client.requireService(SpotService.class); + } + + /** + * Clean up after a few things when we depart from a scene. + */ + protected void handleDeparture () + { + // clear out our last known location id + _location = null; + } + + /** + * Checks to see if our cluster has changed and does the necessary + * subscription machinations if necessary. + */ + protected void maybeUpdateCluster () + { + int cloid = _self.getClusterOid(); + if ((_clobj == null && cloid <= 0) || + (_clobj != null && cloid == _clobj.getOid())) { + // our cluster didn't change, we can stop now + return; + } + + // clear out any old cluster object + clearCluster(false); + + // if there's a new cluster object, subscribe to it + if (_chatdir != null && cloid > 0) { + DObjectManager omgr = _ctx.getDObjectManager(); + // we'll wire up to the chat director when this completes + omgr.subscribeToObject(cloid, this); + } + } + + /** + * Convenience routine to unwire chat for and unsubscribe from our + * current cluster, if any. + * + * @param force clear the cluster even if we're still apparently in it. + */ + protected void clearCluster (boolean force) + { + if (_clobj != null && + (force || (_clobj.getOid() != _self.getClusterOid()))) { + if (_chatdir != null) { + _chatdir.removeAuxiliarySource(_clobj); + } + DObjectManager omgr = _ctx.getDObjectManager(); + omgr.unsubscribeFromObject(_clobj.getOid(), this); + _clobj = null; + } + } + + /** The active client context. */ + protected WhirledContext _ctx; + + /** Access to spot scene services. */ + protected SpotService _sservice; + + /** The scene director with which we are cooperating. */ + protected SceneDirector _scdir; + + /** A casted reference to our clustered body object. */ + protected ClusteredBodyObject _self; + + /** A reference to the chat director with which we coordinate. */ + protected ChatDirector _chatdir; + + /** The location we currently occupy. */ + protected Location _location; + + /** The location to which we have an outstanding change location + * request. */ + protected Location _pendingLoc; + + /** The cluster chat object for the cluster we currently occupy. */ + protected DObject _clobj; +} diff --git a/src/java/com/threerings/whirled/spot/client/SpotService.java b/src/java/com/threerings/whirled/spot/client/SpotService.java new file mode 100644 index 00000000..3552b9d5 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/client/SpotService.java @@ -0,0 +1,83 @@ +// +// $Id: SpotService.java 3363 2005-02-22 18:54:48Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.whirled.client.SceneService.SceneMoveListener; +import com.threerings.whirled.spot.data.Location; + +/** + * Defines the mechanism by which the client can request to move around + * within a scene and between scenes (taking exit and entry locations into + * account). These services should not be used directly, but instead + * should be accessed via the {@link SpotSceneDirector}. + */ +public interface SpotService extends InvocationService +{ + /** + * Requests to traverse the specified portal. + * + * @param sceneId the player's current scene which is used to sanity + * check things when the request actually arrives. + * @param portalId the portal to be traversed. + * @param destSceneVer the version of the destination scene data that + * the client has in its local repository. + */ + public void traversePortal ( + Client client, int sceneId, int portalId, int destSceneVer, + SceneMoveListener listener); + + /** + * Requests that this client's body be made to move to the specified + * location. The user will be removed from any cluster from which they + * are an occupant. + * + * @param sceneId the id of the scene in which to change location. + * @param loc the location to which to move. + */ + public void changeLocation (Client client, int sceneId, Location loc, + ConfirmListener listener); + + /** + * Requests that this client start or join the specified cluster. They + * will be relocated appropriately by the scene manager. + * + * @param friendOid the bodyOid of another user or the oid of an + * existing cluster; the calling user will be made to join the cluster + * or target user's cluster, or create a cluster with the target user + * if they are not already in one. + */ + public void joinCluster (Client client, int friendOid, + ConfirmListener listener); + + /** + * Requests that the supplied message be delivered to listeners in the + * cluster to which the specified location belongs. + * + * @param message the text of the message to be spoken. + * @param mode an associated mode constant that can be used to + * identify different kinds of "speech" (emote, thought bubble, etc.). + */ + public void clusterSpeak (Client client, String message, byte mode); +} diff --git a/src/java/com/threerings/whirled/spot/data/Cluster.java b/src/java/com/threerings/whirled/spot/data/Cluster.java new file mode 100644 index 00000000..ee6967a0 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/Cluster.java @@ -0,0 +1,77 @@ +// +// $Id: Cluster.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import java.awt.Rectangle; + +import com.samskivert.util.StringUtil; + +import com.threerings.io.Streamable; + +import com.threerings.presents.dobj.DSet; + +/** + * Contains information on clusters. + */ +public class Cluster extends Rectangle + implements DSet.Entry, Streamable +{ + /** A unique identifier for this cluster (also the distributed object + * id of the cluster chat object). */ + public int clusterOid; + + // documentation inherited + public Comparable getKey () + { + if (_key == null) { + _key = Integer.valueOf(clusterOid); + } + return _key; + } + + // documentation inherited + public boolean equals (Object other) + { + if (other instanceof Cluster) { + return ((Cluster)other).clusterOid == clusterOid; + } else { + return false; + } + } + + // documentation inherited + public int hashCode () + { + return clusterOid; + } + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + return StringUtil.fieldsToString(this); + } + + /** Used for {@link #getKey}. */ + protected transient Integer _key; +} diff --git a/src/java/com/threerings/whirled/spot/data/ClusterObject.java b/src/java/com/threerings/whirled/spot/data/ClusterObject.java new file mode 100644 index 00000000..3c3b630a --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/ClusterObject.java @@ -0,0 +1,74 @@ +// +// $Id: ClusterObject.java 3288 2004-12-28 03:51:29Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.OidList; + +import com.threerings.crowd.chat.data.SpeakObject; + +/** + * Used to dispatch chat in clusters. + */ +public class ClusterObject extends DObject + implements SpeakObject +{ + // AUTO-GENERATED: FIELDS START + /** The field name of the occupants field. */ + public static final String OCCUPANTS = "occupants"; + // AUTO-GENERATED: FIELDS END + + /** + * Tracks the oid of the body objects that occupy this cluster. + */ + public OidList occupants = new OidList(); + + // documentation inherited + public void applyToListeners (ListenerOp op) + { + for (int ii = 0, ll = occupants.size(); ii < ll; ii++) { + op.apply(occupants.get(ii)); + } + } + + // AUTO-GENERATED: METHODS START + /** + * Requests that oid be added to the occupants + * oid list. The list will not change until the event is actually + * propagated through the system. + */ + public void addToOccupants (int oid) + { + requestOidAdd(OCCUPANTS, oid); + } + + /** + * Requests that oid be removed from the + * occupants oid list. The list will not change until the + * event is actually propagated through the system. + */ + public void removeFromOccupants (int oid) + { + requestOidRemove(OCCUPANTS, oid); + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/whirled/spot/data/ClusteredBodyObject.java b/src/java/com/threerings/whirled/spot/data/ClusteredBodyObject.java new file mode 100644 index 00000000..e5b63ada --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/ClusteredBodyObject.java @@ -0,0 +1,47 @@ +// +// $Id: ClusteredBodyObject.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.whirled.data.ScenedBodyObject; + +/** + * Defines some required methods for a {@link ScenedBodyObject} that is to + * participate in the Whirled Spot system. + */ +public interface ClusteredBodyObject extends ScenedBodyObject +{ + /** + * Returns the field name of the cluster oid distributed object field. + */ + public String getClusterField (); + + /** + * Returns the oid of the cluster to which this user currently + * belongs. + */ + public int getClusterOid (); + + /** + * Sets the oid of the cluster to which this user currently belongs. + */ + public void setClusterOid (int clusterOid); +} diff --git a/src/java/com/threerings/whirled/spot/data/Location.java b/src/java/com/threerings/whirled/spot/data/Location.java new file mode 100644 index 00000000..6804bd1f --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/Location.java @@ -0,0 +1,34 @@ +package com.threerings.whirled.spot.data; + +import com.threerings.io.Streamable; + +public interface Location extends Streamable, Cloneable +{ + /** + * Get a new Location instance that is equals() to this one but that + * has an orientation facing the opposite direction. + */ + public Location getOpposite (); + + /** + * Two locations are equivalent if they specify the same location + * and orientation. + */ + public boolean equivalent (Location other); + + /** + * Two locations are equals if they specify the same coordinates, but + * the orientation may be different. + */ + public boolean equals (Object other); + + /** + * The hashcode of a Location should be based only on its coordinates. + */ + public int hashCode (); + + /** + * Locations are cloneable. + */ + public Object clone (); +} diff --git a/src/java/com/threerings/whirled/spot/data/Portal.java b/src/java/com/threerings/whirled/spot/data/Portal.java new file mode 100644 index 00000000..7af78126 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/Portal.java @@ -0,0 +1,112 @@ +// +// $Id: Portal.java 4140 2006-05-23 21:44:05Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.io.SimpleStreamableObject; + +/** + * Represents an exit to another scene. A body sprite would walk over to a + * portal's coordinates and then either proceed off of the edge of the + * display, or open a door and walk through it, or fizzle away in a Star + * Trekkian transporter style or whatever is appropriate for the game in + * question. It contains information on the scene to which the body exits + * when using this portal and the location at which the body sprite should + * appear in that target scene. + */ +public class Portal extends SimpleStreamableObject + implements Cloneable +{ + /** This portal's unique identifier. */ + public short portalId; + + /** The location of the portal. + * This field is present on client and server, it is streamed specially. */ + public Location loc; + + /** The scene identifier of the scene to which a body will exit when + * they "use" this portal. */ + public int targetSceneId; + + /** The portal identifier of the portal at which a body will enter + * the target scene when they "use" this portal. */ + public short targetPortalId; + + /** + * Returns a location instance configured with the location and + * orientation of this portal. + */ + public Location getLocation () + { + return (Location) loc.clone(); + } + + /** + * Returns a location instance configured with the location and + * opposite orientation of this portal. This is useful for when a body + * is entering a scene at a portal and we want them to face the + * opposite direction (as they are entering via the portal rather than + * leaving, which is the natural "orientation" of a portal). + */ + public Location getOppLocation () + { + return loc.getOpposite(); + } + + /** + * Returns true if the portal has a potentially valid target scene and + * portal id (they are not guaranteed to exist, but they are at least + * potentially valid values rather than -1 or 0). + */ + public boolean isValid () + { + return (targetSceneId > 0) && (targetPortalId > 0); + } + + /** + * Creates a clone of this instance. + */ + public Object clone () + { + try { + return (Portal)super.clone(); + } catch (CloneNotSupportedException cnse) { + throw new RuntimeException("Portal.clone() failed " + cnse); + } + } + + /** + * Portal equality is determined by portal id. + */ + public boolean equals (Object other) + { + return (other instanceof Portal) && + ((Portal) other).portalId == portalId; + } + + /** + * Computes a reasonable hashcode for portal instances. + */ + public int hashCode () + { + return portalId; + } +} diff --git a/src/java/com/threerings/whirled/spot/data/SceneLocation.java b/src/java/com/threerings/whirled/spot/data/SceneLocation.java new file mode 100644 index 00000000..1616a738 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SceneLocation.java @@ -0,0 +1,90 @@ +// +// $Id: SceneLocation.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.io.SimpleStreamableObject; + +import com.threerings.presents.dobj.DSet; + +/** + * Extends {@link Location} with the data and functionality needed to + * represent a particular user's location in a scene. + */ +public class SceneLocation extends SimpleStreamableObject + implements DSet.Entry +{ + /** The oid of the body that occupies this location. */ + public int bodyOid; + + /** The actual location, which is interpreted by the display system. */ + public Location loc; + + /** + * Creates a scene location with the specified information. + */ + public SceneLocation (Location loc, int bodyOid) + { + this.loc = loc; + this.bodyOid = bodyOid; + } + + /** + * Creates a blank instance suitable for unserialization. + */ + public SceneLocation () + { + } + + // documentation inherited + public Comparable getKey () + { + if (_key == null) { + _key = Integer.valueOf(bodyOid); + } + return _key; + } + + // documentation inherited + public boolean equals (Object other) + { + // TEMP + if (other instanceof Location) { + com.threerings.whirled.spot.Log.warning( + "Illegal compare of SceneLocation and Location!!!"); + Thread.dumpStack(); // this will help us find logic errors, + // as a SceneLocation and a Location shouldn't be compared + } + // END: temp + + return (other instanceof SceneLocation) && + this.loc.equals(((SceneLocation) other).loc); + } + + // documentation inherited + public int hashCode () + { + return loc.hashCode(); + } + + /** Used for {@link #getKey}. */ + protected transient Integer _key; +} diff --git a/src/java/com/threerings/whirled/spot/data/SpotCodes.java b/src/java/com/threerings/whirled/spot/data/SpotCodes.java new file mode 100644 index 00000000..28881dec --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SpotCodes.java @@ -0,0 +1,57 @@ +// +// $Id: SpotCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.crowd.chat.data.ChatCodes; + +import com.threerings.whirled.data.SceneCodes; + +/** + * Contains codes used by the Spot invocation services. + */ +public interface SpotCodes extends ChatCodes, SceneCodes +{ + /** An error code indicating that the portal specified in a + * traversePortal request does not exist. */ + public static final String NO_SUCH_PORTAL = "m.no_such_portal"; + + /** An error code indicating that a location is occupied. Usually + * generated by a failed changeLoc request. */ + public static final String LOCATION_OCCUPIED = "m.location_occupied"; + + /** An error code indicating that a location is not valid. Usually + * generated by a failed changeLoc request. */ + public static final String INVALID_LOCATION = "m.invalid_location"; + + /** An error code indicating that a cluster is not valid. Usually + * generated by a failed joinCluster request. */ + public static final String NO_SUCH_CLUSTER = "m.no_such_cluster"; + + /** An error code indicating that a cluster is full. Usually generated + * by a failed joinCluster request. */ + public static final String CLUSTER_FULL = "m.cluster_full"; + + /** The chat type code with which we register our cluster auxiliary + * chat objects. Chat display implementations should interpret chat + * messages with this type accordingly. */ + public static final String CLUSTER_CHAT_TYPE = "clusterChat"; +} diff --git a/src/java/com/threerings/whirled/spot/data/SpotMarshaller.java b/src/java/com/threerings/whirled/spot/data/SpotMarshaller.java new file mode 100644 index 00000000..7fbf1a92 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SpotMarshaller.java @@ -0,0 +1,93 @@ +// +// $Id: SpotMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import com.threerings.whirled.client.SceneService; +import com.threerings.whirled.data.SceneMarshaller; +import com.threerings.whirled.spot.client.SpotService; +import com.threerings.whirled.spot.data.Location; + +/** + * Provides the implementation of the {@link SpotService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class SpotMarshaller extends InvocationMarshaller + implements SpotService +{ + /** The method id used to dispatch {@link #changeLocation} requests. */ + public static final int CHANGE_LOCATION = 1; + + // documentation inherited from interface + public void changeLocation (Client arg1, int arg2, Location arg3, InvocationService.ConfirmListener arg4) + { + InvocationMarshaller.ConfirmMarshaller listener4 = new InvocationMarshaller.ConfirmMarshaller(); + listener4.listener = arg4; + sendRequest(arg1, CHANGE_LOCATION, new Object[] { + Integer.valueOf(arg2), arg3, listener4 + }); + } + + /** The method id used to dispatch {@link #clusterSpeak} requests. */ + public static final int CLUSTER_SPEAK = 2; + + // documentation inherited from interface + public void clusterSpeak (Client arg1, String arg2, byte arg3) + { + sendRequest(arg1, CLUSTER_SPEAK, new Object[] { + arg2, Byte.valueOf(arg3) + }); + } + + /** The method id used to dispatch {@link #joinCluster} requests. */ + public static final int JOIN_CLUSTER = 3; + + // documentation inherited from interface + public void joinCluster (Client arg1, int arg2, InvocationService.ConfirmListener arg3) + { + InvocationMarshaller.ConfirmMarshaller listener3 = new InvocationMarshaller.ConfirmMarshaller(); + listener3.listener = arg3; + sendRequest(arg1, JOIN_CLUSTER, new Object[] { + Integer.valueOf(arg2), listener3 + }); + } + + /** The method id used to dispatch {@link #traversePortal} requests. */ + public static final int TRAVERSE_PORTAL = 4; + + // documentation inherited from interface + public void traversePortal (Client arg1, int arg2, int arg3, int arg4, SceneService.SceneMoveListener arg5) + { + SceneMarshaller.SceneMoveMarshaller listener5 = new SceneMarshaller.SceneMoveMarshaller(); + listener5.listener = arg5; + sendRequest(arg1, TRAVERSE_PORTAL, new Object[] { + Integer.valueOf(arg2), Integer.valueOf(arg3), Integer.valueOf(arg4), listener5 + }); + } + +} diff --git a/src/java/com/threerings/whirled/spot/data/SpotScene.java b/src/java/com/threerings/whirled/spot/data/SpotScene.java new file mode 100644 index 00000000..915dd960 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SpotScene.java @@ -0,0 +1,80 @@ +// +// $Id: SpotScene.java 3451 2005-03-31 19:40:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import java.util.Iterator; + +/** + * Makes available the spot scene information that the server needs to do + * its business. + */ +public interface SpotScene +{ + /** + * Returns a {@link Portal} object for the portal with the specified + * id or null if no portal exists with that id. + */ + public Portal getPortal (int portalId); + + /** + * Returns the number of portals in this scene. + */ + public int getPortalCount (); + + /** + * Returns an iterator over the portals in this scene. + */ + public Iterator getPortals (); + + /** + * Returns the portal id that should be assigned to the next portal + * added to this scene. + */ + public short getNextPortalId (); + + /** + * Returns the portal that represents the default entrance to this + * scene. If a body enters the scene at logon time rather than + * entering from some other scene, this is the portal at which they + * would appear. + */ + public Portal getDefaultEntrance (); + + /** + * Adds a portal to this scene, immediately making the requisite + * modifications to the underlying scene model. The portal id should + * have already been assigned using the value obtained from {@link + * #getNextPortalId}. + */ + public void addPortal (Portal portal); + + /** + * Removes the specified portal from the scene. + */ + public void removePortal (Portal portal); + + /** + * Sets the default entrance in this scene, immediately making the + * requisite modifications to the underlying scene model. + */ + public void setDefaultEntrance (Portal portal); +} diff --git a/src/java/com/threerings/whirled/spot/data/SpotSceneImpl.java b/src/java/com/threerings/whirled/spot/data/SpotSceneImpl.java new file mode 100644 index 00000000..a7d87609 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SpotSceneImpl.java @@ -0,0 +1,157 @@ +// +// $Id: SpotSceneImpl.java 3451 2005-03-31 19:40:55Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import java.util.Iterator; + +import com.samskivert.util.HashIntMap; + +import com.threerings.whirled.spot.Log; + +/** + * An implementation of the {@link SpotScene} interface. + */ +public class SpotSceneImpl + implements SpotScene +{ + /** + * Creates an instance that will obtain data from the supplied spot + * scene model. + */ + public SpotSceneImpl (SpotSceneModel smodel) + { + _smodel = smodel; + readPortals(); + } + + protected void readPortals () + { + _portals.clear(); + for (int ii = 0, ll = _smodel.portals.length; ii < ll; ii++) { + Portal port = _smodel.portals[ii]; + _portals.put(port.portalId, port); + } + } + + /** + * Instantiates a blank scene implementation. + */ + public SpotSceneImpl () + { + _smodel = new SpotSceneModel(); + } + + // documentation inherited from interface + public Portal getPortal (int portalId) + { + return (Portal)_portals.get(portalId); + } + + // documentation inherited from interface + public int getPortalCount () + { + return _portals.size(); + } + + // documentation inherited from interface + public Iterator getPortals () + { + return _portals.values().iterator(); + } + + // documentation inherited from interface + public short getNextPortalId () + { + // compute a new portal id for our friend the portal + for (short ii = 1; ii < MAX_PORTAL_ID; ii++) { + if (!_portals.containsKey(ii)) { + return ii; + } + } + return (short)-1; + } + + // documentation inherited from interface + public Portal getDefaultEntrance () + { + return getPortal(_smodel.defaultEntranceId); + } + + // documentation inherited from interface + public void addPortal (Portal portal) + { + if (portal.portalId <= 0) { + Log.warning("Refusing to add zero-id portal " + + "[scene=" + this + ", portal=" + portal + "]."); + return; + } + + // add it to our model + _smodel.addPortal(portal); + + // and slap it into our table + _portals.put(portal.portalId, portal); + } + + // documentation inherited from interface + public void removePortal (Portal portal) + { + // remove the portal from our mapping + _portals.remove(portal.portalId); + + // remove it from the model + _smodel.removePortal(portal); + } + + /** + * Used when we're being parsed from an XML scene model. + */ + public void setDefaultEntranceId (int defaultEntranceId) + { + _smodel.defaultEntranceId = defaultEntranceId; + } + + // documentation inherited from interface + public void setDefaultEntrance (Portal portal) + { + _smodel.defaultEntranceId = (portal == null) ? -1 : portal.portalId; + } + + /** + * This should be called if a scene update was received that caused + * our underlying scene model to change. + */ + public void updateReceived () + { + readPortals(); + } + + /** A casted reference to our scene model. */ + protected SpotSceneModel _smodel; + + /** A mapping from portal id to portal. */ + protected HashIntMap _portals = new HashIntMap(); + + /** We don't allow more than ~32k portals in a scene. Things would + * slow down *way* before we got there. */ + protected static final int MAX_PORTAL_ID = Short.MAX_VALUE; +} diff --git a/src/java/com/threerings/whirled/spot/data/SpotSceneModel.java b/src/java/com/threerings/whirled/spot/data/SpotSceneModel.java new file mode 100644 index 00000000..5ae61d64 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SpotSceneModel.java @@ -0,0 +1,96 @@ +// +// $Id: SpotSceneModel.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.samskivert.util.ArrayUtil; +import com.samskivert.util.ListUtil; + +import com.threerings.io.SimpleStreamableObject; + +import com.threerings.whirled.data.AuxModel; +import com.threerings.whirled.data.SceneModel; + +/** + * The spot scene model extends the standard scene model with information + * on portals. Portals are referenced by an identifier, unique within the + * scene and unchanging, so that portals can stably reference the target + * portal in the scene to which they connect. + */ +public class SpotSceneModel extends SimpleStreamableObject + implements AuxModel +{ + /** An array containing all portals in this scene. */ + public Portal[] portals = new Portal[0]; + + /** The portal id of the default entrance to this scene. If a body + * enters the scene without coming from another scene, this is the + * portal at which they would appear. */ + public int defaultEntranceId = -1; + + /** + * Adds a portal to this scene model. + */ + public void addPortal (Portal portal) + { + portals = (Portal[])ArrayUtil.append(portals, portal); + } + + /** + * Removes a portal from this model. + */ + public void removePortal (Portal portal) + { + int pidx = ListUtil.indexOf(portals, portal); + if (pidx != -1) { + portals = (Portal[])ArrayUtil.splice(portals, pidx, 1); + } + } + + // documentation inherited + public Object clone () + throws CloneNotSupportedException + { + SpotSceneModel model = (SpotSceneModel)super.clone(); + // clone our portals individually + model.portals = new Portal[portals.length]; + for (int ii = 0, ll = portals.length; ii < ll; ii++) { + model.portals[ii] = (Portal)portals[ii].clone(); + } + return model; + } + + /** + * Locates and returns the {@link SpotSceneModel} among the auxiliary + * scene models associated with the supplied scene + * model. null is returned if no spot scene model could + * be found. + */ + public static SpotSceneModel getSceneModel (SceneModel model) + { + for (int ii = 0; ii < model.auxModels.length; ii++) { + if (model.auxModels[ii] instanceof SpotSceneModel) { + return (SpotSceneModel)model.auxModels[ii]; + } + } + return null; + } +} diff --git a/src/java/com/threerings/whirled/spot/data/SpotSceneObject.java b/src/java/com/threerings/whirled/spot/data/SpotSceneObject.java new file mode 100644 index 00000000..76dee0e0 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/data/SpotSceneObject.java @@ -0,0 +1,140 @@ +// +// $Id: SpotSceneObject.java 4166 2006-05-31 04:16:57Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.data; + +import com.threerings.presents.dobj.DSet; +import com.threerings.whirled.data.SceneObject; + +/** + * Extends the {@link SceneObject} with information specific to spots. + */ +public class SpotSceneObject extends SceneObject +{ + // AUTO-GENERATED: FIELDS START + /** The field name of the occupantLocs field. */ + public static final String OCCUPANT_LOCS = "occupantLocs"; + + /** The field name of the clusters field. */ + public static final String CLUSTERS = "clusters"; + // AUTO-GENERATED: FIELDS END + + /** A distributed set containing {@link SceneLocation} records for all + * occupants of this scene. */ + public DSet occupantLocs = new DSet(); + + /** Contains information on all {@link Cluster}s in this scene. */ + public DSet clusters = new DSet(); + + // AUTO-GENERATED: METHODS START + /** + * Requests that the specified entry be added to the + * occupantLocs set. The set will not change until the event is + * actually propagated through the system. + */ + public void addToOccupantLocs (DSet.Entry elem) + { + requestEntryAdd(OCCUPANT_LOCS, occupantLocs, elem); + } + + /** + * Requests that the entry matching the supplied key be removed from + * the occupantLocs set. The set will not change until the + * event is actually propagated through the system. + */ + public void removeFromOccupantLocs (Comparable key) + { + requestEntryRemove(OCCUPANT_LOCS, occupantLocs, key); + } + + /** + * Requests that the specified entry be updated in the + * occupantLocs set. The set will not change until the event is + * actually propagated through the system. + */ + public void updateOccupantLocs (DSet.Entry elem) + { + requestEntryUpdate(OCCUPANT_LOCS, occupantLocs, elem); + } + + /** + * Requests that the occupantLocs field be set to the + * specified value. Generally one only adds, updates and removes + * entries of a distributed set, but certain situations call for a + * complete replacement of the set value. The local value will be + * updated immediately and an event will be propagated through the + * system to notify all listeners that the attribute did + * change. Proxied copies of this object (on clients) will apply the + * value change when they received the attribute changed notification. + */ + public void setOccupantLocs (DSet value) + { + requestAttributeChange(OCCUPANT_LOCS, value, this.occupantLocs); + this.occupantLocs = (value == null) ? null : value.typedClone(); + } + + /** + * Requests that the specified entry be added to the + * clusters set. The set will not change until the event is + * actually propagated through the system. + */ + public void addToClusters (DSet.Entry elem) + { + requestEntryAdd(CLUSTERS, clusters, elem); + } + + /** + * Requests that the entry matching the supplied key be removed from + * the clusters set. The set will not change until the + * event is actually propagated through the system. + */ + public void removeFromClusters (Comparable key) + { + requestEntryRemove(CLUSTERS, clusters, key); + } + + /** + * Requests that the specified entry be updated in the + * clusters set. The set will not change until the event is + * actually propagated through the system. + */ + public void updateClusters (DSet.Entry elem) + { + requestEntryUpdate(CLUSTERS, clusters, elem); + } + + /** + * Requests that the clusters field be set to the + * specified value. Generally one only adds, updates and removes + * entries of a distributed set, but certain situations call for a + * complete replacement of the set value. The local value will be + * updated immediately and an event will be propagated through the + * system to notify all listeners that the attribute did + * change. Proxied copies of this object (on clients) will apply the + * value change when they received the attribute changed notification. + */ + public void setClusters (DSet value) + { + requestAttributeChange(CLUSTERS, value, this.clusters); + this.clusters = (value == null) ? null : value.typedClone(); + } + // AUTO-GENERATED: METHODS END +} diff --git a/src/java/com/threerings/whirled/spot/images/scene.dia b/src/java/com/threerings/whirled/spot/images/scene.dia new file mode 100644 index 0000000000000000000000000000000000000000..05760b012cd3bd8ee23ce20ec1a61fe414cc739c GIT binary patch literal 2000 zcmV;>2QT;^iwFP!000001MOW)Z{s!)zWY}g-b)L(wD^!ntv6X<_qsp}w0DEHxwcwa zG9-DET>9HfQqDTo!w=Ck9G3wSMaqEVq2`+hIm3MV{Je_$KSY*CY4R|Dz#Dvi?0uR? z{Gbx?+<8m?(@X6~;oX=otn?Y=wL1vplluy=aR%ARX>P)6-8VjD3e2Q!%y7S3rJeI0* zND$b4^zTtoq(?sgcOK`b2v9!$iJWs$xX7aUVG!<7w2bl;&5Pya`OtKE!0$d`<@hhw z#{UuJ(KHrK5=2R1{Jvj@e=!{1^j&YFxyX;a?R}b-k(OJs(J{fRbf-#CKB_n=%_8P6 zBHIrCW#^vy+d19>|II9<)4#>6*oyaO!IL@9=KbILU()Bn&bfMnel&j={O;{fPgNi} zm^47O`B|sC_lSW%gz&94w!O_Z2GePE{7g!~lSM3k@*sicA}?defRE*!uP)uVidd{B zvouTU?)Gw`X(k%Ub@5(NY%=sxM2lt7J~rt#_B74rB0KiMJ>b!9JvEUFwtvRTz^&^bfTdA^KAvN$|H9jCw4%1OrO(I!7M{xQ`lAC}gjvw47~ z=7A2GMZ*w)&>vFyV~h##{2`Jy!Kz8-~lCPpYAQVX$K4UmC9B(_me zm8c0g3K!Vn;%;#fTqdIOEk{&DLPJMXzJ$hAA}arQL`57f?iLsH61h0By@Xt5;~2;b z*=6HIts;{vWaH$B_t5z`aTvK%j4)G-AQfI9&nikJ528>a*{JInF@P0H5Y$)8C~1(e zfr!aZ<4|&^C<%>G5?aRyE45U{SSVD#LiKrdEhG*Y_X-T-9cm1<+FAsLL_?_kgan5Cp+RCD7*(m7NIHw{fN}f4K$i%m z@D)NS?J-K{aRrZ2ld6Tp0pnhQfiIJZ<7?#MNbdyh@^Gwo0n_kKrGFIItOUM7;opAy$t5)NFY#P7siyhn zGy(yoXb2gg9-%fj9VA>CQ;a-Dbp;@Ws;+|pChxm3F4VMC*jAewDl~1cS9gc)(N&ts zzHFj+E`MujCp*v<($)x+CPfoN^?Xb6HQi*9MB>PVPYn=_rj(g6+1IkOG_4Ce-GkV~ z@NILVr@zb?@i8jI%-cL+7Lz*u!&#@qCW*zRbNfm~QNw7jkfXg6llff)(q zeAq>O7`;FWQd$-QkX;|9S}Iss{G@!C%+pIBMgVp`?0?|HS~bo2Fq=N?0_6fjjMT!!5D|5;>czT?<@zyQKj!=C$7~B7j4LE$py}bVFAdVbW_PA^8IVyK zJIKlLL#p~17=}6w#q$34V~4NtCT~AY#f1*X(%&{sbD@Jx6M;PQoj-tB5GdzPp~oz5 zbl{LFPF{$q%7s0KiMnU<-kFGd&o0_D&6x&;3PQVmpne$W^+`%S zG*)bhSF-JD9!@WmXq2(3c@%h>j&$Cnwo)^pb8>473%5~`;onKw62k8Vkv?F)Zpj7| zVWJU<6+#sv{oqz0833s1d;s4eBm)P@8-S#7l@x*i!7{e72}#KYGSwL_gJ$aCvCO(L zm5@}yG_v2;*C=G;PLqZtLLhKX(jl@7BK-?ENq@-f)_!>9k%b5DJzfILhLRM(S{}5b3)6cV#8Sb0l9(+Dqj9 zvX{?9;_O7{{mY8hIV)k)N?`MUXtO=W#`fv8%23GsQ->2O4^@UjrhwkNG!%KdafZUQ iT(Yis&Q_deE6RKQud*I{d;d%8vG*T@-)mu3asUAI8r1jz literal 0 HcmV?d00001 diff --git a/src/java/com/threerings/whirled/spot/images/scene.png b/src/java/com/threerings/whirled/spot/images/scene.png new file mode 100644 index 0000000000000000000000000000000000000000..80b5b82b8635f41150e0e7aa88285c7340bc6c2e GIT binary patch literal 12994 zcmaKTc{tVY*Y3w$hDgRtr9`$dWF8_RBqW3k8OuCI*t@g|$q*rA%n%VuW+Guz#>h;F zEmMZf+q?Gnd){;2-#OQH_8)R-`|Rgg&wB24-}hR=uV2%kqdrNEAPAk7=4Ar}A>~34 z600Mm@ROLE<`>`}GA~uF8%K^D8JW_XM37U6*5yk#?x!w|IjArVj#I9hnvl~xs>}6H zxP6w5*+xj5u{1m1BrAjRPP3|}SQbeSM!!GfNt8{)RaFN4(tAZQh^nJaRQU10mi&B- z(BhtTTwRKAId(`QcMm_?h!4pjGTLVuhlYp8 ztGx2eKDtV~jxtbVb5Nv{0w+$KC^d0bt)@ka&E7lqq{;gUL{xjtealjh)J!C6^l4*8 z&i(oG$HT)TE*ieTx8%P`CY7FskO=tv>ST9wTdA&=!(;93?OUUHcogo76}&-YBDty5 ziV*?d^gZ7mC*DePe{T_H*Vo@`YtwDcaOwnjpSx?%SqY=%WkdR7njwrT7y0qYz2ODUrQiQAb}Cmi z*`9g)+Luuo1hx7qkhqoc=ky4&2;o7H6nHAqcy0z#=-tsONE>I>V#HBpP%mr}e?km_ ztsm2$`^2hyy6~UX!dq*3XjLp0YW>xRavCml*l~wD{D?KULtswW6k92@==djSCTF-5 zF0`LMycvjfYcrOyf9*+NrWD+2;Y@6lgJXLaB70GwK-$k^ZuG75O*Vr~jI|1y0wX2Z z)fR>KM^-6d`%(+$L6_cRJ~hj*A@Vn#ooeqi?!r!;tNTh#c7nkoZHDDU2 z&vhr;2}(A*S?(-l_N4$hNw!{sIMQ0qk+4}2h)2icNufeEUxeg8HYY)li>LqNP#ZJc zXWH4s>|v(_Yt3V$rpV`i-*8`<4RcNNHpMy9te(&Oy(L45{-aD#EUF7@h=Ahx8A z6lunoi*qidXUtB7we1UG_rK7##@$AxbJD_pa(zNucT0@YQ${##shaO3@(YdY}bqg^@eaF?+U$&jnQSQQCEM;M-^*})5kiI zUm_*m0IOs|i8gzn6mzbLyUboq6y{jM^HqHcc) zhz70&nx2?&@} zIJ5I@aSgPzVErGwv=y}*nVXvvwX8P}mQ~xcwzghyqZcw@5$wy;o2Yy6k?@IweLf2#t&mr)PD5pUs_L0;4F`V-2Yb{dhh#|JDPs)J;?mL?UfzqY zqe*FLy45U*6$E_z$kC(zdrKev7Jpo-X889<3`fYxC~4c;+fQ2@LBs&~(kp9fSXo#G z1_t6p$dP!};?>6AgC&U~7Q@}$D%A`=T(X`sf0st89f!uoEY32HRr_eOMv)Fy4rRBsc#Ro4E6U#>~ zsOv8ce>f4@LeH&WRcD7-(RgC1UI>}mI5}1Mt=vYV(Z1POA3PpE-5QNP*xgK(_45A1 z_O77d{BIXhq$UQXFp0+}CnsxZXsf7Tyx*HwdCq<>G|r(OfivBhAB{buF@Jq$f4vI= zWM*JQf|#~32d{2yC{x=&?*XTNQiP3r zhPtcvkg$ikS(G7g)iXgo+dNZXLHTf}Yik1SW^aG4&i(J=5Dkk^cYfN!!oplvdLP== z(b3)0vq_2>$%sMKK2J_QFJYX`GH6Um%f`UU%D*Gy*qx!9a?xq{iOwr#7M9>$e4au; z8ztgRUe$C|FI|c5=u!PDQ+xZ|=x9A%-KdQwGOA;a9v2Tc`t&rOvR%@@0oB^jz`(Xz zbU{;BH^DS6DT$4ZEx*r^n075TrMc5SNQPdeYZt$fkx@@i5A$KA&+nV2rbAy-q}|4< z?e5&kcDhGGLc-3@&dts3GW@g{xfa&f{B^g=*A7lAH@=E2DjQ`qMyegt{{y%Fi+nE4Q@?A8^ zkF~8Fj+~C})9oudcU}onAxPA_7}T1jzllOL4LRvsvuwZ5(!l3}Y)%`0wkho#Y7n=E z`dyow=OOk#xy4KvhIzTrSr|d))0I52RnlsHezv_>LP(&VW@upNs^l$zldIL_!5MRw zr!6QDG3%vZv4>2*#Kq-=ZGBlITjIXD{}j7uXJ;oaE}m0}AinQjplr@Ne<+BXoY?R+ zx9UljaM=C3WOnoB6(R^wMW-~|M-;&7n8PpU+#^9Qmd2psYAWK>hWLzsG2ae~9k-bBVYLgfpiW5`SFxK` z!R6|vbar+EWC@v;1?~B^EBC?WJu*OvM1B6Yj=JU6mlQN>ERH5e5XH$qo6M6X6f`XT z-@kuNSGFuWD<*c`GfhZI>6MiW(yiIXd`+xr$M~`86g;%Td5D+kZnx8yeH1bpW=<0K zt1}r=dFYK1suznO_hL{gKPEqg08_NTb4RV3VN%j=Yb+ma{H*M&y9WJl+8xE6M%IfL zFMj&;3F2d+e?L7hH%e?`FZfxx~n-xfsT^#OAzn*ZkVP|HVpcVFz(|42Hzo5S<@ z=0(oD#oq<{YNI$M-W#?8+*F3^Mks2Y5C?*$)GppHbS&BUJj!HhInZCJAwPe9ai|>D zbzOJ&qn>h})x*(v&QZ4^F~s^^sdzKa=K{$_rIN}V82YGy$__MuZmTf3vXyZhzK zlXxt|VywoG-v|Jof?{d3YJDyp?d$2uYe6$l?tpH=Fy9Zx>V+QT+MSM8zGY&fDVi$j z6c-+T=|H}B2>#oh0378dUI&LVqks@Rs=SS``p@15Cb7Xoc*;kQn*v!AcaA(qvM zo>&o!YRIrlXBmYp>qozRbDgLUViTK^!Wv7@Y8;8T2#Bp(C|C`6TlQg1xMSvFSrA_b zbMOt*^4LBlP})$FyAc-`7fR3b>gCJ3uCA^zRlRS`q?7q zeV`j(K1avv9;8lkEO@WdbC#M*&@k_s?$xVVRG&iWIDG#unl{(etF~_%J03fB3@Y!1 zHckqqpWD*<@y8NoW^CINm?N?j6ctmX?&T()+`M|u zn4I*IySw|fYu9Rgm&lWVosV=|?(lpI-ZNY*N^4!JTk+m?Y#4{TdWu2Fix?Q7!{T zL$J|X70$9=zjTiMJx8KK={f7hS$ng@P~La3t*tFXU}3f+5lVhZVc{8z*dm_bZEN`T zjr6x~-})_&`ooQfB|d*kjoj$fDrOszed#A+UMc1_uF&2J5G8E#4r8TA-M-PKMA)7- zEw>L^|0k{VV?EyhyazY(x|P z_<19*WU-RhIjf5=7YF$=A>3Rt^?wxZ57b$Ju}$?Cp)K z=h$1FY9@l?>!e>F6G#0}ULq`q>sJHubUA|tZ@63X2(m4x(X>pv&}m%15ZW9b=kae< z?P-1p;V`{%ED&9CF5!14KDSSVwe`|3_qL**=3l&e`JNKz3 zIE%#ic;Z`aoz`Ck*7f95ViV2S20Vf2&?pv0C`GYLToGc)%gtTbsVp+73)nPstD9X} z5oHY593({!oBA#FFS?(t64jp4NZ6w$1evimU*a>~{vcm$vMjYtt7hGq=-~bJ%a?Qf z{9*k)JNZt1xd3spva%Dwd&2lz$fRY`vc%1cch8nzo_ZID3re87(q3dE6N8(6b12RG zZEQ?YRJ0^B^P%@?>L)Ef!GMS>RwbdQkDi>IeDdUpCHf!%Lb0l>+DqY1I`oZNU4Ny1 zZyC56Txh~_%?hKCT4q*OR%2r$mWCDRT>Z|k6bR3~L;Ch~t*;~}jvx2fnD4>&o+D4kjD{n93* z7cXAeu52v#NM^iVt8x=V&pfZtC_P7eCh z!kSZf{ceB0zLJtsTr}y!jOyy)zCHX!{b>Q8G9k)Fc^$+$?fCk!CFLRh?|+2 zVFzdAd=}oL4+$Vr04bxNJ$nXHYq#WUf4`)x>`t^Y!KB)|fDnBQVe(Wz%LrK1lgK-d zLTwna41-YBVr^6A3EI#P4kkb@?eFhjiRM0lq`_Aq^71uCg*RQsYj>KkGyzJI$n`en z%C zr|c59Ywu@vO_L&65IDHuVL3&`I(T+3O&BfvBm5DL0=R#z&$vMF`I(!irlh1OJ)l2# z?i>IJlw9**?jw!YPs3%+a&~q$FfceP8ujbE#JjN;G^{W$f*Y7EJ0H0lS@a&)I)||uM#z*bX`R{G4jCXg(#Kd$% z6#_~zH#0LopQk+xOiWl<7z$-U*quK#uPmqJ6&grY^qKe=j@Wl5>EF7wu(&u1$DNUp z0hb8B@q{S-Awf?tGCt#y&+Q{4!H~C`L&DJE-6@E7q(uC1S>A710ZrHkbOT?2o>Qjr zCw)R~W1|Y7CV(Cge(gLeQsm?=N+mQe&*9p&Z@>VR9{gQAtri-ekbs|8A$@2Cz#w|N z)}+!s^Vu;Hq!(EB)9V7}Wwu%#9;F{Xe29$fh+sx}OXdx6xYd?gH-{=d_{*sjXu`Tg zhLN11W*GQzZ!kVS{{8#+P`Tyh<$08Y%~>^$9y!9M6u9k>9t0||Ao4GX3TYK-su&Qu zrlux{(%$y=_UJ;gIlvC4~ZEZ)2Wh;owMl&ab!pqx8(Vb6>{B4%TKngf1%C;h1=M z(%}FXmP*mc84!OWYwPPMVPP&A_sNV)A@*Niw)_lv55Bcc>HTl1vd&URkP9)WMtxzk zihU>_Wo2d5G&D<#i;RqnpPhAHk7lZcRaRE^_xHnArR$~Y*SEG<5#@x$#OH)R`qZ$& zU%!6&&f*dP)x#b?-T;3EPVZ*XVG#ePCOqfcK{jdyE0w} z7Z`L4sOKnW2@#QRot;8rVlfHSJC5K$8+39oi9(50Uds} zMEd+GqQ!`VZQ4ad{GHiEfJxn#k{-Uk&z?Rldi(Z!alwxtKOjn9UrK3re&He^nwf}b z-R6Lqlamt_J-3dIj<<(LkE=l&fV#^>{lVw(6OfX&b*e}KUS2r~X#sctQAo(jSWR9CDaq60u-3Owvh?-!VY#BBq8~qggu?K)q+~E3yZL(% zfThpt>O#IIk_~2;nw6Uy8VnGqZbbH1_h9QA4rhS2M4<{FKYk4P;WAXF?#E2ZMn6N% zcI2VTYCC^A(bSEYAD=STAMWCREhlfKw zCK8X0>!&d!YQ-39kTDkTtS$j&!8IEj9aU9Rv#bwnukC1jNH+I1<)iaZ8Hcp%e4Zu} z0_mwX0F?%Ei$~rU>fqO&9&ts*H2=N){Cvv$qXa|n*v5={rWd~&;-1(nhg}BY@`s{ zG>_>o%&e>r;N|ZRhoKu>ya%KknBLs%th$EAi2?@9U<}IY>8ILLsOZhpO9zkyO|ZWoXrNBI%H&D$G_kyV9jvs$w`L!0TBw2NK|RNRn3|dbB@3U9 zEB=gNRAHTW0k*z8Wqr!C;OT(H zl)@$No2rZ^K*{?1_b=5k&WyyGni^4yYDqysL7=A2SVRF*Ydhut2FY%{F^C@U)t$Y) zdx|JNOer|2`_casAHkRgv{C2n0mmAL!!7?8YHO2tg zkhKEcc*JJt73yB21t8bcd<_WpNla`z^9VVXGotlPc z&jf-_*$o=Ye7`W^#KngDQYNGREAogZ1@R5)!3asii;S2+84p4GoV|i=c1qU^U-q8< z8F5^l*>hhrzF<2bcrT*mx^-~`dtMnGCb@LOI0wIewh7u7A;RlVX58K`mUIP9lQrMM zCXOcE(VCj|#**EnZA}<;I;S|^vPvISqS0s?nvPnN4mH4`zq8l33o7X5qZwnB6~z|| z*O;2k%f+wH72#^5a1W~4DsG@HH>ItV1PsXUe_r)c?>iB5rJ?I&+U0&X(==g^EE1$R zvw?7OdXt&#&f_#@Wb#7uN(r0bs2ep1e*(2-_#t!#Vm2XDfBZ1%H9{Cdpig6}s@eos z0y?b+AUU2FLk|q@OUI)}kAP1BII>bvg+*~l4-5`IFFvAD0?E3#zPTwZBJ%2@%PlY= zz~liZMa!5xLm9Fv?Oj00YP_h|3gkUVzzL)z;QxrHIhlx+uA%CpLQ3$_frIwHqJ`*jp;B=~D<5d`C-9 zU-I@Xp9Pb^ImOi1Ew?GrY9z1=HKpD8Y{&u-mTvVsxj;uD=%*}>AGW10e^};x_x)MZ z8(KDaquI-gE?@*LjE;^1Kc+t^=?Df6bnSB@f3o8<1$Rc8&o*Djy>ivBIKwCXf)9;< zW&JMFTG&gg7SeB$m!CfmYLF{*x}>DOfBIw%r<;TN$sRg&E@4`~x5G}PqL8!ApxiF` z1LeibP<4qD3D3Kf_KGSi?*q3<4_pgROdJF%413Lhqs8>W&VXWOK7INW%u@%4IjEd4 zf&ghm?0~H{f{POw5%KF=T8&Lhgx^Ha7H~{J%IiSg8tI``VfoS3!q>L~$R`*iVqi$Y z)?-DeRY*?bNf8w?aQ}dfjerJ#d(_w43tmGFF#qQw5jmluq2Llije}5BS6AjO1DXYr&|TUU1pbSGFEcWi8+bEKJ=t?XU& zGAT*OvVOlaSwc!m3W8C(o$jFA18Wio?hGAD5SLJ=LC74gb%^Gmdc#0)R##RkfQkSL z30@`m3G`pTe$`@aIZ8)IXBqg10xt;JrZ5O(3V4ASFIR= zQR!uVNZ44JNYBa#W@11=T&-c@&9@b96NElJWvIJgjQs8pZ3x_4V8&;u@Qeqpe}%2h z&dkIJB2}bTZM&Pl6B84+0C!rye36rtwX?U6E7lCVAR*y4T6w9O`kK3+pC}5|cj(b# zBMuZ=>^>0kqck)fU0vw8G{4%qI_SZg>+1umx;R7JfDPaVAZ?;fO1%Nc5y;;n_eaQD zDBzZN!lBu_fpU2Q=*vbV3D6Pe=jTtC^TCae2ZOS4A=niC{>pe9}{!RzfL^J4qK1o zWjI1f&;9hgd@`Wn(|9aa9cs+&+qXehLLMxxtmrj3rmoC8vMUGgLkNKW-Cvv0ipY}u zngA^`5zxND{$OB%mj`XF9|hl4h>v+064I=veDEH+ve4Lm@L&(r07v6yVCHqeWC)l> ztmb9Wo}Vn0Ab$fMJkS;`t*B7)oY4f+V5p$io%o0}l~YG(X@x{Y2=m#|xo|qRZ86Xe zjT3pJfA{WPa1T2=I{36RV?(yyxc`{;^ zD(Aa+f3oo*G+f52yjojY>>$@bHlMTTZxFimejap-pmbqD0jOD7kDre>ngEg{9KJaQ zTn1$XTQD>N_%n#nmy*;3AIK^8xLH|E0yb$T_6hLD3P?(BfY{5S z5f9u*eyD?{Kq^b>>wyvnLj?u_I#TH|4R;gRhI!b|5(y@}pa8$WvmO~435|JRxx^03 z*0Kd)Jw04Yj284)3;k1itvHGXcr z%p`MGoV19fm6OxXnBN58meDDO`vC!z6cmbRLaMiZqdH`0cjJSVy2P}!I*`SmKVJq( z#e?40cMG$Bg{H`%ASZ`kh?A3pnKa2WOw=JEAy99?>4eKB4=#@~VLJq#bDH>^FJPr7 z#KwB8PJUWkbOs~>sSQ&Vuzr46zsiFX=X3;sX+q z6I~k%3&_irm>341@t+iwv@p~p1Va#=U%y6yKOPzB<>?7&!-kK*umQ7s*WB5;SXfdo zC?GIDUPtU{@Lx;a{+DNwz?!TK;Tq_r$$bC-0fHneBjXGPW?#4-W@WWj5t+XX&1X$b zO;69Vu&`FRs z#;wBf2puExxMcXFTLToa6a3dk8_*Nqzx#mC1NK->Bq_jtE#FFpa8)qWWg9Dv5G>kG?wUNKiuXq|6K%n9+N%q_0fd7`Uqt%(Gk|y z2N?)Ufl#GF-v_S9jC2j$#RID8RCEh8Mdao8>bA!F97Q`DNHh3C$DeMfCezI;7ml({ zTkK9PEWKt>X86DO&}?|jL5{W?}KnnuoVIdG~83r){|12~C#sM6R? zoOkpVYjZv|aU#yibl>d>i-m-^c(S~oD;yLwVDog6*PuvDRC$Hn-keReRD#i#o-Fk% zgl99LvAJd3Pbny*vMNV`OC^a94)I3X0&@heS|ggRlp+!C=HP$;#`gC0X=`f}Z)B0F z*Yao;l>LaTU>GBTP{oSGh=F4R)zNREAH4cE5ZOMq0Oslt53xsAo^e7SO%iwqq!x6z zbkm_ z0HhFl>pP$qe1M?$z5lekyZc%@k@xuq<%$Oh;tFbi{m&o$ioYTBN-}=SsRacEWpv=D z_yIG9X(D*RMTQr&kN|{^pSi**>v>t4uXzIO4RthBYQ*n`u`xiZa^Q9h!E$(|;qVm@ z2AF|?hRX%>d+cQ4Bv;~qK(u)cT*d^>c;dsw# zgGUEkPe(^=t;7s&ikJz&23!#he}CBnc;vI%B&XOF^84xHFhdioDGBc2+L~wP>WapB z=y<49oF#g0NC+=uc&<(|@BkYZ7h>lLTsrEiEXy|LQ(A-4_@C#j?x=;~z#au;(o2>3 zL)cs4Q40Lg(Lqy{rFVavn>M+OCM?STiGc>amFjf(32W znBs3wpznd~H!2)L?;L9P`?9jQ$JAd(M+3pl4=9L5wS8-NM4)EK{>F+m*T|>wF z&)lAR7T^DTKaOuEKtDMmoEi*1T*wDM5T67jTXHUMQsf(*xfBtGs1@* z+&8@7oTYV!r{ynGYZX5jY6E{V6}mKedOW^n%RSj)DyQ1Ix(00~Kr&I2KLR!3tNjzE zCEeq7+^$S5n#GNiX;f3({YFhD_j75<<=(vx*Ay`C>-|=^)%7o&FnyDqlk+&(@fY;0 z7H_tAz>N?|SkLHoaC!Ean(Y{)ECN zCm0t3UbKac>;;bQ%tyya=2e4&4sx`fgT}^31JefM4-Bw{MKfb#>EQissLwq->b$Wf zB`d(LeT-18#B{zv8Jd&Fi{8XC5eIYw7#JC0@X!O#2W*5m@CVRxW0`}A$s1-9@O+># zU=h7P`3?iYjI_CNj;dMc{MwdXd!X$rK5V9)RL9K<(dHtt#xqBc9_6^`9Ag;_=m~Us z@cVc1#hQPeDV(S%lr2dogP|8g!=%%!h%! z0PP!ZeU3q(Rm5>o#ERXMLQ~a&qH&d2#-*W0bg7#BIvtF1VR(9bd%ry6+8eY)1JR8w zreJE`1gqmYT(8KMpTNj_u^5;lMb2^hU!W9-C@U`q4$1C`-2*wI(}%Wxk^}@a6AU|$ zTAHk+g8zJg{kK&AnYLIE=`bdfG+C{<8ouAWgd4zhweXr|t!BUciYuWAVvKA78iW!M z5D*e_{{Ds;@2SF4JVMshrsL97`@-5&7|WBKcjr^QAm?d@Bfg72{(wRR=#DAIMn2^#t8FqHujwW}Fi{r9&^)6*xwp?@K4cBPsH-Z-Jz`$*Q< zmIdeBxF4IzmB2wZZ>XlG1`Rm?8+aEOCeZYdhYrW1&F*QdqzUf(S4?jpLKfBU^Yb;= z^w*}3b8#)r&kHmJIXdQV{EaISg0e#WG11j>*Fn#8@4}{LeqTLq31;`;?)my&RP_1r z04M4Y9IMn^A0OAc`A<`6l8bZ?-91&OJW*hoYMe?xft(Iddl+ zWwIb9%$G(PHg_hkyi36^t&@*SyxrqWYGliZg~9wu0TzDq*ddj)^G0mTlFeVO5FBxQ z-378I{u7LYv-B+BP3V=U>5}7sr z;4*R%=sDco-4bey5R7|gKyj}DVndBBb1QP}&1r|B&* + + + + + + + Extends the Whirled services with the notion of locations, clusters, + coordinates and orientation. + +

The basic Whirled services facilitate a game that provides a + connected set of scenes and the ability for the bodies to move from + a scene to its neighboring scenes. The Spot package extends those + services by providing the notion of locations within scenes which a + body can occupy and which, while occupied, cannot be occupied by + other bodies. +

+ +

The philosophy is that a game that uses these services wants to + create a constrained universe where a body cannot simply stand + anywhere on the screen, but can only stand in a set of predefined + locations. This is a simplified mechanism for providing finer + grained locality management, such that expensive distance monitoring + algorithms need not be used to determine who can interact (speak) + with others in a particular scene. For many games, the overhead of + tracking each users' position in near-realtime coupled with a + complex message broadcasting system that dispatches messages to + users in geometric proximity to one another is serious overkill and + does not add sufficient value to the game to merit inclusion. +

+ +

Locations and clusters
+ + The Spot package provides a functional approximation in the form of + clusters of locations, with predefined coordinates, that are + sufficiently proximal that bodies in that cluster of locations can + all speak to one another. The locations for a particular scene are + specified in advance which serves the additional purpose of putting + a limit on the number of bodies that can be standing (and + participating) in a particular scene. This puts a natural upper + bound on the amount of network traffic that will be generated by the + need to broadcast scene updates to all occupants of a scene. +

+ +

Locations also serve as entry and exit points for a scene, + providing a means both for the graphical user interface to identify + the location at which bodies enter and exit a scene as well as + providing a spatial context for the connections between scenes + (i.e. the exit to the scene to the north can be located in the + northward part of a scene display). +

+ +

Orientation
+ + Locations define a notion of orientation which is assigned a priori, + along with the coordinates of the location and which can be + interpreted by the graphical display as the direction that body + sprites should face when standing on that location. Clusters of + locations can be constructed such that all of the bodies standing in + that cluster appear to be facing one another which provides visible + reinforcement that members of the cluster can "hear" the + conversation, while members of other clusters cannot. The + orientation of entry and exit locations are also useful in + establishing an initial orientation for a body sprite that enters a + scene based on the visualization of the entrance mechanism (i.e. if + they appear to enter through the door, the body sprite should be + oriented as if it just walked in the proper direction through the + door). +

+ +

Because the Spot services redefine the scope at which speak + messages are normally distributed (the Crowd services would, by + default, have speak messages dispatched to everyone in the entire + scene because the scene is a place and the default chat mechanisms + dispatch speak messages to all occupants of a place), it defines an + additional communication mode known as shouting, which conveys the + message to everyone in the entire scene. +

+ +

Portals
+ + Portals are a combination of locations and the standard Whirled + notion of neighboring scenes. They contain the coordinates to which + a body sprite would traverse prior to exiting to a neighboring scene + (generally so that they appear to walk to whatever visual + representation of the portal is provided by the scene, like a door + or perhaps simply the edge of the scene). Conversely, they represent + the coordinates at which a body arriving from same neighboring scene + would appear in the displayed scene. +

+ +

Example display
+ + This is an example of how these concepts might look in a game + providing top-down 2D display of its scenes.
+

+ +
+ Example scene image +
+ +

The arrows would, of course, not be rendered but are shown to + indicate the presence of locations (and portals) and their + associated orientation information. +

+ + + diff --git a/src/java/com/threerings/whirled/spot/server/SpotClient.java b/src/java/com/threerings/whirled/spot/server/SpotClient.java new file mode 100644 index 00000000..411e9282 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/server/SpotClient.java @@ -0,0 +1,34 @@ +// +// $Id: SpotClient.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.server; + +import com.threerings.whirled.server.WhirledClient; + +/** + * Extends the Whirled client and handles the necessary notifications that + * take place when a user logs off to let the clients displaying the scene + * they were occupying know that they didn't exit via a portal, but + * instead just disappeared. + */ +public class SpotClient extends WhirledClient +{ +} diff --git a/src/java/com/threerings/whirled/spot/server/SpotDispatcher.java b/src/java/com/threerings/whirled/spot/server/SpotDispatcher.java new file mode 100644 index 00000000..a7e17b11 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/server/SpotDispatcher.java @@ -0,0 +1,95 @@ +// +// $Id: SpotDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.server; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import com.threerings.whirled.client.SceneService; +import com.threerings.whirled.data.SceneMarshaller; +import com.threerings.whirled.spot.client.SpotService; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.SpotMarshaller; + +/** + * Dispatches requests to the {@link SpotProvider}. + */ +public class SpotDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public SpotDispatcher (SpotProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new SpotMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case SpotMarshaller.CHANGE_LOCATION: + ((SpotProvider)provider).changeLocation( + source, + ((Integer)args[0]).intValue(), (Location)args[1], (InvocationService.ConfirmListener)args[2] + ); + return; + + case SpotMarshaller.CLUSTER_SPEAK: + ((SpotProvider)provider).clusterSpeak( + source, + (String)args[0], ((Byte)args[1]).byteValue() + ); + return; + + case SpotMarshaller.JOIN_CLUSTER: + ((SpotProvider)provider).joinCluster( + source, + ((Integer)args[0]).intValue(), (InvocationService.ConfirmListener)args[1] + ); + return; + + case SpotMarshaller.TRAVERSE_PORTAL: + ((SpotProvider)provider).traversePortal( + source, + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), ((Integer)args[2]).intValue(), (SceneService.SceneMoveListener)args[3] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/whirled/spot/server/SpotProvider.java b/src/java/com/threerings/whirled/spot/server/SpotProvider.java new file mode 100644 index 00000000..992bcbaa --- /dev/null +++ b/src/java/com/threerings/whirled/spot/server/SpotProvider.java @@ -0,0 +1,324 @@ +// +// $Id: SpotProvider.java 3832 2006-02-04 03:49:53Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.server; + +import com.samskivert.util.StringUtil; +import com.threerings.util.MessageManager; +import com.threerings.util.Name; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.RootDObjectManager; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +import com.threerings.crowd.chat.data.ChatCodes; +import com.threerings.crowd.chat.server.SpeakProvider; +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.server.PlaceRegistry; + +import com.threerings.whirled.client.SceneService.SceneMoveListener; +import com.threerings.whirled.data.ScenedBodyObject; +import com.threerings.whirled.server.SceneManager; +import com.threerings.whirled.server.SceneRegistry; + +import com.threerings.whirled.spot.Log; +import com.threerings.whirled.spot.client.SpotService; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SpotCodes; +import com.threerings.whirled.spot.data.SpotScene; + +/** + * Provides the server-side implementation of the spot services. + */ +public class SpotProvider + implements SpotCodes, InvocationProvider +{ + /** + * Creates a spot provider that can be registered with the invocation + * manager to handle spot services. + */ + public SpotProvider (RootDObjectManager omgr, PlaceRegistry plreg, + SceneRegistry screg) + { + // we'll need these later + _plreg = plreg; + _screg = screg; + _omgr = omgr; + } + + /** + * Processes a {@link SpotService#traversePortal} request. + */ + public void traversePortal (ClientObject caller, int sceneId, int portalId, + int destSceneVer, SceneMoveListener listener) + throws InvocationException + { + // le sanity check + int cSceneId = getCallerSceneId(caller); + if (cSceneId != sceneId) { + Log.info("Ignoring stale traverse portal request " + + "[caller=" + caller.who() + ", oSceneId=" + sceneId + + ", portalId=" + portalId + ", cSceneId=" + cSceneId + "]."); + InvocationMarshaller.setNoResponse(listener); + return; + } + + // avoid cluttering up the method declaration with final keywords + final BodyObject fsource = (BodyObject)caller; + final int fportalId = portalId; + final int fsceneVer = destSceneVer; + final SceneMoveListener flistener = listener; + + // obtain the source scene + final SpotSceneManager srcmgr = (SpotSceneManager) + _screg.getSceneManager(sceneId); + if (srcmgr == null) { + Log.warning("Traverse portal missing source scene " + + "[user=" + fsource.who() + ", sceneId=" + sceneId + + ", portalId=" + portalId + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // obtain the destination scene and location id + SpotScene rss = (SpotScene)srcmgr.getScene(); + final Portal fdest = rss.getPortal(portalId); + + // give the source scene manager a chance to do access control + String errmsg = srcmgr.mayTraversePortal(fsource, fdest); + if (errmsg != null) { + throw new InvocationException(errmsg); + } + + // make sure this portal has valid info + if (fdest == null || !fdest.isValid()) { + Log.warning("Traverse portal with invalid portal " + + "[user=" + fsource.who() + ", scene=" + srcmgr.where() + + ", pid=" + portalId + ", portal=" + fdest + + ", portals=" + StringUtil.toString(rss.getPortals()) + + "]."); + throw new InvocationException(NO_SUCH_PORTAL); + } + + // resolve their destination scene + SceneRegistry.ResolutionListener rl = + new SceneRegistry.ResolutionListener() { + public void sceneWasResolved (SceneManager scmgr) { + // make sure our caller is still around; under heavy + // load, clients might end their session while the + // scene is resolving + if (!fsource.isActive()) { + Log.info("Abandoning portal traversal, client gone " + + "[who=" + fsource.who() + + ", dest=" + scmgr.where() + "]."); + InvocationMarshaller.setNoResponse(flistener); + return; + } + + // let the source manager know that this guy is + // departing via the specified portal + srcmgr.willTraversePortal(fsource, fdest); + + SpotSceneManager sscmgr = (SpotSceneManager)scmgr; + finishTraversePortalRequest( + fsource, sscmgr, fsceneVer, fdest, flistener); + } + + public void sceneFailedToResolve ( + int rsceneId, Exception reason) { + Log.warning("Unable to resolve target scene " + + "[sceneId=" + rsceneId + + ", reason=" + reason + "]."); + // pretend like the scene doesn't exist to the client + flistener.requestFailed(NO_SUCH_PLACE); + } + }; + _screg.resolveScene(fdest.targetSceneId, rl); + } + + /** + * This is called after the scene to which we are moving is guaranteed + * to have been loaded into the server. + */ + protected void finishTraversePortalRequest ( + BodyObject source, SpotSceneManager destmgr, int sceneVer, + Portal dest, SceneMoveListener listener) + { + // let the destination scene manager know that we're coming in + destmgr.mapEnteringBody(source, dest.targetPortalId); + + try { + // move to the place object associated with this scene + _screg.sceneprov.effectSceneMove( + source, destmgr, sceneVer, listener); + } catch (InvocationException sfe) { + listener.requestFailed(sfe.getMessage()); + // and let the destination scene manager know that we're no + // longer coming in + destmgr.clearEnteringBody(source); + } + } + + /** + * Processes a {@link SpotService#changeLocation} request. + */ + public void changeLocation (ClientObject caller, int sceneId, Location loc, + SpotService.ConfirmListener listener) + throws InvocationException + { + BodyObject source = (BodyObject)caller; + int cSceneId = getCallerSceneId(caller); + if (cSceneId != sceneId) { + Log.info("Rejecting changeLocation for invalid scene " + + "[user=" + source.who() + ", insid=" + cSceneId + + ", wantsid=" + sceneId + ", loc=" + loc + "]."); + throw new InvocationException(INVALID_LOCATION); + } + + // look up the scene manager for the specified scene + SpotSceneManager smgr = (SpotSceneManager) + _screg.getSceneManager(sceneId); + if (smgr == null) { + Log.warning("User requested to change location from " + + "non-existent scene [user=" + source.who() + + ", sceneId=" + sceneId + ", loc=" + loc +"]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // pass the buck to yon scene manager + smgr.handleChangeLoc(source, loc); + + // if that method finished, we're good to go + listener.requestProcessed(); + } + + /** + * Processes a {@link SpotService#joinCluster} request. + */ + public void joinCluster (ClientObject caller, int friendOid, + SpotService.ConfirmListener listener) + throws InvocationException + { + int sceneId = getCallerSceneId(caller); + BodyObject source = (BodyObject)caller; + + // look up the scene manager for the specified scene + SpotSceneManager smgr = (SpotSceneManager) + _screg.getSceneManager(sceneId); + if (smgr == null) { + Log.warning("User requested to join cluster from " + + "non-existent scene [user=" + source.who() + + ", sceneId=" + sceneId + ", foid=" + friendOid +"]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // pass the buck to yon scene manager + smgr.handleJoinCluster(source, friendOid); + + // if that method finished, we're good to go + listener.requestProcessed(); + } + + /** + * Handles request to generate a speak message in the specified cluster. + */ + public void clusterSpeak (ClientObject caller, String message, byte mode) + throws InvocationException + { + // ensure the caller has normal chat access + BodyObject source = (BodyObject)caller; + String errmsg = source.checkAccess(ChatCodes.CHAT_ACCESS, null); + if (errmsg != null) { + SpeakProvider.sendFeedback(source, + MessageManager.GLOBAL_BUNDLE, errmsg); + return; + } + + sendClusterChatMessage(getCallerSceneId(caller), source.getOid(), + source.getVisibleName(), null, message, mode); + } + + /** + * Sends a cluster chat notification to the specified location in the + * specified place object originating with the specified speaker (the + * speaker can be a server entity that wishes to fake a "speak" + * message, in which case the bundle argument should be non-null and + * should contain the id of the bundle to be used to translate the + * message text) and with the supplied message content. + * + * @param sceneId the scene id in which to deliver the chat message. + * @param speakerOid the body object id of the speaker (used to verify + * that they are in the cluster in question). + * @param speaker the username of the user that generated the message + * (or some special speaker name for server messages). + * @param bundle the bundle identifier that will be used by the client + * to translate the message text (or null if the message originated + * from a real live human who wrote it in their native tongue). + * @param message the text of the chat message. + */ + public void sendClusterChatMessage ( + int sceneId, int speakerOid, Name speaker, + String bundle, String message, byte mode) + { + // look up the scene manager for the specified scene + SpotSceneManager smgr = (SpotSceneManager) + _screg.getSceneManager(sceneId); + if (smgr == null) { + Log.warning("User requested cluster chat in non-existent scene " + + "[user=" + speaker + ", sceneId=" + sceneId + + ", message=" + message + "]."); + return; + } + + // pass this request on to the spot scene manager + smgr.handleClusterSpeakRequest( + speakerOid, speaker, bundle, message, mode); + } + + /** + * Obtains the scene id occupied by the supplied caller. + * + * @exception InvocationException thrown if the caller does not + * implement {@link ScenedBodyObject}. + */ + protected static int getCallerSceneId (ClientObject caller) + throws InvocationException + { + if (caller instanceof ScenedBodyObject) { + return ((ScenedBodyObject)caller).getSceneId(); + } else { + Log.warning("Can't get scene from non-scened caller " + + caller.who() + "."); + throw new InvocationException(INTERNAL_ERROR); + } + } + + /** The place registry with which we interoperate. */ + protected PlaceRegistry _plreg; + + /** The scene registry with which we interoperate. */ + protected SceneRegistry _screg; + + /** The object manager we use to do dobject stuff. */ + protected RootDObjectManager _omgr; +} diff --git a/src/java/com/threerings/whirled/spot/server/SpotSceneManager.java b/src/java/com/threerings/whirled/spot/server/SpotSceneManager.java new file mode 100644 index 00000000..b6f6e130 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/server/SpotSceneManager.java @@ -0,0 +1,626 @@ +// +// $Id: SpotSceneManager.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.server; + +import java.util.Iterator; + +import com.samskivert.util.HashIntMap; +import com.samskivert.util.IntIntMap; +import com.threerings.util.Name; + +import com.threerings.presents.dobj.DObject; +import com.threerings.presents.dobj.Subscriber; +import com.threerings.presents.dobj.ObjectAccessException; +import com.threerings.presents.server.InvocationException; + +import com.threerings.crowd.chat.server.SpeakProvider; +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.OccupantInfo; +import com.threerings.crowd.server.CrowdServer; +import com.threerings.whirled.server.SceneManager; + +import com.threerings.whirled.spot.Log; +import com.threerings.whirled.spot.data.Cluster; +import com.threerings.whirled.spot.data.ClusterObject; +import com.threerings.whirled.spot.data.ClusteredBodyObject; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SceneLocation; +import com.threerings.whirled.spot.data.SpotCodes; +import com.threerings.whirled.spot.data.SpotScene; +import com.threerings.whirled.spot.data.SpotSceneObject; + +/** + * Handles the movement of bodies between locations in the scene and + * creates the necessary distributed objects to allow bodies in clusters + * to chat with one another. + */ +public class SpotSceneManager extends SceneManager + implements SpotCodes +{ + /** + * Move the specified body to the default portal, if possible. + */ + public static void moveBodyToDefaultPortal (BodyObject body) + { + SpotSceneManager mgr = (SpotSceneManager) + CrowdServer.plreg.getPlaceManager(body.location); + if (mgr != null) { + SpotScene scene = (SpotScene)mgr.getScene(); + if (scene == null) { + Log.warning("No scene in moveBodyToDefaultPortal()? " + + "[who=" + body.who() + + ", where=" + mgr.where() + "]."); + return; + } + try { + Location eloc = scene.getDefaultEntrance().getLocation(); + mgr.handleChangeLoc(body, eloc); + } catch (InvocationException ie) { + Log.warning("Could not move user to default portal " + + "[where=" + mgr.where() + ", who=" + body.who() + + ", error=" + ie + "]."); + } + } + } + + /** + * Assigns a starting location for an entering body. This will happen + * before the body is made to "occupy" the scene (defined by their + * having an occupant info record). So when they do finally occupy the + * scene, the client will know where to render them. + */ + public void mapEnteringBody (BodyObject body, int portalId) + { + _enterers.put(body.getOid(), portalId); + } + + /** + * Called if a body failed to enter our scene after we assigned them + * an entering position. + */ + public void clearEnteringBody (BodyObject body) + { + _enterers.remove(body.getOid()); + } + + /** + * This is called when a user requests to traverse a portal from this + * scene to another scene. The manager may return an error code string + * that will be reported back to the caller explaining the failure or + * null indicating that it is OK for the caller to + * traverse the portal. + */ + public String mayTraversePortal (BodyObject body, Portal portal) + { + return null; + } + + /** + * This is called to let this scene manager know that the user is + * about to traverse the specified portal. The default implementation + * relocates the user to the location associated with the portal. It + * is still possible that the traversal will fail, so don't do + * anything too crazy. + */ + public void willTraversePortal (BodyObject body, Portal portal) + { + updateLocation(body, portal.getLocation()); + } + + // documentation inherited + protected void didStartup () + { + // get a casted reference to our place object (we need to do this + // before calling super.didStartup() because that will call + // sceneManagerDidResolve() which may start letting people into + // the scene) + _ssobj = (SpotSceneObject)_plobj; + + super.didStartup(); + } + + // documentation inherited + protected void gotSceneData () + { + super.gotSceneData(); + + // keep a casted reference around to our scene + _sscene = (SpotScene)_scene; + } + + // documentation inherited + protected void bodyLeft (int bodyOid) + { + super.bodyLeft(bodyOid); + + // clear out their location information + _ssobj.removeFromOccupantLocs(Integer.valueOf(bodyOid)); + + // clear any cluster they may occupy + removeFromCluster(bodyOid); + + // let's make damned sure they're not in any cluster + Iterator cliter = _clusters.values().iterator(); + while (cliter.hasNext()) { + ClusterRecord clrec = (ClusterRecord)cliter.next(); + if (clrec.containsKey(bodyOid)) { + Log.info("Pruning departed body from cluster [boid=" + bodyOid + + ", cluster=" + clrec + "]."); + clrec.removeBody(bodyOid); + if (clrec.size() == 0) { + // If we just removed the last body, destroy the cluster, + // need to use the iterator's removal so we don't + // hose ourselves. + clrec.destroy(false); + cliter.remove(); + } + } + } + } + + // documentation inherited + protected void insertOccupantInfo (OccupantInfo info, BodyObject body) + { + super.insertOccupantInfo(info, body); + + // we don't actually populate their occupant info, but instead assign + // them their starting location in the scene + int portalId = _enterers.remove(body.getOid()); + Portal entry; + if (portalId != -1) { + entry = _sscene.getPortal(portalId); + if (entry == null) { + Log.warning("Body mapped at invalid portal [where=" + where() + + ", who=" + body.who() + + ", portalId=" + portalId + "]."); + entry = _sscene.getDefaultEntrance(); + } + } else { + entry = _sscene.getDefaultEntrance(); + } + +// Log.debug("Positioning entering body [who=" + body.who() + +// ", where=" + entry.getOppLocation() + "]."); + + // create a scene location for them located on the entrance portal + // but facing the opposite direction + _ssobj.addToOccupantLocs(computeEnteringLocation(body, entry)); + } + + /** + * Called when the supplied body is entering our scene via the + * specified portal. The default location is the one associated with + * the portal, but derived classes may wish to adjust this. + */ + protected SceneLocation computeEnteringLocation ( + BodyObject body, Portal entry) + { + return new SceneLocation(entry.getOppLocation(), body.getOid()); + } + + /** + * Called by the {@link SpotProvider} when we receive a request by a + * user to occupy a particular location. + * + * @param source the body to be moved. + * @param loc the location to which to move the body. + * + * @exception InvocationException thrown with a reason code explaining + * the failure if there is a problem processing the request. + */ + protected void handleChangeLoc (BodyObject source, Location loc) + throws InvocationException + { + // make sure they are in our scene + if (!_ssobj.occupants.contains(source.getOid())) { + Log.warning("Refusing change loc from non-scene occupant " + + "[where=" + where() + ", who=" + source.who() + + ", loc=" + loc + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // let our derived classes decide if this is an OK place to stand + if (!validateLocation(source, loc)) { + throw new InvocationException(INVALID_LOCATION); + } + + // update the user's location information in the scene which will + // indicate to the client that their avatar should be moved from + // its current position to their new position + updateLocation(source, loc); + + // remove them from any cluster as they've departed + removeFromCluster(source.getOid()); + } + + /** + * Derived classes can override this method and validate that the + * specified body can stand in the requested location. The default + * implementation returns true in all circumstances; + * stand where ye may! + */ + protected boolean validateLocation (BodyObject source, Location loc) + { + return true; + } + + /** + * Updates the location of the specified body. + */ + protected void updateLocation (BodyObject source, Location loc) + { + SceneLocation sloc = new SceneLocation(loc, source.getOid()); + if (!_ssobj.occupantLocs.contains(sloc)) { + // complain if they don't already have a location configured + Log.warning("Changing loc for occupant without previous loc " + + "[where=" + where() + ", who=" + source.who() + + ", nloc=" + loc + "]."); + Thread.dumpStack(); + _ssobj.addToOccupantLocs(sloc); + } else { + _ssobj.updateOccupantLocs(sloc); + } + } + + /** + * Called by the {@link SpotProvider} when we receive a request by a + * user to join a particular cluster. + * + * @param joiner the body to be moved. + * @param targetOid the bodyOid of another user or the oid of an + * existing cluster; the moving user will be made to join the other + * user's cluster. + * + * @exception InvocationException thrown with a reason code explaining + * the failure if there is a problem processing the request. + */ + protected void handleJoinCluster (BodyObject joiner, int targetOid) + throws InvocationException + { + // if the cluster already exists, add this user and be done + ClusterRecord clrec = (ClusterRecord)_clusters.get(targetOid); + if (clrec != null) { + clrec.addBody(joiner); + return; + } + + // otherwise see if they sent us the user's oid + DObject tobj = CrowdServer.omgr.getObject(targetOid); + if (!(tobj instanceof BodyObject)) { + Log.info("Can't join cluster, missing target " + + "[creator=" + joiner.who() + + ", targetOid=" + targetOid + "]."); + throw new InvocationException(NO_SUCH_CLUSTER); + } + + // make sure we're in the same scene as said user + BodyObject friend = (BodyObject)tobj; + if (friend.location != joiner.location) { + Log.info("Refusing cluster join from non-proximate user " + + "[joiner=" + joiner.who() + ", jloc=" + joiner.location + + ", target=" + friend.who() + + ", tloc=" + friend.location + "]."); + throw new InvocationException(NO_SUCH_CLUSTER); + } + + // see if the friend is already in a cluster + clrec = getCluster(friend.getOid()); + if (clrec != null) { + clrec.addBody(joiner); + return; + } + + // confirm that they can start a cluster with this unsuspecting + // other person + checkCanCluster(joiner, friend); + + // otherwise we create a new cluster and add our charter members! +// Log.debug("Creating cluster [starter=" + joiner.who() + +// ", target=" + friend.who() + "]."); + clrec = createClusterRecord(); + clrec.addBody(friend); + clrec.addBody(joiner); + } + + /** + * Creates the cluster record instance that we'll use to manage our + * cluster. + */ + protected ClusterRecord createClusterRecord () + { + return new ClusterRecord(); + } + + /** + * Gives derived classes an opportunity to veto a user's attempt to + * start a cluster with another user. If the attempt should be vetoed, + * this method should throw an {@link InvocationException} indicating + * the reason for veto. + */ + protected void checkCanCluster (BodyObject initiator, BodyObject target) + throws InvocationException + { + // nothing to do by default + } + + /** + * Removes the specified user from any cluster they occupy. + */ + protected void removeFromCluster (int bodyOid) + { + ClusterRecord clrec = getCluster(bodyOid); + if (clrec != null) { + clrec.removeBody(bodyOid); + // If that was the last person, destroy the cluster + if (clrec.size() == 0) { + clrec.destroy(true); + } + } + } + + /** + * Fetches the cluster record for the specified body. + */ + protected ClusterRecord getCluster (int bodyOid) + { + ClusteredBodyObject bobj = (ClusteredBodyObject) + CrowdServer.omgr.getObject(bodyOid); + return (bobj == null) ? null : + (ClusterRecord)_clusters.get(bobj.getClusterOid()); + } + + /** + * Called by the {@link SpotProvider} when we receive a cluster speak + * request. + */ + protected void handleClusterSpeakRequest ( + int sourceOid, Name source, String bundle, String message, byte mode) + { + ClusterRecord clrec = getCluster(sourceOid); + if (clrec == null) { + Log.warning("Non-clustered user requested cluster speak " + + "[where=" + where() + ", chatter=" + source + + " (" + sourceOid + "), msg=" + message + "]."); + } else { + SpeakProvider.sendSpeak(clrec.getClusterObject(), + source, bundle, message, mode); + } + } + + /** + * Returns the location of the specified body or null if they have no + * location in this scene. + */ + protected SceneLocation locationForBody (int bodyOid) + { + return (SceneLocation)_ssobj.occupantLocs.get(Integer.valueOf(bodyOid)); + } + + /** + * Verifies that the specified cluster can be expanded to include + * another body. + */ + protected boolean canAddBody (ClusterRecord clrec, BodyObject body) + { + return true; + } + + /** + * Called when a user is added to a cluster. The scene manager + * implementation should take this opportunity to rearrange everyone + * in the cluster appropriately for the new size. + */ + protected void bodyAdded (ClusterRecord clrec, BodyObject body) + { + } + + /** + * Called when a user is removed from a cluster. The scene manager + * implementation should take this opportunity to rearrange everyone + * in the cluster appropriately for the new size. + */ + protected void bodyRemoved (ClusterRecord clrec, BodyObject body) + { + } + + /** + * Used to manage clusters which are groups of users that can chat to + * one another. + */ + protected class ClusterRecord extends HashIntMap + implements Subscriber + { + public ClusterRecord () + { + CrowdServer.omgr.createObject(ClusterObject.class, this); + } + + public boolean addBody (BodyObject body) + throws InvocationException + { + if (!(body instanceof ClusteredBodyObject)) { + Log.warning("Refusing to add non-clustered body to cluster " + + "[cloid=" + _clobj.getOid() + + ", size=" + size() + ", who=" + body.who() + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + + // if they're already in the cluster, do nothing + if (containsKey(body.getOid())) { + return false; + } + + // make sure we can add this body + if (!canAddBody(this, body)) { +// Log.debug("Cluster full, refusing growth " + this + "."); + throw new InvocationException(CLUSTER_FULL); + } + + // make sure our intrepid joiner is not in any another cluster + removeFromCluster(body.getOid()); + + put(body.getOid(), body); + _ssobj.startTransaction(); + try { + body.startTransaction(); + try { + bodyAdded(this, body); // do the hokey pokey + + if (_clobj != null) { + ((ClusteredBodyObject)body).setClusterOid( + _clobj.getOid()); + _clobj.addToOccupants(body.getOid()); + _ssobj.updateClusters(_cluster); + } + + } finally { + body.commitTransaction(); + } + } finally { + _ssobj.commitTransaction(); + } + +// Log.debug("Added " + body.who() + " to "+ this + "."); + return true; + } + + public void removeBody (int bodyOid) + { + BodyObject body = (BodyObject)remove(bodyOid); + if (body == null) { + Log.warning("Requested to remove unknown body from cluster " + + "[cloid=" + _clobj.getOid() + + ", size=" + size() + ", who=" + bodyOid + "]."); + return; + } + + if (body.isActive()) { + body.startTransaction(); + } + try { + _ssobj.startTransaction(); + try { + ((ClusteredBodyObject)body).setClusterOid(-1); + bodyRemoved(this, body); // do the hokey pokey + + if (_clobj != null) { + _clobj.removeFromOccupants(bodyOid); + _ssobj.updateClusters(_cluster); + } + + } finally { + _ssobj.commitTransaction(); + } + } finally { + if (body.isActive()) { + body.commitTransaction(); + } + } + +// Log.debug("Removed " + bodyOid + " from "+ this + "."); + + } + + public ClusterObject getClusterObject () + { + return _clobj; + } + + public Cluster getCluster () + { + return _cluster; + } + + public void objectAvailable (DObject object) + { + // keep this feller around + _clobj = (ClusterObject)object; + _clusters.put(_clobj.getOid(), this); + + // let any mapped users know about our cluster + Iterator iter = values().iterator(); + while (iter.hasNext()) { + ClusteredBodyObject body = (ClusteredBodyObject)iter.next(); + body.setClusterOid(_clobj.getOid()); + _clobj.addToOccupants(((BodyObject)body).getOid()); + } + + // configure our cluster record and publish it + _cluster.clusterOid = _clobj.getOid(); + _ssobj.addToClusters(_cluster); + + // if we didn't manage to add our creating user when we first + // started up, there's no point in our sticking around + if (size() == 0) { + destroy(true); + } + } + + public void requestFailed (int oid, ObjectAccessException cause) + { + Log.warning("Aiya! Failed to create cluster object " + + "[cause=" + cause + ", penders=" + size() + "]."); + + // let any mapped users know that we're hosed + Iterator iter = values().iterator(); + while (iter.hasNext()) { + ClusteredBodyObject body = (ClusteredBodyObject)iter.next(); + body.setClusterOid(-1); + } + } + + public String toString () + { + return "[cluster=" + _cluster + ", size=" + size() + "]"; + } + + protected void destroy (boolean doRemoval) + { +// Log.debug("Cluster empty, going away " + +// "[cloid=" + _clobj.getOid() + "]."); + _ssobj.removeFromClusters(_cluster.getKey()); + + // If we've also been requested to remove ourself from the clusters + // list, do that. + if (doRemoval) { + _clusters.remove(_clobj.getOid()); + } + CrowdServer.omgr.destroyObject(_clobj.getOid()); + } + + protected ClusterObject _clobj; + protected Cluster _cluster = new Cluster(); + } + + /** A casted reference to our place object. */ + protected SpotSceneObject _ssobj; + + /** A casted reference to our scene instance. */ + protected SpotScene _sscene; + + /** Records with information on all clusters in this scene. */ + protected HashIntMap _clusters = new HashIntMap(); + + /** A mapping of entering bodies to portal ids. */ + protected IntIntMap _enterers = new IntIntMap(); +} diff --git a/src/java/com/threerings/whirled/spot/tools/EditablePortal.java b/src/java/com/threerings/whirled/spot/tools/EditablePortal.java new file mode 100644 index 00000000..9448c6f4 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/tools/EditablePortal.java @@ -0,0 +1,42 @@ +// +// $Id: EditablePortal.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.tools; + +import com.threerings.whirled.spot.data.Portal; + +/** + * An editable portal extends the standard portal with information needed + * by the loader and editor. + */ +public class EditablePortal extends Portal +{ + /** The human-readable name of this portal. */ + public String name; + + /** The human-readable name of the scene to which this portal + * links. */ + public String targetSceneName; + + /** The human-readable name of the portal to which this portal links + * in its target scene. */ + public String targetPortalName; +} diff --git a/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneRuleSet.java b/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneRuleSet.java new file mode 100644 index 00000000..55180c1f --- /dev/null +++ b/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneRuleSet.java @@ -0,0 +1,164 @@ +// +// $Id: SpotSceneRuleSet.java 4140 2006-05-23 21:44:05Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.tools.xml; + +import java.lang.reflect.Field; + +import com.samskivert.xml.SetPropertyFieldsRule; +import com.samskivert.util.StringUtil; +import com.samskivert.util.ValueMarshaller; + +import com.threerings.tools.xml.NestableRuleSet; + +import org.apache.commons.digester.Digester; +import org.apache.commons.digester.ObjectCreateRule; +import org.apache.commons.digester.Rule; + +import org.xml.sax.Attributes; + +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.Portal; +import com.threerings.whirled.spot.data.SpotSceneModel; +import com.threerings.whirled.spot.tools.EditablePortal; + +/** + * Used to parse a {@link SpotSceneModel} from XML. + */ +public abstract class SpotSceneRuleSet implements NestableRuleSet +{ + // documentation inherited from interface + public String getOuterElement () + { + return SpotSceneWriter.OUTER_ELEMENT; + } + + // documentation inherited from interface + public void addRuleInstances (String prefix, Digester digester) + { + digester.addObjectCreate(prefix, SpotSceneModel.class.getName()); + + // set up rules to parse and set our fields + digester.addRule(prefix, new SetPropertyFieldsRule()); + + // create EditablePortal instances when we see + digester.addRule(prefix + "/portal", new PortalCreateRule(this)); + digester.addRule(prefix + "/portal", new PortalFieldsRule()); + digester.addSetNext(prefix + "/portal", "addPortal", + Portal.class.getName()); + } + + /** + * Create a new instance of the Location class that should be used + * with Portals. + */ + protected abstract Location createLocation (); + + /** + * A rule used to create the portal but also initialize the Location + * property within it. + */ + protected static class PortalCreateRule extends ObjectCreateRule + { + public PortalCreateRule (SpotSceneRuleSet ruleset) + { + super(EditablePortal.class.getName()); + _ruleset = ruleset; + } + + // documentation inherited + public void begin (String namespace, String name, Attributes attributes) + throws Exception + { + super.begin(namespace, name, attributes); + + // create the empty Location in the Portal + Portal p = (Portal) digester.peek(); + p.loc = _ruleset.createLocation(); + } + + protected SpotSceneRuleSet _ruleset; + } + + /** + * Set fields in the Portal, or in the Location object + * contained therein. If there are ambiguous attribute names then.. + * well. yeah. + */ + protected static class PortalFieldsRule extends Rule + { + // documentation inherited + public void begin (String namespace, String name, Attributes attrs) + throws Exception + { + Portal portal = (Portal) digester.peek(); + Class portalClass = portal.getClass(); + Location loc = portal.loc; + Class locClass = loc.getClass(); + + // iterate over the attributes, setting public fields where + // applicable + for (int i = 0; i < attrs.getLength(); i++) { + String lname = attrs.getLocalName(i); + if (StringUtil.isBlank(lname)) { + lname = attrs.getQName(i); + } + + // look for a public field with this lname + Field field; + Object container; + try { + field = portalClass.getField(lname); + container = portal; + + } catch (NoSuchFieldException nsfe) { + // if we didn't find the field in the Portal, maybe it's + // in the Location + try { + field = locClass.getField(lname); + container = loc; + + } catch (NoSuchFieldException nsfe2) { + digester.getLogger().warn( + "Skipping property '" + lname + + "' for which there is no field."); + continue; + } + } + + // convert the value into the appropriate object type + String valstr = attrs.getValue(i); + // use the value marshaller to parse the + // property based on the type of the target object field + Object value = ValueMarshaller.unmarshal( + field.getType(), valstr); + + if (digester.getLogger().isDebugEnabled()) { + digester.getLogger().debug(" Setting property '" + lname + + "' to '" + valstr + "'"); + } + + // and finally set the field + field.set(container, value); + } + } + } +} diff --git a/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneWriter.java b/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneWriter.java new file mode 100644 index 00000000..99d12888 --- /dev/null +++ b/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneWriter.java @@ -0,0 +1,113 @@ +// +// $Id: SpotSceneWriter.java 4077 2006-04-28 21:27:11Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.tools.xml; + +import java.lang.reflect.Field; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import com.megginson.sax.DataWriter; +import com.threerings.tools.xml.NestableWriter; + +import com.threerings.whirled.spot.Log; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.spot.data.SpotSceneModel; +import com.threerings.whirled.spot.tools.EditablePortal; + +/** + * Generates an XML representation of a {@link SpotSceneModel}. + */ +public class SpotSceneWriter + implements NestableWriter +{ + /** The outer element used to enclose our spot scene definition. */ + public static final String OUTER_ELEMENT = "spot"; + + // documentation inherited from interface + public void write (Object object, DataWriter writer) + throws SAXException + { + SpotSceneModel model = (SpotSceneModel)object; + AttributesImpl attrs = new AttributesImpl(); + addSceneAttributes(model, attrs); + writer.startElement("", OUTER_ELEMENT, "", attrs); + writeSceneData(model, writer); + writer.endElement(OUTER_ELEMENT); + } + + protected void addSceneAttributes (SpotSceneModel model, + AttributesImpl attrs) + { + if (model.defaultEntranceId != -1) { + attrs.addAttribute("", "defaultEntranceId", "", "", + String.valueOf(model.defaultEntranceId)); + } + } + + protected void writeSceneData (SpotSceneModel model, DataWriter writer) + throws SAXException + { + // write out the portal info + for (int ii = 0; ii < model.portals.length; ii++) { + EditablePortal port = (EditablePortal)model.portals[ii]; + AttributesImpl attrs = new AttributesImpl(); + attrs.addAttribute("", "portalId", "", "", + String.valueOf(port.portalId)); + addPortalLocationAttributes(port.loc, attrs); + maybeAddAttr(attrs, "name", port.name); + maybeAddAttr(attrs, "targetSceneName", port.targetSceneName); + maybeAddAttr(attrs, "targetPortalName", port.targetPortalName); + writer.emptyElement("", "portal", "", attrs); + } + } + + protected void addPortalLocationAttributes ( + Location portalLoc, AttributesImpl attrs) + { + // we just add all the visible fields of the location, but something + // more sophisticated could be done + Class clazz = portalLoc.getClass(); + Field[] fields = clazz.getFields(); + for (int ii=0; ii < fields.length; ii++) { + try { + attrs.addAttribute("", fields[ii].getName(), "", "", + String.valueOf(fields[ii].get(portalLoc))); + } catch (IllegalAccessException iae) { + Log.warning("Unable to write portal field, skipping " + + "[field=" + fields[ii].getName() + ", e=" + iae + "]."); + } + } + } + + /** + * Adds the supplied attribute to the attributes object iff the value + * is non-null. + */ + protected void maybeAddAttr ( + AttributesImpl attrs, String name, String value) + { + if (value != null) { + attrs.addAttribute("", name, "", "", value); + } + } +} diff --git a/src/java/com/threerings/whirled/tools/xml/SceneParser.java b/src/java/com/threerings/whirled/tools/xml/SceneParser.java new file mode 100644 index 00000000..1ea689a9 --- /dev/null +++ b/src/java/com/threerings/whirled/tools/xml/SceneParser.java @@ -0,0 +1,113 @@ +// +// $Id: SceneParser.java 3749 2005-11-09 04:00:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.tools.xml; + +import java.io.IOException; +import java.io.FileInputStream; + +import org.xml.sax.SAXException; +import org.apache.commons.digester.Digester; + +import com.samskivert.util.StringUtil; +import com.threerings.tools.xml.NestableRuleSet; +import com.threerings.whirled.data.AuxModel; +import com.threerings.whirled.data.SceneModel; + +/** + * A simple class for parsing an editable scene instance. + */ +public class SceneParser +{ + /** + * Constructs a scene parser that parses scenes with the specified XML + * path prefix. + */ + public SceneParser (String prefix) + { + // create and configure our digester + _digester = new Digester(); + + // create our scene rule set + SceneRuleSet set = createSceneRuleSet(); + + // configure our top-level path prefix + if (StringUtil.isBlank(prefix)) { + _prefix = set.getOuterElement(); + } else { + _prefix = prefix + "/" + set.getOuterElement(); + } + + // add the scene rules + set.addRuleInstances(_prefix, _digester); + + // add a rule to grab the finished scene model + _digester.addSetNext(_prefix, "setScene", SceneModel.class.getName()); + } + + /** + * Creates the rule set used to parse our scene. + */ + protected SceneRuleSet createSceneRuleSet () + { + return new SceneRuleSet(); + } + + /** + * Adds a {@link NestableRuleSet} for parsing auxiliary scene models. + */ + public void registerAuxRuleSet (NestableRuleSet set) + { + // add their outer element to the prefix + String prefix = _prefix + "/" + set.getOuterElement(); + + // add the rules to generate the aux scene model + set.addRuleInstances(prefix, _digester); + + // and add a rule to grab it + _digester.addSetNext(prefix, "addAuxModel", AuxModel.class.getName()); + } + + /** + * Parses the XML file at the specified path into a scene model + * instance. + */ + public SceneModel parseScene (String path) + throws IOException, SAXException + { + _model = null; + _digester.push(this); + _digester.parse(new FileInputStream(path)); + return _model; + } + + /** + * Called by the parser once the scene is parsed. + */ + public void setScene (SceneModel model) + { + _model = model; + } + + protected String _prefix; + protected Digester _digester; + protected SceneModel _model; +} diff --git a/src/java/com/threerings/whirled/tools/xml/SceneRuleSet.java b/src/java/com/threerings/whirled/tools/xml/SceneRuleSet.java new file mode 100644 index 00000000..ce8f99d4 --- /dev/null +++ b/src/java/com/threerings/whirled/tools/xml/SceneRuleSet.java @@ -0,0 +1,60 @@ +// +// $Id: SceneRuleSet.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.tools.xml; + +import org.apache.commons.digester.Digester; + +import com.samskivert.xml.SetPropertyFieldsRule; + +import com.threerings.tools.xml.NestableRuleSet; +import com.threerings.whirled.data.SceneModel; + +/** + * Used to parse a {@link SceneModel} from XML. + */ +public class SceneRuleSet implements NestableRuleSet +{ + // documentation inherited from interface + public String getOuterElement () + { + return SceneWriter.OUTER_ELEMENT; + } + + // documentation inherited from interface + public void addRuleInstances (String prefix, Digester digester) + { + // this creates the appropriate instance when we encounter our tag + digester.addObjectCreate(prefix, getSceneClass().getName()); + + // set up rules to parse and set our fields + digester.addRule(prefix, new SetPropertyFieldsRule()); + } + + /** + * This indicates the class (which should extend {@link SceneModel}) + * to be instantiated during the parsing process. + */ + protected Class getSceneClass () + { + return SceneModel.class; + } +} diff --git a/src/java/com/threerings/whirled/tools/xml/SceneWriter.java b/src/java/com/threerings/whirled/tools/xml/SceneWriter.java new file mode 100644 index 00000000..c8905640 --- /dev/null +++ b/src/java/com/threerings/whirled/tools/xml/SceneWriter.java @@ -0,0 +1,121 @@ +// +// $Id: SceneWriter.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.tools.xml; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.HashMap; + +import org.xml.sax.SAXException; +import org.xml.sax.helpers.AttributesImpl; + +import com.megginson.sax.DataWriter; +import com.threerings.tools.xml.NestableWriter; + +import com.threerings.whirled.Log; +import com.threerings.whirled.data.AuxModel; +import com.threerings.whirled.data.SceneModel; + +/** + * Generates an XML representation of an {@link SceneModel}. + */ +public class SceneWriter +{ + /** The outer element used to enclose our scene definition. */ + public static final String OUTER_ELEMENT = "scene"; + + /** + * Registers a writer for writing auxiliary scene models of the + * supplied class. + */ + public void registerAuxWriter (Class aclass, NestableWriter writer) + { + _auxers.put(aclass, writer); + } + + /** + * Writes the supplied scene out to the specified file. + */ + public void writeScene (File out, SceneModel model) + throws IOException, SAXException + { + FileWriter fout = new FileWriter(out); + DataWriter dout = new DataWriter(fout); + dout.setIndentStep(2); + dout.startDocument(); + writeSceneModel(model, dout); + dout.endDocument(); + fout.close(); + } + + /** + * Writes the data for the supplied {@link SceneModel} to the XML data + * writer supplied. The writer should already be configured with the + * appropriate indentation level so that this writer can simply output + * its elements and allow the calling code to determine where in the + * greater scene description file the scene data should live. + */ + public void writeSceneModel (SceneModel model, DataWriter writer) + throws SAXException + { + AttributesImpl attrs = new AttributesImpl(); + addSceneAttributes(model, attrs); + writer.startElement("", OUTER_ELEMENT, "", attrs); + writeSceneData(model, writer); + writer.endElement(OUTER_ELEMENT); + } + + /** + * Adds attributes to the top-level element before it gets written. + */ + protected void addSceneAttributes ( + SceneModel model, AttributesImpl attrs) + { + attrs.addAttribute("", "name", "", "", model.name); + attrs.addAttribute("", "version", "", "", + Integer.toString(model.version)); + } + + /** + * Writes just the scene data which is handy for derived classes which + * may wish to add their own scene data to the scene output. + */ + protected void writeSceneData (SceneModel model, DataWriter writer) + throws SAXException + { + // write out our auxiliary scene models + for (int ii = 0; ii < model.auxModels.length; ii++) { + AuxModel amodel = model.auxModels[ii]; + NestableWriter awriter = (NestableWriter) + _auxers.get(amodel.getClass()); + if (awriter != null) { + awriter.write(amodel, writer); + } else { + Log.warning("No writer registered for auxiliary scene model " + + "[mclass=" + amodel.getClass() + "]."); + } + } + } + + protected HashMap _auxers = new HashMap(); +} diff --git a/src/java/com/threerings/whirled/util/NoSuchSceneException.java b/src/java/com/threerings/whirled/util/NoSuchSceneException.java new file mode 100644 index 00000000..f5d26e03 --- /dev/null +++ b/src/java/com/threerings/whirled/util/NoSuchSceneException.java @@ -0,0 +1,42 @@ +// +// $Id: NoSuchSceneException.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.util; + +/** + * Thrown when an attempt to load a non-existent scene is made on the + * repository. + */ +public class NoSuchSceneException extends Exception +{ + public NoSuchSceneException (int sceneid) + { + super("No such scene [sceneid=" + sceneid + "]"); + _sceneid = sceneid; + } + + public int getSceneId () + { + return _sceneid; + } + + protected int _sceneid; +} diff --git a/src/java/com/threerings/whirled/util/SceneFactory.java b/src/java/com/threerings/whirled/util/SceneFactory.java new file mode 100644 index 00000000..cc06b439 --- /dev/null +++ b/src/java/com/threerings/whirled/util/SceneFactory.java @@ -0,0 +1,39 @@ +// +// $Id: SceneFactory.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.util; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.whirled.data.Scene; +import com.threerings.whirled.data.SceneModel; + +/** + * This is used by the Whirled services to obtain a {@link Scene} + * implementation given a scene model and associated data. + */ +public interface SceneFactory +{ + /** + * Creates a {@link Scene} implementation given the supplied scene + * model and place config. + */ + public Scene createScene (SceneModel model, PlaceConfig config); +} diff --git a/src/java/com/threerings/whirled/util/UpdateList.java b/src/java/com/threerings/whirled/util/UpdateList.java new file mode 100644 index 00000000..ce9ba621 --- /dev/null +++ b/src/java/com/threerings/whirled/util/UpdateList.java @@ -0,0 +1,107 @@ +// +// $Id: UpdateList.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.util; + +import java.util.ArrayList; + +import com.threerings.whirled.Log; +import com.threerings.whirled.data.SceneUpdate; + +/** + * A list specialized for storing {@link SceneUpdate} objects. + */ +public class UpdateList +{ + public UpdateList () + { + _updates = new ArrayList(); + } + + /** + * Adds an update to this list. The update must follow appropriately + * the chain of updates established by the updates already in the list + * (meaning it must operate on one version higher than the most recent + * update already in the list). + */ + public void addUpdate (SceneUpdate update) + { + if (_minVersion == -1) { + // this is our first update, so we initialize our min version + _minVersion = update.getSceneVersion(); + + } else { + int gotVersion = update.getSceneVersion(); + int expVersion = _minVersion + _updates.size(); + if (gotVersion > expVersion) { + Log.warning("Update continuity broken, flushing list " + + "[got=" + update + ", expect=" + expVersion + + ", ucount=" + _updates.size() + "]."); + // flush out our old updates and start anew from here + _updates.clear(); + _minVersion = expVersion; + + } else if (gotVersion < expVersion) { + // we somehow got an update that's older than updates we + // already have? wick wick wack + String errmsg = "Invalid update version " + + "[want=" + expVersion + ", got=" + update + "]"; + throw new IllegalArgumentException(errmsg); + } + } + _updates.add(update); + } + + /** + * Returns all of the updates that should be applied to a scene with + * the specified version to bring it up to date. null is + * returned if the scene's version is older than the oldest update in + * our list, in which case it cannot be brought up to date by applying + * updates from this list. + */ + public SceneUpdate[] getUpdates (int fromVersion) + { + if (_minVersion == -1 || fromVersion < _minVersion) { + return null; + } + + int offset = fromVersion - _minVersion; + int ucount = _updates.size() - offset; + SceneUpdate[] updates = new SceneUpdate[ucount]; + for (int ii = 0; ii < ucount; ii++) { + updates[ii] = (SceneUpdate)_updates.get(ii+offset); + } + return updates; + } + + /** + * Returns true if the supplied actual scene version is in accordance + * with the updates contained in this list. + */ + public boolean validate (int sceneVersion) + { + return ((_minVersion == -1) || // we have no updates + (_minVersion + _updates.size() == sceneVersion)); + } + + protected ArrayList _updates; + protected int _minVersion = -1; +} diff --git a/src/java/com/threerings/whirled/util/WhirledContext.java b/src/java/com/threerings/whirled/util/WhirledContext.java new file mode 100644 index 00000000..717fa4ab --- /dev/null +++ b/src/java/com/threerings/whirled/util/WhirledContext.java @@ -0,0 +1,37 @@ +// +// $Id: WhirledContext.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.util; + +import com.threerings.crowd.util.CrowdContext; +import com.threerings.whirled.client.SceneDirector; + +/** + * The whirled context provides access to the various managers, etc. that + * are needed by the whirled client code. + */ +public interface WhirledContext extends CrowdContext +{ + /** + * Returns a reference to the scene director. + */ + public SceneDirector getSceneDirector (); +} diff --git a/src/java/com/threerings/whirled/zone/Log.java b/src/java/com/threerings/whirled/zone/Log.java new file mode 100644 index 00000000..eda96ccb --- /dev/null +++ b/src/java/com/threerings/whirled/zone/Log.java @@ -0,0 +1,56 @@ +// +// $Id: Log.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone; + +/** + * A placeholder class that contains a reference to the log object used by + * the Whirled Zone services. + */ +public class Log +{ + public static com.samskivert.util.Log log = + new com.samskivert.util.Log("whirled.zone"); + + /** Convenience function. */ + public static void debug (String message) + { + log.debug(message); + } + + /** Convenience function. */ + public static void info (String message) + { + log.info(message); + } + + /** Convenience function. */ + public static void warning (String message) + { + log.warning(message); + } + + /** Convenience function. */ + public static void logStackTrace (Throwable t) + { + log.logStackTrace(com.samskivert.util.Log.WARNING, t); + } +} diff --git a/src/java/com/threerings/whirled/zone/client/ZoneAdapter.java b/src/java/com/threerings/whirled/zone/client/ZoneAdapter.java new file mode 100644 index 00000000..d064cc36 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/client/ZoneAdapter.java @@ -0,0 +1,51 @@ +// +// $Id: ZoneAdapter.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client; + +import com.threerings.whirled.zone.data.ZoneSummary; + +/** + * The zone adapter makes life easier for a class that really only cares + * about one or two of the zone observer callbacks and doesn't want to + * provide empty implementations of the others. One can either extend zone + * adapter, or create an anonymous instance that overrides the desired + * callback(s). + * + * @see ZoneObserver + */ +public class ZoneAdapter implements ZoneObserver +{ + // documentation inherited from interface + public void zoneWillChange (int zoneId) + { + } + + // documentation inherited from interface + public void zoneDidChange (ZoneSummary summary) + { + } + + // documentation inherited from interface + public void zoneChangeFailed (String reason) + { + } +} diff --git a/src/java/com/threerings/whirled/zone/client/ZoneDecoder.java b/src/java/com/threerings/whirled/zone/client/ZoneDecoder.java new file mode 100644 index 00000000..647fe1e3 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/client/ZoneDecoder.java @@ -0,0 +1,69 @@ +// +// $Id: ZoneDecoder.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client; + +import com.threerings.presents.client.InvocationDecoder; +import com.threerings.whirled.zone.client.ZoneReceiver; + +/** + * Dispatches calls to a {@link ZoneReceiver} instance. + */ +public class ZoneDecoder extends InvocationDecoder +{ + /** The generated hash code used to identify this receiver class. */ + public static final String RECEIVER_CODE = "2d900cf54355111b4bb4befcdff42b82"; + + /** The method id used to dispatch {@link ZoneReceiver#forcedMove} + * notifications. */ + public static final int FORCED_MOVE = 1; + + /** + * Creates a decoder that may be registered to dispatch invocation + * service notifications to the specified receiver. + */ + public ZoneDecoder (ZoneReceiver receiver) + { + this.receiver = receiver; + } + + // documentation inherited + public String getReceiverCode () + { + return RECEIVER_CODE; + } + + // documentation inherited + public void dispatchNotification (int methodId, Object[] args) + { + switch (methodId) { + case FORCED_MOVE: + ((ZoneReceiver)receiver).forcedMove( + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue() + ); + return; + + default: + super.dispatchNotification(methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/whirled/zone/client/ZoneDirector.java b/src/java/com/threerings/whirled/zone/client/ZoneDirector.java new file mode 100644 index 00000000..eb6062db --- /dev/null +++ b/src/java/com/threerings/whirled/zone/client/ZoneDirector.java @@ -0,0 +1,315 @@ +// +// $Id: ZoneDirector.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client; + +import java.util.ArrayList; + +import com.samskivert.util.ResultListener; + +import com.threerings.presents.client.BasicDirector; +import com.threerings.presents.client.Client; + +import com.threerings.crowd.data.PlaceConfig; + +import com.threerings.whirled.client.SceneDirector; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.util.WhirledContext; + +import com.threerings.whirled.zone.Log; +import com.threerings.whirled.zone.data.ZoneSummary; +import com.threerings.whirled.zone.util.ZoneUtil; + +/** + * The zone director augments the scene services with the notion of zones. + * Zones are self-contained, connected groups of scenes. The normal scene + * director services can be used to move from scene to scene, but moving + * to a new zone requires a special move request which can be accomplished + * via the zone director. The zone director also makes available the zone + * summary which provides information on the zone which can be used to + * generate an overview map or similar. + */ +public class ZoneDirector extends BasicDirector + implements ZoneReceiver, ZoneService.ZoneMoveListener, + SceneDirector.MoveHandler +{ + /** + * Constructs a zone director with the supplied context, and delegate + * scene director (which the zone director will coordinate with when + * moving from scene to scene). A zone director is required on the + * client side for systems that wish to use the zone services. + */ + public ZoneDirector (WhirledContext ctx, SceneDirector scdir) + { + super(ctx); + _ctx = ctx; + _scdir = scdir; + _scdir.setMoveHandler(this); + + // register for zone notifications + _ctx.getClient().getInvocationDirector().registerReceiver( + new ZoneDecoder(this)); + } + + /** + * Returns the summary for the zone currently occupied by the client + * or null if the client does not currently occupy a zone (not a + * normal situation). + */ + public ZoneSummary getZoneSummary () + { + return _summary; + } + + /** + * Adds a zone observer to the list. This observer will subsequently + * be notified of effected and failed zone changes. + */ + public void addZoneObserver (ZoneObserver observer) + { + _observers.add(observer); + } + + /** + * Removes a zone observer from the list. + */ + public void removeZoneObserver (ZoneObserver observer) + { + _observers.remove(observer); + } + + /** + * Requests that this client move the specified scene in the specified + * zone. A request will be made and when the response is received, the + * location observers will be notified of success or failure. + */ + public boolean moveTo (int zoneId, int sceneId) + { + return moveTo(zoneId, sceneId, null); + } + + /** + * Requests that this client move the specified scene in the specified + * zone. A request will be made and when the response is received, the + * location observers will be notified of success or failure. + */ + public boolean moveTo (int zoneId, int sceneId, ResultListener rl) + { + // make sure the zoneId and sceneId are valid + if (zoneId < 0 || sceneId < 0) { + Log.warning("Refusing moveTo(): invalid sceneId or zoneId " + + "[zoneId=" + zoneId + ", sceneId=" + sceneId + "]."); + return false; + } + + // if the requested zone is the same as our current zone, we just + // want a regular old moveTo request + if (_summary != null && zoneId == _summary.zoneId) { + return _scdir.moveTo(sceneId); + } + + // otherwise, we make a zoned moveTo request; prepare to move to + // this scene (sets up pending data) + if (!_scdir.prepareMoveTo(sceneId, rl)) { + return false; + } + + // let our zone observers know that we're attempting to switch + // zones + notifyObservers(Integer.valueOf(zoneId)); + + // check the version of our cached copy of the scene to which + // we're requesting to move; if we were unable to load it, assume + // a cached version of zero + int sceneVers = 0; + SceneModel pendingModel = _scdir.getPendingModel(); + if (pendingModel != null) { + sceneVers = pendingModel.version; + } + + // issue a moveTo request + Log.info("Issuing zoned moveTo(" + ZoneUtil.toString(zoneId) + + ", " + sceneId + ", " + sceneVers + ")."); + _zservice.moveTo(_ctx.getClient(), zoneId, sceneId, sceneVers, this); + return true; + } + + // documentation inherited + protected void fetchServices (Client client) + { + _zservice = (ZoneService)client.requireService(ZoneService.class); + } + + // documentation inherited + public void clientDidLogoff (Client client) + { + super.clientDidLogoff(client); + + // clear out our business + _zservice = null; + _summary = null; + _previousZoneId = -1; + } + + /** + * Called in response to a successful {@link ZoneService#moveTo} + * request. + */ + public void moveSucceeded ( + int placeId, PlaceConfig config, ZoneSummary summary) + { + if (_summary != null) { + // keep track of our previous zone info + _previousZoneId = _summary.zoneId; + } + + // keep track of the summary + _summary = summary; + + // pass the rest off to the standard scene transition code + _scdir.moveSucceeded(placeId, config); + + // and let the zone observers know what's up + notifyObservers(summary); + } + + /** + * Called in response to a successful {@link ZoneService#moveTo} + * request when our cached scene was out of date and the server + * determined that we needed some updates. + */ + public void moveSucceededWithUpdates ( + int placeId, PlaceConfig config, ZoneSummary summary, + SceneUpdate[] updates) + { + // keep track of the summary + _summary = summary; + + // pass the rest off to the standard scene transition code + _scdir.moveSucceededWithUpdates(placeId, config, updates); + + // and let the zone observers know what's up + notifyObservers(summary); + } + + /** + * Called in response to a successful {@link ZoneService#moveTo} + * request when our cached scene was out of date and the server + * determined that we needed an updated copy. + */ + public void moveSucceededWithScene ( + int placeId, PlaceConfig config, ZoneSummary summary, SceneModel model) + { + // keep track of the summary + _summary = summary; + + // pass the rest off to the standard scene transition code + _scdir.moveSucceededWithScene(placeId, config, model); + + // and let the zone observers know what's up + notifyObservers(summary); + } + + /** + * Called in response to a failed zoned moveTo request. + */ + public void requestFailed (String reason) + { + // let the scene director cope + _scdir.requestFailed(reason); + + // and let the observers know what's up + notifyObservers(reason); + } + + // documentation inherited from interface + public void forcedMove (int zoneId, int sceneId) + { + Log.info("Moving at request of server [zoneId=" + zoneId + + ", sceneId=" + sceneId + "]."); + + // clear out our old scene and place data + _scdir.didLeaveScene(); + + // move to the new zone and scene + moveTo(zoneId, sceneId, null); + } + + /** + * Called when something breaks down after successfully completely a + * moveTo request. + */ + public void recoverMoveTo (int sceneId) + { + if (_previousZoneId != -1) { + moveTo(_previousZoneId, sceneId); + + } else { + _scdir.moveTo(sceneId); + } + } + + /** + * Notifies observers of success or failure, depending on the type of + * object provided as data. + */ + protected void notifyObservers (Object data) + { + // let our observers know that all is well on the western front + for (int i = 0; i < _observers.size(); i++) { + ZoneObserver obs = (ZoneObserver)_observers.get(i); + try { + if (data instanceof Integer) { + obs.zoneWillChange(((Integer)data).intValue()); + } else if (data instanceof ZoneSummary) { + obs.zoneDidChange((ZoneSummary)data); + } else { + obs.zoneChangeFailed((String)data); + } + + } catch (Throwable t) { + Log.warning("Zone observer choked during notification " + + "[data=" + data + ", obs=" + obs + "]."); + Log.logStackTrace(t); + } + } + } + + /** A reference to the active client context. */ + protected WhirledContext _ctx; + + /** A reference to the scene director with which we coordinate. */ + protected SceneDirector _scdir; + + /** Provides access to zone services. */ + protected ZoneService _zservice; + + /** A reference to the zone summary for the currently occupied + * zone. */ + protected ZoneSummary _summary; + + /** Our zone observer list. */ + protected ArrayList _observers = new ArrayList(); + + /** Our previous zone id. */ + protected int _previousZoneId = -1; +} diff --git a/src/java/com/threerings/whirled/zone/client/ZoneObserver.java b/src/java/com/threerings/whirled/zone/client/ZoneObserver.java new file mode 100644 index 00000000..0f77b864 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/client/ZoneObserver.java @@ -0,0 +1,58 @@ +// +// $Id: ZoneObserver.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client; + +import com.threerings.whirled.zone.data.ZoneSummary; + +/** + * The zone observer interface makes it possible for entities to be + * notified when the client moves to a new zone. + */ +public interface ZoneObserver +{ + /** + * Called when we begin the process of switching to a new zone. This + * will be followed by a call to {@link #zoneDidChange} to indicate + * that the change was successful or {@link #zoneChangeFailed} if the + * change fails. + * + * @param zoneId the zone id of the zone to which we are changing. + */ + public void zoneWillChange (int zoneId); + + /** + * Called when we have switched to a new zone. + * + * @param summary the summary information for the new zone or null if + * we have switched to no zone. + */ + public void zoneDidChange (ZoneSummary summary); + + /** + * This is called on all zone observers when a zone change request is + * rejected by the server or fails for some other reason. + * + * @param reason the reason code that explains why the zone change + * request was rejected or otherwise failed. + */ + public void zoneChangeFailed (String reason); +} diff --git a/src/java/com/threerings/whirled/zone/client/ZoneReceiver.java b/src/java/com/threerings/whirled/zone/client/ZoneReceiver.java new file mode 100644 index 00000000..f6ff74a5 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/client/ZoneReceiver.java @@ -0,0 +1,40 @@ +// +// $Id: ZoneReceiver.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client; + +import com.threerings.presents.client.InvocationReceiver; + +/** + * Defines, for the zone services, a set of notifications delivered + * asynchronously by the server to the client. + */ +public interface ZoneReceiver extends InvocationReceiver +{ + /** + * Used to communicate a required move notification to the client. The + * server will have removed the client from their existing scene and + * the client is then responsible for generating a {@link + * ZoneService#moveTo} request to move to the new scene in the + * specified zone. + */ + public void forcedMove (int zoneId, int sceneId); +} diff --git a/src/java/com/threerings/whirled/zone/client/ZoneService.java b/src/java/com/threerings/whirled/zone/client/ZoneService.java new file mode 100644 index 00000000..b1b3b7fd --- /dev/null +++ b/src/java/com/threerings/whirled/zone/client/ZoneService.java @@ -0,0 +1,67 @@ +// +// $Id: ZoneService.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.client; + +import com.threerings.presents.client.Client; +import com.threerings.presents.client.InvocationService; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; + +import com.threerings.whirled.zone.data.ZoneSummary; + +/** + * Defines the client interface to the zone related invocation services + * (e.g. moving between zones). + */ +public interface ZoneService extends InvocationService +{ + /** Used to deliver responses to {@link #moveTo} requests. */ + public static interface ZoneMoveListener extends InvocationListener + { + public void moveSucceeded ( + int placeId, PlaceConfig config, ZoneSummary summary); + + public void moveSucceededWithUpdates ( + int placeId, PlaceConfig config, ZoneSummary summary, + SceneUpdate[] updates); + + public void moveSucceededWithScene ( + int placeId, PlaceConfig config, ZoneSummary summary, + SceneModel model); + } + + /** + * Requests that that this client's body be moved to the specified + * scene in the specified zone. + * + * @param zoneId the zone id to which we want to move. + * @param sceneId the scene id to which we want to move. + * @param version the version number of the scene object that we have + * in our local repository. + * @param listener the object that will receive the callback when the + * request succeeds or fails. + */ + public void moveTo (Client client, int zoneId, int sceneId, + int version, ZoneMoveListener listener); +} diff --git a/src/java/com/threerings/whirled/zone/data/SceneSummary.java b/src/java/com/threerings/whirled/zone/data/SceneSummary.java new file mode 100644 index 00000000..566027d2 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/data/SceneSummary.java @@ -0,0 +1,58 @@ +// +// $Id: SceneSummary.java 3310 2005-01-24 23:08:21Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.data; + +import com.samskivert.util.StringUtil; + +import com.threerings.io.Streamable; + +/** + * The scene summary class is used to provide info about the connected + * group of scenes that make up a zone. The group of scenes that make up a + * zone is a self-contained set of scenes, connected with one another (by + * portals) but not to any scenes outside the group. + */ +public class SceneSummary implements Streamable +{ + /** The id of this scene. */ + public int sceneId; + + /** The name of this scene. */ + public String name; + + /** The ids of the scenes to which this scene is connected via + * portals. */ + public int[] neighbors; + + /** The directions in which each of the neighbors lay. */ + public int[] neighborDirs; + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + return "[sceneId=" + sceneId + ", name=" + name + + ", neighbors=" + StringUtil.toString(neighbors) + + ", neighborDirs=" + StringUtil.toString(neighborDirs) + "]"; + } +} diff --git a/src/java/com/threerings/whirled/zone/data/ZoneCodes.java b/src/java/com/threerings/whirled/zone/data/ZoneCodes.java new file mode 100644 index 00000000..ba6a0c71 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/data/ZoneCodes.java @@ -0,0 +1,35 @@ +// +// $Id: ZoneCodes.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.data; + +import com.threerings.whirled.data.SceneCodes; + +/** + * Contains codes used by the zone services. + */ +public interface ZoneCodes extends SceneCodes +{ + /** An error code indicating that a zone identified by a particular + * zone id does not exist. Usually generated by a failed moveTo + * request. */ + public static final String NO_SUCH_ZONE = "m.no_such_zone"; +} diff --git a/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java b/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java new file mode 100644 index 00000000..9371dbb7 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/data/ZoneMarshaller.java @@ -0,0 +1,125 @@ +// +// $Id: ZoneMarshaller.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.data; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.dobj.InvocationResponseEvent; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.zone.client.ZoneService; +import com.threerings.whirled.zone.data.ZoneSummary; + +/** + * Provides the implementation of the {@link ZoneService} interface + * that marshalls the arguments and delivers the request to the provider + * on the server. Also provides an implementation of the response listener + * interfaces that marshall the response arguments and deliver them back + * to the requesting client. + */ +public class ZoneMarshaller extends InvocationMarshaller + implements ZoneService +{ + // documentation inherited + public static class ZoneMoveMarshaller extends ListenerMarshaller + implements ZoneMoveListener + { + /** The method id used to dispatch {@link #moveSucceeded} + * responses. */ + public static final int MOVE_SUCCEEDED = 1; + + // documentation inherited from interface + public void moveSucceeded (int arg1, PlaceConfig arg2, ZoneSummary arg3) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, MOVE_SUCCEEDED, + new Object[] { Integer.valueOf(arg1), arg2, arg3 })); + } + + /** The method id used to dispatch {@link #moveSucceededWithScene} + * responses. */ + public static final int MOVE_SUCCEEDED_WITH_SCENE = 2; + + // documentation inherited from interface + public void moveSucceededWithScene (int arg1, PlaceConfig arg2, ZoneSummary arg3, SceneModel arg4) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, MOVE_SUCCEEDED_WITH_SCENE, + new Object[] { Integer.valueOf(arg1), arg2, arg3, arg4 })); + } + + /** The method id used to dispatch {@link #moveSucceededWithUpdates} + * responses. */ + public static final int MOVE_SUCCEEDED_WITH_UPDATES = 3; + + // documentation inherited from interface + public void moveSucceededWithUpdates (int arg1, PlaceConfig arg2, ZoneSummary arg3, SceneUpdate[] arg4) + { + _invId = null; + omgr.postEvent(new InvocationResponseEvent( + callerOid, requestId, MOVE_SUCCEEDED_WITH_UPDATES, + new Object[] { Integer.valueOf(arg1), arg2, arg3, arg4 })); + } + + // documentation inherited + public void dispatchResponse (int methodId, Object[] args) + { + switch (methodId) { + case MOVE_SUCCEEDED: + ((ZoneMoveListener)listener).moveSucceeded( + ((Integer)args[0]).intValue(), (PlaceConfig)args[1], (ZoneSummary)args[2]); + return; + + case MOVE_SUCCEEDED_WITH_SCENE: + ((ZoneMoveListener)listener).moveSucceededWithScene( + ((Integer)args[0]).intValue(), (PlaceConfig)args[1], (ZoneSummary)args[2], (SceneModel)args[3]); + return; + + case MOVE_SUCCEEDED_WITH_UPDATES: + ((ZoneMoveListener)listener).moveSucceededWithUpdates( + ((Integer)args[0]).intValue(), (PlaceConfig)args[1], (ZoneSummary)args[2], (SceneUpdate[])args[3]); + return; + + default: + super.dispatchResponse(methodId, args); + return; + } + } + } + + /** The method id used to dispatch {@link #moveTo} requests. */ + public static final int MOVE_TO = 1; + + // documentation inherited from interface + public void moveTo (Client arg1, int arg2, int arg3, int arg4, ZoneService.ZoneMoveListener arg5) + { + ZoneMarshaller.ZoneMoveMarshaller listener5 = new ZoneMarshaller.ZoneMoveMarshaller(); + listener5.listener = arg5; + sendRequest(arg1, MOVE_TO, new Object[] { + Integer.valueOf(arg2), Integer.valueOf(arg3), Integer.valueOf(arg4), listener5 + }); + } + +} diff --git a/src/java/com/threerings/whirled/zone/data/ZoneSummary.java b/src/java/com/threerings/whirled/zone/data/ZoneSummary.java new file mode 100644 index 00000000..6bd53ebd --- /dev/null +++ b/src/java/com/threerings/whirled/zone/data/ZoneSummary.java @@ -0,0 +1,53 @@ +// +// $Id: ZoneSummary.java 3726 2005-10-11 19:17:43Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.data; + +import com.samskivert.util.StringUtil; + +import com.threerings.io.SimpleStreamableObject; +import com.threerings.util.Name; + +/** + * The zone summary contains information on a zone, including its name and + * summary info on all of the scenes in this zone (which can be used to + * generate a map of the zone on the client). + */ +public class ZoneSummary extends SimpleStreamableObject +{ + /** The zone's fully qualified unique identifier. */ + public int zoneId; + + /** The name of the zone. */ + public Name name; + + /** The summary information for all of the scenes in the zone. */ + public SceneSummary[] scenes; + + /** + * Generates a string representation of this instance. + */ + public String toString () + { + return "[zoneId=" + zoneId + ", name=" + name + + ", scenes=" + StringUtil.toString(scenes) + "]"; + } +} diff --git a/src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java b/src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java new file mode 100644 index 00000000..9b0b97d2 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/data/ZonedBodyObject.java @@ -0,0 +1,41 @@ +// +// $Id: ZonedBodyObject.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.data; + +import com.threerings.whirled.data.ScenedBodyObject; + +/** + * A system that uses the zone services must provide a body object + * extension that implements this interface. + */ +public interface ZonedBodyObject extends ScenedBodyObject +{ + /** + * Returns the zone id currently occupied by this body. + */ + public int getZoneId (); + + /** + * Sets the zone id currently occupied by this body. + */ + public void setZoneId (int zoneId); +} diff --git a/src/java/com/threerings/whirled/zone/server/ZoneDispatcher.java b/src/java/com/threerings/whirled/zone/server/ZoneDispatcher.java new file mode 100644 index 00000000..d5585b8d --- /dev/null +++ b/src/java/com/threerings/whirled/zone/server/ZoneDispatcher.java @@ -0,0 +1,74 @@ +// +// $Id: ZoneDispatcher.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.server; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.presents.client.Client; +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationDispatcher; +import com.threerings.presents.server.InvocationException; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.zone.client.ZoneService; +import com.threerings.whirled.zone.data.ZoneMarshaller; +import com.threerings.whirled.zone.data.ZoneSummary; + +/** + * Dispatches requests to the {@link ZoneProvider}. + */ +public class ZoneDispatcher extends InvocationDispatcher +{ + /** + * Creates a dispatcher that may be registered to dispatch invocation + * service requests for the specified provider. + */ + public ZoneDispatcher (ZoneProvider provider) + { + this.provider = provider; + } + + // documentation inherited + public InvocationMarshaller createMarshaller () + { + return new ZoneMarshaller(); + } + + // documentation inherited + public void dispatchRequest ( + ClientObject source, int methodId, Object[] args) + throws InvocationException + { + switch (methodId) { + case ZoneMarshaller.MOVE_TO: + ((ZoneProvider)provider).moveTo( + source, + ((Integer)args[0]).intValue(), ((Integer)args[1]).intValue(), ((Integer)args[2]).intValue(), (ZoneService.ZoneMoveListener)args[3] + ); + return; + + default: + super.dispatchRequest(source, methodId, args); + return; + } + } +} diff --git a/src/java/com/threerings/whirled/zone/server/ZoneManager.java b/src/java/com/threerings/whirled/zone/server/ZoneManager.java new file mode 100644 index 00000000..1db8f452 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/server/ZoneManager.java @@ -0,0 +1,104 @@ +// +// $Id: ZoneManager.java 3350 2005-02-15 00:58:16Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.server; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.whirled.zone.data.ZoneSummary; +import com.threerings.whirled.zone.data.ZonedBodyObject; + +/** + * A zone is a collection of scenes organized into a connected group. A + * user can wander around within a zone, moving from scene to scene via + * the standard mechanisms. To move between zones, they must use a special + * mechanism (like at the dock, they can move from an island zone into + * their ship zone; or they can move from an island zone into their house + * zone). A zone provides scene summary information that can be used to + * display a map of the zone to the client. + */ +public interface ZoneManager +{ + /** + * Used to notify requesters when an asynchronous zone load has + * completed (successfully or not). + */ + public static interface ResolutionListener + { + /** + * Called when a zone was successfully resolved. + */ + public void zoneWasResolved (ZoneSummary summary); + + /** + * Called when a zone failed to resolve. + */ + public void zoneFailedToResolve (int zoneId, Exception reason); + } + + /** + * Resolves and delivers the scene summary information for the + * requested zone. Zone resolution is an asynchronous process, which + * necessitates this callback-style interface. + * + * @param zoneId the qualified zone id of the zone to resolve. + * @param listener the listener that should be notified when the zone + * is successfully resolved or is known to have failed to resolve. + */ + public void resolveZone (int zoneId, ResolutionListener listener); + + /** + * Called when a body has requested to leave a zone. The zone manager + * may return null to indicate that the body is allowed to leave the + * current zone or a string error code indicating the reason for + * denial of access (which will be propagated back to the requesting + * client). + * + * @param body the body object of the user that desires to depart + * their current zone (which can be obtained by casting the {@link + * BodyObject} to a {@link ZonedBodyObject}). + */ + public String ratifyBodyExit (BodyObject body); + + /** + * Called when a body has requested to enter a zone. The zone manager + * may return null to indicate that the body is allowed access to the + * zone or a string error code indicating the reason for denial of + * access (which will be propagated back to the requesting client). + * This method is called after the zone is resolved so that + * the zone manager may complete the ratification process without + * blocking (which it must do). + * + * @param body the body object of the user that desires access to the + * specified zone. + * @param zoneId the id of the zone to which the user desires access. + */ + public String ratifyBodyEntry (BodyObject body, int zoneId); + + /** + * Called when a body has been granted access to a zone. This method + * must not block. + * + * @param body the body object of the user that was just granted + * access to a zone. + * @param zoneId the id of the zone to which they were granted access. + */ + public void bodyDidEnterZone (BodyObject body, int zoneId); +} diff --git a/src/java/com/threerings/whirled/zone/server/ZoneProvider.java b/src/java/com/threerings/whirled/zone/server/ZoneProvider.java new file mode 100644 index 00000000..c7550788 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/server/ZoneProvider.java @@ -0,0 +1,303 @@ +// +// $Id: ZoneProvider.java 3860 2006-02-17 01:08:01Z mthomas $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.server; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.data.InvocationMarshaller; +import com.threerings.presents.server.InvocationException; +import com.threerings.presents.server.InvocationProvider; + +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.server.LocationProvider; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.data.SceneUpdate; +import com.threerings.whirled.data.ScenedBodyObject; +import com.threerings.whirled.server.SceneManager; +import com.threerings.whirled.server.SceneRegistry; + +import com.threerings.whirled.zone.Log; +import com.threerings.whirled.zone.client.ZoneService.ZoneMoveListener; +import com.threerings.whirled.zone.data.ZoneCodes; +import com.threerings.whirled.zone.data.ZoneSummary; +import com.threerings.whirled.zone.data.ZonedBodyObject; + +/** + * Provides zone related services which are presently the ability to move + * from zone to zone. + */ +public class ZoneProvider + implements ZoneCodes, InvocationProvider +{ + /** + * Constructs a zone provider that will interoperate with the supplied + * zone and scene registries. The zone provider will automatically be + * constructed and registered by the {@link ZoneRegistry}, which a + * zone-using system must create and initialize in their server. + */ + public ZoneProvider (LocationProvider locprov, ZoneRegistry zonereg, + SceneRegistry screg) + { + _locprov = locprov; + _zonereg = zonereg; + _screg = screg; + } + + /** + * Processes a request from a client to move to a scene in a new zone. + * + * @param caller the user requesting the move. + * @param zoneId the qualified zone id of the new zone. + * @param sceneId the identifier of the new scene. + * @param sceneVer the version of the scene model currently held by + * the client. + * @param listener the entity to inform of success or failure. + */ + public void moveTo (ClientObject caller, int zoneId, int sceneId, + int sceneVer, ZoneMoveListener listener) + throws InvocationException + { + // avoid cluttering up the method declaration with final keywords + final BodyObject fsource = (BodyObject)caller; + final int fsceneId = sceneId; + final int fsceneVer = sceneVer; + final ZoneMoveListener flistener = listener; + + // look up the caller's current zone id and make sure it is happy + // about their departure from the current zone + if (!(caller instanceof ZonedBodyObject)) { + Log.warning("Request to switch zones by non-ZonedBodyObject!? " + + "[clobj=" + caller.getClass() + "]."); + throw new InvocationException(INTERNAL_ERROR); + } + ZonedBodyObject zcaller = (ZonedBodyObject)caller; + ZoneManager ozmgr = _zonereg.getZoneManager(zcaller.getZoneId()); + if (ozmgr != null) { + String msg = ozmgr.ratifyBodyExit(fsource); + if (msg != null) { + throw new InvocationException(msg); + } + } + + // look up the zone manager for the zone + ZoneManager zmgr = _zonereg.getZoneManager(zoneId); + if (zmgr == null) { + Log.warning("Requested to enter a zone for which we have no " + + "manager [user=" + fsource.who() + + ", zoneId=" + zoneId + "]."); + throw new InvocationException(NO_SUCH_ZONE); + } + + // resolve the zone! + ZoneManager.ResolutionListener zl = new ZoneManager.ResolutionListener() + { + public void zoneWasResolved (ZoneSummary summary) { + continueMoveTo( + fsource, summary, fsceneId, fsceneVer, flistener); + } + + public void zoneFailedToResolve (int zoneId, Exception reason) { + Log.warning("Unable to resolve zone [zoneId=" + zoneId + + ", reason=" + reason + "]."); + flistener.requestFailed(NO_SUCH_ZONE); + } + }; + zmgr.resolveZone(zoneId, zl); + } + + /** + * This is called after we have resolved our zone. + */ + protected void continueMoveTo ( + BodyObject source, ZoneSummary summary, int sceneId, int sceneVer, + ZoneMoveListener listener) + { + // avoid cluttering up the method declaration with final keywords + final BodyObject fsource = source; + final ZoneSummary fsum = summary; + final int fsceneVer = sceneVer; + final ZoneMoveListener flistener = listener; + + // give the zone manager a chance to veto the request + ZoneManager zmgr = _zonereg.getZoneManager(summary.zoneId); + String errmsg = zmgr.ratifyBodyEntry(source, summary.zoneId); + if (errmsg != null) { + listener.requestFailed(errmsg); + return; + } + + // create a callback object that will handle the resolution or + // failed resolution of the scene + SceneRegistry.ResolutionListener rl = null; + rl = new SceneRegistry.ResolutionListener() { + public void sceneWasResolved (SceneManager scmgr) { + // make sure our caller is still around; under heavy load, + // clients might end their session while the scene is + // resolving + if (!fsource.isActive()) { + Log.info("Abandoning zone move, client gone " + + "[who=" + fsource.who() + + ", dest=" + scmgr.where() + "]."); + // No one to respond to, just mark that we're okay with it. + InvocationMarshaller.setNoResponse(flistener); + return; + } + finishMoveTo(fsource, fsum, scmgr, fsceneVer, flistener); + } + + public void sceneFailedToResolve (int sceneId, Exception reason) { + Log.warning("Unable to resolve scene [sceneid=" + sceneId + + ", reason=" + reason + "]."); + // pretend like the scene doesn't exist to the client + flistener.requestFailed(NO_SUCH_PLACE); + } + }; + + // make sure the scene they are headed to is actually loaded into + // the server + _screg.resolveScene(sceneId, rl); + } + + /** + * This is called after the scene to which we are moving is guaranteed + * to have been loaded into the server. + */ + protected void finishMoveTo ( + BodyObject source, ZoneSummary summary, SceneManager scmgr, + int sceneVersion, ZoneMoveListener listener) + { + // move to the place object associated with this scene + PlaceObject plobj = scmgr.getPlaceObject(); + int ploid = plobj.getOid(); + + try { + // try doing the actual move + PlaceConfig config = _locprov.moveTo(source, ploid); + + // now that we've finally moved, we can update the user object + // with the new scene and zone ids + source.startTransaction(); + try { + ((ScenedBodyObject)source).setSceneId(scmgr.getScene().getId()); + ((ZonedBodyObject)source).setZoneId(summary.zoneId); + } finally { + source.commitTransaction(); + } + + // check to see if they need a newer version of the scene data + SceneModel model = scmgr.getScene().getSceneModel(); + if (sceneVersion < model.version) { + SceneUpdate[] updates = scmgr.getUpdates(sceneVersion); + if (updates != null) { + listener.moveSucceededWithUpdates( + ploid, config, summary, updates); + } else { + listener.moveSucceededWithScene( + ploid, config, summary, model); + } + + } else { + // then send the moveTo response + listener.moveSucceeded(ploid, config, summary); + } + + // let the zone manager know that someone just came on in + ZoneManager zmgr = _zonereg.getZoneManager(summary.zoneId); + zmgr.bodyDidEnterZone(source, summary.zoneId); + + } catch (InvocationException ie) { + listener.requestFailed(ie.getMessage()); + + } catch (RuntimeException re) { + Log.logStackTrace(re); + listener.requestFailed(INTERNAL_ERROR); + } + } + + /** + * Ejects the specified body from their current scene and sends them a + * request to move to the specified new zone and scene. This is the + * zone-equivalent to {@link LocationProvider#moveBody}. + * + * @return null if the user was forcibly moved, or a string indicating + * the reason for denial of departure of their current zone (from + * {@link ZoneManager#ratifyBodyExit}). + */ + public String moveBody (ZonedBodyObject source, int zoneId, int sceneId) + { + if (source.getZoneId() == zoneId) { + // handle the case of moving somewhere in the same zone + _screg.sceneprov.moveBody((BodyObject) source, sceneId); + + } else { + // first remove them from their old location + String reason = leaveOccupiedZone(source); + if (reason != null) { + return reason; + } + + // then send a forced move notification + ZoneSender.forcedMove((BodyObject)source, zoneId, sceneId); + } + return null; + } + + /** + * Ejects the specified body from their current scene and zone. This + * is the zone equivalent to {@link + * LocationProvider#leaveOccupiedPlace}. + * + * @return null if the user was forcibly moved, or a string indicating + * the reason for denial of departure of their current zone (from + * {@link ZoneManager#ratifyBodyExit}). + */ + public String leaveOccupiedZone (ZonedBodyObject source) + { + // look up the caller's current zone id and make sure it is happy + // about their departure from the current zone + ZoneManager zmgr = _zonereg.getZoneManager(source.getZoneId()); + String msg; + if (zmgr != null && + (msg = zmgr.ratifyBodyExit((BodyObject)source)) != null) { + return msg; + } + + // remove them from their occupied scene + _screg.sceneprov.leaveOccupiedScene(source); + + // and clear out their zone information + source.setZoneId(-1); + + return null; + } + + /** The entity that handles basic location changes. */ + protected LocationProvider _locprov; + + /** The zone registry with which we communicate. */ + protected ZoneRegistry _zonereg; + + /** The scene registry with which we communicate. */ + protected SceneRegistry _screg; +} diff --git a/src/java/com/threerings/whirled/zone/server/ZoneRegistry.java b/src/java/com/threerings/whirled/zone/server/ZoneRegistry.java new file mode 100644 index 00000000..2b747588 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/server/ZoneRegistry.java @@ -0,0 +1,88 @@ +// +// $Id: ZoneRegistry.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.server; + +import com.samskivert.util.HashIntMap; + +import com.threerings.presents.server.InvocationManager; + +import com.threerings.crowd.server.PlaceRegistry; +import com.threerings.whirled.server.SceneRegistry; + +import com.threerings.whirled.zone.Log; +import com.threerings.whirled.zone.util.ZoneUtil; + +/** + * The zone registry takes care of mapping zone requests to the + * appropriate registered zone manager. + */ +public class ZoneRegistry +{ + /** Implements the server-side of the zone-related services. */ + public ZoneProvider zoneprov; + + /** + * Creates a zone manager with the supplied configuration. + */ + public ZoneRegistry (InvocationManager invmgr, PlaceRegistry plreg, + SceneRegistry screg) + { + // create a zone provider and register it with the invocation + // services + zoneprov = new ZoneProvider(plreg.locprov, this, screg); + invmgr.registerDispatcher(new ZoneDispatcher(zoneprov), true); + } + + /** + * Registers the supplied zone manager as the manager for the + * specified zone type. Zone types are 7 bits and managers are + * responsible for making sure they don't use a zone type that + * collides with another manager (given that we have only three zone + * types at present, this doesn't seem unreasonable). + */ + public void registerZoneManager (byte zoneType, ZoneManager manager) + { + ZoneManager old = (ZoneManager)_managers.get(zoneType); + if (old != null) { + Log.warning("Zone manager already registered with requested " + + "type [type=" + zoneType + ", old=" + old + + ", new=" + manager + "]."); + } else { + _managers.put(zoneType, manager); + } + } + + /** + * Returns the zone manager that handles the specified zone id. + * + * @param qualifiedZoneId the qualified zone id for which the manager + * should be looked up. + */ + public ZoneManager getZoneManager (int qualifiedZoneId) + { + int zoneType = ZoneUtil.zoneType(qualifiedZoneId); + return (ZoneManager)_managers.get(zoneType); + } + + /** A table of zone managers. */ + protected HashIntMap _managers = new HashIntMap(); +} diff --git a/src/java/com/threerings/whirled/zone/server/ZoneSender.java b/src/java/com/threerings/whirled/zone/server/ZoneSender.java new file mode 100644 index 00000000..121cd715 --- /dev/null +++ b/src/java/com/threerings/whirled/zone/server/ZoneSender.java @@ -0,0 +1,47 @@ +// +// $Id: ZoneSender.java 4145 2006-05-24 01:24:24Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2006 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.server; + +import com.threerings.presents.data.ClientObject; +import com.threerings.presents.server.InvocationSender; +import com.threerings.whirled.zone.client.ZoneDecoder; +import com.threerings.whirled.zone.client.ZoneReceiver; + +/** + * Used to issue notifications to a {@link ZoneReceiver} instance on a + * client. + */ +public class ZoneSender extends InvocationSender +{ + /** + * Issues a notification that will result in a call to {@link + * ZoneReceiver#forcedMove} on a client. + */ + public static void forcedMove ( + ClientObject target, int arg1, int arg2) + { + sendNotification( + target, ZoneDecoder.RECEIVER_CODE, ZoneDecoder.FORCED_MOVE, + new Object[] { Integer.valueOf(arg1), Integer.valueOf(arg2) }); + } + +} diff --git a/src/java/com/threerings/whirled/zone/util/ZoneUtil.java b/src/java/com/threerings/whirled/zone/util/ZoneUtil.java new file mode 100644 index 00000000..cf0f761c --- /dev/null +++ b/src/java/com/threerings/whirled/zone/util/ZoneUtil.java @@ -0,0 +1,66 @@ +// +// $Id: ZoneUtil.java 4191 2006-06-13 22:42:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.zone.util; + +/** + * Server-specific, zone-related utility functions. + */ +public class ZoneUtil +{ + /** + * Composes the zone type and zone id into a qualified zone id. A + * qualified zone id is what should be passed around so that the + * server can determine the zone type from the zone id when necessary. + */ + public static int qualifyZoneId (byte zoneType, int zoneId) + { + int qualifiedZoneId = zoneType; + qualifiedZoneId <<= 24; + qualifiedZoneId |= zoneId; + return qualifiedZoneId; + } + + /** + * Extracts the zone type from a qualified zone id. + */ + public static int zoneType (int qualifiedZoneId) + { + return (0xFF000000 & qualifiedZoneId) >> 24; + } + + /** + * Extracts the zone id from a qualified zone id. + */ + public static int zoneId (int qualifiedZoneId) + { + return (0x00FFFFFF & qualifiedZoneId); + } + + /** + * Returns an easier to read representation of the supplied qualified + * zone id: type:id. + */ + public static String toString (int qualifiedZoneId) + { + return zoneType(qualifiedZoneId) + ":" + zoneId(qualifiedZoneId); + } +} diff --git a/tests/build.xml b/tests/build.xml new file mode 100644 index 00000000..1124c595 --- /dev/null +++ b/tests/build.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/rsrc/whirled/spot/tools/xml/scene.xml b/tests/rsrc/whirled/spot/tools/xml/scene.xml new file mode 100644 index 00000000..007d5da7 --- /dev/null +++ b/tests/rsrc/whirled/spot/tools/xml/scene.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/tests/rsrc/whirled/tools/xml/scene.xml b/tests/rsrc/whirled/tools/xml/scene.xml new file mode 100644 index 00000000..542b407f --- /dev/null +++ b/tests/rsrc/whirled/tools/xml/scene.xml @@ -0,0 +1,8 @@ + + + + + Some neighbor + Some other neighbor + Yet another neighbor + diff --git a/tests/src/java/com/threerings/parlor/TestClient.java b/tests/src/java/com/threerings/parlor/TestClient.java new file mode 100644 index 00000000..33d51dad --- /dev/null +++ b/tests/src/java/com/threerings/parlor/TestClient.java @@ -0,0 +1,127 @@ +// +// $Id: TestClient.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor; + +import com.threerings.util.Name; + +import com.threerings.presents.client.*; +import com.threerings.presents.net.*; + +import com.threerings.crowd.client.*; +import com.threerings.crowd.data.BodyObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.parlor.Log; +import com.threerings.parlor.client.*; +import com.threerings.parlor.game.client.*; +import com.threerings.parlor.game.data.*; +import com.threerings.parlor.util.ParlorContext; + +public class TestClient extends com.threerings.crowd.client.TestClient + implements InvitationHandler, InvitationResponseObserver +{ + public TestClient (String username) + { + super(username); + + // create the handles on our various services + _pardtr = new ParlorDirector(_ctx); + + // register ourselves as the invitation handler + _pardtr.setInvitationHandler(this); + } + + public void clientDidLogon (Client client) + { + // we intentionally don't call super() + + Log.info("Client did logon [client=" + client + "]."); + + // get a casted reference to our body object + _body = (BodyObject)client.getClientObject(); + + // if we're the inviter, do some inviting + if (_body.username.equals("inviter")) { + // send the invitation + TestConfig config = new TestConfig(); + _pardtr.invite(new Name("invitee"), config, this); + } + } + + public void invitationReceived (Invitation invite) + { + Log.info("Invitation received [invite=" + invite + "]."); + + // accept the invitation. we're game... + invite.accept(); + } + + public void invitationCancelled (Invitation invite) + { + Log.info("Invitation cancelled [invite=" + invite + "]."); + } + + public void invitationAccepted (Invitation invite) + { + Log.info("Invitation accepted [invite=" + invite + "]."); + } + + public void invitationRefused (Invitation invite, String message) + { + Log.info("Invitation refused [invite=" + invite + + ", message=" + message + "]."); + } + + public void invitationCountered (Invitation invite, GameConfig config) + { + Log.info("Invitation countered [invite=" + invite + + ", config=" + config + "]."); + } + + protected CrowdContext createContext () + { + return (_ctx = new ParlorContextImpl()); + } + + public static void main (String[] args) + { + // create our test client + TestClient tclient = new TestClient( + System.getProperty("username", "test")); + // start it running + tclient.run(); + } + + protected class ParlorContextImpl + extends com.threerings.crowd.client.TestClient.CrowdContextImpl + implements ParlorContext + { + public ParlorDirector getParlorDirector () + { + return _pardtr; + } + } + + protected ParlorContext _ctx; + protected ParlorDirector _pardtr; + protected BodyObject _body; +} diff --git a/tests/src/java/com/threerings/parlor/TestConfig.java b/tests/src/java/com/threerings/parlor/TestConfig.java new file mode 100644 index 00000000..ff2212ec --- /dev/null +++ b/tests/src/java/com/threerings/parlor/TestConfig.java @@ -0,0 +1,57 @@ +// +// $Id: TestConfig.java 4193 2006-06-13 23:06:48Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor; + +import com.threerings.parlor.game.client.GameConfigurator; +import com.threerings.parlor.game.data.GameConfig; + +public class TestConfig extends GameConfig +{ + /** The foozle parameter. */ + public int foozle; + + public GameConfigurator createConfigurator () + { + return null; + } + + public Class getControllerClass () + { + return TestController.class; + } + + public String getBundleName () + { + return "test"; + } + + public String getManagerClassName () + { + return "com.threerings.parlor.test.TestManager"; + } + + protected void toString (StringBuilder buf) + { + super.toString(buf); + buf.append(", foozle=").append(foozle); + } +} diff --git a/tests/src/java/com/threerings/parlor/TestController.java b/tests/src/java/com/threerings/parlor/TestController.java new file mode 100644 index 00000000..2661fcd4 --- /dev/null +++ b/tests/src/java/com/threerings/parlor/TestController.java @@ -0,0 +1,35 @@ +// +// $Id: TestController.java 3440 2005-03-30 01:09:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor; + +import com.threerings.crowd.client.PlaceView; +import com.threerings.crowd.util.CrowdContext; +import com.threerings.parlor.game.client.GameController; + +public class TestController extends GameController +{ + public PlaceView createPlaceView (CrowdContext ctx) + { + // nothing doing + return null; + } +} diff --git a/tests/src/java/com/threerings/parlor/TestManager.java b/tests/src/java/com/threerings/parlor/TestManager.java new file mode 100644 index 00000000..83e596c9 --- /dev/null +++ b/tests/src/java/com/threerings/parlor/TestManager.java @@ -0,0 +1,29 @@ +// +// $Id: TestManager.java 3381 2005-03-03 19:36:34Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor; + +import com.threerings.parlor.game.server.GameManager; + +public class TestManager extends GameManager +{ + // nothing doing +} diff --git a/tests/src/java/com/threerings/parlor/TestServer.java b/tests/src/java/com/threerings/parlor/TestServer.java new file mode 100644 index 00000000..cef5ba14 --- /dev/null +++ b/tests/src/java/com/threerings/parlor/TestServer.java @@ -0,0 +1,61 @@ +// +// $Id: TestServer.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.parlor; + +import com.threerings.crowd.server.CrowdServer; + +import com.threerings.parlor.Log; +import com.threerings.parlor.server.ParlorManager; + +/** + * A test server for the Parlor services. + */ +public class TestServer extends CrowdServer +{ + /** The parlor manager in operation on this server. */ + public static ParlorManager parmgr = new ParlorManager(); + + /** Initializes the Parlor test server. */ + public void init () + throws Exception + { + super.init(); + + // initialize our parlor manager + parmgr.init(invmgr, plreg); + + Log.info("Parlor server initialized."); + } + + /** Main entry point for test server. */ + public static void main (String[] args) + { + TestServer server = new TestServer(); + try { + server.init(); + server.run(); + } catch (Exception e) { + Log.warning("Unable to initialize server."); + Log.logStackTrace(e); + } + } +} diff --git a/tests/src/java/com/threerings/whirled/DummyClientSceneRepository.java b/tests/src/java/com/threerings/whirled/DummyClientSceneRepository.java new file mode 100644 index 00000000..0aab9769 --- /dev/null +++ b/tests/src/java/com/threerings/whirled/DummyClientSceneRepository.java @@ -0,0 +1,59 @@ +// +// $Id: DummyClientSceneRepository.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled; + +import java.io.IOException; + +import com.threerings.whirled.Log; +import com.threerings.whirled.client.persist.SceneRepository; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.util.NoSuchSceneException; + +/** + * The dummy scene repository just pretends to load and store scenes, but + * in fact it just creates new blank scenes when requested to load a scene + * and does nothing when requested to save one. + */ +public class DummyClientSceneRepository implements SceneRepository +{ + // documentation inherited + public SceneModel loadSceneModel (int sceneId) + throws IOException, NoSuchSceneException + { + Log.info("Creating dummy scene model [id=" + sceneId + "]."); + return new SceneModel(); + } + + // documentation inherited + public void storeSceneModel (SceneModel model) + throws IOException + { + // nothing doing + } + + // documentation inherited + public void deleteSceneModel (int sceneId) + throws IOException + { + // nothing doing + } +} diff --git a/tests/src/java/com/threerings/whirled/TestClient.java b/tests/src/java/com/threerings/whirled/TestClient.java new file mode 100644 index 00000000..0b0ad74e --- /dev/null +++ b/tests/src/java/com/threerings/whirled/TestClient.java @@ -0,0 +1,115 @@ +// +// $Id: TestClient.java 3355 2005-02-17 01:54:54Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled; + +import com.threerings.presents.client.Client; + +import com.threerings.crowd.Log; +import com.threerings.crowd.client.*; +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.crowd.data.PlaceObject; +import com.threerings.crowd.util.CrowdContext; + +import com.threerings.whirled.client.SceneDirector; +import com.threerings.whirled.client.persist.SceneRepository; +import com.threerings.whirled.data.Scene; +import com.threerings.whirled.data.SceneImpl; +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.util.SceneFactory; +import com.threerings.whirled.util.WhirledContext; + +public class TestClient extends com.threerings.crowd.client.TestClient + implements LocationObserver +{ + public TestClient (String username) + { + super(username); + + // create the handles for our various services + _screp = new DummyClientSceneRepository(); + + SceneFactory sfact = new SceneFactory() { + public Scene createScene (SceneModel model, PlaceConfig config) { + return new SceneImpl(model, config); + } + }; + _scdir = new SceneDirector(_ctx, _locdir, _screp, sfact); + + // we want to know about location changes + _locdir.addLocationObserver(this); + } + + public void clientDidLogon (Client client) + { + // we specifically do not call super() + + Log.info("Client did logon [client=" + client + "]."); + + // request to move to scene 0 + _ctx.getSceneDirector().moveTo(0); + } + + public boolean locationMayChange (int placeId) + { + // we're easy + return true; + } + + public void locationDidChange (PlaceObject place) + { + Log.info("At new location [plobj=" + place + + ", scene=" + _scdir.getScene() + "]."); + } + + public void locationChangeFailed (int placeId, String reason) + { + Log.warning("Location change failed [plid=" + placeId + + ", reason=" + reason + "]."); + } + + protected CrowdContext createContext () + { + return (_ctx = new WhirledContextImpl()); + } + + public static void main (String[] args) + { + // create our test client + TestClient tclient = new TestClient("test"); + // start it running + tclient.run(); + } + + protected class WhirledContextImpl + extends com.threerings.crowd.client.TestClient.CrowdContextImpl + implements WhirledContext + { + public SceneDirector getSceneDirector () + { + return _scdir; + } + } + + protected WhirledContext _ctx; + protected SceneDirector _scdir; + protected SceneRepository _screp; +} diff --git a/tests/src/java/com/threerings/whirled/TestConfig.java b/tests/src/java/com/threerings/whirled/TestConfig.java new file mode 100644 index 00000000..d420d796 --- /dev/null +++ b/tests/src/java/com/threerings/whirled/TestConfig.java @@ -0,0 +1,38 @@ +// +// $Id: TestConfig.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled; + +import com.threerings.crowd.data.PlaceConfig; +import com.threerings.whirled.server.SceneManager; + +public class TestConfig extends PlaceConfig +{ + public Class getControllerClass () + { + return TestController.class; + } + + public String getManagerClassName () + { + return SceneManager.class.getName(); + } +} diff --git a/tests/src/java/com/threerings/whirled/TestController.java b/tests/src/java/com/threerings/whirled/TestController.java new file mode 100644 index 00000000..b1721183 --- /dev/null +++ b/tests/src/java/com/threerings/whirled/TestController.java @@ -0,0 +1,34 @@ +// +// $Id: TestController.java 3440 2005-03-30 01:09:30Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled; + +import com.threerings.crowd.client.*; +import com.threerings.crowd.util.CrowdContext; + +public class TestController extends PlaceController +{ + protected PlaceView createPlaceView (CrowdContext ctx) + { + // nothing doing + return null; + } +} diff --git a/tests/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneParserTest.java b/tests/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneParserTest.java new file mode 100644 index 00000000..8cdb133e --- /dev/null +++ b/tests/src/java/com/threerings/whirled/spot/tools/xml/SpotSceneParserTest.java @@ -0,0 +1,74 @@ +// +// $Id: SpotSceneParserTest.java 4143 2006-05-23 23:22:20Z ray $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.spot.tools.xml; + +import com.samskivert.test.TestUtil; + +import junit.framework.Test; +import junit.framework.TestCase; + +import com.threerings.whirled.data.SceneModel; +import com.threerings.whirled.spot.data.Location; +import com.threerings.whirled.tools.xml.SceneParser; + +import com.threerings.stage.data.StageLocation; + +public class SpotSceneParserTest extends TestCase +{ + public SpotSceneParserTest () + { + super(SpotSceneParserTest.class.getName()); + } + + public void runTest () + { + try { + SceneParser parser = new SceneParser("scene"); + parser.registerAuxRuleSet(new SpotSceneRuleSet() { + protected Location createLocation () { + return new StageLocation(); // breaks package, but ok + } + }); + String tspath = TestUtil.getResourcePath(TEST_SCENE_PATH); + SceneModel scene = parser.parseScene(tspath); + System.out.println("Parsed " + scene + "."); + + } catch (Exception e) { + e.printStackTrace(); + fail("Test threw exception"); + } + } + + public static Test suite () + { + return new SpotSceneParserTest(); + } + + public static void main (String[] args) + { + SpotSceneParserTest test = new SpotSceneParserTest(); + test.runTest(); + } + + protected static final String TEST_SCENE_PATH = + "rsrc/whirled/spot/tools/xml/scene.xml"; +} diff --git a/tests/src/java/com/threerings/whirled/tools/xml/SceneParserTest.java b/tests/src/java/com/threerings/whirled/tools/xml/SceneParserTest.java new file mode 100644 index 00000000..a8575f6e --- /dev/null +++ b/tests/src/java/com/threerings/whirled/tools/xml/SceneParserTest.java @@ -0,0 +1,65 @@ +// +// $Id: SceneParserTest.java 3099 2004-08-27 02:21:06Z mdb $ +// +// Narya library - tools for developing networked games +// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved +// http://www.threerings.net/code/narya/ +// +// This library is free software; you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as published +// by the Free Software Foundation; either version 2.1 of the License, or +// (at your option) any later version. +// +// This library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public +// License along with this library; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +package com.threerings.whirled.tools.xml; + +import com.samskivert.test.TestUtil; + +import junit.framework.Test; +import junit.framework.TestCase; + +import com.threerings.whirled.data.SceneModel; + +public class SceneParserTest extends TestCase +{ + public SceneParserTest () + { + super(SceneParserTest.class.getName()); + } + + public void runTest () + { + try { + SceneParser parser = new SceneParser("scene"); + String tspath = TestUtil.getResourcePath(TEST_SCENE_PATH); + SceneModel scene = parser.parseScene(tspath); + System.out.println("Parsed " + scene + "."); + + } catch (Exception e) { + e.printStackTrace(); + fail("Test threw exception"); + } + } + + public static Test suite () + { + return new SceneParserTest(); + } + + public static void main (String[] args) + { + SceneParserTest test = new SceneParserTest(); + test.runTest(); + } + + protected static final String TEST_SCENE_PATH = + "rsrc/whirled/tools/xml/scene.xml"; +}