From 6aaf7a1e751e46e5334379c4411f005e71f41df5 Mon Sep 17 00:00:00 2001 From: Jens Ayton Date: Sat, 29 Aug 2009 23:09:22 +0000 Subject: [PATCH] Committed unspeakable necromancy on the graveyard of the repository. git-svn-id: http://svn.berlios.de/svnroot/repos/oolite-linux/trunk@2331 127b21dd-08f5-0310-b4b7-95ae10353056 --- GNUmakefile | 277 + Oolite.xcodeproj/project.pbxproj | 3894 ++++++++ Schemata/README.txt | 6 + Schemata/demoshipsSchema.plist | 12 + Schemata/hudSchema.plist | 134 + Schemata/plistschema.plist | 434 + Schemata/shipdataEntrySchema.plist | 499 + Schemata/shipyardSchema.plist | 73 + debian/apply-patches | 36 + debian/changelog | 35 + debian/compat | 1 + debian/control | 37 + debian/copyright | 6 + debian/extra/games/oolite | 2 + debian/oolite-data.dirs | 1 + debian/oolite-data.docs | 7 + debian/oolite-data.install | 1 + debian/oolite-logo1.xpm | 219 + debian/oolite.6 | 79 + debian/oolite.dirs | 4 + debian/oolite.install | 7 + debian/oolite.links | 3 + debian/oolite.menu | 5 + debian/rules | 103 + installers/win32/Install.ico | Bin 0 -> 22486 bytes installers/win32/Jupiter Europa.ico | Bin 0 -> 2238 bytes installers/win32/OOlite.nsi | 192 + installers/win32/OOlite_Update.nsi | 53 + installers/win32/Oolite.ico | Bin 0 -> 84070 bytes .../win32/OoliteInstallerHeaderBitmap.bmp | Bin 0 -> 24374 bytes .../OoliteInstallerHeaderBitmap_ModernUI.bmp | Bin 0 -> 9742 bytes installers/win32/OoliteRS.pdf | Bin 0 -> 755175 bytes installers/win32/Oolite_Readme.txt | 300 + installers/win32/RunOolite.bat | 9 + installers/win32/Uninstall.ico | Bin 0 -> 22486 bytes src/BSDCompat/bsd_string.h | 7 + src/BSDCompat/strlcpy.c | 56 + src/Cocoa/Comparison.h | 1 + src/Cocoa/Groolite.h | 60 + src/Cocoa/Groolite.m | 254 + src/Cocoa/Info-Oolite.plist | 114 + src/Cocoa/Info-OoliteDev.plist | 113 + src/Cocoa/JoystickHandler.h | 152 + src/Cocoa/JoystickHandler.m | 98 + src/Cocoa/MyOpenGLView.h | 200 + src/Cocoa/MyOpenGLView.m | 709 ++ src/Cocoa/OOCABufferedSound.h | 70 + src/Cocoa/OOCABufferedSound.m | 217 + src/Cocoa/OOCAMusic.h | 64 + src/Cocoa/OOCAMusic.m | 132 + src/Cocoa/OOCASound.h | 68 + src/Cocoa/OOCASound.m | 305 + src/Cocoa/OOCASoundChannel.h | 110 + src/Cocoa/OOCASoundChannel.m | 723 ++ src/Cocoa/OOCASoundDecoder.h | 81 + src/Cocoa/OOCASoundDecoder.m | 524 + src/Cocoa/OOCASoundInternal.h | 137 + src/Cocoa/OOCASoundMixer.h | 102 + src/Cocoa/OOCASoundMixer.m | 381 + src/Cocoa/OOCASoundReferencePoint.h | 63 + src/Cocoa/OOCASoundReferencePoint.m | 70 + src/Cocoa/OOCAStreamingSound.h | 66 + src/Cocoa/OOCAStreamingSound.m | 405 + src/Cocoa/OOErrorDescription.h | 23 + src/Cocoa/OOErrorDescription.m | 257 + src/Cocoa/OOProgressBar.h | 43 + src/Cocoa/OOProgressBar.m | 73 + src/Cocoa/OoliteApp.h | 44 + src/Cocoa/OoliteApp.m | 58 + src/Cocoa/SoundInspector.nib/classes.nib | 18 + src/Cocoa/SoundInspector.nib/info.nib | 20 + src/Cocoa/SoundInspector.nib/keyedobjects.nib | Bin 0 -> 9658 bytes src/Cocoa/VirtualRingBuffer.h | 87 + src/Cocoa/VirtualRingBuffer.m | 308 + src/Cocoa/debug-exports-64.exp | 67 + src/Cocoa/debug-exports.exp | 82 + src/Cocoa/main.m | 15 + src/Cocoa/oolite-options.xcconfig | 10 + src/Cocoa/oolite-version.xcconfig | 1 + src/Core/AI.h | 115 + src/Core/AI.m | 833 ++ src/Core/CollisionRegion.h | 90 + src/Core/CollisionRegion.m | 596 ++ src/Core/Debug/OODebugMonitor.h | 104 + src/Core/Debug/OODebugMonitor.m | 622 ++ src/Core/Debug/OODebugSupport.h | 59 + src/Core/Debug/OODebugSupport.m | 155 + src/Core/Debug/OODebugTCPConsoleClient.h | 60 + src/Core/Debug/OODebugTCPConsoleClient.m | 710 ++ src/Core/Debug/OODebugTCPConsoleProtocol.h | 331 + src/Core/Debug/OODebuggerInterface.h | 70 + src/Core/Debug/OOJSConsole.h | 38 + src/Core/Debug/OOJSConsole.m | 487 + src/Core/Debug/OOTCPStreamDecoder.c | 266 + src/Core/Debug/OOTCPStreamDecoder.h | 72 + .../OOTCPStreamDecoderAbstractionLayer.h | 122 + .../OOTCPStreamDecoderAbstractionLayer.m | 127 + src/Core/Entities/DustEntity.h | 45 + src/Core/Entities/DustEntity.m | 214 + src/Core/Entities/Entity.h | 278 + src/Core/Entities/Entity.m | 1108 +++ src/Core/Entities/OOEntityWithDrawable.h | 40 + src/Core/Entities/OOEntityWithDrawable.m | 91 + src/Core/Entities/OOSelfDrawingEntity.h | 104 + src/Core/Entities/OOSelfDrawingEntity.m | 1163 +++ src/Core/Entities/ParticleEntity.h | 95 + src/Core/Entities/ParticleEntity.m | 1849 ++++ src/Core/Entities/PlanetEntity.h | 190 + src/Core/Entities/PlanetEntity.m | 2021 ++++ src/Core/Entities/PlayerEntity.h | 748 ++ src/Core/Entities/PlayerEntity.m | 7032 +++++++++++++ src/Core/Entities/PlayerEntityContracts.h | 108 + src/Core/Entities/PlayerEntityContracts.m | 1564 +++ src/Core/Entities/PlayerEntityControls.h | 37 + src/Core/Entities/PlayerEntityControls.m | 3041 ++++++ .../Entities/PlayerEntityLegacyScriptEngine.h | 254 + .../Entities/PlayerEntityLegacyScriptEngine.m | 2705 +++++ src/Core/Entities/PlayerEntityLoadSave.h | 64 + src/Core/Entities/PlayerEntityLoadSave.m | 1010 ++ src/Core/Entities/PlayerEntityScriptMethods.h | 62 + src/Core/Entities/PlayerEntityScriptMethods.m | 276 + src/Core/Entities/PlayerEntitySound.h | 136 + src/Core/Entities/PlayerEntitySound.m | 613 ++ src/Core/Entities/PlayerEntityStickMapper.h | 69 + src/Core/Entities/PlayerEntityStickMapper.m | 507 + src/Core/Entities/RingEntity.h | 40 + src/Core/Entities/RingEntity.m | 158 + src/Core/Entities/ShipEntity.h | 814 ++ src/Core/Entities/ShipEntity.m | 8842 +++++++++++++++++ src/Core/Entities/ShipEntityAI.h | 46 + src/Core/Entities/ShipEntityAI.m | 2370 +++++ src/Core/Entities/ShipEntityScriptMethods.h | 38 + src/Core/Entities/ShipEntityScriptMethods.m | 95 + src/Core/Entities/SkyEntity.h | 42 + src/Core/Entities/SkyEntity.m | 238 + src/Core/Entities/StationEntity.h | 198 + src/Core/Entities/StationEntity.m | 2169 ++++ src/Core/Entities/WormholeEntity.h | 91 + src/Core/Entities/WormholeEntity.m | 557 ++ src/Core/GameController.h | 166 + src/Core/GameController.m | 1028 ++ src/Core/Geometry.h | 66 + src/Core/Geometry.m | 733 ++ src/Core/GuiDisplayGen.h | 216 + src/Core/GuiDisplayGen.m | 1450 +++ src/Core/HeadUpDisplay.h | 260 + src/Core/HeadUpDisplay.m | 2619 +++++ src/Core/Materials/OOBasicMaterial.h | 121 + src/Core/Materials/OOBasicMaterial.m | 353 + src/Core/Materials/OOMaterial.h | 131 + src/Core/Materials/OOMaterial.m | 468 + src/Core/Materials/OONullTexture.h | 59 + src/Core/Materials/OONullTexture.m | 125 + src/Core/Materials/OOPNGTextureLoader.h | 63 + src/Core/Materials/OOPNGTextureLoader.m | 257 + src/Core/Materials/OOShaderMaterial.h | 201 + src/Core/Materials/OOShaderMaterial.m | 657 ++ src/Core/Materials/OOShaderProgram.h | 78 + src/Core/Materials/OOShaderProgram.m | 381 + src/Core/Materials/OOShaderUniform.h | 109 + src/Core/Materials/OOShaderUniform.m | 648 ++ .../Materials/OOShaderUniformMethodType.h | 82 + .../Materials/OOShaderUniformMethodType.m | 256 + src/Core/Materials/OOSingleTextureMaterial.h | 73 + src/Core/Materials/OOSingleTextureMaterial.m | 132 + src/Core/Materials/OOTexture.h | 253 + src/Core/Materials/OOTexture.m | 737 ++ src/Core/Materials/OOTextureLoader.h | 131 + src/Core/Materials/OOTextureLoader.m | 506 + src/Core/Materials/pngusr.h | 45 + src/Core/NSDictionaryOOExtensions.h | 61 + src/Core/NSDictionaryOOExtensions.m | 95 + src/Core/NSFileManagerOOExtensions.h | 42 + src/Core/NSFileManagerOOExtensions.m | 143 + src/Core/NSMutableDictionaryOOExtensions.h | 33 + src/Core/NSMutableDictionaryOOExtensions.m | 67 + src/Core/NSScannerOOExtensions.h | 35 + src/Core/NSScannerOOExtensions.m | 84 + src/Core/NSStringOOExtensions.h | 66 + src/Core/NSStringOOExtensions.m | 171 + src/Core/NSThreadOOExtensions.h | 91 + src/Core/NSThreadOOExtensions.m | 123 + src/Core/OOAsyncQueue.h | 78 + src/Core/OOAsyncQueue.m | 315 + src/Core/OOBasicSoundReferencePoint.h | 39 + src/Core/OOBasicSoundReferencePoint.m | 47 + src/Core/OOBoundingBox.h | 105 + src/Core/OOBrain.h | 81 + src/Core/OOBrain.m | 243 + src/Core/OOCPUInfo.h | 166 + src/Core/OOCPUInfo.m | 142 + src/Core/OOCache.h | 82 + src/Core/OOCache.m | 1057 ++ src/Core/OOCacheManager.h | 71 + src/Core/OOCacheManager.m | 783 ++ src/Core/OOCamera.h | 82 + src/Core/OOCamera.m | 163 + src/Core/OOCharacter.h | 94 + src/Core/OOCharacter.m | 516 + src/Core/OOCocoa.h | 353 + src/Core/OOCocoa.m | 99 + src/Core/OOCollectionExtractors.h | 504 + src/Core/OOCollectionExtractors.m | 1489 +++ src/Core/OOColor.h | 129 + src/Core/OOColor.m | 539 + src/Core/OOConstToString.h | 87 + src/Core/OOConstToString.m | 623 ++ src/Core/OOConvertSystemDescriptions.h | 26 + src/Core/OOConvertSystemDescriptions.m | 372 + src/Core/OOCrosshairs.h | 29 + src/Core/OOCrosshairs.m | 185 + src/Core/OODebugGLDrawing.h | 182 + src/Core/OODebugGLDrawing.m | 190 + src/Core/OODeepCopy.h | 79 + src/Core/OODeepCopy.m | 337 + src/Core/OODisplay.h | 41 + src/Core/OODisplay.m | 106 + src/Core/OODisplayMacOSX.h | 32 + src/Core/OODisplayMacOSX.m | 473 + src/Core/OODisplayMode.h | 46 + src/Core/OODisplayMode.m | 187 + src/Core/OODisplayModeMacOSX.h | 26 + src/Core/OODisplayModeMacOSX.m | 152 + src/Core/OODisplayModeSDL.h | 29 + src/Core/OODisplayModeSDL.m | 91 + src/Core/OODisplaySDL.h | 17 + src/Core/OODisplaySDL.m | 174 + src/Core/OODrawable.h | 76 + src/Core/OODrawable.m | 112 + src/Core/OOEncodingConverter.h | 89 + src/Core/OOEncodingConverter.m | 276 + src/Core/OOEntityFilterPredicate.h | 78 + src/Core/OOEntityFilterPredicate.m | 176 + src/Core/OOEquipmentType.h | 123 + src/Core/OOEquipmentType.m | 412 + src/Core/OOExcludeObjectEnumerator.h | 48 + src/Core/OOExcludeObjectEnumerator.m | 84 + src/Core/OOFastArithmetic.h | 298 + src/Core/OOFastArithmetic.m | 138 + src/Core/OOFilteringEnumerator.h | 137 + src/Core/OOFilteringEnumerator.m | 280 + src/Core/OOFunctionAttributes.h | 42 + src/Core/OOGraphicsResetManager.h | 80 + src/Core/OOGraphicsResetManager.m | 153 + src/Core/OOInstinct.h | 117 + src/Core/OOInstinct.m | 286 + src/Core/OOIsNumberLiteral.h | 50 + src/Core/OOIsNumberLiteral.m | 129 + src/Core/OOLight.h | 114 + src/Core/OOLight.m | 385 + src/Core/OOLogHeader.h | 56 + src/Core/OOLogHeader.m | 342 + src/Core/OOLogOutputHandler.h | 71 + src/Core/OOLogOutputHandler.m | 747 ++ src/Core/OOLogging.h | 176 + src/Core/OOLogging.m | 884 ++ src/Core/OOLoggingExtended.h | 75 + src/Core/OOMacroOpenGL.h | 51 + src/Core/OOMaths.h | 114 + src/Core/OOMatrix.h | 278 + src/Core/OOMatrix.m | 171 + src/Core/OOMesh.h | 156 + src/Core/OOMesh.m | 1573 +++ src/Core/OOMusic.m | 11 + src/Core/OOMusicController.h | 83 + src/Core/OOMusicController.m | 370 + src/Core/OOOpenGL.h | 121 + src/Core/OOOpenGL.m | 448 + src/Core/OOOpenGLExtensionManager.h | 156 + src/Core/OOOpenGLExtensionManager.m | 405 + src/Core/OOPListParsing.h | 41 + src/Core/OOPListParsing.m | 731 ++ src/Core/OOPriorityQueue.h | 71 + src/Core/OOPriorityQueue.m | 712 ++ src/Core/OOProbabilisticTextureManager.h | 98 + src/Core/OOProbabilisticTextureManager.m | 232 + src/Core/OOProbabilitySet.h | 99 + src/Core/OOProbabilitySet.m | 1084 ++ src/Core/OOQuaternion.h | 191 + src/Core/OOQuaternion.m | 376 + src/Core/OORoleSet.h | 92 + src/Core/OORoleSet.m | 376 + src/Core/OOShipGroup.h | 72 + src/Core/OOShipGroup.m | 559 ++ src/Core/OOShipRegistry.h | 81 + src/Core/OOShipRegistry.m | 1336 +++ src/Core/OOSkyDrawable.h | 75 + src/Core/OOSkyDrawable.m | 665 ++ src/Core/OOSound.h | 57 + src/Core/OOSoundInternal.h | 21 + src/Core/OOSoundSource.h | 98 + src/Core/OOSoundSource.m | 275 + src/Core/OOSoundSourcePool.h | 83 + src/Core/OOSoundSourcePool.m | 221 + src/Core/OOSpatialReference.h | 43 + src/Core/OOSpatialReference.m | 39 + src/Core/OOStringParsing.h | 110 + src/Core/OOStringParsing.m | 791 ++ src/Core/OOTextureScaling.h | 71 + src/Core/OOTextureScaling.m | 1379 +++ src/Core/OOTriangle.h | 62 + src/Core/OOTriangle.m | 47 + src/Core/OOTrumble.h | 164 + src/Core/OOTrumble.m | 1006 ++ src/Core/OOTypes.h | 361 + src/Core/OOVector.h | 329 + src/Core/OOVector.m | 87 + src/Core/OOVoxel.h | 47 + src/Core/OOVoxel.m | 173 + src/Core/OOWeakReference.h | 140 + src/Core/OOWeakReference.m | 191 + src/Core/OOXMLExtensions.h | 41 + src/Core/OOXMLExtensions.m | 60 + .../OOAIStateMachineVerifierStage.h | 46 + .../OOAIStateMachineVerifierStage.m | 216 + .../OOCheckDemoShipsPListVerifierStage.h | 36 + .../OOCheckDemoShipsPListVerifierStage.m | 123 + .../OOCheckEquipmentPListVerifierStage.h | 35 + .../OOCheckEquipmentPListVerifierStage.m | 172 + .../OOCheckRequiresPListVerifierStage.h | 36 + .../OOCheckRequiresPListVerifierStage.m | 165 + .../OOCheckShipDataPListVerifierStage.h | 55 + .../OOCheckShipDataPListVerifierStage.m | 390 + .../OXPVerifier/OOFileScannerVerifierStage.h | 137 + .../OXPVerifier/OOFileScannerVerifierStage.m | 805 ++ src/Core/OXPVerifier/OOModelVerifierStage.h | 60 + src/Core/OXPVerifier/OOModelVerifierStage.m | 198 + src/Core/OXPVerifier/OOOXPVerifier.h | 113 + src/Core/OXPVerifier/OOOXPVerifier.m | 745 ++ src/Core/OXPVerifier/OOOXPVerifierStage.h | 103 + src/Core/OXPVerifier/OOOXPVerifierStage.m | 249 + .../OXPVerifier/OOOXPVerifierStageInternal.h | 73 + src/Core/OXPVerifier/OOPListSchemaVerifier.h | 190 + src/Core/OXPVerifier/OOPListSchemaVerifier.m | 1522 +++ src/Core/OXPVerifier/OOTextureVerifierStage.h | 63 + src/Core/OXPVerifier/OOTextureVerifierStage.m | 209 + src/Core/OXPVerifier/OXP verifier design.txt | 120 + .../OXPVerifier/plist verifier design.txt | 47 + src/Core/Octree.h | 95 + src/Core/Octree.m | 801 ++ src/Core/OldSchoolPropertyListWriting.h | 56 + src/Core/OldSchoolPropertyListWriting.m | 339 + src/Core/OpenGLSprite.h | 50 + src/Core/OpenGLSprite.m | 103 + src/Core/ReleaseLockProxy.h | 31 + src/Core/ReleaseLockProxy.m | 192 + src/Core/ResourceManager.h | 85 + src/Core/ResourceManager.m | 894 ++ .../Scripting/EntityOOJavaScriptExtensions.h | 59 + .../Scripting/EntityOOJavaScriptExtensions.m | 138 + src/Core/Scripting/OOJSCall.h | 46 + src/Core/Scripting/OOJSCall.m | 182 + src/Core/Scripting/OOJSClock.h | 31 + src/Core/Scripting/OOJSClock.m | 206 + src/Core/Scripting/OOJSEntity.h | 54 + src/Core/Scripting/OOJSEntity.m | 392 + src/Core/Scripting/OOJSEquipmentInfo.h | 31 + src/Core/Scripting/OOJSEquipmentInfo.m | 311 + src/Core/Scripting/OOJSFunction.h | 56 + src/Core/Scripting/OOJSFunction.m | 136 + src/Core/Scripting/OOJSGlobal.h | 33 + src/Core/Scripting/OOJSGlobal.m | 271 + src/Core/Scripting/OOJSMission.h | 32 + src/Core/Scripting/OOJSMission.m | 310 + src/Core/Scripting/OOJSMissionVariables.h | 32 + src/Core/Scripting/OOJSMissionVariables.m | 132 + src/Core/Scripting/OOJSOolite.h | 32 + src/Core/Scripting/OOJSOolite.m | 182 + src/Core/Scripting/OOJSPlanet.h | 34 + src/Core/Scripting/OOJSPlanet.m | 202 + src/Core/Scripting/OOJSPlayer.h | 46 + src/Core/Scripting/OOJSPlayer.m | 412 + src/Core/Scripting/OOJSPlayerShip.h | 43 + src/Core/Scripting/OOJSPlayerShip.m | 590 ++ src/Core/Scripting/OOJSQuaternion.h | 68 + src/Core/Scripting/OOJSQuaternion.m | 678 ++ src/Core/Scripting/OOJSScript.h | 55 + src/Core/Scripting/OOJSScript.m | 681 ++ src/Core/Scripting/OOJSShip.h | 36 + src/Core/Scripting/OOJSShip.m | 1082 ++ src/Core/Scripting/OOJSShipGroup.h | 31 + src/Core/Scripting/OOJSShipGroup.m | 329 + src/Core/Scripting/OOJSSound.h | 40 + src/Core/Scripting/OOJSSound.m | 267 + src/Core/Scripting/OOJSSoundSource.h | 32 + src/Core/Scripting/OOJSSoundSource.m | 307 + src/Core/Scripting/OOJSSpecialFunctions.h | 38 + src/Core/Scripting/OOJSSpecialFunctions.m | 79 + src/Core/Scripting/OOJSStation.h | 33 + src/Core/Scripting/OOJSStation.m | 248 + src/Core/Scripting/OOJSSun.h | 36 + src/Core/Scripting/OOJSSun.m | 169 + src/Core/Scripting/OOJSSystem.h | 32 + src/Core/Scripting/OOJSSystem.m | 1022 ++ src/Core/Scripting/OOJSSystemInfo.h | 36 + src/Core/Scripting/OOJSSystemInfo.m | 294 + src/Core/Scripting/OOJSTimer.h | 50 + src/Core/Scripting/OOJSTimer.m | 420 + src/Core/Scripting/OOJSVector.h | 68 + src/Core/Scripting/OOJSVector.m | 807 ++ src/Core/Scripting/OOJSWorldScripts.h | 33 + src/Core/Scripting/OOJSWorldScripts.m | 120 + src/Core/Scripting/OOJavaScriptEngine.h | 327 + src/Core/Scripting/OOJavaScriptEngine.m | 1547 +++ src/Core/Scripting/OOLegacyScriptWhitelist.h | 137 + src/Core/Scripting/OOLegacyScriptWhitelist.m | 471 + src/Core/Scripting/OOPListScript.h | 44 + src/Core/Scripting/OOPListScript.m | 172 + src/Core/Scripting/OOScript.h | 76 + src/Core/Scripting/OOScript.m | 300 + src/Core/Scripting/OOScriptTimer.h | 75 + src/Core/Scripting/OOScriptTimer.m | 222 + src/Core/TextureStore.h | 70 + src/Core/TextureStore.m | 549 + src/Core/Universe.h | 650 ++ src/Core/Universe.m | 8764 ++++++++++++++++ src/Core/legacy_random.c | 236 + src/Core/legacy_random.h | 147 + src/SDL/Comparison.h | 121 + src/SDL/Comparison.m | 88 + src/SDL/JoystickHandler.h | 208 + src/SDL/JoystickHandler.m | 581 ++ src/SDL/MyOpenGLView.h | 223 + src/SDL/MyOpenGLView.m | 1678 ++++ src/SDL/OOSDLConcreteSound.h | 66 + src/SDL/OOSDLConcreteSound.m | 116 + src/SDL/OOSDLSound.h | 66 + src/SDL/OOSDLSound.m | 154 + src/SDL/OOSDLSoundChannel.h | 91 + src/SDL/OOSDLSoundChannel.m | 147 + src/SDL/OOSDLSoundInternal.h | 53 + src/SDL/OOSDLSoundMixer.h | 82 + src/SDL/OOSDLSoundMixer.m | 183 + src/SDL/SDLMusic.h | 64 + src/SDL/SDLMusic.m | 236 + src/SDL/main.m | 135 + src/SDL/makebinpkg.sh | 20 + src/SDL/makedist.sh | 43 + 438 files changed, 143410 insertions(+) create mode 100644 GNUmakefile create mode 100644 Oolite.xcodeproj/project.pbxproj create mode 100644 Schemata/README.txt create mode 100644 Schemata/demoshipsSchema.plist create mode 100644 Schemata/hudSchema.plist create mode 100644 Schemata/plistschema.plist create mode 100644 Schemata/shipdataEntrySchema.plist create mode 100644 Schemata/shipyardSchema.plist create mode 100755 debian/apply-patches create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100755 debian/extra/games/oolite create mode 100644 debian/oolite-data.dirs create mode 100644 debian/oolite-data.docs create mode 100644 debian/oolite-data.install create mode 100644 debian/oolite-logo1.xpm create mode 100644 debian/oolite.6 create mode 100644 debian/oolite.dirs create mode 100644 debian/oolite.install create mode 100644 debian/oolite.links create mode 100644 debian/oolite.menu create mode 100755 debian/rules create mode 100644 installers/win32/Install.ico create mode 100644 installers/win32/Jupiter Europa.ico create mode 100644 installers/win32/OOlite.nsi create mode 100644 installers/win32/OOlite_Update.nsi create mode 100644 installers/win32/Oolite.ico create mode 100644 installers/win32/OoliteInstallerHeaderBitmap.bmp create mode 100644 installers/win32/OoliteInstallerHeaderBitmap_ModernUI.bmp create mode 100644 installers/win32/OoliteRS.pdf create mode 100644 installers/win32/Oolite_Readme.txt create mode 100644 installers/win32/RunOolite.bat create mode 100644 installers/win32/Uninstall.ico create mode 100644 src/BSDCompat/bsd_string.h create mode 100644 src/BSDCompat/strlcpy.c create mode 100644 src/Cocoa/Comparison.h create mode 100644 src/Cocoa/Groolite.h create mode 100644 src/Cocoa/Groolite.m create mode 100644 src/Cocoa/Info-Oolite.plist create mode 100644 src/Cocoa/Info-OoliteDev.plist create mode 100644 src/Cocoa/JoystickHandler.h create mode 100644 src/Cocoa/JoystickHandler.m create mode 100644 src/Cocoa/MyOpenGLView.h create mode 100644 src/Cocoa/MyOpenGLView.m create mode 100644 src/Cocoa/OOCABufferedSound.h create mode 100644 src/Cocoa/OOCABufferedSound.m create mode 100644 src/Cocoa/OOCAMusic.h create mode 100644 src/Cocoa/OOCAMusic.m create mode 100644 src/Cocoa/OOCASound.h create mode 100644 src/Cocoa/OOCASound.m create mode 100644 src/Cocoa/OOCASoundChannel.h create mode 100644 src/Cocoa/OOCASoundChannel.m create mode 100644 src/Cocoa/OOCASoundDecoder.h create mode 100644 src/Cocoa/OOCASoundDecoder.m create mode 100644 src/Cocoa/OOCASoundInternal.h create mode 100644 src/Cocoa/OOCASoundMixer.h create mode 100644 src/Cocoa/OOCASoundMixer.m create mode 100644 src/Cocoa/OOCASoundReferencePoint.h create mode 100644 src/Cocoa/OOCASoundReferencePoint.m create mode 100644 src/Cocoa/OOCAStreamingSound.h create mode 100644 src/Cocoa/OOCAStreamingSound.m create mode 100644 src/Cocoa/OOErrorDescription.h create mode 100644 src/Cocoa/OOErrorDescription.m create mode 100644 src/Cocoa/OOProgressBar.h create mode 100644 src/Cocoa/OOProgressBar.m create mode 100644 src/Cocoa/OoliteApp.h create mode 100644 src/Cocoa/OoliteApp.m create mode 100644 src/Cocoa/SoundInspector.nib/classes.nib create mode 100644 src/Cocoa/SoundInspector.nib/info.nib create mode 100644 src/Cocoa/SoundInspector.nib/keyedobjects.nib create mode 100644 src/Cocoa/VirtualRingBuffer.h create mode 100644 src/Cocoa/VirtualRingBuffer.m create mode 100644 src/Cocoa/debug-exports-64.exp create mode 100644 src/Cocoa/debug-exports.exp create mode 100644 src/Cocoa/main.m create mode 100644 src/Cocoa/oolite-options.xcconfig create mode 100644 src/Cocoa/oolite-version.xcconfig create mode 100644 src/Core/AI.h create mode 100644 src/Core/AI.m create mode 100644 src/Core/CollisionRegion.h create mode 100644 src/Core/CollisionRegion.m create mode 100644 src/Core/Debug/OODebugMonitor.h create mode 100644 src/Core/Debug/OODebugMonitor.m create mode 100644 src/Core/Debug/OODebugSupport.h create mode 100644 src/Core/Debug/OODebugSupport.m create mode 100644 src/Core/Debug/OODebugTCPConsoleClient.h create mode 100644 src/Core/Debug/OODebugTCPConsoleClient.m create mode 100644 src/Core/Debug/OODebugTCPConsoleProtocol.h create mode 100644 src/Core/Debug/OODebuggerInterface.h create mode 100644 src/Core/Debug/OOJSConsole.h create mode 100644 src/Core/Debug/OOJSConsole.m create mode 100644 src/Core/Debug/OOTCPStreamDecoder.c create mode 100644 src/Core/Debug/OOTCPStreamDecoder.h create mode 100644 src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.h create mode 100644 src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.m create mode 100644 src/Core/Entities/DustEntity.h create mode 100644 src/Core/Entities/DustEntity.m create mode 100644 src/Core/Entities/Entity.h create mode 100644 src/Core/Entities/Entity.m create mode 100644 src/Core/Entities/OOEntityWithDrawable.h create mode 100644 src/Core/Entities/OOEntityWithDrawable.m create mode 100644 src/Core/Entities/OOSelfDrawingEntity.h create mode 100644 src/Core/Entities/OOSelfDrawingEntity.m create mode 100644 src/Core/Entities/ParticleEntity.h create mode 100644 src/Core/Entities/ParticleEntity.m create mode 100644 src/Core/Entities/PlanetEntity.h create mode 100644 src/Core/Entities/PlanetEntity.m create mode 100644 src/Core/Entities/PlayerEntity.h create mode 100644 src/Core/Entities/PlayerEntity.m create mode 100644 src/Core/Entities/PlayerEntityContracts.h create mode 100644 src/Core/Entities/PlayerEntityContracts.m create mode 100644 src/Core/Entities/PlayerEntityControls.h create mode 100644 src/Core/Entities/PlayerEntityControls.m create mode 100644 src/Core/Entities/PlayerEntityLegacyScriptEngine.h create mode 100644 src/Core/Entities/PlayerEntityLegacyScriptEngine.m create mode 100644 src/Core/Entities/PlayerEntityLoadSave.h create mode 100644 src/Core/Entities/PlayerEntityLoadSave.m create mode 100644 src/Core/Entities/PlayerEntityScriptMethods.h create mode 100644 src/Core/Entities/PlayerEntityScriptMethods.m create mode 100644 src/Core/Entities/PlayerEntitySound.h create mode 100644 src/Core/Entities/PlayerEntitySound.m create mode 100644 src/Core/Entities/PlayerEntityStickMapper.h create mode 100644 src/Core/Entities/PlayerEntityStickMapper.m create mode 100644 src/Core/Entities/RingEntity.h create mode 100644 src/Core/Entities/RingEntity.m create mode 100644 src/Core/Entities/ShipEntity.h create mode 100644 src/Core/Entities/ShipEntity.m create mode 100644 src/Core/Entities/ShipEntityAI.h create mode 100644 src/Core/Entities/ShipEntityAI.m create mode 100644 src/Core/Entities/ShipEntityScriptMethods.h create mode 100644 src/Core/Entities/ShipEntityScriptMethods.m create mode 100644 src/Core/Entities/SkyEntity.h create mode 100644 src/Core/Entities/SkyEntity.m create mode 100644 src/Core/Entities/StationEntity.h create mode 100644 src/Core/Entities/StationEntity.m create mode 100644 src/Core/Entities/WormholeEntity.h create mode 100644 src/Core/Entities/WormholeEntity.m create mode 100644 src/Core/GameController.h create mode 100644 src/Core/GameController.m create mode 100644 src/Core/Geometry.h create mode 100644 src/Core/Geometry.m create mode 100644 src/Core/GuiDisplayGen.h create mode 100644 src/Core/GuiDisplayGen.m create mode 100644 src/Core/HeadUpDisplay.h create mode 100644 src/Core/HeadUpDisplay.m create mode 100644 src/Core/Materials/OOBasicMaterial.h create mode 100644 src/Core/Materials/OOBasicMaterial.m create mode 100644 src/Core/Materials/OOMaterial.h create mode 100644 src/Core/Materials/OOMaterial.m create mode 100644 src/Core/Materials/OONullTexture.h create mode 100644 src/Core/Materials/OONullTexture.m create mode 100644 src/Core/Materials/OOPNGTextureLoader.h create mode 100644 src/Core/Materials/OOPNGTextureLoader.m create mode 100644 src/Core/Materials/OOShaderMaterial.h create mode 100644 src/Core/Materials/OOShaderMaterial.m create mode 100644 src/Core/Materials/OOShaderProgram.h create mode 100644 src/Core/Materials/OOShaderProgram.m create mode 100644 src/Core/Materials/OOShaderUniform.h create mode 100644 src/Core/Materials/OOShaderUniform.m create mode 100644 src/Core/Materials/OOShaderUniformMethodType.h create mode 100644 src/Core/Materials/OOShaderUniformMethodType.m create mode 100644 src/Core/Materials/OOSingleTextureMaterial.h create mode 100644 src/Core/Materials/OOSingleTextureMaterial.m create mode 100644 src/Core/Materials/OOTexture.h create mode 100644 src/Core/Materials/OOTexture.m create mode 100644 src/Core/Materials/OOTextureLoader.h create mode 100644 src/Core/Materials/OOTextureLoader.m create mode 100644 src/Core/Materials/pngusr.h create mode 100644 src/Core/NSDictionaryOOExtensions.h create mode 100644 src/Core/NSDictionaryOOExtensions.m create mode 100644 src/Core/NSFileManagerOOExtensions.h create mode 100644 src/Core/NSFileManagerOOExtensions.m create mode 100644 src/Core/NSMutableDictionaryOOExtensions.h create mode 100644 src/Core/NSMutableDictionaryOOExtensions.m create mode 100644 src/Core/NSScannerOOExtensions.h create mode 100644 src/Core/NSScannerOOExtensions.m create mode 100644 src/Core/NSStringOOExtensions.h create mode 100644 src/Core/NSStringOOExtensions.m create mode 100644 src/Core/NSThreadOOExtensions.h create mode 100644 src/Core/NSThreadOOExtensions.m create mode 100644 src/Core/OOAsyncQueue.h create mode 100644 src/Core/OOAsyncQueue.m create mode 100644 src/Core/OOBasicSoundReferencePoint.h create mode 100644 src/Core/OOBasicSoundReferencePoint.m create mode 100644 src/Core/OOBoundingBox.h create mode 100644 src/Core/OOBrain.h create mode 100644 src/Core/OOBrain.m create mode 100644 src/Core/OOCPUInfo.h create mode 100644 src/Core/OOCPUInfo.m create mode 100644 src/Core/OOCache.h create mode 100644 src/Core/OOCache.m create mode 100644 src/Core/OOCacheManager.h create mode 100644 src/Core/OOCacheManager.m create mode 100644 src/Core/OOCamera.h create mode 100644 src/Core/OOCamera.m create mode 100644 src/Core/OOCharacter.h create mode 100644 src/Core/OOCharacter.m create mode 100644 src/Core/OOCocoa.h create mode 100644 src/Core/OOCocoa.m create mode 100644 src/Core/OOCollectionExtractors.h create mode 100644 src/Core/OOCollectionExtractors.m create mode 100644 src/Core/OOColor.h create mode 100644 src/Core/OOColor.m create mode 100644 src/Core/OOConstToString.h create mode 100644 src/Core/OOConstToString.m create mode 100644 src/Core/OOConvertSystemDescriptions.h create mode 100644 src/Core/OOConvertSystemDescriptions.m create mode 100644 src/Core/OOCrosshairs.h create mode 100644 src/Core/OOCrosshairs.m create mode 100644 src/Core/OODebugGLDrawing.h create mode 100644 src/Core/OODebugGLDrawing.m create mode 100644 src/Core/OODeepCopy.h create mode 100644 src/Core/OODeepCopy.m create mode 100644 src/Core/OODisplay.h create mode 100644 src/Core/OODisplay.m create mode 100644 src/Core/OODisplayMacOSX.h create mode 100644 src/Core/OODisplayMacOSX.m create mode 100644 src/Core/OODisplayMode.h create mode 100644 src/Core/OODisplayMode.m create mode 100644 src/Core/OODisplayModeMacOSX.h create mode 100644 src/Core/OODisplayModeMacOSX.m create mode 100644 src/Core/OODisplayModeSDL.h create mode 100644 src/Core/OODisplayModeSDL.m create mode 100644 src/Core/OODisplaySDL.h create mode 100644 src/Core/OODisplaySDL.m create mode 100644 src/Core/OODrawable.h create mode 100644 src/Core/OODrawable.m create mode 100644 src/Core/OOEncodingConverter.h create mode 100644 src/Core/OOEncodingConverter.m create mode 100644 src/Core/OOEntityFilterPredicate.h create mode 100644 src/Core/OOEntityFilterPredicate.m create mode 100644 src/Core/OOEquipmentType.h create mode 100644 src/Core/OOEquipmentType.m create mode 100644 src/Core/OOExcludeObjectEnumerator.h create mode 100644 src/Core/OOExcludeObjectEnumerator.m create mode 100644 src/Core/OOFastArithmetic.h create mode 100644 src/Core/OOFastArithmetic.m create mode 100644 src/Core/OOFilteringEnumerator.h create mode 100644 src/Core/OOFilteringEnumerator.m create mode 100644 src/Core/OOFunctionAttributes.h create mode 100644 src/Core/OOGraphicsResetManager.h create mode 100644 src/Core/OOGraphicsResetManager.m create mode 100644 src/Core/OOInstinct.h create mode 100644 src/Core/OOInstinct.m create mode 100644 src/Core/OOIsNumberLiteral.h create mode 100644 src/Core/OOIsNumberLiteral.m create mode 100644 src/Core/OOLight.h create mode 100644 src/Core/OOLight.m create mode 100644 src/Core/OOLogHeader.h create mode 100644 src/Core/OOLogHeader.m create mode 100644 src/Core/OOLogOutputHandler.h create mode 100644 src/Core/OOLogOutputHandler.m create mode 100644 src/Core/OOLogging.h create mode 100644 src/Core/OOLogging.m create mode 100644 src/Core/OOLoggingExtended.h create mode 100644 src/Core/OOMacroOpenGL.h create mode 100644 src/Core/OOMaths.h create mode 100644 src/Core/OOMatrix.h create mode 100644 src/Core/OOMatrix.m create mode 100644 src/Core/OOMesh.h create mode 100644 src/Core/OOMesh.m create mode 100644 src/Core/OOMusic.m create mode 100644 src/Core/OOMusicController.h create mode 100644 src/Core/OOMusicController.m create mode 100644 src/Core/OOOpenGL.h create mode 100644 src/Core/OOOpenGL.m create mode 100644 src/Core/OOOpenGLExtensionManager.h create mode 100644 src/Core/OOOpenGLExtensionManager.m create mode 100644 src/Core/OOPListParsing.h create mode 100644 src/Core/OOPListParsing.m create mode 100644 src/Core/OOPriorityQueue.h create mode 100644 src/Core/OOPriorityQueue.m create mode 100644 src/Core/OOProbabilisticTextureManager.h create mode 100644 src/Core/OOProbabilisticTextureManager.m create mode 100644 src/Core/OOProbabilitySet.h create mode 100644 src/Core/OOProbabilitySet.m create mode 100644 src/Core/OOQuaternion.h create mode 100644 src/Core/OOQuaternion.m create mode 100644 src/Core/OORoleSet.h create mode 100644 src/Core/OORoleSet.m create mode 100644 src/Core/OOShipGroup.h create mode 100644 src/Core/OOShipGroup.m create mode 100644 src/Core/OOShipRegistry.h create mode 100644 src/Core/OOShipRegistry.m create mode 100644 src/Core/OOSkyDrawable.h create mode 100644 src/Core/OOSkyDrawable.m create mode 100644 src/Core/OOSound.h create mode 100644 src/Core/OOSoundInternal.h create mode 100644 src/Core/OOSoundSource.h create mode 100644 src/Core/OOSoundSource.m create mode 100644 src/Core/OOSoundSourcePool.h create mode 100644 src/Core/OOSoundSourcePool.m create mode 100644 src/Core/OOSpatialReference.h create mode 100644 src/Core/OOSpatialReference.m create mode 100644 src/Core/OOStringParsing.h create mode 100644 src/Core/OOStringParsing.m create mode 100644 src/Core/OOTextureScaling.h create mode 100644 src/Core/OOTextureScaling.m create mode 100644 src/Core/OOTriangle.h create mode 100644 src/Core/OOTriangle.m create mode 100644 src/Core/OOTrumble.h create mode 100644 src/Core/OOTrumble.m create mode 100644 src/Core/OOTypes.h create mode 100644 src/Core/OOVector.h create mode 100644 src/Core/OOVector.m create mode 100644 src/Core/OOVoxel.h create mode 100644 src/Core/OOVoxel.m create mode 100644 src/Core/OOWeakReference.h create mode 100644 src/Core/OOWeakReference.m create mode 100644 src/Core/OOXMLExtensions.h create mode 100644 src/Core/OOXMLExtensions.m create mode 100644 src/Core/OXPVerifier/OOAIStateMachineVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOAIStateMachineVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOFileScannerVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOFileScannerVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOModelVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOModelVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOOXPVerifier.h create mode 100644 src/Core/OXPVerifier/OOOXPVerifier.m create mode 100644 src/Core/OXPVerifier/OOOXPVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOOXPVerifierStage.m create mode 100644 src/Core/OXPVerifier/OOOXPVerifierStageInternal.h create mode 100644 src/Core/OXPVerifier/OOPListSchemaVerifier.h create mode 100644 src/Core/OXPVerifier/OOPListSchemaVerifier.m create mode 100644 src/Core/OXPVerifier/OOTextureVerifierStage.h create mode 100644 src/Core/OXPVerifier/OOTextureVerifierStage.m create mode 100644 src/Core/OXPVerifier/OXP verifier design.txt create mode 100644 src/Core/OXPVerifier/plist verifier design.txt create mode 100644 src/Core/Octree.h create mode 100644 src/Core/Octree.m create mode 100644 src/Core/OldSchoolPropertyListWriting.h create mode 100644 src/Core/OldSchoolPropertyListWriting.m create mode 100644 src/Core/OpenGLSprite.h create mode 100644 src/Core/OpenGLSprite.m create mode 100644 src/Core/ReleaseLockProxy.h create mode 100644 src/Core/ReleaseLockProxy.m create mode 100644 src/Core/ResourceManager.h create mode 100644 src/Core/ResourceManager.m create mode 100644 src/Core/Scripting/EntityOOJavaScriptExtensions.h create mode 100644 src/Core/Scripting/EntityOOJavaScriptExtensions.m create mode 100644 src/Core/Scripting/OOJSCall.h create mode 100644 src/Core/Scripting/OOJSCall.m create mode 100644 src/Core/Scripting/OOJSClock.h create mode 100644 src/Core/Scripting/OOJSClock.m create mode 100644 src/Core/Scripting/OOJSEntity.h create mode 100644 src/Core/Scripting/OOJSEntity.m create mode 100644 src/Core/Scripting/OOJSEquipmentInfo.h create mode 100644 src/Core/Scripting/OOJSEquipmentInfo.m create mode 100644 src/Core/Scripting/OOJSFunction.h create mode 100644 src/Core/Scripting/OOJSFunction.m create mode 100644 src/Core/Scripting/OOJSGlobal.h create mode 100644 src/Core/Scripting/OOJSGlobal.m create mode 100644 src/Core/Scripting/OOJSMission.h create mode 100644 src/Core/Scripting/OOJSMission.m create mode 100644 src/Core/Scripting/OOJSMissionVariables.h create mode 100644 src/Core/Scripting/OOJSMissionVariables.m create mode 100644 src/Core/Scripting/OOJSOolite.h create mode 100644 src/Core/Scripting/OOJSOolite.m create mode 100644 src/Core/Scripting/OOJSPlanet.h create mode 100644 src/Core/Scripting/OOJSPlanet.m create mode 100644 src/Core/Scripting/OOJSPlayer.h create mode 100644 src/Core/Scripting/OOJSPlayer.m create mode 100644 src/Core/Scripting/OOJSPlayerShip.h create mode 100644 src/Core/Scripting/OOJSPlayerShip.m create mode 100644 src/Core/Scripting/OOJSQuaternion.h create mode 100644 src/Core/Scripting/OOJSQuaternion.m create mode 100644 src/Core/Scripting/OOJSScript.h create mode 100644 src/Core/Scripting/OOJSScript.m create mode 100644 src/Core/Scripting/OOJSShip.h create mode 100644 src/Core/Scripting/OOJSShip.m create mode 100644 src/Core/Scripting/OOJSShipGroup.h create mode 100644 src/Core/Scripting/OOJSShipGroup.m create mode 100644 src/Core/Scripting/OOJSSound.h create mode 100644 src/Core/Scripting/OOJSSound.m create mode 100644 src/Core/Scripting/OOJSSoundSource.h create mode 100644 src/Core/Scripting/OOJSSoundSource.m create mode 100644 src/Core/Scripting/OOJSSpecialFunctions.h create mode 100644 src/Core/Scripting/OOJSSpecialFunctions.m create mode 100644 src/Core/Scripting/OOJSStation.h create mode 100644 src/Core/Scripting/OOJSStation.m create mode 100644 src/Core/Scripting/OOJSSun.h create mode 100644 src/Core/Scripting/OOJSSun.m create mode 100644 src/Core/Scripting/OOJSSystem.h create mode 100644 src/Core/Scripting/OOJSSystem.m create mode 100644 src/Core/Scripting/OOJSSystemInfo.h create mode 100644 src/Core/Scripting/OOJSSystemInfo.m create mode 100644 src/Core/Scripting/OOJSTimer.h create mode 100644 src/Core/Scripting/OOJSTimer.m create mode 100644 src/Core/Scripting/OOJSVector.h create mode 100644 src/Core/Scripting/OOJSVector.m create mode 100644 src/Core/Scripting/OOJSWorldScripts.h create mode 100644 src/Core/Scripting/OOJSWorldScripts.m create mode 100644 src/Core/Scripting/OOJavaScriptEngine.h create mode 100644 src/Core/Scripting/OOJavaScriptEngine.m create mode 100644 src/Core/Scripting/OOLegacyScriptWhitelist.h create mode 100644 src/Core/Scripting/OOLegacyScriptWhitelist.m create mode 100644 src/Core/Scripting/OOPListScript.h create mode 100644 src/Core/Scripting/OOPListScript.m create mode 100644 src/Core/Scripting/OOScript.h create mode 100644 src/Core/Scripting/OOScript.m create mode 100644 src/Core/Scripting/OOScriptTimer.h create mode 100644 src/Core/Scripting/OOScriptTimer.m create mode 100644 src/Core/TextureStore.h create mode 100644 src/Core/TextureStore.m create mode 100644 src/Core/Universe.h create mode 100644 src/Core/Universe.m create mode 100644 src/Core/legacy_random.c create mode 100644 src/Core/legacy_random.h create mode 100644 src/SDL/Comparison.h create mode 100644 src/SDL/Comparison.m create mode 100644 src/SDL/JoystickHandler.h create mode 100644 src/SDL/JoystickHandler.m create mode 100644 src/SDL/MyOpenGLView.h create mode 100644 src/SDL/MyOpenGLView.m create mode 100644 src/SDL/OOSDLConcreteSound.h create mode 100644 src/SDL/OOSDLConcreteSound.m create mode 100644 src/SDL/OOSDLSound.h create mode 100644 src/SDL/OOSDLSound.m create mode 100644 src/SDL/OOSDLSoundChannel.h create mode 100644 src/SDL/OOSDLSoundChannel.m create mode 100644 src/SDL/OOSDLSoundInternal.h create mode 100644 src/SDL/OOSDLSoundMixer.h create mode 100644 src/SDL/OOSDLSoundMixer.m create mode 100644 src/SDL/SDLMusic.h create mode 100644 src/SDL/SDLMusic.m create mode 100644 src/SDL/main.m create mode 100755 src/SDL/makebinpkg.sh create mode 100755 src/SDL/makedist.sh diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 00000000..dd833cba --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,277 @@ +include $(GNUSTEP_MAKEFILES)/common.make +CP = cp +BUILD_WITH_DEBUG_FUNCTIONALITY = yes +DOCKING_CLEARANCE = yes +PROCEDURAL_PLANETS = yes +WORMHOLE_SCANNER = yes +TARGET_INCOMING_MISSILES = yes +vpath %.m src/SDL:src/Core:src/Core/Entities:src/Core/Materials:src/Core/Scripting:src/Core/OXPVerifier:src/Core/Debug +vpath %.h src/SDL:src/Core:src/Core/Entities:src/Core/Materials:src/Core/Scripting:src/Core/OXPVerifier:src/Core/Debug +vpath %.c src/SDL:src/Core:src/BSDCompat:src/Core/Debug +GNUSTEP_INSTALLATION_DIR = $(GNUSTEP_USER_ROOT) +GNUSTEP_OBJ_DIR_BASENAME := $(GNUSTEP_OBJ_DIR_NAME) +ifeq ($(GNUSTEP_HOST_OS),mingw32) + ADDITIONAL_INCLUDE_DIRS = -Ideps/Windows-x86-deps/include -Isrc/SDL -Isrc/Core -Isrc/BSDCompat -Isrc/Core/Scripting -Isrc/Core/Materials -Isrc/Core/Entities -Isrc/Core/OXPVerifier -Isrc/Core/Debug + ADDITIONAL_OBJC_LIBS = -lglu32 -lopengl32 -lpng12.dll -lmingw32 -lSDLmain -lSDL -lSDL_mixer -lgnustep-base -ljs32 -mwindows + ADDITIONAL_CFLAGS = -DWIN32 -DNEED_STRLCPY `sdl-config --cflags` +# note the vpath stuff above isn't working for me, so adding src/SDL and src/Core explicitly + ADDITIONAL_OBJCFLAGS = -DLOADSAVEGUI -DWIN32 -DXP_WIN -Wno-import `sdl-config --cflags` + oolite_LIB_DIRS += -L/usr/local/lib -L$(GNUSTEP_LOCAL_ROOT)/lib -Ldeps/Windows-x86-deps/lib +else + LIBJS_SRC_DIR = deps/Cross-platform-deps/SpiderMonkey/js/src + LIBJS_BIN_DIR = $(LIBJS_SRC_DIR)/Linux_All_OPT.OBJ + ADDITIONAL_INCLUDE_DIRS = -I$(LIBJS_SRC_DIR) -I$(LIBJS_BIN_DIR) -Isrc/SDL -Isrc/Core -Isrc/BSDCompat -Isrc/Core/Scripting -Isrc/Core/Materials -Isrc/Core/Entities -Isrc/Core/OXPVerifier -Isrc/Core/Debug + ADDITIONAL_OBJC_LIBS = -lpng $(LIBJS_BIN_DIR)/libjs.a -lGLU -lGL -lSDL -lSDL_mixer -lgnustep-base + ADDITIONAL_CFLAGS = -DLINUX -DNEED_STRLCPY `sdl-config --cflags` + ADDITIONAL_OBJCFLAGS = -std=c99 -DLOADSAVEGUI -DLINUX -DXP_UNIX -Wno-import `sdl-config --cflags` + oolite_LIB_DIRS += -L/usr/X11R6/lib/ +endif +ifeq ($(libespeak),yes) + ADDITIONAL_OBJC_LIBS += -lespeak + ADDITIONAL_OBJCFLAGS+=-DHAVE_LIBESPEAK=1 + GNUSTEP_OBJ_DIR_NAME := $(GNUSTEP_OBJ_DIR_NAME).spk +endif +ifeq ($(debug),yes) + ADDITIONAL_CFLAGS += -g -O0 + ADDITIONAL_OBJCFLAGS += -g -O0 + GNUSTEP_OBJ_DIR_NAME := $(GNUSTEP_OBJ_DIR_NAME).dbg + ADDITIONAL_CFLAGS += -DDEBUG -DOO_DEBUG -DDEBUG_GRAPHVIZ + ADDITIONAL_OBJCFLAGS += -DDEBUG -DOO_DEBUG -DDEBUG_GRAPHVIZ +endif +ifeq ($(BUILD_WITH_DEBUG_FUNCTIONALITY),no) + ADDITIONAL_CFLAGS += -DNDEBUG + ADDITIONAL_OBJCFLAGS += -DNDEBUG +endif +ifeq ($(PROCEDURAL_PLANETS),yes) + ADDITIONAL_CFLAGS += -DALLOW_PROCEDURAL_PLANETS=1 + ADDITIONAL_OBJCFLAGS += -DALLOW_PROCEDURAL_PLANETS=1 +endif +ifeq ($(DOCKING_CLEARANCE),yes) + ADDITIONAL_CFLAGS += -DDOCKING_CLEARANCE_ENABLED=1 + ADDITIONAL_OBJCFLAGS += -DDOCKING_CLEARANCE_ENABLED=1 +endif +ifeq ($(WORMHOLE_SCANNER),yes) + ADDITIONAL_CFLAGS += -DWORMHOLE_SCANNER=1 + ADDITIONAL_OBJCFLAGS += -DWORMHOLE_SCANNER=1 +endif +ifeq ($(TARGET_INCOMING_MISSILES),yes) + ADDITIONAL_CFLAGS += -DTARGET_INCOMING_MISSILES=1 + ADDITIONAL_OBJCFLAGS += -DTARGET_INCOMING_MISSILES=1 +endif + +ifeq ($(SNAPSHOT_BUILD), yes) + ADDITIONAL_CFLAGS += -DSNAPSHOT_BUILD -DOOLITE_SNAPSHOT_VERSION=\"$(VERSION_STRING)\" + ADDITIONAL_OBJCFLAGS += -DSNAPSHOT_BUILD -DOOLITE_SNAPSHOT_VERSION=\"$(VERSION_STRING)\" +endif + +OBJC_PROGRAM_NAME = oolite + +oolite_C_FILES = \ + legacy_random.c \ + strlcpy.c \ + OOTCPStreamDecoder.c + + +OOLITE_DEBUG_FILES = \ + OODebugMonitor.m \ + OODebugSupport.m \ + OODebugTCPConsoleClient.m \ + OOJSConsole.m \ + OOTCPStreamDecoderAbstractionLayer.m + +OOLITE_ENTITY_FILES = \ + DustEntity.m \ + Entity.m \ + OOEntityWithDrawable.m \ + OOSelfDrawingEntity.m \ + ParticleEntity.m \ + PlanetEntity.m \ + PlayerEntity.m \ + PlayerEntityContracts.m \ + PlayerEntityControls.m \ + PlayerEntityLegacyScriptEngine.m \ + PlayerEntityLoadSave.m \ + PlayerEntityScriptMethods.m \ + PlayerEntitySound.m \ + PlayerEntityStickMapper.m \ + RingEntity.m \ + ShipEntity.m \ + ShipEntityAI.m \ + ShipEntityScriptMethods.m \ + SkyEntity.m \ + StationEntity.m \ + WormholeEntity.m + +OOLITE_GRAPHICS_DRAWABLE_FILES = \ + OODrawable.m \ + OOMesh.m + +OOLITE_GRAPHICS_MATERIAL_FILES = \ + OOBasicMaterial.m \ + OOMaterial.m \ + OONullTexture.m \ + OOPNGTextureLoader.m \ + OOShaderMaterial.m \ + OOShaderProgram.m \ + OOShaderUniform.m \ + OOShaderUniformMethodType.m \ + OOSingleTextureMaterial.m \ + OOTexture.m \ + OOTextureLoader.m \ + OOTextureScaling.m + +OOLITE_GRAPHICS_MISC_FILES = \ + OOCamera.m \ + OOCrosshairs.m \ + OODebugGLDrawing.m \ + OOGraphicsResetManager.m \ + OOLight.m \ + OOOpenGL.m \ + OOOpenGLExtensionManager.m \ + OOProbabilisticTextureManager.m \ + OOSkyDrawable.m \ + OpenGLSprite.m + +OOLITE_MATHS_FILES = \ + CollisionRegion.m \ + Geometry.m \ + Octree.m \ + OOFastArithmetic.m \ + OOMatrix.m \ + OOQuaternion.m \ + OOTriangle.m \ + OOVector.m \ + OOVoxel.m + +OOLITE_OXP_VERIFIER_FILES = \ + OOAIStateMachineVerifierStage.m \ + OOCheckDemoShipsPListVerifierStage.m \ + OOCheckEquipmentPListVerifierStage.m \ + OOCheckRequiresPListVerifierStage.m \ + OOCheckShipDataPListVerifierStage.m \ + OOFileScannerVerifierStage.m \ + OOModelVerifierStage.m \ + OOOXPVerifier.m \ + OOOXPVerifierStage.m \ + OOPListSchemaVerifier.m \ + OOTextureVerifierStage.m + +OOLITE_RSRC_MGMT_FILES = \ + OldSchoolPropertyListWriting.m \ + OOCache.m \ + OOCacheManager.m \ + OOConvertSystemDescriptions.m \ + OOPListParsing.m \ + ResourceManager.m \ + TextureStore.m + +OOLITE_SCRIPTING_FILES = \ + EntityOOJavaScriptExtensions.m \ + OOJavaScriptEngine.m \ + OOJSCall.m \ + OOJSClock.m \ + OOJSEntity.m \ + OOJSEquipmentInfo.m \ + OOJSFunction.m \ + OOJSGlobal.m \ + OOJSMission.m \ + OOJSMissionVariables.m \ + OOJSOolite.m \ + OOJSPlanet.m \ + OOJSPlayer.m \ + OOJSPlayerShip.m \ + OOJSQuaternion.m \ + OOJSScript.m \ + OOJSShip.m \ + OOJSShipGroup.m \ + OOJSSound.m \ + OOJSSoundSource.m \ + OOJSSpecialFunctions.m \ + OOJSStation.m \ + OOJSSun.m \ + OOJSSystem.m \ + OOJSSystemInfo.m \ + OOJSTimer.m \ + OOJSVector.m \ + OOJSWorldScripts.m \ + OOLegacyScriptWhitelist.m \ + OOPListScript.m \ + OOScript.m \ + OOScriptTimer.m + +OOLITE_SOUND_FILES = \ + OOBasicSoundReferencePoint.m \ + OOMusicController.m \ + OOSDLConcreteSound.m \ + OOSDLSound.m \ + OOSDLSoundChannel.m \ + OOSDLSoundMixer.m \ + OOSoundSource.m \ + OOSoundSourcePool.m \ + SDLMusic.m + +OOLITE_UI_FILES = \ + GuiDisplayGen.m \ + HeadUpDisplay.m \ + OOEncodingConverter.m + +OO_UTILITY_FILES = \ + Comparison.m \ + NSDictionaryOOExtensions.m \ + NSFileManagerOOExtensions.m \ + NSMutableDictionaryOOExtensions.m \ + NSScannerOOExtensions.m \ + NSStringOOExtensions.m \ + NSThreadOOExtensions.m \ + OOAsyncQueue.m \ + OOCollectionExtractors.m \ + OOColor.m \ + OOConstToString.m \ + OOCPUInfo.m \ + OOEntityFilterPredicate.m \ + OOExcludeObjectEnumerator.m \ + OOFilteringEnumerator.m \ + OOIsNumberLiteral.m \ + OOLogging.m \ + OOLogHeader.m \ + OOLogOutputHandler.m \ + OOPriorityQueue.m \ + OOProbabilitySet.m \ + OOShipGroup.m \ + OOStringParsing.m \ + OOWeakReference.m \ + OOXMLExtensions.m \ + OODeepCopy.m + +OOLITE_MISC_FILES = \ + AI.m \ + GameController.m \ + JoystickHandler.m \ + main.m \ + MyOpenGLView.m \ + OOCharacter.m \ + OOCocoa.m \ + OOEquipmentType.m \ + OORoleSet.m \ + OOShipRegistry.m \ + OOSpatialReference.m \ + OOTrumble.m \ + Universe.m + +oolite_OBJC_FILES = \ + $(OOLITE_DEBUG_FILES) \ + $(OOLITE_ENTITY_FILES) \ + $(OOLITE_GRAPHICS_DRAWABLE_FILES) \ + $(OOLITE_GRAPHICS_MATERIAL_FILES) \ + $(OOLITE_GRAPHICS_MISC_FILES) \ + $(OOLITE_MATHS_FILES) \ + $(OOLITE_OXP_VERIFIER_FILES) \ + $(OOLITE_RSRC_MGMT_FILES) \ + $(OOLITE_SCRIPTING_FILES) \ + $(OOLITE_SOUND_FILES) \ + $(OOLITE_UI_FILES) \ + $(OO_UTILITY_FILES) \ + $(OOLITE_MISC_FILES) + +include $(GNUSTEP_MAKEFILES)/objc.make +include GNUmakefile.postamble diff --git a/Oolite.xcodeproj/project.pbxproj b/Oolite.xcodeproj/project.pbxproj new file mode 100644 index 00000000..f7175f9d --- /dev/null +++ b/Oolite.xcodeproj/project.pbxproj @@ -0,0 +1,3894 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 44; + objects = { + +/* Begin PBXAggregateTarget section */ + 1AD267500C83050800B4BFD1 /* Build All */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 1AD267680C8305A200B4BFD1 /* Build configuration list for PBXAggregateTarget "Build All" */; + buildPhases = ( + 1AD267610C83057F00B4BFD1 /* Copy Debug OXP */, + ); + dependencies = ( + 1AD267560C83052600B4BFD1 /* PBXTargetDependency */, + 1AD267540C83052200B4BFD1 /* PBXTargetDependency */, + ); + name = "Build All"; + productName = "Build All"; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 083325DD09DDBCDE00F5B8E4 /* OOColor.h in Headers */ = {isa = PBXBuildFile; fileRef = 083325DB09DDBCDE00F5B8E4 /* OOColor.h */; }; + 083325DE09DDBCDE00F5B8E4 /* OOColor.m in Sources */ = {isa = PBXBuildFile; fileRef = 083325DC09DDBCDE00F5B8E4 /* OOColor.m */; }; + 0865427906B8447D000CA0AB /* MainMenu.nib in Resources */ = {isa = PBXBuildFile; fileRef = 29B97319FDCFA39411CA2CEA /* MainMenu.nib */; }; + 0865431C06B8447D000CA0AB /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; + 0865431D06B8447D000CA0AB /* OpenGL.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 06AFA01500A58FB77F000001 /* OpenGL.framework */; }; + 0878FD30086EF845004CB752 /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0878FD2F086EF845004CB752 /* Carbon.framework */; }; + 1A020E0B0D020AFB00C3F51E /* changedScriptHandlers.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A020E0A0D020AFB00C3F51E /* changedScriptHandlers.plist */; }; + 1A02FD460EE0490B008F9B09 /* oolite-tangent-space-vertex.vertex in Copy Shaders */ = {isa = PBXBuildFile; fileRef = 1A02FD340EE048E8008F9B09 /* oolite-tangent-space-vertex.vertex */; }; + 1A0365890D7CA05000B5F46F /* OOSkyDrawable.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A15044A0C12C50D0032F3E8 /* OOSkyDrawable.m */; }; + 1A03658A0D7CA05000B5F46F /* OOSkyDrawable.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A1504490C12C50D0032F3E8 /* OOSkyDrawable.h */; }; + 1A03659B0D7CA0EE00B5F46F /* oolite-nebula-4.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A0365990D7CA0EE00B5F46F /* oolite-nebula-4.png */; }; + 1A0479E90DC9F81000EE1CD0 /* OOShipRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A0479E70DC9F81000EE1CD0 /* OOShipRegistry.h */; }; + 1A0479EA0DC9F81000EE1CD0 /* OOShipRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0479E80DC9F81000EE1CD0 /* OOShipRegistry.m */; }; + 1A047A450DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A047A430DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.h */; }; + 1A047A460DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A047A440DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.m */; }; + 1A047B7E0DCB3D7500EE1CD0 /* OOProbabilitySet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A047B7C0DCB3D7500EE1CD0 /* OOProbabilitySet.h */; }; + 1A047B7F0DCB3D7500EE1CD0 /* OOProbabilitySet.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A047B7D0DCB3D7500EE1CD0 /* OOProbabilitySet.m */; }; + 1A0729D90EF56D1200B0F925 /* OOConvertSystemDescriptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A0729D70EF56D1200B0F925 /* OOConvertSystemDescriptions.h */; }; + 1A0729DA0EF56D1200B0F925 /* OOConvertSystemDescriptions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0729D80EF56D1200B0F925 /* OOConvertSystemDescriptions.m */; }; + 1A0729FE0EF5796500B0F925 /* OldSchoolPropertyListWriting.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A0729FC0EF5796500B0F925 /* OldSchoolPropertyListWriting.h */; }; + 1A0729FF0EF5796500B0F925 /* OldSchoolPropertyListWriting.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0729FD0EF5796500B0F925 /* OldSchoolPropertyListWriting.m */; }; + 1A0DA2EE0D71D280009B0970 /* OOJSSpecialFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A0DA2EC0D71D280009B0970 /* OOJSSpecialFunctions.h */; }; + 1A0DA2EF0D71D280009B0970 /* OOJSSpecialFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A0DA2ED0D71D280009B0970 /* OOJSSpecialFunctions.m */; }; + 1A11F84A0F35F60C001C886C /* OOJSShipGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A11F8480F35F60C001C886C /* OOJSShipGroup.m */; }; + 1A11F84B0F35F60C001C886C /* OOJSShipGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A11F8490F35F60C001C886C /* OOJSShipGroup.h */; }; + 1A1502F60C1201C30032F3E8 /* oolite-unknown-ship.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A1502F50C1201C30032F3E8 /* oolite-unknown-ship.dat */; }; + 1A15049E0C12CA070032F3E8 /* OOProbabilisticTextureManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A15049C0C12CA070032F3E8 /* OOProbabilisticTextureManager.h */; }; + 1A15049F0C12CA070032F3E8 /* OOProbabilisticTextureManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A15049D0C12CA070032F3E8 /* OOProbabilisticTextureManager.m */; }; + 1A1616620D7DCFDC0094AE5B /* OOFilteringEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A1616600D7DCFDC0094AE5B /* OOFilteringEnumerator.h */; }; + 1A1616630D7DCFDC0094AE5B /* OOFilteringEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A1616610D7DCFDC0094AE5B /* OOFilteringEnumerator.m */; }; + 1A1D212E0D2BD4C100F4DEC2 /* bsd_string.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A1D212D0D2BD4C100F4DEC2 /* bsd_string.h */; }; + 1A1E99B60EF04837008B48E2 /* OOProgressBar.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A1E99B40EF04837008B48E2 /* OOProgressBar.h */; }; + 1A1E99B70EF04837008B48E2 /* OOProgressBar.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A1E99B50EF04837008B48E2 /* OOProgressBar.m */; }; + 1A20F7060F36EE0500156DE9 /* OOExcludeObjectEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A20F7040F36EE0500156DE9 /* OOExcludeObjectEnumerator.h */; }; + 1A20F7070F36EE0500156DE9 /* OOExcludeObjectEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A20F7050F36EE0500156DE9 /* OOExcludeObjectEnumerator.m */; }; + 1A21149B0DEA980800444CEB /* oolite-ball-turret.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A21149A0DEA980800444CEB /* oolite-ball-turret.dat */; }; + 1A21149E0DEA98D100444CEB /* oolite-ball-turret.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A21149D0DEA98D100444CEB /* oolite-ball-turret.png */; }; + 1A2315520B9C778400EF0852 /* solar.png in Copy Images */ = {isa = PBXBuildFile; fileRef = 1A23154E0B9C778400EF0852 /* solar.png */; }; + 1A2315530B9C778400EF0852 /* splash.png in Resources */ = {isa = PBXBuildFile; fileRef = 1A23154F0B9C778400EF0852 /* splash.png */; }; + 1A2315540B9C778400EF0852 /* splashback.png in Resources */ = {isa = PBXBuildFile; fileRef = 1A2315500B9C778400EF0852 /* splashback.png */; }; + 1A2315550B9C778400EF0852 /* trumblebox.png in Copy Images */ = {isa = PBXBuildFile; fileRef = 1A2315510B9C778400EF0852 /* trumblebox.png */; }; + 1A2316EE0B9CFAD700EF0852 /* characters.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316DE0B9CFAD700EF0852 /* characters.plist */; }; + 1A2316EF0B9CFAD700EF0852 /* commodities.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316DF0B9CFAD700EF0852 /* commodities.plist */; }; + 1A2316F00B9CFAD700EF0852 /* customsounds.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E00B9CFAD700EF0852 /* customsounds.plist */; }; + 1A2316F10B9CFAD700EF0852 /* demoships.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E10B9CFAD700EF0852 /* demoships.plist */; }; + 1A2316F30B9CFAD700EF0852 /* equipment.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E30B9CFAD700EF0852 /* equipment.plist */; }; + 1A2316F40B9CFAD700EF0852 /* hud-small.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E40B9CFAD700EF0852 /* hud-small.plist */; }; + 1A2316F50B9CFAD700EF0852 /* hud.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E50B9CFAD700EF0852 /* hud.plist */; }; + 1A2316F60B9CFAD700EF0852 /* illegal_goods.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E60B9CFAD700EF0852 /* illegal_goods.plist */; }; + 1A2316F70B9CFAD700EF0852 /* keyconfig.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E70B9CFAD700EF0852 /* keyconfig.plist */; }; + 1A2316F80B9CFAD700EF0852 /* logcontrol.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E80B9CFAD700EF0852 /* logcontrol.plist */; }; + 1A2316F90B9CFAD700EF0852 /* missiontext.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316E90B9CFAD700EF0852 /* missiontext.plist */; }; + 1A2316FB0B9CFAD700EF0852 /* shipdata.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316EB0B9CFAD700EF0852 /* shipdata.plist */; }; + 1A2316FC0B9CFAD800EF0852 /* shipyard.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316EC0B9CFAD700EF0852 /* shipyard.plist */; }; + 1A2316FD0B9CFAD800EF0852 /* speech_pronunciation_guide.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A2316ED0B9CFAD700EF0852 /* speech_pronunciation_guide.plist */; }; + 1A2317B30B9D022500EF0852 /* buoyAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317910B9D022400EF0852 /* buoyAI.plist */; }; + 1A2317B40B9D022500EF0852 /* collectLootAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317920B9D022500EF0852 /* collectLootAI.plist */; }; + 1A2317B50B9D022500EF0852 /* dockingAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317930B9D022500EF0852 /* dockingAI.plist */; }; + 1A2317B60B9D022500EF0852 /* dumbAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317940B9D022500EF0852 /* dumbAI.plist */; }; + 1A2317B70B9D022500EF0852 /* enteringPirateAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317950B9D022500EF0852 /* enteringPirateAI.plist */; }; + 1A2317B80B9D022500EF0852 /* enteringTraderAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317960B9D022500EF0852 /* enteringTraderAI.plist */; }; + 1A2317B90B9D022500EF0852 /* escortAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317970B9D022500EF0852 /* escortAI.plist */; }; + 1A2317BA0B9D022500EF0852 /* exitingTraderAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317980B9D022500EF0852 /* exitingTraderAI.plist */; }; + 1A2317BB0B9D022500EF0852 /* fallingShuttleAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317990B9D022500EF0852 /* fallingShuttleAI.plist */; }; + 1A2317BC0B9D022500EF0852 /* fttAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A23179A0B9D022500EF0852 /* fttAI.plist */; }; + 1A2317BD0B9D022500EF0852 /* gotoWaypointAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A23179B0B9D022500EF0852 /* gotoWaypointAI.plist */; }; + 1A2317BE0B9D022500EF0852 /* hardMissileAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A23179C0B9D022500EF0852 /* hardMissileAI.plist */; }; + 1A2317BF0B9D022500EF0852 /* homeAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A23179D0B9D022500EF0852 /* homeAI.plist */; }; + 1A2317C00B9D022500EF0852 /* interceptAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A23179E0B9D022500EF0852 /* interceptAI.plist */; }; + 1A2317C10B9D022500EF0852 /* minerAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A23179F0B9D022500EF0852 /* minerAI.plist */; }; + 1A2317C20B9D022500EF0852 /* missileAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A00B9D022500EF0852 /* missileAI.plist */; }; + 1A2317C30B9D022500EF0852 /* nullAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A10B9D022500EF0852 /* nullAI.plist */; }; + 1A2317C40B9D022500EF0852 /* pirateAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A20B9D022500EF0852 /* pirateAI.plist */; }; + 1A2317C50B9D022500EF0852 /* planetPatrolAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A30B9D022500EF0852 /* planetPatrolAI.plist */; }; + 1A2317C60B9D022500EF0852 /* policeInterceptAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A40B9D022500EF0852 /* policeInterceptAI.plist */; }; + 1A2317C70B9D022500EF0852 /* receiveDockingAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A50B9D022500EF0852 /* receiveDockingAI.plist */; }; + 1A2317C80B9D022500EF0852 /* risingShuttleAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A60B9D022500EF0852 /* risingShuttleAI.plist */; }; + 1A2317C90B9D022500EF0852 /* rockHermitAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A70B9D022500EF0852 /* rockHermitAI.plist */; }; + 1A2317CA0B9D022500EF0852 /* route1patrolAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A80B9D022500EF0852 /* route1patrolAI.plist */; }; + 1A2317CB0B9D022500EF0852 /* route1traderAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317A90B9D022500EF0852 /* route1traderAI.plist */; }; + 1A2317CC0B9D022500EF0852 /* route2patrolAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317AA0B9D022500EF0852 /* route2patrolAI.plist */; }; + 1A2317CD0B9D022500EF0852 /* route2sunskimAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317AB0B9D022500EF0852 /* route2sunskimAI.plist */; }; + 1A2317CE0B9D022500EF0852 /* scavengerAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317AC0B9D022500EF0852 /* scavengerAI.plist */; }; + 1A2317D00B9D022500EF0852 /* stationAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317AE0B9D022500EF0852 /* stationAI.plist */; }; + 1A2317D10B9D022500EF0852 /* sunSkimExitAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317AF0B9D022500EF0852 /* sunSkimExitAI.plist */; }; + 1A2317D20B9D022500EF0852 /* thargletAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317B00B9D022500EF0852 /* thargletAI.plist */; }; + 1A2317D30B9D022500EF0852 /* thargoidAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317B10B9D022500EF0852 /* thargoidAI.plist */; }; + 1A2317D40B9D022500EF0852 /* timebombAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A2317B20B9D022500EF0852 /* timebombAI.plist */; }; + 1A23185F0B9D026D00EF0852 /* adder_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23181E0B9D026D00EF0852 /* adder_redux.png */; }; + 1A2318600B9D026D00EF0852 /* anaconda_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23181F0B9D026D00EF0852 /* anaconda_redux.png */; }; + 1A2318610B9D026D00EF0852 /* arcdetail.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318200B9D026D00EF0852 /* arcdetail.png */; }; + 1A2318630B9D026D00EF0852 /* asp_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318220B9D026D00EF0852 /* asp_redux.png */; }; + 1A2318640B9D026D00EF0852 /* asteroid.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318230B9D026D00EF0852 /* asteroid.png */; }; + 1A2318650B9D026D00EF0852 /* back_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318240B9D026D00EF0852 /* back_metal.png */; }; + 1A2318660B9D026D00EF0852 /* barrel_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318250B9D026D00EF0852 /* barrel_metal.png */; }; + 1A2318670B9D026D00EF0852 /* blur256.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318260B9D026D00EF0852 /* blur256.png */; }; + 1A2318680B9D026D00EF0852 /* boa2_left.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318270B9D026D00EF0852 /* boa2_left.png */; }; + 1A2318690B9D026D00EF0852 /* boa2_rear.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318280B9D026D00EF0852 /* boa2_rear.png */; }; + 1A23186A0B9D026D00EF0852 /* boa2_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318290B9D026D00EF0852 /* boa2_redux.png */; }; + 1A23186B0B9D026D00EF0852 /* boa2_right.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23182A0B9D026D00EF0852 /* boa2_right.png */; }; + 1A23186C0B9D026D00EF0852 /* boa2_top.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23182B0B9D026D00EF0852 /* boa2_top.png */; }; + 1A23186D0B9D026D00EF0852 /* boa_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23182C0B9D026D00EF0852 /* boa_redux.png */; }; + 1A23186E0B9D026D00EF0852 /* bottom_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23182D0B9D026D00EF0852 /* bottom_metal.png */; }; + 1A23186F0B9D026D00EF0852 /* cobra1_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23182E0B9D026D00EF0852 /* cobra1_redux.png */; }; + 1A2318700B9D026D00EF0852 /* cobra1_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23182F0B9D026D00EF0852 /* cobra1_redux1.png */; }; + 1A2318710B9D026D00EF0852 /* cobra1_redux2.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318300B9D026D00EF0852 /* cobra1_redux2.png */; }; + 1A2318720B9D026D00EF0852 /* cobra1miner_top.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318310B9D026D00EF0852 /* cobra1miner_top.png */; }; + 1A2318730B9D026D00EF0852 /* cobra3_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318320B9D026D00EF0852 /* cobra3_redux.png */; }; + 1A2318740B9D026D00EF0852 /* cobra3_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318330B9D026D00EF0852 /* cobra3_redux1.png */; }; + 1A2318750B9D026D00EF0852 /* constrictor_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318340B9D026D00EF0852 /* constrictor_redux.png */; }; + 1A2318760B9D026D00EF0852 /* dark_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318350B9D026D00EF0852 /* dark_metal.png */; }; + 1A2318770B9D026D00EF0852 /* ferdelance_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318360B9D026D00EF0852 /* ferdelance_redux.png */; }; + 1A2318780B9D026D00EF0852 /* flare256.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318370B9D026D00EF0852 /* flare256.png */; }; + 1A2318790B9D026D00EF0852 /* front_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318380B9D026D00EF0852 /* front_metal.png */; }; + 1A23187B0B9D026D00EF0852 /* gecko_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23183A0B9D026D00EF0852 /* gecko_redux.png */; }; + 1A23187C0B9D026D00EF0852 /* krait_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23183B0B9D026D00EF0852 /* krait_redux.png */; }; + 1A23187D0B9D026D00EF0852 /* left_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23183C0B9D026D00EF0852 /* left_metal.png */; }; + 1A23187E0B9D026D00EF0852 /* mamba_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23183D0B9D026D00EF0852 /* mamba_redux.png */; }; + 1A23187F0B9D026D00EF0852 /* mamba_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23183E0B9D026D00EF0852 /* mamba_redux1.png */; }; + 1A2318800B9D026D00EF0852 /* metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23183F0B9D026D00EF0852 /* metal.png */; }; + 1A2318810B9D026D00EF0852 /* missile.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318400B9D026D00EF0852 /* missile.png */; }; + 1A2318820B9D026D00EF0852 /* moray_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318410B9D026D00EF0852 /* moray_redux.png */; }; + 1A2318830B9D026D00EF0852 /* moray_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318420B9D026D00EF0852 /* moray_redux1.png */; }; + 1A2318840B9D026D00EF0852 /* MorayMED_bottom.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318430B9D026D00EF0852 /* MorayMED_bottom.png */; }; + 1A2318850B9D026D00EF0852 /* MorayMED_top.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318440B9D026D00EF0852 /* MorayMED_top.png */; }; + 1A23188A0B9D026D00EF0852 /* pod2_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318490B9D026D00EF0852 /* pod2_redux.png */; }; + 1A23188B0B9D026D00EF0852 /* python_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23184A0B9D026D00EF0852 /* python_redux.png */; }; + 1A23188C0B9D026D00EF0852 /* python_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23184B0B9D026D00EF0852 /* python_redux1.png */; }; + 1A23188D0B9D026D00EF0852 /* python_redux2.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23184C0B9D026D00EF0852 /* python_redux2.png */; }; + 1A23188E0B9D026D00EF0852 /* qbomb.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23184D0B9D026D00EF0852 /* qbomb.png */; }; + 1A23188F0B9D026D00EF0852 /* right_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23184E0B9D026D00EF0852 /* right_metal.png */; }; + 1A2318900B9D026D00EF0852 /* scarred_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23184F0B9D026D00EF0852 /* scarred_metal.png */; }; + 1A2318910B9D026D00EF0852 /* shuttle_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318500B9D026D00EF0852 /* shuttle_redux.png */; }; + 1A2318920B9D026D00EF0852 /* sidewinder_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318510B9D026D00EF0852 /* sidewinder_redux.png */; }; + 1A2318940B9D026D00EF0852 /* target_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318530B9D026D00EF0852 /* target_metal.png */; }; + 1A2318950B9D026D00EF0852 /* thargoid_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318540B9D026D00EF0852 /* thargoid_redux.png */; }; + 1A2318960B9D026D00EF0852 /* thargon_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318550B9D026D00EF0852 /* thargon_redux.png */; }; + 1A2318970B9D026D00EF0852 /* top_metal.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318560B9D026D00EF0852 /* top_metal.png */; }; + 1A2318980B9D026D00EF0852 /* transporter_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318570B9D026D00EF0852 /* transporter_redux.png */; }; + 1A2318990B9D026D00EF0852 /* transporter_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318580B9D026D00EF0852 /* transporter_redux1.png */; }; + 1A23189A0B9D026D00EF0852 /* trumblekit.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A2318590B9D026D00EF0852 /* trumblekit.png */; }; + 1A23189B0B9D026D00EF0852 /* viper_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23185A0B9D026D00EF0852 /* viper_redux.png */; }; + 1A23189C0B9D026D00EF0852 /* viperi_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23185B0B9D026D00EF0852 /* viperi_redux.png */; }; + 1A23189D0B9D026D00EF0852 /* worm_redux.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23185C0B9D026D00EF0852 /* worm_redux.png */; }; + 1A23189E0B9D026D00EF0852 /* worm_redux1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23185D0B9D026D00EF0852 /* worm_redux1.png */; }; + 1A23189F0B9D026D00EF0852 /* wreck.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A23185E0B9D026D00EF0852 /* wreck.png */; }; + 1A2318E50B9D02AA00EF0852 /* adder_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318A60B9D02A900EF0852 /* adder_redux.dat */; }; + 1A2318E60B9D02AA00EF0852 /* alloy.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318A70B9D02A900EF0852 /* alloy.dat */; }; + 1A2318E70B9D02AA00EF0852 /* anaconda_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318A80B9D02A900EF0852 /* anaconda_redux.dat */; }; + 1A2318E80B9D02AA00EF0852 /* arcdetail.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318A90B9D02A900EF0852 /* arcdetail.dat */; }; + 1A2318E90B9D02AA00EF0852 /* asp_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318AA0B9D02A900EF0852 /* asp_redux.dat */; }; + 1A2318EA0B9D02AA00EF0852 /* asteroid.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318AB0B9D02A900EF0852 /* asteroid.dat */; }; + 1A2318EB0B9D02AA00EF0852 /* asteroid1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318AC0B9D02A900EF0852 /* asteroid1.dat */; }; + 1A2318EC0B9D02AA00EF0852 /* barrel.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318AD0B9D02A900EF0852 /* barrel.dat */; }; + 1A2318ED0B9D02AA00EF0852 /* boa2_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318AE0B9D02A900EF0852 /* boa2_redux.dat */; }; + 1A2318EE0B9D02AA00EF0852 /* boa_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318AF0B9D02A900EF0852 /* boa_redux.dat */; }; + 1A2318EF0B9D02AA00EF0852 /* boulder.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B00B9D02A900EF0852 /* boulder.dat */; }; + 1A2318F00B9D02AA00EF0852 /* boulder1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B10B9D02A900EF0852 /* boulder1.dat */; }; + 1A2318F10B9D02AA00EF0852 /* buoy.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B20B9D02A900EF0852 /* buoy.dat */; }; + 1A2318F20B9D02AA00EF0852 /* cobra1_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B30B9D02A900EF0852 /* cobra1_redux.dat */; }; + 1A2318F30B9D02AA00EF0852 /* cobra1_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B40B9D02A900EF0852 /* cobra1_redux1.dat */; }; + 1A2318F40B9D02AA00EF0852 /* cobra1_redux2.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B50B9D02A900EF0852 /* cobra1_redux2.dat */; }; + 1A2318F50B9D02AA00EF0852 /* cobra3_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B60B9D02A900EF0852 /* cobra3_redux.dat */; }; + 1A2318F60B9D02AA00EF0852 /* cobra3_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B70B9D02A900EF0852 /* cobra3_redux1.dat */; }; + 1A2318F70B9D02AA00EF0852 /* constrictor_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B80B9D02A900EF0852 /* constrictor_redux.dat */; }; + 1A2318F80B9D02AA00EF0852 /* dock-flat.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318B90B9D02A900EF0852 /* dock-flat.dat */; }; + 1A2318F90B9D02AA00EF0852 /* dock.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318BA0B9D02A900EF0852 /* dock.dat */; }; + 1A2318FA0B9D02AA00EF0852 /* escpod_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318BB0B9D02A900EF0852 /* escpod_redux.dat */; }; + 1A2318FB0B9D02AA00EF0852 /* exhaust.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318BC0B9D02A900EF0852 /* exhaust.dat */; }; + 1A2318FC0B9D02AA00EF0852 /* ferdelance_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318BD0B9D02A900EF0852 /* ferdelance_redux.dat */; }; + 1A2318FD0B9D02AA00EF0852 /* gecko_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318BE0B9D02A900EF0852 /* gecko_redux.dat */; }; + 1A2318FE0B9D02AA00EF0852 /* icosahedron.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318BF0B9D02A900EF0852 /* icosahedron.dat */; }; + 1A2318FF0B9D02AA00EF0852 /* icostextured.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C00B9D02A900EF0852 /* icostextured.dat */; }; + 1A2319000B9D02AA00EF0852 /* krait_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C10B9D02A900EF0852 /* krait_redux.dat */; }; + 1A2319010B9D02AA00EF0852 /* mamba_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C20B9D02A900EF0852 /* mamba_redux.dat */; }; + 1A2319020B9D02AA00EF0852 /* mamba_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C30B9D02A900EF0852 /* mamba_redux1.dat */; }; + 1A2319030B9D02AA00EF0852 /* missile.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C40B9D02A900EF0852 /* missile.dat */; }; + 1A2319040B9D02AA00EF0852 /* moray_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C50B9D02A900EF0852 /* moray_redux.dat */; }; + 1A2319050B9D02AA00EF0852 /* moray_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C60B9D02A900EF0852 /* moray_redux1.dat */; }; + 1A2319060B9D02AA00EF0852 /* new-dodo.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C70B9D02A900EF0852 /* new-dodo.dat */; }; + 1A2319070B9D02AA00EF0852 /* new-icos.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C80B9D02A900EF0852 /* new-icos.dat */; }; + 1A2319080B9D02AA00EF0852 /* new-rock.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318C90B9D02A900EF0852 /* new-rock.dat */; }; + 1A2319090B9D02AA00EF0852 /* new_coriolis.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318CA0B9D02A900EF0852 /* new_coriolis.dat */; }; + 1A23190A0B9D02AA00EF0852 /* python_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318CB0B9D02A900EF0852 /* python_redux.dat */; }; + 1A23190B0B9D02AA00EF0852 /* python_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318CC0B9D02A900EF0852 /* python_redux1.dat */; }; + 1A23190C0B9D02AA00EF0852 /* python_redux2.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318CD0B9D02AA00EF0852 /* python_redux2.dat */; }; + 1A23190D0B9D02AA00EF0852 /* qbomb.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318CE0B9D02AA00EF0852 /* qbomb.dat */; }; + 1A23190E0B9D02AA00EF0852 /* ring.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318CF0B9D02AA00EF0852 /* ring.dat */; }; + 1A23190F0B9D02AA00EF0852 /* rock-box.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D00B9D02AA00EF0852 /* rock-box.dat */; }; + 1A2319100B9D02AA00EF0852 /* rock-dock.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D10B9D02AA00EF0852 /* rock-dock.dat */; }; + 1A2319110B9D02AA00EF0852 /* scarred_alloy.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D20B9D02AA00EF0852 /* scarred_alloy.dat */; }; + 1A2319120B9D02AA00EF0852 /* shuttle_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D30B9D02AA00EF0852 /* shuttle_redux.dat */; }; + 1A2319130B9D02AA00EF0852 /* sidewinder_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D40B9D02AA00EF0852 /* sidewinder_redux.dat */; }; + 1A2319140B9D02AA00EF0852 /* splinter.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D50B9D02AA00EF0852 /* splinter.dat */; }; + 1A2319150B9D02AA00EF0852 /* splinter1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D60B9D02AA00EF0852 /* splinter1.dat */; }; + 1A2319160B9D02AA00EF0852 /* strut.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D70B9D02AA00EF0852 /* strut.dat */; }; + 1A2319170B9D02AA00EF0852 /* thargoid_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D80B9D02AA00EF0852 /* thargoid_redux.dat */; }; + 1A2319180B9D02AA00EF0852 /* thargon_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318D90B9D02AA00EF0852 /* thargon_redux.dat */; }; + 1A2319190B9D02AA00EF0852 /* transporter_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318DA0B9D02AA00EF0852 /* transporter_redux.dat */; }; + 1A23191A0B9D02AA00EF0852 /* transporter_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318DB0B9D02AA00EF0852 /* transporter_redux1.dat */; }; + 1A23191B0B9D02AA00EF0852 /* viper_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318DC0B9D02AA00EF0852 /* viper_redux.dat */; }; + 1A23191C0B9D02AA00EF0852 /* viperi_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318DD0B9D02AA00EF0852 /* viperi_redux.dat */; }; + 1A23191D0B9D02AA00EF0852 /* worm_redux.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318DE0B9D02AA00EF0852 /* worm_redux.dat */; }; + 1A23191E0B9D02AA00EF0852 /* worm_redux1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318DF0B9D02AA00EF0852 /* worm_redux1.dat */; }; + 1A23191F0B9D02AA00EF0852 /* wreck1.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318E00B9D02AA00EF0852 /* wreck1.dat */; }; + 1A2319200B9D02AA00EF0852 /* wreck2.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318E10B9D02AA00EF0852 /* wreck2.dat */; }; + 1A2319210B9D02AA00EF0852 /* wreck3.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318E20B9D02AA00EF0852 /* wreck3.dat */; }; + 1A2319220B9D02AA00EF0852 /* wreck4.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318E30B9D02AA00EF0852 /* wreck4.dat */; }; + 1A2319230B9D02AA00EF0852 /* wreck5.dat in Copy Models */ = {isa = PBXBuildFile; fileRef = 1A2318E40B9D02AA00EF0852 /* wreck5.dat */; }; + 1A2319760B9D02F900EF0852 /* BlueDanube.ogg in Copy Music */ = {isa = PBXBuildFile; fileRef = 1A2319740B9D02F900EF0852 /* BlueDanube.ogg */; }; + 1A2319770B9D02F900EF0852 /* OoliteTheme.ogg in Copy Music */ = {isa = PBXBuildFile; fileRef = 1A2319750B9D02F900EF0852 /* OoliteTheme.ogg */; }; + 1A2319A60B9D031D00EF0852 /* afterburner1.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319920B9D031D00EF0852 /* afterburner1.ogg */; }; + 1A2319A80B9D031D00EF0852 /* beep.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319940B9D031D00EF0852 /* beep.ogg */; }; + 1A2319A90B9D031D00EF0852 /* bigbang.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319950B9D031D00EF0852 /* bigbang.ogg */; }; + 1A2319AA0B9D031D00EF0852 /* boop.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319960B9D031D00EF0852 /* boop.ogg */; }; + 1A2319AB0B9D031D00EF0852 /* breakpattern.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319970B9D031D00EF0852 /* breakpattern.ogg */; }; + 1A2319AC0B9D031D00EF0852 /* buy.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319980B9D031D00EF0852 /* buy.ogg */; }; + 1A2319AD0B9D031D00EF0852 /* ecm.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319990B9D031D00EF0852 /* ecm.ogg */; }; + 1A2319AE0B9D031D00EF0852 /* guiclick.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A23199A0B9D031D00EF0852 /* guiclick.ogg */; }; + 1A2319AF0B9D031D00EF0852 /* hit.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A23199B0B9D031D00EF0852 /* hit.ogg */; }; + 1A2319B00B9D031D00EF0852 /* hullbang.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A23199C0B9D031D00EF0852 /* hullbang.ogg */; }; + 1A2319B10B9D031D00EF0852 /* laser.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A23199D0B9D031D00EF0852 /* laser.ogg */; }; + 1A2319B20B9D031D00EF0852 /* laserhits.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A23199E0B9D031D00EF0852 /* laserhits.ogg */; }; + 1A2319B30B9D031D00EF0852 /* missile.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A23199F0B9D031D00EF0852 /* missile.ogg */; }; + 1A2319B40B9D031D00EF0852 /* scoop.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319A00B9D031D00EF0852 /* scoop.ogg */; }; + 1A2319B50B9D031D00EF0852 /* sell.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319A10B9D031D00EF0852 /* sell.ogg */; }; + 1A2319B60B9D031D00EF0852 /* trumble.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319A20B9D031D00EF0852 /* trumble.ogg */; }; + 1A2319B70B9D031D00EF0852 /* trumblesqueal.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319A30B9D031D00EF0852 /* trumblesqueal.ogg */; }; + 1A2319B80B9D031D00EF0852 /* warning.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319A40B9D031D00EF0852 /* warning.ogg */; }; + 1A2319B90B9D031D00EF0852 /* witchabort.ogg in Copy Sounds */ = {isa = PBXBuildFile; fileRef = 1A2319A50B9D031D00EF0852 /* witchabort.ogg */; }; + 1A231A180B9D8B1B00EF0852 /* OOCacheManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A231A160B9D8B1B00EF0852 /* OOCacheManager.h */; }; + 1A26D0AC0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0880BCF9CF70073F257 /* PlayerEntityLegacyScriptEngine.m */; }; + 1A26D0AD0BCF9CF80073F257 /* ShipEntityAI.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0890BCF9CF70073F257 /* ShipEntityAI.m */; }; + 1A26D0AE0BCF9CF80073F257 /* ShipEntityAI.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D08A0BCF9CF70073F257 /* ShipEntityAI.h */; }; + 1A26D0AF0BCF9CF80073F257 /* PlayerEntityScriptMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D08B0BCF9CF70073F257 /* PlayerEntityScriptMethods.m */; }; + 1A26D0B00BCF9CF80073F257 /* PlayerEntityLoadSave.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D08C0BCF9CF70073F257 /* PlayerEntityLoadSave.m */; }; + 1A26D0B10BCF9CF80073F257 /* PlayerEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D08D0BCF9CF70073F257 /* PlayerEntity.m */; }; + 1A26D0B20BCF9CF80073F257 /* PlayerEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D08E0BCF9CF70073F257 /* PlayerEntity.h */; }; + 1A26D0B30BCF9CF80073F257 /* ShipEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D08F0BCF9CF70073F257 /* ShipEntity.m */; }; + 1A26D0B40BCF9CF80073F257 /* ShipEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0900BCF9CF70073F257 /* ShipEntity.h */; }; + 1A26D0B50BCF9CF80073F257 /* PlayerEntityScriptMethods.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0910BCF9CF70073F257 /* PlayerEntityScriptMethods.h */; }; + 1A26D0B60BCF9CF80073F257 /* SkyEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0920BCF9CF70073F257 /* SkyEntity.m */; }; + 1A26D0B70BCF9CF80073F257 /* SkyEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0930BCF9CF70073F257 /* SkyEntity.h */; }; + 1A26D0B80BCF9CF80073F257 /* RingEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0940BCF9CF70073F257 /* RingEntity.m */; }; + 1A26D0B90BCF9CF80073F257 /* RingEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0950BCF9CF70073F257 /* RingEntity.h */; }; + 1A26D0BA0BCF9CF80073F257 /* ParticleEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0960BCF9CF70073F257 /* ParticleEntity.m */; }; + 1A26D0BB0BCF9CF80073F257 /* ParticleEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0970BCF9CF70073F257 /* ParticleEntity.h */; }; + 1A26D0BC0BCF9CF80073F257 /* PlayerEntityContracts.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0980BCF9CF70073F257 /* PlayerEntityContracts.h */; }; + 1A26D0BD0BCF9CF80073F257 /* PlanetEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0990BCF9CF70073F257 /* PlanetEntity.m */; }; + 1A26D0BE0BCF9CF80073F257 /* PlanetEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D09A0BCF9CF70073F257 /* PlanetEntity.h */; }; + 1A26D0BF0BCF9CF80073F257 /* PlayerEntityContracts.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D09B0BCF9CF70073F257 /* PlayerEntityContracts.m */; }; + 1A26D0C00BCF9CF80073F257 /* PlayerEntityControls.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D09C0BCF9CF70073F257 /* PlayerEntityControls.m */; }; + 1A26D0C10BCF9CF80073F257 /* Entity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D09D0BCF9CF70073F257 /* Entity.h */; }; + 1A26D0C40BCF9CF80073F257 /* WormholeEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0A00BCF9CF70073F257 /* WormholeEntity.m */; }; + 1A26D0C50BCF9CF80073F257 /* WormholeEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0A10BCF9CF70073F257 /* WormholeEntity.h */; }; + 1A26D0C60BCF9CF80073F257 /* StationEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0A20BCF9CF70073F257 /* StationEntity.m */; }; + 1A26D0C70BCF9CF80073F257 /* StationEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0A30BCF9CF70073F257 /* StationEntity.h */; }; + 1A26D0C80BCF9CF80073F257 /* PlayerEntityControls.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0A40BCF9CF70073F257 /* PlayerEntityControls.h */; }; + 1A26D0C90BCF9CF80073F257 /* PlayerEntitySound.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0A50BCF9CF70073F257 /* PlayerEntitySound.m */; }; + 1A26D0CA0BCF9CF80073F257 /* PlayerEntitySound.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0A60BCF9CF70073F257 /* PlayerEntitySound.h */; }; + 1A26D0CB0BCF9CF80073F257 /* DustEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0A70BCF9CF70073F257 /* DustEntity.h */; }; + 1A26D0CC0BCF9CF80073F257 /* Entity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0A80BCF9CF70073F257 /* Entity.m */; }; + 1A26D0CD0BCF9CF80073F257 /* DustEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0A90BCF9CF80073F257 /* DustEntity.m */; }; + 1A26D0CE0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0AA0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.h */; }; + 1A26D0CF0BCF9CF80073F257 /* PlayerEntityLoadSave.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0AB0BCF9CF80073F257 /* PlayerEntityLoadSave.h */; }; + 1A26D0D40BCF9D0D0073F257 /* OOShaderMaterial.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0D00BCF9D0D0073F257 /* OOShaderMaterial.h */; }; + 1A26D0D50BCF9D0D0073F257 /* OOShaderMaterial.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0D10BCF9D0D0073F257 /* OOShaderMaterial.m */; }; + 1A26D0D60BCF9D0D0073F257 /* OOMaterial.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0D20BCF9D0D0073F257 /* OOMaterial.m */; }; + 1A26D0D70BCF9D0D0073F257 /* OOMaterial.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0D30BCF9D0D0073F257 /* OOMaterial.h */; }; + 1A26D0DC0BCF9D1E0073F257 /* OOShaderUniform.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0D80BCF9D1E0073F257 /* OOShaderUniform.h */; }; + 1A26D0DD0BCF9D1E0073F257 /* OOShaderProgram.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0D90BCF9D1E0073F257 /* OOShaderProgram.m */; }; + 1A26D0DE0BCF9D1E0073F257 /* OOShaderProgram.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0DA0BCF9D1E0073F257 /* OOShaderProgram.h */; }; + 1A26D0DF0BCF9D1E0073F257 /* OOShaderUniform.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0DB0BCF9D1E0073F257 /* OOShaderUniform.m */; }; + 1A26D0E60BCF9D3B0073F257 /* OOTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0E00BCF9D3B0073F257 /* OOTexture.m */; }; + 1A26D0E70BCF9D3B0073F257 /* OOTexture.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0E10BCF9D3B0073F257 /* OOTexture.h */; }; + 1A26D0E80BCF9D3B0073F257 /* OOPNGTextureLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0E20BCF9D3B0073F257 /* OOPNGTextureLoader.m */; }; + 1A26D0E90BCF9D3B0073F257 /* OOTextureLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0E30BCF9D3B0073F257 /* OOTextureLoader.h */; }; + 1A26D0EA0BCF9D3B0073F257 /* OOPNGTextureLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0E40BCF9D3B0073F257 /* OOPNGTextureLoader.h */; }; + 1A26D0EB0BCF9D3B0073F257 /* OOTextureLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A26D0E50BCF9D3B0073F257 /* OOTextureLoader.m */; }; + 1A26D0F50BCF9D8D0073F257 /* pngusr.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0F40BCF9D8D0073F257 /* pngusr.h */; }; + 1A26D0F60BCF9D8D0073F257 /* pngusr.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A26D0F40BCF9D8D0073F257 /* pngusr.h */; }; + 1A27DB3B0C4E349F00CB4CE8 /* OOOXPVerifierStageInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A27DB380C4E349F00CB4CE8 /* OOOXPVerifierStageInternal.h */; }; + 1A27DB3C0C4E349F00CB4CE8 /* OOOXPVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A27DB390C4E349F00CB4CE8 /* OOOXPVerifierStage.h */; }; + 1A27DB3D0C4E349F00CB4CE8 /* OOOXPVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A27DB3A0C4E349F00CB4CE8 /* OOOXPVerifierStage.m */; }; + 1A27DB420C4E34B300CB4CE8 /* OOFileScannerVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A27DB400C4E34B300CB4CE8 /* OOFileScannerVerifierStage.h */; }; + 1A27DB430C4E34B300CB4CE8 /* OOFileScannerVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A27DB410C4E34B300CB4CE8 /* OOFileScannerVerifierStage.m */; }; + 1A28AA160D55438200BC0CE4 /* OOJSSound.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A28AA140D55438200BC0CE4 /* OOJSSound.h */; }; + 1A28AA170D55438200BC0CE4 /* OOJSSound.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A28AA150D55438200BC0CE4 /* OOJSSound.m */; }; + 1A29967E0B9F064C002D2149 /* OOCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A29967C0B9F064C002D2149 /* OOCache.h */; }; + 1A29967F0B9F064C002D2149 /* OOCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A29967D0B9F064C002D2149 /* OOCache.m */; }; + 1A2A16680BD10B1200152975 /* OOSingleTextureMaterial.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A16660BD10B1200152975 /* OOSingleTextureMaterial.m */; }; + 1A2A16690BD10B1200152975 /* OOSingleTextureMaterial.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A16670BD10B1200152975 /* OOSingleTextureMaterial.h */; }; + 1A2A17D60BD1587D00152975 /* OOCPUInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A17D40BD1587D00152975 /* OOCPUInfo.h */; }; + 1A2A17D70BD1587D00152975 /* OOCPUInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A17D50BD1587D00152975 /* OOCPUInfo.m */; }; + 1A2A1B090BD276A900152975 /* OOEntityWithDrawable.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A1B050BD276A900152975 /* OOEntityWithDrawable.h */; }; + 1A2A1B0A0BD276A900152975 /* OOEntityWithDrawable.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A1B060BD276A900152975 /* OOEntityWithDrawable.m */; }; + 1A2A1B160BD2774300152975 /* OODrawable.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A1B120BD2774300152975 /* OODrawable.h */; }; + 1A2A1B170BD2774300152975 /* OODrawable.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A1B130BD2774300152975 /* OODrawable.m */; }; + 1A2A1B2C0BD277D800152975 /* OOSelfDrawingEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A1B280BD277D800152975 /* OOSelfDrawingEntity.h */; }; + 1A2A1B2D0BD277D800152975 /* OOSelfDrawingEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A1B290BD277D800152975 /* OOSelfDrawingEntity.m */; }; + 1A2A1CAC0BD2914F00152975 /* OOMesh.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A1CA80BD2914F00152975 /* OOMesh.h */; }; + 1A2A1CAD0BD2914F00152975 /* OOMesh.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A1CA90BD2914F00152975 /* OOMesh.m */; }; + 1A2A1DEC0BD2A28E00152975 /* OOMacroOpenGL.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A1DEA0BD2A28E00152975 /* OOMacroOpenGL.h */; }; + 1A2A8C150BC65FFD001E00FB /* OOJSEntity.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A8C130BC65FFD001E00FB /* OOJSEntity.h */; }; + 1A2A8C160BC65FFD001E00FB /* OOJSEntity.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A8C140BC65FFD001E00FB /* OOJSEntity.m */; }; + 1A2A8D3A0BC6765F001E00FB /* EntityOOJavaScriptExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A8D380BC6765F001E00FB /* EntityOOJavaScriptExtensions.h */; }; + 1A2A8D3B0BC6765F001E00FB /* EntityOOJavaScriptExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A8D390BC6765F001E00FB /* EntityOOJavaScriptExtensions.m */; }; + 1A2A8E030BC67CCC001E00FB /* OOWeakReference.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A8E010BC67CCC001E00FB /* OOWeakReference.h */; }; + 1A2A8E040BC67CCC001E00FB /* OOWeakReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A8E020BC67CCC001E00FB /* OOWeakReference.m */; }; + 1A2A91520BC6BC66001E00FB /* OOJSQuaternion.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2A91500BC6BC66001E00FB /* OOJSQuaternion.h */; }; + 1A2A91530BC6BC66001E00FB /* OOJSQuaternion.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2A91510BC6BC66001E00FB /* OOJSQuaternion.m */; }; + 1A2DA2AB0CB4CB5C00DE6823 /* OODebugTCPConsoleProtocol.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA2A40CB4CB5C00DE6823 /* OODebugTCPConsoleProtocol.h */; }; + 1A2DA2AE0CB4CB5C00DE6823 /* OODebugTCPConsoleClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA2A70CB4CB5C00DE6823 /* OODebugTCPConsoleClient.h */; }; + 1A2DA2AF0CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA2A80CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.h */; }; + 1A2DA2B00CB4CB5C00DE6823 /* OOTCPStreamDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA2A90CB4CB5C00DE6823 /* OOTCPStreamDecoder.h */; }; + 1A2DA34D0CB4D0D800DE6823 /* OOJSConsole.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA3490CB4D0D800DE6823 /* OOJSConsole.h */; }; + 1A2DA34E0CB4D0D800DE6823 /* OODebugMonitor.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA34A0CB4D0D800DE6823 /* OODebugMonitor.h */; }; + 1A2DA3550CB4D10800DE6823 /* OODebuggerInterface.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA3540CB4D10800DE6823 /* OODebuggerInterface.h */; }; + 1A2DA3830CB4D25D00DE6823 /* OOTCPStreamDecoderAbstractionLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2DA2AA0CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.m */; }; + 1A2DA3870CB4D27400DE6823 /* OOTCPStreamDecoder.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A2DA2A60CB4CB5C00DE6823 /* OOTCPStreamDecoder.c */; }; + 1A2DA3920CB4D29300DE6823 /* OODebugTCPConsoleClient.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2DA2A50CB4CB5C00DE6823 /* OODebugTCPConsoleClient.m */; }; + 1A2DA39E0CB4D2BB00DE6823 /* OODebugMonitor.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2DA34C0CB4D0D800DE6823 /* OODebugMonitor.m */; }; + 1A2DA3A40CB4D35800DE6823 /* OOJSConsole.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2DA34B0CB4D0D800DE6823 /* OOJSConsole.m */; }; + 1A2DA3EE0CB4E84900DE6823 /* OODebugSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A2DA3EC0CB4E84900DE6823 /* OODebugSupport.m */; }; + 1A2DA3EF0CB4E84900DE6823 /* OODebugSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A2DA3ED0CB4E84900DE6823 /* OODebugSupport.h */; }; + 1A3159590F1B894F00328E4A /* OOAIStateMachineVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3159260F1B7B7E00328E4A /* OOAIStateMachineVerifierStage.m */; }; + 1A31595A0F1B895000328E4A /* OOAIStateMachineVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A3159250F1B7B7E00328E4A /* OOAIStateMachineVerifierStage.h */; }; + 1A34912A0BC25EAA00802DA7 /* world-scripts.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A3491290BC25EAA00802DA7 /* world-scripts.plist */; }; + 1A358CE20C1AB80D00E52220 /* ReadMe.rtfd in Resources */ = {isa = PBXBuildFile; fileRef = 1A358CE10C1AB80D00E52220 /* ReadMe.rtfd */; }; + 1A35916D0C1C375400E52220 /* oolite-nebula-1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A3591690C1C375400E52220 /* oolite-nebula-1.png */; }; + 1A35916E0C1C375400E52220 /* oolite-star-1.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A35916A0C1C375400E52220 /* oolite-star-1.png */; }; + 1A35916F0C1C375400E52220 /* oolite-nebula-3.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A35916B0C1C375400E52220 /* oolite-nebula-3.png */; }; + 1A3591700C1C375400E52220 /* oolite-nebula-2.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1A35916C0C1C375400E52220 /* oolite-nebula-2.png */; }; + 1A3591830C1C382700E52220 /* nebulatextures.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A3591810C1C382700E52220 /* nebulatextures.plist */; }; + 1A3591840C1C382700E52220 /* startextures.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A3591820C1C382700E52220 /* startextures.plist */; }; + 1A38B4AC0B988532001ED4A0 /* OOLogging.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A38B4AA0B988532001ED4A0 /* OOLogging.h */; }; + 1A38B4AD0B988532001ED4A0 /* OOLogging.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A38B4AB0B988532001ED4A0 /* OOLogging.m */; }; + 1A3A04620BC547DC00B5E2D9 /* OOTypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A3A04610BC547DC00B5E2D9 /* OOTypes.h */; }; + 1A3ACFEB0C5FF33A00EC50A7 /* OOJSShip.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A3ACFE90C5FF33A00EC50A7 /* OOJSShip.h */; }; + 1A3ACFEC0C5FF33A00EC50A7 /* OOJSShip.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3ACFEA0C5FF33A00EC50A7 /* OOJSShip.m */; }; + 1A3AFF1F0BC4462200B5E2D9 /* OOJSVector.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A3AFF1D0BC4462200B5E2D9 /* OOJSVector.h */; }; + 1A3AFF200BC4462200B5E2D9 /* OOJSVector.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3AFF1E0BC4462200B5E2D9 /* OOJSVector.m */; }; + 1A3C67FB0F1C910E0000D45B /* OOLegacyScriptWhitelist.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A3C67F40F1C90BF0000D45B /* OOLegacyScriptWhitelist.m */; }; + 1A43234E0BCFC9BB00F65914 /* OOOpenGLExtensionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A43234A0BCFC9BB00F65914 /* OOOpenGLExtensionManager.h */; }; + 1A43234F0BCFC9BB00F65914 /* OOOpenGLExtensionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A43234B0BCFC9BB00F65914 /* OOOpenGLExtensionManager.m */; }; + 1A43A0CC0CB91D2C00D0E239 /* OOJSPlanet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A43A0CA0CB91D2C00D0E239 /* OOJSPlanet.h */; }; + 1A43A0CD0CB91D2C00D0E239 /* OOJSPlanet.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A43A0CB0CB91D2C00D0E239 /* OOJSPlanet.m */; }; + 1A43A1860CB9243B00D0E239 /* OOEntityFilterPredicate.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A43A1840CB9243B00D0E239 /* OOEntityFilterPredicate.h */; }; + 1A43A1870CB9243B00D0E239 /* OOEntityFilterPredicate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A43A1850CB9243B00D0E239 /* OOEntityFilterPredicate.m */; }; + 1A4501E20DBF699A00815C04 /* descriptions.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A4501E10DBF699A00815C04 /* descriptions.plist */; }; + 1A451D8D0BB1BD2A004CD72F /* OOMaths.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9404920BAF4582005F6CF3 /* OOMaths.h */; }; + 1A472917096B5454000E78D8 /* CoreAudio.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A472916096B5454000E78D8 /* CoreAudio.framework */; }; + 1A472921096B5468000E78D8 /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A47291F096B5468000E78D8 /* AudioToolbox.framework */; }; + 1A472922096B5468000E78D8 /* AudioUnit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A472920096B5468000E78D8 /* AudioUnit.framework */; }; + 1A4D67D91041DCF3001AC80F /* oolite-javascript-errors.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A4D67D81041DCF3001AC80F /* oolite-javascript-errors.plist */; }; + 1A4FB23A0C8D6A9A00DC8E1F /* jsautocfg.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4FB2390C8D6A9A00DC8E1F /* jsautocfg.h */; }; + 1A4FB23C0C8D6AA900DC8E1F /* jsapi.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4FB23B0C8D6AA900DC8E1F /* jsapi.h */; }; + 1A4FB23F0C8D6AB400DC8E1F /* jspubtd.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4FB23E0C8D6AB400DC8E1F /* jspubtd.h */; }; + 1A4FB2410C8D6ABA00DC8E1F /* jstypes.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4FB2400C8D6ABA00DC8E1F /* jstypes.h */; }; + 1A4FB3350C8DC86800DC8E1F /* OOJSOolite.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A4FB3330C8DC86800DC8E1F /* OOJSOolite.h */; }; + 1A4FB3360C8DC86800DC8E1F /* OOJSOolite.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A4FB3340C8DC86800DC8E1F /* OOJSOolite.m */; }; + 1A4FB4BA0C8E11D800DC8E1F /* libjs.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A4FB2380C8D6A7500DC8E1F /* libjs.a */; }; + 1A5218DA0D72EC21000865E9 /* OOSpatialReference.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5218D80D72EC21000865E9 /* OOSpatialReference.h */; }; + 1A5218DE0D72ECE5000865E9 /* OOSpatialReference.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5218DD0D72ECE5000865E9 /* OOSpatialReference.m */; }; + 1A54AB530E3E17A1001EB817 /* OOJSPlayerShip.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A54AB510E3E17A0001EB817 /* OOJSPlayerShip.h */; }; + 1A54AB540E3E17A1001EB817 /* OOJSPlayerShip.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A54AB520E3E17A0001EB817 /* OOJSPlayerShip.m */; }; + 1A572FC30D84B640003D4A26 /* oolite-nova-mission.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FB80D84B640003D4A26 /* oolite-nova-mission.js */; }; + 1A572FC40D84B640003D4A26 /* oolite-thargoid-plans-mission.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FB90D84B640003D4A26 /* oolite-thargoid-plans-mission.js */; }; + 1A572FC50D84B640003D4A26 /* oolite-trumbles-mission.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FBA0D84B640003D4A26 /* oolite-trumbles-mission.js */; }; + 1A572FC60D84B640003D4A26 /* oolite-default-ship-script.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FBB0D84B640003D4A26 /* oolite-default-ship-script.js */; }; + 1A572FC70D84B640003D4A26 /* oolite-cloaking-device-mission.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FBC0D84B640003D4A26 /* oolite-cloaking-device-mission.js */; }; + 1A572FC80D84B640003D4A26 /* oolite-constrictor.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FBD0D84B640003D4A26 /* oolite-constrictor.js */; }; + 1A572FC90D84B640003D4A26 /* oolite-constrictor-hunt-mission.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FBE0D84B640003D4A26 /* oolite-constrictor-hunt-mission.js */; }; + 1A572FCA0D84B640003D4A26 /* oolite-cloaking-device-target-ship.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FBF0D84B640003D4A26 /* oolite-cloaking-device-target-ship.js */; }; + 1A572FCB0D84B640003D4A26 /* oolite-cloaking-device-pod.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FC00D84B640003D4A26 /* oolite-cloaking-device-pod.js */; }; + 1A572FCC0D84B640003D4A26 /* oolite-global-prefix.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FC10D84B640003D4A26 /* oolite-global-prefix.js */; }; + 1A572FCD0D84B640003D4A26 /* oolite-thargoid-warship.js in Copy Scripts */ = {isa = PBXBuildFile; fileRef = 1A572FC20D84B640003D4A26 /* oolite-thargoid-warship.js */; }; + 1A572FE50D84B6A3003D4A26 /* oolite-constrictor-AI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A572FE40D84B6A3003D4A26 /* oolite-constrictor-AI.plist */; }; + 1A5AA3230C0098AF0029C78A /* OOOpenGL.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5AA3220C0098AF0029C78A /* OOOpenGL.m */; }; + 1A5DB1EA0BBD8F0000D57389 /* OOConstToString.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5DB1E80BBD8F0000D57389 /* OOConstToString.h */; }; + 1A5DB1EB0BBD8F0000D57389 /* OOConstToString.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DB1E90BBD8F0000D57389 /* OOConstToString.m */; }; + 1A5DBAA60BC000DC00D57389 /* OOJavaScriptEngine.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5DBA9A0BC000DC00D57389 /* OOJavaScriptEngine.h */; }; + 1A5DBAA70BC000DC00D57389 /* OOJavaScriptEngine.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DBA9B0BC000DC00D57389 /* OOJavaScriptEngine.m */; }; + 1A5DBAA80BC000DC00D57389 /* OOJSScript.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5DBA9C0BC000DC00D57389 /* OOJSScript.h */; }; + 1A5DBAA90BC000DC00D57389 /* OOJSScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DBA9D0BC000DC00D57389 /* OOJSScript.m */; }; + 1A5DBAAA0BC000DC00D57389 /* OOPListScript.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5DBA9E0BC000DC00D57389 /* OOPListScript.h */; }; + 1A5DBAAB0BC000DC00D57389 /* OOPListScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DBA9F0BC000DC00D57389 /* OOPListScript.m */; }; + 1A5DBAAD0BC000DC00D57389 /* OOScript.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5DBAA10BC000DC00D57389 /* OOScript.h */; }; + 1A5DBAAE0BC000DC00D57389 /* OOScript.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DBAA20BC000DC00D57389 /* OOScript.m */; }; + 1A5DBD580BC17F0900D57389 /* NSStringOOExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5DBD560BC17F0900D57389 /* NSStringOOExtensions.h */; }; + 1A5DBD590BC17F0900D57389 /* NSStringOOExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5DBD570BC17F0900D57389 /* NSStringOOExtensions.m */; }; + 1A5E462F0C32DACE008104B4 /* OOShaderUniformMethodType.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A5E462D0C32DACE008104B4 /* OOShaderUniformMethodType.m */; }; + 1A5E46300C32DACE008104B4 /* OOShaderUniformMethodType.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A5E462E0C32DACE008104B4 /* OOShaderUniformMethodType.h */; }; + 1A60AFB70D56093B0070510D /* OOMusicController.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A60AFB50D56093B0070510D /* OOMusicController.h */; }; + 1A60AFB80D56093B0070510D /* OOMusicController.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A60AFB60D56093B0070510D /* OOMusicController.m */; }; + 1A62F0FD0E26A2A000897506 /* OOEquipmentType.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A62F0FB0E26A2A000897506 /* OOEquipmentType.m */; }; + 1A62F0FE0E26A2A000897506 /* OOEquipmentType.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A62F0FC0E26A2A000897506 /* OOEquipmentType.h */; }; + 1A6515100CCC9E2E0054D01B /* oolite-standard-vertex.vertex in Copy Shaders */ = {isa = PBXBuildFile; fileRef = 1A65150D0CCC9E220054D01B /* oolite-standard-vertex.vertex */; }; + 1A6515110CCC9E2E0054D01B /* oolite-default-shader.fragment in Copy Shaders */ = {isa = PBXBuildFile; fileRef = 1A65150E0CCC9E220054D01B /* oolite-default-shader.fragment */; }; + 1A6B1EF00C9AA5C6000717CF /* OOScriptTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6B1EEE0C9AA5C6000717CF /* OOScriptTimer.h */; }; + 1A6B1EF10C9AA5C6000717CF /* OOScriptTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6B1EEF0C9AA5C6000717CF /* OOScriptTimer.m */; }; + 1A6B1F360C9AAA60000717CF /* OOPriorityQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6B1F340C9AAA60000717CF /* OOPriorityQueue.m */; }; + 1A6B1F370C9AAA60000717CF /* OOPriorityQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6B1F350C9AAA60000717CF /* OOPriorityQueue.h */; }; + 1A6B228D0C9B40D4000717CF /* OOJSTimer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6B228B0C9B40D4000717CF /* OOJSTimer.h */; }; + 1A6B228E0C9B40D4000717CF /* OOJSTimer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6B228C0C9B40D4000717CF /* OOJSTimer.m */; }; + 1A6B25EE0C9C2746000717CF /* OOJSClock.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6B25EC0C9C2745000717CF /* OOJSClock.h */; }; + 1A6B25EF0C9C2746000717CF /* OOJSClock.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6B25ED0C9C2746000717CF /* OOJSClock.m */; }; + 1A6DD1230C57B5BC00A892F4 /* OOPListSchemaVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6DD1210C57B5BC00A892F4 /* OOPListSchemaVerifier.h */; }; + 1A6DD1240C57B5BC00A892F4 /* OOPListSchemaVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A6DD1220C57B5BC00A892F4 /* OOPListSchemaVerifier.m */; }; + 1A71D8AA0E5F17410088C456 /* OOSoundSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A71D8A80E5F17410088C456 /* OOSoundSource.m */; }; + 1A71D8AB0E5F17410088C456 /* OOSoundSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A71D8A90E5F17410088C456 /* OOSoundSource.h */; }; + 1A71E6F80BCE345000CD5C13 /* libpng.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A71E6F30BCE340C00CD5C13 /* libpng.a */; }; + 1A71E7170BCE34CF00CD5C13 /* pngconf.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A71E7030BCE34CF00CD5C13 /* pngconf.h */; }; + 1A71E7180BCE34CF00CD5C13 /* pngmem.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7040BCE34CF00CD5C13 /* pngmem.c */; }; + 1A71E7190BCE34CF00CD5C13 /* pngwutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7050BCE34CF00CD5C13 /* pngwutil.c */; }; + 1A71E71A0BCE34CF00CD5C13 /* pngwio.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7060BCE34CF00CD5C13 /* pngwio.c */; }; + 1A71E71B0BCE34CF00CD5C13 /* png.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A71E7070BCE34CF00CD5C13 /* png.h */; }; + 1A71E71C0BCE34CF00CD5C13 /* pngtrans.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7080BCE34CF00CD5C13 /* pngtrans.c */; }; + 1A71E71D0BCE34CF00CD5C13 /* pngrtran.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7090BCE34CF00CD5C13 /* pngrtran.c */; }; + 1A71E71E0BCE34CF00CD5C13 /* png.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E70A0BCE34CF00CD5C13 /* png.c */; }; + 1A71E71F0BCE34CF00CD5C13 /* pngwtran.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E70B0BCE34CF00CD5C13 /* pngwtran.c */; }; + 1A71E7200BCE34CF00CD5C13 /* pngread.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E70C0BCE34CF00CD5C13 /* pngread.c */; }; + 1A71E7210BCE34CF00CD5C13 /* pngrutil.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E70D0BCE34CF00CD5C13 /* pngrutil.c */; }; + 1A71E7220BCE34CF00CD5C13 /* pngtest.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E70E0BCE34CF00CD5C13 /* pngtest.c */; }; + 1A71E7230BCE34CF00CD5C13 /* pngerror.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E70F0BCE34CF00CD5C13 /* pngerror.c */; }; + 1A71E7240BCE34CF00CD5C13 /* pngset.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7100BCE34CF00CD5C13 /* pngset.c */; }; + 1A71E7250BCE34CF00CD5C13 /* pngwrite.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7110BCE34CF00CD5C13 /* pngwrite.c */; }; + 1A71E7260BCE34CF00CD5C13 /* pngget.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7120BCE34CF00CD5C13 /* pngget.c */; }; + 1A71E7270BCE34CF00CD5C13 /* pnggccrd.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7130BCE34CF00CD5C13 /* pnggccrd.c */; }; + 1A71E7280BCE34CF00CD5C13 /* pngrio.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7140BCE34CF00CD5C13 /* pngrio.c */; }; + 1A71E7290BCE34CF00CD5C13 /* pngvcrd.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7150BCE34CF00CD5C13 /* pngvcrd.c */; }; + 1A71E72A0BCE34CF00CD5C13 /* pngpread.c in Sources */ = {isa = PBXBuildFile; fileRef = 1A71E7160BCE34CF00CD5C13 /* pngpread.c */; }; + 1A71E8790BCE8EB100CD5C13 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A71E8780BCE8EB100CD5C13 /* libz.dylib */; }; + 1A71EA8C0BCF8C6B00CD5C13 /* OOXMLExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161102099544380037C2E1 /* OOXMLExtensions.m */; }; + 1A71EA8D0BCF8C6C00CD5C13 /* OOXMLExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161107099544390037C2E1 /* OOXMLExtensions.h */; }; + 1A736BD30C61E9370097AC37 /* OOJSPlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A736BD10C61E9370097AC37 /* OOJSPlayer.h */; }; + 1A736BD40C61E9370097AC37 /* OOJSPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A736BD20C61E9370097AC37 /* OOJSPlayer.m */; }; + 1A736C7F0C61FD220097AC37 /* OOJSCall.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A736C7D0C61FD220097AC37 /* OOJSCall.h */; }; + 1A736C800C61FD220097AC37 /* OOJSCall.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A736C7E0C61FD220097AC37 /* OOJSCall.m */; }; + 1A73712D0C623DAE0097AC37 /* OOJSStation.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A73712B0C623DAE0097AC37 /* OOJSStation.h */; }; + 1A73712E0C623DAE0097AC37 /* OOJSStation.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A73712C0C623DAE0097AC37 /* OOJSStation.m */; }; + 1A7376BE0C64AE330097AC37 /* OOJSSystem.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7376BC0C64AE330097AC37 /* OOJSSystem.h */; }; + 1A7376BF0C64AE330097AC37 /* OOJSSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7376BD0C64AE330097AC37 /* OOJSSystem.m */; }; + 1A7B967F0E620C9E00322821 /* OOSoundInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7B967E0E620C9E00322821 /* OOSoundInternal.h */; }; + 1A7BA8830D843485003C6CA3 /* ShipEntityScriptMethods.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7BA80B0D84231A003C6CA3 /* ShipEntityScriptMethods.h */; }; + 1A7BA8840D843485003C6CA3 /* ShipEntityScriptMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7BA80C0D84231A003C6CA3 /* ShipEntityScriptMethods.m */; }; + 1A7C75C30CC39EC3005D0AA2 /* OOJSSun.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7C75980CC39D11005D0AA2 /* OOJSSun.m */; }; + 1A7C75C50CC39EC9005D0AA2 /* OOJSSun.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7C75990CC39D11005D0AA2 /* OOJSSun.h */; }; + 1A7D3A180C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D3A160C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.h */; }; + 1A7D3A190C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D3A170C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.m */; }; + 1A7D3B9B0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D3B990C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.h */; }; + 1A7D3B9C0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D3B9A0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.m */; }; + 1A7D3C490C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D3C470C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.h */; }; + 1A7D3C4A0C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D3C480C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.m */; }; + 1A7D3D2B0C4F8D02008EDC33 /* OOTextureVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D3D290C4F8D01008EDC33 /* OOTextureVerifierStage.h */; }; + 1A7D3D2C0C4F8D02008EDC33 /* OOTextureVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D3D2A0C4F8D02008EDC33 /* OOTextureVerifierStage.m */; }; + 1A7D41860C516B90008EDC33 /* OOModelVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D41840C516B90008EDC33 /* OOModelVerifierStage.m */; }; + 1A7D41870C516B90008EDC33 /* OOModelVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D41850C516B90008EDC33 /* OOModelVerifierStage.h */; }; + 1A7D41E20C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D41E00C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.h */; }; + 1A7D41E30C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D41E10C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.m */; }; + 1A7D833A0C40147800E4A5F5 /* OOAsyncQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A7D83380C40147700E4A5F5 /* OOAsyncQueue.h */; }; + 1A7D833B0C40147800E4A5F5 /* OOAsyncQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A7D83390C40147700E4A5F5 /* OOAsyncQueue.m */; }; + 1A81F7090A7BAC4D006580AD /* OOCAMusic.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A81F7070A7BAC4D006580AD /* OOCAMusic.m */; }; + 1A81F70A0A7BAC4D006580AD /* OOCAMusic.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A81F7080A7BAC4D006580AD /* OOCAMusic.h */; }; + 1A8A37560B960337007D20B8 /* NSMutableDictionaryOOExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8A37540B960337007D20B8 /* NSMutableDictionaryOOExtensions.m */; }; + 1A8A37570B960337007D20B8 /* NSMutableDictionaryOOExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A8A37550B960337007D20B8 /* NSMutableDictionaryOOExtensions.h */; }; + 1A8A394E0B96229C007D20B8 /* NSFileManagerOOExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8A394C0B96229C007D20B8 /* NSFileManagerOOExtensions.m */; }; + 1A8A394F0B96229C007D20B8 /* NSFileManagerOOExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A8A394D0B96229C007D20B8 /* NSFileManagerOOExtensions.h */; }; + 1A8A3A380B962AEF007D20B8 /* NSScannerOOExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A8A3A360B962AEF007D20B8 /* NSScannerOOExtensions.h */; }; + 1A8A3A390B962AEF007D20B8 /* NSScannerOOExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8A3A370B962AEF007D20B8 /* NSScannerOOExtensions.m */; }; + 1A8A3D1B0B9B03A500AB7625 /* Oolite.mdimporter in Copy MDImporter */ = {isa = PBXBuildFile; fileRef = 1A5BF2770916D47300BF238F /* Oolite.mdimporter */; }; + 1A8BB8EA0E8311F900122974 /* OONullTexture.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A8BB8E80E8311F900122974 /* OONullTexture.h */; }; + 1A8BB8EB0E8311F900122974 /* OONullTexture.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A8BB8E90E8311F900122974 /* OONullTexture.m */; }; + 1A9322970DF53C33003FD306 /* OOJSSystemInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9322950DF53C33003FD306 /* OOJSSystemInfo.h */; }; + 1A9322980DF53C33003FD306 /* OOJSSystemInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9322960DF53C33003FD306 /* OOJSSystemInfo.m */; }; + 1A9400BE0BAF0ECD005F6CF3 /* OOStringParsing.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9400BD0BAF0ECD005F6CF3 /* OOStringParsing.m */; }; + 1A9400C00BAF0EDB005F6CF3 /* OOStringParsing.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9400BF0BAF0EDB005F6CF3 /* OOStringParsing.h */; }; + 1A9403D00BAF36C3005F6CF3 /* OOFunctionAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9403CF0BAF36C3005F6CF3 /* OOFunctionAttributes.h */; }; + 1A9404260BAF3DED005F6CF3 /* OOCollectionExtractors.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9404240BAF3DED005F6CF3 /* OOCollectionExtractors.m */; }; + 1A9404270BAF3DED005F6CF3 /* OOCollectionExtractors.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9404250BAF3DED005F6CF3 /* OOCollectionExtractors.h */; }; + 1A9404660BAF42BF005F6CF3 /* OOPListParsing.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9404640BAF42BE005F6CF3 /* OOPListParsing.h */; }; + 1A9404670BAF42BF005F6CF3 /* OOPListParsing.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9404650BAF42BF005F6CF3 /* OOPListParsing.m */; }; + 1A9404A30BAF462D005F6CF3 /* OOVector.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9404A10BAF462D005F6CF3 /* OOVector.h */; }; + 1A9404A40BAF462D005F6CF3 /* OOVector.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9404A20BAF462D005F6CF3 /* OOVector.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32 -Wmissing-field-initializers"; }; }; + 1A9405380BAF4FA6005F6CF3 /* OOMatrix.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9405360BAF4FA6005F6CF3 /* OOMatrix.h */; }; + 1A9405390BAF4FA6005F6CF3 /* OOMatrix.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9405370BAF4FA6005F6CF3 /* OOMatrix.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 1A94057F0BAF52AD005F6CF3 /* OOQuaternion.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A94057D0BAF52AD005F6CF3 /* OOQuaternion.h */; }; + 1A9405800BAF52AD005F6CF3 /* OOQuaternion.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A94057E0BAF52AD005F6CF3 /* OOQuaternion.m */; }; + 1A94062B0BAF6170005F6CF3 /* OOBoundingBox.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9406290BAF6170005F6CF3 /* OOBoundingBox.h */; }; + 1A9406840BAF66D6005F6CF3 /* OOVoxel.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9406820BAF66D6005F6CF3 /* OOVoxel.h */; }; + 1A9406850BAF66D6005F6CF3 /* OOVoxel.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9406830BAF66D6005F6CF3 /* OOVoxel.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 1A9406B40BAF67BF005F6CF3 /* OOTriangle.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A9406B20BAF67BF005F6CF3 /* OOTriangle.h */; }; + 1A9406B50BAF67BF005F6CF3 /* OOTriangle.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A9406B30BAF67BF005F6CF3 /* OOTriangle.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 1A94D5AE0D65A6960072C805 /* OOLight.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A94D5AC0D65A6960072C805 /* OOLight.m */; }; + 1A94D5AF0D65A6960072C805 /* OOLight.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A94D5AD0D65A6960072C805 /* OOLight.h */; }; + 1A94D5B50D65A6B40072C805 /* OOCamera.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A94D5B30D65A6B40072C805 /* OOCamera.h */; }; + 1A94D5B60D65A6B40072C805 /* OOCamera.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A94D5B40D65A6B40072C805 /* OOCamera.m */; }; + 1A94E4FC0F348D5700F1B5D9 /* delayedReactToAttackAI.plist in Copy AIs */ = {isa = PBXBuildFile; fileRef = 1A94E4FB0F348D4300F1B5D9 /* delayedReactToAttackAI.plist */; }; + 1A95338B0C02089E004EBB58 /* material-defaults.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A9533890C02089E004EBB58 /* material-defaults.plist */; }; + 1A95338C0C02089E004EBB58 /* planetinfo.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1A95338A0C02089E004EBB58 /* planetinfo.plist */; }; + 1AA82C8A0CC10E700023B797 /* OOJSWorldScripts.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AA82C820CC10E3D0023B797 /* OOJSWorldScripts.m */; }; + 1AAB9A980D779F4500A9F424 /* OOCocoa.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AAB9A960D779F3C00A9F424 /* OOCocoa.m */; }; + 1AAF56170F1A198400A2F2E6 /* Comparison.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AAF56160F1A198400A2F2E6 /* Comparison.h */; }; + 1AAF565A0F1A27EA00A2F2E6 /* whitelist.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1AAF56590F1A27D900A2F2E6 /* whitelist.plist */; }; + 1AB01ABE0BB15AED00F1B949 /* OOTextureScaling.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AB01ABC0BB15AED00F1B949 /* OOTextureScaling.h */; }; + 1AB01B5F0BB1639600F1B949 /* OOTextureScaling.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB01ABD0BB15AED00F1B949 /* OOTextureScaling.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31"; }; }; + 1AB01BBB0BB16A8A00F1B949 /* OOFastArithmetic.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AB01BB90BB16A8A00F1B949 /* OOFastArithmetic.h */; }; + 1AB01BBC0BB16A8A00F1B949 /* OOFastArithmetic.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB01BBA0BB16A8A00F1B949 /* OOFastArithmetic.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 1AB2AAFA0C4CE0CC0008CF4E /* OOOXPVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AB2AAF80C4CE0CC0008CF4E /* OOOXPVerifier.h */; }; + 1AB2AAFB0C4CE0CC0008CF4E /* OOOXPVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB2AAF90C4CE0CC0008CF4E /* OOOXPVerifier.m */; }; + 1AB2AB130C4CE4070008CF4E /* verifyOXP.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1AB2AB120C4CE4070008CF4E /* verifyOXP.plist */; }; + 1AB4AEB80D688AD9003076D6 /* OOLogHeader.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AB4AEB60D688AD9003076D6 /* OOLogHeader.h */; }; + 1AB4AEB90D688AD9003076D6 /* OOLogHeader.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB4AEB70D688AD9003076D6 /* OOLogHeader.m */; }; + 1AB784F90D554F7B00517983 /* OOJSSoundSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AB784F70D554F7B00517983 /* OOJSSoundSource.h */; }; + 1AB784FA0D554F7B00517983 /* OOJSSoundSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB784F80D554F7B00517983 /* OOJSSoundSource.m */; }; + 1AB8128F0E90179C00A84923 /* TextureStore.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161145099544390037C2E1 /* TextureStore.m */; }; + 1AB812900E90179D00A84923 /* TextureStore.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161134099544390037C2E1 /* TextureStore.h */; }; + 1AB813090E90D8E500A84923 /* OOLogOutputHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AB813070E90D8E500A84923 /* OOLogOutputHandler.m */; }; + 1AB8130A0E90D8E500A84923 /* OOLogOutputHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AB813080E90D8E500A84923 /* OOLogOutputHandler.h */; }; + 1ABAD7310F350B3400FD2CBF /* OOShipGroup.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABAD72F0F350B3400FD2CBF /* OOShipGroup.m */; }; + 1ABAD7320F350B3400FD2CBF /* OOShipGroup.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ABAD7300F350B3400FD2CBF /* OOShipGroup.h */; }; + 1ABB688C0D044306008BE96D /* OOLoggingExtended.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ABB688B0D044306008BE96D /* OOLoggingExtended.h */; }; + 1ABC03ED0EF86110003B740A /* OOCrosshairs.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ABC03EB0EF86110003B740A /* OOCrosshairs.h */; }; + 1ABC03EE0EF86110003B740A /* OOCrosshairs.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABC03EC0EF86110003B740A /* OOCrosshairs.m */; }; + 1ABC04270EF872B7003B740A /* crosshairs.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1ABC04260EF872B7003B740A /* crosshairs.plist */; }; + 1ABC47FE0F155F0500B977AD /* OOJSFunction.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ABC47FC0F155F0500B977AD /* OOJSFunction.h */; }; + 1ABC47FF0F155F0500B977AD /* OOJSFunction.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABC47FD0F155F0500B977AD /* OOJSFunction.m */; }; + 1ABDBA3B0EB365D90086BC3D /* OOIsNumberLiteral.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ABDBA390EB365D90086BC3D /* OOIsNumberLiteral.m */; }; + 1ABDBA3C0EB365D90086BC3D /* OOIsNumberLiteral.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ABDBA3A0EB365D90086BC3D /* OOIsNumberLiteral.h */; }; + 1AC27A0F0EA7E9940054E5F0 /* OOJSEquipmentInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AC27A0D0EA7E9940054E5F0 /* OOJSEquipmentInfo.h */; }; + 1AC27A100EA7E9940054E5F0 /* OOJSEquipmentInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AC27A0E0EA7E9940054E5F0 /* OOJSEquipmentInfo.m */; }; + 1AC544FA0D4D217900C90E5B /* oolite-font.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1AC544F90D4D217900C90E5B /* oolite-font.png */; }; + 1AC545060D4D228400C90E5B /* OOEncodingConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AC545040D4D228400C90E5B /* OOEncodingConverter.h */; }; + 1AC545070D4D228400C90E5B /* OOEncodingConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AC545050D4D228400C90E5B /* OOEncodingConverter.m */; }; + 1AC5452D0D4D298E00C90E5B /* oolite-font.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1AC5452C0D4D298E00C90E5B /* oolite-font.plist */; }; + 1AC775E20C2DD4E900ECFF3B /* OODebugGLDrawing.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AC775E00C2DD4E900ECFF3B /* OODebugGLDrawing.h */; }; + 1AC775E30C2DD4E900ECFF3B /* OODebugGLDrawing.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AC775E10C2DD4E900ECFF3B /* OODebugGLDrawing.m */; }; + 1AC973FA0C9847850010C42B /* pirate-victim-roles.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1AC973F90C9847850010C42B /* pirate-victim-roles.plist */; }; + 1ACBF0AD0D82F79600CC005F /* OOSoundSourcePool.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACBF06F0D82DF9B00CC005F /* OOSoundSourcePool.h */; }; + 1ACBF0AE0D82F79800CC005F /* OOSoundSourcePool.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACBF0700D82DF9B00CC005F /* OOSoundSourcePool.m */; }; + 1ACE208F0D805F78009F6957 /* oolite-scarred-metal-specular.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1ACE208E0D805F78009F6957 /* oolite-scarred-metal-specular.png */; }; + 1ACEA3490C91507000C7CE97 /* OORoleSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA3470C91507000C7CE97 /* OORoleSet.h */; }; + 1ACEA34A0C91507000C7CE97 /* OORoleSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA3480C91507000C7CE97 /* OORoleSet.m */; }; + 1ACEA6BF0C91DA3E00C7CE97 /* OOJSGlobal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA6BD0C91DA3E00C7CE97 /* OOJSGlobal.h */; }; + 1ACEA6C00C91DA3E00C7CE97 /* OOJSGlobal.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA6BE0C91DA3E00C7CE97 /* OOJSGlobal.m */; }; + 1ACEA7280C91DF2800C7CE97 /* OOJSMissionVariables.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA7260C91DF2800C7CE97 /* OOJSMissionVariables.h */; }; + 1ACEA7290C91DF2800C7CE97 /* OOJSMissionVariables.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA7270C91DF2800C7CE97 /* OOJSMissionVariables.m */; }; + 1ACEA7AA0C91E32800C7CE97 /* OOJSMission.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ACEA7A80C91E32800C7CE97 /* OOJSMission.h */; }; + 1ACEA7AB0C91E32800C7CE97 /* OOJSMission.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ACEA7A90C91E32800C7CE97 /* OOJSMission.m */; }; + 1AD0C3300C463FCC0070BD23 /* autoAImap.plist in Copy Config */ = {isa = PBXBuildFile; fileRef = 1AD0C32F0C463FCB0070BD23 /* autoAImap.plist */; }; + 1AD1F4FF0CD9E83700EAE520 /* NSThreadOOExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AD1F4C80CD9E42A00EAE520 /* NSThreadOOExtensions.m */; }; + 1AD1F5000CD9E83800EAE520 /* NSThreadOOExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AD1F4C70CD9E42A00EAE520 /* NSThreadOOExtensions.h */; }; + 1AD267650C83058C00B4BFD1 /* Debug.oxp in Copy Debug OXP */ = {isa = PBXBuildFile; fileRef = 1A0519390C7CCAC900BA5CCA /* Debug.oxp */; }; + 1ADA8AB30F42DBA80001BEC9 /* OODeepCopy.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ADA8AB10F42DBA80001BEC9 /* OODeepCopy.h */; }; + 1ADA8AB40F42DBA80001BEC9 /* OODeepCopy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ADA8AB20F42DBA80001BEC9 /* OODeepCopy.m */; }; + 1ADBA5500BD0F173008FC99C /* OOBasicMaterial.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ADBA54E0BD0F173008FC99C /* OOBasicMaterial.h */; }; + 1ADBA5510BD0F173008FC99C /* OOBasicMaterial.m in Sources */ = {isa = PBXBuildFile; fileRef = 1ADBA54F0BD0F173008FC99C /* OOBasicMaterial.m */; }; + 1ADF5CEC0B9DF59A00FDB2A3 /* OOCacheManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1A231A170B9D8B1B00EF0852 /* OOCacheManager.m */; }; + 1ADF5F130B9E374B00FDB2A3 /* JoystickHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = 1ADF5F110B9E374B00FDB2A3 /* JoystickHandler.h */; }; + 1AE546310C19A2E2005B89F3 /* JoystickHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AE546300C19A2E2005B89F3 /* JoystickHandler.m */; }; + 1AE834CA0D9598C10097CB8A /* oolite-barrel-specular.png in Copy Textures */ = {isa = PBXBuildFile; fileRef = 1AE834C90D9598C10097CB8A /* oolite-barrel-specular.png */; }; + 1AED2D0C0C04586C004A1118 /* OOGraphicsResetManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 1AED2D0A0C04586C004A1118 /* OOGraphicsResetManager.h */; }; + 1AED2D0D0C04586C004A1118 /* OOGraphicsResetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AED2D0B0C04586C004A1118 /* OOGraphicsResetManager.m */; }; + 2512833E09BA27C100F43D55 /* Octree.m in Sources */ = {isa = PBXBuildFile; fileRef = 2512833C09BA27C100F43D55 /* Octree.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 2512833F09BA27C100F43D55 /* Octree.h in Headers */ = {isa = PBXBuildFile; fileRef = 2512833D09BA27C100F43D55 /* Octree.h */; }; + 2512834209BA27EC00F43D55 /* Geometry.h in Headers */ = {isa = PBXBuildFile; fileRef = 2512834009BA27EC00F43D55 /* Geometry.h */; }; + 2512834309BA27EC00F43D55 /* Geometry.m in Sources */ = {isa = PBXBuildFile; fileRef = 2512834109BA27EC00F43D55 /* Geometry.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 2512834609BA281500F43D55 /* CollisionRegion.h in Headers */ = {isa = PBXBuildFile; fileRef = 2512834409BA281500F43D55 /* CollisionRegion.h */; }; + 2512834709BA281500F43D55 /* CollisionRegion.m in Sources */ = {isa = PBXBuildFile; fileRef = 2512834509BA281500F43D55 /* CollisionRegion.m */; settings = {COMPILER_FLAGS = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; }; }; + 25160E2F0995362F0037C2E1 /* OOCocoa.h in Headers */ = {isa = PBXBuildFile; fileRef = 25160E2E0995362F0037C2E1 /* OOCocoa.h */; }; + 251610DD099544090037C2E1 /* OOCABufferedSound.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610CA099544090037C2E1 /* OOCABufferedSound.h */; }; + 251610DE099544090037C2E1 /* OOCASoundMixer.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610CB099544090037C2E1 /* OOCASoundMixer.h */; }; + 251610E0099544090037C2E1 /* OOCASoundChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610CD099544090037C2E1 /* OOCASoundChannel.h */; }; + 251610E2099544090037C2E1 /* OOCASoundReferencePoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610CF099544090037C2E1 /* OOCASoundReferencePoint.m */; }; + 251610E3099544090037C2E1 /* OOCASound.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610D0099544090037C2E1 /* OOCASound.h */; }; + 251610E4099544090037C2E1 /* OOCASoundMixer.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610D1099544090037C2E1 /* OOCASoundMixer.m */; }; + 251610E5099544090037C2E1 /* OOCABufferedSound.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610D2099544090037C2E1 /* OOCABufferedSound.m */; }; + 251610E6099544090037C2E1 /* OOCAStreamingSound.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610D3099544090037C2E1 /* OOCAStreamingSound.m */; }; + 251610E7099544090037C2E1 /* OOCASoundDecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610D4099544090037C2E1 /* OOCASoundDecoder.h */; }; + 251610E8099544090037C2E1 /* OOCASound.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610D5099544090037C2E1 /* OOCASound.m */; }; + 251610E9099544090037C2E1 /* OOCASoundChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610D6099544090037C2E1 /* OOCASoundChannel.m */; }; + 251610EA099544090037C2E1 /* OOCAStreamingSound.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610D7099544090037C2E1 /* OOCAStreamingSound.h */; }; + 251610EB099544090037C2E1 /* OOErrorDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610D8099544090037C2E1 /* OOErrorDescription.h */; }; + 251610EC099544090037C2E1 /* OOCASoundReferencePoint.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610D9099544090037C2E1 /* OOCASoundReferencePoint.h */; }; + 251610ED099544090037C2E1 /* OOCASoundInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610DA099544090037C2E1 /* OOCASoundInternal.h */; }; + 251610EE099544090037C2E1 /* OOCASoundDecoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610DB099544090037C2E1 /* OOCASoundDecoder.m */; }; + 251610EF099544090037C2E1 /* OOErrorDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610DC099544090037C2E1 /* OOErrorDescription.m */; }; + 251610F2099544190037C2E1 /* VirtualRingBuffer.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610F0099544190037C2E1 /* VirtualRingBuffer.m */; }; + 251610F3099544190037C2E1 /* VirtualRingBuffer.h in Headers */ = {isa = PBXBuildFile; fileRef = 251610F1099544190037C2E1 /* VirtualRingBuffer.h */; }; + 25161151099544390037C2E1 /* OpenGLSprite.m in Sources */ = {isa = PBXBuildFile; fileRef = 251610FF099544380037C2E1 /* OpenGLSprite.m */; }; + 25161152099544390037C2E1 /* OpenGLSprite.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161100099544380037C2E1 /* OpenGLSprite.h */; }; + 25161153099544390037C2E1 /* AI.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161101099544380037C2E1 /* AI.m */; }; + 25161158099544390037C2E1 /* AI.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161106099544390037C2E1 /* AI.h */; }; + 2516115A099544390037C2E1 /* OOTrumble.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161108099544390037C2E1 /* OOTrumble.m */; }; + 2516115D099544390037C2E1 /* GameController.h in Headers */ = {isa = PBXBuildFile; fileRef = 2516110B099544390037C2E1 /* GameController.h */; }; + 2516115E099544390037C2E1 /* GameController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516110C099544390037C2E1 /* GameController.m */; }; + 25161162099544390037C2E1 /* OOTrumble.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161110099544390037C2E1 /* OOTrumble.h */; }; + 25161168099544390037C2E1 /* OOSound.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161116099544390037C2E1 /* OOSound.h */; }; + 2516116D099544390037C2E1 /* OOCharacter.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516111B099544390037C2E1 /* OOCharacter.m */; }; + 2516116E099544390037C2E1 /* OOCharacter.h in Headers */ = {isa = PBXBuildFile; fileRef = 2516111C099544390037C2E1 /* OOCharacter.h */; }; + 25161178099544390037C2E1 /* GuiDisplayGen.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161126099544390037C2E1 /* GuiDisplayGen.m */; }; + 25161179099544390037C2E1 /* GuiDisplayGen.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161127099544390037C2E1 /* GuiDisplayGen.h */; }; + 2516117D099544390037C2E1 /* HeadUpDisplay.m in Sources */ = {isa = PBXBuildFile; fileRef = 2516112B099544390037C2E1 /* HeadUpDisplay.m */; }; + 2516117E099544390037C2E1 /* HeadUpDisplay.h in Headers */ = {isa = PBXBuildFile; fileRef = 2516112C099544390037C2E1 /* HeadUpDisplay.h */; }; + 2516118B099544390037C2E1 /* ResourceManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161139099544390037C2E1 /* ResourceManager.m */; }; + 2516118C099544390037C2E1 /* ResourceManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 2516113A099544390037C2E1 /* ResourceManager.h */; }; + 25161195099544390037C2E1 /* Universe.m in Sources */ = {isa = PBXBuildFile; fileRef = 25161143099544390037C2E1 /* Universe.m */; }; + 25161196099544390037C2E1 /* Universe.h in Headers */ = {isa = PBXBuildFile; fileRef = 25161144099544390037C2E1 /* Universe.h */; }; + 2576E7B309B4F418007410F7 /* MyOpenGLView.h in Headers */ = {isa = PBXBuildFile; fileRef = 2576E7B209B4F418007410F7 /* MyOpenGLView.h */; }; + 25F3E6310994F033002F25FD /* legacy_random.h in Headers */ = {isa = PBXBuildFile; fileRef = 25F3E6300994F033002F25FD /* legacy_random.h */; }; + 25F3E6330994F04C002F25FD /* legacy_random.c in Sources */ = {isa = PBXBuildFile; fileRef = 25F3E6320994F04C002F25FD /* legacy_random.c */; }; + 25F3E63B0994F08A002F25FD /* OOOpenGL.h in Headers */ = {isa = PBXBuildFile; fileRef = 25F3E63A0994F08A002F25FD /* OOOpenGL.h */; }; + 25F3E6BD0994F30A002F25FD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 25F3E6BC0994F30A002F25FD /* main.m */; }; + 25F3E6F20994F466002F25FD /* Groolite.h in Headers */ = {isa = PBXBuildFile; fileRef = 25F3E6F00994F466002F25FD /* Groolite.h */; }; + 25F3E6F30994F466002F25FD /* Groolite.m in Sources */ = {isa = PBXBuildFile; fileRef = 25F3E6F10994F466002F25FD /* Groolite.m */; }; + 25F3E8A70994FE65002F25FD /* oolite-document.icns in Resources */ = {isa = PBXBuildFile; fileRef = 25F3E8A40994FE65002F25FD /* oolite-document.icns */; }; + 25F3E8A80994FE65002F25FD /* oolite-expansion-document.icns in Resources */ = {isa = PBXBuildFile; fileRef = 25F3E8A50994FE65002F25FD /* oolite-expansion-document.icns */; }; + 25F3E8A90994FE65002F25FD /* oolite-icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 25F3E8A60994FE65002F25FD /* oolite-icon.icns */; }; + 25F3E8B40994FE9B002F25FD /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 25F3E8B30994FE9B002F25FD /* InfoPlist.strings */; }; + 25F46752099695D5009483BF /* OoliteApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 25F46750099695D5009483BF /* OoliteApp.h */; }; + 25F46753099695D5009483BF /* OoliteApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 25F46751099695D5009483BF /* OoliteApp.m */; }; + 25F4676509969672009483BF /* MyOpenGLView.m in Sources */ = {isa = PBXBuildFile; fileRef = 25F4676309969672009483BF /* MyOpenGLView.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 1A0519380C7CCAC900BA5CCA /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1A0519340C7CCAC900BA5CCA /* DebugOXP.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 8D5B49B6048680CD000E48DA; + remoteInfo = DebugOXP; + }; + 1A4FB2370C8D6A7500DC8E1F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1A6B50370C8B42480035DFCC /* libjs.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = D2AAC046055464E500DB518D; + remoteInfo = libjs; + }; + 1A4FB4CF0C8E130E00DC8E1F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1A6B50370C8B42480035DFCC /* libjs.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = D2AAC045055464E500DB518D; + remoteInfo = libjs; + }; + 1A5BF2760916D47300BF238F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1A5BF2720916D47300BF238F /* Oolite-importer.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 1A1AB1C60909BF46001039FA; + remoteInfo = "Oolite-importer"; + }; + 1A5BF3070916D8F700BF238F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1A5BF2720916D47300BF238F /* Oolite-importer.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 8D57630D048677EA00EA77CD; + remoteInfo = "Oolite-importer"; + }; + 1A71E6F60BCE343400CD5C13 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1A71E6F20BCE340C00CD5C13; + remoteInfo = "libpng-custom"; + }; + 1AD267530C83052200B4BFD1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 1A0519340C7CCAC900BA5CCA /* DebugOXP.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = 8D5B49AC048680CD000E48DA; + remoteInfo = DebugOXP; + }; + 1AD267550C83052600B4BFD1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 29B97313FDCFA39411CA2CEA /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0865423506B8447D000CA0AB; + remoteInfo = Oolite; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 1A23153A0B9C773B00EF0852 /* Copy Images */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Images; + dstSubfolderSpec = 7; + files = ( + 1A2315520B9C778400EF0852 /* solar.png in Copy Images */, + 1A2315550B9C778400EF0852 /* trumblebox.png in Copy Images */, + ); + name = "Copy Images"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A2316C80B9CFAB800EF0852 /* Copy Config */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Config; + dstSubfolderSpec = 7; + files = ( + 1A4D67D91041DCF3001AC80F /* oolite-javascript-errors.plist in Copy Config */, + 1ABC04270EF872B7003B740A /* crosshairs.plist in Copy Config */, + 1A4501E20DBF699A00815C04 /* descriptions.plist in Copy Config */, + 1AC5452D0D4D298E00C90E5B /* oolite-font.plist in Copy Config */, + 1AC973FA0C9847850010C42B /* pirate-victim-roles.plist in Copy Config */, + 1A020E0B0D020AFB00C3F51E /* changedScriptHandlers.plist in Copy Config */, + 1AB2AB130C4CE4070008CF4E /* verifyOXP.plist in Copy Config */, + 1A95338B0C02089E004EBB58 /* material-defaults.plist in Copy Config */, + 1A95338C0C02089E004EBB58 /* planetinfo.plist in Copy Config */, + 1AD0C3300C463FCC0070BD23 /* autoAImap.plist in Copy Config */, + 1A3591830C1C382700E52220 /* nebulatextures.plist in Copy Config */, + 1A3591840C1C382700E52220 /* startextures.plist in Copy Config */, + 1A34912A0BC25EAA00802DA7 /* world-scripts.plist in Copy Config */, + 1A2316EE0B9CFAD700EF0852 /* characters.plist in Copy Config */, + 1A2316EF0B9CFAD700EF0852 /* commodities.plist in Copy Config */, + 1A2316F00B9CFAD700EF0852 /* customsounds.plist in Copy Config */, + 1A2316F10B9CFAD700EF0852 /* demoships.plist in Copy Config */, + 1A2316F30B9CFAD700EF0852 /* equipment.plist in Copy Config */, + 1A2316F40B9CFAD700EF0852 /* hud-small.plist in Copy Config */, + 1A2316F50B9CFAD700EF0852 /* hud.plist in Copy Config */, + 1A2316F60B9CFAD700EF0852 /* illegal_goods.plist in Copy Config */, + 1A2316F70B9CFAD700EF0852 /* keyconfig.plist in Copy Config */, + 1A2316F80B9CFAD700EF0852 /* logcontrol.plist in Copy Config */, + 1A2316F90B9CFAD700EF0852 /* missiontext.plist in Copy Config */, + 1A2316FB0B9CFAD700EF0852 /* shipdata.plist in Copy Config */, + 1A2316FC0B9CFAD800EF0852 /* shipyard.plist in Copy Config */, + 1A2316FD0B9CFAD800EF0852 /* speech_pronunciation_guide.plist in Copy Config */, + 1AAF565A0F1A27EA00A2F2E6 /* whitelist.plist in Copy Config */, + ); + name = "Copy Config"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A2317D50B9D023400EF0852 /* Copy AIs */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = AIs; + dstSubfolderSpec = 7; + files = ( + 1A94E4FC0F348D5700F1B5D9 /* delayedReactToAttackAI.plist in Copy AIs */, + 1A572FE50D84B6A3003D4A26 /* oolite-constrictor-AI.plist in Copy AIs */, + 1A2317B30B9D022500EF0852 /* buoyAI.plist in Copy AIs */, + 1A2317B40B9D022500EF0852 /* collectLootAI.plist in Copy AIs */, + 1A2317B50B9D022500EF0852 /* dockingAI.plist in Copy AIs */, + 1A2317B60B9D022500EF0852 /* dumbAI.plist in Copy AIs */, + 1A2317B70B9D022500EF0852 /* enteringPirateAI.plist in Copy AIs */, + 1A2317B80B9D022500EF0852 /* enteringTraderAI.plist in Copy AIs */, + 1A2317B90B9D022500EF0852 /* escortAI.plist in Copy AIs */, + 1A2317BA0B9D022500EF0852 /* exitingTraderAI.plist in Copy AIs */, + 1A2317BB0B9D022500EF0852 /* fallingShuttleAI.plist in Copy AIs */, + 1A2317BC0B9D022500EF0852 /* fttAI.plist in Copy AIs */, + 1A2317BD0B9D022500EF0852 /* gotoWaypointAI.plist in Copy AIs */, + 1A2317BE0B9D022500EF0852 /* hardMissileAI.plist in Copy AIs */, + 1A2317BF0B9D022500EF0852 /* homeAI.plist in Copy AIs */, + 1A2317C00B9D022500EF0852 /* interceptAI.plist in Copy AIs */, + 1A2317C10B9D022500EF0852 /* minerAI.plist in Copy AIs */, + 1A2317C20B9D022500EF0852 /* missileAI.plist in Copy AIs */, + 1A2317C30B9D022500EF0852 /* nullAI.plist in Copy AIs */, + 1A2317C40B9D022500EF0852 /* pirateAI.plist in Copy AIs */, + 1A2317C50B9D022500EF0852 /* planetPatrolAI.plist in Copy AIs */, + 1A2317C60B9D022500EF0852 /* policeInterceptAI.plist in Copy AIs */, + 1A2317C70B9D022500EF0852 /* receiveDockingAI.plist in Copy AIs */, + 1A2317C80B9D022500EF0852 /* risingShuttleAI.plist in Copy AIs */, + 1A2317C90B9D022500EF0852 /* rockHermitAI.plist in Copy AIs */, + 1A2317CA0B9D022500EF0852 /* route1patrolAI.plist in Copy AIs */, + 1A2317CB0B9D022500EF0852 /* route1traderAI.plist in Copy AIs */, + 1A2317CC0B9D022500EF0852 /* route2patrolAI.plist in Copy AIs */, + 1A2317CD0B9D022500EF0852 /* route2sunskimAI.plist in Copy AIs */, + 1A2317CE0B9D022500EF0852 /* scavengerAI.plist in Copy AIs */, + 1A2317D00B9D022500EF0852 /* stationAI.plist in Copy AIs */, + 1A2317D10B9D022500EF0852 /* sunSkimExitAI.plist in Copy AIs */, + 1A2317D20B9D022500EF0852 /* thargletAI.plist in Copy AIs */, + 1A2317D30B9D022500EF0852 /* thargoidAI.plist in Copy AIs */, + 1A2317D40B9D022500EF0852 /* timebombAI.plist in Copy AIs */, + ); + name = "Copy AIs"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A2318A00B9D027400EF0852 /* Copy Textures */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Textures; + dstSubfolderSpec = 7; + files = ( + 1A21149E0DEA98D100444CEB /* oolite-ball-turret.png in Copy Textures */, + 1AE834CA0D9598C10097CB8A /* oolite-barrel-specular.png in Copy Textures */, + 1AC544FA0D4D217900C90E5B /* oolite-font.png in Copy Textures */, + 1A03659B0D7CA0EE00B5F46F /* oolite-nebula-4.png in Copy Textures */, + 1ACE208F0D805F78009F6957 /* oolite-scarred-metal-specular.png in Copy Textures */, + 1A35916D0C1C375400E52220 /* oolite-nebula-1.png in Copy Textures */, + 1A35916E0C1C375400E52220 /* oolite-star-1.png in Copy Textures */, + 1A35916F0C1C375400E52220 /* oolite-nebula-3.png in Copy Textures */, + 1A3591700C1C375400E52220 /* oolite-nebula-2.png in Copy Textures */, + 1A23185F0B9D026D00EF0852 /* adder_redux.png in Copy Textures */, + 1A2318600B9D026D00EF0852 /* anaconda_redux.png in Copy Textures */, + 1A2318610B9D026D00EF0852 /* arcdetail.png in Copy Textures */, + 1A2318630B9D026D00EF0852 /* asp_redux.png in Copy Textures */, + 1A2318640B9D026D00EF0852 /* asteroid.png in Copy Textures */, + 1A2318650B9D026D00EF0852 /* back_metal.png in Copy Textures */, + 1A2318660B9D026D00EF0852 /* barrel_metal.png in Copy Textures */, + 1A2318670B9D026D00EF0852 /* blur256.png in Copy Textures */, + 1A2318680B9D026D00EF0852 /* boa2_left.png in Copy Textures */, + 1A2318690B9D026D00EF0852 /* boa2_rear.png in Copy Textures */, + 1A23186A0B9D026D00EF0852 /* boa2_redux.png in Copy Textures */, + 1A23186B0B9D026D00EF0852 /* boa2_right.png in Copy Textures */, + 1A23186C0B9D026D00EF0852 /* boa2_top.png in Copy Textures */, + 1A23186D0B9D026D00EF0852 /* boa_redux.png in Copy Textures */, + 1A23186E0B9D026D00EF0852 /* bottom_metal.png in Copy Textures */, + 1A23186F0B9D026D00EF0852 /* cobra1_redux.png in Copy Textures */, + 1A2318700B9D026D00EF0852 /* cobra1_redux1.png in Copy Textures */, + 1A2318710B9D026D00EF0852 /* cobra1_redux2.png in Copy Textures */, + 1A2318720B9D026D00EF0852 /* cobra1miner_top.png in Copy Textures */, + 1A2318730B9D026D00EF0852 /* cobra3_redux.png in Copy Textures */, + 1A2318740B9D026D00EF0852 /* cobra3_redux1.png in Copy Textures */, + 1A2318750B9D026D00EF0852 /* constrictor_redux.png in Copy Textures */, + 1A2318760B9D026D00EF0852 /* dark_metal.png in Copy Textures */, + 1A2318770B9D026D00EF0852 /* ferdelance_redux.png in Copy Textures */, + 1A2318780B9D026D00EF0852 /* flare256.png in Copy Textures */, + 1A2318790B9D026D00EF0852 /* front_metal.png in Copy Textures */, + 1A23187B0B9D026D00EF0852 /* gecko_redux.png in Copy Textures */, + 1A23187C0B9D026D00EF0852 /* krait_redux.png in Copy Textures */, + 1A23187D0B9D026D00EF0852 /* left_metal.png in Copy Textures */, + 1A23187E0B9D026D00EF0852 /* mamba_redux.png in Copy Textures */, + 1A23187F0B9D026D00EF0852 /* mamba_redux1.png in Copy Textures */, + 1A2318800B9D026D00EF0852 /* metal.png in Copy Textures */, + 1A2318810B9D026D00EF0852 /* missile.png in Copy Textures */, + 1A2318820B9D026D00EF0852 /* moray_redux.png in Copy Textures */, + 1A2318830B9D026D00EF0852 /* moray_redux1.png in Copy Textures */, + 1A2318840B9D026D00EF0852 /* MorayMED_bottom.png in Copy Textures */, + 1A2318850B9D026D00EF0852 /* MorayMED_top.png in Copy Textures */, + 1A23188A0B9D026D00EF0852 /* pod2_redux.png in Copy Textures */, + 1A23188B0B9D026D00EF0852 /* python_redux.png in Copy Textures */, + 1A23188C0B9D026D00EF0852 /* python_redux1.png in Copy Textures */, + 1A23188D0B9D026D00EF0852 /* python_redux2.png in Copy Textures */, + 1A23188E0B9D026D00EF0852 /* qbomb.png in Copy Textures */, + 1A23188F0B9D026D00EF0852 /* right_metal.png in Copy Textures */, + 1A2318900B9D026D00EF0852 /* scarred_metal.png in Copy Textures */, + 1A2318910B9D026D00EF0852 /* shuttle_redux.png in Copy Textures */, + 1A2318920B9D026D00EF0852 /* sidewinder_redux.png in Copy Textures */, + 1A2318940B9D026D00EF0852 /* target_metal.png in Copy Textures */, + 1A2318950B9D026D00EF0852 /* thargoid_redux.png in Copy Textures */, + 1A2318960B9D026D00EF0852 /* thargon_redux.png in Copy Textures */, + 1A2318970B9D026D00EF0852 /* top_metal.png in Copy Textures */, + 1A2318980B9D026D00EF0852 /* transporter_redux.png in Copy Textures */, + 1A2318990B9D026D00EF0852 /* transporter_redux1.png in Copy Textures */, + 1A23189A0B9D026D00EF0852 /* trumblekit.png in Copy Textures */, + 1A23189B0B9D026D00EF0852 /* viper_redux.png in Copy Textures */, + 1A23189C0B9D026D00EF0852 /* viperi_redux.png in Copy Textures */, + 1A23189D0B9D026D00EF0852 /* worm_redux.png in Copy Textures */, + 1A23189E0B9D026D00EF0852 /* worm_redux1.png in Copy Textures */, + 1A23189F0B9D026D00EF0852 /* wreck.png in Copy Textures */, + ); + name = "Copy Textures"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A2319640B9D02B900EF0852 /* Copy Models */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Models; + dstSubfolderSpec = 7; + files = ( + 1A21149B0DEA980800444CEB /* oolite-ball-turret.dat in Copy Models */, + 1A1502F60C1201C30032F3E8 /* oolite-unknown-ship.dat in Copy Models */, + 1A2318E50B9D02AA00EF0852 /* adder_redux.dat in Copy Models */, + 1A2318E60B9D02AA00EF0852 /* alloy.dat in Copy Models */, + 1A2318E70B9D02AA00EF0852 /* anaconda_redux.dat in Copy Models */, + 1A2318E80B9D02AA00EF0852 /* arcdetail.dat in Copy Models */, + 1A2318E90B9D02AA00EF0852 /* asp_redux.dat in Copy Models */, + 1A2318EA0B9D02AA00EF0852 /* asteroid.dat in Copy Models */, + 1A2318EB0B9D02AA00EF0852 /* asteroid1.dat in Copy Models */, + 1A2318EC0B9D02AA00EF0852 /* barrel.dat in Copy Models */, + 1A2318ED0B9D02AA00EF0852 /* boa2_redux.dat in Copy Models */, + 1A2318EE0B9D02AA00EF0852 /* boa_redux.dat in Copy Models */, + 1A2318EF0B9D02AA00EF0852 /* boulder.dat in Copy Models */, + 1A2318F00B9D02AA00EF0852 /* boulder1.dat in Copy Models */, + 1A2318F10B9D02AA00EF0852 /* buoy.dat in Copy Models */, + 1A2318F20B9D02AA00EF0852 /* cobra1_redux.dat in Copy Models */, + 1A2318F30B9D02AA00EF0852 /* cobra1_redux1.dat in Copy Models */, + 1A2318F40B9D02AA00EF0852 /* cobra1_redux2.dat in Copy Models */, + 1A2318F50B9D02AA00EF0852 /* cobra3_redux.dat in Copy Models */, + 1A2318F60B9D02AA00EF0852 /* cobra3_redux1.dat in Copy Models */, + 1A2318F70B9D02AA00EF0852 /* constrictor_redux.dat in Copy Models */, + 1A2318F80B9D02AA00EF0852 /* dock-flat.dat in Copy Models */, + 1A2318F90B9D02AA00EF0852 /* dock.dat in Copy Models */, + 1A2318FA0B9D02AA00EF0852 /* escpod_redux.dat in Copy Models */, + 1A2318FB0B9D02AA00EF0852 /* exhaust.dat in Copy Models */, + 1A2318FC0B9D02AA00EF0852 /* ferdelance_redux.dat in Copy Models */, + 1A2318FD0B9D02AA00EF0852 /* gecko_redux.dat in Copy Models */, + 1A2318FE0B9D02AA00EF0852 /* icosahedron.dat in Copy Models */, + 1A2318FF0B9D02AA00EF0852 /* icostextured.dat in Copy Models */, + 1A2319000B9D02AA00EF0852 /* krait_redux.dat in Copy Models */, + 1A2319010B9D02AA00EF0852 /* mamba_redux.dat in Copy Models */, + 1A2319020B9D02AA00EF0852 /* mamba_redux1.dat in Copy Models */, + 1A2319030B9D02AA00EF0852 /* missile.dat in Copy Models */, + 1A2319040B9D02AA00EF0852 /* moray_redux.dat in Copy Models */, + 1A2319050B9D02AA00EF0852 /* moray_redux1.dat in Copy Models */, + 1A2319060B9D02AA00EF0852 /* new-dodo.dat in Copy Models */, + 1A2319070B9D02AA00EF0852 /* new-icos.dat in Copy Models */, + 1A2319080B9D02AA00EF0852 /* new-rock.dat in Copy Models */, + 1A2319090B9D02AA00EF0852 /* new_coriolis.dat in Copy Models */, + 1A23190A0B9D02AA00EF0852 /* python_redux.dat in Copy Models */, + 1A23190B0B9D02AA00EF0852 /* python_redux1.dat in Copy Models */, + 1A23190C0B9D02AA00EF0852 /* python_redux2.dat in Copy Models */, + 1A23190D0B9D02AA00EF0852 /* qbomb.dat in Copy Models */, + 1A23190E0B9D02AA00EF0852 /* ring.dat in Copy Models */, + 1A23190F0B9D02AA00EF0852 /* rock-box.dat in Copy Models */, + 1A2319100B9D02AA00EF0852 /* rock-dock.dat in Copy Models */, + 1A2319110B9D02AA00EF0852 /* scarred_alloy.dat in Copy Models */, + 1A2319120B9D02AA00EF0852 /* shuttle_redux.dat in Copy Models */, + 1A2319130B9D02AA00EF0852 /* sidewinder_redux.dat in Copy Models */, + 1A2319140B9D02AA00EF0852 /* splinter.dat in Copy Models */, + 1A2319150B9D02AA00EF0852 /* splinter1.dat in Copy Models */, + 1A2319160B9D02AA00EF0852 /* strut.dat in Copy Models */, + 1A2319170B9D02AA00EF0852 /* thargoid_redux.dat in Copy Models */, + 1A2319180B9D02AA00EF0852 /* thargon_redux.dat in Copy Models */, + 1A2319190B9D02AA00EF0852 /* transporter_redux.dat in Copy Models */, + 1A23191A0B9D02AA00EF0852 /* transporter_redux1.dat in Copy Models */, + 1A23191B0B9D02AA00EF0852 /* viper_redux.dat in Copy Models */, + 1A23191C0B9D02AA00EF0852 /* viperi_redux.dat in Copy Models */, + 1A23191D0B9D02AA00EF0852 /* worm_redux.dat in Copy Models */, + 1A23191E0B9D02AA00EF0852 /* worm_redux1.dat in Copy Models */, + 1A23191F0B9D02AA00EF0852 /* wreck1.dat in Copy Models */, + 1A2319200B9D02AA00EF0852 /* wreck2.dat in Copy Models */, + 1A2319210B9D02AA00EF0852 /* wreck3.dat in Copy Models */, + 1A2319220B9D02AA00EF0852 /* wreck4.dat in Copy Models */, + 1A2319230B9D02AA00EF0852 /* wreck5.dat in Copy Models */, + ); + name = "Copy Models"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A2319780B9D030400EF0852 /* Copy Music */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Music; + dstSubfolderSpec = 7; + files = ( + 1A2319760B9D02F900EF0852 /* BlueDanube.ogg in Copy Music */, + 1A2319770B9D02F900EF0852 /* OoliteTheme.ogg in Copy Music */, + ); + name = "Copy Music"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A2319BA0B9D032700EF0852 /* Copy Sounds */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Sounds; + dstSubfolderSpec = 7; + files = ( + 1A2319A60B9D031D00EF0852 /* afterburner1.ogg in Copy Sounds */, + 1A2319A80B9D031D00EF0852 /* beep.ogg in Copy Sounds */, + 1A2319A90B9D031D00EF0852 /* bigbang.ogg in Copy Sounds */, + 1A2319AA0B9D031D00EF0852 /* boop.ogg in Copy Sounds */, + 1A2319AB0B9D031D00EF0852 /* breakpattern.ogg in Copy Sounds */, + 1A2319AC0B9D031D00EF0852 /* buy.ogg in Copy Sounds */, + 1A2319AD0B9D031D00EF0852 /* ecm.ogg in Copy Sounds */, + 1A2319AE0B9D031D00EF0852 /* guiclick.ogg in Copy Sounds */, + 1A2319AF0B9D031D00EF0852 /* hit.ogg in Copy Sounds */, + 1A2319B00B9D031D00EF0852 /* hullbang.ogg in Copy Sounds */, + 1A2319B10B9D031D00EF0852 /* laser.ogg in Copy Sounds */, + 1A2319B20B9D031D00EF0852 /* laserhits.ogg in Copy Sounds */, + 1A2319B30B9D031D00EF0852 /* missile.ogg in Copy Sounds */, + 1A2319B40B9D031D00EF0852 /* scoop.ogg in Copy Sounds */, + 1A2319B50B9D031D00EF0852 /* sell.ogg in Copy Sounds */, + 1A2319B60B9D031D00EF0852 /* trumble.ogg in Copy Sounds */, + 1A2319B70B9D031D00EF0852 /* trumblesqueal.ogg in Copy Sounds */, + 1A2319B80B9D031D00EF0852 /* warning.ogg in Copy Sounds */, + 1A2319B90B9D031D00EF0852 /* witchabort.ogg in Copy Sounds */, + ); + name = "Copy Sounds"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A3491320BC25ED400802DA7 /* Copy Scripts */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Scripts; + dstSubfolderSpec = 7; + files = ( + 1A572FC30D84B640003D4A26 /* oolite-nova-mission.js in Copy Scripts */, + 1A572FC40D84B640003D4A26 /* oolite-thargoid-plans-mission.js in Copy Scripts */, + 1A572FC50D84B640003D4A26 /* oolite-trumbles-mission.js in Copy Scripts */, + 1A572FC60D84B640003D4A26 /* oolite-default-ship-script.js in Copy Scripts */, + 1A572FC70D84B640003D4A26 /* oolite-cloaking-device-mission.js in Copy Scripts */, + 1A572FC80D84B640003D4A26 /* oolite-constrictor.js in Copy Scripts */, + 1A572FC90D84B640003D4A26 /* oolite-constrictor-hunt-mission.js in Copy Scripts */, + 1A572FCA0D84B640003D4A26 /* oolite-cloaking-device-target-ship.js in Copy Scripts */, + 1A572FCB0D84B640003D4A26 /* oolite-cloaking-device-pod.js in Copy Scripts */, + 1A572FCC0D84B640003D4A26 /* oolite-global-prefix.js in Copy Scripts */, + 1A572FCD0D84B640003D4A26 /* oolite-thargoid-warship.js in Copy Scripts */, + ); + name = "Copy Scripts"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A5BF29C0916D49800BF238F /* Copy MDImporter */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 12; + dstPath = Contents/Library/Spotlight; + dstSubfolderSpec = 1; + files = ( + 1A8A3D1B0B9B03A500AB7625 /* Oolite.mdimporter in Copy MDImporter */, + ); + name = "Copy MDImporter"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1A6514FE0CCC9D7A0054D01B /* Copy Shaders */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = Shaders; + dstSubfolderSpec = 7; + files = ( + 1A6515100CCC9E2E0054D01B /* oolite-standard-vertex.vertex in Copy Shaders */, + 1A02FD460EE0490B008F9B09 /* oolite-tangent-space-vertex.vertex in Copy Shaders */, + 1A6515110CCC9E2E0054D01B /* oolite-default-shader.fragment in Copy Shaders */, + ); + name = "Copy Shaders"; + runOnlyForDeploymentPostprocessing = 0; + }; + 1AD267610C83057F00B4BFD1 /* Copy Debug OXP */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = AddOns; + dstSubfolderSpec = 16; + files = ( + 1AD267650C83058C00B4BFD1 /* Debug.oxp in Copy Debug OXP */, + ); + name = "Copy Debug OXP"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 06AFA01500A58FB77F000001 /* OpenGL.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = OpenGL.framework; path = /System/Library/Frameworks/OpenGL.framework; sourceTree = ""; }; + 081329B30A6D534900A5C2CE /* OOInstinct.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOInstinct.h; sourceTree = ""; }; + 081329B40A6D534900A5C2CE /* OOInstinct.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOInstinct.m; sourceTree = ""; }; + 083325DB09DDBCDE00F5B8E4 /* OOColor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOColor.h; sourceTree = ""; }; + 083325DC09DDBCDE00F5B8E4 /* OOColor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOColor.m; sourceTree = ""; }; + 083DB4D30A70E51E00B419B2 /* OOBrain.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOBrain.h; sourceTree = ""; }; + 083DB4D40A70E51E00B419B2 /* OOBrain.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOBrain.m; sourceTree = ""; }; + 0865432206B8447D000CA0AB /* OoliteDev.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OoliteDev.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0878FD2F086EF845004CB752 /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; + 1A020E0A0D020AFB00C3F51E /* changedScriptHandlers.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = changedScriptHandlers.plist; sourceTree = ""; }; + 1A02FD340EE048E8008F9B09 /* oolite-tangent-space-vertex.vertex */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "oolite-tangent-space-vertex.vertex"; sourceTree = ""; }; + 1A0365990D7CA0EE00B5F46F /* oolite-nebula-4.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-nebula-4.png"; sourceTree = ""; }; + 1A0479E70DC9F81000EE1CD0 /* OOShipRegistry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOShipRegistry.h; sourceTree = ""; }; + 1A0479E80DC9F81000EE1CD0 /* OOShipRegistry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOShipRegistry.m; sourceTree = ""; }; + 1A047A430DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSDictionaryOOExtensions.h; sourceTree = ""; }; + 1A047A440DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSDictionaryOOExtensions.m; sourceTree = ""; }; + 1A047B7C0DCB3D7500EE1CD0 /* OOProbabilitySet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOProbabilitySet.h; sourceTree = ""; }; + 1A047B7D0DCB3D7500EE1CD0 /* OOProbabilitySet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOProbabilitySet.m; sourceTree = ""; }; + 1A0517D10C7B376700BA5CCA /* debug-exports.exp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.exports; path = "debug-exports.exp"; sourceTree = ""; }; + 1A0519340C7CCAC900BA5CCA /* DebugOXP.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = DebugOXP.xcodeproj; path = "Mac-DebugOXP/DebugOXP.xcodeproj"; sourceTree = SOURCE_ROOT; }; + 1A0729D70EF56D1200B0F925 /* OOConvertSystemDescriptions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOConvertSystemDescriptions.h; sourceTree = ""; }; + 1A0729D80EF56D1200B0F925 /* OOConvertSystemDescriptions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOConvertSystemDescriptions.m; sourceTree = ""; }; + 1A0729FC0EF5796500B0F925 /* OldSchoolPropertyListWriting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OldSchoolPropertyListWriting.h; sourceTree = ""; }; + 1A0729FD0EF5796500B0F925 /* OldSchoolPropertyListWriting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OldSchoolPropertyListWriting.m; sourceTree = ""; }; + 1A0DA2EC0D71D280009B0970 /* OOJSSpecialFunctions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSSpecialFunctions.h; sourceTree = ""; }; + 1A0DA2ED0D71D280009B0970 /* OOJSSpecialFunctions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSSpecialFunctions.m; sourceTree = ""; }; + 1A11F8480F35F60C001C886C /* OOJSShipGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSShipGroup.m; sourceTree = ""; }; + 1A11F8490F35F60C001C886C /* OOJSShipGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSShipGroup.h; sourceTree = ""; }; + 1A1502F50C1201C30032F3E8 /* oolite-unknown-ship.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "oolite-unknown-ship.dat"; sourceTree = ""; }; + 1A1504490C12C50D0032F3E8 /* OOSkyDrawable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSkyDrawable.h; sourceTree = ""; }; + 1A15044A0C12C50D0032F3E8 /* OOSkyDrawable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOSkyDrawable.m; sourceTree = ""; }; + 1A15049C0C12CA070032F3E8 /* OOProbabilisticTextureManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOProbabilisticTextureManager.h; sourceTree = ""; }; + 1A15049D0C12CA070032F3E8 /* OOProbabilisticTextureManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOProbabilisticTextureManager.m; sourceTree = ""; }; + 1A1616600D7DCFDC0094AE5B /* OOFilteringEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOFilteringEnumerator.h; sourceTree = ""; }; + 1A1616610D7DCFDC0094AE5B /* OOFilteringEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOFilteringEnumerator.m; sourceTree = ""; }; + 1A1D212D0D2BD4C100F4DEC2 /* bsd_string.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bsd_string.h; path = src/BSDCompat/bsd_string.h; sourceTree = SOURCE_ROOT; }; + 1A1E99B40EF04837008B48E2 /* OOProgressBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOProgressBar.h; sourceTree = ""; }; + 1A1E99B50EF04837008B48E2 /* OOProgressBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOProgressBar.m; sourceTree = ""; }; + 1A20F7040F36EE0500156DE9 /* OOExcludeObjectEnumerator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOExcludeObjectEnumerator.h; sourceTree = ""; }; + 1A20F7050F36EE0500156DE9 /* OOExcludeObjectEnumerator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOExcludeObjectEnumerator.m; sourceTree = ""; }; + 1A21149A0DEA980800444CEB /* oolite-ball-turret.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "oolite-ball-turret.dat"; sourceTree = ""; }; + 1A21149D0DEA98D100444CEB /* oolite-ball-turret.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-ball-turret.png"; sourceTree = ""; }; + 1A23154E0B9C778400EF0852 /* solar.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = solar.png; sourceTree = ""; }; + 1A23154F0B9C778400EF0852 /* splash.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = splash.png; sourceTree = ""; }; + 1A2315500B9C778400EF0852 /* splashback.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = splashback.png; sourceTree = ""; }; + 1A2315510B9C778400EF0852 /* trumblebox.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = trumblebox.png; sourceTree = ""; }; + 1A2316DE0B9CFAD700EF0852 /* characters.plist */ = {isa = PBXFileReference; explicitFileType = text.plist; fileEncoding = 4; path = characters.plist; sourceTree = ""; }; + 1A2316DF0B9CFAD700EF0852 /* commodities.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = commodities.plist; sourceTree = ""; }; + 1A2316E00B9CFAD700EF0852 /* customsounds.plist */ = {isa = PBXFileReference; explicitFileType = text.plist; fileEncoding = 4; path = customsounds.plist; sourceTree = ""; }; + 1A2316E10B9CFAD700EF0852 /* demoships.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = demoships.plist; sourceTree = ""; }; + 1A2316E30B9CFAD700EF0852 /* equipment.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = equipment.plist; sourceTree = ""; }; + 1A2316E40B9CFAD700EF0852 /* hud-small.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "hud-small.plist"; sourceTree = ""; }; + 1A2316E50B9CFAD700EF0852 /* hud.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = hud.plist; sourceTree = ""; }; + 1A2316E60B9CFAD700EF0852 /* illegal_goods.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = illegal_goods.plist; sourceTree = ""; }; + 1A2316E70B9CFAD700EF0852 /* keyconfig.plist */ = {isa = PBXFileReference; fileEncoding = 4; languageSpecificationIdentifier = plist; lastKnownFileType = text.plist; path = keyconfig.plist; sourceTree = ""; }; + 1A2316E80B9CFAD700EF0852 /* logcontrol.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = logcontrol.plist; sourceTree = ""; }; + 1A2316E90B9CFAD700EF0852 /* missiontext.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = missiontext.plist; sourceTree = ""; }; + 1A2316EB0B9CFAD700EF0852 /* shipdata.plist */ = {isa = PBXFileReference; explicitFileType = text.plist; fileEncoding = 4; path = shipdata.plist; sourceTree = ""; }; + 1A2316EC0B9CFAD700EF0852 /* shipyard.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = shipyard.plist; sourceTree = ""; }; + 1A2316ED0B9CFAD700EF0852 /* speech_pronunciation_guide.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = speech_pronunciation_guide.plist; sourceTree = ""; }; + 1A2317910B9D022400EF0852 /* buoyAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = buoyAI.plist; sourceTree = ""; }; + 1A2317920B9D022500EF0852 /* collectLootAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = collectLootAI.plist; sourceTree = ""; }; + 1A2317930B9D022500EF0852 /* dockingAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = dockingAI.plist; sourceTree = ""; }; + 1A2317940B9D022500EF0852 /* dumbAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = dumbAI.plist; sourceTree = ""; }; + 1A2317950B9D022500EF0852 /* enteringPirateAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = enteringPirateAI.plist; sourceTree = ""; }; + 1A2317960B9D022500EF0852 /* enteringTraderAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = enteringTraderAI.plist; sourceTree = ""; }; + 1A2317970B9D022500EF0852 /* escortAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = escortAI.plist; sourceTree = ""; }; + 1A2317980B9D022500EF0852 /* exitingTraderAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = exitingTraderAI.plist; sourceTree = ""; }; + 1A2317990B9D022500EF0852 /* fallingShuttleAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = fallingShuttleAI.plist; sourceTree = ""; }; + 1A23179A0B9D022500EF0852 /* fttAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = fttAI.plist; sourceTree = ""; }; + 1A23179B0B9D022500EF0852 /* gotoWaypointAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = gotoWaypointAI.plist; sourceTree = ""; }; + 1A23179C0B9D022500EF0852 /* hardMissileAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = hardMissileAI.plist; sourceTree = ""; }; + 1A23179D0B9D022500EF0852 /* homeAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = homeAI.plist; sourceTree = ""; }; + 1A23179E0B9D022500EF0852 /* interceptAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = interceptAI.plist; sourceTree = ""; }; + 1A23179F0B9D022500EF0852 /* minerAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = minerAI.plist; sourceTree = ""; }; + 1A2317A00B9D022500EF0852 /* missileAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = missileAI.plist; sourceTree = ""; }; + 1A2317A10B9D022500EF0852 /* nullAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = nullAI.plist; sourceTree = ""; }; + 1A2317A20B9D022500EF0852 /* pirateAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = pirateAI.plist; sourceTree = ""; }; + 1A2317A30B9D022500EF0852 /* planetPatrolAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = planetPatrolAI.plist; sourceTree = ""; }; + 1A2317A40B9D022500EF0852 /* policeInterceptAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = policeInterceptAI.plist; sourceTree = ""; }; + 1A2317A50B9D022500EF0852 /* receiveDockingAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = receiveDockingAI.plist; sourceTree = ""; }; + 1A2317A60B9D022500EF0852 /* risingShuttleAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = risingShuttleAI.plist; sourceTree = ""; }; + 1A2317A70B9D022500EF0852 /* rockHermitAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = rockHermitAI.plist; sourceTree = ""; }; + 1A2317A80B9D022500EF0852 /* route1patrolAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = route1patrolAI.plist; sourceTree = ""; }; + 1A2317A90B9D022500EF0852 /* route1traderAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = route1traderAI.plist; sourceTree = ""; }; + 1A2317AA0B9D022500EF0852 /* route2patrolAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = route2patrolAI.plist; sourceTree = ""; }; + 1A2317AB0B9D022500EF0852 /* route2sunskimAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = route2sunskimAI.plist; sourceTree = ""; }; + 1A2317AC0B9D022500EF0852 /* scavengerAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = scavengerAI.plist; sourceTree = ""; }; + 1A2317AE0B9D022500EF0852 /* stationAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = stationAI.plist; sourceTree = ""; }; + 1A2317AF0B9D022500EF0852 /* sunSkimExitAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = sunSkimExitAI.plist; sourceTree = ""; }; + 1A2317B00B9D022500EF0852 /* thargletAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = thargletAI.plist; sourceTree = ""; }; + 1A2317B10B9D022500EF0852 /* thargoidAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = thargoidAI.plist; sourceTree = ""; }; + 1A2317B20B9D022500EF0852 /* timebombAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = timebombAI.plist; sourceTree = ""; }; + 1A23181E0B9D026D00EF0852 /* adder_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = adder_redux.png; sourceTree = ""; }; + 1A23181F0B9D026D00EF0852 /* anaconda_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = anaconda_redux.png; sourceTree = ""; }; + 1A2318200B9D026D00EF0852 /* arcdetail.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = arcdetail.png; sourceTree = ""; }; + 1A2318220B9D026D00EF0852 /* asp_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = asp_redux.png; sourceTree = ""; }; + 1A2318230B9D026D00EF0852 /* asteroid.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = asteroid.png; sourceTree = ""; }; + 1A2318240B9D026D00EF0852 /* back_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = back_metal.png; sourceTree = ""; }; + 1A2318250B9D026D00EF0852 /* barrel_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = barrel_metal.png; sourceTree = ""; }; + 1A2318260B9D026D00EF0852 /* blur256.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = blur256.png; sourceTree = ""; }; + 1A2318270B9D026D00EF0852 /* boa2_left.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = boa2_left.png; sourceTree = ""; }; + 1A2318280B9D026D00EF0852 /* boa2_rear.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = boa2_rear.png; sourceTree = ""; }; + 1A2318290B9D026D00EF0852 /* boa2_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = boa2_redux.png; sourceTree = ""; }; + 1A23182A0B9D026D00EF0852 /* boa2_right.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = boa2_right.png; sourceTree = ""; }; + 1A23182B0B9D026D00EF0852 /* boa2_top.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = boa2_top.png; sourceTree = ""; }; + 1A23182C0B9D026D00EF0852 /* boa_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = boa_redux.png; sourceTree = ""; }; + 1A23182D0B9D026D00EF0852 /* bottom_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = bottom_metal.png; sourceTree = ""; }; + 1A23182E0B9D026D00EF0852 /* cobra1_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cobra1_redux.png; sourceTree = ""; }; + 1A23182F0B9D026D00EF0852 /* cobra1_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cobra1_redux1.png; sourceTree = ""; }; + 1A2318300B9D026D00EF0852 /* cobra1_redux2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cobra1_redux2.png; sourceTree = ""; }; + 1A2318310B9D026D00EF0852 /* cobra1miner_top.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cobra1miner_top.png; sourceTree = ""; }; + 1A2318320B9D026D00EF0852 /* cobra3_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cobra3_redux.png; sourceTree = ""; }; + 1A2318330B9D026D00EF0852 /* cobra3_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cobra3_redux1.png; sourceTree = ""; }; + 1A2318340B9D026D00EF0852 /* constrictor_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = constrictor_redux.png; sourceTree = ""; }; + 1A2318350B9D026D00EF0852 /* dark_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dark_metal.png; sourceTree = ""; }; + 1A2318360B9D026D00EF0852 /* ferdelance_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = ferdelance_redux.png; sourceTree = ""; }; + 1A2318370B9D026D00EF0852 /* flare256.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = flare256.png; sourceTree = ""; }; + 1A2318380B9D026D00EF0852 /* front_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = front_metal.png; sourceTree = ""; }; + 1A2318390B9D026D00EF0852 /* galaxy256.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = galaxy256.png; sourceTree = ""; }; + 1A23183A0B9D026D00EF0852 /* gecko_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = gecko_redux.png; sourceTree = ""; }; + 1A23183B0B9D026D00EF0852 /* krait_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = krait_redux.png; sourceTree = ""; }; + 1A23183C0B9D026D00EF0852 /* left_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = left_metal.png; sourceTree = ""; }; + 1A23183D0B9D026D00EF0852 /* mamba_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mamba_redux.png; sourceTree = ""; }; + 1A23183E0B9D026D00EF0852 /* mamba_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = mamba_redux1.png; sourceTree = ""; }; + 1A23183F0B9D026D00EF0852 /* metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = metal.png; sourceTree = ""; }; + 1A2318400B9D026D00EF0852 /* missile.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = missile.png; sourceTree = ""; }; + 1A2318410B9D026D00EF0852 /* moray_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = moray_redux.png; sourceTree = ""; }; + 1A2318420B9D026D00EF0852 /* moray_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = moray_redux1.png; sourceTree = ""; }; + 1A2318430B9D026D00EF0852 /* MorayMED_bottom.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = MorayMED_bottom.png; sourceTree = ""; }; + 1A2318440B9D026D00EF0852 /* MorayMED_top.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = MorayMED_top.png; sourceTree = ""; }; + 1A2318490B9D026D00EF0852 /* pod2_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = pod2_redux.png; sourceTree = ""; }; + 1A23184A0B9D026D00EF0852 /* python_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = python_redux.png; sourceTree = ""; }; + 1A23184B0B9D026D00EF0852 /* python_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = python_redux1.png; sourceTree = ""; }; + 1A23184C0B9D026D00EF0852 /* python_redux2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = python_redux2.png; sourceTree = ""; }; + 1A23184D0B9D026D00EF0852 /* qbomb.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = qbomb.png; sourceTree = ""; }; + 1A23184E0B9D026D00EF0852 /* right_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = right_metal.png; sourceTree = ""; }; + 1A23184F0B9D026D00EF0852 /* scarred_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = scarred_metal.png; sourceTree = ""; }; + 1A2318500B9D026D00EF0852 /* shuttle_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = shuttle_redux.png; sourceTree = ""; }; + 1A2318510B9D026D00EF0852 /* sidewinder_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = sidewinder_redux.png; sourceTree = ""; }; + 1A2318520B9D026D00EF0852 /* star64.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = star64.png; sourceTree = ""; }; + 1A2318530B9D026D00EF0852 /* target_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = target_metal.png; sourceTree = ""; }; + 1A2318540B9D026D00EF0852 /* thargoid_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = thargoid_redux.png; sourceTree = ""; }; + 1A2318550B9D026D00EF0852 /* thargon_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = thargon_redux.png; sourceTree = ""; }; + 1A2318560B9D026D00EF0852 /* top_metal.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = top_metal.png; sourceTree = ""; }; + 1A2318570B9D026D00EF0852 /* transporter_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = transporter_redux.png; sourceTree = ""; }; + 1A2318580B9D026D00EF0852 /* transporter_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = transporter_redux1.png; sourceTree = ""; }; + 1A2318590B9D026D00EF0852 /* trumblekit.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = trumblekit.png; sourceTree = ""; }; + 1A23185A0B9D026D00EF0852 /* viper_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = viper_redux.png; sourceTree = ""; }; + 1A23185B0B9D026D00EF0852 /* viperi_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = viperi_redux.png; sourceTree = ""; }; + 1A23185C0B9D026D00EF0852 /* worm_redux.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = worm_redux.png; sourceTree = ""; }; + 1A23185D0B9D026D00EF0852 /* worm_redux1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = worm_redux1.png; sourceTree = ""; }; + 1A23185E0B9D026D00EF0852 /* wreck.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = wreck.png; sourceTree = ""; }; + 1A2318A60B9D02A900EF0852 /* adder_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = adder_redux.dat; sourceTree = ""; }; + 1A2318A70B9D02A900EF0852 /* alloy.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = alloy.dat; sourceTree = ""; }; + 1A2318A80B9D02A900EF0852 /* anaconda_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = anaconda_redux.dat; sourceTree = ""; }; + 1A2318A90B9D02A900EF0852 /* arcdetail.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = arcdetail.dat; sourceTree = ""; }; + 1A2318AA0B9D02A900EF0852 /* asp_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = asp_redux.dat; sourceTree = ""; }; + 1A2318AB0B9D02A900EF0852 /* asteroid.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = asteroid.dat; sourceTree = ""; }; + 1A2318AC0B9D02A900EF0852 /* asteroid1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = asteroid1.dat; sourceTree = ""; }; + 1A2318AD0B9D02A900EF0852 /* barrel.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = barrel.dat; sourceTree = ""; }; + 1A2318AE0B9D02A900EF0852 /* boa2_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = boa2_redux.dat; sourceTree = ""; }; + 1A2318AF0B9D02A900EF0852 /* boa_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = boa_redux.dat; sourceTree = ""; }; + 1A2318B00B9D02A900EF0852 /* boulder.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = boulder.dat; sourceTree = ""; }; + 1A2318B10B9D02A900EF0852 /* boulder1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = boulder1.dat; sourceTree = ""; }; + 1A2318B20B9D02A900EF0852 /* buoy.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = buoy.dat; sourceTree = ""; }; + 1A2318B30B9D02A900EF0852 /* cobra1_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cobra1_redux.dat; sourceTree = ""; }; + 1A2318B40B9D02A900EF0852 /* cobra1_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cobra1_redux1.dat; sourceTree = ""; }; + 1A2318B50B9D02A900EF0852 /* cobra1_redux2.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cobra1_redux2.dat; sourceTree = ""; }; + 1A2318B60B9D02A900EF0852 /* cobra3_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cobra3_redux.dat; sourceTree = ""; }; + 1A2318B70B9D02A900EF0852 /* cobra3_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = cobra3_redux1.dat; sourceTree = ""; }; + 1A2318B80B9D02A900EF0852 /* constrictor_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = constrictor_redux.dat; sourceTree = ""; }; + 1A2318B90B9D02A900EF0852 /* dock-flat.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "dock-flat.dat"; sourceTree = ""; }; + 1A2318BA0B9D02A900EF0852 /* dock.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dock.dat; sourceTree = ""; }; + 1A2318BB0B9D02A900EF0852 /* escpod_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = escpod_redux.dat; sourceTree = ""; }; + 1A2318BC0B9D02A900EF0852 /* exhaust.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = exhaust.dat; sourceTree = ""; }; + 1A2318BD0B9D02A900EF0852 /* ferdelance_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ferdelance_redux.dat; sourceTree = ""; }; + 1A2318BE0B9D02A900EF0852 /* gecko_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = gecko_redux.dat; sourceTree = ""; }; + 1A2318BF0B9D02A900EF0852 /* icosahedron.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = icosahedron.dat; sourceTree = ""; }; + 1A2318C00B9D02A900EF0852 /* icostextured.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = icostextured.dat; sourceTree = ""; }; + 1A2318C10B9D02A900EF0852 /* krait_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = krait_redux.dat; sourceTree = ""; }; + 1A2318C20B9D02A900EF0852 /* mamba_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mamba_redux.dat; sourceTree = ""; }; + 1A2318C30B9D02A900EF0852 /* mamba_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = mamba_redux1.dat; sourceTree = ""; }; + 1A2318C40B9D02A900EF0852 /* missile.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = missile.dat; sourceTree = ""; }; + 1A2318C50B9D02A900EF0852 /* moray_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = moray_redux.dat; sourceTree = ""; }; + 1A2318C60B9D02A900EF0852 /* moray_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = moray_redux1.dat; sourceTree = ""; }; + 1A2318C70B9D02A900EF0852 /* new-dodo.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "new-dodo.dat"; sourceTree = ""; }; + 1A2318C80B9D02A900EF0852 /* new-icos.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "new-icos.dat"; sourceTree = ""; }; + 1A2318C90B9D02A900EF0852 /* new-rock.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "new-rock.dat"; sourceTree = ""; }; + 1A2318CA0B9D02A900EF0852 /* new_coriolis.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = new_coriolis.dat; sourceTree = ""; }; + 1A2318CB0B9D02A900EF0852 /* python_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = python_redux.dat; sourceTree = ""; }; + 1A2318CC0B9D02A900EF0852 /* python_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = python_redux1.dat; sourceTree = ""; }; + 1A2318CD0B9D02AA00EF0852 /* python_redux2.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = python_redux2.dat; sourceTree = ""; }; + 1A2318CE0B9D02AA00EF0852 /* qbomb.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = qbomb.dat; sourceTree = ""; }; + 1A2318CF0B9D02AA00EF0852 /* ring.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ring.dat; sourceTree = ""; }; + 1A2318D00B9D02AA00EF0852 /* rock-box.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "rock-box.dat"; sourceTree = ""; }; + 1A2318D10B9D02AA00EF0852 /* rock-dock.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "rock-dock.dat"; sourceTree = ""; }; + 1A2318D20B9D02AA00EF0852 /* scarred_alloy.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = scarred_alloy.dat; sourceTree = ""; }; + 1A2318D30B9D02AA00EF0852 /* shuttle_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = shuttle_redux.dat; sourceTree = ""; }; + 1A2318D40B9D02AA00EF0852 /* sidewinder_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = sidewinder_redux.dat; sourceTree = ""; }; + 1A2318D50B9D02AA00EF0852 /* splinter.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = splinter.dat; sourceTree = ""; }; + 1A2318D60B9D02AA00EF0852 /* splinter1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = splinter1.dat; sourceTree = ""; }; + 1A2318D70B9D02AA00EF0852 /* strut.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = strut.dat; sourceTree = ""; }; + 1A2318D80B9D02AA00EF0852 /* thargoid_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = thargoid_redux.dat; sourceTree = ""; }; + 1A2318D90B9D02AA00EF0852 /* thargon_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = thargon_redux.dat; sourceTree = ""; }; + 1A2318DA0B9D02AA00EF0852 /* transporter_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = transporter_redux.dat; sourceTree = ""; }; + 1A2318DB0B9D02AA00EF0852 /* transporter_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = transporter_redux1.dat; sourceTree = ""; }; + 1A2318DC0B9D02AA00EF0852 /* viper_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = viper_redux.dat; sourceTree = ""; }; + 1A2318DD0B9D02AA00EF0852 /* viperi_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = viperi_redux.dat; sourceTree = ""; }; + 1A2318DE0B9D02AA00EF0852 /* worm_redux.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = worm_redux.dat; sourceTree = ""; }; + 1A2318DF0B9D02AA00EF0852 /* worm_redux1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = worm_redux1.dat; sourceTree = ""; }; + 1A2318E00B9D02AA00EF0852 /* wreck1.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wreck1.dat; sourceTree = ""; }; + 1A2318E10B9D02AA00EF0852 /* wreck2.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wreck2.dat; sourceTree = ""; }; + 1A2318E20B9D02AA00EF0852 /* wreck3.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wreck3.dat; sourceTree = ""; }; + 1A2318E30B9D02AA00EF0852 /* wreck4.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wreck4.dat; sourceTree = ""; }; + 1A2318E40B9D02AA00EF0852 /* wreck5.dat */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = wreck5.dat; sourceTree = ""; }; + 1A2319740B9D02F900EF0852 /* BlueDanube.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = BlueDanube.ogg; sourceTree = ""; }; + 1A2319750B9D02F900EF0852 /* OoliteTheme.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = OoliteTheme.ogg; sourceTree = ""; }; + 1A2319920B9D031D00EF0852 /* afterburner1.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = afterburner1.ogg; sourceTree = ""; }; + 1A2319940B9D031D00EF0852 /* beep.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = beep.ogg; sourceTree = ""; }; + 1A2319950B9D031D00EF0852 /* bigbang.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = bigbang.ogg; sourceTree = ""; }; + 1A2319960B9D031D00EF0852 /* boop.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = boop.ogg; sourceTree = ""; }; + 1A2319970B9D031D00EF0852 /* breakpattern.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = breakpattern.ogg; sourceTree = ""; }; + 1A2319980B9D031D00EF0852 /* buy.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = buy.ogg; sourceTree = ""; }; + 1A2319990B9D031D00EF0852 /* ecm.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = ecm.ogg; sourceTree = ""; }; + 1A23199A0B9D031D00EF0852 /* guiclick.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = guiclick.ogg; sourceTree = ""; }; + 1A23199B0B9D031D00EF0852 /* hit.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = hit.ogg; sourceTree = ""; }; + 1A23199C0B9D031D00EF0852 /* hullbang.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = hullbang.ogg; sourceTree = ""; }; + 1A23199D0B9D031D00EF0852 /* laser.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = laser.ogg; sourceTree = ""; }; + 1A23199E0B9D031D00EF0852 /* laserhits.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = laserhits.ogg; sourceTree = ""; }; + 1A23199F0B9D031D00EF0852 /* missile.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = missile.ogg; sourceTree = ""; }; + 1A2319A00B9D031D00EF0852 /* scoop.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = scoop.ogg; sourceTree = ""; }; + 1A2319A10B9D031D00EF0852 /* sell.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = sell.ogg; sourceTree = ""; }; + 1A2319A20B9D031D00EF0852 /* trumble.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = trumble.ogg; sourceTree = ""; }; + 1A2319A30B9D031D00EF0852 /* trumblesqueal.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = trumblesqueal.ogg; sourceTree = ""; }; + 1A2319A40B9D031D00EF0852 /* warning.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = warning.ogg; sourceTree = ""; }; + 1A2319A50B9D031D00EF0852 /* witchabort.ogg */ = {isa = PBXFileReference; lastKnownFileType = file; path = witchabort.ogg; sourceTree = ""; }; + 1A231A160B9D8B1B00EF0852 /* OOCacheManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCacheManager.h; sourceTree = ""; }; + 1A231A170B9D8B1B00EF0852 /* OOCacheManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCacheManager.m; sourceTree = ""; }; + 1A26D0880BCF9CF70073F257 /* PlayerEntityLegacyScriptEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntityLegacyScriptEngine.m; sourceTree = ""; }; + 1A26D0890BCF9CF70073F257 /* ShipEntityAI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShipEntityAI.m; sourceTree = ""; }; + 1A26D08A0BCF9CF70073F257 /* ShipEntityAI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShipEntityAI.h; sourceTree = ""; }; + 1A26D08B0BCF9CF70073F257 /* PlayerEntityScriptMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntityScriptMethods.m; sourceTree = ""; }; + 1A26D08C0BCF9CF70073F257 /* PlayerEntityLoadSave.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntityLoadSave.m; sourceTree = ""; }; + 1A26D08D0BCF9CF70073F257 /* PlayerEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntity.m; sourceTree = ""; }; + 1A26D08E0BCF9CF70073F257 /* PlayerEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntity.h; sourceTree = ""; }; + 1A26D08F0BCF9CF70073F257 /* ShipEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShipEntity.m; sourceTree = ""; }; + 1A26D0900BCF9CF70073F257 /* ShipEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShipEntity.h; sourceTree = ""; }; + 1A26D0910BCF9CF70073F257 /* PlayerEntityScriptMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntityScriptMethods.h; sourceTree = ""; }; + 1A26D0920BCF9CF70073F257 /* SkyEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SkyEntity.m; sourceTree = ""; }; + 1A26D0930BCF9CF70073F257 /* SkyEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SkyEntity.h; sourceTree = ""; }; + 1A26D0940BCF9CF70073F257 /* RingEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RingEntity.m; sourceTree = ""; }; + 1A26D0950BCF9CF70073F257 /* RingEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RingEntity.h; sourceTree = ""; }; + 1A26D0960BCF9CF70073F257 /* ParticleEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ParticleEntity.m; sourceTree = ""; }; + 1A26D0970BCF9CF70073F257 /* ParticleEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ParticleEntity.h; sourceTree = ""; }; + 1A26D0980BCF9CF70073F257 /* PlayerEntityContracts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntityContracts.h; sourceTree = ""; }; + 1A26D0990BCF9CF70073F257 /* PlanetEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlanetEntity.m; sourceTree = ""; }; + 1A26D09A0BCF9CF70073F257 /* PlanetEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlanetEntity.h; sourceTree = ""; }; + 1A26D09B0BCF9CF70073F257 /* PlayerEntityContracts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntityContracts.m; sourceTree = ""; }; + 1A26D09C0BCF9CF70073F257 /* PlayerEntityControls.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntityControls.m; sourceTree = ""; }; + 1A26D09D0BCF9CF70073F257 /* Entity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Entity.h; sourceTree = ""; }; + 1A26D09E0BCF9CF70073F257 /* PlayerEntityStickMapper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntityStickMapper.m; sourceTree = ""; }; + 1A26D09F0BCF9CF70073F257 /* PlayerEntityStickMapper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntityStickMapper.h; sourceTree = ""; }; + 1A26D0A00BCF9CF70073F257 /* WormholeEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WormholeEntity.m; sourceTree = ""; }; + 1A26D0A10BCF9CF70073F257 /* WormholeEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WormholeEntity.h; sourceTree = ""; }; + 1A26D0A20BCF9CF70073F257 /* StationEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StationEntity.m; sourceTree = ""; }; + 1A26D0A30BCF9CF70073F257 /* StationEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StationEntity.h; sourceTree = ""; }; + 1A26D0A40BCF9CF70073F257 /* PlayerEntityControls.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntityControls.h; sourceTree = ""; }; + 1A26D0A50BCF9CF70073F257 /* PlayerEntitySound.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PlayerEntitySound.m; sourceTree = ""; }; + 1A26D0A60BCF9CF70073F257 /* PlayerEntitySound.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntitySound.h; sourceTree = ""; }; + 1A26D0A70BCF9CF70073F257 /* DustEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DustEntity.h; sourceTree = ""; }; + 1A26D0A80BCF9CF70073F257 /* Entity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Entity.m; sourceTree = ""; }; + 1A26D0A90BCF9CF80073F257 /* DustEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DustEntity.m; sourceTree = ""; }; + 1A26D0AA0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PlayerEntityLegacyScriptEngine.h; sourceTree = ""; }; + 1A26D0AB0BCF9CF80073F257 /* PlayerEntityLoadSave.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PlayerEntityLoadSave.h; sourceTree = ""; }; + 1A26D0D00BCF9D0D0073F257 /* OOShaderMaterial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOShaderMaterial.h; sourceTree = ""; }; + 1A26D0D10BCF9D0D0073F257 /* OOShaderMaterial.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOShaderMaterial.m; sourceTree = ""; }; + 1A26D0D20BCF9D0D0073F257 /* OOMaterial.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOMaterial.m; sourceTree = ""; }; + 1A26D0D30BCF9D0D0073F257 /* OOMaterial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMaterial.h; sourceTree = ""; }; + 1A26D0D80BCF9D1E0073F257 /* OOShaderUniform.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOShaderUniform.h; sourceTree = ""; }; + 1A26D0D90BCF9D1E0073F257 /* OOShaderProgram.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOShaderProgram.m; sourceTree = ""; }; + 1A26D0DA0BCF9D1E0073F257 /* OOShaderProgram.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOShaderProgram.h; sourceTree = ""; }; + 1A26D0DB0BCF9D1E0073F257 /* OOShaderUniform.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOShaderUniform.m; sourceTree = ""; }; + 1A26D0E00BCF9D3B0073F257 /* OOTexture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOTexture.m; sourceTree = ""; }; + 1A26D0E10BCF9D3B0073F257 /* OOTexture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTexture.h; sourceTree = ""; }; + 1A26D0E20BCF9D3B0073F257 /* OOPNGTextureLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOPNGTextureLoader.m; sourceTree = ""; }; + 1A26D0E30BCF9D3B0073F257 /* OOTextureLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTextureLoader.h; sourceTree = ""; }; + 1A26D0E40BCF9D3B0073F257 /* OOPNGTextureLoader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOPNGTextureLoader.h; sourceTree = ""; }; + 1A26D0E50BCF9D3B0073F257 /* OOTextureLoader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOTextureLoader.m; sourceTree = ""; }; + 1A26D0F40BCF9D8D0073F257 /* pngusr.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = pngusr.h; path = src/Core/Materials/pngusr.h; sourceTree = SOURCE_ROOT; }; + 1A27DB380C4E349F00CB4CE8 /* OOOXPVerifierStageInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOOXPVerifierStageInternal.h; sourceTree = ""; }; + 1A27DB390C4E349F00CB4CE8 /* OOOXPVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOOXPVerifierStage.h; sourceTree = ""; }; + 1A27DB3A0C4E349F00CB4CE8 /* OOOXPVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOOXPVerifierStage.m; sourceTree = ""; }; + 1A27DB400C4E34B300CB4CE8 /* OOFileScannerVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOFileScannerVerifierStage.h; sourceTree = ""; }; + 1A27DB410C4E34B300CB4CE8 /* OOFileScannerVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOFileScannerVerifierStage.m; sourceTree = ""; }; + 1A28AA140D55438200BC0CE4 /* OOJSSound.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSSound.h; sourceTree = ""; }; + 1A28AA150D55438200BC0CE4 /* OOJSSound.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSSound.m; sourceTree = ""; }; + 1A29967C0B9F064C002D2149 /* OOCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCache.h; sourceTree = ""; }; + 1A29967D0B9F064C002D2149 /* OOCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCache.m; sourceTree = ""; }; + 1A2A16660BD10B1200152975 /* OOSingleTextureMaterial.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOSingleTextureMaterial.m; sourceTree = ""; }; + 1A2A16670BD10B1200152975 /* OOSingleTextureMaterial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSingleTextureMaterial.h; sourceTree = ""; }; + 1A2A17D40BD1587D00152975 /* OOCPUInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCPUInfo.h; sourceTree = ""; }; + 1A2A17D50BD1587D00152975 /* OOCPUInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCPUInfo.m; sourceTree = ""; }; + 1A2A1B050BD276A900152975 /* OOEntityWithDrawable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOEntityWithDrawable.h; sourceTree = ""; }; + 1A2A1B060BD276A900152975 /* OOEntityWithDrawable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOEntityWithDrawable.m; sourceTree = ""; }; + 1A2A1B120BD2774300152975 /* OODrawable.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODrawable.h; sourceTree = ""; }; + 1A2A1B130BD2774300152975 /* OODrawable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OODrawable.m; sourceTree = ""; }; + 1A2A1B280BD277D800152975 /* OOSelfDrawingEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSelfDrawingEntity.h; sourceTree = ""; }; + 1A2A1B290BD277D800152975 /* OOSelfDrawingEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOSelfDrawingEntity.m; sourceTree = ""; }; + 1A2A1CA80BD2914F00152975 /* OOMesh.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMesh.h; sourceTree = ""; }; + 1A2A1CA90BD2914F00152975 /* OOMesh.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOMesh.m; sourceTree = ""; }; + 1A2A1DEA0BD2A28E00152975 /* OOMacroOpenGL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMacroOpenGL.h; sourceTree = ""; }; + 1A2A8C130BC65FFD001E00FB /* OOJSEntity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSEntity.h; sourceTree = ""; }; + 1A2A8C140BC65FFD001E00FB /* OOJSEntity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSEntity.m; sourceTree = ""; }; + 1A2A8D380BC6765F001E00FB /* EntityOOJavaScriptExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EntityOOJavaScriptExtensions.h; sourceTree = ""; }; + 1A2A8D390BC6765F001E00FB /* EntityOOJavaScriptExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EntityOOJavaScriptExtensions.m; sourceTree = ""; }; + 1A2A8E010BC67CCC001E00FB /* OOWeakReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOWeakReference.h; sourceTree = ""; }; + 1A2A8E020BC67CCC001E00FB /* OOWeakReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOWeakReference.m; sourceTree = ""; }; + 1A2A91500BC6BC66001E00FB /* OOJSQuaternion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSQuaternion.h; sourceTree = ""; }; + 1A2A91510BC6BC66001E00FB /* OOJSQuaternion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSQuaternion.m; sourceTree = ""; }; + 1A2DA2A40CB4CB5C00DE6823 /* OODebugTCPConsoleProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODebugTCPConsoleProtocol.h; sourceTree = ""; }; + 1A2DA2A50CB4CB5C00DE6823 /* OODebugTCPConsoleClient.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OODebugTCPConsoleClient.m; sourceTree = ""; }; + 1A2DA2A60CB4CB5C00DE6823 /* OOTCPStreamDecoder.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = OOTCPStreamDecoder.c; sourceTree = ""; }; + 1A2DA2A70CB4CB5C00DE6823 /* OODebugTCPConsoleClient.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODebugTCPConsoleClient.h; sourceTree = ""; }; + 1A2DA2A80CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTCPStreamDecoderAbstractionLayer.h; sourceTree = ""; }; + 1A2DA2A90CB4CB5C00DE6823 /* OOTCPStreamDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTCPStreamDecoder.h; sourceTree = ""; }; + 1A2DA2AA0CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOTCPStreamDecoderAbstractionLayer.m; sourceTree = ""; }; + 1A2DA3490CB4D0D800DE6823 /* OOJSConsole.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSConsole.h; sourceTree = ""; }; + 1A2DA34A0CB4D0D800DE6823 /* OODebugMonitor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODebugMonitor.h; sourceTree = ""; }; + 1A2DA34B0CB4D0D800DE6823 /* OOJSConsole.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSConsole.m; sourceTree = ""; }; + 1A2DA34C0CB4D0D800DE6823 /* OODebugMonitor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OODebugMonitor.m; sourceTree = ""; }; + 1A2DA3540CB4D10800DE6823 /* OODebuggerInterface.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODebuggerInterface.h; sourceTree = ""; }; + 1A2DA3EC0CB4E84900DE6823 /* OODebugSupport.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OODebugSupport.m; sourceTree = ""; }; + 1A2DA3ED0CB4E84900DE6823 /* OODebugSupport.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODebugSupport.h; sourceTree = ""; }; + 1A2F63950C5CC737003872C8 /* demoshipsSchema.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = demoshipsSchema.plist; sourceTree = ""; }; + 1A2F63960C5CC737003872C8 /* hudSchema.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = hudSchema.plist; sourceTree = ""; }; + 1A2F63980C5CC737003872C8 /* shipdataEntrySchema.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = shipdataEntrySchema.plist; sourceTree = ""; }; + 1A2F63990C5CC737003872C8 /* shipyardSchema.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = shipyardSchema.plist; sourceTree = ""; }; + 1A3159250F1B7B7E00328E4A /* OOAIStateMachineVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOAIStateMachineVerifierStage.h; sourceTree = ""; }; + 1A3159260F1B7B7E00328E4A /* OOAIStateMachineVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOAIStateMachineVerifierStage.m; sourceTree = ""; }; + 1A3491290BC25EAA00802DA7 /* world-scripts.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = "world-scripts.plist"; sourceTree = ""; }; + 1A3491AA0BC282DE00802DA7 /* ReleaseLockProxy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ReleaseLockProxy.h; sourceTree = ""; }; + 1A3491AB0BC282DE00802DA7 /* ReleaseLockProxy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ReleaseLockProxy.m; sourceTree = ""; }; + 1A358CE10C1AB80D00E52220 /* ReadMe.rtfd */ = {isa = PBXFileReference; lastKnownFileType = wrapper.rtfd; name = ReadMe.rtfd; path = Doc/ReadMe.rtfd; sourceTree = SOURCE_ROOT; }; + 1A3591690C1C375400E52220 /* oolite-nebula-1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-nebula-1.png"; sourceTree = ""; }; + 1A35916A0C1C375400E52220 /* oolite-star-1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-star-1.png"; sourceTree = ""; }; + 1A35916B0C1C375400E52220 /* oolite-nebula-3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-nebula-3.png"; sourceTree = ""; }; + 1A35916C0C1C375400E52220 /* oolite-nebula-2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-nebula-2.png"; sourceTree = ""; }; + 1A3591810C1C382700E52220 /* nebulatextures.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = nebulatextures.plist; sourceTree = ""; }; + 1A3591820C1C382700E52220 /* startextures.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = startextures.plist; sourceTree = ""; }; + 1A37D1ED1049B2E400BC8976 /* CHANGELOG.TXT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = CHANGELOG.TXT; path = Doc/CHANGELOG.TXT; sourceTree = ""; }; + 1A38B4AA0B988532001ED4A0 /* OOLogging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOLogging.h; sourceTree = ""; }; + 1A38B4AB0B988532001ED4A0 /* OOLogging.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOLogging.m; sourceTree = ""; }; + 1A3A04610BC547DC00B5E2D9 /* OOTypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTypes.h; sourceTree = ""; }; + 1A3ACFE90C5FF33A00EC50A7 /* OOJSShip.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSShip.h; sourceTree = ""; }; + 1A3ACFEA0C5FF33A00EC50A7 /* OOJSShip.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSShip.m; sourceTree = ""; }; + 1A3AFF1D0BC4462200B5E2D9 /* OOJSVector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSVector.h; sourceTree = ""; }; + 1A3AFF1E0BC4462200B5E2D9 /* OOJSVector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSVector.m; sourceTree = ""; }; + 1A3C67F30F1C90BF0000D45B /* OOLegacyScriptWhitelist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOLegacyScriptWhitelist.h; sourceTree = ""; }; + 1A3C67F40F1C90BF0000D45B /* OOLegacyScriptWhitelist.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOLegacyScriptWhitelist.m; sourceTree = ""; }; + 1A43234A0BCFC9BB00F65914 /* OOOpenGLExtensionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOOpenGLExtensionManager.h; sourceTree = ""; }; + 1A43234B0BCFC9BB00F65914 /* OOOpenGLExtensionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOOpenGLExtensionManager.m; sourceTree = ""; }; + 1A43A0CA0CB91D2C00D0E239 /* OOJSPlanet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSPlanet.h; sourceTree = ""; }; + 1A43A0CB0CB91D2C00D0E239 /* OOJSPlanet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSPlanet.m; sourceTree = ""; }; + 1A43A1840CB9243B00D0E239 /* OOEntityFilterPredicate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOEntityFilterPredicate.h; sourceTree = ""; }; + 1A43A1850CB9243B00D0E239 /* OOEntityFilterPredicate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOEntityFilterPredicate.m; sourceTree = ""; }; + 1A4501E10DBF699A00815C04 /* descriptions.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = descriptions.plist; sourceTree = ""; }; + 1A472916096B5454000E78D8 /* CoreAudio.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreAudio.framework; path = /System/Library/Frameworks/CoreAudio.framework; sourceTree = ""; }; + 1A47291F096B5468000E78D8 /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = /System/Library/Frameworks/AudioToolbox.framework; sourceTree = ""; }; + 1A472920096B5468000E78D8 /* AudioUnit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioUnit.framework; path = /System/Library/Frameworks/AudioUnit.framework; sourceTree = ""; }; + 1A4D67D81041DCF3001AC80F /* oolite-javascript-errors.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = "oolite-javascript-errors.plist"; sourceTree = ""; }; + 1A4FB2390C8D6A9A00DC8E1F /* jsautocfg.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = jsautocfg.h; path = xcode/jsautocfg.h; sourceTree = ""; }; + 1A4FB23B0C8D6AA900DC8E1F /* jsapi.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = jsapi.h; path = src/jsapi.h; sourceTree = ""; }; + 1A4FB23E0C8D6AB400DC8E1F /* jspubtd.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = jspubtd.h; path = src/jspubtd.h; sourceTree = ""; }; + 1A4FB2400C8D6ABA00DC8E1F /* jstypes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = jstypes.h; path = src/jstypes.h; sourceTree = ""; }; + 1A4FB3330C8DC86800DC8E1F /* OOJSOolite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSOolite.h; sourceTree = ""; }; + 1A4FB3340C8DC86800DC8E1F /* OOJSOolite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSOolite.m; sourceTree = ""; }; + 1A5218D80D72EC21000865E9 /* OOSpatialReference.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSpatialReference.h; sourceTree = ""; }; + 1A5218DD0D72ECE5000865E9 /* OOSpatialReference.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOSpatialReference.m; sourceTree = ""; }; + 1A54AB510E3E17A0001EB817 /* OOJSPlayerShip.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSPlayerShip.h; sourceTree = ""; }; + 1A54AB520E3E17A0001EB817 /* OOJSPlayerShip.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSPlayerShip.m; sourceTree = ""; }; + 1A572FB80D84B640003D4A26 /* oolite-nova-mission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-nova-mission.js"; sourceTree = ""; }; + 1A572FB90D84B640003D4A26 /* oolite-thargoid-plans-mission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-thargoid-plans-mission.js"; sourceTree = ""; }; + 1A572FBA0D84B640003D4A26 /* oolite-trumbles-mission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-trumbles-mission.js"; sourceTree = ""; }; + 1A572FBB0D84B640003D4A26 /* oolite-default-ship-script.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-default-ship-script.js"; sourceTree = ""; }; + 1A572FBC0D84B640003D4A26 /* oolite-cloaking-device-mission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-cloaking-device-mission.js"; sourceTree = ""; }; + 1A572FBD0D84B640003D4A26 /* oolite-constrictor.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-constrictor.js"; sourceTree = ""; }; + 1A572FBE0D84B640003D4A26 /* oolite-constrictor-hunt-mission.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-constrictor-hunt-mission.js"; sourceTree = ""; }; + 1A572FBF0D84B640003D4A26 /* oolite-cloaking-device-target-ship.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-cloaking-device-target-ship.js"; sourceTree = ""; }; + 1A572FC00D84B640003D4A26 /* oolite-cloaking-device-pod.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-cloaking-device-pod.js"; sourceTree = ""; }; + 1A572FC10D84B640003D4A26 /* oolite-global-prefix.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-global-prefix.js"; sourceTree = ""; }; + 1A572FC20D84B640003D4A26 /* oolite-thargoid-warship.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = "oolite-thargoid-warship.js"; sourceTree = ""; }; + 1A572FE40D84B6A3003D4A26 /* oolite-constrictor-AI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = "oolite-constrictor-AI.plist"; sourceTree = ""; }; + 1A5AA3220C0098AF0029C78A /* OOOpenGL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOOpenGL.m; sourceTree = ""; }; + 1A5BF2720916D47300BF238F /* Oolite-importer.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "Oolite-importer.xcodeproj"; path = "Oolite-importer/Oolite-importer.xcodeproj"; sourceTree = ""; }; + 1A5DB1E80BBD8F0000D57389 /* OOConstToString.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOConstToString.h; sourceTree = ""; }; + 1A5DB1E90BBD8F0000D57389 /* OOConstToString.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOConstToString.m; sourceTree = ""; }; + 1A5DBA9A0BC000DC00D57389 /* OOJavaScriptEngine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJavaScriptEngine.h; sourceTree = ""; }; + 1A5DBA9B0BC000DC00D57389 /* OOJavaScriptEngine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJavaScriptEngine.m; sourceTree = ""; }; + 1A5DBA9C0BC000DC00D57389 /* OOJSScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSScript.h; sourceTree = ""; }; + 1A5DBA9D0BC000DC00D57389 /* OOJSScript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSScript.m; sourceTree = ""; }; + 1A5DBA9E0BC000DC00D57389 /* OOPListScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOPListScript.h; sourceTree = ""; }; + 1A5DBA9F0BC000DC00D57389 /* OOPListScript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOPListScript.m; sourceTree = ""; }; + 1A5DBAA10BC000DC00D57389 /* OOScript.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOScript.h; sourceTree = ""; }; + 1A5DBAA20BC000DC00D57389 /* OOScript.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOScript.m; sourceTree = ""; }; + 1A5DBD560BC17F0900D57389 /* NSStringOOExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSStringOOExtensions.h; sourceTree = ""; }; + 1A5DBD570BC17F0900D57389 /* NSStringOOExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSStringOOExtensions.m; sourceTree = ""; }; + 1A5E462D0C32DACE008104B4 /* OOShaderUniformMethodType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOShaderUniformMethodType.m; sourceTree = ""; }; + 1A5E462E0C32DACE008104B4 /* OOShaderUniformMethodType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOShaderUniformMethodType.h; sourceTree = ""; }; + 1A60AFB50D56093B0070510D /* OOMusicController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMusicController.h; sourceTree = ""; }; + 1A60AFB60D56093B0070510D /* OOMusicController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOMusicController.m; sourceTree = ""; }; + 1A62F0FB0E26A2A000897506 /* OOEquipmentType.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOEquipmentType.m; sourceTree = ""; }; + 1A62F0FC0E26A2A000897506 /* OOEquipmentType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOEquipmentType.h; sourceTree = ""; }; + 1A65150D0CCC9E220054D01B /* oolite-standard-vertex.vertex */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "oolite-standard-vertex.vertex"; sourceTree = ""; }; + 1A65150E0CCC9E220054D01B /* oolite-default-shader.fragment */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "oolite-default-shader.fragment"; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.simpleColoring; }; + 1A6B1EEE0C9AA5C6000717CF /* OOScriptTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOScriptTimer.h; sourceTree = ""; }; + 1A6B1EEF0C9AA5C6000717CF /* OOScriptTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOScriptTimer.m; sourceTree = ""; }; + 1A6B1F340C9AAA60000717CF /* OOPriorityQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOPriorityQueue.m; sourceTree = ""; }; + 1A6B1F350C9AAA60000717CF /* OOPriorityQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOPriorityQueue.h; sourceTree = ""; }; + 1A6B228B0C9B40D4000717CF /* OOJSTimer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSTimer.h; sourceTree = ""; }; + 1A6B228C0C9B40D4000717CF /* OOJSTimer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSTimer.m; sourceTree = ""; }; + 1A6B25EC0C9C2745000717CF /* OOJSClock.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSClock.h; sourceTree = ""; }; + 1A6B25ED0C9C2746000717CF /* OOJSClock.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSClock.m; sourceTree = ""; }; + 1A6B50370C8B42480035DFCC /* libjs.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = libjs.xcodeproj; path = xcode/libjs.xcodeproj; sourceTree = ""; }; + 1A6DD1210C57B5BC00A892F4 /* OOPListSchemaVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOPListSchemaVerifier.h; sourceTree = ""; }; + 1A6DD1220C57B5BC00A892F4 /* OOPListSchemaVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOPListSchemaVerifier.m; sourceTree = ""; }; + 1A6DD19E0C5924A300A892F4 /* plist verifier design.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "plist verifier design.txt"; sourceTree = ""; }; + 1A71D8A80E5F17410088C456 /* OOSoundSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOSoundSource.m; sourceTree = ""; }; + 1A71D8A90E5F17410088C456 /* OOSoundSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSoundSource.h; sourceTree = ""; }; + 1A71E6F30BCE340C00CD5C13 /* libpng.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libpng.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 1A71E7030BCE34CF00CD5C13 /* pngconf.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = pngconf.h; sourceTree = ""; }; + 1A71E7040BCE34CF00CD5C13 /* pngmem.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngmem.c; sourceTree = ""; }; + 1A71E7050BCE34CF00CD5C13 /* pngwutil.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngwutil.c; sourceTree = ""; }; + 1A71E7060BCE34CF00CD5C13 /* pngwio.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngwio.c; sourceTree = ""; }; + 1A71E7070BCE34CF00CD5C13 /* png.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = png.h; sourceTree = ""; }; + 1A71E7080BCE34CF00CD5C13 /* pngtrans.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngtrans.c; sourceTree = ""; }; + 1A71E7090BCE34CF00CD5C13 /* pngrtran.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngrtran.c; sourceTree = ""; }; + 1A71E70A0BCE34CF00CD5C13 /* png.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = png.c; sourceTree = ""; }; + 1A71E70B0BCE34CF00CD5C13 /* pngwtran.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngwtran.c; sourceTree = ""; }; + 1A71E70C0BCE34CF00CD5C13 /* pngread.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngread.c; sourceTree = ""; }; + 1A71E70D0BCE34CF00CD5C13 /* pngrutil.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngrutil.c; sourceTree = ""; }; + 1A71E70E0BCE34CF00CD5C13 /* pngtest.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngtest.c; sourceTree = ""; }; + 1A71E70F0BCE34CF00CD5C13 /* pngerror.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngerror.c; sourceTree = ""; }; + 1A71E7100BCE34CF00CD5C13 /* pngset.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngset.c; sourceTree = ""; }; + 1A71E7110BCE34CF00CD5C13 /* pngwrite.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngwrite.c; sourceTree = ""; }; + 1A71E7120BCE34CF00CD5C13 /* pngget.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngget.c; sourceTree = ""; }; + 1A71E7130BCE34CF00CD5C13 /* pnggccrd.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pnggccrd.c; sourceTree = ""; }; + 1A71E7140BCE34CF00CD5C13 /* pngrio.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngrio.c; sourceTree = ""; }; + 1A71E7150BCE34CF00CD5C13 /* pngvcrd.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngvcrd.c; sourceTree = ""; }; + 1A71E7160BCE34CF00CD5C13 /* pngpread.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pngpread.c; sourceTree = ""; }; + 1A71E8780BCE8EB100CD5C13 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = /usr/lib/libz.dylib; sourceTree = ""; }; + 1A736BD10C61E9370097AC37 /* OOJSPlayer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSPlayer.h; sourceTree = ""; }; + 1A736BD20C61E9370097AC37 /* OOJSPlayer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSPlayer.m; sourceTree = ""; }; + 1A736C7D0C61FD220097AC37 /* OOJSCall.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSCall.h; sourceTree = ""; }; + 1A736C7E0C61FD220097AC37 /* OOJSCall.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSCall.m; sourceTree = ""; }; + 1A73712B0C623DAE0097AC37 /* OOJSStation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSStation.h; sourceTree = ""; }; + 1A73712C0C623DAE0097AC37 /* OOJSStation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSStation.m; sourceTree = ""; }; + 1A7376BC0C64AE330097AC37 /* OOJSSystem.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSSystem.h; sourceTree = ""; }; + 1A7376BD0C64AE330097AC37 /* OOJSSystem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSSystem.m; sourceTree = ""; }; + 1A7B967E0E620C9E00322821 /* OOSoundInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSoundInternal.h; sourceTree = ""; }; + 1A7BA80B0D84231A003C6CA3 /* ShipEntityScriptMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ShipEntityScriptMethods.h; sourceTree = ""; }; + 1A7BA80C0D84231A003C6CA3 /* ShipEntityScriptMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ShipEntityScriptMethods.m; sourceTree = ""; }; + 1A7C75980CC39D11005D0AA2 /* OOJSSun.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSSun.m; sourceTree = ""; }; + 1A7C75990CC39D11005D0AA2 /* OOJSSun.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSSun.h; sourceTree = ""; }; + 1A7D3A160C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCheckRequiresPListVerifierStage.h; sourceTree = ""; }; + 1A7D3A170C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCheckRequiresPListVerifierStage.m; sourceTree = ""; }; + 1A7D3B990C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCheckDemoShipsPListVerifierStage.h; sourceTree = ""; }; + 1A7D3B9A0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCheckDemoShipsPListVerifierStage.m; sourceTree = ""; }; + 1A7D3C470C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCheckEquipmentPListVerifierStage.h; sourceTree = ""; }; + 1A7D3C480C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCheckEquipmentPListVerifierStage.m; sourceTree = ""; }; + 1A7D3D290C4F8D01008EDC33 /* OOTextureVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTextureVerifierStage.h; sourceTree = ""; }; + 1A7D3D2A0C4F8D02008EDC33 /* OOTextureVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOTextureVerifierStage.m; sourceTree = ""; }; + 1A7D3EFD0C50F337008EDC33 /* OXP verifier design.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "OXP verifier design.txt"; sourceTree = ""; }; + 1A7D41840C516B90008EDC33 /* OOModelVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOModelVerifierStage.m; sourceTree = ""; }; + 1A7D41850C516B90008EDC33 /* OOModelVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOModelVerifierStage.h; sourceTree = ""; }; + 1A7D41E00C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCheckShipDataPListVerifierStage.h; sourceTree = ""; }; + 1A7D41E10C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCheckShipDataPListVerifierStage.m; sourceTree = ""; }; + 1A7D83380C40147700E4A5F5 /* OOAsyncQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOAsyncQueue.h; sourceTree = ""; }; + 1A7D83390C40147700E4A5F5 /* OOAsyncQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOAsyncQueue.m; sourceTree = ""; }; + 1A81F7070A7BAC4D006580AD /* OOCAMusic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCAMusic.m; sourceTree = ""; }; + 1A81F7080A7BAC4D006580AD /* OOCAMusic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCAMusic.h; sourceTree = ""; }; + 1A846BA90D79F9570081280D /* oolite-version.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "oolite-version.xcconfig"; path = "src/Cocoa/oolite-version.xcconfig"; sourceTree = ""; }; + 1A8A37540B960337007D20B8 /* NSMutableDictionaryOOExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSMutableDictionaryOOExtensions.m; sourceTree = ""; }; + 1A8A37550B960337007D20B8 /* NSMutableDictionaryOOExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSMutableDictionaryOOExtensions.h; sourceTree = ""; }; + 1A8A394C0B96229C007D20B8 /* NSFileManagerOOExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSFileManagerOOExtensions.m; sourceTree = ""; }; + 1A8A394D0B96229C007D20B8 /* NSFileManagerOOExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSFileManagerOOExtensions.h; sourceTree = ""; }; + 1A8A3A360B962AEF007D20B8 /* NSScannerOOExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSScannerOOExtensions.h; sourceTree = ""; }; + 1A8A3A370B962AEF007D20B8 /* NSScannerOOExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSScannerOOExtensions.m; sourceTree = ""; }; + 1A8BB8E80E8311F900122974 /* OONullTexture.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OONullTexture.h; sourceTree = ""; }; + 1A8BB8E90E8311F900122974 /* OONullTexture.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OONullTexture.m; sourceTree = ""; }; + 1A9322950DF53C33003FD306 /* OOJSSystemInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSSystemInfo.h; sourceTree = ""; }; + 1A9322960DF53C33003FD306 /* OOJSSystemInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSSystemInfo.m; sourceTree = ""; }; + 1A9400BD0BAF0ECD005F6CF3 /* OOStringParsing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOStringParsing.m; sourceTree = ""; }; + 1A9400BF0BAF0EDB005F6CF3 /* OOStringParsing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOStringParsing.h; sourceTree = ""; }; + 1A9403CF0BAF36C3005F6CF3 /* OOFunctionAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOFunctionAttributes.h; sourceTree = ""; }; + 1A9404240BAF3DED005F6CF3 /* OOCollectionExtractors.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCollectionExtractors.m; sourceTree = ""; }; + 1A9404250BAF3DED005F6CF3 /* OOCollectionExtractors.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCollectionExtractors.h; sourceTree = ""; }; + 1A9404640BAF42BE005F6CF3 /* OOPListParsing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOPListParsing.h; sourceTree = ""; }; + 1A9404650BAF42BF005F6CF3 /* OOPListParsing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOPListParsing.m; sourceTree = ""; }; + 1A9404920BAF4582005F6CF3 /* OOMaths.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMaths.h; sourceTree = ""; }; + 1A9404A10BAF462D005F6CF3 /* OOVector.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOVector.h; sourceTree = ""; }; + 1A9404A20BAF462D005F6CF3 /* OOVector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOVector.m; sourceTree = ""; }; + 1A9405360BAF4FA6005F6CF3 /* OOMatrix.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOMatrix.h; sourceTree = ""; }; + 1A9405370BAF4FA6005F6CF3 /* OOMatrix.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOMatrix.m; sourceTree = ""; }; + 1A94057D0BAF52AD005F6CF3 /* OOQuaternion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOQuaternion.h; sourceTree = ""; }; + 1A94057E0BAF52AD005F6CF3 /* OOQuaternion.m */ = {isa = PBXFileReference; comments = "-O3 -falign-loops=32 -falign-loops-max-skip=31 -falign-functions=32"; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOQuaternion.m; sourceTree = ""; }; + 1A9406290BAF6170005F6CF3 /* OOBoundingBox.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOBoundingBox.h; sourceTree = ""; }; + 1A9406820BAF66D6005F6CF3 /* OOVoxel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOVoxel.h; sourceTree = ""; }; + 1A9406830BAF66D6005F6CF3 /* OOVoxel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOVoxel.m; sourceTree = ""; }; + 1A9406B20BAF67BF005F6CF3 /* OOTriangle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTriangle.h; sourceTree = ""; }; + 1A9406B30BAF67BF005F6CF3 /* OOTriangle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOTriangle.m; sourceTree = ""; }; + 1A9407BF0BAF7032005F6CF3 /* GNUmakefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GNUmakefile; sourceTree = ""; }; + 1A94D5AC0D65A6960072C805 /* OOLight.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOLight.m; sourceTree = ""; }; + 1A94D5AD0D65A6960072C805 /* OOLight.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOLight.h; sourceTree = ""; }; + 1A94D5B30D65A6B40072C805 /* OOCamera.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCamera.h; sourceTree = ""; }; + 1A94D5B40D65A6B40072C805 /* OOCamera.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCamera.m; sourceTree = ""; }; + 1A94E4FB0F348D4300F1B5D9 /* delayedReactToAttackAI.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = delayedReactToAttackAI.plist; sourceTree = ""; }; + 1A9533890C02089E004EBB58 /* material-defaults.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = "material-defaults.plist"; sourceTree = ""; }; + 1A95338A0C02089E004EBB58 /* planetinfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = planetinfo.plist; sourceTree = ""; }; + 1AA82C810CC10E3D0023B797 /* OOJSWorldScripts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSWorldScripts.h; sourceTree = ""; }; + 1AA82C820CC10E3D0023B797 /* OOJSWorldScripts.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSWorldScripts.m; sourceTree = ""; }; + 1AAB9A960D779F3C00A9F424 /* OOCocoa.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCocoa.m; sourceTree = ""; }; + 1AAF56160F1A198400A2F2E6 /* Comparison.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Comparison.h; sourceTree = ""; }; + 1AAF56590F1A27D900A2F2E6 /* whitelist.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = whitelist.plist; sourceTree = ""; }; + 1AB01ABC0BB15AED00F1B949 /* OOTextureScaling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OOTextureScaling.h; path = ../OOTextureScaling.h; sourceTree = ""; }; + 1AB01ABD0BB15AED00F1B949 /* OOTextureScaling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OOTextureScaling.m; path = ../OOTextureScaling.m; sourceTree = ""; }; + 1AB01BB90BB16A8A00F1B949 /* OOFastArithmetic.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOFastArithmetic.h; sourceTree = ""; }; + 1AB01BBA0BB16A8A00F1B949 /* OOFastArithmetic.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOFastArithmetic.m; sourceTree = ""; }; + 1AB2AAF80C4CE0CC0008CF4E /* OOOXPVerifier.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOOXPVerifier.h; sourceTree = ""; }; + 1AB2AAF90C4CE0CC0008CF4E /* OOOXPVerifier.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOOXPVerifier.m; sourceTree = ""; }; + 1AB2AB120C4CE4070008CF4E /* verifyOXP.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; name = verifyOXP.plist; path = ../../../Resources/Config/verifyOXP.plist; sourceTree = ""; }; + 1AB4AEB60D688AD9003076D6 /* OOLogHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOLogHeader.h; sourceTree = ""; }; + 1AB4AEB70D688AD9003076D6 /* OOLogHeader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOLogHeader.m; sourceTree = ""; }; + 1AB784F70D554F7B00517983 /* OOJSSoundSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSSoundSource.h; sourceTree = ""; }; + 1AB784F80D554F7B00517983 /* OOJSSoundSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSSoundSource.m; sourceTree = ""; }; + 1AB813070E90D8E500A84923 /* OOLogOutputHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOLogOutputHandler.m; sourceTree = ""; }; + 1AB813080E90D8E500A84923 /* OOLogOutputHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOLogOutputHandler.h; sourceTree = ""; }; + 1ABAD72F0F350B3400FD2CBF /* OOShipGroup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOShipGroup.m; sourceTree = ""; }; + 1ABAD7300F350B3400FD2CBF /* OOShipGroup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOShipGroup.h; sourceTree = ""; }; + 1ABB688B0D044306008BE96D /* OOLoggingExtended.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOLoggingExtended.h; sourceTree = ""; }; + 1ABC03EB0EF86110003B740A /* OOCrosshairs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCrosshairs.h; sourceTree = ""; }; + 1ABC03EC0EF86110003B740A /* OOCrosshairs.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = OOCrosshairs.m; sourceTree = ""; }; + 1ABC04260EF872B7003B740A /* crosshairs.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = crosshairs.plist; sourceTree = ""; }; + 1ABC47FC0F155F0500B977AD /* OOJSFunction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSFunction.h; sourceTree = ""; }; + 1ABC47FD0F155F0500B977AD /* OOJSFunction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSFunction.m; sourceTree = ""; }; + 1ABDBA390EB365D90086BC3D /* OOIsNumberLiteral.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOIsNumberLiteral.m; sourceTree = ""; }; + 1ABDBA3A0EB365D90086BC3D /* OOIsNumberLiteral.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOIsNumberLiteral.h; sourceTree = ""; }; + 1AC0E9460B974DC200C46994 /* GPL.TXT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = GPL.TXT; sourceTree = ""; }; + 1AC0E9470B974DC200C46994 /* FAQ.TXT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = FAQ.TXT; sourceTree = ""; }; + 1AC0E9480B974DC200C46994 /* ReadMe.rtfd */ = {isa = PBXFileReference; lastKnownFileType = wrapper.rtfd; path = ReadMe.rtfd; sourceTree = ""; }; + 1AC0E9490B974DC200C46994 /* LICENSE.TXT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.TXT; sourceTree = ""; }; + 1AC0E94A0B974DC200C46994 /* PORTING.TXT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = PORTING.TXT; sourceTree = ""; }; + 1AC0E94B0B974DC200C46994 /* README_LINUX.TXT */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = README_LINUX.TXT; sourceTree = ""; }; + 1AC27A0D0EA7E9940054E5F0 /* OOJSEquipmentInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSEquipmentInfo.h; sourceTree = ""; }; + 1AC27A0E0EA7E9940054E5F0 /* OOJSEquipmentInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSEquipmentInfo.m; sourceTree = ""; }; + 1AC544F90D4D217900C90E5B /* oolite-font.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-font.png"; sourceTree = ""; }; + 1AC545040D4D228400C90E5B /* OOEncodingConverter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOEncodingConverter.h; sourceTree = ""; }; + 1AC545050D4D228400C90E5B /* OOEncodingConverter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOEncodingConverter.m; sourceTree = ""; }; + 1AC5452C0D4D298E00C90E5B /* oolite-font.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = "oolite-font.plist"; sourceTree = ""; }; + 1AC775E00C2DD4E900ECFF3B /* OODebugGLDrawing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODebugGLDrawing.h; sourceTree = ""; }; + 1AC775E10C2DD4E900ECFF3B /* OODebugGLDrawing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OODebugGLDrawing.m; sourceTree = ""; }; + 1AC973F90C9847850010C42B /* pirate-victim-roles.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist; path = "pirate-victim-roles.plist"; sourceTree = ""; }; + 1ACBF06F0D82DF9B00CC005F /* OOSoundSourcePool.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSoundSourcePool.h; sourceTree = ""; }; + 1ACBF0700D82DF9B00CC005F /* OOSoundSourcePool.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOSoundSourcePool.m; sourceTree = ""; }; + 1ACE208E0D805F78009F6957 /* oolite-scarred-metal-specular.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-scarred-metal-specular.png"; sourceTree = ""; }; + 1ACEA3470C91507000C7CE97 /* OORoleSet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OORoleSet.h; sourceTree = ""; }; + 1ACEA3480C91507000C7CE97 /* OORoleSet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OORoleSet.m; sourceTree = ""; }; + 1ACEA6BD0C91DA3E00C7CE97 /* OOJSGlobal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSGlobal.h; sourceTree = ""; }; + 1ACEA6BE0C91DA3E00C7CE97 /* OOJSGlobal.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSGlobal.m; sourceTree = ""; }; + 1ACEA7260C91DF2800C7CE97 /* OOJSMissionVariables.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSMissionVariables.h; sourceTree = ""; }; + 1ACEA7270C91DF2800C7CE97 /* OOJSMissionVariables.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSMissionVariables.m; sourceTree = ""; }; + 1ACEA7A80C91E32800C7CE97 /* OOJSMission.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOJSMission.h; sourceTree = ""; }; + 1ACEA7A90C91E32800C7CE97 /* OOJSMission.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOJSMission.m; sourceTree = ""; }; + 1AD0C32F0C463FCB0070BD23 /* autoAImap.plist */ = {isa = PBXFileReference; explicitFileType = text.plist; fileEncoding = 4; languageSpecificationIdentifier = plist; path = autoAImap.plist; sourceTree = ""; }; + 1AD1F4C70CD9E42A00EAE520 /* NSThreadOOExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NSThreadOOExtensions.h; sourceTree = ""; }; + 1AD1F4C80CD9E42A00EAE520 /* NSThreadOOExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NSThreadOOExtensions.m; sourceTree = ""; }; + 1AD6B3280E3BB55E001C42D9 /* debug-exports-64.exp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.exports; path = "debug-exports-64.exp"; sourceTree = ""; }; + 1AD88FAF103F29D300AA36F4 /* oolite-options.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = "oolite-options.xcconfig"; path = "src/Cocoa/oolite-options.xcconfig"; sourceTree = ""; }; + 1ADA8AB10F42DBA80001BEC9 /* OODeepCopy.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OODeepCopy.h; sourceTree = ""; }; + 1ADA8AB20F42DBA80001BEC9 /* OODeepCopy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OODeepCopy.m; sourceTree = ""; }; + 1ADBA54E0BD0F173008FC99C /* OOBasicMaterial.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOBasicMaterial.h; sourceTree = ""; }; + 1ADBA54F0BD0F173008FC99C /* OOBasicMaterial.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOBasicMaterial.m; sourceTree = ""; }; + 1ADF5F110B9E374B00FDB2A3 /* JoystickHandler.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JoystickHandler.h; sourceTree = ""; }; + 1AE546300C19A2E2005B89F3 /* JoystickHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JoystickHandler.m; sourceTree = ""; }; + 1AE834C90D9598C10097CB8A /* oolite-barrel-specular.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "oolite-barrel-specular.png"; sourceTree = ""; }; + 1AED2D0A0C04586C004A1118 /* OOGraphicsResetManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOGraphicsResetManager.h; sourceTree = ""; }; + 1AED2D0B0C04586C004A1118 /* OOGraphicsResetManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOGraphicsResetManager.m; sourceTree = ""; }; + 1AF8E33A0CC169F500CA6001 /* contributors.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = contributors.txt; sourceTree = ""; }; + 2512833C09BA27C100F43D55 /* Octree.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Octree.m; sourceTree = ""; }; + 2512833D09BA27C100F43D55 /* Octree.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Octree.h; sourceTree = ""; }; + 2512834009BA27EC00F43D55 /* Geometry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Geometry.h; sourceTree = ""; }; + 2512834109BA27EC00F43D55 /* Geometry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Geometry.m; sourceTree = ""; }; + 2512834409BA281500F43D55 /* CollisionRegion.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CollisionRegion.h; sourceTree = ""; }; + 2512834509BA281500F43D55 /* CollisionRegion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = CollisionRegion.m; sourceTree = ""; }; + 25160E2E0995362F0037C2E1 /* OOCocoa.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCocoa.h; sourceTree = ""; }; + 251610CA099544090037C2E1 /* OOCABufferedSound.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCABufferedSound.h; sourceTree = ""; }; + 251610CB099544090037C2E1 /* OOCASoundMixer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCASoundMixer.h; sourceTree = ""; }; + 251610CD099544090037C2E1 /* OOCASoundChannel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCASoundChannel.h; sourceTree = ""; }; + 251610CF099544090037C2E1 /* OOCASoundReferencePoint.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCASoundReferencePoint.m; sourceTree = ""; }; + 251610D0099544090037C2E1 /* OOCASound.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCASound.h; sourceTree = ""; }; + 251610D1099544090037C2E1 /* OOCASoundMixer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCASoundMixer.m; sourceTree = ""; }; + 251610D2099544090037C2E1 /* OOCABufferedSound.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCABufferedSound.m; sourceTree = ""; }; + 251610D3099544090037C2E1 /* OOCAStreamingSound.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCAStreamingSound.m; sourceTree = ""; }; + 251610D4099544090037C2E1 /* OOCASoundDecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCASoundDecoder.h; sourceTree = ""; }; + 251610D5099544090037C2E1 /* OOCASound.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCASound.m; sourceTree = ""; }; + 251610D6099544090037C2E1 /* OOCASoundChannel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCASoundChannel.m; sourceTree = ""; }; + 251610D7099544090037C2E1 /* OOCAStreamingSound.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCAStreamingSound.h; sourceTree = ""; }; + 251610D8099544090037C2E1 /* OOErrorDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOErrorDescription.h; sourceTree = ""; }; + 251610D9099544090037C2E1 /* OOCASoundReferencePoint.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCASoundReferencePoint.h; sourceTree = ""; }; + 251610DA099544090037C2E1 /* OOCASoundInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCASoundInternal.h; sourceTree = ""; }; + 251610DB099544090037C2E1 /* OOCASoundDecoder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCASoundDecoder.m; sourceTree = ""; }; + 251610DC099544090037C2E1 /* OOErrorDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOErrorDescription.m; sourceTree = ""; }; + 251610F0099544190037C2E1 /* VirtualRingBuffer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VirtualRingBuffer.m; sourceTree = ""; }; + 251610F1099544190037C2E1 /* VirtualRingBuffer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VirtualRingBuffer.h; sourceTree = ""; }; + 251610FF099544380037C2E1 /* OpenGLSprite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OpenGLSprite.m; sourceTree = ""; }; + 25161100099544380037C2E1 /* OpenGLSprite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OpenGLSprite.h; sourceTree = ""; }; + 25161101099544380037C2E1 /* AI.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AI.m; sourceTree = ""; }; + 25161102099544380037C2E1 /* OOXMLExtensions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOXMLExtensions.m; sourceTree = ""; }; + 25161106099544390037C2E1 /* AI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AI.h; sourceTree = ""; }; + 25161107099544390037C2E1 /* OOXMLExtensions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOXMLExtensions.h; sourceTree = ""; }; + 25161108099544390037C2E1 /* OOTrumble.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOTrumble.m; sourceTree = ""; }; + 2516110B099544390037C2E1 /* GameController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GameController.h; sourceTree = ""; }; + 2516110C099544390037C2E1 /* GameController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GameController.m; sourceTree = ""; }; + 25161110099544390037C2E1 /* OOTrumble.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOTrumble.h; sourceTree = ""; }; + 25161116099544390037C2E1 /* OOSound.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOSound.h; sourceTree = ""; }; + 2516111B099544390037C2E1 /* OOCharacter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OOCharacter.m; sourceTree = ""; }; + 2516111C099544390037C2E1 /* OOCharacter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOCharacter.h; sourceTree = ""; }; + 25161126099544390037C2E1 /* GuiDisplayGen.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GuiDisplayGen.m; sourceTree = ""; }; + 25161127099544390037C2E1 /* GuiDisplayGen.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GuiDisplayGen.h; sourceTree = ""; }; + 2516112B099544390037C2E1 /* HeadUpDisplay.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = HeadUpDisplay.m; sourceTree = ""; }; + 2516112C099544390037C2E1 /* HeadUpDisplay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = HeadUpDisplay.h; sourceTree = ""; }; + 25161134099544390037C2E1 /* TextureStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TextureStore.h; sourceTree = ""; }; + 25161139099544390037C2E1 /* ResourceManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ResourceManager.m; sourceTree = ""; }; + 2516113A099544390037C2E1 /* ResourceManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ResourceManager.h; sourceTree = ""; }; + 25161143099544390037C2E1 /* Universe.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Universe.m; sourceTree = ""; }; + 25161144099544390037C2E1 /* Universe.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Universe.h; sourceTree = ""; }; + 25161145099544390037C2E1 /* TextureStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TextureStore.m; sourceTree = ""; }; + 2576E7B209B4F418007410F7 /* MyOpenGLView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MyOpenGLView.h; sourceTree = ""; }; + 25CD0C4D09B4F5A70060106D /* Growl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Growl.framework; path = "deps/Cocoa-deps/Growl.framework"; sourceTree = ""; }; + 25F3E6300994F033002F25FD /* legacy_random.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = legacy_random.h; sourceTree = ""; }; + 25F3E6320994F04C002F25FD /* legacy_random.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = legacy_random.c; sourceTree = ""; }; + 25F3E63A0994F08A002F25FD /* OOOpenGL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OOOpenGL.h; sourceTree = ""; }; + 25F3E6BC0994F30A002F25FD /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 25F3E6F00994F466002F25FD /* Groolite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Groolite.h; sourceTree = ""; }; + 25F3E6F10994F466002F25FD /* Groolite.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Groolite.m; sourceTree = ""; }; + 25F3E8A40994FE65002F25FD /* oolite-document.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "oolite-document.icns"; sourceTree = ""; }; + 25F3E8A50994FE65002F25FD /* oolite-expansion-document.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "oolite-expansion-document.icns"; sourceTree = ""; }; + 25F3E8A60994FE65002F25FD /* oolite-icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "oolite-icon.icns"; sourceTree = ""; }; + 25F3E8B30994FE9B002F25FD /* InfoPlist.strings */ = {isa = PBXFileReference; fileEncoding = 10; lastKnownFileType = text.plist.strings; path = InfoPlist.strings; sourceTree = ""; }; + 25F3E8BC09950088002F25FD /* Info-Oolite.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Info-Oolite.plist"; path = "../src/Cocoa/Info-Oolite.plist"; sourceTree = ""; }; + 25F3E8BD09950088002F25FD /* Info-OoliteDev.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Info-OoliteDev.plist"; path = "../src/Cocoa/Info-OoliteDev.plist"; sourceTree = ""; }; + 25F3E8C3099500F1002F25FD /* SoundInspector.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = SoundInspector.nib; path = ../src/Cocoa/SoundInspector.nib; sourceTree = ""; }; + 25F46750099695D5009483BF /* OoliteApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OoliteApp.h; sourceTree = ""; }; + 25F46751099695D5009483BF /* OoliteApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OoliteApp.m; sourceTree = ""; }; + 25F4676309969672009483BF /* MyOpenGLView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MyOpenGLView.m; sourceTree = ""; }; + 29B97319FDCFA39411CA2CEA /* MainMenu.nib */ = {isa = PBXFileReference; lastKnownFileType = wrapper.nib; name = MainMenu.nib; path = ../src/Cocoa/MainMenu.nib; sourceTree = ""; }; + 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; + 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0865431B06B8447D000CA0AB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0878FD30086EF845004CB752 /* Carbon.framework in Frameworks */, + 0865431C06B8447D000CA0AB /* Cocoa.framework in Frameworks */, + 0865431D06B8447D000CA0AB /* OpenGL.framework in Frameworks */, + 1A472917096B5454000E78D8 /* CoreAudio.framework in Frameworks */, + 1A472921096B5468000E78D8 /* AudioToolbox.framework in Frameworks */, + 1A472922096B5468000E78D8 /* AudioUnit.framework in Frameworks */, + 1A71E6F80BCE345000CD5C13 /* libpng.a in Frameworks */, + 1A71E8790BCE8EB100CD5C13 /* libz.dylib in Frameworks */, + 1A4FB4BA0C8E11D800DC8E1F /* libjs.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1A71E6F10BCE340C00CD5C13 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */ = { + isa = PBXGroup; + children = ( + 0878FD2F086EF845004CB752 /* Carbon.framework */, + 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */, + 06AFA01500A58FB77F000001 /* OpenGL.framework */, + 1A472916096B5454000E78D8 /* CoreAudio.framework */, + 1A47291F096B5468000E78D8 /* AudioToolbox.framework */, + 1A472920096B5468000E78D8 /* AudioUnit.framework */, + ); + name = "Linked Frameworks"; + sourceTree = ""; + }; + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */ = { + isa = PBXGroup; + children = ( + 25CD0C4D09B4F5A70060106D /* Growl.framework */, + 29B97325FDCFA39411CA2CEA /* Foundation.framework */, + 29B97324FDCFA39411CA2CEA /* AppKit.framework */, + ); + name = "Other Frameworks"; + sourceTree = ""; + }; + 19C28FACFE9D520D11CA2CBB /* Products */ = { + isa = PBXGroup; + children = ( + 0865432206B8447D000CA0AB /* OoliteDev.app */, + 1A71E6F30BCE340C00CD5C13 /* libpng.a */, + ); + name = Products; + sourceTree = ""; + }; + 1A0519350C7CCAC900BA5CCA /* Products */ = { + isa = PBXGroup; + children = ( + 1A0519390C7CCAC900BA5CCA /* Debug.oxp */, + ); + name = Products; + sourceTree = ""; + }; + 1A2314300B9C667F00EF0852 /* Resources */ = { + isa = PBXGroup; + children = ( + 25F3E8BC09950088002F25FD /* Info-Oolite.plist */, + 25F3E8BD09950088002F25FD /* Info-OoliteDev.plist */, + 25F3E8A40994FE65002F25FD /* oolite-document.icns */, + 25F3E8A50994FE65002F25FD /* oolite-expansion-document.icns */, + 25F3E8A60994FE65002F25FD /* oolite-icon.icns */, + 1A358CE10C1AB80D00E52220 /* ReadMe.rtfd */, + 1A2317900B9D022400EF0852 /* AIs */, + 1A2316DD0B9CFAD700EF0852 /* Config */, + 1A23154D0B9C778400EF0852 /* Images */, + 1A2318A50B9D02A900EF0852 /* Models */, + 1A2319730B9D02F900EF0852 /* Music */, + 1A34912C0BC25EBC00802DA7 /* Scripts */, + 1A2F63940C5CC737003872C8 /* Schemata */, + 1A6514F90CCC9D670054D01B /* Shaders */, + 1A2319910B9D031D00EF0852 /* Sounds */, + 1A23181D0B9D026D00EF0852 /* Textures */, + 29B97319FDCFA39411CA2CEA /* MainMenu.nib */, + 25F3E8B30994FE9B002F25FD /* InfoPlist.strings */, + 25F3E8C3099500F1002F25FD /* SoundInspector.nib */, + ); + path = Resources; + sourceTree = ""; + }; + 1A23154D0B9C778400EF0852 /* Images */ = { + isa = PBXGroup; + children = ( + 1A23154E0B9C778400EF0852 /* solar.png */, + 1A23154F0B9C778400EF0852 /* splash.png */, + 1A2315500B9C778400EF0852 /* splashback.png */, + 1A2315510B9C778400EF0852 /* trumblebox.png */, + ); + path = Images; + sourceTree = ""; + }; + 1A2316DD0B9CFAD700EF0852 /* Config */ = { + isa = PBXGroup; + children = ( + 1A4D67D81041DCF3001AC80F /* oolite-javascript-errors.plist */, + 1ABC04260EF872B7003B740A /* crosshairs.plist */, + 1AD0C32F0C463FCB0070BD23 /* autoAImap.plist */, + 1A020E0A0D020AFB00C3F51E /* changedScriptHandlers.plist */, + 1A2316DE0B9CFAD700EF0852 /* characters.plist */, + 1A2316DF0B9CFAD700EF0852 /* commodities.plist */, + 1A2316E00B9CFAD700EF0852 /* customsounds.plist */, + 1A2316E10B9CFAD700EF0852 /* demoships.plist */, + 1A4501E10DBF699A00815C04 /* descriptions.plist */, + 1A2316E30B9CFAD700EF0852 /* equipment.plist */, + 1A2316E40B9CFAD700EF0852 /* hud-small.plist */, + 1A2316E50B9CFAD700EF0852 /* hud.plist */, + 1A2316E60B9CFAD700EF0852 /* illegal_goods.plist */, + 1A2316E70B9CFAD700EF0852 /* keyconfig.plist */, + 1A2316E80B9CFAD700EF0852 /* logcontrol.plist */, + 1A9533890C02089E004EBB58 /* material-defaults.plist */, + 1A2316E90B9CFAD700EF0852 /* missiontext.plist */, + 1A3591810C1C382700E52220 /* nebulatextures.plist */, + 1AC5452C0D4D298E00C90E5B /* oolite-font.plist */, + 1AC973F90C9847850010C42B /* pirate-victim-roles.plist */, + 1A95338A0C02089E004EBB58 /* planetinfo.plist */, + 1A2316EB0B9CFAD700EF0852 /* shipdata.plist */, + 1A2316EC0B9CFAD700EF0852 /* shipyard.plist */, + 1A2316ED0B9CFAD700EF0852 /* speech_pronunciation_guide.plist */, + 1A3591820C1C382700E52220 /* startextures.plist */, + 1A3491290BC25EAA00802DA7 /* world-scripts.plist */, + 1AAF56590F1A27D900A2F2E6 /* whitelist.plist */, + ); + path = Config; + sourceTree = ""; + }; + 1A2317900B9D022400EF0852 /* AIs */ = { + isa = PBXGroup; + children = ( + 1A572FE40D84B6A3003D4A26 /* oolite-constrictor-AI.plist */, + 1A2317910B9D022400EF0852 /* buoyAI.plist */, + 1A2317920B9D022500EF0852 /* collectLootAI.plist */, + 1A2317930B9D022500EF0852 /* dockingAI.plist */, + 1A2317940B9D022500EF0852 /* dumbAI.plist */, + 1A2317950B9D022500EF0852 /* enteringPirateAI.plist */, + 1A2317960B9D022500EF0852 /* enteringTraderAI.plist */, + 1A2317970B9D022500EF0852 /* escortAI.plist */, + 1A2317980B9D022500EF0852 /* exitingTraderAI.plist */, + 1A2317990B9D022500EF0852 /* fallingShuttleAI.plist */, + 1A23179A0B9D022500EF0852 /* fttAI.plist */, + 1A23179B0B9D022500EF0852 /* gotoWaypointAI.plist */, + 1A23179C0B9D022500EF0852 /* hardMissileAI.plist */, + 1A23179D0B9D022500EF0852 /* homeAI.plist */, + 1A23179E0B9D022500EF0852 /* interceptAI.plist */, + 1A23179F0B9D022500EF0852 /* minerAI.plist */, + 1A2317A00B9D022500EF0852 /* missileAI.plist */, + 1A2317A10B9D022500EF0852 /* nullAI.plist */, + 1A2317A20B9D022500EF0852 /* pirateAI.plist */, + 1A2317A30B9D022500EF0852 /* planetPatrolAI.plist */, + 1A2317A40B9D022500EF0852 /* policeInterceptAI.plist */, + 1A2317A50B9D022500EF0852 /* receiveDockingAI.plist */, + 1A2317A60B9D022500EF0852 /* risingShuttleAI.plist */, + 1A2317A70B9D022500EF0852 /* rockHermitAI.plist */, + 1A2317A80B9D022500EF0852 /* route1patrolAI.plist */, + 1A2317A90B9D022500EF0852 /* route1traderAI.plist */, + 1A2317AA0B9D022500EF0852 /* route2patrolAI.plist */, + 1A2317AB0B9D022500EF0852 /* route2sunskimAI.plist */, + 1A2317AC0B9D022500EF0852 /* scavengerAI.plist */, + 1A2317AE0B9D022500EF0852 /* stationAI.plist */, + 1A2317AF0B9D022500EF0852 /* sunSkimExitAI.plist */, + 1A2317B00B9D022500EF0852 /* thargletAI.plist */, + 1A2317B10B9D022500EF0852 /* thargoidAI.plist */, + 1A2317B20B9D022500EF0852 /* timebombAI.plist */, + 1A94E4FB0F348D4300F1B5D9 /* delayedReactToAttackAI.plist */, + ); + path = AIs; + sourceTree = ""; + }; + 1A23181D0B9D026D00EF0852 /* Textures */ = { + isa = PBXGroup; + children = ( + 1AE834C90D9598C10097CB8A /* oolite-barrel-specular.png */, + 1A23181E0B9D026D00EF0852 /* adder_redux.png */, + 1A23181F0B9D026D00EF0852 /* anaconda_redux.png */, + 1A2318200B9D026D00EF0852 /* arcdetail.png */, + 1A2318220B9D026D00EF0852 /* asp_redux.png */, + 1A2318230B9D026D00EF0852 /* asteroid.png */, + 1A2318240B9D026D00EF0852 /* back_metal.png */, + 1A2318250B9D026D00EF0852 /* barrel_metal.png */, + 1A2318260B9D026D00EF0852 /* blur256.png */, + 1A2318270B9D026D00EF0852 /* boa2_left.png */, + 1A2318280B9D026D00EF0852 /* boa2_rear.png */, + 1A2318290B9D026D00EF0852 /* boa2_redux.png */, + 1A23182A0B9D026D00EF0852 /* boa2_right.png */, + 1A23182B0B9D026D00EF0852 /* boa2_top.png */, + 1A23182C0B9D026D00EF0852 /* boa_redux.png */, + 1A23182D0B9D026D00EF0852 /* bottom_metal.png */, + 1A23182E0B9D026D00EF0852 /* cobra1_redux.png */, + 1A23182F0B9D026D00EF0852 /* cobra1_redux1.png */, + 1A2318300B9D026D00EF0852 /* cobra1_redux2.png */, + 1A2318310B9D026D00EF0852 /* cobra1miner_top.png */, + 1A2318320B9D026D00EF0852 /* cobra3_redux.png */, + 1A2318330B9D026D00EF0852 /* cobra3_redux1.png */, + 1A2318340B9D026D00EF0852 /* constrictor_redux.png */, + 1A2318350B9D026D00EF0852 /* dark_metal.png */, + 1A2318360B9D026D00EF0852 /* ferdelance_redux.png */, + 1A2318370B9D026D00EF0852 /* flare256.png */, + 1A2318380B9D026D00EF0852 /* front_metal.png */, + 1A2318390B9D026D00EF0852 /* galaxy256.png */, + 1A23183A0B9D026D00EF0852 /* gecko_redux.png */, + 1A23183B0B9D026D00EF0852 /* krait_redux.png */, + 1A23183C0B9D026D00EF0852 /* left_metal.png */, + 1A23183D0B9D026D00EF0852 /* mamba_redux.png */, + 1A23183E0B9D026D00EF0852 /* mamba_redux1.png */, + 1A23183F0B9D026D00EF0852 /* metal.png */, + 1A2318400B9D026D00EF0852 /* missile.png */, + 1A2318410B9D026D00EF0852 /* moray_redux.png */, + 1A2318420B9D026D00EF0852 /* moray_redux1.png */, + 1A2318430B9D026D00EF0852 /* MorayMED_bottom.png */, + 1A2318440B9D026D00EF0852 /* MorayMED_top.png */, + 1A21149D0DEA98D100444CEB /* oolite-ball-turret.png */, + 1AC544F90D4D217900C90E5B /* oolite-font.png */, + 1A3591690C1C375400E52220 /* oolite-nebula-1.png */, + 1A35916C0C1C375400E52220 /* oolite-nebula-2.png */, + 1A35916B0C1C375400E52220 /* oolite-nebula-3.png */, + 1A0365990D7CA0EE00B5F46F /* oolite-nebula-4.png */, + 1ACE208E0D805F78009F6957 /* oolite-scarred-metal-specular.png */, + 1A35916A0C1C375400E52220 /* oolite-star-1.png */, + 1A2318490B9D026D00EF0852 /* pod2_redux.png */, + 1A23184A0B9D026D00EF0852 /* python_redux.png */, + 1A23184B0B9D026D00EF0852 /* python_redux1.png */, + 1A23184C0B9D026D00EF0852 /* python_redux2.png */, + 1A23184D0B9D026D00EF0852 /* qbomb.png */, + 1A23184E0B9D026D00EF0852 /* right_metal.png */, + 1A23184F0B9D026D00EF0852 /* scarred_metal.png */, + 1A2318500B9D026D00EF0852 /* shuttle_redux.png */, + 1A2318510B9D026D00EF0852 /* sidewinder_redux.png */, + 1A2318520B9D026D00EF0852 /* star64.png */, + 1A2318530B9D026D00EF0852 /* target_metal.png */, + 1A2318540B9D026D00EF0852 /* thargoid_redux.png */, + 1A2318550B9D026D00EF0852 /* thargon_redux.png */, + 1A2318560B9D026D00EF0852 /* top_metal.png */, + 1A2318570B9D026D00EF0852 /* transporter_redux.png */, + 1A2318580B9D026D00EF0852 /* transporter_redux1.png */, + 1A2318590B9D026D00EF0852 /* trumblekit.png */, + 1A23185A0B9D026D00EF0852 /* viper_redux.png */, + 1A23185B0B9D026D00EF0852 /* viperi_redux.png */, + 1A23185C0B9D026D00EF0852 /* worm_redux.png */, + 1A23185D0B9D026D00EF0852 /* worm_redux1.png */, + 1A23185E0B9D026D00EF0852 /* wreck.png */, + ); + path = Textures; + sourceTree = ""; + }; + 1A2318A50B9D02A900EF0852 /* Models */ = { + isa = PBXGroup; + children = ( + 1A2318A60B9D02A900EF0852 /* adder_redux.dat */, + 1A2318A70B9D02A900EF0852 /* alloy.dat */, + 1A2318A80B9D02A900EF0852 /* anaconda_redux.dat */, + 1A2318A90B9D02A900EF0852 /* arcdetail.dat */, + 1A2318AA0B9D02A900EF0852 /* asp_redux.dat */, + 1A2318AB0B9D02A900EF0852 /* asteroid.dat */, + 1A2318AC0B9D02A900EF0852 /* asteroid1.dat */, + 1A2318AD0B9D02A900EF0852 /* barrel.dat */, + 1A2318AE0B9D02A900EF0852 /* boa2_redux.dat */, + 1A2318AF0B9D02A900EF0852 /* boa_redux.dat */, + 1A2318B00B9D02A900EF0852 /* boulder.dat */, + 1A2318B10B9D02A900EF0852 /* boulder1.dat */, + 1A2318B20B9D02A900EF0852 /* buoy.dat */, + 1A2318B30B9D02A900EF0852 /* cobra1_redux.dat */, + 1A2318B40B9D02A900EF0852 /* cobra1_redux1.dat */, + 1A2318B50B9D02A900EF0852 /* cobra1_redux2.dat */, + 1A2318B60B9D02A900EF0852 /* cobra3_redux.dat */, + 1A2318B70B9D02A900EF0852 /* cobra3_redux1.dat */, + 1A2318B80B9D02A900EF0852 /* constrictor_redux.dat */, + 1A2318B90B9D02A900EF0852 /* dock-flat.dat */, + 1A2318BA0B9D02A900EF0852 /* dock.dat */, + 1A2318BB0B9D02A900EF0852 /* escpod_redux.dat */, + 1A2318BC0B9D02A900EF0852 /* exhaust.dat */, + 1A2318BD0B9D02A900EF0852 /* ferdelance_redux.dat */, + 1A2318BE0B9D02A900EF0852 /* gecko_redux.dat */, + 1A2318BF0B9D02A900EF0852 /* icosahedron.dat */, + 1A2318C00B9D02A900EF0852 /* icostextured.dat */, + 1A2318C10B9D02A900EF0852 /* krait_redux.dat */, + 1A2318C20B9D02A900EF0852 /* mamba_redux.dat */, + 1A2318C30B9D02A900EF0852 /* mamba_redux1.dat */, + 1A2318C40B9D02A900EF0852 /* missile.dat */, + 1A2318C50B9D02A900EF0852 /* moray_redux.dat */, + 1A2318C60B9D02A900EF0852 /* moray_redux1.dat */, + 1A2318C70B9D02A900EF0852 /* new-dodo.dat */, + 1A2318C80B9D02A900EF0852 /* new-icos.dat */, + 1A2318C90B9D02A900EF0852 /* new-rock.dat */, + 1A2318CA0B9D02A900EF0852 /* new_coriolis.dat */, + 1A21149A0DEA980800444CEB /* oolite-ball-turret.dat */, + 1A1502F50C1201C30032F3E8 /* oolite-unknown-ship.dat */, + 1A2318CB0B9D02A900EF0852 /* python_redux.dat */, + 1A2318CC0B9D02A900EF0852 /* python_redux1.dat */, + 1A2318CD0B9D02AA00EF0852 /* python_redux2.dat */, + 1A2318CE0B9D02AA00EF0852 /* qbomb.dat */, + 1A2318CF0B9D02AA00EF0852 /* ring.dat */, + 1A2318D00B9D02AA00EF0852 /* rock-box.dat */, + 1A2318D10B9D02AA00EF0852 /* rock-dock.dat */, + 1A2318D20B9D02AA00EF0852 /* scarred_alloy.dat */, + 1A2318D30B9D02AA00EF0852 /* shuttle_redux.dat */, + 1A2318D40B9D02AA00EF0852 /* sidewinder_redux.dat */, + 1A2318D50B9D02AA00EF0852 /* splinter.dat */, + 1A2318D60B9D02AA00EF0852 /* splinter1.dat */, + 1A2318D70B9D02AA00EF0852 /* strut.dat */, + 1A2318D80B9D02AA00EF0852 /* thargoid_redux.dat */, + 1A2318D90B9D02AA00EF0852 /* thargon_redux.dat */, + 1A2318DA0B9D02AA00EF0852 /* transporter_redux.dat */, + 1A2318DB0B9D02AA00EF0852 /* transporter_redux1.dat */, + 1A2318DC0B9D02AA00EF0852 /* viper_redux.dat */, + 1A2318DD0B9D02AA00EF0852 /* viperi_redux.dat */, + 1A2318DE0B9D02AA00EF0852 /* worm_redux.dat */, + 1A2318DF0B9D02AA00EF0852 /* worm_redux1.dat */, + 1A2318E00B9D02AA00EF0852 /* wreck1.dat */, + 1A2318E10B9D02AA00EF0852 /* wreck2.dat */, + 1A2318E20B9D02AA00EF0852 /* wreck3.dat */, + 1A2318E30B9D02AA00EF0852 /* wreck4.dat */, + 1A2318E40B9D02AA00EF0852 /* wreck5.dat */, + ); + path = Models; + sourceTree = ""; + }; + 1A2319730B9D02F900EF0852 /* Music */ = { + isa = PBXGroup; + children = ( + 1A2319740B9D02F900EF0852 /* BlueDanube.ogg */, + 1A2319750B9D02F900EF0852 /* OoliteTheme.ogg */, + ); + path = Music; + sourceTree = ""; + }; + 1A2319910B9D031D00EF0852 /* Sounds */ = { + isa = PBXGroup; + children = ( + 1A2319920B9D031D00EF0852 /* afterburner1.ogg */, + 1A2319940B9D031D00EF0852 /* beep.ogg */, + 1A2319950B9D031D00EF0852 /* bigbang.ogg */, + 1A2319960B9D031D00EF0852 /* boop.ogg */, + 1A2319970B9D031D00EF0852 /* breakpattern.ogg */, + 1A2319980B9D031D00EF0852 /* buy.ogg */, + 1A2319990B9D031D00EF0852 /* ecm.ogg */, + 1A23199A0B9D031D00EF0852 /* guiclick.ogg */, + 1A23199B0B9D031D00EF0852 /* hit.ogg */, + 1A23199C0B9D031D00EF0852 /* hullbang.ogg */, + 1A23199D0B9D031D00EF0852 /* laser.ogg */, + 1A23199E0B9D031D00EF0852 /* laserhits.ogg */, + 1A23199F0B9D031D00EF0852 /* missile.ogg */, + 1A2319A00B9D031D00EF0852 /* scoop.ogg */, + 1A2319A10B9D031D00EF0852 /* sell.ogg */, + 1A2319A20B9D031D00EF0852 /* trumble.ogg */, + 1A2319A30B9D031D00EF0852 /* trumblesqueal.ogg */, + 1A2319A40B9D031D00EF0852 /* warning.ogg */, + 1A2319A50B9D031D00EF0852 /* witchabort.ogg */, + ); + path = Sounds; + sourceTree = ""; + }; + 1A26D1090BCFA8D40073F257 /* PlayerEntity */ = { + isa = PBXGroup; + children = ( + 1A26D08E0BCF9CF70073F257 /* PlayerEntity.h */, + 1A26D08D0BCF9CF70073F257 /* PlayerEntity.m */, + 1A26D0980BCF9CF70073F257 /* PlayerEntityContracts.h */, + 1A26D09B0BCF9CF70073F257 /* PlayerEntityContracts.m */, + 1A26D0A40BCF9CF70073F257 /* PlayerEntityControls.h */, + 1A26D09C0BCF9CF70073F257 /* PlayerEntityControls.m */, + 1A26D0AA0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.h */, + 1A26D0880BCF9CF70073F257 /* PlayerEntityLegacyScriptEngine.m */, + 1A26D08C0BCF9CF70073F257 /* PlayerEntityLoadSave.m */, + 1A26D0AB0BCF9CF80073F257 /* PlayerEntityLoadSave.h */, + 1A26D0910BCF9CF70073F257 /* PlayerEntityScriptMethods.h */, + 1A26D08B0BCF9CF70073F257 /* PlayerEntityScriptMethods.m */, + 1A26D0A60BCF9CF70073F257 /* PlayerEntitySound.h */, + 1A26D0A50BCF9CF70073F257 /* PlayerEntitySound.m */, + 1A26D09F0BCF9CF70073F257 /* PlayerEntityStickMapper.h */, + 1A26D09E0BCF9CF70073F257 /* PlayerEntityStickMapper.m */, + ); + name = PlayerEntity; + sourceTree = ""; + }; + 1A28AA0F0D55434800BC0CE4 /* Sound */ = { + isa = PBXGroup; + children = ( + 1A28AA140D55438200BC0CE4 /* OOJSSound.h */, + 1A28AA150D55438200BC0CE4 /* OOJSSound.m */, + 1AB784F70D554F7B00517983 /* OOJSSoundSource.h */, + 1AB784F80D554F7B00517983 /* OOJSSoundSource.m */, + ); + name = Sound; + sourceTree = ""; + }; + 1A28AA130D55435300BC0CE4 /* Global */ = { + isa = PBXGroup; + children = ( + 1A7376BC0C64AE330097AC37 /* OOJSSystem.h */, + 1A7376BD0C64AE330097AC37 /* OOJSSystem.m */, + 1A9322950DF53C33003FD306 /* OOJSSystemInfo.h */, + 1A9322960DF53C33003FD306 /* OOJSSystemInfo.m */, + 1A4FB3330C8DC86800DC8E1F /* OOJSOolite.h */, + 1A4FB3340C8DC86800DC8E1F /* OOJSOolite.m */, + 1ACEA6BD0C91DA3E00C7CE97 /* OOJSGlobal.h */, + 1ACEA6BE0C91DA3E00C7CE97 /* OOJSGlobal.m */, + 1ACEA7260C91DF2800C7CE97 /* OOJSMissionVariables.h */, + 1ACEA7270C91DF2800C7CE97 /* OOJSMissionVariables.m */, + 1ACEA7A80C91E32800C7CE97 /* OOJSMission.h */, + 1ACEA7A90C91E32800C7CE97 /* OOJSMission.m */, + 1A6B25EC0C9C2745000717CF /* OOJSClock.h */, + 1A6B25ED0C9C2746000717CF /* OOJSClock.m */, + 1AA82C810CC10E3D0023B797 /* OOJSWorldScripts.h */, + 1AA82C820CC10E3D0023B797 /* OOJSWorldScripts.m */, + 1A0DA2EC0D71D280009B0970 /* OOJSSpecialFunctions.h */, + 1A0DA2ED0D71D280009B0970 /* OOJSSpecialFunctions.m */, + ); + name = Global; + sourceTree = ""; + }; + 1A2A1B020BD2768300152975 /* Graphics */ = { + isa = PBXGroup; + children = ( + 25F3E63A0994F08A002F25FD /* OOOpenGL.h */, + 1A5AA3220C0098AF0029C78A /* OOOpenGL.m */, + 1A2A1DEA0BD2A28E00152975 /* OOMacroOpenGL.h */, + 1A43234A0BCFC9BB00F65914 /* OOOpenGLExtensionManager.h */, + 1A43234B0BCFC9BB00F65914 /* OOOpenGLExtensionManager.m */, + 1A94D5AD0D65A6960072C805 /* OOLight.h */, + 1A94D5AC0D65A6960072C805 /* OOLight.m */, + 1A94D5B30D65A6B40072C805 /* OOCamera.h */, + 1A94D5B40D65A6B40072C805 /* OOCamera.m */, + 25161100099544380037C2E1 /* OpenGLSprite.h */, + 251610FF099544380037C2E1 /* OpenGLSprite.m */, + 1AED2D0A0C04586C004A1118 /* OOGraphicsResetManager.h */, + 1AED2D0B0C04586C004A1118 /* OOGraphicsResetManager.m */, + 1ADC3F850BFA1388000E0F89 /* Drawables */, + 1A71DDD30BCC0EEF00CD5C13 /* Materials */, + 1A15049C0C12CA070032F3E8 /* OOProbabilisticTextureManager.h */, + 1A15049D0C12CA070032F3E8 /* OOProbabilisticTextureManager.m */, + 1AC775E00C2DD4E900ECFF3B /* OODebugGLDrawing.h */, + 1AC775E10C2DD4E900ECFF3B /* OODebugGLDrawing.m */, + 1ABC03EB0EF86110003B740A /* OOCrosshairs.h */, + 1ABC03EC0EF86110003B740A /* OOCrosshairs.m */, + ); + name = Graphics; + sourceTree = ""; + }; + 1A2DA2A00CB4CB3500DE6823 /* Debug */ = { + isa = PBXGroup; + children = ( + 1A2DA3ED0CB4E84900DE6823 /* OODebugSupport.h */, + 1A2DA3EC0CB4E84900DE6823 /* OODebugSupport.m */, + 1A2DA3540CB4D10800DE6823 /* OODebuggerInterface.h */, + 1A2DA34A0CB4D0D800DE6823 /* OODebugMonitor.h */, + 1A2DA34C0CB4D0D800DE6823 /* OODebugMonitor.m */, + 1A2DA3490CB4D0D800DE6823 /* OOJSConsole.h */, + 1A2DA34B0CB4D0D800DE6823 /* OOJSConsole.m */, + 1A2DA2A40CB4CB5C00DE6823 /* OODebugTCPConsoleProtocol.h */, + 1A2DA2A70CB4CB5C00DE6823 /* OODebugTCPConsoleClient.h */, + 1A2DA2A50CB4CB5C00DE6823 /* OODebugTCPConsoleClient.m */, + 1A2DA2A90CB4CB5C00DE6823 /* OOTCPStreamDecoder.h */, + 1A2DA2A60CB4CB5C00DE6823 /* OOTCPStreamDecoder.c */, + 1A2DA2A80CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.h */, + 1A2DA2AA0CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.m */, + ); + path = Debug; + sourceTree = ""; + }; + 1A2F63940C5CC737003872C8 /* Schemata */ = { + isa = PBXGroup; + children = ( + 1A2F63950C5CC737003872C8 /* demoshipsSchema.plist */, + 1A2F63960C5CC737003872C8 /* hudSchema.plist */, + 1A2F63980C5CC737003872C8 /* shipdataEntrySchema.plist */, + 1A2F63990C5CC737003872C8 /* shipyardSchema.plist */, + ); + path = Schemata; + sourceTree = SOURCE_ROOT; + }; + 1A34912C0BC25EBC00802DA7 /* Scripts */ = { + isa = PBXGroup; + children = ( + 1A572FB80D84B640003D4A26 /* oolite-nova-mission.js */, + 1A572FB90D84B640003D4A26 /* oolite-thargoid-plans-mission.js */, + 1A572FBA0D84B640003D4A26 /* oolite-trumbles-mission.js */, + 1A572FBB0D84B640003D4A26 /* oolite-default-ship-script.js */, + 1A572FBC0D84B640003D4A26 /* oolite-cloaking-device-mission.js */, + 1A572FBD0D84B640003D4A26 /* oolite-constrictor.js */, + 1A572FBE0D84B640003D4A26 /* oolite-constrictor-hunt-mission.js */, + 1A572FBF0D84B640003D4A26 /* oolite-cloaking-device-target-ship.js */, + 1A572FC00D84B640003D4A26 /* oolite-cloaking-device-pod.js */, + 1A572FC10D84B640003D4A26 /* oolite-global-prefix.js */, + 1A572FC20D84B640003D4A26 /* oolite-thargoid-warship.js */, + ); + path = Scripts; + sourceTree = ""; + }; + 1A43A0D10CB91D3600D0E239 /* Entities */ = { + isa = PBXGroup; + children = ( + 1A2A8C130BC65FFD001E00FB /* OOJSEntity.h */, + 1A2A8C140BC65FFD001E00FB /* OOJSEntity.m */, + 1A3ACFE90C5FF33A00EC50A7 /* OOJSShip.h */, + 1A3ACFEA0C5FF33A00EC50A7 /* OOJSShip.m */, + 1A73712B0C623DAE0097AC37 /* OOJSStation.h */, + 1A73712C0C623DAE0097AC37 /* OOJSStation.m */, + 1A736BD10C61E9370097AC37 /* OOJSPlayer.h */, + 1A736BD20C61E9370097AC37 /* OOJSPlayer.m */, + 1A54AB510E3E17A0001EB817 /* OOJSPlayerShip.h */, + 1A54AB520E3E17A0001EB817 /* OOJSPlayerShip.m */, + 1A43A0CA0CB91D2C00D0E239 /* OOJSPlanet.h */, + 1A43A0CB0CB91D2C00D0E239 /* OOJSPlanet.m */, + 1A7C75990CC39D11005D0AA2 /* OOJSSun.h */, + 1A7C75980CC39D11005D0AA2 /* OOJSSun.m */, + 1A2A8D380BC6765F001E00FB /* EntityOOJavaScriptExtensions.h */, + 1A2A8D390BC6765F001E00FB /* EntityOOJavaScriptExtensions.m */, + ); + name = Entities; + sourceTree = ""; + }; + 1A4FB2340C8D6A7500DC8E1F /* Products */ = { + isa = PBXGroup; + children = ( + 1A4FB2380C8D6A7500DC8E1F /* libjs.a */, + ); + name = Products; + sourceTree = ""; + }; + 1A5BF2710916D45B00BF238F /* MDImporter */ = { + isa = PBXGroup; + children = ( + 1A5BF2720916D47300BF238F /* Oolite-importer.xcodeproj */, + ); + name = MDImporter; + sourceTree = ""; + }; + 1A5BF2730916D47300BF238F /* Products */ = { + isa = PBXGroup; + children = ( + 1A5BF2770916D47300BF238F /* Oolite.mdimporter */, + ); + name = Products; + sourceTree = ""; + }; + 1A5DBA980BC000DC00D57389 /* Scripting */ = { + isa = PBXGroup; + children = ( + 1A5DBAA10BC000DC00D57389 /* OOScript.h */, + 1A5DBAA20BC000DC00D57389 /* OOScript.m */, + 1A5DBA9E0BC000DC00D57389 /* OOPListScript.h */, + 1A5DBA9F0BC000DC00D57389 /* OOPListScript.m */, + 1A6B1EEE0C9AA5C6000717CF /* OOScriptTimer.h */, + 1A6B1EEF0C9AA5C6000717CF /* OOScriptTimer.m */, + 1A5DBAB50BC000E700D57389 /* JavaScript */, + 1A3C67F30F1C90BF0000D45B /* OOLegacyScriptWhitelist.h */, + 1A3C67F40F1C90BF0000D45B /* OOLegacyScriptWhitelist.m */, + ); + path = Scripting; + sourceTree = ""; + }; + 1A5DBAB50BC000E700D57389 /* JavaScript */ = { + isa = PBXGroup; + children = ( + 1A5DBA9A0BC000DC00D57389 /* OOJavaScriptEngine.h */, + 1A5DBA9B0BC000DC00D57389 /* OOJavaScriptEngine.m */, + 1ABC47FC0F155F0500B977AD /* OOJSFunction.h */, + 1ABC47FD0F155F0500B977AD /* OOJSFunction.m */, + 1A43A0D10CB91D3600D0E239 /* Entities */, + 1A28AA0F0D55434800BC0CE4 /* Sound */, + 1A28AA130D55435300BC0CE4 /* Global */, + 1A5DBA9C0BC000DC00D57389 /* OOJSScript.h */, + 1A5DBA9D0BC000DC00D57389 /* OOJSScript.m */, + 1A736C7D0C61FD220097AC37 /* OOJSCall.h */, + 1A736C7E0C61FD220097AC37 /* OOJSCall.m */, + 1A3AFF1D0BC4462200B5E2D9 /* OOJSVector.h */, + 1A3AFF1E0BC4462200B5E2D9 /* OOJSVector.m */, + 1A2A91500BC6BC66001E00FB /* OOJSQuaternion.h */, + 1A2A91510BC6BC66001E00FB /* OOJSQuaternion.m */, + 1A6B228B0C9B40D4000717CF /* OOJSTimer.h */, + 1A6B228C0C9B40D4000717CF /* OOJSTimer.m */, + 1AC27A0D0EA7E9940054E5F0 /* OOJSEquipmentInfo.h */, + 1AC27A0E0EA7E9940054E5F0 /* OOJSEquipmentInfo.m */, + 1A11F8490F35F60C001C886C /* OOJSShipGroup.h */, + 1A11F8480F35F60C001C886C /* OOJSShipGroup.m */, + ); + name = JavaScript; + sourceTree = ""; + }; + 1A6514F90CCC9D670054D01B /* Shaders */ = { + isa = PBXGroup; + children = ( + 1A65150D0CCC9E220054D01B /* oolite-standard-vertex.vertex */, + 1A02FD340EE048E8008F9B09 /* oolite-tangent-space-vertex.vertex */, + 1A65150E0CCC9E220054D01B /* oolite-default-shader.fragment */, + ); + path = Shaders; + sourceTree = ""; + }; + 1A71DDD30BCC0EEF00CD5C13 /* Materials */ = { + isa = PBXGroup; + children = ( + 1A26D0D30BCF9D0D0073F257 /* OOMaterial.h */, + 1A26D0D20BCF9D0D0073F257 /* OOMaterial.m */, + 1ADBA54E0BD0F173008FC99C /* OOBasicMaterial.h */, + 1ADBA54F0BD0F173008FC99C /* OOBasicMaterial.m */, + 1A26D0D00BCF9D0D0073F257 /* OOShaderMaterial.h */, + 1A26D0D10BCF9D0D0073F257 /* OOShaderMaterial.m */, + 1A2A16670BD10B1200152975 /* OOSingleTextureMaterial.h */, + 1A2A16660BD10B1200152975 /* OOSingleTextureMaterial.m */, + 1A71E8610BCE8A6E00CD5C13 /* Shaders */, + 1A71E85D0BCE8A5500CD5C13 /* Textures */, + ); + path = Materials; + sourceTree = ""; + }; + 1A71E6FB0BCE345800CD5C13 /* libpng */ = { + isa = PBXGroup; + children = ( + 1A26D0F40BCF9D8D0073F257 /* pngusr.h */, + 1A71E7070BCE34CF00CD5C13 /* png.h */, + 1A71E7030BCE34CF00CD5C13 /* pngconf.h */, + 1A71E7040BCE34CF00CD5C13 /* pngmem.c */, + 1A71E7050BCE34CF00CD5C13 /* pngwutil.c */, + 1A71E7060BCE34CF00CD5C13 /* pngwio.c */, + 1A71E7080BCE34CF00CD5C13 /* pngtrans.c */, + 1A71E7090BCE34CF00CD5C13 /* pngrtran.c */, + 1A71E70A0BCE34CF00CD5C13 /* png.c */, + 1A71E70B0BCE34CF00CD5C13 /* pngwtran.c */, + 1A71E70C0BCE34CF00CD5C13 /* pngread.c */, + 1A71E70D0BCE34CF00CD5C13 /* pngrutil.c */, + 1A71E70E0BCE34CF00CD5C13 /* pngtest.c */, + 1A71E70F0BCE34CF00CD5C13 /* pngerror.c */, + 1A71E7100BCE34CF00CD5C13 /* pngset.c */, + 1A71E7110BCE34CF00CD5C13 /* pngwrite.c */, + 1A71E7120BCE34CF00CD5C13 /* pngget.c */, + 1A71E7130BCE34CF00CD5C13 /* pnggccrd.c */, + 1A71E7140BCE34CF00CD5C13 /* pngrio.c */, + 1A71E7150BCE34CF00CD5C13 /* pngvcrd.c */, + 1A71E7160BCE34CF00CD5C13 /* pngpread.c */, + ); + name = libpng; + path = "deps/Cross-platform-deps/libpng"; + sourceTree = ""; + }; + 1A71E85D0BCE8A5500CD5C13 /* Textures */ = { + isa = PBXGroup; + children = ( + 1A26D0E10BCF9D3B0073F257 /* OOTexture.h */, + 1A26D0E00BCF9D3B0073F257 /* OOTexture.m */, + 1A8BB8E80E8311F900122974 /* OONullTexture.h */, + 1A8BB8E90E8311F900122974 /* OONullTexture.m */, + 1A26D0E30BCF9D3B0073F257 /* OOTextureLoader.h */, + 1A26D0E50BCF9D3B0073F257 /* OOTextureLoader.m */, + 1A26D0E40BCF9D3B0073F257 /* OOPNGTextureLoader.h */, + 1A26D0E20BCF9D3B0073F257 /* OOPNGTextureLoader.m */, + 1AB01ABC0BB15AED00F1B949 /* OOTextureScaling.h */, + 1AB01ABD0BB15AED00F1B949 /* OOTextureScaling.m */, + ); + name = Textures; + sourceTree = ""; + }; + 1A71E8610BCE8A6E00CD5C13 /* Shaders */ = { + isa = PBXGroup; + children = ( + 1A26D0DA0BCF9D1E0073F257 /* OOShaderProgram.h */, + 1A26D0D90BCF9D1E0073F257 /* OOShaderProgram.m */, + 1A26D0D80BCF9D1E0073F257 /* OOShaderUniform.h */, + 1A26D0DB0BCF9D1E0073F257 /* OOShaderUniform.m */, + 1A5E462E0C32DACE008104B4 /* OOShaderUniformMethodType.h */, + 1A5E462D0C32DACE008104B4 /* OOShaderUniformMethodType.m */, + ); + name = Shaders; + sourceTree = ""; + }; + 1A7B96800E620CA000322821 /* Audio */ = { + isa = PBXGroup; + children = ( + 1A60AFB50D56093B0070510D /* OOMusicController.h */, + 1A60AFB60D56093B0070510D /* OOMusicController.m */, + 1A71D8A90E5F17410088C456 /* OOSoundSource.h */, + 1A71D8A80E5F17410088C456 /* OOSoundSource.m */, + 1ACBF06F0D82DF9B00CC005F /* OOSoundSourcePool.h */, + 1ACBF0700D82DF9B00CC005F /* OOSoundSourcePool.m */, + 1A7B967E0E620C9E00322821 /* OOSoundInternal.h */, + 1A8A3D8F0B9B058500AB7625 /* Mac-specific */, + ); + name = Audio; + sourceTree = ""; + }; + 1A8071D70C0480AC00BA1935 /* Debug OXP */ = { + isa = PBXGroup; + children = ( + 1A0519340C7CCAC900BA5CCA /* DebugOXP.xcodeproj */, + 1A0517D10C7B376700BA5CCA /* debug-exports.exp */, + 1AD6B3280E3BB55E001C42D9 /* debug-exports-64.exp */, + ); + name = "Debug OXP"; + path = src/Cocoa; + sourceTree = ""; + }; + 1A8A3BE90B963F02007D20B8 /* Source */ = { + isa = PBXGroup; + children = ( + 1A8A3C0C0B963FF8007D20B8 /* Entities */, + 1A8A3C0F0B964002007D20B8 /* AI */, + 1A8A3C1E0B9640B4007D20B8 /* Mathematics */, + 1A8A3C180B964030007D20B8 /* Resource Management */, + 1A8A3C1B0B96407A007D20B8 /* User Interface */, + 1A8A3BFB0B963F91007D20B8 /* Utilities */, + 1A8A3BF20B963F3C007D20B8 /* Mac-specific */, + 1ACEA3BC0C915A1600C7CE97 /* Misc */, + 1A7B96800E620CA000322821 /* Audio */, + 1A2A1B020BD2768300152975 /* Graphics */, + 1A5DBA980BC000DC00D57389 /* Scripting */, + 1A2DA2A00CB4CB3500DE6823 /* Debug */, + 1AB2AAE70C4CDF890008CF4E /* OXP Verification */, + ); + name = Source; + path = src/Core; + sourceTree = ""; + }; + 1A8A3BF20B963F3C007D20B8 /* Mac-specific */ = { + isa = PBXGroup; + children = ( + 25F3E6BC0994F30A002F25FD /* main.m */, + 25F46750099695D5009483BF /* OoliteApp.h */, + 25F46751099695D5009483BF /* OoliteApp.m */, + 2576E7B209B4F418007410F7 /* MyOpenGLView.h */, + 25F4676309969672009483BF /* MyOpenGLView.m */, + 1A8A3D9E0B9B05C400AB7625 /* Groolite */, + 1ADF5F110B9E374B00FDB2A3 /* JoystickHandler.h */, + 1AE546300C19A2E2005B89F3 /* JoystickHandler.m */, + 1A1E99B40EF04837008B48E2 /* OOProgressBar.h */, + 1A1E99B50EF04837008B48E2 /* OOProgressBar.m */, + 1AAF56160F1A198400A2F2E6 /* Comparison.h */, + ); + name = "Mac-specific"; + path = ../Cocoa; + sourceTree = ""; + }; + 1A8A3BFB0B963F91007D20B8 /* Utilities */ = { + isa = PBXGroup; + children = ( + 1ABAD7300F350B3400FD2CBF /* OOShipGroup.h */, + 1ABAD72F0F350B3400FD2CBF /* OOShipGroup.m */, + 1A20F7040F36EE0500156DE9 /* OOExcludeObjectEnumerator.h */, + 1A20F7050F36EE0500156DE9 /* OOExcludeObjectEnumerator.m */, + 1ABDBA390EB365D90086BC3D /* OOIsNumberLiteral.m */, + 1ABDBA3A0EB365D90086BC3D /* OOIsNumberLiteral.h */, + 1AB4AEB60D688AD9003076D6 /* OOLogHeader.h */, + 1AB4AEB70D688AD9003076D6 /* OOLogHeader.m */, + 25161107099544390037C2E1 /* OOXMLExtensions.h */, + 25161102099544380037C2E1 /* OOXMLExtensions.m */, + 1A047A430DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.h */, + 1A047A440DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.m */, + 1A8A37550B960337007D20B8 /* NSMutableDictionaryOOExtensions.h */, + 1A8A37540B960337007D20B8 /* NSMutableDictionaryOOExtensions.m */, + 1A8A394D0B96229C007D20B8 /* NSFileManagerOOExtensions.h */, + 1A8A394C0B96229C007D20B8 /* NSFileManagerOOExtensions.m */, + 1A8A3A360B962AEF007D20B8 /* NSScannerOOExtensions.h */, + 1A8A3A370B962AEF007D20B8 /* NSScannerOOExtensions.m */, + 083325DB09DDBCDE00F5B8E4 /* OOColor.h */, + 083325DC09DDBCDE00F5B8E4 /* OOColor.m */, + 25160E2E0995362F0037C2E1 /* OOCocoa.h */, + 1A38B4AA0B988532001ED4A0 /* OOLogging.h */, + 1ABB688B0D044306008BE96D /* OOLoggingExtended.h */, + 1A38B4AB0B988532001ED4A0 /* OOLogging.m */, + 1AB813070E90D8E500A84923 /* OOLogOutputHandler.m */, + 1AB813080E90D8E500A84923 /* OOLogOutputHandler.h */, + 1A9400BF0BAF0EDB005F6CF3 /* OOStringParsing.h */, + 1A9400BD0BAF0ECD005F6CF3 /* OOStringParsing.m */, + 1A9403CF0BAF36C3005F6CF3 /* OOFunctionAttributes.h */, + 1A9404240BAF3DED005F6CF3 /* OOCollectionExtractors.m */, + 1A9404250BAF3DED005F6CF3 /* OOCollectionExtractors.h */, + 1A5DB1E80BBD8F0000D57389 /* OOConstToString.h */, + 1A5DB1E90BBD8F0000D57389 /* OOConstToString.m */, + 1A5DBD560BC17F0900D57389 /* NSStringOOExtensions.h */, + 1A5DBD570BC17F0900D57389 /* NSStringOOExtensions.m */, + 1A3491AA0BC282DE00802DA7 /* ReleaseLockProxy.h */, + 1A3491AB0BC282DE00802DA7 /* ReleaseLockProxy.m */, + 1A2A8E010BC67CCC001E00FB /* OOWeakReference.h */, + 1A2A8E020BC67CCC001E00FB /* OOWeakReference.m */, + 1A2A17D40BD1587D00152975 /* OOCPUInfo.h */, + 1A2A17D50BD1587D00152975 /* OOCPUInfo.m */, + 1A7D83380C40147700E4A5F5 /* OOAsyncQueue.h */, + 1A7D83390C40147700E4A5F5 /* OOAsyncQueue.m */, + 1A6B1F340C9AAA60000717CF /* OOPriorityQueue.m */, + 1A6B1F350C9AAA60000717CF /* OOPriorityQueue.h */, + 1A43A1840CB9243B00D0E239 /* OOEntityFilterPredicate.h */, + 1A43A1850CB9243B00D0E239 /* OOEntityFilterPredicate.m */, + 1AD1F4C70CD9E42A00EAE520 /* NSThreadOOExtensions.h */, + 1AD1F4C80CD9E42A00EAE520 /* NSThreadOOExtensions.m */, + 1A1D212D0D2BD4C100F4DEC2 /* bsd_string.h */, + 1AAB9A960D779F3C00A9F424 /* OOCocoa.m */, + 1A1616600D7DCFDC0094AE5B /* OOFilteringEnumerator.h */, + 1A1616610D7DCFDC0094AE5B /* OOFilteringEnumerator.m */, + 1A047B7C0DCB3D7500EE1CD0 /* OOProbabilitySet.h */, + 1A047B7D0DCB3D7500EE1CD0 /* OOProbabilitySet.m */, + 1ADA8AB10F42DBA80001BEC9 /* OODeepCopy.h */, + 1ADA8AB20F42DBA80001BEC9 /* OODeepCopy.m */, + ); + name = Utilities; + sourceTree = ""; + }; + 1A8A3C0C0B963FF8007D20B8 /* Entities */ = { + isa = PBXGroup; + children = ( + 1A26D09D0BCF9CF70073F257 /* Entity.h */, + 1A26D0A80BCF9CF70073F257 /* Entity.m */, + 1A2A1B050BD276A900152975 /* OOEntityWithDrawable.h */, + 1A2A1B060BD276A900152975 /* OOEntityWithDrawable.m */, + 1A2A1B280BD277D800152975 /* OOSelfDrawingEntity.h */, + 1A2A1B290BD277D800152975 /* OOSelfDrawingEntity.m */, + 1A26D0900BCF9CF70073F257 /* ShipEntity.h */, + 1A26D08F0BCF9CF70073F257 /* ShipEntity.m */, + 1A26D08A0BCF9CF70073F257 /* ShipEntityAI.h */, + 1A26D0890BCF9CF70073F257 /* ShipEntityAI.m */, + 1A7BA80B0D84231A003C6CA3 /* ShipEntityScriptMethods.h */, + 1A7BA80C0D84231A003C6CA3 /* ShipEntityScriptMethods.m */, + 1A26D0A30BCF9CF70073F257 /* StationEntity.h */, + 1A26D0A20BCF9CF70073F257 /* StationEntity.m */, + 1A26D1090BCFA8D40073F257 /* PlayerEntity */, + 1A26D0930BCF9CF70073F257 /* SkyEntity.h */, + 1A26D0920BCF9CF70073F257 /* SkyEntity.m */, + 1A26D0950BCF9CF70073F257 /* RingEntity.h */, + 1A26D0940BCF9CF70073F257 /* RingEntity.m */, + 1A26D0970BCF9CF70073F257 /* ParticleEntity.h */, + 1A26D0960BCF9CF70073F257 /* ParticleEntity.m */, + 1A26D09A0BCF9CF70073F257 /* PlanetEntity.h */, + 1A26D0990BCF9CF70073F257 /* PlanetEntity.m */, + 1A26D0A10BCF9CF70073F257 /* WormholeEntity.h */, + 1A26D0A00BCF9CF70073F257 /* WormholeEntity.m */, + 1A26D0A70BCF9CF70073F257 /* DustEntity.h */, + 1A26D0A90BCF9CF80073F257 /* DustEntity.m */, + ); + path = Entities; + sourceTree = ""; + }; + 1A8A3C0F0B964002007D20B8 /* AI */ = { + isa = PBXGroup; + children = ( + 25161106099544390037C2E1 /* AI.h */, + 25161101099544380037C2E1 /* AI.m */, + 2516111C099544390037C2E1 /* OOCharacter.h */, + 2516111B099544390037C2E1 /* OOCharacter.m */, + 083DB4D30A70E51E00B419B2 /* OOBrain.h */, + 083DB4D40A70E51E00B419B2 /* OOBrain.m */, + 081329B30A6D534900A5C2CE /* OOInstinct.h */, + 081329B40A6D534900A5C2CE /* OOInstinct.m */, + ); + name = AI; + sourceTree = ""; + }; + 1A8A3C180B964030007D20B8 /* Resource Management */ = { + isa = PBXGroup; + children = ( + 2516113A099544390037C2E1 /* ResourceManager.h */, + 25161139099544390037C2E1 /* ResourceManager.m */, + 25161134099544390037C2E1 /* TextureStore.h */, + 25161145099544390037C2E1 /* TextureStore.m */, + 1A231A160B9D8B1B00EF0852 /* OOCacheManager.h */, + 1A231A170B9D8B1B00EF0852 /* OOCacheManager.m */, + 1A29967C0B9F064C002D2149 /* OOCache.h */, + 1A29967D0B9F064C002D2149 /* OOCache.m */, + 1A9404640BAF42BE005F6CF3 /* OOPListParsing.h */, + 1A9404650BAF42BF005F6CF3 /* OOPListParsing.m */, + 1A0729FC0EF5796500B0F925 /* OldSchoolPropertyListWriting.h */, + 1A0729FD0EF5796500B0F925 /* OldSchoolPropertyListWriting.m */, + 1A0729D70EF56D1200B0F925 /* OOConvertSystemDescriptions.h */, + 1A0729D80EF56D1200B0F925 /* OOConvertSystemDescriptions.m */, + ); + name = "Resource Management"; + sourceTree = ""; + }; + 1A8A3C1B0B96407A007D20B8 /* User Interface */ = { + isa = PBXGroup; + children = ( + 2516112C099544390037C2E1 /* HeadUpDisplay.h */, + 2516112B099544390037C2E1 /* HeadUpDisplay.m */, + 25161127099544390037C2E1 /* GuiDisplayGen.h */, + 25161126099544390037C2E1 /* GuiDisplayGen.m */, + 1AC545040D4D228400C90E5B /* OOEncodingConverter.h */, + 1AC545050D4D228400C90E5B /* OOEncodingConverter.m */, + ); + name = "User Interface"; + sourceTree = ""; + }; + 1A8A3C1E0B9640B4007D20B8 /* Mathematics */ = { + isa = PBXGroup; + children = ( + 2512834009BA27EC00F43D55 /* Geometry.h */, + 2512834109BA27EC00F43D55 /* Geometry.m */, + 2512833D09BA27C100F43D55 /* Octree.h */, + 2512833C09BA27C100F43D55 /* Octree.m */, + 2512834409BA281500F43D55 /* CollisionRegion.h */, + 2512834509BA281500F43D55 /* CollisionRegion.m */, + 1A9404920BAF4582005F6CF3 /* OOMaths.h */, + 1A9404A10BAF462D005F6CF3 /* OOVector.h */, + 1A9404A20BAF462D005F6CF3 /* OOVector.m */, + 1A9405360BAF4FA6005F6CF3 /* OOMatrix.h */, + 1A9405370BAF4FA6005F6CF3 /* OOMatrix.m */, + 1A94057D0BAF52AD005F6CF3 /* OOQuaternion.h */, + 1A94057E0BAF52AD005F6CF3 /* OOQuaternion.m */, + 1A9406290BAF6170005F6CF3 /* OOBoundingBox.h */, + 1A9406820BAF66D6005F6CF3 /* OOVoxel.h */, + 1A9406830BAF66D6005F6CF3 /* OOVoxel.m */, + 1A9406B20BAF67BF005F6CF3 /* OOTriangle.h */, + 1A9406B30BAF67BF005F6CF3 /* OOTriangle.m */, + 1AB01BB90BB16A8A00F1B949 /* OOFastArithmetic.h */, + 1AB01BBA0BB16A8A00F1B949 /* OOFastArithmetic.m */, + 25F3E6300994F033002F25FD /* legacy_random.h */, + 25F3E6320994F04C002F25FD /* legacy_random.c */, + ); + name = Mathematics; + sourceTree = ""; + }; + 1A8A3D8F0B9B058500AB7625 /* Mac-specific */ = { + isa = PBXGroup; + children = ( + 251610D0099544090037C2E1 /* OOCASound.h */, + 251610DA099544090037C2E1 /* OOCASoundInternal.h */, + 251610D5099544090037C2E1 /* OOCASound.m */, + 1A81F7080A7BAC4D006580AD /* OOCAMusic.h */, + 1A81F7070A7BAC4D006580AD /* OOCAMusic.m */, + 251610CA099544090037C2E1 /* OOCABufferedSound.h */, + 251610D2099544090037C2E1 /* OOCABufferedSound.m */, + 251610D7099544090037C2E1 /* OOCAStreamingSound.h */, + 251610D3099544090037C2E1 /* OOCAStreamingSound.m */, + 251610CB099544090037C2E1 /* OOCASoundMixer.h */, + 251610D1099544090037C2E1 /* OOCASoundMixer.m */, + 251610CD099544090037C2E1 /* OOCASoundChannel.h */, + 251610D6099544090037C2E1 /* OOCASoundChannel.m */, + 251610D9099544090037C2E1 /* OOCASoundReferencePoint.h */, + 251610CF099544090037C2E1 /* OOCASoundReferencePoint.m */, + 251610D4099544090037C2E1 /* OOCASoundDecoder.h */, + 251610DB099544090037C2E1 /* OOCASoundDecoder.m */, + 251610D8099544090037C2E1 /* OOErrorDescription.h */, + 251610DC099544090037C2E1 /* OOErrorDescription.m */, + 251610F1099544190037C2E1 /* VirtualRingBuffer.h */, + 251610F0099544190037C2E1 /* VirtualRingBuffer.m */, + ); + name = "Mac-specific"; + path = ../Cocoa; + sourceTree = ""; + }; + 1A8A3D9E0B9B05C400AB7625 /* Groolite */ = { + isa = PBXGroup; + children = ( + 25F3E6F00994F466002F25FD /* Groolite.h */, + 25F3E6F10994F466002F25FD /* Groolite.m */, + ); + name = Groolite; + sourceTree = ""; + }; + 1AB2AAE70C4CDF890008CF4E /* OXP Verification */ = { + isa = PBXGroup; + children = ( + 1A7D3EFD0C50F337008EDC33 /* OXP verifier design.txt */, + 1A6DD19E0C5924A300A892F4 /* plist verifier design.txt */, + 1AB2AB120C4CE4070008CF4E /* verifyOXP.plist */, + 1AB2AAF80C4CE0CC0008CF4E /* OOOXPVerifier.h */, + 1AB2AAF90C4CE0CC0008CF4E /* OOOXPVerifier.m */, + 1A27DB390C4E349F00CB4CE8 /* OOOXPVerifierStage.h */, + 1A27DB380C4E349F00CB4CE8 /* OOOXPVerifierStageInternal.h */, + 1A27DB3A0C4E349F00CB4CE8 /* OOOXPVerifierStage.m */, + 1A27DB400C4E34B300CB4CE8 /* OOFileScannerVerifierStage.h */, + 1A27DB410C4E34B300CB4CE8 /* OOFileScannerVerifierStage.m */, + 1A7D3A160C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.h */, + 1A7D3A170C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.m */, + 1A7D3B990C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.h */, + 1A7D3B9A0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.m */, + 1A7D3C470C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.h */, + 1A7D3C480C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.m */, + 1A3159250F1B7B7E00328E4A /* OOAIStateMachineVerifierStage.h */, + 1A3159260F1B7B7E00328E4A /* OOAIStateMachineVerifierStage.m */, + 1A7D3D290C4F8D01008EDC33 /* OOTextureVerifierStage.h */, + 1A7D3D2A0C4F8D02008EDC33 /* OOTextureVerifierStage.m */, + 1A7D41850C516B90008EDC33 /* OOModelVerifierStage.h */, + 1A7D41840C516B90008EDC33 /* OOModelVerifierStage.m */, + 1A7D41E00C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.h */, + 1A7D41E10C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.m */, + 1A6DD1210C57B5BC00A892F4 /* OOPListSchemaVerifier.h */, + 1A6DD1220C57B5BC00A892F4 /* OOPListSchemaVerifier.m */, + ); + name = "OXP Verification"; + path = OXPVerifier; + sourceTree = ""; + }; + 1AC0E9380B974D8700C46994 /* Documentation */ = { + isa = PBXGroup; + children = ( + 1AC0E9480B974DC200C46994 /* ReadMe.rtfd */, + 1AC0E9490B974DC200C46994 /* LICENSE.TXT */, + 1AC0E9460B974DC200C46994 /* GPL.TXT */, + 1AC0E9470B974DC200C46994 /* FAQ.TXT */, + 1AC0E94A0B974DC200C46994 /* PORTING.TXT */, + 1AC0E94B0B974DC200C46994 /* README_LINUX.TXT */, + ); + name = Documentation; + path = Doc; + sourceTree = ""; + }; + 1ACEA3BC0C915A1600C7CE97 /* Misc */ = { + isa = PBXGroup; + children = ( + 1A3A04610BC547DC00B5E2D9 /* OOTypes.h */, + 25161144099544390037C2E1 /* Universe.h */, + 25161143099544390037C2E1 /* Universe.m */, + 25161116099544390037C2E1 /* OOSound.h */, + 25161110099544390037C2E1 /* OOTrumble.h */, + 25161108099544390037C2E1 /* OOTrumble.m */, + 2516110B099544390037C2E1 /* GameController.h */, + 2516110C099544390037C2E1 /* GameController.m */, + 1A5218D80D72EC21000865E9 /* OOSpatialReference.h */, + 1A5218DD0D72ECE5000865E9 /* OOSpatialReference.m */, + 1ACEA3470C91507000C7CE97 /* OORoleSet.h */, + 1ACEA3480C91507000C7CE97 /* OORoleSet.m */, + 1A0479E70DC9F81000EE1CD0 /* OOShipRegistry.h */, + 1A0479E80DC9F81000EE1CD0 /* OOShipRegistry.m */, + 1A62F0FC0E26A2A000897506 /* OOEquipmentType.h */, + 1A62F0FB0E26A2A000897506 /* OOEquipmentType.m */, + ); + name = Misc; + sourceTree = ""; + }; + 1ADC3F850BFA1388000E0F89 /* Drawables */ = { + isa = PBXGroup; + children = ( + 1A2A1B120BD2774300152975 /* OODrawable.h */, + 1A2A1B130BD2774300152975 /* OODrawable.m */, + 1A2A1CA80BD2914F00152975 /* OOMesh.h */, + 1A2A1CA90BD2914F00152975 /* OOMesh.m */, + 1A1504490C12C50D0032F3E8 /* OOSkyDrawable.h */, + 1A15044A0C12C50D0032F3E8 /* OOSkyDrawable.m */, + ); + name = Drawables; + sourceTree = ""; + }; + 1AEAAC6C0BA457C3000705D0 /* SpiderMonkey */ = { + isa = PBXGroup; + children = ( + 1A4FB23B0C8D6AA900DC8E1F /* jsapi.h */, + 1A4FB2390C8D6A9A00DC8E1F /* jsautocfg.h */, + 1A4FB23E0C8D6AB400DC8E1F /* jspubtd.h */, + 1A4FB2400C8D6ABA00DC8E1F /* jstypes.h */, + 1A6B50370C8B42480035DFCC /* libjs.xcodeproj */, + ); + name = SpiderMonkey; + path = "deps/Cross-platform-deps/SpiderMonkey/js"; + sourceTree = ""; + }; + 29B97314FDCFA39411CA2CEA /* Oolite_GUSTO */ = { + isa = PBXGroup; + children = ( + 1A9407BF0BAF7032005F6CF3 /* GNUmakefile */, + 1AF8E33A0CC169F500CA6001 /* contributors.txt */, + 1A37D1ED1049B2E400BC8976 /* CHANGELOG.TXT */, + 1A846BA90D79F9570081280D /* oolite-version.xcconfig */, + 1AD88FAF103F29D300AA36F4 /* oolite-options.xcconfig */, + 1A8A3BE90B963F02007D20B8 /* Source */, + 1AC0E9380B974D8700C46994 /* Documentation */, + 1A5BF2710916D45B00BF238F /* MDImporter */, + 1A8071D70C0480AC00BA1935 /* Debug OXP */, + 1A2314300B9C667F00EF0852 /* Resources */, + 29B97323FDCFA39411CA2CEA /* Frameworks */, + 19C28FACFE9D520D11CA2CBB /* Products */, + ); + name = Oolite_GUSTO; + sourceTree = ""; + }; + 29B97323FDCFA39411CA2CEA /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1AEAAC6C0BA457C3000705D0 /* SpiderMonkey */, + 1A71E6FB0BCE345800CD5C13 /* libpng */, + 1A71E8780BCE8EB100CD5C13 /* libz.dylib */, + 1058C7A0FEA54F0111CA2CBB /* Linked Frameworks */, + 1058C7A2FEA54F0111CA2CBB /* Other Frameworks */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 0865423606B8447D000CA0AB /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 25F3E6310994F033002F25FD /* legacy_random.h in Headers */, + 25F3E63B0994F08A002F25FD /* OOOpenGL.h in Headers */, + 25F3E6F20994F466002F25FD /* Groolite.h in Headers */, + 25160E2F0995362F0037C2E1 /* OOCocoa.h in Headers */, + 251610DD099544090037C2E1 /* OOCABufferedSound.h in Headers */, + 251610DE099544090037C2E1 /* OOCASoundMixer.h in Headers */, + 251610E0099544090037C2E1 /* OOCASoundChannel.h in Headers */, + 251610E3099544090037C2E1 /* OOCASound.h in Headers */, + 251610E7099544090037C2E1 /* OOCASoundDecoder.h in Headers */, + 251610EA099544090037C2E1 /* OOCAStreamingSound.h in Headers */, + 251610EB099544090037C2E1 /* OOErrorDescription.h in Headers */, + 251610EC099544090037C2E1 /* OOCASoundReferencePoint.h in Headers */, + 251610ED099544090037C2E1 /* OOCASoundInternal.h in Headers */, + 251610F3099544190037C2E1 /* VirtualRingBuffer.h in Headers */, + 25161152099544390037C2E1 /* OpenGLSprite.h in Headers */, + 25161158099544390037C2E1 /* AI.h in Headers */, + 2516115D099544390037C2E1 /* GameController.h in Headers */, + 25161162099544390037C2E1 /* OOTrumble.h in Headers */, + 25161168099544390037C2E1 /* OOSound.h in Headers */, + 2516116E099544390037C2E1 /* OOCharacter.h in Headers */, + 25161179099544390037C2E1 /* GuiDisplayGen.h in Headers */, + 2516117E099544390037C2E1 /* HeadUpDisplay.h in Headers */, + 2516118C099544390037C2E1 /* ResourceManager.h in Headers */, + 25161196099544390037C2E1 /* Universe.h in Headers */, + 25F46752099695D5009483BF /* OoliteApp.h in Headers */, + 2576E7B309B4F418007410F7 /* MyOpenGLView.h in Headers */, + 2512833F09BA27C100F43D55 /* Octree.h in Headers */, + 2512834209BA27EC00F43D55 /* Geometry.h in Headers */, + 2512834609BA281500F43D55 /* CollisionRegion.h in Headers */, + 083325DD09DDBCDE00F5B8E4 /* OOColor.h in Headers */, + 1A81F70A0A7BAC4D006580AD /* OOCAMusic.h in Headers */, + 1A8A37570B960337007D20B8 /* NSMutableDictionaryOOExtensions.h in Headers */, + 1A8A394F0B96229C007D20B8 /* NSFileManagerOOExtensions.h in Headers */, + 1A8A3A380B962AEF007D20B8 /* NSScannerOOExtensions.h in Headers */, + 1A38B4AC0B988532001ED4A0 /* OOLogging.h in Headers */, + 1A231A180B9D8B1B00EF0852 /* OOCacheManager.h in Headers */, + 1ADF5F130B9E374B00FDB2A3 /* JoystickHandler.h in Headers */, + 1A29967E0B9F064C002D2149 /* OOCache.h in Headers */, + 1A9400C00BAF0EDB005F6CF3 /* OOStringParsing.h in Headers */, + 1A9403D00BAF36C3005F6CF3 /* OOFunctionAttributes.h in Headers */, + 1A9404270BAF3DED005F6CF3 /* OOCollectionExtractors.h in Headers */, + 1A9404660BAF42BF005F6CF3 /* OOPListParsing.h in Headers */, + 1A9404A30BAF462D005F6CF3 /* OOVector.h in Headers */, + 1A9405380BAF4FA6005F6CF3 /* OOMatrix.h in Headers */, + 1A94057F0BAF52AD005F6CF3 /* OOQuaternion.h in Headers */, + 1A94062B0BAF6170005F6CF3 /* OOBoundingBox.h in Headers */, + 1A9406840BAF66D6005F6CF3 /* OOVoxel.h in Headers */, + 1A9406B40BAF67BF005F6CF3 /* OOTriangle.h in Headers */, + 1AB01ABE0BB15AED00F1B949 /* OOTextureScaling.h in Headers */, + 1AB01BBB0BB16A8A00F1B949 /* OOFastArithmetic.h in Headers */, + 1A451D8D0BB1BD2A004CD72F /* OOMaths.h in Headers */, + 1A5DB1EA0BBD8F0000D57389 /* OOConstToString.h in Headers */, + 1A5DBAA60BC000DC00D57389 /* OOJavaScriptEngine.h in Headers */, + 1A5DBAA80BC000DC00D57389 /* OOJSScript.h in Headers */, + 1A5DBAAA0BC000DC00D57389 /* OOPListScript.h in Headers */, + 1A5DBAAD0BC000DC00D57389 /* OOScript.h in Headers */, + 1A5DBD580BC17F0900D57389 /* NSStringOOExtensions.h in Headers */, + 1A3AFF1F0BC4462200B5E2D9 /* OOJSVector.h in Headers */, + 1A3A04620BC547DC00B5E2D9 /* OOTypes.h in Headers */, + 1A2A8C150BC65FFD001E00FB /* OOJSEntity.h in Headers */, + 1A2A8D3A0BC6765F001E00FB /* EntityOOJavaScriptExtensions.h in Headers */, + 1A2A8E030BC67CCC001E00FB /* OOWeakReference.h in Headers */, + 1A2A91520BC6BC66001E00FB /* OOJSQuaternion.h in Headers */, + 1A71EA8D0BCF8C6C00CD5C13 /* OOXMLExtensions.h in Headers */, + 1A26D0AE0BCF9CF80073F257 /* ShipEntityAI.h in Headers */, + 1A26D0B20BCF9CF80073F257 /* PlayerEntity.h in Headers */, + 1A26D0B40BCF9CF80073F257 /* ShipEntity.h in Headers */, + 1A26D0B50BCF9CF80073F257 /* PlayerEntityScriptMethods.h in Headers */, + 1A26D0B70BCF9CF80073F257 /* SkyEntity.h in Headers */, + 1A26D0B90BCF9CF80073F257 /* RingEntity.h in Headers */, + 1A26D0BB0BCF9CF80073F257 /* ParticleEntity.h in Headers */, + 1A26D0BC0BCF9CF80073F257 /* PlayerEntityContracts.h in Headers */, + 1A26D0BE0BCF9CF80073F257 /* PlanetEntity.h in Headers */, + 1A26D0C10BCF9CF80073F257 /* Entity.h in Headers */, + 1A26D0C50BCF9CF80073F257 /* WormholeEntity.h in Headers */, + 1A26D0C70BCF9CF80073F257 /* StationEntity.h in Headers */, + 1A26D0C80BCF9CF80073F257 /* PlayerEntityControls.h in Headers */, + 1A26D0CA0BCF9CF80073F257 /* PlayerEntitySound.h in Headers */, + 1A26D0CB0BCF9CF80073F257 /* DustEntity.h in Headers */, + 1A26D0CE0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.h in Headers */, + 1A26D0CF0BCF9CF80073F257 /* PlayerEntityLoadSave.h in Headers */, + 1A26D0D40BCF9D0D0073F257 /* OOShaderMaterial.h in Headers */, + 1A26D0D70BCF9D0D0073F257 /* OOMaterial.h in Headers */, + 1A26D0DC0BCF9D1E0073F257 /* OOShaderUniform.h in Headers */, + 1A26D0DE0BCF9D1E0073F257 /* OOShaderProgram.h in Headers */, + 1A26D0E70BCF9D3B0073F257 /* OOTexture.h in Headers */, + 1A26D0E90BCF9D3B0073F257 /* OOTextureLoader.h in Headers */, + 1A26D0EA0BCF9D3B0073F257 /* OOPNGTextureLoader.h in Headers */, + 1A26D0F50BCF9D8D0073F257 /* pngusr.h in Headers */, + 1A43234E0BCFC9BB00F65914 /* OOOpenGLExtensionManager.h in Headers */, + 1ADBA5500BD0F173008FC99C /* OOBasicMaterial.h in Headers */, + 1A2A16690BD10B1200152975 /* OOSingleTextureMaterial.h in Headers */, + 1A2A17D60BD1587D00152975 /* OOCPUInfo.h in Headers */, + 1A2A1B090BD276A900152975 /* OOEntityWithDrawable.h in Headers */, + 1A2A1B160BD2774300152975 /* OODrawable.h in Headers */, + 1A2A1B2C0BD277D800152975 /* OOSelfDrawingEntity.h in Headers */, + 1A2A1CAC0BD2914F00152975 /* OOMesh.h in Headers */, + 1A2A1DEC0BD2A28E00152975 /* OOMacroOpenGL.h in Headers */, + 1AED2D0C0C04586C004A1118 /* OOGraphicsResetManager.h in Headers */, + 1A15049E0C12CA070032F3E8 /* OOProbabilisticTextureManager.h in Headers */, + 1AC775E20C2DD4E900ECFF3B /* OODebugGLDrawing.h in Headers */, + 1A5E46300C32DACE008104B4 /* OOShaderUniformMethodType.h in Headers */, + 1A7D833A0C40147800E4A5F5 /* OOAsyncQueue.h in Headers */, + 1AB2AAFA0C4CE0CC0008CF4E /* OOOXPVerifier.h in Headers */, + 1A27DB3B0C4E349F00CB4CE8 /* OOOXPVerifierStageInternal.h in Headers */, + 1A27DB3C0C4E349F00CB4CE8 /* OOOXPVerifierStage.h in Headers */, + 1A27DB420C4E34B300CB4CE8 /* OOFileScannerVerifierStage.h in Headers */, + 1A7D3A180C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.h in Headers */, + 1A7D3B9B0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.h in Headers */, + 1A7D3C490C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.h in Headers */, + 1A7D3D2B0C4F8D02008EDC33 /* OOTextureVerifierStage.h in Headers */, + 1A7D41870C516B90008EDC33 /* OOModelVerifierStage.h in Headers */, + 1A7D41E20C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.h in Headers */, + 1A6DD1230C57B5BC00A892F4 /* OOPListSchemaVerifier.h in Headers */, + 1A3ACFEB0C5FF33A00EC50A7 /* OOJSShip.h in Headers */, + 1A736BD30C61E9370097AC37 /* OOJSPlayer.h in Headers */, + 1A736C7F0C61FD220097AC37 /* OOJSCall.h in Headers */, + 1A73712D0C623DAE0097AC37 /* OOJSStation.h in Headers */, + 1A7376BE0C64AE330097AC37 /* OOJSSystem.h in Headers */, + 1A4FB23A0C8D6A9A00DC8E1F /* jsautocfg.h in Headers */, + 1A4FB23C0C8D6AA900DC8E1F /* jsapi.h in Headers */, + 1A4FB23F0C8D6AB400DC8E1F /* jspubtd.h in Headers */, + 1A4FB2410C8D6ABA00DC8E1F /* jstypes.h in Headers */, + 1A4FB3350C8DC86800DC8E1F /* OOJSOolite.h in Headers */, + 1ACEA3490C91507000C7CE97 /* OORoleSet.h in Headers */, + 1ACEA6BF0C91DA3E00C7CE97 /* OOJSGlobal.h in Headers */, + 1ACEA7280C91DF2800C7CE97 /* OOJSMissionVariables.h in Headers */, + 1ACEA7AA0C91E32800C7CE97 /* OOJSMission.h in Headers */, + 1A6B1EF00C9AA5C6000717CF /* OOScriptTimer.h in Headers */, + 1A6B1F370C9AAA60000717CF /* OOPriorityQueue.h in Headers */, + 1A6B228D0C9B40D4000717CF /* OOJSTimer.h in Headers */, + 1A6B25EE0C9C2746000717CF /* OOJSClock.h in Headers */, + 1A2DA2AB0CB4CB5C00DE6823 /* OODebugTCPConsoleProtocol.h in Headers */, + 1A2DA2AE0CB4CB5C00DE6823 /* OODebugTCPConsoleClient.h in Headers */, + 1A2DA2AF0CB4CB5C00DE6823 /* OOTCPStreamDecoderAbstractionLayer.h in Headers */, + 1A2DA2B00CB4CB5C00DE6823 /* OOTCPStreamDecoder.h in Headers */, + 1A2DA34D0CB4D0D800DE6823 /* OOJSConsole.h in Headers */, + 1A2DA34E0CB4D0D800DE6823 /* OODebugMonitor.h in Headers */, + 1A2DA3550CB4D10800DE6823 /* OODebuggerInterface.h in Headers */, + 1A2DA3EF0CB4E84900DE6823 /* OODebugSupport.h in Headers */, + 1A43A0CC0CB91D2C00D0E239 /* OOJSPlanet.h in Headers */, + 1A43A1860CB9243B00D0E239 /* OOEntityFilterPredicate.h in Headers */, + 1A7C75C50CC39EC9005D0AA2 /* OOJSSun.h in Headers */, + 1AD1F5000CD9E83800EAE520 /* NSThreadOOExtensions.h in Headers */, + 1ABB688C0D044306008BE96D /* OOLoggingExtended.h in Headers */, + 1A1D212E0D2BD4C100F4DEC2 /* bsd_string.h in Headers */, + 1AC545060D4D228400C90E5B /* OOEncodingConverter.h in Headers */, + 1A28AA160D55438200BC0CE4 /* OOJSSound.h in Headers */, + 1AB784F90D554F7B00517983 /* OOJSSoundSource.h in Headers */, + 1A60AFB70D56093B0070510D /* OOMusicController.h in Headers */, + 1A94D5AF0D65A6960072C805 /* OOLight.h in Headers */, + 1A94D5B50D65A6B40072C805 /* OOCamera.h in Headers */, + 1AB4AEB80D688AD9003076D6 /* OOLogHeader.h in Headers */, + 1A0DA2EE0D71D280009B0970 /* OOJSSpecialFunctions.h in Headers */, + 1A5218DA0D72EC21000865E9 /* OOSpatialReference.h in Headers */, + 1A03658A0D7CA05000B5F46F /* OOSkyDrawable.h in Headers */, + 1A1616620D7DCFDC0094AE5B /* OOFilteringEnumerator.h in Headers */, + 1ACBF0AD0D82F79600CC005F /* OOSoundSourcePool.h in Headers */, + 1A7BA8830D843485003C6CA3 /* ShipEntityScriptMethods.h in Headers */, + 1A0479E90DC9F81000EE1CD0 /* OOShipRegistry.h in Headers */, + 1A047A450DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.h in Headers */, + 1A047B7E0DCB3D7500EE1CD0 /* OOProbabilitySet.h in Headers */, + 1A9322970DF53C33003FD306 /* OOJSSystemInfo.h in Headers */, + 1A62F0FE0E26A2A000897506 /* OOEquipmentType.h in Headers */, + 1A54AB530E3E17A1001EB817 /* OOJSPlayerShip.h in Headers */, + 1A71D8AB0E5F17410088C456 /* OOSoundSource.h in Headers */, + 1A7B967F0E620C9E00322821 /* OOSoundInternal.h in Headers */, + 1A8BB8EA0E8311F900122974 /* OONullTexture.h in Headers */, + 1AB812900E90179D00A84923 /* TextureStore.h in Headers */, + 1AB8130A0E90D8E500A84923 /* OOLogOutputHandler.h in Headers */, + 1AC27A0F0EA7E9940054E5F0 /* OOJSEquipmentInfo.h in Headers */, + 1ABDBA3C0EB365D90086BC3D /* OOIsNumberLiteral.h in Headers */, + 1A1E99B60EF04837008B48E2 /* OOProgressBar.h in Headers */, + 1A0729D90EF56D1200B0F925 /* OOConvertSystemDescriptions.h in Headers */, + 1A0729FE0EF5796500B0F925 /* OldSchoolPropertyListWriting.h in Headers */, + 1ABC03ED0EF86110003B740A /* OOCrosshairs.h in Headers */, + 1ABC47FE0F155F0500B977AD /* OOJSFunction.h in Headers */, + 1AAF56170F1A198400A2F2E6 /* Comparison.h in Headers */, + 1A31595A0F1B895000328E4A /* OOAIStateMachineVerifierStage.h in Headers */, + 1ABAD7320F350B3400FD2CBF /* OOShipGroup.h in Headers */, + 1A11F84B0F35F60C001C886C /* OOJSShipGroup.h in Headers */, + 1A20F7060F36EE0500156DE9 /* OOExcludeObjectEnumerator.h in Headers */, + 1ADA8AB30F42DBA80001BEC9 /* OODeepCopy.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1A71E6EF0BCE340C00CD5C13 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A71E7170BCE34CF00CD5C13 /* pngconf.h in Headers */, + 1A71E71B0BCE34CF00CD5C13 /* png.h in Headers */, + 1A26D0F60BCF9D8D0073F257 /* pngusr.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 0865423506B8447D000CA0AB /* Oolite */ = { + isa = PBXNativeTarget; + buildConfigurationList = 08B31CAC08FE63D70038D42F /* Build configuration list for PBXNativeTarget "Oolite" */; + buildPhases = ( + 0865423606B8447D000CA0AB /* Headers */, + 0865424C06B8447D000CA0AB /* Resources */, + 0865430306B8447D000CA0AB /* Sources */, + 0865431B06B8447D000CA0AB /* Frameworks */, + 1A5BF29C0916D49800BF238F /* Copy MDImporter */, + 1A23153A0B9C773B00EF0852 /* Copy Images */, + 1A2316C80B9CFAB800EF0852 /* Copy Config */, + 1A2317D50B9D023400EF0852 /* Copy AIs */, + 1A2318A00B9D027400EF0852 /* Copy Textures */, + 1A2319640B9D02B900EF0852 /* Copy Models */, + 1A2319780B9D030400EF0852 /* Copy Music */, + 1A2319BA0B9D032700EF0852 /* Copy Sounds */, + 1A3491320BC25ED400802DA7 /* Copy Scripts */, + 1A6514FE0CCC9D7A0054D01B /* Copy Shaders */, + 1A2F63BD0C5CC80B003872C8 /* Copy Schemata */, + ); + buildRules = ( + ); + dependencies = ( + 1A5BF3080916D8F700BF238F /* PBXTargetDependency */, + 1A71E6F70BCE343400CD5C13 /* PBXTargetDependency */, + 1A4FB4D00C8E130E00DC8E1F /* PBXTargetDependency */, + ); + name = Oolite; + productInstallPath = "$(HOME)/Applications"; + productName = Oolite; + productReference = 0865432206B8447D000CA0AB /* OoliteDev.app */; + productType = "com.apple.product-type.application"; + }; + 1A71E6F20BCE340C00CD5C13 /* libpng-custom */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1A71E6FC0BCE345800CD5C13 /* Build configuration list for PBXNativeTarget "libpng-custom" */; + buildPhases = ( + 1A71E6EF0BCE340C00CD5C13 /* Headers */, + 1A71E6F00BCE340C00CD5C13 /* Sources */, + 1A71E6F10BCE340C00CD5C13 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "libpng-custom"; + productName = "libpng-custom"; + productReference = 1A71E6F30BCE340C00CD5C13 /* libpng.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 29B97313FDCFA39411CA2CEA /* Project object */ = { + isa = PBXProject; + buildConfigurationList = 08B31CB008FE63D70038D42F /* Build configuration list for PBXProject "Oolite" */; + compatibilityVersion = "Xcode 3.0"; + hasScannedForEncodings = 1; + mainGroup = 29B97314FDCFA39411CA2CEA /* Oolite_GUSTO */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 1A0519350C7CCAC900BA5CCA /* Products */; + ProjectRef = 1A0519340C7CCAC900BA5CCA /* DebugOXP.xcodeproj */; + }, + { + ProductGroup = 1A4FB2340C8D6A7500DC8E1F /* Products */; + ProjectRef = 1A6B50370C8B42480035DFCC /* libjs.xcodeproj */; + }, + { + ProductGroup = 1A5BF2730916D47300BF238F /* Products */; + ProjectRef = 1A5BF2720916D47300BF238F /* Oolite-importer.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 1AD267500C83050800B4BFD1 /* Build All */, + 0865423506B8447D000CA0AB /* Oolite */, + 1A71E6F20BCE340C00CD5C13 /* libpng-custom */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 1A0519390C7CCAC900BA5CCA /* Debug.oxp */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = Debug.oxp; + remoteRef = 1A0519380C7CCAC900BA5CCA /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1A4FB2380C8D6A7500DC8E1F /* libjs.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libjs.a; + remoteRef = 1A4FB2370C8D6A7500DC8E1F /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 1A5BF2770916D47300BF238F /* Oolite.mdimporter */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = Oolite.mdimporter; + remoteRef = 1A5BF2760916D47300BF238F /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 0865424C06B8447D000CA0AB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A2315530B9C778400EF0852 /* splash.png in Resources */, + 1A2315540B9C778400EF0852 /* splashback.png in Resources */, + 0865427906B8447D000CA0AB /* MainMenu.nib in Resources */, + 25F3E8A70994FE65002F25FD /* oolite-document.icns in Resources */, + 25F3E8A80994FE65002F25FD /* oolite-expansion-document.icns in Resources */, + 25F3E8A90994FE65002F25FD /* oolite-icon.icns in Resources */, + 25F3E8B40994FE9B002F25FD /* InfoPlist.strings in Resources */, + 1A358CE20C1AB80D00E52220 /* ReadMe.rtfd in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1A2F63BD0C5CC80B003872C8 /* Copy Schemata */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Copy Schemata"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "TARGET_DIR=\"$BUILT_PRODUCTS_DIR/$PRODUCT_NAME.app/Contents/Resources/Schemata\"\n\nif [ $COPY_SCHEMATA ]\nthen\n\techo \"Copying schemata\"\n\t\n\tSRC_DIR=\"$SRCROOT/Schemata/\";\n\t\n\tif [ ! -e \"$TARGET_DIR\" ]\n\tthen\n\t\tmkdir \"$TARGET_DIR\"\n\tfi\n\tfor item in `ls \"$SRC_DIR\"`\n\tdo\n\t\tcp \"$SRC_DIR/$item\" \"$TARGET_DIR/\"\n\tdone\n\trm \"$TARGET_DIR/README.txt\"\nelse\n\tif [ -e \"$TARGET_DIR\" ]\n\tthen\n\t\trmdir \"$TARGET_DIR\"\n\tfi\nfi\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0865430306B8447D000CA0AB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 25F3E6330994F04C002F25FD /* legacy_random.c in Sources */, + 25F3E6BD0994F30A002F25FD /* main.m in Sources */, + 25F3E6F30994F466002F25FD /* Groolite.m in Sources */, + 251610E2099544090037C2E1 /* OOCASoundReferencePoint.m in Sources */, + 251610E4099544090037C2E1 /* OOCASoundMixer.m in Sources */, + 251610E5099544090037C2E1 /* OOCABufferedSound.m in Sources */, + 251610E6099544090037C2E1 /* OOCAStreamingSound.m in Sources */, + 251610E8099544090037C2E1 /* OOCASound.m in Sources */, + 251610E9099544090037C2E1 /* OOCASoundChannel.m in Sources */, + 251610EE099544090037C2E1 /* OOCASoundDecoder.m in Sources */, + 251610EF099544090037C2E1 /* OOErrorDescription.m in Sources */, + 251610F2099544190037C2E1 /* VirtualRingBuffer.m in Sources */, + 25161151099544390037C2E1 /* OpenGLSprite.m in Sources */, + 25161153099544390037C2E1 /* AI.m in Sources */, + 2516115A099544390037C2E1 /* OOTrumble.m in Sources */, + 2516115E099544390037C2E1 /* GameController.m in Sources */, + 2516116D099544390037C2E1 /* OOCharacter.m in Sources */, + 25161178099544390037C2E1 /* GuiDisplayGen.m in Sources */, + 2516117D099544390037C2E1 /* HeadUpDisplay.m in Sources */, + 2516118B099544390037C2E1 /* ResourceManager.m in Sources */, + 25161195099544390037C2E1 /* Universe.m in Sources */, + 25F46753099695D5009483BF /* OoliteApp.m in Sources */, + 25F4676509969672009483BF /* MyOpenGLView.m in Sources */, + 2512833E09BA27C100F43D55 /* Octree.m in Sources */, + 2512834309BA27EC00F43D55 /* Geometry.m in Sources */, + 2512834709BA281500F43D55 /* CollisionRegion.m in Sources */, + 083325DE09DDBCDE00F5B8E4 /* OOColor.m in Sources */, + 1A81F7090A7BAC4D006580AD /* OOCAMusic.m in Sources */, + 1A8A37560B960337007D20B8 /* NSMutableDictionaryOOExtensions.m in Sources */, + 1A8A394E0B96229C007D20B8 /* NSFileManagerOOExtensions.m in Sources */, + 1A8A3A390B962AEF007D20B8 /* NSScannerOOExtensions.m in Sources */, + 1A38B4AD0B988532001ED4A0 /* OOLogging.m in Sources */, + 1ADF5CEC0B9DF59A00FDB2A3 /* OOCacheManager.m in Sources */, + 1A29967F0B9F064C002D2149 /* OOCache.m in Sources */, + 1A9400BE0BAF0ECD005F6CF3 /* OOStringParsing.m in Sources */, + 1A9404260BAF3DED005F6CF3 /* OOCollectionExtractors.m in Sources */, + 1A9404670BAF42BF005F6CF3 /* OOPListParsing.m in Sources */, + 1A9404A40BAF462D005F6CF3 /* OOVector.m in Sources */, + 1A9405390BAF4FA6005F6CF3 /* OOMatrix.m in Sources */, + 1A9405800BAF52AD005F6CF3 /* OOQuaternion.m in Sources */, + 1A9406850BAF66D6005F6CF3 /* OOVoxel.m in Sources */, + 1A9406B50BAF67BF005F6CF3 /* OOTriangle.m in Sources */, + 1AB01B5F0BB1639600F1B949 /* OOTextureScaling.m in Sources */, + 1AB01BBC0BB16A8A00F1B949 /* OOFastArithmetic.m in Sources */, + 1A5DB1EB0BBD8F0000D57389 /* OOConstToString.m in Sources */, + 1A5DBAA70BC000DC00D57389 /* OOJavaScriptEngine.m in Sources */, + 1A5DBAA90BC000DC00D57389 /* OOJSScript.m in Sources */, + 1A5DBAAB0BC000DC00D57389 /* OOPListScript.m in Sources */, + 1A5DBAAE0BC000DC00D57389 /* OOScript.m in Sources */, + 1A5DBD590BC17F0900D57389 /* NSStringOOExtensions.m in Sources */, + 1A3AFF200BC4462200B5E2D9 /* OOJSVector.m in Sources */, + 1A2A8C160BC65FFD001E00FB /* OOJSEntity.m in Sources */, + 1A2A8D3B0BC6765F001E00FB /* EntityOOJavaScriptExtensions.m in Sources */, + 1A2A8E040BC67CCC001E00FB /* OOWeakReference.m in Sources */, + 1A2A91530BC6BC66001E00FB /* OOJSQuaternion.m in Sources */, + 1A71EA8C0BCF8C6B00CD5C13 /* OOXMLExtensions.m in Sources */, + 1A26D0AC0BCF9CF80073F257 /* PlayerEntityLegacyScriptEngine.m in Sources */, + 1A26D0AD0BCF9CF80073F257 /* ShipEntityAI.m in Sources */, + 1A26D0AF0BCF9CF80073F257 /* PlayerEntityScriptMethods.m in Sources */, + 1A26D0B00BCF9CF80073F257 /* PlayerEntityLoadSave.m in Sources */, + 1A26D0B10BCF9CF80073F257 /* PlayerEntity.m in Sources */, + 1A26D0B30BCF9CF80073F257 /* ShipEntity.m in Sources */, + 1A26D0B60BCF9CF80073F257 /* SkyEntity.m in Sources */, + 1A26D0B80BCF9CF80073F257 /* RingEntity.m in Sources */, + 1A26D0BA0BCF9CF80073F257 /* ParticleEntity.m in Sources */, + 1A26D0BD0BCF9CF80073F257 /* PlanetEntity.m in Sources */, + 1A26D0BF0BCF9CF80073F257 /* PlayerEntityContracts.m in Sources */, + 1A26D0C00BCF9CF80073F257 /* PlayerEntityControls.m in Sources */, + 1A26D0C40BCF9CF80073F257 /* WormholeEntity.m in Sources */, + 1A26D0C60BCF9CF80073F257 /* StationEntity.m in Sources */, + 1A26D0C90BCF9CF80073F257 /* PlayerEntitySound.m in Sources */, + 1A26D0CC0BCF9CF80073F257 /* Entity.m in Sources */, + 1A26D0CD0BCF9CF80073F257 /* DustEntity.m in Sources */, + 1A26D0D50BCF9D0D0073F257 /* OOShaderMaterial.m in Sources */, + 1A26D0D60BCF9D0D0073F257 /* OOMaterial.m in Sources */, + 1A26D0DD0BCF9D1E0073F257 /* OOShaderProgram.m in Sources */, + 1A26D0DF0BCF9D1E0073F257 /* OOShaderUniform.m in Sources */, + 1A26D0E60BCF9D3B0073F257 /* OOTexture.m in Sources */, + 1A26D0E80BCF9D3B0073F257 /* OOPNGTextureLoader.m in Sources */, + 1A26D0EB0BCF9D3B0073F257 /* OOTextureLoader.m in Sources */, + 1A43234F0BCFC9BB00F65914 /* OOOpenGLExtensionManager.m in Sources */, + 1ADBA5510BD0F173008FC99C /* OOBasicMaterial.m in Sources */, + 1A2A16680BD10B1200152975 /* OOSingleTextureMaterial.m in Sources */, + 1A2A17D70BD1587D00152975 /* OOCPUInfo.m in Sources */, + 1A2A1B0A0BD276A900152975 /* OOEntityWithDrawable.m in Sources */, + 1A2A1B170BD2774300152975 /* OODrawable.m in Sources */, + 1A2A1B2D0BD277D800152975 /* OOSelfDrawingEntity.m in Sources */, + 1A2A1CAD0BD2914F00152975 /* OOMesh.m in Sources */, + 1A5AA3230C0098AF0029C78A /* OOOpenGL.m in Sources */, + 1AED2D0D0C04586C004A1118 /* OOGraphicsResetManager.m in Sources */, + 1A15049F0C12CA070032F3E8 /* OOProbabilisticTextureManager.m in Sources */, + 1AE546310C19A2E2005B89F3 /* JoystickHandler.m in Sources */, + 1AC775E30C2DD4E900ECFF3B /* OODebugGLDrawing.m in Sources */, + 1A5E462F0C32DACE008104B4 /* OOShaderUniformMethodType.m in Sources */, + 1A7D833B0C40147800E4A5F5 /* OOAsyncQueue.m in Sources */, + 1AB2AAFB0C4CE0CC0008CF4E /* OOOXPVerifier.m in Sources */, + 1A27DB3D0C4E349F00CB4CE8 /* OOOXPVerifierStage.m in Sources */, + 1A27DB430C4E34B300CB4CE8 /* OOFileScannerVerifierStage.m in Sources */, + 1A7D3A190C4F6162008EDC33 /* OOCheckRequiresPListVerifierStage.m in Sources */, + 1A7D3B9C0C4F7843008EDC33 /* OOCheckDemoShipsPListVerifierStage.m in Sources */, + 1A7D3C4A0C4F818C008EDC33 /* OOCheckEquipmentPListVerifierStage.m in Sources */, + 1A7D3D2C0C4F8D02008EDC33 /* OOTextureVerifierStage.m in Sources */, + 1A7D41860C516B90008EDC33 /* OOModelVerifierStage.m in Sources */, + 1A7D41E30C516E9E008EDC33 /* OOCheckShipDataPListVerifierStage.m in Sources */, + 1A6DD1240C57B5BC00A892F4 /* OOPListSchemaVerifier.m in Sources */, + 1A3ACFEC0C5FF33A00EC50A7 /* OOJSShip.m in Sources */, + 1A736BD40C61E9370097AC37 /* OOJSPlayer.m in Sources */, + 1A736C800C61FD220097AC37 /* OOJSCall.m in Sources */, + 1A73712E0C623DAE0097AC37 /* OOJSStation.m in Sources */, + 1A7376BF0C64AE330097AC37 /* OOJSSystem.m in Sources */, + 1A4FB3360C8DC86800DC8E1F /* OOJSOolite.m in Sources */, + 1ACEA34A0C91507000C7CE97 /* OORoleSet.m in Sources */, + 1ACEA6C00C91DA3E00C7CE97 /* OOJSGlobal.m in Sources */, + 1ACEA7290C91DF2800C7CE97 /* OOJSMissionVariables.m in Sources */, + 1ACEA7AB0C91E32800C7CE97 /* OOJSMission.m in Sources */, + 1A6B1EF10C9AA5C6000717CF /* OOScriptTimer.m in Sources */, + 1A6B1F360C9AAA60000717CF /* OOPriorityQueue.m in Sources */, + 1A6B228E0C9B40D4000717CF /* OOJSTimer.m in Sources */, + 1A6B25EF0C9C2746000717CF /* OOJSClock.m in Sources */, + 1A2DA3830CB4D25D00DE6823 /* OOTCPStreamDecoderAbstractionLayer.m in Sources */, + 1A2DA3870CB4D27400DE6823 /* OOTCPStreamDecoder.c in Sources */, + 1A2DA3920CB4D29300DE6823 /* OODebugTCPConsoleClient.m in Sources */, + 1A2DA39E0CB4D2BB00DE6823 /* OODebugMonitor.m in Sources */, + 1A2DA3A40CB4D35800DE6823 /* OOJSConsole.m in Sources */, + 1A2DA3EE0CB4E84900DE6823 /* OODebugSupport.m in Sources */, + 1A43A0CD0CB91D2C00D0E239 /* OOJSPlanet.m in Sources */, + 1A43A1870CB9243B00D0E239 /* OOEntityFilterPredicate.m in Sources */, + 1AA82C8A0CC10E700023B797 /* OOJSWorldScripts.m in Sources */, + 1A7C75C30CC39EC3005D0AA2 /* OOJSSun.m in Sources */, + 1AD1F4FF0CD9E83700EAE520 /* NSThreadOOExtensions.m in Sources */, + 1AC545070D4D228400C90E5B /* OOEncodingConverter.m in Sources */, + 1A28AA170D55438200BC0CE4 /* OOJSSound.m in Sources */, + 1AB784FA0D554F7B00517983 /* OOJSSoundSource.m in Sources */, + 1A60AFB80D56093B0070510D /* OOMusicController.m in Sources */, + 1A94D5AE0D65A6960072C805 /* OOLight.m in Sources */, + 1A94D5B60D65A6B40072C805 /* OOCamera.m in Sources */, + 1AB4AEB90D688AD9003076D6 /* OOLogHeader.m in Sources */, + 1A0DA2EF0D71D280009B0970 /* OOJSSpecialFunctions.m in Sources */, + 1A5218DE0D72ECE5000865E9 /* OOSpatialReference.m in Sources */, + 1AAB9A980D779F4500A9F424 /* OOCocoa.m in Sources */, + 1A0365890D7CA05000B5F46F /* OOSkyDrawable.m in Sources */, + 1A1616630D7DCFDC0094AE5B /* OOFilteringEnumerator.m in Sources */, + 1ACBF0AE0D82F79800CC005F /* OOSoundSourcePool.m in Sources */, + 1A7BA8840D843485003C6CA3 /* ShipEntityScriptMethods.m in Sources */, + 1A0479EA0DC9F81000EE1CD0 /* OOShipRegistry.m in Sources */, + 1A047A460DCA0F4F00EE1CD0 /* NSDictionaryOOExtensions.m in Sources */, + 1A047B7F0DCB3D7500EE1CD0 /* OOProbabilitySet.m in Sources */, + 1A9322980DF53C33003FD306 /* OOJSSystemInfo.m in Sources */, + 1A62F0FD0E26A2A000897506 /* OOEquipmentType.m in Sources */, + 1A54AB540E3E17A1001EB817 /* OOJSPlayerShip.m in Sources */, + 1A71D8AA0E5F17410088C456 /* OOSoundSource.m in Sources */, + 1A8BB8EB0E8311F900122974 /* OONullTexture.m in Sources */, + 1AB8128F0E90179C00A84923 /* TextureStore.m in Sources */, + 1AB813090E90D8E500A84923 /* OOLogOutputHandler.m in Sources */, + 1AC27A100EA7E9940054E5F0 /* OOJSEquipmentInfo.m in Sources */, + 1ABDBA3B0EB365D90086BC3D /* OOIsNumberLiteral.m in Sources */, + 1A1E99B70EF04837008B48E2 /* OOProgressBar.m in Sources */, + 1A0729DA0EF56D1200B0F925 /* OOConvertSystemDescriptions.m in Sources */, + 1A0729FF0EF5796500B0F925 /* OldSchoolPropertyListWriting.m in Sources */, + 1ABC03EE0EF86110003B740A /* OOCrosshairs.m in Sources */, + 1ABC47FF0F155F0500B977AD /* OOJSFunction.m in Sources */, + 1A3159590F1B894F00328E4A /* OOAIStateMachineVerifierStage.m in Sources */, + 1A3C67FB0F1C910E0000D45B /* OOLegacyScriptWhitelist.m in Sources */, + 1ABAD7310F350B3400FD2CBF /* OOShipGroup.m in Sources */, + 1A11F84A0F35F60C001C886C /* OOJSShipGroup.m in Sources */, + 1A20F7070F36EE0500156DE9 /* OOExcludeObjectEnumerator.m in Sources */, + 1ADA8AB40F42DBA80001BEC9 /* OODeepCopy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1A71E6F00BCE340C00CD5C13 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A71E7180BCE34CF00CD5C13 /* pngmem.c in Sources */, + 1A71E7190BCE34CF00CD5C13 /* pngwutil.c in Sources */, + 1A71E71A0BCE34CF00CD5C13 /* pngwio.c in Sources */, + 1A71E71C0BCE34CF00CD5C13 /* pngtrans.c in Sources */, + 1A71E71D0BCE34CF00CD5C13 /* pngrtran.c in Sources */, + 1A71E71E0BCE34CF00CD5C13 /* png.c in Sources */, + 1A71E71F0BCE34CF00CD5C13 /* pngwtran.c in Sources */, + 1A71E7200BCE34CF00CD5C13 /* pngread.c in Sources */, + 1A71E7210BCE34CF00CD5C13 /* pngrutil.c in Sources */, + 1A71E7220BCE34CF00CD5C13 /* pngtest.c in Sources */, + 1A71E7230BCE34CF00CD5C13 /* pngerror.c in Sources */, + 1A71E7240BCE34CF00CD5C13 /* pngset.c in Sources */, + 1A71E7250BCE34CF00CD5C13 /* pngwrite.c in Sources */, + 1A71E7260BCE34CF00CD5C13 /* pngget.c in Sources */, + 1A71E7270BCE34CF00CD5C13 /* pnggccrd.c in Sources */, + 1A71E7280BCE34CF00CD5C13 /* pngrio.c in Sources */, + 1A71E7290BCE34CF00CD5C13 /* pngvcrd.c in Sources */, + 1A71E72A0BCE34CF00CD5C13 /* pngpread.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 1A4FB4D00C8E130E00DC8E1F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = libjs; + targetProxy = 1A4FB4CF0C8E130E00DC8E1F /* PBXContainerItemProxy */; + }; + 1A5BF3080916D8F700BF238F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "Oolite-importer"; + targetProxy = 1A5BF3070916D8F700BF238F /* PBXContainerItemProxy */; + }; + 1A71E6F70BCE343400CD5C13 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 1A71E6F20BCE340C00CD5C13 /* libpng-custom */; + targetProxy = 1A71E6F60BCE343400CD5C13 /* PBXContainerItemProxy */; + }; + 1AD267540C83052200B4BFD1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = DebugOXP; + targetProxy = 1AD267530C83052200B4BFD1 /* PBXContainerItemProxy */; + }; + 1AD267560C83052600B4BFD1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0865423506B8447D000CA0AB /* Oolite */; + targetProxy = 1AD267550C83052600B4BFD1 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 08B31CAD08FE63D70038D42F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1AD88FAF103F29D300AA36F4 /* oolite-options.xcconfig */; + buildSettings = { + COPY_PHASE_STRIP = NO; + COPY_SCHEMATA = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + EXPORTED_SYMBOLS_FILE = "src/Cocoa/debug-exports.exp"; + FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/deps/Cocoa-deps/\""; + GCC_DEBUGGING_SYMBOLS = full; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = XP_UNIX; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = ( + "OO_DEBUG=1", + "DEBUG_GRAPHVIZ=1", + DEBUG, + $OOLITE_OPTION_MACROS, + ); + GCC_REUSE_STRINGS = YES; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + HEADER_SEARCH_PATHS = ( + "$(HEADER_SEARCH_PATHS_QUOTED_1)", + "$(HEADER_SEARCH_PATHS_QUOTED_2)", + "$(HEADER_SEARCH_PATHS_QUOTED_3)", + "$(HEADER_SEARCH_PATHS_QUOTED_4)", + "$(HEADER_SEARCH_PATHS_QUOTED_5)", + "$(HEADER_SEARCH_PATHS_QUOTED_6)", + ); + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/deps/Cocoa-deps/Ogg Vorbis\""; + HEADER_SEARCH_PATHS_QUOTED_2 = "\"$(SRCROOT)/src/Core/\""; + HEADER_SEARCH_PATHS_QUOTED_3 = "\"$(SRCROOT)/src/Core/Entites/\""; + HEADER_SEARCH_PATHS_QUOTED_4 = "\"$(SRCROOT)/src/Core/Materials/\""; + HEADER_SEARCH_PATHS_QUOTED_5 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/src\""; + HEADER_SEARCH_PATHS_QUOTED_6 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/xcode\""; + INFOPLIST_EXPAND_BUILD_SETTINGS = YES; + INFOPLIST_FILE = "src/Cocoa/Info-OoliteDev.plist"; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/deps/Cocoa-deps/SCRDynamicShim/bin\"", + ); + MACOSX_DEPLOYMENT_TARGET = 10.3; + OGGVORBISROOT = "$(SRCROOT)/deps/Cocoa-deps/Ogg\\ Vorbis"; + OTHER_CFLAGS = "-DLOADSAVEGUIXX"; + OTHER_LDFLAGS = ( + "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_1)", + "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_2)", + ); + OTHER_LDFLAGS_QUOTED_FOR_TARGET_1 = "\"$(OGGVORBISROOT)/libOgg.a\""; + OTHER_LDFLAGS_QUOTED_FOR_TARGET_2 = "\"$(OGGVORBISROOT)/libVorbisAll.a\""; + PRODUCT_NAME = OoliteDev; + SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk; + SECTORDER_FLAGS = ""; + USER_HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/\" \"$(SRCROOT)/src/Core/Entites/\" \"$(SRCROOT)/src/Core/Materials/\" \"$(SRCROOT)/src/BSDCompat/\""; + WARNING_CFLAGS = ( + "-Wall", + "-Wextra", + "-Wno-unused-parameter", + "-Wno-missing-field-initializers", + ); + WRAPPER_EXTENSION = app; + ZERO_LINK = NO; + }; + name = Debug; + }; + 08B31CAE08FE63D70038D42F /* Deployment */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1AD88FAF103F29D300AA36F4 /* oolite-options.xcconfig */; + buildSettings = { + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/deps/Cocoa-deps/\""; + GCC_DEBUGGING_SYMBOLS = used; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = XP_UNIX; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = ( + $OOLITE_OPTION_MACROS, + NDEBUG, + OO_EXCLUDE_DEBUG_SUPPORT, + ); + GCC_REUSE_STRINGS = YES; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + HEADER_SEARCH_PATHS = ( + "$(HEADER_SEARCH_PATHS_QUOTED_1)", + "$(HEADER_SEARCH_PATHS_QUOTED_2)", + "$(HEADER_SEARCH_PATHS_QUOTED_3)", + "$(HEADER_SEARCH_PATHS_QUOTED_4)", + "$(HEADER_SEARCH_PATHS_QUOTED_5)", + "$(HEADER_SEARCH_PATHS_QUOTED_6)", + ); + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/deps/Cocoa-deps/Ogg Vorbis\""; + HEADER_SEARCH_PATHS_QUOTED_2 = "\"$(SRCROOT)/src/Core/\""; + HEADER_SEARCH_PATHS_QUOTED_3 = "\"$(SRCROOT)/src/Core/Entites/\""; + HEADER_SEARCH_PATHS_QUOTED_4 = "\"$(SRCROOT)/src/Core/Materials/\""; + HEADER_SEARCH_PATHS_QUOTED_5 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/src\""; + HEADER_SEARCH_PATHS_QUOTED_6 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/xcode\""; + INFOPLIST_FILE = "src/Cocoa/Info-Oolite.plist"; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/deps/Cocoa-deps/SCRDynamicShim/bin\"", + ); + MACOSX_DEPLOYMENT_TARGET = 10.3; + OGGVORBISROOT = "$(SRCROOT)/deps/Cocoa-deps/Ogg\\ Vorbis"; + OTHER_LDFLAGS = ( + "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_1)", + "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_2)", + ); + OTHER_LDFLAGS_QUOTED_FOR_TARGET_1 = "\"$(OGGVORBISROOT)/libOgg.a\""; + OTHER_LDFLAGS_QUOTED_FOR_TARGET_2 = "\"$(OGGVORBISROOT)/libVorbisAll.a\""; + PREBINDING = NO; + PRODUCT_NAME = Oolite; + SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk; + SECTORDER_FLAGS = ""; + USER_HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/\" \"$(SRCROOT)/src/Core/Entites/\" \"$(SRCROOT)/src/Core/Materials/\" \"$(SRCROOT)/src/BSDCompat/\""; + WARNING_CFLAGS = ( + "-Wall", + "-Wextra", + "-Wno-unused-parameter", + "-Wno-missing-field-initializers", + ); + WRAPPER_EXTENSION = app; + ZERO_LINK = NO; + }; + name = Deployment; + }; + 08B31CB108FE63D70038D42F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = "$(HEADER_SEARCH_PATHS_QUOTED_1)"; + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + LIBRARY_SEARCH_PATHS = "$(LIBRARY_SEARCH_PATHS_QUOTED_1)"; + LIBRARY_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + MACOSX_DEPLOYMENT_TARGET = 10.3; + MACOSX_DEPLOYMENT_TARGET_i386 = 10.4; + MACOSX_DEPLOYMENT_TARGET_ppc = 10.3; + OTHER_CFLAGS = "-DLOADSAVEGUI"; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.3.9.sdk"; + }; + name = Debug; + }; + 08B31CB208FE63D70038D42F /* Deployment */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + HEADER_SEARCH_PATHS = "$(HEADER_SEARCH_PATHS_QUOTED_1)"; + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + LIBRARY_SEARCH_PATHS = "$(LIBRARY_SEARCH_PATHS_QUOTED_1)"; + LIBRARY_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + MACOSX_DEPLOYMENT_TARGET = 10.3; + MACOSX_DEPLOYMENT_TARGET_i386 = 10.4; + MACOSX_DEPLOYMENT_TARGET_ppc = 10.3; + OTHER_CFLAGS = "-DLOADSAVEGUI"; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.3.9.sdk"; + }; + name = Deployment; + }; + 1A0832D60C2F105000E3AE25 /* TestRelease */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_MODEL_TUNING = G5; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = PNG_USER_CONFIG; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/Materials/\""; + INSTALL_PATH = /usr/local/lib; + MACOSX_DEPLOYMENT_TARGET = 10.3; + PREBINDING = NO; + PRODUCT_NAME = png; + SDKROOT_i386 = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + ZERO_LINK = NO; + }; + name = TestRelease; + }; + 1A0832D70C2F105000E3AE25 /* TestRelease */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1A846BA90D79F9570081280D /* oolite-version.xcconfig */; + buildSettings = { + COPY_PHASE_STRIP = NO; + COPY_SCHEMATA = 1; + COPY_SCR_SHIM = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + EXPORTED_SYMBOLS_FILE = "src/Cocoa/debug-exports.exp"; + FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/deps/Cocoa-deps/\""; + GCC_DEBUGGING_SYMBOLS = used; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = XP_UNIX; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = $OOLITE_OPTION_MACROS; + GCC_REUSE_STRINGS = YES; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + HEADER_SEARCH_PATHS = ( + "$(HEADER_SEARCH_PATHS_QUOTED_1)", + "$(HEADER_SEARCH_PATHS_QUOTED_2)", + "$(HEADER_SEARCH_PATHS_QUOTED_3)", + "$(HEADER_SEARCH_PATHS_QUOTED_4)", + "$(HEADER_SEARCH_PATHS_QUOTED_5)", + "$(HEADER_SEARCH_PATHS_QUOTED_6)", + ); + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/deps/Cocoa-deps/Ogg Vorbis\""; + HEADER_SEARCH_PATHS_QUOTED_2 = "\"$(SRCROOT)/src/Core/\""; + HEADER_SEARCH_PATHS_QUOTED_3 = "\"$(SRCROOT)/src/Core/Entites/\""; + HEADER_SEARCH_PATHS_QUOTED_4 = "\"$(SRCROOT)/src/Core/Materials/\""; + HEADER_SEARCH_PATHS_QUOTED_5 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/src\""; + HEADER_SEARCH_PATHS_QUOTED_6 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/xcode\""; + INFOPLIST_FILE = "src/Cocoa/Info-Oolite.plist"; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/deps/Cocoa-deps/SCRDynamicShim/bin\"", + ); + MACOSX_DEPLOYMENT_TARGET = 10.3; + OGGVORBISROOT = "$(SRCROOT)/deps/Cocoa-deps/Ogg\\ Vorbis"; + OTHER_LDFLAGS = ( + "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_1)", + "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_2)", + ); + OTHER_LDFLAGS_QUOTED_FOR_TARGET_1 = "\"$(OGGVORBISROOT)/libOgg.a\""; + OTHER_LDFLAGS_QUOTED_FOR_TARGET_2 = "\"$(OGGVORBISROOT)/libVorbisAll.a\""; + PREBINDING = NO; + PRODUCT_NAME = Oolite; + SDKROOT_i386 = /Developer/SDKs/MacOSX10.4u.sdk; + SECTORDER_FLAGS = ""; + USER_HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/\" \"$(SRCROOT)/src/Core/Entites/\" \"$(SRCROOT)/src/Core/Materials/\" \"$(SRCROOT)/src/BSDCompat/\""; + WARNING_CFLAGS = ( + "-Wall", + "-Wextra", + "-Wno-unused-parameter", + "-Wno-missing-field-initializers", + ); + WRAPPER_EXTENSION = app; + ZERO_LINK = NO; + }; + name = TestRelease; + }; + 1A0832D80C2F105000E3AE25 /* TestRelease */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = ( + ppc, + i386, + ); + HEADER_SEARCH_PATHS = "$(HEADER_SEARCH_PATHS_QUOTED_1)"; + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + MACOSX_DEPLOYMENT_TARGET = 10.3; + MACOSX_DEPLOYMENT_TARGET_i386 = 10.4; + MACOSX_DEPLOYMENT_TARGET_ppc = 10.3; + OTHER_CFLAGS = "-DLOADSAVEGUI"; + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.3.9.sdk"; + }; + name = TestRelease; + }; + 1A581F5C0E311A59007594C1 /* Debug GCC 4.2/Leopard */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1)"; + ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1 = "ppc i386 ppc64 x86_64"; + GCC_VERSION = 4.2; + HEADER_SEARCH_PATHS = "$(HEADER_SEARCH_PATHS_QUOTED_1)"; + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + LIBRARY_SEARCH_PATHS = "$(LIBRARY_SEARCH_PATHS_QUOTED_1)"; + LIBRARY_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/Cocoa-deps/Ogg Vorbis\""; + MACOSX_DEPLOYMENT_TARGET = 10.3; + MACOSX_DEPLOYMENT_TARGET_i386 = 10.4; + MACOSX_DEPLOYMENT_TARGET_ppc = 10.3; + OTHER_CFLAGS = ( + "-DLOADSAVEGUI", + "-fstack-protector-all", + ); + SDKROOT = "$(DEVELOPER_SDK_DIR)/MacOSX10.5.sdk"; + }; + name = "Debug GCC 4.2/Leopard"; + }; + 1A581F5D0E311A59007594C1 /* Debug GCC 4.2/Leopard */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + PRODUCT_NAME = "Build All"; + }; + name = "Debug GCC 4.2/Leopard"; + }; + 1A581F5E0E311A59007594C1 /* Debug GCC 4.2/Leopard */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1AD88FAF103F29D300AA36F4 /* oolite-options.xcconfig */; + buildSettings = { + ARCHS = "$(ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1)"; + ARCHS_STANDARD_32_64_BIT_PRE_XCODE_3_1 = "ppc i386 ppc64 x86_64"; + COPY_PHASE_STRIP = NO; + COPY_SCHEMATA = 1; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/deps/Cocoa-deps/\""; + GCC_DEBUGGING_SYMBOLS = full; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = XP_UNIX; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = ( + "OO_DEBUG=1", + "DEBUG_GRAPHVIZ=1", + DEBUG, + $OOLITE_OPTION_MACROS, + ); + GCC_REUSE_STRINGS = YES; + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_TREAT_WARNINGS_AS_ERRORS = YES; + HEADER_SEARCH_PATHS = ( + "$(HEADER_SEARCH_PATHS_QUOTED_1)", + "$(HEADER_SEARCH_PATHS_QUOTED_2)", + "$(HEADER_SEARCH_PATHS_QUOTED_3)", + "$(HEADER_SEARCH_PATHS_QUOTED_4)", + "$(HEADER_SEARCH_PATHS_QUOTED_5)", + "$(HEADER_SEARCH_PATHS_QUOTED_6)", + ); + HEADER_SEARCH_PATHS_QUOTED_1 = "\"$(SRCROOT)/deps/Cocoa-deps/Ogg Vorbis\""; + HEADER_SEARCH_PATHS_QUOTED_2 = "\"$(SRCROOT)/src/Core/\""; + HEADER_SEARCH_PATHS_QUOTED_3 = "\"$(SRCROOT)/src/Core/Entites/\""; + HEADER_SEARCH_PATHS_QUOTED_4 = "\"$(SRCROOT)/src/Core/Materials/\""; + HEADER_SEARCH_PATHS_QUOTED_5 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/src\""; + HEADER_SEARCH_PATHS_QUOTED_6 = "\"$(SRCROOT)/deps/Cross-platform-deps/SpiderMonkey/js/xcode\""; + INFOPLIST_EXPAND_BUILD_SETTINGS = YES; + INFOPLIST_FILE = "src/Cocoa/Info-OoliteDev.plist"; + INSTALL_PATH = "$(HOME)/Applications"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "\"$(SRCROOT)/deps/Cocoa-deps/SCRDynamicShim/bin\"", + ); + MACOSX_DEPLOYMENT_TARGET = 10.3; + "MACOSX_DEPLOYMENT_TARGET[arch=ppc64]" = 10.5; + "MACOSX_DEPLOYMENT_TARGET[arch=x86_64]" = 10.5; + OGGVORBISROOT = "$(SRCROOT)/deps/Cocoa-deps/Ogg\\ Vorbis"; + OTHER_LDFLAGS = $OTHER_LINKER_FLAGS_32; + "OTHER_LDFLAGS[arch=ppc64]" = $OTHER_LINKER_FLAGS_64; + "OTHER_LDFLAGS[arch=x86_64]" = $OTHER_LINKER_FLAGS_64; + OTHER_LDFLAGS_QUOTED_FOR_TARGET_1 = "\"$(OGGVORBISROOT)/libOgg.a\""; + OTHER_LDFLAGS_QUOTED_FOR_TARGET_2 = "\"$(OGGVORBISROOT)/libVorbisAll.a\""; + OTHER_LINKER_FLAGS_32 = "$(OTHER_LINKER_FLAGS_BASE) -exported_symbols_list src/Cocoa/debug-exports.exp"; + OTHER_LINKER_FLAGS_64 = "$(OTHER_LINKER_FLAGS_BASE) -exported_symbols_list src/Cocoa/debug-exports-64.exp"; + OTHER_LINKER_FLAGS_BASE = "$(OTHER_LDFLAGS_QUOTED_FOR_TARGET_1) $(OTHER_LDFLAGS_QUOTED_FOR_TARGET_2)"; + PRODUCT_NAME = OoliteDev; + SECTORDER_FLAGS = ""; + USER_HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/\" \"$(SRCROOT)/src/Core/Entites/\" \"$(SRCROOT)/src/Core/Materials/\" \"$(SRCROOT)/src/BSDCompat/\""; + WARNING_CFLAGS = ( + "-Wall", + "-Wextra", + "-Wno-unused-parameter", + "-Wno-missing-field-initializers", + ); + WRAPPER_EXTENSION = app; + ZERO_LINK = NO; + }; + name = "Debug GCC 4.2/Leopard"; + }; + 1A581F5F0E311A59007594C1 /* Debug GCC 4.2/Leopard */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = PNG_USER_CONFIG; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/Materials/\""; + INSTALL_PATH = /usr/local/lib; + MACOSX_DEPLOYMENT_TARGET = 10.3; + PREBINDING = NO; + PRODUCT_NAME = png; + SDKROOT_i386 = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + ZERO_LINK = NO; + }; + name = "Debug GCC 4.2/Leopard"; + }; + 1A71E6FD0BCE345800CD5C13 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_DYNAMIC_NO_PIC = NO; + GCC_ENABLE_FIX_AND_CONTINUE = YES; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_MODEL_TUNING = G5; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = PNG_USER_CONFIG; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/Materials/\""; + INSTALL_PATH = /usr/local/lib; + MACOSX_DEPLOYMENT_TARGET = 10.3; + PREBINDING = NO; + PRODUCT_NAME = png; + SDKROOT_i386 = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + ZERO_LINK = NO; + }; + name = Debug; + }; + 1A71E6FE0BCE345800CD5C13 /* Deployment */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + GCC_MODEL_TUNING = G5; + GCC_PREPROCESSOR_DEFINITIONS_NOT_USED_IN_PRECOMPS = PNG_USER_CONFIG; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/src/Core/Materials/\""; + INSTALL_PATH = /usr/local/lib; + MACOSX_DEPLOYMENT_TARGET = 10.3; + PREBINDING = NO; + PRODUCT_NAME = png; + SDKROOT_i386 = "$(DEVELOPER_SDK_DIR)/MacOSX10.4u.sdk"; + ZERO_LINK = NO; + }; + name = Deployment; + }; + 1AD267690C8305A200B4BFD1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = NO; + GCC_DYNAMIC_NO_PIC = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + PRODUCT_NAME = "Build All"; + }; + name = Debug; + }; + 1AD2676A0C8305A200B4BFD1 /* Deployment */ = { + isa = XCBuildConfiguration; + buildSettings = { + COPY_PHASE_STRIP = YES; + GCC_ENABLE_FIX_AND_CONTINUE = NO; + GCC_GENERATE_DEBUGGING_SYMBOLS = NO; + PRODUCT_NAME = "Build All"; + ZERO_LINK = NO; + }; + name = Deployment; + }; + 1AD2676B0C8305A200B4BFD1 /* TestRelease */ = { + isa = XCBuildConfiguration; + buildSettings = { + PRODUCT_NAME = "Build All"; + }; + name = TestRelease; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 08B31CAC08FE63D70038D42F /* Build configuration list for PBXNativeTarget "Oolite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 08B31CAD08FE63D70038D42F /* Debug */, + 1A581F5E0E311A59007594C1 /* Debug GCC 4.2/Leopard */, + 08B31CAE08FE63D70038D42F /* Deployment */, + 1A0832D70C2F105000E3AE25 /* TestRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Deployment; + }; + 08B31CB008FE63D70038D42F /* Build configuration list for PBXProject "Oolite" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 08B31CB108FE63D70038D42F /* Debug */, + 1A581F5C0E311A59007594C1 /* Debug GCC 4.2/Leopard */, + 08B31CB208FE63D70038D42F /* Deployment */, + 1A0832D80C2F105000E3AE25 /* TestRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Deployment; + }; + 1A71E6FC0BCE345800CD5C13 /* Build configuration list for PBXNativeTarget "libpng-custom" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1A71E6FD0BCE345800CD5C13 /* Debug */, + 1A581F5F0E311A59007594C1 /* Debug GCC 4.2/Leopard */, + 1A71E6FE0BCE345800CD5C13 /* Deployment */, + 1A0832D60C2F105000E3AE25 /* TestRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Deployment; + }; + 1AD267680C8305A200B4BFD1 /* Build configuration list for PBXAggregateTarget "Build All" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1AD267690C8305A200B4BFD1 /* Debug */, + 1A581F5D0E311A59007594C1 /* Debug GCC 4.2/Leopard */, + 1AD2676A0C8305A200B4BFD1 /* Deployment */, + 1AD2676B0C8305A200B4BFD1 /* TestRelease */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Deployment; + }; +/* End XCConfigurationList section */ + }; + rootObject = 29B97313FDCFA39411CA2CEA /* Project object */; +} diff --git a/Schemata/README.txt b/Schemata/README.txt new file mode 100644 index 00000000..1ef18459 --- /dev/null +++ b/Schemata/README.txt @@ -0,0 +1,6 @@ +Schemata +======= + +This folder contains property list schemata for the various plists used in Oolite. + +For more information, see src/Core/OXPVerifier/OOPListSchemaVerifier.h and tools/plistSchemaVerifier. diff --git a/Schemata/demoshipsSchema.plist b/Schemata/demoshipsSchema.plist new file mode 100644 index 00000000..ae06cbbb --- /dev/null +++ b/Schemata/demoshipsSchema.plist @@ -0,0 +1,12 @@ +/* + Schema for demoships.plist. +*/ + +{ + type = array; + valueType = + { + type = delegatedType; + baseType = string; + }; +} diff --git a/Schemata/hudSchema.plist b/Schemata/hudSchema.plist new file mode 100644 index 00000000..1238ecb0 --- /dev/null +++ b/Schemata/hudSchema.plist @@ -0,0 +1,134 @@ +/* + Schema for hud.plist. +*/ + +{ + type = dictionary; + schema = + { + "dials" = + { + type = array; + valueType = + { + type = dictionary; + schema = + { + // Not all values apply to all types of dial. + "alpha" = $floatZeroToTwo; + "equipment_required" = $equipmentKey; + "selector" = $oneParamSelector; + "x" = integer; + "y" = integer; + "width" = positiveFloat; + "height" = positiveFloat; + "rgb_color" = $simpleRGBColor; + "color" = $colourSpecifier; // Alternative to rgb_color as of 1.70 + "draw_surround" = boolean; + "labelled" = boolean; + "spacing" = positiveInteger; + "n_bars" = positiveInteger; + + // These are for use with the resizeGuis: selector, which isn't actually useful at this time. + "message_gui" = $guiParameters; + "comm_log_gui" = $guiParameters; + }; + allowOthers = NO; + requiredKeys = ( "selector" ); + }; + }; + legends = + { + type = array; + valueType = + { + type = dictionary; + schema = + { + "text" = string; + "x" = integer; + "y" = integer; + "width" = positiveFloat; + "height" = positiveFloat; + "image" = $imageFileName; + }; + allowOthers = NO; + }; + allowOthers = NO; + }; + }; + allowOthers = NO; + $definitions = + { + $equipmentKey = + { + type = string; + requiredPrefix = "EQ_"; + }; + $oneParamSelector = + { + type = delegatedType; + key = oneParamSelector; + baseType = + { + type = string; + requiredSuffix = ":"; + }; + }; + $floatZeroToOne = + { + type = positiveFloat; + maximum = 1; + }; + $floatZeroToTwo = + { + // Alpha can reasonably be two for aegis, since it's scaled by 0.5. + type = positiveFloat; + maximum = 2; + }; + $simpleRGBColor = + { + type = array; + valueType = $floatZeroToOne; + minCount = 3; + maxCount = 3; + }; + $colorSpecifier = + { + type = delegatedType; + baseType = + { + type = oneOf; + options = + ( + array, + dictionary, + string + ); + }; + key = colorSpecifier; + }; + $imageFileName = + { + type = delegatedType; + baseType = string; + key = imageFileName; + }; + $guiParameters = + { + type = dictionary; + schema = + { + "x" = integer; + "y" = integer; + "width" = positiveFloat; + "height" = positiveFloat; + "row_height" = positiveInteger; + "alpha" = $floatZeroToOne; + "background_rgba" = string; + "title" = string; + }; + allowOthers = NO; + }; + }; +} diff --git a/Schemata/plistschema.plist b/Schemata/plistschema.plist new file mode 100644 index 00000000..6d663cda --- /dev/null +++ b/Schemata/plistschema.plist @@ -0,0 +1,434 @@ +/* plistschema.plist + + Schema for plist schemas. Oooh, meta. + + Note: for clarity, quoted strings are used only for strings that will + appear in the file being checked. This may cause problems under GNUstep. +*/ + +{ + type = $rootTypeSpecifier; + + $definitions = + { + $typeSpecifier = + { + type = oneOf; + options = + ( + $simpleTypeSpecifier, + $macroInvocation, + $stringTypeSpecifier, + $arrayTypeSpecifier, + $dictionaryTypeSpecifier, + $numberTypeSpecifier, + $positiveNumberTypeSpecifier, + $parameterLessTypeSpecifier, + $enumerationTypeSpecifier, + $oneOfTypeSpecifier, + $delegatedTypeSpecifier + ); + }; + + $rootTypeSpecifier = + { + type = oneOf; + options = + ( + $simpleTypeSpecifier, + $rootMacroInvocation, + $stringTypeSpecifier, + $numberTypeSpecifier, + $positiveNumberTypeSpecifier, + $parameterLessTypeSpecifier, + $enumerationTypeSpecifier, + $rootArrayTypeSpecifier, + $rootDictionaryTypeSpecifier, + $rootOneOfTypeSpecifier, + $rootDelegatedTypeSpecifier + ); + }; + + $simpleTypeSpecifier = + { + // Simple types that can be referred to by name with no parameters + type = enumeration; + values = + ( + "string", + "array", + "dictionary", + "integer", + "positiveInteger", + "float", + "positiveFloat", + "boolean", + "fuzzyBoolean", + "vector", + "quaternion" + // Not oneOf, enumeration or delegatedType + ); + }; + + $macroInvocation = + { + type = oneOf; + options = + ( + $macroInvocationString, + { + type = dictionary; + schema = + { + "type" = $macroInvocation; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + } + ); + }; + $macroInvocationString = + { + type = string; + minLength = 2; + requiredPrefix = "$"; + }; + + + $stringTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "string" ); + }; + + "filter" = $filterSpecifier; + "requiredPrefix" = $stringOrStringArray; + "requiredSuffix" = $stringOrStringArray; + "requiredSubString" = $stringOrStringArray; + "minLength" = positiveInteger; + "maxLength" = positiveInteger; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $arrayTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "array" ); + }; + + "minCount" = positiveInteger; + "maxCount" = positiveInteger; + "valueType" = $typeSpecifier; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $dictionaryTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "dictionary" ); + }; + + "minCount" = positiveInteger; + "maxCount" = positiveInteger; + "valueType" = $typeSpecifier; + "schema" = + { + type = dictionary; + valueType = $typeSpecifier; + }; + "allowOthers" = boolean; + "requiredKeys" = + { + type = array; + valueType = string; + }; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $numberTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = + ( + "integer", + "float" + ); + }; + + "minimum" = integer; + "maximum" = integer; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $positiveNumberTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = + ( + "positiveInteger", + "positiveFloat" + ); + }; + + "minimum" = positiveInteger; + "maximum" = positiveInteger; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $parameterLessTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = + ( + "boolean", + "fuzzyBoolean", + "vector", + "quaternion" + ); + }; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $enumerationTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "enumeration" ); + }; + + "values" = + { + type = array; + valueType = string; + minCount = 1; + }; + "filter" = $filterSpecifier; + }; + allowOthers = NO; + requiredKeys = ( "type", "values" ); + }; + $oneOfTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "oneOf" ); + }; + + "options" = + { + type = array; + valueType = $typeSpecifier; + minCount = 1; + }; + }; + allowOthers = NO; + requiredKeys = ( "type", "options" ); + }; + $delegatedTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "delegatedType" ); + }; + + "baseType" = $typeSpecifier; + "key" = string; + }; + allowOthers = NO; + requiredKeys = ( "type", "key" ); + }; + + // "Root" variants of types which can have typeSpecifier arguments. These are the only types for which a $definitions entry is meaningful. + $rootArrayTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "array" ); + }; + "$definitions" = $definitionsDictionary; + + "minCount" = positiveInteger; + "maxCount" = positiveInteger; + "valueType" = $typeSpecifier; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $rootDictionaryTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "dictionary") ; + }; + "$definitions" = $definitionsDictionary; + + "minCount" = positiveInteger; + "maxCount" = positiveInteger; + "valueType" = $typeSpecifier; + "schema" = + { + type = dictionary; + valueType = $typeSpecifier; + }; + "allowOthers" = boolean; + "requiredKeys" = + { + type = array; + valueType = string; + }; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + }; + $rootOneOfTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "oneOf" ); + }; + "$definitions" = $definitionsDictionary; + + "options" = + { + type = array; + valueType = $typeSpecifier; + }; + }; + allowOthers = NO; + requiredKeys = ( "type", "options" ); + }; + $rootDelegatedTypeSpecifier = + { + type = dictionary; + schema = + { + "type" = + { + type = enumeration; + values = ( "delegatedType") ; + }; + "$definitions" = $definitionsDictionary; + + "baseType" = $typeSpecifier; + "key" = string; + }; + allowOthers = NO; + requiredKeys = ( "type", "key" ); + }; + $rootMacroInvocation = + { + type = oneOf; + options = + ( + $macroInvocationString, + { + type = dictionary; + schema = + { + "type" = $macroInvocation; + "$definitions" = $definitionsDictionary; + }; + allowOthers = NO; + requiredKeys = ( "type" ); + } + ); + }; + + + $stringOrStringArray = + { + type = oneOf; + options = + ( + string, + { + type = array; + valueType = string; + } + ); + }; + $filterSpecifier = + { + type = enumeration; + filter = subStringToInclusive::; + values = + ( + "lowerCase", + "upperCase", + "capitalized", + "truncFront:", + "truncBack:", + "subStringTo:", + "subStringFrom:", + "subStringToInclusive:", + "subStringFromInclusive:" + ); + }; + $definitionsDictionary = + { + // NOTE: there is currently no way to apply string requirements (like requiredPrefix = "$") to dictionary keys. + type = dictionary; + valueType = $typeSpecifier; + }; + }; +} diff --git a/Schemata/shipdataEntrySchema.plist b/Schemata/shipdataEntrySchema.plist new file mode 100644 index 00000000..bb0e74b1 --- /dev/null +++ b/Schemata/shipdataEntrySchema.plist @@ -0,0 +1,499 @@ +/* This is a schema for a single entry in a shipdata.plist file (not the file + as a whole). +*/ + +{ + type = "dictionary"; + requiredKeys = ( "model", "name", "roles" ); + schema = + { + is_template = "boolean"; + like_ship = "$shipRole"; + max_flight_speed = "positiveFloat"; + max_flight_roll = "positiveFloat"; + max_flight_pitch = "positiveFloat"; + max_flight_yaw = "positiveFloat"; + thrust = "positiveFloat"; + accuracy = "float"; + max_energy = "positiveFloat"; + energy_recharge_rate = "positiveFloat"; + forward_weapon_type = "$weaponType"; + aft_weapon_type = "$weaponType"; + weapon_energy = "positiveFloat"; + scanner_range = "positiveFloat"; + missiles = "positiveInteger"; + has_ecm = "fuzzyBoolean"; + has_scoop = "fuzzyBoolean"; + has_escape_pod = "positiveInteger"; + has_energy_bomb = "fuzzyBoolean"; + has_fuel_injection = "fuzzyBoolean"; + has_cloaking_device = "fuzzyBoolean"; + has_military_jammer = "fuzzyBoolean"; + has_military_scanner_filter = "fuzzyBoolean"; + fragment_chance = "fuzzyBoolean"; + has_shield_booster = "fuzzyBoolean"; + has_shield_enhancer = "fuzzyBoolean"; + fuel = "positiveInteger"; + bounty = "positiveInteger"; + ai_type = "$aiFileName"; + max_cargo = "positiveInteger"; + likely_cargo = "positiveInteger"; + extra_cargo = "positiveInteger"; + cargo_carried = "$cargoCarried"; + cargo_type = "$cargoType"; + model = "$modelName"; + materials = "$materialDict"; + shaders = "$materialDict"; + smooth = "boolean"; + density = "positiveFloat"; + name = "string"; + roles = "$roles"; + exhaust = + { + type = "array"; + valueType = "$exhaustSpecifier"; + }; + is_hulk = "boolean"; + subentities = + { + type = "array"; + valueType = "$subEntitySpecifier"; + }; + frangible = "boolean"; + laser_color = "$colorSpecifier"; + scanClass = "$scanClass"; + launch_actions = "$scriptActions"; + script_actions = "$scriptActions"; + death_actions = "$scriptActions"; + setup_actions = "$scriptActions"; + escorts = "positiveInteger"; + beacon = "string"; + rotational_velocity = "quaternion"; + track_contacts = "boolean"; + weapon_position_forward = "vector"; + weapon_position_aft = "vector"; + weapon_position_port = "vector"; + weapon_position_starboard = "vector"; + weapon_offset_x = "float"; // NOTE: ignored after 1.65 + scoop_position = "vector"; + heat_insulation = "positiveFloat"; + pilot = "$characterKey"; + unpiloted = "fuzzyBoolean"; + escort-role = "$shipRole"; + escort-ship = "$shipKey"; + missile_launch_position = "vector"; + missile_role = "$shipRole"; + escape_pod_model = "$shipRole"; + aft_eject_position = "vector"; + auto_ai = "boolean"; + rotating = "boolean"; + defense_ship = "$shipKey"; + defense_ship_role = "$shipRole"; + hasShipyard = + { + type = "oneOf"; + options = + ( + "$scriptCondition", + "boolean" + ); + }; + conditions = "$scriptConditionList"; + port_radius = "positiveFloat"; + port_dimensions = "$portDimensions"; + equivalent_tech_level = "integer"; + max_scavengers = "positiveInteger"; + max_defense_ships = "positiveInteger"; + max_police = "positiveInteger"; + equipment_price_factor = "positiveFloat"; + extra_equipment = "$extraEquipmentDictionary"; + hud = "$hudFileName"; + view_position_forward = "vector"; + view_position_aft = "vector"; + view_position_port = "vector"; + view_position_starboard = "vector"; + custom_views = + { + type = "array"; + valueType = "$customViewSpec"; + }; + max_missiles = "positiveInteger"; + script = "$scriptFileName"; + has_npc_traffic = "fuzzyBoolean"; + script_info = "dictionary"; + is_submunition = "boolean"; + isCarrier = "boolean"; + no_boulders = "fuzzyBoolean"; + hyperspace_motor_spin_time = "positiveFloat"; + requires_docking_clearance = "boolean"; + }; + $definitions = + { + // "Special" types referred to above. + $weaponType = + { + type = "enumeration"; + values = + ( + "WEAPON_NONE", + "WEAPON_PLASMA_CANNON", + "WEAPON_PULSE_LASER", + "WEAPON_BEAM_LASER", + "WEAPON_MINING_LASER", + "WEAPON_MILITARY_LASER", + "WEAPON_THARGOID_LASER" + ); + }; + $materialDict = + { + type = "dictionary"; + schema = + { + ambient = "$colorSpecifier"; + diffuse = "$colorSpecifier"; + diffuse_map = "$textureSpecifier"; + emission = "$colorSpecifier"; + shininess = + { + type = "positiveInteger"; + maximum = "128"; + }; + specular = "$colorSpecifier"; + fragment_shader = "$shaderFileName"; + textures = + { + type = "array"; + valueType = "$textureSpecifier"; + }; + uniforms = + { + type = "dictionary"; + valueType = + { + type = "oneOf"; + options = + ( + "string", + { + type = "dictionary"; + schema = + { + asMatrix = "boolean"; + binding = "string"; + clamped = "boolean"; + normalized = "boolean"; + type = + { + type = "enumeration"; + values = + ( + "binding", + "float", + "real", + "int", + "integer", + "texture" + ); + }; + value = "float"; + }; + } + ); + }; + }; + vertex_shader = "$shaderFileName"; + }; + }; + $textureSpecifier = + { + type = "oneOf"; + options = + ( + "string", + { + type = "dictionary"; + schema = + { + anisotropy = "positiveFloat"; + mag_filter = + { + type = "enumeration"; + values = + ( + "nearest", + "linear" + ); + }; + min_filter = + { + type = "enumeration"; + values = + ( + "nearest", + "linear", + "mipmap", + "default" + ); + }; + name = "string"; + no_shrink = "boolean"; + repeat_s = "boolean"; + repeat_t = "boolean"; + texture_LOD_bias = "float"; + }; + } + ); + }; + $scanClass = + { + type = "enumeration"; + values = + ( + "CLASS_NOT_SET", + "CLASS_BUOY", + "CLASS_CARGO", + "CLASS_MILITARY", + "CLASS_MISSILE", + "CLASS_POLICE", + "CLASS_ROCK", + "CLASS_STATION", + "CLASS_THARGOID" + ); + }; + $cargoType = + { + type = "enumeration"; + values = + ( + "CARGO_NOT_CARGO", + "CARGO_SLAVES", + "CARGO_ALLOY", + "CARGO_MINERALS", + "CARGO_THARGOID", + "CARGO_RANDOM", + "CARGO_CARRIED", + "CARGO_SCRIPTED_ITEM", + "CARGO_CHARACTER" + ); + }; + $customViewSpec = + { + type = "dictionary"; + schema = + { + "view_description" = "string"; + "view_position" = "vector"; + "view_orientation" = "quaternion"; + "weapon_facing" = + { + type = "enumeration"; + filter = "lowerCase"; + values = + ( + "forward", + "aft", + "port", + "starboard" + ); + }; + }; + }; + + // Types handled in code. + $modelName = + { + type = "delegatedType"; + baseType = + { + type = "string"; + filter = "lowerCase"; + requiredSuffix = ".dat"; + }; + key = "modelName"; + }; + $colorSpecifier = + { + type = "delegatedType"; + baseType = + { + type = "oneOf"; + options = + ( + "array", + "dictionary", + "string" + ); + }; + key = "colorSpecifier"; + }; + $textureFileName = + { + type = "delegatedType"; + baseType = + { + type = "string"; + filter = "lowerCase"; + requiredSuffix = ".png"; + }; + key = "textureFileName"; + }; + $aiFileName = + { + type = "delegatedType"; + baseType = + { + type = "string"; + filter = "lowerCase"; + requiredSuffix = ".plist"; + }; + key = "aiFileName"; + }; + $shaderFileName = + { + type = "delegatedType"; + baseType = + { + type = "string"; + filter = "lowerCase"; + requiredSuffix = + ( + ".vertex", + ".vert", + ".fragment", + ".frag" + ); + }; + key = "shaderFileName"; + }; + $exhaustSpecifier = + { + type = "delegatedType"; + baseType = "string"; + key = "exhaustSpecifier"; + }; + $subEntitySpecifier = + { + type = "oneOf"; + options = + ( + { + type = "delegatedType"; + baseType = "string"; + key = "subEntitySpecifier"; + }, + { + type = "dictionary"; + requiredKeys = ( "type", "subentity_key" ); + schema = + { + "type" = + { + type = "enumeration"; + values = ( "standard", "ball_turret" ); + }; + subentity_key = "$shipKey"; + position = "vector"; + orientation = "quaternion"; + is_dock = "boolean"; + }; + }, + { + type = "dictionary"; + requiredKeys = ( "type" ); + schema = + { + "type" = + { + type = "enumeration"; + values = ( "flasher" ); + }; + color = "$colorSpecifier"; + colors = + { + type = array; + valueType = "$colorSpecifier"; + }; + position = "vector"; + orientation = "quaternion"; + size = "positiveFloat"; + frequency = "float"; + phase = "float"; + initially_on = "boolean"; + }; + } + ); + }; + $scriptActions = + { + type = "delegatedType"; + baseType = "array"; + key = "scriptActions"; + }; + $characterKey = + { + type = "delegatedType"; + baseType = "string"; + key = "characterKey"; + }; + $shipRole = + { + type = "delegatedType"; + baseType = "string"; + key = "shipRole"; + }; + $shipKey = + { + type = "delegatedType"; + baseType = "string"; + key = "shipKey"; + }; + $roles = + { + type = "delegatedType"; + baseType = "string"; + key = "roles"; + }; + $scriptCondition = + { + type = "delegatedType"; + baseType = "string"; + key = "scriptCondition"; + }; + $scriptConditionList = + { + type = "delegatedType"; + baseType = "array"; + key = "scriptConditions"; + }; + $portDimensions = + { + type = "delegatedType"; + baseType = "string"; + key = "portDimensions"; + }; + $extraEquipmentDictionary = + { + type = "delegatedType"; + baseType = "dictionary"; + key = "extraEquipmentDictionary"; + }; + $hudFileName = + { + type = "delegatedType"; + baseType = "string"; + key = "hudFileName"; + }; + $cargoCarried = + { + type = "delegatedType"; + baseType = "string"; + key = "cargoCarried"; + }; + $scriptFileName = + { + type = "delegatedType"; + baseType = "string"; + key = "$scriptFileName"; + }; + }; +} diff --git a/Schemata/shipyardSchema.plist b/Schemata/shipyardSchema.plist new file mode 100644 index 00000000..c43e403d --- /dev/null +++ b/Schemata/shipyardSchema.plist @@ -0,0 +1,73 @@ +/* + Schema for shipyard.plist. +*/ + +{ + type = dictionary; + valueType = + { + type = dictionary; + schema = + { + "chance" = float; + "optional_equipment" = $equipmentKeyArray; + "price" = positiveInteger; + "standard_equipment" = + { + type = dictionary; + schema = + { + "forward_weapon_type" = $weaponType; + "extras" = $equipmentKeyArray; + "missiles" = positiveInteger; + }; + allowOthers = NO; + }; + techLevel = + { + type = positiveInteger; + maximum = 99; // Normally 0..13, but 99 is special. + }; + weaponFacings = + { + type = positiveInteger; + maximum = 15; + }; + "conditions" = $scriptConditions; + "max_cargo" = positiveInteger; + }; + }; + $definitions = + { + $equipmentKey = + { + type = string; + requiredPrefix = "EQ_"; + }; + $equipmentKeyArray = + { + type = array; + valueType = $equipmentKey; + }; + $weaponType = + { + type = enumeration; + values = + ( + "EQ_WEAPON_NONE", + // "EQ_WEAPON_PLASMA_CANNON", // Currently not supported in code. + "EQ_WEAPON_PULSE_LASER", + "EQ_WEAPON_BEAM_LASER", + "EQ_WEAPON_MINING_LASER", + "EQ_WEAPON_MILITARY_LASER", + "EQ_WEAPON_THARGOID_LASER" + ); + }; + $scriptConditions = + { + type = delegatedType; + baseType = array; + key = scriptConditions; + }; + }; +} diff --git a/debian/apply-patches b/debian/apply-patches new file mode 100755 index 00000000..5bbffd9e --- /dev/null +++ b/debian/apply-patches @@ -0,0 +1,36 @@ +#!/bin/sh +# +# Quick and dirty patching script. +# Copyright (C) 2007 Chris Crowther +# +# Additional Changes: Michael Werle +# +# History: +# 2008.11.02 MKW Auto-read all patches from patch directory +# +# $Id$ +# +PATCHES=`ls -1 debian/patches` +FLAGS="" + +while getopts ":R" option +do + case $option in + R) + if test -f patch; then + echo "Patches have been applied: reversing." + export FLAGS="$FLAGS -R"; + else + echo "Patches have not been applied: not reversing." + exit 0; + fi + ;; + esac +done + +echo "Using patch flags: $FLAGS" + +for PATCH in $PATCHES; do + patch $FLAGS -p0 < debian/patches/$PATCH +done; + diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..c16af31b --- /dev/null +++ b/debian/changelog @@ -0,0 +1,35 @@ +oolite (1.73) unstable; urgency=low + + * + + -- Michael Werle Fri, 07 Nov 2008 01:00:00 +0100 + +oolite (1.72.1-1) testing; urgency=low + + * Fixed crash-bug with no sound-device. + * Fixed some compiler warnings. + * Tweaks to build rules. + + -- Michael Werle Wed, 05 Nov 2008 07:14:00 +0100 + +oolite (1.72~svn-1) testing; urgency=low + + * Many build changes... + * Build-depend on debhelper. + * Build using internal libjs; oolite doesn't like libmozjs :-\ + (New patch 02_libjs_linkage: we want the static lib.) + * Updated patch 01_oolite.desktop to point at the "correct" image. + * Imported oolite.menu, oolite.6 & oolite-logo1.xpm into debian/ from + 1.65-6 (in Debian lenny). + * Don't include SVN directories in the .debs. + * Clean up properly after building. + + -- Darren Salt Fri, 04 Jul 2008 15:00:02 +0100 + +oolite (1.69-1) testing; urgency=low + + * Initial creation of debian build rules. + * Fixed missing libpng and libmozjs hooks for the Linux build. + + -- Chris Crowther Mon, 11 Jun 2007 10:50:06 +0100 + diff --git a/debian/compat b/debian/compat new file mode 100644 index 00000000..7ed6ff82 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +5 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..98c98a7f --- /dev/null +++ b/debian/control @@ -0,0 +1,37 @@ +Source: oolite +Section: games +Priority: optional +Maintainer: Chris Crowther +Standards-Version: 3.7.2 +Build-Depends: debhelper (>= 5), + libsdl1.2-dev, libsdl-mixer1.2-dev, libespeak-dev, + libgnustep-base-dev, gnustep-core-devel, + libpng-dev, mesa-common-dev +Homepage: http://oolite.org/ +Vcs-Svn: svn://svn.berlios.de/oolite-linux +Vcs-Browser: http://svn.berlios.de/wsvn/oolite-linux + +Package: oolite +Architecture: any +Depends: ${shlibs:Depends}, oolite-data (>= ${source:Version}) +Description: space sim game, inspired by Elite + Oolite is a space sim game, inspired by Elite, powered by Objective-C + and OpenGL, and designed as a small game that is easy for users to pick + up, modify and expand upon. Almost every aspect of the game can be changed + by using simple, free graphics packages and text-editors. + . + Originally written for Mac OS X, and under continuous development for over + three years. Oolite is available for Mac OS X (10.3.9 and higher), Windows XP + and Linux. It requires at least a 400MHz processor, 256Mb of memory, and a + graphics card capable of accelerating OpenGL graphics. + +Package: oolite-data +Architecture: all +Recommends: oolite (>= ${source:Version}) +Description: space sim game, inspired by Elite (data files) + Oolite is a space sim game, inspired by Elite, powered by Objective-C + and OpenGL, and designed as a small game that is easy for users to pick + up, modify and expand upon. Almost every aspect of the game can be changed + by using simple, free graphics packages and text-editors. + . + This package contains the data files for the game. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..bf4a76e5 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,6 @@ +This package was debianized by Chris Crowther on +Mon, 11 Jun 2007 10:50:06 +0100 + +Copyright Holder: Giles Williams and the Oolite project. + +License: GNU GPL v2 or later, see /usr/share/common-licenses/GPL-2 diff --git a/debian/extra/games/oolite b/debian/extra/games/oolite new file mode 100755 index 00000000..e83c204e --- /dev/null +++ b/debian/extra/games/oolite @@ -0,0 +1,2 @@ +#!/bin/sh +openapp oolite & diff --git a/debian/oolite-data.dirs b/debian/oolite-data.dirs new file mode 100644 index 00000000..7ee5a8b7 --- /dev/null +++ b/debian/oolite-data.dirs @@ -0,0 +1 @@ +usr/lib/GNUstep/Applications/oolite.app diff --git a/debian/oolite-data.docs b/debian/oolite-data.docs new file mode 100644 index 00000000..107a7404 --- /dev/null +++ b/debian/oolite-data.docs @@ -0,0 +1,7 @@ +Doc/FAQ.TXT +Doc/PORTING.TXT +Doc/README_LINUX.TXT +Doc/ReadMe.rtfd/TXT.rtf +oolite.*/Resources/README.TXT +deps/Linux-x86-deps/PLAYING.TXT +installers/win32/OoliteRS.pdf diff --git a/debian/oolite-data.install b/debian/oolite-data.install new file mode 100644 index 00000000..b980920c --- /dev/null +++ b/debian/oolite-data.install @@ -0,0 +1 @@ +oolite.app/Resources usr/lib/GNUstep/Applications/oolite.app diff --git a/debian/oolite-logo1.xpm b/debian/oolite-logo1.xpm new file mode 100644 index 00000000..142a35ef --- /dev/null +++ b/debian/oolite-logo1.xpm @@ -0,0 +1,219 @@ +/* XPM */ +static char *oolite-logo1[] = { +/* columns rows colors chars-per-pixel */ +"32 32 181 2", +" c #000000", +". c #050609", +"X c #030906", +"o c #050A09", +"O c #0A070F", +"+ c #080E0A", +"@ c #080A17", +"# c #0B0A1C", +"$ c #0A110B", +"% c #0A1213", +"& c #0B111A", +"* c #0D1B13", +"= c #0A1A1B", +"- c #1F1F1F", +"; c #0F0920", +": c #0C1C20", +"> c #121120", +", c #181735", +"< c #18183F", +"1 c #0D2016", +"2 c #112517", +"3 c #133317", +"4 c #15351F", +"5 c #0E2A23", +"6 c #0D2B29", +"7 c #102027", +"8 c #112B25", +"9 c #16292B", +"0 c #162134", +"q c #162C35", +"w c #1D3B28", +"e c #153331", +"r c #123935", +"t c #193140", +"y c #271B4A", +"u c #252D5A", +"i c #31245A", +"p c #3E2C73", +"a c #343F7E", +"s c #1D4B2A", +"d c #1F4D35", +"f c #204C25", +"g c #21482D", +"h c #245B2C", +"j c #21543A", +"k c #28622F", +"l c #256034", +"z c #2B6936", +"x c #2C6C3B", +"c c #2F7335", +"v c #1F4B4E", +"b c #1F4451", +"n c #224D4F", +"m c #25435A", +"M c #205446", +"N c #255E4B", +"B c #245557", +"V c #245E56", +"C c #234466", +"Z c #284C63", +"A c #2B4074", +"S c #335D7F", +"D c #286645", +"F c #2C604A", +"G c #2A6A47", +"H c #2A6B4A", +"J c #29695E", +"K c #2D7341", +"L c #326352", +"P c #326B56", +"I c #376E5B", +"U c #307943", +"Y c #307B54", +"T c #39725F", +"R c #276F68", +"E c #2C6272", +"W c #2D6F73", +"Q c #3D7C67", +"! c #347877", +"~ c #33787C", +"^ c #397E7E", +"/ c #412E7B", +"( c #3F5FAA", +") c #37699B", +"_ c #41488E", +"` c #4942A6", +"' c #4E59B1", +"] c #467C9D", +"[ c #4263A5", +"{ c #4469A2", +"} c #476CA8", +"| c #564EC2", +" . c #5B50C4", +".. c #4D73C2", +"X. c #5565D8", +"o. c #5969E2", +"O. c #675BDE", +"+. c #35833B", +"@. c #368A3D", +"#. c #368441", +"$. c #32844E", +"%. c #3D9A44", +"&. c #3D9C58", +"*. c #36886C", +"=. c #3F8868", +"-. c #399060", +";. c #3C9D6C", +":. c #3FA05B", +">. c #3FA860", +",. c #438B5C", +"<. c #409F4C", +"1. c #47935F", +"2. c #48945F", +"3. c #418F74", +"4. c #479361", +"5. c #489460", +"6. c #47967A", +"7. c #48967A", +"8. c #41A249", +"9. c #46A85D", +"0. c #4ABC67", +"q. c #4ABC7E", +"w. c #4DC165", +"e. c #4EC268", +"r. c #4FD077", +"t. c #56D669", +"y. c #56D861", +"u. c #55D777", +"i. c #53D27A", +"p. c #57D975", +"a. c #59E37E", +"s. c #3C849A", +"d. c #3A9586", +"f. c #399488", +"g. c #3C9196", +"h. c #418F9A", +"j. c #499E80", +"k. c #428AA5", +"l. c #4C84A6", +"z. c #4B85A8", +"x. c #41A281", +"c. c #42A598", +"v. c #4AB681", +"b. c #4AB495", +"n. c #53B291", +"m. c #48A6AD", +"M. c #4CADBA", +"N. c #5598D7", +"B. c #5A86E5", +"V. c #599FE2", +"C. c #4DA1C2", +"Z. c #5BB1D6", +"A. c #5BA1E6", +"S. c #5BBCE5", +"D. c #4EC98A", +"F. c #55D891", +"G. c #57DD98", +"H. c #5BD899", +"J. c #4DC1B3", +"K. c #54CBA7", +"L. c #53D3AD", +"P. c #5DDEA2", +"I. c #52DAB5", +"U. c #5AE483", +"Y. c #58E097", +"T. c #5BE69C", +"R. c #5BE5A2", +"E. c #5AE5BC", +"W. c #65DFB4", +"Q. c #66E2B5", +"!. c #68E6B9", +"~. c #55CCD7", +"^. c #53DACE", +"/. c #5AD7E4", +"(. c #5BD8E6", +"). c #5AE5D6", +"_. c #5AE5D8", +"`. c #DFDFDF", +"'. c #E3E3E3", +"]. c gray100", +"[. c None", +/* pixels */ +"[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"- [.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[. ", +"- p . [.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[. i ; ", +"- .| , [.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[. # ` O.# ", +"- . _ o.a . [.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[. u X.' & ", +"[. A B...0 [.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[. & [ B.[ [.", +"[.[.[.C A.A.V._ X [.[.[.[.[.[.[.[.[.[.[.[. m N.A.A.) [.[.", +"[.[. q Z.S.S.S.M.e [.[.[. [.[.[. = k.S.S.S.S.Z [.[.", +"[.[.[. % h.(.(.(./.~ . [.X g.m.= [. B ~.(.(.(.M.9 [.[.", +"[.[.[.[. ^._.).).).J.e r ).).R = c._._._._._.6 [.[.[.[.", +"[.[.[.[.[. I.E.E.E.E.E.M o b.K.5 8 L.E.E.E.E.E.5 [.[.[.[.[.", +"[.[.[.[.[. N P.R.R.R.D.X * * X ;.R.R.R.R.=.X [.[.[.[.[.", +"[.[.[.[.[.[. 2 ,.5.5.5.g >.r.+ 2 5.5.4.4.w [.[.[.[.[.", +"[.[.[.[. $ $ $ $ $ $ $ $ $ 3 h $ $ $ $ $ $ $ $ $ + [.[.[.[.", +"[.[.[. x w.w.z x t.8.z #.8.%.@.y.c k f #.h x x u.9.4 [.[.", +"[.[.[. X $.G U.i.K G U.&.h 0.a.u.U a.x U.s &.&.U.8 X [.[.", +"[.[.[. d H.G G.D.Y H T.&.D q.T.F.Y Y.G T.j v.v.R.v.* [.[.", +"[.[.[.[. * 3.T Q W.n.I 7.x.N D *.6.Q.7.!.L P L P [.[.[.[.", +"[.[.[.[.[.[. X X X V f.R f.X . X [.[.[.[.[.", +"[.[.[.[.[.[.[.[. : n v v ^ W g.! n v v e [.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[. . O 7 s.E b . o [.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[. : ] z.Z S ] l.q [.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[. } { & [.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[. @ [.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[. # < [.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[. y / [.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[. O > [.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.`.`.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.", +"[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[.[." +}; diff --git a/debian/oolite.6 b/debian/oolite.6 new file mode 100644 index 00000000..7fc9f0c1 --- /dev/null +++ b/debian/oolite.6 @@ -0,0 +1,79 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH OOLITE 6 "April 9th, 2006" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.de URL +\\$2 \(laURL: \\$1 \(ra\\$3 +.. +.if \n[.g] .mso www.tmac + +.SH NAME +Oolite \- a space-sim game +.SH SYNOPSIS +.B oolite +.br +.SH DESCRIPTION +This manual page documents briefly the +.B Oolite. +.PP +\fBOolite\fP is a port of the space-sim game Oolite to the GNUStep/OpenGL linux platform. +.br 2 +.SH FAQ +.PP +.B What is the purpose of the game? +.br +To fly from planet to planet, buying and selling goods, shooting pirates or committing acts +of piracy. There's no goal other than perhaps to achieve the rank of +.B ELITE. +.br +.PP +.B I'm still confused, how do I play? +.br +Have a look at Ian Bell's Flight Training Manual for the original BBC Elite, +some of Oolite's control keys are different from the original, so be sure to +read it alongside the Oolite PLAYING.TXT file. +.PP +.TP +.B What do the various colors represent on the radar? +.br +\(bu White - unpowered items that can't mass-lock the in-system drive. +.br +\(bu Green/Yellow - navigation buoys. +.br +\(bu Yellow - powered craft. +.br +\(bu Red - powered craft identified as hostile. +.br +\(bu Green - space stations. +.br +\(bu Green/Red - thargoids +.br +\(bu Purple - police +.br +\(bu Blue/Red - police on intercept +.br +\(bu Red/Yellow - active mine (about to detonate) +.br +.mso www.tmac +.SH AUTHOR +Oolite was written by Giles Williams and other, and it was ported to the GNU/Linux operating +system by the +.URL "http://oolite-linux.berlios.de/" "Oolite Linux" " project." +The original version can be found at +.URL http://oolite.aegidian.org/ "the Oolite home page" "." +.PP +This manual page was written by Eddy Petrisor , +for the Debian project (but may be used by others). diff --git a/debian/oolite.dirs b/debian/oolite.dirs new file mode 100644 index 00000000..0fe2b267 --- /dev/null +++ b/debian/oolite.dirs @@ -0,0 +1,4 @@ +usr/games +usr/share/icons +usr/lib/GNUstep/Applications/oolite.app +usr/lib/GNUstep/Library/DTDs diff --git a/debian/oolite.install b/debian/oolite.install new file mode 100644 index 00000000..59ac0dc4 --- /dev/null +++ b/debian/oolite.install @@ -0,0 +1,7 @@ +oolite.app/oolite usr/lib/GNUstep/Applications/oolite.app +oolite.app/Resources/Info-gnustep.plist usr/lib/GNUstep/Applications/oolite.app/Resources +deps/Cross-platform-deps/DTDs usr/lib/GNUstep/Library +debian/extra/games/oolite /usr/games +FreeDesktop/oolite.desktop usr/share/applications +FreeDesktop/oolite-icon.png usr/share/icons +debian/oolite-logo1.xpm usr/share/pixmaps diff --git a/debian/oolite.links b/debian/oolite.links new file mode 100644 index 00000000..72e96587 --- /dev/null +++ b/debian/oolite.links @@ -0,0 +1,3 @@ +usr/lib/GNUstep/Applications/oolite.app/Resources/Textures/oolite-logo.png usr/share/pixmaps/oolite-logo.png +usr/lib/GNUstep/Applications/oolite.app/Resources/Textures/oolite-logo1.png usr/share/pixmaps/oolite-logo1.png +usr/share/doc/oolite-data usr/share/doc/oolite diff --git a/debian/oolite.menu b/debian/oolite.menu new file mode 100644 index 00000000..c8ef1dc8 --- /dev/null +++ b/debian/oolite.menu @@ -0,0 +1,5 @@ +?package(oolite):needs="X11" section="Games/Simulation" \ + longtitle="Oolite - space-sim game" \ + title="Oolite" command="/usr/games/oolite" \ + icon="/usr/share/pixmaps/oolite-logo1.xpm" \ + hints="Flight,GNUstep" diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000..f88f8deb --- /dev/null +++ b/debian/rules @@ -0,0 +1,103 @@ +#!/usr/bin/make -f +# +# Ubuntu/Debian build rules file for OOlite +# Copyright (c) 2007 Chris Crowther +# +# Originally based on the exemplar hello project. +# +# Re-written by Darren Salt +# Tweaked by Michael Werle + +GSMAKE = gs_make +JS_SRC = deps/Cross-platform-deps/SpiderMonkey/js/src + +CFLAGS += -g +ifeq (,$(findstring noopt,$(DEB_BUILD_OPTIONS))) + CFLAGS += -O2 + GSMAKE += debug=no + override EXT = +else + CFLAGS += -O0 + GSMAKE += debug=yes + override EXT = .dbg +endif +export CFLAGS + +export GNUSTEP_INSTALLATION_DOMAIN = SYSTEM + +patch: + debian/apply-patches + touch patch + +unpatch: + debian/apply-patches -R + rm -f patch + +build: patch build-stamp +build-stamp: + dh_testdir + $(MAKE) -C $(JS_SRC)/fdlibm -f Makefile.ref BUILD_OPT=1 + $(MAKE) -C $(JS_SRC) -f Makefile.ref BUILD_OPT=1 + $(GSMAKE) libespeak=yes + touch $@ + +clean: clean-stamp unpatch +clean-stamp: + dh_testdir + rm -f build-stamp + $(GSMAKE) clean + $(MAKE) -C $(JS_SRC)/fdlibm -f Makefile.ref clean + $(MAKE) -C $(JS_SRC) -f Makefile.ref clean + rm -rf oolite.app oolite.debug + rm -f http+_www.apple.com_DTDs_PropertyList-1.0.dtd + find $(JS_SRC) -name Linux_All_OPT.OBJ | xargs -r rm -rf + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs +ifneq (,$(EXT)) + mv -f "oolite.app/oolite$(EXT)" oolite.app/oolite +endif + +binary-indep: build install + dh_testdir + dh_testroot + dh_installchangelogs -i + dh_installdocs -i + dh_install -i + cd debian/oolite-data/usr/lib/GNUstep/Applications/oolite.app && rm -f oolite Resources/Info-gnustep.plist + find debian/oolite-data -name .svn | xargs -r rm -rf + dh_link -i + dh_strip -i + dh_compress -i + dh_fixperms -i + dh_installdeb -i + dh_gencontrol -i + dh_md5sums -i + dh_builddeb -i + +binary-arch: build install + dh_testdir + dh_testroot + dh_installman -a debian/oolite.6 + dh_installmenu -a + dh_install -a + dh_desktop -a FreeDesktop/oolite.desktop + rm -rf debian/oolite/usr/share/doc/oolite + find debian/oolite -name .svn | xargs -r rm -rf + dh_link -a + dh_strip -a + dh_compress -a + dh_fixperms -a + dh_installdeb -a + dh_shlibdeps -a + dh_gencontrol -a + dh_md5sums -a + dh_builddeb -a + +binary: binary-arch binary-indep + +.PHONY: binary binary-arch binary-indep build clean install diff --git a/installers/win32/Install.ico b/installers/win32/Install.ico new file mode 100644 index 0000000000000000000000000000000000000000..a786d9afd0ebd40daac0eb177292e55e253c19f3 GIT binary patch literal 22486 zcmeHvb$pdq^7j;KTcz8TwrC4AytsRDmq2hQkl-N@NFZ1YqQr%`ySv^bH}38pVvrCg z$;>;?4cW46_ubu}ynnq9pToKLK1b%*%$)D}o&dZ9@5883!10G5^B%xyfY;wAfXur9 z>HOP-2}AFlKLnWf(eU>P;LQ2T{|GR1=Ii&L0jN$K{?6~e2bew!;3uI>4qS|+9*%?$ zzdgd|*WXwDJOM*r@e^37%Ymu70yx~2 z1&@d4!AJEnc&lE60G&%9QacKl@X@)?b!mXJu{L;`8h|zb?)B6Jyv+4U<_0Q z#X(MB8l?KtlLwt;g>y3eujBIF(kB6G{ zSZGR0fQINKXc4DEZB{z8XU9N$UIM(#NrU#>bm%Y0faghh(3YAH%|#{9k(mYEMOn~R zQ~ZXnRwMi34TU#IeGyiZ>%GlBU z;*9UVv-Eh%Istx?H@0%rQ=0jMhlj)YZvZ}-Q+iovS)`_N-k@Ow7`*uC714T}E0;>LM%6 z``N5>sH-TG2J35x^#`$DCSHrg>y=nv4IYz{E`Iv->G96aPPRTnPjz+mcw3DJrL0Sc z*DUc`B-S;=I-6K`6YF>4^-H{#iS-;F+apyG5D>6&IKP{zp)Ko0YR`)&Uf)Db4a7Q( zSdSCyd}13wtQ&~eH?e*o)~$H>z=2?)y(L9O(`}Sab+UdV)-BXioK37Bh;S&vbi;I-6v;9@JS;TgOSXa`^JTb9eBG!$>dXZRn66;)I zT|=yMh;oAgEtiwb1q*e1{=IT=@7wYz$3Ux{rfu}vhlX~edL*ya%13u4>E%ux0`h@u}&toU&MBc*lrNp4Pu)@Y&(f<7O~AHb@?MjrKP3c33I^1!C8U* z3uIeKtiO5gRT0}WVw+8DyNT@$vE3oIL&Ua~ZtnlJ-_hRw%<%jXV#V{9gV-k_ww=T_ zh1d=f+g4&bOKcyBZ7#82LFYCt8q|Dv->jpfM)B>uZb0h!(F;l{qTaBBZvICuO2oI80E zg<9-bUo0h*^a zf&Qtrpenl?jLz(Yr#JS4>(x!*AR`S9iU+{*z%8(rR)YI-XTj=>4Cq`w3r5%E;Ng|4 z@K8w}9<$G=dPf-^-Bt#D`Rkx}`wo~Z-vMX2lMu?W z12^2j^f2^z;T_Utb9I_k+;j5D-7qf>!=Au zt`8yESr6hJpF*0gDWthtK#sd1B>I^_j-Mgq`I$q7uLb+FmQWvL0I90ZkfY%MVk1Y$ zH}HToV;?9p^M@L9Td1}OhC~}{$ac4fcwc)c3~+-u*8s?M4TjXvQ1+2SpvA!fUb(qL zwOa`EdIUjLSO^RTg#slSLUD`-oQ7!^@x` zC?A>}+0S*0g__U==nsg4h}aYe6xD%PlmNwv36Lr-ftq@;Am`1iBvEZ(|(uk^k>i^hym_{St#%kDa*G`8{6 zm~6A)&+G~P-J@vklG@hO-(RyNq^GA%Y%}N2N^iV3-DV&EqPxGp|M`ZG{~Xfa)7Bh# z`Oi20mY{HXV0>dw&p?0I#x-lc9bMhu-`yOj@>_y=fiK%$^82nsYkvLp*9m{F>3`MJ zI;6~|I#rb=G5Bn z)L4-ISFKT&Lo)6;ker%2RDeS|C-~t>fB%4_fcfms0!-fIoW$?Bh8~CX^r82h|5bD7 zy>D~?Zhbc8s67kN#wpa|<6Td$FB0Cq>h6fLox|NU=hIO$j#?$Xc+u9<*7)MZ%NK3@ z^3|)(&W;BAU1I>g{@}A8XB<2kmzw%ac({Z4o#biqKQJ2LtC1gl{xc_V7K_D!VsTV` zM@J_cDo)WXdH>1C4?Z3B^Nh_00%8MQ9327!i;9ZMOPV{Ho12>KIsYdkMt=0^q@QQp z(!71n!O`E+%Zn2fm6n$`4ZZ);(B^!cPskL1sMVR%mYp_f;%kbvJ8S?&<(A2H&C zk3RkRLB_Rwc|)1XKWb|S2??5tiab_MAII%`hu=@y8CzRho0p%TC71oBtfod`M@Nx| z)Q)L9#zyas82Ra@r0$-VHKLrn{DPeGd1uvSZ#Ea*liD$r6*c_nospkC?HL#x=#F&= zDz44Vtjo-~aIqusp46s=LjT_$G2&C}{=vb)o>W&ePv4Td-rU~Iy7EA&=~IWM>0d^S z+%fdt*<#i#xAKm@-kiE9^J$~D2-yJ&-yivP+`vG8PimOY)!A;W)wpf8(>~+I4Lz6M zA2~U-r{`r_c$oVY-%@WkH;a{j{rs!n7QuK<;N03C9UU3&URLJqc6IJwKl;n-*(WJN z=MN)4n0PEKJTfwp72e#>-g|d!^>FD!@!0oAy!ZJ}GiRF@oN2_QTwqa@LP`9Gf9A2l>I{I#T{ zWMX!9_SDqW)LAJhDYJz*D=TYSG4G>zsr=>T%a@=0mn-?5jLpr>U#6$0&*c5Xp8NOj zE1y4q-sr@M6Lv?A9C1B-_^=xX7yfQ7FE6j9udgr1GikFRAFrf8{Z4WJ_D@1P($dm? zF*PwgclF{$`-`Vf=4suy)@pYDN~fdtl^#z+`CeE3TYXQ}ZuY8QyV!M9N~(G5h7AQf zcI*hgcJ10D2L}h~yu7@hpFe;8ANPEHef?M7-rifUT)5(Bbq zp4zgLdGD0Yd$sPI5ShsFn6#Fa6Jo{JhxPA5SHatH+ z|0i7?U8RRtu2;m`JfPAD3u;OYqmKM^dR1ObgEdt&SXWEM@o~g{AEg8aP=7_a&uFl@iK=rli2X-m-$`1#( ztXcnT_wL=^*4EaCYier#reEHcMVPxfnp%&HuPb(x207ESBp>eMczRVK)Llu`{EV=* zg=)CZ*>9y*i;>i7Hj>!SCYfXVDI+$PWoTd-s;Q@-m|BvPsleNlobIWy4C|Wq?%nI| z=jXSnv9WQ;1im4Fu(o)-_i3Cv_Od9*gu)0VrHOrPVjr8>k0;W3pGX_% zr6~~m`ow-J_0=^IHM9^`)>2PyJ~fJ>DaYNB^sg!mE?l@UTT4spa#>l~x5LeU@#4iO zDZxzwInv7Vw!Fq4D(Gc;8dZh`QM9Qp?ccnmLt0weH8eDI&9I)zNKap&Eq}f! z)?AtDM4{B25>7ADBYD2ZQ#WHXugZl!s3a8nfMpQ-+Ypt766Jdmi4BPN6{I0Mi~JlN z8B5{18yWkM;2$hYSq=9=fn*%A-5tnCUUqQJiWPZ69}Le$XKU-5s>e3>d#D_y0xvUa zObi^VKbOa#FpatyW9cg^qJgS%o`+Q=_Ol`?4=0{nT)!dr!C87FH=F$Jxc++X_u58D z+o`%vQh#4z2|Z6qp;E>$!k?&-^yW=%r%#`@5Oga0HJ_`Tmy5r!X%6WfUro`vGF0H9 zOV8rGsXZfxIt!AxA5uBaqVB>x>M1FrXm@RD$@Qe>OgCzYG9lhm5%1?HKQfM}s)?|I zai5YpNf;=uq&}X9FVfN_I8dsc38|ku@oMwt&Efw3{u^1(eJy`*Tjj}>QyEWLKrYv| zkXY|D@g9a6#lDO&#Zp^NJhfyd((|lj)^+LBkQ_~|S<&<&M?@WY3DnKw)Si_@uZpW_ zu%wpy3v0N}D(cRwr0&d8>c}eK`ZK65I)buXt%>)OxOx4$TuV#KV>}OkIJS97z4XHG z={#dXS_hYq`<0CpuXd1l&qxh%p45^aO3zXws4gjrLp0ST#!!8dh#HdOsWl^+x){Ie z%*>~roKot}=K8qKm+3{+p2GEV=*TE$JSvCkqoXO;-Hv$AO1n3&t<=%cxybtN7paxA z8h6b8mX0p{nQm^KO}tMhk1Lx=bpHTlm|SOU$BOFWeHh>JrSd3WDvR`@vIq`Qfm9hC zPIWONYKcpu=i*Fyk(kAEjYE7kwQzWmk}tu)+Bj|$B~iY&D@7P7(Voq#Yc(`9uI1$9 zEZn+idh>>9<49`mL^``>Dk<-rONCloN>HNAGbdTqP(a9C$tgwZ6e@(hioXFhJj1+G? zq(ruZ*?vzb!^?^iE%hn&i8p0C1o3hRqf-B9Di0Fmh?9iM5CO*%Q+;F-J&O_QkE3*F z3p%)cL#?m|*=L!)ZRxDW6+eDM>xDj;KZ%Yn|Cz3C`h_$OE+Y2_M~L_5bVo}>j2jSR zGUVdoOA6Q2$-dF+!|j#v_#TavTN zX_DH!s@&Mv_zdfS3H#P8u3IV8KVuy2n)@C1!4KRA(}-~zV%&guKTM20kd1d0-BM2= zTjwZZ{DOFoONC^e~&(iuw) zj4UA=_d>GvE+Z5BY+~Gmc#lquQIV3e5m_3z5#t(^#BFa*cE!BgSCJ zU+p@%-abONj&E*Px^+uKz_W$=W5c6&?_9RHZ8?v@#+lJp-kcA~Bh z!j@;$$+n<6#)DE_%qY&)n;3hb^yo73@U9@nD=0mwhD>aW$lbSu7)K(;k?2`sCKdSk zQN$BfuKyBws!G#^6ViD?-w)ehwtrtLpH~Q9HDx@lnlhSBton%NG8@rl#@`zwmgAyyxqu8 z;~H6C+eaF5I|gnk-gqMTSwh=~pVXAJHPUN;eYtA#H?(X1x8(Kc0`=Ec5Mvq=jG6Hb z8mKO#7g;gX=w(gmn!03l=ms%1NU3VhROc(AR&g#dCPHyBRb*>lNEL~NR2?2cj8Bo? znQe6Y;3|?iemE^KFmQw5?+n*1#NA!pADieH8?KxA1FoAsmX!7_p~@IP$=L?AHcM>p ztMUTsWF6epDB0+Q@V{AJ(1Z# zm$oe+8L3S#4GfJg2)^~3bq{~8Z+JFN=HRKUwKFFTZk;`Ww9jl{Um%+LD@$mws+3-p z=23ra1a*1(QKPjB73tUzQf+$nfor13FlD+I{lHa(3j;@$a$2YI+yKzIwQLy*FSLfTm z6%|)bQ@(LGV8^1_gKK^sOVYo5PmCcGf9s>!y827Y<6bKGro<{a5XNTZZPA)~{`C0u@f|I#9h~#1mo7dCf`M=Qe@5j)hNvz9e&+0vO{H)l|&wrob zW4vAdzxzEeuWY`dp0)nLt@|rh&zRY}c+xmrGI2C5<2qJ;Kbkg79nWo@!1a!$`4h+E z`USIJ9zS?8*}~lV2JfS$c6WFG!`a`zTlbskDr@V1boWX)X`-X2`QWC4)%Ek|JTA+e z^t&b}<8?>=lKrE*w~dVSOdke@$L!(r!EwKL=J;lL!=I^=@dA}0e1b8A@x8y{69P8z zju2Eh@o?ae|MF4c6t9CMe>CG9m5f)sg;m@Uun_^L5GLhO4u3mOd9%mfI8T`~{;#lP z(l@Z0c`_Sj@tMlhiLhh(1lTnDM_{`R>*mgd9Si5e@gmtgIZEwFk02H3Z5E$rT~1NQCS z1?To|gqzZQ-Xbjp@`nz<&0|~P{0S+zcIF_g+js%C@3{bbwp@b^J8pvXeg%-*e-h4~ zmVp~GXF%@6B~ZM06K-C=0T!}ffs^tmK8slbYEp|pm(OA>FP!Zys;JR6r{ob)=n_ps{poqHlub{ z1~e38K>yY`P?x(34{j)ck)k35T{r^{N+-bO>`ky$z6`Mn=ON(1N8qn98bb8Hf&{IR zkf%BV()33{it*4n&2v7d88ChaNbgH{=EQ)K)enG{vl;te3dO$5038;Av&sf=RgnV! z2M59Ez6_YEUI(|kG7zkJ8lv>if}4gS_%UZFNdLOzT&CZ5EuiNSNO*J#lAoM~IJ4`J zV{rxIoaG=T{4~US-iC^BML_=(%yYR66(Tv9ze)>sAAAT0PMX8fllS5Lr3bJ@It2Ef zbOu=kA2@$A2`r>l!116eJXBNxd`FkmG&OGxP#S69LbX0r~)EWRVdfff)rIF$b4)8F~+7)$7Lls zs6no?8kB^pL5c%&l-x`p!N&{=0!(2r^d8hZnn6{75hSTPLhfT{NHueTLIVdV)b@jP z6JIDX_JdjrH%M}`gIsqfNDOp^!XRf5JNrVeOE5%7MnFkmFtjp1s?*s6>O!2ME6@oV z9Qi!O)eov8n4=UB1f{X}0OM5wlg*(l!2=SbBltHTsEcz0%yEPIgkb1SW^Phu1Yl_} zJTl4wbNeE2^UejIkSZwgNCt6`i0g{sGnX7FiQ@AR$5?pj&b*{>;mk!0RYAEB5myYM zqFRU-i6A#V5fT#%pe(rn^0NxznK%JDlf}@Sl>{&IlAt*$2VSP-Lt|kP^c3auxk?G7 zXEs26ZZ-6m)By6i3bwTXc6YIM_<#M_8R+WH5FTA!8;5^Yxr2e4+NSlqsXo17)6|J$ zr_A6X{O6B@uG*RNej-s$eqmuwPE54%=CNa^{PP?JYKETvdF_0*&?`J09qoD1dSkzt zV)MH-*iD!9@-Hmz=}xl(?HK6m?I_IgQ6KZoW4nLMVW8*ipHtk~+s8=$;EOY# ze`D4&IM~FLU(A%BPHGK2t ziGItvtL?qqDU$R(7W)oJp1q$+Y7jESm{0vi_qR5ypY<(d$Fon8KIhoQi_B`_XaAqf zdT?-XptmbM#(nZwgW(KMWv&M07YmIQTB~sDR#ukc@q^M*yN~7ytsm^|E)x65elu-2 z{TXL}QGT(c0+xTU!>^zqJL}T%nG;*NHIfRt3u8>C4p(5d)w8g@xQH_h3Dn_NQc{o| zpg4D8E+-b!4|Eq*`)nWcy2%?>J16sYwYa2LlA&)Xeb%M9T>enPTijhJa^F7ovCx7|UwpaZtT1zg7IgOY_O<%; z^>r3y2b|dOLXbsxdb``jzDD0l1RD3rmvh!!@d_#w1QP1Vxt__-jholRT(ab0=cheA z#(7AfPd@%+_Tn{{<%75h9l}38EbHmzdisVE5A=7l{6*<8J{uMj&{z}+1uSa(9Ma59v(7J*$F(x2_Nuz+m~}V!xgXm;zC|ng@t(>wZBf- z-Pqm8<##p2_?#cd4R(0<-48$hawvm*P)>eMp(rm;EN)FtZ(!f4tFfy~$jF~WzH;NZ ze7lkFzW?DTf1EAUaM_uU%tRtze}9oErdr$}u54gTSIE=Y*w7$$p2+3fjo=I)kNR@< z;uR~;KGBcm$KQ+5-_ZE@>bmL%aYIAnv#!Qx4Mp5Tlhh>XN522zC`kr6eJux-AM|4V z{e3-yLn7kE)eUuAF6ZiM=n~3jc^pP^1wtRqp0~nSyHx9|zxP0Oay{AGcr>cj`&|2M_MvziVmv)t%fQp2iDLb#=o~@`iXvnbqTj zCGei4{EtV?UVOg1w6yfqgL31cZ1R9ae5$_JoIN^Q!2xnpf#+tCp3ZX_qO>^sR4Hh6C&H+a-ZQ3o%_-p-AB}wXQ<*|R8}xN{rUXWXX*|@3Obw5UjI3*@i}I0~ z|HpBYh7$|%^bbb9H|i{RXkTA%QASdthnwo7y1K~7kl=7PN2B?(zW8F|-`lZiAHMhA zsPo0VXgiBCGLoXAG=l5vQzL^PX&N~${P>G6CRo2&Jb#|{!MpE{S`)ipUmEon30x|QJ<0atiC=f+;QO_x#DqZe=p2X zUb|@@yf@VossVg&&YR#)W2_hhjW1SkAL7SoO0xQ@4ff+-e{+!-Q0c>%IE)tev$MKS0w2#Zn&iXxPYO9bWS6rqux70{P+9R30w%b z^?OP5zw)1e4Yszne!*vwe-+M&Dk>_*3uk7miN6zq@LBjxcqg1&GKTp1FrN6oBGA84 zs8GKk3tQbE;^N{K31^*-j*k0;a~1*XW^VO4=E=zl!O+m~teKhFNqc+y{XRZETZHs{ z7BQu=vT~eo-v9ryE$mu+D%2~Si!$GCr>?H{;08MJ--_K0oU zwnc5-x;0t|TefV8;NODyy~n9jrz~&Yy!lYbC*Znl>X!(8AoRumW;@t9`H;td4BPL8 z%vnEl>(;FYM~@zLJAL#-`nA(%s+46Sf zn4dD6X;9zrSp1LhQUv^1BoZyw)6+YD{OEDt+vl#gI;r06jkA4-d48rWuLm}z^7$m6 zNxsbGv(x-c>?+LRv*cXn`sYbLcjjkNdrmsGW+d@BVl-_)Tv;X`pyY(NjD?Q20&25j8;w}9vCoeX5XenWa zyE+z!8e(m{H@0NNBFl_DEN6dtF*2V5v9=lqYO1g*ISDnh38Z9*|TSj?Ck6g2>R*YolAlq z^KkdrD=%|7-tg-6zAP7Ws)}-8E|M2M&yHfANGfw=xG!tUxzB3}n;Vgyx6lBe+td{n zQn1rqj58cdj4cWQ8G|J0P2=d$zP;$<#P1M8NpZWY?V92Px+<0aTY=5T!cd-G-ZH|95dCY&=~mAf4D;PmlVnQm6d zykBa~jAH&k4EK97^_Ax^$EF0Sp63kTg%SFFkmavu*`qyFD8uZp*k#GC0CR580XhE#v+@Qsi!HM=#2Sabw<1Z9NWDR#Pu?F4{6Ou{JuA@_pPW%E%Da&Yd4vzGzVk z>nG7LFRQMu?rYv>zP?B>EcI`(FizT!wbdV=J=q&=s)WxHqp_nf13QaTk^3Ec%d)V) zs*uN@>u;z==11|oX~EX=G7NW9$8?iX*vB%X2?wL!!YeaC#(i#^cgME! z3SJL9?`j)xu)GSHw}CHGld&c;9P_-~Fx=1pl}?=OdXkWhG9HMao^E{r=GjR~M7l`>RHHFW1GqIP?^ag8+x!)>K!f|SDl=OdT ziW}ddSw@T<6S^%!#-WJ}J|ol}i2~nf4JT-LMaT+R^p2g|Cl zH?NS|lKJdCG8FSXohZi0oX+eyFfe=e>|*8&S_t+*(5r9TX+htxKd|WGb>$HI+o$`J zEU#dpml4&AyjVwuQ(JBv>vHb@@_Y$DOCoMiiE(VI4!J^@q=^)FpD5%U zF_w;utut@x17hqQ8HXpv)+sGI9J^{I+$ENuIYjl$qp7aL{-Sc~%Hnf$aU7P122iS_ z1-U-BgUc2yYFoW}wPYT#En4z6*HN%{PfaY8_2uOnV=V4rQJ^hWggIkFoHw?nhGAP) z4C~88>?z6S`BOBccj|fmG}a?F3v;Lmt68Uuy$z8EA2HhjG2IF=Rt*`;N3@=ge5QdH zPVUAmkqEK%Ip^Wf)XHVF@Lc5eT=NVE%c^(`im*K`73*UnG2hD-Ba9y5ol~a<=FFZ` z!Q4#=H&7S@?(@I-7>VM_YedXR zy~Ql>`NK9rSPw3Cj#t%Xk2Ekr z4f)t#S<366itoP zg?^_7=HJz`{MFnKEdM|W^Putyu`Ml?YNNur7zYf}QX!?|hhEK@GpB_8S932fuWf=Z z5>{_fQQ>5rdn(RK((4CxPpw1myN5B&Rud~j9kDUdA6wJHdH%2-%1gk`f@Jn*GI$)a zcrN8*Us*Bx4W(=gDzGM1gkoQF6y(R!0K{Am#3VDsh`Wfs$I$ZH47_ww3bP_&Sob&c z`0<>n=DEl72CKR6D{GPYb2wO1fqnTrhcYs-AvOk!eB3eKQXg$^U%_4LH$G=SJix@n z$q$anHAk?)=`zb=A#S|XM`iMeel z%yCQOv0#oH`)J)o`PfrhOqB_d6z|Pv@0HouTb)O}#p%?W9FDzV_SEO2$~>M8X!t;Z zDiYGUueslcxNkU6%05LY^FYhkj+a)`Kyd~0(Teb8W;XNc;+UJ`j~UKSDOmd+UEH^) zn|<$O6&01+0+(8lKgi4L)VVFIdrz&IPS>{1CG8_C$@|tW6dTA;ww(%AF#oDi?8i1T z8ecO1t)n23^-&VGv9I}(xvA`1^0?$sl{k#}Y>mp|?qR8jdA!m0s63MMhbvN5*d^lg zH_Q+9u1v$MK-;@3HNWZBOoyLKkslUhiYvTHHu&Mr#SIf1!W z@>CgU%sfXwY|D)1yRIU7o*jqH841*qA!gg0$UadjJ`E`C&J5<_Ww4K$o=we( zVzvb#tkN7wY^_VC%Cfj+?dk^RlG_PufcJu*TN>%9>{)E^uWkATA01eX zHr$3l#ci0NwjZ-iFJq~fHa_G1$n(rFY{`tk#asuxO zxLuj4*qNV!Jq4NAnU%-7v6Mp@_6V}(l<{w+$b3O${vf_gEkx!IVtZ=Q&~IsZ*qo4r zH4$N~N1ZUqP7hr*FQfFez6q z5=ByNd@Qn$OzmlD^fDzIJJRx~GcBJw)AL#8LVBKBfX|up*ecFr{-K~Vb0u8J*KMMf zMCM^eMe`bVW<9D;e!B8_c*n18%$fHS_}RSo|BJTTeZzIr#^TmrCXm#;?ms?MPH?~vk{r#By_#9W0E+fiGFcen4ep81s^1sWCbko8wZcH6fFpCuCp~^AsE7GO#f&i@B0n{Cf@xyvg=7 zfybH8W06PC6L}4blCUtqpVA#oD2U}hx?}ZA#s&g~Ju}2*fDDY z?w>yiWmZi=h0VXBm zWQF+wW|-@5%E27-{OyrJV~)MpFAl@Xpct$P6LH`@OGpA%g^IB{Gzn`XlJHqfDmIDI zu}PeXEkcOrc8Pdj5S5H|QDSU}iD#AUhp`rFcxLbVH|76YO-0Fi*^gf%^DJl+-<94q z=Q|#QA5oV1u{XBOLG`_h$oRxcvOB*P-4!-dsHO~=KhnmB4@@XI(3#T0JTTVJg>u3i zDA(VRvUr_jGN(Ah(~>egOfb#Dma?sbnd==SF-bta2anHUlX-rLFx$x!BlI=V;r3BHxMkUM##`K6U0rtx`qN1FvBs*Y<5A$W z3U}o?f4{bs8ydpF=vw7NgtUz2xrTOP7?>@Pbk@ZIKS5qZdPQ z=TS#ee-cRg#=bOb(N&Z>e4k=PB9fN6icSWuld+4iBB>lVTH8+Z)7LQ%qd4UEC^ zU>=9SSb+=7b9{(@TNf!t4$Oy*qZ~(H4As&iw>z@Th24vXx36kCcI=pqfLAik^^vW) zsp8rh-{Z0$SpLZ@|1aZN{%=uw(L_43^anhB*Ufb+ei}ytCwI-%F|_%Y@wk`!|MZ&4sC|41 z#hPBB!VpKSOAMt}_Ip~{e{STzLr|L;&-~Loj1%QCKe~j}_3}vXX)!v7RFMvIbRV1K zqnmpH**j*?g4IuO;c6Ai$tXRyvNRS1+F)yT0`^rF;Xr)__SaPidtrQ$n}U^TVq|U(CWc3&hfe_p z$JL-HyBQt4Dlssk28%MQ(dcO*ZrLA>4;jlqlv0mm|I4y=rGWVMjYsYa_j?DyHW-iC8W2 zVcxR^C3rYev}XV&MCMUINHqloR$y*w4Vl}Q(rxu@jEkwJwD=0LcP~S8`#kaqD#0_C z-01rmH|Um%Gd88?Q8oV!j9gC-?td)`dUTTlH7=pA#t|}6+%b6SuyiKhqqr>GSrcR* ze$rDiR>+^0N&a;Pj{(~OVGOo0xBcqYnHXt$n|4K34s^oS5BDvo^i7qO8=)r|O&(EDbC(%1^@88)D z{Lw_yOn>*XW!*dmxZRwoo;Q(*;$Mb)C2Z3$)K z#!RKl3JIGROdnh>=%MMO>A=!SXm{r*HK#{&Uq3?$zr3%H&(C{Mm`B8%_g59X*UJ&J z|L96}PDYgRNDG~0u9Ml`Ys7p<3^=PxMaDtY5W+epA%kAzmQqzg9VUru$kDZw?RX}+ zI2TYI`_9!-F;p7pN3jME(E93rVtysvJ-iy_k4v>(xpGBcxEuM;@T(r??z-9eUp}^F z>+?0!#^LHI-_Z7X6Y#;I#gyl7O|L3Tk=lg$&+|r@|1TInU<|R3*KvCm@5v&)nXB$V z^_I4ntY%0~hwhW<-WzCl{2paKa-lMFAFQ=w?zn#vwXpyBg8k^$+)Av>uA!EkN@^Cd zuND+Y*-lnudu2b~*|&n^cP^qU(!a88KM^I|sT9twhUfh6#f;sK(YW)-ee3+0xPs>q zbL;WM%Ae8k&Ix>$6oG?vl|z`JfcY@)%or~3<8!bxGXXn%-LTo#3I+K~jBGGOP6wHD zi9WJVu*`(dVJri&h~rWVU#xf`^@>M^FFLRP5 z{CviK7&oY-fyy%KV=SVN?`ZV~22#6=2Q^wbW0jd5WjwSX$AcOebHj=n?IWq#DHba& zf~e5Io5#Zg(=;5ZK;H$Kze$N|dK95@AJzA7BgNfI1P(vSZCgUerFOI`-n?ZXoVoED z{axJi_uAm=9eQm4w!_sM=T6~yG?s)m$gZD);kuXTIj@D@;w;8o`EM!lUDJWGT*P=@ zAAzAX;LBK-lQ%Y4xl*l>6NR7G!;HI*%=s5lyF2@2wqaEEgzp{PbLMv0QH+u)Mk+s~ z=(`%Ee`psTTRfG{uA4)rH!i>-uTN@-Y<;9WfMo^+8@T^{#iew+|pn0#@czvoO#sT zu^b=mT#n3>M~h=S@R8IyJilT-O3$8#>nBgZ`4hfEz8`>#ei(-62;cq%pW)!q>Ej!;*>L z(2DQJ;F=%D&|01oTV_na9Wy7==IP(^{gE*^fBaW8d;DK$@sDG1^{iR_ySMBvzjfn| zBa@j<=j7&2{ddpk-j?k@`Oar(U*zPKFK~4WkiT;4QQ*2|=bG2eo~#dMxjYCeYfclW@(9DYSC_`rZSl*$=E(%G@mF&n)-+>hBk*2nu_G1kbgC}>a zEX=ODIM|;G@b#67juNfQ$j%iw0%M2og#TCNAtqi)ctcD$8ZYpNm`^0(7gheoHyY+2 z{R1Cs{A*qopYTg~Ow4bR@UfWpG#qm=5`I+#X3h+&+#a?d5h$cxl7^u>3V&?GE>&kdOcS8?TKEkn}5Ol=L(A J_Z@!!e*lp-<@5jm literal 0 HcmV?d00001 diff --git a/installers/win32/Jupiter Europa.ico b/installers/win32/Jupiter Europa.ico new file mode 100644 index 0000000000000000000000000000000000000000..6125ef244a1222602bd61c3df895533eec15b1c9 GIT binary patch literal 2238 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 V-y3mze!FokS9-yl6aPPi{txof$3g%A literal 0 HcmV?d00001 diff --git a/installers/win32/OoliteInstallerHeaderBitmap.bmp b/installers/win32/OoliteInstallerHeaderBitmap.bmp new file mode 100644 index 0000000000000000000000000000000000000000..1bdfd63bbeaa59d60fee7473f1a9669106a17eea GIT binary patch literal 24374 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

RwGsE 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%0D+5&3^<^idvlRw)QIGM?lQk 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

=S+UZuI)#&}0BETeNt{5NUrv zV0tltSD35|yI16VdvoO@^guzvBvjDEV(2!Es)nd0bcxT%lGYdWPN|U=(992A^G+xZ z)3VlUXs*qQvU^&ZvQ8Aw7Ec)S_R0C!6EyS5ojde;qGYhI#N^rW%>0F;9ii4_ZBrEnh$YX z9?2~@)yE6~W{4R+mbFP3v}|b+Xyi(BAt|j(#|^hH$pR?)aFR(VV>lStNG)UMs7utx z9wvf{ORSdWqw~U1+{YS0$PiW>Cz7V9-6m`56R>maN{ZtArE z*vG2-kgKiaA+D>p0u0JFf1vI%*^mAR@~{BzzS7^Rlo8ShjZkk%*M4vAq$~scc4B-{ zjxv}$iq!L?Ds!M8A@R2CM}!X)uCNdiLBn(mMO}CCPQEAXG?syA;b^`*&lhVTx&?^~ zhK)J#HLm&M-n3e)7GvbU;>RR=u60HEP6W60Mxm zQv4%#OINS2#@~`t!V#y>JE-yj6Sc0oDqr}tH0E0R*Hml4hyfwRp4)xDLGp) zW9~dOmxqV51sOt*d~(J3^fT1UM4$(B~N^AEXXGjbgz}Wo)McmB#vnADk8L zt$@S{^^(uo22P7SOp>F%T(MiWZCgLoNQ{AJ__v1>P-o9q{KL-vXj=6lD$6D#UpMs1 zuo~5y(5r5InCHbjQ3lODZ-9&I3oXivYb!HD?i6?{bCA|g7iX25Q3-EW@V`dl&L8BJ zTKgP3PrUk{Az<`)Z-AR2f~En))C+2q}`Eq9Vd|5Uqql>od4LGmEJqFi1SpKn-=__b$%@r4|wkQT5ZlC3C-TU0#aMi=r zZos76s3>Iv1Mg=j5Zh~T;68$oc(Qy}Wxeyr4t{%3bsQ4^#D$RQOrleHShGazLUjAmPF%CHB+$EuKlJIC^IQ z-8M$DoI7Wr%0LG4`*C}1U4T&D9&sl^^{(l(v*Sl$6%$xz#U8K35!WApl^xM@y$N2d z*DmQ1cLfw}kKdqN5@nci-*`BktipALXr=rT{w&Y!LMjz5FcIgVTvy9xJ0AG~>*Da| zG`b}veU|cG*&eE?#9a9i)XQDqI68E4Eijd`nRc?L{7E0IwB~q^A59h)aT2ZG>c^(6#R7C-y#6S{1#f zfnTF|LD@y7IoYaeIA$~5ZIC>tQEQTGIP-Eg8w;H)S5ct0wdJQJ%|Jh*nC@A<7^K*+ zmXTT--LBHXN{n*C74fRVv~S}WxT(hs^#qL(PRcAVw2rHHfZ&sHd2I|C)m$IfVB|$8 zX_AO(Lu^iNigh;K=K;vbRQ#$>xpn=t$-!WsHq>{)e$*-65%zO)m9A-slY>7MS(~?h z-6Pk3wlI;H)%?z5azixmc>RTq(WlxLNwk-sE;Pf)`nJy1ZFK~ngg4IBA%&J^9DKna z;7Z8Dn%`_~AI18tvkCe#)8N1y{~iAF{EPG#90n2w_TSU}e~{ehXzVDc7$oGZ38XBd zMid+(;wtKnWXgu7E`ND|{|D^&bpq16J5-jnvc$HT!!-o4VkWUn(CN)h4lzY(r#@-y zt2X(=l?Zqn6Icb!fppOVPT~ot|X2URHYiyl&6T;i!LcFBr2=-8twKf;3qnnq;!%IHJZ}WMunZ0bo)K(C?4XX&oGbvs4*1-dZFvY{6RNzQ1(7Ow zcudr0&|F8lap44-;Q9skcv}KD)PJ?WxP26&i0`2nx;`gek4(Hg;;HGHc1WDe28{8z ziq8;wKDfQKE_T^GK8rE*E< z+o5$-t%t$%a0zEGnHekWc2oc6ZBzY`UVn@F%tY`9&~EfT;az(02jD`nvie$pw=KOX zTuo*u-$YYtn)}6hB^{hUt~sA_^W)60HX2hskA|O#Qch&t3IERH2 z7i&J9lYo34jr(WfXWK>i`p}N(36H7Yw(RWOSx%=}h`V%L=|aw56v%UDH4EVh`PM64 zgXVZLy-e0$EN~ba3%Xf!)cyb-VBm*%#_yNe1h{s}g)@x*08oA6SHESr{uYZkyq~Q9 zxb*d|llHRe8ii+}a-NZ5hl=*^FOE%#(<=v>J&9R%0Vb+G^;;lZea~8_zc}9O6i)hd zJ|?BzMOg&ed-)cs%|ol=+*SAOONSBQVq?vuI}QT=h+i6iQiQ#UsjODMmWSiTvkcyo z&}h%Li+w9H|&=J_5l>TzKhg}n5h?<`lRSyQE~Gajwp3Z)1$ z3_u2>CScFj?qR?Ydc@v|%dU?G_cSGEOw%(}v*Q}y$JWZ_Rn72|d`1DX!GbVHJ6`@6 z;9Y^O8hL&479_^jw4KUpZBH98G1HbZC%2~4bYHDVe*>t=t(huR`H^4KL`Wxq-5$#^ z0qfK3GPQrTT*_a+TDm}U^qMb4POJ{T&iCspFFNRp7VGsSN^S}Ml)&TpQ2Lz_J-l0Z z&*g!h0Zc^Y{+Ubn!1wcMD%|;k(u+f)kpnv%$}VQplc+TLg}?UY9+)MZZ$;+0_~RW< z*6p-8>jZ@@3+u#8x+E^+MM`(UaqZ$FDt%$ioYv1CETv=tVt9+P(iKH>s?ghzQ^huy z(gg%$H#$Yb9~a;ouYJm(2aP*RMA#C5f4iMtlP_y!fcqR{LOfUrN25p6gC*Y>QXXW& zTw$^@c-WcijO715n>;{ViWaaT&xg7>o=9@$L?Y);>~d0Pags6ZVGF(fBc7d}&^EX?=NmW7Kvv{sntJiAxMxzIeU z?fOip{{Tl`oUn?6Kw=}M*Pf*6WiEvq7p~fedMv$z7&mzgdK4IPS$chK8tRReX!!$B zZ5@DA2_eq^dQnt?3k`#!b#x$*1)^#FMJU&s(bp}7s-{5 zu7m%B75#R`sH!5BZmDB~`40O&*b##~^|!bm?NXQNe*FjJPFJfRAvLd{%p>SL| z9m*v7V&vMO(h~I~0c=jKvnMJ72K7zlAn46gX$i&jN>yaNf-YWM!0J2>UGy)$F3PE} zD4!=6U|AlT8!^EW>H3JDCramY6}p_ZHM9$qYoMjm$)!Z1lwJXX;^(F)#wAPfke@h=rQtr-_nHcWY}X7#dboDJ6M~wNa~)<*Fk20blqSQ0 zw;E7N+NIf0pvg!$Do#=Z2G#mlM#$c6AX$&V1!7P}rXd+V-PAR{!60O^udG=HVX+iB zsm)eu9UxLgIi2qpG;So&InhSV{G4T^b8RPLn-M6`8X{up76rzZ`|DS$k&%cIvBsIu zn+glYFzHtU%HT0*2dBXp{Aj$CG{`0IJqrLFL`Ih6&Ky(vld0k(I)ve+iT}vA!)(~! z!B1h;`xX_To5q{iXdZhmf4K&=wAo-Sd*j-jNgbJ(x;cT;C9xtC8CM(Z5R_uUSqZ_b zW{Oz-tWpCz8FWUD!1O&o+;+zRYD)|U9qYc@WH$&g;*EH<3*w-?3}UQJbQXwPPc|co zOG`uNWPgSmmH3T3TgIkSgAAtVRS2;zk|oy)%; zyZNueQ#m?J&>=?*Nv*o5mm~~5?;yB7FqH z3773_4~7 zNxz!S3r;_$(g37toij`qxvOeF(jrEy46d%ei|TBSC;OW+Tm&VZT$-hOUx^DP&)^_O zb%C3ZT_g<+$#D~>Fa$gh#wY+ZhXK$*0R9vRq#zJK-~b#5IwXJvoPr+I1Ayue z2n2w#T!Q%iHUcSG0sX;I0Ho1>D}M`0ygvY3TlAmG-zAVnaOc0Z|MzR4!8{!*D837l zpZFIg`M-Mpr6UJ`GG-h}Kwb|N5TttdTEuYfpV!!5X9vnh2$gyJoK<&v%k$ZA*szQV ziE=tOrLmmtPk!xeQ&Abi_x)!&G&~(B|F7OH-gm{-CfTL<->c#lM0YD_#xbVJh%J-k zO;Nv6ZYDczhfoVMo&`|)Tpw7Uv?LY|h?yx$$~XP!-@^FGkpa0Hh_lXKR1c&ASA_uE zzTOh>;og60FAEa#Qok7H&*f#h$<4X1j$z0io`$hw(jyB=6iw755U9`I7o7wpPymoZ zk$6vp1iKyEt9K1Q>GnbPb4Ns&_=u;VMCOxmpJ86nk-`K^&)u+w@0zgyDalO3qq8bI zZO{XQTqp)jYyl);aDs3*`*QRFsfOTGShC@BV&!5n$`l#z4pzOUhz;mVV-p|i+2>)v z#FW8y+^INVAcQRiAn;{sT8Z~;#sJbdhKx}g$QV%(dRSi`#g&{GX-dmNNPkd{CMDNo z?>?M0M|w_{l2cZbkwn|NxSf`LyO5k7|3#L)$@9cc9)}06J*c#pt^89%lerxg&Pw>k zwV(qS^d%1W5wVYK9{~zV#;SLE4;By_z)I=m>z02riq<^6)gciGEG`g>5J&IjQcRbl z($GmsU1yvYdWnEcEy$%yd@EQNHI*UPF0R-+q`M35ch)(o9TG_mQgI-I&w^!FUIk&w zQQ+WMBaVdB;^OREkA6H(r;rrVjLW!13_r}ydse2d%1V{_bVw_>oH|^!S#%5U7G6Z= zm(I_W2|oSo_m{#COi1pp3p@sZ`}^+*ps{Z-DIsSIJ|hpt^h3#v-rzj)>?Fz;9&OdD*^5B)&O`6ZIRzdzn@f7Gpvaem%|^?g9Xl z0#K`HzlgC(N=G-uARf~n;&_yAELx{bzO+KP&71`~nRzsS4F_D?AiKt;Z>$*wAR2ta zZaVH+s!y;`d%r+FgSj1jIB~>-Emoe5S&*TC*u^#JiD@jJX0G<2Z5m-EjVTdh#QF|I zT69b*jtT;l;m}zfdmIVOeh=_Gw`v3NJ0dzp-tACR@+qur;xnn3lcoo#5TpA~#8qNE zK{0~9+@XsvFDD7r5sea~JMmPwIYpvkZ~(d%;C|;jG00Ns|81!t#P&b2Nx=WMR1}b< z?vniV#juFXDgUEs_wVF~xv;Q>r=%~D{JnT#Vec@*1(XgZg;Q^t5X^>For4^N~0q@5dVDt>pB$n?KH{6pco+5UJ1{Bgs2Fb68=TF29FWTjAPRUO; zpTBqFxzXw-YaeI5c!?N6uMDreaR2_`NDih(y{kCSVYZk1DInatu>3|7sA2x7oKhle zHe6y>UI}9&SQY*-XZmUW*UdVU=~-FQm(A?{>3PMEj+rIQLVDK7=~qH)`Ia!xQ~4!7 z!H+HwCva$+8_(m?Z?Lpxp4X%jTeB9syQi0y-@e{R3bm>^mMrv`jyPMd#}#NK!@F?9 z>|RGcVibsK6hb3s58!+=P8QsWze}5B>b@va@ktuEXP+v3Z|{B=@Ou9cir0kKqhphW zOygY7UbdB~XRzi+hFRk|(&;DR#bQLAIjj(!Bc~>XSK%hmx9o3K6p9)Mp>G5ZC4~O^ z#dl&P27xqSKe%j{F=&fQ90zjr`o$q_+vg$n800}vSlH@;yOK8stZ{%r(-dvKZo8RC zkf?f&O;ZVXmy|kpvRC`Da*DZF(bnHKrjr4Bs z_m9%ZBub3P0@s~I0FW>tQH_u2F(ggpeaf&o#_Gk~9?jwps#z0Mz}XZ$~jXs!C zu(2e3()^Ovjq68R$w2eR*z6b0XlkP%W0-aPGgt@TZxCeW^rLHcgk?H~bp+}LcH{;3 z@fa9jr<_N5zbofOwCvYDeBd^#x4#RQ=-}A|v)c)veeb5DZAz`IJ6Q{{bAsBFaLk zP(4Qr%FJE|!~LB%T;ynovU4`MB~n~a2~YLT-0VHc}{*K&^1uX3?t#l zA0HteXHO2I?dA`_5=$ZX3W%#(OP95OQ{~@>7?!(`Z3&s_Q3_?`4^-~TWI%kft@~Nn zBzMC|!sxQ@dtQNtUGcrr8s8xMtrr<5d<&N2w`&-<_GJpjsU~dUPkJUy5fN3>8JS`D zUGjK!^SaOK#SI8S!8yW5$t)DHLz)$-chRn=WbwP|kY9I>>x_B(5697*jSwkPJDZ)N zw6z7K%In$K^y7~iI9ujAGF^XY&+}tVAL2%_IoeN|-dJz-9AU5fYX{-N5yfYGbD!8aw<< zQK{v{z_O?n;$WL{X%_Nt9|cQ1L*~jT*^s_Kykgkc#32hf1hWbYo%* zUXVayW=I07yLg82>6^jTB@!@6HVW*IQW{hxC5jyqkd5&x;%I1h_SXB?hjWxz8hNa``z&G} z`aC?4bcqyAmclH}f ze!guBkdMOl-#3Se?jJ1`=OlLOT-Ilnkj|(q?n3ZyWzO|Q7rZ`Es0Rkp*?#+tOYZWL zZJaK#*(n|MTOft5ZoS!RdWf#)$Kbj42>XbN)tzhTVY-NIEUKzt_n277OE9R zVTZW{{$82V-=$yLz74QTqgVE7J79gSXsR-ePYR@y+A(3TN^^%e2j3aCZnN87ktzz{ zqQHQQ)tlRLjz&Sv`IQjCS)~AW66`j=<}g3%U$!6tg?^h5+G<{2{_fA~X_o37QMK{q z6G8mP>-S;>Y9jD2hlGP}4<@PDAyRs9O0qo;V@=v_c{q}v%KF)_xI$PFv(8od$lDS+ z&Rqb<-9HYvyit;L56^Ddv>Ja<76el>v3J}tA)XRV(59x5Lt~wni4PPdLt(KCM`c?k zfx&ESG7E^mz05Gf0JN~n#nY2#eJ%8AQQ`N!u3ZZo%!fsr=&+F3Qz@bMZCj>kc#`)H zjy`qCRjAZ*|JvxRKdVixAaOU1ik3&a4JY|o)Lpgy9_cL63@gZHk~5W6<; zVNQ#*O=~0;w%9a$WNMrM>^7j}TL`u~kBxh$LdrD*g301 zEV_aWEV@{Da?5gw-Bc9OLIMLlP~10RcHB$U-!!m5Y?oH2lcQcIx$D_Utb#*Fy<(hy zr{p^AXRx`84K|bN_8kB3zyGo0H0JR>%rJ*>CVwc9Af}X6J2lV;v zi!FO?pV&JSkDyLqYTd4H<{Z6{63TZwL-v!S zLtNf9)GgDH83>LkE=aCb%$3Y@%xfa(%TNa8-~`8x@QCO`OhSeWEMC>4bc{}LkD2dN zz<46}jiqtFT2_c-leug1mo5X+${S{)I?}psVWYrdl#joE7XIey*TRxW# zN6bu#(pFJN5Zi+Oj9#SVP4X#nR6{CIOa!UU-=BZ`z!);L)_eHieZWTzn4aNsVl&(J zbmZ)I=`MJ4SSAg9LxfGz?EiA_?nQ4i(4Ue>|P%RZE7pI zQgbFuNy>(aCW~)Ed!a5W1CaYc(XQV8T8vCpUXSA_&5GHETSZV7o~6Dd*mA$%We#y) zhI*t#x{HREU4JkT@d`aO_mk$Fj#FA+GFB;iHu2GK+;kY?mSG9Rnl-U>FNFEfV^du$ zq^rK3G2lXb44+cWG3zjx1KE9uZz(vCZcFJy44I@tG@7#TYySXxtee`0@q!5!F=NfF z7I5zJi3Y7<6yW0rbOb-0`&?;`6PVcA&*RL5A&BcXX&=J1tzV!aHh;@>yFsdi*<^T1 zYU<&qgd*1$?~TYziQeyf!fuUtePZRl8$1iv{&~8XI%lk$uh@LE7D|Kk2)x5Xdlm3} ziqSK?n%G7jWIYj?mT##a5ILh-xaIZiZC(#|^cifz3>;5N=?(xJ<{ck7Fu-<>j5;qt z@)Y7329iypL6epD12u{wci&(|WS}%`UV*$imxd)y`j(RehICS+q`CGFiQt2+Ty~NO zDSU=kDSVP>gAM6QVxmovY-^Nzj_N2fb-Sm|T$WK|Maq%B_leG@r=x=OlI9mHbgX+vJ{Xo>m z_NCpwItafp_?ucPmHf2L`BP%axGjSH*NhUE;!m8{#q zrn%qy?p9<1iy09Ds2L?f`~s)?03?*QlfL1BUh*p=TD~fZwy~}xi#Sv7K?wO6T=vG= z{WuRw!k^qnI^VR$>G*h=BTwnSWi-rhp!8gTn##|ECxweG1QM#$i)zsH z1z|c;R^1c#i)6^ai|};=C7B)^Fv6vG0Ju^mZdpql0B4Qo`qI5P}A_cNLHOneVoM+PyXv zdYD-*+3L&H!olZ;ZB9i^W?Tf`U2;>n+NMCMfrZp}vOV}xml1bKX2RxQ^Nc16b&8~k z+g+*!4Oq|0&cib3ZzeyC~Rq&F8(_NegShp zQH3DL4USSAB`2Mf{K~GhYNr6kB%;nHjZ@!(g<;mO=u4w4ec%o`4t3HZ0CJ&;VioW( zDEacH`txbEVS_4M(q`C7bpKcwLcPWV;PIBzalIhD&g>VF1RJROUu`2Yrq4g^f#7&Y zQ$odVDGw9YNbILVx(`5AH& z5Sfwn8xD~8L!*Lb1w%)+C!K8Rb(mj)OQPq&;cOvNz}jWuS1rv5Hd5zIl{oy8oeXn#JBZp8`6AbmLQqXqX>JiP&EMywS#r(ll)m3_i;J7?NjY z0IzO7G*DBmNGuV^q70~6p^M$#gJ9_$Vhl!;(V#pC_h%r7ADrQr<(FL1)Hyw^{eT<1 zeQBj3Yx5DneD=3_%%DoW zA4jL$TOn(PCaC)E8b_uz;T8&z-4~ui7cmrKy92_}qB?zVo9%NT-9V_=G|HftZ{14H zc0sKHdwq!=Q1Z}Jl=#YN(7E(2?nixnU>~5v#K?k%sL&tMIgDkUu?;A{7Dk=@~U_-iRRLlMenkuP)}ibSvw5D+w||zq9SE2Vq-WzqPgn z9CvQ#1^Ui~k5z{tZ*1aHRir&3>fr)#Am#Iin}1#x+Vwb9(LMu@g+GMxr>l~BXDCsI z%?JCe5%URo$~i0TCQ7!7(l~+fsFC13M>aRQR$1v;)Mpz_DqatW41XN)Kq*96NQzWt zmP`%&52i@)k5ZCTbg^^iXj)SF^6u6M8q)F4YsYc&aZ0v4*;WT5B`ro*QhCqThmR<< zy}w+tT@b^XQ>mw{^n&Hf85Jj;>gI7LPLBgH-3!_y;K(PulXL1w64rD3tPkF7KQR@XQfMa zyGuL6nF%z~$&LOE(#!aEP14?SED*aP8C*-0J}I(Pt35Ki|GC3ug2rrnWOq&FobrBg zCmqUS^QC4C0Jsft?59R9#A>9Cx2g`u8Av(KhZR7ml5YwG4oQw-wrUOuAG3j%NiX8L z*7aL1RFLHFm=IVRsgA&vKcGWCU4Tu@rCCq&VEb(~PQ1Z%XfT&(eyu%uTjUX!{ifCe zN>8ZH2G6~T8Yq3)os<=!e(LjPjTpaI4pvX_3|;F>m`p{>J@`o~PhQ35 zk2wul3?9S@nXOHbDH8#y4kwEa23bli_X9Nizv~4dSXbf9>`a|qoXm`D|E}2^e+9Yi zx!L}k6)A6KXW?Q=!phAK+NR1ZWo6@H=EN*zW8`8cVP;})YWCmsMfdC}J-IjvUb~*B z+VVT_MSRa82ulj(IEEiR-v#}V;qz>2!G4&SJT>@j@5vR5sY;knyL>AOIGv0JMv&2- zl1tK+)?S`m8GO8O2ns40{8+%-T)v>SrS4CIe>VC7WI~XVAmz^5*`b(dArpJ3G&A3B=zuh)y zW?@2tyIXIs)#~u|&qbQwi_`nzL|)I^3G8S%tG8a>TZbvxGN&H~Uc`XkM;LR6darMc z676&C-9oLBU6cri?`vL}-*4mIKQ9Mqh|qd9ss`M*}hEv75itUNg5AtI+Dj~4=5AeM2VQc4nKG0mb1v(+K6&(y- zuy&K~elLeyc~!M%!yM!qud(4;O*2gUVX?cu!Kv@VINyN|dl>iUx$4W_!s{Qw_Y)y1 zbI0PcgHi^7IT_;SR3A^8#%KVS0Q0R|h=FmNK$$)1pP0TqsBiRte5Q!?OtGF}F&U5r zS~>l)YSJFEAY=#4Xd;>p3UHky&_+8QC_$>w?C;q+xxxhqHSJ%FZ}>mrg^4FR37h@^ zaA%&~HPfK7ZaK*CBG(&KZ=?@kVtjih2Lh3Qt>oM1sa`g-4_ucPzn&dCVrXqF$@?k% zUO)7JBK$UZ^mgQ7Z8fb1VdvHP983nc-8DhZlN@^TU1lF*WzQfEcY~C@%j8e7Y+qBg zVLqM+H(NHXZ%{7>o;qzZ)cjefD&@3m`4k_F*!}c=H9YL$qwnM%CnvjD1ydo`8vE+> zS$06)uF#cxIUAx#tpkpQTHn1vD5~s*{oDo8Z(qqOcUnj^GoHkarZ4ydalbc69Yh`3U_s9+C!VXp{D8-GGC7Up^0Fn4;v7C4aCo-@S)ozBXP`Snl0be z$LB@Fr%^hhzSKAax0)u~8Q!)MfEXGNie!hzvBeQER3%Jtd`(56b-by0kMpqg8N}Vo zX;2WUUdJ(xB{7?%h)C_BF;1-t(R?`jh~RkTQRswi;5|6P8#nJp-iZpZ-1sGA!5R24 zA3YC>IWu3hmFO;!B0&t08BV$yjd7M=(>^$}bp8o=zt$;DJB?wc`vjeVzbK40%;Jq7 zd{{|&rFc>!Bk>RVNr1x%z6iDuyZoE}wsL!D&FFEbl6gr#zJYTi2X;w5-uUvQFY(o? zzLlgbyd^Q-D5hl*gm;+i@`n(DjfZ3g0)t4OD{Ko@*`LFw@U=CaL(GsHUYQHKig)nh zO;kRwJU_3?&FKKCKIUKX!7HVIqgKmLy#C-jcZt1U!PoO0%(8mH*9@>;sqIRM+K5eq zeX}3A^o$}%W7 zjiQmn8jB7_`(94{&(=S90o*p2{F-`HpPTFR_h ziweI~^)#@P(6FaFU|26eVJaI9;DNA$ibq+#g_U{=3|SHwcZ0(A07E7W(g1UTbmGd< z%cYV>wug>QraM6|n0E$395o)R|C-Ryd>81~Ro&qHoMgtz=0RY5!zqeygGfI$2kE*i z3gF_t9`M8Mvl%@F-8y%2#QFAs0Mb7Fwc^+Ak`a)hdX;RNoDxo>f*?0pTW34B{lSAf zX_Hq-UVIDg!4p*Sq1(D_WP)ffS)W%>W(0fw76A&3^_xFb7KxuBq9o}7E z1*aqDj$&Agv{2aQ#m-a|{=UE)h41?ct=RlHq z1^oLi7SgEO;hP|~Kalxx|Ca%TW|zuj+KwZ^aIJBtOeoAZGSg|xMN+-qv>o0D`6ri) z71;#>4fAjiQ=An%i0^RNh(Ul71$ZBC{B4q@Kb^Rk7Hti#?#@|;EvG$1Def@N-1bN^ zZzoM8$n5gl=&=)OqWj(te(ZnPYW4i#aG9`4Fa<{(GJ-_f=1kW|wGPUJWP%_!xYI{l zpOuTe{s<`Nu*dk}<~<_M{t<8n-TDr_TI%{}bR1lqaE@KSKhS36##NU9=cMC^Yp)Nd zZa_G6=JoTQ%iJGco$J80;nm%!Pq6RIA|Tyt1Mzjf-rFLizDHX31H9_Z%IeX+mq?wf zdoz5I&(zPqdaK~E_PTk(#Y_l85Un`-^GYi2pC2e;i{vWpx>@JD(EPo{+FSyw!)Yi4f$1{jufxZb4Aq3r zQ$IyFS?Hkm>SL_tJ#-LDU~K-hoV%!I3=$9E7EQPU^_E9D2-@gmSc!gu_0uI9iVtY| zmzW=U*kJ5d;j&qdh^B)S8o-I}7>k(yus{@fsT^fVGrVBOiUg@);1 z!$u;ACSm=Q`o=r$M;X_q`gi)eq2&C_9j>&S@Ll@4d|@M=U_H^7(A77_9=*@m_Oyq} zQ5RP%TvfryyTI+1yTOC8NlTF=#8?9kXzJkY^AL_}@;+W;y%w~Rkekp+x zp6(2RS=)UtCkw?xFy^IG-xODPRCQwhz6fmLL-GXY^R^-FTIGPH__e4xtY#T5^=mES zk<9vi)-!;8sD%pukcg4I%qVbU3 zdK%_>nO~ugJZqBKlt#_nW1*eElULrmq|*2aqQY0av^#aPKexKbNHTeS zrMdl;781$VUpBdrUs50IwUHLJ*}qSuau9amA7h`^k3?f>64nM`=RvXUoh%#R=PNWN z9JMiS+S@%Ii!4(&c}ZY*Fqztot_cdq0mg+)*Y;pVnHkWZuU_3>Xa1x&{~dwwp0iXB9hhS$^AtAr>w_OE68F5Ek6`s%NQ)EP!e4;Qanj0f zkuKd?%7`asF91p%dQGCX^=#G$W5iQ`66aD`Yw0EQjp*Ya*mLWIslU#tx8T=sf)ZFX z`aVfm$maa0yLpWqw1Cbk+t^qWoz}<5`j@4j9NnGhwKekt^)F==x+Xe4${p?kfm0Td zUGyhJ${k_RhJ1ojDkCZ@ubvq1#lJYxB+4A^!j|?ONsJ%;><@8oAFA!A@&PeK8tsyk ze)W#A*q;J$%K@DPd-gJ0W=o&+i#C&|Bi4Nx^FpQG2%*>5oAkj%%3Z_oSLk(PEQo|2 zi{F8}4~3e&IPxYThv4!=r;J8F^qn@qGO`peOzWHV3mfad#i!$|X{47gzSMSt1AuMx zCoPd8I+hN~2tR=c-N;-fT1P#?#$sl%BOFn-10!+*dLUnciLTb~wj~4>p7)iz+>Y>; z@rum`Txf3i{gyP|(a_8-CsTe<*xjlcr&`p#cZam!eUMF`Xpgp-T*l0$ZLy`A))lzZ z_++*~SCV{axB71Fx}&%%mF`r}O2b<&P)#6kkz_aJ;}yvUeQYwbyLGM`e4vRu=z4D1 zJsR{?1$ZUeLpDR=2E=x_3Dr7a>Ryl$AV(^MO*n6*vIpspoMP9zz=m@;%>pQe3sKl9 z-D8*AL}KyQ+<9QFF31pRH(G;O0O(|M*rfXpl7{$16%Hmx?&s^RER25c$=44erW;I- z+4i#TfMLSLmY3fW!_C-;iBA0j$dKG(!wog~U~b%vsun}GXG^fSj~)TLehlkY#2q@% zZcOm*{l5&=;-mr`eVegkGWi)zvOOG=C|5a!`-c#{X|ejV5fiWX-E67DoN#pFujw@3 zzRYlFULUYgKqV7#gy{NJHUFu(XPm ztBZcgnyd(`1t8^2x~59>lEA{HI!9Y1X7$mqsna=~`sWyNK;;#F&oS`EP8O4+Z&)W0 zQJ?s5(ENPUu2Uz)u^F^7FoL*{YgLAw?H4h|pj-s2E*$T$IKTz!9f?@jO2>AqrW&t}( z=QJ-_&O95ocA<;5Qk*G~u-@wcq#rv7{IAn2>y&W3jaUjKgO$V~p*fV3^=v=ME}R^? z*^a%YbWRD~!c&0;8nvzOVeQ9n@#2%tsDx~ORq$=kWyz>BA*f%=Ozb51fcV7JVs||l z&H!V7Sus}p>adr`;pSP%0B8&QRr}`V!|(JMov)h2;hgnf{%bV(X^l)Q!g0~%z|E*v z9LR26nMixg^kG6z4F^mgF(ijj8A?D&-KF#2lKTuDnEQ!v^sm)*7=n3fmCs;Tuwd9G z!Gq^!6Bv1<*D3DP^kYjwTuwv(ZLl_RC442lqtcW%OHDTWHhm+)uZN0Z4=p9VyTN(> zdCfp81x(*oILx@C;K*RnH%pDumG}yAzL0z=kJb{!r?)Kv^AyOKGUezl-zFVWJgQf3 zYU(i>h0e{xTg0?l=uk{KR#mn6siJz`g@s|~1C8fMuVG>xc`1x(M~L|twX9XgG(?!N zrexUQ*xwxe0bvy$>%xL=G)1bpia~u#UNg=WEf2R`f63(@V4533c(L2^&feCBChF!1 zFcQ(Th~=4U&|L33%k#X#aG$5ad<8}3IRB|o)DkyT{irtRRL@(Y9%rHc%j`$bj3uq{ z@N^_omn6ZM{=QhR*83Vd!NCtY4-S{HFi14c&x9rJC|eCRYhq?6emo8NiOreshkTnT zmB0^L8}!8)95*ZVaACIhmVmq}-OrQX#oTAyoxEPqO&q`Y(p#@|4s@FHRk_gc+|RWj zb}t&|;^J%tX(azl1KJyaWPm^BO@W@qbEA;cJ!5)hE4evM7HU$e`aS|t8;alqlfqb$i`-9 zTkADx$ntlw!PJat$0%H{*LZxgqlmVeE7kcglypskgujK8{6txHe=~dvIF}mhMg`& zo?S4c=hhTqZrxH%8E$QwhyW@3aIH^2*1955eX!y~^;1(TOn%A^8n>_0YC4oM!cHNe z@pW#5*L;d1jaITX*$Te(EF(0lQj6n%8ZAJ#NA|7?j}j!1*(Si7L6XU^<9|T9wdBr5OJ!Q0fU{|7R1_@s z&l76XN><`;a{dr4Y8ft}nf_D$7kPcltPQhJ2dWieDHV}m_*obDLp#n;I6ccbEoE;w zbUL4FOAI4w7CVK`kBu=|K!pbsXXFY`x?tjvjj!z`&OYNuyyV?%0J~$4KGdQ>c~LJ} zsJx51r7UP~f~#{h0Fu20+6cy(pJ+P4Z0g%@NC}Q@Nq)`mt&IeP`1Dd1!&YujIAtxT zxkYY#DVVgBZ90ktbcsHRk(+CLD>tVj8}6mxKr46UF4MWy@*7hCX}I0fK(8g@zF>r* zz?qJRjAJW=Ca2tlcp)(ot7A+ zkuOYgqCUca@l1SL%gQQ>fpiHO3Iq{>8tI(&73F_w=7U~nFdY+@@O0{|O9hrf- zBh&N3li@>7V=UdbnyyD`&Q}7-m+Z7@BhT|br>Ck@%3DtoT=Gzt=0i2NT6-A6E!K-y zGqEb`5NBRJFYdE=$oZX0dAh^&NyeB`4@_04ECTL27inyAHvj^{AqP)W11>L7B z7L^UDwIX%lz7=47x8lp&fsQSl-#!>288L_M>R)9}Y1S`1^R=Fmi%sopVY!ohf>5ZBx{FyH(R9iEYiZJciWs{K)vZH^N9bi{meocD zjZlU=t86Al^A`$eKoXmeD)Rxs7Dco8pcPQ(Gxi$4&UPSe>x&?kyT zU%e-k9!#*E3$Q91K#pV9Y*NC4fKqh}8Jv-U|w z?CAH0S=4h7Fk}1~J0ntF_PVKCBa8L8j&UM{DR@wW(DPd>JCx8nh94PTlE1;!=3!3t zL!wLLoQy+PY>RVP?2@arU0Guc#T#1vkX4t>BKC(DyEQB&il~&hc zsE+S=__VQO5@u?zz8+|G;r#9%{S${Khkh6qDLG+7ZF9y>@0n@RS2$dH%I`2?!=;7O zQCV@%k|TT0n%G!HY=ah0P(DZ8xC&)SbB}Pjse@VA>Y zd>Q4kb06IXfU1rs(u8rN#!5h8;#~NVy>e!aGPr7DWySglAbY9H9T8mgDD2X zA-!n9qn=wCibg2%>*{>9>Dnf1gA<>%Bo%2_NM>z#mJ#}Si(<(#Y=2ZnAXCJ2F?1ukS3^vq{)s=a2)dK-8K++l zO&ZW`w~DfiO5+f`#39yJbH^VZ3uWZ6{2Mgik}nsTmie5H;W>AW*9 zgwsGg#BSSa{;?Cc$}_Bp!5QI}`V4jJ@-#SWuaqF{8E#iHpwcQK+#yz5g5!=g)2CZsV*v`>51; zv|wsjsyqi#`-g%$E0*7O#5Rb2Vkpj92dcunCidNub~4xo?x+Dw4@0_)a25AVKyB)!j#h&lN(b39^Y z{?1B?djiJ7?9e3oPxpC;ZxYdi3Fnh)E`QZPv{KWtz!i8_a}ylfk8y_|AT>3wDwg0G z3-mjEWMmmEihvb5ToALpw>lfAmfzM$8r@{GgJ_!y*x@-tT2lK0OA3O9C5;`?Pqh!3 z)?;JpC$5z$PSUo*|FBr*N`RH`r(xwp*uxQijMk;5%oGs=D8~t6lMwlW>RmK%z8CH7 zsc+f+=tJ?P=89f6A)E0u=jBBAb=q$!32^CTSF=wqmDyO`6^dn__9RUj8IT&g`4j8|7E=HBXLbTA1`EiHvN^6waHT zl|Klvw@Wx*fNQ>DiGdM4BqGhX{RGXG*ys+J|KRsuV%V8AXF&76YIFrxkG!bx-lJ_9d&YoCE`v;jSVsg9QY>S2g=((_9nRh~z9i3bKJ zgnximiZ{i5;2?H_^hWrBWcF4USnP9qy9J!1?31z21}9985Bp>RnpReGs`au8mvPfMvl(w~o9ZAJ*lt;Q zluPXP#lB^*sPRKP$9rp>e5Em`z9Y8VGvTC*GkpjV6tX=0N1pm0PWm6pnv08z?cek@ zH}}8kYgI2tGbT+1V{0=LS0)uVW7mJqfp$g~W=vo4f&UQO%*@10(q>i`mafFCoWx9` zR<16JX3k;`c8(7AX7;Yc+{8@(;=p;n+?0uxVgCaO&Lrkw>)@>7Xk=o>ByQ$zWn!i* zB?|jLQ{$KEFAnI(I3s@pK2<9|3Y4?eMXeS;l~+O3qyv$&P!nVp6bA^m2EeIWX-SE9 zvIa6_%Lj)l0>iLPm>4MsJq0>lW-e;G9~sXdUJbiB1#cp6Kv-5NIi=d?uI>=ikSn#5 zl77qhy<6Y^dCuMR61w}|Gs9-^orCC(h3M|47lVw9Oh7;&h!`CclQ!zM(BW8OcyjWa zhK2?+cFd6Rv*+E>)Y6hTH$EdZHMi%ru(`Qp<&%Mtp#O$8`tdQ^ua^+| z8KsaK6wcJ;`P#t6#pUi+P@L1u%#JM+Fak=i7`=)3O?{H3L4?D7hg1fV+HqiZx7Y=b zvnx3Nvo-Tx5BKEH8H{@99@bvsU81Ahpih?yt?ctV?eVVX*8YQc0~0WfTq}V*zH+>f z+A?cznL{nvR3gC)1TA0T;l)i0#ZB+w#TAHZrlpFm8&(Cbx7=4&N!uqclT{i;8V{#e zyLWJK5DI5NL;o(WG&m40#GaT?Eh(?1!cbg3;t0raD5r4iU3DB=1`qyPXR#F^E-&pkgU8s>&}%h zDjls;$UsMK^w_1vWht`6yW^Sg{z}Yd>&nUxwV{sK68}gb=?ok`XM`SIWl*GycYu5s zxCjP_+!Kv3h)mANlou`dY%om9{@n{PKXA`UQVAjNnKpCmTS$4|*~u)~ijTMVYk^q! z?cH5syjoyoVIi7&)v7Uvj>?oUI76a{)+7#7_l2ih#eOVC++nW^qyIq9z1v9AvTfBi zsFd!rF7jrFi-n^jGiDVORE#+3*4Eb2TAE>xb3sD`NWY`Rk>jGOhFbcC4ect?K$@9^ zGKbbY21c_gK*fw%B|$eRFv6@PfK4%CLeQaWcdKN=?d1|0&z5}4pNvPC>~uEm1vI7fMIZ&3gL3A5Dm;( zbdrZO2Awe4c-?R==e+U8M*o%Hlt#?DDVFX!N3w9OEP@KL_XfS77VE8c=gWI{UrT;( zzFemeQ+O7mD{|BAjsWD3GiK-a&D4B&xPN*AHT2*@Ilr)lV%Y(uD~B6{wpv^O|L_BK zarQ5!E>qeO35LkgBX@ax;SghnUFBnf=BIuk3ZJQWl2J<)hH;HVA{PlL6OOzlg;7(} zaBLaO7tm4H^xQb46`5$ZUK|(jaVDPL+d~6{u0dHDagU|g#w|M7y?(}j-VYF2yqk&) zD;W4)S@e2(^fS%-f6~2G$ueNhf4xZZAZ9NvF5EEZ=edrJy{)aC_g)8&^Vu}__l3FG z*}0ilkOCi2YY2Z^Us*cX*+5Xko0j*}qE`lRK|)KnBpe=cf**2om4Bvn2=aJj3Y zxSaXEGM3X$3K}R`ZAy3u?;i39@I#aWt&*t)T?c2RWLuC)5emIUv{B9U;;U6NQpGKy za-dFb#DEY*Q3Ru~uA|-fUDDZJ)E7KVDpCT?(PN# z_V@G%wqv3uyr|(<!+Yau>-NS3r@8 zjvP{~Q^Aal%7r2u-Px0#)|h@7^{uI>frOM3!JU`}NQ)j>o>Z&X=r;JioWHzy<>v~3 z#qNsuzMh`m-+RO`qW^Xj*J96tT1sKnabL=q1b_~&hPW9WpV?}n3>3_y)xpb)6(*S z+Nda~3YtJQdllfoeXsjsVmwF)&qR+|6CbYK_ZL?@r&wN<_sPkjj4>y)qZdIp{es($ z!jXtbg=U%i5kVs_M;e*DTQo5Q)u z0)$B76f+Q-BH926!9n<(W4fEtAag>V63|>yRJvMF(19P6VC)nbyy4p;0gK@SE%h85 zGm&VaK2Lw9Mn_jVJ!TTgAUZla+Z<-5r$5;zPFmh=b!io!(7Jge7MF*(vMo|+<7QrcGKG1K8maBuO(=H~mp z!0#)f^3&68%kP1Pg~XZ+oJ&3C=H|q~pR&3NkUK@}kijj#dlc$^mDg6rE^@iIdbqPA zaan3~^1Ui5WIo9K0249#9@qPM!NSG{Rws;^l7hZZd(*FcE z{{>6M#Z>>Votgi_t?V3}|L25E^Z`(y0VV)bFc4AzC<+J|3dom`ndr;J+0`c-vhu=AW**o=^)XRjG#!J&{@AU z&(LI|4gDC(v)ANo#?C=7u$WlbIJgv)RMa%IU(3YB&BH4uE+Hu;4U|z)Ra4i{)Y3LF zH8Z!cw6b<_b#wRd^zsf42@MO6h>S`~PDxEm&&bRwEGjN3Ei3;52Tjc_t!?ccKYtAj z4h@ftj*ZXFFDx!CudJ@^?(H8O9vz>Yp55Hu-9J1&J-@vEQy0iTRt*0={$pML1Xh08 zB!YoM{ZkhRsK-B!@+jaCBrK4qB1%w3PH3d8fzarp|ERfP$k>#xF^rvOVKK?scPVcE zDeb>1`+rtg(En0p|94^kuIs;}aZ$if078IIOnsxr0FvQr*bd*_MK7fwwhL(dc%>nh z%QP)XGMr>5Ta=i6Fm9ZYcW|`C1ZSso@)@FOKXVEoI4C3^$S?p5`$P&KB&q}AguHey zW+Ul`8xMb8_p9r$Ja_*sJ}z)XM{@dUU8+upvKeSu(|SIov^GUq<1g^@@!_P#Rl>J) zj9`o$WS&5JXIJ<#{~Opg564W}o2v7-6%a{C$vGeyRT;lG+A_I@$amIh3{BLk09mJ; zOPrZGYLO>Bef1()d`k z#Yn>64;8kVgoF#@a+>Qg$Gyw{j7Ct6+Suo9-NoH-tG}O|-^9D#MOM08!)k^mZ+H~VR zr6#S-Oj#~Ht}<}(kP+yuwN{kaPsn!L&3|Wb`DPHABf8t;%*+FRkWe7yvYnh9<7gu@ zF9B+v<#A^~UTtcG;T>j+Oz!A4LhQ|Qpfq=Xhx$=!wn?9H>&()~mY)svoZWEIjYO`# zlG_iK1Q?zlBjZo_{`U5r#bA*T^BEXi5cJE53w4(ya?pfk04?pXJ%VQU`q`?4SF=lw z#;2u0&3I9powm+QGC8IUD#A+fwjra#MJA5Ww;{e$8Q6SCVZNY$Y9M>E+19u;Q7-W! ztsDXt7vTD$k=M-<0}@{lsSCfePcM}I!1EVC8RqJc!9{N8)(>^tcbc5X5@V>Qyka!7 zd~^EZ_GPEJd~c81*l+CkP-OEZ+u2c3vUBGSq`AYn5Nmr9gZTrbhP-cRv*Asqh%5ZdG~bN`LbAAeFKc6nIs3rEX%EN)049B_XyR8<;K$cq0v&Q|z8{Dnh zxxu*3s8lJ498j+aA5^Tu!cDPJ$}UsS*?Uk4*{2?JwaWjNm82Xk2c*neEv?PEwNd;F z5XwbY(uCh^%O8_~mj_{J=rlz6ZEARb%7rDr|5TiqHv$b@P~uE`)!*{pVxKG>LF9k{0%LoP|Tl|Nb@JP5Lcxm&g^ny-lMh$L7)ILIwXKYo$gEB0gb0xIc-t)=6 z^EJxVC0?A{zYkqft~Z)IT#xp8f6XN+%yux5C&5w|_M!`*iF-*nLTZTsOpc6^gn0B$QqEY=DBpD)R#eqdQw_M-8iQ@@ z@!O#~tBYgG7G-N7=jCGw7u9X#-E3^Ktd8}2tL&`Zsb2{#%^nX9p@byMu%h%1YPsnK zGRp~^on6Oya9qmx9VEJvY+!S4AF?pq^nUIt8JIm50KcBcD8Q=^6yD?XQl&zrk5(X_ zXr8D>vJ!hg=R-;;mwza(*Rj6oqcaHeK0qBU592!gl9{;3F{Vk}QPO&#td*muU;}RU>Kc5hT28$trWGsOZnu?(nn56ms-^cE zIMy55Kr47Gb7?*MZ-pGdf;3&xb)xA&ayFfq`r9?nuQtvU;)6OF^r@hk8}~RPlNts2 z-ah$a-s$SrtNt5yadtYZyoT&nXKO{7j{w1CUE$VwWmCu7HyFD2y4c1a{4Y^FRCVJU z%w%~!3D)jW4vakCl2aJ@$bZyuk2hqvm)79v2!K7)b|+a>!O7t3RB=nI)1RN>>x`MFF#3z^1CU-U86fs z>QkmNl1yA*e{DeLDOa7OnG5gERCKZ8l}CQeSVZ!GQR2ENf=wwC${#T@3r)^3dCQ!T zOiOUE6sZ2Q%Mx+Kcb<9qggZ-hSFKX{DD)D? zHAzlln$8W2?si)}xu+s0>XLFX`*eL`xi^zu-Y5-6RPu-m2>LCWUS4ScB8>}zJhmQe zxUY(ab^v_A)LG>G(=R_%lh&OKrC%xd`S|?<$=Y_JB_)nk2>#)-qqcI@w?aABJ)eQ8 zisws8rxDcjB6Di_{zhJ$o9?r|sQniZ+Pga{Jq&%~1=af(P^}hmtaLju{ukf_7P#&4 zEnYjuFsOJ3r2{wug(KMKHlyI8J6uu$h2UU6VlSPfMh4GUiL&Uj7}$TpifD7hogGup z4-t^&6j1|tHmrKR>cDQM!aUedc|BYF=OeuvX9jsL7M4xr=*TIittc#y)^D2TFTmS} zTB0}V6{$zppR$ z)YKP7(T%fnki!NvmQ5C2A-YVnO!GSWAa*$^(O1rTDVtc453YayH)}{pW?z->Smhax#FD%gCbSyl6wvOp} z7`O);;KlJG3JA&M(RCQAB45TX0SQX`!uN4h=%)d$dh8)f$|BQ!(p{*g zKVp9Y@gTZp7_Y+``v%xMG{53lcGx0RVn}c^F8C||{W(h+n3k=B=?%QDirZ5;0f*HDbAou4lYIvK@eYDWu zlH0-fR11&s-)Gulo?y4<+J7c#uHoQvTCS!8GR&hqr&YNo=$XhB`T>_NVdFW>#NP>R z?xXkN-LX(Y`^_>jweaQO1z?s#;nkT?Dt}JnPLl8l~z@A>ZOR0%MeQX`vl7fUtZmQxLsYwb$lVP-79@3qabfk ze^}ZD>k{Xy;N%gU5!<+twWx6RY;|uwe}nk@`#durn#U$?}zh)%~EfEQH zMUemL0GqGd{lqXs(F90JUU=Z+JB=*t&$pnfDwv(WvYK7@o>!nK!hjK*J_+qhpsO*Y z7vipZll^B2FVDZIQGR}=|5qF+$NwiCCl+qDf0LhoSN`q*kYyx+l7N3Q@xH9A0f4{j z0I{#L|Gn|gB>#IeeBJ%`zdBBQ|9{8nzdLFF=XabwX_BN9$tLKkorYt{llUOS#UXMx zBSF+uG0n*VAmn@b2E3n(e*6XiU!srIG`=puv?R^AB?>!TC#8 z5f{Tc`zJ_M5(+DZ!`r8f{#AnJj6`XYvN#LHk)R*QF2~6+u8U<~A&NSS=ap-l!XM@f zLOX@|h?iXrCObmd+nsFm5%$g4Atr|?^*%mthel+mP??8z091KR3e~T~X?b zO&ft7ERuHo5#$tO&gZcHidPa__^f*Hldh2npW0P@bhGUfh5mAs+XAMY4w<#RZX#Rf z)Ps&(mTbdZ3_KQ23O+teS*B`xRQOBA4+Vvtlqs&VI2sBUNe`ju{iEMoTcvIG}(sqiu(ZFd?%l;f6Byl6n# zqP~>MZL=6()m;h0e%#(T-B6On&n*5z>G8zs=sPSenMmU}4mHOR3v5;4KgaQkgmO8)n8sMR|3B6)W(lX-TOuJa81$oCUW=yP70B0fMZSI`eMdR*1UE z548MGSeyz;W$`7pgUGkEW! z&rLh|;mdxo*JoCO)LqMl3vRKzSE^B0k}Y$-czaAJ1Ktc}Hlwvk-(`XG&0ZK=Thznl zlvt3SMJ@TaHFkk!9X}TgJF)2C29CAJSR)eCpHM(iSzk1~!t{QnX<>@rA+6=t`-NrY zk03Z`TeP=F{}MuU(@`Y1Qr}uvx*_b?J*loGjdX#X;0Q~u9f~Z;Qec#{?1RYpMxiuC zBS28y+_~P8x7_`wW_9c8&-%|;fqp!#=QMmdOh{>Gm z>AOfd;}LRCMHNrW{Ev1+VW8~=17*8tr$TZS22&*G@|kZEx%BPmIlfa7kgNl?JOYld z2_HgDQ5LNakZdqxF|2?-b%V6$5W{c$8bbF`SGUoVlpW&|iwm!HqJBpgSHrRV-qP;YrtUSUKrsoqMMIKgMFvOiC`%lXY6TdK8Y9_mbBz6U z7$yR6V2BnEqS>(p;sGXb#TCck+8sE0z471=sz!IEm45_iiEzRXZ*k+2Sek>O$z!N< zj`}hus`B)W!jBKN!)xR&Ty%+*aa=4-&@+%ENeAXpIg%6GvvPYmqnn1Pz$OTP-)I<+ zt!nYGsj9lDv&((u)3Ph-?kjLA`4&kGOx$lvM5P~TRHH|)7*9AkDR5NW9sk3RYJDmv zxwVCZH_;JmlEA|v;Y2>|-Brmj9@u0ZQx7u1#wi241?^t!KF0nVh&RSZM$3}LmQ2kC zT9RQ39y^Op4UR#g&WYSzeQPXb<3@pa?wRLClA2RlKwo)GoLfE>pA85{Ra!i$aZlFP zVxqe9FUPrF|2f2;kek%bS`|;v@**!e+U9^4seGt51|bRhrwWE&hRi`wr(@E#k@LyBaS9BI_IEvmvCv#fC9dqj21uU0Dm^d_~Ej;Bu?CFFem8$;XyuTM#*`)RO`yWoUV}#BCy+(o?#W_=ZgzE6 z;3z_(^iwOr*mjZyX2KX`kXz1B$(1?5y@|a+odPYcF$v6!)5!K_aGskq>6q5QQs2$iD!o^-0Vr>fc$Ts(~F98 zRJ-;amUbj>x=qsX2oBCkBUZtVvlf=$AV4Tts@RoBaCj*CD98*}h}O||+lGlEIsM=o zpKS`Y&%NCf1*`lkbqEZ^=l={>IJ&+u2s$~`ZCE{Opy*iLo=xF5ShL8XjnO+m{Q8#I zb}Vpw(eWn7{-jXzPJM&i+^(q_P2k^$oB!S|3)>Jba}!p?npB>y;-wf!(3O}J%i{TC z+;^(E@S$0yCu?>4JN>=gMOW>BMAh=dtDbMmho4E@?U7Gw+};+n?UoQP_GK{eNTwu^ zkEA`>Q~?uI{N@0Y;ueJBPMl$-*)cYRJqTG#FYFeKCswm`f$GQ%kN~M9MOaeni}5^C zQcZMY(L*Uk%Mal<^W_CSgw-H9S6!ej+fDYZCjvv0pp?F!#DT`%x4fhc^?K=5uxNMC zi(jeeIa*4}iH)+O`0VxDEMB(zPs;;2b!9paN)AMc!s2<3xo7%b}t_jV?+lpa8 z3iH@f7W6z3cu_5Ks^lq&JnS^xlbzG!YPx zE=0c4LqMcMND!n*i-3Sq1q4J&q=Zf)T||15P!m8pBuI%Q#FO8i*)#taXU~7mp7Z8+ zW-~lP-Y|qG>sf2v*ZsM!#b9KRtQ?~u`SbiL@#ljfUQC<@pMGrhH4Q|7bQp{N*Go{; z4GXusdFjvRm~rg#EpeRgo*4sSH8&m`o7}7+y^bx;_I@VnYxx&&<4z*8R)20cadCvfw zgMkCSiDPrd%974pq0#tXjOJ2W`6Z@*v})8|(;3Ve2)>t>XqwzrYjJ!^eDmec)vSop z>ssnjU!%E$FVz`d?#iBju_UQi0ugL10blawdm*2}Zg>^aY3G*yLc(-N^kMUDKK(lk zoR2c>EA$s-i4m3M5Dg!sZ?1R29IAfGem^1@V|Lb|>^+y2`#3aTaZLF@uXW|SN$j09 zXy18@OOndxWfph5?F!lyelStp5UFH3?lWrWRr=F@bGr5uwgTejC)mKmop&Ko?2+Pg1}Se- zwC|v4NSiUbE_jm&cHMb3a};sBMCFB^f@lhn%Y|zecm8z=>>+7;^oyY{-Ap68sDjhe zuIU=twqwde#%`&0J&OI39jJFk>G$X@rOqRHu70@F1G+FcpSW;gb6J`o-q#4N|N{gMlwCH~f{P6y5RJexlS1uL?!+QDINZ?4y4 zKrJT@K8&l*x#VlG(dVHvUz@_;kZ!5P9)c2J^fRb4J#*qnstl@+&=f~;DG7~ttg9tZ z3Yp>c`^Y0xpn1zgF{+s;Ug*;oHdf(v@;CCj#Mgi=dA4@THrA!3Bo6>NK|B>`P>Iejk1`iR`(ePA{Yh(^wH& ziof=@pfO{~MEA8JmB$vghxF{Aj8h`RWt3+Q8F3g2XFT;%0HMFn!7r7fL=L7-I%G9{ zs42!qB6|FisIsKa<;}R#(k0Im;G8fkTE<&fl@x&&mYsOioNG>9A`F+GWqj^`R^xSV z&~ZX};ob~cfOZF-{snXng3bfICO%R`&zj)4i)YzrZFAaklz9kZM=wBd!n3x$nl^}I zh@;-bUjR;uxRip5^uvXu)HE0$MF{V}`L`UhV}j;Jn9jrqyL)o98zM?*IEANDq&|}non`AiS-QOF86R!PGb9OQuv6~9$-XJK2D6zx zTAKMbW(<^=rRI-}Zb=rSB>w_r_epZd1d0lNpCmQ2n_Ee;>d_Q_ec0xA1u9OMS}esa zIGRsa$+Vj+BW6!<646i(+;-BCYD8{m&RAD_ZQ$mHlbb;<^nDk6*isF70$tr8znTTKr9`ecGq^6hS54kNzWN{ zZ3b3Q1c>EC%fVIP*E8>0o*}rcIG*9cVCd5xeh~X|(L|I%+`ZsRZHe`zPby*KD@@IbPQ?T0T@CW^?KH zaV2iCdYf*J56eyI2(Q2=#}}NF7LMwTL4GnFFu2+Xzn+vqz?+^_g~AR0-NNbCk@J*? zBm^O%uT{7Q{tA9Y8$xKpbH|HJGR9NF2rIqe`_bASMS>s`%qYsGgjCY4cIw_#N*kh{ z;RU57-oSs*F}tlcMUmxC{xoKXTp4f#P5iJ@y3lE>!-(|NMMsf6kYa&~=NL(~2olrC zc7c@ZQM^ed2n@>Jo8lSNm)%5mzoq{C3s}y^5CT6jmrw+011ujSLFJGl!{<1<3V4qj zr)jg2`RhV@T}~1QwNIGU4tj^=LVFKmz;SIo!UWY2kz{>l2=D#@T+Ra?zLEQx@fW~e zzijvypq5GX0uc<^e%_%y@Ol_O^H;Qu8ETs&f)L+T0Ph20na(PAs2-h#0B?u8)9o76 z_!TyKQa%vp9$IKi$nFB}!s)kYvLV5fNPAEu8grcX3UQ|9v>vYgrQMI~l3Noq22-bD zDkt0MniJ*Xb;-7d@jBDWCO4#Y8Mec*W0q=MNU%@`uDBF7jXqLbY8=_MTMC2=_alUA zwa>zITT~8AOS9}@Ptp}8c)oR>^l2W`p(1aQA4378U9u{5LYPmNi8_%}OBC*3xh zyV?a<5e^L4Ei`*~v{H~d-Y*_FH>DT2rw*L>UCmo%^3x!=@NIjjjs|^d!X#ougaBk1GL@6}$hbjsL zZ9SGh(q3KBF$zD8IRX1yr&`&_|FYno|A(e35e@Ul~oOP$TqE znX~-%yUUnV3>WqK#yVc;HJl+A$HwQ`gk|hTFocqEXCv`Q%2lEj-nj^(QuqorXD&?Q z?{&UL`1N{aB+;1>Jxv>+y;Utabviaa&^%Kr>VRGN7 zJ+voaLHOrK*taEuIpQLz$$1RQ@)vLg=-rJI#!5XRj>M`Q5e>@m=5dQ!qyU@@948TX zG&qNhU1AGDFw4DEhJbDB;qjKC@%Z)lLj?D>kS1{^PWNVH2HtukhH{HMr_Z@5=9=xA zR%a}lHDE`z0xqu$|4bL=qiB*`$YCVQZ>nts&rRS3v}#r>R+f5&G}mhd@2V#XQ%u`I zZsl{r=O|Bzrb}_~I&F#N1yHZ*!c_S%j|z87PTLH@tztBO9lP& zyQfc|hr>M1z%UW0E&6YV&}c(g z6a6}cdFM$ksM2_&dN^08&GGmC9yslEwZKUJ9+*rcfj~UzUI^%-swQ=v2;G=O`7w9n znF23|dmaq|A2r3tbF&(J?)Ai49)v@-PrN?mSz$s*5UMlUV}0Hs@Xdn)S2y$TFAUCo z6#*=>b}yyo+3Fw5)YBn4;wS{&f{uX60@Pl3{7a=4XsUtHin?yThVU|5_^-WoF@FKD zvbS;4>Kjh&b8o8CzA?^heyuSS3nbS#H=?r+J93UDO5c>YPM?+)TB8o(1T)3q5pKW* zGO_M(KxJP)3aSj^`nC)IR}*@#osV_WLmne2{hGZ5Jvr`GBb z08u~L-@#nD^k(8YsR7aC+I|7=HmqUVtOe&C>EWHvAK2~hAP;FMOSA;MQ_BytTu@ji z*cd|;xjWXdgf(0NCPY}mhz0gmh!+y9j?IbLaTKtq ztwTuiPA=ZI&j_O8K$l4c$Wg4k8^joj`0B79 zqbs|mS%5fOw1u%>)*srH*jt=a&V{Q5Hg1m=`bZWh?(o?DrbTmKm^q6- zl1pgij>Y&2$5$K|azHN<0!Sh&z__Oq6p!j{T?LX{0KPd$hp~t60y;b5b10m~dBjRp zhvxR{-X=mPNLHq%R$pl^&uyS^W? zF1n@^f8;cJz>YP6jRek=&{5WqP&UCHi}|n7MrA4PuP5E?1sH9<(H}60sH=-?K7q`< zq_i-3nrF!0?)FtEP}J${q(5a{FB}(660UczC96PRkSfW_q?bVvgMRU+1CzSEp&1UT zr;ddJP+{T~&B7?{2SJsHi=lERMA>@3j}Os2#JzHy>1AyR7b38ms!r&89T314FP7fE ztxqz=`z1Il)|0@5l}@TC2|?tyZfZn-ApoNT-v=$gbX0#^H;z`f8`e~#hqO9jOisba z{f3j&o1~RTgr#l_tMigQf+MujAlgNp01?x$O!qa$4mfpKng(r0?0cNdrH0I1)OW=u0h9^l7dYijPz+n z_QldBeO->JTo6$R=~1>tWuD=tavIbxI|3P@cL-=?nSCf3zaA|IIihVs7ZLQKY+wHZ zSSTtk%b0i}7D72~Ffw`tY-MYDyrNviEr@g#@J-YN4LSb>fTnAtm1P))yG`l~?PgG> z=8jWhO{i2;J=;mYBdNxmgFU>L)+gL;V*~4sAwfJUqt$pR=-TcOWWV*^n2w5r3Yxs# zh(&(;V?w>SPQKvOYJ5~B8^4UOztZ2z^KxiGDTudPg~Tu)Kq) zi{uRxu^EHjD%Fpq9mU7;Wsu*bd^694~_40p>l7aW+pyJ|7yogHl_HrsZW_axP zS>9O-2d_6ub;WD%=>FUyu27k85DYsl7f2Dy5^*Xch=N-w$;?%bXXRbM%ex--uF+4S zcPs2nYqskg>ps#{nx~<@>-Q&@pF`SBnQlIyI1|lch=a$kW~!m)03`WeGe;Zut2{J8bPH;AfMD4sHD{>_F8d`Rk+k{sVDmsXD~Wo^pX?J# zeCO%RLb|&wb@8L_v zsLAdP6hObuy@NXgykGyk7H?T| zu=I*B){)yA%v34Ma%lQR*(+$oDzES?W!+jbsqxs}mnb(oiTLT>fxoP08T|`>)%z5g zP!=ZSB{p+pa^{P-cg66%Q#7uacTxyb5jDMQe7zc9y2M`G9P$d|$CkWwPIh;<0RjVZ zki6Ne{c&vE9eouaORJ2YIYx@Fl_MUH-uv7%i>I~3YcKN~2e`brWA$rMYSL2SI4~%M zPF~uGKQH;)&WnIp7Dfr6+DS8oNJ8RuEQ$Nq8&boLv~qD}o8dvJn@7xTb7fpvHjS#a z{n{rXwu;7r)eTqf{5Dt4y>ihhK-q)aVXp*n(=s@ty(K5vx~8UKN_QHyh_Ymx^OJK~-;PKP5QYahQ0(x^y_ZG>_u8I%;YhRCO7+=<&ir?;9lo`*W ztMs&0W6**(eAW;0__R3#yU##2oNSnzm8o`Xj<9<`!JQ=nm(%|OE>k!N9NmND2b)we zqC}rLr}IMSU~g8dc@K!6v z(}Ea8@+}!Bp6_sqkn5nCKFh|qw+)$e!>$t{BdO?{n{~zzf5Hk5RRt3zUP##z z$?!sgB8w-w3-}5&Y}EIBj;h^fY+k$D|1-XKMHR?Tm8OY1W7`haC*ssLIf&l`ETd_EkK2Z@7+bdJP&LAM{E} zNw*GH2Kmbzt^RKNF~i-Yq&%ahW^2`utrns*Br>AYGp;=GVDG?ar>F3F{lreyQ%A7+ zP1Ng0#R(#h>F9X048jFom}A6SJRD4e9kH@oi!_F@35E8!?Da zaf(vO#`D;^x3+iusJH}Rru>MB@}H4QRqPji!>A+p=88eKQiBVmB>1|EYucFEx2p%L8Tsw5 z71FE1?^3;|N(LobQE#oM7E|o!+_4kYi|eL*mcCN%`Kx3Z4hVWc4dYP(=1P*t=5w0{ z${Nlk&#SVeYYF%T4a_qD0^)o*4Y)87exfS*xG?s|J2eX}ewE>kHCfqa`>3JZ1S$%iX$aBG77<7Npi)SSP$QQUk;O1qk{; z#U#=7B&+2X8I}DcA(0058}WJ(?{fybu#wozE8s=>^zn7miM2)7iJco$sD{MVO`vNt zwOjczqXD5YzJ%n_o#R%JgroJ8HnTKkUw`x>l%i7KmwIK5+5fb&{~9H#I-T{UmtNsh zS6`yWwsRJHBE0=AjMrFy$=3a(NqDkZc||Bs*TyRvJE$fHf)*j(WT27*tqQn< z1Ky@7StW%H!iiAEkY(hn#mhu(w4jjmC~C;D|8jY5(B!_d$$*u?SOcmp)U|D52e~K+ zGZ~N;uWQf4VtZsSmpJLEtxgwwk!g*5>+|X|Mv|F_7kE1gASmyD`b=e*`V-FA-@A{M z!rP`w``te^%dHv2fmjpfm4yk(B>OpoiTxWFXeU!v>igHIveLJ{-HmGQ7haYVU|X7f zQfIPdd7T7;A zUTMcu0~wkeYJCxP-hA*a?PL0q50is{&3MaQMUw1fr=Nvg#3>N0sR*bYPBgN zeM}+_*K?g}-4ct@RfE4p#^RPA*lD9-$-Ki!Vy3!n5m)j@d;wWH z{Th07CKFH*%Vqr=!R)WZeY@v#thoH=a60yVI$=en=zvtG>f!CudH%;%D#?p3QD?K) z-`+5sXMIsspeL{1?W=D2s|BKE3-&5Ec3bdxPW(>LT-&L6!j#hE5k30w{Oc0>7=*%u zx+}BJ3PkI|_#Nk!AL73}7bWqJ5CUm$E`3{j-BrU3aob8X186mW*3i(`7hh5Ters_& zr5H87#Ox0#|302RR(86K8<3E2lT`mcZEDi$;5D=_5g8mDE`UgG4-m@s(JviUonY{= zBtOyWZ@#&LeA~|$EdSWm7wbK;GeG8pK0d2&w>jDN55uv=te^(nVEG>7t*15VIwfQ30(oo zZNgM}ytY<3o%Bv(y*Kt(>CI2A(-i&BbE+jJ1^l+^{7~cd3^t{@b zOwe3C?$2Y#sz(O(8|9|WvGg`nhX-#4{ph6Mhy*V2mgeJ)y_X}?rq*7++tmPQrUtCF z`dOa4arf&=BDDez#*oArce=?=3*OE$M26Tf!f*WUj>R*4e-xjf))P!~V>EMw0nr_y z{bEB`E>4~HW`!cn22@rjmQ7^TKoeJ=1yWyNyx=5p;BEj}I%zjp<|I6yQz5Tg|e#2d}NSY3Cv5!X`%X8Fq(~&=Ce_2d_A-*1u*}Wf{cDgPxz*x z-mh`WX4pI>jpDblb43hac*wra`ULvjg!LTDb7qLB;K!>ca2bjMe&`?4>|ChtlEt)~ zy%5ePu+1_5-eEU;<@ZepSYSf4%S0(n_*?w1m5d{am(seL+l>VCGUyFbb>-are8P>^ zakNlq>nYZLsvUDf3^gwLy42=*^5@=eP8K@8cs9?LOtQ3mi+h|t z@VN%dDCXWb{{yaW7DY9Ealgxy^Jkw``m=x}4@|OZG0#efCg!EKUJ+8P5!JSB;}AZm z_+`L|&lV6DS)7yV z*P1g+6LQPttla&=I^d2YZY27c&jXLP50`|>t46K0XI17LEY$NgzyE$a z&tc6WK+H>}1Vz^0$Z5=S6jiadu)b=2_1f*5QJ+7TT>AV0K>MbB$ur!Vv~cRbZlQk- zK)cTU7rK@Fe^a;mzmkolrl73+@5OBWPd3tjWrsl1zgoo8whJX9kdah}EbN+1OtQ-zO&H|5Vu0(A=T7mwI#+o5%NdYIfG@F~pEw-gckHlZxZo z07y-!(p`cX(k;m2nNI9fRqeda4E|sI1|99AD>>S1w;+;N{Ca`O;XNlae95+;tL^@H zInK`V`6mrk#EqO_Bs`#OLskCc2rbasvoV;@H{YYK-@LxDK2ee*#`Ko$g|2!9o=3d2 zWJp&Q;_jbkDwRSA5rFHXkBCfGwR3yid*?Qcb-U_=_))7zduOVk+`}U!NXNp?^hw>E z*-S*`)Bq`^6bYs)q1@%l?V&AhJ3zL~?n#P0>*Hm26vK&d$u-k?pM*hPk zMZO2+8&`q&b=vau!2O8(T}86hG^C65-7fC;kst_nww}uUc*>8(AtXv$skEC0LM0Dg zKCk>Zpyf5M2y+AZY4XaOFp&+dz|oDQz%oXrE77X0(fOL}cSLlQ(kK=88cGOGr5(q? zb+E{qzW~z;4TFV+{3i3Y^^?Nwd_wjLV;2UYdu|kV3(8H3#R(@0vr^3IGd5rf1cSD6 zQv5x%{Db3@tl|^1g3#R~8N~yQhBJ_H+kuLXuw_%rM39Mz39YH=c_xGz$UgQzD+hWb ze)cWzoU2YIx^$_;UO7Qd0;50(VDZrM?!>$V&2EQ{3Ewd6(`@!fuU#-es?fNwi-dTLUmAd)B#1JT4N}Q7Vi=T9CoB$`a>JYe*tHuKyNrZ>Et+&!~w=5XA#Td zFRQ(&Y((exLP!_3KH-G$_;^%@t?6WTAM7@v`e>mr4hjtXGUwqvEpgiG(ebN~qU8h~ukG~bB-Y=RQL~A%| z6X!CVBi?>o`>{6>9_+-~3Pp#{uY8&d@jcT~4UgPj4*(g(uH~r6yv!?f`zk6aIP#QD z^mjq#HlI#;PQf$Em%je%Z|E}TVpl$m$fPPoncb=3?`{n1*#oRx`oVOmWMBSh_&L++ z@(SmcgE!l)e_{>Pd_BJMY7AJ+S{u{<9)$MSFsOJSrNdv89iD^_lrK3-v~JFiUv038 z=&;<{OLqYIwP5@8T%KJ^c1@LcTV{OvL9UMV=ZH`E5uMf-uN$j_w}Mx$2mS@1;|#Tr zMKU&Sor)!=j%D6^toDsb;~#_ZpTkDmsY?ZnpEh%t=K%~0XS6@w$MlT~jgGu+_29Ir z5lQp%I-8huXckX~(YS~lU2JvLSrFJN9X0;tJM70@3(8$ro5TIQnE4eRVBA?j*2jJS z-;oP#p?%@3tV|;Tlx=hq;Z-?*DjQ^|Nx=VQvk+;RZQV%*@k;zF{hmmwktpJ+p>9;V z8JmT%`r;YZ0pw^ORyqMvwl1wMDL$OB?x@043Pt!8>!}}fMTve@K+7}e9vmplOaNB$ zrq0e!^Z{A4pW#4>pv$NZlPRyi#)rs$3DUZ;+yGjX<)C^~r00^n7WvzOJc z@%`y8W7kRLp}YT(w=g5+)Qi<*J1~Y|zK;bxAU6DVo`+r~A&!Napq``EH^3c>Lg1!I ztzK2G_coCMo!m2u-i-`5B|664XOxw(L3g&PQLVbQrKEVHanuwGswWBOC{fam`KdFB-d~2^!llbntOz+BpHkHH|?|S;(2w@xH{f z5D+4V+AvpCqJPb%8#ri1WxYhuc0Ny2?b>0q5D?Op^B zZYBXmUQop?M{9M+0=bq*@urOta8)haiC+_<1tyiu(LkOwKI7w8(jm`31g$4i870#C zpG%$6>a3V*%K1eaA03@W67CHqZjC!YAJE`q0q}`h_H)kh3CB^WKM~lO-D;B-m_;;H z`*{dr3k|TuG=}Z@$@r@DX4xc;ANPq-A%_uej-(g>bm}%U3uW0gXr+m+b~|S|4`S&@ zgLHH1oU(EQkNncT55NsO*%^(mLw0}f1fI&I^dzxgdHfVmlBHv;?ce{9P!t|+i`?M5 z*yEP{GG_jh1RD7IwZ>-h+300M+|Wf=yH)FX9@dCL(;opZQ$x2{0opmo3(WWEJodUd z`+wYJ`gja@(cj2mtj$Ho`6~ZRF2rxd6k2n#Tb*JFS1aDHqhxfPI+oO%6m8t!xp1k1 zXEWPn%V?EuL;lX*g$Sl!M%4s3DEKB0G>_0L~dJnXZLNezFNHaBt0>=t&Lg4)_IcT#~l*l(4r&K*u; z{d}ht12LDn1FtQ&`M=CdRoGJ?of&U|s~ZS`#4la!<@|R|yQ%bdR+@+RA{nG68f4CM zNy=8&82}@$9Fz&_9whtn>}W=5AVFhLUa!Cnc4FL%#U}^!*UwG3IIU9wb_Iq4lbYXU z7B{_j#=)gKhxHqo=@*llsk)ecy#hHpU4G|Aj6^?Q?E-+ujs~NAB)iqbtOX z++*qu;#y?aHCt~=luwG)wd=lceb0wWUB^B%b>4UR60HSwGtLE2Z<f+iQ>mG zdUdfF-oRZ}9hIhDH|fS5YmX+;_q*?e#9s+LxjxS)l5Qxur6(2K*lLC>+ScVK&P1b_ zN7zCqx*V$G6DU%6K~+5gT2g$jrjvsy%ivhL=agHt)0ioTv7e^4r3uGpp7{8x3{$Ir z1&cMbE;LSWoz7VYy_nZn8R8Bzq*<3;X(85_Q_FheHsN_4&fkSQMZ`s6tlqMDH0+j6 zvV%cOh2x@^klZM~KqC%;UpRk2b6UI%R#H~YKA7vT@wzh7xEW~b_%PzF@xmnMp+frG z$HjXeGt67VOY*1?;PNgoDhnolmJ4?WVKv9cx19WNetj}voWZAQK8pe+oO#kBM>~<- z8+Lv2!^=|CL<5kWa(g^5MPPA6FgctS5YZTwv|ugLOuZ%pNrqheEdNg$2yr*s5H>sV zRDFNsbdI*uyHE2?--3hve!`FQ&I+m^PupjxHz>De@C56Q@vi9+B6qj5VKSPZKdyjx zn&%nuB)(lkx=EqW7jl*F*BHJvj@OEkqr8!Ymt1{xskH+51kjzT!>X_}lk=;Z-)!GK z8&}d~T7BjhYeXK{4sQBwgF2JWFF-h#qeI0w(?wn04J4lo=rtT4Jrq)3ua4Bib+Br1 zsrB8_vQh6JI#{{R;_{fSeJA{i0r;03AbQNP^4aY&8fP5NQQ!2iraSe6@CRfqh4saa zZ{%-9FV_o?T+=4R?_15Jvca_fEO-_MMJk6o&-r&!hvcI$2yD#20xu;5PeJ+__tdaC2B%$2-Z)3@^tWS zH5ii|ydb>}pv=EfA3^H&ms{%G3*599@}DS?)Q#&1F&y1pv$l6pNN2G0 zeD$V4y|a@pb?t8F_SfzP9U<;ZygUMIxx%WUedhjoCo340odUt}9M!vZ{GW`@r9b{- zTZn)A>8e_@%LvrQPalRojM=Ow`JVbL=KO3f52{B03Ep*Y?cmt57}r)1`Qs*2e9#!H zc#l5j!z1-?RufX`clNOz;@zE=?=oLq0!S5RT#oCCzaC|DIm%3@|7Pogh|!l%f#}z1e6PvKt~^mU0&i4*0Z@!|-10t)*%S zi$g$IxUyQt<}{(7sihi*Gg411OXu-1;1ealiz^IcR z65PlasIZxV9TAWN)2d-XEj_|u-)OINN^x#(DR^K{^hHON4$FN{ODm-MfE7YH?D`+4 zO#G5fWAOM%V^*V5rf%4azU}w3OF==OTW7J`GtNz?x|r`|>ZKyGrs2QPLPNMW=9Q|$ z!&YFdtgh%1#gGK|8C0@EM(avB&QbJ6s}8~Miw)YxDZ1#JErguG)-5ABW#$gut%JSO z?|Zaf!4w1LEzKw9N4=;-L>_-6f@lJU4`LSHWkLE2=oexj!Is#lGEjNK(PzUIkVBbv6T2Q-To{VpG zjYw!&w5$Eo_k0>xG&6-QNqUd;uZDdmp3OH4`GoRN#d;C}%h2kHj<6{g@qM0wN}0?f z8%N)X*;$>RmTw{M)7ef9t=o0=UX-fH7~Eilsllv_tXhNF7`IV*!8B<8dqyAqbe*2! zg|x@2;&--NL@)De78X;0o}bcg#~OWg`XE4@QFj_%7fm0UJaE)NzH8@&)iE_T1eDpT z_w?G*t$mYH)=PcI*d(EHBoT|1%1i*f@Q*cCWYvNM$!Z3?=`UkK>Y8o``8GxB3Y`U` z^Y?;2?Rs>iV+QM*R@TAw(}AujP?WWzYZSxitajj?=jJ|K0sXf_kx$1UODFDL?6Y@x z*nWR!v|8(WCslYtGpa@CK)a2Y8Gd~Ga(KZ?MvCno<%>|;{JFES@eV!xv^+%J*P)jg zQkU1GF8)asu{vZikiV$@$*Z}nG14hBb9xu6dHiJHc*E;j)+riGOMlLQWv{jBfc3#l zm#U6`n+T+#H%ryi;yCSkilS`!PefFI&6ls&Y@591lt|DrZhGe``gw%@B}ghyFe5|Y z-ExU^8q2k>zfCTpN8=cP9j`KYpEa~Rt@`JMKkC&>11-rQoyw+hvGAPw2$$z2vpXE22iiPd0H?LtcCv)asmR z;?9CLoN{JYe!hz3mCFBo*QZrroki{Hx0MjOjz~GO1XQ7sgd!rlr!@=G#QtSog8;mn${f98Ou;fDGO${xsO#cB#0Xd41>4)3>?klp34j z8Pjl`slx{w>!0RDWMC5CGqV!ss<~3NbW^bl!cWKaIkyE~{tKAJ7gDa3P??GdbjMpx zw)l{j1>MWGyXsjdP0yhQqYV)OxWWW%uHmy|uz4~c=i5gM+oHYYw2gy3so%IcrYGm7 zHwS2Oj)b1>n&{rASVR5J^8Nxm&3}&IHAo<`AyjvYasjF2G*IMS2QLe?>I=8dT(NI# zS>9)-JwGaEvW85(yQ|!dx=?xVV~3y>mAb!xPnSc@ciG5F6qd6}nBXE*hD0W8-zB}q zF~1)_9lmVCdwr^1r?LlejgRI%r06{pt7*eQ!g<^ZCcUv-MJ^$_g+UReP6Pg>-rUEw z3X-?HbIl_)Pie-SiZsbvpth-F80H0S=Lydni3!RD$I$63Ec(aY(L&Zywx8`rrllHCx4ih;sVdiRwnkj@_@tx*qwlWvae@{`3$rR~OK?G2db*+(kl zw!<2_@B=M>h-C{cPa4E=W4IOPeumC{u?74P-t|vuJC$he*CuuMAnCTfz*o#`V#@Zy=uzc2tVzAx>nejtZx2!aP`I3x0gIz zkG^9xX_4#qKo z%$Y3MPKlL({S1PtxrZlm9~?i>9yb=>X-JIt(*XgCfQ~iJ-jt>(`z3+64&S>(b2Uv; zE-dTVFc^mv+3!G|@Y}qTnod%XR>FF`Yzd?3*>r7XmkPYr-V^hA(!%*n)4!aPhcdTl z4sM#tQ_RR{BctjL(Veuk$n^ij$N%+{CbQr{vwFu*iLb=v*kL2U3n*i5qmI#PMDKn3Qdz=4^KYl`rT#gXt zCQ0E;%i~bnNInur_iyu3Qc3?6JDTw9?0xV19@C*Y$v9Up8eADHXT)IY~*}|DY10K2?D&QeHlPTHZZ2xM*PlMpk)ma$+6!^ml6)p3P=|m}Q#RoJ- zz_zaG5ltp-{SQd^f8U2x*ZA-DA^(e01OJb*Bh*z?|IM-^p8gL%|NjaZ#H ze2W!$;Y-x77_|(fQhTBDVhwFyO>*P{Hn2OMo}Mv7c`qs zSabOcV46xdEA(qkpnwQX)Jt?^eaelxF9cARTmpt&+g>$^#U@~bDl_8;6Y;%v6*$8psivC40@Gxi+mel`Q+9Sq@T_a*&MRk{G>_j zdjP8%{s((+gdPo%lZPHjlLmi#GOb zhC7y4u}eLNzoH6TWrf9*CzhIVN^1Um@WiQaw)8f~NN5b#*1Z%X*Y{$4kG{WXefMc1 zj{DXi_?I;dX(gNrED?^z5Nz16y2F4aL0uk-!t_q@liWgPZ@#8iLA}mZCkN&M`1}it zDlGn9VE2@UxT|@Yr?xc9Uo;NZnZB)x<|krOQ5e9s<(I@Q`*y7g(qMO%?S)M3_d)yT z9Z9ch#B?1yh6}w`-#Dy2)pr;E-Vy(G1S_@D!<94B3+DsIX-j?T6d!}F zh}|yD5i)&}C^n@3`jusc=7&BaQlH-pybyqk*}eE)g%h7S-X+Z7*SqDC9j-Zxkz7^~ ztfV=EbV+e=HM2$HmZNKnO@#j^yY%c+*NkGNUe+QoSgAPM?)z`&Bv4;@7bqEm7jGf5 zb?pm~%Dcm90{KzK$HgVDvCyQi&Kd-Flwe0esZe2cXoT~ZF^X}iNm#kF*J<{#`Rj(e zWu%yUn5KxN%jS0{YXis^$+VD#PP09T%B~a+xEm8vv=QPkHC2;MGpilPPgJdpLNDNl z?%}4H;g>09b+*(YQqYRydk2-;X$W@dtFu_<$GEFF)BE-tGgOpjld;)%W2uImq+nYw z)p@H3@r4YN_w|2%JxcMOf>oM*^lBM?>^WJx_;#@{dp!0dZD{CN3=k+=J<5qzwB}Uc`+VD(AtPYB;z8ez*)5}BW$uY z&;KVBNZQ0Xnd9IoK^jI$5656i6wNPug)f5^^oK*1LT7Lj-a`DZqr~7i`;C(8#W@sx zl5NQo>KKSijw&0PinXH(lGu0bLe zG**`e6%cF-XSJ!bG#lC%NzB-DU(0trZr5b|t(|J{AL zFLq|%?9L98Hw?*ShI{V&`d;Up&xvK2+2VdM`26~pN6xx9f^RW`E<^u$5C&fwIK!AfPh?d0xub|wv0hqMnpzdbLh;H9;z zT0jGsP>bI|?1hvR8+Uh}KZG~FHXHX?s#C>tHPJf;`kLnJH+Y7`FAbV>{}G6dexs#7 zXK1tF7hsy)331w_lbgUB#41->gX0JMobDP(plGBS$HjW-m{1ZMM;9SwtnUWrAhw|+ zs>ye(#lRj~3svuGQmn(;WEoRo*Z*KrhaRz-wROUOoRI7kzmGswZGG{4*(?c9(k7Ui z2mRB&8r13W^?fa5Jid%61cOK?&mN4SEiaQgM+5+WzPXZ0iQjwj!2yH*?FBHrg(jxe z0hf}!{9bou$2NNWQMB!QU;h*s8tf_EHgTlZ4+00fiT5_F%`-+AH-=P=YS2U&RI9gX zSNLDPJ*o4m*pHfS!?bK)a%pF+|f`=?NJ@_g#x+(T)FT z<8VDGfC?94wWCZN8l}w9LxM{HJaQp4LR=22DIZ;E1}Sr2Vjy{uiU|L=%23k#47wF$TM}82r%9qyQhU!e19wX!%?18RXyHpM4e_H>O_mLUG#Io ziCl=I#jc+8E(A)m;$J_lu|p;Y>t~1CdFjrScurK{6AkAByN$!J!XF8oSjK@IBxORI zA?vU@+v3YB^@?&(yNoAG&Yj8Uuj~xo^ACc($J#*#aa*13*(%#9S_F0c#g%%5 z_H~N-eMf>Vo^K1gxj`SH-8t7Ivo34Mipp@rn`t2;uFwLgNcc7+(UEsa0>FuaGfY(? z2O(I5)H5XjhIpHg=G=6$nBwjJUu=jMjtIh)a)F^}*s@xl1XZZR|owL|t?zj~5mzV~i z+Mc;ZFvg^O*glOd5}5_KZ_Y3QF`j+ASm{_W^;=`zg*Q^HvNNFIH>aG;FK3OZ-xMJO zv)6Y;suFaU@b9ia_bDGb@kVm5;Hzvg59&}A2Gy?+;BA+tpLnw^o~+PH8_#wg5zLz+ z(b=AdTU3GDw>A-Ab{~0aLj`D}+AijT*jt*}t{$ki1)Z(!Uq)YlIX~JzoZtD6DdfLi|B}g`;zeireHbA9N|4Rcf5|xN0sI*u zY6GmrcfHkR2;7}9bNe1mn5!*fgK(TzgoZok5c~~hDJJ4+9X=M-yJ{x?L&A}@%Pzl$ zg{7zpxUos-~1PNSIGY|18{*dQ0-1PB7AYgKTm*0+jtrMB6 z(+LB+>H9lW=}kQWP0Q3-ayOXTIxZ4J)Y%EoF&bza%t9l3{&OsCI1(HKK$Y1T+YW;4tN2*2*idl_1?tpdrmTp72Jp-0Mep-~M;C?vC>ULMd z-6?@3+siN9r*?m?ztTF{Xk=?{#g4=tqn30TAWFg#C#=0`k9iErOQ}CYg=0c)70v+< zi+`L(5u7nsxNMA3)>bd85u$M?VcqVLi|)L`)NX9;E98j}{UI8(V?R}?N3z}g)_Mr6 zsi>&fPts$Lk*1U%cT3Ea4FO&O+@!47D>v)R5(A=Dre(5pM;>O8%>+k7+*Be z_86qnI5)4#4zUI2`NsH&r8h(uDkX)!mS)svv$s^E%2^t5mLq{vtVL>>MYew(m^B&P zN>1wZg<^hg_+nU7VxT^X4V90sTSfuds*P#2D~2wRs`a6?@yoXt1>+fZYt-R!?#Ze5 z>{f>XZ?Hxcy(f}7hKmx<{l)ChLLVraEhc zMmHv_VeO=<`Y;FCSDvOBgp5Y4={hdohjq=LxOzQ(!K#ED%5FB268zj-l5t|-U<;*y%&;9a#&1T z;L~0&XVK6ZoIu$Z8jKWVWkYhSYj*wy_IB6hT1JZ*P{q_F8SYTa9?_lyKfzKV`!8Lk}-I|qc zG2CiKmPBf9b-DH*!WQQiG;?Ht1MYLjCCmrX7Uy)BkFG3~mB8=FR4a_{G96yPJ1M&35QNqM^Y-Zu>H9+Yitd0x+$Rb)+A#fUgTWw^F( zWMnd1TZ_|}qocUw!^>_KY3U5nm+P?n`a5MW7wsA9V7uVED*Tk0?MioZe{o zf?f~uw`gU^+vnxE4euwgw_SZMnjXJgF`lpIZ(n%T)&u*O?8zx!cobc-*|f5Ov%-U~ zo`pP7mS5Acg=Rb^ZG4q0AL?zU#^=$Hrasu9)Z~=Fa{2kn5y!2eVRtyDues##;V}7< zzuhpQ1n-0c;SEv7w1<&Q8G}DO4H|*09NtZhhZD3Xb-}cAOuiYqG1SJQnT9lqF9w;+ zw761QzULp6kuEvi$XQkQW6CQ#eDUT|Lb)O1=AY3=AU(`de1+0Iqz4CmVo4|p!#WhM zM&+&=lcnaV5BBI9N?rXR8@ zjI-MiU?J8MBlR6Mb&qkc00aEY0o4|3)H>hZ-#%y547rQ4QDB`j&rP|K=x=U60Vnd? z7ZHJep*(j2_jdw4`=7GDZ|s96W$7EGJSe(X7;H>?4KvYk%(Pl4p8U@a|36bbC6?r| zrMycziLj$1fTef@?$>obK@|rZ^Lg7yoLj$%nHL3tedE$+La`(TF8k|0*A3q3YX(^D zS<(>`h*Tmxc%^)TCRhYl|4YXIAD#(XXuIhh{4N?TWl?k@a&PX`Fdb)wh~&qcj6E*#7>vy0RLv zaZ<_E{U}< zn30TA>o>8|gEmA7VAwC0fzt0&;N3T@i%P!3tVns#$_wEX+}>A*k(FN@Oe0je8)qSx!Cjr2d}o9J)@Y z8_SFXUD9aYTG}9_FF+DiBe9FsE*2lj%e2bK<(SdVzV(XefNIZO$3FBCsyB@*KhJ*^ z={MYVZa>QTVI0$zKWLumx9f$|KUfv5~*?{Y{b}v#BK~(gRI&%DhPg z+pV7QyIT>oG;X&|hh{%|qT~R8YBOsiUbDjH&dT7=MkyREiB-0u&NA3&33;r-(Y4)` zFoELzD;U0bn^sjca!wOdnFYCFcpLUzx^dNZReXf#?2zZL$NnN==_NPrSi_7&7q(e@ zh78x{>kLNTdmbuR`=M>+o}T4Dk39;0fuC##Z3sdUQ_)G5pR%5{QPI4JwqB&R-ln24 zxsNOnc#)7mG3=B{CY!=Iy1~Hs4H>{0%AfDpOUsjcli^J7oFQ1yrQEU`Mh6L19NHgh z+VF)@FJxlaSlSl*O?z!XWBLW=Rl>p(+DTGtr7J{Ltlbdt0X9EH zmS9*gUz`1)t=hE{05Yni7a0G7j`@ZvMU$h9#2S_El~BFHnl>cVu$@;wJJpBY z$Mq~;+-^W;6_&(*4n6Ng%^{iTC62%AqqXk)byQDS?O36W8y)Zi$^;gX3 zIYWlf({S(RiuF?V+%&#ky){>+Cbs zWW&dAchmm3I4Yn05Lc1dY-Lq1MrlMNHxe}BZnM!A~NeH?&vrXy;zsM|8 z&62!IU@CqHQPUBgz*vVaj9PyDwS~xR;s{)^(l1aK%jk=5TWwErskxSb^fQo5mtS!3 zcK72Q(mnoaBrg||0w?p5Po)iMR9k6kbHc;=?>)oy(O~BcMbA%9IH*-39&LrLOm`ZB z3}Y?k1)px&F$klG2!}0RUL(PsWeNu;?0(p*gis}KD96EAfjbkqI!Ci9~z4X@5 z>I;*dePH6TkOS#uw&FNt_Wja4&&#q7vST0QITNPRx1aBb;P4{)kfEQWab z=l#m-ORk92jhuim030yg*7H49C|XMgDI9Y5y8#dN@`f!_Rz>qX`XB3)0FShM<|E2z0@1E|^?on2Gm8I;4qk)2zO?0;Kz$fD#2U=6Zb43?en3I>A7 zQ)!3Wuwyi{YNPyevSx(M$W>m97l~>*g-s^tVCiv>-wNPf;VWv}x}rCtzO-}}Z(1cl z?;N$l1F7+t^PpZ>)qA}=)AIBU~J>OuB#D575}xhf%PA(P9EGQ26s#84r; z0cI?W4GVAdB$sGO{xrn-tNp!0Pc@%e#z#{n#eagO1tMlf2mU2% zXFblkvLN<5NBLCoV1VN02=#!0hs`9XE*{4 z0=uBe&~eSC{ma1CM_+JG%f?&8c|M4EuKkJmRm|(1(+822{;t}yj z?}uU4wGP#{&C(Ssx^ycm9T>}lLHeR~N~cdR>szICCfL_-hJEdl<<^yxi|<;ZowJi$ z%mH4Aan6*x?}D9k$g%(@#kYzIf$m=9K$@UtEiS)rUj+T4E#hC?d{^E1f=W24NJiT| zkzjz@APX_G!dk_hTCNx%(23VO=Z%faiFV7&mBEH!-(Nahz@&`y8x(GYx}=a+)?u4$ z0p(E*=Z_WagH$n@OD*3vzpQ53c?qkCFEm+Hpv6?|vKc<|jd6tLFzj|br@KiNH?n**FJCIwpU3Hb_7 zB)EA!nm?)|8PVkqU!TmKqwm%ui&KbPb`{4a-p(8j_?0J6ty?te4{d}49^`nyr)GvU zQi@ctxb8-WRnN=hoERvm+6Ev@dp&+|7ip7wD?!f(K9;vDpS2Fkd4hO8^i?cPEMk@m z!l=$p8ZwMWHKOPnfa>BHctYC@TS&0otmK5fhq^pw?V_>f)98g-upVBgk{)jN zeWx4R8)IOQ!Bm_)yX^$}{FF9&Kn1m0p1Cu5xTp^~J7Ive?v@vPsVrYD%uBQ-T^&o8 zwDYJ_IzM~k5iU0!$|1k1C-r9p;e~xS6(&D6<&Js;ccKN)tLoQg)^F^;$_)%;v0a5( zh!1$1!xtkH@Jl$_>!t{mD^KDiFD+5+CsK>Fk^*sr%&H6G<*Y(Z0E}4ar!niiETrvs zC;#^GlU)yRD*cScsSiQAZa**Sy)C9R>rz6P<5RNk+K69LSskSX@0p4paJ$S5KUIBtG8m?xcHp`y<$030B-XJ2QLGrWk=*(Aw)_5?+G*1SfVb^ws(~7Jz@!LA>QJlg=xJSdmmLF4%MTrqq*QANZ zwk@+*Z~@Vs*b|zSRi2)1EBqQ!p`xWPW~i+{XEjS`@5?_$yoMA?4;rUH82hM0*8gm> zqH|C_3E^_yG?Eh)9NPn_D7T_6^ZX;jNzWFde~40SFCpFW*nm@BTfDUE-nZxe zC40Rw2#TndgsOp#6tj`WzZh{HB#9-MWDF`^9@+gf1aE1Izc5C+Y$n*VyI@j!xf_~B zH)3rACdfeyAB>__wDH}g)@3t(k~1;Kff{TpPtuUU^*f8RG+2TL+spGemQEZt!3z3- zv+RfThLM+TtO8XNq=L?*jLZXVrK~dXJ`2|C(BH~ir*}e>hG<^9ZwVf}d>HoGR@w3H zOaPT|#tvKFzhu@iE&%<&0-4V}hLk$=F_X0m=9u4m@nsXY;<7x5@(~hvwl4iqG?dd6v+?ZE;o|LE39nX0}T)!(Uj4m z{q82U(mXS(Qv!;~pR#=Yy}o2@n{HS$C223fLO89UoD<;O7G~%v=5LA{z=sdw^U}s; zW{Q%|C*PRwFU(+n7fZlHsm!agoogH4JfJJzQyyeY74757Bi4x_boPmz9zE zKfqH|l$DYFzblvSN@n)|yF~uq=o0x)F8#mr6vc?4P^(_OW)~7)qzX0(u3B|C;Vo-SPVUDG( zMKl@>$NV8pS6{B-5f0dg);XJzIa`A1SLO-hBo~|6eoh9&4XCO*#X}#L`-mT6vsJAs{Tu6rs)&P9Nm=hd6({E$;Uze2~J1AtnqpbNAh2{`1&SHRmf~5 z5`5Tb;h|lR+H>(sFW-(CL|WH2?^-Rgg;})@Sy)-=t(jH@9+}(MRBM-`&Xz`<@S>w= zz3nFC$}qg`&!A6|qY~b$!5}D4DOwUBUF&Fr58LRseKwh=9a|luX416~p;qhh=O3ez zfYPHy+o|rg)knj>a=LQztEaA{ukJ(ozr#vTjWDTb7-nuv!rl%kJ>v9%!|mkiZ4K9h zs}U!frVGxic*3Yv_dL}2re}SBVd)%!qPccNq)p^t}@C$2rhinMbS5;NshVsvkutQf`9GT?%(YP7v5H_ ziC%|GQcX|CEt-F7@1TV&C1Ju3_E^>%O>LEx7Rs~Mb2R2>HdE2;7;#)WrcM1Jo)K%+ z@5BUnBq3v{FcS@~15Kh7X+(fos}Sud+oOT+)@aaHN~F zIB!fble!>*4V#^hrAP6mMmS&)qpDBqtnpR-swX9=Yll~KO+v{F!yjj*Dk>a4o`GpO z%~HX9aQ-)eE&Awi5G)+76%7Y5GuO< zC4*%(G|3*I&h-_%IN+W6hijxE27OYDJA>t3$IUtwJ2H|s!}XAURf02WN?>SF1A5x; zz`tbOW>?(?XT^8T^hZAUsLmWoY_|I;KJ^ zaW(V=I*r{hp1FLaZ}7@`3Jluyx2c!+3u&)l<7^Inq(7m9Xu5c7w54XKX3)|#?{iY| zJq0;EPTAR2^!!H9QyL$EmyFTMSN>_gZH>um=-vwuW<~@wAQAfq_J5Bfd$$M#}aI?QWx7GFGbE` znh;9Ow4?{2VCt9*!ym71?>e=4nj#15Ll$wYHd6eETbTjeYFdbM!po41p|C$mgr^Po zvY|iehh0Q?wf$jUV_D$0snGCH0RxFn``Suf-Lg0dOH8fSJwcBIxLxF`Q(GcQhi{O| zM_tV*p_bmcb+42_pAJvH(59!e{qz3p`#;X_-=DLy{L1Omo?gJtE6(;1Kp=Y5DEt!pe#7ATtqbv6Vxiq1&!-iYa)123>ey~p z&zdn!RxD^3pX=Q&4LVzKf=})=#8ilkTmDKnTvwH3zOW1H7T%4lA~0Vk_b_7ZP&qL@ zZ|m^z;XKE(CdixX(!u8(nk^r$q=z^f6Is)6b?jBw#~guXdb_$Ced&fBhP8KyHKY~c zCkUl9SQ;;Eb-YvKglB!$S8M{s7ecsqcHps%jo$T5k@6(d0>V8VSP`0!m#x-O> z?sh*-AS6#-{72dQq#Dx<{j(|~i8re^_v`;$!ywo23YFon%zM4=&thZk)sowrZaVt}uQygH0wCd<4f4r6ZnTrH6Q# zb$KnYJY_;lpV*T)yb@kG*d1?JN7}YjW(^cXpSKjNP-O4_}-=>Gn_i%aI8;$`<+GzEL~iW^-o*&PSG&aUo{C z>H6!293}fj&BufE$Kpw@j3Z>EAIe8E%*Ny}&znsfmx08F^IHrtVoXu~ir@bs+fKTx zvR4I2>HHf|Qp%Ta2?gA%KfC*I1GZlekWafe^_PKBJR&dJe#xsoRkROy6Sy8oby98K{?|X2kTlGhJF!qv(t5E z9Frf=vu|o5GDIl;AkvZ+06v(U!N=S;QC06F#5WE3HGR12?5*)u&st_*_T=485xoxE6G_S5pB0xp3(99^RrEU_UH&C|H!S_*w?2Wi z=!}XX&gu+waS#|V!v~30Snba~5y5Xk!5n&3A+I+x+cB$A{B8+8jQmrIwdsIcfJgW2 z3~3jNt3n?%g^&>1eUg}*k3IUAIdKHdw?Y4sG4w!Oc2-6Mf>mT4Ja{-2 zt0rJk%uj(}@S6$dhFYB}Yh_sj3F}uSmOYUTMn(mm;#T#2Vu6hrNvyV6W_~G$47>RY zT6)tY4PHoe230k=wZpU|f2a%05)EQC4j*2H9jNRa1F@aK@X1ztu3=ak9_MzC-Ro z{|GPL&)Pfn-o)Kss+b`A#PrEc$(=sdiir7}xfyTGdv{8NpZ(H0fE zhV-o%O-gTl>>q8AuCvUKiaX-;xAdvA3!iJ89Q8~3D5xkeg{_-rm>s;H4*Lo4QcYxt z9nuf5uglT*784cX&}T_j^)tx6o1E$5QQ;cE$$Jyz3LZImn6kS4{{3oL(fp4&KKA>m zPgH)itGPHS&M%^?pQ`Cbm8FmhQ1C*YEzV+V~XX&xK(%3XAwgG1B zuol%wr#@|gfuiEdm@4GaMo>X?WSIZj1N0Q7;Knnk#PZM*qt*>ySKo)}iq(SY#xKa& z0h@Q}ot+lo1WxBGeqLsR9zOZ%RY;SLT`Da>y1fVQj7ix&FdrZSu*_LmOG)5v-8{+N z)4@oZ&gQvS1>=%9{MlbiQSak9JB^*C{yz34fLZ;1A1h`gE4Tq)HQa^EL21x>AR}Ib zuFY(gywUX%ZC4^oyXl9x!;|&#HKv^tDHn^eri+``F{Bk4_e~t&I$NEdaC6qniyw|j zjYdmFfFPva@1_W?B}&|A3KZ5OJ1qIj%8|djFV-M*30xWNRRR1IhnA2mNww$J0LATr zR=<##-(ZIZ#Bp8sZ=g>A$(4-U3f<_BwFQ>dNfuvsHSGna)>oG%;8N|0yc(Nr#Taud zJPK=UgBQl|4yF2Gj!=6m1o7~#2B{#Hu)cN#fJda~N2ANxaDQ*em)RUEooqEPabxgo zW%NaZqn<|Lqsf2#N6Ed%INzOwJkq^i{GCqxPb-mHizI1%?YT|`1cR~Xn|-l@@K)&| z2S2`WHmO^a8&na&^%KE?MAlB|nM*n^h391Y*yh?(Fw(kf0@Z8eA)Kg{Z%yDb`IP_8 zG?<3$OIiVq)IVz{lv87yThOHX>@}9WK-*;p!qp$6J;OVJ2O~!Gc^16#yB8Tb&n{*) zZDpf^3_$HGjWCL5_AY8L9y%GifOjgCD%(Z9_!<|XVh{E^22%8fYopkg4Bs}BM^os` z>qK3WqR3Eh{}~hbQ0(&k+pphMu30;w_oMG)0+L*PeM2_4zib9le%Bk*+~>%u zaa$$#@A`1gnELSM&6@AuZWe4ak9`hMyPt$)4jXsBT|3s6(&kM?dD9{m&Ul&^u& zJ6-0{_<4$x!uxC%nhxv^ZYd41Ep5{eSNk}FKu6L%AxF>W_HqVcL6g&xCjeH|)g#LO z9NnbZsqilj>orp=RtE|INof~takRlkaU{^r5Ik!Wk^FT&OHA)=P59YGj&XX+Us4~m z1H~RUmYO8&e25go3k>tpsq=*d8(`f@^^awa9y~%ATq}_Fq_7s4Gy8crCPY}raDPR( zz3_3~+#w0vvaS~QDh)EjO1u+Fy85@VGX%z~ZTl{JgBpC|rbbHRsgF8cgjSEPThYk~ z8~VhjhkGkj(j{dbF~Y+Zo(6gtYPm?fk6>9EQU&*RboR~tZu$*sD%SAiUCu|jqXRH@ zBN{jRr1qOVi#V}H#lBsURa}bb2E5WxJjjnOe%tBJn!S$L&ydu?B_)cJ0snM62W@Zp zUeonIy6|}ksl1@$Ylz0tZ|%(8O+wMpg(C>NLh`ssE-RkLQ@!%_mGLXmWZMIrFTK-Y zrh<^53VPcB*PG}WZr%Iad6!4Hd6ETLJaUMn(`#Yew^EAzXOUCEZ#R87p1Lj~N%nk- zNAqb1j^aP84Z~>+0`8QDRc89Q_x@1O>X-xHle$kYJc??McM(i0EZ+lK0-{2H`9FN$ z5x&P3%J zIoUNU@zVC)LiS_xN2ea;b|EepQ4O}=Lo^0jb6;kU6%R#9bs%gH$M?$|@B#W*ffYu{ zcFLWF@v7xrOEuWA)a^T};x0uGQ$F_}AnsfJA$=-2#q}o*RPMiI&Tu-yW9)-mFE6Y# zF1Mov4{cZtRi2lKpafTql2RzNt44N`Ot$1Ob!kvg0((7_m*Y_N?Lbeq{zbA!xWR+o zpVI|vzu{uli$}dV6-Z@2mXNpyjYT}A2d>u*RG`R1NxR_4B@I81(gfG&|50_$%SJ@P zzZM^3ufP%NBq$JeV=ymzsE?(m*cy|XL=b=LRf0bnfXR(~Fm0cOE*~q1Ea@+zU{SOn z9e6W*?sN*8wT+ajs08LLIz^LOYAOCi4j-%FbFPf`1d8#`iOlZR+z^xjn7g{PR&pb9 zNda;ENeT?5%nfVa%_OOtu@iC~&Y2@eliofmLyR&4A};*g^#9vtkEZ574? zuO1WBt0a+Y+*u9ObG)0s|GB5lADUGXEZYOQO_JDV`m-pBVm|ScL222G5h8kQCpuBl zOcs@BZYQT}u?IEqU@NRT9HnlFfV*$}1Kfb=d?SsqjD4^={LK*!_P(2VV<$t)voR>B zmvM_SsYkjZB;lu{HL#s!R=!=eVO4QXD9%GOxp>NT>|e6yYU1LOa^naiM1y4DwT3Yi z*7Egh3oK!7e0+RPFlupbZjn5N8f51okir=r;-Xs;e@}VVJ0|sqJV|`BL4QVFu^j46N4jQC|Xa8zFaG?yfzRC{X#hDs!T@^5@gh_@Di& z%8b8na3Qbc%hBr^x#JOcNMCgQnO+m&{~kMf6wV zZ`?vO?g9s`0l|@g7`qw)-IAaVsEy^SS+1bkVYQ}1!r0GG_JjFU zUiL3M)@;*;3buNIf z+_PmArq$PtuV@HKG`?3LHx*hoxCxOQf&(!YiLmu#{hd-ZAEC^PA^ zn^~}iLsK`|I=KYuKws=awUa4~QRrYQsRD{o5vz5FWM0XOWf+Ezm2FB<*v z%iyU~>nMRmeA{g=sf`+rG#-G{f5Cgz3jFkBBq(4`dMl1NWO}k!j@QLHVr{U-@xSI9 z>S>+}&DJNE&yoISbx`O6-CI#Z^pE&wOPbE}GP?dU`$J%RklC|8bVB!548^h-9v?RU zZG6|1>+6#6T)To(Z6W8lYT*r3QVq=k`q?2TP#yc{M;7f)k8Cs$8Smheire-$|K2(w8mbDKoZy z%Ahx$RN=Z3RjdGQu`OO*Bt;Ipa9Nc{c?#%0k^;!0m{*ijjnox__90Hc7~~sthg!wP zY*7hhD{@=B|8`)@O!xumfAlYzkD<nXIdOFQj7FSz)F0$BG=7a-lfO%OBy*Scu?4Jg^ zymbq{XABB;2Ya7s27m&{%kWy3v~$emH+nq`KDp@Au_3dq>yy%pg-uJd+Rd9F?4|QQZj`7-FbUH zjl)^~v3ddFb7F*e9FU+|#_^5{_`kJy(~W*bqST_30p=tT2v)3;ghltLxVWL#?t1<~ zUnMs9+Zn2hLStJ}xYgDE_`Qs}WeF*Qdz>PbwtWap!dr!;)ml^hW#P<6BJ2L2WXktF zH{C-+m0}|JO3Nlc*?3?8ewq#V#*=j*=a8d(cWcVY<>Bju+kw|SEBcs|;$sN)o4`WW zC&sitfA)haZAClUpK$Li`{Fwb*C89@>o4`nL+`YXJyc?U96=K)y;5bwVfL}{q;Ut4 zTh(F}WYwGR5)z81(4tTGvqYb7*_%a7i&0D5anjfB$ z99mxT+P8y-maGhYDE5Ccv1QzsA{|iby(25{@w;=|0MG86~N|ErU7LM z`S5V-TdntEv;#lVZ|Jg}O75!GNZ43^c;!PD^4hLb!TL=J;slo~CH&Q?r9YsjF1M;J zN~2{$)Tc<#cG>&3vRwLY^@ps(#rJFzttHFm(t{Xs!0p{u$+vIPSf0|5-Si(j`-Nh9 z>0UESl$oNv)Tuv6^t&@~^KDd6^z+aCK+|8VRJX|sD}EH5tg0}VPPL5LO8xG%_4O}t zEx(vEG2J=LU($JvhJT31Gz~omh|hl%-MVg*jH`;B0#1h{_yN!6WBAPl93EtrQK_iA z7Fen3I0tNA+pdRi9DjE1wEphqO2MZl?_K`1RxPE3_iRvq88& zOeZ<;ZKR%i%K~~fa=O$&1-&3;`DCX&bm6m)kh+D(M2U(78y6L)L8>j#rGUDl|D0OC zxuN{dm%^W}<2!VZ&4I>(cgdI!o&8!Wx8nw=#2V6V13%7YFX@aCPYf;XfORCO zd?{kTXZ4=SB!ZWv!U)rvLFUNK7&pOTI9PeD2N4&s?Vx}kh!=#5(@7Q=m z^33~41bS%OflG+b;{1d;mrdi#Z>pem`pZCLFA(vIAjUGTb|c`nGaVF6vXQ@L=lzp(?^ z*N2l1-gpTOv=Rn_i}kq?In_spC0Cc2C1DD1D_cs*7gKk(*a?qn@KK0C9)1ii71b*+ z#9|wpRr4SRwB=>AJ*45*-i25{3#V3d(3dpRng0UIz@3xkxxx*CQ`-WOnK+$yJ^dcj z>kXz=8rooxUwgKE#CxwE@ZMulm8Ko#6X-feHgswL$5l)?FmX&L=ZW-bHg?sUq6)*GcTM~XIV6<@uNz*YIkl&%gALHe zsJRK9fG7*0xEdNM9>xaCwry>O%KU=EX?>Y0Lm$y`LoJ~??+MFXn0j-WN7yr)1o205 zNTK!^=41ig^!qF#rHP)+reD0wP3>-qFLs{NN4UPjSLsBFL=C)z>V3xlx~K#|K#26L`p(0ie!1X=H9blbR#rtomCrO)Ng2GgWis zmh-4@I}6$`oPYT4i)oU3;j56Z&CFv8Jr{%m2A{OT7~gbG7$^>w7` z<$)4AU#852>NP$9TWCqRmWjOgMb@IPnqmjGq`sxH}=0pAhwLD1#zk)X+h*x^tjKr!Q?u==`E$q zD&JyAwDxq$#cZ$l<^f4u0c?x?&UIF0F_V&PU3lw}a=y46iHy{?Ds8WRJ>~HihiSk2 zXkYEcPCT4u8CJL;@=Vo@$R0?jKQNk$@>#FMn#(f!+i63E!PEI?h6W>!Owk{|I<0n> ze}ONfD|4oWL-~>ipR#Fw(l(&^#H?e&NO<-m@5Xpn!e6g+%;1|!ZKvF{@o8Vq61JNH zf(5U7VlKz+LS0kj)wZPsqcbV3bVMxtef2**6=k0$JI#C6P(8JXPi%blrhJ<+{mTb} zPD<`Rm4LvXOGkQ)m$F%vYHn6oMy!Hr>nOskj;VA<%&$tjK(HRs^K#xi>o|2my<23O zX-CzsM&13kV&-m%yV7mgln`f?xWmXFF9)9Gs*t0{MLNL7M(0p28H2cA@z%}%SX4U6 zG6n8nzio3dE=sO_1?TZ|h`vyb6SOq)i;;kb6s|dZPN!V?p&)fCLLYPW0A&xS z`n(fT0Lm8U_?kYjRcl(u%56#(7TgwT$mlV1 z8qb5S5ukQFRjBU$rm3f=qa))MM{`11?|Az> zh#DuemXOV0a~)&I9r)<=%|*1WG@VtM2DB;C;N7?6jn&Hmb$fh?HlT6~eUPHOZCb^* z7N;TBysbQ@fTJx$ua^OLHijABmYBCx)0OC2h_}JU*I%7T9k=lIwiPa5xYT#X5>gt) z0`v5gfXXxZT5oMly4%O>i6PjnLZMXDICclOAd?%dI-@15UuZAzr7#PbRBLHsbTz)x z4$*YN7@SfqAM%m`9BS>WVlPB4q?35~i^hzK$q_)@B9R`$Qm|+xXXqIqzHYsCDErqN zd!6&5{qM)3SE7z2DggNo<~wHQI36=4_(4D6)V33w6DL(QhjwWPLE+e0>*rs>jAre-4%z|ep9HH-tYYl)UQaEcE>p4 z3%Czyd-o%P?z*Y(A)cR8FAzfS5EA3VjaS%C7f$e@4fzEb&~f*2#eU7@pOrfG=>f!p z**dgm3(G-YT>3ikoWqy9y_G=Au(y@1wZ=Y=hFX-nI@VhR)o!32=IY~n&u9uWz1Reo zF8;GKn8C~BkcvA<6?zBAu!#UvgZ>o?o*=~l4-1@f6JF|(1T=qt63W7noC5cBi!S>- z)v!`N7I1H55i+S1|9cn9oSVQ*0kQU zD{oK{6)0q?rbB3v_}jF-!FI1=0N*2JH?-_1%(cxWBw_WiM&V>OlVoNBb=n@viyJZe z!#hf5a3-iZJj|}0uOfysYgNYfZvDfBW`=0Lf<3N+y&8q#kc=(v;cMj2iI-zu9t=Nq z)L!Xv2UGk!`0({^?M+{LQ(sU;cL=&NTZMZWl{TmQBna-QkulADwN3LTQ^n4B zZ7(p(!_3PGsV^p~H{5h+`}4oC_m)v@eciigfEFnQN|EA};!xZ*ZJ{^>cPkDlq_`8D z;uJ0JZl$;vFJ9aoO7R2@5Ym(1d*1(f@BMH-oICE9bH>RSke!k2owZlinrqIvp7}h> z6=|&o!$L2*8`P-J&LjlS+^$UJ|HvP|hg*zDdG?zTmL&dO^$Nz7d|O~NqAl}KmtPqB zj_tZV`^Hc<7o53g^6O745gi}vd@ji)=cxZXE<+NUri@k0y)0~evhC#O`Ice)3PHIG zE9q?!=-s=kz{bJ9u((vcG!GGt-i_Q$NHWro9Ax{No~RFmDIb!x~8s0@Mvnd!-~j z*b-m0XesWnmk%nk)mCp=QmBa=axVXJTdy^^P%ND}AG2}gCfqr0k1Q&tsh2GJM%f$c zKVN=NMM(&_Ba{NPdf{~m;owv3L`B=jpSkF1JmbdgK zG!>YqEkpr&v4=g&hnBBPlX3Ingdbz&>}2iFHYw07EakBOsuiNCT7NZ696ru3Pt}Sd-nQU#k zqJb~W{=_ko{v|=e(^4*ukj{6vbYQQWccHt=yoqm;5_{Fhj&-C>s$MY0 zj=T%xJO`4tl*B)?f&H+)vNU=<;$O@*7v4}uvHU%L-l11<+0DMf?T<#H8c%Aqf{0p` z)!nxd_2`uB-K68`Kh>RAj@4I+#}F^DQtK>ItN5M4GPRBB+uzT>ux38_f+xpCX(Pgg z`yS_$1%|F9eQDa5@U&dd<)Y(G97Pi_>Gcd5K}QzBu}MN(7(oiq6^BMI6e$8J{UhwN zJL9eAsYk5*iK}Pragh1TW$nZmvj;R8+RwNga|Xq zH89(4~U zJ@l8HA;;ZjZm9>P8B25FI(A#ER4tMv_^%7u8g?|RsS;E9zpo2>MNTAUv}KF)CI-MC zJ_d}paUfi;eGruSOc@Q8#eR$kcN=N1Y&sNQvL*xvtmX)u<-50STx0G>oiRn$!t#2 zJQN1tdO@#a5S(zV&Hc~2%0?hcJFR8=yUG>+>IR}`%iqnsT>JTq@XWyxQ||3x+PBrI z+pBARkD44M6+fKVh4I#7G=CK9FN@c#j$tz zu>yMai3A`}Rz=5k8nF_5;8lk`DpUx8)--tR{P3A*C_X$F*3m`j6~LJrV}^fhon~A= zv(*%JKeR#kK})#!gMh?J$1kGR+OMJZF~*#|VL^9MweN$M+*( zBM686TJhSzx}v+DVN;x+EX`)dTu)P)+sXLFcU8~E)G8Faq`%!=K(%BYn?Cnnz_cP3 zV6|>wB!RcpW32sMnY1lnCs)*-u}zWdw{cg^dT_{$>)Uc{b{%Hl~n_l z4}UaZ>&i$~R$?)^aF7`g>L=p;;P^y|Z;6B5L8LwfaXae%Hc8^7OK0%LuRYL+xI@$O zX>1Na3}^2X1uuQ}j@@jZp8J6ag?P?1Nl3D_T7h#)a!Qs}ZYOrtf_(mcGX|W6UU<}Q zuO%udhKR$^)vyP zVougFL@}jGRU8_F=M$;I$$81fB|AV%qm0aCDz}0?cXq{bcAna8r)Pc6gzvQgyJb5s z*I984)&^7g`5Yv#)l3JSlL~c5Om|y%UWhwOqhrO)lAq0m1L|_W(Iaznj!~AQ&H;x0p5Wm+u<%xUA<(q(9}(%L;?l z0=i+)NF+O)@dt^+`yZ-GqHZVhN**>XQ&)DTl+4Wr!g-m;C;XGvE_GpE#gU7guj_cm z*F}HTA|;^LMzaDx&Qlt*8Ev9Yg&9AX6iT}U_A0O+s+94@?WEc%%d8bmGmAZLM1!&HmjeeJ4%6Ivd39XuX;V33|<^3CC;n} zf-nVTU#m)z3{pfoPEi&H77o6vmv|K{|L5$O8C{bfy`3A`!3-}n>7Mmtht>9Wu}%3Y z78-LjHg)fuSoXGkopt28<>jMU$-=v26 z^)Kn8H)sVz8({3|?hrhr#4OkDJtQU{@6mEV{o(ypUe28Ba!5Xh7S52TG!qT`Cevt zNk|FUFg3Rb`F=*y6PEP;SI7IpmEl+RhP`%FXZ^|`vPI^-qWIU#y2VBK(bS`njvb?w zUb?5y+@VdTVpgT(ika#FJ2@I;^-8+v(tV;~OIp!~E|i6m@MuEbi~LpHeV8 z=6^eD_)XFE;z)~PH||E+-DM!1xg{aS+UOJceQ+hlc><*uv-gh&D)|1f$i1=OOBkA! zZ3v2k-Xr*W$}m$S@?Nef^GFE>b(1jA0eW$ zCUYF9409u?F7Pf{mvZfEib+s8j1W++l%o?}t^GQ(VfQ6|B#3_wz4Kt94$06r{I+mh zk1c;=$@2QY6yYKD4CV-`#?d%x;u2q)@lG*4n2cMLa3Momu#K7Sw8)&6@B+Ihvj6#k24I;o29kR-fw)&5I-%t}oep z)-qScY}mBN{i$e)vk4g$;QZFl#5FporX6!G!Yx1f7w`f}TjfXD3frZB{(7c&r{DR| z#jf%?8oj3p{T|TUmWmTmGB?oH_fTn026@UnmL597m^^qE-X${4(@9^{c7~@wN`D!%h>P|FQVu#{>4Oy!U zk2#9COGGNLYcghs4K3mu}LvRz`usH1q1YLZXKP~N? z3(sle8O+6nZe~FnatHi@%5(WMeterC9EV`#vJRQ;T#|#E>N5L5re~`MXfYQiIC&x0 zSd#bL&mj@Ph^*j&bmwRIkeCUqn^^ z6J~ay|Bs-U|9?txg~a}MlZdwZzo?l1lTa}sKUk*&NrI)i(fGAks1&qm14)BsM0~XU z5anmTLmvvNLaLy_74X6y@#4RLa6fr;t?p(-IEn7Gq{udc3Ah|U1vl$Q(=dPoUceqv z)kge~hrjz{$_S@Hk;g8tpRS8pq>>`r(R&x@I@=z9chJ*S#L6`u`gH(eL>Ut4aaQI9 zzW(x1+QIBn6Yv}vE@iZN(_>=oQo21OH-^?Ti6F*<<NCD8Eh0r@c0^M}-R zo>O2ELWbYfCHgO@9FaO_03@1wUIf!nOwr9#=Lr%3Zp+ zmO$Xa_IpwdI6ZZB3pyqFf$pLH&dPUSFchENNia*@#e1)NKICFertyK_e4@eBq^Z-t-rNN!wQI1gXn`Jfti zq+cSS-mmNZE9l|-3&_8Kt2U{KTy$Cna8@1I$DPS(a57`IaN1RRWm$^umVL8^hc;W| zhll{o^>`D`?Tz><=-bU)V#3C?KWN5kpVIz6yMx{6U*#0$ehFNM?VoU#>*p-)7$B76 zvV%914noWZa>}=ctC(&MC4{slgH6-eGpL+#NeGiDf97Xid_3fE^QR}o0M6qxQCxzhVbes&GO^>2v2O|Ca? z$CY`#t}|CVQ7DX}&%Uk*D>UQ=eP`nC^m@vL_Y2C43oJJIhsa__{yk_p|Ds*VBxs;XuAs~XH z_DTZH8xYjoiBJLJ;m@ziZQMuG&jhv}!7j z808aEOt1Yf8@1=-P%3(uaW{h7sgn#neSjZ$7U>6F4^S-4;t`=hd(g63ZyI~|*1dw6 z;(QkYl4P!s>p4>A;Q&P8_wMr-SW7spG?WP%eHM=fd#UF(L5JFbOTO2d4|!d{HUDf# z02z`E9uXx-ZJ~|Ev3fPSwHCs{6Z)9zwt1+ezZ4LxI(?<{9=i4Sg7mbdrWHSYN6|Ms z=!uvuXIvM77TpQZgS#84CIm&+K*3%iSaE#oIoGC;86&?*SEdmdV*e^e$zE=?lf=Ws z|6}5s(HV>$A1OHF#wq$CTJ@X(2tQaTy&M7+`_SeF3vp?JUa{E*Pohj5(UUp%9DLAC zfFQg;H%w)Y4;2TTf=+oO1rR9xUAxWbDy&8Wr?$1;LDDF_*DSn+hAi{ui$ZF5VOm0BS4&(Wa&|5R2m0@u0e%D3>+DR8d zYUH6V9zeB!0cOp&XMg-^)qnX_d7Glgq$|S5Iewz+2b?9?3XKTV+L&3{z=>M}7JKW~ z<=txBPX8wK`U?P-mX@lC6LEAB9hstX_q&Y_E|>?Z=1Xrn?lEoy=y$ZiEpUR`JNdtW z+UDAsi-Vl2p<(b}z!Cju?xDZ4@jf5A!qkK2K`kJiadzPph@_rFr-?9u-+)L`v#s%s zXd%~PKfH(HiEJ3wj*+nm8YPpa03kih?|Sa1GTUs!gDCCzq?6mv&y5r2_hDyM+$tO? znn;gozUl#%0k;g=vdkQ2u0$>V0!*A8fPJ^nEcLk=lesua%{FjbGb$0iI11s5 z!kdFTKw^R4=#RDF4!`eq3}#Ubs8Jwhhfb}7Uj2rmtNXSiXsbJ-2rhvJl$2Ts!OLol zV|F+&6WD4cVUuGZgs9eb^M{xMV{pc4&|&a^92~E-J#-goXC$du>(icjkll zQOr~uaaX{IZ^F2P3%ZYkhR*Hj`=W>4@i|A!%*j2;yZ?0W$E)ph@0fLNz;uD1sYC|i zS6BylHw=`k5nAYFMUXyRxa8MU!$WRROotf*hTeXX@!VxL9%2wpGTO}Ta&il$j{r6b zDV9V*UZDvC>AD>rKr_u{NKRwITGP=zYlIODs51JEljzGPt$S6jfIV{n*zme(RJG}< zbQg0+iQu1YD1B4_nsmDDF8UsutuTCo9qNzX4&u=h?ywD7 zIrM+<(RCaOX2}pMNrl|IqsPV0(8bmBc13rO`$^p}GVrBxDWtDCZ5VPM+poT{>U@vJ zb)m7BL(RzBtAm-`gGr%HG+9>ja#7~-x5<6zOtV@4&H{SSWE=}Zqv)98c23gJHa^bb z+e6v%nlZXFc^)FJCy_OmmqSNUNPa~7y)yC>Vzk2H+VnNNdLTXmyS&VB#S7Hqu>p&= z!|uD+>E7iKd?WN={!H6ZB9cR&m!QE^grk4+xmSeS+hY^?Ke92VA3yF6-~r*_u2#`na$9PdP$7@FI5xUP}VE(vqRsKvkjqef|O>&&(*7 z;=jYuOI6+_(5C6&4ys1*(Eu@OI@RsE54r~4USA`fXK1!tI-4`^3;cG*h0yj&ZJ`Y* z*j$`(pLFP!u~&O%wJ`(yI(f8mHN?pzvy$7#dPl#1yP)%U_ip!+1A2KpIU|G+7%wh@0>Q*m)qdWoKG(A*L*wnBGb z?obE{XezGmntS+wE*V%xDnxJdAc=jixiY|E;_<WpCNo9;`OWD zMCW`%d!ig^6yE^ZE!t*a7X{4bV8c9-B&-7{EO<`d$686^dn2Uvv|D9>w8;xS*iau4 zF_Q2%^wWANGV~0TpHa@SDBa$9pm)G>{R71@c z8fit8{{p^rtHV7?++!L0`>}!w5T6ehU%CO|3J-oP8(YQMRcUF7<9`7!kq;G%hVgyK ztL^SW9t1n<4{-MjKgoK;%4WNAB+?NUN6CHF?u!|)u?-9lz0Fu_@w99~*wrJV9veSy z20fE4BW&44p{aB&RiUNh-nbW+EAfb{8Mj})(4UNST|2GFqmrol)L4fT=$E@Djf@ZV ze&u@ht^>?WDTy8jbZLK@nli4=jijhYnmaStpss%bK^v1bJ~`f};DkB1)`EN6l&4q^ zFHj~mT^;+~2fucr?pT1>>n6qlhZo1Ze*u#dM0d8glclC!wNkT^)Z3NiEce)%(v<|A z3(m9tyxs>3=e2Jo1n&jWme*IfdIQuAnxhZA3#cz#b9k0}!o}_7c-lyWxSkX2zT|!w zx;-VZ|h#`6^|lrk2V3``)Z`f}Fu^I_Px3d_Z>3kg+=|1*A-s8^tpi zzqy8^0}ro(?ydCsn3nq^c4l%2`Fdz}$SxY##A%(@+cw(HWc6n`We6X zCD}as_on%7f;+C)J2nEVm~TV}-!S?6%->u;<)go%TZO)ZP6$MuNqrQD>NGYsZLsAK zIp>W(0Pp?+N*8KX^hCV##(#hE_o0W^7V!v@+@Jk3PoMeMotV{|!DP|=%K^>kg^(LD z$J!5#rgEw*MD+u){nTC}GOK@7TbB8`!%vgyfmle^3Zk!ee38T`PIRG}5dhXMza{fp znhP$Hz4f-8W#Xfc9fw8GdzuDa1 z{kiQXN|G50his=-!#4Ec+QREJu!t~gIDL3SSnyxKz2%Q8Kj944a6iqNeUlK`oE>v* zT?=$6QeO@wY(MOh;6)!DJ*+FG2u&`Qp_ZQS1(p4ESuLUT>ynm8v97spTF``AUOt?Q zUS;Ov%jV{W>8&WX*E?q16w^`_Pc9}&R=P${FE6f;k`qxzUbbP|ZWphT#3pc=;#>rV z%776ww?7{$qyaK8tnK;Y5(E3`+z3-HG^RnM?^kMd)5Pe#ibyn)JMUbtuX}kjt_9Y7 z{UoVX4Oj#qOKQQ&ous(Dtj*}Q?+#8yFNgu8N2bp%!XiFzTFrM*S8kq8cjy%3N#;o| zkx-d7M<_-vlWY}e=(bXjqtSr}k|F~fx!(_QU7pNSkZo;9gdz5J| zzn=mEZB_B;(yHvFx$0phty?Bs-QnWith$@EQKfWJwq>5nl1^bZdA-7@ghkP4$i zU7zY*a(;AJ1-~hW_Rk#Cf@!RwEEI>CE{?LU z_NxOHkg_oP)885P-M{YO(R4tbyi4iE+j@A&J@(yP)YaRZ4 zH{ztz@hincS9jZeKb-!4V0Br)c2ruE3gnQ~*xa}rofLWSMBZDXBvEJ!fcA&8jO~Et zarS7}csBQ9m=%d4yB3Ll{qeHpcg9~pI@%?cn=Cy*NBxGI-L{iUX*-~k{d;AE-nITi z&Lx6J61a1mZKh}r@9J`}O~cmpL0 zCv&B;JIx)^z3bP+P*z*Dk>cU7*4Wtf7myvdg2F>LvPB+94adIl8OVc7iIuF|jt=@8uVVYd3E=I?>$*`%NFMd2!-O+|3W(52TcOHJ& z2ufJ{wGBcBcCjha!}G*G02`qL>+UmzhWRmW#Xi}v&Pgjicr(^zj^C{@-D5(b4#ea!`#mx81=^zcp(b*$AF{e|4qQq?wsu_05xIjR@b|3I z(rjKAdVC^LC}zJma~n5p&7YC-l59Hh={VsXLXZLksCN`6Oj67qLbOUjF(~>Mz|sQN zeR%#I)u|a!TObXh{Cqc-)V|kU*jA5rHkIwTNHyLLl76LNEb(Wz%Ogq+5DggFcoHD4 zZ<0&=C()jcEo02k0CCngbT2WWD`UR+G>Xz)$C!-KEY6zp@h8060s44o9dzI<@Grn= z!O_jA_H)4FCRS-7m`REII6a8y6?8yn%YCA?kYq2_5z-gYzfKdW65yvH5LSDj^2`T{ z4vY*f9S`DbM29YgI-i_|Ou3q=n%-^f*F(dH$^$3hUvUau4DHsY4Y}y5eYkiWg@Ezh z&Gy}`IlYdA{vztX=l5mifHRZEP_S)A#`Zk847F4|4y*;u+#ALDLrV=GP96%;xua|| zY-x$E;_YmJG4ds%T>POHMFM?n_-YC4iMZc*CdrtB&M?JdD-ykqhHQISV*^QE@nXTy zG$_X__ok>x6dxK!3#P~SlZW$zC>%Vm*B`?nJw`Iy{zLu3bsGzh^38sn)TN|L zs#I=;J;nF>X459sYyw#yLB*Hfh(5jN>IbuIUUsfEDK`7N9l6&;%T6YBy#|XqySY!e zCBB&weA^@a5>Ag~q$8uPyyHbMqerzfA|FnEPV@pbmjle)stIxF{f#Dg=~>e|$>C}@ zm9g)7ji?KGh|vqy+nT_8v@@_oQM<|$Y*t^|16r!=1;Qfyh!ECWkZ^Hw-NW;ZK5@Ia zd;RY7-3uXFu&#BIDcrO|5ink>EZLvMNl?K_1ldDCdqf3Hnfm0{`hX{_C5OXO!ktOC z()T9xqpZhMXa{%KPXkdZlokVZ+=z7b(4w3-*-2-4#lpiquwgBBQ9Tc4$<`U$SSX+S>uf`%OI(|Ijbl@kU0y3#bXM9w zrT%HyD{rnVqFqX5`yX}y00~`eRy!J;bIB5sy3tU+B;p8+T&@vd?R|ER+n-G2CtZ%Y z*jeZ*X@tWoGr7)Ya^mQ0=*lVowDHjFr-!}mH5K@GPYD1(^3d`wV8ega<$btU6VQtv$icW1rUmTFZ37#NR2h;#W&W*ICc*+r4ze`9 zR2Y<&7pkKE3XXQFtCjQXYkcXIH2PeLeKKF2-@x_THO&v#$)=<#a7Eo; zBhnGyEsCV&(>2v-uJXvkpX5}3{7t_VVvr=U=bg$kMxBjHtD|FdOZ*x9PMwIqbyqA= z9n1Mo(zokDNS}lBkE0uT{*63M%v|x+bI>8qPm9mSk{jz$tA`;#8Au91KpdnT}~q^d=qPjcdHmg_vXy zb zyL{x=yCn&ay|D||v2@7dFH%(va(Pn_w`QH(>pLP7s~`AEV1^{7aX14TV+nVCdcDQG zIYf+rNGm+R|F+Uly-9AHkrTc*mqI={u_`Gbz3?_yQ~aiWp7s`$eThQ>OC3yAu)NsxI>I}t1De^2Tu}gkZ-L`pz`S;dQlv2mn9o1it|W9u zaaxr*ls#wCH<*!M3o&W3J|7Ejc&tRLD#xhuNZrYY;fcglsocBgE=^G@!;6g!5o4*= z7NOtAdg)DCnb(AklOOY=r!7 z%xlbr-$1=T_|C+>WHn+poAuQVX^#7N$U=j;KH-~xE(&-85*GJW^X!YNnR19hC8xwo z$cn|pkBS=S;BaX`S5OY%6*crl7FOA-l8c#*(dv_lSM?O0TzV9}7}p=N@ynni+* z5)+jsoB4R?O3PZ=$;Y2}TcL~jKmB#W+nJBlT)&L!=(x0ra|p`@(!br4%0KC=#~;M< zK1kC%5nC(0D$Ze}HcH)@eLDQ`*0PB#^{%+8D)C~rWAbI9jBABgaUgjh_WXS+B53X8 zdGph89$KkRw}o=yvH5AB;?=Xkl`{?JG5=P*@>SDFtM#JqN3E^DDNPPe3#@A5SmcB* z?ZK_ipYE8z%r?ZC>puQo2hEy}VsoYEDMsE+MuReQNV$=*v^g#*0T;ryJxRt|x9vVn(WxdM$S9_<&v zq&juMCZUrRL}Rb>ueueL$pj*bH*>P4&&A4%pFQz^%^&H=p(Iwo`T=V(tiku^pvo$a zWQW^1d}cjwMIc&!>q^5n(dTmp)kMDsn?%TureD%cHjT+}Z%1BNofkG4oYIL)Gi*0+B+%?Dy`>SVZ&!e z=O1UU2uJ|uzApR)3@*B4^f`|Oge+C_cP&9k)z7RRNeKR#7Z?r#H_e3q!uOF$jO$%U z^ncB*x%^gh3Pk*g>+O&7S5{*O3mOn9u?};Cy;6}zf2mKMeGWbNT^3tiKRlM5Iffhw zi0;4O`a%@)j{|=H+`z;6K8EtR4$S7{rfU@HWrIi{_D$)vKyzY zjZZP#bLoa%@!AU?2vVc}0*vl_9Yqn*T;FDYz5)Axxmjkkf)&z8oVXhssY-e-Sdb@p z;=IE z@2Q$B|3<6mLXHTgZd7(Xr1B%LG=EL=N8y|Y(_DMv78Fn$S!O#|lh+gUN`waUkm|n( zT!<#Nn*o?Ih!O-g7m1nn-4`RNU0`s>oM|xqRHi~vBe=;Djcz0OUA9^qZTAX)!@l{6 zev|SSQ~Mg(6DZZR>gF&=Rn@yZ*mv&^yqg-?MW7tlL&I~LKm)}y&I`iL!D(fWT_PYL zKg8qQFAhr>ooN%0kH0>u9$VpacxT3Ot;R9ra1Jt+OvnoHo{u(Ev{C>(wlX7+D-P%Q zg{5V(BZS}IFth~d&}w;;_w2pBOUAUaLCx%xqMlUS2JK^Rpz=l9sH13c@yTH=iyyg< zUFchqe7mNu3b}(=Mq5Pov2Z3a8(^+sI`G`!HD=DYbKL2neMG}vrEx(#6QNW*>?PhS zUL0If5zb?eFY+Wg#y)zE88xt;5|1k}Pa)4iX#58mrMsnr#SsN$3%6`_-n23W6&~)Z z7nc%hBM10fC0>ZRc&k48+%0Y2;OMk8YE#7_`_L9)Zb3{>TZt@(pNeU3v>n)d_#gzf z;9^TVwzgL97h_jlQ+!v?SIQ{g;}8CnJ)ciw`G!@uw8~Q8);xK=Pc-)Ht1`{YK&@#d zi>lZy80#adI&;Z>vy#S$p<1 z^K(FhkyXo_%u_dY$myq*D#73N40Uf_HhNrP_>v=qQ~mW?;LY+_yh{bV0_!Wn39yoOb58Ph3FljJ@3Dp-V-(Fy-2Qwir2Wc# zxRP&S#_;%)ajZTKL-396s%qWD_6H$^-oSd7c~3CuGf(TObkapK|M@`erluj@gLm!}H!yc_4gS36@#IVJ9UyAojj=3duKW5NSzpBC(|UFGZK~ z`~vGPY>_k(c^^Bb!qH;SP!o!#)5^o2e0N5BEHd#6J1M=oVmRd0J)B^^iYHTg;i(yw zLXOl~2g}zjkeq|NXyPK^~ zq^=M!CmnU^A3(HQ<=744=H+15*^g) zda0belSUg`AS&$?AYuDV?o|05gt;GUuOz~8{V<9|!+YT6t?!G?4g-6q%Oz7rgAbLT zBlQi6PJ$O6s_zxjftzE?F#*BfjCsd7+L`?+V-yB<`xavZi>Qe<9mSR@9@54qXWl#t z>Y+RP3wYyS!d3cd6?4@oo%uCpMlNj4QE%aVI(ZmR8>W7+SR zi2O1#U3|a8dLoQQU8B%PY5j(<)0t+4hl*hbe=9f!3HcPYjCpH5X2+L>csZ^?10e0l z3dlKxuh*f%N{vDkzLN%fh&5el_GuXoWFFkx(@q6Y5PHuroA5&yAYuT0$^YpHDf z@W+l2cWQA;0)5`r_YGUIllNPPDghatv-%9U$JM@T7TSb3 zTL{c^nkgJsq~OS(Bvj!0kt2XyI)Xy#`}&Wrmp&w)`O;CpMOSTC7hKYW{CJbML}y>U z`I2J(1c$Z-cH5^VZKLu2Lzk|2M5*go884^ftKc3k7bl&i#6=|Dh8>0Tv#nuYyDMR!i^RY7qGI=8LnWtk(n zUwRgV^yXXVOvcXU^jO?X)Ow5)C{2vr8ur$)ZC*USw3#Z*{$RWbL*62<5F^I!A^k&~ zhph>asYMZg@dO9xkjTSKZFOEc=+sy;Hxrp-$GhVHDr!jvmw*I zT&lJ-O+3i)VBoby5^1AG*|rs$S2;`zEv!L*^Y-MP9_?du5d$zVA7f(SJpQkg4kiHW z*>jSYycmxd1z-%X`1s$-XpjoN(fX4AFBcmN0|SUL@G7ZhfW@9PO4$m1NE{Amn7#RG zOeb2}(EDpxJcrfX9ofJ)xnLcHVCa`W2>s>B8>k4l3d%nJbZBnf?JTzHgb|jMbjb0t z$!*jX{t}5*``oq4j6dbEM~Xp zPREP)%1^lJ!=1BKr9((yQ6h$&$f^EA6|mBdu*m!KqN9fAmg0ip`@qICsqs!#oaZ|q z8s9C)V!9ff;jrh4r%e{rOmodd73PXt;INlWls*@tdgNOKY4Kj+k}>UvJg?fH0HM2v z@1L%Dgzg(`oPS3qSOCmN=&r$dM$E{|AiyUm^O%(EjRuVWt-6-^hcEx?9?XBb2Zhah za>^QVJsV2@Gcce8H1d|?$vc7MkL8&s2};2UFL2uXM>Bn@TJ)NIUDi| zW`sc}_g%RG>_PcWlIwSNq2*4+$U2p%mou-zuU2=&_O~dRRH7J3{&`h*`}N`^KF*Kd zMzrKTapV^nld%b?ibr!|QREj{3qRe3Kj(gNS>5RFlKCfE!^FgWgo%yw-+K)6pYV;Brnn9=p7@krUj`B)0?+4A3_8)v|NJ|3kB7+EtBeHor~(GW_JD^!eIk3?}|rR zWas8~NTP(N+TKy_(oHWFgonqa>fbpNPNHR+3HZ2p38xL6lNDMpr7D89%CX9$*tV19 zuX9(4nyZ3&))j*!QWFYORvJq|v}Eif0cFZmyi*tRnn8?-^~%a@iUXl3`*6@~i{VKB zg7sr-rgJ1GYRK5hORAaREN=|Bi@z+o9S?HJj z%}A$lMUI&ErBe#={o`MG1Rj8Mk*(T6g$*BJd~V6se*tUMU0-WTW4g|s51E;9X}$Ou zbuZjj@3i6kWBD+nE7f~azSqdWuJDhLs-UYZnZ#4gzUUa~l2c7Aqt)yq2O}QEp}&CS ztwIM7m8hG^n~%QEtcY6#ekGn#Q({&Pl)ql&i@WV!+ooTnyv}>!Ct{`X+jTG2${Y?@ zhZr}X_nV-6JFSh?>wQv&R5V5cvXm6u{AqMR%NmJa@*PHa6S_{d_jO7*38EW{O$AQO zW|c!JJCG|zLHxnra}@Ur;wv&=1eTWsSKyRW07}E6V?g86WWgVCK&ni^N7 z?u)k3L;&(~5-ZqkDMBC=*Ys)cDHO&oo`!_c-WMly#19Cbi{P}H)4i)Yf-UrqEDGpR zJB;W4{Nd=Ku1KKR6q{&Lt<85cSPnk0!cP1!@QvNi8ClCIKUKF!rb{E51Tip~k!XHo zmBG7L$?S2W>%StwKW@zZ{c->Ny4PrXD}fGsM}rILr)T5bytt^FUiG}kj zfDB`;{L`e1r=*Qp?9I!3AER5b1e_D#P6pB!4%^_GaO>XVu4#jiQ*+^V4ts3xNjXQ4 z`Wg9O4yjG4UQN>jpYsL=sju8Uo5t;jhffD|qWudAy;6od)T-z8Zy=qSujdBRmfV}lzZ3Vb#eJIFu|D0uE4t|83{Byvs76CD^qZ?B=ht-)uU@x8 zB*dH3$DM^{VEmp~X5dni_`O%h+B1%&6;o)wQ$rO~3=W?WB{aH}aezzEdWihQDSpTz zHJ6-zL<8AAE?ND9DT8lUT2A;b94tI8YV=BDGZQpIT zN`79HaznB|<9(SrPN~a5#$=#Z9ZL*#^RS27eC&L!cyPy&KoTQoPz6B#j z56ua)X;lU~NqXWJS0C1JhrYZ7d*#SWaAi_k-lpQgEx8%gdh1t5M~97*TAKlP$pW|r zb>L}DiF(uE+J|BzZICC`NY7tD#p+UoP=oLeH#ZN{ZuHlvm9^CeCM7XkD%Lozv?t?t zkpj)7!;_v;Z_^?UwcVx!jHB_`EMN9M7>@VD919bL3?XBctUQOKHRG9y4(XY&nc)q=M>DPitGN|e!S(UZ=)(>*fOOh6W7TC zOFWvR&4B*COxi?ce)BDbu>Z=f6Tf|WdlIEkuk_p&_9No<%Dd*8>UNv)VIh@=;%Q|p z7tpKn@|O>S`k{^E+8~=qd$j9f-OrvbXuh_p_#IAmBcO{`vR3$6Vzb})MPR?7uRm}j zF>Ga~Gv}zJ{HvsNzAA+a9W4m=DzT6s?1+{Tu=Q-hO--oiTz1+EG(1&yRb}F)SEAcu)K!qk)~>%LNlSKo>*mS0%dqB*fJqUVJZ*J2 zmyc#9-SdnT!HtiR6*f*OL9I`|k6Tyrund>l97OUrDeQDN8tH8K9OPsBa^ma%)M~G+ zN})Mjz<3Rudq;2$4*zU5;$WIPqR}b>fTPFHXw&hKQ!?q~So9MqcIwvSdo4xSWMFi; zamM8Cqn8TdvXE4K1 zM;s_u2=+YXUep;1=v}IfOt0r=n9F6hnNy)}NafR~t=fHKl&CvD-Hy{guT1>n_`2fv zya6yRwPtO!ISG;YBqRJAxJZPY; zyyHyxhq`Esm{8v-c|ng7I6g6pB!I=bIr=uQ`JvQ z#L>=My+&#NYKK0(Pm7|%gfA0DDIAqc-koR)488tF+9LZ6hPm~-wNe?2`aY>8R72c9 zW*o_R=`ymxQTNXAg4_F6800@6#}ZI8=VIe6UdQS`DqI@somiB5h80 zIe}(Hx5RIa>1K()zc2V!rI5z05|4&G;`%9B&XDC?M@Yu3%J!k)gqREwUv3h>UQ;7E z7ip5JRxMy6At+WVNWSz!t^`JtPw;_g z3IqGUZE@-8=^E75S!dXK)^i)w_ShWtJ%w`EM5PAsD3rJeM0WND9zJEhP4Xw@?hx&l zu5!*4sEZ?GNt|+SuV)Q6E{QKUxP9H~(DvU*T%wM}=WCYHa;*fN;5eyc z-yxX|Im)bB(j|lfXwO*4iR8hJTdJj=# z8KQAP4u8|^=Uz{-`PkL5sp{d*91tIy0nzlrT-i&ry}*Iqh<^0w5jHxa`LCS^jF+%y#JqfO-UfYP(D;yF*Q4&d1)uu2Uy1=_=%s&( zQL6NIjEHCq^+dzUJoLX&_trsibX~vb;4Xu^yAQ6x-8E=%_u%gCuEB%b;1CGz?!g@r z0>NFLJ9*yse&^J!x__TKt7^KsXLs-3)3f&4tC#$i={Q9#=)NlJkaM-Gs3Eamj96EhMq z#o@MMG1^$U5%gjUibq5U7pePZSV1SEms~L#XL@we_h9iILG((np!Dy-Cpeu!d?HAe z&89kstIAq&G#U_fN-$14V|FAjY58}qBGwILVF%bw2Xi53{6zQ}F(#{i5Z>n9D40ZI z!c^u-jLVY9P1+>O3r{tyN6Sc8VY>B@!v?#tJJvj4Rvi)AgA^H(!{FTvuX-?#rbjPz z)-F?@)2Kjc`>{{W6gQ?5W@p%(-yv@8W?!d$B#*92D4Tbg#$O|cA~U&*(yUH?Xw(Kp z?MjICV%>n9+10D?u_DlowUDom8}17rodZFh0j9|lAv)N3dr;vglFM_Wl^=?!d?LVVX+Y=}MDw)P?xY95kQ7g(P=v7A(F%@`biHlD@}8A0K2^A1YEI6@D> za(N)S5W)zbk1x5QFmUURS2``tnV6V!JJvR^#3dWS=rhQKYGs9)vrV%hm_Bw3PKK9e zj&1bkt(b7o97yf6O$7rZ=0S;S^DI)+?Ko0^s-@Cj}r2$=%`sywmBTvJvdXNT~F zNE{@mF@zS{%XG-b_h;M zqSMIbvVj>F%_kgZ)E>B&DjGQnHYG6J!7Rf7Ya|v8t|7X|qA!bQgBP`F#q*LBiU`i2 zlk)E?XELL6T7fSK;gu8?+i0U_xRuPeFx}uC->RJGAr9FlrlyV%I)U%m6##RACuO)Z z+)_r2=2KLNhhg6nBc^Zb6cg|jYHb`4PdKKVM&H=Z@m}mo+Isg(tmrd9~X#jnKPTfT4T{TfS$QM(h5(G4)hv3m7<}G$fNbT1sX4M2k7&f zZ7>rK@Q-J+s4IdMBc~ z9ZyMj@kIYwU#GT-t2Rp|`K)Vk83yAmWZ}u3hwKFdiXexOtC6Ck0bu~Kza2qG z0{|4^M9=_}%AsIr06+{j*c9SwCIAFehzSRaVFHSvDG(`Gp!*=#Qv#x)p#TJk3;+tq zQF0h;9{?Hvi|7i#LIhBNOeko>A)_NmE)s*>7*>o1at?r_pJF5#761zk`3`bJ2#o*` z`vJ&+N(ym^PZ&ZW(3pU10JMZSY>OCVC=0N#e@BQ4g$W%455zaLNW%8)^jtMRnIKD&O_D(0H%0%Q_3@-^G*C4T3@jy?#ebZB zqtw};p__FM)1m$34qe(0R}L-8-%+MZ6<$Oi_EdwE!2*0?%QRs_ly`4FM@VKG7f*?l z-C5Oi#m@ciwN3W}RZN=;!>>g0_ibM#tTH=(6Ea#Z9VBt9xmsD0y83a?pB$D&r8yuT zn`xrK_UP*Ew1jE&7(V-e5B--t<{?nA zMw4Uy-0`-bleV6=Lgl!7e*>~nrXIfDRpn*FmOa5zAYC_4{D^JHL*1QAbVnxK8%?kxwZ%3@c6HC&Po;J5oQ6J2n<>YZ%N{9yNCQ>2pGZ zHMy(F+KhOs=h9jv9ECAWGh@|GI?{pJNs?HGRWd;Hu zmPvRxH~`GQ^93@4fs`Nsgn3;d5&eg$bRx%_nFb@B<$P2I{JJ1=P$OtP(QQe%c6t)kv7ts?O#J916vET^ zjzf|NJt?{H$CX|LkDuU7+*G;^G9ETZyj)-bW2R8lzDg!dO)wmSO8)@bE(^a?(x5sp z6#*LI0f2+9r>>>)80G=Fd|5g;hsI2}J?fW-{_2TdFQP|pm?RM}nUtFE@2A)O3JO|k z*4H=y$Q&-XF$Gb3E5U3~02_4=X4F*dcToAaQ6OZ%Dt-y;Ya16qV8fPME3P*1OHd=hS0y_^oZKQTRjc`^BW zMB~(P2N}7<%`dqX>rkRpE15260k<1`K8sB*?+$IAe5?60F$BpZvLR^&PGT?+52AJS zS`Z)574y);TAAMe))CCXXUYB?k zSq6ug{|~diCFSt);@D0CYnZopOPGvK0(9B-fijKO9o!^Z@oKWY?WSd zh9&?V9qe@cFcIzO^-b+0AGEAjT0)KkfzYgQTX~ML8SY;k_(u*LuqJQyazgt5XRJd-sV!zJov4h!l@NT#KY5vT`Mu8gPYKk`!wOJr~Y+POrVg>3>dP6V9aixgL z$pz~#BRtcT086noWaO4dk_eb8p6FitJd3)88Kzujal?9jyWWY4CH->W5Gl`wbN#dt zQA|_%4*7sYdcC#Xwc}s+6M}9J+fKEMIznW(BhL0S@Ult#HT@|F+?+PW1~E$*&s zi$YgGS(=+x+q~(@Q8KfzkJ-^_@*bUnP_hee$0Y-WZEfq2KXbc2l(y=fnAa*|&9z~l zLfwO^yrb6&o?rs3XdTKNd27$nOhxq%fPU?>*g-)Gqzt?pwK<8!7&ZYnHQsVtQMb1H zfXeFa*B%k|hS!|}g|2EEsLJxP_C6kHS`<{%##m8%5kq1n^BF7Qe8y(?Yu2CzNBvO@ zJAt*Y9DO)+e1l2FlJ}-~AYf>){Vh8`B9TiRS2t9%m5osrQQ&_@RoUGk-1G8GnDND+Gm3OKXZeEr7jFpbi5U_4VFHmJOO z=bdWyo0Hox7g3tyiyfA%>w!a+u*q{zGx~Dxh@Ik(Y^q*W`jQ;_VV?S4@|O0BYMwof zy${>53m(;9oI&=5S!AVPN6Di}W!b(om^^m@)RgX^RP)l%TAKJu1c^IpZt>TC`c!j#6%%{?W8B|C|HQ9JK6Kx4T z^iIT=81Zh3k|b8tWn)g>{(?J`gF;-cnw9}g{G!06zBDbf@j2VnR{z0Ap%Vti@h(mj z$cl;u((W2BhK5p0onRN!jZ>pgQzvyb<$Fm&)ooFzB5Jk~gZFoej$Wy_SzbLDQ}bN0 z=uBG~Wl@dEQ#0?GksoD-6hOtXFecS%bG9%5tyVphxXn9-` zXB|rdW1uTxO1%1oyV)k6v`SQcv@xjnI0awbo+x}JCBf^bylVn6w@{{M~` zeCPQgTy4);W|o@27sZ>`gNH*k2Jh1pNj#w_;$mevvceY5Gz0poWIO`(@cMV8AL3lw zXR9&{tN{3)4SpW=e*iS$Ql;`7oV?8DuednhqN6MA`5GhQd(rEAA*1z?%oCYGqype6 z2b97)z+8u*R;IZY3Pzb!{ghb0nux{mm^>@ljVbP#(2Ixt;XCX|CrEeBZTJ1h;Y-|o zndFJeTM2E2whOs!cy9(4e2IstTF!?gn|3}cga|H-z#USfTOpvb=u1#|~;32**En0ckRj{MMhX(lKi z@c9G*D$${hs#ewxahu$#Ox<$l1jAJ5^Uae#dsy8#C1X|80>$!ZkRo3^hFtD}Sbq(# za)zWN208O2p3$hH`mU*kyB)uNmE|`Nlbz23CNXi)#x+JC@e)20sHZY-MT?ATP`7b0 zq)_I_>&s%S@UdQ=MtukiMjRL_$FiW@+Bx}XoCyouV4yELwR;;mt@cQqSPK4gCQql8 zT4^1EF!Lpu`WLg4LDbWwc};Py)%_>_sJ<5U9eJb!9Mn7SP)xSW>4Bjv!qLHgs&eF% z9mm0agU4+N(`~Z>t=SnE3y0;_F$EFDKCSOjU{6if=|>CpI;_8U_}xrO8czovgT1>ER=m;n+)k|Cy%A!$ zd>cD*>-iRIeklm&T(B&PUhHxJwyBnkIF0)`oTV{FuZky~+}Y*P;Si={+vLY_$*-T{#bkJ{8gnBz=r;y{j;N;gfI7!C zJjy|sovVApWXf^-X;zEy1`A5D3^T8=8_@wm!ae_?&|)i&Hl*ZHD>~&_(g{afY#NQt zUKO%IiirLLmeI7aw=88;sa`bZyf1$RVEv;*DZ$l*4(_HBMaa0}8Z4TkOsyjU+L;>X za$sZ>2B&jMbSIzm4D@hj{d&+BBWUc(?HgZiqLB}5b=jL+!iP<2e|oaLvS?^Ro3Et$ zgbB9Vp?!S=32P9*o*Jz2-BBU?m)d6>u;|8mnG96%WGkM!DC*)dvSS&x!ul3gK}k)A zVQSo_6EVQXOMGcT|7i@&ek8uj4*1GNY@tBIvDOk%9~7ePCwdH78`;jn5Xg}8GP+to z_*3sN2A4vg!u32wNQ(;&17V)s#i~?^xXWou4!l7(coBTLjZ7t^Z9llgE@@PXq5J3U zN5FQSHJBdmEarl{o5E>sS6<5y;mHikW<)~}re_TsA`2>-JDWF#LQYI%o`F9piYFDk z6_?^Fpc?h1kRT*2@kOI=F)=z~6;t>GWr2x%EZy`a>y%+`ikh0*l~6h%EuCKld*NsQ zE%}f$JZt6cS4j>ty-bC@Z`|f21IuZ&f6ny%czNygqD%9nCO#LL^;lw|H42jBVQ4|r zLCVC{a+|Cnsv@hi&ZmeNRwGDUBO=9^O82QUAj*}0xPcHFBL_Zk)Xf^|;j0sW znogyVM8=)gTMoC`CdcmQWKVo)GV_q%1jPM>yt#iy{;m&4PL86u_Myzv)ZjC73z95h z`qFVpQ&H%sfu5`8r1R08)-{hz$_XZoYj3?fwt2ear z_@^E-jQWe}r&jeJm&5>a3sua)EqLJ%PK@8WVPwZycc;_;qu}6%qpMF9N1@_@dOka2 ze7Ihi(rsGaP!gI<#cxLZqpWQ+;bV5t8<7{5RD0V*RHxj9b5FF~!4Qr3xAcI%0*KrPiKzQNm+`d`dtTS;+@E(#q)3q z76!m_{xV-fE>uCs#x<0GXNDzho_m$0xWnfPV)|1Is|jE2C5rfCwx4E~M?ZIdc%&Fk z>3s$R!(Nk0VC}dvaonh4G^ylc>hQp}a?dy_Hl0<}+Evl@65Vz!9_HiE#{JX2|4cub zI1Q&LJ33{8c2s`!U-Pm3@BRdQt~uC}a8$&%DiRimz&8K1*Ju^JYUxGU2AHVE)>w1{ z-Ujx@Arm_OZ$*OxibFlnm+4>^$KmLp0cb+-6#wrSz0jXn9lVuRsyq^AOnKU<1IHS) z<2CZYG-`Gv^N@l97^!w=cpN1yP5u>jbR%MAj#STrQV4j?3qkCKC?6l{Mb-VZK2iYx zw_{iq`JZox!tjSx#SA$Oj3gn#Sae@*07u^3g?q8g%Vz?D`{z}F(0XHQd^i*C?ujc* z86?Z23R;LZpwvcn&5dKNgaZ8%q#IdG{*SF8@WcVsCY{f`r7QK z4B%m@b1mwDu85%`{!%yO(q3H@z8?J>D^O=iX?Q$eE>&=T-jW>Ri4a%2!j5+`-uj=i z?yfB@jrnnxJMF^cKa8?d<8P9aoqhaD7w$i0M$|5yor;2HwXNk3m@;(~FZHJu zm{TgH`T6J<`oUi2}%|c19S< zAC<0X?EBk@l3{X95Cp(TwV z?sF)u%5R}s-Jy2)^?Eh%kAc!Sf%08xHX9CJjwIp<9O@H4?dIUVejr0K^FQ z&EyiVgPKSog{03~psLD+p53+ve`UeOyOufrhtjA?n9`S73y_U!47?gqv4vCrETtA@ zotv->2~>+}8q6Zn>?AA9k;;_R%ix(A7cp&TfeeGhbxAEWN3DOE#m8X(se%cj64Lv6$Ip18a`23y8{fKM%ePDHqe3z{0UDmTccaI?fBB>q$uZRKA2Ed=0l1!@Wv zb^9Cz9o_sKgAoMNw#_DP*}+lF8%D?MG`+?!psxn}?K>EHE50I6|IMDny!ca8Omt)S zD0C0MA1o3+0exgVE~lSftSk#mgnk6Cu&f*n-C|5O7{;Tj(nEabuX%6OU3nN6CZ6XRhD=CU z4~HJ`oHV*y#J2c|WP<*ALVs+lnR<*L`Wiml@(+^X7==0`4|H4!NAqHQ87VqAuG&wT zp-Y7DZL&!)+Qs)e#lh8g5d<OfFN%F4pdK?!v!c~cI>z`{yq5mcO~z%s%IU5|K%#BHD~oP1 z(X?`BiYM~W*y)J12vmlyB9BV%IXf|dn=K*PxbPrF{&XfM!+{QuAysMIUel4VCuVte zbEAznduwqAOFUJ!Rf3i|GYJ})(??-Y!->U*4ErCzAXpObhT`Hq>;DJhgzNt(#0e)C zA1}xM{SYUAz5M@mP5pIH{hM`;h=lMj#0fE^1^RzQ0sL);K%BtALm~XX@fH7Hj1x={ zn}B|jy?3wcQ2-q4)32zB>$^_@rH8$gJ%B3v?^b!|aC)2}Omcs_`rimn-m*rpb<%M- zNbZI(`5E!p=ev*Op8zlVK~Dk7+X6fab_kA%)!rmI&|!k&f=Mp zAH0@Nw{zQ3_bWZVZ@8L1&sjm)kQTtx$|M$*3JQw`(x%Y+3JzW=FKjvPlpf3mASe9W z^DDgJ;wE^x-c*RWsXU@Pj-Xp6l|sgvjO7np%ES#%JBn|eAn<6!=h$?tKY%DrJ$+F2m6pDhQFzUCE61X^L%`M%)T!%imm?VfT|4l>pBF`~ z%Pv;(?7YP5E-O-!CJEn*Cnvvw8;rVK(wU*tpmQrgV`W{GKoCWUT0bFO-|IK*6nL#U z?iW3QU|pgRaMzju^aW26w4S{1Ob4pHM|wQ)#v|XWPqXIlI_>w+rVy9a zQ|NA%fA+pCY~vz3@~L!~swn-muR?Bj|8&SEmmJqM?^-cJR@OSQJ?qQ;5Epr8E9uhp z<8aI)&&25_D?!)Vnfv{IRI=BRiQPzYs^_fvLwumedrGzS94Z&X0j_dlvyY*GhzB

$s>i(ndhp2V! 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 literal 0 HcmV?d00001 diff --git a/installers/win32/Oolite_Readme.txt b/installers/win32/Oolite_Readme.txt new file mode 100644 index 00000000..1c27c5a9 --- /dev/null +++ b/installers/win32/Oolite_Readme.txt @@ -0,0 +1,300 @@ + + +How to run Oolite +----------------- + +A folder has been created in Start -> Program Files called Oolite. This +folder has icons for running the game, the reference sheet, the Advice +for New Commanders guide, the link to the official Oolite website,a more +detailed ReadMe document in PDF format and an uninstall program. + +To run the game, choose the Oolite icon in the Oolite folder. + + +The user preferences defaults file .GNUstepDefaults +----------------------------------------------------------------------- + +The file /oolite.app/GNUstep/Defaults/.GNUstepDefaults +contains the current settings for fullscreen mode and display resolutions, +together with the user preference settings for sound volume, reduced +detail (Yes/No), wireframe graphics display (Yes/No) and the shader +effects level (Off, Simple, Full), in case your system supports shaders. +All these can be changed by either running the game and navigating to the +Game Options... menu ('F2' or '2' key, then select Game Options...), or +by directly editing the .GNUstepDefaults file. The recommended way to +change settings is to use the in-game menu. See below for examples of +editing the preferences file. Note that .GNUstepDefaults will not be +present after the game's installation. You will need to run Oolite at +least once to have it generated. + + +Switching between full screen and windowed mode, or changing resolution +----------------------------------------------------------------------- + +The long standing issues with textures being corrupted when resizing the +game window have been resolved as of version 1.73. + +If in doubt, delete .GNUstepDefaults and restart the game. That will start +you in windowed mode. + +To change the full screen mode resolution, you can use the Game Options... +menu or alternatively edit the .GNUstepDefaults file by changing the +display_width and display_height values, and ensuring the fullscreen +property has a value of <*BY>. + + +.GNUstepDefaults Editing Examples +----------------------------------------------------------------------- + +These settings will give a full screen display of 800x600, about one +third sound volume, reduced detail set to No, wireframe graphics set +to Yes and shader effects set to Simple: + +{ + NSGlobalDomain = { + }; + oolite.exe = { + display_width = <*I800>; + display_height = <*I600>; + fullscreen = <*BY>; + "reduced-detail-graphics" = <*BN>; + "shader-effects-level" = <*I2>; + volume_control = <*R0.26>; + "wireframe-graphics" = <*BY>; + }; +} + +And these settings will give a full screen display of 1400x1050, +full sound volume, reduced detail set to No, wireframe graphics +set to No and shader effects set to Full: + +{ + NSGlobalDomain = { + }; + oolite.exe = { + display_width = <*I1400>; + display_height = <*I1050>; + fullscreen = <*BY>; + "reduced-detail-graphics" = <*BN>; + "shader-effects-level" = <*I3>; + volume_control = <*R1>; + "wireframe-graphics" = <*BN>; + }; +} + + +Tips +---- + +* Read the installed "Oolite reference sheet" PDF for the controls + +* More detailed information about the game can be found inside the Acrobat + PDF OoliteReadMe file, already installed in your root Oolite folder + +* Use Shift+Escape to quit the game + +* Read the tutorial before you begin! ( http://oolite.aegidian.org/tutorial ) + +* You can read the Advice for New Commanders at the bottom of this file for + a quick introduction to the game, with hints and tips + + +Links +----- +Oolite website at: http://oolite.org + +Oolite Message Board at: http://www.aegidian.org/bb + +Oolite Development Project Page at: https://developer.berlios.de/projects/oolite-linux/ + +Browse the Oolite wiki at: http://wiki.alioth.net/index.php/Oolite_Main_Page + +Get OXPs at: http://wiki.alioth.net/index.php/OXP + http://oosat.alioth.net/ + + + + + + + + + + +Advice for New Commanders* +by Disembodied, 24-Mar-2008 +--------------------------- + +It is an ancient mariner, +and he stoppeth one of three... + +All right there! You just got your pilots ticket. Can I just say that your zip-clip +there doesnt do you justice? Youre itching to get off and out into the big black, I +can tell; but we just got a few final once-overs before I can stamp that thing legal. +Shall we? + +So. You got yourself a brand and shiny-new Cobra Mark III. Cowell and MgRaths finest, +yes siree: moren sixty years since the first one rolled off the line right here on Lave, +and its still one of the best. An all-round ship, you get me? It aint the fastest, and +it aint the strongest, nor the most killing neither, and it definitely aint the biggest, +by a long shot, but a sweet little number in her own right, no error. + +Lets take a tour around... Hoo boy, she is mint, aint she! I just love that new-ship +smell. Take a sniff, go on: yeah, well, most of them long-chain monomers is carcinogenic, +so dont you snort too deep... + +Hah! Im just funnin ya, kid. If pulling a tick from sniffing the command console was all +a pilot had to worry about, life would be gravy! No, theres moren enough out there to kill +you plenty quick, if you dont watch out, shiny new ship or no. + +I see a lot of blanks on this here board... Im guessing your ship is, whadda they call it, +a basic model, yeah? Legal minimum? Uh-huh, I thought so. Man oh man, they shouldnt oughta +let kids out in a machine like this; its a sin, is what it is. Some bandit takes a pop at +you, and what you got to hold your end up with? A Pulse Laser. A Pulse Lasers one step up +from a penlight, kiddo. Oh, its a better defence than just harsh language, and theres +always a chance you might be attacked by a really nervous pirate but seriously: if you +ever want to shift that Harmless tag you better beef up your armaments, and soon! Beam +laser, minimum. Until then youd best stick to the cop-end worlds: Democracies and Corporates, +Confederacies maybe if youre feeling lucky, you hear me? You stay sharp, and maybe youll +stay alive. + +See, right here is what Im talking about: this is where you need to fit an ECM. Someone locks +a missile on you, you pop that sucker fast. Oh, I know theres Hardheads out there, shielded +missiles proofed against countermeasures, but a good ECM can pop those too, if youre lucky. +You get one of those running on you, you turn tail and run from it as fast as you can. A +warheads nasty, but nosense in giving it a kinetic advantage too, right? Keep slapping the ECM +as you go, if youve got the energy for it: if the first burst dont kill it, maybe the next +one will. + +Speaking of running... over here is where youd control your Witchdrive Fuel Injectors, ifn +you had em... dumps fuel straight from the tanks into the drive, and shoots you off like an +Oresquan on a hot date. Good for whatever ails ya, from pushing past a mass-lock to getting the +hell out of town! + +Down here, now, this is your Fuel Scoop indicator... huh, offline, I see. Sure, sure, you +dont think youll ever need to kiss the stars: why bother, when fuels cheaper than Celabiler +poetry? Well, maybe its true, and maybe it aint, but anyways this piece of kit scoops up more +than just sunshine. Theres scraps and salvage out there, kid, and good money to be had. Skim on +over the top and this puppy drops em straight into the cargo bay. Pays for itself in no time. +Sweeps up Escape Pods, too: you get the chance to bring someone safe home, you take it even if +it means dumping some of your own payload to take them on board. Look out for the other guys and +theyll look out for you. + +And... sweet Lord Giles on a gyrospider, they didnt even fit you out with a Docking Computer! +Optional Extra, my shiny blue ass... Oh, sure, manual dockings easy enough, but theres a +knack to it. You gotta get that knack first, though. Practice it. Before you go anywhere, +practice it. Fly out to the station buoy, turn around and come back in again, until you got it +pat. And match the rotation: you put scrapes or dents or a big long greasy smear all over my bay, +and I will NOT be pleased... + +Oh, theres a whole bunch of other shit you can stick on here: a Scanner Targeting Enhancement, +for one, if you ever get yourself set up right for a firefight. Even before then, maybe: if you +can clock pirates before they start their run on you, thats half the battle. Well, quarter of +the battle. Or a fifth. Some proportion, anyhow. The Advanced Space Compass, too, now thats a +handy doodad to have on board. And an Extra Energy Unit to boost your recharge. And Shield +Boosters, now theyre a no-brainer. And okay, most of this junk is too high-tech for Lave: you +can get most everything at Zaonce, though, just a wormhole away. Dull kinda burg, Zaonce, but +they know their quarks from their quaternions. Shouldnt set you back moren ten, twenty thou. + +You got how much? One hundred creds. One ... hundred ... creds. Ayoha. All right then. Lets +break it down. Your problem here is financial, not technical. Maybe at bottom its psychological, +but Ill give you the benefit. + +Theres two types of money, kid: fast, and slow. Fast money comes easy, and slow money comes hard. +The slow is sure and steady, though, and the fast, well, it might make you wish you had waited. +Ill run you through them both, though, and you can make up your own mind. + +First of all, for the fast money, theres this sweet and cherry Cobra III: you sell it, right now, +youll net yourself enough to buy a second-hand ship with enough scratch left over for some half- +decent kit. Course, some of these second-hand numbers are pretty, well, used, if you know what I +mean, and come with problems of their own. I mean, you ever try to take a dump in a head designed +for some other guys anatomy? And the resale sucks, ifn you ever want to move on up. But its an +option. + +Second, now, theres the... ah, let me just check that were alone here... okay: theres the Black +Monks. Great guys, Id like to make that clear, absolutely: most fine and upstanding! Theyll be +happy to loan you what you need to get you started. Theyre a not-for-profit organisation, a charity, +really, but what with overheads and all they do have to charge a wee bit of interest on any loans +they make: and they are keen eager, even to see that they get paid back. You take a loan from +them, you make every gram of cargo pay, every time. Work hard though and it can be done: in all my +years Ive never met anyone who defaulted on a loan from the Bank of St Herod. Not one. Ever. + +Slow money, now, thats less chancy. You buy up whats cheap, you take it to where its expensive, +and you sell it at a profit. Rinse and repeat. Whats cheap where, and whats expensive? Supply and +demand, kid. Like the philosopher said, its the economy, stupid. Agricultural worlds produce raw +materials like minerals, metals and radioactives, and the bio-products like food, textiles, booze +and furs, too. Industrial planets make finished goods, like luxuries, computers and machinery. So +you take the produce of one and you sell it on the other, and chances are youre making money on +the deal. Politics dont matter squat: farmers need harvesters and factories need feedstock! + +Ocourse, money matters: rich Industrials are rich because theyve got the most efficient processes, +so not only do they make the cheapest products, their factories are the hungriest and theyll pay +the best prices for raw materials. Poor Agriculturals, on the other hand, theyre most desperate for +fine articles and will scrape together whatever they can to pay for em: meanwhile, theyll offer +you the cheapest deals anywhere for what they make themselves. Which puts a vicious lock on the +poverty trap, but hey: nobody said life was fair. Folks like you whove climbed up the gravity well, +youre just filling a need. Buy and sell between rich Industrials and poor Agriculturals, thats my +advice! Theres money to be made elsewhere, no error, but those are the sweetest runs youre likely +to hit on. Bulk is the key, kid: the more you carry, the more you make. This Cobra III here can take +twenty tons, right now: for just 400 creds more you can get a Cargo Bay Expansion to take you up to +thirty-five.That extra fifteen tons of space will pay for itself and more in one good run, if you +can fill it up. + +It aint all bulk, though. Watch the board for cheap deals on precious metals and gemstones: they +might not offer the greatest profits, but they dont take up any cargo space at all. See this safe +over here, behind this bulkhead? You take on platinum, or gold, or a sack of IOUN gemstones when +youre docked, they go right in here. You can keep em here as long as you like, until you find +somewhere to offload em. Co-op rules stop you dropping too much of em, or too much of anything, +come to that, in one station so much for free trade! but as a slow-burn money-maker theres not +much to beat it. You can mine for em yourself, ifn you get a Mining Laser and an Ore Processor to +go with your Fuel Scoop, and you dont mind scraping carbon scoring off the scoop every few jumps. +Only dont, for any sake, put the Mining Laser on the nose! Its a tool, not a weapon. Or you can +just buy the shinies cheap off the miners direct, if you run across a Rock Hermit. Powerful fond of +liquor, Rock Hermits are, too. + +What other products? What you winking for, kid? You mean slaves, narcotics and firearms? Why dont +you just damn well say so? They aint illegal. Theys what we call controlled merchandise. Bring as +much of em in as you want... what will get you into trouble with the Blues is shipping them out of +a main system station. But theres plenty of other places to buy em up, all nice and legal, along +with reglar trade goods, too. Some of the Commie worlds have Slap-Yous and Cee-Zed-Gee-Effs, whatever +the hell they all are, and Astro-Gulags too, which are just plain depressing. Some industrial +Dictatorship systems, they got Imperial AstroFactories, although some of em seem to sell stuff they +dont ever make... go figure. And some spots, if they got the population size to make it worthwhile, +theres Convenience Stores way out by the Witchpoint. You want to give these guys a try, you sail on +in. Check the system prices first, note down what you got yourself, see whats on offer, and do the +sums. + +Theres long-range shipping contracts on offer, too, in some stations: F8-F8 will bring em up, if +theres any there. You buy the deal and then get paid a bonus if you make the delivery on time. +Theyll be out your price-range just now, and anyway most of em call for a bigger cargo-hold than +a Cobra can carry. Keep an eye out for any you might be able to do, though; if you build a rep as a +reliable carrier then the jobs can get real juicy. + +Thats slow money, kid: work, save, invest, and work again, thats what its all about! It aint pretty +but it gets you there in the end. + +One final tip, kid: Ill say this cos I like ya. It wont save you work but it will save you time, +and it might just save your life, too: if you want to get from the Witchpoint to the station fast, +without getting your jumpdrive mass-locked by anyone, friendly or otherwise, heres what you do. Line +up on the planet; angle up away from it by near enough ninety degrees; then hit the Torus jumpdrive +and scoot on out of the main spacelane for a few hundred klicks or so. Then, when youve given yourself +enough sky, pull the nose back round and come on down to the station. Chances are you wont meet a soul, +whether youre cruising into Ensoreus or creeping into Qudira. The spacelanes is where the action is, +where theres help and hostility both; you get nervous, you go off-beam. Most times, youll come through +safe. + +Huh. Anyhow. Im a busy frog, I cant stay here all day filling in every Jameson on what they should have +learned in the spawning pond. Gimme your ticket, kid, and Ill stamp it flight-ready, though Giles knows I +probly shouldnt... there ya go. Thats you ready to take on the Witch. Jens help us all... dont know +enough to keep a level bearing through a wormhole... what they send up here for us to deal with... pick up +the pieces more like... + + +--------------------------- +*Disclaimer: +The above text makes reference to certain Oolite eXpansion Packs (OXPs), that are not part of the core +Oolite game. The fact that certain elements from OXPs are mentioned does not necessarily mean that +these OXPs are recommended by the Oolite Team, as OXP selection and usage is subject to user personal +preferences. The OXPs mentioned in Advice for New Commanders are Rusties, Bank of the Black Monks, +Ore Processor, Communist flavour pack, Dictatorship flavour pack, Your Ad Here. All Oolite OXPs are +available for download from either http://wiki.alioth.net/index.php/OXP or http://oosat.alioth.net/ diff --git a/installers/win32/RunOolite.bat b/installers/win32/RunOolite.bat new file mode 100644 index 00000000..db284b53 --- /dev/null +++ b/installers/win32/RunOolite.bat @@ -0,0 +1,9 @@ +@echo off +set BASE=%~dp0 +set GNUSTEP_PATH_HANDLING=windows +set GNUSTEP_SYSTEM_ROOT=%BASE%\oolite.app +set GNUSTEP_LOCAL_ROOT=%BASE%\oolite.app +set GNUSTEP_NETWORK_ROOT=%BASE%\oolite.app +set GNUSTEP_USERS_ROOT=%BASE%\oolite.app +set HOMEPATH=%BASE%\oolite.app +"%BASE%\oolite.app\oolite.exe" %1 %2 %3 %4 diff --git a/installers/win32/Uninstall.ico b/installers/win32/Uninstall.ico new file mode 100644 index 0000000000000000000000000000000000000000..572cc031365cf3d0ccbc8ee7395b557aa8917ea3 GIT binary patch literal 22486 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 Dlr{~- literal 0 HcmV?d00001 diff --git a/src/BSDCompat/bsd_string.h b/src/BSDCompat/bsd_string.h new file mode 100644 index 00000000..3b8616f7 --- /dev/null +++ b/src/BSDCompat/bsd_string.h @@ -0,0 +1,7 @@ +#ifndef BSD_STRING_H +#define BSD_STRING_H +#ifdef NEED_STRLCPY +size_t strlcpy(char *dst, const char *src, size_t siz); +#endif +#endif + diff --git a/src/BSDCompat/strlcpy.c b/src/BSDCompat/strlcpy.c new file mode 100644 index 00000000..2a7fd24f --- /dev/null +++ b/src/BSDCompat/strlcpy.c @@ -0,0 +1,56 @@ +/* $OpenBSD: strlcpy.c,v 1.10 2005/08/08 08:05:37 espie Exp $ */ + +/* + * Copyright (c) 1998 Todd C. Miller + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifdef NEED_STRLCPY + +#include +#include +#include "bsd_string.h" /* DJS for Oolite on Linux/win32 */ + +/* + * Copy src to string dst of size siz. At most siz-1 characters + * will be copied. Always NUL terminates (unless siz == 0). + * Returns strlen(src); if retval >= siz, truncation occurred. + */ +size_t +strlcpy(char *dst, const char *src, size_t siz) +{ + char *d = dst; + const char *s = src; + size_t n = siz; + + /* Copy as many bytes as will fit */ + if (n != 0 && --n != 0) { + do { + if ((*d++ = *s++) == 0) + break; + } while (--n != 0); + } + + /* Not enough room in dst, add NUL and traverse rest of src */ + if (n == 0) { + if (siz != 0) + *d = '\0'; /* NUL-terminate dst */ + while (*s++) + ; + } + + return(s - src - 1); /* count does not include NUL */ +} +#endif + diff --git a/src/Cocoa/Comparison.h b/src/Cocoa/Comparison.h new file mode 100644 index 00000000..b3d494ef --- /dev/null +++ b/src/Cocoa/Comparison.h @@ -0,0 +1 @@ +// Empty for OS X, compatibility stuff for GNUstep. diff --git a/src/Cocoa/Groolite.h b/src/Cocoa/Groolite.h new file mode 100644 index 00000000..1c6e73e7 --- /dev/null +++ b/src/Cocoa/Groolite.h @@ -0,0 +1,60 @@ +/* + +Groolite.h + +Growl integration class. The singleton Groolite object listens for Growl +notifications and reports them by printing to the game communication log. For +more about Growl, see . + +To do: reimplement using delegate pattern, remove dependency on Growl +framework headers (the framework is not needed at run time). + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +@class GameController; + + +enum +{ + kGrooliteAllMessages = -2, + kGrooliteLowPriority, + kGrooliteMediumPriority, + kGrooliteHighPriority, + kGrooliteHighestPriority, + kGrooliteOff, + + kGroolitePriorityMinimum = kGrooliteAllMessages, + kGroolitePriorityMaximum = kGrooliteOff +}; + + +@interface Groolite: NSObject +{ + NSConnection *connection; + IBOutlet GameController *gameController; +} + ++ (NSString*) priorityDescription: (int) min_priority; + +@end diff --git a/src/Cocoa/Groolite.m b/src/Cocoa/Groolite.m new file mode 100644 index 00000000..f48415ce --- /dev/null +++ b/src/Cocoa/Groolite.m @@ -0,0 +1,254 @@ +/* + +Groolite.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Groolite.h" +#import +#import "GameController.h" +#import "GuiDisplayGen.h" +#import "Universe.h" +#import "PlayerEntityLegacyScriptEngine.h" + +static NSString * const kOOLogGrooliteError = @"growl.error"; +static NSString * const kOOLogGrooliteDebug = @"growl.debug"; + +// #define GROOLITE_DEBUG + + +@protocol GrowlNotificationObserver +- (oneway void) notifyWithDictionary:(bycopy NSDictionary *)dict; +@end + + +@protocol GrowlNotificationCenterProtocol + +- (oneway void) addObserver:(byref id)observer; +- (oneway void) removeObserver:(byref id)observer; + +@end + + +@interface Groolite (Private) + +- (void)connectToGrowl:unused; +- (void)disconnectFromGrowl:unused; +- (void)connectionDied:unused; +- (void)displayGrowlNotificiationWithTitle:(NSString *)inTitle andMessage:(NSString *)inMessage fromApp:(NSString *) inAppname; + +@end + + +@implementation Groolite + +- (void)displayGrowlNotificiationWithTitle:(NSString *)inTitle andMessage:(NSString *)inMessage fromApp:(NSString *) inAppname +{ + PlayerEntity *player; + NSString *notificationString; + NSString *displayString; + + player = [PlayerEntity sharedPlayer]; + + if (!inTitle) + return; // catch blank messages + + if (!inAppname) + { + // standard response + notificationString = @"Growl"; + } + else + { + // response if we're told which application is sending the message + notificationString = inAppname; + } + + if (nil == inMessage) + { + // Terse mode + displayString = [NSString stringWithFormat:@"%@: %@", notificationString, inTitle]; + } + else + { + // Standard 'verbose' mode + displayString = [NSString stringWithFormat:@"%@: %@\n%@", notificationString, inTitle, inMessage]; + } + + [player commsMessage: displayString]; + + if ([player isSpeechOn]) + { + [UNIVERSE stopSpeaking]; + [UNIVERSE startSpeakingString:[NSString stringWithFormat:@"%@ message: %@", notificationString, inTitle]]; + } +} + + +- (id)init +{ + NSDistributedNotificationCenter *dnc; + + self = [super init]; + if (nil != self) + { + /* + Subscribe to GROWL_IS_READY notifications. + This is necessary in case GrowlHelperApp currently isn't running, and in case + it is restarted. + */ + dnc = [NSDistributedNotificationCenter defaultCenter]; + [dnc addObserver:self selector:@selector(connectToGrowl:) name:GROWL_IS_READY object:nil]; + + // Also, try to connect on the off chance it’s running now. + [self connectToGrowl:nil]; + } + return self; +} + + +- (void)dealloc +{ + [self disconnectFromGrowl:nil]; + + [super dealloc]; +} + + +- (void)connectToGrowl:unused +{ + NSConnection *theConnection; + NSNotificationCenter *nc; + id growlNC; + + NS_DURING + theConnection = [NSConnection connectionWithRegisteredName:@"GrowlNotificationCenter" host:nil]; + if (nil != theConnection) + { + growlNC = (id)[theConnection rootProxy]; + [growlNC addObserver:self]; + + // Subscribe to connection-died and application-quit notifications, so we can unregister appropriately. + nc = [NSNotificationCenter defaultCenter]; + [nc addObserver:self selector:@selector(connectionDied:) name:NSConnectionDidDieNotification object:theConnection]; + [nc addObserver:self selector:@selector(disconnectFromGrowl:) name:NSApplicationWillTerminateNotification object:nil]; + + connection = [theConnection retain]; + } + NS_HANDLER + OOLog(kOOLogGrooliteError, @"DEBUG GROOLITE exception : %@ : %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + + +- (void)disconnectFromGrowl:unused +{ + NSNotificationCenter *nc; + id growlNC; + + if (nil != connection) + { + NS_DURING + growlNC = (id)[connection rootProxy]; + [growlNC removeObserver:self]; + [connection release]; + connection = nil; + + nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self]; + NS_HANDLER + OOLog(kOOLogGrooliteError, @"DEBUG GROOLITE exception : %@ : %@", [localException name], [localException reason]); + NS_ENDHANDLER + } +} + + +- (void)connectionDied:unused +{ + NSNotificationCenter *nc; + + if (nil != connection) + { + NS_DURING + [connection release]; + connection = nil; + + nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self]; + NS_HANDLER + OOLog(kOOLogGrooliteError, @"DEBUG GROOLITE exception : %@ : %@", [localException name], [localException reason]); + NS_ENDHANDLER + } +} + + +- (oneway void) notifyWithDictionary:(bycopy NSDictionary *)inDict +{ + NSUserDefaults *prefs; + int priority; + NSString *title; + NSString *message; + NSString *appname; + + prefs = [NSUserDefaults standardUserDefaults]; + + // Ignore if we're in a window + #ifndef GROOLITE_DEBUG + if (![gameController inFullScreenMode]) + return; + #endif + + // Ignore if we're paused + if ([gameController gameIsPaused]) + return; + + // Check that priority is not below our threshold + priority = [[inDict objectForKey:GROWL_NOTIFICATION_PRIORITY] intValue]; + #ifndef GROOLITE_DEBUG + if (priority < [prefs integerForKey:@"groolite-min-priority"]) + return; + #endif + + // If we get here, we need to handle the message + title = [inDict objectForKey:GROWL_NOTIFICATION_TITLE]; + message = [inDict objectForKey:GROWL_NOTIFICATION_DESCRIPTION]; + appname = [inDict objectForKey:GROWL_APP_NAME]; + + OOLog(kOOLogGrooliteDebug, @"Received Growl notification: inDict\n%@\n\n", inDict); + OOLog(kOOLogGrooliteDebug, @"Groolite: priority = %d appname = \"%@\" title = \"%@\", message = \"%@\"", priority, appname, title, message); + + if (nil == title || [@"" isEqual:title]) + { + title = message; + message = nil; + } + + [self displayGrowlNotificiationWithTitle:title andMessage:message fromApp:appname]; +} + + ++ (NSString*) priorityDescription: (int) min_priority +{ + if (min_priority < kGroolitePriorityMinimum || min_priority > kGroolitePriorityMaximum) return @"?"; + return [UNIVERSE descriptionForArrayKey:@"growl-priority-levels" index:min_priority - kGroolitePriorityMinimum]; +} + +@end diff --git a/src/Cocoa/Info-Oolite.plist b/src/Cocoa/Info-Oolite.plist new file mode 100644 index 00000000..8ac8c88a --- /dev/null +++ b/src/Cocoa/Info-Oolite.plist @@ -0,0 +1,114 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + oolite-save + + CFBundleTypeIconFile + oolite-document + CFBundleTypeName + Oolite Saved Game + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + org.aegidian.oolite.save + + + + CFBundleTypeExtensions + + oxp + + CFBundleTypeIconFile + oolite-expansion-document + CFBundleTypeName + Oolite Expansion Pack + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSTypeIsPackage + + LSItemContentTypes + + org.aegidian.oolite.oxp + + + + CFBundleExecutable + Oolite + CFBundleIconFile + oolite-icon + CFBundleIdentifier + org.aegidian.oolite + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Oolite + CFBundlePackageType + APPL + CFBundleSignature + Ool8 + CFBundleVersion + 1.73 + NSMainNibFile + MainMenu + NSPrincipalClass + OoliteApp + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.aegidian.oolite.save + UTTypeDescription + Oolite Saved Game + UTTypeConformsTo + + public.text + com.apple.property-list + + UTTypeTagSpecification + + public.filename-extension + + oolite-save + + + + + UTTypeIdentifier + org.aegidian.oolite.oxp + UTTypeDescription + Oolite Expansion Pack + UTTypeConformsTo + + public.item + com.apple.package + + UTTypeTagSpecification + + public.filename-extension + + oxp + + + + + SmartCrashReports_CompanyName + the Oolite team + SmartCrashReports_EmailTicket + SCR-A36F37AFFD + SmartCrashReports_CommentsTemplate + Please paste the last few lines of the run log here (in your home folder, open Library, then Logs, then Oolite, then Latest.log). If a large portion of the log appears relevant, consider sending a bug report by e-mail to bugs@oolite.org instead; please include the crash log if you do (Library, Logs, CrashReporter, Oolite.crash.log). + + diff --git a/src/Cocoa/Info-OoliteDev.plist b/src/Cocoa/Info-OoliteDev.plist new file mode 100644 index 00000000..ee3982dc --- /dev/null +++ b/src/Cocoa/Info-OoliteDev.plist @@ -0,0 +1,113 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + oolite-save + + CFBundleTypeIconFile + oolite-document + CFBundleTypeName + Oolite Saved Game + CFBundleTypeRole + Editor + LSHandlerRank + Owner + LSItemContentTypes + + org.aegidian.oolite.save + + + + CFBundleTypeExtensions + + oxp + + CFBundleTypeIconFile + oolite-expansion-document + CFBundleTypeName + Oolite Expansion Pack + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSTypeIsPackage + + LSItemContentTypes + + org.aegidian.oolite.oxp + + + + CFBundleExecutable + OoliteDev + CFBundleIconFile + oolite-icon + CFBundleIdentifier + org.aegidian.oolite + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + OoliteDev + CFBundlePackageType + APPL + CFBundleSignature + Ool8 + CFBundleVersion + $OOLITE_VERSION + NSMainNibFile + MainMenu + NSPrincipalClass + OoliteApp + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.aegidian.oolite.save + UTTypeDescription + Oolite Saved Game + UTTypeConformsTo + + public.text + com.apple.property-list + + UTTypeTagSpecification + + public.filename-extension + + oolite-save + + + + + UTTypeIdentifier + org.aegidian.oolite.oxp + UTTypeDescription + Oolite Expansion Pack + UTTypeConformsTo + + com.apple.package + + UTTypeTagSpecification + + public.filename-extension + + oxp + + + + + SmartCrashReports_CompanyName + the Oolite team + SmartCrashReports_EmailTicket + SCR-A36F37AFFD + SmartCrashReports_CommentsTemplate + Please paste the last few lines of the run log here (in your home folder, open Library, then Logs, then Oolite, then Latest.log). If a large portion of the log appears relevant, consider sending a bug report by e-mail to bugs@oolite.org instead; please include the crash log if you do (Library, Logs, CrashReporter, Oolite.crash.log). + + diff --git a/src/Cocoa/JoystickHandler.h b/src/Cocoa/JoystickHandler.h new file mode 100644 index 00000000..4b8e5fa4 --- /dev/null +++ b/src/Cocoa/JoystickHandler.h @@ -0,0 +1,152 @@ +/* + +JoystickHandler.h + +Non-functional JoystickHandler. +This exists to reduce the amount of #ifdefs and duplicated code. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + +#define STICK_AXISUNASSIGNED -10.0 + +// Enums are used here rather than a more complex ObjC object because +// these are required very frequently (once per frame) so must be light +// on CPU cycles (try and avoid too many objc sendmsgs). +// Controls that can be an axis +enum { + AXIS_ROLL, + AXIS_PITCH, + AXIS_YAW, + AXIS_PRECISION, + AXIS_THRUST, + AXIS_VIEWX, + AXIS_VIEWY, + AXIS_end +} axfn; + +// Controls that can be a button +enum { + BUTTON_INCTHRUST, + BUTTON_DECTHRUST, + BUTTON_SCANNERZOOM, + BUTTON_JETTISON, + BUTTON_COMPASSMODE, + BUTTON_COMMSLOG, + BUTTON_DOCKCPU, + BUTTON_DOCKCPUFAST, + BUTTON_DOCKCPUTARGET, + BUTTON_FUELINJECT, + BUTTON_HYPERSPEED, + BUTTON_HYPERDRIVE, + BUTTON_GALACTICDRIVE, + BUTTON_FIRE, + BUTTON_ARMMISSILE, + BUTTON_LAUNCHMISSILE, + BUTTON_UNARM, +#if TARGET_INCOMING_MISSILES + BUTTON_TARGETINCOMINGMISSILE, +#endif + BUTTON_CYCLEMISSILE, + BUTTON_ENERGYBOMB, + BUTTON_ID, + BUTTON_ECM, + BUTTON_ESCAPE, + BUTTON_CLOAK, + BUTTON_PRECISION, + BUTTON_VIEWFORWARD, + BUTTON_VIEWAFT, + BUTTON_VIEWPORT, + BUTTON_VIEWSTARBOARD, + BUTTON_end +} butfn; + +// Stick constants +#define MAX_STICKS 4 +#define MAX_AXES 16 +#define MAX_REAL_BUTTONS 64 +#define MAX_HATS 0 +#define MAX_BUTTONS (MAX_REAL_BUTTONS + 4 * MAX_HATS) +#define STICK_NOFUNCTION -1 +#define STICK_AXISUNASSIGNED -10.0 +#define STICK_PRECISIONDIV 98304 // 3 times more precise +#define STICK_NORMALDIV 32768 +#define STICK_PRECISIONFAC (STICK_PRECISIONDIV/STICK_NORMALDIV) +#define STICK_DEADZONE 0.05 + +// Kind of stick device (these are bits - if any more are added, +// the next one is 4 and so on). +#define HW_AXIS 1 +#define HW_BUTTON 2 + +// The threshold at which an axis can trigger a call back. +// The max of abs(axis) is 32767. +#define AXCBTHRESH 20000 + +// Dictionary keys - used in the defaults file +#define AXIS_SETTINGS @"JoystickAxes" // NSUserDefaults +#define BUTTON_SETTINGS @"JoystickButs" // NSUserDefaults +#define STICK_ISAXIS @"isAxis" // YES=axis NO=button +#define STICK_NUMBER @"stickNum" // Stick number 0 to 4 +#define STICK_AXBUT @"stickAxBt" // Axis or button number +#define STICK_FUNCTION @"stickFunc" // Function of axis/button + + +@interface JoystickHandler: NSObject +{ + BOOL butstate[BUTTON_end]; +} + ++ (id) sharedStickHandler; + +- (int) getNumSticks; +- (NSPoint) getRollPitchAxis; +- (NSPoint) getViewAxis; +- (double) getAxisState:(int)function; +- (double) getSensitivity; +- (const BOOL *) getAllButtonStates; + +@end diff --git a/src/Cocoa/JoystickHandler.m b/src/Cocoa/JoystickHandler.m new file mode 100644 index 00000000..23488465 --- /dev/null +++ b/src/Cocoa/JoystickHandler.m @@ -0,0 +1,98 @@ +/* + +JoystickHandler.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "JoystickHandler.h" + + +JoystickHandler *sSharedStickHandler = nil; + + +@implementation JoystickHandler + ++ (id) sharedStickHandler +{ + if (sSharedStickHandler == nil) sSharedStickHandler = [[JoystickHandler alloc] init]; + return sSharedStickHandler; +} + + +- (int) getNumSticks +{ + return 0; +} + + +- (NSPoint) getRollPitchAxis +{ + return NSZeroPoint; +} + + +- (NSPoint) getViewAxis +{ + return NSZeroPoint; +} + + +- (double) getAxisState:(int)function +{ + return 0.0; +} + + +- (double) getSensitivity +{ + return 1.0; +} + + +- (const BOOL *) getAllButtonStates +{ + return butstate; +} + +@end diff --git a/src/Cocoa/MyOpenGLView.h b/src/Cocoa/MyOpenGLView.h new file mode 100644 index 00000000..f02145ae --- /dev/null +++ b/src/Cocoa/MyOpenGLView.h @@ -0,0 +1,200 @@ +/* + +MyOpenGLView.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" + +#ifdef GNUSTEP +#include +#endif + +#define MAX_CLEAR_DEPTH 100000000.0 +#define MAX_CLEAR_DEPTH2 10000000000000000.0 +// 100 000 km. + +#define NUM_KEYS 320 + +#define MOUSE_DOUBLE_CLICK_INTERVAL 0.40 + +@class Entity, GameController; +@class JoystickHandler; + +#ifdef GNUSTEP +#define OpenGLViewSuperClass NSObject +#else +#define OpenGLViewSuperClass NSOpenGLView +#endif + +enum GameViewKeys +{ + gvArrowKeyUp = 255, + gvArrowKeyDown = 254, + gvArrowKeyLeft = 253, + gvArrowKeyRight = 252, + gvFunctionKey1 = 241, + gvFunctionKey2 = 242, + gvFunctionKey3 = 243, + gvFunctionKey4 = 244, + gvFunctionKey5 = 245, + gvFunctionKey6 = 246, + gvFunctionKey7 = 247, + gvFunctionKey8 = 248, + gvFunctionKey9 = 249, + gvFunctionKey10 = 250, + gvFunctionKey11 = 251, + gvMouseLeftButton = 301, + gvMouseDoubleClick = 303, + gvHomeKey = 302, + gvNumberKey0 = 48, + gvNumberKey1 = 49, + gvNumberKey2 = 50, + gvNumberKey3 = 51, + gvNumberKey4 = 52, + gvNumberKey5 = 53, + gvNumberKey6 = 54, + gvNumberKey7 = 55, + gvNumberKey8 = 56, + gvNumberKey9 = 57 +}; + +enum StringInput +{ + gvStringInputNo = 0, + gvStringInputAlpha = 1, + gvStringInputAll = 2 +}; + +extern int debug; + +@interface MyOpenGLView : OpenGLViewSuperClass +{ + GameController *gameController; + + BOOL keys[NUM_KEYS]; + BOOL supressKeys; // DJS + + BOOL opt, ctrl, command, shift; + BOOL allowingStringInput; + BOOL isAlphabetKeyDown; + + int keycodetrans[255]; + + BOOL m_glContextInitialized; + NSPoint mouseDragStartPoint; + + NSTimeInterval timeIntervalAtLastClick; + BOOL doubleClick; + + NSMutableString *typedString; + + NSPoint virtualJoystickPosition; + + NSSize viewSize; + GLfloat display_z; + +#ifdef GNUSTEP + double squareX,squareY; + NSRect bounds; + + // Full screen sizes + NSMutableArray *screenSizes; + int currentSize; + BOOL fullScreen; + + // Windowed mode + NSSize currentWindowSize; + SDL_Surface* surface; +#endif + JoystickHandler *stickHandler; +} + + +- (void) setStringInput: (enum StringInput) value; +- (void) allowStringInput: (BOOL) value; +- (enum StringInput) allowingStringInput; +- (NSString *) typedString; +- (void) resetTypedString; +- (void) setTypedString:(NSString*) value; + +- (NSSize) viewSize; +- (GLfloat) display_z; + +- (GameController *) gameController; +- (void) setGameController:(GameController *) controller; + +- (void) initialiseGLWithSize:(NSSize) v_size; + +- (void) drawRect:(NSRect)rect; +- (void) updateScreen; + +- (void) snapShot; + +#ifndef GNUSTEP +- (void)mouseDown:(NSEvent *)theEvent; +- (void)mouseUp:(NSEvent *)theEvent; +#else +- (NSRect) bounds; +- (void) display; ++ (NSMutableDictionary *) getNativeSize; + +- (void) setFullScreenMode:(BOOL)fsm; +- (BOOL) inFullScreenMode; +- (void) toggleScreenMode; +- (void) setDisplayMode:(int)mode fullScreen:(BOOL)fsm; + +- (int) indexOfCurrentSize; +- (void) setScreenSize: (int)sizeIndex; +- (NSMutableArray *)getScreenSizeArray; +- (void) populateFullScreenModelist; +- (NSSize) modeAsSize: (int)sizeIndex; +- (void) saveWindowSize: (NSSize) windowSize; +- (NSSize) loadWindowSize; +- (int) loadFullscreenSettings; +- (int) findDisplayModeForWidth: (unsigned int) d_width Height:(unsigned int) d_height + Refresh: (unsigned int)d_refresh; +- (NSSize) currentScreenSize; + +- (void) pollControls: (id)sender; +- (void) handleStringInput: (SDL_KeyboardEvent *) kbd_event; // DJS +#endif +- (JoystickHandler *)getStickHandler; // DJS + +- (void) setVirtualJoystick:(double) vmx :(double) vmy; +- (NSPoint) virtualJoystickPosition; + +- (void) clearKeys; +- (void) clearMouse; +- (BOOL) isAlphabetKeyDown; +- (void) supressKeysUntilKeyUp; // DJS +- (BOOL) isDown: (int) key; +- (BOOL) isOptDown; +- (BOOL) isCtrlDown; +- (BOOL) isCommandDown; +- (BOOL) isShiftDown; +- (int) numKeys; + +// Check current state of shift key rather than relying on last event. +- (BOOL)pollShiftKey; + +@end diff --git a/src/Cocoa/MyOpenGLView.m b/src/Cocoa/MyOpenGLView.m new file mode 100644 index 00000000..8d27f061 --- /dev/null +++ b/src/Cocoa/MyOpenGLView.m @@ -0,0 +1,709 @@ +/* + +MyOpenGLView.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "MyOpenGLView.h" + +#import "GameController.h" +#import "Universe.h" +#import "Entity.h" +#import "PlanetEntity.h" +#import "ResourceManager.h" +#import "GuiDisplayGen.h" +#import +#import "JoystickHandler.h" + + +static NSString * kOOLogKeyCodeOutOfRange = @"input.keyMapping.codeOutOfRange"; +static NSString * kOOLogKeyUp = @"input.keyMapping.keyPress.keyUp"; +static NSString * kOOLogKeyDown = @"input.keyMapping.keyPress.keyDown"; + + +@interface MyOpenGLView(Internal) + +- (int) translateKeyCode: (int) input; +- (void)performLateSetup; + +@end + + +@implementation MyOpenGLView + +- (id) initWithFrame:(NSRect)frameRect +{ + // Pixel Format Attributes for the View-based (non-FullScreen) NSOpenGLContext + NSOpenGLPixelFormatAttribute attrs[] = + { +// // Specify that we want a full-screen OpenGL context. +// NSOpenGLPFAFullScreen, + // and that we want a windowed OpenGL context. + NSOpenGLPFAWindow, + + // We may be on a multi-display system (and each screen may be driven by a different renderer), so we need to specify which screen we want to take over. + // For this demo, we'll specify the main screen. + NSOpenGLPFAScreenMask, CGDisplayIDToOpenGLDisplayMask(kCGDirectMainDisplay), + + // Specifying "NoRecovery" gives us a context that cannot fall back to the software renderer. + //This makes the View-based context a compatible with the fullscreen context, enabling us to use the "shareContext" + // feature to share textures, display lists, and other OpenGL objects between the two. + NSOpenGLPFANoRecovery, + + // Attributes Common to FullScreen and non-FullScreen + NSOpenGLPFACompliant, + + NSOpenGLPFAColorSize, 32, + NSOpenGLPFADepthSize, 32, + NSOpenGLPFADoubleBuffer, + NSOpenGLPFAAccelerated, + 0 + }; + + // Create our non-FullScreen pixel format. + NSOpenGLPixelFormat* pixelFormat = [[[NSOpenGLPixelFormat alloc] initWithAttributes:attrs] autorelease]; + + self = [super initWithFrame:frameRect pixelFormat:pixelFormat]; + + virtualJoystickPosition = NSMakePoint(0.0,0.0); + + typedString = [[NSMutableString alloc] initWithString:@""]; + allowingStringInput = gvStringInputNo; + isAlphabetKeyDown = NO; + + timeIntervalAtLastClick = [NSDate timeIntervalSinceReferenceDate]; + + return self; +} + + +- (void) dealloc +{ + if (typedString) + [typedString release]; + [super dealloc]; +} + + +- (void) setStringInput: (enum StringInput) value +{ + allowingStringInput = value; +} + + +- (void) allowStringInput: (BOOL) value +{ + if (value) + allowingStringInput = gvStringInputAlpha; + else + allowingStringInput = gvStringInputNo; +} + +-(enum StringInput) allowingStringInput +{ + return allowingStringInput; +} + + +- (NSString *) typedString +{ + return typedString; +} + + +- (void) resetTypedString +{ + [typedString setString:@""]; +} + + +- (void) setTypedString:(NSString*) value +{ + [typedString setString:value]; +} + + +- (NSSize) viewSize +{ + return viewSize; +} + + +- (GLfloat) display_z +{ + return display_z; +} + + +- (GameController *)gameController +{ + return gameController; +} + + +- (void) setGameController:(GameController *) controller +{ + gameController = controller; +} + + +- (void) updateScreen +{ + [self drawRect:NSMakeRect(0, 0, viewSize.width, viewSize.height)]; +} + + +- (void) drawRect:(NSRect)rect +{ + if ((viewSize.width != [self frame].size.width)||(viewSize.height != [self frame].size.height)) // resized + { + m_glContextInitialized = NO; + viewSize = [self frame].size; + } + + if (!m_glContextInitialized) [self initialiseGLWithSize:viewSize]; + + // do all the drawing! + if (UNIVERSE != nil) [UNIVERSE drawUniverse]; + else + { + // not set up yet, draw a black screen + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + } + + [[self openGLContext] flushBuffer]; +} + + +- (void) initialiseGLWithSize:(NSSize) v_size +{ + GLfloat sun_ambient[] = {0.0, 0.0, 0.0, 1.0}; + GLfloat sun_diffuse[] = {1.0, 1.0, 1.0, 1.0}; + GLfloat sun_specular[] = {1.0, 1.0, 1.0, 1.0}; + GLfloat sun_center_position[] = {4000000.0, 0.0, 0.0, 1.0}; + GLfloat stars_ambient[] = {0.25, 0.2, 0.25, 1.0}; + + viewSize = v_size; + if (viewSize.width/viewSize.height > 4.0/3.0) + display_z = 480.0 * viewSize.width/viewSize.height; + else + display_z = 640.0; + + float ratio = 0.5; + float aspect = viewSize.height/viewSize.width; + + glShadeModel(GL_FLAT); + glClearColor(0.0, 0.0, 0.0, 0.0); + glClear(GL_COLOR_BUFFER_BIT); + [[self openGLContext] flushBuffer]; + + glClearDepth(MAX_CLEAR_DEPTH); + glViewport( 0, 0, viewSize.width, viewSize.height); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); // reset matrix + glFrustum( -ratio, ratio, -aspect*ratio, aspect*ratio, 1.0, MAX_CLEAR_DEPTH); // set projection matrix + + glMatrixMode( GL_MODELVIEW); + + glEnable( GL_DEPTH_TEST); // depth buffer + glDepthFunc( GL_LESS); // depth buffer + + glFrontFace( GL_CCW); // face culling - front faces are AntiClockwise! + glCullFace( GL_BACK); // face culling + glEnable( GL_CULL_FACE); // face culling + + glEnable( GL_BLEND); // alpha blending + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // alpha blending + + if (UNIVERSE) + { + [UNIVERSE setLighting]; + } + else + { + glLightfv(GL_LIGHT1, GL_AMBIENT, sun_ambient); + glLightfv(GL_LIGHT1, GL_SPECULAR, sun_specular); + glLightfv(GL_LIGHT1, GL_DIFFUSE, sun_diffuse); + glLightfv(GL_LIGHT1, GL_POSITION, sun_center_position); + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, stars_ambient); + + glEnable(GL_LIGHT1); // lighting + + } + glEnable(GL_LIGHTING); // lighting + + + // world's simplest OpenGL optimisations... +#if GL_APPLE_transform_hint + glHint(GL_TRANSFORM_HINT_APPLE, GL_FASTEST); +#endif + + glDisable(GL_NORMALIZE); + glDisable(GL_RESCALE_NORMAL); + + m_glContextInitialized = YES; +} + + +- (void) snapShot +{ + int w = viewSize.width; + int h = viewSize.height; + + if (w & 3) + w = w + 4 - (w & 3); + + long nPixels = w * h + 1; + + unsigned char *red = (unsigned char *) malloc( nPixels); + unsigned char *green = (unsigned char *) malloc( nPixels); + unsigned char *blue = (unsigned char *) malloc( nPixels); + + NSString *filepath = [[[NSBundle mainBundle] bundlePath] stringByDeletingLastPathComponent]; + int imageNo = 0; + NSString *pathToPic = nil; + + do + { + imageNo++; + pathToPic = [filepath stringByAppendingPathComponent:[NSString stringWithFormat:@"oolite-%03d.png",imageNo]]; + } while ([[NSFileManager defaultManager] fileExistsAtPath:pathToPic]); + + OOLog(@"snapshot", @">>>>> Snapshot %d x %d file path chosen = %@", w, h, pathToPic); + + NSBitmapImageRep* bitmapRep = + [[NSBitmapImageRep alloc] + initWithBitmapDataPlanes:NULL // --> let the class allocate it + pixelsWide: w + pixelsHigh: h + bitsPerSample: 8 // each component is 8 bits (1 byte) + samplesPerPixel: 3 // number of components (R, G, B) + hasAlpha: NO // no transparency + isPlanar: NO // data integrated into single plane + colorSpaceName: NSDeviceRGBColorSpace + bytesPerRow: 3*w // can no longer let the class figure it out + bitsPerPixel: 24 // can no longer let the class figure it out + ]; + + unsigned char *pixels = [bitmapRep bitmapData]; + + glReadPixels(0,0, w,h, GL_RED, GL_UNSIGNED_BYTE, red); + glReadPixels(0,0, w,h, GL_GREEN, GL_UNSIGNED_BYTE, green); + glReadPixels(0,0, w,h, GL_BLUE, GL_UNSIGNED_BYTE, blue); + + int x,y; + for (y = 0; y < h; y++) + { + long index = (h - y - 1)*w; + for (x = 0; x < w; x++) // set bitmap pixels + { + *pixels++ = red[index]; + *pixels++ = green[index]; + *pixels++ = blue[index++]; + } + } + + [[bitmapRep representationUsingType:NSPNGFileType properties:[NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], NSImageInterlaced, NULL]] + writeToFile:pathToPic atomically:YES]; // save PNG representation of image + + // free allocated objects and memory + [bitmapRep release]; + free(red); + free(green); + free(blue); + +} + + +- (BOOL) acceptsFirstResponder +{ + return YES; +} + + +- (void) keyUp:(NSEvent *)theEvent +{ + NSString *stringValue = nil; + int key; + int keycode; + + stringValue = [theEvent charactersIgnoringModifiers]; + + /* Bug: exception when releasing accent key. + Analysis: Dead keys (accents and similar) return an empty string. + Fix: reject zero-length strings. This is the Wrong Thing - we should + really be using KeyTranslate()/UCKeyTranslate() to find out what the + string would be if you pressed the key and then space. + -- Ahruman 20070714 + */ + if ([stringValue length] < 1) return; + + key = [stringValue characterAtIndex:0]; + keycode = [theEvent keyCode] & 255; + + supressKeys = NO; + + key = keycodetrans[keycode]; // retrieve the character we got for pressing the hardware at key location 'keycode' + + OOLog(kOOLogKeyUp, @"Key up: stringValue = \"%@\", keyCode = %d, key = %u", stringValue, keycode, key); + + isAlphabetKeyDown = NO; + if ((key >= 0)&&(key < [self numKeys])&&(keys[key])) + { + keys[key] = NO; + } + else + { + if (key > [self numKeys]) + OOLog(kOOLogKeyCodeOutOfRange, @"Translated key: %d out of range", key); + } +} + + +- (void) keyDown:(NSEvent *)theEvent +{ + NSString *stringValue = nil; + int key; + int keycode; + + stringValue = [theEvent charactersIgnoringModifiers]; + + /* Bug: exception when pressing accent key. + Analysis: Dead keys (accents and similar) return an empty string. + Fix: reject zero-length strings. This is the Wrong Thing - we should + really be using KeyTranslate()/UCKeyTranslate() to find out what the + string would be if you pressed the key and then space. + -- Ahruman 20070714 + */ + if ([stringValue length] < 1) return; + + key = [stringValue characterAtIndex:0]; + keycode = [theEvent keyCode] & 255; + + key = [self translateKeyCode:key]; + + OOLog(kOOLogKeyDown, @"Key down: stringValue = \"%@\", keyCode = %d, key = %u", stringValue, keycode, key); + + keycodetrans[keycode] = key; // record the chracter we got for pressing the hardware at key location 'keycode' + + if ((key >= 0)&&(key < [self numKeys])&&(!keys[key])) + { + keys[key] = YES; + + if (allowingStringInput) + { + // limited input for planet find screen + if (allowingStringInput == gvStringInputAlpha) + { + if (((key > 64)&&(key < 91))||((key > 96)&&(key < 123))) + { + // alphanumeric + isAlphabetKeyDown = YES; + // convert to lowercase + [typedString appendFormat:@"%c", (key | 64)]; + } + else + isAlphabetKeyDown = NO; + if (key == NSDeleteCharacter) + { + //delete + [typedString setString:@""]; + } + } + + // full input for load-save screen + if (allowingStringInput == gvStringInputAll) + { + if ((key > 31)&&(key < 123)) + { + // alphanumeric + isAlphabetKeyDown = YES; + // convert to lowercase + [typedString appendFormat:@"%c", key]; + } + else + isAlphabetKeyDown = NO; + if ((key == NSDeleteCharacter) && [typedString length]) + { + //delete + [typedString deleteCharactersInRange:NSMakeRange([typedString length] - 1, 1)]; + } + } + + } + } + else + { + if (key > [self numKeys]) + OOLog(kOOLogKeyCodeOutOfRange, @"Translated key: %d out of range", key); + } +} + +/* Capture shift, ctrl, opt and command press & release */ +- (void)flagsChanged:(NSEvent *)theEvent +{ + int flags = [theEvent modifierFlags]; + opt = (flags & NSAlternateKeyMask) ? YES : NO; + ctrl = (flags & NSControlKeyMask) ? YES : NO; + command = (flags & NSCommandKeyMask) ? YES : NO; + shift = ( flags & NSShiftKeyMask ) ? YES : NO; + +} + + +- (void)mouseDown:(NSEvent *)theEvent +{ + if (doubleClick) + { + doubleClick = NO; + keys[gvMouseDoubleClick] = NO; + } + keys[gvMouseLeftButton] = YES; // 'a' down +} + + +- (void)mouseUp:(NSEvent *)theEvent +{ + NSTimeInterval timeBetweenClicks = [NSDate timeIntervalSinceReferenceDate] - timeIntervalAtLastClick; + timeIntervalAtLastClick += timeBetweenClicks; + + if (!doubleClick) + { + doubleClick = (timeBetweenClicks < MOUSE_DOUBLE_CLICK_INTERVAL); // One fifth of a second + keys[gvMouseDoubleClick] = doubleClick; + } + + keys[gvMouseLeftButton] = NO; // 'a' up +} + + +- (void)mouseMoved:(NSEvent *)theEvent +{ + double mx = [theEvent locationInWindow].x - viewSize.width/2.0; + double my = [theEvent locationInWindow].y - viewSize.height/2.0; + + if (display_z > 640.0) + { + mx /= viewSize.width * MAIN_GUI_PIXEL_WIDTH / display_z; + my /= viewSize.height; + } + else + { + mx /= MAIN_GUI_PIXEL_WIDTH * viewSize.width / 640.0; + my /= MAIN_GUI_PIXEL_HEIGHT * viewSize.width / 640.0; + } + + [self setVirtualJoystick:mx :-my]; +} + +///////////////////////////////////////////////////////////// +/* Turn the Cocoa ArrowKeys into our arrow key constants. */ +- (int) translateKeyCode: (int) input +{ + int key = input; + switch ( input ) + { + case NSUpArrowFunctionKey: + key = gvArrowKeyUp; + break; + + case NSDownArrowFunctionKey: + key = gvArrowKeyDown; + break; + + case NSLeftArrowFunctionKey: + key = gvArrowKeyLeft; + break; + + case NSRightArrowFunctionKey: + key = gvArrowKeyRight; + break; + + case NSF1FunctionKey: + key = gvFunctionKey1; + break; + + case NSF2FunctionKey: + key = gvFunctionKey2; + break; + + case NSF3FunctionKey: + key = gvFunctionKey3; + break; + + case NSF4FunctionKey: + key = gvFunctionKey4; + break; + + case NSF5FunctionKey: + key = gvFunctionKey5; + break; + + case NSF6FunctionKey: + key = gvFunctionKey6; + break; + + case NSF7FunctionKey: + key = gvFunctionKey7; + break; + + case NSF8FunctionKey: + key = gvFunctionKey8; + break; + + case NSF9FunctionKey: + key = gvFunctionKey9; + break; + + case NSF10FunctionKey: + key = gvFunctionKey10; + break; + + case NSF11FunctionKey: + key = gvFunctionKey11; + break; + + case NSHomeFunctionKey: + key = gvHomeKey; + break; + + default: + break; + } + return key; +} + + +- (JoystickHandler *)getStickHandler +{ + return [JoystickHandler sharedStickHandler]; +} + + +- (void) setVirtualJoystick:(double) vmx :(double) vmy +{ + virtualJoystickPosition.x = vmx; + virtualJoystickPosition.y = vmy; +} + + +- (NSPoint) virtualJoystickPosition +{ + return virtualJoystickPosition; +} + + +///////////////////////////////////////////////////////////// + +- (void) clearKeys +{ + int i; + for (i = 0; i < [self numKeys]; i++) + keys[i] = NO; +} + + +- (void) clearMouse +{ + keys[gvMouseDoubleClick] = NO; + keys[gvMouseLeftButton] = NO; + doubleClick = NO; +} + + +- (BOOL) isAlphabetKeyDown +{ + return isAlphabetKeyDown = NO;; +} + +// DJS: When entering submenus in the gui, it is not helpful if the +// key down that brought you into the submenu is still registered +// as down when we're in. This makes isDown return NO until a key up +// event has been received from SDL. +- (void) supressKeysUntilKeyUp +{ + if (keys[gvMouseDoubleClick] == NO) + { + supressKeys = YES; + [self clearKeys]; + } + else + { + [self clearMouse]; + } +} + + +- (BOOL) isDown: (int) key +{ + if( supressKeys ) + return NO; + if ( key < 0 ) + return NO; + if ( key >= [self numKeys] ) + return NO; + return keys[key]; +} + + +- (BOOL) isOptDown +{ + return opt; +} + + +- (BOOL) isCtrlDown +{ + return ctrl; +} + + +- (BOOL) isCommandDown +{ + return command; +} + + +- (BOOL) isShiftDown +{ + return shift; +} + + +- (int) numKeys +{ + return NUM_KEYS; +} + + +- (BOOL)pollShiftKey +{ + #define KEYMAP_GET(m, index) ((((uint8_t*)(m))[(index) >> 3] & (1L << ((index) & 7))) ? 1 : 0) + + KeyMap map; + + GetKeys(map); + return KEYMAP_GET(map, 56) || KEYMAP_GET(map, 60); // Left shift or right shift -- although 60 shouldn't occur. +} + +@end diff --git a/src/Cocoa/OOCABufferedSound.h b/src/Cocoa/OOCABufferedSound.h new file mode 100644 index 00000000..e8a2656f --- /dev/null +++ b/src/Cocoa/OOCABufferedSound.h @@ -0,0 +1,70 @@ +/* + +OOCABufferedSound.h + +Subclass of OOSound playing from an in-memory buffer. + +This class is an implementation detail. Do not use it directly; use OOSound. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOCASound.h" + +@class OOCASoundDecoder; + + +@interface OOCABufferedSound: OOSound +{ + float *_bufferL, + *_bufferR; + size_t _size; + Float64 _sampleRate; + NSString *_name; + BOOL _stereo; +} + +- (id)initWithDecoder:(OOCASoundDecoder *)inDecoder; + +@end diff --git a/src/Cocoa/OOCABufferedSound.m b/src/Cocoa/OOCABufferedSound.m new file mode 100644 index 00000000..d2fcea3a --- /dev/null +++ b/src/Cocoa/OOCABufferedSound.m @@ -0,0 +1,217 @@ +/* + +OOCABufferedSound.m + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundInternal.h" +#import "OOCASoundDecoder.h" + + +@interface OOCABufferedSound (Private) + +- (BOOL)bufferSound:(NSString *)inPath; + +@end + + +@implementation OOCABufferedSound + +#pragma mark NSObject + +- (void)dealloc +{ + if (_bufferL) free(_bufferL); + if (_stereo) _bufferR = NULL; + else if (_bufferR) free(_bufferR); + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{\"%@\", %s, %g Hz, %u bytes}", [self className], self, [self name], _stereo ? "stereo" : "mono", _sampleRate, _size * sizeof (float) * (_stereo ? 2 : 1)]; +} + + +#pragma mark OOSound + +- (NSString *)name +{ + return _name; +} + + +- (void)play +{ + [[OOSoundMixer sharedMixer] playSound:self]; +} + + +- (BOOL)getAudioStreamBasicDescription:(AudioStreamBasicDescription *)outFormat +{ + assert(NULL != outFormat); + + outFormat->mSampleRate = _sampleRate; + outFormat->mFormatID = kAudioFormatLinearPCM; + outFormat->mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kLinearPCMFormatFlagIsNonInterleaved; + outFormat->mBytesPerPacket = sizeof (float); + outFormat->mFramesPerPacket = 1; + outFormat->mBytesPerFrame = sizeof (float); + outFormat->mChannelsPerFrame = 2; + outFormat->mBitsPerChannel = sizeof (float) * 8; + outFormat->mReserved = 0; + + return YES; +} + + +// Context is (offset << 1) | loop. Offset is initially 0. +- (BOOL)prepareToPlayWithContext:(OOCASoundRenderContext *)outContext looped:(BOOL)inLoop +{ + *outContext = inLoop ? 1 : 0; + return YES; +} + + +- (OSStatus)renderWithFlags:(AudioUnitRenderActionFlags *)ioFlags frames:(UInt32)inNumFrames context:(OOCASoundRenderContext *)ioContext data:(AudioBufferList *)ioData +{ + size_t toCopy, remaining, underflow, offset; + BOOL loop, done = NO; + + loop = (*ioContext) & 1; + offset = (*ioContext) >> 1; + assert (ioData->mNumberBuffers == 2); + + if (offset < _size) + { + remaining = _size - offset; + if (remaining < inNumFrames) + { + toCopy = remaining; + underflow = inNumFrames - remaining; + } + else + { + toCopy = inNumFrames; + underflow = 0; + } + + bcopy(_bufferL + offset, ioData->mBuffers[0].mData, toCopy * sizeof (float)); + bcopy(_bufferR + offset, ioData->mBuffers[1].mData, toCopy * sizeof (float)); + + if (underflow && loop) + { + offset = toCopy; + toCopy = inNumFrames - toCopy; + if (_size < toCopy) toCopy = _size; + + bcopy(_bufferL, ((float *)ioData->mBuffers[0].mData) + offset, toCopy * sizeof (float)); + bcopy(_bufferR, ((float *)ioData->mBuffers[1].mData) + offset, toCopy * sizeof (float)); + + underflow -= toCopy; + offset = 0; + } + + *ioContext = ((offset + toCopy) << 1) | loop; + } + else + { + toCopy = 0; + underflow = inNumFrames; + *ioFlags |= kAudioUnitRenderAction_OutputIsSilence; + done = YES; + } + + if (underflow) + { + bzero(ioData->mBuffers[0].mData + toCopy, underflow * sizeof (float)); + bzero(ioData->mBuffers[1].mData + toCopy, underflow * sizeof (float)); + } + + return done ? endOfDataReached : noErr; +} + + +#pragma mark OOCABufferedSound + +- (id)initWithDecoder:(OOCASoundDecoder *)inDecoder +{ + BOOL OK = YES; + + assert(gOOSoundSetUp); + if (gOOSoundBroken || nil == inDecoder) OK = NO; + + if (OK) + { + self = [super init]; + if (nil == self) OK = NO; + } + + if (OK) + { + _name = [[inDecoder name] copy]; + _sampleRate = [inDecoder sampleRate]; + if ([inDecoder isStereo]) + { + OK = [inDecoder readStereoCreatingLeftBuffer:&_bufferL rightBuffer:&_bufferR withFrameCount:&_size]; + _stereo = YES; + } + else + { + OK = [inDecoder readMonoCreatingBuffer:&_bufferL withFrameCount:&_size]; + _bufferR = _bufferL; + } + } + + if (!OK) + { + [self release]; + self = nil; + } + return self; +} + +@end diff --git a/src/Cocoa/OOCAMusic.h b/src/Cocoa/OOCAMusic.h new file mode 100644 index 00000000..541124d3 --- /dev/null +++ b/src/Cocoa/OOCAMusic.h @@ -0,0 +1,64 @@ +/* + +OOCAMusic.h + +Subclass of OOSound with additional controls specific to music playback. Only +one instance of OOMusic may be playing at a time. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOCASound.h" + + +@interface OOMusic: OOSound +{ + OOSound *sound; +} + +- (void) playLooped:(BOOL)looped; +- (void) stop; +- (BOOL) isPlaying; + +@end diff --git a/src/Cocoa/OOCAMusic.m b/src/Cocoa/OOCAMusic.m new file mode 100644 index 00000000..99362826 --- /dev/null +++ b/src/Cocoa/OOCAMusic.m @@ -0,0 +1,132 @@ +/* + +OOCAMusic.m + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundInternal.h" + +static OOMusic *sPlayingMusic = nil; +static OOSoundSource *sMusicSource = nil; + + +@implementation OOMusic + +#pragma mark NSObject + ++ (id)allocWithZone:(NSZone *)inZone +{ + return NSAllocateObject([OOMusic class], 0, inZone); +} + + +- (void)dealloc +{ + if (sPlayingMusic == self) [self stop]; + [sound release]; + + [super dealloc]; +} + +#pragma mark OOSound + +- (id)initWithContentsOfFile:(NSString *)inPath +{ + self = [super init]; + if (nil != self) + { + sound = [[OOSound alloc] initWithContentsOfFile:inPath]; + if (nil == sound) + { + [self release]; + self = nil; + } + } + + return self; +} + + +- (NSString *)name +{ + return [sound name]; +} + + +#pragma mark OOMusic + +- (void)playLooped:(BOOL)inLoop +{ + if (sPlayingMusic != self) + { + if (nil == sMusicSource) + { + sMusicSource = [[OOSoundSource alloc] init]; + } + [sMusicSource stop]; + [sMusicSource setLoop:inLoop]; + [sMusicSource setSound:sound]; + [sMusicSource play]; + + sPlayingMusic = self; + } +} + + +- (BOOL)isPlaying +{ + return sPlayingMusic == self && [sMusicSource isPlaying]; +} + + +- (void)stop +{ + if (sPlayingMusic == self) + { + sPlayingMusic = nil; + [sMusicSource stop]; + } +} + +@end diff --git a/src/Cocoa/OOCASound.h b/src/Cocoa/OOCASound.h new file mode 100644 index 00000000..ca60cccd --- /dev/null +++ b/src/Cocoa/OOCASound.h @@ -0,0 +1,68 @@ +/* + +OOCASound.h + +Abstract base class for sounds, and primary sound loading interface. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +@interface OOSound: NSObject +{ + uint32_t _playingCount; +} + ++ (void) setUp; ++ (void) update; + ++ (void) setMasterVolume:(float) fraction; ++ (float) masterVolume; + +- (id) initWithContentsOfFile:(NSString *)path; + +- (NSString *)name; + +@end diff --git a/src/Cocoa/OOCASound.m b/src/Cocoa/OOCASound.m new file mode 100644 index 00000000..4902a0ca --- /dev/null +++ b/src/Cocoa/OOCASound.m @@ -0,0 +1,305 @@ +/* + +OOCASound.m + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +/* OOSound is a class cluster. A single OOSound object exists, which + represents a sound that has been alloced but not inited. The designated + initialiser returns a concrete subclass, either OOCABufferedSound or + OOCAStreamingSound, depending on the size of the sound data. +*/ + +#import "OOCASoundInternal.h" +#import "OOCASoundDecoder.h" +#import +#import +#import "NSThreadOOExtensions.h" + +#define KEY_VOLUME_CONTROL @"volume_control" +#define KEY_MAX_BUFFERED_SOUND @"max_buffered_sound" + + +NSString * const kOOLogDeprecatedMethodOOCASound = @"general.error.deprecatedMethod.oocasound"; +NSString * const kOOLogSoundInitError = @"sound.initialization.error"; +static NSString * const kOOLogSoundLoadingSuccess = @"sound.load.success"; +static NSString * const kOOLogSoundLoadingError = @"sound.load.error"; + + +static float sNominalVolume = 1.0f; +BOOL gOOSoundSetUp = NO; +BOOL gOOSoundBroken = NO; +NSRecursiveLock *gOOCASoundSyncLock = nil; // Used to ensure thread-safety of play and stop, specifically because stop may be called from the CoreAudio thread. + +static OOSound *sSingletonOOSound = nil; +static size_t sMaxBufferedSoundSize = 1 << 20; // 1 MB + + +@implementation OOSound + +#pragma mark NSObject + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (self != [OOSound class]) return [super allocWithZone:inZone]; + + if (nil == sSingletonOOSound) + { + sSingletonOOSound = [super allocWithZone:inZone]; + } + + return sSingletonOOSound; +} + + +- (id)init +{ + if ([self isMemberOfClass:[OOSound class]]) + { + [self release]; + return nil; + } + else + { + return [super init]; + } +} + + +- (void)dealloc +{ + if (self == sSingletonOOSound) sSingletonOOSound = nil; + + [super dealloc]; +} + + +- (NSString *)description +{ + if ([self isMemberOfClass:[OOSound class]]) + { + return [NSString stringWithFormat:@"<%@ %p>(singleton placeholder)", [self className], self]; + } + else + { + return [NSString stringWithFormat:@"<%@ %p>{\"%@\"}", [self className], self, [self name]]; + } +} + + +#pragma mark OOSound + ++ (void) setUp +{ + NSUserDefaults *prefs = nil; + + if (!gOOSoundSetUp) + { + gOOCASoundSyncLock = [[NSRecursiveLock alloc] init]; + [gOOCASoundSyncLock ooSetName:@"OOCASound synchronization lock"]; + if (nil == gOOCASoundSyncLock) + { + OOLog(kOOLogSoundInitError, @"Failed to set up sound (lock allocation failed). No sound will be played."); + gOOSoundBroken = YES; + } + if (!gOOSoundBroken) + { + if (![OOSoundChannel setUp]) gOOSoundBroken = YES; + } + + gOOSoundSetUp = YES; // Must be before [OOSoundMixer sharedMixer] below. + + prefs = [NSUserDefaults standardUserDefaults]; + + if ([prefs objectForKey:KEY_VOLUME_CONTROL]) sNominalVolume = [prefs floatForKey:KEY_VOLUME_CONTROL]; + else sNominalVolume = 0.75; // default setting at 75% system volume + [[OOSoundMixer sharedMixer] setMasterVolume:sNominalVolume]; + + if ([prefs objectForKey:KEY_MAX_BUFFERED_SOUND]) + { + int maxSize = [prefs integerForKey:KEY_MAX_BUFFERED_SOUND]; + if (0 <= maxSize) sMaxBufferedSoundSize = maxSize; + } + } +} + + ++ (void) update +{ + [[OOSoundMixer sharedMixer] update]; +} + + ++ (void) setMasterVolume:(float)fraction +{ + if (fraction != sNominalVolume) + { + [[OOSoundMixer sharedMixer] setMasterVolume:fraction]; + + sNominalVolume = fraction; + [[NSUserDefaults standardUserDefaults] setFloat:fraction forKey:KEY_VOLUME_CONTROL]; + } +} + + ++ (float) masterVolume +{ + return sNominalVolume; +} + + +/* Designated initialiser for OOSound. + Note: OOSound is a class cluster. This will always return a subclass of OOSound. +*/ +- (id) initWithContentsOfFile:(NSString *)inPath +{ + OOCASoundDecoder *decoder; + + if (!gOOSoundSetUp) [OOSound setUp]; + + decoder = [[OOCASoundDecoder alloc] initWithPath:inPath]; + if (nil == decoder) return nil; + + if ([decoder sizeAsBuffer] <= sMaxBufferedSoundSize) + { + self = [[OOCABufferedSound alloc] initWithDecoder:decoder]; + } + else + { + self = [[OOCAStreamingSound alloc] initWithDecoder:decoder]; + } + [decoder release]; + + if (nil != self) + { + #ifndef NDEBUG + OOLog(kOOLogSoundLoadingSuccess, @"Loaded sound %@", self); + #endif + } + else + { + OOLog(kOOLogSoundLoadingError, @"Failed to load sound \"%@\"", inPath); + } + + return self; +} + + +- (id)initWithDecoder:(OOCASoundDecoder *)inDecoder +{ + [self release]; + return nil; +} + + +- (OSStatus)renderWithFlags:(AudioUnitRenderActionFlags *)ioFlags frames:(UInt32)inNumFrames context:(OOCASoundRenderContext *)ioContext data:(AudioBufferList *)ioData +{ + OOLog(@"general.error.subclassResponsibility.OOCASound-renderWithFlags", @"%s shouldn't be called - subclass responsibility.", __PRETTY_FUNCTION__); + return unimpErr; +} + + +- (void)incrementPlayingCount +{ + ++_playingCount; +} + + +- (void)decrementPlayingCount +{ + //assert(0 != _playingCount); + if (EXPECT(_playingCount != 0)) --_playingCount; + else OOLog(@"sound.playUnderflow", @"Playing count for %@ dropped below 0!", self); +} + + +- (BOOL) isPlaying +{ + return 0 != _playingCount; +} + + +- (uint32_t)playingCount +{ + return _playingCount; +} + + +- (BOOL)prepareToPlayWithContext:(OOCASoundRenderContext *)outContext looped:(BOOL)inLoop +{ + return YES; +} + + +- (void)finishStoppingWithContext:(OOCASoundRenderContext)inContext +{ + +} + + +- (BOOL)doPlay +{ + return YES; +} + + +- (BOOL)doStop +{ + return YES; +} + + +- (NSString *)name +{ + return nil; +} + + +- (BOOL)getAudioStreamBasicDescription:(AudioStreamBasicDescription *)outFormat +{ + return NO; +} + +@end diff --git a/src/Cocoa/OOCASoundChannel.h b/src/Cocoa/OOCASoundChannel.h new file mode 100644 index 00000000..c1c6f164 --- /dev/null +++ b/src/Cocoa/OOCASoundChannel.h @@ -0,0 +1,110 @@ +/* + +OOCASoundChannel.h + +A channel for audio playback. + +This class is an implementation detail. Do not use it directly; use an +OOSoundSource to play an OOSound. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import + +@class OOSound; + + +typedef uintptr_t OOCASoundRenderContext; +typedef OSStatus (*OOSoundChannel_RenderIMP)(id inSelf, SEL inSelector, AudioUnitRenderActionFlags *ioFlags, UInt32 inNumFrames, OOCASoundRenderContext *ioContext, AudioBufferList *ioData); + + +@interface OOSoundChannel: NSObject +{ + OOSoundChannel *_next; + id _delegate; + AUNode _subGraphNode; + AUGraph _subGraph; + AUNode _node; + AudioUnit _au; + OOSound *_sound; + OOCASoundRenderContext _context; + OOSoundChannel_RenderIMP Render; + uint8_t _state, + _id, + _stopReq; + OSStatus _error; +} + ++ (BOOL)setUp; ++ (void)tearDown; + +- (id)initWithID:(uint32_t)inID auGraph:(AUGraph)inGraph; + +- (void)setDelegate:(id)inDelegate; + +- (uint32_t)ID; + +- (AUNode)auSubGraphNode; + +// Unretained pointer used to maintain simple stack +- (OOSoundChannel *)next; +- (void)setNext:(OOSoundChannel *)inNext; + +- (BOOL)playSound:(OOSound *)inSound looped:(BOOL)inLoop; +- (void)stop; + +- (OOSound *)sound; + +- (BOOL)isOK; + +@end + + +@interface NSObject(OOSoundChannelDelegate) + +// Note: this will be called in a separate thread. +- (void)channel:(OOSoundChannel *)inChannel didFinishPlayingSound:(OOSound *)inSound; + +@end diff --git a/src/Cocoa/OOCASoundChannel.m b/src/Cocoa/OOCASoundChannel.m new file mode 100644 index 00000000..da9d4659 --- /dev/null +++ b/src/Cocoa/OOCASoundChannel.m @@ -0,0 +1,723 @@ +/* + +OOCASoundChannel.m + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundChannel.h" +#import "OOCASoundInternal.h" +#import +#import +#import +#import "NSThreadOOExtensions.h" + + +static NSString * const kOOLogSoundNULLError = @"sound.render.undexpectedNull"; +static NSString * const kOOLogSoundPlaySuccess = @"sound.play.success"; +static NSString * const kOOLogSoundBadReuse = @"sound.play.failed.badReuse"; +static NSString * const kOOLogSoundSetupFailed = @"sound.play.failed.setupFailed"; +static NSString * const kOOLogSoundPlayAUError = @"sound.play.failed.auError"; +static NSString * const kOOLogSoundPlayUnknownError = @"sound.play.failed"; +static NSString * const kOOLogSoundCleanUpSuccess = @"sound.channel.cleanup.success"; +static NSString * const kOOLogSoundCleanUpBroken = @"sound.channel.cleanup.failed.broken"; +static NSString * const kOOLogSoundCleanUpBadState = @"sound.channel.cleanup.failed.badState"; +static NSString * const kOOLogSoundMachPortError = @"sound.channel.machPortError"; + + +// Tracks a kind of error that isn’t happening any more. +#define COUNT_NULLS 0 + + +static mach_port_t sReapPort = MACH_PORT_NULL; +static mach_port_t sStatusPort = MACH_PORT_NULL; +static BOOL sReaperRunning = NO; +static OOSoundChannel_RenderIMP SoundChannelRender = NULL; + +#if COUNT_NULLS +static int32_t sDebugUnexpectedNullCount = 0; +#endif + +/* + When a channel finishes playing, it is put in the “play thread dead list.” At the end of each + sound callback, if the play thread dead list is not empty and the reap queue mutex can be + acquired, the play thread dead list is copied to the reap queue under the mutex. This allows us + to use a mutex-protected list to communicate with the reap queue without having to wait on the + mutex in the real-time thread. +*/ +static OOSoundChannel *sPlayThreadDeadList = NULL; +static OOSoundChannel *sReapQueue = NULL; +static pthread_mutex_t sReapQueueMutex = { 0 }; + +enum +{ + kState_Stopped, + kState_Playing, + kState_Ended, + kState_Reap, + + kState_Broken +}; + + +#define kAURenderSelector @selector(renderWithFlags:frames:context:data:) + + +@interface OOSoundChannel(Private) + ++ (void)reaperThread:junk; + +- (void)reap; +- (void)cleanUp; + +- (OSStatus)renderWithFlags:(AudioUnitRenderActionFlags *)ioFlags frames:(UInt32)inNumFrames context:(OOCASoundRenderContext *)ioContext data:(AudioBufferList *)ioData; + +@end + + +enum +{ + // Port messages + kMsgThreadUp = 1UL, + kMsgDie, + kMsgThreadDied, + kMsgWakeUp +}; + + +typedef struct +{ + uintptr_t tag; + void *value; +} PortMessage; + + +typedef struct +{ + mach_msg_header_t header; + mach_msg_size_t descCount; + mach_msg_descriptor_t descriptor; + PortMessage message; +} PortSendMsgBody; + + +typedef struct +{ + mach_msg_header_t header; + mach_msg_size_t descCount; + mach_msg_descriptor_t descriptor; + PortMessage message; + mach_msg_trailer_t trailer; +} PortWaitMsgBody; + + +static OSStatus ChannelRenderProc(void *inRefCon, AudioUnitRenderActionFlags *ioFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumFrames, AudioBufferList *ioData); + +static mach_port_t CreatePort(void); +static void PortSend(mach_port_t inPort, PortMessage inMessage); +static BOOL PortWait(mach_port_t inPort, PortMessage *outMessage); + + +@implementation OOSoundChannel + ++ (BOOL)setUp +{ + BOOL OK = YES; + PortMessage message; + + if (sReaperRunning) return YES; + + SoundChannelRender = (OOSoundChannel_RenderIMP)[OOSoundChannel instanceMethodForSelector:kAURenderSelector]; + if (NULL == SoundChannelRender) OK = NO; + + if (OK) + { + sReapPort = CreatePort(); + if (MACH_PORT_NULL == sReapPort) OK = NO; + } + + if (OK) + { + sStatusPort = CreatePort(); + if (MACH_PORT_NULL == sStatusPort) OK = NO; + } + + if (OK) + { + if (0 != pthread_mutex_init(&sReapQueueMutex, NULL)) OK = NO; + } + + if (OK) + { + [NSThread detachNewThreadSelector:@selector(reaperThread:) toTarget:self withObject:nil]; + OK = PortWait(sStatusPort, &message); + if (OK) + { + if (kMsgThreadUp != message.tag) OK = NO; + } + } + + if (!OK) + { + OOLog(kOOLogSoundInitError, @"Failed to set up sound (channel queue allocation failed). No sound will be played."); + } + return OK; +} + + ++ (void)tearDown +{ + ipc_space_t task; + PortMessage message = { kMsgDie, NULL }; + + if (sReaperRunning) + { + PortSend(sReapPort, message); + PortWait(sStatusPort, &message); + } + + task = mach_task_self(); + if (MACH_PORT_NULL != sReapPort) mach_port_destroy(task, sReapPort); + if (MACH_PORT_NULL != sStatusPort) mach_port_destroy(task, sStatusPort); +} + + ++ (void)reaperThread:junk +{ + PortMessage message = { kMsgThreadUp, NULL }; + OOSoundChannel *chan; + NSAutoreleasePool *pool = nil; + + [NSThread ooSetCurrentThreadName:@"OOSoundChannel reaper thread"]; + sReaperRunning = YES; + PortSend(sStatusPort, message); + + [NSThread setThreadPriority:0.5]; + + for (;;) + { + if (PortWait(sReapPort, &message)) + { + pool = [[NSAutoreleasePool alloc] init]; + + if (kMsgWakeUp == message.tag) + { + assert (!pthread_mutex_lock(&sReapQueueMutex)); + + while (sReapQueue) + { + chan = sReapQueue; + sReapQueue = chan->_next; + if (kState_Reap == chan->_state) [chan reap]; + [chan cleanUp]; + } + + pthread_mutex_unlock(&sReapQueueMutex); + } + else if (kMsgDie == message.tag) + { + [pool release]; + break; + } + + [pool release]; + } + } + + message.tag = kMsgThreadDied; + message.value = NULL; + sReaperRunning = NO; + PortSend(sStatusPort, message); +} + + +- (id)init +{ + [self release]; + return nil; +} + + +- (id)initWithID:(uint32_t)inID auGraph:(AUGraph)inGraph +{ + OSStatus err = noErr; + ComponentDescription desc; + AURenderCallbackStruct input; + + assert(sReaperRunning); + + self = [super init]; + if (nil != self) + { + _id = inID; + + // Create a subgraph (since we can’t have multiple output units otherwise) + err = AUGraphNewNodeSubGraph(inGraph, &_subGraphNode); + if (!err) err = AUGraphGetNodeInfoSubGraph(inGraph, _subGraphNode, &_subGraph); + + // Create an output unit + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_GenericOutput; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + if (!err) err = OOAUGraphAddNode(_subGraph, &desc, &_node); + if (!err) err = OOAUGraphNodeInfo(_subGraph, _node, NULL, &_au); + + // Set render callback + input.inputProc = ChannelRenderProc; + input.inputProcRefCon = self; + if (!err) err = AudioUnitSetProperty(_au, kAudioUnitProperty_SetRenderCallback, + kAudioUnitScope_Input, 0, &input, sizeof input); + + // Init & check errors + if (!err) err = AudioUnitInitialize(_au); + + if (err) + { + OOLog(kOOLogSoundInitError, @"AudioUnit setup error %@ preparing channel ID %u.", AudioErrorNSString(err), inID); + + [self release]; + self = nil; + } + } + + return self; +} + + +- (void)dealloc +{ + [self stop]; + if (NULL != _au) CloseComponent(_au); + + [super dealloc]; +} + + +- (void)setDelegate:(id)inDelegate +{ + _delegate = inDelegate; +} + + +- (uint32_t)ID +{ + return _id; +} + + +- (AUNode)auSubGraphNode +{ + return _subGraphNode; +} + + +- (OOSoundChannel *)next +{ + return _next; +} + + +- (void)setNext:(OOSoundChannel *)inNext +{ + _next = inNext; +} + + +- (OOSound *)sound +{ + return _sound; +} + + +- (BOOL)playSound:(OOSound *)inSound looped:(BOOL)inLooped +{ + BOOL OK = YES; + OSStatus err = noErr; + AudioStreamBasicDescription format; + OOSound *temp; + + #if COUNT_NULLS + SInt32 unexpectedNulls; + + unexpectedNulls = sDebugUnexpectedNullCount; + if (0 != unexpectedNulls) + { + OOSoundAtomicAdd(-unexpectedNulls, &sDebugUnexpectedNullCount); + if (1 == unexpectedNulls) + { + OOLog(kOOLogSoundNULLError, @"A NULL Render() or nil _sound error has occured."); + } + else + { + OOLog(kOOLogSoundNULLError, @"%i NULL Render() or nil _sound errors have occured.", (int)unexpectedNulls); + } + } + #endif + + if (nil != inSound) + { + [gOOCASoundSyncLock lock]; + if (kState_Stopped != _state) + { + OOLog(kOOLogSoundBadReuse, @"Channel %@ reused while playing.", self); + + [[OOSoundMixer sharedMixer] disconnectChannel:self]; + if (_sound) + { + Render = NULL; + temp = _sound; + _sound = nil; + [temp finishStoppingWithContext:_context]; + _context = 0; + [temp release]; + } + _stopReq = NO; + _state = kState_Stopped; + } + + Render = (OOSoundChannel_RenderIMP)[inSound methodForSelector:kAURenderSelector]; + OK = (NULL != Render); + + if (OK) OK = [inSound getAudioStreamBasicDescription:&format]; + if (OK) OK = [inSound prepareToPlayWithContext:&_context looped:inLooped]; + + if (!OK) + { + OOLog(kOOLogSoundSetupFailed, @"Failed to play sound %@ - set-up failed.", inSound); + } + + if (OK) + { + _sound = inSound; + + err = AudioUnitSetProperty(_au, kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, 0, &format, sizeof format); + + if (err) OOLog(kOOLogSoundPlayAUError, @"Failed to play %@ (error %@)", inSound, AudioErrorNSString(err)); + OK = !err; + } + + if (OK) OK = [[OOSoundMixer sharedMixer] connectChannel:self]; + + if (OK) + { + [_sound retain]; + _state = kState_Playing; + OOLog(kOOLogSoundPlaySuccess, @"Playing sound %@", _sound); + } + else + { + _sound = nil; + if (!err) OOLog(kOOLogSoundPlayUnknownError, @"Failed to play %@", inSound); + } + [gOOCASoundSyncLock unlock]; + } + + return OK; +} + + +- (void)stop +{ + if (kState_Playing == _state) + { + _stopReq = YES; + } + + if (kState_Ended == _state) [self cleanUp]; +} + + +- (void)reap +{ + OSStatus err; + + err = [[OOSoundMixer sharedMixer] disconnectChannel:self]; + + if (noErr == err) + { + _state = kState_Ended; + } + else + { + _state = kState_Broken; + _error = err; + } +} + + +- (void)cleanUp +{ + OOSound *sound; + + [gOOCASoundSyncLock lock]; + + if (kState_Broken == _state) + { + OOLog(kOOLogSoundCleanUpBroken, @"Sound channel %@ broke with error %@.", self, AudioErrorNSString(_error)); + } + + if (kState_Ended == _state || kState_Broken == _state) + { + Render = NULL; + sound = _sound; + _sound = nil; + [sound finishStoppingWithContext:_context]; + _context = 0; + + _state = kState_Stopped; + _stopReq = NO; + + if (nil != _delegate && [_delegate respondsToSelector:@selector(channel:didFinishPlayingSound:)]) + { + [_delegate channel:self didFinishPlayingSound:sound]; + } + [sound release]; + + OOLog(kOOLogSoundCleanUpSuccess, @"Sound channel id %u cleaned up successfully.", _id); + } + else + { + OOLog(kOOLogSoundCleanUpBadState, @"Sound channel %@ cleaned up in invalid state %u.", self, _state); + } + + [gOOCASoundSyncLock unlock]; +} + + +- (BOOL)isOK +{ + return kState_Broken != _state; +} + + +- (NSString *)description +{ + NSString *result, *stateString; + + [gOOCASoundSyncLock lock]; + switch (_state) + { + case kState_Stopped: + stateString = @"stopped"; + break; + + case kState_Playing: + stateString = @"playing"; + break; + + case kState_Ended: + stateString = @"ended"; + break; + + case kState_Broken: + stateString = [NSString stringWithFormat:@"broken (%@)", AudioErrorShortNSString(_error)]; + break; + + default: + stateString = [NSString stringWithFormat:@"unknown (%u)", _state]; + } + + result = [NSString stringWithFormat:@"<%@ %p>{ID=%u, state=%@, sound=%@}", [self className], self, _id, stateString, _sound]; + [gOOCASoundSyncLock unlock]; + + return result; +} + + +- (OSStatus)renderWithFlags:(AudioUnitRenderActionFlags *)ioFlags frames:(UInt32)inNumFrames context:(OOCASoundRenderContext *)ioContext data:(AudioBufferList *)ioData +{ + OSStatus err = noErr; + PortMessage message; + + if (__builtin_expect(_stopReq, 0)) err = endOfDataReached; + else if (__builtin_expect(kState_Playing == _state, 1)) + { + if (NULL != Render && nil != _sound) + { + err = Render(_sound, kAURenderSelector, ioFlags, inNumFrames, &_context, ioData); + } + else + { + unsigned i, count; + + err = endOfDataReached; + count = ioData->mNumberBuffers; + + for (i = 0; i != count; ++count) + { + bzero(ioData->mBuffers[i].mData, ioData->mBuffers[i].mDataByteSize); + } + *ioFlags |= kAudioUnitRenderAction_OutputIsSilence; + + #if COUNT_NULLS + // Logging in real-time thread _baaaaaad_. + if (NULL == Render || nil == _sound) + { + OOSoundAtomicAdd(1, &sDebugUnexpectedNullCount); + } + #endif + } + } + + if (__builtin_expect(endOfDataReached == err, 0)) + { + err = noErr; + if (__builtin_expect(kState_Playing == _state, 1)) + { + _state = kState_Reap; + + _next = sPlayThreadDeadList; + sPlayThreadDeadList = self; + } + } + + if (__builtin_expect(nil != sPlayThreadDeadList && !pthread_mutex_trylock(&sReapQueueMutex), 0)) + { + // Put sPlayThreadDeadList at front of sReapQueue + OOSoundChannel *curr; + + curr = sPlayThreadDeadList; + while (nil != curr->_next) curr = curr->_next; + + curr->_next = sReapQueue; + sReapQueue = sPlayThreadDeadList; + sPlayThreadDeadList = nil; + + pthread_mutex_unlock(&sReapQueueMutex); + + // Wake up reaper thread + message.tag = kMsgWakeUp; + message.value = NULL; + PortSend(sReapPort, message); + } + + return err; +} + +@end + + +static OSStatus ChannelRenderProc(void *inRefCon, AudioUnitRenderActionFlags *ioFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumFrames, AudioBufferList *ioData) +{ + return SoundChannelRender((id)inRefCon, kAURenderSelector, ioFlags, inNumFrames, 0, ioData); +} + + +static mach_port_t CreatePort(void) +{ + kern_return_t err; + mach_port_t result; + ipc_space_t task; + mach_msg_type_name_t type; + mach_port_t sendRight; + + task = mach_task_self(); + err = mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &result); + if (KERN_SUCCESS == err) err = mach_port_insert_right(task, result, result, MACH_MSG_TYPE_MAKE_SEND); + if (KERN_SUCCESS == err) err = mach_port_extract_right(task, result, MACH_MSG_TYPE_MAKE_SEND, &sendRight, &type); + + if (KERN_SUCCESS != err) + { + OOLog(kOOLogSoundInitError, @"Mach port creation failure: %@", KernelResultNSString(err)); + result = MACH_PORT_NULL; + } + + return result; +} + + +static void PortSend(mach_port_t inPort, PortMessage inMessage) +{ + PortSendMsgBody message; + mach_msg_return_t result; + + bzero(&message, sizeof message); + + message.header.msgh_bits = MACH_MSGH_BITS_REMOTE(MACH_MSG_TYPE_MAKE_SEND); + message.header.msgh_size = sizeof message; + message.header.msgh_remote_port = inPort; + message.header.msgh_local_port = MACH_PORT_NULL; + + message.descCount = 1; + + message.message = inMessage; + + result = mach_msg(&message.header, MACH_SEND_MSG | MACH_SEND_TIMEOUT, sizeof message, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + if (MACH_MSG_SUCCESS != result) + { + OOLog(kOOLogSoundMachPortError, @"Mach port transient send failure: %@", KernelResultNSString(result)); + result = mach_msg(&message.header, MACH_SEND_MSG, sizeof message, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL); + if (MACH_MSG_SUCCESS != result) + { + OOLog(kOOLogSoundMachPortError, @"Mach port send failure: %@", KernelResultNSString(result)); + } + } +} + + +static BOOL PortWait(mach_port_t inPort, PortMessage *outMessage) +{ + PortWaitMsgBody message; + mach_msg_return_t result; + + bzero(&message, sizeof message); + + message.header.msgh_bits = MACH_MSGH_BITS_LOCAL(MACH_MSG_TYPE_COPY_RECEIVE); + message.header.msgh_size = sizeof message; + message.header.msgh_local_port = inPort; + + result = mach_msg_receive(&message.header); + if (MACH_MSG_SUCCESS == result) + { + if (NULL != outMessage) *outMessage = message.message; + } + else + { + if (MACH_RCV_TIMED_OUT != result) OOLog(kOOLogSoundMachPortError, @"Mach port receive failure: %@", KernelResultNSString(result)); + } + + return MACH_MSG_SUCCESS == result; +} diff --git a/src/Cocoa/OOCASoundDecoder.h b/src/Cocoa/OOCASoundDecoder.h new file mode 100644 index 00000000..67b4767d --- /dev/null +++ b/src/Cocoa/OOCASoundDecoder.h @@ -0,0 +1,81 @@ +/* + +OOCASoundDecoder.h + +Class responsible for converting a sound to a PCM buffer for playback. This +class is an implementation detail. Do not use it directly; use OOSound to +load sounds. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +@interface OOCASoundDecoder: NSObject + +- (id)initWithPath:(NSString *)inPath; ++ (OOCASoundDecoder *)codecWithPath:(NSString *)inPath; + +// Full-buffer reading. +- (BOOL)readMonoCreatingBuffer:(float **)outBuffer withFrameCount:(size_t *)outSize; +- (BOOL)readStereoCreatingLeftBuffer:(float **)outLeftBuffer rightBuffer:(float **)outRightBuffer withFrameCount:(size_t *)outSize; + +// Stream reading. This will always provide two channels (as non-interleaved PCM), discarding extra channels or doubling mono as necessary. +- (size_t)streamStereoToBufferL:(float *)ioBufferL bufferR:(float *)ioBufferR maxFrames:(size_t)inMax; + +// Returns the size of the data -readMonoCreatingBuffer:withFrameCount: will create. +- (size_t)sizeAsBuffer; + +- (BOOL)isStereo; + +- (Float64)sampleRate; + +// For streaming +- (BOOL)atEnd; +- (BOOL)scanToOffset:(uint64_t)inOffset; +- (void)rewindToBeginning; + +- (NSString *)name; + +@end diff --git a/src/Cocoa/OOCASoundDecoder.m b/src/Cocoa/OOCASoundDecoder.m new file mode 100644 index 00000000..217f73e5 --- /dev/null +++ b/src/Cocoa/OOCASoundDecoder.m @@ -0,0 +1,524 @@ +/* + +OOCASoundDecoder.m + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundDecoder.h" +#import +#import +#import "OOLogging.h" + + +enum +{ + kMaxDecodeSize = 1 << 20 // 2^20 frames = 4 MB +}; + + +static void MixDown(float *inChan1, float *inChan2, float *outMix, size_t inCount); + + +@interface OOCASoundVorbisCodec: OOCASoundDecoder +{ + OggVorbis_File _vf; + NSString *_name; + BOOL _atEnd; +} + +- (NSDictionary *)comments; + +@end + + +@implementation OOCASoundDecoder + +#ifndef NDEBUG +- (id)init +{ + OOLog(kOOLogDeprecatedMethod, @"Invalid call of %s; designated initializer is -initWithPath:", __PRETTY_FUNCTION__); + [self release]; + return nil; +} +#endif + + +- (id)initWithPath:(NSString *)inPath +{ + [self release]; + self = nil; + + if ([[inPath pathExtension] isEqual:@"ogg"]) + { + self = [[OOCASoundVorbisCodec alloc] initWithPath:inPath]; + } + + return self; +} + + ++ (OOCASoundDecoder *)codecWithPath:(NSString *)inPath +{ + if ([[inPath pathExtension] isEqual:@"ogg"]) + { + return [[[OOCASoundVorbisCodec alloc] initWithPath:inPath] autorelease]; + } + return nil; +} + + +- (size_t)streamStereoToBufferL:(float *)ioBufferL bufferR:(float *)ioBufferR maxFrames:(size_t)inMax +{ + return 0; +} + + +- (BOOL)readMonoCreatingBuffer:(float **)outBuffer withFrameCount:(size_t *)outSize +{ + if (NULL != outBuffer) *outBuffer = NULL; + if (NULL != outSize) *outSize = 0; + + return NO; +} + + +- (BOOL)readStereoCreatingLeftBuffer:(float **)outLeftBuffer rightBuffer:(float **)outRightBuffer withFrameCount:(size_t *)outSize +{ + if (NULL != outLeftBuffer) *outLeftBuffer = NULL; + if (NULL != outRightBuffer) *outRightBuffer = NULL; + if (NULL != outSize) *outSize = 0; + + return NO; +} + + +- (size_t)sizeAsBuffer +{ + return 0; +} + + +- (BOOL)isStereo +{ + return NO; +} + + +- (double)sampleRate +{ + return 0; +} + + +- (BOOL)atEnd +{ + return YES; +} + + +- (void)rewindToBeginning +{ + +} + + +- (BOOL)scanToOffset:(uint64_t)inOffset +{ + return NO; +} + + +- (NSString *)name +{ + return @""; +} + +@end + + +@implementation OOCASoundVorbisCodec + +- (id)initWithPath:(NSString *)inPath +{ + BOOL OK = NO; + int err; + FILE *file; + + _name = [[inPath lastPathComponent] retain]; + + if (nil != inPath) + { + file = fopen([inPath UTF8String], "rb"); + if (NULL != file) + { + err = ov_open(file, &_vf, NULL, 0); + if (0 == err) + { + OK = YES; + } + } + } + + if (!OK) + { + [self release]; + self = nil; + } + + return self; +} + + +- (void)dealloc +{ + [_name release]; + ov_clear(&_vf); + + [super dealloc]; +} + + +- (NSDictionary *)comments +{ + vorbis_comment *comments; + unsigned i, count; + NSMutableDictionary *result = nil; + NSString *comment, *key, *value; + NSRange range; + + comments = ov_comment(&_vf, -1); + if (NULL != comments) + { + count = comments->comments; + if (0 != count) + { + result = [NSMutableDictionary dictionaryWithCapacity:count]; + for (i = 0; i != count; ++i) + { + comment = [[NSString alloc] initWithBytesNoCopy:comments->user_comments[i] length:comments->comment_lengths[i] encoding:NSUTF8StringEncoding freeWhenDone:NO]; + range = [comment rangeOfString:@"="]; + if (0 != range.length) + { + key = [comment substringToIndex:range.location]; + value = [comment substringFromIndex:range.location + 1]; + } + else + { + key = comment; + value = @""; + } + [result setObject:value forKey:key]; + + [comment release]; + } + } + } + + return result; +} + + +- (BOOL)readMonoCreatingBuffer:(float **)outBuffer withFrameCount:(size_t *)outSize +{ + float *buffer = NULL, *dst, **src; + size_t sizeInFrames = 0, remaining; + unsigned chanCount; + long framesRead; + ogg_int64_t totalSizeInFrames; + BOOL OK = YES; + + if (NULL != outBuffer) *outBuffer = NULL; + if (NULL != outSize) *outSize = 0; + if (NULL == outBuffer || NULL == outSize) OK = NO; + + if (OK) + { + totalSizeInFrames = ov_pcm_total(&_vf, -1); + assert (kMaxDecodeSize < SIZE_T_MAX); // Should have been checked by caller + sizeInFrames = totalSizeInFrames; + } + + if (OK) + { + buffer = malloc(sizeof (float) * sizeInFrames); + if (!buffer) OK = NO; + } + + if (OK && sizeInFrames) + { + remaining = sizeInFrames; + dst = buffer; + + do + { + chanCount = ov_info(&_vf, -1)->channels; + framesRead = ov_read_float(&_vf, &src, remaining, NULL); + if (framesRead <= 0) + { + if (OV_HOLE == framesRead) continue; + //else: + break; + } + + if (1 == chanCount) bcopy(src[0], dst, sizeof (float) * framesRead); + else MixDown(src[0], src[1], dst, framesRead); + + remaining -= framesRead; + dst += framesRead; + } while (0 != remaining); + + sizeInFrames -= remaining; // In case we stopped at an error + } + + if (OK) + { + *outBuffer = buffer; + *outSize = sizeInFrames; + } + else + { + if (buffer) free(buffer); + } + return OK; +} + + +- (BOOL)readStereoCreatingLeftBuffer:(float **)outLeftBuffer rightBuffer:(float **)outRightBuffer withFrameCount:(size_t *)outSize; +{ + float *bufferL = NULL, *bufferR = NULL, *dstL, *dstR, **src; + size_t sizeInFrames = 0, remaining; + unsigned chanCount; + long framesRead; + ogg_int64_t totalSizeInFrames; + BOOL OK = YES; + + if (NULL != outLeftBuffer) *outLeftBuffer = NULL; + if (NULL != outRightBuffer) *outRightBuffer = NULL; + if (NULL != outSize) *outSize = 0; + if (NULL == outLeftBuffer || NULL == outRightBuffer || NULL == outSize) OK = NO; + + if (OK) + { + totalSizeInFrames = ov_pcm_total(&_vf, -1); + assert (kMaxDecodeSize < SIZE_T_MAX); // Should have been checked by caller + sizeInFrames = totalSizeInFrames; + } + + if (OK) + { + bufferL = malloc(sizeof (float) * sizeInFrames); + if (!bufferL) OK = NO; + } + + if (OK) + { + bufferR = malloc(sizeof (float) * sizeInFrames); + if (!bufferR) OK = NO; + } + + if (OK && sizeInFrames) + { + remaining = sizeInFrames; + dstL = bufferL; + dstR = bufferR; + + do + { + chanCount = ov_info(&_vf, -1)->channels; + framesRead = ov_read_float(&_vf, &src, remaining, NULL); + if (framesRead <= 0) + { + if (OV_HOLE == framesRead) continue; + //else: + break; + } + + bcopy(src[0], dstL, sizeof (float) * framesRead); + if (1 == chanCount) bcopy(src[0], dstR, sizeof (float) * framesRead); + else bcopy(src[1], dstR, sizeof (float) * framesRead); + + remaining -= framesRead; + dstL += framesRead; + dstR += framesRead; + } while (0 != remaining); + + sizeInFrames -= remaining; // In case we stopped at an error + } + + if (OK) + { + *outLeftBuffer = bufferL; + *outRightBuffer = bufferR; + *outSize = sizeInFrames; + } + else + { + if (bufferL) free(bufferL); + if (bufferR) free(bufferR); + } + return OK; +} + + +- (size_t)streamStereoToBufferL:(float *)ioBufferL bufferR:(float *)ioBufferR maxFrames:(size_t)inMax +{ + float **src; + unsigned chanCount; + long framesRead; + size_t size, remaining; + unsigned rightChan; + + // Note: for our purposes, a frame is a set of one sample for each channel. + if (NULL == ioBufferL || NULL == ioBufferR || 0 == inMax) return 0; + if (_atEnd) return inMax; + + remaining = inMax; + do + { + chanCount = ov_info(&_vf, -1)->channels; + framesRead = ov_read_float(&_vf, &src, remaining, NULL); + if (framesRead <= 0) + { + if (OV_HOLE == framesRead) continue; + //else: + _atEnd = YES; + break; + } + + size = sizeof (float) * framesRead; + + rightChan = (1 == chanCount) ? 0 : 1; + bcopy(src[0], ioBufferL, size); + bcopy(src[rightChan], ioBufferR, size); + + remaining -= framesRead; + ioBufferL += framesRead; + ioBufferR += framesRead; + } while (0 != remaining); + + return inMax - remaining; +} + + +- (size_t)sizeAsBuffer +{ + ogg_int64_t size; + + size = ov_pcm_total(&_vf, -1); + size *= sizeof(float) * ([self isStereo] ? 2 : 1); + if ((ogg_int64_t)SIZE_T_MAX < size) size = (ogg_int64_t)SIZE_T_MAX; + return size; +} + + +- (BOOL)isStereo +{ + return 1 < ov_info(&_vf, -1)->channels; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{\"%@\", comments=%@}", [self className], self, _name, [self comments]]; +} + + +- (Float64)sampleRate +{ + return ov_info(&_vf, -1)->rate; +} + + +- (BOOL)atEnd +{ + return _atEnd; +} + + +- (void)rewindToBeginning +{ + if (!ov_pcm_seek(&_vf, 0)) _atEnd = NO; +} + + +- (BOOL)scanToOffset:(uint64_t)inOffset +{ + if (!ov_pcm_seek(&_vf, inOffset)) + { + _atEnd = NO; + return YES; + } + else return NO; +} + + +- (NSString *)name +{ + NSDictionary *comments; + NSString *result = nil; + + comments = [self comments]; + if (nil != comments) + { + result = [comments objectForKey:@"TITLE"]; + if (nil == result) result = [comments objectForKey:@"NAME"]; + } + + if (nil == result) result = [[_name retain] autorelease]; + if (nil == result) result = [super name]; + + return result; +} + +@end + + +// TODO: optimise, vectorise +static void MixDown(float *inChan1, float *inChan2, float *outMix, size_t inCount) +{ + while (inCount--) + { + *outMix++ = (*inChan1++ + *inChan2++) * 0.5f; + } +} diff --git a/src/Cocoa/OOCASoundInternal.h b/src/Cocoa/OOCASoundInternal.h new file mode 100644 index 00000000..694fd08c --- /dev/null +++ b/src/Cocoa/OOCASoundInternal.h @@ -0,0 +1,137 @@ +/* + +OOCASoundInternal.h + +Declarations used within OOCASound. This file should not be used by client +code. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSoundInternal.h" +#import "OOCASoundChannel.h" +#import "OOCABufferedSound.h" +#import "OOCAStreamingSound.h" +#import +#import +#import "OOErrorDescription.h" +#import "OOLogging.h" + + +@interface OOSound (Internal) + +- (OSStatus)renderWithFlags:(AudioUnitRenderActionFlags *)ioFlags frames:(UInt32)inNumFrames context:(OOCASoundRenderContext *)ioContext data:(AudioBufferList *)ioData; + +// Called by -play and -stop only if in appropriate state +- (BOOL)prepareToPlayWithContext:(OOCASoundRenderContext *)outContext looped:(BOOL)inLoop; +- (void)finishStoppingWithContext:(OOCASoundRenderContext)inContext; + +- (BOOL)getAudioStreamBasicDescription:(AudioStreamBasicDescription *)outFormat; + +- (void)incrementPlayingCount; +- (void)decrementPlayingCount; + +- (BOOL) isPlaying; + +@end + + +@interface OOSoundMixer (Internal) + +- (BOOL)connectChannel:(OOSoundChannel *)inChannel; +- (OSStatus)disconnectChannel:(OOSoundChannel *)inChannel; + +@end + + +extern BOOL gOOSoundSetUp, gOOSoundBroken; + +extern NSString * const kOOLogDeprecatedMethodOOCASound; +extern NSString * const kOOLogSoundInitError; + +#define kOOLogUnconvertedNSLog @"unclassified.OOCASound" + + +#if MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_4 +#import + +static inline void OOSoundAtomicAdd(int32_t delta, int32_t *value) +{ + OSAtomicAdd32(delta, value); +} +#else +static inline void OOSoundAtomicAdd(int32_t delta, int32_t *value) +{ + OTAtomicAdd32(delta, (SInt32 *)value); +} +#endif + + +// Wrappers for AU APIs changed in Leopard SDK +#if OOLITE_LEOPARD + +static inline OSStatus OOAUGraphAddNode(AUGraph inGraph, const ComponentDescription *inDescription, AUNode *outNode) +{ + return AUGraphAddNode(inGraph, inDescription, outNode); +} + + +static inline OSStatus OOAUGraphNodeInfo(AUGraph inGraph, AUNode inNode, ComponentDescription *outDescription, AudioUnit *outAudioUnit) +{ + return AUGraphNodeInfo(inGraph, inNode, outDescription, outAudioUnit); +} + +#else + +static inline OSStatus OOAUGraphAddNode(AUGraph inGraph, const ComponentDescription *inDescription, AUNode *outNode) +{ + return AUGraphNewNode(inGraph, inDescription, 0, NULL, outNode); +} + + +static inline OSStatus OOAUGraphNodeInfo(AUGraph inGraph, AUNode inNode, ComponentDescription *outDescription, AudioUnit *outAudioUnit) +{ + return AUGraphGetNodeInfo(inGraph, inNode, outDescription, NULL, NULL, outAudioUnit); +} + +#endif diff --git a/src/Cocoa/OOCASoundMixer.h b/src/Cocoa/OOCASoundMixer.h new file mode 100644 index 00000000..268b607f --- /dev/null +++ b/src/Cocoa/OOCASoundMixer.h @@ -0,0 +1,102 @@ +/* + +OOCASoundMixer.h + +Class responsible for managing and mixing sound channels. This class is an +implementation detail. Do not use it directly; use an OOSoundSource to play an +OOSound. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import +#import + +@class OOMusic, OOSoundChannel, OOSoundSource; + + +enum +{ + kMixerGeneralChannels = 32 +}; + + +#define SUPPORT_SOUND_INSPECTOR 0 + + +@interface OOSoundMixer: NSObject +{ + OOSoundChannel *_channels[kMixerGeneralChannels]; + OOSoundChannel *_freeList; + NSLock *_listLock; + + AUGraph _graph; + AUNode _mixerNode; + AUNode _outputNode; + AudioUnit _mixerUnit; + + uint32_t _activeChannels; + uint32_t _maxChannels; + uint32_t _playMask; + +#if SUPPORT_SOUND_INSPECTOR + IBOutlet NSMatrix *checkBoxes; + IBOutlet NSTextField *currentField; + IBOutlet NSTextField *maxField; + IBOutlet NSTextField *loadField; + IBOutlet NSProgressIndicator *loadBar; +#endif +} + +// Singleton accessor ++ (id) sharedMixer; + +- (void) update; + +- (void) setMasterVolume:(float)inVolume; + +- (OOSoundChannel *) popChannel; +- (void) pushChannel:(OOSoundChannel *)inChannel; + +@end diff --git a/src/Cocoa/OOCASoundMixer.m b/src/Cocoa/OOCASoundMixer.m new file mode 100644 index 00000000..6b3eade4 --- /dev/null +++ b/src/Cocoa/OOCASoundMixer.m @@ -0,0 +1,381 @@ +/* + +OOCASoundMixer.m + +Class responsible for managing and mixing sound channels. This class is an +implementation detail. Do not use it directly; use an OOSoundSource to play an +OOSound. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundInternal.h" +#import "OOCASoundChannel.h" +#import "NSThreadOOExtensions.h" + + +static NSString * const kOOLogSoundInspetorNotLoaded = @"sound.mixer.inspector.loadFailed"; +static NSString * const kOOLogSoundMixerOutOfChannels = @"sound.mixer.outOfChannels"; +static NSString * const kOOLogSoundMixerReplacingBrokenChannel = @"sound.mixer.replacingBrokenChannel"; +static NSString * const kOOLogSoundMixerFailedToConnectChannel = @"sound.mixer.failedToConnectChannel"; + + +@interface OOSoundMixer (Private) + +- (void)pushChannel:(OOSoundChannel *)inChannel; +- (OOSoundChannel *)popChannel; + +@end + + +static OOSoundMixer *sSingleton = nil; + +@implementation OOSoundMixer + ++ (id) sharedMixer +{ + if (nil == sSingleton) + { + [[self alloc] init]; + } + return sSingleton; +} + + +- (id)init +{ + OSStatus err = noErr; + BOOL OK; + uint32_t idx = 0, count = kMixerGeneralChannels; + OOSoundChannel *temp; + ComponentDescription desc; + + if (!gOOSoundSetUp) [OOSound setUp]; + + self = [super init]; + if (nil != self) + { + _listLock = [[NSLock alloc] init]; + [_listLock ooSetName:@"OOSoundMixer list lock"]; + OK = nil != _listLock; + + if (OK) + { + // Create audio graph + err = NewAUGraph(&_graph); + + // Add output node + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_DefaultOutput; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + if (!err) err = OOAUGraphAddNode(_graph, &desc, &_outputNode); + + // Add mixer node + desc.componentType = kAudioUnitType_Mixer; + desc.componentSubType = kAudioUnitSubType_StereoMixer; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + if (!err) err = OOAUGraphAddNode(_graph, &desc, &_mixerNode); + + // Connect mixer to output + if (!err) err = AUGraphConnectNodeInput(_graph, _mixerNode, 0, _outputNode, 0); + + // Open the graph (turn it into concrete AUs) and extract mixer AU + if (!err) err = AUGraphOpen(_graph); + if (!err) err = OOAUGraphNodeInfo(_graph, _mixerNode, NULL, &_mixerUnit); + + if (err) OK = NO; + } + + if (OK) + { + // Allocate channels + do + { + temp = [[OOSoundChannel alloc] initWithID:count auGraph:_graph]; + if (nil != temp) + { + _channels[idx++] = temp; + [temp setNext:_freeList]; + _freeList = temp; + } + } while (--count); + + if (noErr != AUGraphInitialize(_graph)) OK = NO; + } + + if (!OK) + { + [super release]; + self = nil; + } + + #if SUPPORT_SOUND_INSPECTOR + if (![NSBundle loadNibNamed:@"SoundInspector" owner:self]) + { + OOLog(kOOLogSoundInspetorNotLoaded, @"Failed to load sound inspector panel."); + } + #endif + } + sSingleton = self; + + return sSingleton; +} + + +- (void)dealloc +{ + uint32_t idx; + + if (NULL != _graph) + { + AUGraphStop(_graph); + AUGraphUninitialize(_graph); + AUGraphClose(_graph); + DisposeAUGraph(_graph); + } + for (idx = 0; idx != kMixerGeneralChannels; ++idx) + { + [_channels[idx] release]; + } + + [super dealloc]; +} + + +- (void)channel:(OOSoundChannel *)inChannel didFinishPlayingSound:(OOSound *)inSound +{ + uint32_t ID; + + [inSound decrementPlayingCount]; + + if (![inChannel isOK]) + { + OOLog(kOOLogSoundMixerReplacingBrokenChannel, @"Sound mixer: replacing broken channel %@.", inChannel); + ID = [inChannel ID]; + [inChannel release]; + inChannel = [[OOSoundChannel alloc] initWithID:ID auGraph:_graph]; + } + + [self pushChannel:inChannel]; +} + + +- (void)update +{ +#if SUPPORT_SOUND_INSPECTOR + uint32_t i; + Float32 load; + + for (i = 0; i != kMixerGeneralChannels && i != 32; ++i) + { + [[checkBoxes cellWithTag:i] setIntValue:_playMask & (1 << i)]; + } + + if (_maxChannels < _activeChannels) + { + _maxChannels = _activeChannels; + [maxField setIntValue:_maxChannels]; + } + [currentField setIntValue:_activeChannels]; + + if (!AUGraphGetCPULoad(_graph, &load)) + { + [loadBar setDoubleValue:load]; + [loadField setObjectValue:[NSString stringWithFormat:@"%.2g%%", load * 100.0]]; + } +#endif +} + + +#if SUPPORT_SOUND_INSPECTOR +- (void)awakeFromNib +{ + uint32_t i; + + if (nil != checkBoxes) + { + for (i = 0; i != kMixerGeneralChannels; ++i) + { + [[checkBoxes cellWithTag:i] setIntValue:0]; + } + } +} +#endif + + +- (void)setMasterVolume:(float)inVolume +{ + AudioUnitSetParameter(_mixerUnit, kStereoMixerParam_Volume, kAudioUnitScope_Output, 0, inVolume, 0); +} + + +- (OOSoundChannel *)popChannel +{ + OOSoundChannel *result; + uint32_t ID; + + [_listLock lock]; + result = _freeList; + _freeList = [result next]; + + if (nil != result) + { + if (0 == _activeChannels++) + { + AUGraphStart(_graph); + } + + ID = [result ID] - 1; + if (ID < 32) _playMask |= (1 << ID); + } + [_listLock unlock]; + + return result; +} + + +- (void)pushChannel:(OOSoundChannel *)inChannel +{ + uint32_t ID; + + assert(nil != inChannel); + + [_listLock lock]; + + [inChannel setNext:_freeList]; + _freeList = inChannel; + + if (0 == --_activeChannels) + { + AUGraphStop(_graph); + } + ID = [inChannel ID] - 1; + if (ID < 32) _playMask &= ~(1 << ID); + [_listLock unlock]; +} + + +- (BOOL)connectChannel:(OOSoundChannel *)inChannel +{ + AUNode node; + OSStatus err; + + assert(nil != inChannel); + + node = [inChannel auSubGraphNode]; + err = AUGraphConnectNodeInput(_graph, node, 0, _mixerNode, [inChannel ID]); + if (!err) err = AUGraphUpdate(_graph, NULL); + + if (err) OOLog(kOOLogSoundMixerFailedToConnectChannel, @"Sound mixer: failed to connect channel %@, error = %@.", inChannel, AudioErrorNSString(err)); + + return !err; +} + + +- (OSStatus)disconnectChannel:(OOSoundChannel *)inChannel +{ + OSStatus err; + + assert(nil != inChannel); + + err = AUGraphDisconnectNodeInput(_graph, _mixerNode, [inChannel ID]); + if (noErr == err) AUGraphUpdate(_graph, NULL); + + return err; +} + +@end + + +@implementation OOSoundMixer (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedMixer above. + + NOTE: assumes single-threaded access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end diff --git a/src/Cocoa/OOCASoundReferencePoint.h b/src/Cocoa/OOCASoundReferencePoint.h new file mode 100644 index 00000000..d81181ef --- /dev/null +++ b/src/Cocoa/OOCASoundReferencePoint.h @@ -0,0 +1,63 @@ +/* + +OOCASoundReferencePoint.h + +A reference point for positional audio. The position of an OOSoundSource is +specified relative to an OOSoundReferencePoint. Since positional audio is +currently unimplemented, this class does nothing. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOMaths.h" + + +@interface OOSoundReferencePoint: NSObject + +// Positional audio attributes are ignored in this implementation +- (void)setPosition:(Vector)inPosition; +- (void)setVelocity:(Vector)inVelocity; +- (void)setOrientation:(Vector)inOrientation; + +@end diff --git a/src/Cocoa/OOCASoundReferencePoint.m b/src/Cocoa/OOCASoundReferencePoint.m new file mode 100644 index 00000000..c1152134 --- /dev/null +++ b/src/Cocoa/OOCASoundReferencePoint.m @@ -0,0 +1,70 @@ +/* + +OOCASoundReferencePoint.m + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2005-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundReferencePoint.h" + + +@implementation OOSoundReferencePoint + +- (void)setPosition:(Vector)inPosition +{ + +} + + +- (void)setVelocity:(Vector)inVelocity +{ + +} + + +- (void)setOrientation:(Vector)inOrientation +{ + +} + +@end diff --git a/src/Cocoa/OOCAStreamingSound.h b/src/Cocoa/OOCAStreamingSound.h new file mode 100644 index 00000000..81101e96 --- /dev/null +++ b/src/Cocoa/OOCAStreamingSound.h @@ -0,0 +1,66 @@ +/* + +OOCAStreamingSound.h + +Subclass of OOSound which streams audio from disk. + +This class is an implementation detail. Do not use it directly; use OOSound. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2005-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOSound.h" + +@class OOCASoundDecoder; +@class VirtualRingBuffer; + + +@interface OOCAStreamingSound: OOSound +{ + OOCASoundDecoder *_decoder; +} + +- (id) initWithDecoder:(OOCASoundDecoder *)inDecoder; + +@end diff --git a/src/Cocoa/OOCAStreamingSound.m b/src/Cocoa/OOCAStreamingSound.m new file mode 100644 index 00000000..69ab5805 --- /dev/null +++ b/src/Cocoa/OOCAStreamingSound.m @@ -0,0 +1,405 @@ +/* + +OOCAStreamingSound.m + +This class is an implementation detail. Do not use it directly; use OOSound. + +OOCASound - Core Audio sound implementation for Oolite. +Copyright (C) 2005-2008 Jens Ayton + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2005-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCASoundInternal.h" +#import "OOCASoundDecoder.h" +#import "VirtualRingBuffer.h" +#import "NSThreadOOExtensions.h" + + +static NSString * const kOOLogSoundStreamingRefill = @"sound.streaming.refill"; +static NSString * const kOOLogSoundStreamingLoop = @"sound.streaming.loop"; +static NSString * const kOOLogSoundStreamingUnderflow = @"sound.streaming.underflow"; + + +static BOOL sFeederThreadActive = NO; +static MPQueueID sFeederQueue = kInvalidID; + + +// For IMP caching of VirtualRingBuffer methods +typedef UInt32 (*VRB_lengthAvailableToReadReturningPointerIMP)(id inSelf, SEL inSel, void ** outReturnedReadPointer); +typedef void (*VRB_didReadLengthIMP)(id inSelf, SEL inSel, UInt32 inLength); + +#define kSEL_VRB_lengthAvailableToReadReturningPointer @selector(lengthAvailableToReadReturningPointer:) +#define kSEL_VRB_didReadLength @selector(didReadLength:) + +static VRB_lengthAvailableToReadReturningPointerIMP VRB_lengthAvailableToReadReturningPointer = NULL; +static VRB_didReadLengthIMP VRB_didReadLength = NULL; + + +typedef struct +{ + uint64_t readOffset; + VirtualRingBuffer *bufferL, + *bufferR; + int32_t pendingCount; + BOOL atEnd, + loop, + empty, + stopped; +} *OOCAStreamingSoundRenderContext; + + +@interface OOCAStreamingSound (Internal) + ++ (void)feederThread:ignored; +- (BOOL)setUpAudioUnit; +- (void)fillBuffersWithContext:(OOCAStreamingSoundRenderContext)inContext; +- (void)releaseContext:(OOCAStreamingSoundRenderContext)inContext; + +@end + + +enum +{ + // Port messages + kMsgFillBuffers = 1UL +}; + + +enum +{ + kStreamBufferCount = 400000, // Number of frames in buffer + kStreamBufferRefillThreshold = kStreamBufferCount / 3, // When to request refill + kMinRebufferFrames = kStreamBufferCount / 10, // When to ignore fill request (may get multiple requests before filler thread is scheduled) + kStreamBufferSize = kStreamBufferCount * sizeof (float) +}; + + +@implementation OOCAStreamingSound + +- (id)initWithDecoder:(OOCASoundDecoder *)inDecoder +{ + BOOL OK = YES; + + assert(gOOSoundSetUp); + if (gOOSoundBroken || nil == inDecoder) OK = NO; + + if (OK && VRB_lengthAvailableToReadReturningPointer == NULL) + { + VRB_lengthAvailableToReadReturningPointer = (VRB_lengthAvailableToReadReturningPointerIMP)[VirtualRingBuffer instanceMethodForSelector:@selector(lengthAvailableToReadReturningPointer:)]; + VRB_didReadLength = (VRB_didReadLengthIMP)[VirtualRingBuffer instanceMethodForSelector:@selector(didReadLength:)]; + + if (VRB_lengthAvailableToReadReturningPointer == NULL || VRB_didReadLength == NULL) OK = NO; + } + + if (OK) + { + self = [super init]; + if (nil == self) OK = NO; + } + + if (OK) + { + _decoder = [inDecoder retain]; + } + + if (!OK) + { + [self release]; + self = nil; + } + return self; +} + + +- (void)dealloc +{ + assert(![self isPlaying]); + [_decoder release]; + + [super dealloc]; +} + + +- (void)play +{ + [[OOSoundMixer sharedMixer] playSound:self]; +} + + +- (BOOL)prepareToPlayWithContext:(OOCASoundRenderContext *)outContext looped:(BOOL)inLoop +{ + BOOL OK = YES; + OOCAStreamingSoundRenderContext context = NULL; + + assert(outContext != NULL); + + if (OK) + { + context = calloc(1, sizeof *context); + if (context != NULL) + { + *outContext = (OOCASoundRenderContext)context; + context->loop = inLoop; + context->empty = YES; + } + else + { + OK = NO; + } + } + + if (OK) + { + context->bufferL = [[VirtualRingBuffer alloc] initWithLength:kStreamBufferSize]; + context->bufferR = [[VirtualRingBuffer alloc] initWithLength:kStreamBufferSize]; + if (!context->bufferL || !context->bufferR) + { + OK = NO; + } + } + + if (OK && sFeederQueue == kInvalidID) + { + OK = (noErr == MPCreateQueue(&sFeederQueue)) && (sFeederQueue != kInvalidID); + } + + if (OK) + { + if (!sFeederThreadActive) [NSThread detachNewThreadSelector:@selector(feederThread:) toTarget:[OOCAStreamingSound class] withObject:nil]; + [self retain]; // Will be released by fillBuffers... when stopped is true and pendingCount is 0. + ++context->pendingCount; + [self fillBuffersWithContext:context]; + } + + return OK; +} + + +- (void)finishStoppingWithContext:(OOCASoundRenderContext)inContext +{ + OOCAStreamingSoundRenderContext context; + + context = (OOCAStreamingSoundRenderContext) inContext; + + [gOOCASoundSyncLock lock]; + context->stopped = YES; + if (0 == context->pendingCount) + { + OOLog(@"sound.streaming.releaseContext.immediate", @"Streaming sound %@ stopped with 0 pendingCount, releasing context.", self); + [self releaseContext:context]; + } + else + { + OOLog(@"sound.streaming.releaseContext.deferring", @"Streaming sound %@ stopped with %i pendingCount, deferring release.", self, context->pendingCount); + } + [gOOCASoundSyncLock unlock]; +} + + ++ (void)feederThread:ignored +{ + uintptr_t msgID; + void *param1, *param2; + OOCAStreamingSound *sound; + OOCAStreamingSoundRenderContext context = 0; + NSAutoreleasePool *pool = nil; + + assert(sFeederQueue != kInvalidID); + + [NSThread ooSetCurrentThreadName:@"OOCAStreamingSound feeder thread"]; + sFeederThreadActive = YES; + + for (;;) + { + if (noErr != MPWaitOnQueue(sFeederQueue, (void **)&msgID, ¶m1, ¶m2, kDurationForever)) continue; + + pool = [[NSAutoreleasePool alloc] init]; + + switch (msgID) + { + case kMsgFillBuffers: + sound = param1; + context = param2; + [sound fillBuffersWithContext:context]; + } + + [pool release]; + } +} + + +- (void)fillBuffersWithContext:(OOCAStreamingSoundRenderContext)inContext +{ + size_t spaceL, spaceR; + void *ptrL, *ptrR; + size_t frames; + + [gOOCASoundSyncLock lock]; + spaceL = [inContext->bufferL lengthAvailableToWriteReturningPointer:&ptrL]; + spaceR = [inContext->bufferR lengthAvailableToWriteReturningPointer:&ptrR]; + + // These ought to be the same + frames = MIN(spaceL, spaceR) / sizeof (float); + + if (kMinRebufferFrames < frames) + { + if (![_decoder scanToOffset:inContext->readOffset]) + { + inContext->atEnd = YES; + } + else + { + if (kStreamBufferCount < frames) + { + if (!inContext->empty) OOLog(kOOLogSoundStreamingUnderflow, @"Buffer underflow for sound %@.", self); + inContext->empty = NO; + } + + OOLog(kOOLogSoundStreamingRefill, @"Buffering %u frames for sound %@.", frames, self); + frames = [_decoder streamStereoToBufferL:(float *)ptrL bufferR:(float *)ptrR maxFrames:frames]; + inContext->readOffset += frames; + [inContext->bufferL didWriteLength:frames * sizeof (float)]; + [inContext->bufferR didWriteLength:frames * sizeof (float)]; + + if ([_decoder atEnd]) + { + if (inContext->loop) + { + OOLog(kOOLogSoundStreamingLoop, @"Resetting streaming sound %@ for looping.", self); + inContext->readOffset = 0; + } + else inContext->atEnd = YES; + } + } + } + + OOSoundAtomicAdd(-1, &inContext->pendingCount); + if (inContext->pendingCount == 0 && inContext->stopped) + { + OOLog(@"sound.streaming.releaseContext.deferred", @"Stopped streaming sound %@ reached 0 pendingCount, releasing context.", self); + [self releaseContext:inContext]; + } + + [gOOCASoundSyncLock unlock]; +} + + +- (void)releaseContext:(OOCAStreamingSoundRenderContext)inContext +{ + [inContext->bufferL release]; + [inContext->bufferR release]; + free(inContext); + [self autorelease]; +} + + +- (BOOL)getAudioStreamBasicDescription:(AudioStreamBasicDescription *)outFormat +{ + assert(NULL != outFormat); + + outFormat->mSampleRate = [_decoder sampleRate]; + outFormat->mFormatID = kAudioFormatLinearPCM; + outFormat->mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kLinearPCMFormatFlagIsNonInterleaved; + outFormat->mBytesPerPacket = sizeof (float); + outFormat->mFramesPerPacket = 1; + outFormat->mBytesPerFrame = sizeof (float); + outFormat->mChannelsPerFrame = 2; + outFormat->mBitsPerChannel = sizeof (float) * 8; + outFormat->mReserved = 0; + + return YES; +} + + +- (OSStatus)renderWithFlags:(AudioUnitRenderActionFlags *)ioFlags frames:(UInt32)inNumFrames context:(OOCASoundRenderContext *)ioContext data:(AudioBufferList *)ioData +{ + size_t available, availL, availR, remaining, underflow; + size_t numBytes; + void *ptrL, *ptrR; + OOCAStreamingSoundRenderContext context; + + context = (OOCAStreamingSoundRenderContext)*ioContext; + + assert(2 == ioData->mNumberBuffers); + + availL = VRB_lengthAvailableToReadReturningPointer(context->bufferL, kSEL_VRB_lengthAvailableToReadReturningPointer, &ptrL); + availR = VRB_lengthAvailableToReadReturningPointer(context->bufferR, kSEL_VRB_lengthAvailableToReadReturningPointer, &ptrR); + + numBytes = inNumFrames * sizeof (float); + + available = MIN(availL, availR); + remaining = available; + if (numBytes < available) available = numBytes; + + bcopy(ptrL, ioData->mBuffers[0].mData, available); + bcopy(ptrR, ioData->mBuffers[1].mData, available); + + VRB_didReadLength(context->bufferL, kSEL_VRB_didReadLength, available); + VRB_didReadLength(context->bufferR, kSEL_VRB_didReadLength, available); + + if (available < numBytes) + { + // Buffer underflow! Fill with silence. + underflow = numBytes - available; + + if (0 == available) *ioFlags |= kAudioUnitRenderAction_OutputIsSilence; + + bzero(ioData->mBuffers[0].mData + available, underflow); + bzero(ioData->mBuffers[0].mData + available, underflow); + } + + remaining -= available; + if (!context->atEnd && remaining < kStreamBufferRefillThreshold * sizeof (float) && sFeederQueue != kInvalidID) + { + OOSoundAtomicAdd(1, &context->pendingCount); + MPNotifyQueue(sFeederQueue, (void *)kMsgFillBuffers, self, context); + } + + return (context->atEnd && remaining == 0) ? endOfDataReached : noErr; +} + + +- (NSString *)name +{ + return [_decoder name]; +} + +@end diff --git a/src/Cocoa/OOErrorDescription.h b/src/Cocoa/OOErrorDescription.h new file mode 100644 index 00000000..0e9eea45 --- /dev/null +++ b/src/Cocoa/OOErrorDescription.h @@ -0,0 +1,23 @@ +/* + +OOErrorDescription.h + +A set of functions for mapping various types of Mac OS error codes to names, +primarily for debugging purposes. + +For OOCASound - Core Audio sound implementation for Oolite. +By Jens Ayton, 2005 + +This file is hereby placed in the public domain. + +*/ + +#import + + +// Provide descriptions of various Mac-specific error codes +NSString *OSStatusErrorNSString(OSStatus inCode); +NSString *AudioErrorNSString(ComponentResult inCode); +NSString *KernelResultNSString(kern_return_t inCode); +NSString *FourCharCodeToNSString(FourCharCode inCode); +NSString *AudioErrorShortNSString(OSStatus inCode); diff --git a/src/Cocoa/OOErrorDescription.m b/src/Cocoa/OOErrorDescription.m new file mode 100644 index 00000000..d02ac086 --- /dev/null +++ b/src/Cocoa/OOErrorDescription.m @@ -0,0 +1,257 @@ +/* + +OOErrorDescription.m + +A set of functions for mapping various types of Mac OS error codes to names, +primarily for debugging purposes. + +For OOCASound - Core Audio sound implementation for Oolite. +By Jens Ayton, 2005 + +This file is hereby placed in the public domain. + +*/ + +#import "OOErrorDescription.h" +#import +#import +#import + + +#define CASE(foo) case foo: return @#foo + + +#ifndef NDEBUG +static NSString *GetGenericOSStatusString(OSStatus inCode); +#endif + + +NSString *OSStatusErrorNSString(OSStatus inCode) +{ + NSString *result = nil; + + #ifndef NDEBUG + result = GetGenericOSStatusString(inCode); + #else + if (noErr == inCode) result = @"no error"; + #endif + + if (nil == result) result = [NSString stringWithFormat:@"%i", (int)inCode]; + + return result; +} + + +NSString *AudioErrorNSString(ComponentResult inCode) +{ + #ifndef NDEBUG + switch (inCode) + { + CASE(kAudioConverterErr_FormatNotSupported); + CASE(kAudioConverterErr_OperationNotSupported); + CASE(kAudioConverterErr_PropertyNotSupported); + CASE(kAudioConverterErr_InvalidInputSize); + CASE(kAudioConverterErr_InvalidOutputSize); + CASE(kAudioConverterErr_UnspecifiedError); + CASE(kAudioConverterErr_BadPropertySizeError); + CASE(kAudioConverterErr_RequiresPacketDescriptionsError); + CASE(kAudioConverterErr_InputSampleRateOutOfRange); + CASE(kAudioConverterErr_OutputSampleRateOutOfRange); + + CASE(kAudioCodecUnknownPropertyError); + CASE(kAudioCodecIllegalOperationError); + CASE(kAudioCodecUnsupportedFormatError); + CASE(kAudioCodecStateError); + CASE(kAudioCodecNotEnoughBufferSpaceError); + + CASE(kAudioUnitErr_InvalidProperty); + CASE(kAudioUnitErr_InvalidParameter); + CASE(kAudioUnitErr_InvalidElement); + CASE(kAudioUnitErr_NoConnection); + CASE(kAudioUnitErr_FailedInitialization); + CASE(kAudioUnitErr_TooManyFramesToProcess); + CASE(kAudioUnitErr_IllegalInstrument); + CASE(kAudioUnitErr_InstrumentTypeNotFound); + CASE(kAudioUnitErr_InvalidFile); + CASE(kAudioUnitErr_UnknownFileType); + CASE(kAudioUnitErr_FileNotSpecified); + CASE(kAudioUnitErr_FormatNotSupported); + CASE(kAudioUnitErr_Uninitialized); + CASE(kAudioUnitErr_InvalidScope); + CASE(kAudioUnitErr_PropertyNotWritable); + CASE(kAudioUnitErr_CannotDoInCurrentContext); + CASE(kAudioUnitErr_InvalidPropertyValue); + CASE(kAudioUnitErr_PropertyNotInUse); + CASE(kAudioUnitErr_Initialized); + CASE(kAudioUnitErr_InvalidOfflineRender); + CASE(kAudioUnitErr_Unauthorized); + + CASE(kAUGraphErr_NodeNotFound); + CASE(kAUGraphErr_InvalidConnection); + CASE(kAUGraphErr_OutputNodeErr); + CASE(kAUGraphErr_InvalidAudioUnit); + + default: + { + NSString *result = GetGenericOSStatusString(inCode); + if (nil != result) return result; + } + } + #else + if (noErr == inCode) return @"no error"; + #endif + + return AudioErrorShortNSString(inCode); +} + + +NSString *AudioErrorShortNSString(OSStatus inCode) +{ + #define PRINTABLE(x) (!((x) < ' ') && !((x) == 0x7F)) + + if (!(inCode & 0x80000000) && + PRINTABLE(inCode >> 24) && + PRINTABLE((inCode >> 16) & 0xFF) && + PRINTABLE((inCode >> 8) & 0xFF) && + PRINTABLE(inCode & 0xFF)) + { + // Assume a four-char code + return [NSString stringWithFormat:@"\'%@\'", FourCharCodeToNSString(inCode)]; + } + else + { + return [NSString stringWithFormat:@"%i", (int)inCode]; + } +} + + +NSString *KernelResultNSString(kern_return_t inCode) +{ + #ifndef NDEBUG + switch (inCode) + { + CASE(KERN_INVALID_ADDRESS); + CASE(KERN_PROTECTION_FAILURE); + CASE(KERN_NO_SPACE); + CASE(KERN_INVALID_ARGUMENT); + CASE(KERN_FAILURE); + CASE(KERN_RESOURCE_SHORTAGE); + CASE(KERN_NOT_RECEIVER); + CASE(KERN_NO_ACCESS); + CASE(KERN_MEMORY_FAILURE); + CASE(KERN_MEMORY_ERROR); + CASE(KERN_ALREADY_IN_SET); + CASE(KERN_NOT_IN_SET); + CASE(KERN_NAME_EXISTS); + CASE(KERN_ABORTED); + CASE(KERN_INVALID_NAME); + CASE(KERN_INVALID_TASK); + CASE(KERN_INVALID_RIGHT); + CASE(KERN_INVALID_VALUE); + CASE(KERN_UREFS_OVERFLOW); + CASE(KERN_INVALID_CAPABILITY); + CASE(KERN_RIGHT_EXISTS); + CASE(KERN_INVALID_HOST); + CASE(KERN_MEMORY_PRESENT); + CASE(KERN_MEMORY_DATA_MOVED); + CASE(KERN_MEMORY_RESTART_COPY); + CASE(KERN_INVALID_PROCESSOR_SET); + CASE(KERN_POLICY_LIMIT); + CASE(KERN_INVALID_POLICY); + CASE(KERN_INVALID_OBJECT); + CASE(KERN_ALREADY_WAITING); + CASE(KERN_DEFAULT_SET); + CASE(KERN_EXCEPTION_PROTECTED); + CASE(KERN_INVALID_LEDGER); + CASE(KERN_INVALID_MEMORY_CONTROL); + CASE(KERN_INVALID_SECURITY); + CASE(KERN_NOT_DEPRESSED); + CASE(KERN_TERMINATED); + CASE(KERN_LOCK_SET_DESTROYED); + CASE(KERN_LOCK_UNSTABLE); + CASE(KERN_LOCK_OWNED); + CASE(KERN_LOCK_OWNED_SELF); + CASE(KERN_SEMAPHORE_DESTROYED); + CASE(KERN_RPC_SERVER_TERMINATED); + CASE(KERN_RPC_TERMINATE_ORPHAN); + CASE(KERN_RPC_CONTINUE_ORPHAN); + CASE(KERN_NOT_SUPPORTED); + CASE(KERN_NODE_DOWN); + CASE(KERN_NOT_WAITING); + CASE(KERN_OPERATION_TIMED_OUT); + CASE(MACH_MSG_IPC_SPACE); + CASE(MACH_MSG_VM_SPACE); + CASE(MACH_MSG_IPC_KERNEL); + CASE(MACH_MSG_VM_KERNEL); + CASE(MACH_SEND_IN_PROGRESS); + CASE(MACH_SEND_INVALID_DATA); + CASE(MACH_SEND_INVALID_DEST); + CASE(MACH_SEND_TIMED_OUT); + CASE(MACH_SEND_INTERRUPTED); + CASE(MACH_SEND_MSG_TOO_SMALL); + CASE(MACH_SEND_INVALID_REPLY); + CASE(MACH_SEND_INVALID_RIGHT); + CASE(MACH_SEND_INVALID_NOTIFY); + CASE(MACH_SEND_INVALID_MEMORY); + CASE(MACH_SEND_NO_BUFFER); + CASE(MACH_SEND_TOO_LARGE); + CASE(MACH_SEND_INVALID_TYPE); + CASE(MACH_SEND_INVALID_HEADER); + CASE(MACH_SEND_INVALID_TRAILER); + CASE(MACH_SEND_INVALID_RT_OOL_SIZE); + CASE(MACH_RCV_IN_PROGRESS); + CASE(MACH_RCV_INVALID_NAME); + CASE(MACH_RCV_TIMED_OUT); + CASE(MACH_RCV_TOO_LARGE); + CASE(MACH_RCV_INTERRUPTED); + CASE(MACH_RCV_PORT_CHANGED); + CASE(MACH_RCV_INVALID_NOTIFY); + CASE(MACH_RCV_INVALID_DATA); + CASE(MACH_RCV_PORT_DIED); + CASE(MACH_RCV_IN_SET); + CASE(MACH_RCV_HEADER_ERROR); + CASE(MACH_RCV_BODY_ERROR); + CASE(MACH_RCV_INVALID_TYPE); + CASE(MACH_RCV_SCATTER_SMALL); + CASE(MACH_RCV_INVALID_TRAILER); + CASE(MACH_RCV_IN_PROGRESS_TIMED); + + case 0: return @"no error"; + } + #else + if (0 == inCode) return @"no error"; + #endif + + return [NSString stringWithFormat:@"0x%.8X", (unsigned)inCode]; +} + + +NSString *FourCharCodeToNSString(FourCharCode inCode) +{ + NSString *result; + + result = (NSString *)UTCreateStringForOSType(inCode); + return [result autorelease]; +} + + +#ifndef NDEBUG +static NSString *GetGenericOSStatusString(OSStatus inCode) +{ + switch (inCode) + { + case noErr: return @"no error"; + + CASE(paramErr); + CASE(memFullErr); + CASE(unimpErr); + CASE(userCanceledErr); + CASE(dskFulErr); + CASE(fnfErr); + CASE(errFSBadFSRef); + CASE(gestaltUnknownErr); + } + + return nil; +} +#endif diff --git a/src/Cocoa/OOProgressBar.h b/src/Cocoa/OOProgressBar.h new file mode 100644 index 00000000..164c334d --- /dev/null +++ b/src/Cocoa/OOProgressBar.h @@ -0,0 +1,43 @@ +/* + +OOProgressBar.h + +Simple custom progress indicator. + + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +@interface OOProgressBar: NSView +{ +@private + double _value; +} + +// Meaningful values range from 0 to 1. Negative values hide the view. Default is -1. +- (double) doubleValue; +- (void) setDoubleValue:(double)value; + +@end diff --git a/src/Cocoa/OOProgressBar.m b/src/Cocoa/OOProgressBar.m new file mode 100644 index 00000000..8333dc1c --- /dev/null +++ b/src/Cocoa/OOProgressBar.m @@ -0,0 +1,73 @@ +/* + +OOProgressBar.m + + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOProgressBar.h" + + +@implementation OOProgressBar + +- (id) initWithFrame:(NSRect)frame +{ + if ((self = [super initWithFrame:frame])) + { + _value = -1.0f; + } + return self; +} + + +- (void)drawRect:(NSRect)rect +{ + if (_value >= 0.0) + { + [NSGraphicsContext saveGraphicsState]; + [[NSColor yellowColor] set]; + + rect = NSInsetRect(rect, 0.5f, 0.5f); + [NSBezierPath strokeRect:rect]; + + rect = NSInsetRect(rect, 1.5f, 1.5f); + rect.size.width *= fmin(_value, 1.0); + [NSBezierPath fillRect:rect]; + + [NSGraphicsContext restoreGraphicsState]; + } +} + + +- (double) doubleValue +{ + return _value; +} + + +- (void) setDoubleValue:(double)value +{ + _value = value; +} + +@end diff --git a/src/Cocoa/OoliteApp.h b/src/Cocoa/OoliteApp.h new file mode 100644 index 00000000..d6e39bd5 --- /dev/null +++ b/src/Cocoa/OoliteApp.h @@ -0,0 +1,44 @@ +/* + +OoliteApp.h + +This is a subclass of NSApplication for Oolite. + +It gets around problems with the system intercepting certain events (NSKeyDown +and NSKeyUp) before MyOpenGLView gets to see them, it does this by sending +those events to MyOpenGLView regardless of any other processing NSApplication +will do with them. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + +@class GameController; + + +@interface OoliteApp: NSApplication +{ + IBOutlet NSWindow *_gameWindow; + IBOutlet GameController *_gameController; +} + + +@end diff --git a/src/Cocoa/OoliteApp.m b/src/Cocoa/OoliteApp.m new file mode 100644 index 00000000..ceb3ad34 --- /dev/null +++ b/src/Cocoa/OoliteApp.m @@ -0,0 +1,58 @@ +/* + +OoliteApp.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OoliteApp.h" +#import "GameController.h" +#import "MyOpenGLView.h" + + +@implementation OoliteApp + +- (void)sendEvent:(NSEvent *)theEvent +{ + NSEventType etype = [theEvent type]; + MyOpenGLView* gameView = [_gameController gameView]; + + if ([NSApp keyWindow] == _gameWindow) + { + // Ensure key events are handled at least once when game window is key + switch (etype) + { + case NSKeyDown: + [gameView keyDown:theEvent]; + break; + + case NSKeyUp: + [gameView keyUp:theEvent]; + break; + + default: + break; + } + } + [super sendEvent:theEvent]; // perform the default event behaviour +} + + +@end diff --git a/src/Cocoa/SoundInspector.nib/classes.nib b/src/Cocoa/SoundInspector.nib/classes.nib new file mode 100644 index 00000000..4f88aedd --- /dev/null +++ b/src/Cocoa/SoundInspector.nib/classes.nib @@ -0,0 +1,18 @@ +{ + IBClasses = ( + {CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }, + { + CLASS = OOCASoundMixer; + LANGUAGE = ObjC; + OUTLETS = { + checkBoxes = NSMatrix; + currentField = NSTextField; + loadBar = NSProgressIndicator; + loadField = NSTextField; + maxField = NSTextField; + }; + SUPERCLASS = NSObject; + } + ); + IBVersion = 1; +} \ No newline at end of file diff --git a/src/Cocoa/SoundInspector.nib/info.nib b/src/Cocoa/SoundInspector.nib/info.nib new file mode 100644 index 00000000..b480bf95 --- /dev/null +++ b/src/Cocoa/SoundInspector.nib/info.nib @@ -0,0 +1,20 @@ + + + + + IBDocumentLocation + 677 140 356 240 0 0 1440 878 + IBFramework Version + 443.0 + IBLockedObjects + + 46 + + IBOpenObjects + + 46 + + IBSystem Version + 8F46 + + diff --git a/src/Cocoa/SoundInspector.nib/keyedobjects.nib b/src/Cocoa/SoundInspector.nib/keyedobjects.nib new file mode 100644 index 0000000000000000000000000000000000000000..17a8f9c08a9b7c3ce2d68ed5c19583b2f0767629 GIT binary patch literal 9658 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 literal 0 HcmV?d00001 diff --git a/src/Cocoa/VirtualRingBuffer.h b/src/Cocoa/VirtualRingBuffer.h new file mode 100644 index 00000000..fd2da735 --- /dev/null +++ b/src/Cocoa/VirtualRingBuffer.h @@ -0,0 +1,87 @@ +// +// VirtualRingBuffer.h +// PlayBufferedSoundFile +// +/* + Copyright (c) 2002, Kurt Revis. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Snoize nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// +// VirtualRingBuffer implements a classic ring buffer (or circular buffer), with a couple of twists. +// +// * It allows reads and writes to happen in different threads, with no explicit locking, +// so readers and writers will never block. This is useful if either thread uses the +// time-constraint scheduling policy, since it is bad for such threads to block for +// indefinite amounts of time. +// +// * It uses a virtual memory trick to allow the client to read or write using just one +// operation, even if the data involved wraps around the end of the buffer. We allocate +// our buffer normally, and then place a VM region immediately after it in the address +// space which maps back to the "real" buffer. So reads and writes into both sections +// are transparently translated into the same physical memory. +// This makes the API much simpler to use, and saves us from doing some math to +// calculate the wraparound points. +// The tradeoff is that we use twice as much address space for the buffer than we would +// otherwise. Address space is not typically constrained for most applications, though, +// so this isn't a big problem. +// The idea for this trick came from (via sweetcode.org), +// although none of that code is used here. (We use the Mach VM API directly.) +// + +// Threading note: +// It is expected that this object will be shared between exactly two threads; one will +// always read and the other will always write. In that situation, the implementation is +// thread-safe, and this object will never block or yield. +// It will also work in one thread, of course (although I don't know why you'd bother). +// However, if you have multiple reader or writer threads, all bets are off! + +#import "OOCocoa.h" +#import + + +@interface VirtualRingBuffer : NSObject +{ + void *buffer; + void *bufferEnd; + OOUInteger bufferLength; + // buffer is the start of the ring buffer's address space. + // bufferEnd is the end of the "real" buffer (always buffer + bufferLength). + // Note that the "virtual" portion of the buffer extends from bufferEnd to bufferEnd+bufferLength. + + void *readPointer; + void *writePointer; +} + +- (id)initWithLength:(OOUInteger)length; +// Note: The specified length will be rounded up to an integral number of VM pages. + +// Empties the buffer. It is NOT safe to do this while anyone is reading from or writing to the buffer. +- (void)empty; +// Checks if the buffer is empty or not. This is safe in any thread. +- (BOOL)isEmpty; + +// Read operations: + +// The reading thread must call this method first. +- (OOUInteger)lengthAvailableToReadReturningPointer:(void **)returnedReadPointer; +// Iff a value > 0 is returned, the reading thread may go on to read that much data from the returned pointer. +// Afterwards, the reading thread must call didReadLength:. +- (void)didReadLength:(OOUInteger)length; + +// Write operations: + +// The writing thread must call this method first. +- (OOUInteger)lengthAvailableToWriteReturningPointer:(void **)returnedWritePointer; +// Iff a value > 0 is returned, the writing thread may then write that much data into the returned pointer. +// Afterwards, the writing thread must call didWriteLength:. +- (void)didWriteLength:(OOUInteger)length; + +@end diff --git a/src/Cocoa/VirtualRingBuffer.m b/src/Cocoa/VirtualRingBuffer.m new file mode 100644 index 00000000..1120827a --- /dev/null +++ b/src/Cocoa/VirtualRingBuffer.m @@ -0,0 +1,308 @@ +// +// VirtualRingBuffer.m +// PlayBufferedSoundFile +// +/* + Copyright (c) 2002, Kurt Revis. All rights reserved. + + Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + * Neither the name of Snoize nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +#import "VirtualRingBuffer.h" +#import "OOLogging.h" + +#include +#include + + +#define kOOLogUnconvertedNSLog @"unclassified.VirtualRingBuffer" + + +@implementation VirtualRingBuffer + +static void *allocateVirtualBuffer(OOUInteger bufferLength); +static void deallocateVirtualBuffer(void *buffer, OOUInteger bufferLength); + + +- (id)initWithLength:(OOUInteger)length +{ + if (![super init]) + return nil; + + // We need to allocate entire VM pages, so round the specified length up to the next page if necessary. + bufferLength = round_page(length); + + buffer = allocateVirtualBuffer(bufferLength); + if (buffer) { + bufferEnd = buffer + bufferLength; + } else { + [self release]; + return nil; + } + + readPointer = NULL; + writePointer = NULL; + + return self; +} + +- (void)dealloc +{ + if (buffer) + deallocateVirtualBuffer(buffer, bufferLength); + + [super dealloc]; +} + +- (void)empty +{ + // Assumption: + // No one is reading or writing from the buffer, in any thread, when this method is called. + + readPointer = NULL; + writePointer = NULL; +} + +- (BOOL)isEmpty +{ + return (readPointer != NULL && writePointer != NULL); +} + + +// +// Theory of operation: +// +// This class keeps a pointer to the next byte to be read (readPointer) and a pointer to the next byte to be written (writePointer). +// readPointer is only advanced in the reading thread (except for one case: when the buffer first has data written to it). +// writePointer is only advanced in the writing thread. +// +// Since loading and storing word length data is atomic, each pointer can safely be modified in one thread while the other thread +// uses it, IF each thread is careful to make a local copy of the "opposite" pointer when necessary. +// + +// +// Read operations +// + +- (OOUInteger)lengthAvailableToReadReturningPointer:(void **)returnedReadPointer +{ + // Assumptions: + // returnedReadPointer != NULL + + OOUInteger length; + // Read this pointer exactly once, so we're safe in case it is changed in another thread + void *localWritePointer = writePointer; + + // Depending on out-of-order execution and memory storage, either one of these may be NULL when the buffer is empty. So we must check both. + if (!readPointer || !localWritePointer) { + // The buffer is empty + length = 0; + } else if (localWritePointer > readPointer) { + // Write is ahead of read in the buffer + length = localWritePointer - readPointer; + } else { + // Write has wrapped around past read, OR write == read (the buffer is full) + length = bufferLength - (readPointer - localWritePointer); + } + + *returnedReadPointer = readPointer; + return length; +} + +- (void)didReadLength:(OOUInteger)length +{ + // Assumptions: + // [self lengthAvailableToReadReturningPointer:] currently returns a value >= length + // length > 0 + + void *newReadPointer; + + newReadPointer = readPointer + length; + if (newReadPointer >= bufferEnd) + newReadPointer -= bufferLength; + + if (newReadPointer == writePointer) { + // We just read the last data out of the buffer, so it is now empty. + newReadPointer = NULL; + } + + // Store the new read pointer. This is the only place this happens in the read thread. + readPointer = newReadPointer; +} + + +// +// Write operations +// + +- (OOUInteger)lengthAvailableToWriteReturningPointer:(void **)returnedWritePointer +{ + // Assumptions: + // returnedWritePointer != NULL + + OOUInteger length; + // Read this pointer exactly once, so we're safe in case it is changed in another thread + void *localReadPointer = readPointer; + + // Either one of these may be NULL when the buffer is empty. So we must check both. + if (!localReadPointer || !writePointer) { + // The buffer is empty. Set it up to be written into. + // This is one of the two places the write pointer can change; both are in the write thread. + writePointer = buffer; + length = bufferLength; + } else if (writePointer <= localReadPointer) { + // Write is before read in the buffer, OR write == read (meaning that the buffer is full). + length = localReadPointer - writePointer; + } else { + // Write is behind read in the buffer. The available space wraps around. + length = (bufferEnd - writePointer) + (localReadPointer - buffer); + } + + *returnedWritePointer = writePointer; + return length; +} + +- (void)didWriteLength:(OOUInteger)length +{ + // Assumptions: + // [self lengthAvailableToWriteReturningPointer:] currently returns a value >= length + // length > 0 + + void *oldWritePointer = writePointer; + void *newWritePointer; + + // Advance the write pointer, wrapping around if necessary. + newWritePointer = writePointer + length; + if (newWritePointer >= bufferEnd) + newWritePointer -= bufferLength; + + // This is one of the two places the write pointer can change; both are in the write thread. + writePointer = newWritePointer; + + // Also, if the read pointer is NULL, then we just wrote into a previously empty buffer, so set the read pointer. + // This is the only place the read pointer is changed in the write thread. + // The read thread should never change the read pointer when it is NULL, so this is safe. + if (!readPointer) + readPointer = oldWritePointer; +} + +@end + + +void *allocateVirtualBuffer(OOUInteger bufferLength) +{ + kern_return_t error; + vm_address_t originalAddress = 0; + vm_address_t realAddress = 0; + mach_port_t memoryEntry; + vm_size_t memoryEntryLength; + vm_address_t virtualAddress = 0; + + // We want to find where we can get 2 * bufferLength bytes of contiguous address space. + // So let's just allocate that space, remember its address, and deallocate it. + // (This doesn't actually have to touch all of that memory so it's not terribly expensive.) + error = vm_allocate(mach_task_self(), &originalAddress, 2 * bufferLength, TRUE); + if (error) { +#if DEBUG + mach_error("vm_allocate initial chunk", error); +#endif + return NULL; + } + + error = vm_deallocate(mach_task_self(), originalAddress, 2 * bufferLength); + if (error) { +#if DEBUG + mach_error("vm_deallocate initial chunk", error); +#endif + return NULL; + } + + // Then allocate a "real" block of memory at the same address, but with the normal bufferLength. + realAddress = originalAddress; + error = vm_allocate(mach_task_self(), &realAddress, bufferLength, FALSE); + if (error) { +#if DEBUG + mach_error("vm_allocate real chunk", error); +#endif + return NULL; + } + if (realAddress != originalAddress) { +#if DEBUG + NSLog(@"allocateVirtualBuffer: vm_allocate 2nd time didn't return same address (%p vs %p)", originalAddress, realAddress); +#endif + goto errorReturn; + } + + // Then make a memory entry for the area we just allocated. + memoryEntryLength = bufferLength; + error = mach_make_memory_entry(mach_task_self(), &memoryEntryLength, realAddress, VM_PROT_READ | VM_PROT_WRITE, &memoryEntry, 0); + if (error) { +#if DEBUG + mach_error("mach_make_memory_entry", error); +#endif + goto errorReturn; + } + if (!memoryEntry) { +#if DEBUG + NSLog(@"mach_make_memory_entry: returned memoryEntry of NULL"); +#endif + goto errorReturn; + } + if (memoryEntryLength != bufferLength) { +#if DEBUG + NSLog(@"mach_make_memory_entry: size changed (from %0x to %0x)", bufferLength, memoryEntryLength); +#endif + goto errorReturn; + } + + // And map the area immediately after the first block, with length bufferLength, to that memory entry. + virtualAddress = realAddress + bufferLength; + error = vm_map(mach_task_self(), &virtualAddress, bufferLength, 0, FALSE, memoryEntry, 0, FALSE, VM_PROT_READ | VM_PROT_WRITE, VM_PROT_READ | VM_PROT_WRITE, VM_INHERIT_DEFAULT); + if (error) { +#if DEBUG + mach_error("vm_map", error); +#endif + // TODO Retry from the beginning, instead of failing completely. There is a tiny (but > 0) probability that someone + // will allocate this space out from under us. + virtualAddress = 0; + goto errorReturn; + } + if (virtualAddress != realAddress + bufferLength) { +#if DEBUG + NSLog(@"vm_map: didn't return correct address (%p vs %p)", realAddress + bufferLength, virtualAddress); +#endif + goto errorReturn; + } + + // Success! + return (void *)realAddress; + +errorReturn: + if (realAddress) + vm_deallocate(mach_task_self(), realAddress, bufferLength); + if (virtualAddress) + vm_deallocate(mach_task_self(), virtualAddress, bufferLength); + + return NULL; +} + +void deallocateVirtualBuffer(void *buffer, OOUInteger bufferLength) +{ + kern_return_t error; + + // We can conveniently deallocate both the vm_allocated memory and + // the vm_mapped region at the same time. + error = vm_deallocate(mach_task_self(), (vm_address_t)buffer, bufferLength * 2); + if (error) { +#if DEBUG + mach_error("vm_deallocate in dealloc", error); +#endif + } +} diff --git a/src/Cocoa/debug-exports-64.exp b/src/Cocoa/debug-exports-64.exp new file mode 100644 index 00000000..4e3555a2 --- /dev/null +++ b/src/Cocoa/debug-exports-64.exp @@ -0,0 +1,67 @@ +# Symbols used by Debug.OXP +.objc_class_name_PlayerEntity +.objc_class_name_OOCacheManager +.objc_class_name_OOGraphicsResetManager +.objc_class_name_OOTexture +.objc_class_name_ResourceManager +.objc_class_name_OOJavaScriptEngine +.objc_class_name_OOColor +.objc_class_name_OOWeakRefObject +.objc_class_name_OOScript +.objc_class_name_OOJSScript +.objc_class_name_OODebugMonitor +.objc_class_name_ShipEntity +.objc_class_name_OOEntityWithDrawable + +_OBJC_CLASS_$_ShipEntity +_OBJC_CLASS_$_Entity +_OBJC_IVAR_$_PlayerEntity.player_name +_OBJC_CLASS_$_AI + +_OOLogWithFunctionFileAndLine +_OOLogInsertMarker +_OOLogSetDisplayMessagesInClass +_OOLogSetShowFileAndLine +_OOLogSetShowFunction +_OOLogSetShowMessageClass +_OOLogShowFileAndLine +_OOLogShowFunction +_OOLogShowMessageClass +_OOLogWillDisplayMessagesInClass +_OOLogHandlerGetLogPath +_OOLogIndent +_OOLogOutdent +_gDebugFlags +_gSharedUniverse +_JSBasicPrivateObjectConverter +_JSObjectToObject +_JSRegisterObjectConverter +_JS_ConvertStub +_JS_EnumerateStub +_JS_GetPrivate +_JS_SetPrivate +_JS_InitClass +_JS_NewObject +_JS_PropertyStub +_JS_ResolveStub +_OOReportJSBadPropertySelector +_OOReportJSError +_JS_InternString +_JS_AddNamedRoot +_JS_RemoveRoot +_JSValueToObject +_JS_ValueToNumber +_JS_SetProperty +_OOReportJSWarning +_OOBooleanFromObject +_ScanClassToString +_EntityStatusToString +_QuaternionDescription +_VectorDescription +_BehaviourToString +_kOOLogUnconvertedNSLog + +# Debug value formatter support +_JSValueToStrDbg +_JSObjectToStrDbg +_JSValueTypeDbg diff --git a/src/Cocoa/debug-exports.exp b/src/Cocoa/debug-exports.exp new file mode 100644 index 00000000..5bd90df8 --- /dev/null +++ b/src/Cocoa/debug-exports.exp @@ -0,0 +1,82 @@ +# Symbols used by Debug.OXP +.objc_class_name_PlayerEntity +.objc_class_name_OOCacheManager +.objc_class_name_OOGraphicsResetManager +.objc_class_name_OOTexture +.objc_class_name_ResourceManager +.objc_class_name_OOJavaScriptEngine +.objc_class_name_OOColor +.objc_class_name_OOWeakRefObject +.objc_class_name_OOScript +.objc_class_name_OOJSScript +.objc_class_name_OODebugMonitor +.objc_class_name_ShipEntity +.objc_class_name_OOEntityWithDrawable +_OOLogWithFunctionFileAndLine +_OOLogInsertMarker +_OOLogSetDisplayMessagesInClass +_OOLogSetShowFileAndLine +_OOLogSetShowFunction +_OOLogSetShowMessageClass +_OOLogShowFileAndLine +_OOLogShowFunction +_OOLogShowMessageClass +_OOLogWillDisplayMessagesInClass +_OOLogHandlerGetLogPath +_OOLogIndent +_OOLogOutdent +_gDebugFlags +_gSharedUniverse +_JSBasicPrivateObjectConverter +_JSObjectToObject +_JSRegisterObjectConverter +_JS_ConvertStub +_JS_EnumerateStub +_JS_GetPrivate +_JS_SetPrivate +_JS_InitClass +_JS_NewObject +_JS_PropertyStub +_JS_ResolveStub +_OOReportJSBadPropertySelector +_OOReportJSError +_JS_InternString +_JS_AddNamedRoot +_JS_RemoveRoot +_JSValueToObject +_JS_ValueToNumber +_JS_SetProperty +_OOReportJSWarning +_OOBooleanFromObject +_ScanClassToString +_EntityStatusToString +_QuaternionDescription +_VectorDescription +_BehaviourToString +_kOOLogUnconvertedNSLog + +# Symbols exported for warning supression. Without these, we get lines like +# /usr/bin/ld: warning symbol: _NXArgc referenced dynamically and must be exported +_NXArgc +_NXArgv +___progname +_environ +_catch_exception_raise +_catch_exception_raise_state +_catch_exception_raise_state_identity +_clock_alarm_reply +_do_mach_notify_dead_name +_do_mach_notify_no_senders +_do_mach_notify_port_deleted +_do_mach_notify_send_once +_receive_samples +__mh_execute_header +_do_seqnos_mach_notify_dead_name +_do_seqnos_mach_notify_no_senders +_do_seqnos_mach_notify_port_deleted +_do_seqnos_mach_notify_send_once + +# Debug value formatter support +_JSValueToStrDbg +_JSObjectToStrDbg +_JSValueTypeDbg diff --git a/src/Cocoa/main.m b/src/Cocoa/main.m new file mode 100644 index 00000000..a770e85f --- /dev/null +++ b/src/Cocoa/main.m @@ -0,0 +1,15 @@ +#import +#import "OOLoggingExtended.h" + + +#ifndef NDEBUG +uint32_t gDebugFlags = 0; +#endif + + +int main(int argc, const char *argv[]) +{ + OOLoggingInit(); + return NSApplicationMain(argc, argv); +} + diff --git a/src/Cocoa/oolite-options.xcconfig b/src/Cocoa/oolite-options.xcconfig new file mode 100644 index 00000000..0b067b05 --- /dev/null +++ b/src/Cocoa/oolite-options.xcconfig @@ -0,0 +1,10 @@ +#include "oolite-version.xcconfig" + + +ALLOW_PROCEDURAL_PLANETS = 0 +DOCKING_CLEARANCE_ENABLED = 1 +TARGET_INCOMING_MISSILES = 1 +WORMHOLE_SCANNER = 1 + + +OOLITE_OPTION_MACROS = ALLOW_PROCEDURAL_PLANETS=$(ALLOW_PROCEDURAL_PLANETS) DOCKING_CLEARANCE_ENABLED=$(DOCKING_CLEARANCE_ENABLED) TARGET_INCOMING_MISSILES=$(TARGET_INCOMING_MISSILES) WORMHOLE_SCANNER=$(WORMHOLE_SCANNER) diff --git a/src/Cocoa/oolite-version.xcconfig b/src/Cocoa/oolite-version.xcconfig new file mode 100644 index 00000000..f2c6d2f4 --- /dev/null +++ b/src/Cocoa/oolite-version.xcconfig @@ -0,0 +1 @@ +OOLITE_VERSION = 1.73 diff --git a/src/Core/AI.h b/src/Core/AI.h new file mode 100644 index 00000000..2273ed47 --- /dev/null +++ b/src/Core/AI.h @@ -0,0 +1,115 @@ +/* + +AI.h + +Core NPC behaviour/artificial intelligence class. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOWeakReference.h" +#import "OOTypes.h" + +#define AI_THINK_INTERVAL 0.125 + + +@class ShipEntity; +#ifdef OO_BRAIN_AI +@class OOInstinct; +#endif + + +@interface AI : OOWeakRefObject +{ + id _owner; // OOWeakReference to the ShipEntity this is the AI for + NSString *ownerDesc; // describes the object this is the AI for + + NSDictionary *stateMachine; + NSString *stateMachineName; + NSString *currentState; + NSMutableSet *pendingMessages; + + NSMutableArray *aiStack; + +#ifdef OO_BRAIN_AI + OOInstinct *rulingInstinct; +#endif + + OOTimeAbsolute nextThinkTime; + OOTimeDelta thinkTimeInterval; + +} + ++ (AI *) currentlyRunningAI; ++ (NSString *) currentlyRunningAIDescription; + +- (NSString *) name; +- (NSString *) state; + +- (void) setStateMachine:(NSString *)smName; +- (void) setState:(NSString *)stateName; + +- (void) setStateMachine:(NSString *)smName afterDelay:(NSTimeInterval)delay; +- (void) setState:(NSString *)stateName afterDelay:(NSTimeInterval)delay; + +- (id) initWithStateMachine:(NSString *) smName andState:(NSString *) stateName; + +#ifdef OO_BRAIN_AI +- (OOInstinct *) rulingInstinct; +- (void) setRulingInstinct:(OOInstinct*) instinct; +#endif + +- (ShipEntity *)owner; +- (void) setOwner:(ShipEntity *)ship; + +- (void) preserveCurrentStateMachine; + +- (void) restorePreviousStateMachine; + +- (BOOL) hasSuspendedStateMachines; +- (void) exitStateMachineWithMessage:(NSString *)message; + +- (unsigned) stackDepth; + +- (void) reactToMessage:(NSString *) message; + +- (void) takeAction:(NSString *) action; + +- (void) think; + +- (void) message:(NSString *) ms; +- (void) dropMessage:(NSString *) ms; +- (NSSet *) pendingMessages; +- (void) debugDumpPendingMessages; + +- (void) setNextThinkTime:(OOTimeAbsolute) ntt; +- (OOTimeAbsolute) nextThinkTime; + +- (void) setThinkTimeInterval:(OOTimeDelta) tti; +- (OOTimeDelta) thinkTimeInterval; + +- (void) clearStack; + +- (void) clearAllData; + +- (void)dumpState; + +@end diff --git a/src/Core/AI.m b/src/Core/AI.m new file mode 100644 index 00000000..c52f3928 --- /dev/null +++ b/src/Core/AI.m @@ -0,0 +1,833 @@ +/* + +AI.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "AI.h" +#import "ResourceManager.h" +#import "OOStringParsing.h" +#import "OOWeakReference.h" +#import "OOCacheManager.h" +#import "OOCollectionExtractors.h" + +#import "ShipEntity.h" + +#ifdef OO_BRAIN_AI +#import "OOInstinct.h" +#endif + +#define kOOLogUnconvertedNSLog @"unclassified.AI" + + +typedef struct +{ + AI *ai; + SEL selector; + id parameter; +} OOAIDeferredCallTrampolineInfo; + + +static AI *sCurrentlyRunningAI = nil; + + +@interface AI (OOPrivate) + +// Wrapper for performSelector:withObject:afterDelay: to catch/fix bugs. +- (void)performDeferredCall:(SEL)selector withObject:(id)object afterDelay:(NSTimeInterval)delay; ++ (void)deferredCallTrampolineWithInfo:(NSValue *)info; + +- (void)refreshOwnerDesc; + +// Loading/whitelisting +- (NSDictionary *) loadStateMachine:(NSString *)smName; +- (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName; +- (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName; + +@end + + +@implementation AI + ++ (AI *) currentlyRunningAI +{ + return sCurrentlyRunningAI; +} + + ++ (NSString *) currentlyRunningAIDescription +{ + if (sCurrentlyRunningAI != nil) + { + return [NSString stringWithFormat:@"%@ in state %@", [sCurrentlyRunningAI name], [sCurrentlyRunningAI state]]; + } + else + { + return @""; + } +} + + +- (id) init +{ + self = [super init]; + + aiStack = [[NSMutableArray alloc] init]; + pendingMessages = [[NSMutableSet alloc] init]; + + nextThinkTime = [[NSDate distantFuture] timeIntervalSinceNow]; // don't think for a while + thinkTimeInterval = AI_THINK_INTERVAL; + + stateMachineName = [[NSString stringWithString:@"None allocated"] retain]; // no initial brain + + return self; +} + + +- (id) initWithStateMachine:(NSString *) smName andState:(NSString *) stateName +{ + self = [self init]; + + if (smName != nil) [self setStateMachine:smName]; + if (stateName != nil) currentState = [stateName retain]; + + return self; +} + + +- (void) dealloc +{ + [_owner release]; + [ownerDesc release]; + [aiStack release]; + [stateMachine release]; + [stateMachineName release]; + [currentState release]; + [pendingMessages release]; + + if (sCurrentlyRunningAI == self) sCurrentlyRunningAI = nil; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\" in state: \"%@\" for %@", stateMachineName, currentState, ownerDesc]; +} + + +#ifdef OO_BRAIN_AI +- (OOInstinct *) rulingInstinct +{ + return rulingInstinct; +} + + +- (void) setRulingInstinct:(OOInstinct*) instinct +{ + rulingInstinct = instinct; +} +#endif + + +- (ShipEntity *)owner +{ + ShipEntity *owner = [_owner weakRefUnderlyingObject]; + if (owner == nil) + { + [_owner release]; + _owner = nil; + } + + return owner; +} + + +- (void) setOwner:(ShipEntity *)ship +{ + [_owner release]; + _owner = [ship weakRetain]; + [self refreshOwnerDesc]; +} + + +- (void) preserveCurrentStateMachine +{ + if (!stateMachine) + return; + + NSMutableDictionary *pickledMachine = [NSMutableDictionary dictionaryWithCapacity:3]; + + [pickledMachine setObject:stateMachine forKey:@"stateMachine"]; + [pickledMachine setObject:currentState forKey:@"currentState"]; + [pickledMachine setObject:stateMachineName forKey:@"stateMachineName"]; + [pickledMachine setObject:[[pendingMessages copy] autorelease] forKey:@"pendingMessages"]; + + if (aiStack == nil) aiStack = [[NSMutableArray alloc] init]; + + if ([aiStack count] > 32) + { + OOLogERR(@"ai.pushStateMachine.overflow", @"AI stack overflow for %@ stack:\n%@", [_owner shortDescription], aiStack); + [NSException raise:@"OoliteException" + format:@"AI stack overflow for %@", _owner]; + } + +#ifndef NDEBUG + if ([[self owner] reportAIMessages]) OOLog(@"ai.stack.push", @"Pushing state machine for %@", self); +#endif + [aiStack insertObject:pickledMachine atIndex:0]; // PUSH +} + + +- (void) restorePreviousStateMachine +{ + if ([aiStack count] == 0) return; + + NSMutableDictionary *pickledMachine = [aiStack objectAtIndex:0]; + +#ifndef NDEBUG + if ([[self owner] reportAIMessages]) OOLog(@"ai.stack.pop", @"Popping previous state machine for %@", self); +#endif + + [stateMachine release]; + stateMachine = [[pickledMachine objectForKey:@"stateMachine"] retain]; + + [currentState release]; + currentState = [[pickledMachine objectForKey:@"currentState"] retain]; + + [stateMachineName release]; + stateMachineName = [[pickledMachine objectForKey:@"stateMachineName"] retain]; + + [pendingMessages release]; + pendingMessages = [[pickledMachine objectForKey:@"pendingMessages"] mutableCopy]; // restore a MUTABLE set + + [aiStack removeObjectAtIndex:0]; // POP +} + + +- (BOOL) hasSuspendedStateMachines +{ + return [aiStack count] != 0; +} + + +- (void) exitStateMachineWithMessage:(NSString *)message +{ + if ([aiStack count] != 0) + { + [self restorePreviousStateMachine]; + if (message == nil) message = @"RESTARTED"; + [self reactToMessage:message]; + } +} + + +- (void) setStateMachine:(NSString *) smName +{ + NSDictionary *newSM = [self loadStateMachine:smName]; + + if (newSM) + { + [self preserveCurrentStateMachine]; + [stateMachine release]; // release old state machine + stateMachine = [newSM retain]; + nextThinkTime = 0.0; // think at next tick + } + + [currentState release]; + currentState = @"GLOBAL"; + /* CRASH in objc_msgSend, apparently on [self reactToMessage:@"ENTER"] (1.69, OS X/x86). + Analysis: self corrupted. We're being called by __NSFireDelayedPerform, which doesn't go + through -[NSObject performSelector:withObject:], suggesting it's using IMP caching. An + invalid self is therefore possible. + Attempted fix: new delayed dispatch with trampoline, see -[AI setStateMachine:afterDelay:]. + -- Ahruman, 20070706 + */ + [self reactToMessage:@"ENTER"]; + + // refresh name + [self refreshOwnerDesc]; + + // refresh stateMachineName + [stateMachineName release]; + stateMachineName = [smName copy]; +} + + +- (void) setState:(NSString *) stateName +{ + if ([stateMachine objectForKey:stateName]) + { + /* CRASH in objc_msgSend, apparently on [self reactToMessage:@"EXIT"] (1.69, OS X/x86). + Analysis: self corrupted. We're being called by __NSFireDelayedPerform, which doesn't go + through -[NSObject performSelector:withObject:], suggesting it's using IMP caching. An + invalid self is therefore possible. + Attempted fix: new delayed dispatch with trampoline, see -[AI setState:afterDelay:]. + -- Ahruman, 20070706 + */ + [self reactToMessage:@"EXIT"]; + [currentState release]; + currentState = [stateName retain]; + [self reactToMessage:@"ENTER"]; + } +} + + +- (void) setStateMachine:(NSString *)smName afterDelay:(NSTimeInterval)delay +{ + [self performDeferredCall:@selector(setStateMachine:) withObject:smName afterDelay:delay]; +} + + +- (void) setState:(NSString *)stateName afterDelay:(NSTimeInterval)delay +{ + [self performDeferredCall:@selector(setState:) withObject:stateName afterDelay:delay]; +} + + +- (NSString *) name +{ + return [[stateMachineName retain] autorelease]; +} + + +- (NSString *) state +{ + return [[currentState retain] autorelease]; +} + + +- (unsigned) stackDepth +{ + return [aiStack count]; +} + + +- (void) reactToMessage:(NSString *)message +{ + unsigned i; + NSArray *actions = nil; + NSDictionary *messagesForState = nil; + ShipEntity *owner = [self owner]; + static unsigned recursionLimiter = 0; + AI *previousRunning = sCurrentlyRunningAI; + + + /* CRASH in _freedHandler when called via -setState: __NSFireDelayedPerform (1.69, OS X/x86). + Analysis: owner invalid. + Fix: make owner an OOWeakReference. + -- Ahruman, 20070706 + */ + if (message == nil || owner == nil || [owner universalID] == NO_TARGET) return; + + + /* CRASH when calling reactToMessage: FOO in state FOO causes infinite + recursion. + FIX: recursion limiter. Alternative is to explicitly catch this case + in takeAction:, but that could potentially miss indirect recursion via + scripts. + */ + if (recursionLimiter > 32) + { + OOLogERR(@"ai.error.recursion", @"AI reactToMessage: recursion in AI %@, state %@, aborting. It is not valid to call reactToMessage: FOO in state FOO.", stateMachineName, currentState); + return; + } + + messagesForState = [stateMachine objectForKey:currentState]; + if (messagesForState == nil) return; + +#ifndef NDEBUG + if (currentState != nil && ![message isEqual:@"UPDATE"] && [owner reportAIMessages]) + { + OOLog(@"ai.message.receive", @"AI %@ for %@ in state '%@' receives message '%@'", stateMachineName, ownerDesc, currentState, message); + } +#endif + + actions = [[[messagesForState objectForKey:message] copy] autorelease]; + +#ifdef OO_BRAIN_AI + if (rulingInstinct != nil) [rulingInstinct freezeShipVars]; // preserve the pre-thinking state +#endif + + sCurrentlyRunningAI = self; + if ([actions count] > 0) + { + NS_DURING + ++recursionLimiter; + for (i = 0; i < [actions count]; i++) + { + [self takeAction:[actions objectAtIndex:i]]; + } + --recursionLimiter; + NS_HANDLER + --recursionLimiter; + NS_ENDHANDLER + } + else + { + if (currentState != nil) + { + if ([owner respondsToSelector:@selector(interpretAIMessage:)]) + { + [owner performSelector:@selector(interpretAIMessage:) withObject:message]; + } + } + } + sCurrentlyRunningAI = previousRunning; + +#ifdef OO_BRAIN_AI + if (rulingInstinct != nil) + { + [rulingInstinct getShipVars]; // record the post-thinking state + [rulingInstinct unfreezeShipVars]; // restore the pre-thinking state (AI is now abstract thought = instincts motivate) + } +#endif +} + + +- (void) takeAction:(NSString *) action +{ + NSArray *tokens = ScanTokensFromString(action); + NSString *dataString = nil; + NSString *selectorStr; + SEL selector; + ShipEntity *owner = [self owner]; + +#ifndef NDEBUG + BOOL report = [owner reportAIMessages]; + if (report) + { + OOLog(@"ai.takeAction", @"%@ to take action %@", ownerDesc, action); + OOLogIndent(); + } +#endif + + if ([tokens count] != 0) + { + selectorStr = [tokens objectAtIndex:0]; + + if (owner != nil) + { + if ([tokens count] > 1) + { + dataString = [[tokens subarrayWithRange:NSMakeRange(1, [tokens count] - 1)] componentsJoinedByString:@" "]; + } + + selector = NSSelectorFromString(selectorStr); + if ([owner respondsToSelector:selector]) + { + if (dataString) [owner performSelector:selector withObject:dataString]; + else [owner performSelector:selector]; + } + else + { + if ([selectorStr isEqual:@"setStateTo:"]) [self setState:dataString]; + else if ([selectorStr isEqual:@"debugMessage:"]) + { + OOLog(@"ai.takeAction.debugMessage", @"DEBUG: AI MESSAGE from %@: %@", ownerDesc, dataString); + } + else + { + OOLogERR(@"ai.takeAction.badSelector", @"in AI %@ in state %@: %@ does not respond to %@", stateMachineName, currentState, ownerDesc, selectorStr); + } + } + } + else + { + OOLog(@"ai.takeAction.orphaned", @"***** AI %@, trying to perform %@, is orphaned (no owner)", stateMachineName, selectorStr); + } + } + else + { +#ifndef NDEBUG + if (report) OOLog(@"ai.takeAction.noAction", @"DEBUG: - no action '%@'", action); +#endif + } + +#ifndef NDEBUG + if (report) + { + OOLogOutdent(); + } +#endif +} + + +- (void) think +{ + NSArray *ms_list = nil; + unsigned i; + + if ([[self owner] universalID] == NO_TARGET || stateMachine == nil) return; // don't think until launched + + [self reactToMessage:@"UPDATE"]; + + if ([pendingMessages count] > 0) ms_list = [pendingMessages allObjects]; + [pendingMessages removeAllObjects]; + + if (ms_list != nil) + { + for (i = 0; i < [ms_list count]; i++) + { + [self reactToMessage:[ms_list objectAtIndex:i]]; + } + } +} + + +- (void) message:(NSString *) ms +{ + if ([[self owner] universalID] == NO_TARGET) return; // don't think until launched + + if ([pendingMessages count] > 32) + { + OOLogERR(@"ai.message.failed.overflow", @"AI pending messages overflow for '%@'; pending messages:\n%@", ownerDesc, pendingMessages); + [NSException raise:@"OoliteException" + format:@"AI pendingMessages overflow for %@", ownerDesc]; + } + + [pendingMessages addObject:ms]; +} + + +- (void) dropMessage:(NSString *) ms +{ + [pendingMessages removeObject:ms]; +} + + +- (NSSet *) pendingMessages +{ + return [[pendingMessages copy] autorelease]; +} + + +- (void) debugDumpPendingMessages +{ + NSArray *sortedMessages = nil; + NSString *displayMessages = nil; + + if ([pendingMessages count] > 0) + { + sortedMessages = [[pendingMessages allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + displayMessages = [sortedMessages componentsJoinedByString:@", "]; + } + else + { + displayMessages = @"none"; + } + + OOLog(@"ai.debug.pendingMessages", @"Pending messages for AI %@: %@", [self descriptionComponents], displayMessages); +} + + +- (void) setNextThinkTime:(OOTimeAbsolute) ntt +{ + nextThinkTime = ntt; +} + + +- (OOTimeAbsolute) nextThinkTime +{ + if (!stateMachine) + return [[NSDate distantFuture] timeIntervalSinceNow]; + + return nextThinkTime; +} + + +- (void) setThinkTimeInterval:(OOTimeDelta) tti +{ + thinkTimeInterval = tti; +} + + +- (OOTimeDelta) thinkTimeInterval +{ + return thinkTimeInterval; +} + + +- (void) clearStack +{ + [aiStack removeAllObjects]; +} + + +- (void) clearAllData +{ + [aiStack removeAllObjects]; + [pendingMessages removeAllObjects]; + + nextThinkTime += 36000.0; // should dealloc in under ten hours! + thinkTimeInterval = 36000.0; +} + + +- (void)dumpState +{ + OOLog(@"dumpState.ai", @"State machine name: %@", stateMachineName); + OOLog(@"dumpState.ai", @"Current state: %@", currentState); +#ifdef OO_BRAIN_AI + if (rulingInstinct!= nil && OOLogWillDisplayMessagesInClass(@"dumpState.ai.instinct")) + { + OOLog(@"dumpState.ai.instinct", @"Ruling instinct:"); + OOLogPushIndent(); + OOLogIndent(); + NS_DURING + [rulingInstinct dumpState]; + NS_HANDLER + NS_ENDHANDLER + OOLogPopIndent(); + } +#endif + OOLog(@"dumpState.ai", @"Next think time: %g", nextThinkTime); + OOLog(@"dumpState.ai", @"Next think interval: %g", thinkTimeInterval); +} + +@end + + +/* This is an attempt to fix the bugs referred to above regarding calls from + __NSFireDelayedPerform with a corrupt self. I'm not certain whether this + will fix the issue or merely cause a less weird crash in + +deferredCallTrampolineWithInfo:. + -- Ahruman 20070706 +*/ +@implementation AI (OOPrivate) + +- (void)performDeferredCall:(SEL)selector withObject:(id)object afterDelay:(NSTimeInterval)delay +{ + OOAIDeferredCallTrampolineInfo infoStruct; + NSValue *info = nil; + + if (selector != NULL) + { + infoStruct.ai = [self retain]; + infoStruct.selector = selector; + infoStruct.parameter = object; + + info = [[NSValue alloc] initWithBytes:&infoStruct objCType:@encode(OOAIDeferredCallTrampolineInfo)]; + + [[AI class] performSelector:@selector(deferredCallTrampolineWithInfo:) + withObject:info + afterDelay:delay]; + [info release]; + } +} + + ++ (void)deferredCallTrampolineWithInfo:(NSValue *)info +{ + OOAIDeferredCallTrampolineInfo infoStruct; + + if (info != nil) + { + assert(strcmp([info objCType], @encode(OOAIDeferredCallTrampolineInfo)) == 0); + [info getValue:&infoStruct]; + + [infoStruct.ai performSelector:infoStruct.selector withObject:infoStruct.parameter]; + + [infoStruct.ai release]; + [infoStruct.parameter release]; + } +} + + +- (void)refreshOwnerDesc +{ + ShipEntity *owner = [self owner]; + [ownerDesc release]; + if (owner != nil) + { + ownerDesc = [[NSString alloc] initWithFormat:@"%@ %d", [owner name], [owner universalID]]; + } + else + { + ownerDesc = @"no owner"; + } +} + + +- (NSDictionary *) loadStateMachine:(NSString *)smName +{ + NSDictionary *newSM = nil; + NSMutableDictionary *cleanSM = nil; + OOCacheManager *cacheMgr = [OOCacheManager sharedCache]; + NSEnumerator *stateEnum = nil; + NSString *stateKey = nil; + NSDictionary *stateHandlers = nil; + NSAutoreleasePool *pool = nil; + + newSM = [cacheMgr objectForKey:smName inCache:@"AIs"]; + if (newSM != nil && ![newSM isKindOfClass:[NSDictionary class]]) return nil; // catches use of @"nil" to indicate no AI found. + + if (newSM == nil) + { + pool = [[NSAutoreleasePool alloc] init]; + OOLog(@"ai.load", @"Loading and sanitizing AI \"%@\"", smName); + OOLogPushIndent(); + OOLogIndentIf(@"ai.load"); + + NS_DURING + // Load state machine and validate against whitelist. + newSM = [ResourceManager dictionaryFromFilesNamed:smName inFolder:@"AIs" andMerge:NO]; + if (newSM == nil) + { + [cacheMgr setObject:@"nil" forKey:smName inCache:@"AIs"]; + NS_VALUERETURN(nil, NSDictionary *); + } + + cleanSM = [NSMutableDictionary dictionaryWithCapacity:[newSM count]]; + + for (stateEnum = [newSM keyEnumerator]; (stateKey = [stateEnum nextObject]); ) + { + stateHandlers = [newSM objectForKey:stateKey]; + if (![stateHandlers isKindOfClass:[NSDictionary class]]) + { + OOLogWARN(@"ai.invalidFormat.state", @"State \"%@\" in AI \"%@\" is not a dictionary, ignoring.", stateKey, smName); + continue; + } + + stateHandlers = [self cleanHandlers:stateHandlers forState:stateKey stateMachine:smName]; + [cleanSM setObject:stateHandlers forKey:stateKey]; + } + + // Make immutable. + newSM = [[cleanSM copy] autorelease]; + + // Cache. + [cacheMgr setObject:newSM forKey:smName inCache:@"AIs"]; + NS_HANDLER + OOLogPopIndent(); + [localException raise]; + NS_ENDHANDLER + + OOLogPopIndent(); + [pool release]; + } + + return newSM; +} + + +- (NSDictionary *) cleanHandlers:(NSDictionary *)handlers forState:(NSString *)stateKey stateMachine:(NSString *)smName +{ + NSEnumerator *handlerEnum = nil; + NSString *handlerKey = nil; + NSArray *handlerActions = nil; + NSMutableDictionary *result = nil; + + result = [NSMutableDictionary dictionaryWithCapacity:[handlers count]]; + for (handlerEnum = [handlers keyEnumerator]; (handlerKey = [handlerEnum nextObject]); ) + { + handlerActions = [handlers objectForKey:handlerKey]; + if (![handlerActions isKindOfClass:[NSArray class]]) + { + OOLogWARN(@"ai.invalidFormat.handler", @"Handler \"%@\" for state \"%@\" in AI \"%@\" is not an array, ignoring.", handlerKey, stateKey, smName); + continue; + } + + handlerActions = [self cleanActions:handlerActions forHandler:handlerKey state:stateKey stateMachine:smName]; + [result setObject:handlerActions forKey:handlerKey]; + } + + // Return immutable copy. + return [[result copy] autorelease]; +} + + +- (NSArray *) cleanActions:(NSArray *)actions forHandler:(NSString *)handlerKey state:(NSString *)stateKey stateMachine:(NSString *)smName +{ + NSEnumerator *actionEnum = nil; + NSString *action = nil; + NSRange spaceRange; + NSString *selector = nil; + id aliasedSelector = nil; + NSMutableArray *result = nil; + static NSSet *whitelist = nil; + static NSDictionary *aliases = nil; + NSArray *whitelistArray1 = nil; + NSArray *whitelistArray2 = nil; + + if (whitelist == nil) + { + whitelistArray1 = [[ResourceManager whitelistDictionary] arrayForKey:@"ai_methods"]; + if (whitelistArray1 == nil) whitelistArray1 = [NSArray array]; + whitelistArray2 = [[ResourceManager whitelistDictionary] arrayForKey:@"ai_and_action_methods"]; + if (whitelistArray2 != nil) whitelistArray1 = [whitelistArray1 arrayByAddingObjectsFromArray:whitelistArray2]; + + whitelist = [[NSSet alloc] initWithArray:whitelistArray1]; + aliases = [[[ResourceManager whitelistDictionary] dictionaryForKey:@"ai_method_aliases"] retain]; + } + + result = [NSMutableArray arrayWithCapacity:[actions count]]; + for (actionEnum = [actions objectEnumerator]; (action = [actionEnum nextObject]); ) + { + if (![action isKindOfClass:[NSString class]]) + { + OOLogWARN(@"ai.invalidFormat.action", @"An action in handler \"%@\" for state \"%@\" in AI \"%@\" is not a string, ignoring.", handlerKey, stateKey, smName); + continue; + } + + // Trim spaces from beginning and end. + action = [action stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + // Cut off parameters. + spaceRange = [action rangeOfString:@" "]; + if (spaceRange.location == NSNotFound) selector = action; + else selector = [action substringToIndex:spaceRange.location]; + + // Look in alias table. + aliasedSelector = [aliases objectForKey:selector]; + if (aliasedSelector != nil) + { + if ([aliasedSelector isKindOfClass:[NSString class]]) + { + // Change selector and action to use real method name. + selector = aliasedSelector; + if (spaceRange.location == NSNotFound) action = aliasedSelector; + else action = [aliasedSelector stringByAppendingString:[action substringFromIndex:spaceRange.location]]; + } + else if ([aliasedSelector isKindOfClass:[NSArray class]] && [aliasedSelector count] != 0) + { + // Alias is complete expression, pretokenized in anticipation of a tokenized future. + action = [aliasedSelector componentsJoinedByString:@" "]; + selector = [[aliasedSelector objectAtIndex:0] description]; + } + } + + // Check for selector in whitelist. + if (![whitelist containsObject:selector]) + { + OOLog(@"ai.unpermittedMethod", @"Handler \"%@\" for state \"%@\" in AI \"%@\" uses \"%@\", which is not a permitted AI method. In a future version of Oolite, this method will be removed from the handler. If you believe the handler should be a permitted method, please report it to bugs@oolite.org.", handlerKey, stateKey, smName, selector); + // continue; + } + + [result addObject:action]; + } + + // Return immutable copy. + return [[result copy] autorelease]; +} + +@end diff --git a/src/Core/CollisionRegion.h b/src/Core/CollisionRegion.h new file mode 100644 index 00000000..3feead0c --- /dev/null +++ b/src/Core/CollisionRegion.h @@ -0,0 +1,90 @@ +/* + +CollisionRegion.h + +Collision regions are used to group entities which may potentially collide, to +reduce the number of collision checks required. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOMaths.h" + +#define COLLISION_REGION_BORDER_RADIUS 32000.0f +#define COLLISION_MAX_ENTITIES 128 + +@class Entity; + +@interface CollisionRegion : NSObject +{ +@public + + BOOL isUniverse; // if YES location is origin and radius is 0.0f + + int crid; // identifier + Vector location; // center of the region + GLfloat radius; // inner radius of the region + GLfloat border_radius; // additiønal, border radius of the region (typically 32km or some value > the scanner range) + + int checks_this_tick; + + NSMutableArray *subregions; + +@protected + + BOOL isPlayerInRegion; + + Entity **entity_array; // entities within the region + int n_entities; // number of entities + int max_entities; // so storage can be expanded + + + CollisionRegion *parentRegion; + +} + +- (id) initAsUniverse; +- (id) initAtLocation:(Vector) locn withRadius:(GLfloat) rad withinRegion:(CollisionRegion*) otherRegion; + +- (void) clearSubregions; +- (void) addSubregionAtPosition:(Vector) pos withRadius:(GLfloat) rad; + +// update routines to check if a position is within the radius or within it's borders +// +BOOL positionIsWithinRegion( Vector position, CollisionRegion* region); +BOOL sphereIsWithinRegion( Vector position, GLfloat rad, CollisionRegion* region); +BOOL positionIsWithinBorders( Vector position, CollisionRegion* region); +BOOL positionIsOnBorders( Vector position, CollisionRegion* region); +NSArray* subregionsContainingPosition( Vector position, CollisionRegion* region); + +// collision checking +// +- (void) clearEntityList; +- (void) addEntity:(Entity*) ent; +// +- (BOOL) checkEntity:(Entity*) ent; +// +- (void) findCollisions; +- (void) findShadowedEntities; + +- (NSString*) debugOut; + +@end diff --git a/src/Core/CollisionRegion.m b/src/Core/CollisionRegion.m new file mode 100644 index 00000000..b515e80c --- /dev/null +++ b/src/Core/CollisionRegion.m @@ -0,0 +1,596 @@ +/* + +CollisionRegion.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "CollisionRegion.h" +#import "OOMaths.h" +#import "Universe.h" +#import "Entity.h" +#import "ShipEntity.h" +#import "PlanetEntity.h" +#import "StationEntity.h" +#import "PlayerEntity.h" + + +@implementation CollisionRegion + +- (NSString *) description +{ + int n_subs = [subregions count]; + return [NSString stringWithFormat:@"ID: %d, %d subregions, %d ents", crid, n_subs, n_entities]; +} + +// basic alloc/ dealloc routines +// +static int crid_counter = 1; +// +- (id) initAsUniverse +{ + self = [super init]; + + location = kZeroVector; + radius = 0.0f; + border_radius = 0.0f; + isUniverse = YES; + isPlayerInRegion = NO; + + max_entities = COLLISION_MAX_ENTITIES; + n_entities = 0; + entity_array = (Entity**) malloc( max_entities * sizeof(Entity*)); + + subregions = [[NSMutableArray alloc] initWithCapacity: 32]; // retained + + parentRegion = nil; + + crid = crid_counter++; + + return self; +} + +- (id) initAtLocation:(Vector) locn withRadius:(GLfloat) rad withinRegion:(CollisionRegion*) otherRegion +{ + self = [super init]; + + location = locn; + radius = rad; + border_radius = COLLISION_REGION_BORDER_RADIUS; + isUniverse = NO; + isPlayerInRegion = NO; + + max_entities = COLLISION_MAX_ENTITIES; + n_entities = 0; + entity_array = (Entity**) malloc( max_entities * sizeof(Entity*)); + + subregions = [[NSMutableArray alloc] initWithCapacity: 32]; // retained + + if (otherRegion) + parentRegion = otherRegion; + + crid = crid_counter++; + + return self; +} + +- (void) dealloc +{ + if (entity_array) + free((void *)entity_array); // free up the allocated space + if (subregions) + [subregions release]; + [super dealloc]; +} + +- (void) clearSubregions +{ + int i; + int n_subs = [subregions count]; + for (i = 0; i < n_subs; i++) + [(CollisionRegion*)[subregions objectAtIndex: i] clearSubregions]; + [subregions removeAllObjects]; +} + +- (void) addSubregionAtPosition:(Vector) pos withRadius:(GLfloat) rad +{ + // check if this can be fitted within any of the subregions + // + int i; + int n_subs = [subregions count]; + for (i = 0; i < n_subs; i++) + { + CollisionRegion* sub = (CollisionRegion*)[subregions objectAtIndex: i]; + if (sphereIsWithinRegion( pos, rad, sub)) + { + // if it fits, put it in! + [sub addSubregionAtPosition: pos withRadius: rad]; + return; + } + if (positionIsWithinRegion( pos, sub)) + { + // crosses the border of this region already - leave it out + return; + } + } + // no subregion fit - move on... + // + CollisionRegion* sub = [[CollisionRegion alloc] initAtLocation: pos withRadius: rad withinRegion: self]; + [subregions addObject:sub]; + [sub release]; +} + + +// update routines to check if a position is within the radius or within it's borders +// +BOOL positionIsWithinRegion( Vector position, CollisionRegion* region) +{ + if (!region) + return NO; + + if (region->isUniverse) + return YES; + + Vector loc = region->location; + GLfloat r1 = region->radius; + + if ((position.x < loc.x - r1)||(position.x > loc.x + r1)|| + (position.y < loc.y - r1)||(position.y > loc.y + r1)|| + (position.z < loc.z - r1)||(position.z > loc.z + r1)) + return NO; + + return YES; +} + +BOOL sphereIsWithinRegion( Vector position, GLfloat rad, CollisionRegion* region) +{ + if (!region) + return NO; + + if (region->isUniverse) + return YES; + + Vector loc = region->location; + GLfloat r1 = region->radius; + + if ((position.x - rad < loc.x - r1)||(position.x + rad > loc.x + r1)|| + (position.y - rad < loc.y - r1)||(position.y + rad > loc.y + r1)|| + (position.z - rad < loc.z - r1)||(position.z + rad > loc.z + r1)) + return NO; + + return YES; +} + +BOOL positionIsWithinBorders( Vector position, CollisionRegion* region) +{ + if (!region) + return NO; + + if (region->isUniverse) + return YES; + + Vector loc = region->location; + GLfloat r1 = region->radius + region->border_radius; + + if ((position.x < loc.x - r1)||(position.x > loc.x + r1)|| + (position.y < loc.y - r1)||(position.y > loc.y + r1)|| + (position.z < loc.z - r1)||(position.z > loc.z + r1)) + return NO; + + return YES; +} + +BOOL positionIsOnBorders( Vector position, CollisionRegion* region) +{ + if (!region) + return NO; + + if (region->isUniverse) + return NO; + + Vector loc = region->location; + GLfloat r2 = region->radius + region->border_radius; + + if ((position.x < loc.x - r2)||(position.x > loc.x + r2)|| + (position.y < loc.y - r2)||(position.y > loc.y + r2)|| + (position.z < loc.z - r2)||(position.z > loc.z + r2)) + return NO; + + return (!positionIsWithinRegion( position, region)); +} + +NSArray* subregionsContainingPosition( Vector position, CollisionRegion* region) +{ + NSArray* subs = region->subregions; + NSMutableArray* result = [NSMutableArray array]; // autoreleased + + if (!subs) + return result; // empty array + + int i; + int n_subs = [subs count]; + for (i = 0; i < n_subs; i++) + if (positionIsWithinBorders( position, (CollisionRegion*)[subs objectAtIndex: i])) + [result addObject: [subs objectAtIndex: i]]; + return result; +} + + +// collision checking +// +- (void) clearEntityList +{ + n_entities = 0; + int i; + int n_subs = [subregions count]; + for (i = 0; i < n_subs; i++) + [(CollisionRegion*)[subregions objectAtIndex: i] clearEntityList]; + isPlayerInRegion = NO; +} + +- (void) addEntity:(Entity*) ent +{ + // expand if necessary + // + if (n_entities == max_entities) + { + max_entities = 1 + max_entities * 2; + Entity** new_store = (Entity**) malloc( max_entities * sizeof(Entity*)); + int i; + for (i = 0; i < n_entities; i++) + new_store[i] = entity_array[i]; + free( (void*)entity_array); + entity_array = new_store; + } + + isPlayerInRegion |= (ent->isPlayer); + entity_array[n_entities++] = ent; +} + +- (BOOL) checkEntity:(Entity*) ent +{ + Vector position = ent->position; + + // check subregions + BOOL foundRegion = NO; + int n_subs = [subregions count]; + int i; + for (i = 0; i < n_subs; i++) + { + CollisionRegion* sub = (CollisionRegion*)[subregions objectAtIndex:i]; + if (positionIsWithinBorders( position, sub)) + foundRegion |= [sub checkEntity:ent]; + } + if (foundRegion) + return YES; // it's in a subregion so no further action is neccesary + + if (!positionIsWithinBorders( position, self)) + return NO; + + [self addEntity: ent]; + [ent setCollisionRegion: self]; + return YES; +} + + +- (void) findCollisions +{ + // + // According to Shark, when this was in Universe this was where Oolite spent most time! + // + Entity *e1,*e2; + Vector p1, p2; + double dist2, r1, r2, r0, min_dist2; + int i; + Entity* entities_to_test[n_entities]; + // + + // reject trivial cases + // + if (n_entities < 2) + return; + + // only check unfiltered entities + int n_entities_to_test = 0; + for (i = 0; i < n_entities; i++) + { + e1 = entity_array[i]; + if (!(e1->collisionTestFilter)) + entities_to_test[n_entities_to_test++] = e1; + } + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_COLLISIONS) + OOLog(@"collisionRegion.debug", @"DEBUG in collision region %@ testing %d out of %d entities", self, n_entities_to_test, n_entities); +#endif + + if (n_entities_to_test < 2) + return; + + // clear collision variables + // + for (i = 0; i < n_entities_to_test; i++) + { + e1 = entities_to_test[i]; + if (e1->hasCollided) + [[e1 collisionArray] removeAllObjects]; + e1->hasCollided = NO; + if (e1->isShip) + [(ShipEntity*)e1 setProximity_alert:nil]; + e1->collider = nil; + } + + // test for collisions in each subregion + // + int n_subs = [subregions count]; + for (i = 0; i < n_subs; i++) + [(CollisionRegion*)[subregions objectAtIndex: i] findCollisions]; + // + + checks_this_tick = 0; + + // test each entity in this region against the entities in its collision chain + // + for (i = 0; i < n_entities_to_test; i++) + { + e1 = entities_to_test[i]; + p1 = e1->position; + r1 = e1->collision_radius; + + // check against the first in the collision chain + e2 = e1->collision_chain; + while (e2 != nil) + { +#ifndef NDEBUG + if (gDebugFlags & DEBUG_COLLISIONS) + { + OOLog(@"collisionRegion.debug", @"DEBUG Testing collision between %@ (%@) and %@ (%@)", + e1, (e1->collisionTestFilter)?@"YES":@"NO", e2, (e2->collisionTestFilter)?@"YES":@"NO"); + } +#endif + + checks_this_tick++; + + p2 = e2->position; + r2 = e2->collision_radius; + r0 = r1 + r2; + p2.x -= p1.x; p2.y -= p1.y; p2.z -= p1.z; + dist2 = p2.x*p2.x + p2.y*p2.y + p2.z*p2.z; + min_dist2 = r0 * r0; + if (dist2 < PROXIMITY_WARN_DISTANCE2 * min_dist2) + { + if ((e1->isShip) && (e2->isShip)) + { + if ((dist2 < PROXIMITY_WARN_DISTANCE2 * r2 * r2) || (dist2 < PROXIMITY_WARN_DISTANCE2 * r1 * r1)) + { + [(ShipEntity*)e1 setProximity_alert:(ShipEntity*)e2]; + [(ShipEntity*)e2 setProximity_alert:(ShipEntity*)e1]; + } + } + if (dist2 < min_dist2) + { + BOOL collision = NO; + + if (e1->isStation) + { + StationEntity* se1 = (StationEntity*) e1; + if ([se1 shipIsInDockingCorridor: (ShipEntity*)e2]) + collision = NO; + else + collision = [e1 checkCloseCollisionWith: e2]; + } + else if (e2->isStation) + { + StationEntity* se2 = (StationEntity*) e2; + if ([se2 shipIsInDockingCorridor: (ShipEntity*)e1]) + collision = NO; + else + collision = [e2 checkCloseCollisionWith: e1]; + } + else + collision = [e1 checkCloseCollisionWith: e2]; + + if (collision) + { + // now we have no need to check the e2-e1 collision + if (e1->collider) + [[e1 collisionArray] addObject:e1->collider]; + else + [[e1 collisionArray] addObject:e2]; + e1->hasCollided = YES; + // + if (e2->collider) + [[e2 collisionArray] addObject:e2->collider]; + else + [[e2 collisionArray] addObject:e1]; + e2->hasCollided = YES; + } + } + } + // check the next in the collision chain + e2 = e2->collision_chain; + } + } +} + +BOOL testEntityOccludedByEntity(Entity* e1, Entity* e2, PlanetEntity* the_sun) +{ + // simple tests + if (e1 == e2) + return NO; // you can't shade self + // + if (e2 == the_sun) + return NO; // sun can't shade itself + // + if ((e2->isShip == NO)&&(e2->isPlanet == NO)) + return NO; // only ships and planets shade + // + if (e2->collision_radius < e1->collision_radius) + return NO; // smaller can't shade bigger + // + if (e2->isSunlit == NO) + return NO; // things already /in/ shade can't shade things more. + // + // check projected sizes of discs + GLfloat d2_sun = distance2( e1->position, the_sun->position); + GLfloat d2_e2sun = distance2( e2->position, the_sun->position); + if (d2_e2sun > d2_sun) + return NO; // you are nearer the sun than the potential occluder, so it can't shade you + // + GLfloat d2_e2 = distance2( e1->position, e2->position); + GLfloat cr_sun = the_sun->collision_radius; + GLfloat cr_e2 = e2->collision_radius; + if (e2->isShip) + cr_e2 *= 0.90f; // 10% smaller shadow for ships + if (e2->isPlanet) + cr_e2 = e2->collision_radius; // use collision radius for planets + // + GLfloat cr2_sun_scaled = cr_sun * cr_sun * d2_e2 / d2_sun; + if (cr_e2 * cr_e2 < cr2_sun_scaled) + return NO; // if solar disc projected to the distance of e2 > collision radius it can't be shaded by e2 + // + // check angles subtended by sun and occluder + double theta_sun = asin( cr_sun / sqrt(d2_sun)); // 1/2 angle subtended by sun + double theta_e2 = asin( cr_e2 / sqrt(d2_e2)); // 1/2 angle subtended by e2 + Vector p_sun = the_sun->position; + Vector p_e2 = e2->position; + Vector p_e1 = e1->position; + Vector v_sun = make_vector( p_sun.x - p_e1.x, p_sun.y - p_e1.y, p_sun.z - p_e1.z); + if (v_sun.x||v_sun.y||v_sun.z) + v_sun = vector_normal(v_sun); + else + v_sun.z = 1.0f; + // + Vector v_e2 = make_vector( p_e2.x - p_e1.x, p_e2.y - p_e1.y, p_e2.z - p_e1.z); + if (v_e2.x||v_e2.y||v_e2.z) + v_e2 = vector_normal(v_e2); + else + v_e2.x = 1.0f; + double phi = acos( dot_product( v_sun, v_e2)); // angle between sun and e2 from e1's viewpoint + // + if (theta_sun + phi > theta_e2) + return NO; // sun is not occluded + // + // all tests done e1 is in shade! + // + return YES; +} + +- (void) findShadowedEntities +{ + // + // Copy/pasting the collision code to detect occlusion! + // + Entity* e1; + int i,j; + + if ([UNIVERSE reducedDetail]) return; // don't do this in reduced detail mode + + PlanetEntity* the_sun = [UNIVERSE sun]; + + if (!the_sun) + return; // sun is required + + // + // get a list of planet entities because they can shade across regions + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + Entity* planets[ent_count]; + int n_planets = 0; + for (i = 0; i < ent_count; i++) + if ((uni_entities[i]->isPlanet)&&(uni_entities[i] != the_sun)) + planets[n_planets++] = uni_entities[i]; // don't bother retaining - nothing will happen to them! + // + + // reject trivial cases + // + if (n_entities < 2) + return; + + // test for shadows in each subregion + // + int n_subs = [subregions count]; + for (i = 0; i < n_subs; i++) + [[subregions objectAtIndex: i] findShadowedEntities]; + // + + // test each entity in this region against the others + // + for (i = 0; i < n_entities; i++) + { + e1 = entity_array[i]; + BOOL occluder_moved = NO; + if ([e1 status] == STATUS_COCKPIT_DISPLAY) + { + e1->isSunlit = YES; + e1->shadingEntityID = NO_TARGET; + continue; // don't check shading in demo mode + } + if (e1->isSunlit == NO) + { + Entity* occluder = [UNIVERSE entityForUniversalID:e1->shadingEntityID]; + if (occluder) + occluder_moved = occluder->hasMoved; + } + if (((e1->isShip)||(e1->isPlanet))&&((e1->hasMoved)||occluder_moved)) + { + e1->isSunlit = YES; // sunlit by default + e1->shadingEntityID = NO_TARGET; + // + // check demo mode here.. + if ((e1->isPlayer)&&([(PlayerEntity*)e1 showDemoShips])) + continue; // don't check shading in demo mode + // + // test planets + // + for (j = 0; j < n_planets; j++) + { + if (testEntityOccludedByEntity(e1, planets[j], the_sun)) + { + e1->isSunlit = NO; + e1->shadingEntityID = [planets[j] universalID]; + } + } + // + // test local entities + // + for (j = i + 1; j < n_entities; j++) + { + if (testEntityOccludedByEntity(e1, entity_array[j], the_sun)) + { + e1->isSunlit = NO; + e1->shadingEntityID = [entity_array[j] universalID]; + } + } + } + } +} + +- (NSString*) debugOut +{ + int i; + int n_subs = [subregions count]; + NSMutableString* result = [[NSMutableString alloc] initWithFormat:@"%d:", n_entities]; + for (i = 0; i < n_subs; i++) + [result appendString:[(CollisionRegion*)[subregions objectAtIndex:i] debugOut]]; + return [result autorelease]; +} + +@end diff --git a/src/Core/Debug/OODebugMonitor.h b/src/Core/Debug/OODebugMonitor.h new file mode 100644 index 00000000..097699d2 --- /dev/null +++ b/src/Core/Debug/OODebugMonitor.h @@ -0,0 +1,104 @@ +/* + +OODebugMonitor.h + +Debugging services object for Oolite. + +The debug controller implements Oolite's part of debugging support. It can +connect to one debugger object, which conforms to the OODebuggerInterface +formal protocol. This can either be (part of) a debugger loaded into Oolite +itself (as in the Mac Debug OXP), or provide communications with an external +debugger (for instance, over Distributed Objects or TCP/IP). + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOWeakReference.h" +#import "OODebuggerInterface.h" + +@class OOScript; + + +@protocol OODebugMonitorInterface + +// Note: disconnectDebugger:message: will cause a disconnectDebugMonitor:message: message to be sent to the debugger. The debugger should not send disconnectDebugger:message: in response to disconnectDebugMonitor:message:. +- (void)disconnectDebugger:(in id)debugger + message:(in NSString *)message; + + +// *** JavaScript console support. + +// Perform a JS command as though entered at the console, including echoing. +- (oneway void)performJSConsoleCommand:(in NSString *)command; + +- (id)configurationValueForKey:(in NSString *)key; +- (void)setConfigurationValue:(in id)value forKey:(in NSString *)key; + +- (NSString *)sourceCodeForFile:(in NSString *)filePath line:(in unsigned)line; + +@end + + +@interface OODebugMonitor: OOWeakRefObject +{ + id _debugger; + + // JavaScript console support. + OOScript *_script; + struct JSObject *_jsSelf; + + NSDictionary *_configFromOXPs; // Settings from debugConfig.plist + NSMutableDictionary *_configOverrides; // Settings from preferences, modifiable through JS. + + // Caches + NSMutableDictionary *_fgColors, + *_bgColors, + *_sourceFiles; +} + ++ (id)sharedDebugMonitor; +- (BOOL)setDebugger:(id)debugger; + + // *** JavaScript console support. +- (void)appendJSConsoleLine:(id)string + colorKey:(NSString *)colorKey + emphasisRange:(NSRange)emphasisRange; + +- (void)appendJSConsoleLine:(id)string + colorKey:(NSString *)colorKey; + +- (void)clearJSConsole; +- (void)showJSConsole; + +- (id)configurationValueForKey:(NSString *)key class:(Class)class defaultValue:(id)value; +- (long long)configurationIntValueForKey:(NSString *)key defaultValue:(long long)value; + +- (NSArray *)configurationKeys; + +- (BOOL) debuggerConnected; + +@end + diff --git a/src/Core/Debug/OODebugMonitor.m b/src/Core/Debug/OODebugMonitor.m new file mode 100644 index 00000000..d8eda155 --- /dev/null +++ b/src/Core/Debug/OODebugMonitor.m @@ -0,0 +1,622 @@ +/* + +OODebugMonitor.m + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + + +#import "OODebugMonitor.h" +#import "OOCollectionExtractors.h" +#import "OOLogging.h" +#import "ResourceManager.h" +#import "NSStringOOExtensions.h" + +#import "OOJSConsole.h" +#import "OOScript.h" +#import "OOJSScript.h" +#import "OOJavaScriptEngine.h" +#import "OOJSSpecialFunctions.h" + + +static OODebugMonitor *sSingleton = nil; + + +@interface OODebugMonitor (Private) + +- (void)disconnectDebuggerWithMessage:(NSString *)message; + +- (NSDictionary *)mergedConfiguration; + +/* Convert a configuration dictionary to a standard form. In particular, + convert all colour specifiers to RGBA arrays with values in [0, 1], and + converts "show-console" values to booleans. +*/ +- (NSMutableDictionary *)normalizeConfigDictionary:(NSDictionary *)dictionary; +- (id)normalizeConfigValue:(id)value forKey:(NSString *)key; + +- (NSArray *)loadSourceFile:(NSString *)filePath; + +@end + + +@implementation OODebugMonitor + +- (id)init +{ + NSUserDefaults *defaults = nil; + NSDictionary *jsProps = nil; + NSDictionary *config = nil; +#if OOLITE_GNUSTEP + NSString *NSApplicationWillTerminateNotification = @"ApplicationWillTerminate"; +#endif + + self = [super init]; + if (self != nil) + { + config = [[[ResourceManager dictionaryFromFilesNamed:@"debugConfig.plist" + inFolder:@"Config" + andMerge:YES] mutableCopy] autorelease]; + _configFromOXPs = [[self normalizeConfigDictionary:config] copy]; + + defaults = [NSUserDefaults standardUserDefaults]; + config = [defaults dictionaryForKey:@"debug-settings-override"]; + config = [self normalizeConfigDictionary:config]; + if (config == nil) config = [NSMutableDictionary dictionary]; + _configOverrides = [config retain]; + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; + +#if OOJSENGINE_MONITOR_SUPPORT + [[OOJavaScriptEngine sharedEngine] setMonitor:self]; +#endif + + // Set up JavaScript side of console. + jsProps = [NSDictionary dictionaryWithObjectsAndKeys: + self, @"console", + JSSpecialFunctionsObjectWrapper(NULL), @"special", + nil]; + _script = [[OOScript nonLegacyScriptFromFileNamed:@"oolite-debug-console.js" properties:jsProps] retain]; + } + + return self; +} + + +- (void)dealloc +{ + [self disconnectDebuggerWithMessage:@"Debug controller object destroyed while debugging in progress."]; + + [_configFromOXPs release]; + [_configOverrides release]; + + [_fgColors release]; + [_bgColors release]; + [_sourceFiles release]; + + if (_jsSelf != NULL) + { + [[OOJavaScriptEngine sharedEngine] removeGCRoot:&_jsSelf]; + } + + [super dealloc]; +} + + ++ (id)sharedDebugMonitor +{ + // NOTE: assumes single-threaded access. The debug monitor is not, on the whole, thread safe. + if (sSingleton == nil) + { + [[self alloc] init]; + } + + return sSingleton; +} + + +- (BOOL)setDebugger:(id)newDebugger +{ + NSString *error = nil; + + if (newDebugger != _debugger) + { + // Disconnect existing debugger, if any. + if (newDebugger != nil) + { + [self disconnectDebuggerWithMessage:@"New debugger set."]; + } + else + { + [self disconnectDebuggerWithMessage:@"Debugger disconnected programatically."]; + } + + // If a new debugger was specified, try to connect it. + if (newDebugger != nil) + { + NS_DURING + if ([newDebugger connectDebugMonitor:self errorMessage:&error]) + { + [newDebugger debugMonitor:self + noteConfiguration:[self mergedConfiguration]]; + _debugger = [newDebugger retain]; + } + else + { + OOLog(@"debugMonitor.setDebugger.failed", @"Could not connect to debugger %@, because an error occurred: %@", newDebugger, error); + } + NS_HANDLER + OOLog(@"debugMonitor.setDebugger.failed", @"Could not connect to debugger %@, because an exception occurred: %@ -- %@", newDebugger, [localException name], [localException reason]); + NS_ENDHANDLER + } + } + + return _debugger == newDebugger; +} + + +- (oneway void)performJSConsoleCommand:(in NSString *)command +{ + [_script doEvent:@"consolePerformJSCommand" withArgument:command]; +} + + +- (void)appendJSConsoleLine:(id)string + colorKey:(NSString *)colorKey + emphasisRange:(NSRange)emphasisRange +{ + if (string == nil) return; + NS_DURING + [_debugger debugMonitor:self + jsConsoleOutput:string + colorKey:colorKey + emphasisRange:emphasisRange]; + NS_HANDLER + OOLog(@"debugMonitor.debuggerConnection.exception", @"Exception while attempting to send JavaScript console text to debugger: %@ -- %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + + +- (void)appendJSConsoleLine:(id)string + colorKey:(NSString *)colorKey +{ + [self appendJSConsoleLine:string + colorKey:colorKey + emphasisRange:NSMakeRange(0, 0)]; +} + + +- (void)clearJSConsole +{ + NS_DURING + [_debugger debugMonitorClearConsole:self]; + NS_HANDLER + OOLog(@"debugMonitor.debuggerConnection.exception", @"Exception while attempting to clear JavaScript console: %@ -- %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + + +- (void)showJSConsole +{ + NS_DURING + [_debugger debugMonitorShowConsole:self]; + NS_HANDLER + OOLog(@"debugMonitor.debuggerConnection.exception", @"Exception while attempting to show JavaScript console: %@ -- %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + + +- (id)configurationValueForKey:(in NSString *)key +{ + return [self configurationValueForKey:key class:Nil defaultValue:nil]; +} + + +- (id)configurationValueForKey:(NSString *)key class:(Class)class defaultValue:(id)value +{ + id result = nil; + + if (class == Nil) class = [NSObject class]; + + result = [_configOverrides objectForKey:key]; + if (![result isKindOfClass:class] && result != [NSNull null]) result = [_configFromOXPs objectForKey:key]; + if (![result isKindOfClass:class] && result != [NSNull null]) result = [[value retain] autorelease]; + if (result == [NSNull null]) result = nil; + + return result; +} + + +- (long long)configurationIntValueForKey:(NSString *)key defaultValue:(long long)value +{ + long long result; + id object = nil; + + object = [self configurationValueForKey:key]; + if ([object respondsToSelector:@selector(longLongValue)]) result = [object longLongValue]; + else if ([object respondsToSelector:@selector(intValue)]) result = [object intValue]; + else result = value; + + return result; +} + + +- (void)setConfigurationValue:(in id)value forKey:(in NSString *)key +{ + if (key == nil) return; + + value = [self normalizeConfigValue:value forKey:key]; + + if (value == nil) + { + [_configOverrides removeObjectForKey:key]; + } + else + { + if (_configOverrides == nil) _configOverrides = [[NSMutableDictionary alloc] init]; + [_configOverrides setObject:value forKey:key]; + } + + // Send changed value to debugger + if (value == nil) + { + // Setting a nil value removes an override, and may reveal an underlying OXP-defined value + value = [self configurationValueForKey:key]; + } + NS_DURING + [_debugger debugMonitor:self + noteChangedConfigrationValue:value + forKey:key]; + NS_HANDLER + OOLog(@"debugMonitor.debuggerConnection.exception", @"Exception while attempting to send configuration update to debugger: %@ -- %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + + +- (NSArray *)configurationKeys +{ + NSMutableSet *result = nil; + + result = [NSMutableSet setWithCapacity:[_configFromOXPs count] + [_configOverrides count]]; + [result addObjectsFromArray:[_configFromOXPs allKeys]]; + [result addObjectsFromArray:[_configOverrides allKeys]]; + + return [[result allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; +} + + +- (BOOL) debuggerConnected +{ + return _debugger != nil; +} + + +- (NSString *)sourceCodeForFile:(in NSString *)filePath line:(in unsigned)line +{ + id linesForFile = nil; + + linesForFile = [_sourceFiles objectForKey:filePath]; + + if (linesForFile == nil) + { + linesForFile = [self loadSourceFile:filePath]; + if (linesForFile == nil) linesForFile = [NSArray arrayWithObject:[NSString stringWithFormat:@"", filePath]]; + + if (_sourceFiles == nil) _sourceFiles = [[NSMutableDictionary alloc] init]; + [_sourceFiles setObject:linesForFile forKey:filePath]; + } + + if ([linesForFile count] < line || line == 0) return @""; + + return [linesForFile objectAtIndex:line - 1]; +} + + +- (void)disconnectDebugger:(in id)debugger + message:(in NSString *)message +{ + if (debugger == nil) return; + + if (debugger == _debugger) + { + [self disconnectDebuggerWithMessage:message]; + } + else + { + OOLog(@"debugMonitor.disconnect.ignored", @"Attempt to disconnect debugger %@, which is not current debugger; ignoring.", debugger); + } +} + + +- (void)applicationWillTerminate:(NSNotification *)notification +{ + if (_configOverrides != nil) + { + [[NSUserDefaults standardUserDefaults] setObject:_configOverrides forKey:@"debug-settings-override"]; + } + + [self disconnectDebuggerWithMessage:@"Oolite is terminating."]; +} + + +@end + + +@implementation OODebugMonitor (Private) + +- (void)disconnectDebuggerWithMessage:(NSString *)message +{ + NS_DURING + [_debugger disconnectDebugMonitor:self message:message]; + NS_HANDLER + OOLog(@"debugMonitor.debuggerConnection.exception", @"Exception while attempting to disconnect debugger: %@ -- %@", [localException name], [localException reason]); + NS_ENDHANDLER + + id debugger = _debugger; + _debugger = nil; + [debugger release]; +} + + +- (NSDictionary *)mergedConfiguration +{ + NSMutableDictionary *result = nil; + + result = [NSMutableDictionary dictionary]; + if (_configFromOXPs != nil) [result addEntriesFromDictionary:_configFromOXPs]; + if (_configOverrides != nil) [result addEntriesFromDictionary:_configOverrides]; + + return result; +} + + +- (NSArray *)loadSourceFile:(NSString *)filePath +{ + NSString *contents = nil; + NSArray *lines = nil; + + if (filePath == nil) return nil; + + contents = [NSString stringWithContentsOfUnicodeFile:filePath]; + if (contents == nil) return nil; + + /* Extract lines from file. +FIXME: this works with CRLF and LF, but not CR. + */ + lines = [contents componentsSeparatedByString:@"\n"]; + return lines; +} + + +- (NSMutableDictionary *)normalizeConfigDictionary:(NSDictionary *)dictionary +{ + NSMutableDictionary *result = nil; + NSEnumerator *keyEnum = nil; + NSString *key = nil; + id value = nil; + + result = [NSMutableDictionary dictionaryWithCapacity:[dictionary count]]; + for (keyEnum = [dictionary keyEnumerator]; (key = [keyEnum nextObject]); ) + { + value = [dictionary objectForKey:key]; + value = [self normalizeConfigValue:value forKey:key]; + + if (key != nil && value != nil) [result setObject:value forKey:key]; + } + + return result; +} + + +- (id)normalizeConfigValue:(id)value forKey:(NSString *)key +{ + OOColor *color = nil; + BOOL boolValue; + + if (value != nil) + { + if ([key hasSuffix:@"-color"] || [key hasSuffix:@"-colour"]) + { + color = [OOColor colorWithDescription:value]; + value = [color normalizedArray]; + } + else if ([key hasPrefix:@"show-console"]) + { + boolValue = OOBooleanFromObject(value, NO); + value = [NSNumber numberWithBool:boolValue]; + } + } + + return value; +} + + +- (oneway void)jsEngine:(in byref OOJavaScriptEngine *)engine + context:(in JSContext *)context + error:(in JSErrorReport *)errorReport + stackSkip:(unsigned)stackSkip + withMessage:(in NSString *)message +{ + NSString *colorKey = nil; + NSString *prefix = nil; + NSString *filePath = nil; + NSString *sourceLine = nil; + NSString *scriptLine = nil; + NSMutableString *formattedMessage = nil; + NSRange emphasisRange; + NSString *showKey = nil; + + if (_debugger == nil) return; + + if (errorReport->flags & JSREPORT_WARNING) + { + colorKey = @"warning"; + prefix = @"Warning"; + } + else if (errorReport->flags & JSREPORT_EXCEPTION) + { + colorKey = @"exception"; + prefix = @"Exception"; + } + else + { + colorKey = @"error"; + prefix = @"Error"; + } + + if (errorReport->flags & JSREPORT_STRICT) + { + prefix = [prefix stringByAppendingString:@" (strict mode)"]; + } + + // Prefix and subsequent colon should be bold: + emphasisRange = NSMakeRange(0, [prefix length] + 1); + + formattedMessage = [NSMutableString stringWithFormat:@"%@: %@", prefix, message]; + + // Note that the "active script" isn't necessarily the one causing the + // error, since one script can call another's methods. + scriptLine = [[OOJSScript currentlyRunningScript] displayName]; + if (scriptLine != nil) + { + [formattedMessage appendFormat:@"\n Active script: %@", scriptLine]; + } + + if (stackSkip == 0) + { + // Append file name and line + if (errorReport->filename != NULL) filePath = [NSString stringWithUTF8String:errorReport->filename]; + if ([filePath length] != 0) + { + [formattedMessage appendFormat:@"\n %@, line %u", [filePath lastPathComponent], errorReport->lineno]; + + // Append source code + sourceLine = [self sourceCodeForFile:filePath line:errorReport->lineno]; + if (sourceLine != nil) + { + [formattedMessage appendFormat:@":\n %@", sourceLine]; + } + } + } + + [self appendJSConsoleLine:formattedMessage + colorKey:colorKey + emphasisRange:emphasisRange]; + + if (errorReport->flags & JSREPORT_WARNING) showKey = @"show-console-on-warning"; + else showKey = @"show-console-on-warning"; + if (OOBooleanFromObject([self configurationValueForKey:showKey], NO)) + { + [self showJSConsole]; + } +} + + +- (oneway void)jsEngine:(in byref OOJavaScriptEngine *)engine + context:(in JSContext *)context + logMessage:(in NSString *)message + ofClass:(in NSString *)messageClass +{ + [self appendJSConsoleLine:message colorKey:@"log"]; + if (OOBooleanFromObject([self configurationValueForKey:@"show-console-on-log"], NO)) + { + [self showJSConsole]; + } +} + + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + if (_jsSelf == NULL) + { + _jsSelf = DebugMonitorToJSConsole(context, self); + if (_jsSelf != NULL) + { + if (!JS_AddNamedRoot(context, &_jsSelf, "debug console")) + { + _jsSelf = NULL; + } + } + } + + if (_jsSelf != NULL) return OBJECT_TO_JSVAL(_jsSelf); + else return JSVAL_NULL; +} + +@end + + +@implementation OODebugMonitor (Singleton) + +/* Canonical singleton boilerplate. +See Cocoa Fundamentals Guide: Creating a Singleton Instance. +See also +sharedDebugMonitor above. + +NOTE: assumes single-threaded access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end + +#endif /* OO_EXCLUDE_DEBUG_SUPPORT */ diff --git a/src/Core/Debug/OODebugSupport.h b/src/Core/Debug/OODebugSupport.h new file mode 100644 index 00000000..a41bb520 --- /dev/null +++ b/src/Core/Debug/OODebugSupport.h @@ -0,0 +1,59 @@ +/* + +OODebugSupport.h + +Set up debug support. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + +void OOInitDebugSupport(void); + +#else + +#define OOInitDebugSupport() do {} while (0) + +#endif diff --git a/src/Core/Debug/OODebugSupport.m b/src/Core/Debug/OODebugSupport.m new file mode 100644 index 00000000..6c02c3b7 --- /dev/null +++ b/src/Core/Debug/OODebugSupport.m @@ -0,0 +1,155 @@ +/* + +OODebugSupport.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + + +#import "OODebugSupport.h" +#import "ResourceManager.h" +#import "OOCollectionExtractors.h" +#import "OODebugMonitor.h" +#import "OODebugTCPConsoleClient.h" + +#if OOLITE_MAC_OS_X +static id LoadDebugPlugIn(NSString *path); +#else +#define LoadDebugPlugIn(path) nil +#endif + + +@interface NSObject (OODebugPlugInController) + +- (id) setUpDebugger; + +@end + + +void OOInitDebugSupport(void) +{ + NSString *debugOXPPath = nil; + id plugInController = nil; + NSDictionary *debugSettings = nil; + NSString *consoleHost = nil; + unsigned short consolePort = 0; + id debugger = nil; + + // Check that the debug OXP is installed. If not, we don't enable debug support. + debugOXPPath = [ResourceManager pathForFileNamed:@"DebugOXPLocatorBeacon.magic" inFolder:@"nil"]; + if (debugOXPPath != nil) + { + // Load plug-in debugging code on platforms where this is supported. + plugInController = LoadDebugPlugIn(debugOXPPath); + + // Load debug settings. + debugSettings = [ResourceManager dictionaryFromFilesNamed:@"debugConfig.plist" + inFolder:@"Config" + mergeMode:MERGE_BASIC + cache:NO]; + + consoleHost = [debugSettings stringForKey:@"console-host"]; + consolePort = [debugSettings unsignedShortForKey:@"console-port"]; + + // If consoleHost is nil, and the debug plug-in can set up a debugger, use that. + if (consoleHost == nil && [plugInController respondsToSelector:@selector(setUpDebugger)]) + { + debugger = [plugInController setUpDebugger]; + } + + // Otherwise, use TCP debugger connection. + if (debugger == nil) + { + debugger = [[OODebugTCPConsoleClient alloc] initWithAddress:consoleHost + port:consolePort]; + [debugger autorelease]; + } + + // Set up monitor and register debugger, if any. + if (debugger != nil) + { + [[OODebugMonitor sharedDebugMonitor] setDebugger:debugger]; + } + } +} + + +#if OOLITE_MAC_OS_X + +// Note: it should in principle be possible to use this code to load a plug-in under GNUstep, too. +static id LoadDebugPlugIn(NSString *path) +{ + Class principalClass = Nil; + NSString *bundlePath = nil; + NSBundle *bundle = nil; + id debugController = nil; + + bundlePath = [path stringByDeletingLastPathComponent]; + bundle = [NSBundle bundleWithPath:bundlePath]; + if ([bundle load]) + { + principalClass = [bundle principalClass]; + if (principalClass != Nil) + { + // Instantiate principal class of debug bundle, and let it do whatever it wants. + debugController = [[principalClass alloc] init]; + } + else + { + OOLog(@"debugOXP.load.failed", @"Failed to find principal class of debug bundle."); + } + } + else + { + OOLog(@"debugOXP.load.failed", @"Failed to load debug OXP plug-in from %@.", bundlePath); + } + + return debugController; +} + +#endif + +#endif /* OO_EXCLUDE_DEBUG_SUPPORT */ diff --git a/src/Core/Debug/OODebugTCPConsoleClient.h b/src/Core/Debug/OODebugTCPConsoleClient.h new file mode 100644 index 00000000..03b8fa48 --- /dev/null +++ b/src/Core/Debug/OODebugTCPConsoleClient.h @@ -0,0 +1,60 @@ +/* + +OODebugTCPConsoleClient.h + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OODebuggerInterface.h" + +@class OODebugMonitor; + + +typedef enum +{ + kOOTCPClientNotConnected, + kOOTCPClientStartedConnectionStage1, + kOOTCPClientStartedConnectionStage2, + kOOTCPClientConnected, + kOOTCPClientConnectionRefused, + kOOTCPClientDisconnected +} OOTCPClientConnectionStatus; + + +@interface OODebugTCPConsoleClient: NSObject +{ + NSHost *_host; + NSOutputStream *_outStream; + NSInputStream *_inStream; + OOTCPClientConnectionStatus _status; + OODebugMonitor *_monitor; + struct OOTCPStreamDecoder *_decoder; +} + +- (id) initWithAddress:(NSString *)address // Pass nil for localhost + port:(uint16_t)port; // Pass 0 for default port + +@end diff --git a/src/Core/Debug/OODebugTCPConsoleClient.m b/src/Core/Debug/OODebugTCPConsoleClient.m new file mode 100644 index 00000000..ef444ed2 --- /dev/null +++ b/src/Core/Debug/OODebugTCPConsoleClient.m @@ -0,0 +1,710 @@ +/* + +OODebugTCPConsoleClient.m + + +Oolite Debug OXP + +Copyright (C) 2009 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + + +#import "OODebugTCPConsoleClient.h" +#import "OODebugTCPConsoleProtocol.h" +#import "OODebugMonitor.h" +#import "OOFunctionAttributes.h" +#import "OOLogging.h" +#import + +#if OOLITE_WINDOWS +#import +#else +#import // For htonl +#endif + +#import "OOCollectionExtractors.h" +#import "OOTCPStreamDecoder.h" + + +#ifdef OO_LOG_DEBUG_PROTOCOL_PACKETS +static void LogSendPacket(NSDictionary *packet); +#else +#define LogSendPacket(packet) do {} while (0) +#endif + + +static void DecoderPacket(void *cbInfo, OOALStringRef packetType, OOALDictionaryRef packet); +static void DecoderError(void *cbInfo, OOALStringRef errorDesc); + + +OOINLINE BOOL StatusIsSendable(OOTCPClientConnectionStatus status) +{ + return status == kOOTCPClientStartedConnectionStage1 || status == kOOTCPClientStartedConnectionStage2 || status == kOOTCPClientConnected; +} + + +@interface OODebugTCPConsoleClient (OOPrivate) + +- (void) closeConnection; + +- (BOOL) sendBytes:(const void *)bytes count:(size_t)count; +- (void) sendDictionary:(NSDictionary *)dictionary; + +- (void) sendPacket:(NSString *)packetType + withParameters:(NSDictionary *)parameters; + +- (void) sendPacket:(NSString *)packetType + withValue:(id)value + forParameter:(NSString *)paramKey; + +- (void) readData; +- (void) dispatchPacket:(NSDictionary *)packet ofType:(NSString *)packetType; + +- (void) handleApproveConnectionPacket:(NSDictionary *)packet; +- (void) handleRejectConnectionPacket:(NSDictionary *)packet; +- (void) handleCloseConnectionPacket:(NSDictionary *)packet; +- (void) handleNoteConfigurationChangePacket:(NSDictionary *)packet; +- (void) handlePerformCommandPacket:(NSDictionary *)packet; +- (void) handleRequestConfigurationValuePacket:(NSDictionary *)packet; +- (void) handlePingPacket:(NSDictionary *)packet; +- (void) handlePongPacket:(NSDictionary *)packet; + +- (void) disconnectFromServerWithMessage:(NSString *)message; +- (void) breakConnectionWithMessage:(NSString *)message; +- (void) breakConnectionWithBadStream:(NSStream *)stream; + +@end + + +@implementation OODebugTCPConsoleClient + +- (id) init +{ + return [self initWithAddress:nil port:0]; +} + + +- (id) initWithAddress:(NSString *)address port:(uint16_t)port +{ + BOOL OK = NO; + NSDictionary *parameters = nil; + + if (address == nil) address = @"127.0.0.1"; + if (port == 0) port = kOOTCPConsolePort; + + self = [super init]; + if (self != nil) + { + _host = [NSHost hostWithName:address]; + if (_host != nil) + { + [_host retain]; + [NSStream getStreamsToHost:_host + port:port + inputStream:&_inStream + outputStream:&_outStream]; + } + + if (_inStream != nil && _outStream != nil) + { + [_inStream retain]; + [_outStream retain]; + [_inStream setDelegate:self]; + [_outStream setDelegate:self]; + [_inStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [_outStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; + [_inStream open]; + [_outStream open]; + + // Need to wait for the streams to reach open status before we can send packets + // TODO: Might be neater to use the handleEvent callback to flag this.. - Micha 20090425 + NSRunLoop * myRunLoop = [NSRunLoop currentRunLoop]; + NSDate * timeOut = [NSDate dateWithTimeIntervalSinceNow:3]; // Wait up to 3 seconds + while( _host != nil && ([_inStream streamStatus] < 2 || [_outStream streamStatus] < 2) && + [myRunLoop runMode:NSDefaultRunLoopMode beforeDate:timeOut] ) + ; // Wait + + _decoder = OOTCPStreamDecoderCreate(DecoderPacket, DecoderError, NULL, self); + } + + if (_decoder != NULL) + { + OK = YES; + _status = kOOTCPClientStartedConnectionStage1; + + + // Attempt to connect + parameters = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:kOOTCPProtocolVersion_1_1_0], kOOTCPProtocolVersion, + [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"], kOOTCPOoliteVersion, + nil]; + [self sendPacket:kOOTCPPacket_RequestConnection + withParameters:parameters]; + + if (_status == kOOTCPClientStartedConnectionStage1) _status = kOOTCPClientStartedConnectionStage2; + else OK = NO; // Connection failed. + } + + if (!OK) + { + OOLog(@"debugTCP.connect.failed", @"Failed to connect to debug console at %@:%i.", address, port); + [self release]; + self = nil; + } + } + + return self; +} + + +- (void) dealloc +{ + if (StatusIsSendable(_status)) + { + [self disconnectFromServerWithMessage:@"TCP console bridge unexpectedly released while active."]; + } + if (_monitor) + { + [_monitor disconnectDebugger:self message:@"TCP console bridge unexpectedly released while active."]; + } + + + [self closeConnection]; + + OOTCPStreamDecoderDestroy(_decoder); + _decoder = NULL; + + [super dealloc]; +} + + +- (BOOL)connectDebugMonitor:(in OODebugMonitor *)debugMonitor + errorMessage:(out NSString **)message +{ + if (_status == kOOTCPClientConnectionRefused) + { + if (message != NULL) *message = @"Connection refused."; + return NO; + } + if (_status == kOOTCPClientDisconnected) + { + if (message != NULL) *message = @"Cannot reconnect after disconnecting."; + return NO; + } + + _monitor = debugMonitor; + + return YES; +} + + +- (void)disconnectDebugMonitor:(in OODebugMonitor *)debugMonitor + message:(in NSString *)message +{ + [self disconnectFromServerWithMessage:message]; + _monitor = nil; +} + + +- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor + jsConsoleOutput:(in NSString *)output + colorKey:(in NSString *)colorKey + emphasisRange:(in NSRange)emphasisRange +{ + NSMutableDictionary *parameters = nil; + NSArray *range = nil; + + parameters = [NSMutableDictionary dictionaryWithCapacity:3]; + [parameters setObject:output forKey:kOOTCPMessage]; + [parameters setObject:colorKey ? colorKey : (NSString *)@"general" forKey:kOOTCPColorKey]; + if (emphasisRange.length != 0) + { + range = [NSArray arrayWithObjects: + [NSNumber numberWithUnsignedInt:emphasisRange.location], + [NSNumber numberWithUnsignedInt:emphasisRange.length], + nil]; + [parameters setObject:range forKey:kOOTCPEmphasisRanges]; + } + + [self sendPacket:kOOTCPPacket_ConsoleOutput + withParameters:parameters]; +} + + +- (oneway void)debugMonitorClearConsole:(in OODebugMonitor *)debugMonitor +{ + [self sendPacket:kOOTCPPacket_ClearConsole + withParameters:nil]; +} + + +- (oneway void)debugMonitorShowConsole:(in OODebugMonitor *)debugMonitor; +{ + [self sendPacket:kOOTCPPacket_ShowConsole + withParameters:nil]; +} + + +- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor + noteConfiguration:(in NSDictionary *)configuration +{ + [self sendPacket:kOOTCPPacket_NoteConfiguration + withValue:configuration + forParameter:kOOTCPConfiguration]; +} + + +- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor +noteChangedConfigrationValue:(in id)newValue + forKey:(in NSString *)key +{ + if (newValue != nil) + { + [self sendPacket:kOOTCPPacket_NoteConfiguration + withValue:[NSDictionary dictionaryWithObject:newValue forKey:key] + forParameter:kOOTCPConfiguration]; + } + else + { + [self sendPacket:kOOTCPPacket_NoteConfiguration + withValue:[NSArray arrayWithObject:key] + forParameter:kOOTCPRemovedConfigurationKeys]; + } +} + + +- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)eventCode +{ + if (_status > kOOTCPClientConnected) return; + + if (stream == _inStream && eventCode == NSStreamEventHasBytesAvailable) + { + [self readData]; + } + else if (eventCode == NSStreamEventErrorOccurred) + { + [self breakConnectionWithBadStream:stream]; + } + else if (eventCode == NSStreamEventErrorOccurred) + { + [self breakConnectionWithMessage:[NSString stringWithFormat: + @"Console closed the connection."]]; + } +} + +@end + + +@implementation OODebugTCPConsoleClient (OOPrivate) + +- (void) closeConnection +{ + [_inStream close]; + [_inStream setDelegate:nil]; + [_inStream release]; + _inStream = nil; + + [_outStream close]; + [_outStream setDelegate:nil]; + [_outStream release]; + _outStream = nil; + + [_host release]; + _host = nil; +} + + +- (BOOL) sendBytes:(const void *)bytes count:(size_t)count +{ + int written; + + if (bytes == NULL || count == 0) return YES; + if (!StatusIsSendable(_status) || _outStream == nil) return NO; + + do + { + written = [_outStream write:bytes maxLength:count]; + if (written < 1) return NO; + + count -= written; + bytes += written; + } + while (count > 0); + + return YES; +} + + +- (void) sendDictionary:(NSDictionary *)dictionary +{ + NSData *data = nil; + NSString *errorDesc = nil; + size_t count; + const uint8_t *bytes = NULL; + uint32_t header; + + if (dictionary == nil || !StatusIsSendable(_status)) return; + + data = [NSPropertyListSerialization dataFromPropertyList:dictionary + format:NSPropertyListXMLFormat_v1_0 + errorDescription:&errorDesc]; + + if (data == nil) + { + OOLog(@"debugTCP.conversionFailure", @"Could not convert dictionary to data for transmission to debug console: %@", errorDesc ? errorDesc : (NSString *)@"unknown error."); +#if OOLITE_RELEASE_PLIST_ERROR_STRINGS + [errorDesc autorelease]; +#endif + return; + } + + LogSendPacket(dictionary); + + count = [data length]; + if (count == 0) return; + header = htonl(count); + + bytes = [data bytes]; + if (![self sendBytes:&header count:sizeof header] || ![self sendBytes:bytes count:count]) + { + [self breakConnectionWithBadStream:_outStream]; + } +} + + +- (void) sendPacket:(NSString *)packetType + withParameters:(NSDictionary *)parameters +{ + NSMutableDictionary *dict = nil; + + if (packetType == nil) return; + + if (parameters != nil) + { + dict = [parameters mutableCopy]; + [dict setObject:packetType forKey:kOOTCPPacketType]; + } + else + { + dict = [[NSDictionary alloc] initWithObjectsAndKeys:packetType, kOOTCPPacketType, nil]; + } + + [self sendDictionary:dict]; + [dict release]; +} + + +- (void) sendPacket:(NSString *)packetType + withValue:(id)value + forParameter:(NSString *)paramKey +{ + if (packetType == nil) return; + if (paramKey == nil) value = nil; + + [self sendDictionary:[NSDictionary dictionaryWithObjectsAndKeys: + packetType, kOOTCPPacketType, + value, paramKey, + nil]]; +} + + +- (void) readData +{ + enum { kBufferSize = 16 << 10 }; + + uint8_t buffer[kBufferSize]; + int length; + NSData *data; + + length = [_inStream read:buffer maxLength:kBufferSize]; + while( length > 0 ) + { + /* This test is superfluous after the rewrite to fix Bug#014643 + * TODO: Put the BadStream test back into the code + if (length < 1) + { + // Under GNUstep, but not OS X (currently), -hasBytesAvailable will return YES when the buffer is in fact empty. + if ([_inStream streamStatus] == NSStreamStatusReading) break; + + [self breakConnectionWithBadStream:_inStream]; + return; + } + */ + + data = [NSData dataWithBytesNoCopy:buffer length:length freeWhenDone:NO]; + OOTCPStreamDecoderReceiveData(_decoder, data); + length = [_inStream read:buffer maxLength:kBufferSize]; + } +} + + +- (void) dispatchPacket:(NSDictionary *)packet ofType:(NSString *)packetType +{ + if (packet == nil || packetType == nil) return; + +#define PACKET_CASE(x) else if ([packetType isEqualToString:kOOTCPPacket_##x]) { [self handle##x##Packet:packet]; } + + if (0) {} + PACKET_CASE(ApproveConnection) + PACKET_CASE(RejectConnection) + PACKET_CASE(CloseConnection) + PACKET_CASE(NoteConfigurationChange) + PACKET_CASE(PerformCommand) + PACKET_CASE(RequestConfigurationValue) + PACKET_CASE(Ping) + PACKET_CASE(Pong) + else + { + OOLog(@"debugTCP.protocolError.unknownPacketType", @"Unhandled packet type %@.", packetType); + } +} + + +- (void) handleApproveConnectionPacket:(NSDictionary *)packet +{ + NSMutableString *connectedMessage = nil; + NSString *consoleIdentity = nil; + NSString *hostName = nil; + + if (_status == kOOTCPClientStartedConnectionStage2) + { + _status = kOOTCPClientConnected; + + // Build "Connected..." message with two optional parts, console identity and host name. + connectedMessage = [NSMutableString stringWithString:@"Connected to external debug console"]; + + consoleIdentity = [packet stringForKey:kOOTCPConsoleIdentity]; + if (consoleIdentity != nil) [connectedMessage appendFormat:@" \"%@\"", consoleIdentity]; + + hostName = [_host name]; + if ([hostName length] != 0 && + ![hostName isEqual:@"localhost"] && + ![hostName isEqual:@"127.0.0.1"] && + ![hostName isEqual:@"::1"]) + { + [connectedMessage appendFormat:@" at %@", hostName]; + } + + OOLog(@"debugTCP.connected", @"%@.", connectedMessage); + } + else + { + OOLog(@"debugTCP.protocolError.outOfOrder", @"Got %@ packet from debug console in wrong context.", kOOTCPPacket_ApproveConnection); + } +} + + +- (void) handleRejectConnectionPacket:(NSDictionary *)packet +{ + NSString *message = nil; + + if (_status == kOOTCPClientStartedConnectionStage2) + { + _status = kOOTCPClientConnectionRefused; + } + else + { + OOLog(@"debugTCP.protocolError.outOfOrder", @"Got %@ packet from debug console in wrong context.", kOOTCPPacket_RejectConnection); + } + + message = [packet stringForKey:kOOTCPMessage]; + if (message == nil) message = @"Console refused connection."; + [self breakConnectionWithMessage:message]; +} + + +- (void) handleCloseConnectionPacket:(NSDictionary *)packet +{ + NSString *message = nil; + + if (!StatusIsSendable(_status)) + { + OOLog(@"debugTCP.protocolError.outOfOrder", @"Got %@ packet from debug console in wrong context.", kOOTCPPacket_CloseConnection); + } + message = [packet stringForKey:kOOTCPMessage]; + if (message == nil) message = @"Console closed connection."; + [self breakConnectionWithMessage:message]; +} + + +- (void) handleNoteConfigurationChangePacket:(NSDictionary *)packet +{ + NSDictionary *configuration = nil; + NSArray *removed = nil; + NSEnumerator *keyEnum = nil; + NSString *key = nil; + id value = nil; + + if (_monitor == nil) return; + + configuration = [packet dictionaryForKey:kOOTCPConfiguration]; + if (configuration != nil) + { + for (keyEnum = [configuration keyEnumerator]; (key = [keyEnum nextObject]); ) + { + value = [configuration objectForKey:key]; + [_monitor setConfigurationValue:value forKey:key]; + } + } + + removed = [configuration arrayForKey:kOOTCPRemovedConfigurationKeys]; + for (keyEnum = [removed objectEnumerator]; (key = [keyEnum nextObject]); ) + { + [_monitor setConfigurationValue:nil forKey:key]; + } +} + + +- (void) handlePerformCommandPacket:(NSDictionary *)packet +{ + NSString *message = nil; + + message = [packet stringForKey:kOOTCPMessage]; + if (message != nil) [_monitor performJSConsoleCommand:message]; +} + + +- (void) handleRequestConfigurationValuePacket:(NSDictionary *)packet +{ + NSString *key = nil; + id value = nil; + + key = [packet stringForKey:kOOTCPConfigurationKey]; + if (key != nil) + { + value = [_monitor configurationValueForKey:key]; + [self debugMonitor:_monitor + noteChangedConfigrationValue:value + forKey:key]; + } +} + + +- (void) handlePingPacket:(NSDictionary *)packet +{ + id message = nil; + + message = [packet objectForKey:kOOTCPMessage]; + [self sendPacket:kOOTCPPacket_Pong + withValue:message + forParameter:kOOTCPMessage]; +} + + +- (void) handlePongPacket:(NSDictionary *)packet +{ + // Do nothing; we don't currently send pings. +} + + +- (void) disconnectFromServerWithMessage:(NSString *)message +{ + if (StatusIsSendable(_status)) + { + [self sendPacket:kOOTCPPacket_CloseConnection + withValue:message + forParameter:kOOTCPMessage]; + } + [self closeConnection]; + + _status = kOOTCPClientDisconnected; +} + + +- (void) breakConnectionWithMessage:(NSString *)message +{ + [self closeConnection]; + + if (_status != kOOTCPClientConnectionRefused) _status = kOOTCPClientDisconnected; + + if ([message length] > 0) + { + OOLog(@"debugTCP.disconnect", @"Debug console disconnected with message %@", message); + } + else + { + OOLog(@"debugTCP.disconnect", @"Debug console disconnected."); + } + +#if 0 + // Disconnecting causes crashiness for reasons I don't understand, and isn't very important anyway. + [_monitor disconnectDebugger:self message:message]; + _monitor = nil; +#endif +} + + +- (void) breakConnectionWithBadStream:(NSStream *)stream +{ + NSString *errorDesc = nil; + NSError *error = nil; + + error = [stream streamError]; + errorDesc = [error localizedDescription]; + if (errorDesc == nil) errorDesc = [error description]; + if (errorDesc == nil) errorDesc = @"unknown error."; + [self breakConnectionWithMessage:[NSString stringWithFormat: + @"Lost connection to remote debug console. outStream status: %i, inStream status: %i. Stream error: %@", + [_outStream streamStatus], [_inStream streamStatus], errorDesc]]; +} + +@end + + +static void DecoderPacket(void *cbInfo, OOALStringRef packetType, OOALDictionaryRef packet) +{ + [(OODebugTCPConsoleClient *)cbInfo dispatchPacket:packet ofType:packetType]; +} + + +static void DecoderError(void *cbInfo, OOALStringRef errorDesc) +{ + [(OODebugTCPConsoleClient *)cbInfo breakConnectionWithMessage:errorDesc]; +} + + +#ifdef OO_LOG_DEBUG_PROTOCOL_PACKETS +void LogOOTCPStreamDecoderPacket(NSDictionary *packet) +{ + NSData *data = nil; + NSString *xml = nil; + + data = [NSPropertyListSerialization dataFromPropertyList:packet format:NSPropertyListXMLFormat_v1_0 errorDescription:NULL]; + xml = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + OOLog(@"debugTCP.receive", @"Received packet:\n%@", xml); +} + + +static void LogSendPacket(NSDictionary *packet) +{ + NSData *data = nil; + NSString *xml = nil; + + data = [NSPropertyListSerialization dataFromPropertyList:packet format:NSPropertyListXMLFormat_v1_0 errorDescription:NULL]; + xml = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + OOLog(@"debugTCP.send", @"Sent packet:\n%@", xml); +} +#endif + +#endif /* OO_EXCLUDE_DEBUG_SUPPORT */ diff --git a/src/Core/Debug/OODebugTCPConsoleProtocol.h b/src/Core/Debug/OODebugTCPConsoleProtocol.h new file mode 100644 index 00000000..c73ad983 --- /dev/null +++ b/src/Core/Debug/OODebugTCPConsoleProtocol.h @@ -0,0 +1,331 @@ +/* + +OODebugTCPConsoleProtocol.h + +Definitions used in Oolite remote debug console protocol. + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#ifndef OOALSTR +#ifdef __OBJC__ +#import +#define OOALSTR(x) @""x +#else // C +#include +#define OOALSTR(x) CFSTR(x) +#endif +#endif + + +enum +{ + kOOTCPConsolePort = 0x002173 /* = 8563 */ +}; + + +/* PROTOCOL OVERVIEW + + The basic unit of the protocol is property lists. Property lists originate + with OpenStep and are thus defined in Cocoa and GNUstep, but they're also + implemented in CoreFoundation (Mac OS X) and CFLite (cross-platform), with + a C interface. + + The protocol uses a TCP stream in each direction. Each stream consists of + a series of property lists in the Apple XML encoding. Each property list + is framed in the simplest possible way: it is preceeded by an unsigned + 32-bit length, in network-endian order. Each length + property list + combination is referred to as a packet. + + Every packet's property list must have a dictionary as its root element. + The dictionary must contain a kOOTCPConsolePacketType key, whose value + determines the meaning of the rest of the dictionary. +*/ + + + + +/* *** Packet types *** */ +/* kOOTCPacket_RequestConnection + client --> server + Message sent from client (Oolite) to server (console) to request a cosole + connection. + + Required values: + kOOTCPProtocolVersion + kOOTCPOoliteVersion + Expected responses: + kOOTCPacket_ApproveConnection + OR + kOOTCPacket_RejectConnection +*/ +#define kOOTCPPacket_RequestConnection OOALSTR("Request Connection") + +/* kOOTCPPacket_ApproveConnection + client <-- server + Message sent in response to kOOTCPacket_RequestConnection if connection is + established successfully. + + Optional values: + kOOTCPConsoleIdentity +*/ +#define kOOTCPPacket_ApproveConnection OOALSTR("Approve Connection") + +/* kOOTCPPacket_RejectConnection + client <-- server + Message sent in response to kOOTCPacket_RequestConnection if connection is + not established successfully. After this message is sent, the connection + is closed with no further traffic. + + Optional values: + kOOTCPMessage + Expected responses: + None permitted. +*/ +#define kOOTCPPacket_RejectConnection OOALSTR("Reject Connection") + +/* kOOTCPPacket_CloseConnection + client <-> server + Message sent by either party to cleanly close connection. + + Optional values: + kOOTCPMessage + Expected responses: + None permitted. +*/ +#define kOOTCPPacket_CloseConnection OOALSTR("Close Connection") + +/* kOOTCPPacket_ConsoleOutput + client --> server + Message sent by Oolite to print text to console. + + Required values: + kOOTCPMessage + kOOTCPColorKey + Optional values: + kOOTCPEmphasisRanges +*/ +#define kOOTCPPacket_ConsoleOutput OOALSTR("Console Output") + +/* kOOTCPPacket_ClearConsole + client --> server + Message sent by Oolite to clear console output. +*/ +#define kOOTCPPacket_ClearConsole OOALSTR("Clear Console") + +/* kOOTCPPacket_ShowConsole + client --> server + Message sent by Oolite to request that the cosole makes itself visible and + active. +*/ +#define kOOTCPPacket_ShowConsole OOALSTR("Show Console") + +/* kOOTCPPacket_NoteConfiguration + client --> server + Message sent by Oolite to appraise the console of the contents of the + configuration dictionary. Sent once after the initial handshake. + + Required values: + kOOTCPConfiguration +*/ +#define kOOTCPPacket_NoteConfiguration OOALSTR("Note Configuration") + +/* kOOTCPPacket_NoteConfigurationChange + client <-> server + Message sent by Oolite when the contents of the configuration dictionary + change, or by console to change the configuration dictionary (in which + case a confirmation will be returned in form of one or more + kOOTCPPacket_NoteConfigurationChange messages). For this message, + kOOTCPConfiguration is a delta -- keys not contained in it are not to be + removed, they are simply unchanged. Deletions are handled with + kOOTCPRemovedConfigurationKeys. + + This key is also sent in response to a kOOTCPPacket_RequestConfigurationValue + message, even if no configuration value has changed. + + Required values (at least one of): + kOOTCPConfiguration + kOOTCPRemovedConfigurationKeys +*/ +#define kOOTCPPacket_NoteConfigurationChange OOALSTR("Note Configuration Change") + +/* kOOTCPPacket_PerformCommand + client <-- server + Message sent by console to issue a command. + + Required values: + kOOTCPMessage +*/ +#define kOOTCPPacket_PerformCommand OOALSTR("Perform Command") + +/* kOOTCPPacket_RequestConfigurationValue + client <-- server + Message sent by console to request a configuration value. This will result + in a kOOTCPPacket_NoteConfigurationChange message being sent. If the value + is nil, the response will contain a kOOTCPRemovedConfigurationKeys value. + + Required values: + kOOTCPConfigurationKey + Expected response: + kOOTCPPacket_NoteConfigurationChange +*/ +#define kOOTCPPacket_RequestConfigurationValue OOALSTR("Request Configuration Value") + +/* kOOTCPPacket_Ping + client <-> server + Must be responded to with a kOOTCPPacket_Pong message containing the same + kOOTCPMessage, if any. + + Optional values: + kOOTCPMessage + Expected response: + kOOTCPPacket_Pong +*/ +#define kOOTCPPacket_Ping OOALSTR("Ping") + +/* kOOTCPPacket_Pong + client <-> server + Must be sent in response to kOOTCPPacket_Ping. If the kOOTCPPacket_Ping + packet had a kOOTCPMessage, the same kOOTCPMessage value must be attached + to the kOOTCPPacket_Pong. + + Optional values: + kOOTCPMessage (required if included in ping) +*/ +#define kOOTCPPacket_Pong OOALSTR("Pong") + + + + +/* *** Value keys *** */ +/* kOOTCPPacketType + String indicating packet type. See above under + + See constants below under *** Packet types ***. +*/ +#define kOOTCPPacketType OOALSTR("packet type") + +/* kOOTCPProtocolVersion + Number indicating version of debug console TCP protocol. Sent with + kOOTCPPacket_RequestConnection. + + See constants below under *** Version constants ***. +*/ +#define kOOTCPProtocolVersion OOALSTR("protocol version") + +/* kOOTCPOoliteVersion + String indicating the version of Oolite, for example "1.70" or "1.71.1 b2". + Consists of two or more integers separated by .s, optionally followed by + a space and additional information in unspecified format. Sent with + kOOTCPPacket_RequestConnection. +*/ +#define kOOTCPOoliteVersion OOALSTR("Oolite version") + +/* kOOTCPMessage + Textual message sent with various packet types. No specified format. +*/ +#define kOOTCPMessage OOALSTR("message") + +/* kOOTCPConsoleIdentity + String identifying console software. No specified format. +*/ +#define kOOTCPConsoleIdentity OOALSTR("console identity") + +/* kOOTCPColorKey + String identifying colour/formatting to be used. The configuration + dictionary contains keys of the form foo-foreground-color and + foo-background-color to be used. If no colour is specified for the + specified key, the key "general" should be tried. The colour values are + specified as arrays of three numbers in the range 0-1, specifying RGB + colours. + + For example, if the configuration key contains: + { + general-background-color = (1,1,1); + general-foreground-color = (0,0,0); + foo-background-color = (1,0,0); + } + the colour key "foo" maps to the background colour (1,0,0) and the + foreground color (0,0,0). Sent with kOOTCPPacket_ConsoleOutput. +*/ +#define kOOTCPColorKey OOALSTR("color key") + +/* kOOTCPEmphasisRanges + An array containing an even number of integers. Each pair of integers + specifies a range (in the form offset, length) which should be emphasized. + Sent with kOOTCPPacket_ConsoleOutput. +*/ +#define kOOTCPEmphasisRanges OOALSTR("emphasis ranges") + +/* kOOTCPConfiguration + A dictionary of key/value pairs to add/set in the configuration + dictionary. Sent with kOOTCPPacket_NoteConfiguration and + kOOTCPPacket_NoteConfiguration. +*/ +#define kOOTCPConfiguration OOALSTR("configuration") + +/* kOOTCPConfiguration + An array of keys to remove from the configuration dictionary. Sent with + kOOTCPPacket_NoteConfiguration. +*/ +#define kOOTCPRemovedConfigurationKeys OOALSTR("removed configuration keys") + +/* kOOTCPConfigurationKey + A string specifying the configuration key for which a value is requested. + Sent with kOOTCPPacket_RequestConfigurationValue. +*/ +#define kOOTCPConfigurationKey OOALSTR("configuration key") + + + +/* *** Version constants *** */ +/* Version constants have three components: format, major and minor. The + format version will change if the framing mechanism is changed, that is, + if we switch from the property-list basted protocol in use. The major + version will be changed to indicate compatibility-breaking changes. The + minor version will be changed when new non-critical packets are added. +*/ + +#define OOTCP_ENCODE_VERSION(f, mj, mi) \ + ((((f) << 16) & kOOTCPProtocolVersionFormatMask) | \ + (((f) << 8) & kOOTCPProtocolVersionMajorMask) | \ + ((mi) & kOOTCPProtocolVersionMinorMask)) + +#define OOTCP_VERSION_FORMAT(v) (((v) & kOOTCPProtocolVersionFormatMask) >> 16) +#define OOTCP_VERSION_MAJOR(v) (((v) & kOOTCPProtocolVersionMajorMask) >> 8) +#define OOTCP_VERSION_MINOR(v) ((v) & kOOTCPProtocolVersionMinorMask) + +enum +{ + kOOTCPProtocolVersionFormatMask = 0x00FF0000, + kOOTCPProtocolVersionMajorMask = 0x0000FF00, + kOOTCPProtocolVersionMinorMask = 0x000000FF, + + kOOTCPProtocolVersionPListFormat = 1, + + // 1:1.0, first version. + kOOTCPProtocolVersion_1_1_0 = OOTCP_ENCODE_VERSION(kOOTCPProtocolVersionPListFormat, 1, 0) +}; diff --git a/src/Core/Debug/OODebuggerInterface.h b/src/Core/Debug/OODebuggerInterface.h new file mode 100644 index 00000000..b0e2c934 --- /dev/null +++ b/src/Core/Debug/OODebuggerInterface.h @@ -0,0 +1,70 @@ +/* + +OODebuggerInterface.h + +Protocols for communication between OODebugMonitor and OODebuggerInterface. + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +@class OODebugMonitor; + +// Interface for debugger. + +@protocol OODebuggerInterface + +// Sent to establish connection. +- (BOOL)connectDebugMonitor:(in OODebugMonitor *)debugMonitor + errorMessage:(out NSString **)message; + + // Sent to close connection. +- (void)disconnectDebugMonitor:(in OODebugMonitor *)debugMonitor + message:(in NSString *)message; + + // Sent to print to the JavaScript console. + // colorKey is intended to be used to look up a foreground/background colour pair + // in the configuration. EmphasisRange is to specify a bold section of text. +- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor + jsConsoleOutput:(in NSString *)output + colorKey:(in NSString *)colorKey + emphasisRange:(in NSRange)emphasisRange; + + // Sent to clear the JavaScript console. +- (oneway void)debugMonitorClearConsole:(in OODebugMonitor *)debugMonitor; + + // Sent to show the console, for instance in response to a warning or error message. +- (oneway void)debugMonitorShowConsole:(in OODebugMonitor *)debugMonitor; + + // Sent once when the debugger is connected. +- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor + noteConfiguration:(in NSDictionary *)configuration; + + // Sent when configuration changes. newValue may be nil. +- (oneway void)debugMonitor:(in OODebugMonitor *)debugMonitor +noteChangedConfigrationValue:(in id)newValue + forKey:(in NSString *)key; + +@end diff --git a/src/Core/Debug/OOJSConsole.h b/src/Core/Debug/OOJSConsole.h new file mode 100644 index 00000000..3736b1cf --- /dev/null +++ b/src/Core/Debug/OOJSConsole.h @@ -0,0 +1,38 @@ +/* + +OOJSConsole.h + +JavaScript object representing the JavaScript console. + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import + +@class OODebugMonitor; + + +JSObject *DebugMonitorToJSConsole(JSContext *context, OODebugMonitor *monitor); diff --git a/src/Core/Debug/OOJSConsole.m b/src/Core/Debug/OOJSConsole.m new file mode 100644 index 00000000..51f3d28e --- /dev/null +++ b/src/Core/Debug/OOJSConsole.m @@ -0,0 +1,487 @@ +/* + +OOJSConsole.m + + +Oolite Debug OXP + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + +#import "OOJSConsole.h" +#import "OODebugMonitor.h" +#import + +#import "OOJavaScriptEngine.h" +#import "OOJSScript.h" +#import "OOJSVector.h" +#import "OOJSEntity.h" +#import "OOJSCall.h" +#import "OOLoggingExtended.h" + + +@interface Entity (OODebugInspector) + +// Method added by inspector in Debug OXP under OS X only. +- (void) inspect; + +@end + + +static JSObject *sConsolePrototype = NULL; +static JSObject *sConsoleSettingsPrototype = NULL; + + +static JSBool ConsoleGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool ConsoleSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static void ConsoleFinalize(JSContext *context, JSObject *this); + +// Methods +static JSBool ConsoleConsoleMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleClearConsole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleScriptStack(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleInspectEntity(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleCallObjCMethod(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleIsExecutableJavaScript(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleDisplayMessagesInClass(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ConsoleSetDisplayMessagesInClass(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +static JSBool ConsoleSettingsDeleteProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool ConsoleSettingsGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool ConsoleSettingsSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + + +static JSClass sConsoleClass = +{ + "Console", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_ANONYMOUS, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + ConsoleGetProperty, // getProperty + ConsoleSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + ConsoleFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS +}; + + +enum +{ + // Property IDs + kConsole_debugFlags // debug flags, integer, read/write +}; + + +static JSPropertySpec sConsoleProperties[] = +{ + // JS name ID flags + { "debugFlags", kConsole_debugFlags, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sConsoleMethods[] = +{ + // JS name Function min args + { "consoleMessage", ConsoleConsoleMessage, 2 }, + { "clearConsole", ConsoleClearConsole, 0 }, + { "scriptStack", ConsoleScriptStack, 0 }, + { "inspectEntity", ConsoleInspectEntity, 1 }, + { "__callObjCMethod", ConsoleCallObjCMethod, 1 }, + { "isExecutableJavaScript", ConsoleIsExecutableJavaScript, 2 }, + { "displayMessagesInClass", ConsoleDisplayMessagesInClass, 1 }, + { "setDisplayMessagesInClass", ConsoleSetDisplayMessagesInClass, 2 }, + { 0 } +}; + + +static JSClass sConsoleSettingsClass = +{ + "ConsoleSettings", + JSCLASS_HAS_PRIVATE, + + JS_PropertyStub, // addProperty + ConsoleSettingsDeleteProperty, // delProperty + ConsoleSettingsGetProperty, // getProperty + ConsoleSettingsSetProperty, // setProperty + JS_EnumerateStub, // enumerate. FIXME: this should work. + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + ConsoleFinalize, // finalize (same as Console) + JSCLASS_NO_OPTIONAL_MEMBERS +}; + + +static void InitOOJSConsole(JSContext *context, JSObject *global) +{ + sConsolePrototype = JS_InitClass(context, global, NULL, &sConsoleClass, NULL, 0, sConsoleProperties, sConsoleMethods, NULL, NULL); + JSRegisterObjectConverter(&sConsoleClass, JSBasicPrivateObjectConverter); + + sConsoleSettingsPrototype = JS_InitClass(context, global, NULL, &sConsoleSettingsClass, NULL, 0, NULL, NULL, NULL, NULL); + JSRegisterObjectConverter(&sConsoleSettingsClass, JSBasicPrivateObjectConverter); +} + + +JSObject *DebugMonitorToJSConsole(JSContext *context, OODebugMonitor *monitor) +{ + OOJavaScriptEngine *engine = nil; + JSObject *object = NULL; + JSObject *settingsObject = NULL; + jsval value; + + engine = [OOJavaScriptEngine sharedEngine]; + + if (sConsolePrototype == NULL) + { + InitOOJSConsole(context, [engine globalObject]); + } + + // Create Console object + object = JS_NewObject(context, &sConsoleClass, sConsolePrototype, NULL); + if (object != NULL) + { + if (!JS_SetPrivate(context, object, [monitor weakRetain])) object = NULL; + } + + if (object != NULL) + { + // Create ConsoleSettings object + settingsObject = JS_NewObject(context, &sConsoleSettingsClass, sConsoleSettingsPrototype, NULL); + if (settingsObject != NULL) + { + if (!JS_SetPrivate(context, settingsObject, [monitor weakRetain])) settingsObject = NULL; + } + if (settingsObject != NULL) + { + value = OBJECT_TO_JSVAL(settingsObject); + if (!JS_SetProperty(context, object, "settings", &value)) + { + settingsObject = NULL; + } + } + + if (settingsObject == NULL) object = NULL; + } + + return object; +} + + +static JSBool ConsoleGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { +#ifndef NDEBUG + case kConsole_debugFlags: + *outValue = INT_TO_JSVAL(gDebugFlags); + break; +#endif + default: + OOReportJSBadPropertySelector(context, @"Console", JSVAL_TO_INT(name)); + return NO; + } + + return YES; +} + + +static JSBool ConsoleSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + int32 iValue; + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { +#ifndef NDEBUG + case kConsole_debugFlags: + if (JS_ValueToInt32(context, *value, &iValue)) + { + gDebugFlags = iValue; + } + break; +#endif + default: + OOReportJSBadPropertySelector(context, @"Console", JSVAL_TO_INT(name)); + return NO; + } + + return YES; +} + + +static void ConsoleFinalize(JSContext *context, JSObject *this) +{ + [(id)JS_GetPrivate(context, this) release]; + JS_SetPrivate(context, this, nil); +} + + +static JSBool ConsoleSettingsDeleteProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + NSString *key = nil; + id monitor = nil; + + if (!JSVAL_IS_STRING(name)) return NO; + + key = [NSString stringWithJavaScriptValue:name inContext:context]; + + monitor = JSObjectToObject(context, this); + if (![monitor isKindOfClass:[OODebugMonitor class]]) + { + OOReportJSError(context, @"Expected OODebugMonitor, got %@ in %s. This is an internal error, please report it.", [monitor class], __PRETTY_FUNCTION__); + return NO; + } + + [monitor setConfigurationValue:nil forKey:key]; + *outValue = JSVAL_TRUE; + return YES; +} + + +static JSBool ConsoleSettingsGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + NSString *key = nil; + id value = nil; + id monitor = nil; + + if (!JSVAL_IS_STRING(name)) return YES; + key = [NSString stringWithJavaScriptValue:name inContext:context]; + + monitor = JSObjectToObject(context, this); + if (![monitor isKindOfClass:[OODebugMonitor class]]) + { + OOReportJSError(context, @"Expected OODebugMonitor, got %@ in %s. This is an internal error, please report it.", [monitor class], __PRETTY_FUNCTION__); + return NO; + } + + value = [monitor configurationValueForKey:key]; + *outValue = [value javaScriptValueInContext:context]; + + return YES; +} + + +static JSBool ConsoleSettingsSetProperty(JSContext *context, JSObject *this, jsval name, jsval *inValue) +{ + NSString *key = nil; + id value = nil; + id monitor = nil; + + if (!JSVAL_IS_STRING(name)) return YES; + key = [NSString stringWithJavaScriptValue:name inContext:context]; + + monitor = JSObjectToObject(context, this); + if (![monitor isKindOfClass:[OODebugMonitor class]]) + { + OOReportJSError(context, @"Expected OODebugMonitor, got %@ in %s. This is an internal error, please report it.", [monitor class], __PRETTY_FUNCTION__); + return NO; + } + + if (JSVAL_IS_NULL(*inValue) || JSVAL_IS_VOID(*inValue)) + { + [monitor setConfigurationValue:nil forKey:key]; + } + else + { + value = JSValueToObject(context, *inValue); + if (value != nil) + { + [monitor setConfigurationValue:value forKey:key]; + } + else + { + OOReportJSWarning(context, @"debugConsole.settings: could not convert %@ to native object.", [NSString stringWithJavaScriptValue:*inValue inContext:context]); + } + } + + return YES; +} + + +// *** Methods *** + +// function consoleMessage(message : String) : void +static JSBool ConsoleConsoleMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + id monitor = nil; + NSString *colorKey = nil, + *message = nil; + NSRange emphasisRange = {0, 0}; + jsdouble location, length; + + monitor = JSObjectToObject(context, this); + if (![monitor isKindOfClass:[OODebugMonitor class]]) + { + OOReportJSError(context, @"Expected OODebugMonitor, got %@ in %s. This is an internal error, please report it.", [monitor class], __PRETTY_FUNCTION__); + return NO; + } + + colorKey = JSValToNSString(context,argv[0]); + message = JSValToNSString(context,argv[1]); + + if (4 <= argc) + { + // Attempt to get two numbers, specifying an emphasis range. + if (JS_ValueToNumber(context, argv[2], &location) && + JS_ValueToNumber(context, argv[3], &length)) + { + emphasisRange = NSMakeRange(location, length); + } + } + + if (message == nil) + { + if (colorKey == nil) + { + OOReportJSWarning(context, @"Console.consoleMessage() called with no parameters."); + } + else + { + message = colorKey; + colorKey = @"command-result"; + } + } + + if (message != nil) + { + [monitor appendJSConsoleLine:message + colorKey:colorKey + emphasisRange:emphasisRange]; + } + + return YES; +} + + +// function clearConsole() : void +static JSBool ConsoleClearConsole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + id monitor = nil; + + monitor = JSObjectToObject(context, this); + if (![monitor isKindOfClass:[OODebugMonitor class]]) + { + OOReportJSError(context, @"Expected OODebugMonitor, got %@ in %s. This is an internal error, please report it.", [monitor class], __PRETTY_FUNCTION__); + return NO; + } + + [monitor clearJSConsole]; + return YES; +} + + +// function scriptStack() : Array +static JSBool ConsoleScriptStack(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSArray *result = nil; + + result = [OOJSScript scriptStack]; + *outResult = [result javaScriptValueInContext:context]; + return YES; +} + + +// function inspectEntity(entity : Entity) : void +static JSBool ConsoleInspectEntity(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Entity *entity = nil; + + if (JSValueToEntity(context, argv[0], &entity)) + { + if ([entity respondsToSelector:@selector(inspect)]) + { + [entity inspect]; + } + } + + return YES; +} + + +// function __callObjCMethod(selector : String [, ...]) : Object +static JSBool ConsoleCallObjCMethod(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + id object = nil; + + object = JSObjectToObject(context, this); + if (object == nil) + { + OOReportJSError(context, @"Attempt to call __callObjCMethod() for non-Objective-C object %@.", [NSString stringWithJavaScriptValue:OBJECT_TO_JSVAL(this) inContext:context]); + return NO; + } + + return OOJSCallObjCObjectMethod(context, object, [object jsClassName], argc, argv, outResult); +} + + +// function isExecutableJavaScript(this : Object, string : String) : Boolean +static JSBool ConsoleIsExecutableJavaScript(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + JSObject *target = NULL; + JSString *string = NULL; + + *outResult = JSVAL_FALSE; + if (argc < 2) return YES; + if (!JS_ValueToObject(context, argv[0], &target) || !JSVAL_IS_STRING(argv[1])) return YES; // Fail silently + string = JSVAL_TO_STRING(argv[1]); + + *outResult = BOOLEAN_TO_JSVAL(JS_BufferIsCompilableUnit(context, target, JS_GetStringBytes(string), JS_GetStringLength(string))); + return YES; +} + + +// function displayMessagesInClass(class : String) : Boolean +static JSBool ConsoleDisplayMessagesInClass(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *messageClass = nil; + + messageClass = [NSString stringWithJavaScriptValue:argv[0] inContext:context]; + *outResult = BOOLEAN_TO_JSVAL(OOLogWillDisplayMessagesInClass(messageClass)); + + return YES; +} + + +// function setDisplayMessagesInClass(class : String, flag : Boolean) : void +static JSBool ConsoleSetDisplayMessagesInClass(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *messageClass = nil; + JSBool flag; + + messageClass = JSValToNSString(context, argv[0]); + if (messageClass != nil && JS_ValueToBoolean(context, argv[1], &flag)) + { + OOLogSetDisplayMessagesInClass(messageClass, flag); + } + + return YES; +} + +#endif /* OO_EXCLUDE_DEBUG_SUPPORT */ diff --git a/src/Core/Debug/OOTCPStreamDecoder.c b/src/Core/Debug/OOTCPStreamDecoder.c new file mode 100644 index 00000000..6ca610d4 --- /dev/null +++ b/src/Core/Debug/OOTCPStreamDecoder.c @@ -0,0 +1,266 @@ +/* + +OOTCPStreamDecoder.c + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + + +#include "OOTCPStreamDecoder.h" +#include +#include +#include +#include "OODebugTCPConsoleProtocol.h" + + +#ifdef OO_LOG_DEBUG_PROTOCOL_PACKETS +extern void LogOOTCPStreamDecoderPacket(OOALDictionaryRef packet); +#else +#define LogOOTCPStreamDecoderPacket(packet) do {} while (0) +#endif + + +struct OOTCPStreamDecoder +{ + uint8_t header[4]; + uint32_t headerSpaceUsed; + OOALMutableDataRef nextPacketData; + uint32_t nextSize; + + OOTCPStreamDecoderPacketCallback Packet; + OOTCPStreamDecoderErrorCallback Error; + OOTCPStreamDecoderFinalizeCallback Finalize; + + void *cbInfo; +}; + + +static void Error(OOTCPStreamDecoderRef decoder, OOALStringRef format, ...); +static void PacketReady(OOTCPStreamDecoderRef decoder); + + +OOTCPStreamDecoderRef OOTCPStreamDecoderCreate(OOTCPStreamDecoderPacketCallback packetCB, OOTCPStreamDecoderErrorCallback errorCB, OOTCPStreamDecoderFinalizeCallback finalizeCB, void *cbInfo) +{ + OOTCPStreamDecoderRef decoder = NULL; + + if (packetCB == NULL) return NULL; + + decoder = malloc(sizeof *decoder); + if (decoder == NULL) return NULL; + + decoder->headerSpaceUsed = 0; + decoder->nextPacketData = NULL; + decoder->nextSize = 0; + decoder->Packet = packetCB; + decoder->Error = errorCB; + decoder->Finalize = finalizeCB; + decoder->cbInfo = cbInfo; + + return decoder; +} + + +void OOTCPStreamDecoderDestroy(OOTCPStreamDecoderRef decoder) +{ + if (decoder == NULL) return; + + if (decoder->Finalize != NULL) + { + decoder->Finalize(decoder->cbInfo); + } + + if (decoder->nextPacketData != NULL) + { + OOALRelease(decoder->nextPacketData); + decoder->nextPacketData = NULL; + } + + free(decoder); +} + + +void OOTCPStreamDecoderReceiveData(OOTCPStreamDecoderRef decoder, OOALDataRef data) +{ + if (decoder == NULL || data == NULL) return; + + OOTCPStreamDecoderReceiveBytes(decoder, OOALDataGetBytePtr(data), OOALDataGetLength(data)); +} + + +void OOTCPStreamDecoderReceiveBytes(OOTCPStreamDecoderRef decoder, const void *inBytes, size_t length) +{ + const unsigned char *bytes = NULL; + size_t remaining; + size_t bytesToAdd; + OOALAutoreleasePoolRef pool = NULL; + + if (decoder == NULL) return; + + bytes = inBytes; + remaining = length; + + if (bytes == NULL && remaining != 0) + { + Error(decoder, OOALSTR("Invalid data -- NULL bytes but %u byte count."), remaining); + return; + } + + while (remaining != 0) + { + if (decoder->nextPacketData != NULL) + { + // More data expected + bytesToAdd = remaining; + if (decoder->nextSize < bytesToAdd) bytesToAdd = decoder->nextSize; + + OOALMutableDataAppendBytes(decoder->nextPacketData, bytes, bytesToAdd); + + remaining -= bytesToAdd; + decoder->nextSize -= bytesToAdd; + bytes += bytesToAdd; + + if (decoder->nextSize == 0) + { + // Packet is ready. + pool = OOALCreateAutoreleasePool(); + PacketReady(decoder); + OOALDestroyAutoreleasePool(pool); + pool = NULL; + + OOALRelease(decoder->nextPacketData); + decoder->nextPacketData = NULL; + } + } + else if (decoder->headerSpaceUsed < 4) + { + // Read bytes for packet header + remaining--; + decoder->header[decoder->headerSpaceUsed++] = *bytes++; + } + else if (decoder->headerSpaceUsed == 4) + { + // We've read a header, start on a packet. + decoder->nextSize = (decoder->header[0] << 24) | + (decoder->header[1] << 16) | + (decoder->header[2] << 8) | + (decoder->header[3] << 0); + + decoder->headerSpaceUsed = 0; + if (decoder->nextSize != 0) + { + decoder->nextPacketData = OOALDataCreateMutable(decoder->nextSize); + } + } + else + { + Error(decoder, OOALSTR("OOTCPStreamDecoder internal error: reached unreachable state. nextSize = %lu, bufferUsed = %lu, nextPacketData = %@."), (unsigned long)decoder->nextSize, (unsigned long)decoder->headerSpaceUsed, decoder->nextPacketData); + } + } +} + + +static void PacketReady(OOTCPStreamDecoderRef decoder) +{ + OOALDictionaryRef packet = NULL; + OOALStringRef errorString = NULL; + OOALStringRef packetType = NULL; + + packet = OOALPropertyListFromData(decoder->nextPacketData, &errorString); + + // Ensure that it's a property list. + if (packet == NULL) + { + Error(decoder, OOALSTR("Protocol error: packet is not property list (property list error: %@)."), errorString); + OOALRelease(errorString); + return; + } + + // Ensure that it's a dictionary. + if (!OOALIsDictionary(packet)) + { + Error(decoder, OOALSTR("Protocol error: packet is a %@, not a dictionary."), OOTypeDescription(packet)); + return; + } + + LogOOTCPStreamDecoderPacket(packet); + + // Get packet type (and ensure that there is one). + packetType = OOALDictionaryGetValue(packet, kOOTCPPacketType); + if (packetType == NULL) + { + Error(decoder, OOALSTR("Protocol error: packet contains no packet type.")); + return; + } + + if (!OOALIsString(packetType)) + { + Error(decoder, OOALSTR("Protocol error: packet type is a %@, not a string."), OOTypeDescription(packetType)); + return; + } + + decoder->Packet(decoder->cbInfo, packetType, packet); +} + + +static void Error(OOTCPStreamDecoderRef decoder, OOALStringRef format, ...) +{ + va_list args; + OOALStringRef string = NULL; + + if (decoder == NULL || decoder->Error == NULL || format == NULL) return; + + va_start(args, format); + string = OOALStringCreateWithFormatAndArguments(format, args); + va_end(args); + + if (string != NULL) + { + decoder->Error(decoder->cbInfo, string); + OOALRelease(string); + } +} + +#endif /* OO_EXCLUDE_DEBUG_SUPPORT */ diff --git a/src/Core/Debug/OOTCPStreamDecoder.h b/src/Core/Debug/OOTCPStreamDecoder.h new file mode 100644 index 00000000..593ec736 --- /dev/null +++ b/src/Core/Debug/OOTCPStreamDecoder.h @@ -0,0 +1,72 @@ +/* + +OOTCPStreamDecoder.h + +Psuedo-object to take blobs of data, create Oolite TCP debug console +protocol packets. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#ifndef INCLUDED_OOTCPStreamDecoder_h +#define INCLUDED_OOTCPStreamDecoder_h + +#include "OOTCPStreamDecoderAbstractionLayer.h" + + +typedef struct OOTCPStreamDecoder *OOTCPStreamDecoderRef; + +typedef void (*OOTCPStreamDecoderPacketCallback)(void *cbInfo, OOALStringRef packetType, OOALDictionaryRef packet); +typedef void (*OOTCPStreamDecoderErrorCallback)(void *cbInfo, OOALStringRef errorDesc); +typedef void (*OOTCPStreamDecoderFinalizeCallback)(void *cbInfo); + + +OOTCPStreamDecoderRef OOTCPStreamDecoderCreate(OOTCPStreamDecoderPacketCallback packetCB, OOTCPStreamDecoderErrorCallback errorCB, OOTCPStreamDecoderFinalizeCallback finalizeCB, void *cbInfo); +void OOTCPStreamDecoderDestroy(OOTCPStreamDecoderRef decoder); + +void OOTCPStreamDecoderReceiveData(OOTCPStreamDecoderRef decoder, OOALDataRef data); +void OOTCPStreamDecoderReceiveBytes(OOTCPStreamDecoderRef decoder, const void *bytes, size_t length); + +#endif /* INCLUDED_OOTCPStreamDecoder_h */ diff --git a/src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.h b/src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.h new file mode 100644 index 00000000..e73ad30b --- /dev/null +++ b/src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.h @@ -0,0 +1,122 @@ +/* OOTCPStreamDecoderAbstractionLayer.h + +Abstraction layer to allow OOTCPStreamDecoder to work with CoreFoundation/ +CF-Lite, Cocoa Foundation or GNUstep Foundation. +*/ + +#ifndef INCLUDED_OOTCPStreamDecoderAbstractionLayer_h +#define INCLUDED_OOTCPStreamDecoderAbstractionLayer_h + +#ifndef OOTCPSTREAM_USE_COREFOUNDATION +#define OOTCPSTREAM_USE_COREFOUNDATION 0 +#endif + +#if OOTCPSTREAM_USE_COREFOUNDATION + +#include +#import "JAAutoreleasePool.h" + + +#define OOALRelease(object) CFRelease(object) + +#define OOTypeDescription(object) JAAutorelease(CFCopyTypeIDDescription(CFGetTypeID(object))) + + + +typedef CFStringRef OOALStringRef; +#define OOALIsString(object) (CFGetTypeID(object) == CFStringGetTypeID()) + +#define OOALSTR(str) CFSTR(str) + +#define OOALStringCreateWithFormatAndArguments(format, args) CFStringCreateWithFormatAndArguments(kCFAllocatorDefault, NULL, format, args) + + + +typedef CFDictionaryRef OOALDictionaryRef; +#define OOALIsDictionary(object) (CFGetTypeID(object) == CFDictionaryGetTypeID()) + +#define OOALDictionaryGetValue(dictionary, key) CFDictionaryGetValue(dictionary, key) + + + +typedef CFDataRef OOALDataRef; +typedef CFMutableDataRef OOALMutableDataRef; +#define OOALIsData(object) (CFGetTypeID(object) == CFDataGetTypeID()) + +#define OOALDataCreateMutable(capacity) CFDataCreateMutable(kCFAllocatorDefault, capacity) + +#define OOALMutableDataAppendBytes(data, bytes, length) CFDataAppendBytes(data, bytes, length) + +#define OOALDataGetBytePtr(data) CFDataGetBytePtr(data) +#define OOALDataGetLength(data) CFDataGetLength(data) + + + +typedef JAAutoreleasePoolRef OOALAutoreleasePoolRef; + +#define OOALCreateAutoreleasePool() JACreateAutoreleasePool() +#define OOALDestroyAutoreleasePool(pool) JADestroyAutoreleasePool(pool) + + + +#define OOALPropertyListFromData(data, errStr) JAAutorelease(CFPropertyListCreateFromXMLData(kCFAllocatorDefault, data, kCFPropertyListImmutable, errStr)) + +#else /* !OOTCPSTREAM_USE_COREFOUNDATION */ + +#include +#include +#include + + +#if __OBJC__ + +#import + +typedef id OOALObjectRef; + +typedef NSString *OOALStringRef; +typedef NSData *OOALDataRef; +typedef NSMutableData *OOALMutableDataRef; +typedef NSDictionary *OOALDictionaryRef; +typedef NSAutoreleasePool *OOALAutoreleasePoolRef; + +#define OOALSTR(x) @""x + +#else + +typedef const void *OOALObjectRef; + +typedef const struct NSString *OOALStringRef; +typedef const struct NSData *OOALDataRef; +typedef struct NSData *OOALMutableDataRef; +typedef const struct NSDictionary *OOALDictionaryRef; +typedef const struct NSAutoreleasePool *OOALAutoreleasePoolRef; + +OOALStringRef OOALGetConstantString(const char *string); // Should only be used with string literals! +#define OOALSTR(string) OOALGetConstantString("" string "") + +#endif + + +void OOALRelease(OOALObjectRef object); +OOALStringRef OOTypeDescription(OOALObjectRef object); + +bool OOALIsString(OOALObjectRef object); +OOALStringRef OOALStringCreateWithFormatAndArguments(OOALStringRef format, va_list args); + +bool OOALIsDictionary(OOALObjectRef object); +OOALObjectRef OOALDictionaryGetValue(OOALDictionaryRef dictionary, OOALObjectRef key); + +bool OOALIsData(OOALObjectRef object); +OOALMutableDataRef OOALDataCreateMutable(size_t capacity); +void OOALMutableDataAppendBytes(OOALMutableDataRef data, const void *bytes, size_t length); +const void *OOALDataGetBytePtr(OOALDataRef data); +size_t OOALDataGetLength(OOALDataRef data); + +OOALAutoreleasePoolRef OOALCreateAutoreleasePool(void); +#define OOALDestroyAutoreleasePool(pool) OOALRelease(pool) + +OOALObjectRef OOALPropertyListFromData(OOALMutableDataRef data, OOALStringRef *errStr); + +#endif /* OOTCPSTREAM_USE_COREFOUNDATION */ +#endif /* INCLUDED_OOTCPStreamDecoderAbstractionLayer_h */ diff --git a/src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.m b/src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.m new file mode 100644 index 00000000..1d20d31e --- /dev/null +++ b/src/Core/Debug/OOTCPStreamDecoderAbstractionLayer.m @@ -0,0 +1,127 @@ +/* OOTCPStreamDecoderAbstractionLayer.h + + Abstraction layer to allow OOTCPStreamDecoder to work with CoreFoundation/ + CF-Lite, Cocoa Foundation or GNUstep Foundation. + + Foundation implementation. +*/ + +#ifndef OO_EXCLUDE_DEBUG_SUPPORT + +#import "OOTCPStreamDecoderAbstractionLayer.h" +#import "OOCocoa.h" + + +// Simulate literal CF/NS strings. Each literal string that is used becomes a single object. Since it uses pointers as keys, it should only be used with literals. +OOALStringRef OOALGetConstantString(const char *string) +{ + static NSMutableDictionary *sStrings = nil; + NSValue *key = nil; + NSString *value = nil; + + if (sStrings == nil) + { + sStrings = [[NSMutableDictionary alloc] init]; + } + + key = [NSValue valueWithPointer:string]; + value = [sStrings objectForKey:key]; + if (value == nil) + { + // Note: non-ASCII strings are not permitted, but we don't bother to detect them. + value = [NSString stringWithUTF8String:string]; + if (value != nil) [sStrings setObject:value forKey:key]; + } + + return value; +} + + +void OOALRelease(OOALObjectRef object) +{ + [object release]; +} + + +OOALStringRef OOTypeDescription(OOALObjectRef object) +{ + return [[object class] description]; +} + + +bool OOALIsString(OOALObjectRef object) +{ + return [object isKindOfClass:[NSString class]]; +} + + +OOALStringRef OOALStringCreateWithFormatAndArguments(OOALStringRef format, va_list args) +{ + return [[NSString alloc] initWithFormat:format arguments:args]; +} + + +bool OOALIsDictionary(OOALObjectRef object) +{ + return [object isKindOfClass:[NSDictionary class]]; +} + + +OOALObjectRef OOALDictionaryGetValue(OOALDictionaryRef dictionary, OOALObjectRef key) +{ + return [dictionary objectForKey:key]; +} + + +bool OOALIsData(OOALObjectRef object) +{ + return [object isKindOfClass:[NSData class]]; +} + + +OOALMutableDataRef OOALDataCreateMutable(size_t capacity) +{ + return [[NSMutableData alloc] initWithCapacity:capacity]; +} + + +void OOALMutableDataAppendBytes(OOALMutableDataRef data, const void *bytes, size_t length) +{ + [data appendBytes:bytes length:length]; +} + + +const void *OOALDataGetBytePtr(OOALDataRef data) +{ + return [data bytes]; +} + + +size_t OOALDataGetLength(OOALDataRef data) +{ + return [data length]; +} + + +OOALAutoreleasePoolRef OOALCreateAutoreleasePool(void) +{ + return [[NSAutoreleasePool alloc] init]; +} + + +OOALObjectRef OOALPropertyListFromData(OOALMutableDataRef data, OOALStringRef *errStr) +{ + id result = [NSPropertyListSerialization propertyListFromData:data + mutabilityOption:NSPropertyListImmutable + format:NULL + errorDescription:errStr]; + [result retain]; + +#if !OOLITE_RELEASE_PLIST_ERROR_STRINGS + [*errStr retain]; +#endif + + return result; +} + +#endif /* OO_EXCLUDE_DEBUG_SUPPORT */ diff --git a/src/Core/Entities/DustEntity.h b/src/Core/Entities/DustEntity.h new file mode 100644 index 00000000..a70b026e --- /dev/null +++ b/src/Core/Entities/DustEntity.h @@ -0,0 +1,45 @@ +/* + +DustEntity.h + +Entity representing a number of dust particles. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOSelfDrawingEntity.h" + +#define DUST_SCALE 2000 +#define DUST_N_PARTICLES 600 + +@class OOColor; + +@interface DustEntity: Entity +{ + OOColor *dust_color; + Vector vertices[DUST_N_PARTICLES]; + GLfloat color_fv[4]; + GLuint displayListName; +} + +- (void) setDustColor:(OOColor *) color; +- (OOColor *) dust_color; + +@end diff --git a/src/Core/Entities/DustEntity.m b/src/Core/Entities/DustEntity.m new file mode 100644 index 00000000..086e5b5a --- /dev/null +++ b/src/Core/Entities/DustEntity.m @@ -0,0 +1,214 @@ +/* + +DustEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "DustEntity.h" + +#import "OOMaths.h" +#import "Universe.h" +#import "MyOpenGLView.h" +#import "OOGraphicsResetManager.h" + +#import "PlayerEntity.h" + + +// Declare protocol conformance +@interface DustEntity (OOGraphicsResetClient) +@end + + +@implementation DustEntity + +- (id) init +{ + int vi; + + ranrot_srand([[NSDate date] timeIntervalSince1970]); // seed randomiser by time + + self = [super init]; + + for (vi = 0; vi < DUST_N_PARTICLES; vi++) + { + vertices[vi].x = (ranrot_rand() % DUST_SCALE) - DUST_SCALE / 2; + vertices[vi].y = (ranrot_rand() % DUST_SCALE) - DUST_SCALE / 2; + vertices[vi].z = (ranrot_rand() % DUST_SCALE) - DUST_SCALE / 2; + } + + dust_color = [[OOColor colorWithCalibratedRed:0.5 green:1.0 blue:1.0 alpha:1.0] retain]; + displayListName = 0; + [self setStatus:STATUS_ACTIVE]; + + [[OOGraphicsResetManager sharedManager] registerClient:self]; + + return self; +} + + +- (void) dealloc +{ + [dust_color release]; + [[OOGraphicsResetManager sharedManager] unregisterClient:self]; + glDeleteLists(displayListName, 1); + + [super dealloc]; +} + + +- (void) setDustColor:(OOColor *) color +{ + if (dust_color) [dust_color release]; + dust_color = [color retain]; + [dust_color getGLRed:&color_fv[0] green:&color_fv[1] blue:&color_fv[2] alpha:&color_fv[3]]; +} + + +- (OOColor *) dust_color +{ + return dust_color; +} + + +- (BOOL) canCollide +{ + return NO; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + assert(player != nil); + + zero_distance = 0.0; + + Vector offset = player->position; + GLfloat half_scale = DUST_SCALE * 0.50; + int vi; + for (vi = 0; vi < DUST_N_PARTICLES; vi++) + { + while (vertices[vi].x - offset.x < -half_scale) + vertices[vi].x += DUST_SCALE; + while (vertices[vi].x - offset.x > half_scale) + vertices[vi].x -= DUST_SCALE; + + while (vertices[vi].y - offset.y < -half_scale) + vertices[vi].y += DUST_SCALE; + while (vertices[vi].y - offset.y > half_scale) + vertices[vi].y -= DUST_SCALE; + + while (vertices[vi].z - offset.z < -half_scale) + vertices[vi].z += DUST_SCALE; + while (vertices[vi].z - offset.z > half_scale) + vertices[vi].z -= DUST_SCALE; + } + +} + + +- (void) drawEntity:(BOOL) immediate :(BOOL) translucent +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + assert(player != nil); + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_NO_DUST) return; +#endif + + int ct; + int vi; + + GLfloat *fogcolor = [UNIVERSE skyClearColor]; + int dust_size = floor([[UNIVERSE gameView] viewSize].width / 480.0); + if (dust_size < 1.0) + dust_size = 1.0; + int line_size = dust_size / 2; + if (line_size < 1.0) + line_size = 1.0; + GLfloat half_scale = DUST_SCALE * 0.50; + GLfloat quarter_scale = DUST_SCALE * 0.25; + + if ([UNIVERSE breakPatternHide]) return; // DON'T DRAW + + BOOL warp_stars = [player atHyperspeed]; + Vector warp_vector = vector_multiply_scalar([player velocityVector], 1.0f / HYPERSPEED_FACTOR); + + if (translucent) + { + glEnable(GL_FOG); + glFogi(GL_FOG_MODE, GL_LINEAR); + glFogfv(GL_FOG_COLOR, fogcolor); + glHint(GL_FOG_HINT, GL_NICEST); + glFogf(GL_FOG_START, quarter_scale); + glFogf(GL_FOG_END, half_scale); + // + // disapply lighting and texture + glDisable(GL_TEXTURE_2D); + // + if (player->isSunlit) + glColor4fv(color_fv); + else + glColor4fv(UNIVERSE->stars_ambient); + // + ct = 0; + + GLenum dustMode; + + if (!warp_stars) + { + glEnable(GL_POINT_SMOOTH); + glPointSize(dust_size); + dustMode = GL_POINTS; + } + else + { + glEnable(GL_LINE_SMOOTH); + glLineWidth(line_size); + dustMode = GL_LINES; + } + + glBegin(dustMode); + + for (vi = 0; vi < DUST_N_PARTICLES; vi++) + { + GLVertexOOVector(vertices[vi]); + if (warp_stars) GLVertexOOVector(vector_subtract(vertices[vi], warp_vector)); + } + glEnd(); + // reapply normal conditions + glDisable(GL_FOG); + } + + CheckOpenGLErrors(@"DustEntity after drawing %@", self); +} + + +- (void)resetGraphicsState +{ + if (displayListName != 0) + { + glDeleteLists(displayListName, 1); + displayListName = 0; + } +} + +@end diff --git a/src/Core/Entities/Entity.h b/src/Core/Entities/Entity.h new file mode 100644 index 00000000..85f74439 --- /dev/null +++ b/src/Core/Entities/Entity.h @@ -0,0 +1,278 @@ +/* + +Entity.h + +Base class for entities, i.e. drawable world objects. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOCocoa.h" +#import "OOMaths.h" +#import "OOCacheManager.h" +#import "OOTypes.h" +#import "OOWeakReference.h" + +@class Universe, Geometry, CollisionRegion, ShipEntity; + + +#ifndef NDEBUG + +#define DEBUG_ALL 0xffffffff +#define DEBUG_LINKED_LISTS 0x00000001 +#define DEBUG_ENTITIES 0x00000002 +#define DEBUG_COLLISIONS 0x00000004 +#define DEBUG_DOCKING 0x00000008 +#define DEBUG_OCTREE 0x00000010 +#define DEBUG_OCTREE_TEXT 0x00000020 +#define DEBUG_BOUNDING_BOXES 0x00000040 +#define DEBUG_OCTREE_DRAW 0x00000080 +#define DEBUG_DRAW_NORMALS 0x00000100 +#define DEBUG_HIDE_HUD 0x00000200 +#define DEBUG_NO_DUST 0x00000400 +#define DEBUG_MISC 0x10000000 + + +extern uint32_t gDebugFlags; + +#endif + +#define NO_DRAW_DISTANCE_FACTOR 512.0 +#define ABSOLUTE_NO_DRAW_DISTANCE2 (2500.0 * 2500.0 * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR) +// ie. the furthest away thing we can draw is at 1280km (a 2.5km wide object would disappear at that range) + + +#define SCANNER_MAX_RANGE 25600.0 +#define SCANNER_MAX_RANGE2 655360000.0 + +#define CLOSE_COLLISION_CHECK_MAX_RANGE2 1000000000.0 + + +typedef struct +{ + double timeframe; // universal time for this frame + Vector position; // position + Quaternion orientation; // rotation + Vector k; // direction vectors +} Frame; + + +@interface Entity: OOWeakRefObject +{ + // the base object for ships/stations/anything actually + ////////////////////////////////////////////////////// + // + // @public variables: + // + // we forego encapsulation for some variables in order to + // lose the overheads of Obj-C accessor methods... + // +@public + OOUniversalID universalID; // used to reference the entity + + unsigned isParticle: 1, + isRing: 1, + isShip: 1, + isStation: 1, + isPlanet: 1, + isPlayer: 1, + isSky: 1, + isWormhole: 1, + isSubEntity: 1, + hasMoved: 1, + hasRotated: 1, + hasCollided: 1, + isSunlit: 1, + collisionTestFilter: 1, + throw_sparks: 1, + isImmuneToBreakPatternHide: 1, + isExplicitlyNotMainStation: 1; + + OOScanClass scanClass; + + GLfloat zero_distance; + GLfloat no_draw_distance; // 10 km initially + GLfloat collision_radius; + Vector position; + Quaternion orientation; + + int zero_index; + + // Linked lists of entites, sorted by position on each (world) axis + Entity *x_previous, *x_next; + Entity *y_previous, *y_next; + Entity *z_previous, *z_next; + + Entity *collision_chain; + + OOUniversalID shadingEntityID; + + Vector relativePosition; + + Entity *collider; + + CollisionRegion *collisionRegion; // initially nil - then maintained + +@protected + Vector lastPosition; + Quaternion lastOrientation; + + GLfloat distanceTravelled; // set to zero initially + + OOMatrix rotMatrix; + + Vector velocity; + + // positions+rotations for trails and trackbacks + // TODO: Can these be moved into a subclass? -- Ahruman + Frame track[256]; + int trackIndex; + OOTimeAbsolute trackTime; + + GLfloat energy; + GLfloat maxEnergy; + + BoundingBox boundingBox; + GLfloat mass; + + NSMutableArray *collidingEntities; + + OOTimeAbsolute spawnTime; + + struct JSObject *jsSelf; + +@private + OOWeakReference *_owner; + OOEntityStatus _status; +} + +- (BOOL) isShip; +- (BOOL) isStation; +- (BOOL) isSubEntity; +- (BOOL) isPlayer; +- (BOOL) isPlanet; +- (BOOL) isSun; +- (BOOL) isSky; +- (BOOL) isWormhole; + +- (BOOL) validForAddToUniverse; +- (void) addToLinkedLists; +- (void) removeFromLinkedLists; + +- (void) updateLinkedLists; + +- (void) wasAddedToUniverse; +- (void) wasRemovedFromUniverse; + +- (void) warnAboutHostiles; + +- (CollisionRegion*) collisionRegion; +- (void) setCollisionRegion:(CollisionRegion*)region; + +- (void) setUniversalID:(OOUniversalID)uid; +- (OOUniversalID) universalID; + +- (BOOL) throwingSparks; +- (void) setThrowSparks:(BOOL)value; +- (void) throwSparks; + +- (void) setOwner:(Entity *)ent; +- (id)owner; +- (ShipEntity *)parentEntity; // owner if self is subentity of owner, otherwise nil. +- (ShipEntity *)rootShipEntity; // like parentEntity, but recursive. + +- (void) setPosition:(Vector)posn; +- (void) setPositionX:(GLfloat)x y:(GLfloat)y z:(GLfloat)z; +- (Vector) position; + +- (double) zeroDistance; +- (Vector) relativePosition; +- (NSComparisonResult) compareZeroDistance:(Entity *)otherEntity; + +- (BoundingBox) boundingBox; + +- (GLfloat) mass; + +- (Quaternion) orientation; +- (void) setOrientation:(Quaternion) quat; +- (Quaternion) normalOrientation; // Historical wart: orientation.w is reversed for player; -normalOrientation corrects this. +- (void) setNormalOrientation:(Quaternion) quat; +- (void) orientationChanged; + +- (void) setVelocity:(Vector)vel; +- (Vector) velocity; +- (double) speed; + +- (GLfloat) distanceTravelled; +- (void) setDistanceTravelled:(GLfloat)value; + + +- (void) setStatus:(OOEntityStatus)stat; +- (OOEntityStatus) status; + +- (void) setScanClass:(OOScanClass)sClass; +- (OOScanClass) scanClass; + +- (void) setEnergy:(GLfloat)amount; +- (GLfloat) energy; + +- (void) setMaxEnergy:(GLfloat)amount; +- (GLfloat) maxEnergy; + +- (void) applyRoll:(GLfloat)roll andClimb:(GLfloat)climb; +- (void) applyRoll:(GLfloat)roll climb:(GLfloat) climb andYaw:(GLfloat)yaw; +- (void) moveForward:(double)amount; + +- (OOMatrix) rotationMatrix; +- (OOMatrix) drawRotationMatrix; + +- (BOOL) canCollide; +- (GLfloat) collisionRadius; +- (void) setCollisionRadius:(GLfloat)amount; +- (NSMutableArray *)collisionArray; + +- (void) update:(OOTimeDelta) delta_t; +- (void) saveToLastFrame; +- (void) savePosition:(Vector)pos atTime:(double)t_time atIndex:(int)t_index; +- (void) saveFrame:(Frame)frame atIndex:(int)t_index; +- (void) resetFramesFromFrame:(Frame) resetFrame withVelocity:(Vector) vel1; +- (BOOL) resetToTime:(double) t_frame; +- (Frame) frameAtTime:(double) t_frame; // timeframe is relative to now ie. -0.5 = half a second ago. +- (Frame) frameAtTime:(double) t_frame fromFrame:(Frame) frame_zero; // t_frame is relative to now ie. -0.5 = half a second ago. + +- (BOOL) checkCloseCollisionWith:(Entity *)other; + +- (void) takeEnergyDamage:(double) amount from:(Entity *) ent becauseOf:(Entity *) other; + +- (void) dumpState; // General "describe situtation verbosely in log" command. +- (void) dumpSelfState; // Subclasses should override this, not -dumpState, and call throught to super first. + +// Subclass repsonsibilities +- (double) findCollisionRadius; +- (Geometry*) geometry; +- (void) drawEntity:(BOOL)immediate :(BOOL)translucent; + +// For shader bindings. +- (GLfloat) universalTime; +- (GLfloat) spawnTime; +- (GLfloat) timeElapsedSinceSpawn; + +@end diff --git a/src/Core/Entities/Entity.m b/src/Core/Entities/Entity.m new file mode 100644 index 00000000..2a10d037 --- /dev/null +++ b/src/Core/Entities/Entity.m @@ -0,0 +1,1108 @@ +/* + +Entity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Entity.h" +#import "EntityOOJavaScriptExtensions.h" +#import "PlayerEntity.h" +#import "PlanetEntity.h" + +#import "OOMaths.h" +#import "Geometry.h" +#import "Universe.h" +#import "GameController.h" +#import "ResourceManager.h" +#import "OOConstToString.h" + +#import "CollisionRegion.h" + +#import "NSScannerOOExtensions.h" + +#define kOOLogUnconvertedNSLog @"unclassified.Entity" + + +static NSString * const kOOLogEntityAddToList = @"entity.linkedList.add"; +static NSString * const kOOLogEntityAddToListError = @"entity.linkedList.add.error"; +static NSString * const kOOLogEntityRemoveFromList = @"entity.linkedList.remove"; +static NSString * const kOOLogEntityRemoveFromListError = @"entity.linkedList.remove.error"; + NSString * const kOOLogEntityVerificationError = @"entity.linkedList.verify.error"; +static NSString * const kOOLogEntityUpdateError = @"entity.linkedList.update.error"; + + +@interface Entity (OOPrivate) + +- (BOOL) checkLinkedLists; + +@end + + +@implementation Entity + +- (id) init +{ + self = [super init]; + + orientation = kIdentityQuaternion; + rotMatrix = kIdentityMatrix; + position = kZeroVector; + + no_draw_distance = 100000.0; // 10 km + + collidingEntities = [[NSMutableArray alloc] init]; + + scanClass = CLASS_NOT_SET; + [self setStatus:STATUS_COCKPIT_DISPLAY]; + + spawnTime = [UNIVERSE getTime]; + + isSunlit = YES; + + return self; +} + + +- (void) dealloc +{ + [UNIVERSE ensureEntityReallyRemoved:self]; + [collidingEntities release]; + [collisionRegion release]; + [self deleteJSSelf]; + [self setOwner:nil]; + + [super dealloc]; +} + + +- (NSString *)descriptionComponents +{ + return [NSString stringWithFormat:@"ID: %u position: %@ scanClass: %@ status: %@", [self universalID], VectorDescription([self position]), ScanClassToString([self scanClass]), EntityStatusToString([self status])]; +} + + +- (BOOL)isShip +{ + return isShip; +} + + +- (BOOL)isStation +{ + return isStation; +} + + +- (BOOL)isSubEntity +{ + return isSubEntity; +} + + +- (BOOL)isPlayer +{ + return isPlayer; +} + + +- (BOOL)isPlanet +{ + return isPlanet; +} + + +- (BOOL)isSun +{ + return isPlanet && [(PlanetEntity *)self planetType] == PLANET_TYPE_SUN; +} + +- (BOOL)isSky +{ + return isSky; +} + +- (BOOL)isWormhole +{ + return isWormhole; +} + + +- (BOOL) validForAddToUniverse +{ + return YES; +} + + +- (void) addToLinkedLists +{ +#ifndef NDEBUG + if (gDebugFlags & DEBUG_LINKED_LISTS) + OOLog(kOOLogEntityAddToList, @"DEBUG adding entity %@ to linked lists", self); +#endif + // + // insert at the start + if (UNIVERSE) + { + x_previous = nil; x_next = UNIVERSE->x_list_start; + // move UP the list + while ((x_next)&&(x_next->position.x - x_next->collision_radius < position.x - collision_radius)) + { + x_previous = x_next; + x_next = x_next->x_next; + } + if (x_next) x_next->x_previous = self; + if (x_previous) x_previous->x_next = self; + else UNIVERSE->x_list_start = self; + + y_previous = nil; y_next = UNIVERSE->y_list_start; + // move UP the list + while ((y_next)&&(y_next->position.y - y_next->collision_radius < position.y - collision_radius)) + { + y_previous = y_next; + y_next = y_next->y_next; + } + if (y_next) y_next->y_previous = self; + if (y_previous) y_previous->y_next = self; + else UNIVERSE->y_list_start = self; + + z_previous = nil; z_next = UNIVERSE->z_list_start; + // move UP the list + while ((z_next)&&(z_next->position.z - z_next->collision_radius < position.z - collision_radius)) + { + z_previous = z_next; + z_next = z_next->z_next; + } + if (z_next) z_next->z_previous = self; + if (z_previous) z_previous->z_next = self; + else UNIVERSE->z_list_start = self; + + } + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_LINKED_LISTS) + { + if (![self checkLinkedLists]) + { + OOLog(kOOLogEntityAddToListError, @"DEBUG LINKED LISTS - problem encountered while adding %@ to linked lists", self); + [UNIVERSE obj_dump]; + + exit(-1); + } + } +#endif +} + + +- (void) removeFromLinkedLists +{ +#ifndef NDEBUG + if (gDebugFlags & DEBUG_LINKED_LISTS) + OOLog(kOOLogEntityRemoveFromList, @"DEBUG removing entity %@ from linked lists", self); +#endif + + if ((x_next == nil)&&(x_previous == nil)) // removed already! + return; + + // make sure the starting point is still correct + if (UNIVERSE) + { + if ((UNIVERSE->x_list_start == self)&&(x_next)) + UNIVERSE->x_list_start = x_next; + if ((UNIVERSE->y_list_start == self)&&(y_next)) + UNIVERSE->y_list_start = y_next; + if ((UNIVERSE->z_list_start == self)&&(z_next)) + UNIVERSE->z_list_start = z_next; + } + // + if (x_previous) x_previous->x_next = x_next; + if (x_next) x_next->x_previous = x_previous; + // + if (y_previous) y_previous->y_next = y_next; + if (y_next) y_next->y_previous = y_previous; + // + if (z_previous) z_previous->z_next = z_next; + if (z_next) z_next->z_previous = z_previous; + // + x_previous = nil; x_next = nil; + y_previous = nil; y_next = nil; + z_previous = nil; z_next = nil; + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_LINKED_LISTS) + { + if (![self checkLinkedLists]) + { + OOLog(kOOLogEntityRemoveFromListError, @"DEBUG LINKED LISTS - problem encountered while removing %@ from linked lists", self); + [UNIVERSE obj_dump]; + + exit(-1); + } + } +#endif +} + + +- (BOOL) checkLinkedLists +{ + // DEBUG check for loops + if (UNIVERSE->n_entities > 0) + { + int n; + Entity *check, *last; + // + last = nil; + // + n = UNIVERSE->n_entities; + check = UNIVERSE->x_list_start; + while ((n--)&&(check)) + { + last = check; + check = check->x_next; + } + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken x_next %@ list (%d) ***", UNIVERSE->x_list_start, n); + return NO; + } + // + n = UNIVERSE->n_entities; + check = last; + while ((n--)&&(check)) check = check->x_previous; + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken x_previous %@ list (%d) ***", UNIVERSE->x_list_start, n); + return NO; + } + // + n = UNIVERSE->n_entities; + check = UNIVERSE->y_list_start; + while ((n--)&&(check)) + { + last = check; + check = check->y_next; + } + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken y_next %@ list (%d) ***", UNIVERSE->y_list_start, n); + return NO; + } + // + n = UNIVERSE->n_entities; + check = last; + while ((n--)&&(check)) check = check->y_previous; + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken y_previous %@ list (%d) ***", UNIVERSE->y_list_start, n); + return NO; + } + // + n = UNIVERSE->n_entities; + check = UNIVERSE->z_list_start; + while ((n--)&&(check)) + { + last = check; + check = check->z_next; + } + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken z_next %@ list (%d) ***", UNIVERSE->z_list_start, n); + return NO; + } + // + n = UNIVERSE->n_entities; + check = last; + while ((n--)&&(check)) check = check->z_previous; + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken z_previous %@ list (%d) ***", UNIVERSE->z_list_start, n); + return NO; + } + } + return YES; +} + + +- (void) updateLinkedLists +{ + if (!UNIVERSE) + return; // not in the UNIVERSE - don't do this! + if ((x_next == nil)&&(x_previous == nil)) + return; // not in the lists - don't do this! + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_LINKED_LISTS) + { + if (![self checkLinkedLists]) + { + OOLog(kOOLogEntityVerificationError, @"DEBUG LINKED LISTS problem encountered before updating linked lists for %@", self); + [UNIVERSE obj_dump]; + + exit(-1); + } + } +#endif + + // update position in linked list for position.x + // take self out of list.. + if (x_previous) x_previous->x_next = x_next; + if (x_next) x_next->x_previous = x_previous; + // sink DOWN the list + while ((x_previous)&&(x_previous->position.x - x_previous->collision_radius > position.x - collision_radius)) + { + x_next = x_previous; + x_previous = x_previous->x_previous; + } + // bubble UP the list + while ((x_next)&&(x_next->position.x - x_next->collision_radius < position.x - collision_radius)) + { + x_previous = x_next; + x_next = x_next->x_next; + } + if (x_next) // insert self into the list before x_next.. + x_next->x_previous = self; + if (x_previous) // insert self into the list after x_previous.. + x_previous->x_next = self; + if ((x_previous == nil)&&(UNIVERSE)) // if we're the first then tell the UNIVERSE! + UNIVERSE->x_list_start = self; + + // update position in linked list for position.y + // take self out of list.. + if (y_previous) y_previous->y_next = y_next; + if (y_next) y_next->y_previous = y_previous; + // sink DOWN the list + while ((y_previous)&&(y_previous->position.y - y_previous->collision_radius > position.y - collision_radius)) + { + y_next = y_previous; + y_previous = y_previous->y_previous; + } + // bubble UP the list + while ((y_next)&&(y_next->position.y - y_next->collision_radius < position.y - collision_radius)) + { + y_previous = y_next; + y_next = y_next->y_next; + } + if (y_next) // insert self into the list before y_next.. + y_next->y_previous = self; + if (y_previous) // insert self into the list after y_previous.. + y_previous->y_next = self; + if ((y_previous == nil)&&(UNIVERSE)) // if we're the first then tell the UNIVERSE! + UNIVERSE->y_list_start = self; + + // update position in linked list for position.z + // take self out of list.. + if (z_previous) z_previous->z_next = z_next; + if (z_next) z_next->z_previous = z_previous; + // sink DOWN the list + while ((z_previous)&&(z_previous->position.z - z_previous->collision_radius > position.z - collision_radius)) + { + z_next = z_previous; + z_previous = z_previous->z_previous; + } + // bubble UP the list + while ((z_next)&&(z_next->position.z - z_next->collision_radius < position.z - collision_radius)) + { + z_previous = z_next; + z_next = z_next->z_next; + } + if (z_next) // insert self into the list before z_next.. + z_next->z_previous = self; + if (z_previous) // insert self into the list after z_previous.. + z_previous->z_next = self; + if ((z_previous == nil)&&(UNIVERSE)) // if we're the first then tell the UNIVERSE! + UNIVERSE->z_list_start = self; + + // done +#ifndef NDEBUG + if (gDebugFlags & DEBUG_LINKED_LISTS) + { + if (![self checkLinkedLists]) + { + OOLog(kOOLogEntityUpdateError, @"DEBUG LINKED LISTS problem encountered after updating linked lists for %@", self); + [UNIVERSE obj_dump]; + + exit(-1); + } + } +#endif +} + + +- (void) wasAddedToUniverse +{ + // Do nothing +} + + +- (void) wasRemovedFromUniverse +{ + // Do nothing +} + + +- (void) warnAboutHostiles +{ + // do nothing for now, this can be expanded in sub classes + OOLog(@"general.error.subclassResponsibility.Entity-warnAboutHostiles", @"***** Entity does nothing in warnAboutHostiles"); +} + + +- (CollisionRegion*) collisionRegion +{ + return collisionRegion; +} + + +- (void) setCollisionRegion: (CollisionRegion*) region +{ + if (collisionRegion) [collisionRegion release]; + collisionRegion = [region retain]; +} + + +- (void) setUniversalID:(OOUniversalID)uid +{ + universalID = uid; +} + + +- (OOUniversalID) universalID +{ + return universalID; +} + + +- (BOOL) throwingSparks +{ + return throw_sparks; +} + + +- (void) setThrowSparks:(BOOL) value +{ + throw_sparks = value; +} + + +- (void) throwSparks; +{ + // do nothing for now +} + + +- (void) setOwner:(Entity *)ent +{ + [_owner release]; + _owner = [ent weakRetain]; +} + + +- (id) owner +{ + return [_owner weakRefUnderlyingObject]; +} + + +- (ShipEntity *)parentEntity +{ + id owner = [self owner]; + if ([owner isShipWithSubEntityShip:self]) return owner; + return nil; +} + + +- (id) superShaderBindingTarget +{ + return [self parentEntity]; +} + + +- (ShipEntity *) rootShipEntity +{ + ShipEntity *parent = [self parentEntity]; + if (parent != nil) return [parent rootShipEntity]; + if ([self isShip]) return (ShipEntity *)self; + return nil; +} + + +- (void) setPosition:(Vector) posn +{ + position = posn; +} + + +- (void) setPositionX:(GLfloat)x y:(GLfloat)y z:(GLfloat)z +{ + position.x = x; + position.y = y; + position.z = z; +} + + +- (double) zeroDistance +{ + return zero_distance; +} + + +- (Vector) relativePosition +{ + return relativePosition; +} + + +- (NSComparisonResult) compareZeroDistance:(Entity *)otherEntity; +{ + if ((otherEntity)&&(zero_distance > otherEntity->zero_distance)) + return NSOrderedAscending; + else + return NSOrderedDescending; +} + + +- (BoundingBox) boundingBox +{ + return boundingBox; +} + + +- (GLfloat) mass +{ + return mass; +} + + +- (void) setOrientation:(Quaternion) quat +{ + orientation = quat; + [self orientationChanged]; +} + + +- (Quaternion) orientation +{ + return orientation; +} + + +- (Quaternion) normalOrientation +{ + return [self orientation]; +} + + +- (void) setNormalOrientation:(Quaternion) quat +{ + [self setOrientation:quat]; +} + + +- (void) orientationChanged +{ + quaternion_normalize(&orientation); + rotMatrix = OOMatrixForQuaternionRotation(orientation); +} + + +- (void) setVelocity:(Vector) vel +{ + velocity = vel; +} + + +- (Vector) velocity +{ + return velocity; +} + + +- (double) speed +{ + return magnitude2(velocity); +} + + +- (GLfloat) distanceTravelled +{ + return distanceTravelled; +} + + +- (void) setDistanceTravelled: (GLfloat) value +{ + distanceTravelled = value; +} + + +- (void) setStatus:(OOEntityStatus) stat +{ + _status = stat; +} + + +- (OOEntityStatus) status +{ + return _status; +} + + +- (void) setScanClass:(OOScanClass)sClass +{ + scanClass = sClass; +} + + +- (OOScanClass) scanClass +{ + return scanClass; +} + + +- (void) setEnergy:(GLfloat) amount +{ + energy = amount; +} + + +- (GLfloat) energy +{ + return energy; +} + + +- (void) setMaxEnergy:(GLfloat)amount +{ + maxEnergy = amount; +} + + +- (GLfloat) maxEnergy +{ + return maxEnergy; +} + + +- (void) applyRoll:(GLfloat) roll andClimb:(GLfloat) climb +{ + if ((roll == 0.0)&&(climb == 0.0)&&(!hasRotated)) + return; + + if (roll) + quaternion_rotate_about_z(&orientation, -roll); + if (climb) + quaternion_rotate_about_x(&orientation, -climb); + + [self orientationChanged]; +} + + +- (void) applyRoll:(GLfloat) roll climb:(GLfloat) climb andYaw:(GLfloat) yaw +{ + if ((roll == 0.0)&&(climb == 0.0)&&(yaw == 0.0)&&(!hasRotated)) + return; + + if (roll) + quaternion_rotate_about_z(&orientation, -roll); + if (climb) + quaternion_rotate_about_x(&orientation, -climb); + if (yaw) + quaternion_rotate_about_y(&orientation, -yaw); + + [self orientationChanged]; +} + + +- (void) moveForward:(double) amount +{ + Vector forward = vector_forward_from_quaternion(orientation); + distanceTravelled += amount; + position.x += amount * forward.x; + position.y += amount * forward.y; + position.z += amount * forward.z; +} + + +- (OOMatrix) rotationMatrix +{ + return rotMatrix; +} + + +- (OOMatrix) drawRotationMatrix +{ + return rotMatrix; +} + + +- (Vector) position +{ + return position; +} + + +- (BOOL) canCollide +{ + return YES; +} + + +- (GLfloat) collisionRadius +{ + return collision_radius; +} + + +- (void) setCollisionRadius:(GLfloat) amount +{ + collision_radius = amount; +} + + +- (NSMutableArray *) collisionArray +{ + return collidingEntities; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + if (player) + { + if ([self status] != STATUS_COCKPIT_DISPLAY) + relativePosition = vector_between(player->position, position); + else + relativePosition = position; + // + zero_distance = magnitude2(relativePosition); + } + else + zero_distance = -1; + + hasMoved = !vector_equal(position, lastPosition); + hasRotated = !quaternion_equal(orientation, lastOrientation); + lastPosition = position; + lastOrientation = orientation; +} + + +- (void) saveToLastFrame +{ + double t_now = [UNIVERSE getTime]; + if (t_now >= trackTime + 0.1) // update every 1/10 of a second + { + // save previous data + trackTime = t_now; + track[trackIndex].position = position; + track[trackIndex].orientation = orientation; + track[trackIndex].timeframe = trackTime; + track[trackIndex].k = vector_forward_from_quaternion(orientation); + trackIndex = (trackIndex + 1 ) & 0xff; + } +} + + +- (void) savePosition:(Vector)pos atTime:(double)t_time atIndex:(int)t_index +{ + trackTime = t_time; + track[t_index].position = pos; + track[t_index].timeframe = t_time; + trackIndex = (t_index + 1 ) & 0xff; +} + + +- (void) saveFrame:(Frame)frame atIndex:(int)t_index +{ + track[t_index] = frame; + trackTime = frame.timeframe; + trackIndex = (t_index + 1 ) & 0xff; +} + +// reset frames +// +- (void) resetFramesFromFrame:(Frame) resetFrame withVelocity:(Vector) vel1 +{ + Vector v1 = make_vector(0.1 * vel1.x, 0.1 * vel1.y, 0.1 * vel1.z); + double t_now = [UNIVERSE getTime]; + Vector pos = resetFrame.position; + Vector vk = resetFrame.k; + Quaternion qr = resetFrame.orientation; + int i; + for (i = 0; i < 256; i++) + { + track[255-i].position = make_vector(pos.x - i * v1.x, pos.y - i * v1.y, pos.z - i * v1.z); + track[255-i].timeframe = t_now - 0.1 * i; + track[255-i].orientation = qr; + track[255-i].k = vk; + } + trackTime = t_now; + trackIndex = 0; +} + + +- (BOOL) resetToTime:(double) t_frame // timeframe is relative to now ie. -0.5 = half a second ago. +{ + if (t_frame >= 0) + return NO; + + Frame selectedFrame = [self frameAtTime:t_frame]; + [self setPosition:selectedFrame.position]; + [self setOrientation:selectedFrame.orientation]; + return YES; +} + + +- (Frame) frameAtTime:(double) t_frame // t_frame is relative to now ie. -0.5 = half a second ago. +{ + Frame result; + result.position = position; + result.orientation = orientation; + result.timeframe = [UNIVERSE getTime]; + result.k = vector_forward_from_quaternion(orientation); + // + if (t_frame >= 0.0) + return result; + // + double moment_in_time = [UNIVERSE getTime] + t_frame; + if (moment_in_time >= trackTime) // between the last saved frame and now + { + int t1 = (trackIndex - 1)&0xff; // last saved moment + double period = result.timeframe - trackTime; + double f0 = (result.timeframe - moment_in_time)/period; + double f1 = 1.0 - f0; + Vector posn; + posn.x = f0 * result.position.x + f1 * track[t1].position.x; + posn.y = f0 * result.position.y + f1 * track[t1].position.y; + posn.z = f0 * result.position.z + f1 * track[t1].position.z; + Quaternion qrot; + qrot.w = f0 * result.orientation.w + f1 * track[t1].orientation.w; + qrot.x = f0 * result.orientation.x + f1 * track[t1].orientation.x; + qrot.y = f0 * result.orientation.y + f1 * track[t1].orientation.y; + qrot.z = f0 * result.orientation.z + f1 * track[t1].orientation.z; + result.position = posn; + result.orientation = qrot; + result.timeframe = moment_in_time; + result.k = vector_forward_from_quaternion(qrot); + return result; + } + // + if (moment_in_time < track[trackIndex].timeframe) // more than 256 frames back + { + return track[trackIndex]; + } + // + int t1 = (trackIndex - 1)&0xff; + while (moment_in_time < track[t1].timeframe) + t1 = (t1 - 1) & 0xff; + int t0 = (t1 + 1) & 0xff; + // interpolate between t0 and t1 + double period = track[0].timeframe - track[1].timeframe; + double f0 = (track[t0].timeframe - moment_in_time)/period; + double f1 = 1.0 - f0; + Vector posn; + posn.x = f0 * track[t0].position.x + f1 * track[t1].position.x; + posn.y = f0 * track[t0].position.y + f1 * track[t1].position.y; + posn.z = f0 * track[t0].position.z + f1 * track[t1].position.z; + Quaternion qrot; + qrot.w = f0 * track[t0].orientation.w + f1 * track[t1].orientation.w; + qrot.x = f0 * track[t0].orientation.x + f1 * track[t1].orientation.x; + qrot.y = f0 * track[t0].orientation.y + f1 * track[t1].orientation.y; + qrot.z = f0 * track[t0].orientation.z + f1 * track[t1].orientation.z; + result.position = posn; + result.orientation = qrot; + result.timeframe = moment_in_time; + result.k = vector_forward_from_quaternion(qrot); + return result; +} + + +- (Frame) frameAtTime:(double) t_frame fromFrame:(Frame) frame_zero // t_frame is relative to now ie. -0.5 = half a second ago. +{ + Frame result = frame_zero; + // + if (t_frame >= 0.0) + return result; + // + double moment_in_time = [UNIVERSE getTime] + t_frame; + if (moment_in_time > trackTime) // between the last saved frame and now + { + Frame fr1 = track[(trackIndex - 1)&0xff]; // last saved moment + double period = (moment_in_time - t_frame) - trackTime; + double f1 = -t_frame/period; + double f0 = 1.0 - f1; + + Vector posn; + posn.x = f0 * result.position.x + f1 * fr1.position.x; + posn.y = f0 * result.position.y + f1 * fr1.position.y; + posn.z = f0 * result.position.z + f1 * fr1.position.z; + Quaternion qrot; + qrot.w = f0 * result.orientation.w + f1 * fr1.orientation.w; + qrot.x = f0 * result.orientation.x + f1 * fr1.orientation.x; + qrot.y = f0 * result.orientation.y + f1 * fr1.orientation.y; + qrot.z = f0 * result.orientation.z + f1 * fr1.orientation.z; + result.position = posn; + result.orientation = qrot; + result.timeframe = moment_in_time; + result.k = vector_forward_from_quaternion(qrot); + return result; + } + // + if (moment_in_time < track[trackIndex].timeframe) // more than 256 frames back + { + return track[trackIndex]; + } + // + int t1 = (trackIndex - 1)&0xff; + while (moment_in_time < track[t1].timeframe) + t1 = (t1 - 1) & 0xff; + int t0 = (t1 + 1) & 0xff; + // interpolate between t0 and t1 + double period = track[t0].timeframe - track[t1].timeframe; + double f0 = (moment_in_time - track[t1].timeframe)/period; + double f1 = 1.0 - f0; + + Vector posn; + posn.x = f0 * track[t0].position.x + f1 * track[t1].position.x; + posn.y = f0 * track[t0].position.y + f1 * track[t1].position.y; + posn.z = f0 * track[t0].position.z + f1 * track[t1].position.z; + Quaternion qrot; + qrot.w = f0 * track[t0].orientation.w + f1 * track[t1].orientation.w; + qrot.x = f0 * track[t0].orientation.x + f1 * track[t1].orientation.x; + qrot.y = f0 * track[t0].orientation.y + f1 * track[t1].orientation.y; + qrot.z = f0 * track[t0].orientation.z + f1 * track[t1].orientation.z; + result.position = posn; + result.orientation = qrot; + result.timeframe = moment_in_time; + result.k = vector_forward_from_quaternion(qrot); + return result; +} + + +- (BOOL) checkCloseCollisionWith:(Entity *)other +{ + return YES; +} + + +- (double)findCollisionRadius +{ + OOLogGenericSubclassResponsibility(); + return 0; +} + + +- (Geometry *)geometry +{ + OOLogGenericSubclassResponsibility(); + return nil; +} + + +- (void) drawEntity:(BOOL)immediate :(BOOL)translucent +{ + OOLogGenericSubclassResponsibility(); +} + + +- (void) takeEnergyDamage:(double) amount from:(Entity *) ent becauseOf:(Entity *) other +{ + +} + + +- (void)dumpState +{ + if (OOLogWillDisplayMessagesInClass(@"dumpState")) + { + OOLog(@"dumpState", @"State for %@:", self); + OOLogPushIndent(); + OOLogIndent(); + NS_DURING + [self dumpSelfState]; + NS_HANDLER + NS_ENDHANDLER + OOLogPopIndent(); + } +} + + +- (void)dumpSelfState +{ + NSMutableArray *flags = nil; + NSString *flagsString = nil; + Entity *owner = [self owner]; + + OOLog(@"dumpState.entity", @"Universal ID: %u", universalID); + OOLog(@"dumpState.entity", @"Scan class: %@", ScanClassToString(scanClass)); + OOLog(@"dumpState.entity", @"Status: %@", EntityStatusToString([self status])); + OOLog(@"dumpState.entity", @"Position: %@", VectorDescription(position)); + OOLog(@"dumpState.entity", @"Orientation: %@", QuaternionDescription(orientation)); + OOLog(@"dumpState.entity", @"Distance travelled: %g", distanceTravelled); + OOLog(@"dumpState.entity", @"Energy: %g of %g", energy, maxEnergy); + OOLog(@"dumpState.entity", @"Mass: %g", mass); + if (owner != nil) OOLog(@"dumpState.entity", @"Owner: %@", owner); + + flags = [NSMutableArray array]; + #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; } + ADD_FLAG_IF_SET(isParticle); + ADD_FLAG_IF_SET(isRing); + ADD_FLAG_IF_SET(isShip); + ADD_FLAG_IF_SET(isStation); + ADD_FLAG_IF_SET(isPlanet); + ADD_FLAG_IF_SET(isPlayer); + ADD_FLAG_IF_SET(isSky); + ADD_FLAG_IF_SET(isWormhole); + ADD_FLAG_IF_SET(isSubEntity); + ADD_FLAG_IF_SET(hasMoved); + ADD_FLAG_IF_SET(hasRotated); + ADD_FLAG_IF_SET(isSunlit); + ADD_FLAG_IF_SET(collisionTestFilter); + ADD_FLAG_IF_SET(throw_sparks); + flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none"; + OOLog(@"dumpState.entity", @"Flags: %@", flagsString); +} + + +- (void)subEntityReallyDied:(ShipEntity *)sub +{ + OOLog(@"entity.bug", @"%s called for non-ship entity %p by %p", __FUNCTION__, self, sub); +} + + +// For shader bindings. +- (GLfloat)universalTime +{ + return [UNIVERSE getTime]; +} + + +- (GLfloat)spawnTime +{ + return spawnTime; +} + + +- (GLfloat)timeElapsedSinceSpawn +{ + return [UNIVERSE getTime] - spawnTime; +} + +@end diff --git a/src/Core/Entities/OOEntityWithDrawable.h b/src/Core/Entities/OOEntityWithDrawable.h new file mode 100644 index 00000000..60b251d7 --- /dev/null +++ b/src/Core/Entities/OOEntityWithDrawable.h @@ -0,0 +1,40 @@ +/* + +OOEntityWithDrawable.h + +Abstract intermediate class for entities which use an OODrawable to render. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Entity.h" + +@class OODrawable; + + +@interface OOEntityWithDrawable: Entity +{ + OODrawable *drawable; +} + +- (OODrawable *)drawable; +- (void)setDrawable:(OODrawable *)drawable; + +@end diff --git a/src/Core/Entities/OOEntityWithDrawable.m b/src/Core/Entities/OOEntityWithDrawable.m new file mode 100644 index 00000000..0ffe30d5 --- /dev/null +++ b/src/Core/Entities/OOEntityWithDrawable.m @@ -0,0 +1,91 @@ +/* + +OOEntityWithDrawable.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOEntityWithDrawable.h" +#import "OODrawable.h" +#import "Universe.h" + + +@implementation OOEntityWithDrawable + +- (void)dealloc +{ + [drawable release]; + drawable = nil; + + [super dealloc]; +} + + +- (OODrawable *)drawable +{ + return drawable; +} + + +- (void)setDrawable:(OODrawable *)inDrawable +{ + if (inDrawable != drawable) + { + [drawable autorelease]; + drawable = [inDrawable retain]; + [drawable setBindingTarget:self]; + + collision_radius = [drawable collisionRadius]; + no_draw_distance = [drawable maxDrawDistance]; + boundingBox = [drawable boundingBox]; + } +} + + +- (double)findCollisionRadius +{ + return [drawable collisionRadius]; +} + + +- (Geometry *)geometry +{ + return [drawable geometry]; +} + + +- (void)drawEntity:(BOOL)immediate :(BOOL)translucent +{ + if (no_draw_distance < zero_distance) + { + // Don't draw. + return; + } + + if ([UNIVERSE wireframeGraphics]) GLDebugWireframeModeOn(); + + if (translucent) [drawable renderTranslucentParts]; + else [drawable renderOpaqueParts]; + + if ([UNIVERSE wireframeGraphics]) GLDebugWireframeModeOff(); +} + +@end diff --git a/src/Core/Entities/OOSelfDrawingEntity.h b/src/Core/Entities/OOSelfDrawingEntity.h new file mode 100644 index 00000000..c2b38951 --- /dev/null +++ b/src/Core/Entities/OOSelfDrawingEntity.h @@ -0,0 +1,104 @@ +/* + +OOSelfDrawingEntity.h + +Abstract intermediate class for entities which draw themselves directly using +mesh data contained in the object itself. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Entity.h" +#import "OOMesh.h" // Currently, we're sharing structures and constants with OOMesh + + +enum +{ + MAX_VERTICES_PER_ENTITY = 320, + MAX_FACES_PER_ENTITY = 512, + MAX_TEXTURES_PER_ENTITY = 8, + MAX_VERTICES_PER_FACE = 16, + + NUM_VERTEX_ARRAY_RANGES = 16 +}; + + +typedef char OOStr255[256]; // Not the same as the previously-abused Str255 + + +typedef struct +{ + GLfloat red; + GLfloat green; + GLfloat blue; + + Vector normal; + + unsigned n_verts; + + GLint vertex[MAX_VERTICES_PER_FACE]; + + OOStr255 textureFileName; + GLuint textureName; + GLfloat s[MAX_VERTICES_PER_FACE]; + GLfloat t[MAX_VERTICES_PER_FACE]; +} Face; + + +typedef struct +{ + GLint index_array[3 * MAX_FACES_PER_ENTITY]; // triangles + GLfloat texture_uv_array[3 * MAX_FACES_PER_ENTITY * 2]; + Vector vertex_array[3 * MAX_FACES_PER_ENTITY]; + Vector normal_array[3 * MAX_FACES_PER_ENTITY]; + + int n_triangles; // Actually number of entries, i.e. triangle count * 3. +} EntityData; + + +@interface OOSelfDrawingEntity: Entity +{ + uint8_t isSmoothShaded: 1; + + OOMeshMaterialCount textureCount; + OOMeshVertexCount vertexCount; + OOMeshFaceCount faceCount; + + NSString *basefile; + + Vector vertices[MAX_VERTICES_PER_ENTITY]; + Vector vertex_normal[MAX_VERTICES_PER_ENTITY]; + Face faces[MAX_FACES_PER_ENTITY]; + GLuint displayListName; + + EntityData entityData; + NSRange triangle_range[MAX_TEXTURES_PER_ENTITY]; + OOStr255 textureFileName[MAX_TEXTURES_PER_ENTITY]; + GLuint textureNames[MAX_TEXTURES_PER_ENTITY]; +} + + +- (void) setModelName:(NSString *)modelName; +- (NSString *) modelName; + + +- (void)generateDisplayList; + +@end diff --git a/src/Core/Entities/OOSelfDrawingEntity.m b/src/Core/Entities/OOSelfDrawingEntity.m new file mode 100644 index 00000000..31e53895 --- /dev/null +++ b/src/Core/Entities/OOSelfDrawingEntity.m @@ -0,0 +1,1163 @@ +/* + +OOSelfDrawingEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOSelfDrawingEntity.h" +#import "Universe.h" +#import "Geometry.h" +#import "ResourceManager.h" +#import "OOOpenGLExtensionManager.h" +#import "OOGraphicsResetManager.h" + +#if !OOLITE_MAC_OS_X +#define NEED_STRLCPY +#endif + +#import "bsd_string.h" + + +static NSString * const kOOLogEntityDataNotFound = @"entity.loadMesh.failed.fileNotFound"; +static NSString * const kOOLogEntityTooManyVertices = @"entity.loadMesh.failed.tooManyVertices"; +static NSString * const kOOLogEntityTooManyFaces = @"entity.loadMesh.failed.tooManyFaces"; + + +@interface OOSelfDrawingEntity (Private) + +- (void)loadData:(NSString *)filename; +- (void)checkNormalsAndAdjustWinding; +- (void)calculateVertexNormals; + +- (NSDictionary *)modelData; +- (BOOL)setModelFromModelData:(NSDictionary*) dict; + +- (Vector)normalForVertex:(int)v_index withSharedRedValue:(GLfloat)red_value; + +- (void)fakeTexturesWithImageFile: (NSString *) textureFile andMaxSize:(NSSize) maxSize; + +- (void)setUpVertexArrays; + +@end + + +@interface OOCacheManager (OSSelfDrawingEntity) + ++ (NSDictionary *)entityDataForName:(NSString *)inShipName; ++ (void)setEntityData:(NSDictionary *)inData forName:(NSString *)inShipName; + +@end + + +@implementation OOSelfDrawingEntity + +- (id)init +{ + self = [super init]; + if (self == nil) return nil; + + basefile = @"No Model"; + [[OOGraphicsResetManager sharedManager] registerClient:self]; + + return self; +} + + +- (void) dealloc +{ + [basefile release]; + [[OOGraphicsResetManager sharedManager] unregisterClient:self]; + glDeleteLists(displayListName, 1); + + [super dealloc]; +} + + +- (void) setModelName:(NSString *)modelName +{ + NSAutoreleasePool* mypool = [[NSAutoreleasePool alloc] init]; + + [basefile autorelease]; + basefile = [modelName retain]; + + glDeleteLists(displayListName,1); + displayListName = 0; + + NS_DURING + [self loadData:basefile]; + NS_HANDLER + if ([[localException name] isEqual: OOLITE_EXCEPTION_DATA_NOT_FOUND]) + { + OOLog(kOOLogFileNotFound, @"***** Oolite Data Not Found Exception : '%@' in %s *****", [localException reason], __FUNCTION__); + } + [localException retain]; + [mypool release]; + [localException autorelease]; + [localException raise]; + NS_ENDHANDLER + + [self checkNormalsAndAdjustWinding]; + + // set the collision radius + collision_radius = [self findCollisionRadius]; + + [mypool release]; +} + + +- (NSString *) modelName +{ + return basefile; +} + + +- (BOOL) isSmoothShaded +{ + return isSmoothShaded; +} + + +- (void) setSmoothShaded:(BOOL) value +{ + isSmoothShaded = value; +} + + +- (Geometry*) geometry +{ + Geometry* result = [(Geometry *)[Geometry alloc] initWithCapacity: faceCount]; + int i; + for (i = 0; i < faceCount; i++) + { + Triangle tri; + tri.v[0] = vertices[faces[i].vertex[0]]; + tri.v[1] = vertices[faces[i].vertex[1]]; + tri.v[2] = vertices[faces[i].vertex[2]]; + [result addTriangle: tri]; + } + return [result autorelease]; +} + + +- (void) drawSubEntity:(BOOL) immediate :(BOOL) translucent +{ + Entity* my_owner = [self owner]; + if (my_owner) + { + // this test provides an opportunity to do simple LoD culling + // + zero_distance = [my_owner zeroDistance]; + if (zero_distance > no_draw_distance) + { + return; // TOO FAR AWAY + } + } + if ([self status] == STATUS_ACTIVE) + { + Vector abspos = position; // STATUS_ACTIVE means it is in control of it's own orientation + Entity *last = nil; + Entity *father = my_owner; + OOMatrix r_mat; + + while ((father)&&(father != last)) + { + r_mat = [father drawRotationMatrix]; + abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]); + + last = father; + father = [father owner]; + } + glPopMatrix(); // one down + glPushMatrix(); + // position and orientation is absolute + GLTranslateOOVector(abspos); + GLMultOOMatrix(rotMatrix); + + [self drawEntity:immediate :translucent]; + } + else + { + glPushMatrix(); + + GLTranslateOOVector(position); + GLMultOOMatrix(rotMatrix); + + [self drawEntity:immediate :translucent]; + + glPopMatrix(); + } +} + + +- (void) generateDisplayList +{ + displayListName = glGenLists(1); + if (displayListName != 0) + { + glNewList(displayListName, GL_COMPILE); + [self drawEntity:YES:NO]; // immediate YES translucent NO + glEndList(); + } +} + +@end + + +@implementation OOSelfDrawingEntity (Private) + +- (NSDictionary*)modelData +{ + NSMutableDictionary* mdict = [NSMutableDictionary dictionaryWithCapacity:8]; + [mdict setObject:[NSNumber numberWithInt: vertexCount] forKey:@"vertexCount"]; + [mdict setObject:[NSData dataWithBytes: vertices length: sizeof(Vector)*vertexCount] forKey:@"vertices"]; + [mdict setObject:[NSData dataWithBytes: vertex_normal length: sizeof(Vector)*vertexCount] forKey:@"normals"]; + [mdict setObject:[NSNumber numberWithInt: faceCount] forKey:@"faceCount"]; + [mdict setObject:[NSData dataWithBytes: faces length: sizeof(Face)*faceCount] forKey:@"faces"]; + return [NSDictionary dictionaryWithDictionary:mdict]; +} + + +- (BOOL)setModelFromModelData:(NSDictionary*) dict +{ + vertexCount = [[dict objectForKey:@"vertexCount"] intValue]; + faceCount = [[dict objectForKey:@"faceCount"] intValue]; + NSData* vdata = (NSData*)[dict objectForKey:@"vertices"]; + NSData* ndata = (NSData*)[dict objectForKey:@"normals"]; + NSData* fdata = (NSData*)[dict objectForKey:@"faces"]; + if ((vdata) && (ndata) && (fdata)) + { + Vector* vbytes = (Vector*)[vdata bytes]; + Vector* nbytes = (Vector*)[ndata bytes]; + Face* fbytes = (Face*)[fdata bytes]; + int i; + for (i = 0; i < vertexCount; i++) + { + vertices[i] = vbytes[i]; + vertex_normal[i] = nbytes[i]; + } + for (i = 0; i < faceCount; i++) + { + faces[i] = fbytes[i]; + } + return YES; + } + else + { + return NO; + } +} + + +- (void)resetGraphicsState +{ + if (displayListName != 0) + { + glDeleteLists(displayListName, 1); + displayListName = 0; + } +} + + +- (void)loadData:(NSString *) filename +{ + NSScanner *scanner; + NSDictionary *cacheData = nil; + NSString *data = nil; + NSMutableArray *lines; + BOOL failFlag = NO; + NSString *failString = @"***** "; + unsigned i, j; + + BOOL using_preloaded = NO; + + // TODO: rejigger this to look for the file and check modification date. + cacheData = [OOCacheManager entityDataForName:filename]; + if (cacheData != nil) + { + if ([self setModelFromModelData:cacheData]) using_preloaded = YES; + } + + if (!using_preloaded) + { + data = [ResourceManager stringFromFilesNamed:filename inFolder:@"Models"]; + if (data == nil) + { + // Model not found + OOLog(kOOLogEntityDataNotFound, @"***** ERROR: could not find '%@'.", filename); + [NSException raise:OOLITE_EXCEPTION_DATA_NOT_FOUND format:@"No data for model called '%@' could be found in %@.", filename, [ResourceManager paths]]; + } + + // strip out comments and commas between values + // + lines = [NSMutableArray arrayWithArray:[data componentsSeparatedByString:@"\n"]]; + for (i = 0; i < [ lines count]; i++) + { + NSString *line = [lines objectAtIndex:i]; + NSArray *parts; + // + // comments + // + parts = [line componentsSeparatedByString:@"#"]; + line = [parts objectAtIndex:0]; + parts = [line componentsSeparatedByString:@"//"]; + line = [parts objectAtIndex:0]; + // + // commas + // + line = [[line componentsSeparatedByString:@","] componentsJoinedByString:@" "]; + // + [lines replaceObjectAtIndex:i withObject:line]; + } + data = [lines componentsJoinedByString:@"\n"]; + + scanner = [NSScanner scannerWithString:data]; + + // get number of vertices + // + [scanner setScanLocation:0]; //reset + if ([scanner scanString:@"NVERTS" intoString:NULL]) + { + int n_v; + if ([scanner scanInt:&n_v]) + vertexCount = n_v; + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read value of NVERTS\n",failString]; + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read NVERTS\n",failString]; + } + + if (vertexCount > MAX_VERTICES_PER_ENTITY) + { + //2 error lines for just one error? + OOLog(kOOLogEntityTooManyVertices, @"***** ERROR: model %@ has too many vertices (model has %d, maximum is %d).", filename, vertexCount, MAX_VERTICES_PER_ENTITY); + failFlag = YES; + // ERROR model file not found + [NSException raise:@"OoliteException" + format:@"***** ERROR: model %@ has too many vertices (model has %d, maximum is %d).", filename, vertexCount, MAX_VERTICES_PER_ENTITY]; + } + + // get number of faces + // + //[scanner setScanLocation:0]; //reset + if ([scanner scanString:@"NFACES" intoString:NULL]) + { + int n_f; + if ([scanner scanInt:&n_f]) + faceCount = n_f; + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read value of NFACES\n",failString]; + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read NFACES\n",failString]; + } + + if (faceCount > MAX_FACES_PER_ENTITY) + { + //2 error lines for just one error? + OOLog(kOOLogEntityTooManyFaces, @"***** ERROR: model %@ has too many faces (model has %d, maximum is %d).", filename, faceCount, MAX_FACES_PER_ENTITY); + failFlag = YES; + // ERROR model file not found + [NSException raise:@"OoliteException" + format:@"***** ERROR: model %@ has too many faces (model has %d, maximum is %d).", filename, faceCount, MAX_FACES_PER_ENTITY]; + } + + // get vertex data + // + //[scanner setScanLocation:0]; //reset + if ([scanner scanString:@"VERTEX" intoString:NULL]) + { + for (j = 0; j < vertexCount; j++) + { + float x, y, z; + if (!failFlag) + { + if (![scanner scanFloat:&x]) + failFlag = YES; + if (![scanner scanFloat:&y]) + failFlag = YES; + if (![scanner scanFloat:&z]) + failFlag = YES; + if (!failFlag) + { + vertices[j].x = x; vertices[j].y = y; vertices[j].z = z; + } + else + { + failString = [NSString stringWithFormat:@"%@Failed to read a value for vertex[%d] in VERTEX\n", failString, j]; + } + } + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to find VERTEX data\n",failString]; + } + + // get face data + // + if ([scanner scanString:@"FACES" intoString:NULL]) + { + for (j = 0; j < faceCount; j++) + { + int r, g, b; + float nx, ny, nz; + int n_v; + if (!failFlag) + { + // colors + // + if (![scanner scanInt:&r]) + failFlag = YES; + if (![scanner scanInt:&g]) + failFlag = YES; + if (![scanner scanInt:&b]) + failFlag = YES; + if (!failFlag) + { + faces[j].red = r / 255.0; + faces[j].green = g / 255.0; + faces[j].blue = b / 255.0; + } + else + { + failString = [NSString stringWithFormat:@"%@Failed to read a color for face[%d] in FACES\n", failString, j]; + } + + // normal + // + if (![scanner scanFloat:&nx]) + failFlag = YES; + if (![scanner scanFloat:&ny]) + failFlag = YES; + if (![scanner scanFloat:&nz]) + failFlag = YES; + if (!failFlag) + { + faces[j].normal.x = nx; + faces[j].normal.y = ny; + faces[j].normal.z = nz; + } + else + { + failString = [NSString stringWithFormat:@"%@Failed to read a normal for face[%d] in FACES\n", failString, j]; + } + + // vertices + // + if ([scanner scanInt:&n_v]) + { + faces[j].n_verts = n_v; + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read number of vertices for face[%d] in FACES\n", failString, j]; + } + // + if (!failFlag) + { + int vi; + for (i = 0; i < faces[j].n_verts; i++) + { + if ([scanner scanInt:&vi]) + { + faces[j].vertex[i] = vi; + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read vertex[%d] for face[%d] in FACES\n", failString, i, j]; + } + } + } + } + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to find FACES data\n",failString]; + } + + // get textures data + // + if ([scanner scanString:@"TEXTURES" intoString:NULL]) + { + for (j = 0; j < faceCount; j++) + { + NSString *texfile; + float max_x, max_y; + float s, t; + if (!failFlag) + { + // texfile + // + [scanner scanCharactersFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] intoString:NULL]; + if (![scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&texfile]) + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read texture filename for face[%d] in TEXTURES\n", failString, j]; + } + else + { + strlcpy(faces[j].textureFileName, [texfile UTF8String], 256); + } + faces[j].textureName = 0; + + // texture size + if (!failFlag) + { + if (![scanner scanFloat:&max_x]) + failFlag = YES; + if (![scanner scanFloat:&max_y]) + failFlag = YES; + if (failFlag) + failString = [NSString stringWithFormat:@"%@Failed to read texture size for max_x and max_y in face[%d] in TEXTURES\n", failString, j]; + } + + // vertices + if (!failFlag) + { + for (i = 0; i < faces[j].n_verts; i++) + { + if (![scanner scanFloat:&s]) + failFlag = YES; + if (![scanner scanFloat:&t]) + failFlag = YES; + if (!failFlag) + { + faces[j].s[i] = s / max_x; + faces[j].t[i] = t / max_y; + } + else + failString = [NSString stringWithFormat:@"%@Failed to read s t coordinates for vertex[%d] in face[%d] in TEXTURES\n", failString, i, j]; + } + } + } + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to find TEXTURES data\n",failString]; + } + + + + // check normals before creating new textures + [self checkNormalsAndAdjustWinding]; + + if ((failFlag)&&([failString rangeOfString:@"TEXTURES"].location != NSNotFound)) + { + [self fakeTexturesWithImageFile:@"metal.png" andMaxSize:NSMakeSize(256.0,256.0)]; + } + + if (failFlag) + OOLog(@"selfDrawingEntity.load.failed", @"%@ ..... from %@ %@", failString, filename, (using_preloaded)? @"(from preloaded data)" : @"(from file)"); + + // check for smooth shading and recalculate normals + if (isSmoothShaded) [self calculateVertexNormals]; + + // save the resulting data for possible reuse + [OOCacheManager setEntityData:[self modelData] forName:filename]; + } + + // set the collision radius + collision_radius = [self findCollisionRadius]; + + // set up vertex arrays for drawing + [self setUpVertexArrays]; +} + + +// FIXME: this isn't working, we're getting smoothed models with inside-out winding. --Ahruman +- (void) checkNormalsAndAdjustWinding +{ + Vector calculatedNormal; + unsigned i, j; + + for (i = 0; i < faceCount; i++) + { + Vector v0, v1, v2, norm; + v0 = vertices[faces[i].vertex[0]]; + v1 = vertices[faces[i].vertex[1]]; + v2 = vertices[faces[i].vertex[2]]; + norm = faces[i].normal; + calculatedNormal = normal_to_surface (v2, v1, v0); + if (vector_equal(norm, kZeroVector)) + { + faces[i].normal = normal_to_surface (v0, v1, v2); + norm = normal_to_surface (v0, v1, v2); + } + if (norm.x*calculatedNormal.x < 0 || norm.y*calculatedNormal.y < 0 || norm.z*calculatedNormal.z < 0) + { + // normal lies in the WRONG direction! + // reverse the winding + int v[faces[i].n_verts]; + GLfloat s[faces[i].n_verts]; + GLfloat t[faces[i].n_verts]; + + for (j = 0; j < faces[i].n_verts; j++) + { + v[j] = faces[i].vertex[faces[i].n_verts - 1 - j]; + s[j] = faces[i].s[faces[i].n_verts - 1 - j]; + t[j] = faces[i].t[faces[i].n_verts - 1 - j]; + } + for (j = 0; j < faces[i].n_verts; j++) + { + faces[i].vertex[j] = v[j]; + faces[i].s[j] = s[j]; + faces[i].t[j] = t[j]; + } + } + } +} + + +- (void) calculateVertexNormals +{ + unsigned i,j; + float triangle_area[faceCount]; + for (i = 0 ; i < faceCount; i++) + { + // calculate areas using Herons formula + // in the form Area = sqrt(2*(a2*b2+b2*c2+c2*a2)-(a4+b4+c4))/4 + float a2 = distance2( vertices[faces[i].vertex[0]], vertices[faces[i].vertex[1]]); + float b2 = distance2( vertices[faces[i].vertex[1]], vertices[faces[i].vertex[2]]); + float c2 = distance2( vertices[faces[i].vertex[2]], vertices[faces[i].vertex[0]]); + triangle_area[i] = sqrt( 2.0 * (a2 * b2 + b2 * c2 + c2 * a2) - 0.25 * (a2 * a2 + b2 * b2 +c2 * c2)); + } + for (i = 0; i < vertexCount; i++) + { + Vector normal_sum = kZeroVector; + for (j = 0; j < faceCount; j++) + { + BOOL is_shared = (((unsigned)faces[j].vertex[0] == i)||((unsigned)faces[j].vertex[1] == i)||((unsigned)faces[j].vertex[2] == i)); + if (is_shared) + { + float t = triangle_area[j]; // weight sum by area + normal_sum.x += t * faces[j].normal.x; normal_sum.y += t * faces[j].normal.y; normal_sum.z += t * faces[j].normal.z; + } + } + if (normal_sum.x||normal_sum.y||normal_sum.z) + normal_sum = vector_normal(normal_sum); + else + normal_sum.z = 1.0; + vertex_normal[i] = normal_sum; + } +} + + +- (Vector) normalForVertex:(int) v_index withSharedRedValue:(GLfloat) red_value +{ + int j; + Vector normal_sum = kZeroVector; + for (j = 0; j < faceCount; j++) + { + if (faces[j].red == red_value) + { + if ((faces[j].vertex[0] == v_index)||(faces[j].vertex[1] == v_index)||(faces[j].vertex[2] == v_index)) + { + float a2 = distance2( vertices[faces[j].vertex[0]], vertices[faces[j].vertex[1]]); + float b2 = distance2( vertices[faces[j].vertex[1]], vertices[faces[j].vertex[2]]); + float c2 = distance2( vertices[faces[j].vertex[2]], vertices[faces[j].vertex[0]]); + float t = sqrt( 2.0 * (a2 * b2 + b2 * c2 + c2 * a2) - 0.25 * (a2 * a2 + b2 * b2 +c2 * c2)); + normal_sum.x += t * faces[j].normal.x; normal_sum.y += t * faces[j].normal.y; normal_sum.z += t * faces[j].normal.z; + } + } + } + if (normal_sum.x||normal_sum.y||normal_sum.z) + normal_sum = vector_normal(normal_sum); + else + normal_sum.z = 1.0; + return normal_sum; +} + + +- (void) setUpVertexArrays +{ + NSMutableDictionary* texturesProcessed = [NSMutableDictionary dictionaryWithCapacity:MAX_TEXTURES_PER_ENTITY]; + + unsigned face, fi, vi, texi; + + // if isSmoothShaded find any vertices that are between faces of two different colour (by red value) + // and mark them as being on an edge and therefore NOT smooth shaded + BOOL is_edge_vertex[vertexCount]; + GLfloat red_value[vertexCount]; + for (vi = 0; vi < vertexCount; vi++) + { + is_edge_vertex[vi] = NO; + red_value[vi] = -1; + } + if (isSmoothShaded) + { + for (fi = 0; fi < faceCount; fi++) + { + GLfloat rv = faces[fi].red; + int i; + for (i = 0; i < 3; i++) + { + vi = faces[fi].vertex[i]; + if (red_value[vi] < 0.0) // unassigned + red_value[vi] = rv; + else if (red_value[vi] != rv) // a different colour + is_edge_vertex[vi] = YES; + } + } + } + + + // base model, flat or smooth shaded, all triangles + int tri_index = 0; + int uv_index = 0; + int vertex_index = 0; + + texi = 1; // index of first texture + + for (face = 0; face < faceCount; face++) + { + NSString* tex_string = [NSString stringWithUTF8String:faces[face].textureFileName]; + if (![texturesProcessed objectForKey:tex_string]) + { + // do this texture + triangle_range[texi].location = tri_index; + strlcpy(textureFileName[texi], faces[face].textureFileName, 256); + textureNames[texi] = faces[face].textureName; + + for (fi = 0; fi < faceCount; fi++) + { + Vector normal = make_vector( 0.0, 0.0, 1.0); + int v; + if (!isSmoothShaded) + normal = faces[fi].normal; + if (strcmp(faces[fi].textureFileName, faces[face].textureFileName) == 0) + { + for (vi = 0; vi < 3; vi++) + { + v = faces[fi].vertex[vi]; + if (isSmoothShaded) + { + if (is_edge_vertex[v]) + normal = [self normalForVertex: v withSharedRedValue: faces[fi].red]; + else + normal = vertex_normal[v]; + } + else + normal = faces[fi].normal; + entityData.index_array[tri_index++] = vertex_index; + entityData.normal_array[vertex_index] = normal; + entityData.vertex_array[vertex_index++] = vertices[v]; + entityData.texture_uv_array[uv_index++] = faces[fi].s[vi]; + entityData.texture_uv_array[uv_index++] = faces[fi].t[vi]; + } + } + } + triangle_range[texi].length = tri_index - triangle_range[texi].location; + + //finally... + [texturesProcessed setObject:tex_string forKey:tex_string]; // note this texture done + texi++; + } + } + entityData.n_triangles = tri_index; // total number of triangle vertices + triangle_range[0] = NSMakeRange( 0, tri_index); + + textureCount = texi - 1; +} + + +- (double) findCollisionRadius +{ + int i; + double d_squared, result, length_longest_axis, length_shortest_axis; + + result = 0.0; + if (vertexCount) + bounding_box_reset_to_vector(&boundingBox,vertices[0]); + else + bounding_box_reset(&boundingBox); + + for (i = 0; i < vertexCount; i++) + { + d_squared = magnitude2(vertices[i]); + if (d_squared > result) + { + result = d_squared; + } + bounding_box_add_vector(&boundingBox,vertices[i]); + } + + length_longest_axis = boundingBox.max.x - boundingBox.min.x; + if (boundingBox.max.y - boundingBox.min.y > length_longest_axis) + length_longest_axis = boundingBox.max.y - boundingBox.min.y; + if (boundingBox.max.z - boundingBox.min.z > length_longest_axis) + length_longest_axis = boundingBox.max.z - boundingBox.min.z; + + length_shortest_axis = boundingBox.max.x - boundingBox.min.x; + if (boundingBox.max.y - boundingBox.min.y < length_shortest_axis) + length_shortest_axis = boundingBox.max.y - boundingBox.min.y; + if (boundingBox.max.z - boundingBox.min.z < length_shortest_axis) + length_shortest_axis = boundingBox.max.z - boundingBox.min.z; + + d_squared = (length_longest_axis + length_shortest_axis) * (length_longest_axis + length_shortest_axis) * 0.25; // square of average length + no_draw_distance = d_squared * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR; // no longer based on the collision radius + + mass = (boundingBox.max.x - boundingBox.min.x) * (boundingBox.max.y - boundingBox.min.y) * (boundingBox.max.z - boundingBox.min.z); + + return sqrt(result); +} + + +- (void) fakeTexturesWithImageFile: (NSString *) textureFile andMaxSize:(NSSize) maxSize +{ + unsigned i, j, k; + Vector vec; + unsigned nf = 0; + unsigned fi[MAX_FACES_PER_ENTITY]; + float max_s, min_s, max_t, min_t, st_width, st_height; + float tolerance; + Face fa[MAX_FACES_PER_ENTITY]; + unsigned faces_to_match; + BOOL face_matched[MAX_FACES_PER_ENTITY]; + + tolerance = 1.00; + faces_to_match = faceCount; + for (i = 0; i < faceCount; i++) + { + face_matched[i] = NO; + } + while (faces_to_match > 0) + { + tolerance -= 0.05; + + // Top (+y) first + vec = kBasisYVector; + // build list of faces that face in that direction... + nf = 0; + max_s = -999999.0; min_s = 999999.0; + max_t = -999999.0; min_t = 999999.0; + for (i = 0; i < faceCount; i++) + { + float s, t; + float g = dot_product(vec, faces[i].normal) * sqrt(2.0); + if ((g >= tolerance)&&(!face_matched[i])) + { + fi[nf++] = i; + face_matched[i] = YES; + faces_to_match--; + for (j = 0; j < faces[i].n_verts; j++) + { + s = vertices[faces[i].vertex[j]].x; + t = vertices[faces[i].vertex[j]].z; + max_s = (max_s > s) ? max_s:s ; min_s = (min_s < s) ? min_s:s ; + max_t = (max_t > t) ? max_t:t ; min_t = (min_t < t) ? min_t:t ; + } + } + } + + st_width = max_s - min_s; + st_height = max_t - min_t; + + for (j = 0; j < nf; j++) + { + i = fi[j]; + + strlcpy(fa[i].textureFileName, [[NSString stringWithFormat:@"top_%@", textureFile] UTF8String], 256); + for (k = 0; k < faces[i].n_verts; k++) + { + float s, t; + s = vertices[faces[i].vertex[k]].x; + t = vertices[faces[i].vertex[k]].z; + fa[i].s[k] = (s - min_s) * maxSize.width / st_width; + fa[i].t[k] = (t - min_t) * maxSize.height / st_height; + // + // TESTING + // + fa[i].t[k] = maxSize.height - fa[i].t[k]; // REVERSE t locations + } + } + + // Bottom (-y) + vec = vector_flip(kBasisYVector); + // build list of faces that face in that direction... + nf = 0; + max_s = -999999.0; min_s = 999999.0; + max_t = -999999.0; min_t = 999999.0; + for (i = 0; i < faceCount; i++) + { + float s, t; + float g = dot_product(vec, faces[i].normal) * sqrt(2.0); + if ((g >= tolerance)&&(!face_matched[i])) + { + fi[nf++] = i; + face_matched[i] = YES; + faces_to_match--; + for (j = 0; j < faces[i].n_verts; j++) + { + s = -vertices[faces[i].vertex[j]].x; + t = -vertices[faces[i].vertex[j]].z; + max_s = (max_s > s) ? max_s:s ; min_s = (min_s < s) ? min_s:s ; + max_t = (max_t > t) ? max_t:t ; min_t = (min_t < t) ? min_t:t ; + } + } + } + st_width = max_s - min_s; + st_height = max_t - min_t; + for (j = 0; j < nf; j++) + { + i = fi[j]; + strlcpy(fa[i].textureFileName, [[NSString stringWithFormat:@"bottom_%@", textureFile] UTF8String], 256); + for (k = 0; k < faces[i].n_verts; k++) + { + float s, t; + s = -vertices[faces[i].vertex[k]].x; + t = -vertices[faces[i].vertex[k]].z; + fa[i].s[k] = (s - min_s) * maxSize.width / st_width; + fa[i].t[k] = (t - min_t) * maxSize.height / st_height; + } + } + + // Right (+x) + vec = kBasisXVector; + // build list of faces that face in that direction... + nf = 0; + max_s = -999999.0; min_s = 999999.0; + max_t = -999999.0; min_t = 999999.0; + for (i = 0; i < faceCount; i++) + { + float s, t; + float g = dot_product(vec, faces[i].normal) * sqrt(2.0); + if ((g >= tolerance)&&(!face_matched[i])) + { + fi[nf++] = i; + face_matched[i] = YES; + faces_to_match--; + for (j = 0; j < faces[i].n_verts; j++) + { + s = vertices[faces[i].vertex[j]].z; + t = vertices[faces[i].vertex[j]].y; + max_s = (max_s > s) ? max_s:s ; min_s = (min_s < s) ? min_s:s ; + max_t = (max_t > t) ? max_t:t ; min_t = (min_t < t) ? min_t:t ; + } + } + } + st_width = max_s - min_s; + st_height = max_t - min_t; + for (j = 0; j < nf; j++) + { + i = fi[j]; + strlcpy(fa[i].textureFileName, [[NSString stringWithFormat:@"right_%@", textureFile] UTF8String], 256); + for (k = 0; k < faces[i].n_verts; k++) + { + float s, t; + s = vertices[faces[i].vertex[k]].z; + t = vertices[faces[i].vertex[k]].y; + fa[i].s[k] = (s - min_s) * maxSize.width / st_width; + fa[i].t[k] = (t - min_t) * maxSize.height / st_height; + } + } + + // Left (-x) + vec = vector_flip(kBasisXVector); + // build list of faces that face in that direction... + nf = 0; + max_s = -999999.0; min_s = 999999.0; + max_t = -999999.0; min_t = 999999.0; + for (i = 0; i < faceCount; i++) + { + float s, t; + float g = dot_product(vec, faces[i].normal) * sqrt(2.0); + if ((g >= tolerance)&&(!face_matched[i])) + { + fi[nf++] = i; + face_matched[i] = YES; + faces_to_match--; + for (j = 0; j < faces[i].n_verts; j++) + { + s = -vertices[faces[i].vertex[j]].z; + t = -vertices[faces[i].vertex[j]].y; + max_s = (max_s > s) ? max_s:s ; min_s = (min_s < s) ? min_s:s ; + max_t = (max_t > t) ? max_t:t ; min_t = (min_t < t) ? min_t:t ; + } + } + } + st_width = max_s - min_s; + st_height = max_t - min_t; + for (j = 0; j < nf; j++) + { + i = fi[j]; + strlcpy(fa[i].textureFileName, [[NSString stringWithFormat:@"left_%@", textureFile] UTF8String], 256); + for (k = 0; k < faces[i].n_verts; k++) + { + float s, t; + s = -vertices[faces[i].vertex[k]].z; + t = -vertices[faces[i].vertex[k]].y; + fa[i].s[k] = (s - min_s) * maxSize.width / st_width; + fa[i].t[k] = (t - min_t) * maxSize.height / st_height; + } + } + + // Front (+z) + vec = kBasisZVector; + // build list of faces that face in that direction... + nf = 0; + max_s = -999999.0; min_s = 999999.0; + max_t = -999999.0; min_t = 999999.0; + for (i = 0; i < faceCount; i++) + { + float s, t; + float g = dot_product(vec, faces[i].normal) * sqrt(2.0); + if ((g >= tolerance)&&(!face_matched[i])) + { + fi[nf++] = i; + face_matched[i] = YES; + faces_to_match--; + for (j = 0; j < faces[i].n_verts; j++) + { + s = vertices[faces[i].vertex[j]].x; + t = vertices[faces[i].vertex[j]].y; + max_s = (max_s > s) ? max_s:s ; min_s = (min_s < s) ? min_s:s ; + max_t = (max_t > t) ? max_t:t ; min_t = (min_t < t) ? min_t:t ; + } + } + } + st_width = max_s - min_s; + st_height = max_t - min_t; + for (j = 0; j < nf; j++) + { + i = fi[j]; + strlcpy(fa[i].textureFileName, [[NSString stringWithFormat:@"front_%@", textureFile] UTF8String], 256); + for (k = 0; k < faces[i].n_verts; k++) + { + float s, t; + s = vertices[faces[i].vertex[k]].x; + t = vertices[faces[i].vertex[k]].y; + fa[i].s[k] = (s - min_s) * maxSize.width / st_width; + fa[i].t[k] = (t - min_t) * maxSize.height / st_height; + } + } + + // Back (-z) + vec = vector_flip(kBasisZVector); + // build list of faces that face in that direction... + nf = 0; + max_s = -999999.0; min_s = 999999.0; + max_t = -999999.0; min_t = 999999.0; + for (i = 0; i < faceCount; i++) + { + float s, t; + float g = dot_product(vec, faces[i].normal) * sqrt(2.0); + if ((g >= tolerance)&&(!face_matched[i])) + { + fi[nf++] = i; + face_matched[i] = YES; + faces_to_match--; + for (j = 0; j < faces[i].n_verts; j++) + { + s = -vertices[faces[i].vertex[j]].x; + t = -vertices[faces[i].vertex[j]].y; + max_s = (max_s > s) ? max_s:s ; min_s = (min_s < s) ? min_s:s ; + max_t = (max_t > t) ? max_t:t ; min_t = (min_t < t) ? min_t:t ; + } + } + } + st_width = max_s - min_s; + st_height = max_t - min_t; + for (j = 0; j < nf; j++) + { + i = fi[j]; + strlcpy(fa[i].textureFileName, [[NSString stringWithFormat:@"back_%@", textureFile] UTF8String], 256); + for (k = 0; k < faces[i].n_verts; k++) + { + float s, t; + s = -vertices[faces[i].vertex[k]].x; + t = -vertices[faces[i].vertex[k]].y; + fa[i].s[k] = (s - min_s) * maxSize.width / st_width; + fa[i].t[k] = (t - min_t) * maxSize.height / st_height; + } + } + } + + for (i = 0; i < faceCount; i++) + { + strlcpy(faces[i].textureFileName, fa[i].textureFileName, 256); + faces[i].textureName = 0; + for (j = 0; j < faces[i].n_verts; j++) + { + faces[i].s[j] = fa[i].s[j] / maxSize.width; + faces[i].t[j] = fa[i].t[j] / maxSize.height; + } + } +} + + +#ifndef NDEBUG +- (void)dumpSelfState +{ + NSMutableArray *flags = nil; + NSString *flagsString = nil; + + [super dumpSelfState]; + + if (basefile != nil) OOLog(@"dumpState.selfDrawingEntity", @"Model file: %@", basefile); + + flags = [NSMutableArray array]; + #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; } + ADD_FLAG_IF_SET(isSmoothShaded); + flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none"; + OOLog(@"dumpState.selfDrawingEntity", @"Flags: %@", flagsString); +} +#endif + +@end + + +static NSString * const kOOCacheMeshes = @"OOSelfDrawingEntity-mesh"; + +@implementation OOCacheManager (OSSelfDrawingEntity) + ++ (NSDictionary *)entityDataForName:(NSString *)inShipName +{ + return [[self sharedCache] objectForKey:inShipName inCache:kOOCacheMeshes]; +} + + ++ (void)setEntityData:(NSDictionary *)inData forName:(NSString *)inShipName +{ + if (inData != nil && inShipName != nil) + { + [[self sharedCache] setObject:inData forKey:inShipName inCache:kOOCacheMeshes]; + } +} + +@end diff --git a/src/Core/Entities/ParticleEntity.h b/src/Core/Entities/ParticleEntity.h new file mode 100644 index 00000000..8e0e35d5 --- /dev/null +++ b/src/Core/Entities/ParticleEntity.h @@ -0,0 +1,95 @@ +/* + +ParticleEntity.h + +Entity subclass implementing a variety of special effects. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOSelfDrawingEntity.h" + +#import "Universe.h" +#import "OOMaths.h" + +#define PARTICLE_LASER_DURATION 0.20 +#define PARTICLE_LASER_LENGTH 10000.0 +#define PARTICLE_LASER_RANGE_LIMIT 1000000000.0 + +#define LASER_FLASH_SIZE (1.0 + 2.0 * randf()) + +@class OOTexture; + + +@interface ParticleEntity: OOSelfDrawingEntity +{ + OOTexture *texture; + + OOColor *color; + GLfloat color_fv[4]; + + GLfloat alpha; + OOTimeDelta time_counter; + + OOTimeDelta duration; + OOTimeDelta activation_time; + double growth_rate; + + GLfloat ring_inner_radius, ring_outer_radius; + + int particle_type; + + NSSize size; + + Vector exhaustScale; + GLfloat exhaustBaseColors[34 * 4], verts[34 * 3]; +} + +- (id) initLaserFromShip:(ShipEntity *) ship view:(OOViewID) view offset:(Vector)offset; +- (id) initExhaustFromShip:(ShipEntity *) ship details:(NSString *) details; +- (id) initECMMineFromShip:(ShipEntity *) ship; +- (id) initEnergyMineFromShip:(ShipEntity *) ship; +- (id) initHyperringFromShip:(ShipEntity *) ship; +- (id) initFragburstSize:(GLfloat) fragSize fromPosition:(Vector) fragPos; +- (id) initBurst2Size:(GLfloat) burstSize fromPosition:(Vector) fragPos; +- (id) initFlashSize:(GLfloat) burstSize fromPosition:(Vector) fragPos; +- (id) initFlashSize:(GLfloat) flashSize fromPosition:(Vector) fragPos color:(OOColor*) flashColor; +- (id) initBillboard:(NSSize) billSize withTexture:(NSString*) textureFile; +- (id) initFlasherWithSize:(float)size frequency:(float)frequency phase:(float)phase; +- (id) initPlasmaShotAt:(Vector)position velocity:(Vector)velocity energy:(float)energy duration:(OOTimeDelta)duration color:(OOColor *)color; +- (id) initSparkAt:(Vector)position velocity:(Vector)velocity duration:(OOTimeDelta)duration size:(float)size color:(OOColor *)color; + +- (void) setColor:(OOColor *) a_color; + +- (void) setDuration:(double) dur; +- (void) setSize:(NSSize) siz; +- (NSSize) size; + + +@end + + +@interface Entity (OOParticleExtensions) + +- (BOOL)isParticle; +- (BOOL)isFlasher; +- (BOOL)isExhaust; + +@end diff --git a/src/Core/Entities/ParticleEntity.m b/src/Core/Entities/ParticleEntity.m new file mode 100644 index 00000000..2612a654 --- /dev/null +++ b/src/Core/Entities/ParticleEntity.m @@ -0,0 +1,1849 @@ +/* + +ParticleEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ParticleEntity.h" + +#import "Universe.h" +#import "AI.h" +#import "OOColor.h" +#import "OOTexture.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" + +#import "ShipEntity.h" +#import "PlayerEntity.h" +#import "PlanetEntity.h" + + +// NOTE: these values are documented for scripting, be careful about changing them. +#define ECM_EFFECT_DURATION 2.0 +#define ECM_PULSE_COUNT 4 +#define ECM_PULSE_INTERVAL (ECM_EFFECT_DURATION / (double)ECM_PULSE_COUNT) + + +typedef enum +{ + PARTICLE_TEST = 1, + PARTICLE_SPARK = 102, + PARTICLE_SHOT_PLASMA = 110, + PARTICLE_LASER_BEAM = 160, + PARTICLE_EXPLOSION = 201, + PARTICLE_FLASH = 230, + PARTICLE_FRAGBURST = 250, + PARTICLE_BURST2 = 270, + PARTICLE_EXHAUST = 300, + PARTICLE_ECM_MINE = 400, + PARTICLE_ENERGY_MINE = 500, + PARTICLE_FLASHER = 600, + PARTICLE_BILLBOARD = 700, + PARTICLE_HYPERRING = 800 +} OOParticleType; + + +@interface ParticleEntity (OOPrivate) + +- (void) updateExplosion:(double) delta_t; +- (void) updateFlasher:(double) delta_t; +- (void) updateECMMine:(double) delta_t; +- (void) updateEnergyMine:(double) delta_t; +- (void) updateShot:(double) delta_t; +- (void) updateSpark:(double) delta_t; +- (void) updateLaser:(double) delta_t; +- (void) updateHyperring:(double) delta_t; +- (void) updateFragburst:(double) delta_t; +- (void) updateBurst2:(double) delta_t; +- (void) updateExhaust2:(double) delta_t; +- (void) updateFlash:(double) delta_t; + +- (void) drawParticle; +- (void) drawLaser; +- (void) drawExhaust2; +- (void) drawHyperring; +- (void) drawEnergyMine; +- (void) drawFragburst; +- (void) drawBurst2; +- (void) drawBillboard; + +- (void) setTexture:(NSString *) filename; +- (void) setParticleType:(OOParticleType) p_type; +- (OOParticleType) particleType; + +@end + + +#ifndef ADDITIVE_BLENDING +#define ADDITIVE_BLENDING 1 +#endif + +#if ADDITIVE_BLENDING +static inline void BeginAdditiveBlending(BOOL withGL_ONE) +{ + glPushAttrib(GL_COLOR_BUFFER_BIT); + glEnable(GL_BLEND); + if (withGL_ONE) + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE); + } + else + { + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + } +} + + +static inline void EndAdditiveBlending(void) +{ + glPopAttrib(); +} + +#define GL_ONE_YES YES +#define GL_ONE_NO NO +#else +#define BeginAdditiveBlending(x) do {} while (0) +#define EndAdditiveBlending() do {} while (0) +#endif + + +static void DrawQuadForView(GLfloat x, GLfloat y, GLfloat z, GLfloat xx, GLfloat yy); + + + +static Vector circleVertex[65]; // holds vector coordinates for a unit circle + +@implementation ParticleEntity + ++ (void)initialize +{ + unsigned i; + for (i = 0; i < 65; i++) + { + circleVertex[i].x = sin(i * M_PI / 32.0); + circleVertex[i].y = cos(i * M_PI / 32.0); + circleVertex[i].z = 0.0; + } +} + + +- (id) init +{ + if ((self = [super init])) + { + time_counter = 0.0; + + particle_type = PARTICLE_TEST; + isParticle = YES; + [self setStatus:STATUS_EFFECT]; + + basefile = @"Particle"; + [self setTexture:@"blur256.png"]; + [self setColor:[OOColor greenColor]]; + + size = NSMakeSize(32.0,32.0); + collision_radius = 32.0; + } + return self; +} + + +- (id) initLaserFromShip:(ShipEntity *) srcEntity view:(OOViewID) view offset:(Vector)offset +{ + ShipEntity *ship = [srcEntity rootShipEntity]; + BoundingBox bbox = [srcEntity boundingBox]; + float midx = 0.5 * (bbox.max.x + bbox.min.x); + float midz = 0.5 * (bbox.max.z + bbox.min.z); + + self = [super init]; + if (self == nil) goto FAIL; + +#ifndef NDEBUG + if (![srcEntity isShip]) goto FAIL; + if (ship == nil) goto FAIL; +#endif + + [self setStatus:STATUS_EFFECT]; + + Vector middle = make_vector(midx, 0.5 * (bbox.max.y + bbox.min.y), midz); + if (ship == srcEntity) + { + // main laser offset + position = vector_add([ship position],OOVectorMultiplyMatrix(offset, [ship drawRotationMatrix])); + } + else + { + // subentity laser + position = [srcEntity absolutePositionForSubentityOffset:middle]; + } + + orientation = [ship normalOrientation]; + Vector v_up = vector_up_from_quaternion(orientation); + Vector v_forward = vector_forward_from_quaternion(orientation); + Vector v_right = vector_right_from_quaternion(orientation); + velocity = vector_multiply_scalar(v_forward, [ship flightSpeed]); + + Vector viewOffset; + switch (view) + { + default: + case VIEW_AFT: + quaternion_rotate_about_axis(&orientation, v_up, M_PI); + case VIEW_FORWARD: + viewOffset = vector_multiply_scalar(v_forward, midz); + break; + + case VIEW_PORT: + quaternion_rotate_about_axis(&orientation, v_up, M_PI/2.0); + viewOffset = vector_multiply_scalar(v_right, midx); + break; + + case VIEW_STARBOARD: + quaternion_rotate_about_axis(&orientation, v_up, -M_PI/2.0); + viewOffset = vector_multiply_scalar(v_right, midx); + break; + } + position = vector_add(position, viewOffset); + rotMatrix = OOMatrixForQuaternionRotation(orientation); + + time_counter = 0.0; + + particle_type = PARTICLE_LASER_BEAM; + + [self setColor:[OOColor redColor]]; + + duration = PARTICLE_LASER_DURATION; + + [self setOwner:ship]; + + collision_radius = [srcEntity weaponRange]; + + isParticle = YES; + + return self; + +FAIL: + [self release]; + return nil; +} + + +- (id) initExhaustFromShip:(ShipEntity *) ship details:(NSString *) details +{ + if ((self = [super init])) + { + NSArray *values = ScanTokensFromString(details); + if ([values count] != 6) + { + [self release]; + return nil; + } + Vector offset; + offset.x = [values floatAtIndex:0]; + offset.y = [values floatAtIndex:1]; + offset.z = [values floatAtIndex:2]; + position = offset; + + Vector scale; + scale.x = [values floatAtIndex:3]; + scale.y = [values floatAtIndex:4]; + scale.z = [values floatAtIndex:5]; + exhaustScale = scale; + + [self setStatus:STATUS_EFFECT]; + + particle_type = PARTICLE_EXHAUST; + + [self setOwner:ship]; + isParticle = YES; + } + + return self; +} + + +- (id) initECMMineFromShip:(ShipEntity *) ship +{ + if (ship == nil) + { + [self release]; + return nil; + } + + if ((self = [super init])) + { + time_counter = 0.0; + activation_time = ECM_PULSE_INTERVAL; + duration = ECM_EFFECT_DURATION; + position = [ship position]; + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_NO_DRAW; + + particle_type = PARTICLE_ECM_MINE; + + [self setOwner:ship]; + + isParticle = YES; + } + + return self; +} + + +- (id) initEnergyMineFromShip:(ShipEntity *) ship +{ + if (ship == nil) + { + [self release]; + return nil; + } + + if ((self = [super init])) + { + time_counter = 0.0; + duration = 20.0; + position = ship->position; + + [self setVelocity: kZeroVector]; + + [self setColor:[OOColor blueColor]]; + + alpha = 0.5; + collision_radius = 0; + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_MINE; + + particle_type = PARTICLE_ENERGY_MINE; + + [self setOwner:[ship owner]]; + + isParticle = YES; + } + + return self; +} + + +- (id) initHyperringFromShip:(ShipEntity *) ship +{ + if (ship == nil) + { + [self release]; + return nil; + } + + if ((self = [super init])) + { + time_counter = 0.0; + duration = 2.0; + size.width = ship->collision_radius * 0.5; + size.height = size.width * 1.25; + ring_inner_radius = size.width; + ring_outer_radius = size.height; + position = ship->position; + [self setOrientation:ship->orientation]; + [self setVelocity:[ship velocity]]; + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_NO_DRAW; + + particle_type = PARTICLE_HYPERRING; + + [self setOwner:ship]; + + isParticle = YES; + } + + return self; +} + + +- (id) initFragburstSize:(GLfloat) fragSize fromPosition:(Vector) fragPos +{ + if ((self = [super init])) + { + int speed_low = 100; + int speed_high = 400; + int n_fragments = 0.4 * fragSize; + if (n_fragments > 63) n_fragments = 63; // must also be less than MAX_FACES_PER_ENTITY + n_fragments |= 12; + int i; + + basefile = @"Particle"; + [self setTexture:@"blur256.png"]; + + size = NSMakeSize(fragSize, fragSize); + + vertexCount = n_fragments; + time_counter = 0.0; + duration = 1.5; + position = fragPos; + [self setColor:[OOColor colorWithCalibratedHue:0.12 + 0.08 * randf() saturation:1.0 brightness:1.0 alpha:1.0]]; // yellow/orange (0.12) through yellow (0.1667) to yellow/slightly green (0.20) + + for (i = 0 ; i < n_fragments; i++) + { + GLfloat speed = speed_low + 0.5 * (randf()+randf()) * (speed_high - speed_low); // speed tends toward mean of speed_high and speed_low + vertices[i] = kZeroVector; // position + vertex_normal[i] = make_vector(randf() - 0.5, randf() - 0.5, randf() - 0.5); + vertex_normal[i] = vector_normal(vertex_normal[i]); + vertex_normal[i].x *= speed; // velocity + vertex_normal[i].y *= speed; + vertex_normal[i].z *= speed; + Vector col = make_vector(color_fv[0] * 0.1 * (9.5 + randf()), color_fv[1] * 0.1 * (9.5 + randf()), color_fv[2] * 0.1 * (9.5 + randf())); + col = vector_normal(col); + faces[i].red = col.x; + faces[i].green = col.y; + faces[i].blue = col.z; + faces[i].normal.x = 16.0 * speed_low / speed; + } + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_NO_DRAW; + + particle_type = PARTICLE_FRAGBURST; + + collision_radius = 0; + energy = 0; + + isParticle = YES; + } + + return self; +} + + +- (id) initBurst2Size:(GLfloat) burstSize fromPosition:(Vector) fragPos +{ + if ((self = [super init])) + { + int speed_low = 1 + burstSize * 0.5; + int speed_high = speed_low * 4; + int n_fragments = 0.2 * burstSize; + if (n_fragments > 15) n_fragments = 15; // must also be less than MAX_FACES_PER_ENTITY + n_fragments |= 3; + int i; + + basefile = @"Particle"; + [self setTexture:@"blur256.png"]; + + size = NSMakeSize(burstSize, burstSize); + + vertexCount = n_fragments; + time_counter = 0.0; + duration = 1.0; + position = fragPos; + + [self setColor:[[OOColor yellowColor] blendedColorWithFraction:0.5 ofColor:[OOColor whiteColor]]]; + + for (i = 0 ; i < n_fragments; i++) + { + GLfloat speed = speed_low + 0.5 * (randf()+randf()) * (speed_high - speed_low); // speed tends toward mean of speed_high and speed_low + vertices[i] = kZeroVector; // position + vertex_normal[i] = make_vector(randf() - 0.5, randf() - 0.5, randf() - 0.5); + vertex_normal[i] = vector_normal(vertex_normal[i]); + vertex_normal[i].x *= speed; // velocity + vertex_normal[i].y *= speed; + vertex_normal[i].z *= speed; + Vector col = make_vector(color_fv[0] * 0.1 * (9.5 + randf()), color_fv[1] * 0.1 * (9.5 + randf()), color_fv[2] * 0.1 * (9.5 + randf())); + col = vector_normal(col); + faces[i].red = col.x; + faces[i].green = col.y; + faces[i].blue = col.z; + faces[i].normal.z = 1.0; + } + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_NO_DRAW; + + particle_type = PARTICLE_BURST2; + + collision_radius = 0; + energy = 0; + + isParticle = YES; + } + + return self; +} + +// used exclusively for explosion flashes +- (id) initFlashSize:(GLfloat) flashSize fromPosition:(Vector) fragPos +{ + self = [self initFlashSize:flashSize fromPosition:fragPos color:[OOColor whiteColor]]; + if (self != nil) + { + if (growth_rate < 6000.0) growth_rate = 6000.0; + duration = 0.4; + } + + return self; +} + +// used for laser flashes +- (id) initFlashSize:(GLfloat) flashSize fromPosition:(Vector) fragPos color:(OOColor*) flashColor +{ + if ((self = [super init])) + { + basefile = @"Particle"; + [self setTexture:@"flare256.png"]; + + size = NSMakeSize(flashSize, flashSize); + + growth_rate = 150.0 * flashSize; // if average flashSize is 80 then this is 12000 + + time_counter = 0.0; + duration = 0.3; + position = fragPos; + + [self setColor:flashColor]; + color_fv[3] = 1.0; + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_NO_DRAW; + + particle_type = PARTICLE_FLASH; + + collision_radius = 0; + energy = 0; + + isParticle = YES; + + [self setVelocity: kZeroVector]; + } + return self; +} + +// used for background billboards +- (id) initBillboard:(NSSize) billSize withTexture:(NSString*) textureFile +{ + if ((self = [super init])) + { + basefile = @"Particle"; + [self setTexture:textureFile]; + if (texture == nil) + { + [self release]; + return nil; + } + + size = billSize; + + time_counter = 0.0; + duration = 0.0; //infinite + + [self setColor:[OOColor whiteColor]]; + color_fv[3] = 1.0; + + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_NO_DRAW; + + particle_type = PARTICLE_BILLBOARD; + + collision_radius = 0; + energy = 0; + + isParticle = YES; + + [self setVelocity: kZeroVector]; + [self setPosition: make_vector(0.0f, 0.0f, 640.0f)]; + } + return self; +} + + +- (id) initFlasherWithSize:(float)inSize frequency:(float)frequency phase:(float)phase +{ + self = [self init]; + if (self != nil) + { + [self setSize:NSMakeSize(inSize, inSize)]; + [self setEnergy:phase]; + [self setDuration:frequency]; + particle_type = PARTICLE_FLASHER; + } + return self; +} + + +- (id) initPlasmaShotAt:(Vector)inPosition + velocity:(Vector)inVelocity + energy:(float)inEnergy + duration:(OOTimeDelta)inDuration + color:(OOColor *)inColor +{ + self = [self init]; + if (self != nil) + { + particle_type = PARTICLE_SHOT_PLASMA; + [self setPosition:inPosition]; + [self setVelocity:inVelocity]; + [self setScanClass:CLASS_NO_DRAW]; + [self setDuration:inDuration]; + [self setCollisionRadius:2.0]; + [self setEnergy:inEnergy]; + [self setSize:NSMakeSize(12, 12)]; + [self setColor:inColor]; + } + return self; +} + + +- (id) initSparkAt:(Vector)inPosition + velocity:(Vector)inVelocity + duration:(OOTimeDelta)inDuration + size:(float)inSize + color:(OOColor *)inColor +{ + self = [self init]; + if (self != nil) + { + particle_type = PARTICLE_SPARK; + [self setPosition:inPosition]; + [self setVelocity:inVelocity]; + [self setScanClass:CLASS_NO_DRAW]; + [self setDuration:inDuration]; + [self setCollisionRadius:2.0]; + [self setEnergy:0.0]; + [self setSize:NSMakeSize(inSize, inSize)]; + [self setColor:inColor]; + } + return self; +} + + +- (void) dealloc +{ + [texture release]; + [color release]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ +#ifndef NDEBUG + NSString *type_string = nil; + switch ([self particleType]) + { +#define CASE(x) case x: type_string = @#x; break; + CASE(PARTICLE_TEST); + CASE(PARTICLE_LASER_BEAM); + CASE(PARTICLE_SHOT_PLASMA); + CASE(PARTICLE_SPARK); + CASE(PARTICLE_EXPLOSION); + CASE(PARTICLE_FLASH); + CASE(PARTICLE_FRAGBURST); + CASE(PARTICLE_BURST2); + CASE(PARTICLE_EXHAUST); + CASE(PARTICLE_ECM_MINE); + CASE(PARTICLE_ENERGY_MINE); + CASE(PARTICLE_FLASHER); + CASE(PARTICLE_BILLBOARD); + CASE(PARTICLE_HYPERRING); + } + if (type_string == nil) type_string = [NSString stringWithFormat:@"UNKNOWN (%i)", particle_type]; + + return [NSString stringWithFormat:@"%@ ttl: %.3fs", type_string, duration - time_counter]; +#else + return [NSString stringWithFormat:@"ttl: %.3fs", duration - time_counter]; +#endif +} + + +- (BOOL) canCollide +{ + if (particle_type == PARTICLE_SHOT_PLASMA || particle_type == PARTICLE_ENERGY_MINE) + { + return time_counter > 0.05; + } + return NO; +} + + +- (BOOL) checkCloseCollisionWith:(Entity *)other +{ + if (particle_type == PARTICLE_ENERGY_MINE) + return YES; + if (other == [self owner]) + return NO; + return !(other->isParticle); +} + + +- (void) setTexture:(NSString *)name +{ + if (name != nil) + { + [texture autorelease]; + texture = [[OOTexture textureWithName:name inFolder:@"Textures"] retain]; + } +} + + +- (void) setColor:(OOColor *) a_color +{ + if (!a_color) return; + + OOColor *rgbColor = a_color; + + [color release]; + color = [rgbColor retain]; + + color_fv[0] = [color redComponent]; + color_fv[1] = [color greenComponent]; + color_fv[2] = [color blueComponent]; +} + + + +- (void) setParticleType:(OOParticleType) p_type +{ + particle_type = p_type; +} + + +- (OOParticleType) particleType +{ + return particle_type; +} + + +- (void) setDuration:(double) dur +{ + duration = dur; + time_counter = 0.0; +} + + +- (void) setSize:(NSSize) siz +{ + size = siz; + collision_radius = sqrt (size.width * size.width + size.height * size.height); + no_draw_distance = collision_radius * collision_radius * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR; +} + + +- (NSSize) size +{ + return size; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + [super update:delta_t]; + + time_counter += delta_t; + + if (UNIVERSE) + { + switch ([self particleType]) + { + case PARTICLE_TEST: + case PARTICLE_SPARK: + case PARTICLE_SHOT_PLASMA: + case PARTICLE_FLASHER: + case PARTICLE_EXPLOSION: + case PARTICLE_FRAGBURST: + case PARTICLE_BURST2: + case PARTICLE_FLASH: + { + PlayerEntity *player = [PlayerEntity sharedPlayer]; + assert(player != nil); + rotMatrix = [player drawRotationMatrix]; + } + break; + default: + break; + + } + switch ([self particleType]) + { + case PARTICLE_TEST: + alpha = (sin(time_counter) + 2.0) / 3.0; + break; + + case PARTICLE_EXPLOSION: + [self updateExplosion:delta_t]; + break; + + case PARTICLE_HYPERRING: + [self updateHyperring:delta_t]; + break; + + case PARTICLE_LASER_BEAM: + [self updateLaser:delta_t]; + break; + + case PARTICLE_EXHAUST: + [self updateExhaust2:delta_t]; + break; + + case PARTICLE_ECM_MINE: + [self updateECMMine:delta_t]; + break; + + case PARTICLE_ENERGY_MINE: + [self updateEnergyMine:delta_t]; + break; + + case PARTICLE_FLASHER: + [self updateFlasher:delta_t]; + break; + + case PARTICLE_SPARK: + [self updateSpark:delta_t]; + break; + + case PARTICLE_FRAGBURST: + [self updateFragburst:delta_t]; + break; + + case PARTICLE_BURST2: + [self updateBurst2:delta_t]; + break; + + case PARTICLE_FLASH: + [self updateFlash:delta_t]; + break; + + case PARTICLE_BILLBOARD: + break; + + case PARTICLE_SHOT_PLASMA: + [self updateShot:delta_t]; + break; + + default: + OOLog(@"particle.unknown", @"Invalid particle %@, removing.", self); + [UNIVERSE removeEntity:self]; + } + } + +} + + +- (void) updateExplosion:(double) delta_t +{ + float diameter = (1.0 + time_counter)*64.0; + [self setSize:NSMakeSize(diameter, diameter)]; + alpha = (duration - time_counter); + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateFlasher:(double) delta_t +{ + alpha = 0.5 * sin(duration * M_PI * (time_counter + energy)) + 0.5; +} + + +- (void) updateECMMine:(double) delta_t +{ + if (time_counter > activation_time) + { + // do ecm stuff + GLfloat radius = ECM_PULSE_INTERVAL * activation_time * SCANNER_MAX_RANGE; + if (radius > SCANNER_MAX_RANGE) + { + radius = SCANNER_MAX_RANGE; + } + NSArray* targets = [UNIVERSE getEntitiesWithinRange:radius ofEntity:self]; + if ([targets count] > 0) + { + NSNumber *ecmPulsesRemaining = [NSNumber numberWithInt:(duration - activation_time) / ECM_PULSE_INTERVAL]; + unsigned i; + + for (i = 0; i < [targets count]; i++) + { + Entity *e2 = [targets objectAtIndex:i]; + if (e2->isShip) + { + [(ShipEntity *)e2 doScriptEvent:@"shipHitByECM" + withArgument:ecmPulsesRemaining + andReactToAIMessage:@"ECM"]; + } + } + } + activation_time += ECM_PULSE_INTERVAL; // go off every half second + } + if (time_counter > duration) // until the timer runs out! + [UNIVERSE removeEntity:self]; +} + + +- (void) updateEnergyMine:(double) delta_t +{ + // new billboard routine (working at last!) + PlayerEntity *player = [PlayerEntity sharedPlayer]; + assert(player != nil); + rotMatrix = OOMatrixForBillboard(position, [player position]); + + GLfloat tf = time_counter / duration; + GLfloat stf = tf * tf; + GLfloat expansion_speed = 0.0; + if (time_counter > 0) + expansion_speed = 240 + 10 / (tf * tf); + if (expansion_speed > 1000.0) + expansion_speed = 1000.0; + + velocity.z = expansion_speed; + + collision_radius += delta_t * expansion_speed; // expand +// energy = 10000 - 9000 * tf; // 10000 -> 1000 + energy = delta_t * (100000 - 90000 * tf); // adjusted to take into account delta_t + + alpha = 0.5 * ((0.025 / tf) + 1.0 - stf); + if (alpha > 1.0) alpha = 1.0; + color_fv[0] = 1.0 - 5.0 * tf; + if (color_fv[0] > 1.0) color_fv[0] = 1.0; + if (color_fv[0] < 0.0) color_fv[0] = 0.25 * tf * randf(); + color_fv[1] = 1.0 - 5.0 * tf; + if (color_fv[1] > 1.0) color_fv[1] = 1.0; + if (color_fv[1] < 0.0) color_fv[1] = 0.0; + + // manageCollisions + if ([collidingEntities count] > 0) + { + unsigned i; + for (i = 0; i < [collidingEntities count]; i++) + { + Entity * e = (Entity *)[collidingEntities objectAtIndex:i]; + [e takeEnergyDamage:energy from:self becauseOf:[self owner]]; + } + } + + // expire after ttl + if (time_counter > duration) // until the timer runs out! + [UNIVERSE removeEntity:self]; +} + + +- (void) updateShot:(double) delta_t +{ + if ([collidingEntities count] > 0) + { + unsigned i; + for (i = 0; i < [collidingEntities count]; i++) + { + Entity * e = (Entity *)[collidingEntities objectAtIndex:i]; + if (e != [self owner]) + { + [e takeEnergyDamage:energy from:self becauseOf:[self owner]]; + velocity.x = 0.0; + velocity.y = 0.0; + velocity.z = 0.0; + [self setColor:[OOColor redColor]]; + [self setSize:NSMakeSize(64.0,64.0)]; + duration = 2.0; + time_counter = 0.0; + particle_type = PARTICLE_EXPLOSION; + } + } + } + position.x += velocity.x * delta_t; + position.y += velocity.y * delta_t; + position.z += velocity.z * delta_t; + alpha = (duration - time_counter); + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateSpark:(double) delta_t +{ + position.x += velocity.x * delta_t; + position.y += velocity.y * delta_t; + position.z += velocity.z * delta_t; + + alpha = (duration - time_counter) / duration; + if (alpha < 0.0) alpha = 0.0; + if (alpha > 1.0) alpha = 1.0; + + // fade towards transparent red + color_fv[0] = alpha * [color redComponent] + (1.0 - alpha) * 1.0; + color_fv[1] = alpha * [color greenComponent];// + (1.0 - alpha) * 0.0; + color_fv[2] = alpha * [color blueComponent];// + (1.0 - alpha) * 0.0; + + // disappear eventually + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateLaser:(double) delta_t +{ + position.x += velocity.x * delta_t; + position.y += velocity.y * delta_t; + position.z += velocity.z * delta_t; + alpha = (duration - time_counter) / PARTICLE_LASER_DURATION; + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateHyperring:(double) delta_t +{ + position.x += velocity.x * delta_t; + position.y += velocity.y * delta_t; + position.z += velocity.z * delta_t; + alpha = (duration - time_counter) / PARTICLE_LASER_DURATION; + ring_inner_radius += delta_t * size.width * 1.1; + ring_outer_radius += delta_t * size.height; + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateFragburst:(double) delta_t +{ + int i; + + for (i = 0 ; i < vertexCount; i++) + { + GLfloat du = 0.5 + 0.03125 * (32 - i); + GLfloat alf = 1.0 - time_counter / du; + if (alf < 0.0) alf = 0.0; + if (alf > 1.0) alf = 1.0; + faces[i].normal.z = alf; + vertices[i].x += vertex_normal[i].x * delta_t; + vertices[i].y += vertex_normal[i].y * delta_t; + vertices[i].z += vertex_normal[i].z * delta_t; + } + + // disappear eventually + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateBurst2:(double) delta_t +{ + int i; + size.width = (1.0 + time_counter) * size.height; // current size vs starting size + + GLfloat di = 1.0 / (vertexCount - 1); + for (i = 0 ; i < vertexCount; i++) + { + GLfloat du = duration * (0.5 + di * i); + GLfloat alf = 1.0 - time_counter / du; + if (alf < 0.0) alf = 0.0; + if (alf > 1.0) alf = 1.0; + faces[i].normal.z = alf; + vertices[i].x += vertex_normal[i].x * delta_t; + vertices[i].y += vertex_normal[i].y * delta_t; + vertices[i].z += vertex_normal[i].z * delta_t; + } + + // disappear eventually + if (time_counter > duration) + [UNIVERSE removeEntity:self]; +} + + +- (void) updateFlash:(double) delta_t +{ + GLfloat tf = duration * 0.667; + GLfloat tf1 = duration - tf; + + // move as necessary + position.x += velocity.x * delta_t; + position.y += velocity.y * delta_t; + position.z += velocity.z * delta_t; + + // scale up + size.width += delta_t * growth_rate; + size.height = size.width; + + // fade up + if ((time_counter)&&(time_counter < tf)) + alpha = time_counter/tf; + + // fade out + if (time_counter > tf) + alpha = (duration - time_counter)/tf1; + + // disappear eventually + if (time_counter > duration) + [UNIVERSE removeEntity:self]; + +} + + +- (void) updateExhaust2:(double) delta_t +{ + #if ADDITIVE_BLENDING + #define OVERALL_ALPHA 0.5f + #else + #define OVERALL_ALPHA 1.0f + #endif + + GLfloat ex_emissive[4] = {0.6f, 0.8f, 1.0f, 0.9f * OVERALL_ALPHA}; // pale blue + const GLfloat s1[8] = { 0.0, 0.707, 1.0, 0.707, 0.0, -0.707, -1.0, -0.707}; + const GLfloat c1[8] = { 1.0, 0.707, 0.0, -0.707, -1.0, -0.707, 0.0, 0.707}; + ShipEntity *ship = [self owner]; + + if ((!ship)||(!ship->isShip)) + return; + + Quaternion shipQrotation = [ship orientation]; + if (ship->isPlayer) shipQrotation.w = -shipQrotation.w; // WTF sort of silliness is this? -- Ahruman + + Frame zero; + zero.orientation = shipQrotation; + int dam = [ship damage]; + GLfloat flare_length = [ship speedFactor]; + + if (!flare_length) // don't draw if there's no fire! + return; + + GLfloat hyper_fade = 8.0f / (8.0f + flare_length * flare_length * flare_length); + + GLfloat flare_factor = flare_length * ex_emissive[3] * hyper_fade; + GLfloat red_factor = flare_length * ex_emissive[0] * (ranrot_rand() % 11) * 0.1; // random fluctuations + GLfloat green_factor = flare_length * ex_emissive[1] * hyper_fade; + + if (flare_length > 1.0) // afterburner! + { + red_factor = 1.5; + flare_length = 1.0 + 0.25 * flare_length; + } + + if ((int)(ranrot_rand() % 50) < dam - 50) // flicker the damaged engines + red_factor = 0.0; + if ((int)(ranrot_rand() % 40) < dam - 60) + green_factor = 0.0; + if ((int)(ranrot_rand() % 25) < dam - 75) + flare_factor = 0.0; + + if (flare_length < 0.1) { flare_length = 0.1;} + Vector currentPos = ship->position; + Vector vfwd = vector_forward_from_quaternion(shipQrotation); + GLfloat spd = 0.5 * [ship flightSpeed]; + vfwd = vector_multiply_scalar(vfwd, spd); + Vector master_i = vector_right_from_quaternion(shipQrotation); + Vector vi,vj,vk; + vi = master_i; + vj = vector_up_from_quaternion(shipQrotation); + vk = cross_product(vi, vj); + zero.position = make_vector(currentPos.x + vi.x * position.x + vj.x * position.y + vk.x * position.z, + currentPos.y + vi.y * position.x + vj.y * position.y + vk.y * position.z, + currentPos.z + vi.z * position.x + vj.z * position.y + vk.z * position.z); + + GLfloat i01 = -0.03 * hyper_fade;// * flare_length; + GLfloat i03 = -0.12;// * flare_length; + GLfloat i06 = -0.25;// * flare_length; + GLfloat i08 = -0.32;// * flare_length; + GLfloat i10 = -0.40;// * flare_length; + GLfloat q01 = i01/i10; // factor for trail + GLfloat q03 = i03/i10; + GLfloat q06 = i06/i10; + GLfloat q08 = i08/i10; + GLfloat r01 = 1.0 - q01; // factor for jet + GLfloat r03 = 1.0 - q03; + GLfloat r06 = 1.0 - q06; + GLfloat r08 = 1.0 - q08; + Frame f01 = [self frameAtTime: i01 fromFrame: zero]; + Vector b01 = make_vector(r01 * i01 * vfwd.x, r01 * i01 * vfwd.y, r01 * i01 * vfwd.z); + Frame f03 = [self frameAtTime: i03 fromFrame: zero]; + Vector b03 = make_vector(r03 * i03 * vfwd.x, r03 * i03 * vfwd.y, r03 * i03 * vfwd.z); + Frame f06 = [self frameAtTime: i06 fromFrame: zero]; + Vector b06 = make_vector(r06 * i06 * vfwd.x, r06 * i06 * vfwd.y, r06 * i06 * vfwd.z); + Frame f08 = [self frameAtTime: i08 fromFrame: zero]; + Vector b08 = make_vector(r08 * i08 * vfwd.x, r08 * i08 * vfwd.y, r08 * i08 * vfwd.z); + Frame f10 = [self frameAtTime: i10 fromFrame: zero]; + + int ci = 0; + int iv = 0; + int i; + float r1; + + ex_emissive[3] = flare_factor * OVERALL_ALPHA; // fade alpha towards rear of exhaust + ex_emissive[1] = green_factor; // diminish green part towards rear of exhaust + ex_emissive[0] = red_factor; // diminish red part towards rear of exhaust + verts[iv++] = f03.position.x + b03.x;// + zero.k.x * flare_factor * 4.0; + verts[iv++] = f03.position.y + b03.y;// + zero.k.y * flare_factor * 4.0; + verts[iv++] = f03.position.z + b03.z;// + zero.k.z * flare_factor * 4.0; + exhaustBaseColors[ci++] = ex_emissive[0]; + exhaustBaseColors[ci++] = ex_emissive[1]; + exhaustBaseColors[ci++] = ex_emissive[2]; + exhaustBaseColors[ci++] = ex_emissive[3]; + + ex_emissive[3] = 0.9 * flare_factor * OVERALL_ALPHA; // fade alpha towards rear of exhaust + ex_emissive[1] = 0.9 * green_factor; // diminish green part towards rear of exhaust + ex_emissive[0] = 0.9 * red_factor; // diminish red part towards rear of exhaust + Vector k1 = f01.k; + Vector j1 = cross_product(master_i, k1); + Vector i1 = cross_product(j1, k1); + + f01.position = vector_subtract(zero.position, vk); // 1m out from zero + + i1.x *= exhaustScale.x; i1.y *= exhaustScale.x; i1.z *= exhaustScale.x; + j1.x *= exhaustScale.y; j1.y *= exhaustScale.y; j1.z *= exhaustScale.y; + for (i = 0; i < 8; i++) + { + verts[iv++] = f01.position.x + b01.x + s1[i] * i1.x + c1[i] * j1.x; + verts[iv++] = f01.position.y + b01.y + s1[i] * i1.y + c1[i] * j1.y; + verts[iv++] = f01.position.z + b01.z + s1[i] * i1.z + c1[i] * j1.z; + exhaustBaseColors[ci++] = ex_emissive[0]; + exhaustBaseColors[ci++] = ex_emissive[1]; + exhaustBaseColors[ci++] = ex_emissive[2]; + exhaustBaseColors[ci++] = ex_emissive[3]; + } + + ex_emissive[3] = 0.6 * flare_factor * OVERALL_ALPHA; // fade alpha towards rear of exhaust + ex_emissive[1] = 0.6 * green_factor; // diminish green part towards rear of exhaust + ex_emissive[0] = 0.6 * red_factor; // diminish red part towards rear of exhaust + k1 = f03.k; + j1 = cross_product(master_i, k1); + i1 = cross_product(j1, k1); + i1.x *= exhaustScale.x; i1.y *= exhaustScale.x; i1.z *= exhaustScale.x; + j1.x *= exhaustScale.y; j1.y *= exhaustScale.y; j1.z *= exhaustScale.y; + for (i = 0; i < 8; i++) + { + r1 = randf(); + verts[iv++] = f03.position.x + b03.x + s1[i] * i1.x + c1[i] * j1.x + r1 * k1.x; + verts[iv++] = f03.position.y + b03.y + s1[i] * i1.y + c1[i] * j1.y + r1 * k1.y; + verts[iv++] = f03.position.z + b03.z + s1[i] * i1.z + c1[i] * j1.z + r1 * k1.z; + exhaustBaseColors[ci++] = ex_emissive[0]; + exhaustBaseColors[ci++] = ex_emissive[1]; + exhaustBaseColors[ci++] = ex_emissive[2]; + exhaustBaseColors[ci++] = ex_emissive[3]; + } + + ex_emissive[3] = 0.4 * flare_factor * OVERALL_ALPHA; // fade alpha towards rear of exhaust + ex_emissive[1] = 0.4 * green_factor; // diminish green part towards rear of exhaust + ex_emissive[0] = 0.4 * red_factor; // diminish red part towards rear of exhaust + k1 = f06.k; + j1 = cross_product(master_i, k1); + i1 = cross_product(j1, k1); + i1.x *= 0.8 * exhaustScale.x; i1.y *= 0.8 * exhaustScale.x; i1.z *= 0.8 * exhaustScale.x; + j1.x *= 0.8 * exhaustScale.y; j1.y *= 0.8 * exhaustScale.y; j1.z *= 0.8 * exhaustScale.y; + for (i = 0; i < 8; i++) + { + r1 = randf(); + verts[iv++] = f06.position.x + b06.x + s1[i] * i1.x + c1[i] * j1.x + r1 * k1.x; + verts[iv++] = f06.position.y + b06.y + s1[i] * i1.y + c1[i] * j1.y + r1 * k1.y; + verts[iv++] = f06.position.z + b06.z + s1[i] * i1.z + c1[i] * j1.z + r1 * k1.z; + exhaustBaseColors[ci++] = ex_emissive[0]; + exhaustBaseColors[ci++] = ex_emissive[1]; + exhaustBaseColors[ci++] = ex_emissive[2]; + exhaustBaseColors[ci++] = ex_emissive[3]; + } + + ex_emissive[3] = 0.2 * flare_factor * OVERALL_ALPHA; // fade alpha towards rear of exhaust + ex_emissive[1] = 0.2 * green_factor; // diminish green part towards rear of exhaust + ex_emissive[0] = 0.2 * red_factor; // diminish red part towards rear of exhaust + k1 = f08.k; + j1 = cross_product(master_i, k1); + i1 = cross_product(j1, k1); + i1.x *= 0.5 * exhaustScale.x; i1.y *= 0.5 * exhaustScale.x; i1.z *= 0.5 * exhaustScale.x; + j1.x *= 0.5 * exhaustScale.y; j1.y *= 0.5 * exhaustScale.y; j1.z *= 0.5 * exhaustScale.y; + for (i = 0; i < 8; i++) + { + r1 = randf(); + verts[iv++] = f08.position.x + b08.x + s1[i] * i1.x + c1[i] * j1.x + r1 * k1.x; + verts[iv++] = f08.position.y + b08.y + s1[i] * i1.y + c1[i] * j1.y + r1 * k1.y; + verts[iv++] = f08.position.z + b08.z + s1[i] * i1.z + c1[i] * j1.z + r1 * k1.z; + exhaustBaseColors[ci++] = ex_emissive[0]; + exhaustBaseColors[ci++] = ex_emissive[1]; + exhaustBaseColors[ci++] = ex_emissive[2]; + exhaustBaseColors[ci++] = ex_emissive[3]; + } + + ex_emissive[3] = 0.0; // fade alpha towards rear of exhaust + ex_emissive[1] = 0.0; // diminish green part towards rear of exhaust + ex_emissive[0] = 0.0; // diminish red part towards rear of exhaust + verts[iv++] = f10.position.x; + verts[iv++] = f10.position.y; + verts[iv++] = f10.position.z; + exhaustBaseColors[ci++] = ex_emissive[0]; + exhaustBaseColors[ci++] = ex_emissive[1]; + exhaustBaseColors[ci++] = ex_emissive[2]; + exhaustBaseColors[ci++] = ex_emissive[3]; +} + + +- (void) drawEntity:(BOOL) immediate:(BOOL) translucent; +{ + if (UNIVERSE == nil || [UNIVERSE breakPatternHide]) return; + + if ((particle_type == PARTICLE_FLASHER)&&(zero_distance > no_draw_distance)) return; // TOO FAR AWAY TO SEE + + if (translucent) + { + switch ([self particleType]) + { + case PARTICLE_LASER_BEAM: + [self drawLaser]; + break; + + case PARTICLE_EXHAUST: + [self drawExhaust2]; + break; + + case PARTICLE_HYPERRING: + [self drawHyperring]; + break; + + case PARTICLE_ECM_MINE: + // not a visible entity + break; + + case PARTICLE_ENERGY_MINE: + [self drawEnergyMine]; + break; + + case PARTICLE_FRAGBURST: + [self drawFragburst]; + break; + + case PARTICLE_BURST2: + [self drawBurst2]; + break; + + case PARTICLE_FLASH: + [self drawParticle]; + break; + + case PARTICLE_BILLBOARD: + [self drawBillboard]; + break; + + default: + [self drawParticle]; + break; + } + } + CheckOpenGLErrors(@"ParticleEntity after drawing %@ %@", self); +} + + +- (void) drawSubEntity:(BOOL) immediate:(BOOL) translucent +{ + if (particle_type == PARTICLE_EXHAUST) + { + if (translucent) + [self drawExhaust2]; + return; + } + + Entity *my_owner = [self owner]; + + if ([self isSubEntity] && my_owner) + { + // this test provides an opportunity to do simple LoD culling + + zero_distance = [my_owner zeroDistance]; + if (zero_distance > no_draw_distance) + return; // TOO FAR AWAY TO DRAW + } + + if (particle_type == PARTICLE_FLASHER && [self status] != STATUS_INACTIVE) + { + Vector abspos = position; // in control of it's own orientation + int view_dir = [UNIVERSE viewDirection]; + Entity *last = nil; + Entity *father = my_owner; + OOMatrix r_mat; + OOMatrix temp_matrix; + + while ((father) && (father != last) && father != NO_TARGET) + { + r_mat = [father drawRotationMatrix]; + abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]); + last = father; + + if (![last isSubEntity]) break; + father = [father owner]; + } + + if (view_dir == VIEW_GUI_DISPLAY) + { + if (translucent) + { + temp_matrix = OOMatrixLoadGLMatrix(GL_MODELVIEW_MATRIX); + glPopMatrix(); glPushMatrix(); // restore zero! + glTranslatef(abspos.x, abspos.y, abspos.z); // move to absolute position + GLfloat xx = 0.5 * size.width; + GLfloat yy = 0.5 * size.height; + + alpha = OOClamp_0_1_f(alpha); + + color_fv[3] = alpha; + glEnable(GL_TEXTURE_2D); + glColor4fv(color_fv); + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color_fv); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND); + [texture apply]; + + glBegin(GL_QUADS); + glTexCoord2f(0.0, 1.0); + glVertex3f(-xx, -yy, -xx); + + glTexCoord2f(1.0, 1.0); + glVertex3f(xx, -yy, -xx); + + glTexCoord2f(1.0, 0.0); + glVertex3f(xx, yy, -xx); + + glTexCoord2f(0.0, 0.0); + glVertex3f(-xx, yy, -xx); + + glEnd(); + + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + GLLoadOOMatrix(temp_matrix); + } + } + else + { + temp_matrix = OOMatrixLoadGLMatrix(GL_MODELVIEW_MATRIX); + glPopMatrix(); // restore zero! + glPushMatrix(); + // position and orientation is absolute + GLTranslateOOVector(abspos); + GLMultOOMatrix([[PlayerEntity sharedPlayer] drawRotationMatrix]); + + [self drawEntity:immediate:translucent]; + + GLLoadOOMatrix(temp_matrix); + } + } +} + + +- (void) drawParticle +{ + int viewdir; + + GLfloat xx = 0.5 * size.width; + GLfloat yy = 0.5 * size.height; + + alpha = OOClamp_0_1_f(alpha); + color_fv[3] = alpha; + + // movies: + // draw data required xx, yy, color_fv[0], color_fv[1], color_fv[2] + + glEnable(GL_TEXTURE_2D); + + glColor4fv(color_fv); + + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, color_fv); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND); + + [texture apply]; + + BeginAdditiveBlending(GL_ONE_YES); + + glBegin(GL_QUADS); + + viewdir = [UNIVERSE viewDirection]; + + switch (viewdir) + { + case VIEW_FORWARD: + glTexCoord2f(0.0, 1.0); + glVertex3f(-xx, -yy, -xx); + + glTexCoord2f(1.0, 1.0); + glVertex3f(xx, -yy, -xx); + + glTexCoord2f(1.0, 0.0); + glVertex3f(xx, yy, -xx); + + glTexCoord2f(0.0, 0.0); + glVertex3f(-xx, yy, -xx); + break; + + case VIEW_AFT: + glTexCoord2f(0.0, 1.0); + glVertex3f(xx, -yy, xx); + + glTexCoord2f(1.0, 1.0); + glVertex3f(-xx, -yy, xx); + + glTexCoord2f(1.0, 0.0); + glVertex3f(-xx, yy, xx); + + glTexCoord2f(0.0, 0.0); + glVertex3f(xx, yy, xx); + break; + + case VIEW_STARBOARD: + glTexCoord2f(0.0, 1.0); + glVertex3f(-xx, -yy, xx); + + glTexCoord2f(1.0, 1.0); + glVertex3f(-xx, -yy, -xx); + + glTexCoord2f(1.0, 0.0); + glVertex3f(-xx, yy, -xx); + + glTexCoord2f(0.0, 0.0); + glVertex3f(-xx, yy, xx); + break; + + case VIEW_PORT: + glTexCoord2f(0.0, 1.0); + glVertex3f(xx, -yy, -xx); + + glTexCoord2f(1.0, 1.0); + glVertex3f(xx, -yy, xx); + + glTexCoord2f(1.0, 0.0); + glVertex3f(xx, yy, xx); + + glTexCoord2f(0.0, 0.0); + glVertex3f(xx, yy, -xx); + break; + + case VIEW_CUSTOM: + { + PlayerEntity *player = [PlayerEntity sharedPlayer]; + Vector vi = [player customViewRightVector]; vi.x *= xx; vi.y *= xx; vi.z *= xx; + Vector vj = [player customViewUpVector]; vj.x *= yy; vj.y *= yy; vj.z *= yy; + Vector vk = [player customViewForwardVector]; vk.x *= xx; vk.y *= xx; vk.z *= xx; + glTexCoord2f(0.0, 1.0); + glVertex3f(-vi.x -vj.x -vk.x, -vi.y -vj.y -vk.y, -vi.z -vj.z -vk.z); + glTexCoord2f(1.0, 1.0); + glVertex3f(+vi.x -vj.x -vk.x, +vi.y -vj.y -vk.y, +vi.z -vj.z -vk.z); + glTexCoord2f(1.0, 0.0); + glVertex3f(+vi.x +vj.x -vk.x, +vi.y +vj.y -vk.y, +vi.z +vj.z -vk.z); + glTexCoord2f(0.0, 0.0); + glVertex3f(-vi.x +vj.x -vk.x, -vi.y +vj.y -vk.y, -vi.z +vj.z -vk.z); + } + break; + + default: + glTexCoord2f(0.0, 1.0); + glVertex3f(-xx, -yy, -xx); + + glTexCoord2f(1.0, 1.0); + glVertex3f(xx, -yy, -xx); + + glTexCoord2f(1.0, 0.0); + glVertex3f(xx, yy, -xx); + + glTexCoord2f(0.0, 0.0); + glVertex3f(-xx, yy, -xx); + break; + } + glEnd(); + + EndAdditiveBlending(); + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); +} + + +- (void) drawLaser +{ + color_fv[3] = 0.75; // set alpha + + glDisable(GL_CULL_FACE); // face culling + + glDisable(GL_TEXTURE_2D); + + glColor4fv(color_fv); + + BeginAdditiveBlending(GL_ONE_NO); + + glBegin(GL_QUADS); + + glVertex3f(0.25, 0.0, 0.0); + glVertex3f(0.25, 0.0, collision_radius); + glVertex3f(-0.25, 0.0, collision_radius); + glVertex3f(-0.25, 0.0, 0.0); + + glVertex3f(0.0, 0.25, 0.0); + glVertex3f(0.0, 0.25, collision_radius); + glVertex3f(0.0, -0.25, collision_radius); + glVertex3f(0.0, -0.25, 0.0); + + glEnd(); + + EndAdditiveBlending(); + + glEnable(GL_CULL_FACE); // face culling +} + +GLuint tfan1[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 1}; // initial fan 0..9 +GLuint qstrip1[18] = { 1, 9, 2, 10, 3, 11, 4, 12, 5, 13, 6, 14, 7, 15, 8, 16, 1, 9}; // first quadstrip 10..27 +GLuint qstrip2[18] = { 9, 17, 10, 18, 11, 19, 12, 20, 13, 21, 14, 22, 15, 23, 16, 24, 9, 17}; // second quadstrip 28..45 +GLuint qstrip3[18] = { 17, 25, 18, 26, 19, 27, 20, 28, 21, 29, 22, 30, 23, 31, 24, 32, 17, 25}; // third quadstrip 46..63 +GLuint tfan2[10] = { 33, 25, 26, 27, 28, 29, 30, 31, 32, 25}; // final fan 64..73 + +- (void) drawExhaust2 +{ + ShipEntity *ship = [self owner]; + + if (!ship) + return; + + if ([ship speedFactor] <= 0.0) // don't draw if there's no fire! + return; + + glPopMatrix(); // restore absolute positioning + glPushMatrix(); // avoid stack underflow + + glDisable(GL_TEXTURE_2D); + glDisable(GL_CULL_FACE); // face culling + glShadeModel(GL_SMOOTH); + + BeginAdditiveBlending(GL_ONE_YES); + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, 0, verts); + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(4, GL_FLOAT, 0, exhaustBaseColors); + glDisableClientState(GL_NORMAL_ARRAY); + glDisableClientState(GL_INDEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_EDGE_FLAG_ARRAY); + + glDrawElements(GL_TRIANGLE_FAN, 10, GL_UNSIGNED_INT, tfan1); + glDrawElements(GL_QUAD_STRIP, 18, GL_UNSIGNED_INT, qstrip1); + glDrawElements(GL_QUAD_STRIP, 18, GL_UNSIGNED_INT, qstrip2); + glDrawElements(GL_QUAD_STRIP, 18, GL_UNSIGNED_INT, qstrip3); + glDrawElements(GL_TRIANGLE_FAN, 10, GL_UNSIGNED_INT, tfan2); + + EndAdditiveBlending(); + + glEnable(GL_CULL_FACE); // face culling + glEnable(GL_TEXTURE_2D); +} + + +- (void) drawHyperring +{ + int i; + GLfloat aleph = (alpha < 2.0) ? alpha*0.5: 1.0; + + GLfloat ex_em_hi[4] = {0.6, 0.8, 1.0, aleph}; // pale blue + GLfloat ex_em_lo[4] = {0.2, 0.0, 1.0, 0.0}; // purplish-blue-black + + glPushMatrix(); + glDisable(GL_CULL_FACE); // face culling + glDisable(GL_TEXTURE_2D); + glShadeModel(GL_SMOOTH); + + BeginAdditiveBlending(GL_ONE_YES); + + // movies: + // draw data required ring_inner_radius, ring_outer_radius + + glBegin(GL_TRIANGLE_STRIP); + for (i = 0; i < 65; i++) + { + glColor4fv(ex_em_lo); + glVertex3f(ring_inner_radius*circleVertex[i].x, ring_inner_radius*circleVertex[i].y, ring_inner_radius*circleVertex[i].z ); + glColor4fv(ex_em_hi); + glVertex3f(ring_outer_radius*circleVertex[i].x, ring_outer_radius*circleVertex[i].y, ring_outer_radius*circleVertex[i].z ); + } + glEnd(); + + EndAdditiveBlending(); + + glEnable(GL_CULL_FACE); // face culling + glPopMatrix(); +} + + +- (void) drawEnergyMine +{ + double szd = sqrt(zero_distance); + + color_fv[3] = alpha; // set alpha + + glDisable(GL_CULL_FACE); // face culling + glDisable(GL_TEXTURE_2D); + + BeginAdditiveBlending(GL_ONE_YES); + + int step = 4; + + glColor4fv(color_fv); + glBegin(GL_TRIANGLE_FAN); + + GLDrawBallBillboard(collision_radius, step, szd); + + glEnd(); + + EndAdditiveBlending(); + + glEnable(GL_CULL_FACE); // face culling +} + + +- (void) drawFragburst +{ + int i; + + glEnable(GL_TEXTURE_2D); + [texture apply]; + glPushMatrix(); + + BeginAdditiveBlending(GL_ONE_YES); + + glBegin(GL_QUADS); + for (i = 0; i < vertexCount; i++) + { + glColor4f(faces[i].red, faces[i].green, faces[i].blue, faces[i].normal.z); + DrawQuadForView(vertices[i].x, vertices[i].y, vertices[i].z, faces[i].normal.x, faces[i].normal.x); + } + glEnd(); + + EndAdditiveBlending(); + + glPopMatrix(); + glDisable(GL_TEXTURE_2D); +} + + +- (void) drawBurst2 +{ + int i; + + glEnable(GL_TEXTURE_2D); + [texture apply]; + glPushMatrix(); + + BeginAdditiveBlending(GL_ONE_YES); + + glBegin(GL_QUADS); + for (i = 0; i < vertexCount; i++) + { + glColor4f(faces[i].red, faces[i].green, faces[i].blue, faces[i].normal.z); + DrawQuadForView(vertices[i].x, vertices[i].y, vertices[i].z, size.width, size.width); + } + glEnd(); + + EndAdditiveBlending(); + + glPopMatrix(); + glDisable(GL_TEXTURE_2D); +} + + +- (void) drawBillboard +{ + glColor4fv(color_fv); + glEnable(GL_TEXTURE_2D); + [texture apply]; + glPushMatrix(); + + glBegin(GL_QUADS); + DrawQuadForView(position.x, position.y, position.z, size.width, size.height); + glEnd(); + + glPopMatrix(); +} + + +static void DrawQuadForView(GLfloat x, GLfloat y, GLfloat z, GLfloat xx, GLfloat yy) +{ + int viewdir = [UNIVERSE viewDirection]; + + switch (viewdir) + { + case VIEW_FORWARD: + glTexCoord2f(0.0, 1.0); glVertex3f(x-xx, y-yy, z); + glTexCoord2f(1.0, 1.0); glVertex3f(x+xx, y-yy, z); + glTexCoord2f(1.0, 0.0); glVertex3f(x+xx, y+yy, z); + glTexCoord2f(0.0, 0.0); glVertex3f(x-xx, y+yy, z); + case VIEW_AFT: + glTexCoord2f(0.0, 1.0); glVertex3f(x+xx, y-yy, z); + glTexCoord2f(1.0, 1.0); glVertex3f(x-xx, y-yy, z); + glTexCoord2f(1.0, 0.0); glVertex3f(x-xx, y+yy, z); + glTexCoord2f(0.0, 0.0); glVertex3f(x+xx, y+yy, z); + break; + case VIEW_STARBOARD: + glTexCoord2f(0.0, 1.0); glVertex3f(x, y-yy, z+xx); + glTexCoord2f(1.0, 1.0); glVertex3f(x, y-yy, z-xx); + glTexCoord2f(1.0, 0.0); glVertex3f(x, y+yy, z-xx); + glTexCoord2f(0.0, 0.0); glVertex3f(x, y+yy, z+xx); + break; + case VIEW_PORT: + glTexCoord2f(0.0, 1.0); glVertex3f(x, y-yy, z-xx); + glTexCoord2f(1.0, 1.0); glVertex3f(x, y-yy, z+xx); + glTexCoord2f(1.0, 0.0); glVertex3f(x, y+yy, z+xx); + glTexCoord2f(0.0, 0.0); glVertex3f(x, y+yy, z-xx); + break; + case VIEW_CUSTOM: + { + PlayerEntity *player = [PlayerEntity sharedPlayer]; + Vector vi = [player customViewRightVector]; vi.x *= xx; vi.y *= xx; vi.z *= xx; + Vector vj = [player customViewUpVector]; vj.x *= yy; vj.y *= yy; vj.z *= yy; + glTexCoord2f(0.0, 1.0); glVertex3f(x - vi.x - vj.x, y - vi.y - vj.y, z - vi.z - vj.z); + glTexCoord2f(1.0, 1.0); glVertex3f(x + vi.x - vj.x, y + vi.y - vj.y, z + vi.z - vj.z); + glTexCoord2f(1.0, 0.0); glVertex3f(x + vi.x + vj.x, y + vi.y + vj.y, z + vi.z + vj.z); + glTexCoord2f(0.0, 0.0); glVertex3f(x - vi.x + vj.x, y - vi.y + vj.y, z - vi.z + vj.z); + break; + } + default: + glTexCoord2f(0.0, 1.0); glVertex3f(x-xx, y-yy, z); + glTexCoord2f(1.0, 1.0); glVertex3f(x+xx, y-yy, z); + glTexCoord2f(1.0, 0.0); glVertex3f(x+xx, y+yy, z); + glTexCoord2f(0.0, 0.0); glVertex3f(x-xx, y+yy, z); + break; + } +} + + +- (BOOL)isFlasher +{ + return particle_type == PARTICLE_FLASHER; +} + + +- (BOOL)isExhaust +{ + return particle_type == PARTICLE_EXHAUST; +} + +@end + + +@implementation Entity (OOParticleExtensions) + +- (BOOL)isParticle +{ + return isParticle; +} + + +- (BOOL)isFlasher +{ + return NO; +} + + +- (BOOL)isExhaust +{ + return NO; +} + +@end diff --git a/src/Core/Entities/PlanetEntity.h b/src/Core/Entities/PlanetEntity.h new file mode 100644 index 00000000..a5f49be7 --- /dev/null +++ b/src/Core/Entities/PlanetEntity.h @@ -0,0 +1,190 @@ +/* + +PlanetEntity.h + +Entity subclass representing a planet. Or a sun. Or an atmosphere. Basically +anything big and ball-shaped. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOSelfDrawingEntity.h" +#import "legacy_random.h" +#import "OOColor.h" + + +typedef enum +{ + PLANET_TYPE_GREEN, + PLANET_TYPE_SUN, + PLANET_TYPE_ATMOSPHERE, + PLANET_TYPE_MOON, + PLANET_TYPE_MINIATURE +} OOPlanetType; + + +#define ATMOSPHERE_DEPTH 500.0 +#define MAX_CORONAFLARE 600000.0 // nova flare +#define PLANET_MINIATURE_FACTOR 0.00185 + +#define MAX_SUBDIVIDE 6 // 0 -> 20 verts + // 1 -> 80 verts + // 2 -> 320 verts + // 3 -> 1280 verts + // 4 -> 5120 verts + // 5 -> 20480 verts !! +#define MAX_TRI_INDICES 3*(20+80+320+1280+5120+20480) + + +@class ShipEntity; + + +typedef struct +{ + Vector vertex_array[10400 + 2]; + GLfloat color_array[4*10400]; + GLfloat uv_array[2*10400]; + Vector normal_array[10400]; + GLuint index_array[MAX_TRI_INDICES]; +} VertexData; + +@interface PlanetEntity: OOSelfDrawingEntity +{ +@public + GLfloat sun_diffuse[4]; + GLfloat sun_specular[4]; + + int lastSubdivideLevel; + +@protected + OOPlanetType planet_type; + int r_seed[MAX_VERTICES_PER_ENTITY]; + GLuint displayListNames[MAX_SUBDIVIDE]; + + BOOL isTextured; + BOOL isTextureImage; //is the texture a png image? + GLuint textureName; + NSString *textureFile; + unsigned char *textureData; + +#ifndef NO_SHADERS + BOOL isShadered; + GLhandleARB shader_program; +#endif + + GLuint normalMapTextureName; + unsigned char *normalMapTextureData; + + int planet_seed; + double polar_color_factor; + + double rotational_velocity; + + GLfloat amb_land[4]; + GLfloat amb_polar_land[4]; + GLfloat amb_sea[4]; + GLfloat amb_polar_sea[4]; + + PlanetEntity *atmosphere; // secondary sphere used to show atmospheric details + PlanetEntity *root_planet; // link back to owning planet + + int shuttles_on_ground; // starting number of shuttles + double last_launch_time; // space launches out by about 15 minutes + double shuttle_launch_interval; // space launches out by about 15 minutes + + double sqrt_zero_distance; + + // the normal array can be the base_vertex_array + // the index array can come from the vertex_index_array + VertexData vertexdata; + + double cor4k, lim4k; + double cor8k, lim8k; + double cor16k, lim16k; + + Vector rotationAxis; +} + +// straight c +double corona_speed_factor; // multiply delta_t by this before adding it to corona_stage +double corona_stage; // 0.0 -> 1.0 +GLfloat rvalue[729]; // stores random values for adjusting colors in the corona + +- (id) initSunWithColor:(OOColor*)sun_color andDictionary:(NSDictionary*) dict; +- (id) initWithSeed:(Random_Seed) p_seed; +- (void) miniaturize; +- (id) initMiniatureFromPlanet:(PlanetEntity*) planet; +- (id) initMiniatureFromPlanet:(PlanetEntity*) planet withAlpha:(float) alpha; + +- (id) initMoonFromDictionary:(NSDictionary*) dict; +- (id) initPlanetFromDictionary:(NSDictionary*) dict; +- (id) initPlanetFromDictionary:(NSDictionary*) dict withAtmosphere: (BOOL) atmo andSeed:(Random_Seed) p_seed; +- (BOOL) setUpPlanetFromTexture:(NSString *)fileName; +- (BOOL) setSunColor:(OOColor*)sun_color; +- (BOOL) changeSunProperty:(NSString *)key withDictionary:(NSDictionary*) dict; + +void drawActiveCorona(GLfloat inner_radius, GLfloat outer_radius, GLfloat step, GLfloat z_distance, GLfloat* col4v1, int rv); + +- (int*) r_seed; +- (int) planet_seed; +- (BOOL) isTextured; +- (GLuint) textureName; +- (NSString *) textureFileName; + +- (double) polar_color_factor; +- (GLfloat *) amb_land; +- (GLfloat *) amb_polar_land; +- (GLfloat *) amb_sea; +- (GLfloat *) amb_polar_sea; + +- (OOPlanetType) planetType; +- (void) setPlanetType:(OOPlanetType) pt; + + +- (double) radius; // metres +- (void) setRadius:(double) rad; +- (void) rescaleTo:(double) rad; + +- (double) sqrtZeroDistance; + +- (BOOL) hasAtmosphere; + +- (void) drawModelWithVertexArraysAndSubdivision: (int) subdivide; + +- (void) launchShuttle; + +- (void) welcomeShuttle:(ShipEntity *) shuttle; + ++ (void) resetBaseVertexArray; +- (void) initialiseBaseVertexArray; + +int baseVertexIndexForEdge(int va, int vb, BOOL textured); + +- (void) initialiseBaseTerrainArray:(int) percent_land; +- (void) paintVertex:(int) vi :(int) seed; +- (void) scaleVertices; + +double longitudeFromVector(Vector v); + +- (BOOL) willGoNova; +- (BOOL) goneNova; +- (void) setGoingNova:(BOOL) yesno inTime:(double)interval; + +@end diff --git a/src/Core/Entities/PlanetEntity.m b/src/Core/Entities/PlanetEntity.m new file mode 100644 index 00000000..4ff548ce --- /dev/null +++ b/src/Core/Entities/PlanetEntity.m @@ -0,0 +1,2021 @@ +/* + +PlanetEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlanetEntity.h" +#import "OOOpenGLExtensionManager.h" + +#import "Universe.h" +#import "AI.h" +#import "TextureStore.h" +#import "MyOpenGLView.h" +#import "ShipEntityAI.h" +#import "OOColor.h" +#import "OOCharacter.h" +#import "OOStringParsing.h" +#import "PlayerEntity.h" +#import "OOCollectionExtractors.h" + +#define kOOLogUnconvertedNSLog @"unclassified.PlanetEntity" + + +#define LIM500 500.0*500.0 * NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR +#define LIM4K 4000.0*4000.0 * NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR +#define LIM8K 8000.0*8000.0 * NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR +#define LIM16K 16000.0*16000.0 * NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR + + +// straight c +static Vector base_vertex_array[10400]; +static int base_terrain_array[10400]; +static int next_free_vertex; +NSMutableDictionary* edge_to_vertex = nil; + +static int n_triangles[MAX_SUBDIVIDE]; +static int triangle_start[MAX_SUBDIVIDE]; +static GLuint vertex_index_array[3*(20+80+320+1280+5120+20480)]; + +static GLfloat texture_uv_array[10400 * 2]; +static float corona_blending; + + +@interface PlanetEntity (OOPrivate) + +- (id) initAsAtmosphereForPlanet:(PlanetEntity *)planet; +- (id) initAsAtmosphereForPlanet:(PlanetEntity *)planet dictionary:(NSDictionary *)dict; +- (void) setTextureColorForPlanet:(BOOL)isMain inSystem:(BOOL)isLocal; +@end + + +@implementation PlanetEntity + +- (id) init +{ + unsigned i; + unsigned percent_land; + + self = [super init]; + + isTextured = NO; + textureFile = nil; + + collision_radius = 25000.0; // 25km across + + scanClass = CLASS_NO_DRAW; + + orientation.w = M_SQRT1_2; // represents a 90 degree rotation around x axis + orientation.x = M_SQRT1_2; // (I hope!) + orientation.y = 0.0; + orientation.z = 0.0; + + planet_type = PLANET_TYPE_GREEN; + + shuttles_on_ground = 0; + last_launch_time = 0.0; + shuttle_launch_interval = 3600.0; + + for (i = 0; i < 5; i++) + displayListNames[i] = 0; // empty for now! + + [self setModelName:@"icosahedron.dat"]; + + [self rescaleTo:1.0]; + + planet_seed = 54321; + + ranrot_srand(planet_seed); + percent_land = (ranrot_rand() % 50); + + for (i = 0; i < vertexCount; i++) + { + if (Ranrot() % 100 < percent_land) r_seed[i] = 0; // land + else r_seed[i] = 1; // sea + } + + polar_color_factor = 1.0; + + amb_land[0] = 0.0; + amb_land[1] = 1.0; + amb_land[2] = 0.0; + amb_land[3] = 1.0; + amb_sea[0] = 0.0; + amb_sea[1] = 0.0; + amb_sea[2] = 1.0; + amb_sea[3] = 1.0; + amb_polar_land[0] = 0.9; + amb_polar_land[1] = 0.9; + amb_polar_land[2] = 0.9; + amb_polar_land[3] = 1.0; + amb_polar_sea[0] = 1.0; + amb_polar_sea[1] = 1.0; + amb_polar_sea[2] = 1.0; + amb_polar_sea[3] = 1.0; + + isPlanet = YES; + + root_planet = self; + + textureData = NULL; + + rotationAxis = kBasisYVector; + + return self; +} + + +- (BOOL) setSunColor:(OOColor*)sun_color +{ + if (sun_color == nil) return NO; + + OOCGFloat hue, sat, bri, alf; + OOColor *color; + + [sun_color getHue:&hue saturation:&sat brightness:&bri alpha:&alf]; + hue /=360; + + float hue_drift = 0.34f * abs(randf() - randf()); + + // set the lighting color for the sun + GLfloat r,g,b,a; + [sun_color getGLRed:&r green:&g blue:&b alpha:&a]; + + GLfloat sun_ambient[] = { 0.0, 0.0, 0.0, 1.0}; // real ambient light inside gl_LightModel.ambient + sun_diffuse[0] = 0.5 * (1.0 + r); // paler + sun_diffuse[1] = 0.5 * (1.0 + g); // paler + sun_diffuse[2] = 0.5 * (1.0 + b); // paler + sun_diffuse[3] = 1.0; + sun_specular[0] = r; + sun_specular[1] = g; + sun_specular[2] = b; + sun_specular[3] = 1.0; + + glLightfv(GL_LIGHT1, GL_AMBIENT, sun_ambient); + glLightfv(GL_LIGHT1, GL_DIFFUSE, sun_diffuse); + glLightfv(GL_LIGHT1, GL_SPECULAR, sun_specular); + + // main disc less saturation more brightness + color = [OOColor colorWithCalibratedHue: hue saturation: sat * 0.333 brightness: 1.0 alpha: alf]; + amb_land[0] = [color redComponent]; + amb_land[1] = [color greenComponent]; + amb_land[2] = [color blueComponent]; + amb_land[3] = 1.0; + + // nearest corona much more saturation + hue += hue_drift; + if (hue > 1.0) hue -= 1.0; + color = [OOColor colorWithCalibratedHue:hue saturation: sat * 0.625 brightness:(bri + 2.0)/3.0 alpha:alf]; + amb_polar_land[0] = [color redComponent]; + amb_polar_land[1] = [color greenComponent]; + amb_polar_land[2] = [color blueComponent]; + amb_polar_land[3] = 1.0; + + // next corona slightly more saturation + hue += hue_drift; + if (hue > 1.0) hue -= 1.0; + color = [OOColor colorWithCalibratedHue:hue saturation:sat brightness:bri alpha:alf]; + amb_sea[0] = [color redComponent]; + amb_sea[1] = [color greenComponent]; + amb_sea[2] = [color blueComponent]; + amb_sea[3] = 1.0; + + // last corona, highest saturation, less bright + hue += hue_drift; + if (hue > 1.0) hue -= 1.0; + // saturation = 1 would shift white to red + color = [OOColor colorWithCalibratedHue:hue saturation:OOClamp_0_1_f(sat*1.3) brightness:bri * 0.75 alpha:alf*0.6]; + amb_polar_sea[0] = [color redComponent]; + amb_polar_sea[1] = [color greenComponent]; + amb_polar_sea[2] = [color blueComponent]; + amb_polar_sea[3] = 1.0; + + return YES; +} + + +- (id) initSunWithColor:(OOColor *)sun_color andDictionary:(NSDictionary *) dict +{ + int i; + double sun_radius; + + self = [super init]; + + isTextured = NO; +#ifndef NO_SHADERS + isShadered = NO; +#endif + + collision_radius = 100000.0; // 100km across + + scanClass = CLASS_NO_DRAW; + planet_type = PLANET_TYPE_SUN; + shuttles_on_ground = 0; + last_launch_time = 0.0; + shuttle_launch_interval = 3600.0; + + for (i = 0; i < 5; i++) displayListNames[i] = 0; // empty for now! + + [self setSunColor:sun_color]; + + corona_blending=OOClamp_0_1_f([dict floatForKey:@"corona_hues" defaultValue:1.0f]); + corona_speed_factor=[dict floatForKey:@"corona_shimmer" defaultValue:-1.0]; + if(corona_speed_factor<0) + { + // from .22222 to 2 + corona_speed_factor = 1.0 / (0.5 + 2.0 * (randf() + randf())); + } + else + { + //on average: 0 = .25 , 1 = 2.25 - the same sun should give the same random component + corona_speed_factor=OOClamp_0_1_f(corona_speed_factor) * 2.0 + randf() * randf(); + } + corona_stage = 0.0; + for (i = 0; i < 729; i++) + rvalue[i] = randf(); + + sun_radius=[dict floatForKey:@"sun_radius"]; + // set the corona first + [self setRadius: sun_radius + (0.66*MAX_CORONAFLARE * OOClamp_0_1_f([dict floatForKey:@"corona_flare" defaultValue:0.0f]))]; + // then set the real radius + collision_radius = sun_radius; + + isPlanet = YES; + root_planet = self; + textureData = NULL; + return self; +} + + +- (id) initAsAtmosphereForPlanet:(PlanetEntity *)planet +{ + return [self initAsAtmosphereForPlanet:planet dictionary:nil]; +} + + +- (id) initAsAtmosphereForPlanet:(PlanetEntity *)planet dictionary:(NSDictionary *)dict +{ + int i; + int percent_land; + +#if ALLOW_PROCEDURAL_PLANETS + BOOL procGen = [UNIVERSE doProcedurallyTexturedPlanets]; +#endif + + if (dict == nil) dict = [NSDictionary dictionary]; + + self = [super init]; + + percent_land = 100 - [dict intForKey:@"percent_cloud" defaultValue:100 - (3 + (gen_rnd_number() & 31)+(gen_rnd_number() & 31))]; + + polar_color_factor = 1.0; + +#define CLEAR_SKY_ALPHA 0.05 +#define CLOUD_ALPHA 0.50 +#define POLAR_CLEAR_SKY_ALPHA 0.34 +#define POLAR_CLOUD_ALPHA 0.75 + + amb_land[0] = gen_rnd_number() / 256.0; + amb_land[1] = gen_rnd_number() / 256.0; + amb_land[2] = gen_rnd_number() / 256.0; + amb_land[3] = CLEAR_SKY_ALPHA; // blue sky, zero clouds + amb_sea[0] = 0.5 + gen_rnd_number() / 512.0; + amb_sea[1] = 0.5 + gen_rnd_number() / 512.0; + amb_sea[2] = 0.5 + gen_rnd_number() / 512.0; + amb_sea[3] = CLOUD_ALPHA; // 50% opaque clouds + amb_polar_land[0] = gen_rnd_number() / 256.0; + amb_polar_land[1] = gen_rnd_number() / 256.0; + amb_polar_land[2] = gen_rnd_number() / 256.0; + amb_polar_land[3] = POLAR_CLEAR_SKY_ALPHA; // 34% gray clouds + amb_polar_sea[0] = 0.9 + gen_rnd_number() / 2560.0; + amb_polar_sea[1] = 0.9 + gen_rnd_number() / 2560.0; + amb_polar_sea[2] = 0.9 + gen_rnd_number() / 2560.0; + amb_polar_sea[3] = POLAR_CLOUD_ALPHA; // 75% clouds + + // Colour overrides from dictionary + OOColor *clearSkyColor = nil; + OOColor *cloudColor = nil; + OOColor *polarClearSkyColor = nil; + OOColor *polarCloudColor = nil; + float cloudAlpha; + + + clearSkyColor = [OOColor colorWithDescription:[dict objectForKey:@"clear_sky_color"]]; + cloudColor = [OOColor colorWithDescription:[dict objectForKey:@"cloud_color"]]; + polarClearSkyColor = [OOColor colorWithDescription:[dict objectForKey:@"polar_clear_sky_color"]]; + polarCloudColor = [OOColor colorWithDescription:[dict objectForKey:@"polar_cloud_color"]]; + cloudAlpha = OOClamp_0_1_f([dict floatForKey:@"cloud_alpha" defaultValue:1.0]); + + if (clearSkyColor != nil) + { + [clearSkyColor getGLRed:&amb_land[0] green:&amb_land[1] blue:&amb_land[2] alpha:&amb_land[3]]; + } + + if (cloudColor != nil) + { + [cloudColor getGLRed:&amb_sea[0] green:&amb_sea[1] blue:&amb_sea[2] alpha:&amb_sea[3]]; + } + + if (polarClearSkyColor != nil) + { + [polarClearSkyColor getGLRed:&amb_polar_land[0] green:&amb_polar_land[1] blue:&amb_polar_land[2] alpha:&amb_polar_land[3]]; + } + else if (clearSkyColor != nil) + { + memmove(amb_polar_land, amb_land, sizeof amb_polar_land); + amb_polar_land[3] = OOClamp_0_1_f(amb_polar_land[3] * (POLAR_CLEAR_SKY_ALPHA / CLEAR_SKY_ALPHA)); + } + + if (polarCloudColor != nil) + { + [polarCloudColor getGLRed:&amb_polar_sea[0] green:&amb_polar_sea[1] blue:&amb_polar_sea[2] alpha:&amb_polar_sea[3]]; + } + else if (cloudColor != nil) + { + memmove(amb_polar_sea, amb_sea, sizeof amb_polar_sea); + amb_polar_sea[3] *= (POLAR_CLOUD_ALPHA / CLOUD_ALPHA); + } + + //amb_land[3] is already 0.05 + amb_sea[3] *= cloudAlpha; + amb_polar_land[3] *= cloudAlpha; + amb_polar_sea[3] *= cloudAlpha; + + amb_sea[3] = OOClamp_0_1_f(amb_sea[3]); + amb_polar_sea[3] = OOClamp_0_1_f(amb_polar_sea[3]); + + atmosphere = nil; + +#if ALLOW_PROCEDURAL_PLANETS + if (procGen) + { + RANROTSeed ranrotSavedSeed = RANROTGetFullSeed(); + RNG_Seed saved_seed = currentRandomSeed(); + cloudColor = [OOColor colorWithCalibratedRed: amb_sea[0] green: amb_sea[1] blue: amb_sea[2] alpha: amb_sea[3]]; + float cloud_bias = -0.01 * (float)percent_land; + float cloud_impress = 1.0 - cloud_bias; + + textureName = [TextureStore getCloudTextureNameFor:cloudColor :cloud_impress :cloud_bias intoData: &textureData]; + isTextured = (textureName != 0); + setRandomSeed(saved_seed); + RANROTSetFullSeed(ranrotSavedSeed); + } + else +#endif + { + textureName = 0; + isTextured = NO; + } +#ifndef NO_SHADERS + isShadered = NO; + shader_program = NULL_SHADER; +#endif + if (!planet) + { + OOLogERR(@"planet.atmosphere.init.noPlanet", @"planet entity initAsAtmosphereForPlanet: no planet found."); + return self; + } + + [self setOwner: planet]; + + position = planet->position; + orientation = planet->orientation; + + if (planet->planet_type == PLANET_TYPE_GREEN) + collision_radius = planet->collision_radius + ATMOSPHERE_DEPTH; // atmosphere is 500m deep only + if (planet->planet_type == PLANET_TYPE_MINIATURE) + collision_radius = planet->collision_radius + ATMOSPHERE_DEPTH * PLANET_MINIATURE_FACTOR*2.0; //not to scale: invisible otherwise + + shuttles_on_ground = 0; + last_launch_time = 0.0; + shuttle_launch_interval = 3600.0; + + scanClass = CLASS_NO_DRAW; + + orientation.w = M_SQRT1_2; + orientation.x = M_SQRT1_2; + orientation.y = 0.0; + orientation.z = 0.0; + + planet_type = PLANET_TYPE_ATMOSPHERE; + + planet_seed = ranrot_rand(); // random set-up for vertex colours + + for (i = 0; i < 5; i++) + displayListNames[i] = 0; // empty for now! + + [self setModelName:(isTextured)? @"icostextured.dat" : @"icosahedron.dat"]; + [self rescaleTo:1.0]; + [self initialiseBaseVertexArray]; + [self initialiseBaseTerrainArray:percent_land]; + for (i = 0; i < next_free_vertex; i++) + [self paintVertex:i :planet_seed]; + + [self scaleVertices]; + + // set speed of rotation + rotational_velocity = [dict floatForKey:@"atmosphere_rotational_velocity" defaultValue:0.01f + 0.02f * randf()]; // 0.01 .. 0.03 avr 0.02 + + isPlanet = YES; + + root_planet = planet; + + rotationAxis = kBasisYVector; + + return self; +} + + +- (id) initWithSeed:(Random_Seed) p_seed +{ + // this is exclusively called to initialise the main planet + NSMutableDictionary* planetInfo = [NSMutableDictionary dictionaryWithDictionary:[UNIVERSE generateSystemData:p_seed]]; +#if ALLOW_PROCEDURAL_PLANETS + if (![UNIVERSE doProcedurallyTexturedPlanets]) +#endif + { + //only allow .png textures with procedural textures + [planetInfo removeObjectForKey:@"texture"]; + } + + [planetInfo setBool:equal_seeds(p_seed, [UNIVERSE systemSeed]) forKey:@"mainForLocalSystem"]; + return [self initPlanetFromDictionary:planetInfo withAtmosphere:YES andSeed:p_seed]; +} + + +- (void) miniaturize +{ + shuttles_on_ground = 0; + last_launch_time = 0.0; + shuttle_launch_interval = 3600.0; + [self setStatus:STATUS_COCKPIT_DISPLAY]; + planet_type = PLANET_TYPE_MINIATURE; + collision_radius = [self collisionRadius] * PLANET_MINIATURE_FACTOR; // teeny tiny + [self rescaleTo:1.0]; + [self scaleVertices]; + if (!!atmosphere) + { + atmosphere->collision_radius = collision_radius + ATMOSPHERE_DEPTH * PLANET_MINIATURE_FACTOR*2.0; //not to scale: invisible otherwise + [atmosphere rescaleTo:1.0]; + [atmosphere scaleVertices]; + } + rotational_velocity = 0.04; + rotationAxis = kBasisYVector; +} + + +- (id) initMiniatureFromPlanet:(PlanetEntity*) planet +{ + return [self initMiniatureFromPlanet:planet withAlpha:1.0f]; +} + + +- (id) initMiniatureFromPlanet:(PlanetEntity*) planet withAlpha:(float) alpha +{ + int i; + + if (!planet) return nil; + + self = [super init]; + + isTextured = [planet isTextured]; + textureName = [planet textureName]; + planet_seed = [planet planet_seed]; + + shuttles_on_ground = 0; + last_launch_time = 0.0; + shuttle_launch_interval = 3600.0; + + collision_radius = [planet collisionRadius] * PLANET_MINIATURE_FACTOR; // teeny tiny + + scanClass = CLASS_NO_DRAW; + [self setStatus:STATUS_COCKPIT_DISPLAY]; + + orientation = planet->orientation; + + if ([planet planetType] == PLANET_TYPE_ATMOSPHERE) + { + planet_type = PLANET_TYPE_ATMOSPHERE; + rotational_velocity = 0.02; + } + else + { + planet_type = PLANET_TYPE_MINIATURE; + rotational_velocity = 0.04; + } + + for (i = 0; i < 5; i++) + displayListNames[i] = 0; // empty for now! + + [self setModelName:(isTextured)? @"icostextured.dat" : @"icosahedron.dat"]; + + [self rescaleTo:1.0]; + + for (i = 0; i < 3; i++) + { + amb_land[i] = [planet amb_land][i]; + amb_sea[i] = [planet amb_sea][i]; + amb_polar_land[i] = [planet amb_polar_land][i]; + amb_polar_sea[i] = [planet amb_polar_sea][i]; + } + //alpha channel + amb_land[i] = [planet amb_land][i] * alpha; + amb_sea[i] = [planet amb_sea][i] * alpha; + amb_polar_land[i] = [planet amb_polar_land][i] * alpha; + amb_polar_sea[i] = [planet amb_polar_sea][i] * alpha; + + + vertexdata=planet->vertexdata; + [self scaleVertices]; + + if (planet->atmosphere) + { + // copy clouds but make fainter if isTextureImage + atmosphere = [[PlanetEntity alloc] initMiniatureFromPlanet:planet->atmosphere withAlpha:planet->isTextureImage ? 0.6f : 1.0f ]; + atmosphere->collision_radius = collision_radius + ATMOSPHERE_DEPTH * PLANET_MINIATURE_FACTOR*2.0; //not to scale: invisible otherwise + [atmosphere rescaleTo:1.0]; + [atmosphere scaleVertices]; + + isPlanet = YES; + root_planet = self; + } + + rotationAxis = kBasisYVector; + + return self; +} + +- (id) initPlanetFromDictionary:(NSDictionary*) dict + +{ + return [self initPlanetFromDictionary:dict withAtmosphere:YES andSeed:[UNIVERSE systemSeed]]; +} + +- (id) initPlanetFromDictionary:(NSDictionary*)dict withAtmosphere:(BOOL)atmo andSeed:(Random_Seed)p_seed +{ + int i; + int percent_land; + BOOL procGen = NO; +#if ALLOW_PROCEDURAL_PLANETS + procGen = [UNIVERSE doProcedurallyTexturedPlanets]; +#endif + + if (dict == nil) dict = [NSDictionary dictionary]; + RANROTSeed ranrotSavedSeed = RANROTGetFullSeed(); + + self = [super init]; + + if (atmo) + planet_seed = p_seed.a * 13 + p_seed.c * 11 + p_seed.e * 7; // pseudo-random set-up for vertex colours + else + planet_seed = p_seed.a * 7 + p_seed.c * 11 + p_seed.e * 13; // pseudo-random set-up for vertex colours + + isTextureImage = NO; + textureFile = nil; + if ([dict objectForKey:@"texture"]) + { + textureFile = [[dict stringForKey:@"texture"] retain]; + textureName = [TextureStore getTextureNameFor:textureFile]; + isTextureImage = isTextured = (textureName != 0); + } + else + { + if (procGen) + { + //generate the texture later + isTextured = NO; + } + else + { + textureName = atmo ? 0: [TextureStore getTextureNameFor:@"metal.png"]; + isTextured = (textureName != 0); + } + } + + NSString *seedStr = [dict stringForKey:@"seed"]; + if (seedStr != nil) + { + Random_Seed seed = RandomSeedFromString(seedStr); + if (!is_nil_seed(seed)) + { + p_seed = seed; + } + else + { + OOLogERR(@"planet.fromDict", @"could not interpret \"%@\" as planet seed, using default.", seedStr); + } + } + + seed_for_planet_description(p_seed); + + NSMutableDictionary *planetInfo = [NSMutableDictionary dictionaryWithDictionary:[UNIVERSE generateSystemData:p_seed]]; + int radius_km = [dict intForKey:KEY_RADIUS + defaultValue:[planetInfo intForKey:KEY_RADIUS]]; + int techlevel = [dict intForKey:KEY_TECHLEVEL + defaultValue:[planetInfo intForKey:KEY_TECHLEVEL]]; + + shuttles_on_ground = 1 + floor(techlevel * 0.5); + last_launch_time = 0.0; + shuttle_launch_interval = 3600.0 / shuttles_on_ground; // all are launched in an hour + + last_launch_time = 30.0 - shuttle_launch_interval; // debug - launch 30s after player enters universe + + collision_radius = radius_km * 10.0; // scale down by a factor of 100 ! + + scanClass = CLASS_NO_DRAW; + + orientation.w = M_SQRT1_2; + orientation.x = M_SQRT1_2; + orientation.y = 0.0; + orientation.z = 0.0; + + planet_type = atmo ? PLANET_TYPE_GREEN : PLANET_TYPE_MOON; + + for (i = 0; i < 5; i++) + displayListNames[i] = 0; // empty for now! + + [self setModelName:(procGen || isTextured) ? @"icostextured.dat" : @"icosahedron.dat"]; + + [self rescaleTo:1.0]; + + percent_land = [planetInfo intForKey:@"percent_land" defaultValue:24 + (gen_rnd_number() % 48)]; + //if (isTextured) percent_land = atmo ? 0 :100; // moon/planet override + + // save the current random number generator seed + RNG_Seed saved_seed = currentRandomSeed(); + + for (i = 0; i < vertexCount; i++) + { + if (gen_rnd_number() < 256 * percent_land / 100) + r_seed[i] = 0; // land + else + r_seed[i] = 100; // sea + } + + [planetInfo setObject:[NSNumber numberWithFloat:0.01 * percent_land] forKey:@"land_fraction"]; + + polar_color_factor = [dict doubleForKey:@"polar_color_factor" defaultValue:0.5f]; + + Vector land_hsb, sea_hsb, land_polar_hsb, sea_polar_hsb; + + if (!isTextured) + { + land_hsb.x = gen_rnd_number() / 256.0; land_hsb.y = gen_rnd_number() / 256.0; land_hsb.z = 0.5 + gen_rnd_number() / 512.0; + sea_hsb.x = gen_rnd_number() / 256.0; sea_hsb.y = gen_rnd_number() / 256.0; sea_hsb.z = 0.5 + gen_rnd_number() / 512.0; + while (dot_product(land_hsb,sea_hsb) > .80) // make sure land and sea colors differ significantly + { + sea_hsb.x = gen_rnd_number() / 256.0; sea_hsb.y = gen_rnd_number() / 256.0; sea_hsb.z = 0.5 + gen_rnd_number() / 512.0; + } + } + else + { + land_hsb.x = 0.0; land_hsb.y = 0.0; land_hsb.z = 1.0; // non-saturated fully bright (white) + sea_hsb.x = 0.0; sea_hsb.y = 1.0; sea_hsb.z = 1.0; // fully-saturated fully bright (red) + } + + if (isTextureImage) + { + // override the mainPlanet texture colour... + [self setTextureColorForPlanet:!![dict objectForKey:@"mainForLocalSystem"] inSystem:[dict boolForKey:@"mainForLocalSystem" defaultValue:NO]]; + } + else + { + // possibly get land_hsb and sea_hsb from planetinfo.plist entry + ScanVectorFromString([dict objectForKey:@"land_hsb_color"], &land_hsb); + ScanVectorFromString([dict objectForKey:@"sea_hsb_color"], &sea_hsb); + + // polar areas are brighter but have less color (closer to white) + land_polar_hsb.x = land_hsb.x; land_polar_hsb.y = (land_hsb.y / 4.0); land_polar_hsb.z = 1.0 - (land_hsb.z / 10.0); + sea_polar_hsb.x = sea_hsb.x; sea_polar_hsb.y = (sea_hsb.y / 4.0); sea_polar_hsb.z = 1.0 - (sea_hsb.z / 10.0); + + OOColor* amb_land_color = [OOColor colorWithCalibratedHue:land_hsb.x saturation:land_hsb.y brightness:land_hsb.z alpha:1.0]; + OOColor* amb_sea_color = [OOColor colorWithCalibratedHue:sea_hsb.x saturation:sea_hsb.y brightness:sea_hsb.z alpha:1.0]; + OOColor* amb_polar_land_color = [OOColor colorWithCalibratedHue:land_polar_hsb.x saturation:land_polar_hsb.y brightness:land_polar_hsb.z alpha:1.0]; + OOColor* amb_polar_sea_color = [OOColor colorWithCalibratedHue:sea_polar_hsb.x saturation:sea_polar_hsb.y brightness:sea_polar_hsb.z alpha:1.0]; + + amb_land[0] = [amb_land_color redComponent]; + amb_land[1] = [amb_land_color blueComponent]; + amb_land[2] = [amb_land_color greenComponent]; + amb_land[3] = 1.0; + amb_sea[0] = [amb_sea_color redComponent]; + amb_sea[1] = [amb_sea_color blueComponent]; + amb_sea[2] = [amb_sea_color greenComponent]; + amb_sea[3] = 1.0; + amb_polar_land[0] = [amb_polar_land_color redComponent]; + amb_polar_land[1] = [amb_polar_land_color blueComponent]; + amb_polar_land[2] = [amb_polar_land_color greenComponent]; + amb_polar_land[3] = 1.0; + amb_polar_sea[0] = [amb_polar_sea_color redComponent]; + amb_polar_sea[1] = [amb_polar_sea_color blueComponent]; + amb_polar_sea[2] = [amb_polar_sea_color greenComponent]; + amb_polar_sea[3] = 1.0; + + [planetInfo setObject:amb_land_color forKey:@"land_color"]; + [planetInfo setObject:amb_sea_color forKey:@"sea_color"]; + [planetInfo setObject:amb_polar_land_color forKey:@"polar_land_color"]; + [planetInfo setObject:amb_polar_sea_color forKey:@"polar_sea_color"]; + } + +#if ALLOW_PROCEDURAL_PLANETS + if (procGen) + { + if (!isTextured) + { + fillRanNoiseBuffer(); + textureName = [TextureStore getPlanetTextureNameFor: planetInfo intoData: &textureData]; + isTextured = (textureName != 0); +#ifndef NO_SHADERS + isShadered = NO; +#if OLD_SHADERS + if (UNIVERSE) + { + NSDictionary* shader_info = [[UNIVERSE descriptions] objectForKey:@"planet-surface-shader"]; + if (shader_info) + { + NSLog(@"TESTING: creating planet shader from:\n%@", shader_info); + shader_program = [TextureStore shaderProgramFromDictionary:shader_info]; + isShadered = (shader_program != NULL_SHADER); + normalMapTextureName = [TextureStore getPlanetNormalMapNameFor: planetInfo intoData: &normalMapTextureData]; + NSLog(@"TESTING: planet-surface-shader: %d normalMapTextureName: %d", (int)shader_program, (int)normalMapTextureName); + } + } +#endif +#endif + } + } + else +#endif + { +#ifndef NO_SHADERS + isShadered = NO; + shader_program = NULL_SHADER; +#endif + } + + + [self initialiseBaseVertexArray]; + + [self initialiseBaseTerrainArray:percent_land]; + + for (i = 0; i < next_free_vertex; i++) + [self paintVertex:i :planet_seed]; + + [self scaleVertices]; + // set speed of rotation + if ([dict objectForKey:@"rotational_velocity"]) + { + rotational_velocity = [dict floatForKey:@"rotational_velocity" defaultValue:0.01f * randf()]; // 0.0 .. 0.01 avr 0.005 + } + else + { + rotational_velocity = [planetInfo floatForKey:@"rotation_speed" defaultValue:0.005 * randf()]; // 0.0 .. 0.005 avr 0.0025 + rotational_velocity *= [planetInfo floatForKey:@"rotation_speed_factor" defaultValue:1.0f]; + } + + // do atmosphere + atmosphere = atmo ? [[PlanetEntity alloc] initAsAtmosphereForPlanet:self dictionary:dict] : nil; + + setRandomSeed(saved_seed); + RANROTSetFullSeed(ranrotSavedSeed); + + // set energy + energy = collision_radius * 1000.0; + + isPlanet = YES; + + root_planet = self; + + rotationAxis = kBasisYVector; + + return self; +} + + +- (id) initMoonFromDictionary:(NSDictionary*) dict +{ + return [self initPlanetFromDictionary:dict withAtmosphere:NO andSeed:[UNIVERSE systemSeed]]; +} + + +- (void) dealloc +{ + [atmosphere release]; + if (textureData) + { + free(textureData); + textureData = NULL; + } + if (normalMapTextureData) + { + free(normalMapTextureData); + normalMapTextureData = NULL; + } + [super dealloc]; +} + + +- (NSString*) descriptionComponents +{ + NSString *typeString; + switch (planet_type) + { + case PLANET_TYPE_MINIATURE: + typeString = @"PLANET_TYPE_MINIATURE"; break; + case PLANET_TYPE_SUN: + typeString = @"PLANET_TYPE_SUN"; break; + case PLANET_TYPE_GREEN: + typeString = @"PLANET_TYPE_GREEN"; break; + case PLANET_TYPE_ATMOSPHERE: + typeString = @"PLANET_TYPE_ATMOSPHERE"; break; + case PLANET_TYPE_MOON: + typeString = @"PLANET_TYPE_MOON"; break; + default : + typeString = @"UNKNOWN"; + } + return [NSString stringWithFormat:@"ID: %u position: %@ type: %@ radius: %.3fkm", [self universalID], VectorDescription([self position]), typeString, 0.001 * [self radius]]; +} + + +- (BOOL) canCollide +{ + switch (planet_type) + { + case PLANET_TYPE_MINIATURE: + case PLANET_TYPE_ATMOSPHERE: + return NO; + break; + case PLANET_TYPE_MOON: + case PLANET_TYPE_GREEN: + case PLANET_TYPE_SUN: + return YES; + break; + } + return YES; +} + + +- (BOOL) checkCloseCollisionWith:(Entity *)other +{ +#ifndef NDEBUG + if (gDebugFlags & DEBUG_COLLISIONS) + OOLog(@"planet.collide", @"PLANET Collision!"); +#endif + + if (!other) + return NO; + if (other->isShip) + { + ShipEntity *ship = (ShipEntity *)other; + if ([ship isShuttle]) + { + [ship landOnPlanet:self]; + return NO; + } +#ifndef NDEBUG + if ([ship reportAIMessages]) + { + Vector p1 = ship->position; + OOLog(@"planet.collide.shipHit", @"DEBUG: %@ %d collided with planet at (%.1f,%.1f,%.1f)",[ship name], [ship universalID], p1.x,p1.y,p1.z); + } +#endif + } + + return YES; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + [super update:delta_t]; + sqrt_zero_distance = sqrt(zero_distance); + + switch (planet_type) + { + case PLANET_TYPE_MOON: + case PLANET_TYPE_GREEN: + { + double ugt = [UNIVERSE getTime]; + + if ((shuttles_on_ground > 0)&&(ugt > last_launch_time + shuttle_launch_interval)) + { + [self launchShuttle]; + shuttles_on_ground--; + last_launch_time = ugt; + } + } + + case PLANET_TYPE_MINIATURE: + // normal planetary rotation + //quaternion_rotate_about_y(&orientation, rotational_velocity * delta_t); + quaternion_rotate_about_axis(&orientation, rotationAxis, rotational_velocity * delta_t); + [self orientationChanged]; + + if (atmosphere) + { + [atmosphere update:delta_t]; + double alt = sqrt_zero_distance - collision_radius; + double atmo = 10.0 * (atmosphere->collision_radius - collision_radius); // effect starts at 10x the height of the clouds + + if ((alt > 0)&&(alt <= atmo)) + { + double aleph = (atmo - alt) / atmo; + if (aleph < 0.0) aleph = 0.0; + if (aleph > 1.0) aleph = 1.0; + + [UNIVERSE setSkyColorRed:0.8 * aleph * aleph + green:0.8 * aleph * aleph + blue:0.9 * aleph + alpha:aleph]; + } + } + break; + + case PLANET_TYPE_ATMOSPHERE: + { + // atmospheric rotation + quaternion_rotate_about_y(&orientation, rotational_velocity * delta_t); + [self orientationChanged]; + } + break; + + case PLANET_TYPE_SUN: + { + PlayerEntity *player = [PlayerEntity sharedPlayer]; + assert(player != nil); + rotMatrix = OOMatrixForBillboard(position, [player position]); + + if (throw_sparks&&(planet_type == PLANET_TYPE_SUN)&&(velocity.z > 0)) // going NOVA! + { + if (velocity.x >= 0.0) // countdown + { + velocity.x -= delta_t; + if (corona_speed_factor < 5.0) + corona_speed_factor += 0.75 * delta_t; + } + else + { + if (velocity.y <= 60.0) // expand for a minute + { + double sky_bri = 1.0 - 1.5 * velocity.y; + if (sky_bri < 0) + { + [UNIVERSE setSkyColorRed:0.0f // back to black + green:0.0f + blue:0.0f + alpha:0.0f]; + } + else + { + [UNIVERSE setSkyColorRed:sky_bri // whiteout + green:sky_bri + blue:sky_bri + alpha:1.0f]; + } + if (sky_bri == 1.0) + OOLog(@"sun.nova.start", @"DEBUG: NOVA original radius %.1f", collision_radius); + amb_land[0] = 1.0; amb_land[1] = 1.0; amb_land[2] = 1.0; amb_land[3] = 1.0; + velocity.y += delta_t; + [self setRadius: collision_radius + delta_t * velocity.z]; + } + else + { + OOLog(@"sun.nova.end", @"DEBUG: NOVA final radius %.1f", collision_radius); + // reset at the new size + velocity = kZeroVector; + throw_sparks = YES; // keep throw_sparks at YES to indicate the higher temperature + } + } + } + + // update corona + if (![UNIVERSE reducedDetail]) + { + corona_stage += corona_speed_factor * delta_t; + if (corona_stage > 1.0) + { + int i; + corona_stage -= 1.0; + for (i = 0; i < 360; i++) + { + rvalue[i] = rvalue[360 + i]; + rvalue[360 + i] = randf(); + } + } + } + + } + break; + } +} + + +- (void) setPosition:(Vector)posn +{ + position = posn; + if (atmosphere) + [atmosphere setPosition:posn]; +} + + +- (void) setPosition:(GLfloat) x:(GLfloat) y:(GLfloat) z +{ + position.x = x; + position.y = y; + position.z = z; + if (atmosphere) + [atmosphere setPosition:position]; +} + + +- (void) setOrientation:(Quaternion)inOrientation +{ + rotationAxis = quaternion_rotate_vector(inOrientation, kBasisYVector); + [super setOrientation:inOrientation]; +} + + +- (void) setModelName:(NSString *)modelName +{ + double old_collision_radius = collision_radius; + [super setModelName:modelName]; + collision_radius = old_collision_radius; // preserve the radius +} + + + +// TODO: some translucent stuff is drawn in the opaque pass, which is Naughty. +- (void) drawEntity:(BOOL) immediate :(BOOL) translucent; +{ + int subdivideLevel = 2; // 4 is probably the maximum! + + double drawFactor = [[UNIVERSE gameView] viewSize].width / 100.0; + double drawRatio2 = drawFactor * collision_radius / sqrt_zero_distance; // equivalent to size on screen in pixels + + if ([UNIVERSE breakPatternHide]) return; // DON'T DRAW + + if (zero_distance > 0.0) + { + subdivideLevel = 2 + floor(drawRatio2); + if (subdivideLevel > 4) + subdivideLevel = 4; + } + + if (planet_type == PLANET_TYPE_MINIATURE) + subdivideLevel = [UNIVERSE reducedDetail]? 3 : 4 ; // max detail or less + + lastSubdivideLevel = subdivideLevel; // record + + glFrontFace(GL_CW); // face culling - front faces are AntiClockwise! + + /* + + The depth test gets disabled in parts of this and instead + we rely on the painters algorithm instead. + + The depth buffer isn't granular enough to cope with huge objects at vast + distances. + + */ + + BOOL ignoreDepthBuffer = (planet_type == PLANET_TYPE_ATMOSPHERE); + + if (zero_distance > collision_radius * collision_radius * 25) // is 'far away' + ignoreDepthBuffer |= YES; + + switch (planet_type) + { + case PLANET_TYPE_ATMOSPHERE: + if (root_planet) + { + subdivideLevel = root_planet->lastSubdivideLevel; // copy it from the planet (stops jerky LOD and such) + } + GLMultOOMatrix(rotMatrix); // rotate the clouds! + case PLANET_TYPE_MOON: + case PLANET_TYPE_GREEN: + case PLANET_TYPE_MINIATURE: + //if ((gDebugFlags & DEBUG_WIREFRAME_GRAPHICS) + if ([UNIVERSE wireframeGraphics]) + { + // Drop the detail level a bit, it still looks OK in wireframe and does not penalize + // the system that much. + subdivideLevel = 2; + GLDebugWireframeModeOn(); + } + + if (!translucent) + { + GLfloat mat1[] = { 1.0, 1.0, 1.0, 1.0 }; // opaque white + + if (!isTextured) + glDisable(GL_TEXTURE_2D); // stop any problems from this being left on! + else + { + glEnable(GL_TEXTURE_2D); + glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, mat1); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //wrap around horizontally + } + + glShadeModel(GL_SMOOTH); + + // far enough away to draw flat ? + if (ignoreDepthBuffer) + glDisable(GL_DEPTH_TEST); + + glColor4fv(mat1); + glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat1); + + glFrontFace(GL_CCW); + if (displayListNames[subdivideLevel] != 0) + { + + glDisableClientState(GL_INDEX_ARRAY); + glDisableClientState(GL_EDGE_FLAG_ARRAY); + + if (isTextured) + { + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(4, GL_FLOAT, 0, vertexdata.color_array); + + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(2, GL_FLOAT, 0, vertexdata.uv_array); + glBindTexture(GL_TEXTURE_2D, textureName); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //wrap around horizontally + } + else + { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(4, GL_FLOAT, 0, vertexdata.color_array); + } + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, 0, vertexdata.vertex_array); + glEnableClientState(GL_NORMAL_ARRAY); + glNormalPointer(GL_FLOAT, 0, vertexdata.normal_array); + + glCallList(displayListNames[subdivideLevel]); + + } + else + { + glDisableClientState(GL_INDEX_ARRAY); + glDisableClientState(GL_EDGE_FLAG_ARRAY); + +#ifndef NO_SHADERS + if (isShadered) + { + GLint locator; + glUseProgramObjectARB(shader_program); // shader ON! + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(4, GL_FLOAT, 0, vertexdata.color_array); + + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(2, GL_FLOAT, 0, vertexdata.uv_array); + + glActiveTextureARB(GL_TEXTURE1_ARB); + glBindTexture(GL_TEXTURE_2D, normalMapTextureName); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //wrap around horizontally + locator = glGetUniformLocationARB(shader_program, "tex1"); + if (locator == -1) + OOLogERR(@"planet.shaders.noUniform", @"GLSL couldn't find location of tex0 in shader_program %d", shader_program); + else + glUniform1iARB(locator, 1); // associate texture unit number i with texture 0 + + glActiveTextureARB(GL_TEXTURE0_ARB); + glBindTexture(GL_TEXTURE_2D, textureName); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //wrap around horizontally + locator = glGetUniformLocationARB(shader_program, "tex0"); + if (locator == -1) + OOLogERR(@"planet.shaders.noUniform", @"GLSL couldn't find location of tex0 in shader_program %d", shader_program); + else + glUniform1iARB(locator, 0); // associate texture unit number i with texture 0 + } + else +#endif + { + if (isTextured) + { + glEnableClientState(GL_COLOR_ARRAY); // test shading + glColorPointer(4, GL_FLOAT, 0, vertexdata.color_array); + + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glTexCoordPointer(2, GL_FLOAT, 0, vertexdata.uv_array); + glBindTexture(GL_TEXTURE_2D, textureName); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); //wrap around horizontally + } + else + { + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer(4, GL_FLOAT, 0, vertexdata.color_array); + } + } + + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer(3, GL_FLOAT, 0, vertexdata.vertex_array); + glEnableClientState(GL_NORMAL_ARRAY); + glNormalPointer(GL_FLOAT, 0, vertexdata.normal_array); + +#ifndef NO_SHADERS + if (isShadered) + { + glColor4fv(mat1); + glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat1); + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); + glEnable(GL_COLOR_MATERIAL); + + [self drawModelWithVertexArraysAndSubdivision:subdivideLevel]; + + glDisable(GL_COLOR_MATERIAL); + glUseProgramObjectARB(NULL_SHADER); // shader OFF + } + else +#endif + { + displayListNames[subdivideLevel] = glGenLists(1); + if (displayListNames[subdivideLevel] != 0) // sanity check + { + glNewList(displayListNames[subdivideLevel], GL_COMPILE); + + glColor4fv(mat1); + glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat1); + glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); + glEnable(GL_COLOR_MATERIAL); + + [self drawModelWithVertexArraysAndSubdivision:subdivideLevel]; + + glDisable(GL_COLOR_MATERIAL); + + glEndList(); + } + } + + } + glFrontFace(GL_CW); + + glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, mat1); + + + if (atmosphere) + { + glDisable(GL_DEPTH_TEST); + + glPopMatrix(); // get old draw matrix back + glPushMatrix(); // and store it again + glTranslatef(position.x,position.y,position.z); // centre on the planet + // rotate + GLMultOOMatrix([atmosphere rotationMatrix]); + // draw atmosphere entity + [atmosphere drawEntity:immediate :translucent]; + + glEnable(GL_DEPTH_TEST); + } + else if (ignoreDepthBuffer) + glEnable(GL_DEPTH_TEST); + + } + + //if ((gDebugFlags & DEBUG_WIREFRAME_GRAPHICS) + if ([UNIVERSE wireframeGraphics]) + GLDebugWireframeModeOff(); + break; + + case PLANET_TYPE_SUN: + if (!translucent) + { + int steps = 2 * (MAX_SUBDIVIDE - subdivideLevel); + + // far enough away to draw flat ? + if (ignoreDepthBuffer) + glDisable(GL_DEPTH_TEST); + + glDisable(GL_TEXTURE_2D); + glDisable(GL_LIGHTING); + glColor4fv(amb_land); + + glBegin(GL_TRIANGLE_FAN); + GLDrawBallBillboard(collision_radius, steps, sqrt_zero_distance); + glEnd(); + + if (![UNIVERSE reducedDetail]) + { + glDisable(GL_DEPTH_TEST); + if (zero_distance < lim4k) + { + GLfloat col1[4] = { amb_polar_land[0], amb_polar_land[1], amb_polar_land[2], 0.75}; + drawActiveCorona(collision_radius, collision_radius + cor4k, steps, sqrt_zero_distance, col1, 6); + } + if (zero_distance < lim8k) + { + GLfloat col1[4] = { amb_sea[0], amb_sea[1], amb_sea[2], 0.625}; + drawActiveCorona(collision_radius, collision_radius + cor8k, steps, sqrt_zero_distance, col1, 3); + } + if (zero_distance < lim16k) + { + GLfloat col1[4] = { amb_polar_sea[0], amb_polar_sea[1], amb_polar_sea[2], 0.5}; + drawActiveCorona(collision_radius, collision_radius + cor16k, steps, sqrt_zero_distance, col1, 0); + } + glEnable(GL_DEPTH_TEST); + } + glEnable(GL_LIGHTING); + + // far enough away to draw flat ? + if (ignoreDepthBuffer) + glEnable(GL_DEPTH_TEST); + + } + break; + } + glFrontFace(GL_CCW); // face culling - front faces are AntiClockwise! + CheckOpenGLErrors(@"PlanetEntity after drawing %@", self); +} + + +void drawActiveCorona(GLfloat inner_radius, GLfloat outer_radius, GLfloat step, GLfloat z_distance, GLfloat *col4v1, int rv) +{ + if (EXPECT_NOT(inner_radius >= z_distance)) return; // inside the sphere + + NSRange activity = { 0.34, 1.0 }; + + GLfloat si, ci; + GLfloat s0, c0, s1, c1; + + GLfloat r = inner_radius; + GLfloat c = outer_radius; + GLfloat z = z_distance; + GLfloat x = sqrt(z * z - r * r); + + GLfloat r1 = r * x / z; + GLfloat z1 = r * r / z; + + GLfloat r0 = c * x / z; + GLfloat z0 = c * r / z; + + GLfloat rv0, rv1, rv2; + GLfloat pt0, pt1; + + unsigned short i; + GLfloat theta = 0.0f, delta; + + delta = step * M_PI / 180.0f; // Convert step from degrees to radians + pt0=(1.0 - corona_stage) * corona_blending; + pt1=corona_stage * corona_blending; + + glShadeModel(GL_SMOOTH); + glBegin(GL_TRIANGLE_STRIP); + for (i = 0; i < 360; i += step) + { + si = sinf(theta); + ci = cosf(theta); + theta += delta; + + rv0 = pt0 * rvalue[i + rv] + pt1 * rvalue[i + rv + 360]; + rv1 = pt0 * rvalue[i + rv + 1] + pt1 * rvalue[i + rv + 361]; + rv2 = pt0 * rvalue[i + rv + 2] + pt1 * rvalue[i + rv + 362]; + + s1 = r1 * si; + c1 = r1 * ci; + glColor4f(col4v1[0] * (activity.location + rv0*activity.length), col4v1[1] * (activity.location + rv1*activity.length), col4v1[2] * (activity.location + rv2*activity.length), col4v1[3]); + glVertex3f(s1, c1, -z1); + + s0 = r0 * si; + c0 = r0 * ci; + glColor4f(col4v1[0], col4v1[1], col4v1[2], 0); + glVertex3f(s0, c0, -z0); + } + + rv0 = pt0 * rvalue[rv] + pt1 * rvalue[360 + rv]; + rv1 = pt0 * rvalue[1 + rv] + pt1 * rvalue[361 + rv]; + rv2 = pt0 * rvalue[2 + rv] + pt1 * rvalue[362 + rv]; + + glColor4f(col4v1[0] * (activity.location + rv0*activity.length), col4v1[1] * (activity.location + rv1*activity.length), col4v1[2] * (activity.location + rv2*activity.length), col4v1[3]); + glVertex3f(0.0, r1, -z1); //repeat the zero value to close + glColor4f(col4v1[0], col4v1[1], col4v1[2], 0); + glVertex3f(0.0, r0, -z0); //repeat the zero value to close + glEnd(); +} + + +- (int*) r_seed +{ + return r_seed; +} + + +- (int) planet_seed +{ + return planet_seed; +} + + +- (BOOL) isTextured +{ + return isTextured; +} + + +- (GLuint) textureName +{ + return textureName; +} + + +- (NSString *) textureFileName +{ + return textureFile; +} + + +- (BOOL) changeSunProperty:(NSString *)key withDictionary:(NSDictionary*) dict +{ + id object = [dict objectForKey:key]; + + if ([key isEqualToString:@"sun_radius"]) + { + // clamp corona_flare in case planetinfo.plist / savegame contains the wrong value + [self setRadius: [object doubleValue] + (0.66*MAX_CORONAFLARE * OOClamp_0_1_f([dict floatForKey:@"corona_flare" defaultValue:0.0f]))]; + collision_radius = [object doubleValue]; + } + else if ([key isEqualToString:@"corona_flare"]) + { + double rad=collision_radius; + [self setRadius: rad + (0.66*MAX_CORONAFLARE * OOClamp_0_1_f([object floatValue]))]; + collision_radius = rad; + } + else if ([key isEqualToString:@"corona_shimmer"]) + { + corona_speed_factor=OOClamp_0_1_f([object floatValue]) * 2.0 + randf() * randf(); + } + else if ([key isEqualToString:@"corona_hues"]) + { + corona_blending=OOClamp_0_1_f([object floatValue]); + } + else + { + OOLogWARN(@"script.warning", @"Change to property '%@' not applied, will apply only on leaving and re-entering this system.",key); + return NO; + } + return YES; +} + + +- (void) setTextureColorForPlanet:(BOOL)isMain inSystem:(BOOL)isLocal +{ + Vector land_hsb, land_polar_hsb; + land_hsb.x = 0.0; land_hsb.y = 0.0; land_hsb.z = 1.0; // white + + // the colour override should only apply to main planets + if (isMain) + { + if (isLocal) + ScanVectorFromString([[UNIVERSE currentSystemData] objectForKey:@"texture_hsb_color"], &land_hsb); + else + ScanVectorFromString([[UNIVERSE generateSystemData:[[PlayerEntity sharedPlayer] target_system_seed]] objectForKey:@"texture_hsb_color"], &land_hsb); + } + + land_polar_hsb.x = land_hsb.x; land_polar_hsb.y = (land_hsb.y / 5.0); land_polar_hsb.z = 1.0 - (land_hsb.z / 10.0); + + amb_sea[0] = amb_land[0] = [[OOColor colorWithCalibratedHue:land_hsb.x saturation:land_hsb.y brightness:land_hsb.z alpha:1.0] redComponent]; + amb_sea[1] = amb_land[1] = [[OOColor colorWithCalibratedHue:land_hsb.x saturation:land_hsb.y brightness:land_hsb.z alpha:1.0] blueComponent]; + amb_sea[2] = amb_land[2] = [[OOColor colorWithCalibratedHue:land_hsb.x saturation:land_hsb.y brightness:land_hsb.z alpha:1.0] greenComponent]; + amb_sea[3] = amb_land[3] = 1.0; + amb_polar_sea[0] =amb_polar_land[0] = [[OOColor colorWithCalibratedHue:land_polar_hsb.x saturation:land_polar_hsb.y brightness:land_polar_hsb.z alpha:1.0] redComponent]; + amb_polar_sea[1] =amb_polar_land[1] = [[OOColor colorWithCalibratedHue:land_polar_hsb.x saturation:land_polar_hsb.y brightness:land_polar_hsb.z alpha:1.0] blueComponent]; + amb_polar_sea[2] = amb_polar_land[2] = [[OOColor colorWithCalibratedHue:land_polar_hsb.x saturation:land_polar_hsb.y brightness:land_polar_hsb.z alpha:1.0] greenComponent]; + amb_polar_sea[3] =amb_polar_land[3] = 1.0; +} + + +- (BOOL) setUpPlanetFromTexture:(NSString *)fileName +{ + GLuint tName=[TextureStore getTextureNameFor:fileName]; + if (tName == 0) return NO; + int i; + BOOL wasTextured=isTextured; + //if(!!textureFile) [textureFile release]; + textureFile=[[NSString stringWithString:fileName] retain]; + textureName = tName; + isTextureImage = isTextured = YES; + +#if ALLOW_PROCEDURAL_PLANETS + // We always need to reset the model in order to repaint it - otherwise if someone + // has specified colour overrides in an OXP, those overrides affect the texture! + // What if someone -wants- to re-colour a planetary texture using an OXP? + // -- Micha 20090419 + //if (![UNIVERSE doProcedurallyTexturedPlanets]) +#endif + { + [self setModelName:@"icostextured.dat" ]; + [self rescaleTo:1.0]; + for (i = 0; i < vertexCount; i++) r_seed[i] = 0; // land + // recolour main planet according to "texture_hsb_color" + // this function is only called for local systems! + [self setTextureColorForPlanet:([UNIVERSE planet] == self) inSystem:YES]; + + [self initialiseBaseVertexArray]; + [self initialiseBaseTerrainArray:100]; + for (i = 0; i < next_free_vertex; i++) + [self paintVertex:i :planet_seed]; + } + + if(wasTextured) + { + if (textureData) + { + free(textureData); + textureData = NULL; + } + if (normalMapTextureData) + { + free(normalMapTextureData); + normalMapTextureData = NULL; + } + } + [self scaleVertices]; + + NSMutableDictionary *atmo_dictionary = [NSMutableDictionary dictionary]; + [atmo_dictionary setObject:[NSNumber numberWithInt:0] forKey:@"percent_cloud"]; + [atmosphere autorelease]; + atmosphere = [[PlanetEntity alloc] initAsAtmosphereForPlanet:self dictionary:atmo_dictionary]; + + rotationAxis = kBasisYVector; + + return isTextured; +} + +- (double) polar_color_factor +{ + return polar_color_factor; +} + + +- (GLfloat *) amb_land +{ + return amb_land; +} + + +- (GLfloat *) amb_polar_land +{ + return amb_polar_land; +} + + +- (GLfloat *) amb_sea +{ + return amb_sea; +} + + +- (GLfloat *) amb_polar_sea +{ + return amb_polar_sea; +} + + +- (OOPlanetType) planetType +{ + return planet_type; +} + + +- (void) setPlanetType:(OOPlanetType) pt +{ + planet_type = pt; +} + + +- (double) radius +{ + return collision_radius; +} + + +- (void) setRadius:(double) rad +{ + collision_radius = rad; + cor4k = rad * 4 / 100; lim4k = cor4k * cor4k * NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR; + cor8k = rad * 8 / 100; lim8k = cor8k * cor8k * NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR; + cor16k = rad * rad * 16/ 10000000; lim16k = cor16k * cor16k* NO_DRAW_DISTANCE_FACTOR*NO_DRAW_DISTANCE_FACTOR; +} + + +- (double) sqrtZeroDistance +{ + return sqrt_zero_distance; +} + + +- (void) rescaleTo:(double) rad +{ + int i; + + for (i = 0; i < vertexCount; i++) + { + vertices[i] = vector_multiply_scalar(vector_normal(vertices[i]), rad); + } +} + + +- (BOOL) hasAtmosphere +{ + return atmosphere != nil; +} + + +- (void) drawModelWithVertexArraysAndSubdivision: (int) subdivide +{ + glDrawElements(GL_TRIANGLES, 3 * n_triangles[subdivide], GL_UNSIGNED_INT, &vertexdata.index_array[triangle_start[subdivide]]); +} + + +- (void) launchShuttle +{ + ShipEntity *shuttle_ship; + Quaternion q1; + Vector launch_pos = position; + double start_distance = collision_radius + 125.0; + + quaternion_set_random(&q1); + + Vector vf = vector_forward_from_quaternion(q1); + + launch_pos.x += start_distance * vf.x; + launch_pos.y += start_distance * vf.y; + launch_pos.z += start_distance * vf.z; + + shuttle_ship = [UNIVERSE newShipWithRole:@"shuttle"]; // retain count = 1 + if (shuttle_ship) + { + if (![shuttle_ship crew]) + [shuttle_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"trader" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [shuttle_ship setPosition:launch_pos]; + [shuttle_ship setOrientation:q1]; + + [shuttle_ship setScanClass: CLASS_NEUTRAL]; + + [shuttle_ship setCargoFlag:CARGO_FLAG_FULL_PLENTIFUL]; + + [shuttle_ship setStatus:STATUS_IN_FLIGHT]; + + [UNIVERSE addEntity:shuttle_ship]; + [[shuttle_ship getAI] setStateMachine:@"risingShuttleAI.plist"]; // must happen after adding to the universe! + + [shuttle_ship release]; + } +} + + +- (void) welcomeShuttle:(ShipEntity *) shuttle +{ + shuttles_on_ground++; +} + + ++ (void) resetBaseVertexArray +{ + if (edge_to_vertex) + [edge_to_vertex release]; + edge_to_vertex = nil; +} + + +static BOOL last_one_was_textured; + +- (void) initialiseBaseVertexArray +{ + int i; + NSAutoreleasePool* mypool = [[NSAutoreleasePool alloc] init]; // use our own pool since this routine is quite hard on memory + + if (last_one_was_textured != isTextured) + { + [PlanetEntity resetBaseVertexArray]; + last_one_was_textured = isTextured; + } + + if (edge_to_vertex == nil) + { + edge_to_vertex = [[NSMutableDictionary dictionaryWithCapacity:7680] retain]; // make a new one + + int vi,fi; + next_free_vertex = 0; + + // set first 12 or 14 vertices + + for (vi = 0; vi < vertexCount; vi++) + base_vertex_array[next_free_vertex++] = vertices[vi]; + + // set first 20 triangles + + triangle_start[0] = 0; + n_triangles[0] = faceCount; + for (fi = 0; fi < faceCount; fi++) + { + vertex_index_array[fi * 3 + 0] = faces[fi].vertex[0]; + vertex_index_array[fi * 3 + 1] = faces[fi].vertex[1]; + vertex_index_array[fi * 3 + 2] = faces[fi].vertex[2]; + if (isTextured) + { + texture_uv_array[faces[fi].vertex[0] * 2] = faces[fi].s[0]; + texture_uv_array[faces[fi].vertex[0] * 2 + 1] = faces[fi].t[0]; + texture_uv_array[faces[fi].vertex[1] * 2] = faces[fi].s[1]; + texture_uv_array[faces[fi].vertex[1] * 2 + 1] = faces[fi].t[1]; + texture_uv_array[faces[fi].vertex[2] * 2] = faces[fi].s[2]; + texture_uv_array[faces[fi].vertex[2] * 2 + 1] = faces[fi].t[2]; + } + } + + // for the next levels of subdivision simply build up from the level below!... + + int sublevel; + for (sublevel = 0; sublevel < MAX_SUBDIVIDE - 1; sublevel++) + { + int newlevel = sublevel + 1; + triangle_start[newlevel] = triangle_start[sublevel] + n_triangles[sublevel] * 3; + n_triangles[newlevel] = n_triangles[sublevel] * 4; + + int tri; + for (tri = 0; tri < n_triangles[sublevel]; tri++) + { + // get the six vertices for this group of four triangles + int v0 = vertex_index_array[triangle_start[sublevel] + tri * 3 + 0]; + int v1 = vertex_index_array[triangle_start[sublevel] + tri * 3 + 1]; + int v2 = vertex_index_array[triangle_start[sublevel] + tri * 3 + 2]; + int v01 = baseVertexIndexForEdge(v0, v1, isTextured); // sets it up if required + int v12 = baseVertexIndexForEdge(v1, v2, isTextured); // .. + int v20 = baseVertexIndexForEdge(v2, v0, isTextured); // .. + // v0 v01 v20 + vertex_index_array[triangle_start[newlevel] + tri * 12 + 0] = v0; + vertex_index_array[triangle_start[newlevel] + tri * 12 + 1] = v01; + vertex_index_array[triangle_start[newlevel] + tri * 12 + 2] = v20; + // v01 v1 v12 + vertex_index_array[triangle_start[newlevel] + tri * 12 + 3] = v01; + vertex_index_array[triangle_start[newlevel] + tri * 12 + 4] = v1; + vertex_index_array[triangle_start[newlevel] + tri * 12 + 5] = v12; + // v20 v12 v2 + vertex_index_array[triangle_start[newlevel] + tri * 12 + 6] = v20; + vertex_index_array[triangle_start[newlevel] + tri * 12 + 7] = v12; + vertex_index_array[triangle_start[newlevel] + tri * 12 + 8] = v2; + // v01 v12 v20 + vertex_index_array[triangle_start[newlevel] + tri * 12 + 9] = v01; + vertex_index_array[triangle_start[newlevel] + tri * 12 +10] = v12; + vertex_index_array[triangle_start[newlevel] + tri * 12 +11] = v20; + + } + } + } + + // all done - copy the indices to the instance + for (i = 0; i < MAX_TRI_INDICES; i++) + vertexdata.index_array[i] = vertex_index_array[i]; + + [mypool release]; +} + + +int baseVertexIndexForEdge(int va, int vb, BOOL textured) +{ + NSString* key = [[NSString alloc] initWithFormat:@"%d:%d", (va < vb)? va:vb, (va < vb)? vb:va]; + NSObject* num = [edge_to_vertex objectForKey:key]; + if (num) + { + [key release]; + return [(NSNumber*)num intValue]; + } + else + { + int vindex = next_free_vertex++; + + // calculate position of new vertex + base_vertex_array[vindex] = base_vertex_array[va]; + base_vertex_array[vindex].x += base_vertex_array[vb].x; + base_vertex_array[vindex].y += base_vertex_array[vb].y; + base_vertex_array[vindex].z += base_vertex_array[vb].z; + base_vertex_array[vindex] = vector_normal(base_vertex_array[vindex]); // guaranteed non-zero + + if (textured) + { + //calculate new texture coordinates + + NSPoint uva = NSMakePoint(texture_uv_array[va * 2], texture_uv_array[va * 2 + 1]); + NSPoint uvb = NSMakePoint(texture_uv_array[vb * 2], texture_uv_array[vb * 2 + 1]); + + // if either of these is the polar vertex treat it specially to help with polar distortion: + if ((uva.y == 0.0)||(uva.y == 1.0)) + uva.x = uvb.x; + if ((uvb.y == 0.0)||(uvb.y == 1.0)) + uvb.x = uva.x; + + texture_uv_array[vindex * 2] = 0.5 * (uva.x + uvb.x); + texture_uv_array[vindex * 2 + 1] = 0.5 * (uva.y + uvb.y); + } + + // add new edge to the look-up + [edge_to_vertex setObject:[NSNumber numberWithInt:vindex] forKey:key]; + [key release]; + return vindex; + } +} + + +- (void) initialiseBaseTerrainArray:(int) percent_land +{ + int vi; + // set first 12 or 14 vertices + if (percent_land >= 0) + { + for (vi = 0; vi < vertexCount; vi++) + { + if (gen_rnd_number() < 256 * percent_land / 100) + base_terrain_array[vi] = 0; // land + else + base_terrain_array[vi] = 100; // sea + + } + } + + // for the next levels of subdivision simply build up from the level below!... + + int sublevel; + for (sublevel = 0; sublevel < MAX_SUBDIVIDE - 1; sublevel++) + { + int tri; + for (tri = 0; tri < n_triangles[sublevel]; tri++) + { + // get the six vertices for this group of four triangles + int v0 = vertex_index_array[triangle_start[sublevel] + tri * 3 + 0]; + int v1 = vertex_index_array[triangle_start[sublevel] + tri * 3 + 1]; + int v2 = vertex_index_array[triangle_start[sublevel] + tri * 3 + 2]; + int v01 = baseVertexIndexForEdge(v0, v1, isTextured); // sets it up if required + int v12 = baseVertexIndexForEdge(v1, v2, isTextured); // .. + int v20 = baseVertexIndexForEdge(v2, v0, isTextured); // .. + // v01 + if (base_terrain_array[v0] == base_terrain_array[v1]) + base_terrain_array[v01] = base_terrain_array[v0]; + else + { + int s1 = 0xffff0000 * base_vertex_array[v01].x; + int s2 = 0x00ffff00 * base_vertex_array[v01].y; + int s3 = 0x0000ffff * base_vertex_array[v01].z; + ranrot_srand(s1+s2+s3); + base_terrain_array[v01] = (ranrot_rand() & 4) *25; + } + // v12 + if (base_terrain_array[v1] == base_terrain_array[v2]) + base_terrain_array[v12] = base_terrain_array[v1]; + else + { + int s1 = 0xffff0000 * base_vertex_array[v12].x; + int s2 = 0x00ffff00 * base_vertex_array[v12].y; + int s3 = 0x0000ffff * base_vertex_array[v12].z; + ranrot_srand(s1+s2+s3); + base_terrain_array[v12] = (ranrot_rand() & 4) *25; + } + // v20 + if (base_terrain_array[v2] == base_terrain_array[v0]) + base_terrain_array[v20] = base_terrain_array[v2]; + else + { + int s1 = 0xffff0000 * base_vertex_array[v20].x; + int s2 = 0x00ffff00 * base_vertex_array[v20].y; + int s3 = 0x0000ffff * base_vertex_array[v20].z; + ranrot_srand(s1+s2+s3); + base_terrain_array[v20] = (ranrot_rand() & 4) *25; + } + } + } +} + + +- (void) paintVertex:(int) vi :(int) seed +{ + GLfloat paint_land[4] = { 0.2, 0.9, 0.0, 1.0}; + GLfloat paint_sea[4] = { 0.0, 0.2, 0.9, 1.0}; + GLfloat paint_color[4]; + Vector v = base_vertex_array[vi]; + int r = (isTextured)? 0 : base_terrain_array[vi]; // use land color (0) for textured planets + int i; + double pole_blend = v.z * v.z * polar_color_factor; + if (pole_blend < 0.0) pole_blend = 0.0; + if (pole_blend > 1.0) pole_blend = 1.0; + + paint_land[0] = (1.0 - pole_blend)*amb_land[0] + pole_blend*amb_polar_land[0]; + paint_land[1] = (1.0 - pole_blend)*amb_land[1] + pole_blend*amb_polar_land[1]; + paint_land[2] = (1.0 - pole_blend)*amb_land[2] + pole_blend*amb_polar_land[2]; + paint_sea[0] = (1.0 - pole_blend)*amb_sea[0] + pole_blend*amb_polar_sea[0]; + paint_sea[1] = (1.0 - pole_blend)*amb_sea[1] + pole_blend*amb_polar_sea[1]; + paint_sea[2] = (1.0 - pole_blend)*amb_sea[2] + pole_blend*amb_polar_sea[2]; + if (planet_type == PLANET_TYPE_ATMOSPHERE) // do alphas + { + paint_land[3] = (1.0 - pole_blend)*amb_land[3] + pole_blend*amb_polar_land[3]; + paint_sea[3] = (1.0 - pole_blend)*amb_sea[3] + pole_blend*amb_polar_sea[3]; + } + + ranrot_srand(seed+v.x*1000+v.y*100+v.z*10); + + for (i = 0; i < 3; i++) + { + double cv = (ranrot_rand() % 100)*0.01; // 0..1 ***** DON'T CHANGE THIS LINE, '% 100' MAY NOT BE EFFICIENT BUT THE PATTERNING IS GOOD. + paint_land[i] += (cv - 0.5)*0.1; + paint_sea[i] += (cv - 0.5)*0.1; + } + + for (i = 0; i < 4; i++) + { + if (planet_type == PLANET_TYPE_ATMOSPHERE && isTextured) + paint_color[i] = 1.0; + else + paint_color[i] = (r * paint_sea[i])*0.01 + ((100 - r) * paint_land[i])*0.01; + // finally initialise the color array entry + vertexdata.color_array[vi*4 + i] = paint_color[i]; + } +} + + +- (void) scaleVertices +{ + int vi; + for (vi = 0; vi < next_free_vertex; vi++) + { + Vector v = base_vertex_array[vi]; + vertexdata.normal_array[vi] = v; + vertexdata.vertex_array[vi] = make_vector(v.x * collision_radius, v.y * collision_radius, v.z * collision_radius); + + vertexdata.uv_array[vi * 2] = texture_uv_array[vi * 2]; + vertexdata.uv_array[vi * 2 + 1] = texture_uv_array[vi * 2 + 1]; + } +} + + +double longitudeFromVector(Vector v) +{ + double lon = 0.0; + if (v.z != 0.0) + { + if (v.z > 0) + lon = -atan(v.x / v.z); + else + lon = -M_PI - atan(v.x / v.z); + } + else + { + if (v.x > 0) + lon = -0.5 * M_PI; + else + lon = -1.5 * M_PI; + } + while (lon < 0) + lon += 2 * M_PI; + return lon; +} + + +- (BOOL) willGoNova +{ + return throw_sparks; +} + + +- (BOOL) goneNova +{ + return throw_sparks && velocity.x <= 0; +} + + +- (void) setGoingNova:(BOOL) yesno inTime:(double)interval +{ + throw_sparks = yesno; + if (throw_sparks) + { + OOLog(@"script.debug.setSunNovaIn", @"NOVA activated! time until Nova : %.1f s", interval); + velocity.x = fmax(interval, 0.0); + } + + velocity.y = 0; + velocity.z = 10000; +} + +@end diff --git a/src/Core/Entities/PlayerEntity.h b/src/Core/Entities/PlayerEntity.h new file mode 100644 index 00000000..35011c62 --- /dev/null +++ b/src/Core/Entities/PlayerEntity.h @@ -0,0 +1,748 @@ +/* + +PlayerEntity.m + +Entity subclass nominally representing the player's ship, but also +implementing much of the interaction, menu system etc. Breaking it up into +ten or so different classes is a perennial to-do item. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#if WORMHOLE_SCANNER +#import "WormholeEntity.h" +#endif +#import "ShipEntity.h" +#import "OOTypes.h" + +@class GuiDisplayGen, OOTrumble, MyOpenGLView, HeadUpDisplay, ShipEntity; +@class OOSound, OOSoundSource, OOSoundReferencePoint; +@class JoystickHandler, OOTexture, OOCamera; + +#define SCRIPT_TIMER_INTERVAL 10.0 + +#define GUI_ROW_INIT(GUI) /*int n_rows = [(GUI) rows]*/ +#define GUI_FIRST_ROW(GROUP) ((GUI_DEFAULT_ROWS - GUI_ROW_##GROUP##OPTIONS_END_OF_LIST) / 2) +// reposition menu +#define GUI_ROW(GROUP,ITEM) (GUI_FIRST_ROW(GROUP) - 5 + GUI_ROW_##GROUP##OPTIONS_##ITEM) + +enum +{ + GUI_ROW_OPTIONS_QUICKSAVE, + GUI_ROW_OPTIONS_SAVE, + GUI_ROW_OPTIONS_LOAD, + GUI_ROW_OPTIONS_BEGIN_NEW, + GUI_ROW_OPTIONS_SPACER1, + GUI_ROW_OPTIONS_GAMEOPTIONS, + GUI_ROW_OPTIONS_SPACER2, + GUI_ROW_OPTIONS_STRICT, +#if OOLITE_SDL + GUI_ROW_OPTIONS_SPACER3, + GUI_ROW_OPTIONS_QUIT, +#endif + GUI_ROW_OPTIONS_END_OF_LIST, + + STATUS_EQUIPMENT_FIRST_ROW = 10, + STATUS_EQUIPMENT_MAX_ROWS = 8, + + GUI_ROW_EQUIPMENT_START = 3, + GUI_MAX_ROWS_EQUIPMENT = 12, + GUI_ROW_EQUIPMENT_DETAIL = GUI_ROW_EQUIPMENT_START + GUI_MAX_ROWS_EQUIPMENT + 1, + GUI_ROW_EQUIPMENT_CASH = 1, + GUI_ROW_MARKET_KEY = 1, + GUI_ROW_MARKET_START = 2, + GUI_ROW_MARKET_CASH = 20 +}; +#if GUI_FIRST_ROW() < 0 +# error Too many items in OPTIONS list! +#endif + +enum +{ + GUI_ROW_GAMEOPTIONS_AUTOSAVE, + GUI_ROW_GAMEOPTIONS_SPACER1, + GUI_ROW_GAMEOPTIONS_VOLUME, +#if OOLITE_MAC_OS_X + GUI_ROW_GAMEOPTIONS_GROWL, +#endif +#if OOLITE_SPEECH_SYNTH + GUI_ROW_GAMEOPTIONS_SPEECH, + GUI_ROW_GAMEOPTIONS_SPEECH_LANGUAGE, + GUI_ROW_GAMEOPTIONS_SPEECH_GENDER, +#endif + GUI_ROW_GAMEOPTIONS_MUSIC, + GUI_ROW_GAMEOPTIONS_SPACER2, + GUI_ROW_GAMEOPTIONS_DISPLAY, + GUI_ROW_GAMEOPTIONS_DISPLAYSTYLE, + GUI_ROW_GAMEOPTIONS_DETAIL, + GUI_ROW_GAMEOPTIONS_WIREFRAMEGRAPHICS, +#if ALLOW_PROCEDURAL_PLANETS + GUI_ROW_GAMEOPTIONS_PROCEDURALLYTEXTUREDPLANETS, +#endif + GUI_ROW_GAMEOPTIONS_SHADEREFFECTS, +#if OOLITE_SDL + GUI_ROW_GAMEOPTIONS_SPACER_SDLSTICKMAPPER, + GUI_ROW_GAMEOPTIONS_STICKMAPPER, +#endif + GUI_ROW_GAMEOPTIONS_SPACER3, + GUI_ROW_GAMEOPTIONS_BACK, + + GUI_ROW_GAMEOPTIONS_END_OF_LIST +}; +#if GUI_FIRST_ROW() < 0 +# error Too many items in GAMEOPTIONS list! +#endif + + +enum +{ + // Exposed to shaders. + SCOOP_STATUS_NOT_INSTALLED = 0, + SCOOP_STATUS_FULL_HOLD, + SCOOP_STATUS_OKAY, + SCOOP_STATUS_ACTIVE +}; + + +enum +{ + ALERT_FLAG_DOCKED = 0x010, + ALERT_FLAG_MASS_LOCK = 0x020, + ALERT_FLAG_YELLOW_LIMIT = 0x03f, + ALERT_FLAG_TEMP = 0x040, + ALERT_FLAG_ALT = 0x080, + ALERT_FLAG_ENERGY = 0x100, + ALERT_FLAG_HOSTILES = 0x200 +}; +typedef uint16_t OOAlertFlags; + + +typedef enum +{ + // Exposed to shaders. + MISSILE_STATUS_SAFE, + MISSILE_STATUS_ARMED, + MISSILE_STATUS_TARGET_LOCKED +} OOMissileStatus; + + +#define WEAPON_COOLING_FACTOR 6.0f +#define ENERGY_RECHARGE_FACTOR energy_recharge_rate +#define ECM_ENERGY_DRAIN_FACTOR 20.0f +#define ECM_DURATION 2.5f + +#define ROLL_DAMPING_FACTOR 1.0f +#define PITCH_DAMPING_FACTOR 1.0f +#define YAW_DAMPING_FACTOR 1.0f + +#define PLAYER_MAX_WEAPON_TEMP 256.0f +#define PLAYER_MAX_FUEL 70 +#define PLAYER_MAX_MISSILES 4 +#define PLAYER_STARTING_MISSILES 3 +#define PLAYER_DIAL_MAX_ALTITUDE 40000.0 +#define PLAYER_SUPER_ALTITUDE2 10000000000.0 + +#define PLAYER_MAX_TRUMBLES 24 + +#define PLAYER_TARGET_MEMORY_SIZE 16 + + // ~~~~~~~~~~~~~~~~~~~~~~~~ = 40km + +#define SHOT_RELOAD 0.25 + +#define HYPERSPEED_FACTOR 32.0 + +#define PLAYER_SHIP_DESC @"cobra3-player" + +#define ESCAPE_SEQUENCE_TIME 10.0 + +#define MS_WITCHSPACE_SF @"[witch-to-@-in-f-seconds]" +#define MS_GAL_WITCHSPACE_F @"[witch-galactic-in-f-seconds]" + + +#define WEAPON_FACING_NONE 0 +#define WEAPON_FACING_FORWARD 1 +#define WEAPON_FACING_AFT 2 +#define WEAPON_FACING_PORT 4 +#define WEAPON_FACING_STARBOARD 8 + +#define WEAPON_OFFSET_DOWN 20 + +#define FORWARD_FACING_STRING DESC(@"forward-facing-string") +#define AFT_FACING_STRING DESC(@"aft-facing-string") +#define PORT_FACING_STRING DESC(@"port-facing-string") +#define STARBOARD_FACING_STRING DESC(@"starboard-facing-string") + +#define KEY_REPEAT_INTERVAL 0.20 + +#define PLAYER_SHIP_CLOCK_START (2084004 * 86400.0) + +#define CONTRACTS_GOOD_KEY @"contracts_fulfilled" +#define CONTRACTS_BAD_KEY @"contracts_expired" +#define CONTRACTS_UNKNOWN_KEY @"contracts_unknown" +#define PASSAGE_GOOD_KEY @"passage_fulfilled" +#define PASSAGE_BAD_KEY @"passage_expired" +#define PASSAGE_UNKNOWN_KEY @"passage_unknown" + + +typedef enum +{ + // Exposed to shaders. + COMPASS_MODE_BASIC, + COMPASS_MODE_PLANET, + COMPASS_MODE_STATION, + COMPASS_MODE_SUN, + COMPASS_MODE_TARGET, + COMPASS_MODE_BEACONS +} OOCompassMode; + + +#define SCANNER_ZOOM_RATE_UP 2.0 +#define SCANNER_ZOOM_RATE_DOWN -8.0 + +#define PLAYER_INTERNAL_DAMAGE_FACTOR 31 + +#define PLAYER_DOCKING_AI_NAME @"dockingAI.plist" + +@interface PlayerEntity: ShipEntity +{ +@public + + Random_Seed system_seed; + Random_Seed target_system_seed; + +@protected + + NSString *ship_desc; + int ship_trade_in_factor; + + NSDictionary *worldScripts; + NSMutableDictionary *mission_variables; + NSMutableDictionary *localVariables; + int missionTextRow; + NSString *missionChoice; + + NSString *specialCargo; + + NSMutableArray *commLog; + + NSMutableDictionary *oxpKeys; + + OOTexture *missionBackgroundTexture; + + BOOL found_equipment; + + NSMutableDictionary *reputation; + + unsigned max_passengers; + NSMutableArray *passengers; + NSMutableDictionary *passenger_record; + + NSMutableArray *contracts; + NSMutableDictionary *contract_record; + + NSMutableDictionary *shipyard_record; + + NSMutableArray *missionDestinations; + + double script_time; + double script_time_check; + double script_time_interval; + NSString *lastTextKey; + + double ship_clock; + double ship_clock_adjust; + + double fps_check_time; + int fps_counter; + double last_fps_check_time; + + NSString *planetSearchString; + + OOMatrix playerRotMatrix; + + // For OO-GUI based save screen + NSString *commanderNameString; + NSMutableArray *cdrDetailArray; + int currentPage; + BOOL pollControls; +// ...end save screen + + StationEntity *dockedStation; +#if DOCKING_CLEARANCE_ENABLED +/* Used by the DOCKING_CLEARANCE code to implement docking at non-main + * stations. Could possibly overload use of 'dockedStation' instead + * but that needs futher investigation to ensure it doesn't break anything. */ + StationEntity *targetDockStation; +#endif + + HeadUpDisplay *hud; + + GLfloat roll_delta, pitch_delta, yaw_delta; + + GLfloat forward_shield, aft_shield; + GLfloat weapon_temp; + GLfloat forward_weapon_temp, aft_weapon_temp, port_weapon_temp, starboard_weapon_temp; + GLfloat weapon_energy_per_shot, weapon_heat_increment_per_shot, weapon_reload_time; + + int chosen_weapon_facing; // for purchasing weapons + + double ecm_start_time; + + OOSoundReferencePoint *refPoint; + + OOGUIScreenID gui_screen; + OOAlertFlags alertFlags; + OOAlertCondition alertCondition; + OOAlertCondition lastScriptAlertCondition; + OOMissileStatus missile_status; + unsigned activeMissile; + + OOCargoQuantity current_cargo; + + NSPoint cursor_coordinates; + float witchspaceCountdown; + + // player commander data + NSString *player_name; + NSPoint galaxy_coordinates; + + Random_Seed galaxy_seed; + + OOCreditsQuantity credits; + OOGalaxyID galaxy_number; + OOWeaponType forward_weapon; // Is there a reason for having both this and forward_weapon_type? -- ahruman + OOWeaponType aft_weapon; // ditto + OOWeaponType port_weapon; + OOWeaponType starboard_weapon; + + NSMutableArray *shipCommodityData; + + unsigned max_missiles; // no. of missile pylons + ShipEntity *missile_entity[SHIPENTITY_MAX_MISSILES]; // holds the actual missile entities or equivalents + + int legalStatus; + int market_rnd; + unsigned ship_kills; + + OOCompassMode compassMode; + + GLfloat fuel_leak_rate; + + // keys! + OOKeyCode key_roll_left; + OOKeyCode key_roll_right; + OOKeyCode key_pitch_forward; + OOKeyCode key_pitch_back; + OOKeyCode key_yaw_left; + OOKeyCode key_yaw_right; + + OOKeyCode key_increase_speed; + OOKeyCode key_decrease_speed; + OOKeyCode key_inject_fuel; + + OOKeyCode key_fire_lasers; + OOKeyCode key_launch_missile; + OOKeyCode key_next_missile; + OOKeyCode key_ecm; + + OOKeyCode key_target_missile; + OOKeyCode key_untarget_missile; +#if TARGET_INCOMING_MISSILES + OOKeyCode key_target_incoming_missile; +#endif + OOKeyCode key_ident_system; + + OOKeyCode key_scanner_zoom; + OOKeyCode key_scanner_unzoom; + + OOKeyCode key_launch_escapepod; + OOKeyCode key_energy_bomb; + + OOKeyCode key_galactic_hyperspace; + OOKeyCode key_hyperspace; + OOKeyCode key_jumpdrive; + + OOKeyCode key_dump_cargo; + OOKeyCode key_rotate_cargo; + + OOKeyCode key_autopilot; + OOKeyCode key_autopilot_target; + OOKeyCode key_autodock; + + OOKeyCode key_snapshot; + OOKeyCode key_docking_music; + + OOKeyCode key_advanced_nav_array; + OOKeyCode key_map_home; + OOKeyCode key_map_info; + + OOKeyCode key_pausebutton; + OOKeyCode key_show_fps; + OOKeyCode key_mouse_control; + + OOKeyCode key_comms_log; + OOKeyCode key_next_compass_mode; + + OOKeyCode key_cloaking_device; + + OOKeyCode key_contract_info; + + OOKeyCode key_next_target; + OOKeyCode key_previous_target; + + OOKeyCode key_custom_view; + +#if DOCKING_CLEARANCE_ENABLED + OOKeyCode key_docking_clearance_request; +#endif + +#ifndef NDEBUG + OOKeyCode key_dump_target_state; +#endif + + // save-file + NSString *save_path; + + // position of viewports + Vector forwardViewOffset, aftViewOffset, portViewOffset, starboardViewOffset; + + // trumbles + OOUInteger trumbleCount; + OOTrumble *trumble[PLAYER_MAX_TRUMBLES]; + + // smart zoom + GLfloat scanner_zoom_rate; + + // target memory + int target_memory[PLAYER_TARGET_MEMORY_SIZE]; + int target_memory_index; + + // custom view points + Quaternion customViewQuaternion; + OOMatrix customViewMatrix; + Vector customViewOffset, customViewForwardVector, customViewUpVector, customViewRightVector; + NSString *customViewDescription; + + OOViewID currentWeaponFacing; // decoupled from view direction + + // docking reports + NSMutableString *dockingReport; + + // Woo, flags. + unsigned suppressTargetLost: 1, // smart target lst reports + scoopsActive: 1, // smart fuelscoops + + game_over: 1, + finished: 1, + bomb_detonated: 1, + autopilot_engaged: 1, + + afterburner_engaged: 1, + afterburnerSoundLooping: 1, + + hyperspeed_engaged: 1, + travelling_at_hyperspeed: 1, + hyperspeed_locked: 1, + + scripted_misjump: 1, + + ident_engaged: 1, + + galactic_witchjump: 1, + + ecm_in_operation: 1, + + show_info_flag: 1, + + showDemoShips: 1, + + rolling, pitching, yawing: 1, + using_mining_laser: 1, + + mouse_control_on: 1, + + isSpeechOn: 1, + + keyboardRollPitchOverride: 1, + keyboardYawOverride: 1, +waitingForStickCallback: 1; +#if OOLITE_ESPEAK + unsigned int voice_no; + BOOL voice_gender_m; +#endif + + // Note: joystick stuff does nothing under OS X. + // Keeping track of joysticks + int numSticks; + JoystickHandler *stickHandler; + + // For PlayerEntity (StickMapper) + int selFunctionIdx; + NSArray *stickFunctions; + + OOGalacticHyperspaceBehaviour galacticHyperspaceBehaviour; + NSPoint galacticHyperspaceFixedCoords; + +@private + NSArray *_customViews; + OOUInteger _customViewIndex; + +#if DOCKING_CLEARANCE_ENABLED + OODockingClearanceStatus dockingClearanceStatus; +#endif +#if WORMHOLE_SCANNER + NSMutableArray *scannedWormholes; +#endif +} + ++ (PlayerEntity *)sharedPlayer; + +- (BOOL) isDocked; + +- (void)completeInitialSetUp; + +- (void) warnAboutHostiles; + +- (void) unloadCargoPods; +- (void) loadCargoPods; + +- (int) random_factor; +- (Random_Seed) galaxy_seed; +- (NSPoint) galaxy_coordinates; +- (NSPoint) cursor_coordinates; + +- (Random_Seed) system_seed; +- (void) setSystem_seed:(Random_Seed) s_seed; +- (Random_Seed) target_system_seed; + +- (NSDictionary *) commanderDataDictionary; +- (BOOL)setCommanderDataFromDictionary:(NSDictionary *) dict; + +- (void) set_up; +- (void) set_up:(BOOL) andReset; + +- (void) doBookkeeping:(double) delta_t; + +- (BOOL) massLocked; +- (BOOL) atHyperspeed; +- (Vector) velocityVector; + +- (NSString *) ship_desc; + +- (void) setDockedAtMainStation; +- (StationEntity *) dockedStation; + +#if DOCKING_CLEARANCE_ENABLED +- (void) setTargetDockStationTo:(StationEntity *) value; +- (StationEntity *) getTargetDockStation; +#endif + +- (HeadUpDisplay *) hud; + +- (void) setShowDemoShips:(BOOL) value; +- (BOOL) showDemoShips; + +- (GLfloat) forwardShieldLevel; +- (GLfloat) aftShieldLevel; + +- (void) setForwardShieldLevel:(GLfloat)level; +- (void) setAftShieldLevel:(GLfloat)level; + +- (GLfloat) dialRoll; +- (GLfloat) dialPitch; +- (GLfloat) dialYaw; +- (GLfloat) dialSpeed; +- (GLfloat) dialHyperSpeed; + +- (GLfloat) dialForwardShield; +- (GLfloat) dialAftShield; + +- (GLfloat) dialEnergy; +- (GLfloat) dialMaxEnergy; + +- (GLfloat) dialFuel; +- (GLfloat) dialHyperRange; + +- (GLfloat) dialAltitude; + +- (unsigned) countMissiles; +- (OOMissileStatus) dialMissileStatus; + +- (int) dialFuelScoopStatus; + +- (double) clockTime; // Note that this is not an OOTimeAbsolute +- (double) clockTimeAdjusted; // Note that this is not an OOTimeAbsolute +- (BOOL) clockAdjusting; + +- (NSString *) dial_clock; +- (NSString *) dial_clock_adjusted; +- (NSString *) dial_fpsinfo; +- (NSString *) dial_objinfo; + +- (NSMutableArray *) commLog; + +- (OOCompassMode) compassMode; +- (void) setCompassMode:(OOCompassMode)value; +- (void) setNextCompassMode; + +- (unsigned) activeMissile; +- (void) setActiveMissile:(unsigned)value; +- (unsigned) dialMaxMissiles; +- (BOOL) dialIdentEngaged; +- (NSString *) specialCargo; +- (NSString *) dialTargetName; +- (ShipEntity *) missileForStation:(unsigned)value; +- (void) sortMissiles; +- (void) safeAllMissiles; +- (void) selectNextMissile; +- (void) tidyMissilePylons; + +- (void) clearAlertFlags; +- (int) alertFlags; +- (void) setAlertFlag:(int)flag to:(BOOL)value; +- (OOAlertCondition) alertCondition; + +- (BOOL) mountMissile:(ShipEntity *)missile; + +- (OOEnergyUnitType) installedEnergyUnitType; +- (OOEnergyUnitType) energyUnitType; + +- (BOOL) fireEnergyBomb; +- (BOOL) launchMine:(ShipEntity *)mine; + +- (BOOL) fireMainWeapon; +- (OOWeaponType) weaponForView:(OOViewID)view; + +- (void) rotateCargo; + +- (void) enterGalacticWitchspace; + +- (void) interpretAIMessage:(NSString *)ms; + +- (void) takeInternalDamage; + +- (void) loseTargetStatus; + +- (void) docked; + +- (void) setGuiToStatusScreen; +- (NSArray *) equipmentList; // Each entry is an array with a string followed by a boolean indicating availability (NO = damaged). +- (NSArray *) cargoList; +- (void) setGuiToSystemDataScreen; +- (NSArray *) markedDestinations; +- (void) setGuiToLongRangeChartScreen; +- (void) setGuiToShortRangeChartScreen; +- (void) setGuiToLoadSaveScreen; +- (void) setGuiToGameOptionsScreen; +- (void) setGuiToEquipShipScreen:(int)skip selectingFacingFor:(NSString *)eqKeyForSelectFacing; +- (void) setGuiToEquipShipScreen:(int)skip; +- (void) showInformationForSelectedUpgrade; +- (void) calculateCurrentCargo; +- (void) setGuiToMarketScreen; + +- (void) setGuiToIntroFirstGo: (BOOL) justCobra; + +- (void) noteGuiChangeFrom:(OOGUIScreenID)fromScreen to:(OOGUIScreenID)toScreen; + +- (OOGUIScreenID) guiScreen; + +- (void) buySelectedItem; +- (BOOL) marketFlooded:(int) index; +- (BOOL) tryBuyingCommodity:(int) index all:(BOOL) all; +- (BOOL) trySellingCommodity:(int) index all:(BOOL) all; + +- (BOOL) isSpeechOn; + +- (void) addEquipmentFromCollection:(id)equipment; // equipment may be an array, a set, a dictionary whose values are all YES, or a string. + +- (void) getFined; + +- (void) setDefaultViewOffsets; +- (void) setDefaultCustomViews; +- (Vector) weaponViewOffset; + +- (void) setUpTrumbles; +- (void) addTrumble:(OOTrumble*) papaTrumble; +- (void) removeTrumble:(OOTrumble*) deadTrumble; +- (OOTrumble**)trumbleArray; +- (OOUInteger) trumbleCount; +// loading and saving trumbleCount +- (id)trumbleValue; +- (void) setTrumbleValueFrom:(NSObject*) trumbleValue; + +- (void) mungChecksumWithNSString:(NSString *)str; + +- (NSString *)screenModeStringForWidth:(unsigned)inWidth height:(unsigned)inHeight refreshRate:(float)inRate; + +- (void) suppressTargetLost; + +- (void) setScoopsActive; + +- (void) clearTargetMemory; +- (BOOL) moveTargetMemoryBy:(int)delta; + +- (void) printIdentLockedOnForMissile:(BOOL)missile; + +- (void) applyYaw:(GLfloat) yaw; + +/* GILES custom viewpoints */ + +// custom view points +- (Quaternion)customViewQuaternion; +- (OOMatrix)customViewMatrix; +- (Vector)customViewOffset; +- (Vector)customViewForwardVector; +- (Vector)customViewUpVector; +- (Vector)customViewRightVector; +- (NSString *)customViewDescription; +- (void)setCustomViewDataFromDictionary:(NSDictionary*) viewDict; +- (Vector) viewpointPosition; +- (Vector) viewpointOffset; + +- (OOCamera *) currentCamera; + +- (NSArray *) worldScriptNames; +- (NSDictionary *) worldScriptsByName; + +// *** World cript events. +// In general, script events should be sent through doScriptEvent:..., which +// will forward to the world scripts. +- (void) doWorldScriptEvent:(NSString *)message withArguments:(NSArray *)arguments; + +- (BOOL)showInfoFlag; + +- (void) setGalacticHyperspaceBehaviour:(OOGalacticHyperspaceBehaviour) galacticHyperspaceBehaviour; +- (OOGalacticHyperspaceBehaviour) galacticHyperspaceBehaviour; +- (void) setGalacticHyperspaceFixedCoordsX:(unsigned char)x y:(unsigned char)y; +- (NSPoint) galacticHyperspaceFixedCoords; + +- (BOOL) scriptedMisjump; +- (void) setScriptedMisjump:(BOOL)newValue; + +#if DOCKING_CLEARANCE_ENABLED +- (BOOL) clearedToDock; +- (void) setDockingClearanceStatus:(OODockingClearanceStatus) newValue; +- (OODockingClearanceStatus) getDockingClearanceStatus; +- (void) penaltyForUnauthorizedDocking; +#endif + +@end diff --git a/src/Core/Entities/PlayerEntity.m b/src/Core/Entities/PlayerEntity.m new file mode 100644 index 00000000..926a9d41 --- /dev/null +++ b/src/Core/Entities/PlayerEntity.m @@ -0,0 +1,7032 @@ +/* + +PlayerEntity.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "PlayerEntityContracts.h" +#import "PlayerEntityControls.h" +#import "PlayerEntitySound.h" + +#import "StationEntity.h" +#import "ParticleEntity.h" +#import "PlanetEntity.h" +#import "WormholeEntity.h" + +#import "OOMaths.h" +#import "GameController.h" +#import "ResourceManager.h" +#import "Universe.h" +#import "AI.h" +#import "MyOpenGLView.h" +#import "OOTrumble.h" +#import "PlayerEntityLoadSave.h" +#import "OOSound.h" +#import "OOColor.h" +#import "Octree.h" +#import "OOCacheManager.h" +#import "OOStringParsing.h" +#import "OOPListParsing.h" +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" +#import "OOTexture.h" +#import "OORoleSet.h" +#import "HeadUpDisplay.h" +#import "OOOpenGLExtensionManager.h" +#import "OOMusicController.h" +#import "OOEntityFilterPredicate.h" +#import "OOShipRegistry.h" +#import "OOEquipmentType.h" +#import "OOCamera.h" + +#import "OOScript.h" +#import "OOScriptTimer.h" + +#if OOLITE_MAC_OS_X +#import "Groolite.h" +#endif +#if OOLITE_SDL +#import "JoystickHandler.h" +#import "PlayerEntityStickMapper.h" +#endif + +#define kOOLogUnconvertedNSLog @"unclassified.PlayerEntity" + + +// 10m/s forward drift +#define OG_ELITE_FORWARD_DRIFT 10.0f + + +#define PLAYER_DEFAULT_NAME @"Jameson" + + +enum +{ + // If comm log is kCommLogTrimThreshold or more lines long, it will be cut to kCommLogTrimSize. + kCommLogTrimThreshold = 125, + kCommLogTrimSize = 100 +}; + + +static NSString * const kOOLogBuyMountedOK = @"equip.buy.mounted"; +static NSString * const kOOLogBuyMountedFailed = @"equip.buy.mounted.failed"; + + +static PlayerEntity *sSharedPlayer = nil; + + +@interface PlayerEntity (OOPrivate) + +- (void) setExtraEquipmentFromFlags; +-(void) doTradeIn:(OOCreditsQuantity)tradeInValue forPriceFactor:(double)priceFactor; + +// Subs of update: +- (void) updateMovementFlags; +- (void) updateAlertCondition; +- (void) updateFuelScoops:(OOTimeDelta)delta_t; +- (void) updateClocks:(OOTimeDelta)delta_t; +- (void) checkScriptsIfAppropriate; +- (void) updateTrumbles:(OOTimeDelta)delta_t; +- (void) performAutopilotUpdates:(OOTimeDelta)delta_t; +- (void) performInFlightUpdates:(OOTimeDelta)delta_t; +- (void) performWitchspaceCountdownUpdates:(OOTimeDelta)delta_t; +- (void) performWitchspaceExitUpdates:(OOTimeDelta)delta_t; +- (void) performLaunchingUpdates:(OOTimeDelta)delta_t; +- (void) performDockingUpdates:(OOTimeDelta)delta_t; +- (void) performDeadUpdates:(OOTimeDelta)delta_t; +- (void) updateTargetting; +- (BOOL) isValidTarget:(Entity*)target; +#if WORMHOLE_SCANNER +- (void) addScannedWormhole:(WormholeEntity*)wormhole; +- (void) updateWormholes; +#endif + +// Shopping +- (BOOL) tryBuyingItem:(NSString *)eqKey; + +@end + + +@implementation PlayerEntity + ++ (PlayerEntity *)sharedPlayer +{ + return sSharedPlayer; +} + + +- (void) setName:(NSString *)inName +{ + // Block super method; player ship can't be renamed. +} + + +- (void)completeInitialSetUp +{ + dockedStation = [UNIVERSE station]; + [self doWorldScriptEvent:@"startUp" withArguments:nil]; +} + + +- (void) unloadCargoPods +{ + /* loads commodities from the cargo pods onto the ship's manifest */ + unsigned i; + NSMutableArray* localMarket = [dockedStation localMarket]; + NSMutableArray* manifest = [[NSMutableArray arrayWithArray:localMarket] retain]; // retain + + // copy the quantities in ShipCommodityData to the manifest + // (was: zero the quantities in the manifest, making a mutable array of mutable arrays) + + for (i = 0; i < [manifest count]; i++) + { + NSMutableArray* commodityInfo = [NSMutableArray arrayWithArray:(NSArray *)[manifest objectAtIndex:i]]; + NSArray* shipCommInfo = [NSArray arrayWithArray:(NSArray *)[shipCommodityData objectAtIndex:i]]; + int amount = [(NSNumber*)[shipCommInfo objectAtIndex:MARKET_QUANTITY] intValue]; + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:amount]]; + [manifest replaceObjectAtIndex:i withObject:commodityInfo]; + } + + NSEnumerator *cargoEnumerator = nil; + ShipEntity *cargoItem = nil; + + // step through the cargo pods adding in the quantities + for (cargoEnumerator = [cargo objectEnumerator]; (cargoItem = [cargoEnumerator nextObject]); ) + { + NSMutableArray *commodityInfo; + int co_type, co_amount, quantity; + + co_type = [cargoItem commodityType]; + co_amount = [cargoItem commodityAmount]; + + if (co_type == CARGO_UNDEFINED) + { + OOLog(@"player.badCargoPod", @"Cargo pod %@ has bad commodity type (CARGO_UNDEFINED), rejecting.", cargoItem); + continue; + } + commodityInfo = [manifest objectAtIndex:co_type]; + quantity = [[commodityInfo objectAtIndex:MARKET_QUANTITY] intValue] + co_amount; + + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:quantity]]; // enter the adjusted quantity + } + + [shipCommodityData release]; + shipCommodityData = manifest; + + [cargo removeAllObjects]; // empty the hold + + [self calculateCurrentCargo]; // work out the correct value for current_cargo +} + + +- (void) loadCargoPods +{ + /* loads commodities from the ships manifest into individual cargo pods */ + unsigned i,j; + NSMutableArray* localMarket = [dockedStation localMarket]; + NSMutableArray* manifest = [[NSMutableArray arrayWithArray:shipCommodityData] retain]; // retain + + if (cargo == nil) cargo = [[NSMutableArray alloc] init]; + + for (i = 0; i < [manifest count]; i++) + { + NSMutableArray* commodityInfo = [[NSMutableArray arrayWithArray:(NSArray *)[manifest objectAtIndex:i]] retain]; // retain + OOCargoQuantity quantity = [[commodityInfo objectAtIndex:MARKET_QUANTITY] intValue]; + OOMassUnit units = [UNIVERSE unitsForCommodity:i]; + if (quantity > 0) + { + if (units == UNITS_TONS) + { + // put each ton in a separate container + for (j = 0; j < quantity; j++) + { + ShipEntity *container = [UNIVERSE newShipWithRole:@"1t-cargopod"]; + if (container) + { + [container setScanClass: CLASS_CARGO]; + [container setStatus:STATUS_IN_HOLD]; + [container setCommodity:i andAmount:1]; + [cargo addObject:container]; + [container release]; + } + else + { + OOLogERR(@"player.loadCargoPods.noContainer", @"couldn't create a container in [PlayerEntity loadCargoPods]"); + // throw an exception here... + [NSException raise:OOLITE_EXCEPTION_FATAL + format:@"[PlayerEntity loadCargoPods] failed to create a container for cargo with role 'cargopod'"]; + } + } + // zero this commodity + [commodityInfo setArray:(NSArray *)[localMarket objectAtIndex:i]]; + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:0]]; + [manifest replaceObjectAtIndex:i withObject:[NSArray arrayWithArray:commodityInfo]]; + } + } + [commodityInfo release]; // release, done + } + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + [manifest release]; // release, done +} + + +- (int) random_factor +{ + return market_rnd; +} + + +- (Random_Seed) galaxy_seed +{ + return galaxy_seed; +} + + +- (NSPoint) galaxy_coordinates +{ + return galaxy_coordinates; +} + + +- (NSPoint) cursor_coordinates +{ + return cursor_coordinates; +} + + +- (Random_Seed) system_seed +{ + return system_seed; +} + + +- (void) setSystem_seed:(Random_Seed) s_seed +{ + system_seed = s_seed; + galaxy_coordinates = NSMakePoint(s_seed.d, s_seed.b); +} + + +- (Random_Seed) target_system_seed +{ + return target_system_seed; +} + + +- (NSDictionary *) commanderDataDictionary +{ + NSMutableDictionary *result = [NSMutableDictionary dictionary]; + + NSString *gal_seed = [NSString stringWithFormat:@"%d %d %d %d %d %d",galaxy_seed.a, galaxy_seed.b, galaxy_seed.c, galaxy_seed.d, galaxy_seed.e, galaxy_seed.f]; + NSString *gal_coords = [NSString stringWithFormat:@"%d %d",(int)galaxy_coordinates.x,(int)galaxy_coordinates.y]; + NSString *tgt_coords = [NSString stringWithFormat:@"%d %d",(int)cursor_coordinates.x,(int)cursor_coordinates.y]; + + [result setObject:gal_seed forKey:@"galaxy_seed"]; + [result setObject:gal_coords forKey:@"galaxy_coordinates"]; + [result setObject:tgt_coords forKey:@"target_coordinates"]; + + // Write the name of the current system. Useful for looking up saved game information. + [result setObject:[UNIVERSE getSystemName:[self system_seed]] forKey:@"current_system_name"]; + + [result setObject:player_name forKey:@"player_name"]; + + [result setUnsignedLongLong:credits forKey:@"credits"]; + [result setUnsignedInteger:fuel forKey:@"fuel"]; + + [result setInteger:galaxy_number forKey:@"galaxy_number"]; + + [result setInteger:forward_weapon forKey:@"forward_weapon"]; + [result setInteger:aft_weapon forKey:@"aft_weapon"]; + [result setInteger:port_weapon forKey:@"port_weapon"]; + [result setInteger:starboard_weapon forKey:@"starboard_weapon"]; + + [result setInteger:max_cargo + 5 * max_passengers forKey:@"max_cargo"]; + + [result setObject:shipCommodityData forKey:@"shipCommodityData"]; + + // Deprecated equipment flags. New equipment shouldn't be added here (it'll be handled by the extra_equipment dictionary). + [result setBool:[self hasDockingComputer] forKey:@"has_docking_computer"]; + [result setBool:[self hasGalacticHyperdrive] forKey:@"has_galactic_hyperdrive"]; + [result setBool:[self hasEscapePod] forKey:@"has_escape_pod"]; + [result setBool:[self hasECM] forKey:@"has_ecm"]; + [result setBool:[self hasScoop] forKey:@"has_scoop"]; + [result setBool:[self hasEnergyBomb] forKey:@"has_energy_bomb"]; + [result setBool:[self hasFuelInjection] forKey:@"has_fuel_injection"]; + + if ([self hasEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT"]) + { + [result setBool:YES forKey:@"has_energy_unit"]; + [result setInteger:OLD_ENERGY_UNIT_NAVAL forKey:@"energy_unit"]; + } + else if ([self hasEquipmentItem:@"EQ_ENERGY_UNIT"]) + { + [result setBool:YES forKey:@"has_energy_unit"]; + [result setInteger:OLD_ENERGY_UNIT_NORMAL forKey:@"energy_unit"]; + } + + NSMutableArray* missileRoles = [NSMutableArray arrayWithCapacity:max_missiles]; + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if (missile_entity[i]) + { + [missileRoles addObject:[missile_entity[i] primaryRole]]; + } + else + { + [missileRoles addObject:@"NONE"]; + } + } + [result setObject:missileRoles forKey:@"missile_roles"]; + + [result setInteger:missiles forKey:@"missiles"]; + + [result setInteger:legalStatus forKey:@"legal_status"]; + [result setInteger:market_rnd forKey:@"market_rnd"]; + [result setInteger:ship_kills forKey:@"ship_kills"]; + + // ship depreciation + [result setInteger:ship_trade_in_factor forKey:@"ship_trade_in_factor"]; + + // mission variables + if (mission_variables != nil) + { + [result setObject:[NSDictionary dictionaryWithDictionary:mission_variables] forKey:@"mission_variables"]; + } + + // communications log + NSArray *log = [self commLog]; + if (log != nil) [result setObject:log forKey:@"comm_log"]; + + // extra equipment flags + NSMutableDictionary *equipment = [NSMutableDictionary dictionary]; + NSEnumerator *eqEnum = nil; + NSString *eqDesc = nil; + for (eqEnum = [self equipmentEnumerator]; (eqDesc = [eqEnum nextObject]); ) + { + [equipment setBool:YES forKey:eqDesc]; + } + if ([equipment count] != 0) + { + [result setObject:equipment forKey:@"extra_equipment"]; + } + + // reputation + [result setObject:reputation forKey:@"reputation"]; + + // passengers + [result setInteger:max_passengers forKey:@"max_passengers"]; + [result setObject:passengers forKey:@"passengers"]; + [result setObject:passenger_record forKey:@"passenger_record"]; + + //specialCargo + if (specialCargo) [result setObject:specialCargo forKey:@"special_cargo"]; + + // contracts + [result setObject:contracts forKey:@"contracts"]; + [result setObject:contract_record forKey:@"contract_record"]; + + [result setObject:missionDestinations forKey:@"missionDestinations"]; + + //shipyard + [result setObject:shipyard_record forKey:@"shipyard_record"]; + + //ship's clock + [result setObject:[NSNumber numberWithDouble:ship_clock] forKey:@"ship_clock"]; + + //speech + [result setObject:[NSNumber numberWithBool:isSpeechOn] forKey:@"speech_on"]; +#if OOLITE_ESPEAK + [result setObject:[UNIVERSE voiceName:voice_no] forKey:@"speech_voice"]; + [result setObject:[NSNumber numberWithBool:voice_gender_m] forKey:@"speech_gender"]; +#endif + + //base ship description + [result setObject:ship_desc forKey:@"ship_desc"]; + [result setObject:[[[OOShipRegistry sharedRegistry] shipInfoForKey:ship_desc] stringForKey:KEY_NAME] forKey:@"ship_name"]; + + //custom view no. + [result setUnsignedInteger:_customViewIndex forKey:@"custom_view_index"]; + + //local market + if ([dockedStation localMarket]) [result setObject:[dockedStation localMarket] forKey:@"localMarket"]; + + // strict UNIVERSE? + if ([UNIVERSE strict]) + { + [result setObject:[NSNumber numberWithBool:YES] forKey:@"strict"]; + } + + // persistant UNIVERSE information + if ([UNIVERSE localPlanetInfoOverrides]) + { + [result setObject:[UNIVERSE localPlanetInfoOverrides] forKey:@"local_planetinfo_overrides"]; + } + + // trumble information + [result setObject:[self trumbleValue] forKey:@"trumbles"]; + +#if WORMHOLE_SCANNER + // wormhole information + NSMutableArray * wormholeDicts = [NSMutableArray arrayWithCapacity:[scannedWormholes count]]; + NSEnumerator * wormholes = [scannedWormholes objectEnumerator]; + WormholeEntity * wh; + while ((wh = (WormholeEntity*)[wormholes nextObject])) + { + [wormholeDicts addObject:[wh getDict]]; + } + [result setObject:wormholeDicts forKey:@"wormholes"]; +#endif + + // create checksum + clear_checksum(); + munge_checksum(galaxy_seed.a); munge_checksum(galaxy_seed.b); munge_checksum(galaxy_seed.c); + munge_checksum(galaxy_seed.d); munge_checksum(galaxy_seed.e); munge_checksum(galaxy_seed.f); + munge_checksum((int)galaxy_coordinates.x); munge_checksum((int)galaxy_coordinates.y); + munge_checksum((int)credits); munge_checksum(fuel); + munge_checksum(max_cargo); munge_checksum(missiles); + munge_checksum(legalStatus); munge_checksum(market_rnd); munge_checksum(ship_kills); + + if (mission_variables != nil) + munge_checksum([[mission_variables description] length]); + if (equipment != nil) + munge_checksum([[equipment description] length]); + + int final_checksum = munge_checksum([[ship_desc description] length]); + + //set checksum + [result setInteger:final_checksum forKey:@"checksum"]; + + return result; +} + + +- (BOOL)setCommanderDataFromDictionary:(NSDictionary *) dict +{ + // Required keys + if ([dict stringForKey:@"ship_desc"] == nil) return NO; + if ([dict stringForKey:@"galaxy_seed"] == nil) return NO; + if ([dict stringForKey:@"galaxy_coordinates"] == nil) return NO; + + // TODO: use CollectionExtractors for type-safety. -- Ahruman + [UNIVERSE setStrict:[dict boolForKey:@"strict" defaultValue:NO]]; + + //base ship description + [ship_desc release]; + ship_desc = [[dict objectForKey:@"ship_desc"] copy]; + + NSDictionary *shipDict = [[OOShipRegistry sharedRegistry] shipInfoForKey:ship_desc]; + if (shipDict == nil) return NO; + if (![self setUpShipFromDictionary:shipDict]) return NO; + + // ship depreciation + ship_trade_in_factor = [dict intForKey:@"ship_trade_in_factor" defaultValue:95]; + + galaxy_seed = RandomSeedFromString([dict stringForKey:@"galaxy_seed"]); + if (is_nil_seed(galaxy_seed)) return NO; + [UNIVERSE setGalaxy_seed: galaxy_seed andReinit:YES]; + + NSArray *coord_vals = ScanTokensFromString([dict stringForKey:@"galaxy_coordinates"]); + galaxy_coordinates.x = [coord_vals unsignedCharAtIndex:0]; + galaxy_coordinates.y = [coord_vals unsignedCharAtIndex:1]; + cursor_coordinates = galaxy_coordinates; + + NSString *coords = [dict stringForKey:@"target_coordinates"]; + if (coords != nil) + { + coord_vals = ScanTokensFromString([dict objectForKey:@"target_coordinates"]); + cursor_coordinates.x = [coord_vals unsignedCharAtIndex:0]; + cursor_coordinates.y = [coord_vals unsignedCharAtIndex:1]; + } + + [player_name release]; + player_name = [[dict stringForKey:@"player_name" defaultValue:PLAYER_DEFAULT_NAME] copy]; + + [shipCommodityData autorelease]; + shipCommodityData = [[dict arrayForKey:@"shipCommodityData" defaultValue:shipCommodityData] copy]; + + // extra equipment flags + [self removeAllEquipment]; + [self addEquipmentFromCollection:[dict objectForKey:@"extra_equipment"]]; + + // Equipment flags (deprecated in favour of equipment dictionary, keep for compatibility) + if ([dict boolForKey:@"has_docking_computer"]) [self addEquipmentItem:@"EQ_DOCK_COMP"]; + if ([dict boolForKey:@"has_galactic_hyperdrive"]) [self addEquipmentItem:@"EQ_GAL_DRIVE"]; + if ([dict boolForKey:@"has_escape_pod"]) [self addEquipmentItem:@"EQ_ESCAPE_POD"]; + if ([dict boolForKey:@"has_ecm"]) [self addEquipmentItem:@"EQ_ECM"]; + if ([dict boolForKey:@"has_scoop"]) [self addEquipmentItem:@"EQ_FUEL_SCOOPS"]; + if ([dict boolForKey:@"has_energy_bomb"]) [self addEquipmentItem:@"EQ_ENERGY_BOMB"]; + if ([dict boolForKey:@"has_fuel_injection"]) [self addEquipmentItem:@"EQ_FUEL_INJECTION"]; + + // Legacy energy unit type -> energy unit equipment item + if ([dict boolForKey:@"has_energy_unit"] && [self installedEnergyUnitType] == ENERGY_UNIT_NONE) + { + OOEnergyUnitType eType = [dict intForKey:@"energy_unit" defaultValue:ENERGY_UNIT_NORMAL]; + switch (eType) + { + case OLD_ENERGY_UNIT_NORMAL: + [self addEquipmentItem:@"EQ_ENERGY_UNIT"]; + break; + + case OLD_ENERGY_UNIT_NAVAL: + [self addEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT"]; + break; + + default: + break; + } + } + + if ([self hasEquipmentItem:@"EQ_ADVANCED_COMPASS"]) compassMode = COMPASS_MODE_PLANET; + else compassMode = COMPASS_MODE_BASIC; + + // speech + isSpeechOn = [dict boolForKey:@"speech_on"]; +#if OOLITE_ESPEAK + voice_gender_m = [dict boolForKey:@"speech_gender" defaultValue:YES]; + voice_no = [UNIVERSE setVoice:[UNIVERSE voiceNumber:[dict stringForKey:@"speech_voice" defaultValue:nil]] withGenderM:voice_gender_m]; +#endif + + // reputation + [reputation release]; + reputation = [[dict dictionaryForKey:@"reputation"] mutableCopy]; + if (reputation == nil) reputation = [[NSMutableDictionary alloc] init]; + + // passengers + max_passengers = [dict intForKey:@"max_passengers"]; + [passengers release]; + passengers = [[dict arrayForKey:@"passengers"] mutableCopy]; + if (passengers == nil) passengers = [[NSMutableArray alloc] init]; + [passenger_record release]; + passenger_record = [[dict dictionaryForKey:@"passenger_record"] mutableCopy]; + if (passenger_record == nil) passenger_record = [[NSMutableDictionary alloc] init]; + + //specialCargo + [specialCargo release]; + specialCargo = [[dict stringForKey:@"special_cargo"] copy]; + + // contracts + [contracts release]; + contracts = [[dict arrayForKey:@"contracts"] mutableCopy]; + if (contracts == nil) contracts = [[NSMutableArray alloc] init]; + contract_record = [[dict dictionaryForKey:@"contract_record"] mutableCopy]; + if (contract_record == nil) contract_record = [[NSMutableDictionary alloc] init]; + + // mission destinations + missionDestinations = [[dict arrayForKey:@"missionDestinations"] mutableCopy]; + if (missionDestinations == nil) missionDestinations = [[NSMutableArray alloc] init]; + + // shipyard + shipyard_record = [[dict dictionaryForKey:@"shipyard_record"] mutableCopy]; + if (shipyard_record == nil) shipyard_record = [[NSMutableDictionary alloc] init]; + + // Normalize cargo capacity + unsigned original_hold_size = [UNIVERSE maxCargoForShip:ship_desc]; + max_cargo = [dict intForKey:@"max_cargo" defaultValue:max_cargo]; + if (max_cargo > original_hold_size) [self addEquipmentItem:@"EQ_CARGO_BAY"]; + max_cargo = original_hold_size + ([self hasExpandedCargoBay] ? extra_cargo : 0) - max_passengers * 5; + + credits = [dict unsignedLongLongForKey:@"credits" defaultValue:credits]; + fuel = [dict unsignedIntForKey:@"fuel" defaultValue:fuel]; + + galaxy_number = [dict intForKey:@"galaxy_number"]; + forward_weapon = [dict intForKey:@"forward_weapon"]; + aft_weapon = [dict intForKey:@"aft_weapon"]; + port_weapon = [dict intForKey:@"port_weapon"]; + starboard_weapon = [dict intForKey:@"starboard_weapon"]; + + [self setWeaponDataFromType:forward_weapon]; + scannerRange = (float)SCANNER_MAX_RANGE; + + missiles = [dict unsignedIntForKey:@"missiles"]; + // sanity check the number of missiles... + if (max_missiles > SHIPENTITY_MAX_MISSILES) max_missiles = SHIPENTITY_MAX_MISSILES; + if (missiles > max_missiles) missiles = max_missiles; + // end sanity check + + legalStatus = [dict intForKey:@"legal_status"]; + market_rnd = [dict intForKey:@"market_rnd"]; + ship_kills = [dict intForKey:@"ship_kills"]; + + ship_clock = [dict doubleForKey:@"ship_clock" defaultValue:PLAYER_SHIP_CLOCK_START]; + fps_check_time = ship_clock; + + // mission_variables + [mission_variables release]; + mission_variables = [[dict dictionaryForKey:@"mission_variables"] mutableCopy]; + if (mission_variables == nil) mission_variables = [[NSMutableArray alloc] init]; + + // persistant UNIVERSE info + NSDictionary *planetInfoOverrides = [dict dictionaryForKey:@"local_planetinfo_overrides"]; + if (planetInfoOverrides != nil) [UNIVERSE setLocalPlanetInfoOverrides:planetInfoOverrides]; + + // communications log + [commLog release]; + commLog = [[dict arrayForKey:@"comm_log"] mutableCopy]; + + // set up missiles + unsigned i,j; + [self setActiveMissile:0]; + for (i = 0; i < SHIPENTITY_MAX_MISSILES; i++) + { + [missile_entity[i] release]; + missile_entity[i] = nil; + } + NSArray *missileRoles = [dict arrayForKey:@"missile_roles"]; + if (missileRoles != nil) + { + if (max_missiles < [missileRoles count]) + { + missileRoles = [missileRoles subarrayWithRange:NSMakeRange(0, max_missiles)]; + } + missiles = [missileRoles count]; // sanity check the number of missiles + + for (i = 0, j = 0; i < max_missiles && i < [missileRoles count]; i++, j++) + { + NSString *missile_desc = [missileRoles stringAtIndex:i]; + if (missile_desc != nil && ![missile_desc isEqualToString:@"NONE"]) + { + ShipEntity *amiss = [UNIVERSE newShipWithRole:missile_desc]; + if (amiss) + { + missile_entity[j] = amiss; // retain count = 1 + } + else + { + j--; + OOLog(@"load.failed.missileNotFound", @"----- WARNING: couldn't find missile with role '%@' while trying [PlayerEntity setCommanderDataFromDictionary:], missile entry discarded. -----", missile_desc); + } + } + } + } + else // no missile_roles + { + for (i = 0; i < missiles; i++) + { + missile_entity[i] = [UNIVERSE newShipWithRole:@"EQ_MISSILE"]; // retain count = 1 - should be okay as long as we keep a missile with this role + // in the base package. + } + } + + // Sanity check: ensure the missiles variable holds the correct missile count. + // FIXME: get rid of "missiles" and just count them when needed. -- Ahruman + missiles = 0; + for (i = 0; i != SHIPENTITY_MAX_MISSILES; i++) + { + if (missile_entity[i] != nil) missiles++; + } + + while (missiles > 0 && missile_entity[activeMissile] == nil) + { + [self selectNextMissile]; + } + + forward_shield = [self maxForwardShieldLevel]; + aft_shield = [self maxAftShieldLevel]; + + // things... + system_seed = [UNIVERSE findSystemAtCoords:galaxy_coordinates withGalaxySeed:galaxy_seed]; + target_system_seed = [UNIVERSE findSystemAtCoords:cursor_coordinates withGalaxySeed:galaxy_seed]; + +#if WORMHOLE_SCANNER + // wormholes + NSArray * whArray; + whArray = [dict objectForKey:@"wormholes"]; + NSEnumerator * whDicts = [whArray objectEnumerator]; + NSDictionary * whCurrDict; + [scannedWormholes release]; + scannedWormholes = [[NSMutableArray alloc] initWithCapacity:[whArray count]]; + while ((whCurrDict = [whDicts nextObject]) != nil) + { + WormholeEntity * wh = [[WormholeEntity alloc] initWithDict:whCurrDict]; + [scannedWormholes addObject:wh]; + /* TODO - add to Universe if the wormhole hasn't expired yet; but in this case + * we need to save/load position and mass as well, which we currently + * don't + if (equal_seeds([wh origin], system_seed)) + { + [UNIVERSE addEntity:wh]; + } + */ + } +#endif + + // custom view no. + if (_customViews != nil) + _customViewIndex = [dict unsignedIntForKey:@"custom_view_index"] % [_customViews count]; + + // trumble information + [self setUpTrumbles]; + [self setTrumbleValueFrom:[dict objectForKey:@"trumbles"]]; // if it doesn't exist we'll check user-defaults + + // finally + missiles = [self countMissiles]; + + return YES; +} + +///////////////////////////////////////////////////////// + +- (id) init +{ + if (sSharedPlayer != nil) + { + [NSException raise:NSInternalInconsistencyException format:@"%s: expected only one PlayerEntity to exist at a time.", __FUNCTION__]; + } + + self = [super init]; + sSharedPlayer = self; + + compassMode = COMPASS_MODE_BASIC; + + afterburnerSoundLooping = NO; + + int i; + for (i = 0; i < SHIPENTITY_MAX_MISSILES; i++) + missile_entity[i] = nil; + [self set_up]; + + isPlayer = YES; + + save_path = nil; + + [self setUpSound]; + + scoopsActive = NO; + + target_memory_index = 0; + + dockingReport = [[NSMutableString alloc] init]; + + worldScripts = [[ResourceManager loadScripts] retain]; + + [self initControls]; + + return self; +} + + +- (void) set_up +{ + [self set_up:YES]; +} + + +- (void) set_up:(BOOL) andReset; +{ + unsigned i; + Random_Seed gal_seed = {0x4a, 0x5a, 0x48, 0x02, 0x53, 0xb7}; + + showDemoShips = NO; + + show_info_flag = NO; + + // if there is cargo remaining from previously (e.g. a game restart), remove it + if ([self cargoList] != nil) + { + [self removeAllCargo:YES]; // force removal of cargo + } + + [ship_desc release]; + ship_desc = PLAYER_SHIP_DESC; + ship_trade_in_factor = 95; + +#ifndef NDEBUG + gDebugFlags &= ~DEBUG_HIDE_HUD; +#endif + NSDictionary *huddict = [ResourceManager dictionaryFromFilesNamed:@"hud.plist" inFolder:@"Config" andMerge:YES]; + [hud release]; + hud = [[HeadUpDisplay alloc] initWithDictionary:huddict]; + [hud setScannerZoom:1.0]; + [hud resizeGuis:huddict]; + scanner_zoom_rate = 0.0f; + + [mission_variables release]; + mission_variables = [[NSMutableDictionary alloc] init]; + + [localVariables release]; + localVariables = [[NSMutableDictionary alloc] init]; + + [self setScriptTarget:nil]; + [self resetMissionChoice]; + + [reputation release]; + reputation = [[NSMutableDictionary alloc] initWithCapacity:6]; + [reputation setInteger:0 forKey:CONTRACTS_GOOD_KEY]; + [reputation setInteger:0 forKey:CONTRACTS_BAD_KEY]; + [reputation setInteger:7 forKey:CONTRACTS_UNKNOWN_KEY]; + [reputation setInteger:0 forKey:PASSAGE_GOOD_KEY]; + [reputation setInteger:0 forKey:PASSAGE_BAD_KEY]; + [reputation setInteger:7 forKey:PASSAGE_UNKNOWN_KEY]; + + energy = 256; + weapon_temp = 0.0f; + forward_weapon_temp = 0.0f; + aft_weapon_temp = 0.0f; + port_weapon_temp = 0.0f; + starboard_weapon_temp = 0.0f; + ship_temperature = 60.0f; + alertFlags = 0; + + max_passengers = 0; + [passengers release]; + passengers = [[NSMutableArray alloc] init]; + [passenger_record release]; + passenger_record = [[NSMutableDictionary alloc] init]; + + [contracts release]; + contracts = [[NSMutableArray alloc] init]; + [contract_record release]; + contract_record = [[NSMutableDictionary alloc] init]; + + [missionDestinations release]; + missionDestinations = [[NSMutableArray alloc] init]; + + [shipyard_record release]; + shipyard_record = [[NSMutableDictionary alloc] init]; + + [missionBackgroundTexture release]; + missionBackgroundTexture = nil; + + script_time = 0.0; + script_time_check = SCRIPT_TIMER_INTERVAL; + script_time_interval = SCRIPT_TIMER_INTERVAL; + + NSCalendarDate *nowDate = [NSCalendarDate calendarDate]; + ship_clock = PLAYER_SHIP_CLOCK_START; + ship_clock += [nowDate hourOfDay] * 3600.0; + ship_clock += [nowDate minuteOfHour] * 60.0; + ship_clock += [nowDate secondOfMinute]; + fps_check_time = ship_clock; + ship_clock_adjust = 0.0; + + isSpeechOn = NO; +#if OOLITE_ESPEAK + voice_gender_m = YES; + voice_no = [UNIVERSE setVoice:-1 withGenderM:voice_gender_m]; +#endif + + [_customViews release]; + _customViews = nil; + _customViewIndex = 0; + + mouse_control_on = NO; + + // player commander data + // Most of this is probably also set more than once + + player_name = [PLAYER_DEFAULT_NAME copy]; + galaxy_coordinates = NSMakePoint(0x14,0xAD); // 20,173 + galaxy_seed = gal_seed; + credits = 1000; + fuel = PLAYER_MAX_FUEL; + fuel_accumulator = 0.0f; + + galaxy_number = 0; + forward_weapon = WEAPON_PULSE_LASER; + aft_weapon = WEAPON_NONE; + port_weapon = WEAPON_NONE; + starboard_weapon = WEAPON_NONE; + [self setWeaponDataFromType:forward_weapon]; + scannerRange = (float)SCANNER_MAX_RANGE; + + ecm_in_operation = NO; + compassMode = COMPASS_MODE_BASIC; + ident_engaged = NO; + + + max_cargo = 20; // will be reset later + + shipCommodityData = [[[ResourceManager dictionaryFromFilesNamed:@"commodities.plist" inFolder:@"Config" andMerge:YES] objectForKey:@"default"] retain]; + + // set up missiles + missiles = PLAYER_STARTING_MISSILES; + max_missiles = PLAYER_MAX_MISSILES; + + [self setActiveMissile: 0]; + for (i = 0; i < missiles; i++) + { + [missile_entity[i] release]; + missile_entity[i] = nil; + } + [self safeAllMissiles]; + + [self clearSubEntities]; + + legalStatus = 0; + + market_rnd = 0; + ship_kills = 0; + cursor_coordinates = galaxy_coordinates; + + scripted_misjump = NO; + + forward_shield = [self maxForwardShieldLevel]; + aft_shield = [self maxAftShieldLevel]; + + scanClass = CLASS_PLAYER; + + [UNIVERSE clearGUIs]; + +#if DOCKING_CLEARANCE_ENABLED + dockingClearanceStatus = DOCKING_CLEARANCE_STATUS_GRANTED; + targetDockStation = nil; +#endif + + dockedStation = [UNIVERSE station]; + + [commLog release]; + commLog = nil; + + [specialCargo release]; + specialCargo = nil; + + // views + forwardViewOffset = kZeroVector; + aftViewOffset = kZeroVector; + portViewOffset = kZeroVector; + starboardViewOffset = kZeroVector; + customViewOffset = kZeroVector; + + currentWeaponFacing = VIEW_FORWARD; + + [save_path autorelease]; + save_path = nil; + +#if WORMHOLE_SCANNER + [scannedWormholes release]; + scannedWormholes = [[NSMutableArray alloc] init]; +#endif + + [self setUpTrumbles]; + + suppressTargetLost = NO; + + scoopsActive = NO; + + [dockingReport release]; + dockingReport = [[NSMutableString alloc] init]; + + [shipAI release]; + shipAI = [[AI alloc] initWithStateMachine:PLAYER_DOCKING_AI_NAME andState:@"GLOBAL"]; + [shipAI setOwner:self]; + + lastScriptAlertCondition = [self alertCondition]; + + entity_personality = ranrot_rand() & 0x7FFF; + + [self setSystem_seed:[UNIVERSE findSystemAtCoords:[self galaxy_coordinates] withGalaxySeed:[self galaxy_seed]]]; + + [self setGalacticHyperspaceBehaviourTo:[[UNIVERSE planetInfo] stringForKey:@"galactic_hyperspace_behaviour" defaultValue:@"BEHAVIOUR_STANDARD"]]; + [self setGalacticHyperspaceFixedCoordsTo:[[UNIVERSE planetInfo] stringForKey:@"galactic_hyperspace_fixed_coords" defaultValue:@"96 96"]]; + + [self setCloaked:NO]; + + [[OOMusicController sharedController] stop]; + [OOScriptTimer noteGameReset]; + if (andReset) [self doScriptEvent:@"reset"]; +} + + +- (BOOL) setUpShipFromDictionary:(NSDictionary *)shipDict +{ + // In order for default values to work and float values to not be junk, + // replace nil with empty dictionary. -- Ahruman 2008-05-01 + if (shipDict == nil) shipDict = [NSDictionary dictionary]; + + [shipinfoDictionary release]; + shipinfoDictionary = [shipDict copy]; + + // set things from dictionary from here out + + maxFlightSpeed = [shipDict floatForKey:@"max_flight_speed" defaultValue:160.0f]; + max_flight_roll = [shipDict floatForKey:@"max_flight_roll" defaultValue:2.0f]; + max_flight_pitch = [shipDict floatForKey:@"max_flight_pitch" defaultValue:1.0f]; + max_flight_yaw = [shipDict floatForKey:@"max_flight_yaw" defaultValue:max_flight_pitch]; // Note by default yaw == pitch + + // set control factors.. + roll_delta = 2.0f * max_flight_roll; + pitch_delta = 2.0f * max_flight_pitch; + yaw_delta = 2.0f * max_flight_yaw; + + thrust = [shipDict floatForKey:@"thrust" defaultValue:thrust]; + + if (![UNIVERSE strict]) + { + hyperspaceMotorSpinTime = [shipDict floatForKey:@"hyperspace_motor_spin_time" defaultValue:DEFAULT_HYPERSPACE_SPIN_TIME]; + } + else + { + hyperspaceMotorSpinTime = DEFAULT_HYPERSPACE_SPIN_TIME; + } + + maxEnergy = [shipDict floatForKey:@"max_energy" defaultValue:maxEnergy]; + energy_recharge_rate = [shipDict floatForKey:@"energy_recharge_rate" defaultValue:energy_recharge_rate]; + energy = maxEnergy; + + // Each new ship should start in seemingly good operating condition, unless specifically told not to + [self setThrowSparks:[shipDict boolForKey:@"throw_sparks" defaultValue:NO]]; + + cloakPassive = [shipDict boolForKey:@"cloak_passive" defaultValue:NO]; + + forward_weapon_type = StringToWeaponType([shipDict stringForKey:@"forward_weapon_type" defaultValue:@"WEAPON_NONE"]); + aft_weapon_type = StringToWeaponType([shipDict stringForKey:@"aft_weapon_type" defaultValue:@"WEAPON_NONE"]); + [self setWeaponDataFromType:forward_weapon_type]; + scannerRange = (float)SCANNER_MAX_RANGE; + + missiles = (unsigned)[shipDict doubleForKey:@"missiles"]; + max_missiles = [shipDict intForKey:@"max_missiles" defaultValue:missiles]; + if (max_missiles > SHIPENTITY_MAX_MISSILES) max_missiles = SHIPENTITY_MAX_MISSILES; + if (missiles > max_missiles) missiles = max_missiles; + + if ([shipDict fuzzyBooleanForKey:@"has_ecm"]) [self addEquipmentItem:@"EQ_ECM"]; + if ([shipDict fuzzyBooleanForKey:@"has_scoop"]) [self addEquipmentItem:@"EQ_FUEL_SCOOPS"]; + if ([shipDict fuzzyBooleanForKey:@"has_escape_pod"]) [self addEquipmentItem:@"EQ_ESCAPE_POD"]; + + max_cargo = [shipDict intForKey:@"max_cargo"]; + extra_cargo = [shipDict intForKey:@"extra_cargo" defaultValue:15]; + + // Load the model (must be before subentities) + NSString *modelName = [shipDict stringForKey:@"model"]; + if (modelName != nil) + { + OOMesh *mesh = [OOMesh meshWithName:modelName + materialDictionary:[shipDict dictionaryForKey:@"materials"] + shadersDictionary:[shipDict dictionaryForKey:@"shaders"] + smooth:[shipDict boolForKey:@"smooth" defaultValue:NO] + shaderMacros:DefaultShipShaderMacros() + shaderBindingTarget:self]; + [self setMesh:mesh]; + } + + float density = [shipDict floatForKey:@"density" defaultValue:1.0f]; + if (octree) + { + mass = (GLfloat)(density * 20.0 * [octree volume]); + } + [name autorelease]; + name = [[shipDict stringForKey:@"name" defaultValue:name] copy]; + + [displayName autorelease]; + displayName = [[shipDict stringForKey:@"display_name" defaultValue:name] copy]; + + [roleSet release]; + roleSet = nil; + [self setPrimaryRole:@"player"]; + + OOColor *color = [OOColor brightColorWithDescription:[shipDict objectForKey:@"laser_color"]]; + if (color == nil) color = [OOColor redColor]; + [self setLaserColor:color]; + + [self removeAllEquipment]; + [self addEquipmentFromCollection:[shipDict objectForKey:@"extra_equipment"]]; + + NSString *hud_desc = [shipDict stringForKey:@"hud"]; + if (hud_desc != nil) + { + NSDictionary *huddict = [ResourceManager dictionaryFromFilesNamed:hud_desc inFolder:@"Config" andMerge:YES]; + if (huddict) + { +#ifndef NDEBUG + gDebugFlags &= ~DEBUG_HIDE_HUD; +#endif + [hud release]; + hud = [[HeadUpDisplay alloc] initWithDictionary:huddict]; + [hud setScannerZoom:1.0]; + [hud resizeGuis: huddict]; + } + } + + + // set up missiles + unsigned i; + for (i = 0; i < SHIPENTITY_MAX_MISSILES; i++) + { + [missile_entity[i] release]; + missile_entity[i] = nil; + } + for (i = 0; i < missiles; i++) + { + missile_entity[i] = [UNIVERSE newShipWithRole:@"EQ_MISSILE"]; // retain count = 1 + } + [self setActiveMissile:0]; + + + // set view offsets + [self setDefaultViewOffsets]; + + ScanVectorFromString([shipDict stringForKey:@"view_position_forward"], &forwardViewOffset); + ScanVectorFromString([shipDict stringForKey:@"view_position_aft"], &aftViewOffset); + ScanVectorFromString([shipDict stringForKey:@"view_position_port"], &portViewOffset); + ScanVectorFromString([shipDict stringForKey:@"view_position_starboard"], &starboardViewOffset); + + [self setDefaultCustomViews]; + + NSArray *customViews = [shipDict arrayForKey:@"custom_views"]; + if (customViews != nil) + { + [_customViews release]; + _customViews = [customViews retain]; + _customViewIndex = 0; + } + + // set weapon offsets + [self setDefaultWeaponOffsets]; + + ScanVectorFromString([shipDict stringForKey:@"weapon_position_forward"], &forwardWeaponOffset); + ScanVectorFromString([shipDict stringForKey:@"weapon_position_aft"], &aftWeaponOffset); + ScanVectorFromString([shipDict stringForKey:@"weapon_position_port"], &portWeaponOffset); + ScanVectorFromString([shipDict stringForKey:@"weapon_position_starboard"], &starboardWeaponOffset); + + // fuel scoop destination position (where cargo gets sucked into) + tractor_position = kZeroVector; + ScanVectorFromString([shipDict stringForKey:@"scoop_position"], &tractor_position); + + [subEntities autorelease]; + subEntities = nil; + + + if (![self setUpSubEntities: shipDict]) return NO; + + // rotating subentities + subentityRotationalVelocity = kIdentityQuaternion; + ScanQuaternionFromString([shipDict objectForKey:@"rotational_velocity"], &subentityRotationalVelocity); + + // Load script + [script release]; + script = [OOScript nonLegacyScriptFromFileNamed:[shipDict stringForKey:@"script"] + properties:[NSDictionary dictionaryWithObject:self forKey:@"ship"]]; + + return YES; +} + + +- (void) dealloc +{ + [ship_desc release]; + [hud release]; + [commLog release]; + + [worldScripts release]; + [mission_variables release]; + + [localVariables release]; + + [lastTextKey release]; + + [reputation release]; + [passengers release]; + [passenger_record release]; + [contracts release]; + [contract_record release]; + [missionDestinations release]; + [shipyard_record release]; + + [missionBackgroundTexture release]; + + [player_name release]; + [shipCommodityData release]; + + [specialCargo release]; + + [save_path release]; + + [_customViews release]; + + [dockingReport release]; + + [self destroySound]; + +#if WORMHOLE_SCANNER + [scannedWormholes release]; + scannedWormholes = nil; +#endif + + int i; + for (i = 0; i < SHIPENTITY_MAX_MISSILES; i++) [missile_entity[i] release]; + + for (i = 0; i < PLAYER_MAX_TRUMBLES; i++) [trumble[i] release]; + + [super dealloc]; +} + + +- (void) warnAboutHostiles +{ + [self playHostileWarning]; +} + + +- (BOOL) canCollide +{ + switch ([self status]) + { + case STATUS_START_GAME: + case STATUS_DOCKING: + case STATUS_DOCKED: + case STATUS_DEAD: + case STATUS_ESCAPE_SEQUENCE: + return NO; + + default: + return YES; + } +} + + +- (NSComparisonResult) compareZeroDistance:(Entity *)otherEntity; +{ + return NSOrderedDescending; // always the most near +} + + +- (BOOL) validForAddToUniverse +{ + return YES; +} + + +- (void) update:(OOTimeDelta)delta_t +{ + [self updateMovementFlags]; + [self updateAlertCondition]; + [self updateFuelScoops:delta_t]; // TODO: this should probably be called from performInFlightUpdates: instead. -- Ahruman 20080322 + + [self updateClocks:delta_t]; + + // scripting + [OOScriptTimer updateTimers]; + [self checkScriptsIfAppropriate]; + + // deal with collisions + [self manageCollisions]; + [self saveToLastFrame]; + + [self pollControls:delta_t]; + + [self updateTrumbles:delta_t]; + + OOEntityStatus status = [self status]; + if (status == STATUS_START_GAME && gui_screen != GUI_SCREEN_INTRO1 && gui_screen != GUI_SCREEN_INTRO2) + { + [self setGuiToIntroFirstGo:YES]; //set up demo mode + } + + if (status == STATUS_AUTOPILOT_ENGAGED || status == STATUS_ESCAPE_SEQUENCE) + { + [self performAutopilotUpdates:delta_t]; + } + else if (![self isDocked]) [self performInFlightUpdates:delta_t]; + + if (status == STATUS_IN_FLIGHT) [self doBookkeeping:delta_t]; + if (status == STATUS_WITCHSPACE_COUNTDOWN) [self performWitchspaceCountdownUpdates:delta_t]; + if (status == STATUS_EXITING_WITCHSPACE) [self performWitchspaceExitUpdates:delta_t]; + if (status == STATUS_LAUNCHING) [self performLaunchingUpdates:delta_t]; + if (status == STATUS_DOCKING) [self performDockingUpdates:delta_t]; + if (status == STATUS_DEAD) [self performDeadUpdates:delta_t]; + + // TODO: this should probably be called from performInFlightUpdates: instead. -- Ahruman 20080322 + // Moved to performInFlightUpdates. -- Micha 20090403 + //[self updateTargetting]; +#if WORMHOLE_SCANNER + [self updateWormholes]; +#endif +} + + +- (void) doBookkeeping:(double) delta_t +{ + // Bookeeping; + + double speed_delta = 5.0 * thrust; + + PlanetEntity* sun = [UNIVERSE sun]; + double external_temp = 0; + GLfloat air_friction = 0.0f; + if (UNIVERSE) + air_friction = 0.5f * [UNIVERSE airResistanceFactor]; + + // cool all weapons + forward_weapon_temp = fmaxf(forward_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f); + aft_weapon_temp = fmaxf(aft_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f); + port_weapon_temp = fmaxf(port_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f); + starboard_weapon_temp = fmaxf(starboard_weapon_temp - (float)(WEAPON_COOLING_FACTOR * delta_t), 0.0f); + + // copy new temp to main temp + switch (currentWeaponFacing) + { + case VIEW_GUI_DISPLAY: + case VIEW_NONE: + case VIEW_BREAK_PATTERN: + case VIEW_FORWARD: + case VIEW_CUSTOM: + weapon_temp = forward_weapon_temp; + break; + case VIEW_AFT: + weapon_temp = aft_weapon_temp; + break; + case VIEW_PORT: + weapon_temp = port_weapon_temp; + break; + case VIEW_STARBOARD: + weapon_temp = starboard_weapon_temp; + break; + } + + // cloaking device + if ([self hasCloakingDevice] && cloaking_device_active) + { + energy -= (float)delta_t * CLOAKING_DEVICE_ENERGY_RATE; + if (energy < CLOAKING_DEVICE_MIN_ENERGY) + [self deactivateCloakingDevice]; + } + + // military_jammer + if ([self hasMilitaryJammer]) + { + if (military_jammer_active) + { + energy -= (float)delta_t * MILITARY_JAMMER_ENERGY_RATE; + if (energy < MILITARY_JAMMER_MIN_ENERGY) + military_jammer_active = NO; + } + else + { + if (energy > 1.5 * MILITARY_JAMMER_MIN_ENERGY) + military_jammer_active = YES; + } + } + + if (energy < maxEnergy) + { + double energy_multiplier = 1.0 + 0.1 * [self installedEnergyUnitType]; // 1.8x recharge with normal energy unit, 2.6x with naval! + energy += (float)(energy_recharge_rate * energy_multiplier * delta_t); + if (energy > maxEnergy) + energy = maxEnergy; + } + + // Recharge shields from energy banks + float rechargeFwd = (float)([self shieldRechargeRate] * delta_t); + float rechargeAft = rechargeFwd; + float fwdMax = [self maxForwardShieldLevel]; + float aftMax = [self maxAftShieldLevel]; + + if (forward_shield < fwdMax) + { + if (forward_shield + rechargeFwd > fwdMax) rechargeFwd = fwdMax - forward_shield; + forward_shield += rechargeFwd; + energy -= rechargeFwd; + } + if (aft_shield < aftMax) + { + if (aft_shield + rechargeAft > aftMax) rechargeAft = aftMax - aft_shield; + aft_shield += rechargeAft; + energy -= rechargeAft; + } + forward_shield = OOClamp_0_max_f(forward_shield, fwdMax); + aft_shield = OOClamp_0_max_f(aft_shield, aftMax); + + if (ecm_in_operation) + { + if (energy > 0.0) + energy -= (float)(ECM_ENERGY_DRAIN_FACTOR * delta_t); // drain energy because of the ECM + else + { + ecm_in_operation = NO; + [UNIVERSE addMessage:DESC(@"ecm-out-of-juice") forCount:3.0]; + } + if ([UNIVERSE getTime] > ecm_start_time + ECM_DURATION) + { + ecm_in_operation = NO; + } + } + + if (sun) + { + // set the ambient temperature here + double sun_zd = sun->zero_distance; // square of distance + double sun_cr = sun->collision_radius; + double alt1 = sun_cr * sun_cr / sun_zd; + external_temp = SUN_TEMPERATURE * alt1; + if ([[UNIVERSE sun] goneNova]) + external_temp *= 100; + + // do Revised sun-skimming check here... + if ([self hasScoop] && alt1 > 0.75 && [self fuel] < [self fuelCapacity]) + { + fuel_accumulator += (float)(delta_t * flightSpeed * 0.010); + scoopsActive = YES; + while (fuel_accumulator > 1.0) + { + [self setFuel:[self fuel] + 1]; + fuel_accumulator -= 1.0f; + } + [UNIVERSE displayCountdownMessage:DESC(@"fuel-scoop-active") forCount:1.0]; + } + } + + //Bug #11692 CmdrJames added Status entering witchspace + OOEntityStatus status = [self status]; + if ((status != STATUS_AUTOPILOT_ENGAGED)&&(status != STATUS_ESCAPE_SEQUENCE) && (status != STATUS_ENTERING_WITCHSPACE)) + { + // work on the cabin temperature + float deltaInsulation = delta_t/[self heatInsulation]; + ship_temperature += (float)( flightSpeed * air_friction * deltaInsulation); // wind_speed + + if (external_temp > ship_temperature) + ship_temperature += (float)((external_temp - ship_temperature) * SHIP_INSULATION_FACTOR * deltaInsulation); + else + { + if (ship_temperature > SHIP_MIN_CABIN_TEMP) + ship_temperature += (float)((external_temp - ship_temperature) * SHIP_COOLING_FACTOR * deltaInsulation); + } + + if (ship_temperature > SHIP_MAX_CABIN_TEMP) + [self takeHeatDamage: delta_t * ship_temperature]; + } + + if ((status == STATUS_ESCAPE_SEQUENCE)&&(shot_time > ESCAPE_SEQUENCE_TIME)) + { + [[UNIVERSE entityForUniversalID:found_target] becomeExplosion]; // blow up the doppelganger + [self setTargetToNearestStation]; + if ([self primaryTarget]) + { + // restore player ship + ShipEntity *player_ship = [UNIVERSE newShipWithName:ship_desc]; // retained + if (player_ship) + { + // FIXME: this should use OOShipType, which should exist. -- Ahruman + [self setMesh:[player_ship mesh]]; + [player_ship release]; // we only wanted it for its polygons! + } + [UNIVERSE setViewDirection:VIEW_FORWARD]; + [self enterDock:(StationEntity *)[self primaryTarget]]; + } + } + + + // MOVED THE FOLLOWING FROM PLAYERENTITY POLLFLIGHTCONTROLS: + travelling_at_hyperspeed = (flightSpeed > maxFlightSpeed); + if (hyperspeed_engaged) + { + // increase speed up to maximum hyperspeed + if (flightSpeed < maxFlightSpeed * HYPERSPEED_FACTOR) + flightSpeed += (float)(speed_delta * delta_t * HYPERSPEED_FACTOR); + if (flightSpeed > maxFlightSpeed * HYPERSPEED_FACTOR) + flightSpeed = (float)(maxFlightSpeed * HYPERSPEED_FACTOR); + + // check for mass lock + hyperspeed_locked = [self massLocked]; + // check for mass lock & external temperature? + //hyperspeed_locked = flightSpeed * air_friction > 40.0f+(ship_temperature - external_temp ) * SHIP_COOLING_FACTOR || [self massLocked]; + + if (hyperspeed_locked) + { + [self playJumpMassLocked]; + [UNIVERSE addMessage:DESC(@"jump-mass-locked") forCount:4.5]; + hyperspeed_engaged = NO; + } + } + else + { + if (afterburner_engaged) + { + float abFactor = [self afterburnerFactor]; + if (flightSpeed < maxFlightSpeed * abFactor) + flightSpeed += (float)(speed_delta * delta_t * abFactor); + if (flightSpeed > maxFlightSpeed * abFactor) + flightSpeed = maxFlightSpeed * abFactor; + fuel_accumulator -= (float)(delta_t * AFTERBURNER_BURNRATE); + while ((fuel_accumulator < 0)&&(fuel > 0)) + { + fuel_accumulator += 1.0f; + if (--fuel <= MIN_FUEL) + afterburner_engaged = NO; + } + } + else + { + // slow back down... + if (travelling_at_hyperspeed) + { + // decrease speed to maximum normal speed + flightSpeed -= (float)(speed_delta * delta_t * HYPERSPEED_FACTOR); + if (flightSpeed < maxFlightSpeed) + flightSpeed = maxFlightSpeed; + } + } + } + + + + // fuel leakage + + if ((fuel_leak_rate > 0.0)&&(fuel > 0)) + { + fuel_accumulator -= (float)(fuel_leak_rate * delta_t); + while ((fuel_accumulator < 0)&&(fuel > 0)) + { + fuel_accumulator += 1.0f; + fuel--; + } + if (fuel == 0) + fuel_leak_rate = 0; + } + + // smart_zoom + if (scanner_zoom_rate) + { + double z = [hud scanner_zoom]; + double z1 = z + scanner_zoom_rate * delta_t; + if (scanner_zoom_rate > 0.0) + { + if (floor(z1) > floor(z)) + { + z1 = floor(z1); + scanner_zoom_rate = 0.0f; + } + } + else + { + if (z1 < 1.0) + { + z1 = 1.0; + scanner_zoom_rate = 0.0f; + } + } + [hud setScannerZoom:z1]; + } + + // update subentities + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); ) + { + [se update:delta_t]; + } +} + + +- (void) updateMovementFlags +{ + hasMoved = !vector_equal(position, lastPosition); + hasRotated = !quaternion_equal(orientation, lastOrientation); + lastPosition = position; + lastOrientation = orientation; +} + + +- (void) updateAlertCondition +{ + /* TODO: update alert condition once per frame. Tried this before, but + there turned out to be complications. See mailing list archive. + -- Ahruman 20070802 + */ + OOAlertCondition cond = [self alertCondition]; + if (cond != lastScriptAlertCondition) + { + [self doScriptEvent:@"alertConditionChanged" + withArgument:[NSNumber numberWithInt:cond] + andArgument:[NSNumber numberWithInt:lastScriptAlertCondition]]; + lastScriptAlertCondition = cond; + } +} + + +- (void) updateFuelScoops:(OOTimeDelta)delta_t +{ + if (scoopsActive) + { + [self updateFuelScoopSoundWithInterval:delta_t]; + scoopsActive = NO; + } +} + + +- (void) updateClocks:(OOTimeDelta)delta_t +{ + shot_time += delta_t; + script_time += delta_t; + ship_clock += delta_t; + if (ship_clock_adjust != 0.0) // adjust for coming out of warp (add LY * LY hrs) + { + double fine_adjust = delta_t * 7200.0; + if (ship_clock_adjust > 86400) // more than a day + fine_adjust = delta_t * 115200.0; // 16 times faster + if (ship_clock_adjust > 0) + { + if (fine_adjust > ship_clock_adjust) + fine_adjust = ship_clock_adjust; + ship_clock += fine_adjust; + ship_clock_adjust -= fine_adjust; + } + else + { + if (fine_adjust < ship_clock_adjust) + fine_adjust = ship_clock_adjust; + ship_clock -= fine_adjust; + ship_clock_adjust += fine_adjust; + } + } + + //fps + if (ship_clock > fps_check_time) + { + if (![self clockAdjusting]) + { + fps_counter = (int)([UNIVERSE timeAccelerationFactor] * floor([UNIVERSE framesDoneThisUpdate] / (fps_check_time - last_fps_check_time))); + last_fps_check_time = fps_check_time; + fps_check_time = ship_clock + MINIMUM_GAME_TICK; + } + else + { + // Good approximation for when the clock is adjusting and proper fps calculation + // cannot be performed. + fps_counter = (int)([UNIVERSE timeAccelerationFactor] * floor(1.0 / delta_t)); + fps_check_time = ship_clock + MINIMUM_GAME_TICK; + } + [UNIVERSE resetFramesDoneThisUpdate]; // Reset frame counter + } +} + + +- (void) checkScriptsIfAppropriate +{ + if (script_time <= script_time_check) return; + + if ([self status] != STATUS_IN_FLIGHT) + { + switch (gui_screen) + { + // Screens where no world script tickles are performed + case GUI_SCREEN_MAIN: + case GUI_SCREEN_INTRO1: + case GUI_SCREEN_INTRO2: + case GUI_SCREEN_MARKET: + case GUI_SCREEN_OPTIONS: + case GUI_SCREEN_GAMEOPTIONS: + case GUI_SCREEN_LOAD: + case GUI_SCREEN_SAVE: + case GUI_SCREEN_SAVE_OVERWRITE: + case GUI_SCREEN_STICKMAPPER: + case GUI_SCREEN_MISSION: + case GUI_SCREEN_REPORT: + return; + break; + + // Screens from which it's safe to jump to the mission screen + case GUI_SCREEN_CONTRACTS: + case GUI_SCREEN_EQUIP_SHIP: + case GUI_SCREEN_LONG_RANGE_CHART: + case GUI_SCREEN_MANIFEST: + case GUI_SCREEN_SHIPYARD: + case GUI_SCREEN_SHORT_RANGE_CHART: + case GUI_SCREEN_STATUS: + case GUI_SCREEN_SYSTEM_DATA: + [self checkScript]; + script_time_check += script_time_interval; + break; + } + } + + // We passed the test, update the script. + [self checkScript]; + script_time_check += script_time_interval; +} + + +- (void) updateTrumbles:(OOTimeDelta)delta_t +{ + OOTrumble **trumbles = [self trumbleArray]; + unsigned i; + + for (i = [self trumbleCount] ; i > 0; i--) + { + OOTrumble* trum = trumbles[i - 1]; + [trum updateTrumble:delta_t]; + } +} + + +- (void) performAutopilotUpdates:(OOTimeDelta)delta_t +{ + [super update:delta_t]; + [self doBookkeeping:delta_t]; +} + + +#define VELOCITY_CLEANUP_MIN 2000.0f // Minimum speed for "power braking". +#define VELOCITY_CLEANUP_FULL 5000.0f // Speed at which full "power braking" factor is used. +#define VELOCITY_CLEANUP_RATE 0.001f // Factor for full "power braking". + +- (void) performInFlightUpdates:(OOTimeDelta)delta_t +{ + // do flight routines + //// velocity stuff + assert(VELOCITY_CLEANUP_FULL > VELOCITY_CLEANUP_MIN); + + position = vector_add(position, vector_multiply_scalar(velocity, (float)delta_t)); + + GLfloat velmag = magnitude(velocity); + if (velmag > 0) + { + GLfloat velmag2 = velmag - (float)delta_t * thrust; + if (velmag > VELOCITY_CLEANUP_MIN) + { + GLfloat rate; + // Fix up extremely ridiculous speeds that can happen in collisions or explosions + if (velmag > VELOCITY_CLEANUP_FULL) rate = VELOCITY_CLEANUP_RATE; + else rate = (velmag - VELOCITY_CLEANUP_MIN) / (VELOCITY_CLEANUP_FULL - VELOCITY_CLEANUP_MIN) * VELOCITY_CLEANUP_RATE; + velmag2 -= velmag * rate; + } + if (velmag2 < 0.0f) velocity = kZeroVector; + else velocity = vector_multiply_scalar(velocity, velmag2 / velmag); + + if ([UNIVERSE strict]) + { + if (velmag2 < OG_ELITE_FORWARD_DRIFT) + { + // add acceleration + velocity = vector_add(velocity, vector_multiply_scalar(v_forward, (float)delta_t * OG_ELITE_FORWARD_DRIFT * 20.0f)); + } + } + } + + [self applyRoll:(float)delta_t*flightRoll andClimb:(float)delta_t*flightPitch]; + if (flightYaw != 0.0) + { + [self applyYaw:(float)delta_t*flightYaw]; + } + [self moveForward:delta_t*flightSpeed]; + + [self updateTargetting]; +} + + +- (void) performWitchspaceCountdownUpdates:(OOTimeDelta)delta_t +{ + [self doBookkeeping:delta_t]; + witchspaceCountdown -= delta_t; + if (witchspaceCountdown < 0.0f) witchspaceCountdown = 0.0f; + if (galactic_witchjump) + [UNIVERSE displayCountdownMessage:[NSString stringWithFormat:DESC(@"witch-galactic-in-f-seconds"), witchspaceCountdown] forCount:1.0]; + else + [UNIVERSE displayCountdownMessage:[NSString stringWithFormat:DESC(@"witch-to-@-in-f-seconds"), [UNIVERSE getSystemName:target_system_seed], witchspaceCountdown] forCount:1.0]; + if (witchspaceCountdown == 0.0f) + { + BOOL go = YES; + + // check nearby masses + ShipEntity* blocker = [UNIVERSE entityForUniversalID:[self checkShipsInVicinityForWitchJumpExit]]; + if (blocker) + { + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"witch-blocked-by-@"), [blocker name]] forCount: 4.5]; + [self playWitchjumpBlocked]; + [self setStatus:STATUS_IN_FLIGHT]; + [self doScriptEvent:@"playerJumpFailed" withArgument:@"blocked"]; + go = NO; + } + + // check max distance permitted + double jump_distance = 0.0; + if (!galactic_witchjump) + { + jump_distance = distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + if (jump_distance > 7.0) + { + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-too-far") forCount: 4.5]; + [self playWitchjumpDistanceTooGreat]; + [self setStatus:STATUS_IN_FLIGHT]; + [self doScriptEvent:@"playerJumpFailed" withArgument:@"too far"]; + go = NO; + } + } + + // check fuel level + double fuel_required = 10.0 * jump_distance; + if (galactic_witchjump) + fuel_required = 0.0; + if (fuel < fuel_required) + { + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-no-fuel") forCount: 4.5]; + [self playWitchjumpInsufficientFuel]; + [self setStatus:STATUS_IN_FLIGHT]; + [self doScriptEvent:@"playerJumpFailed" withArgument:@"insufficient fuel"]; + go = NO; + } + + if (go) + { + [self safeAllMissiles]; + [UNIVERSE setViewDirection:VIEW_FORWARD]; + currentWeaponFacing = VIEW_FORWARD; + if (galactic_witchjump) + [self enterGalacticWitchspace]; + else + [self enterWitchspace]; + } + } +} + + +- (void) performWitchspaceExitUpdates:(OOTimeDelta)delta_t +{ + if ([UNIVERSE breakPatternOver]) + { + // time to check the script! + [self checkScript]; + // next check in 10s + [self resetScriptTimer]; // reset the in-system timer + + // announce arrival + if ([UNIVERSE planet]) + [UNIVERSE addMessage:[NSString stringWithFormat:@" %@. ",[UNIVERSE getSystemName:system_seed]] forCount:3.0]; + else + [UNIVERSE addMessage:DESC(@"witch-engine-malfunction") forCount:3.0]; + + [self setStatus:STATUS_IN_FLIGHT]; + + // If we are exiting witchspace after a scripted misjump. then make sure it gets reset now. + // Scripted misjump situations should have a lifespan of one jump only, to keep things + // simple - Nikos 20090728 + if ([self scriptedMisjump]) [self setScriptedMisjump:NO]; + + [self doScriptEvent:@"shipExitedWitchspace"]; + suppressAegisMessages=NO; + } +} + + +- (void) performLaunchingUpdates:(OOTimeDelta)delta_t +{ + if ([UNIVERSE breakPatternOver]) + { + // time to check the script! + [self checkScript]; + // next check in 10s + + [self setStatus:STATUS_IN_FLIGHT]; + +#if DOCKING_CLEARANCE_ENABLED + [self setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; +#endif + [self doScriptEvent:@"shipLaunchedFromStation"]; + } +} + + +- (void) performDockingUpdates:(OOTimeDelta)delta_t +{ + if ([UNIVERSE breakPatternOver]) + { + [self docked]; // bookkeeping for docking + } +} + + +- (void) performDeadUpdates:(OOTimeDelta)delta_t +{ + if ([self shotTime] > 30.0) + { + BOOL was_mouse_control_on = mouse_control_on; + [UNIVERSE game_over]; // we restart the UNIVERSE + mouse_control_on = was_mouse_control_on; + } +} + + +// Target is valid if it's within Scanner range, AND +// Target is a ship AND is not cloaked or jamming, OR +// Target is a wormhole AND player has the Wormhole Scanner +- (BOOL)isValidTarget:(Entity*)target +{ + // Just in case we got called with a bad target. + if (!target) + return NO; + + // If target is beyond scanner range, it's lost + if(target->zero_distance > SCANNER_MAX_RANGE2) + return NO; + + // If target is a ship, check whether it's cloaked or is actively jamming our scanner + if ([target isShip]) + { + ShipEntity *targetShip = (ShipEntity*)target; + if ([targetShip isCloaked] || // checks for cloaked ships + ([targetShip isJammingScanning] && ![self hasMilitaryScannerFilter])) // checks for activated jammer + { + return NO; + } + return YES; + } + +#if WORMHOLE_SCANNER + // If target is an unexpired wormhole and the player has bought the Wormhole Scanner and we're in ID mode + if ([target isWormhole] && [target scanClass] != CLASS_NO_DRAW && + [self hasEquipmentItem:@"EQ_WORMHOLE_SCANNER"] && ident_engaged) + return YES; +#endif + + // Target is neither a wormhole nor a ship + return NO; +} + +// Check for lost targetting - both on the ships' main target as well as each +// missile. +// If we're actively scanning and we don't have a current target, then check +// to see if we've locked onto a new target. +// Finally, if we have a target and it's a wormhole, check whether we have more +// information +- (void) updateTargetting +{ + // check for lost ident target and ensure the ident system is actually scanning + if (ident_engaged && [self primaryTargetID] != NO_TARGET) + { + if (![self isValidTarget:[self primaryTarget]]) + { + if (!suppressTargetLost) + { + [UNIVERSE addMessage:DESC(@"target-lost") forCount:3.0]; + [self playTargetLost]; + } + else + { + suppressTargetLost = NO; + } + + primaryTarget = NO_TARGET; + } + } + + // check each unlaunched missile's target still exists and is in-range + if (missile_status != MISSILE_STATUS_SAFE) + { + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if ([missile_entity[i] primaryTargetID] != NO_TARGET && + ![self isValidTarget:[missile_entity[i] primaryTarget]]) + { + [UNIVERSE addMessage:DESC(@"target-lost") forCount:3.0]; + [self playTargetLost]; + [missile_entity[i] removeTarget:nil]; + if (i == activeMissile) + { + primaryTarget = NO_TARGET; + missile_status = MISSILE_STATUS_ARMED; + } + } + } + } + + // if we don't have a primary target, and we're scanning, then check for a new + // target to lock on to + if ([self primaryTargetID] == NO_TARGET && + (ident_engaged || missile_status != MISSILE_STATUS_SAFE) && + ([self status] == STATUS_IN_FLIGHT || [self status] == STATUS_WITCHSPACE_COUNTDOWN)) + { + Entity *target = [UNIVERSE getFirstEntityTargetedByPlayer]; + if ([self isValidTarget:target]) + { + [self addTarget:target]; + } + } + +#if WORMHOLE_SCANNER + // If our primary target is a wormhole, check to see if we have additional + // information + if ([[self primaryTarget] isWormhole]) + { + WormholeEntity *wh = [self primaryTarget]; + switch ([wh scanInfo]) + { + case WH_SCANINFO_NONE: + OOLog(kOOLogInconsistentState, @"Internal Error - WH_SCANINFO_NONE reached in [PlayerEntity updateTargeting:]"); + assert(NO); + break; + case WH_SCANINFO_SCANNED: + if ([self clockTimeAdjusted] > [wh scanTime] + 2) + { + [wh setScanInfo:WH_SCANINFO_COLLAPSE_TIME]; + //[UNIVERSE addCommsMessage:[NSString stringWithFormat:DESC(@"wormhole-collapse-time-computed"), + // [UNIVERSE getSystemName:[wh destination]]] forCount:5.0]; + } + break; + case WH_SCANINFO_COLLAPSE_TIME: + if([self clockTimeAdjusted] > [wh scanTime] + 4) + { + [wh setScanInfo:WH_SCANINFO_ARRIVAL_TIME]; + [UNIVERSE addCommsMessage:[NSString stringWithFormat:DESC(@"wormhole-arrival-time-computed-@"), + ClockToString([wh arrivalTime], NO)] forCount:5.0]; + } + break; + case WH_SCANINFO_ARRIVAL_TIME: + if ([self clockTimeAdjusted] > [wh scanTime] + 7) + { + [wh setScanInfo:WH_SCANINFO_DESTINATION]; + [UNIVERSE addCommsMessage:[NSString stringWithFormat:DESC(@"wormhole-destination-computed-@"), + [UNIVERSE getSystemName:[wh destination]]] forCount:5.0]; + } + break; + case WH_SCANINFO_DESTINATION: + if ([self clockTimeAdjusted] > [wh scanTime] + 10) + { + [wh setScanInfo:WH_SCANINFO_SHIP]; + // TODO: Extract last ship from wormhole and display its name + } + break; + case WH_SCANINFO_SHIP: + break; + } + } +#endif +} + + +- (void) orientationChanged +{ + quaternion_normalize(&orientation); + rotMatrix = OOMatrixForQuaternionRotation(orientation); + OOMatrixGetBasisVectors(rotMatrix, &v_right, &v_up, &v_forward); + + orientation.w = -orientation.w; + playerRotMatrix = OOMatrixForQuaternionRotation(orientation); // this is the rotation similar to ordinary ships + orientation.w = -orientation.w; +} + + +- (void) applyRoll:(GLfloat) roll1 andClimb:(GLfloat) climb1 +{ + if (roll1 == 0.0 && climb1 == 0.0 && hasRotated == NO) + return; + + if (roll1) + quaternion_rotate_about_z(&orientation, -roll1); + if (climb1) + quaternion_rotate_about_x(&orientation, -climb1); + + /* Bugginess may put us in a state where the orientation quat is all + zeros, at which point it’s impossible to move. + */ + if (EXPECT_NOT(quaternion_equal(orientation, kZeroQuaternion))) + { + if (!quaternion_equal(lastOrientation, kZeroQuaternion)) + { + orientation = lastOrientation; + } + else + { + orientation = kIdentityQuaternion; + } + } + + [self orientationChanged]; +} + +/* + * This method should not be necessary, but when I replaced the above with applyRoll:andClimb:andYaw, the + * ship went crazy. Perhaps applyRoll:andClimb is called from one of the subclasses and that was messing + * things up. + */ +- (void) applyYaw:(GLfloat) yaw +{ + quaternion_rotate_about_y(&orientation, -yaw); + + [self orientationChanged]; +} + + +- (OOMatrix) drawRotationMatrix // override to provide the 'correct' drawing matrix +{ + return playerRotMatrix; +} + + +- (Quaternion) normalOrientation +{ + return make_quaternion(-orientation.w, orientation.x, orientation.y, orientation.z); +} + + +- (void) setNormalOrientation:(Quaternion) quat +{ + [self setOrientation:make_quaternion(-quat.w, quat.x, quat.y, quat.z)]; +} + + +- (void) moveForward:(double) amount +{ + distanceTravelled += (float)amount; + position = vector_add(position, vector_multiply_scalar(v_forward, (float)amount)); +} + + +- (Vector) viewpointOffset +{ + if ([UNIVERSE breakPatternHide]) + return kZeroVector; // center view for break pattern + + switch ([UNIVERSE viewDirection]) + { + case VIEW_FORWARD: + return forwardViewOffset; + case VIEW_AFT: + return aftViewOffset; + case VIEW_PORT: + return portViewOffset; + case VIEW_STARBOARD: + return starboardViewOffset; + /* GILES custom viewpoints */ + case VIEW_CUSTOM: + return customViewOffset; + /* -- */ + + default: + break; + } + + return kZeroVector; +} + + +- (Vector) viewpointPosition +{ + Vector viewpoint = position; + Vector offset = [self viewpointOffset]; + + // FIXME: this ought to be done with matrix or quaternion functions. + OOMatrix r = rotMatrix; + + viewpoint.x += offset.x * r.m[0][0]; viewpoint.y += offset.x * r.m[1][0]; viewpoint.z += offset.x * r.m[2][0]; + viewpoint.x += offset.y * r.m[0][1]; viewpoint.y += offset.y * r.m[1][1]; viewpoint.z += offset.y * r.m[2][1]; + viewpoint.x += offset.z * r.m[0][2]; viewpoint.y += offset.z * r.m[1][2]; viewpoint.z += offset.z * r.m[2][2]; + + return viewpoint; +} + + +/* Return the current player-centric camera. + FIXME: this should store a set of cameras and return the current one. + Currently, it synthesizes a camera based on the various legacy things. +*/ +- (OOCamera *) currentCamera +{ + OOCamera *camera = nil; + Quaternion orient = kIdentityQuaternion; + + camera = [[OOCamera alloc] init]; + [camera autorelease]; + + [camera setPosition:[self viewpointPosition]]; + + /*switch ([UNIVERSE viewDirection]) + { + case VIEW_FORWARD: + case VIEW_NONE: + case VIEW_GUI_DISPLAY: + case VIEW_BREAK_PATTERN: + orient = kIdentityQuaternion; + break; + + case VIEW_AFT: + static const OOMatrix aft_matrix = + {{ + {-1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, -1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }}; + + }*/ + + [camera setOrientation:orient]; + + return camera; +} + + +- (void) drawEntity:(BOOL) immediate :(BOOL) translucent +{ + switch ([self status]) + { + case STATUS_DEAD: + case STATUS_COCKPIT_DISPLAY: + case STATUS_DOCKED: + case STATUS_START_GAME: + return; + + default: + if ([UNIVERSE breakPatternHide]) return; + } + + [super drawEntity:immediate :translucent]; +} + + +- (BOOL) massLocked +{ + return ((alertFlags & ALERT_FLAG_MASS_LOCK) != 0); +} + + +- (BOOL) atHyperspeed +{ + return travelling_at_hyperspeed; +} + + +- (Vector) velocityVector +{ + Vector result = v_forward; + result.x *= flightSpeed; + result.y *= flightSpeed; + result.z *= flightSpeed; + return result; +} + + +// dial routines = all return 0.0 .. 1.0 or -1.0 .. 1.0 + +- (NSString *) ship_desc +{ + return ship_desc; +} + +- (void) setDockedAtMainStation +{ + dockedStation = [UNIVERSE station]; + [self setStatus:STATUS_DOCKED]; +} + +- (StationEntity *) dockedStation +{ + return dockedStation; +} + +#if DOCKING_CLEARANCE_ENABLED +- (void) setTargetDockStationTo:(StationEntity *) value +{ + targetDockStation = value; +} + +- (StationEntity *) getTargetDockStation +{ + return targetDockStation; +} +#endif + +- (HeadUpDisplay *) hud +{ + return hud; +} + + +- (void) setShowDemoShips:(BOOL) value +{ + showDemoShips = value; +} + + +- (BOOL) showDemoShips +{ + return showDemoShips; +} + + +- (GLfloat) forwardShieldLevel +{ + return forward_shield; +} + + +- (GLfloat) aftShieldLevel +{ + return aft_shield; +} + + +- (void) setForwardShieldLevel:(GLfloat)level +{ + forward_shield = OOClamp_0_max_f(level, [self maxForwardShieldLevel]); +} + + +- (void) setAftShieldLevel:(GLfloat)level +{ + aft_shield = OOClamp_0_max_f(level, [self maxAftShieldLevel]); +} + + +- (GLfloat) dialRoll +{ + GLfloat result = flightRoll / max_flight_roll; + if ((result < 1.0f)&&(result > -1.0f)) + return result; + if (result > 0.0f) + return 1.0f; + return -1.0f; +} + + +- (GLfloat) dialPitch +{ + GLfloat result = flightPitch / max_flight_pitch; + if ((result < 1.0f)&&(result > -1.0f)) + return result; + if (result > 0.0f) + return 1.0f; + return -1.0f; +} + + +- (GLfloat) dialYaw +{ + GLfloat result = -flightYaw / max_flight_yaw; + if ((result < 1.0f)&&(result > -1.0f)) + return result; + if (result > 0.0f) + return 1.0f; + return -1.0f; +} + + +- (GLfloat) dialSpeed +{ + GLfloat result = flightSpeed / maxFlightSpeed; + if (result < 1.0f) + return result; + return 1.0f; +} + + +- (GLfloat) dialHyperSpeed +{ + return flightSpeed / maxFlightSpeed; +} + + +- (GLfloat) dialForwardShield +{ + GLfloat result = forward_shield / [self maxForwardShieldLevel]; + if (result < 1.0f) + return result; + return 1.0f; +} + + +- (GLfloat) dialAftShield +{ + GLfloat result = aft_shield / [self maxAftShieldLevel]; + if (result < 1.0f) + return result; + return 1.0f; +} + + +- (GLfloat) dialEnergy +{ + GLfloat result = energy / maxEnergy; + if (result < 1.0f) + return result; + return 1.0f; +} + + +- (GLfloat) dialMaxEnergy +{ + return maxEnergy; +} + + +- (GLfloat) dialFuel +{ + if (fuel <= 0.0f) + return 0.0f; + if (fuel > [self fuelCapacity]) + return 1.0f; + return (GLfloat)fuel / (GLfloat)[self fuelCapacity]; +} + + +- (GLfloat) dialHyperRange +{ + GLfloat distance = (float)distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + return 10.0f * distance / (GLfloat)PLAYER_MAX_FUEL; +} + + +- (GLfloat) hullHeatLevel +{ + GLfloat result = (GLfloat)ship_temperature / (GLfloat)SHIP_MAX_CABIN_TEMP; + if (result < 1.0) + return result; + return 1.0f; +} + + +- (GLfloat) laserHeatLevel +{ + GLfloat result = (GLfloat)weapon_temp / (GLfloat)PLAYER_MAX_WEAPON_TEMP; + return OOClamp_0_1_f(result); +} + + +- (GLfloat) dialAltitude +{ + if ([self isDocked]) return 0.0f; + + // find nearest planet type entity... + assert(UNIVERSE != nil); + + PlanetEntity *nearestPlanet = [self findNearestStellarBody]; + if (nearestPlanet == nil) return 1.0f; + + GLfloat zd = nearestPlanet->zero_distance; + GLfloat cr = nearestPlanet->collision_radius; + GLfloat alt = sqrtf(zd) - cr; + + return OOClamp_0_1_f(alt / (GLfloat)PLAYER_DIAL_MAX_ALTITUDE); +} + + +- (double) clockTime +{ + return ship_clock; +} + +- (double) clockTimeAdjusted +{ + return ship_clock + ship_clock_adjust; +} + + +- (BOOL) clockAdjusting +{ + return ship_clock_adjust != 0; +} + + +- (NSString*) dial_clock +{ + return ClockToString(ship_clock, ship_clock_adjust != 0); +} + + +- (NSString*) dial_clock_adjusted +{ + return ClockToString(ship_clock + ship_clock_adjust, NO); +} + + +- (NSString*) dial_fpsinfo +{ + unsigned fpsVal = fps_counter; + return [NSString stringWithFormat:@"FPS: %3d", fpsVal]; +} + + +- (NSString*) dial_objinfo +{ + return [NSString stringWithFormat:@"Objs: %3d", [UNIVERSE obj_count]]; +} + + +- (unsigned) countMissiles +{ + unsigned n_missiles = 0; + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if (missile_entity[i]) + n_missiles++; + } + return n_missiles; +} + + +- (OOMissileStatus) dialMissileStatus +{ + return missile_status; +} + +- (BOOL) canScoop:(ShipEntity*)other +{ + if (specialCargo) return NO; + return [super canScoop:other]; +} + +- (int) dialFuelScoopStatus +{ + if ([self hasScoop]) + { + if (scoopsActive) + return SCOOP_STATUS_ACTIVE; + if ([cargo count] >= max_cargo) + return SCOOP_STATUS_FULL_HOLD; + return SCOOP_STATUS_OKAY; + } + else + return SCOOP_STATUS_NOT_INSTALLED; +} + + +- (NSMutableArray*) commLog +{ + unsigned count; + + assert(kCommLogTrimSize < kCommLogTrimThreshold); + + if (commLog != nil) + { + count = [commLog count]; + if (count >= kCommLogTrimThreshold) + { + [commLog removeObjectsInRange:NSMakeRange(0, count - kCommLogTrimSize)]; + } + } + else + { + commLog = [[NSMutableArray alloc] init]; + } + + return commLog; +} + + +- (OOCompassMode) compassMode +{ + return compassMode; +} + + +- (void) setCompassMode:(OOCompassMode) value +{ + compassMode = value; +} + + +- (void) setNextCompassMode +{ + switch (compassMode) + { + case COMPASS_MODE_BASIC: + case COMPASS_MODE_PLANET: + if ([self checkForAegis] == AEGIS_NONE) + [self setCompassMode:COMPASS_MODE_SUN]; + else + [self setCompassMode:COMPASS_MODE_STATION]; + break; + case COMPASS_MODE_STATION: + [self setCompassMode:COMPASS_MODE_SUN]; + break; + case COMPASS_MODE_SUN: + if ([self primaryTarget]) + [self setCompassMode:COMPASS_MODE_TARGET]; + else + { + nextBeaconID = [[UNIVERSE firstBeacon] universalID]; + while ((nextBeaconID != NO_TARGET)&&[[UNIVERSE entityForUniversalID:nextBeaconID] isJammingScanning]) + { + nextBeaconID = [[UNIVERSE entityForUniversalID:nextBeaconID] nextBeaconID]; + } + + if (nextBeaconID != NO_TARGET) + [self setCompassMode:COMPASS_MODE_BEACONS]; + else + [self setCompassMode:COMPASS_MODE_PLANET]; + } + break; + case COMPASS_MODE_TARGET: + nextBeaconID = [[UNIVERSE firstBeacon] universalID]; + while ((nextBeaconID != NO_TARGET)&&[[UNIVERSE entityForUniversalID:nextBeaconID] isJammingScanning]) + { + nextBeaconID = [[UNIVERSE entityForUniversalID:nextBeaconID] nextBeaconID]; + } + + if (nextBeaconID != NO_TARGET) + [self setCompassMode:COMPASS_MODE_BEACONS]; + else + [self setCompassMode:COMPASS_MODE_PLANET]; + break; + case COMPASS_MODE_BEACONS: + do + { + nextBeaconID = [[UNIVERSE entityForUniversalID:nextBeaconID] nextBeaconID]; + } while ((nextBeaconID != NO_TARGET)&&[[UNIVERSE entityForUniversalID:nextBeaconID] isJammingScanning]); + + if (nextBeaconID == NO_TARGET) + [self setCompassMode:COMPASS_MODE_PLANET]; + break; + } +} + + +- (unsigned) activeMissile +{ + return activeMissile; +} + + +- (void) setActiveMissile: (unsigned) value +{ + activeMissile = value; +} + + +- (unsigned) dialMaxMissiles +{ + return max_missiles; +} + + +- (BOOL) dialIdentEngaged +{ + return ident_engaged; +} + + +- (NSString *) specialCargo +{ + return specialCargo; +} + + +- (NSString *) dialTargetName +{ + Entity* target_entity = [UNIVERSE entityForUniversalID:primaryTarget]; + if (!target_entity) + return DESC(@"no-target-string"); + if ([target_entity isShip]) + return [(ShipEntity*)target_entity identFromShip:self]; +#if WORMHOLE_SCANNER + if ([target_entity isWormhole]) + return [(WormholeEntity*)target_entity identFromShip:self]; +#endif + + return DESC(@"unknown-target"); +} + + +- (ShipEntity *) missileForStation: (unsigned) value +{ + if (value < max_missiles) return missile_entity[value]; + return nil; +} + + +- (void) sortMissiles +{ + // puts all missiles into the first available slots + + unsigned i; + missiles = [self countMissiles]; + for (i = 0; i < missiles; i++) + { + if (missile_entity[i] == nil) + { + unsigned j; + for (j = i + 1; j < max_missiles; j++) + { + if (missile_entity[j]) + { + missile_entity[i] = missile_entity[j]; + missile_entity[j] = nil; + j = max_missiles; + } + } + } + } +} + + +- (void) safeAllMissiles +{ + // sets all missile targets to NO_TARGET + + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if (missile_entity[i]) + [missile_entity[i] removeTarget:nil]; + } + missile_status = MISSILE_STATUS_SAFE; +} + + +- (void) tidyMissilePylons +{ + // Shuffle missiles up so there's: + // no gaps between missiles + // the first missile is in the first pylon + int i; + int pylon=0; + for(i = 0; i < SHIPENTITY_MAX_MISSILES; i++) + { + if(missile_entity[i]) + { + missile_entity[pylon]=missile_entity[i]; + pylon++; + } + } + + // missiles have been shoved up, now make sure the remainder + // of the pylons are cleaned up. + for(i = pylon; i < SHIPENTITY_MAX_MISSILES; i++) + { + missile_entity[i]=nil; + } +} + + +- (void) selectNextMissile +{ + unsigned i; + for (i = 1; i < max_missiles; i++) + { + int next_missile = (activeMissile + i) % max_missiles; + if (missile_entity[next_missile]) + { + // If we don't have the multi-targetting module installed, clear the active missiles' target + if( ![self hasEquipmentItem:@"EQ_MULTI_TARGET"] && [missile_entity[activeMissile] isMissile] ) + { + [missile_entity[activeMissile] removeTarget:nil]; + } + + // Set next missile to active + [self setActiveMissile:next_missile]; + + if (missile_status != MISSILE_STATUS_SAFE) + { + missile_status = MISSILE_STATUS_ARMED; + + // If the newly active pylon contains a missile then work out its target, if any + if( [missile_entity[activeMissile] isMissile] ) + { + if( [self hasEquipmentItem:@"EQ_MULTI_TARGET"] && + ([missile_entity[next_missile] primaryTargetID] != NO_TARGET)) + { + // copy the missile's target + [self addTarget:[missile_entity[next_missile] primaryTarget]]; + missile_status = MISSILE_STATUS_TARGET_LOCKED; + } + else if ([self primaryTargetID] != NO_TARGET) + { + [missile_entity[activeMissile] addTarget:[self primaryTarget]]; + missile_status = MISSILE_STATUS_TARGET_LOCKED; + } + } + } + return; + } + } +} + + +- (void) clearAlertFlags +{ + alertFlags = 0; +} + + +- (int) alertFlags +{ + return alertFlags; +} + + +- (void) setAlertFlag:(int)flag to:(BOOL)value +{ + if (value) + { + alertFlags |= flag; + } + else + { + int comp = ~flag; + alertFlags &= comp; + } +} + + +- (OOAlertCondition) alertCondition +{ + OOAlertCondition old_alert_condition = alertCondition; + alertCondition = ALERT_CONDITION_GREEN; + + [self setAlertFlag:ALERT_FLAG_DOCKED to:[self status] == STATUS_DOCKED]; + + if (alertFlags & ALERT_FLAG_DOCKED) + { + alertCondition = ALERT_CONDITION_DOCKED; + } + else + { + if (alertFlags != 0) + alertCondition = ALERT_CONDITION_YELLOW; + if (alertFlags > ALERT_FLAG_YELLOW_LIMIT) + alertCondition = ALERT_CONDITION_RED; + } + if ((alertCondition == ALERT_CONDITION_RED)&&(old_alert_condition < ALERT_CONDITION_RED)) + { + [self playAlertConditionRed]; + } + + return alertCondition; +} + +///////////////////////////////////////////////////////////////////// + + +- (void) interpretAIMessage:(NSString *)ms +{ + if ([ms isEqual:@"HOLD_FULL"]) + { + [self playHoldFull]; + [UNIVERSE addMessage:DESC(@"hold-full") forCount:4.5]; + } + + if ([ms isEqual:@"INCOMING_MISSILE"]) + { + [self playIncomingMissile]; + [UNIVERSE addMessage:DESC(@"incoming-missile") forCount:4.5]; + } + + if ([ms isEqual:@"ENERGY_LOW"]) + { + [UNIVERSE addMessage:DESC(@"energy-low") forCount:6.0]; + } + + if ([ms isEqual:@"ECM"]) [self playHitByECMSound]; + + if ([ms isEqual:@"DOCKING_REFUSED"] && [self status] == STATUS_AUTOPILOT_ENGAGED) + { + [self playDockingDenied]; + [UNIVERSE addMessage:DESC(@"autopilot-denied") forCount:4.5]; + autopilot_engaged = NO; + primaryTarget = NO_TARGET; + [self setStatus:STATUS_IN_FLIGHT]; + [[OOMusicController sharedController] stopDockingMusic]; + [self doScriptEvent:@"playerDockingRefused"]; + } + + // aegis messages to advanced compass so in planet mode it behaves like the old compass + if (compassMode != COMPASS_MODE_BASIC) + { + if ([ms isEqual:@"AEGIS_CLOSE_TO_MAIN_PLANET"]&&(compassMode == COMPASS_MODE_PLANET)) + { + [self playAegisCloseToPlanet]; + [self setCompassMode:COMPASS_MODE_STATION]; + } + if ([ms isEqual:@"AEGIS_IN_DOCKING_RANGE"]&&(compassMode == COMPASS_MODE_PLANET)) + { + [self playAegisCloseToStation]; + [self setCompassMode:COMPASS_MODE_STATION]; + } + if ([ms isEqual:@"AEGIS_NONE"]&&(compassMode == COMPASS_MODE_STATION)) + { + [self setCompassMode:COMPASS_MODE_PLANET]; + } + } +} + + +- (BOOL) mountMissile: (ShipEntity *)missile +{ + if (missile == nil) return NO; + + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if (missile_entity[i] == nil) + { + missile_entity[i] = [missile retain]; + missiles = [self countMissiles]; + if (missiles == 1) [self selectNextMissile]; + return YES; + } + } + missiles = [self countMissiles]; + return NO; +} + + +- (BOOL) fireMissile +{ + ShipEntity *missile = missile_entity[activeMissile]; // retain count is 1 + + if (missile == nil) + return NO; + + double mcr = missile->collision_radius; + + if ([missile isMine]&&(missile_status != MISSILE_STATUS_SAFE)) + { + BOOL launchedOK = [self launchMine:missile]; + if (launchedOK) + { + [self playMineLaunched]; + [missile release]; // release + } + missile_entity[activeMissile] = nil; + [self selectNextMissile]; + missiles = [self countMissiles]; + return launchedOK; + } + + if (missile_status != MISSILE_STATUS_TARGET_LOCKED) + return NO; + + Vector vel; + Vector origin = position; + Vector start, v_eject; + + // default launching position + start.x = 0.0f; // in the middle + start.y = boundingBox.min.y - 4.0f; // 4m below bounding box + start.z = boundingBox.max.z + 1.0f; // 1m ahead of bounding box + // custom launching position + ScanVectorFromString([shipinfoDictionary objectForKey:@"missile_launch_position"], &start); + + float throw_speed = 250.0f; + Quaternion q1 = orientation; + q1.w = -q1.w; // player view is reversed remember! + + Entity *target = [self primaryTarget]; + + // select a new active missile and decrease the missiles count + missile_entity[activeMissile] = nil; + [self selectNextMissile]; + missiles = [self countMissiles]; + + + v_eject = vector_normal(start); + + // check if start is within bounding box... + while ( (start.x > boundingBox.min.x - mcr)&&(start.x < boundingBox.max.x + mcr)&& + (start.y > boundingBox.min.y - mcr)&&(start.y < boundingBox.max.y + mcr)&& + (start.z > boundingBox.min.z - mcr)&&(start.z < boundingBox.max.z + mcr)) + { + start.x += (float)mcr * v_eject.x; start.y += (float)mcr * v_eject.y; start.z += (float)mcr * v_eject.z; + } + + vel = vector_multiply_scalar(v_forward, flightSpeed + throw_speed); + + origin.x = position.x + v_right.x * start.x + v_up.x * start.y + v_forward.x * start.z; + origin.y = position.y + v_right.y * start.x + v_up.y * start.y + v_forward.y * start.z; + origin.z = position.z + v_right.z * start.x + v_up.z * start.y + v_forward.z * start.z; + + [missile setPosition:origin]; + [missile setScanClass: CLASS_MISSILE]; + [missile addTarget:target]; + [missile setOrientation:q1]; + [missile setStatus: STATUS_IN_FLIGHT]; // necessary to get it going! + [missile setIsMissileFlag:YES]; + [missile setVelocity: vel]; + [missile setSpeed:150.0]; + [missile setOwner:self]; + [missile setDistanceTravelled:0.0f]; + [missile resetShotTime]; + + [UNIVERSE addEntity:missile]; + [missile release]; + + if ([target isShip]) + { + ShipEntity *targetShip = (ShipEntity*)target; + [targetShip setPrimaryAggressor:self]; + [targetShip doScriptEvent:@"shipAttackedWithMissile" withArgument:missile andArgument:self]; + [targetShip reactToAIMessage:@"INCOMING_MISSILE"]; + } + + [self playMissileLaunched]; + + if (cloaking_device_active && cloakPassive) + { + [self deactivateCloakingDevice]; + [UNIVERSE addMessage:DESC(@"cloak-off") forCount:2]; + } + + return YES; +} + + +- (BOOL) launchMine:(ShipEntity*) mine +{ + if (!mine) + return NO; + float mine_speed = 500.0f; + [self dumpItem: mine]; + Vector mvel = [mine velocity]; + mvel.x -= mine_speed * v_forward.x; + mvel.y -= mine_speed * v_forward.y; + mvel.z -= mine_speed * v_forward.z; + [mine setVelocity: mvel]; + [mine setScanClass: CLASS_MINE]; + [mine setStatus: STATUS_IN_FLIGHT]; + [mine setBehaviour: BEHAVIOUR_IDLE]; + [mine setOwner: self]; + [[mine getAI] setState:@"GLOBAL"]; // start the timer !!!! + return YES; +} + + +- (BOOL) fireECM +{ + if ([super fireECM]) + { + ecm_in_operation = YES; + ecm_start_time = [UNIVERSE getTime]; + return YES; + } + else + { + return NO; + } +} + + +- (OOEnergyUnitType) installedEnergyUnitType +{ + if ([self hasEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT"]) return ENERGY_UNIT_NAVAL; + if ([self hasEquipmentItem:@"EQ_ENERGY_UNIT"]) return ENERGY_UNIT_NORMAL; + return ENERGY_UNIT_NONE; +} + +- (OOEnergyUnitType) energyUnitType +{ + if ([self hasEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT"]) return ENERGY_UNIT_NAVAL; + if ([self hasEquipmentItem:@"EQ_ENERGY_UNIT"]) return ENERGY_UNIT_NORMAL; + if ([self hasEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT_DAMAGED"]) return ENERGY_UNIT_NAVAL_DAMAGED; + if ([self hasEquipmentItem:@"EQ_ENERGY_UNIT_DAMAGED"]) return ENERGY_UNIT_NORMAL_DAMAGED; + return ENERGY_UNIT_NONE; +} + +- (float) heatInsulation +{ + return [self hasHeatShield] ? 2.0f : 1.0f; +} + + +- (BOOL) fireEnergyBomb +{ + NSArray* targets = [UNIVERSE getEntitiesWithinRange:SCANNER_MAX_RANGE ofEntity:self]; + if ([targets count] > 0) + { + unsigned i; + for (i = 0; i < [targets count]; i++) + { + Entity *e2 = [targets objectAtIndex:i]; + if (e2->isShip) + [(ShipEntity *)e2 takeEnergyDamage:1000 from:self becauseOf:self]; + } + } + [UNIVERSE addMessage:DESC(@"energy-bomb-activated") forCount:4.5]; + [self playEnergyBombFired]; + + return YES; +} + + +- (BOOL) fireMainWeapon +{ + int weapon_to_be_fired = [self weaponForView: currentWeaponFacing]; + + if (weapon_temp / PLAYER_MAX_WEAPON_TEMP >= 0.85) + { + [self playWeaponOverheated]; + [UNIVERSE addMessage:DESC(@"weapon-overheat") forCount:3.0]; + return NO; + } + + if (weapon_to_be_fired == WEAPON_NONE) + return NO; + + switch (weapon_to_be_fired) + { + case WEAPON_PLASMA_CANNON : + weapon_energy = 6.0f; + weapon_energy_per_shot = 6.0f; + weapon_heat_increment_per_shot = 8.0f; + weapon_reload_time = 0.25f; + weaponRange = 5000; + break; + case WEAPON_PULSE_LASER : + weapon_energy = 15.0f; + weapon_energy_per_shot = 1.0f; + weapon_heat_increment_per_shot = 8.0f; + weapon_reload_time = 0.5f; + weaponRange = 12500; + break; + case WEAPON_BEAM_LASER : + weapon_energy = 15.0f; + weapon_energy_per_shot = 1.0f; + weapon_heat_increment_per_shot = 8.0f; + weapon_reload_time = 0.1f; + weaponRange = 15000; + break; + case WEAPON_MINING_LASER : + weapon_energy = 50.0f; + weapon_energy_per_shot = 1.0f; + weapon_heat_increment_per_shot = 8.0f; + weapon_reload_time = 2.5f; + weaponRange = 12500; + break; + case WEAPON_THARGOID_LASER : + case WEAPON_MILITARY_LASER : + weapon_energy = 23.0f; + weapon_energy_per_shot = 1.0f; + weapon_heat_increment_per_shot = 8.0f; + weapon_reload_time = 0.1f; + weaponRange = 30000; + break; + } + + if (energy <= weapon_energy_per_shot) + { + [UNIVERSE addMessage:DESC(@"weapon-out-of-juice") forCount:3.0]; + return NO; + } + + using_mining_laser = (weapon_to_be_fired == WEAPON_MINING_LASER); + + energy -= weapon_energy_per_shot; + + switch (currentWeaponFacing) + { + case VIEW_GUI_DISPLAY: + case VIEW_NONE: + case VIEW_BREAK_PATTERN: + case VIEW_FORWARD: + forward_weapon_temp += weapon_heat_increment_per_shot; + break; + case VIEW_AFT: + aft_weapon_temp += weapon_heat_increment_per_shot; + break; + case VIEW_PORT: + port_weapon_temp += weapon_heat_increment_per_shot; + break; + case VIEW_STARBOARD: + starboard_weapon_temp += weapon_heat_increment_per_shot; + break; + case VIEW_CUSTOM: + break; + } + + BOOL weaponFired = NO; + switch (weapon_to_be_fired) + { + case WEAPON_PLASMA_CANNON : + [self firePlasmaShot:10.0:1000.0:[OOColor greenColor]]; + weaponFired = YES; + break; + + case WEAPON_PULSE_LASER: + case WEAPON_BEAM_LASER: + case WEAPON_MINING_LASER: + case WEAPON_MILITARY_LASER: + [self fireLaserShotInDirection: currentWeaponFacing]; + weaponFired = YES; + break; + + case WEAPON_THARGOID_LASER: + break; + } + + if (weaponFired && cloaking_device_active && cloakPassive) + { + [self deactivateCloakingDevice]; + [UNIVERSE addMessage:DESC(@"cloak-off") forCount:2]; + } + + if (weaponFired) + return YES; + else + return NO; +} + + +- (OOWeaponType) weaponForView:(OOViewID)view +{ + if (view == VIEW_CUSTOM) + view = currentWeaponFacing; + + switch (view) + { + case VIEW_PORT : + return port_weapon; + case VIEW_STARBOARD : + return starboard_weapon; + case VIEW_AFT : + return aft_weapon; + case VIEW_FORWARD : + return forward_weapon; + default : + return WEAPON_NONE; + } +} + + +- (void) takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other +{ + Vector rel_pos; + double d_forward; + BOOL internal_damage = NO; // base chance + + OOLog(@"player.ship.damage", @"Player took damage from %@ becauseOf %@", ent, other); + + if ([self status] == STATUS_DEAD) return; + if (amount == 0.0) return; + + [[ent retain] autorelease]; + [[other retain] autorelease]; + + rel_pos = (ent != nil) ? [ent position] : kZeroVector; + rel_pos = vector_subtract(rel_pos, position); + + [self doScriptEvent:@"shipBeingAttacked" withArgument:ent]; + + d_forward = dot_product(rel_pos, v_forward); + + [self playShieldHit]; + + // firing on an innocent ship is an offence + if ((other)&&(other->isShip)) + { + [self broadcastHitByLaserFrom:(ShipEntity*) other]; + } + + if (d_forward >= 0) + { + forward_shield -= (float)amount; + if (forward_shield < 0.0) + { + amount = -forward_shield; + forward_shield = 0.0f; + } + else + { + amount = 0.0; + } + } + else + { + aft_shield -= (float)amount; + if (aft_shield < 0.0) + { + amount = -aft_shield; + aft_shield = 0.0f; + } + else + { + amount = 0.0; + } + } + + if (amount > 0.0) + { + internal_damage = ((ranrot_rand() & PLAYER_INTERNAL_DAMAGE_FACTOR) < amount); // base chance of damage to systems + energy -= (float)amount; + [self playDirectHit]; + ship_temperature += ((float)amount / [self heatInsulation]); + } + + if (energy <= 0.0) //use normal ship temperature calculations for heat damage + { + if ([other isShip]) + { + [(ShipEntity *)other noteTargetDestroyed:self]; + } + + [self getDestroyedBy:other context:@"energy damage"]; + } + else + { + if (internal_damage) [self takeInternalDamage]; + } +} + + +- (void) takeScrapeDamage:(double) amount from:(Entity *) ent +{ + Vector rel_pos; + double d_forward; + BOOL internal_damage = NO; // base chance + if (amount < 0) + { + OOLog(@"player.ship.damage", @"Player took negative scrape damage %.3f so we made it positive", amount); + amount = -amount; + } + OOLog(@"player.ship.damage", @"Player took %.3f scrape damage from %@", amount, ent); + + if ([self status] == STATUS_DEAD) + return; + + [[ent retain] autorelease]; + rel_pos = ent ? [ent position] : kZeroVector; + rel_pos = vector_subtract(rel_pos, position); + d_forward = dot_product(rel_pos, v_forward); + + [self playScrapeDamage]; + if (d_forward >= 0) + { + forward_shield -= amount; + if (forward_shield < 0.0) + { + amount = -forward_shield; + forward_shield = 0.0f; + } + else + { + amount = 0.0; + } + } + else + { + aft_shield -= amount; + if (aft_shield < 0.0) + { + amount = -aft_shield; + aft_shield = 0.0f; + } + else + { + amount = 0.0; + } + } + + if (amount) + { + internal_damage = ((ranrot_rand() & PLAYER_INTERNAL_DAMAGE_FACTOR) < amount); // base chance of damage to systems + } + + energy -= amount; + if (energy <= 0.0) + { + if ([ent isShip]) + { + [(ShipEntity *)ent noteTargetDestroyed:self]; + } + + [self getDestroyedBy:ent context:@"scrape damage"]; + } + + if (internal_damage) + { + [self takeInternalDamage]; + } +} + + +- (void) takeHeatDamage:(double) amount +{ + if ([self status] == STATUS_DEAD) // it's too late for this one! + return; + + if (amount < 0.0) + return; + + // hit the shields first! + + float fwd_amount = (float)(0.5 * amount); + float aft_amount = (float)(0.5 * amount); + + forward_shield -= fwd_amount; + if (forward_shield < 0.0) + { + fwd_amount = -forward_shield; + forward_shield = 0.0f; + } + else + fwd_amount = 0.0f; + + aft_shield -= aft_amount; + if (aft_shield < 0.0) + { + aft_amount = -aft_shield; + aft_shield = 0.0f; + } + else + aft_amount = 0.0f; + + double residual_amount = fwd_amount + aft_amount; + if (residual_amount <= 0.0) + return; + + energy -= (float)residual_amount; + + throw_sparks = YES; + + // oops we're burning up! + if (energy <= 0.0) + { + [self getDestroyedBy:nil context:@"heat damage"]; + } + else + { + // warn if I'm low on energy + if (energy < maxEnergy *0.25) + [shipAI message:@"ENERGY_LOW"]; + } +} + + +- (OOUniversalID)launchEscapeCapsule +{ + ShipEntity *doppelganger = nil; + ShipEntity *escapePod = nil; + Vector vel; + Vector origin = position; + int result = NO; + Quaternion q1 = orientation; + + [self setStatus:STATUS_ESCAPE_SEQUENCE]; // firstly + ship_clock_adjust += 43200 + 5400 * (ranrot_rand() & 127); // add up to 8 days until rescue! +#if DOCKING_CLEARANCE_ENABLED + dockingClearanceStatus = DOCKING_CLEARANCE_STATUS_NOT_REQUIRED; +#endif + + q1.w = -q1.w; // player view is reversed remember! + + flightSpeed = OOMax_f(flightSpeed, 50.0f); + vel = vector_multiply_scalar(v_forward, flightSpeed); + + doppelganger = [UNIVERSE newShipWithName: ship_desc]; // retain count = 1 + if (doppelganger) + { + [doppelganger setPosition:origin]; // directly below + [doppelganger setScanClass:CLASS_NEUTRAL]; + [doppelganger setOrientation:q1]; + [doppelganger setVelocity:vel]; + [doppelganger setSpeed:flightSpeed]; + [doppelganger setRoll:0.2 * (randf() - 0.5)]; + [doppelganger setDesiredSpeed:flightSpeed]; + [doppelganger setOwner:self]; + [doppelganger setStatus:STATUS_IN_FLIGHT]; // necessary to get it going! + [doppelganger setBehaviour:BEHAVIOUR_IDLE]; + + [UNIVERSE addEntity:doppelganger]; + + [[doppelganger getAI] setStateMachine:@"nullAI.plist"]; // fly straight on + + result = [doppelganger universalID]; + + [doppelganger release]; //release + } + + // set up you + escapePod = [UNIVERSE newShipWithName:@"escape-capsule"]; // retained + if (escapePod != nil) + { + // FIXME: this should use OOShipType, which should exist. -- Ahruman + [self setMesh:[escapePod mesh]]; + [escapePod release]; + } + + [UNIVERSE setViewDirection:VIEW_FORWARD]; + flightSpeed = 1.0f; + flightPitch = 0.2f * (randf() - 0.5f); + flightRoll = 0.2f * (randf() - 0.5f); + + float sheight = (float)(boundingBox.max.y - boundingBox.min.y); + position = vector_subtract(position, vector_multiply_scalar(v_up, sheight)); + + //remove escape pod + [self removeEquipmentItem:@"EQ_ESCAPE_POD"]; + + [self doScriptEvent:@"shipLaunchedEscapePod" withArgument:escapePod]; + + // reset legal status + legalStatus = 0; + bounty = 0; + + // reset trumbles + if (trumbleCount != 0) trumbleCount = 1; + + // remove cargo + [cargo removeAllObjects]; + + energy = 25; + [UNIVERSE addMessage:DESC(@"escape-sequence") forCount:4.5]; + [self resetShotTime]; + + return result; +} + + +- (OOCargoType) dumpCargo +{ + if (flightSpeed > 4.0 * maxFlightSpeed) + { + [UNIVERSE addMessage:DESC(@"hold-locked") forCount:3.0]; + return CARGO_NOT_CARGO; + } + + int result = [super dumpCargo]; + if (result != CARGO_NOT_CARGO) + { + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"@-ejected") ,[UNIVERSE displayNameForCommodity:result]] forCount:3.0]; + [self playCargoJettisioned]; + } + return result; +} + + +- (void) rotateCargo +{ + int n_cargo = [cargo count]; + if (n_cargo == 0) + return; + ShipEntity* pod = (ShipEntity*)[[cargo objectAtIndex:0] retain]; + int current_contents = [pod commodityType]; + int contents = [pod commodityType]; + int rotates = 0; + do { + [cargo removeObjectAtIndex:0]; // take it from the eject position + [cargo addObject:pod]; // move it to the last position + [pod release]; + pod = (ShipEntity*)[[cargo objectAtIndex:0] retain]; + contents = [pod commodityType]; + rotates++; + } while ((contents == current_contents)&&(rotates < n_cargo)); + [pod release]; + if (contents != CARGO_NOT_CARGO) + { + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"@-ready-to-eject"), [UNIVERSE displayNameForCommodity:contents]] forCount:3.0]; + } + else + { + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"ready-to-eject-@") ,[pod name]] forCount:3.0]; + } + // now scan through the remaining 1..(n_cargo - rotates) places moving similar cargo to the last place + // this means the cargo gets to be sorted as it is rotated through + int i; + for (i = 1; i < (n_cargo - rotates); i++) + { + pod = [cargo objectAtIndex:i]; + if ([pod commodityType] == current_contents) + { + [pod retain]; + [cargo removeObjectAtIndex:i--]; + [cargo addObject:pod]; + [pod release]; + rotates++; + } + } +} + + +- (void) setBounty:(OOCreditsQuantity) amount +{ + legalStatus = (int)amount; +} + + +- (OOCreditsQuantity) bounty // overrides returning 'bounty' +{ + return legalStatus; +} + + +- (int) legalStatus +{ + return legalStatus; +} + + +- (void) markAsOffender:(int)offence_value +{ + legalStatus |= offence_value; +} + + +- (void) collectBountyFor:(ShipEntity *)other +{ + if (other == nil || [other isSubEntity]) return; + + OOCreditsQuantity score = 10 * [other bounty]; + OOScanClass killClass = [other scanClass]; // **tgape** change (+line) + BOOL killAward = YES; + + if ([other isPolice]) // oops, we shot a copper! + legalStatus |= 64; + + if (![UNIVERSE strict]) // only mess with the scores if we're not in 'strict' mode + { + BOOL killIsCargo = ((killClass == CLASS_CARGO) && ([other commodityAmount] > 0)); + if ((killIsCargo) || (killClass == CLASS_BUOY) || (killClass == CLASS_ROCK)) + { + if (![other hasRole:@"tharglet"]) // okay, we'll count tharglets as proper kills + { + score /= 10; // reduce bounty awarded + killAward = NO; // don't award a kill + } + } + } + + credits += score; + + if (score > 9) + { + NSString *bonusMsg = [NSString stringWithFormat:DESC(@"bounty-@-total-@"), OOCredits(score), OOCredits(credits)]; + + [UNIVERSE addDelayedMessage:bonusMsg forCount:6 afterDelay:0.15]; + } + + if (killAward) + { + ship_kills++; + if ((ship_kills % 256) == 0) + { + // congratulations method needs to be delayed a fraction of a second + [UNIVERSE addDelayedMessage:DESC(@"right-on-commander") forCount:4 afterDelay:0.2]; + } + } +} + + +- (void) takeInternalDamage +{ + unsigned n_cargo = max_cargo; + unsigned n_mass = [self mass] / 10000; + unsigned n_considered = n_cargo + n_mass; + unsigned damage_to = n_considered ? (ranrot_rand() % n_considered) : 0; // n_considered can be 0 for small ships. + // cargo damage + if (damage_to < [cargo count]) + { + ShipEntity* pod = (ShipEntity*)[cargo objectAtIndex:damage_to]; + NSString* cargo_desc = [UNIVERSE displayNameForCommodity:[pod commodityType]]; + if (!cargo_desc) + return; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"@-destroyed"), cargo_desc] forCount:4.5]; + [cargo removeObject:pod]; + return; + } + else + { + damage_to = n_considered - (damage_to + 1); // reverse the die-roll + } + // equipment damage + if (damage_to < [self equipmentCount]) + { + NSArray *systems = [[self equipmentEnumerator] allObjects]; + NSString *system_key = [systems objectAtIndex:damage_to]; + NSString *system_name = nil; + + if ([system_key hasSuffix:@"MISSILE"] || [system_key hasSuffix:@"MINE"] || [system_key isEqual:@"EQ_CARGO_BAY"]) return; + + system_name = [[OOEquipmentType equipmentTypeWithIdentifier:system_key] name]; + if (system_name == nil) return; + + // set the following so removeEquipment works on the right entity + [self setScriptTarget:self]; + [UNIVERSE clearPreviousMessage]; + [self removeEquipmentItem:system_key]; + if (![UNIVERSE strict]) + { + [self addEquipmentItem:[NSString stringWithFormat:@"%@_DAMAGED", system_key]]; // for possible future repair + [self doScriptEvent:@"equipmentDamaged" withArgument:system_key]; + if (![self hasEquipmentItem:system_name]) // Because script may have undestroyed it + { + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"@-damaged"), system_name] forCount:4.5]; + } + } + else + { + [self doScriptEvent:@"equipmentDestroyed" withArgument:system_key]; + if (![self hasEquipmentItem:system_name]) // Because script may have undestroyed it + { + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"@-destroyed"), system_name] forCount:4.5]; + } + } + return; + } + //cosmetic damage + if (((damage_to & 7) == 7)&&(ship_trade_in_factor > 75)) + ship_trade_in_factor--; +} + + +- (void) getDestroyedBy:(Entity *)whom context:(NSString *)why +{ + + OOLog(@"player.ship.damage", @"Player destroyed by %@ due to %@", whom, why); + NSString *scoreMS = [NSString stringWithFormat:DESC(@"gameoverscreen-score-@-f"), + KillCountToRatingAndKillString(ship_kills),credits/10.0]; + + + if (![[UNIVERSE gameController] playerFileToLoad]) + [[UNIVERSE gameController] setPlayerFileToLoad: save_path]; // make sure we load the correct game + + energy = 0.0f; + afterburner_engaged = NO; + [UNIVERSE setDisplayText:NO]; + [UNIVERSE setDisplayCursor:NO]; + [UNIVERSE setViewDirection:VIEW_AFT]; + [self becomeLargeExplosion:4.0]; + [self moveForward:100.0]; + + [self playGameOver]; + + flightSpeed = 160.0f; + [self setStatus:STATUS_DEAD]; + [UNIVERSE displayMessage:DESC(@"gameoverscreen-game-over") forCount:30.0]; + [UNIVERSE displayMessage:@"" forCount:30.0]; + [UNIVERSE displayMessage:scoreMS forCount:30.0]; + [UNIVERSE displayMessage:@"" forCount:30.0]; + [UNIVERSE displayMessage:DESC(@"gameoverscreen-press-space") forCount:30.0]; + [self resetShotTime]; + + if (whom == nil) whom = (id)[NSNull null]; + [self doScriptEvent:@"shipDied" withArguments:[NSArray arrayWithObjects:whom, why, nil]]; + [self loseTargetStatus]; +} + + +- (void) loseTargetStatus +{ + if (!UNIVERSE) + return; + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + Entity* my_entities[ent_count]; + int i; + for (i = 0; i < ent_count; i++) + my_entities[i] = [uni_entities[i] retain]; // retained + for (i = 0; i < ent_count ; i++) + { + Entity* thing = my_entities[i]; + if (thing->isShip) + { + ShipEntity* ship = (ShipEntity *)thing; + if (self == [ship primaryTarget]) + { + [ship noteLostTarget]; + } + } + } + for (i = 0; i < ent_count; i++) + { + [my_entities[i] release]; // released + } +} + + +- (void) enterDock:(StationEntity *)station +{ + if ([self status] == STATUS_DEAD) + return; + + [self setStatus:STATUS_DOCKING]; + [self doScriptEvent:@"shipWillDockWithStation" withArgument:station]; + + ident_engaged = NO; + afterburner_engaged = NO; + + cloaking_device_active = NO; + hyperspeed_engaged = NO; + hyperspeed_locked = NO; + [self safeAllMissiles]; + + [hud setScannerZoom:1.0]; + scanner_zoom_rate = 0.0f; + [UNIVERSE setDisplayText:NO]; + [UNIVERSE setDisplayCursor:NO]; + + [self setOrientation: kIdentityQuaternion]; // reset orientation to dock + + [UNIVERSE set_up_break_pattern:position quaternion:orientation]; + [self playDockWithStation]; + + [station noteDockedShip:self]; + dockedStation = station; + + [[UNIVERSE gameView] clearKeys]; // try to stop key bounces + +} + + +- (void) docked +{ + if (dockedStation == nil) + { + [self setStatus:STATUS_IN_FLIGHT]; + return; + } + + [self setStatus:STATUS_DOCKED]; + [UNIVERSE setViewDirection:VIEW_GUI_DISPLAY]; + + [self loseTargetStatus]; + + Vector launchPos = [dockedStation position]; + [self setPosition:launchPos]; + + [self setOrientation:kIdentityQuaternion]; // reset orientation to dock + + flightRoll = 0.0f; + flightPitch = 0.0f; + flightYaw = 0.0f; + flightSpeed = 0.0f; + + hyperspeed_engaged = NO; + hyperspeed_locked = NO; + [self safeAllMissiles]; + + primaryTarget = NO_TARGET; + [self clearTargetMemory]; + + forward_shield = [self maxForwardShieldLevel]; + aft_shield = [self maxAftShieldLevel]; + energy = maxEnergy; + weapon_temp = 0.0f; + ship_temperature = 60.0f; + + [self setAlertFlag:ALERT_FLAG_DOCKED to:YES]; + + if ([dockedStation localMarket] == nil) + { + [dockedStation initialiseLocalMarketWithSeed:system_seed andRandomFactor:market_rnd]; + } + + NSString *escapepodReport = [self processEscapePods]; + [self addMessageToReport:escapepodReport]; + + [self unloadCargoPods]; // fill up the on-ship commodities before... + + // check contracts + NSString *passengerAndCargoReport = [self checkPassengerContracts]; // Is also processing cargo contracts. + [self addMessageToReport:passengerAndCargoReport]; + + [UNIVERSE setDisplayText:YES]; + + [[OOMusicController sharedController] stopDockingMusic]; + [[OOMusicController sharedController] playDockedMusic]; + +#if DOCKING_CLEARANCE_ENABLED + // Did we fail to observe traffic control regulations? However, due to the state of emergency, + // apply no unauthorized docking penalties if a nova is ongoing. + if (![UNIVERSE strict] && [dockedStation requiresDockingClearance] && + ![self clearedToDock] && ![[UNIVERSE sun] willGoNova]) + { + [self penaltyForUnauthorizedDocking]; + } +#endif + + // time to check the script! + if (!being_fined) [self checkScript]; + + // if we've not switched to the mission screen then proceed normally.. + if (gui_screen != GUI_SCREEN_MISSION) + { + // check for fines + if (being_fined) [self getFined]; + + [self setGuiToStatusScreen]; + } + + [[OOCacheManager sharedCache] flush]; + + [self doScriptEvent:@"shipDockedWithStation" withArgument:dockedStation]; +} + + +#if 0 +- (void) setStatus:(OOEntityStatus)val +{ + [super setStatus:val]; + OOLog(@"player.temp.status", @"Player status set to %@", EntityStatusToString(val)); +} +#endif + + +- (void) leaveDock:(StationEntity *)station +{ + if (station == nil) return; + + // ensure we've not left keyboard entry on + [[UNIVERSE gameView] allowStringInput: NO]; + + if (gui_screen == GUI_SCREEN_MISSION) + { + [[UNIVERSE gui] clearBackground]; + [self doScriptEvent:@"missionScreenEnded"]; + } + + if (station == [UNIVERSE station]) + { + legalStatus |= [UNIVERSE legal_status_of_manifest:shipCommodityData]; // 'leaving with those guns were you sir?' + } + [self loadCargoPods]; + + // clear the way + [station autoDockShipsOnApproach]; + [station clearDockingCorridor]; + + [self setAlertFlag:ALERT_FLAG_DOCKED to:NO]; +#if DOCKING_CLEARANCE_ENABLED + [self setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; +#endif + + [hud setScannerZoom:1.0]; + scanner_zoom_rate = 0.0f; + gui_screen = GUI_SCREEN_MAIN; + [self clearTargetMemory]; + [self setShowDemoShips:NO]; + [UNIVERSE setDisplayText:NO]; + [UNIVERSE setDisplayCursor:NO]; + + [[UNIVERSE gameView] clearKeys]; // try to stop keybounces + + [[OOMusicController sharedController] stop]; + + ship_clock_adjust = 600.0; // 10 minutes to leave dock + + [self setStatus: STATUS_LAUNCHING]; // Required before shipWillLaunchFromStation so Planet.setTexture() will work. + [self doScriptEvent:@"shipWillLaunchFromStation" withArgument:station]; + + [station launchShip:self]; + orientation.w = -orientation.w; // need this as a fix... + flightRoll = -flightRoll; + [UNIVERSE set_up_break_pattern:position quaternion:orientation]; + + dockedStation = nil; + + suppressAegisMessages = YES; +#if 0 + // "Fix" for "simple" issue where space compass shows station with planet icon on launch. + // Has the slight unwanted side-effect of effectively giving the player an advanced compass. + if ([self checkForAegis] != AEGIS_NONE) + { + [self setCompassMode:COMPASS_MODE_STATION]; + } + else + { + [self setCompassMode:COMPASS_MODE_PLANET]; + } +#else + [self checkForAegis]; +#endif + suppressAegisMessages = NO; + ident_engaged = NO; + + [UNIVERSE removeDemoShips]; + + [self playLaunchFromStation]; +} + + +- (void) enterGalacticWitchspace +{ + [self setStatus:STATUS_ENTERING_WITCHSPACE]; + [self doScriptEvent:@"shipWillEnterWitchspace" withArgument:@"galactic jump"]; + [self transitionToAegisNone]; + suppressAegisMessages=YES; + + if (primaryTarget != NO_TARGET) + primaryTarget = NO_TARGET; + + hyperspeed_engaged = NO; + + [hud setScannerZoom:1.0]; + scanner_zoom_rate = 0.0f; + + [UNIVERSE setDisplayText:NO]; + + [UNIVERSE allShipsDoScriptEvent:@"playerWillEnterWitchspace" andReactToAIMessage:@"PLAYER WITCHSPACE"]; + + [UNIVERSE removeAllEntitiesExceptPlayer:NO]; + + // remove any contracts for the old galaxy + if (contracts) + [contracts removeAllObjects]; + + // remove any mission destinations for the old galaxy + if (missionDestinations) + [missionDestinations removeAllObjects]; + + // expire passenger contracts for the old galaxy + if (passengers) + { + unsigned i; + for (i = 0; i < [passengers count]; i++) + { + // set the expected arrival time to now, so they storm off the ship at the first port + NSMutableDictionary* passenger_info = [NSMutableDictionary dictionaryWithDictionary:(NSDictionary *)[passengers objectAtIndex:i]]; + [passenger_info setObject:[NSNumber numberWithDouble:ship_clock] forKey:PASSENGER_KEY_ARRIVAL_TIME]; + [passengers replaceObjectAtIndex:i withObject:passenger_info]; + } + } + + [self removeEquipmentItem:@"EQ_GAL_DRIVE"]; + + galaxy_number++; + galaxy_number &= 7; + + galaxy_seed.a = rotate_byte_left(galaxy_seed.a); + galaxy_seed.b = rotate_byte_left(galaxy_seed.b); + galaxy_seed.c = rotate_byte_left(galaxy_seed.c); + galaxy_seed.d = rotate_byte_left(galaxy_seed.d); + galaxy_seed.e = rotate_byte_left(galaxy_seed.e); + galaxy_seed.f = rotate_byte_left(galaxy_seed.f); + + [UNIVERSE setGalaxy_seed:galaxy_seed]; + + // Choose the galactic hyperspace behaviour. Refers to where we may actually end up after an intergalactic jump. + // The default behaviour is that the player cannot arrive on unreachable or isolated systems. The options + // in planetinfo.plist, galactic_hyperspace_behaviour key can be used to allow arrival even at unreachable systems, + // or at fixed coordinates on the galactic chart. The key galactic_hyperspace_fixed_coords in planetinfo.plist is + // used in the fixed coordinates case and specifies the exact coordinates for the intergalactic jump. + switch (galacticHyperspaceBehaviour) + { + case GALACTIC_HYPERSPACE_BEHAVIOUR_FIXED_COORDINATES: + system_seed = [UNIVERSE findSystemAtCoords:galacticHyperspaceFixedCoords withGalaxySeed:galaxy_seed]; + break; + case GALACTIC_HYPERSPACE_BEHAVIOUR_ALL_SYSTEMS_REACHABLE: + system_seed = [UNIVERSE findSystemAtCoords:galaxy_coordinates withGalaxySeed:galaxy_seed]; + break; + case GALACTIC_HYPERSPACE_BEHAVIOUR_STANDARD: + default: + // instead find a system connected to system 0 near the current coordinates... + system_seed = [UNIVERSE findConnectedSystemAtCoords:galaxy_coordinates withGalaxySeed:galaxy_seed]; + break; + } + + target_system_seed = system_seed; + + [UNIVERSE setSystemTo:system_seed]; + galaxy_coordinates.x = system_seed.d; + galaxy_coordinates.y = system_seed.b; + ranrot_srand((unsigned int)[[NSDate date] timeIntervalSince1970]); // seed randomiser by time + market_rnd = ranrot_rand() & 255; // random factor for market values is reset + legalStatus = 0; + [UNIVERSE set_up_universe_from_witchspace]; + + [self doScriptEvent:@"playerEnteredNewGalaxy" withArgument:[NSNumber numberWithUnsignedInt:galaxy_number]]; +} + + +- (void) enterWormhole:(WormholeEntity *) w_hole replacing:(BOOL)replacing +{ + // Before anything, store player's target system, it may not be the same as the wormhole destination + Random_Seed playersOriginalDestination = target_system_seed ; + + target_system_seed = [w_hole destination]; + [self setStatus:STATUS_ENTERING_WITCHSPACE]; + [self doScriptEvent:@"shipWillEnterWitchspace" withArgument:@"wormhole"]; + [self transitionToAegisNone]; + suppressAegisMessages=YES; + + hyperspeed_engaged = NO; + + if (primaryTarget != NO_TARGET) + primaryTarget = NO_TARGET; + + // reset the compass + + if ([self hasEquipmentItem:@"EQ_ADVANCED_COMPASS"]) + compassMode = COMPASS_MODE_PLANET; + else + compassMode = COMPASS_MODE_BASIC; + + double distance = distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + ship_clock_adjust = distance * distance * 3600.0; // LY * LY hrs + + [hud setScannerZoom:1.0]; + scanner_zoom_rate = 0.0f; + + [UNIVERSE setDisplayText:NO]; + + [UNIVERSE allShipsDoScriptEvent:@"playerWillEnterWitchspace" andReactToAIMessage:@"PLAYER WITCHSPACE"]; + + [UNIVERSE removeAllEntitiesExceptPlayer:NO]; + [UNIVERSE setSystemTo:target_system_seed]; + + system_seed = target_system_seed; + galaxy_coordinates.x = system_seed.d; + galaxy_coordinates.y = system_seed.b; + target_system_seed = playersOriginalDestination; // restore the player's target system now + legalStatus /= 2; // 'another day, another system' + ranrot_srand((unsigned int)[[NSDate date] timeIntervalSince1970]); // seed randomiser by time + market_rnd = ranrot_rand() & 255; // random factor for market values is reset + + [UNIVERSE set_up_universe_from_witchspace]; + [[UNIVERSE planet] update: 2.34375 * market_rnd]; // from 0..10 minutes + [[UNIVERSE station] update: 2.34375 * market_rnd]; // from 0..10 minutes +} + + +- (void) enterWitchspace +{ + double distance = distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + + [self setStatus:STATUS_ENTERING_WITCHSPACE]; + [self doScriptEvent:@"shipWillEnterWitchspace" withArgument:@"standard jump"]; + [self transitionToAegisNone]; + suppressAegisMessages=YES; + + hyperspeed_engaged = NO; + + if (primaryTarget != NO_TARGET) + primaryTarget = NO_TARGET; + + [hud setScannerZoom:1.0]; + scanner_zoom_rate = 0.0f; + + [UNIVERSE setDisplayText:NO]; + + [UNIVERSE allShipsDoScriptEvent:@"playerWillEnterWitchspace" andReactToAIMessage:@"PLAYER WITCHSPACE"]; + + [UNIVERSE removeAllEntitiesExceptPlayer:NO]; + + // reset the compass + + if ([self hasEquipmentItem:@"EQ_ADVANCED_COMPASS"]) + compassMode = COMPASS_MODE_PLANET; + else + compassMode = COMPASS_MODE_BASIC; + + // perform any check here for forced witchspace encounters + + unsigned malfunc_chance = 253; + if (ship_trade_in_factor < 80) + malfunc_chance -= (1 + ranrot_rand() % (81-ship_trade_in_factor)) / 2; // increase chance of misjump in worn-out craft + ranrot_srand((unsigned int)[[NSDate date] timeIntervalSince1970]); // seed randomiser by time + BOOL malfunc = ((ranrot_rand() & 0xff) > malfunc_chance); + // 75% of the time a malfunction means a misjump + BOOL misjump = [self scriptedMisjump] || ((flightPitch == max_flight_pitch) || (malfunc && (randf() > 0.75))); + + fuel -= 10.0 * distance; // fuel cost to target system + ship_clock_adjust = distance * distance * 3600.0; // LY * LY hrs + if (!misjump) + { + [UNIVERSE setSystemTo:target_system_seed]; + system_seed = target_system_seed; + galaxy_coordinates.x = system_seed.d; + galaxy_coordinates.y = system_seed.b; + legalStatus /= 2; // 'another day, another system' + market_rnd = ranrot_rand() & 255; // random factor for market values is reset + if (market_rnd < 8) + [self erodeReputation]; // every 32 systems or so, drop back towards 'unknown' + + if (2 * market_rnd < ship_trade_in_factor) // every eight jumps or so + ship_trade_in_factor -= 1 + (market_rnd & 3); // drop the price down towards 75% + if (ship_trade_in_factor < 75) + ship_trade_in_factor = 75; // lower limit for trade in value is 75% + + [UNIVERSE set_up_universe_from_witchspace]; + [[UNIVERSE planet] update: 2.34375 * market_rnd]; // from 0..10 minutes + [[UNIVERSE station] update: 2.34375 * market_rnd]; // from 0..10 minutes + if (malfunc) + { + if (randf() > 0.5) + { + [self setFuelLeak:[NSString stringWithFormat:@"%f", (randf() + randf()) * 5.0]]; + } + else + { + [self playWitchjumpFailure]; + [self takeInternalDamage]; + } + } + } + else + { + // move sort of halfway there... + galaxy_coordinates.x += target_system_seed.d; + galaxy_coordinates.y += target_system_seed.b; + galaxy_coordinates.x /= 2; + galaxy_coordinates.y /= 2; + [self playWitchjumpMisjump]; + [UNIVERSE set_up_universe_from_misjump]; + } +} + + +- (void) leaveWitchspace +{ + Vector pos = [UNIVERSE getWitchspaceExitPosition]; + Quaternion q_rtn = [UNIVERSE getWitchspaceExitRotation]; + Quaternion q1; + + quaternion_set_random(&q1); + float d1 = (float)(SCANNER_MAX_RANGE*((ranrot_rand() % 256)/256.0 - 0.5)); + if (abs((int)d1) < 750) + {// no closer than 750m. Eric, was original 500m but that collides with some buoy variants. + d1 += ((d1 > 0.0)? 750.0f: -750.0f); + } + Vector v1 = vector_forward_from_quaternion(q1); + pos.x += v1.x * d1; // randomise exit position + pos.y += v1.y * d1; + pos.z += v1.z * d1; + + position = pos; + orientation = q_rtn; + flightRoll = 0.0f; + flightPitch = 0.0f; + flightYaw = 0.0f; + flightSpeed = maxFlightSpeed * 0.25f; + [self setStatus:STATUS_EXITING_WITCHSPACE]; + gui_screen = GUI_SCREEN_MAIN; + being_fined = NO; // until you're scanned by a copper! + [self clearTargetMemory]; + [self setShowDemoShips:NO]; + [UNIVERSE setDisplayCursor:NO]; + [UNIVERSE setDisplayText:NO]; + [UNIVERSE set_up_break_pattern:position quaternion:orientation]; + [self playExitWitchspace]; + [self doScriptEvent:@"shipWillExitWitchspace"]; +} + + +/////////////////////////////////// + +- (void) setGuiToStatusScreen +{ + // intercept any docking messages + if ([dockingReport length] > 0) + { + [self setGuiToDockingReportScreen]; // go here instead! + return; + } + + NSString *systemName = nil; + NSString *targetSystemName = nil; + NSString *text = nil; + + system_seed = [UNIVERSE findSystemAtCoords:galaxy_coordinates withGalaxySeed:galaxy_seed]; + target_system_seed = [UNIVERSE findSystemAtCoords:cursor_coordinates withGalaxySeed:galaxy_seed]; + + systemName = [UNIVERSE getSystemName:system_seed]; + if ([self isDocked] && dockedStation != [UNIVERSE station]) + { + systemName = [NSString stringWithFormat:@"%@ : %@", systemName, [dockedStation displayName]]; + } + + targetSystemName = [UNIVERSE getSystemName:target_system_seed]; + + // GUI stuff + { + GuiDisplayGen *gui = [UNIVERSE gui]; + NSDictionary *ship_dict = nil; + NSString *shipName = nil; + NSString *legal_desc = nil, *rating_desc = nil, + *alert_desc = nil, *fuel_desc = nil, + *credits_desc = nil; + + OOGUITabSettings tab_stops; + tab_stops[0] = 20; + tab_stops[1] = 160; + tab_stops[2] = 290; + [gui setTabStops:tab_stops]; + + ship_dict = [[OOShipRegistry sharedRegistry] shipInfoForKey:ship_desc]; + shipName = [ship_dict stringForKey:@"display_name" defaultValue:[ship_dict stringForKey:KEY_NAME]]; + + NSString *lightYearsDesc = DESC(@"status-light-years-desc"); + + legal_desc = LegalStatusToString(legalStatus); + rating_desc = KillCountToRatingAndKillString(ship_kills); + alert_desc = AlertConditionToString([self alertCondition]); + fuel_desc = [NSString stringWithFormat:@"%.1f %@", fuel/10.0, lightYearsDesc]; + credits_desc = OOCredits(credits); + + [gui clear]; + + text = DESC(@"status-commander-@"); + [gui setTitle:[NSString stringWithFormat:text, player_name]]; + + [gui setText:shipName forRow:0 align:GUI_ALIGN_CENTER]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-present-system"), systemName, nil] forRow:1]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-hyperspace-system"), targetSystemName, nil] forRow:2]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-condition"), alert_desc, nil] forRow:3]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-fuel"), fuel_desc, nil] forRow:4]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-cash"), credits_desc, nil] forRow:5]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-legal-status"), legal_desc, nil] forRow:6]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"status-rating"), rating_desc, nil] forRow:7]; + + [gui setText:DESC(@"status-equipment") forRow:9]; + + [gui setShowTextCursor:NO]; + } + /* ends */ + + if (lastTextKey) + { + [lastTextKey release]; + lastTextKey = nil; + } + + [[UNIVERSE gameView] clearMouse]; + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_STATUS; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; + +#if 0 +// DEBUG SCENE TEST ROUTINES + [UNIVERSE removeDemoShips]; + [self debugOn]; + [self setBackgroundFromDescriptionsKey:@"test-scene"]; + [self debugOff]; + [self setShowDemoShips: YES]; +// END TEST +#endif + if (oldScreen != GUI_SCREEN_STATUS) + { + [[UNIVERSE gui] setStatusPage:0]; + [self noteGuiChangeFrom:oldScreen to:gui_screen]; + } +} + + +- (NSArray *) equipmentList +{ + NSArray *eqTypes = [OOEquipmentType allEquipmentTypes]; + NSMutableArray *quip = [NSMutableArray arrayWithCapacity:[eqTypes count]]; + NSEnumerator *eqTypeEnum = nil; + OOEquipmentType *eqType = nil; + NSString *desc = nil; + + for (eqTypeEnum = [eqTypes objectEnumerator]; (eqType = [eqTypeEnum nextObject]); ) + { + if ([self hasEquipmentItem:[eqType identifier]]) + { + [quip addObject:[NSArray arrayWithObjects:[eqType name], [NSNumber numberWithBool:YES], nil]]; + } + else if (![UNIVERSE strict]) + { + // Check for damaged version + if ([self hasEquipmentItem:[[eqType identifier] stringByAppendingString:@"_DAMAGED"]]) + { + desc = [NSString stringWithFormat:DESC(@"equipment-@-not-available"), [eqType name]]; + [quip addObject:[NSArray arrayWithObjects:desc, [NSNumber numberWithBool:NO], nil]]; + } + } + } + + if (forward_weapon > WEAPON_NONE) + { + desc = [NSString stringWithFormat:DESC(@"equipment-fwd-weapon-@"),[UNIVERSE descriptionForArrayKey:@"weapon_name" index:forward_weapon]]; + [quip addObject:[NSArray arrayWithObjects:desc, [NSNumber numberWithBool:YES], nil]]; + } + if (aft_weapon > WEAPON_NONE) + { + desc = [NSString stringWithFormat:DESC(@"equipment-aft-weapon-@"),[UNIVERSE descriptionForArrayKey:@"weapon_name" index:aft_weapon]]; + [quip addObject:[NSArray arrayWithObjects:desc, [NSNumber numberWithBool:YES], nil]]; + } + if (starboard_weapon > WEAPON_NONE) + { + desc = [NSString stringWithFormat:DESC(@"equipment-stb-weapon-@"),[UNIVERSE descriptionForArrayKey:@"weapon_name" index:starboard_weapon]]; + [quip addObject:[NSArray arrayWithObjects:desc, [NSNumber numberWithBool:YES], nil]]; + } + if (port_weapon > WEAPON_NONE) + { + desc = [NSString stringWithFormat:DESC(@"equipment-port-weapon-@"),[UNIVERSE descriptionForArrayKey:@"weapon_name" index:port_weapon]]; + [quip addObject:[NSArray arrayWithObjects:desc, [NSNumber numberWithBool:YES], nil]]; + } + + if (max_passengers > 0) + { + desc = [NSString stringWithFormat:DESC_PLURAL(@"equipment-pass-berth-@", max_passengers), max_passengers]; + [quip addObject:[NSArray arrayWithObjects:desc, [NSNumber numberWithBool:YES], nil]]; + } + + return quip; +} + + +- (NSArray *) cargoList +{ + NSMutableArray *manifest = [NSMutableArray array]; + + if (specialCargo) [manifest addObject:specialCargo]; + + unsigned n_commodities = [shipCommodityData count]; + OOCargoQuantity in_hold[n_commodities]; + unsigned i; + + // following changed to work whether docked or not + for (i = 0; i < n_commodities; i++) + { + in_hold[i] = [[shipCommodityData arrayAtIndex:i] unsignedIntAtIndex:MARKET_QUANTITY]; + } + for (i = 0; i < [cargo count]; i++) + { + ShipEntity *container = [cargo objectAtIndex:i]; + in_hold[[container commodityType]] += [container commodityAmount]; + } + + for (i = 0; i < n_commodities; i++) + { + if (in_hold[i] > 0) + { + NSString *desc = CommodityDisplayNameForCommodityArray([shipCommodityData arrayAtIndex:i]); + NSString *units = DisplayStringForMassUnitForCommodity(i); + [manifest addObject:[NSString stringWithFormat:DESC(@"manifest-cargo-quantity-format"), in_hold[i], units, desc]]; + } + } + + return manifest; +} + + +- (void) setGuiToSystemDataScreen +{ + NSDictionary* targetSystemData; + NSString* targetSystemName; + + targetSystemData = [[UNIVERSE generateSystemData:target_system_seed] retain]; // retained + targetSystemName = [targetSystemData stringForKey:KEY_NAME]; + + BOOL sunGoneNova = NO; + if ([targetSystemData objectForKey:@"sun_gone_nova"]) + sunGoneNova = YES; + + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + + OOGUITabSettings tab_stops; + tab_stops[0] = 0; + tab_stops[1] = 96; + tab_stops[2] = 144; + [gui setTabStops:tab_stops]; + + int techlevel = [targetSystemData intForKey:KEY_TECHLEVEL]; + int population = [targetSystemData intForKey:KEY_POPULATION]; + int productivity = [targetSystemData intForKey:KEY_PRODUCTIVITY]; + int radius = [targetSystemData intForKey:KEY_RADIUS]; + + NSString *government_desc = GovernmentToString([targetSystemData intForKey:KEY_GOVERNMENT]); + NSString *economy_desc = EconomyToString([targetSystemData intForKey:KEY_ECONOMY]); + NSString *inhabitants = [targetSystemData stringForKey:KEY_INHABITANTS]; + NSString *system_desc = [targetSystemData stringForKey:KEY_DESCRIPTION]; + + if ((sunGoneNova && equal_seeds(target_system_seed, system_seed) && [[UNIVERSE sun] goneNova])|| + (sunGoneNova && (!equal_seeds(target_system_seed, system_seed)))) + { + population = 0; + productivity = 0; + radius = 0; + techlevel = -1; // So it dispalys as 0 on the system info screen + government_desc = DESC(@"nova-system-government"); + economy_desc = DESC(@"nova-system-economy"); + inhabitants = DESC(@"nova-system-inhabitants"); + system_desc = ExpandDescriptionForSeed(@"[nova-system-description]", target_system_seed); + } + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"sysdata-planet-name-@"), targetSystemName]]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"sysdata-eco"), economy_desc, nil] forRow:1]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"sysdata-govt"), government_desc, nil] forRow:3]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"sysdata-tl"), [NSString stringWithFormat:@"%d", techlevel + 1], nil] forRow:5]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"sysdata-pop"), [NSString stringWithFormat:@"%.1f %@", 0.1*population, DESC(@"sysdata-billion-word")], nil] forRow:7]; + [gui setArray:[NSArray arrayWithObjects:@"", [NSString stringWithFormat:@"(%@)", inhabitants], nil] forRow:8]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"sysdata-prod"), @"", [NSString stringWithFormat:DESC(@"sysdata-prod-worth"), productivity], nil] forRow:10]; + + [gui setArray:[NSArray arrayWithObjects:DESC(@"sysdata-radius"), @"", [NSString stringWithFormat:@"%5d km", radius], nil] forRow:12]; + + int i = [gui addLongText:system_desc startingAtRow:15 align:GUI_ALIGN_LEFT]; + missionTextRow = i; + for (i-- ; i > 14 ; i--) + [gui setColor:[OOColor greenColor] forRow:i]; + + + [gui setShowTextCursor:NO]; + + } + /* ends */ + + [lastTextKey release]; + lastTextKey = nil; + + [[UNIVERSE gameView] clearMouse]; + + [targetSystemData release]; + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_SYSTEM_DATA; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; + + [UNIVERSE removeDemoShips]; + if ([targetSystemName isEqual: [UNIVERSE getSystemName:system_seed]]) + [self setBackgroundFromDescriptionsKey:@"gui-scene-show-local-planet"]; + else + [self setBackgroundFromDescriptionsKey:@"gui-scene-show-planet"]; + [self noteGuiChangeFrom:oldScreen to:gui_screen]; + if (oldScreen != gui_screen) [self checkScript]; +} + + +- (NSArray *) markedDestinations +{ + // get a list of systems marked as contract destinations + NSMutableArray *destinations = [NSMutableArray arrayWithCapacity:256]; + BOOL mark[256] = {0}; + unsigned i; + + for (i = 0; i < [passengers count]; i++) + { + mark[[[passengers dictionaryAtIndex:i] unsignedCharForKey:PASSENGER_KEY_DESTINATION]] = YES; + } + for (i = 0; i < [contracts count]; i++) + { + mark[[[contracts dictionaryAtIndex:i] unsignedCharForKey:CONTRACT_KEY_DESTINATION]] = YES; + } + for (i = 0; i < [missionDestinations count]; i++) + { + mark[[missionDestinations unsignedCharAtIndex:i]] = YES; + } + for (i = 0; i < 256; i++) + { + [destinations addObject:[NSNumber numberWithBool:mark[i]]]; + } + + return destinations; +} + + +- (void) setGuiToLongRangeChartScreen +{ + NSString *targetSystemName; + double distance = distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + double estimatedTravelTime = distance * distance; + + if ((target_system_seed.d != cursor_coordinates.x)||(target_system_seed.b != cursor_coordinates.y)) + target_system_seed = [UNIVERSE findSystemAtCoords:cursor_coordinates withGalaxySeed:galaxy_seed]; + targetSystemName = [[UNIVERSE getSystemName:target_system_seed] retain]; // retained + + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"long-range-chart-title-d"), galaxy_number+1]]; + + [gui setText:targetSystemName forRow:17]; + [gui setText:[NSString stringWithFormat:DESC(@"long-range-chart-distance-f"), distance] forRow:18]; + [gui setText:(distance <= (fuel/10.0f) ? [NSString stringWithFormat:DESC(@"long-range-chart-est-travel-time-f"), estimatedTravelTime] : (id)@"") forRow:19]; + + NSString *displaySearchString = planetSearchString ? [planetSearchString capitalizedString] : (NSString *)@""; + [gui setText:[NSString stringWithFormat:DESC(@"long-range-chart-find-planet-@"), displaySearchString] forRow:16]; + [gui setColor:[OOColor cyanColor] forRow:16]; + + [gui setShowTextCursor:YES]; + [gui setCurrentRow:16]; + } + /* ends */ + + [[UNIVERSE gameView] clearMouse]; + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_LONG_RANGE_CHART; + + [targetSystemName release]; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; + + [self noteGuiChangeFrom:oldScreen to:gui_screen]; +} + + +- (void) setGuiToShortRangeChartScreen +{ + NSString* targetSystemName; + double distance = distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + double estimatedTravelTime = distance * distance; + + if ((target_system_seed.d != cursor_coordinates.x)||(target_system_seed.b != cursor_coordinates.y)) + target_system_seed = [UNIVERSE findSystemAtCoords:cursor_coordinates withGalaxySeed:galaxy_seed]; + targetSystemName = [[UNIVERSE getSystemName:target_system_seed] retain]; // retained + + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + + if ((abs(cursor_coordinates.x-galaxy_coordinates.x)>=20)||(abs(cursor_coordinates.y-galaxy_coordinates.y)>=38)) + cursor_coordinates = galaxy_coordinates; // home + + [gui clear]; + [gui setTitle:DESC(@"short-range-chart-title")]; + + [gui setText:targetSystemName forRow:19]; + [gui setText:[NSString stringWithFormat:DESC(@"short-range-chart-distance-f"), distance] forRow:20]; + [gui setText:(distance <= (fuel/10.0f) ? [NSString stringWithFormat:DESC(@"short-range-chart-est-travel-time-f"), estimatedTravelTime] : (id)@"") forRow:21]; + + + [gui setShowTextCursor:NO]; + } + /* ends */ + + [[UNIVERSE gameView] clearMouse]; + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_SHORT_RANGE_CHART; + + [targetSystemName release]; // released + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; + + [self noteGuiChangeFrom:oldScreen to:gui_screen]; +} + + +- (void) setGuiToGameOptionsScreen +{ +#ifdef GNUSTEP + MyOpenGLView *gameView = [UNIVERSE gameView]; +#endif + GameController *controller = [UNIVERSE gameController]; + OOUInteger displayModeIndex = [controller indexOfCurrentDisplayMode]; + NSArray *modeList = [controller displayModes]; + NSDictionary *mode = nil; + + if (displayModeIndex == NSNotFound) + { + OOLogWARN(@"display.currentMode.notFound", @"couldn't find current fullscreen setting, switching to default."); + displayModeIndex = 0; + } + + // in linux modeList may be empty & cause an exception here + if ([modeList count]) + { + mode = [modeList objectAtIndex:displayModeIndex]; + } + int modeWidth = [[mode objectForKey:kOODisplayWidth] intValue]; + int modeHeight = [[mode objectForKey:kOODisplayHeight] intValue]; + float modeRefresh = (float)[[mode objectForKey:kOODisplayRefreshRate] doubleValue]; + + NSString *displayModeString = [self screenModeStringForWidth:modeWidth height:modeHeight refreshRate:modeRefresh]; + + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + GUI_ROW_INIT(gui); + + int first_sel_row = GUI_FIRST_ROW(GAME)-5; // repositioned menu + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"status-commander-@"), player_name]]; // Same title as status screen. + + [gui setText:displayModeString forRow:GUI_ROW(GAME,DISPLAY) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,DISPLAY)]; + + if ([UNIVERSE autoSave]) + [gui setText:DESC(@"gameoptions-autosave-yes") forRow:GUI_ROW(GAME,AUTOSAVE) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-autosave-no") forRow:GUI_ROW(GAME,AUTOSAVE) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,AUTOSAVE)]; + + // volume control + if ([OOSound respondsToSelector:@selector(masterVolume)]) + { + int volume = 20 * [OOSound masterVolume]; + NSString* soundVolumeWordDesc = DESC(@"gameoptions-sound-volume"); + NSString* v1_string = @"|||||||||||||||||||||||||"; + NSString* v0_string = @"........................."; + v1_string = [v1_string substringToIndex:volume]; + v0_string = [v0_string substringToIndex:20 - volume]; + if (volume > 0) + [gui setText:[NSString stringWithFormat:@"%@%@%@ ", soundVolumeWordDesc, v1_string, v0_string] forRow:GUI_ROW(GAME,VOLUME) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-sound-volume-mute") forRow:GUI_ROW(GAME,VOLUME) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,VOLUME)]; + } + else + { + [gui setText:DESC(@"gameoptions-volume-external-only") forRow:GUI_ROW(GAME,VOLUME) align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor grayColor] forRow:GUI_ROW(GAME,VOLUME)]; + } + +#if OOLITE_MAC_OS_X + // Growl priority control + { + NSUserDefaults* prefs = [NSUserDefaults standardUserDefaults]; + NSString* growl_priority_desc; + int growl_min_priority = 3; + if ([prefs objectForKey:@"groolite-min-priority"]) + growl_min_priority = [prefs integerForKey:@"groolite-min-priority"]; + if ((growl_min_priority < kGroolitePriorityMinimum)||(growl_min_priority > kGroolitePriorityMaximum)) + { + growl_min_priority = kGroolitePriorityMaximum; + [prefs setInteger:kGroolitePriorityMaximum forKey:@"groolite-min-priority"]; + } + growl_priority_desc = [Groolite priorityDescription:growl_min_priority]; + [gui setText:[NSString stringWithFormat:DESC(@"gameoptions-show-growl-messages-@"), growl_priority_desc] + forRow:GUI_ROW(GAME,GROWL) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,GROWL)]; + } +#endif +#if OOLITE_SPEECH_SYNTH + // Speech control + if (isSpeechOn) + [gui setText:DESC(@"gameoptions-spoken-messages-yes") forRow:GUI_ROW(GAME,SPEECH) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-spoken-messages-no") forRow:GUI_ROW(GAME,SPEECH) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,SPEECH)]; +#if OOLITE_ESPEAK + { + NSString *message = [NSString stringWithFormat:DESC(@"gameoptions-voice-@"), [UNIVERSE voiceName: voice_no]]; + [gui setText:message forRow:GUI_ROW(GAME,SPEECH_LANGUAGE) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,SPEECH_LANGUAGE)]; + + message = [NSString stringWithFormat:DESC(voice_gender_m ? @"gameoptions-voice-M" : @"gameoptions-voice-F")]; + [gui setText:message forRow:GUI_ROW(GAME,SPEECH_GENDER) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,SPEECH_GENDER)]; + } +#endif +#endif +#if !OOLITE_MAC_OS_X + // window/fullscreen + if([gameView inFullScreenMode]) + { + [gui setText:DESC(@"gameoptions-play-in-window") forRow:GUI_ROW(GAME,DISPLAYSTYLE) align:GUI_ALIGN_CENTER]; + } + else + { + [gui setText:DESC(@"gameoptions-play-in-fullscreen") forRow:GUI_ROW(GAME,DISPLAYSTYLE) align:GUI_ALIGN_CENTER]; + } + [gui setKey: GUI_KEY_OK forRow: GUI_ROW(GAME,DISPLAYSTYLE)]; + + + [gui setText:DESC(@"gameoptions-joystick-configuration") forRow: GUI_ROW(GAME,STICKMAPPER) align: GUI_ALIGN_CENTER]; + if ([[gameView getStickHandler] getNumSticks]) + { + // TODO: Modify input code to put this in a better place + stickHandler=[gameView getStickHandler]; + numSticks=[stickHandler getNumSticks]; + // end TODO + + [gui setKey: GUI_KEY_OK forRow: GUI_ROW(GAME,STICKMAPPER)]; + } + else + { + [gui setColor:[OOColor grayColor] forRow:GUI_ROW(GAME,STICKMAPPER)]; + } +#endif + NSString *message = [NSString stringWithFormat:DESC(@"gameoptions-music-mode-@"), [UNIVERSE descriptionForArrayKey:@"music-mode" index:[[OOMusicController sharedController] mode]]]; + [gui setText:message forRow:GUI_ROW(GAME,MUSIC) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,MUSIC)]; + + if ([UNIVERSE wireframeGraphics]) + [gui setText:DESC(@"gameoptions-wireframe-graphics-yes") forRow:GUI_ROW(GAME,WIREFRAMEGRAPHICS) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-wireframe-graphics-no") forRow:GUI_ROW(GAME,WIREFRAMEGRAPHICS) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,WIREFRAMEGRAPHICS)]; + +#if ALLOW_PROCEDURAL_PLANETS + if ([UNIVERSE doProcedurallyTexturedPlanets]) + [gui setText:DESC(@"gameoptions-procedurally-textured-planets-yes") forRow:GUI_ROW(GAME,PROCEDURALLYTEXTUREDPLANETS) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-procedurally-textured-planets-no") forRow:GUI_ROW(GAME,PROCEDURALLYTEXTUREDPLANETS) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,PROCEDURALLYTEXTUREDPLANETS)]; +#endif + + if ([UNIVERSE reducedDetail]) + [gui setText:DESC(@"gameoptions-reduced-detail-yes") forRow:GUI_ROW(GAME,DETAIL) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-reduced-detail-no") forRow:GUI_ROW(GAME,DETAIL) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,DETAIL)]; + + // Shader effects level. + int shaderEffects = [UNIVERSE shaderEffectsLevel]; + NSString* shaderEffectsOptionsString = nil; + if (shaderEffects == SHADERS_NOT_SUPPORTED) + { + [gui setText:DESC(@"gameoptions-shaderfx-not-available") forRow:GUI_ROW(GAME,SHADEREFFECTS) align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor grayColor] forRow:GUI_ROW(GAME,SHADEREFFECTS)]; + } + else + { + shaderEffectsOptionsString = [NSString stringWithFormat:DESC(@"gameoptions-shaderfx-@"), ShaderSettingToDisplayString(shaderEffects)]; + [gui setText:shaderEffectsOptionsString forRow:GUI_ROW(GAME,SHADEREFFECTS) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,SHADEREFFECTS)]; + } + + // Back menu option + [gui setText:DESC(@"gui-back") forRow:GUI_ROW(GAME,BACK) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(GAME,BACK)]; + + [gui setSelectableRange:NSMakeRange(first_sel_row, GUI_ROW_GAMEOPTIONS_END_OF_LIST)]; + [gui setSelectedRow: first_sel_row]; + + [gui setShowTextCursor:NO]; + } + /* ends */ + + [self setShowDemoShips:NO]; + gui_screen = GUI_SCREEN_GAMEOPTIONS; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) setGuiToLoadSaveScreen +{ + BOOL canLoadOrSave = NO; + MyOpenGLView *gameView = [UNIVERSE gameView]; + GameController *controller = [UNIVERSE gameController]; + + if ([self status] == STATUS_DOCKED) + { + if (dockedStation == nil) + dockedStation = [UNIVERSE station]; + canLoadOrSave = (dockedStation == [UNIVERSE station]); + } + + BOOL canQuickSave = (canLoadOrSave && ([[gameView gameController] playerFileToLoad] != nil)); + OOUInteger displayModeIndex = [controller indexOfCurrentDisplayMode]; + NSArray *modeList = [controller displayModes]; + NSDictionary *mode = nil; + + if (displayModeIndex == NSNotFound) + { + OOLogWARN(@"display.currentMode.notFound", @"couldn't find current fullscreen setting, switching to default."); + displayModeIndex = 0; + } + + // in linux modeList may be empty & cause an exception here + if ([modeList count]) + { + mode = [modeList objectAtIndex:displayModeIndex]; + } + + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + GUI_ROW_INIT(gui); + + int first_sel_row = (canLoadOrSave)? GUI_ROW(,SAVE) : GUI_ROW(,BEGIN_NEW); + if (canQuickSave) + first_sel_row = GUI_ROW(,QUICKSAVE); + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"status-commander-@"), player_name]]; //Same title as status screen. + + [gui setText:DESC(@"options-quick-save") forRow:GUI_ROW(,QUICKSAVE) align:GUI_ALIGN_CENTER]; + if (canQuickSave) + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,QUICKSAVE)]; + else + [gui setColor:[OOColor grayColor] forRow:GUI_ROW(,QUICKSAVE)]; + + [gui setText:DESC(@"options-save-commander") forRow:GUI_ROW(,SAVE) align:GUI_ALIGN_CENTER]; + [gui setText:DESC(@"options-load-commander") forRow:GUI_ROW(,LOAD) align:GUI_ALIGN_CENTER]; + if (canLoadOrSave) + { + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,SAVE)]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,LOAD)]; + } + else + { + [gui setColor:[OOColor grayColor] forRow:GUI_ROW(,SAVE)]; + [gui setColor:[OOColor grayColor] forRow:GUI_ROW(,LOAD)]; + } + + [gui setText:DESC(@"options-begin-new-game") forRow:GUI_ROW(,BEGIN_NEW) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,BEGIN_NEW)]; + + [gui setText:DESC(@"options-game-options") forRow:GUI_ROW(,GAMEOPTIONS) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,GAMEOPTIONS)]; + +#if OOLITE_SDL + // GNUstep needs a quit option at present (no Cmd-Q) but + // doesn't need speech. + + // quit menu option + [gui setText:DESC(@"options-exit-game") forRow:GUI_ROW(,QUIT) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,QUIT)]; +#endif + + if ([UNIVERSE strict]) + [gui setText:DESC(@"options-reset-to-unrestricted-play") forRow:GUI_ROW(,STRICT) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"options-reset-to-strict-play") forRow:GUI_ROW(,STRICT) align:GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow:GUI_ROW(,STRICT)]; + + [gui setSelectableRange:NSMakeRange(first_sel_row, GUI_ROW_OPTIONS_END_OF_LIST)]; + + if ([[UNIVERSE gameController] gameIsPaused]) + { + [gui setSelectedRow: GUI_ROW(,GAMEOPTIONS)]; + } + else + { + [gui setSelectedRow: first_sel_row]; + } + + [gui setShowTextCursor:NO]; + } + /* ends */ + + [[UNIVERSE gameView] clearMouse]; + + [self setShowDemoShips:NO]; + gui_screen = GUI_SCREEN_OPTIONS; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +static int last_outfitting_index; + +- (void) setGuiToEquipShipScreen:(int)skipParam selectingFacingFor:(NSString *)eqKeyForSelectFacing +{ + missiles = [self countMissiles]; + + unsigned skip; + + // if skip < 0 then use the last recorded index + if (skipParam < 0) + { + if (last_outfitting_index >= 0) + skip = last_outfitting_index; + else + skip = 0; + } + else + { + skip = skipParam; + } + + // don't show a "Back" item if we're only skipping one item - just show the item + if (skip == 1) + skip = 0; + + last_outfitting_index = skip; + + OOCargoQuantityDelta cargo_space = max_cargo - current_cargo; + if (cargo_space < 0) cargo_space = 0; + + double priceFactor = 1.0; + OOTechLevelID techlevel = [[UNIVERSE generateSystemData:system_seed] intForKey:KEY_TECHLEVEL]; + + if (dockedStation) + { + priceFactor = [dockedStation equipmentPriceFactor]; + if ([dockedStation equivalentTechLevel] != NSNotFound) + techlevel = [dockedStation equivalentTechLevel]; + } + + // build an array of all equipment - and take away that which has been bought (or is not permitted) + NSMutableArray *equipmentAllowed = [NSMutableArray array]; + + // find options that agree with this ship + OOShipRegistry *registry = [OOShipRegistry sharedRegistry]; + NSDictionary *shipyardInfo = [registry shipyardInfoForKey:ship_desc]; + NSMutableSet *options = [NSMutableSet setWithArray:[shipyardInfo arrayForKey:KEY_OPTIONAL_EQUIPMENT]]; + + // add standard items too! + [options addObjectsFromArray:[[[registry shipyardInfoForKey:ship_desc] dictionaryForKey:KEY_STANDARD_EQUIPMENT] arrayForKey:KEY_EQUIPMENT_EXTRAS]]; + + unsigned i = 0; + NSEnumerator *eqEnum = nil; + OOEquipmentType *eqType = nil; + + for (eqEnum = [OOEquipmentType equipmentEnumerator]; (eqType = [eqEnum nextObject]); i++) + { + NSString *eqKey = [eqType identifier]; + OOTechLevelID minTechLevel = [eqType effectiveTechLevel]; + + // set initial availability to NO + BOOL isOK = NO; + + // check special availability + if ([eqType isAvailableToAll]) [options addObject:eqKey]; + + // if you have a dmaged system you can get it repaired at a tech level one less than that required to buy it + if (minTechLevel != 0 && [self hasEquipmentItem:[eqType damagedIdentifier]]) minTechLevel--; + + // reduce the minimum techlevel occasionally as a bonus.. + if (![UNIVERSE strict] && techlevel < minTechLevel && techlevel + 3 > minTechLevel) + { + unsigned day = i * 13 + (unsigned)floor([UNIVERSE getTime] / 86400.0); + unsigned char dayRnd = (day & 0xff) ^ system_seed.a; + OOTechLevelID originalMinTechLevel = minTechLevel; + + while (minTechLevel > 0 && minTechLevel > originalMinTechLevel - 3 && !(dayRnd & 7)) // bargain tech days every 1/8 days + { + dayRnd = dayRnd >> 2; + minTechLevel--; // occasional bonus items according to TL + } + } + + // check initial availability against options AND standard extras + if ([options containsObject:eqKey]) + { + isOK = YES; + [options removeObject:eqKey]; + } + + if (isOK) + { + if (techlevel < minTechLevel) isOK = NO; + if (![self canAddEquipment:eqKey]) isOK = NO; + if (isOK) [equipmentAllowed addObject:eqKey]; + } + + if ([eqKeyForSelectFacing isEqualToString:eqKey]) + { + skip = [equipmentAllowed count] - 1; // skip to this upgrade + unsigned available_facings = [shipyardInfo unsignedIntForKey:KEY_WEAPON_FACINGS]; + if (available_facings & WEAPON_FACING_FORWARD) [equipmentAllowed addObject:eqKey]; + if (available_facings & WEAPON_FACING_AFT) [equipmentAllowed addObject:eqKey]; + if (available_facings & WEAPON_FACING_PORT) [equipmentAllowed addObject:eqKey]; + if (available_facings & WEAPON_FACING_STARBOARD) [equipmentAllowed addObject:eqKey]; + } + } + + // GUI stuff + { + GuiDisplayGen *gui = [UNIVERSE gui]; + OOGUIRow start_row = GUI_ROW_EQUIPMENT_START; + OOGUIRow row = start_row; + unsigned facing_count = 0; + BOOL weaponMounted = NO; + + [gui clear]; + [gui setTitle:DESC(@"equip-title")]; + + [gui setText:[NSString stringWithFormat:DESC(@"equip-cash-@"), OOCredits(credits)] forRow: GUI_ROW_EQUIPMENT_CASH]; + + OOGUITabSettings tab_stops; + tab_stops[0] = 0; + tab_stops[1] = -380; + [gui setTabStops:tab_stops]; + + unsigned n_rows = GUI_MAX_ROWS_EQUIPMENT; + unsigned count = [equipmentAllowed count]; + + if (count > 0) + { + if (skip > 0) // lose the first row to Back <-- + { + unsigned int previous; + + if (count <= n_rows || skip < n_rows) + previous = 0; // single page + else + { + previous = skip - n_rows - 2; // multi-page + if (previous < 2) + previous = 0; // if only one previous item, just show it + } + + if (eqKeyForSelectFacing != nil) previous = -1; // ie. last index! + [gui setColor:[OOColor greenColor] forRow:row]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-back"), @" <-- ", nil] forRow:row]; + [gui setKey:[NSString stringWithFormat:@"More:%d", previous] forRow:row]; + row++; + } + for (i = skip; i < count && (row - start_row < (OOGUIRow)n_rows); i++) + { + NSString *eqKey = [equipmentAllowed stringAtIndex:i]; + OOEquipmentType *eqInfo = [OOEquipmentType equipmentTypeWithIdentifier:eqKey]; + OOCreditsQuantity pricePerUnit = [eqInfo price]; + NSString *desc = [NSString stringWithFormat:@" %@ ", [eqInfo name]]; + NSString *eq_key_damaged = [eqInfo damagedIdentifier]; + double price; + + if ([eqKey isEqual:@"EQ_FUEL"]) + { + price = (PLAYER_MAX_FUEL - fuel) * pricePerUnit; + } + else if ([eqKey isEqual:@"EQ_RENOVATION"]) + { + price = cunningFee(0.1 * [UNIVERSE tradeInValueForCommanderDictionary:[self commanderDataDictionary]]); + } + else price = pricePerUnit; + + price *= priceFactor; // increased prices at some stations + + // color repairs and renovation items orange + if ([self hasEquipmentItem:eq_key_damaged]) + { + desc = [NSString stringWithFormat:DESC(@"equip-repair-@"), desc]; + price /= 2.0; + [gui setColor:[OOColor orangeColor] forRow:row]; + } + if ([eqKey isEqual:@"EQ_RENOVATION"]) + { + [gui setColor:[OOColor orangeColor] forRow:row]; + } + + NSString *priceString = [NSString stringWithFormat:@" %@ ", OOCredits(price)]; + + if ([eqKeyForSelectFacing isEqualToString:eqKey]) + { + switch (facing_count) + { + case 0: + priceString = @""; + break; + + case 1: + desc = FORWARD_FACING_STRING; + weaponMounted=forward_weapon > WEAPON_NONE; + break; + + case 2: + desc = AFT_FACING_STRING; + weaponMounted=aft_weapon > WEAPON_NONE; + break; + + case 3: + desc = PORT_FACING_STRING; + weaponMounted=port_weapon > WEAPON_NONE; + break; + + case 4: + desc = STARBOARD_FACING_STRING; + weaponMounted=starboard_weapon > WEAPON_NONE; + break; + } + + facing_count++; + if(weaponMounted) + { + [gui setColor:[OOColor colorWithCalibratedRed:0.0f green:0.6f blue:0.0f alpha:1.0f] forRow:row]; + } + else + { + [gui setColor:[OOColor greenColor] forRow:row]; + } + } + [gui setKey:eqKey forRow:row]; + [gui setArray:[NSArray arrayWithObjects:desc, priceString, nil] forRow:row]; + row++; + } + if (i < count) + { + // just overwrite the last item :-) + [gui setColor:[OOColor greenColor] forRow:row - 1]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-more"), @" --> ", nil] forRow:row - 1]; + [gui setKey:[NSString stringWithFormat:@"More:%d", i - 1] forRow:row - 1]; + } + + [gui setSelectableRange:NSMakeRange(start_row,row - start_row)]; + + if ([gui selectedRow] != start_row) + [gui setSelectedRow:start_row]; + + if (eqKeyForSelectFacing != nil) + { + [gui setSelectedRow:start_row + ((skip > 0)? 1: 0)]; + } + + [self showInformationForSelectedUpgrade]; + + } + else + { + [gui setText:DESC(@"equip-no-equipment-available-for-purchase") forRow:GUI_ROW_NO_SHIPS align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_NO_SHIPS]; + + [gui setSelectableRange:NSMakeRange(0,0)]; + [gui setNoSelectedRow]; + [self showInformationForSelectedUpgrade]; + } + + [gui setShowTextCursor:NO]; + } + /* ends */ + + chosen_weapon_facing = WEAPON_FACING_NONE; + [self setShowDemoShips:NO]; + gui_screen = GUI_SCREEN_EQUIP_SHIP; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) setGuiToEquipShipScreen:(int)skip +{ + [self setGuiToEquipShipScreen:skip selectingFacingFor:nil]; +} + + +- (void) showInformationForSelectedUpgrade +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + NSString* eqKey = [gui selectedRowKey]; + int i; + + for (i = GUI_ROW_EQUIPMENT_DETAIL; i < GUI_MAX_ROWS; i++) + { + [gui setText:@"" forRow:i]; + [gui setColor:[OOColor greenColor] forRow:i]; + } + if (eqKey) + { + if (![eqKey hasPrefix:@"More:"]) + { + NSString* desc = [[OOEquipmentType equipmentTypeWithIdentifier:eqKey] descriptiveText]; + NSString* eq_key_damaged = [NSString stringWithFormat:@"%@_DAMAGED", eqKey]; + if ([self hasEquipmentItem:eq_key_damaged]) + desc = [NSString stringWithFormat:DESC(@"upgradeinfo-@-price-is-for-repairing"), desc]; + else if([eqKey hasSuffix:@"ENERGY_UNIT"] && ([self hasEquipmentItem:@"EQ_ENERGY_UNIT_DAMAGED"] || [self hasEquipmentItem:@"EQ_ENERGY_UNIT"] || [self hasEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT_DAMAGED"])) + desc = [NSString stringWithFormat:DESC(@"@-will-replace-other-energy"), desc]; + [gui addLongText:desc startingAtRow:GUI_ROW_EQUIPMENT_DETAIL align:GUI_ALIGN_LEFT]; + } + } +} + + +- (void) setGuiToIntroFirstGo: (BOOL) justCobra +{ + NSString *text; + GuiDisplayGen* gui = [UNIVERSE gui]; + int msgLine = 2; + if(justCobra==YES) + { + [[OOCacheManager sharedCache] flush]; // At first startup, a lot of stuff is cached + } + [gui clear]; + [gui setTitle:@"Oolite"]; + + if(justCobra==YES) + { + text = DESC(@"game-copyright"); + [gui setText:text forRow:17 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor whiteColor] forRow:17]; + + text = DESC(@"theme-music-credit"); + [gui setText:text forRow:19 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor grayColor] forRow:19]; + + text = DESC(@"load-previous-commander"); + [gui setText:text forRow:21 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor yellowColor] forRow:21]; + + // check for error messages from Resource Manager + [ResourceManager paths]; + NSString *errors = [ResourceManager errors]; + if (errors != nil) + { + int ms_start = msgLine; + int i = msgLine = [gui addLongText:errors startingAtRow:ms_start align:GUI_ALIGN_LEFT]; + for (i-- ; i >= ms_start ; i--) [gui setColor:[OOColor redColor] forRow:i]; + msgLine++; + } + + // check for messages from the command line + NSArray* arguments = [[NSProcessInfo processInfo] arguments]; + unsigned i; + for (i = 0; i < [arguments count]; i++) + { + if (([[arguments objectAtIndex:i] isEqual:@"-message"])&&(i < [arguments count] - 1)) + { + int ms_start = msgLine; + NSString* message = (NSString*)[arguments objectAtIndex: i + 1]; + int i = msgLine = [gui addLongText:message startingAtRow:ms_start align:GUI_ALIGN_CENTER]; + for (i-- ; i >= ms_start; i--) [gui setColor:[OOColor magentaColor] forRow:i]; + } + if ([[arguments objectAtIndex:i] isEqual:@"-showversion"]) + { + int ms_start = msgLine; + NSString *version = [NSString stringWithFormat:@"Version %@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]]; + int i = msgLine = [gui addLongText:version startingAtRow:ms_start align:GUI_ALIGN_CENTER]; + for (i-- ; i >= ms_start; i--) [gui setColor:[OOColor magentaColor] forRow:i]; + } + } + } + else + { + text = DESC(@"press-space-commander"); + [gui setText:text forRow:21 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor yellowColor] forRow:21]; + } + + [gui setShowTextCursor:NO]; + + [UNIVERSE setupIntroFirstGo: justCobra]; + + if (gui !=NULL) + { + gui_screen = (justCobra==YES) ? GUI_SCREEN_INTRO1 : GUI_SCREEN_INTRO2; + } + [[OOMusicController sharedController] playThemeMusic]; + + [self setShowDemoShips: YES]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) noteGuiChangeFrom:(OOGUIScreenID)fromScreen to:(OOGUIScreenID)toScreen +{ + if (fromScreen != toScreen) + { + [self doScriptEvent:@"guiScreenChanged" + withArgument:GUIScreenIDToString(toScreen) + andArgument:GUIScreenIDToString(fromScreen)]; + } +} + + +- (void) buySelectedItem +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + NSString* key = [gui selectedRowKey]; + + if ([key hasPrefix:@"More:"]) + { + int from_item = [[key componentsSeparatedByString:@":"] intAtIndex:1]; + + [self setGuiToEquipShipScreen:from_item]; + if ([gui selectedRow] < 0) + [gui setSelectedRow:GUI_ROW_EQUIPMENT_START]; + if (from_item == 0) + [gui setSelectedRow:GUI_ROW_EQUIPMENT_START + GUI_MAX_ROWS_EQUIPMENT - 1]; + [self showInformationForSelectedUpgrade]; + + return; + } + + NSString *itemText = [gui selectedRowText]; + + // FIXME: this is nuts, should be associating lines with keys in some sensible way. --Ahruman 20080311 + if ([itemText isEqual:FORWARD_FACING_STRING]) + chosen_weapon_facing = WEAPON_FACING_FORWARD; + if ([itemText isEqual:AFT_FACING_STRING]) + chosen_weapon_facing = WEAPON_FACING_AFT; + if ([itemText isEqual:PORT_FACING_STRING]) + chosen_weapon_facing = WEAPON_FACING_PORT; + if ([itemText isEqual:STARBOARD_FACING_STRING]) + chosen_weapon_facing = WEAPON_FACING_STARBOARD; + + OOCreditsQuantity old_credits = credits; + if ([self tryBuyingItem:key]) + { + if (credits == old_credits) + { + // we did get the item, but it didn't cost anything. CantBuy is associated with failiure to get the equipment + // better to use a generic menu sound for audio feedback. + [self playMenuNavigationDown]; + } + else + { + [self playBuyCommodity]; + } + [self doScriptEvent:@"playerBoughtEquipment" withArgument:key]; + [self setGuiToEquipShipScreen:0]; // show any change due to playerBoughtEquipment + // wind the clock forward by 10 minutes plus 10 minutes for every 60 credits spent + double time_adjust = (old_credits > credits) ? (old_credits - credits) : 0.0; + ship_clock_adjust += time_adjust + 600.0; + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + } + else + { + [self playCantBuyCommodity]; + } +} + + +- (BOOL) tryBuyingItem:(NSString *)eqKey +{ + // note this doesn't check the availability by tech-level + OOEquipmentType *eqType = [OOEquipmentType equipmentTypeWithIdentifier:eqKey]; + OOCreditsQuantity pricePerUnit = [eqType price]; + NSString *eqKeyDamaged = [eqType damagedIdentifier]; + double price = pricePerUnit; + double priceFactor = 1.0; + OOCargoQuantityDelta cargoSpace = max_cargo - current_cargo; + OOCreditsQuantity tradeIn = 0; + + // repairs cost 50% + if ([self hasEquipmentItem:eqKeyDamaged]) + { + price /= 2.0; + } + + if ([eqKey isEqualToString:@"EQ_RENOVATION"]) + { + price = cunningFee(0.1 * [UNIVERSE tradeInValueForCommanderDictionary:[self commanderDataDictionary]]); + } + + if (dockedStation) + { + priceFactor = [dockedStation equipmentPriceFactor]; + } + + price *= priceFactor; // increased prices at some stations + + if (price > credits) + { + return NO; + } + + if ([eqType isPrimaryWeapon] && chosen_weapon_facing == WEAPON_FACING_NONE) + { + [self setGuiToEquipShipScreen:0 selectingFacingFor:eqKey]; // reset + return YES; + } + + if ([eqType isPrimaryWeapon] && chosen_weapon_facing != WEAPON_FACING_NONE) + { + int chosen_weapon = WEAPON_NONE; + int current_weapon = WEAPON_NONE; + + if ([eqKey isEqualToString:@"EQ_WEAPON_TWIN_PLASMA_CANNON"]) + chosen_weapon = WEAPON_PLASMA_CANNON; + if ([eqKey isEqualToString:@"EQ_WEAPON_PULSE_LASER"]) + chosen_weapon = WEAPON_PULSE_LASER; + if ([eqKey isEqualToString:@"EQ_WEAPON_BEAM_LASER"]) + chosen_weapon = WEAPON_BEAM_LASER; + if ([eqKey isEqualToString:@"EQ_WEAPON_MINING_LASER"]) + chosen_weapon = WEAPON_MINING_LASER; + if ([eqKey isEqualToString:@"EQ_WEAPON_MILITARY_LASER"]) + chosen_weapon = WEAPON_MILITARY_LASER; + if ([eqKey isEqualToString:@"EQ_WEAPON_THARGOID_LASER"]) + chosen_weapon = WEAPON_THARGOID_LASER; + + switch (chosen_weapon_facing) + { + case WEAPON_FACING_FORWARD : + current_weapon = forward_weapon; + forward_weapon = chosen_weapon; + break; + case WEAPON_FACING_AFT : + current_weapon = aft_weapon; + aft_weapon = chosen_weapon; + break; + case WEAPON_FACING_PORT : + current_weapon = port_weapon; + port_weapon = chosen_weapon; + break; + case WEAPON_FACING_STARBOARD : + current_weapon = starboard_weapon; + starboard_weapon = chosen_weapon; + break; + } + + credits -= price; + + // refund here for current_weapon + /* BUG: equipment_priceFactor does not affect trade-ins. This means + that an equipment_priceFactor less than one can be exploited. + Analysis: price factor simply not being applied here. + Fix: trivial. + Acknowledgment: bug and fix both reported by Cmdr James on forum. + -- Ahruman 20070724 + */ + switch (current_weapon) + { + case WEAPON_PLASMA_CANNON : + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_WEAPON_TWIN_PLASMA_CANNON"]; + break; + case WEAPON_PULSE_LASER : + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_WEAPON_PULSE_LASER"]; + break; + case WEAPON_BEAM_LASER : + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_WEAPON_BEAM_LASER"]; + break; + case WEAPON_MINING_LASER : + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_WEAPON_MINING_LASER"]; + break; + case WEAPON_MILITARY_LASER : + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_WEAPON_MILITARY_LASER"]; + break; + case WEAPON_THARGOID_LASER : + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_WEAPON_THARGOID_LASER"]; + break; + case WEAPON_NONE : + break; + } + [self doTradeIn:tradeIn forPriceFactor:priceFactor]; + //if equipped, remove damaged weapon after repairs. + [self removeEquipmentItem:eqKeyDamaged]; + return YES; + } + + if ([eqType isMissileOrMine] && missiles >= max_missiles) + { + OOLog(@"equip.buy.mounted.failed.full", @"rejecting missile because already full"); + return NO; + } + + if ([eqKey isEqualToString:@"EQ_PASSENGER_BERTH"] && cargoSpace < 5) + { + return NO; + } + + if ([eqKey isEqualToString:@"EQ_FUEL"]) + { + OOCreditsQuantity creditsForRefuel = ([self fuelCapacity] - [self fuel]) * pricePerUnit; + if (credits >= creditsForRefuel) // Ensure we don't overflow + { + credits -= creditsForRefuel; + fuel = [self fuelCapacity]; + return YES; + } + else + { + return NO; + } + } + + // check energy unit replacement + if ([eqKey hasSuffix:@"ENERGY_UNIT"] && [self energyUnitType] != ENERGY_UNIT_NONE) + { + switch ([self energyUnitType]) + { + case ENERGY_UNIT_NAVAL : + [self removeEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT"]; + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_NAVAL_ENERGY_UNIT"] / 2; // 50 % refund + break; + case ENERGY_UNIT_NAVAL_DAMAGED : + [self removeEquipmentItem:@"EQ_NAVAL_ENERGY_UNIT_DAMAGED"]; + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_NAVAL_ENERGY_UNIT"] / 4; // half of the working one + case ENERGY_UNIT_NORMAL : + [self removeEquipmentItem:@"EQ_ENERGY_UNIT"]; + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_ENERGY_UNIT"] * 3 / 4; // 75 % refund + break; + case ENERGY_UNIT_NORMAL_DAMAGED : + [self removeEquipmentItem:@"EQ_ENERGY_UNIT_DAMAGED"]; + tradeIn = [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_ENERGY_UNIT"] * 3 / 8; // half of the working one + break; + + default : + break; + } + [self doTradeIn:tradeIn forPriceFactor:priceFactor]; + } + + // maintain ship + if ([eqKey isEqualToString:@"EQ_RENOVATION"]) + { + OOTechLevelID techLevel = NSNotFound; + if (dockedStation != nil) techLevel = [dockedStation equivalentTechLevel]; + if (techLevel == NSNotFound) techLevel = [[UNIVERSE generateSystemData:system_seed] unsignedIntForKey:KEY_TECHLEVEL]; + + credits -= price; + ship_trade_in_factor += 5 + techLevel; // you get better value at high-tech repair bases + if (ship_trade_in_factor > 100) ship_trade_in_factor = 100; + return YES; + } + + if ([eqKey hasSuffix:@"MISSILE"] || [eqKey hasSuffix:@"MINE"]) + { + ShipEntity* weapon = [[UNIVERSE newShipWithRole:eqKey] autorelease]; + if (weapon) OOLog(kOOLogBuyMountedOK, @"Got ship for mounted weapon role %@", eqKey); + else OOLog(kOOLogBuyMountedFailed, @"Could not find ship for mounted weapon role %@", eqKey); + + BOOL mounted_okay = [self mountMissile:weapon]; + if (mounted_okay) + { + credits -= price; + [self safeAllMissiles]; + [self sortMissiles]; + [self setActiveMissile:0]; + } + return mounted_okay; + } + + if ([eqKey isEqualToString:@"EQ_PASSENGER_BERTH"]) + { + max_passengers++; + max_cargo -= 5; + credits -= price; + return YES; + } + + if ([eqKey isEqualToString:@"EQ_PASSENGER_BERTH_REMOVAL"]) + { + max_passengers--; + max_cargo += 5; + credits -= price; + return YES; + } + + if ([eqKey isEqualToString:@"EQ_MISSILE_REMOVAL"]) + { + credits -= price; + [self safeAllMissiles]; + [self sortMissiles]; + unsigned i; + for (i = 0; i < missiles; i++) + { + ShipEntity* weapon = missile_entity[i]; + missile_entity[i] = nil; + if (weapon) + { + NSString* weapon_key = [weapon primaryRole]; + int weapon_value = (int)[UNIVERSE getPriceForWeaponSystemWithKey:weapon_key]; + tradeIn += weapon_value; + [weapon release]; + } + } + missiles = 0; + [self doTradeIn:tradeIn forPriceFactor:priceFactor]; + return YES; + } + + if ([self canAddEquipment:eqKey]) + { + credits -= price; + [self addEquipmentItem:eqKey]; + return YES; + } + + return NO; +} + + +-(void) doTradeIn:(OOCreditsQuantity)tradeInValue forPriceFactor:(double)priceFactor +{ + if (tradeInValue != 0) + { + if (priceFactor < 1.0f) tradeInValue *= priceFactor; + credits += tradeInValue; + } +} + + +- (void) calculateCurrentCargo +{ + unsigned i; + unsigned n_commodities = [shipCommodityData count]; + OOCargoQuantity in_hold[n_commodities]; + + // following works whether docked or not + + for (i = 0; i < n_commodities; i++) + in_hold[i] = [[shipCommodityData arrayAtIndex:i] intAtIndex:MARKET_QUANTITY]; + for (i = 0; i < [cargo count]; i++) + { + ShipEntity *container = (ShipEntity *)[cargo objectAtIndex:i]; + in_hold[[container commodityType]] += [container commodityAmount]; + } + + current_cargo = 0; // for calculating remaining hold space + + for (i = 0; i < n_commodities; i++) + { + if ([[shipCommodityData arrayAtIndex:i] intAtIndex:MARKET_UNITS] == UNITS_TONS) + { + current_cargo += in_hold[i]; + } + } +} + + +- (NSMutableArray *) localMarket +{ + StationEntity *station = nil; + NSMutableArray *localMarket = nil; + + if ([self isDocked]) station = dockedStation; + else station = [UNIVERSE station]; + localMarket = [station localMarket]; + if (localMarket == nil) localMarket = [station initialiseLocalMarketWithSeed:system_seed andRandomFactor:market_rnd]; + + return localMarket; +} + + +- (void) setGuiToMarketScreen +{ + NSArray *localMarket = [self localMarket]; + + // fix problems with economies in witch-space + if ([UNIVERSE station] == nil) + { + unsigned i; + NSMutableArray *ourEconomy = [NSMutableArray arrayWithArray:[UNIVERSE commodityDataForEconomy:0 andStation:(StationEntity*)nil andRandomFactor:0]]; + for (i = 0; i < [ourEconomy count]; i++) + { + NSMutableArray *commodityInfo = [NSMutableArray arrayWithArray:[ourEconomy objectAtIndex:i]]; + [commodityInfo replaceObjectAtIndex:MARKET_PRICE withObject:[NSNumber numberWithInt: 0]]; + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt: 0]]; + [ourEconomy replaceObjectAtIndex:i withObject:[NSArray arrayWithArray:commodityInfo]]; + } + localMarket = [NSArray arrayWithArray:ourEconomy]; + } + + // GUI stuff + { + GuiDisplayGen *gui = [UNIVERSE gui]; + OOGUIRow start_row = GUI_ROW_MARKET_START; + OOGUIRow row = start_row; + unsigned i; + unsigned n_commodities = [shipCommodityData count]; + OOCargoQuantity in_hold[n_commodities]; + NSArray *marketDef = nil; + + // following changed to work whether docked or not + + for (i = 0; i < n_commodities; i++) + in_hold[i] = [(NSNumber *)[(NSArray *)[shipCommodityData objectAtIndex:i] objectAtIndex:MARKET_QUANTITY] intValue]; + for (i = 0; i < [cargo count]; i++) + { + ShipEntity *container = (ShipEntity *)[cargo objectAtIndex:i]; + in_hold[[container commodityType]] += [container commodityAmount]; + } + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"@-commodity-market"),[UNIVERSE getSystemName:system_seed]]]; + + OOGUITabSettings tab_stops; + tab_stops[0] = 0; + tab_stops[1] = 192; + tab_stops[2] = 288; + tab_stops[3] = 384; + [gui setTabStops:tab_stops]; + + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_MARKET_KEY]; + [gui setArray:[NSArray arrayWithObjects: DESC(@"commodity-column-title"), DESC(@"price-column-title"), + DESC(@"for-sale-column-title"), DESC(@"in-hold-column-title"), nil] forRow:GUI_ROW_MARKET_KEY]; + + current_cargo = 0; // for calculating remaining hold space + + for (i = 0; i < n_commodities; i++) + { + marketDef = [localMarket arrayAtIndex:i]; + + NSString* desc = [NSString stringWithFormat:@" %@ ", CommodityDisplayNameForCommodityArray(marketDef)]; + OOCargoQuantity available_units = [marketDef unsignedIntAtIndex:MARKET_QUANTITY]; + OOCargoQuantity units_in_hold = in_hold[i]; + OOCreditsQuantity pricePerUnit = [marketDef unsignedIntAtIndex:MARKET_PRICE]; + OOMassUnit unit = [marketDef unsignedIntAtIndex:MARKET_UNITS]; + + NSString *available = (available_units > 0) ? OOPadStringTo([NSString stringWithFormat:@"%d",available_units],3.0) : OOPadStringTo(DESC(@"commodity-quantity-none"),2.4); + NSString *price = OOPadStringTo([NSString stringWithFormat:@" %.1f ",0.1 * pricePerUnit],7.0); + NSString *owned = (units_in_hold > 0) ? OOPadStringTo([NSString stringWithFormat:@"%d",units_in_hold],3.0) : OOPadStringTo(DESC(@"commodity-quantity-none"),2.4); + NSString *units = DisplayStringForMassUnit(unit); + NSString *units_available = [NSString stringWithFormat:@" %@ %@ ",available, units]; + NSString *units_owned = [NSString stringWithFormat:@" %@ %@ ",owned, units]; + + if (unit == UNITS_TONS) + current_cargo += units_in_hold; + + [gui setKey:[NSString stringWithFormat:@"%d",i] forRow:row]; + [gui setArray:[NSArray arrayWithObjects: desc, price, units_available, units_owned, nil] forRow:row++]; + } + + if ([cargo count] > 0) + current_cargo = ([cargo count] <= max_cargo) ? [cargo count] : max_cargo; // actually count the containers and things (may be > max_cargo) + + [gui setText:[NSString stringWithFormat:DESC(@"cash-@-load-d-of-d"), OOCredits(credits), current_cargo, max_cargo] forRow: GUI_ROW_MARKET_CASH]; + + if ([self status] == STATUS_DOCKED) // can only buy or sell in dock + { + [gui setSelectableRange:NSMakeRange(start_row,row - start_row)]; + if (([gui selectedRow] < start_row)||([gui selectedRow] >=row)) + [gui setSelectedRow:start_row]; + } + else + { + [gui setNoSelectedRow]; + } + + + [gui setShowTextCursor:NO]; + } + + [[UNIVERSE gameView] clearMouse]; + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_MARKET; + + [self setShowDemoShips:NO]; + [UNIVERSE setDisplayText:YES]; + [UNIVERSE setDisplayCursor:[self status] == STATUS_DOCKED]; + [UNIVERSE setViewDirection:VIEW_GUI_DISPLAY]; + + [self noteGuiChangeFrom:oldScreen to:gui_screen]; +} + + +- (OOGUIScreenID) guiScreen +{ + return gui_screen; +} + + +- (BOOL) marketFlooded:(int) index +{ + NSArray *commodityArray = [[self localMarket] arrayAtIndex:index]; + int available_units = [commodityArray intAtIndex:MARKET_QUANTITY]; + + return (available_units >= 127); +} + + +- (BOOL) tryBuyingCommodity:(int) index all:(BOOL) all +{ + if (![self isDocked]) return NO; // can't buy if not docked. + + NSMutableArray *localMarket = [self localMarket]; + NSArray *commodityArray = [localMarket objectAtIndex:index]; + OOCreditsQuantity pricePerUnit = [commodityArray unsignedIntAtIndex:MARKET_PRICE]; + OOMassUnit unit = [(NSNumber *)[commodityArray objectAtIndex:MARKET_UNITS] intValue]; + + if ((specialCargo != nil)&&(unit == UNITS_TONS)) + return NO; // can't buy tons of stuff when carrying a specialCargo + + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + NSMutableArray* manifest_commodity = [NSMutableArray arrayWithArray:[manifest arrayAtIndex:index]]; + NSMutableArray* market_commodity = [NSMutableArray arrayWithArray:[localMarket arrayAtIndex:index]]; + int manifest_quantity = [manifest_commodity intAtIndex:MARKET_QUANTITY]; + int market_quantity = [market_commodity intAtIndex:MARKET_QUANTITY]; + + int purchase = all ? 127 : 1; + if (purchase > market_quantity) + purchase = market_quantity; // limit to what's available + if (purchase * pricePerUnit > credits) + purchase = floor (credits / pricePerUnit); // limit to what's affordable + if (purchase + current_cargo > (unit == UNITS_TONS ? max_cargo : 10000)) + purchase = max_cargo - current_cargo; // limit to available cargo space + if (purchase == 0) + return NO; // stop if that results in nothing to be bought + + manifest_quantity += purchase; + market_quantity -= purchase; + credits -= pricePerUnit * purchase; + if (unit == UNITS_TONS) + current_cargo += purchase; + + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:manifest_quantity]]; + [market_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:market_quantity]]; + [manifest replaceObjectAtIndex:index withObject:[NSArray arrayWithArray:manifest_commodity]]; + [localMarket replaceObjectAtIndex:index withObject:[NSArray arrayWithArray:market_commodity]]; + + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + + return YES; +} + + +- (BOOL) trySellingCommodity:(int) index all:(BOOL) all +{ + if (![self isDocked]) return NO; // can't sell if not docked. + + NSMutableArray *localMarket = [self localMarket]; + int available_units = [[shipCommodityData arrayAtIndex:index] intAtIndex:MARKET_QUANTITY]; + int pricePerUnit = [[localMarket arrayAtIndex:index] intAtIndex:MARKET_PRICE]; + + if (available_units == 0) return NO; + + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + NSMutableArray* manifest_commodity = [NSMutableArray arrayWithArray:[manifest arrayAtIndex:index]]; + NSMutableArray* market_commodity = [NSMutableArray arrayWithArray:[localMarket arrayAtIndex:index]]; + int manifest_quantity = [manifest_commodity intAtIndex:MARKET_QUANTITY]; + int market_quantity = [market_commodity intAtIndex:MARKET_QUANTITY]; + + int sell = all ? 127 : 1; + if (sell > available_units) + sell = available_units; // limit to what's in the hold + if (sell + market_quantity > 127) + sell = 127 - market_quantity; // avoid flooding the market + if (sell == 0) + return NO; // stop if that results in nothing to be sold + + current_cargo -= sell; + manifest_quantity -= sell; + market_quantity += sell; + credits += pricePerUnit * sell; + + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:manifest_quantity]]; + [market_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:market_quantity]]; + [manifest replaceObjectAtIndex:index withObject:[NSArray arrayWithArray:manifest_commodity]]; + [localMarket replaceObjectAtIndex:index withObject:[NSArray arrayWithArray:market_commodity]]; + + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + + return YES; +} + + +- (BOOL) isMining +{ + return using_mining_laser; +} + + +- (BOOL) isSpeechOn +{ + return isSpeechOn; +} + + +- (BOOL) canAddEquipment:(NSString *)equipmentKey +{ + if ([equipmentKey isEqual:@"EQ_RENOVATION"] && !((75 <= ship_trade_in_factor) && (ship_trade_in_factor < 85))) return NO; + if (![super canAddEquipment:equipmentKey]) return NO; + + NSArray *conditions = [[OOEquipmentType equipmentTypeWithIdentifier:equipmentKey] conditions]; + if (conditions != nil && ![self scriptTestConditions:conditions]) return NO; + + return YES; +} + + +- (void) addEquipmentItem:(NSString *)equipmentKey +{ + // deal with trumbles.. + if ([equipmentKey isEqualToString:@"EQ_TRUMBLE"]) + { + /* Bug fix: must return here if eqKey == @"EQ_TRUMBLE", even if + trumbleCount >= 1. Otherwise, the player becomes immune to + trumbles. See comment in -setCommanderDataFromDictionary: for more + details. + -- Ahruman 2008-12-04 + */ + if (trumbleCount < 1) + { + [self addTrumble:trumble[ranrot_rand() % PLAYER_MAX_TRUMBLES]]; // first one! + } + return; + } + + if ([equipmentKey isEqual:@"EQ_ADVANCED_COMPASS"]) + { + [self setCompassMode:COMPASS_MODE_PLANET]; + } + + if ([equipmentKey isEqual:@"EQ_CARGO_BAY"]) + { + max_cargo += extra_cargo; + } + + [super addEquipmentItem:equipmentKey]; +} + + +- (void) addEquipmentFromCollection:(id)equipment +{ + NSDictionary *dict = nil; + NSEnumerator *eqEnum = nil; + NSString *eqDesc = nil; + + // Pass 1: Load the entire collection. + if ([equipment isKindOfClass:[NSDictionary class]]) + { + dict = equipment; + eqEnum = [equipment keyEnumerator]; + } + else if ([equipment isKindOfClass:[NSArray class]] || [equipment isKindOfClass:[NSSet class]]) + { + eqEnum = [equipment objectEnumerator]; + } + else if ([equipment isKindOfClass:[NSString class]]) + { + eqEnum = [[NSArray arrayWithObject:equipment] objectEnumerator]; + } + else + { + return; + } + + while ((eqDesc = [eqEnum nextObject])) + { + /* Bug workaround: extra_equipment should never contain EQ_TRUMBLE, + which is basically a magic flag passed to awardEquipment: to infect + the player. However, prior to Oolite 1.70.1, if the player had a + trumble infection and awardEquipment:EQ_TRUMBLE was called, an + EQ_TRUMBLE would be added to the equipment list. Subsequent calls + to awardEquipment:EQ_TRUMBLE would exit early because there was an + EQ_TRUMBLE in the equipment list. as a result, it would no longer + be possible to infect the player after the current infection ended. + + The bug is fixed in 1.70.1. The following line is to fix old saved + games which had been "corrupted" by the bug. + -- Ahruman 2007-12-04 + */ + if ([eqDesc isEqualToString:@"EQ_TRUMBLE"]) continue; + + // Traditional form is a dictionary of booleans; we only accept those where the value is true. + if (dict != nil && ![dict boolForKey:eqDesc]) continue; + + // We need to add the entire collection without validation first and then remove the items that are + // not compliant (like items that do not satisfy the requiresEquipment criterion). This is to avoid + // unintentionally excluding valid equipment, just because the required equipment existed but had + // not been yet added to the equipment list at the time of the canAddEquipment validation check. + // Nikos, 20080817. + [self addEquipmentItem:eqDesc withValidation:NO]; + } + + // Pass 2: Remove items that do not satisfy validation criteria (like requires_equipment etc.). + if ([equipment isKindOfClass:[NSDictionary class]]) + { + dict = equipment; + eqEnum = [equipment keyEnumerator]; + } + else if ([equipment isKindOfClass:[NSArray class]] || [equipment isKindOfClass:[NSSet class]]) + { + eqEnum = [equipment objectEnumerator]; + } + else if ([equipment isKindOfClass:[NSString class]]) + { + eqEnum = [[NSArray arrayWithObject:equipment] objectEnumerator]; + } + // Now remove items that should not be in the equipment list. + while ((eqDesc = [eqEnum nextObject])) + { + if (![self equipmentValidToAdd:eqDesc]) + { + [self removeEquipmentItem:eqDesc]; + } + } +} + + +- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeMissiles:(BOOL)includeMissiles +{ + // Check basic equipment the normal way. + if ([super hasOneEquipmentItem:itemKey includeMissiles:NO]) return YES; + + // Custom handling for player missiles. + if (includeMissiles) + { + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if ([[self missileForStation:i] hasPrimaryRole:itemKey]) return YES; + } + } + + return NO; +} + + +- (BOOL) hasPrimaryWeapon:(OOWeaponType)weaponType +{ + if (forward_weapon == weaponType || aft_weapon == weaponType) return YES; + if (port_weapon == weaponType || starboard_weapon == weaponType) return YES; + + return [super hasPrimaryWeapon:weaponType]; +} + + +- (void) removeExternalStore:(OOEquipmentType *)eqType +{ + NSString *identifier = [eqType identifier]; + + // Look for matching missile. + unsigned i; + for (i = 0; i < max_missiles; i++) + { + if ([[self missileForStation:i] hasPrimaryRole:identifier]) + { + // Remove the missile. + [missile_entity[i] release]; + missile_entity[i] = nil; + + // If it's the currently selected missile, deselect it. + if (i == [self activeMissile]) + { + [self selectNextMissile]; + } + + // Just remove one at a time. + break; + } + } + + missiles = [self countMissiles]; +} + + +- (unsigned) passengerCount +{ + return [passengers count]; +} + + +- (unsigned) passengerCapacity +{ + return max_passengers; +} + + +- (unsigned) missileCapacity +{ + return max_missiles; +} + + +- (BOOL) hasHostileTarget +{ + ShipEntity *playersTarget = [self primaryTarget]; + return ([playersTarget isShip] && [playersTarget hasHostileTarget] && [playersTarget primaryTarget] == self); +} + + +- (void) receiveCommsMessage:(NSString *) message_text +{ + [UNIVERSE addCommsMessage:message_text forCount:4.5]; +} + + +- (void) getFined +{ + if (legalStatus == 0) return; // nothing to pay for + + OOGovernmentID local_gov = [[UNIVERSE currentSystemData] intForKey:KEY_GOVERNMENT]; + if ([UNIVERSE inInterstellarSpace]) local_gov = 1; // equivalent to Feudal. I'm assuming any station in interstellar space is military. -- Ahruman 2008-05-29 + OOCreditsQuantity fine = 500 + ((local_gov < 2)||(local_gov > 5))? 500:0; + fine *= legalStatus; + if (fine > credits) + { + int payback = (int)(legalStatus * credits / fine); + legalStatus -= payback; + credits = 0; + } + else + { + legalStatus = 0; + credits -= fine; + } + + // one of the fined-@-credits strings includes expansion tokens + NSString* fined_message = [NSString stringWithFormat:ExpandDescriptionForCurrentSystem(DESC(@"fined-@-credits")), OOStringFromDeciCredits(fine, YES, NO)]; + [UNIVERSE addCommsMessage:fined_message forCount:8 andShowComms:NO]; + ship_clock_adjust = 24 * 3600; // take up a day + if (gui_screen != GUI_SCREEN_STATUS) + { + [self setGuiToStatusScreen]; + } +} + + +- (void) setDefaultViewOffsets +{ + float halfLength = 0.5f * (boundingBox.max.z - boundingBox.min.z); + float halfWidth = 0.5f * (boundingBox.max.x - boundingBox.min.x); + + forwardViewOffset = make_vector(0.0f, 0.0f, boundingBox.max.z - halfLength); + aftViewOffset = make_vector(0.0f, 0.0f, boundingBox.min.z + halfLength); + portViewOffset = make_vector(boundingBox.min.x + halfWidth, 0.0f, 0.0f); + starboardViewOffset = make_vector(boundingBox.max.x - halfWidth, 0.0f, 0.0f); + customViewOffset = kZeroVector; +} + + +- (void) setDefaultCustomViews +{ + NSArray *customViews = [[[OOShipRegistry sharedRegistry] shipInfoForKey:@"cobra3-player"] arrayForKey:@"custom_views"]; + + [_customViews release]; + _customViews = nil; + _customViewIndex = 0; + if (customViews != nil) + { + _customViews = [customViews retain]; + } +} + + +- (Vector) weaponViewOffset +{ + switch (currentWeaponFacing) + { + case VIEW_FORWARD: + return forwardViewOffset; + case VIEW_AFT: + return aftViewOffset; + case VIEW_PORT: + return portViewOffset; + case VIEW_STARBOARD: + return starboardViewOffset; + case VIEW_CUSTOM: + return customViewOffset; + + case VIEW_NONE: + case VIEW_GUI_DISPLAY: + case VIEW_BREAK_PATTERN: + break; + } + return kZeroVector; +} + + +- (void) setUpTrumbles +{ + NSMutableString* trumbleDigrams = [NSMutableString stringWithCapacity:256]; + unichar xchar = (unichar)0; + unichar digramchars[2]; + + while ([trumbleDigrams length] < PLAYER_MAX_TRUMBLES + 2) + { + if ((player_name)&&[player_name length]) + [trumbleDigrams appendFormat:@"%@%@", player_name, [[self mesh] modelName]]; + else + [trumbleDigrams appendString:@"Some Random Text!"]; + } + int i; + for (i = 0; i < PLAYER_MAX_TRUMBLES; i++) + { + digramchars[0] = ([trumbleDigrams characterAtIndex:i] & 0x007f) | 0x0020; + digramchars[1] = (([trumbleDigrams characterAtIndex:i + 1] ^ xchar) & 0x007f) | 0x0020; + xchar = digramchars[0]; + NSString* digramstring = [NSString stringWithCharacters:digramchars length:2]; + if (trumble[i]) + [trumble[i] release]; + trumble[i] = [[OOTrumble alloc] initForPlayer:self digram:digramstring]; + } + + trumbleCount = 0; + + trumbleAppetiteAccumulator = 0.0f; +} + + +- (void) addTrumble:(OOTrumble*) papaTrumble +{ + if (trumbleCount >= PLAYER_MAX_TRUMBLES) + { + return; + } + OOTrumble* trumblePup = trumble[trumbleCount]; + [trumblePup spawnFrom:papaTrumble]; + trumbleCount++; +} + + +- (void) removeTrumble:(OOTrumble*) deadTrumble +{ + if (trumbleCount <= 0) + { + return; + } + OOUInteger trumble_index = NSNotFound; + OOUInteger i; + + for (i = 0; (trumble_index == NSNotFound)&&(i < trumbleCount); i++) + { + if (trumble[i] == deadTrumble) + trumble_index = i; + } + if (trumble_index == NSNotFound) + { + OOLog(@"trumble.zombie", @"DEBUG can't get rid of inactive trumble %@", deadTrumble); + return; + } + trumbleCount--; // reduce number of trumbles + trumble[trumble_index] = trumble[trumbleCount]; // swap with the current last trumble + trumble[trumbleCount] = deadTrumble; // swap with the current last trumble +} + + +- (OOTrumble**) trumbleArray +{ + return trumble; +} + + +- (OOUInteger) trumbleCount +{ + return trumbleCount; +} + + +- (id)trumbleValue +{ + NSString *namekey = [NSString stringWithFormat:@"%@-humbletrash", player_name]; + int trumbleHash; + + clear_checksum(); + [self mungChecksumWithNSString:player_name]; + munge_checksum((int)credits); + munge_checksum(ship_kills); + trumbleHash = munge_checksum(trumbleCount); + + [[NSUserDefaults standardUserDefaults] setInteger:trumbleHash forKey:namekey]; + + int i; + NSMutableArray* trumbleArray = [NSMutableArray arrayWithCapacity:PLAYER_MAX_TRUMBLES]; + for (i = 0; i < PLAYER_MAX_TRUMBLES; i++) + { + [trumbleArray addObject:[trumble[i] dictionary]]; + } + + return [NSArray arrayWithObjects:[NSNumber numberWithInt:trumbleCount],[NSNumber numberWithInt:trumbleHash], trumbleArray, nil]; +} + + +- (void) setTrumbleValueFrom:(NSObject*) trumbleValue +{ + BOOL info_failed = NO; + int trumbleHash; + int putativeHash = 0; + int putativeNTrumbles = 0; + NSArray* putativeTrumbleArray = nil; + int i; + NSString* namekey = [NSString stringWithFormat:@"%@-humbletrash", player_name]; + + [self setUpTrumbles]; + + if (trumbleValue) + { + BOOL possible_cheat = NO; + if (![trumbleValue isKindOfClass:[NSArray class]]) + info_failed = YES; + else + { + NSArray* values = (NSArray*) trumbleValue; + if ([values count] >= 1) + putativeNTrumbles = [values intAtIndex:0]; + if ([values count] >= 2) + putativeHash = [values intAtIndex:1]; + if ([values count] >= 3) + putativeTrumbleArray = [values arrayAtIndex:2]; + } + // calculate a hash for the putative values + clear_checksum(); + [self mungChecksumWithNSString:player_name]; + munge_checksum((int)credits); + munge_checksum(ship_kills); + trumbleHash = munge_checksum(putativeNTrumbles); + + if (putativeHash != trumbleHash) + info_failed = YES; + + if (info_failed) + { + OOLog(@"cheat.tentative", @"POSSIBLE CHEAT DETECTED"); + possible_cheat = YES; + } + + for (i = 1; (info_failed)&&(i < PLAYER_MAX_TRUMBLES); i++) + { + // try to determine trumbleCount from the key in the saved game + clear_checksum(); + [self mungChecksumWithNSString:player_name]; + munge_checksum((int)credits); + munge_checksum(ship_kills); + trumbleHash = munge_checksum(i); + if (putativeHash == trumbleHash) + { + info_failed = NO; + putativeNTrumbles = i; + } + } + + if (possible_cheat && !info_failed) + OOLog(@"cheat.verified", @"CHEAT DEFEATED - that's not the way to get rid of trumbles!"); + } + + if (info_failed && [[NSUserDefaults standardUserDefaults] objectForKey:namekey]) + { + // try to determine trumbleCount from the key in user defaults + putativeHash = [[NSUserDefaults standardUserDefaults] integerForKey:namekey]; + for (i = 1; (info_failed)&&(i < PLAYER_MAX_TRUMBLES); i++) + { + clear_checksum(); + [self mungChecksumWithNSString:player_name]; + munge_checksum((int)credits); + munge_checksum(ship_kills); + trumbleHash = munge_checksum(i); + if (putativeHash == trumbleHash) + { + info_failed = NO; + putativeNTrumbles = i; + } + } + + if (!info_failed) + OOLog(@"cheat.verified", @"CHEAT DEFEATED - that's not the way to get rid of trumbles!"); + } + // at this stage we've done the best we can to stop cheaters + trumbleCount = putativeNTrumbles; + + if ((putativeTrumbleArray != nil) && ([putativeTrumbleArray count] == PLAYER_MAX_TRUMBLES)) + { + for (i = 0; i < PLAYER_MAX_TRUMBLES; i++) + [trumble[i] setFromDictionary:(NSDictionary *)[putativeTrumbleArray objectAtIndex:i]]; + } + + clear_checksum(); + [self mungChecksumWithNSString:player_name]; + munge_checksum((int)credits); + munge_checksum(ship_kills); + trumbleHash = munge_checksum(trumbleCount); + + [[NSUserDefaults standardUserDefaults] setInteger:trumbleHash forKey:namekey]; +} + + +- (void) mungChecksumWithNSString:(NSString*) str +{ + if (!str) + return; + int i; + int len = [str length]; + for (i = 0; i < len; i++) + munge_checksum((int)[str characterAtIndex:i]); +} + + +- (NSString *)screenModeStringForWidth:(unsigned)inWidth height:(unsigned)inHeight refreshRate:(float)inRate +{ + if (0.0f != inRate) + { + return [NSString stringWithFormat:DESC(@"gameoptions-fullscreen-mode-d-by-d-at-g-hz"), inWidth, inHeight, inRate]; + } + else + { + return [NSString stringWithFormat:DESC(@"gameoptions-fullscreen-mode-d-by-d"), inWidth, inHeight]; + } +} + + +- (void) suppressTargetLost +{ + suppressTargetLost = YES; +} + + +- (void) setScoopsActive +{ + scoopsActive = YES; +} + +// override shipentity addTarget to implement target_memory +- (void) addTarget:(Entity *) targetEntity +{ + if ([self status] != STATUS_IN_FLIGHT && [self status] != STATUS_WITCHSPACE_COUNTDOWN) return; + if (targetEntity == self) return; + + [super addTarget:targetEntity]; + +#if WORMHOLE_SCANNER + if ([targetEntity isWormhole]) + { + assert ([self hasEquipmentItem:@"EQ_WORMHOLE_SCANNER"]); + [self addScannedWormhole:(WormholeEntity*)targetEntity]; + } +#endif + + if ([self hasEquipmentItem:@"EQ_TARGET_MEMORY"]) + { + int i = 0; + BOOL foundSlot = NO; + // if targeted previously use that memory space + for (i = 0; i < PLAYER_TARGET_MEMORY_SIZE; i++) + { + if (primaryTarget == target_memory[i]) + { + target_memory_index = i; + foundSlot = YES; + break; + } + } + // find and use a blank space in memory + for (i = 0; i < PLAYER_TARGET_MEMORY_SIZE; i++) + { + if (target_memory[target_memory_index] == NO_TARGET) + { + target_memory[target_memory_index] = primaryTarget; + foundSlot = YES; + break; + } + target_memory_index = (target_memory_index + 1) % PLAYER_TARGET_MEMORY_SIZE; + } + if (!foundSlot) + { + // use the next memory space + target_memory_index = (target_memory_index + 1) % PLAYER_TARGET_MEMORY_SIZE; + target_memory[target_memory_index] = primaryTarget; + } + } + + if (ident_engaged) + { + [self playIdentLockedOn]; + [self printIdentLockedOnForMissile:NO]; + } + else if( [targetEntity isShip] ) // Only let missiles target-lock onto ships + { + if ([missile_entity[activeMissile] isMissile]) + { + missile_status = MISSILE_STATUS_TARGET_LOCKED; + [missile_entity[activeMissile] addTarget:targetEntity]; + [self playMissileLockedOn]; + [self printIdentLockedOnForMissile:YES]; + } + else // It's a mine or something + { + missile_status = MISSILE_STATUS_ARMED; + [self playIdentLockedOn]; + [self printIdentLockedOnForMissile:NO]; + } + } +} + + +- (void) clearTargetMemory +{ + int i = 0; + for (i = 0; i < PLAYER_TARGET_MEMORY_SIZE; i++) + target_memory[i] = NO_TARGET; + target_memory_index = 0; +} + + +- (BOOL) moveTargetMemoryBy:(int)delta +{ + unsigned i = 0; + while (i++ < PLAYER_TARGET_MEMORY_SIZE) // limit loops + { + target_memory_index += delta; + while (target_memory_index < 0) target_memory_index += PLAYER_TARGET_MEMORY_SIZE; + while (target_memory_index >= PLAYER_TARGET_MEMORY_SIZE) target_memory_index -= PLAYER_TARGET_MEMORY_SIZE; + + int targ_id = target_memory[target_memory_index]; + ShipEntity* potential_target = [UNIVERSE entityForUniversalID: targ_id]; + + if ((potential_target)&&(potential_target->isShip)) + { + if (potential_target->zero_distance < SCANNER_MAX_RANGE2) + { + [super addTarget:potential_target]; + if (missile_status != MISSILE_STATUS_SAFE) + { + if( [missile_entity[activeMissile] isMissile]) + { + missile_status = MISSILE_STATUS_TARGET_LOCKED; + [self printIdentLockedOnForMissile:YES]; + } + else + { + missile_status = MISSILE_STATUS_ARMED; + [self playIdentLockedOn]; + [self printIdentLockedOnForMissile:NO]; + } + } + else + { + [self printIdentLockedOnForMissile:NO]; + } + [self playTargetSwitched]; + return YES; + } + } + else + target_memory[target_memory_index] = NO_TARGET; // tidy up + } + + [self playNoTargetInMemory]; + return NO; +} + + +- (void) printIdentLockedOnForMissile:(BOOL)missile +{ + NSString *fmt = nil; + + if (missile) fmt = DESC(@"missile-locked-onto-@"); + else fmt = DESC(@"ident-locked-onto-@"); + + [UNIVERSE addMessage:[NSString stringWithFormat:fmt, [[self primaryTarget] identFromShip:self]] + forCount:4.5]; +} + + +- (Quaternion)customViewQuaternion +{ + return customViewQuaternion; +} + + +- (OOMatrix)customViewMatrix +{ + return customViewMatrix; +} + + +- (Vector)customViewOffset +{ + return customViewOffset; +} + + +- (Vector)customViewForwardVector +{ + return customViewForwardVector; +} + + +- (Vector)customViewUpVector +{ + return customViewUpVector; +} + + +- (Vector)customViewRightVector +{ + return customViewRightVector; +} + + +- (NSString*)customViewDescription +{ + return customViewDescription; +} + +- (void)setCustomViewDataFromDictionary:(NSDictionary *)viewDict +{ + customViewMatrix = kIdentityMatrix; + customViewOffset = kZeroVector; + if (viewDict == nil) return; + + customViewQuaternion = [viewDict quaternionForKey:@"view_orientation"]; + + customViewRightVector = vector_right_from_quaternion(customViewQuaternion); + customViewUpVector = vector_up_from_quaternion(customViewQuaternion); + customViewForwardVector = vector_forward_from_quaternion(customViewQuaternion); + + Quaternion q1 = customViewQuaternion; + q1.w = -q1.w; + customViewMatrix = OOMatrixForQuaternionRotation(q1); + + customViewOffset = [viewDict vectorForKey:@"view_position"]; + customViewDescription = [viewDict stringForKey:@"view_description"]; + + NSString *facing = [[viewDict stringForKey:@"weapon_facing"] lowercaseString]; + if ([facing isEqual:@"aft"]) + { + currentWeaponFacing = VIEW_AFT; + } + else if ([facing isEqual:@"port"]) + { + currentWeaponFacing = VIEW_PORT; + } + else if ([facing isEqual:@"starboard"]) + { + currentWeaponFacing = VIEW_STARBOARD; + } + else if ([facing isEqual:@"forward"]) + { + currentWeaponFacing = VIEW_FORWARD; + } + // if the weapon facing is unset / unknown, + // don't change current weapon facing! +} + + +- (BOOL)showInfoFlag +{ + return show_info_flag; +} + + +- (NSArray *) worldScriptNames +{ + return [worldScripts allKeys]; +} + + +- (NSDictionary *) worldScriptsByName +{ + return [[worldScripts copy] autorelease]; +} + + +- (void) doScriptEvent:(NSString *)message withArguments:(NSArray *)arguments +{ + [super doScriptEvent:message withArguments:arguments]; + [self doWorldScriptEvent:message withArguments:arguments]; +} + + +- (void) doWorldScriptEvent:(NSString *)message withArguments:(NSArray *)arguments +{ + NSEnumerator *scriptEnum; + OOScript *theScript; + + for (scriptEnum = [worldScripts objectEnumerator]; (theScript = [scriptEnum nextObject]); ) + { + [theScript doEvent:message withArguments:arguments]; + } +} + + +- (void) setGalacticHyperspaceBehaviour:(OOGalacticHyperspaceBehaviour)inBehaviour +{ + if (GALACTIC_HYPERSPACE_BEHAVIOUR_UNKNOWN < inBehaviour && inBehaviour <= GALACTIC_HYPERSPACE_MAX) + { + galacticHyperspaceBehaviour = inBehaviour; + } +} + + +- (OOGalacticHyperspaceBehaviour) galacticHyperspaceBehaviour +{ + return galacticHyperspaceBehaviour; +} + + +- (void) setGalacticHyperspaceFixedCoordsX:(unsigned char)x y:(unsigned char)y +{ + galacticHyperspaceFixedCoords.x = x; + galacticHyperspaceFixedCoords.y = y; +} + + +- (NSPoint) galacticHyperspaceFixedCoords +{ + return galacticHyperspaceFixedCoords; +} + + +- (BOOL) scriptedMisjump +{ + return scripted_misjump; +} + + +- (void) setScriptedMisjump:(BOOL)newValue +{ + scripted_misjump = !!newValue; +} + + +- (BOOL) isDocked +{ + BOOL isDockedStatus = NO; + + switch ([self status]) + { + case STATUS_DOCKED: + case STATUS_DOCKING: + case STATUS_START_GAME: + isDockedStatus = YES; + break; + case STATUS_EFFECT: + case STATUS_ACTIVE: + case STATUS_COCKPIT_DISPLAY: + case STATUS_TEST: + case STATUS_INACTIVE: + case STATUS_DEAD: + case STATUS_IN_FLIGHT: + case STATUS_AUTOPILOT_ENGAGED: + case STATUS_LAUNCHING: + case STATUS_WITCHSPACE_COUNTDOWN: + case STATUS_ENTERING_WITCHSPACE: + case STATUS_EXITING_WITCHSPACE: + case STATUS_ESCAPE_SEQUENCE: + case STATUS_IN_HOLD: + case STATUS_BEING_SCOOPED: + case STATUS_HANDLING_ERROR: + break; + //no default, so that we get notified by the compiler if something is missing + } + +#ifndef NDEBUG + // Sanity check + if (isDockedStatus) + { + if (dockedStation == nil) + { + //there are a number of possible current statuses, not just STATUS_DOCKED + OOLog(kOOLogInconsistentState, @"***** ERROR: status is %@, but dockedStation is nil; treating as not docked. This is an internal error, please report it.", EntityStatusToString([self status])); + [self setStatus:STATUS_IN_FLIGHT]; + isDockedStatus = NO; + } + } + else + { + if (dockedStation != nil) + { + OOLog(kOOLogInconsistentState, @"***** ERROR: status is %@, not STATUS_DOCKED, but dockedStation is not nil; treating as docked. This is an internal error, please report it.", EntityStatusToString([self status])); + [self setStatus:STATUS_DOCKED]; + isDockedStatus = YES; + } + } +#endif + + return isDockedStatus; +} + + +#if DOCKING_CLEARANCE_ENABLED +- (BOOL)clearedToDock +{ + return dockingClearanceStatus > DOCKING_CLEARANCE_STATUS_REQUESTED; +} + + +- (void)setDockingClearanceStatus:(OODockingClearanceStatus)newValue +{ + dockingClearanceStatus = newValue; + if (dockingClearanceStatus == DOCKING_CLEARANCE_STATUS_NONE) + { + targetDockStation = nil; + } + else if (dockingClearanceStatus == DOCKING_CLEARANCE_STATUS_REQUESTED) + { + if ([[self primaryTarget] isStation]) + { + targetDockStation = [self primaryTarget]; + } + else + { + OOLog(@"player.badDockingTarget", @"Attempt to dock at %@.", [self primaryTarget]); + targetDockStation = nil; + dockingClearanceStatus = DOCKING_CLEARANCE_STATUS_NONE; + } + } +} + +- (OODockingClearanceStatus)getDockingClearanceStatus +{ + return dockingClearanceStatus; +} + + +- (void)penaltyForUnauthorizedDocking +{ + OOCreditsQuantity amountToPay = 0; + OOCreditsQuantity calculatedFine = credits * 0.05; + OOCreditsQuantity maximumFine = 50000ULL; + + if ([UNIVERSE strict] || [self clearedToDock]) + return; + + amountToPay = MIN(maximumFine, calculatedFine); + credits -= amountToPay; + [UNIVERSE addCommsMessage:[NSString stringWithFormat:DESC(@"you-have-been-fined-@-cr-for-unauthorized-docking"), OOCredits(amountToPay)] forCount:8 andShowComms:NO]; +} + +#endif + +#if WORMHOLE_SCANNER +// +// Wormhole Scanner support functions +// +- (void)addScannedWormhole:(WormholeEntity*)wormhole +{ + assert(scannedWormholes != nil); + assert(wormhole != nil); + + // Only add if we don't have it already! + NSEnumerator * wormholes = [scannedWormholes objectEnumerator]; + WormholeEntity * wh; + while ((wh = [wormholes nextObject])) + { + if ([wh universalID] == [wormhole universalID]) + return; + } + [wormhole setScannedAt:[self clockTimeAdjusted]]; + [scannedWormholes addObject:wormhole]; +} + +// Checks through our array of wormholes for any which have expired +// If it is in the current system, spawn ships +// Else remove it +- (void)updateWormholes +{ + assert(scannedWormholes != nil); + + if ([scannedWormholes count] == 0) + return; + + double now = [self clockTimeAdjusted]; + + NSMutableArray * savedWormholes = [[NSMutableArray alloc] initWithCapacity:[scannedWormholes count]]; + NSEnumerator * wormholes = [scannedWormholes objectEnumerator]; + WormholeEntity *wh; + + while ((wh = (WormholeEntity*)[wormholes nextObject])) + { + // TODO: Start drawing wormhole exit a few seconds before the first + // ship is disgorged. + if ([wh arrivalTime] > now) + { + [savedWormholes addObject:wh]; + } + else if (equal_seeds([wh destination], [self system_seed])) + { + [wh disgorgeShips]; + if ([[wh shipsInTransit] count] > 0) + { + [savedWormholes addObject:wh]; + } + } + // Else wormhole has expired in another system, let it expire + } + + [scannedWormholes release]; + scannedWormholes = savedWormholes; +} +#endif + +#ifndef NDEBUG +- (void)dumpSelfState +{ + NSMutableArray *flags = nil; + NSString *flagsString = nil; + + [super dumpSelfState]; + + OOLog(@"dumpState.playerEntity", @"Ship: %@", ship_desc); + OOLog(@"dumpState.playerEntity", @"Script time: %g", script_time); + OOLog(@"dumpState.playerEntity", @"Script time check: %g", script_time_check); + OOLog(@"dumpState.playerEntity", @"Script time interval: %g", script_time_interval); + OOLog(@"dumpState.playerEntity", @"Roll/pitch/yaw delta: %g, %g, %g", roll_delta, pitch_delta, yaw_delta); + OOLog(@"dumpState.playerEntity", @"Shield: %g fore, %g aft", forward_weapon, aft_shield); + OOLog(@"dumpState.playerEntity", @"Alert level: %u, flags: %#x", alertFlags, alertCondition); + OOLog(@"dumpState.playerEntity", @"Missile status: %i", missile_status); + OOLog(@"dumpState.playerEntity", @"Energy unit: %@", EnergyUnitTypeToString([self installedEnergyUnitType])); + OOLog(@"dumpState.playerEntity", @"Fuel leak rate: %g", fuel_leak_rate); + OOLog(@"dumpState.playerEntity", @"Trumble count: %u", trumbleCount); + + flags = [NSMutableArray array]; + #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; } + ADD_FLAG_IF_SET(found_equipment); + ADD_FLAG_IF_SET(pollControls); + ADD_FLAG_IF_SET(suppressTargetLost); + ADD_FLAG_IF_SET(scoopsActive); + ADD_FLAG_IF_SET(game_over); + ADD_FLAG_IF_SET(finished); + ADD_FLAG_IF_SET(bomb_detonated); + ADD_FLAG_IF_SET(autopilot_engaged); + ADD_FLAG_IF_SET(afterburner_engaged); + ADD_FLAG_IF_SET(afterburnerSoundLooping); + ADD_FLAG_IF_SET(hyperspeed_engaged); + ADD_FLAG_IF_SET(travelling_at_hyperspeed); + ADD_FLAG_IF_SET(hyperspeed_locked); + ADD_FLAG_IF_SET(ident_engaged); + ADD_FLAG_IF_SET(galactic_witchjump); + ADD_FLAG_IF_SET(ecm_in_operation); + ADD_FLAG_IF_SET(show_info_flag); + ADD_FLAG_IF_SET(showDemoShips); + ADD_FLAG_IF_SET(rolling); + ADD_FLAG_IF_SET(pitching); + ADD_FLAG_IF_SET(yawing); + ADD_FLAG_IF_SET(using_mining_laser); + ADD_FLAG_IF_SET(mouse_control_on); + ADD_FLAG_IF_SET(isSpeechOn); + ADD_FLAG_IF_SET(keyboardRollPitchOverride); + ADD_FLAG_IF_SET(keyboardYawOverride); + ADD_FLAG_IF_SET(waitingForStickCallback); + flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none"; + OOLog(@"dumpState.playerEntity", @"Flags: %@", flagsString); +} +#endif + +@end diff --git a/src/Core/Entities/PlayerEntityContracts.h b/src/Core/Entities/PlayerEntityContracts.h new file mode 100644 index 00000000..b5f617ee --- /dev/null +++ b/src/Core/Entities/PlayerEntityContracts.h @@ -0,0 +1,108 @@ +/* + +PlayerEntityContracts.h + +Methods relating to passenger and cargo contract handling. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" + +#define PASSENGER_KEY_NAME @"name" +#define PASSENGER_KEY_DESTINATION_NAME @"destination_name" +#define PASSENGER_KEY_START @"start" +#define PASSENGER_KEY_DESTINATION @"destination" +#define PASSENGER_KEY_LONG_DESCRIPTION @"long_description" +#define PASSENGER_KEY_DEPARTURE_TIME @"departure_time" +#define PASSENGER_KEY_ARRIVAL_TIME @"arrival_time" +#define PASSENGER_KEY_FEE @"fee" +#define PASSENGER_KEY_PREMIUM @"premium" + +#define CONTRACT_KEY_ID @"id" +#define CONTRACT_KEY_START @"start" +#define CONTRACT_KEY_DESTINATION @"destination" +#define CONTRACT_KEY_DESTINATION_NAME @"destination_name" +#define CONTRACT_KEY_CARGO_TYPE @"co_type" +#define CONTRACT_KEY_CARGO_AMOUNT @"co_amount" +#define CONTRACT_KEY_CARGO_DESCRIPTION @"cargo_description" +#define CONTRACT_KEY_LONG_DESCRIPTION @"long_description" +#define CONTRACT_KEY_DEPARTURE_TIME @"departure_time" +#define CONTRACT_KEY_ARRIVAL_TIME @"arrival_time" +#define CONTRACT_KEY_FEE @"fee" +#define CONTRACT_KEY_PREMIUM @"premium" + +#define GUI_ROW_PASSENGERS_LABELS 1 +#define GUI_ROW_PASSENGERS_START 2 +#define GUI_ROW_CARGO_LABELS 8 +#define GUI_ROW_CARGO_START 9 +#define GUI_ROW_CONTRACT_INFO_START 15 + +#define GUI_ROW_SHIPYARD_LABELS 1 +#define GUI_ROW_SHIPYARD_START 2 +#define GUI_ROW_SHIPYARD_INFO_START 15 +#define GUI_ROW_NO_SHIPS 10 + +#define MAX_ROWS_SHIPS_FOR_SALE 12 + +@interface PlayerEntity (Contracts) + +- (NSString*) processEscapePods; // removes pods from cargo bay and treats categories of characters carried +- (NSString *) checkPassengerContracts; // returns messages from any passengers whose status have changed + +- (NSDictionary*) reputation; + +- (int) passengerReputation; +- (void) increasePassengerReputation; +- (void) decreasePassengerReputation; + +- (int) contractReputation; +- (void) increaseContractReputation; +- (void) decreaseContractReputation; + +- (void) erodeReputation; + +- (void) addMessageToReport:(NSString*) report; + +- (void) setGuiToContractsScreen; +- (BOOL) pickFromGuiContractsScreen; +- (void) highlightSystemFromGuiContractsScreen; + +- (NSArray*) passengerList; +- (NSArray*) contractList; +- (void) setGuiToManifestScreen; + +- (void) setGuiToDeliveryReportScreenWithText:(NSString*) report; +- (void) setGuiToDockingReportScreen; + +// ---------------------------------------------------------------------- // + +- (void) setGuiToShipyardScreen:(unsigned) skip; + +- (void) showShipyardInfoForSelection; + +- (void) showTradeInInformationFooter; + +- (void) showShipyardModel: (NSDictionary *)shipDict; + +- (BOOL) buySelectedShip; + +@end diff --git a/src/Core/Entities/PlayerEntityContracts.m b/src/Core/Entities/PlayerEntityContracts.m new file mode 100644 index 00000000..80771016 --- /dev/null +++ b/src/Core/Entities/PlayerEntityContracts.m @@ -0,0 +1,1564 @@ +/* + +PlayerEntityContracts.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "PlayerEntityContracts.h" +#import "Universe.h" +#import "AI.h" +#import "OOColor.h" +#import "OOCharacter.h" +#import "StationEntity.h" +#import "GuiDisplayGen.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" +#import "MyOpenGLView.h" +#import "NSStringOOExtensions.h" +#import "OOShipRegistry.h" +#import "OOEquipmentType.h" + + +static NSString * const kOOLogNoteShowShipyardModel = @"script.debug.note.showShipyardModel"; + + +@interface PlayerEntity (ContractsPrivate) + +- (OOCreditsQuantity) tradeInValue; + +@end + + +@implementation PlayerEntity (Contracts) + +- (NSString *) processEscapePods // removes pods from cargo bay and treats categories of characters carried +{ + if ([UNIVERSE strict]) return [NSString string]; // return a blank string + + unsigned i; + BOOL added_entry = NO; // to prevent empty lines for slaves and the rare empty report. + NSMutableString *result = [NSMutableString string]; + NSMutableArray *rescuees = [NSMutableArray array]; + OOGovernmentID government = [[[UNIVERSE currentSystemData] objectForKey:KEY_GOVERNMENT] intValue]; + if ([UNIVERSE inInterstellarSpace]) government = 1; // equivalent to Feudal. I'm assuming any station in interstellar space is military. -- Ahruman 2008-05-29 + + // step through the cargo removing crew from any escape pods + // No enumerator because we're mutating the array -- Ahruman + for (i = 0; i < [cargo count]; i++) + { + ShipEntity *cargoItem = [cargo objectAtIndex:i]; + NSArray *podCrew = [cargoItem crew]; + + if (podCrew != nil) + { + // Has crew -> is escape pod. + [rescuees addObjectsFromArray:podCrew]; + [cargoItem setCrew:nil]; + [cargo removeObjectAtIndex:i]; + i--; + } + } + + // step through the rescuees awarding insurance or bounty or adding to slaves + for (i = 0; i < [rescuees count]; i++) + { + OOCharacter* rescuee = (OOCharacter*)[rescuees objectAtIndex: i]; + if ([rescuee script]) + { + [self runUnsanitizedScriptActions:[rescuee script] + allowingAIMethods:YES + withContextName:[NSString stringWithFormat:@"character \"%@\" script", [rescuee name]] + forTarget:nil]; + } + else if ([rescuee insuranceCredits]) + { + // claim insurance reward + [result appendFormat:DESC(@"rescue-reward-for-@@-@-credits"), + [rescuee name], [rescuee shortDescription], OOStringFromDeciCredits([rescuee insuranceCredits] * 10, YES, NO)]; + credits += 10 * [rescuee insuranceCredits]; + added_entry = YES; + } + else if ([rescuee legalStatus]) + { + // claim bounty for capture + float reward = (5.0 + government) * [rescuee legalStatus]; + [result appendFormat:DESC(@"capture-reward-for-@@-@-credits"), + [rescuee name], [rescuee shortDescription], OOStringFromDeciCredits(reward, YES, NO)]; + credits += reward; + added_entry = YES; + } + else + { + // sell as slave - increase no. of slaves in manifest + [self awardCargo:@"1 Slaves"]; + } + if ((i < [rescuees count] - 1) && added_entry) + [result appendString:@"\n"]; + added_entry = NO; + } + + [self calculateCurrentCargo]; + + return result; +} + + +- (NSString *) checkPassengerContracts // returns messages from any passengers whose status have changed +{ + if (dockedStation != [UNIVERSE station]) // only drop off passengers or fulfil contracts at main station + return nil; + + // check escape pods... + // TODO + + NSMutableString *result = [NSMutableString string]; + unsigned i; + + // check passenger contracts + for (i = 0; i < [passengers count]; i++) + { + NSDictionary* passenger_info = (NSDictionary *)[passengers objectAtIndex:i]; + NSString* passenger_name = (NSString *)[passenger_info objectForKey:PASSENGER_KEY_NAME]; + NSString* passenger_dest_name = (NSString *)[passenger_info objectForKey:PASSENGER_KEY_DESTINATION_NAME]; + int dest = [(NSNumber*)[passenger_info objectForKey:PASSENGER_KEY_DESTINATION] intValue]; + int dest_eta = [(NSNumber*)[passenger_info objectForKey:PASSENGER_KEY_ARRIVAL_TIME] doubleValue] - ship_clock; + + if (equal_seeds( system_seed, [UNIVERSE systemSeedForSystemNumber:dest])) + { + // we've arrived in system! + if (dest_eta > 0) + { + // and in good time + long long fee = [passenger_info longLongForKey:PASSENGER_KEY_FEE]; + while ((randf() < 0.75)&&(dest_eta > 3600)) // delivered with more than an hour to spare and a decent customer? + { + fee *= 110; // tip + 10% + fee /= 100; + dest_eta *= 0.5; + } + credits += 10 * fee; + + [result appendFormatLine:DESC(@"passenger-delivered-okay-@-@-@"), passenger_name, OOIntCredits(fee), passenger_dest_name]; + + [passengers removeObjectAtIndex:i--]; + [self increasePassengerReputation]; + } + else + { + // but we're late! + long long fee = [passenger_info longLongForKey:PASSENGER_KEY_FEE] / 2; // halve fare + while (randf() < 0.5) // maybe halve fare a few times! + fee /= 2; + credits += 10 * fee; + + [result appendFormatLine:DESC(@"passenger-delivered-late-@-@-@"), passenger_name, OOIntCredits(fee), passenger_dest_name]; + + [passengers removeObjectAtIndex:i--]; + } + } + else + { + if (dest_eta < 0) + { + // we've run out of time! + [result appendFormatLine:DESC(@"passenger-failed-@"), passenger_name]; + + [passengers removeObjectAtIndex:i--]; + [self decreasePassengerReputation]; + } + } + } + + // check cargo contracts + for (i = 0; i < [contracts count]; i++) + { + NSDictionary* contract_info = (NSDictionary *)[contracts objectAtIndex:i]; + NSString* contract_cargo_desc = (NSString *)[contract_info objectForKey:CONTRACT_KEY_CARGO_DESCRIPTION]; + int dest = [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_DESTINATION] intValue]; + int dest_eta = [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_ARRIVAL_TIME] doubleValue] - ship_clock; + + int premium = 10 * [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_PREMIUM] floatValue]; + int fee = 10 * [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_FEE] floatValue]; + + int contract_cargo_type = [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_CARGO_TYPE] intValue]; + int contract_amount = [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_CARGO_AMOUNT] intValue]; + + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + NSMutableArray* commodityInfo = [NSMutableArray arrayWithArray:[manifest arrayAtIndex:contract_cargo_type]]; + int quantity_on_hand = [(NSNumber *)[commodityInfo objectAtIndex:MARKET_QUANTITY] intValue]; + + if (equal_seeds( system_seed, [UNIVERSE systemSeedForSystemNumber:dest])) + { + // we've arrived in system! + if (dest_eta > 0) + { + // and in good time + if (quantity_on_hand >= contract_amount) + { + // with the goods too! + + // remove the goods... + quantity_on_hand -= contract_amount; + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:quantity_on_hand]]; + [manifest replaceObjectAtIndex:contract_cargo_type withObject:commodityInfo]; + if (shipCommodityData) + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + // pay the premium and fee + credits += fee + premium; + + [result appendFormatLine:DESC(@"cargo-delivered-okay-@-@"), contract_cargo_desc, OOCredits(fee + premium)]; + + [contracts removeObjectAtIndex:i--]; + // repute++ + [self increaseContractReputation]; + } + else + { + // see if the amount of goods delivered is acceptable + + float percent_delivered = 100.0 * (float)quantity_on_hand/(float)contract_amount; + float acceptable_ratio = 100.0 - 10.0 * system_seed.a / 256.0; // down to 90% + + if (percent_delivered >= acceptable_ratio) + { + + // remove the goods... + quantity_on_hand = 0; + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:0]]; + [manifest replaceObjectAtIndex:contract_cargo_type withObject:commodityInfo]; + if (shipCommodityData) + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + // pay the premium and fee + int shortfall = 100 - percent_delivered; + int payment = percent_delivered * (fee + premium) / 100.0; + credits += payment; + + [result appendFormatLine:DESC(@"cargo-delivered-short-@-@-d"), contract_cargo_desc, OOCredits(payment), shortfall]; + + [contracts removeObjectAtIndex:i--]; + // repute unchanged + } + else + { + [result appendFormatLine:DESC(@"cargo-refused-short-%@"), contract_cargo_desc]; + // FIXME: should we be removing here? + } + } + } + else + { + // but we're late! + [result appendFormatLine:DESC(@"cargo-delivered-late-@"), contract_cargo_desc]; + // repute-- + [self decreaseContractReputation]; + // FIXME: should we be removing here? + } + } + else + { + if (dest_eta < 0) + { + // we've run out of time! + [result appendFormatLine:DESC(@"cargo-failed-@"), contract_cargo_desc]; + + [contracts removeObjectAtIndex:i--]; + // repute-- + [self decreaseContractReputation]; + } + } + } + + // check passenger_record for expired contracts + NSArray* names = [passenger_record allKeys]; + for (i = 0; i < [names count]; i++) + { + double dest_eta = [(NSNumber*)[passenger_record objectForKey:[names objectAtIndex:i]] doubleValue] - ship_clock; + if (dest_eta < 0) + { + // check they're not STILL on board + BOOL on_board = NO; + unsigned j; + for (j = 0; j < [passengers count]; j++) + { + NSDictionary* passenger_info = (NSDictionary *)[passengers objectAtIndex:j]; + if ([[passenger_info objectForKey:PASSENGER_KEY_NAME] isEqual:[names objectAtIndex:i]]) + on_board = YES; + } + if (!on_board) + { + [passenger_record removeObjectForKey:[names objectAtIndex:i]]; + } + } + } + + // check contract_record for expired contracts + NSArray* ids = [contract_record allKeys]; + for (i = 0; i < [ids count]; i++) + { + double dest_eta = [(NSNumber*)[contract_record objectForKey:[ids objectAtIndex:i]] doubleValue] - ship_clock; + if (dest_eta < 0) + { + [contract_record removeObjectForKey:[ids objectAtIndex:i]]; + } + } + + if ([result length] == 0) + { + result = nil; + } + else + { + // Should have a trailing \n + [result deleteCharacterAtIndex:[result length] - 1]; + } + + return result; +} + + +- (void) addMessageToReport:(NSString*) report +{ + if ([report length] != 0) + { + if ([dockingReport length] == 0) + [dockingReport appendString:report]; + else + [dockingReport appendFormat:@"\n\n%@", report]; + } +} + + +- (NSDictionary*) reputation +{ + return reputation; +} + + +- (int) passengerReputation +{ + int good = [reputation intForKey:PASSAGE_GOOD_KEY]; + int bad = [reputation intForKey:PASSAGE_BAD_KEY]; + int unknown = [reputation intForKey:PASSAGE_UNKNOWN_KEY]; + + if (unknown > 0) + unknown = 7 - (market_rnd % unknown); + else + unknown = 7; + + return (good + unknown - 2 * bad) / 2; // return a number from -7 to +7 +} + + +- (void) increasePassengerReputation +{ + int good = [reputation intForKey:PASSAGE_GOOD_KEY]; + int bad = [reputation intForKey:PASSAGE_BAD_KEY]; + int unknown = [reputation intForKey:PASSAGE_UNKNOWN_KEY]; + + if (bad > 0) + { + // shift a bean from bad to unknown + bad--; + if (unknown < 7) + unknown++; + } + else + { + // shift a bean from unknown to good + if (unknown > 0) + unknown--; + if (good < 7) + good++; + } + [reputation setInteger:good forKey:PASSAGE_GOOD_KEY]; + [reputation setInteger:bad forKey:PASSAGE_BAD_KEY]; + [reputation setInteger:unknown forKey:PASSAGE_UNKNOWN_KEY]; +} + + +- (void) decreasePassengerReputation +{ + int good = [reputation intForKey:PASSAGE_GOOD_KEY]; + int bad = [reputation intForKey:PASSAGE_BAD_KEY]; + int unknown = [reputation intForKey:PASSAGE_UNKNOWN_KEY]; + + if (good > 0) + { + // shift a bean from good to bad + good--; + if (bad < 7) + bad++; + } + else + { + // shift a bean from unknown to bad + if (unknown > 0) + unknown--; + if (bad < 7) + bad++; + } + [reputation setInteger:good forKey:PASSAGE_GOOD_KEY]; + [reputation setInteger:bad forKey:PASSAGE_BAD_KEY]; + [reputation setInteger:unknown forKey:PASSAGE_UNKNOWN_KEY]; +} + + +- (int) contractReputation +{ + int good = [reputation intForKey:CONTRACTS_GOOD_KEY]; + int bad = [reputation intForKey:CONTRACTS_BAD_KEY]; + int unknown = [reputation intForKey:CONTRACTS_UNKNOWN_KEY]; + + if (unknown > 0) + unknown = 7 - (market_rnd % unknown); + else + unknown = 7; + + return (good + unknown - 2 * bad) / 2; // return a number from -7 to +7 +} + + +- (void) increaseContractReputation +{ + int good = [reputation intForKey:CONTRACTS_GOOD_KEY]; + int bad = [reputation intForKey:CONTRACTS_BAD_KEY]; + int unknown = [reputation intForKey:CONTRACTS_UNKNOWN_KEY]; + + if (bad > 0) + { + // shift a bean from bad to unknown + bad--; + if (unknown < 7) + unknown++; + } + else + { + // shift a bean from unknown to good + if (unknown > 0) + unknown--; + if (good < 7) + good++; + } + [reputation setInteger:good forKey:CONTRACTS_GOOD_KEY]; + [reputation setInteger:bad forKey:CONTRACTS_BAD_KEY]; + [reputation setInteger:unknown forKey:CONTRACTS_UNKNOWN_KEY]; +} + + +- (void) decreaseContractReputation +{ + int good = [reputation intForKey:CONTRACTS_GOOD_KEY]; + int bad = [reputation intForKey:CONTRACTS_BAD_KEY]; + int unknown = [reputation intForKey:CONTRACTS_UNKNOWN_KEY]; + + if (good > 0) + { + // shift a bean from good to bad + good--; + if (bad < 7) + bad++; + } + else + { + // shift a bean from unknown to bad + if (unknown > 0) + unknown--; + if (bad < 7) + bad++; + } + [reputation setInteger:good forKey:CONTRACTS_GOOD_KEY]; + [reputation setInteger:bad forKey:CONTRACTS_BAD_KEY]; + [reputation setInteger:unknown forKey:CONTRACTS_UNKNOWN_KEY]; +} + + +- (void) erodeReputation +{ + int c_good = [reputation intForKey:CONTRACTS_GOOD_KEY]; + int c_bad = [reputation intForKey:CONTRACTS_BAD_KEY]; + int c_unknown = [reputation intForKey:CONTRACTS_UNKNOWN_KEY]; + int p_good = [reputation intForKey:PASSAGE_GOOD_KEY]; + int p_bad = [reputation intForKey:PASSAGE_BAD_KEY]; + int p_unknown = [reputation intForKey:PASSAGE_UNKNOWN_KEY]; + + if (c_unknown < 7) + { + if (c_bad > 0) + c_bad--; + else + { + if (c_good > 0) + c_good--; + } + c_unknown++; + } + + if (p_unknown < 7) + { + if (p_bad > 0) + p_bad--; + else + { + if (p_good > 0) + p_good--; + } + p_unknown++; + } + + [reputation setObject:[NSNumber numberWithInt:c_good] forKey:CONTRACTS_GOOD_KEY]; + [reputation setObject:[NSNumber numberWithInt:c_bad] forKey:CONTRACTS_BAD_KEY]; + [reputation setObject:[NSNumber numberWithInt:c_unknown] forKey:CONTRACTS_UNKNOWN_KEY]; + [reputation setObject:[NSNumber numberWithInt:p_good] forKey:PASSAGE_GOOD_KEY]; + [reputation setObject:[NSNumber numberWithInt:p_bad] forKey:PASSAGE_BAD_KEY]; + [reputation setObject:[NSNumber numberWithInt:p_unknown] forKey:PASSAGE_UNKNOWN_KEY]; + +} + +- (void) setGuiToContractsScreen +{ + unsigned i; + NSMutableArray *row_info = [NSMutableArray arrayWithCapacity:5]; + + // set up initial markets if there are none + StationEntity* the_station = [UNIVERSE station]; + if (![the_station localPassengers]) + [the_station setLocalPassengers:[NSMutableArray arrayWithArray:[UNIVERSE passengersForSystem:system_seed atTime:ship_clock]]]; + if (![the_station localContracts]) + [the_station setLocalContracts:[NSMutableArray arrayWithArray:[UNIVERSE contractsForSystem:system_seed atTime:ship_clock]]]; + + NSMutableArray* passenger_market = [the_station localPassengers]; + NSMutableArray* contract_market = [the_station localContracts]; + + // remove passenger contracts that the player has already agreed to or done + for (i = 0; i < [passenger_market count]; i++) + { + NSDictionary* info = [passenger_market dictionaryAtIndex:i]; + NSString* p_name = [info stringForKey:PASSENGER_KEY_NAME]; + if ([passenger_record objectForKey:p_name]) + [passenger_market removeObjectAtIndex:i--]; + } + + // remove cargo contracts that the player has already agreed to or done + for (i = 0; i < [contract_market count]; i++) + { + NSDictionary* info = [contract_market dictionaryAtIndex:i]; + NSString* cid = [info stringForKey:CONTRACT_KEY_ID]; + if ([contract_record objectForKey:cid]) + [contract_market removeObjectAtIndex:i--]; + } + + // if there are more than 5 contracts remove cargo contracts that are larger than the space available or cost more than can be afforded + for (i = 0; ([contract_market count] > 5) && (i < [contract_market count]); i++) + { + NSDictionary *info = [contract_market objectAtIndex:i]; + OOCargoQuantity cargoSpaceRequired = [info unsignedIntForKey:CONTRACT_KEY_CARGO_AMOUNT]; + OOMassUnit cargoUnits = [UNIVERSE unitsForCommodity:[info intForKey:CONTRACT_KEY_CARGO_TYPE]]; + + if (cargoUnits == UNITS_KILOGRAMS) cargoSpaceRequired /= 1000; + if (cargoUnits == UNITS_GRAMS) cargoSpaceRequired /= 1000000; + + float premium = [info floatForKey:CONTRACT_KEY_PREMIUM]; + if ((cargoSpaceRequired > max_cargo - current_cargo)||(premium * 10 > credits)) + [contract_market removeObjectAtIndex:i--]; + } + + // GUI stuff + { + GuiDisplayGen *gui = [UNIVERSE gui]; + + unsigned n_passengers = [passenger_market count]; + if (n_passengers > 5) + n_passengers = 5; + unsigned n_contracts = [contract_market count]; + if (n_contracts > 5) + n_contracts = 5; + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"@-contracts-title"),[UNIVERSE getSystemName:system_seed]]]; + + OOGUITabSettings tab_stops; + tab_stops[0] = 0; + tab_stops[1] = 160; + tab_stops[2] = 240; + tab_stops[3] = -410; + tab_stops[4] = -476; + + [gui setTabStops:tab_stops]; + + [row_info addObject:DESC(@"contracts-passenger-name")]; + [row_info addObject:DESC(@"contracts-to")]; + [row_info addObject:DESC(@"contracts-within")]; + [row_info addObject:DESC(@"contracts-passenger-advance")]; + [row_info addObject:DESC(@"contracts-passenger-fee")]; + + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_PASSENGERS_LABELS]; + [gui setArray:[NSArray arrayWithArray:row_info] forRow:GUI_ROW_PASSENGERS_LABELS]; + + BOOL can_take_passengers = (max_passengers > [passengers count]); + + for (i = 0; i < n_passengers; i++) + { + NSDictionary* passenger_info = [passenger_market dictionaryAtIndex:i]; + NSString *Name = [passenger_info stringForKey:PASSENGER_KEY_NAME]; + if([Name length] >27) Name =[[Name substringToIndex:25] stringByAppendingString:@"..."]; + int dest_eta = [passenger_info doubleForKey:PASSENGER_KEY_ARRIVAL_TIME] - ship_clock; + [row_info removeAllObjects]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",Name]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[passenger_info stringForKey:PASSENGER_KEY_DESTINATION_NAME]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[UNIVERSE shortTimeDescription:dest_eta]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[passenger_info stringForKey:PASSENGER_KEY_PREMIUM]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[passenger_info stringForKey:PASSENGER_KEY_FEE]]]; + [gui setColor:[OOColor yellowColor] forRow:GUI_ROW_PASSENGERS_START + i]; + [gui setArray:[NSArray arrayWithArray:row_info] forRow:GUI_ROW_PASSENGERS_START + i]; + if (can_take_passengers) + [gui setKey:GUI_KEY_OK forRow:GUI_ROW_PASSENGERS_START + i]; + else + { + [gui setKey:GUI_KEY_SKIP forRow:GUI_ROW_PASSENGERS_START + i]; + [gui setColor:[OOColor grayColor] forRow:GUI_ROW_PASSENGERS_START + i]; + } + } + + [row_info removeAllObjects]; + [row_info addObject:DESC(@"contracts-cargo-cargotype")]; + [row_info addObject:DESC(@"contracts-to")]; + [row_info addObject:DESC(@"contracts-within")]; + [row_info addObject:DESC(@"contracts-cargo-premium")]; + [row_info addObject:DESC(@"contracts-cargo-pays")]; + + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_CARGO_LABELS]; + [gui setArray:[NSArray arrayWithArray:row_info] forRow:GUI_ROW_CARGO_LABELS]; + + for (i = 0; i < n_contracts; i++) + { + NSDictionary *contract_info = [contract_market dictionaryAtIndex:i]; + OOCargoQuantity cargo_space_required = [contract_info unsignedIntForKey:CONTRACT_KEY_CARGO_AMOUNT]; + OOMassUnit cargo_units = [UNIVERSE unitsForCommodity:[contract_info unsignedIntForKey:CONTRACT_KEY_CARGO_TYPE]]; + if (cargo_units == UNITS_KILOGRAMS) cargo_space_required /= 1000; + if (cargo_units == UNITS_GRAMS) cargo_space_required /= 1000000; + + float premium = [(NSNumber *)[contract_info objectForKey:CONTRACT_KEY_PREMIUM] floatValue]; + BOOL not_possible = ((cargo_space_required > max_cargo - current_cargo)||(premium * 10 > credits)); + int dest_eta = [(NSNumber*)[contract_info objectForKey:CONTRACT_KEY_ARRIVAL_TIME] doubleValue] - ship_clock; + [row_info removeAllObjects]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[contract_info objectForKey:CONTRACT_KEY_CARGO_DESCRIPTION]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[contract_info objectForKey:CONTRACT_KEY_DESTINATION_NAME]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[UNIVERSE shortTimeDescription:dest_eta]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[contract_info stringForKey:CONTRACT_KEY_PREMIUM]]]; + [row_info addObject:[NSString stringWithFormat:@" %@ ",[contract_info stringForKey:CONTRACT_KEY_FEE]]]; + [gui setColor:[OOColor yellowColor] forRow:GUI_ROW_CARGO_START + i]; + [gui setArray:[NSArray arrayWithArray:row_info] forRow:GUI_ROW_CARGO_START + i]; + if (not_possible) + { + [gui setKey:GUI_KEY_SKIP forRow:GUI_ROW_CARGO_START + i]; + [gui setColor:[OOColor grayColor] forRow:GUI_ROW_CARGO_START + i]; + } + else + [gui setKey:GUI_KEY_OK forRow:GUI_ROW_CARGO_START + i]; + } + + [gui setText:[NSString stringWithFormat:DESC_PLURAL(@"contracts-cash-@-load-d-of-d-passengers-d-of-d-berths", max_passengers), OOCredits(credits), current_cargo, max_cargo, [passengers count], max_passengers] forRow: GUI_ROW_MARKET_CASH]; + + for (i = GUI_ROW_CARGO_START + n_contracts; i < GUI_ROW_MARKET_CASH; i++) + { + [gui setText:@"" forRow:i]; + [gui setColor:[OOColor greenColor] forRow:i]; + } + + [gui setSelectableRange:NSMakeRange(GUI_ROW_PASSENGERS_START, GUI_ROW_CARGO_START + n_contracts)]; + if ([[gui selectedRowKey] isEqual:GUI_KEY_SKIP]) + [gui setFirstSelectableRow]; + + if (([gui selectedRow] >= GUI_ROW_PASSENGERS_START)&&([gui selectedRow] < (int)(GUI_ROW_PASSENGERS_START + n_passengers))) + { + NSString* long_info = (NSString*)[(NSDictionary*)[passenger_market objectAtIndex:[gui selectedRow] - GUI_ROW_PASSENGERS_START] objectForKey:PASSENGER_KEY_LONG_DESCRIPTION]; + [gui addLongText:long_info startingAtRow:GUI_ROW_CONTRACT_INFO_START align:GUI_ALIGN_LEFT]; + } + if (([gui selectedRow] >= GUI_ROW_CARGO_START)&&([gui selectedRow] < (int)(GUI_ROW_CARGO_START + n_contracts))) + { + NSString* long_info = [[contract_market dictionaryAtIndex:[gui selectedRow] - GUI_ROW_CARGO_START] stringForKey:CONTRACT_KEY_LONG_DESCRIPTION]; + [gui addLongText:long_info startingAtRow:GUI_ROW_CONTRACT_INFO_START align:GUI_ALIGN_LEFT]; + } + + [gui setShowTextCursor:NO]; + } + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_CONTRACTS; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; + + [self noteGuiChangeFrom:oldScreen to:gui_screen]; +} + + +- (BOOL) pickFromGuiContractsScreen +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + + NSMutableArray* passenger_market = [[UNIVERSE station] localPassengers]; + NSMutableArray* contract_market = [[UNIVERSE station] localContracts]; + + if (([gui selectedRow] >= GUI_ROW_PASSENGERS_START)&&([gui selectedRow] < GUI_ROW_CARGO_START)) + { + NSDictionary* passenger_info = (NSDictionary*)[passenger_market objectAtIndex:[gui selectedRow] - GUI_ROW_PASSENGERS_START]; + NSString* passenger_name = (NSString *)[passenger_info objectForKey:PASSENGER_KEY_NAME]; + NSNumber* passenger_arrival_time = (NSNumber*)[passenger_info objectForKey:PASSENGER_KEY_ARRIVAL_TIME]; + int passenger_premium = [(NSNumber*)[passenger_info objectForKey:PASSENGER_KEY_PREMIUM] intValue]; + if ([passengers count] >= max_passengers) + return NO; + [passengers addObject:passenger_info]; + [passenger_record setObject:passenger_arrival_time forKey:passenger_name]; + [passenger_market removeObject:passenger_info]; + credits += 10 * passenger_premium; + + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + + return YES; + } + + if (([gui selectedRow] >= GUI_ROW_CARGO_START)&&([gui selectedRow] < GUI_ROW_MARKET_CASH)) + { + NSDictionary *contractInfo = nil; + NSString *contractID = nil; + NSNumber *contractArrivalTime = nil; + OOCreditsQuantity contractPremium; + OOCargoQuantity contractAmount; + OOCargoType contractCargoType; + OOMassUnit contractCargoUnits; + OOCargoQuantity cargoSpaceRequired; + + contractInfo = [contract_market objectAtIndex:[gui selectedRow] - GUI_ROW_CARGO_START]; + contractID = [contractInfo stringForKey:CONTRACT_KEY_ID]; + contractArrivalTime = [contractInfo objectOfClass:[NSNumber class] forKey:CONTRACT_KEY_ARRIVAL_TIME]; + contractPremium = [contractInfo intForKey:PASSENGER_KEY_PREMIUM]; + contractAmount = [contractInfo intForKey:CONTRACT_KEY_CARGO_AMOUNT]; + contractCargoType = [contractInfo intForKey:CONTRACT_KEY_CARGO_TYPE]; + contractCargoUnits = [UNIVERSE unitsForCommodity:contractCargoType]; + + cargoSpaceRequired = contractAmount; + if (contractCargoUnits == UNITS_KILOGRAMS) cargoSpaceRequired /= 1000; + if (contractCargoUnits == UNITS_GRAMS) cargoSpaceRequired /= 1000000; + + // tests for refusal... + if (cargoSpaceRequired > max_cargo - current_cargo) // no room for cargo + return NO; + + if (contractPremium * 10 > credits) // can't afford contract + return NO; + + // okay passed all tests ... + + // pay the premium + credits -= 10 * contractPremium; + // add commodity to what's being carried + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + NSMutableArray* manifest_commodity = [NSMutableArray arrayWithArray:[manifest objectAtIndex:contractCargoType]]; + int manifest_quantity = [(NSNumber *)[manifest_commodity objectAtIndex:MARKET_QUANTITY] intValue]; + manifest_quantity += contractAmount; + current_cargo += cargoSpaceRequired; + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:manifest_quantity]]; + [manifest replaceObjectAtIndex:contractCargoType withObject:[NSArray arrayWithArray:manifest_commodity]]; + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + + [contracts addObject:contractInfo]; + [contract_record setObject:contractArrivalTime forKey:contractID]; + [contract_market removeObject:contractInfo]; + + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + + return YES; + } + return NO; +} + + +- (void) highlightSystemFromGuiContractsScreen +{ + MyOpenGLView* gameView = [UNIVERSE gameView]; + GuiDisplayGen* gui = [UNIVERSE gui]; + + NSMutableArray* passenger_market = [[UNIVERSE station] localPassengers]; + NSMutableArray* contract_market = [[UNIVERSE station] localContracts]; + + if (([gui selectedRow] >= GUI_ROW_PASSENGERS_START)&&([gui selectedRow] < GUI_ROW_CARGO_START)) + { + NSDictionary* passenger_info = (NSDictionary*)[passenger_market objectAtIndex:[gui selectedRow] - GUI_ROW_PASSENGERS_START]; + NSString* passenger_dest_name = (NSString *)[passenger_info objectForKey:PASSENGER_KEY_DESTINATION_NAME]; + [gameView setTypedString:[passenger_dest_name lowercaseString]]; + [self setGuiToLongRangeChartScreen]; + } + + if (([gui selectedRow] >= GUI_ROW_CARGO_START)&&([gui selectedRow] < GUI_ROW_MARKET_CASH)) + { + NSDictionary* contract_info = (NSDictionary*)[contract_market objectAtIndex:[gui selectedRow] - GUI_ROW_CARGO_START]; + NSString* contract_dest_name = (NSString *)[contract_info objectForKey:CONTRACT_KEY_DESTINATION_NAME]; + [gameView setTypedString:[contract_dest_name lowercaseString]]; + [self setGuiToLongRangeChartScreen]; + } +} + + +- (NSArray*) passengerList +{ + NSMutableArray* result = [NSMutableArray arrayWithCapacity:5]; + // check passenger contracts + unsigned i; + for (i = 0; i < [passengers count]; i++) + { + NSDictionary* passenger_info = (NSDictionary *)[passengers objectAtIndex:i]; + NSString* passenger_name = (NSString *)[passenger_info objectForKey:PASSENGER_KEY_NAME]; + NSString* passenger_dest_name = (NSString *)[passenger_info objectForKey:PASSENGER_KEY_DESTINATION_NAME]; + int dest_eta = [(NSNumber*)[passenger_info objectForKey:PASSENGER_KEY_ARRIVAL_TIME] doubleValue] - ship_clock; + + NSString* short_desc = [NSString stringWithFormat:DESC(@"manifest-@-travelling-to-@-to-arrive-within-@"), + passenger_name, passenger_dest_name, [UNIVERSE shortTimeDescription:dest_eta]]; + + [result addObject:short_desc]; + } + return result; +} + + +- (NSArray*) contractList +{ + NSMutableArray *result = [NSMutableArray arrayWithCapacity:5]; + // check cargo contracts + unsigned i; + for (i = 0; i < [contracts count]; i++) + { + NSDictionary* contract_info = (NSDictionary *)[contracts objectAtIndex:i]; + NSString* contract_cargo_desc = (NSString *)[contract_info objectForKey:CONTRACT_KEY_CARGO_DESCRIPTION]; + NSString* contract_dest_name = (NSString *)[contract_info objectForKey:PASSENGER_KEY_DESTINATION_NAME]; + int dest_eta = [(NSNumber*)[contract_info objectForKey:PASSENGER_KEY_ARRIVAL_TIME] doubleValue] - ship_clock; + + NSString* short_desc = [NSString stringWithFormat:DESC(@"manifest-deliver-@-to-@within-@"), + contract_cargo_desc, contract_dest_name, [UNIVERSE shortTimeDescription:dest_eta]]; + + [result addObject:short_desc]; + } + return result; +} + + +- (void) setGuiToManifestScreen +{ + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + unsigned i = 0; + + unsigned n_manifest_rows = 8; + OOGUIRow cargo_row = 2; + OOGUIRow passenger_row = 2; + OOGUIRow contracts_row = 2; + OOGUIRow missions_row = 2; + + OOGUITabSettings tab_stops; + tab_stops[0] = 20; + tab_stops[1] = 256; + [gui setTabStops:tab_stops]; + + NSArray* cargoManifest = [self cargoList]; + NSArray* passengerManifest = [self passengerList]; + NSArray* contractManifest = [self contractList]; + NSArray* missionsManifest = [self missionsList]; + + int legal_index = 0; + if (legalStatus != 0) + legal_index = (legalStatus <= 50) ? 1 : 2; + unsigned rating = 0; + unsigned kills[8] = { 0x0008, 0x0010, 0x0020, 0x0040, 0x0080, 0x0200, 0x0A00, 0x1900 }; + while ((rating < 8)&&(kills[rating] <= ship_kills)) + { + rating ++; + } + + + if ([self status] == STATUS_DOCKED) + { + unsigned n_commodities = [shipCommodityData count]; + current_cargo = 0; // for calculating remaining hold space + + for (i = 0; i < n_commodities; i++) + { + if ([UNIVERSE unitsForCommodity:i] == UNITS_TONS) + current_cargo += [[(NSArray *)[shipCommodityData objectAtIndex:i] objectAtIndex:MARKET_QUANTITY] intValue]; + } + } + + + [gui clear]; + [gui setTitle:DESC(@"manifest-title")]; + + [gui setText:[NSString stringWithFormat:DESC(@"manifest-cargo-d-d"), ([self status] == STATUS_DOCKED)? current_cargo : [cargo count], max_cargo] forRow:cargo_row - 1]; + [gui setText:DESC(@"manifest-none") forRow:cargo_row]; + [gui setColor:[OOColor yellowColor] forRow:cargo_row - 1]; + [gui setColor:[OOColor greenColor] forRow:cargo_row]; + + if ([cargoManifest count] > 0) + { + for (i = 0; i < n_manifest_rows; i++) + { + NSMutableArray* row_info = [NSMutableArray arrayWithCapacity:2]; + if (i < [cargoManifest count]) + [row_info addObject:[cargoManifest objectAtIndex:i]]; + else + [row_info addObject:@""]; + if (i + n_manifest_rows < [cargoManifest count]) + [row_info addObject:[cargoManifest objectAtIndex:i + n_manifest_rows]]; + else + [row_info addObject:@""]; + [gui setArray:(NSArray *)row_info forRow:cargo_row + i]; + [gui setColor:[OOColor greenColor] forRow:cargo_row + i]; + } + } + + if ([cargoManifest count] < n_manifest_rows) + passenger_row = cargo_row + [cargoManifest count] + 2; + else + passenger_row = cargo_row + n_manifest_rows + 2; + + [gui setText:[NSString stringWithFormat:DESC(@"manifest-passengers-d-d"), [passengerManifest count], max_passengers] forRow:passenger_row - 1]; + [gui setText:DESC(@"manifest-none") forRow:passenger_row]; + [gui setColor:[OOColor yellowColor] forRow:passenger_row - 1]; + [gui setColor:[OOColor greenColor] forRow:passenger_row]; + + if ([passengerManifest count] > 0) + { + for (i = 0; i < [passengerManifest count]; i++) + { + [gui setText:(NSString*)[passengerManifest objectAtIndex:i] forRow:passenger_row + i]; + [gui setColor:[OOColor greenColor] forRow:passenger_row + i]; + } + } + + contracts_row = passenger_row + [passengerManifest count] + 2; + + [gui setText:DESC(@"manifest-contracts") forRow:contracts_row - 1]; + [gui setText:DESC(@"manifest-none") forRow:contracts_row]; + [gui setColor:[OOColor yellowColor] forRow:contracts_row - 1]; + [gui setColor:[OOColor greenColor] forRow:contracts_row]; + + if ([contractManifest count] > 0) + { + for (i = 0; i < [contractManifest count]; i++) + { + [gui setText:(NSString*)[contractManifest objectAtIndex:i] forRow:contracts_row + i]; + [gui setColor:[OOColor greenColor] forRow:contracts_row + i]; + } + } + + if ([missionsManifest count] > 0) + { + missions_row = contracts_row + [contractManifest count] + 2; + + [gui setText:DESC(@"manifest-missions") forRow:missions_row - 1]; + [gui setColor:[OOColor yellowColor] forRow:missions_row - 1]; + + if ([missionsManifest count] > 0) + { + for (i = 0; i < [missionsManifest count]; i++) + { + [gui setText:(NSString*)[missionsManifest objectAtIndex:i] forRow:missions_row + i]; + [gui setColor:[OOColor greenColor] forRow:missions_row + i]; + } + } + } + [gui setShowTextCursor:NO]; + } + /* ends */ + + if (lastTextKey) + { + [lastTextKey release]; + lastTextKey = nil; + } + + OOGUIScreenID oldScreen = gui_screen; + gui_screen = GUI_SCREEN_MANIFEST; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; + + [self noteGuiChangeFrom:oldScreen to:gui_screen]; +} + + +- (void) setGuiToDeliveryReportScreenWithText:(NSString*) report +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + + int text_row = 1; + + // GUI stuff + { + [gui clear]; + [gui setTitle:ExpandDescriptionForCurrentSystem(@"[arrival-report-title]")]; + + + // report might be a multi-line message + + if ([report rangeOfString:@"\n"].location != NSNotFound) + { + NSArray *sections = [report componentsSeparatedByString:@"\n"]; + unsigned i; + for (i = 0; i < [sections count]; i++) + text_row = [gui addLongText:(NSString *)[sections objectAtIndex:i] startingAtRow:text_row align:GUI_ALIGN_LEFT]; + } + else + text_row = [gui addLongText:report startingAtRow:text_row align:GUI_ALIGN_LEFT]; + + [gui setText:[NSString stringWithFormat:DESC_PLURAL(@"contracts-cash-@-load-d-of-d-passengers-d-of-d-berths", max_passengers), OOCredits(credits), current_cargo, max_cargo, [passengers count], max_passengers] forRow: GUI_ROW_MARKET_CASH]; + + [gui setText:@"press-space-commander" forRow:21 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor yellowColor] forRow:21]; + + [gui setShowTextCursor:NO]; + } + /* ends */ + + + if (lastTextKey) + { + [lastTextKey release]; + lastTextKey = nil; + } + + gui_screen = GUI_SCREEN_REPORT; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) setGuiToDockingReportScreen +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + + int text_row = 1; + + [dockingReport setString:[dockingReport stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + + // GUI stuff + { + [gui clear]; + [gui setTitle:ExpandDescriptionForCurrentSystem(@"[arrival-report-title]")]; + + + // dockingReport might be a multi-line message + + while (([dockingReport length] > 0)&&(text_row < 18)) + { + if ([dockingReport rangeOfString:@"\n"].location != NSNotFound) + { + while (([dockingReport rangeOfString:@"\n"].location != NSNotFound)&&(text_row < 18)) + { + int line_break = [dockingReport rangeOfString:@"\n"].location; + NSString* line = [dockingReport substringToIndex:line_break]; + [dockingReport deleteCharactersInRange: NSMakeRange( 0, line_break + 1)]; + text_row = [gui addLongText:line startingAtRow:text_row align:GUI_ALIGN_LEFT]; + } + [dockingReport setString:[dockingReport stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]]; + } + else + { + text_row = [gui addLongText:[NSString stringWithString:dockingReport] startingAtRow:text_row align:GUI_ALIGN_LEFT]; + [dockingReport setString:@""]; + } + } + + [gui setText:[NSString stringWithFormat:DESC_PLURAL(@"contracts-cash-@-load-d-of-d-passengers-d-of-d-berths", max_passengers), OOCredits(credits), current_cargo, max_cargo, [passengers count], max_passengers] forRow: GUI_ROW_MARKET_CASH]; + + [gui setText:DESC(@"press-space-commander") forRow:21 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor yellowColor] forRow:21]; + + [gui setShowTextCursor:NO]; + } + /* ends */ + + + if (lastTextKey) + { + [lastTextKey release]; + lastTextKey = nil; + } + + gui_screen = GUI_SCREEN_REPORT; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + +// ---------------------------------------------------------------------- + +static NSMutableDictionary* currentShipyard = nil; + +- (void) setGuiToShipyardScreen:(unsigned) skip +{ + unsigned i; + + // set up initial market if there is none + StationEntity *the_station; + OOTechLevelID station_tl; + + if (dockedStation) + { + the_station = dockedStation; + station_tl = [dockedStation equivalentTechLevel]; + } + else + { + the_station = [UNIVERSE station]; + station_tl = NSNotFound; + } + if (![the_station localShipyard]) + [the_station setLocalShipyard:[UNIVERSE shipsForSaleForSystem:system_seed withTL:station_tl atTime:ship_clock]]; + + NSMutableArray* shipyard = [the_station localShipyard]; + + // remove ships that the player has already bought + for (i = 0; i < [shipyard count]; i++) + { + NSDictionary* info = (NSDictionary *)[shipyard objectAtIndex:i]; + NSString* ship_id = [info stringForKey:SHIPYARD_KEY_ID]; + if ([shipyard_record objectForKey:ship_id]) + [shipyard removeObjectAtIndex:i--]; + } + + if (currentShipyard) [currentShipyard release]; + currentShipyard = [[NSMutableDictionary alloc] initWithCapacity:[shipyard count]]; + + for (i = 0; i < [shipyard count]; i++) + { + [currentShipyard setObject:[shipyard objectAtIndex:i] + forKey:[(NSDictionary *)[shipyard objectAtIndex:i] stringForKey:SHIPYARD_KEY_ID]]; + } + + NSDictionary *shipInfo = [[OOShipRegistry sharedRegistry] shipInfoForKey:ship_desc]; + NSString *shipName = [shipInfo stringForKey:@"display_name"]; + if (shipName == nil) shipName = [shipInfo stringForKey:KEY_NAME]; + + unsigned n_ships = [shipyard count]; + + //error check + if (skip >= n_ships) + skip = n_ships - 1; + if (skip < 2) + skip = 0; + + // GUI stuff + { + GuiDisplayGen* gui = [UNIVERSE gui]; + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"@-shipyard-title"),[UNIVERSE getSystemName:system_seed]]]; + + OOGUITabSettings tab_stops; + tab_stops[0] = 0; + tab_stops[1] = -258; + tab_stops[2] = 270; + tab_stops[3] = 370; + tab_stops[4] = 450; + [gui setTabStops:tab_stops]; + + int n_rows = MAX_ROWS_SHIPS_FOR_SALE; + int start_row = GUI_ROW_SHIPYARD_START; + int previous = 0; + + if (n_ships <= MAX_ROWS_SHIPS_FOR_SALE) + skip = 0; + else + { + if (skip > 0) + { + n_rows -= 1; + start_row += 1; + previous = skip - MAX_ROWS_SHIPS_FOR_SALE + 2; + if (previous < 2) + previous = 0; + } + if (skip + n_rows < n_ships) + n_rows -= 1; + } + + if (n_ships > 0) + { + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_SHIPYARD_LABELS]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"shipyard-shiptype"), DESC(@"shipyard-price"), + DESC(@"shipyard-cargo"), DESC(@"shipyard-speed"), nil] forRow:GUI_ROW_SHIPYARD_LABELS]; + + if (skip > 0) + { + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_SHIPYARD_START]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-back"), @" <-- ", nil] forRow:GUI_ROW_SHIPYARD_START]; + [gui setKey:[NSString stringWithFormat:@"More:%d", previous] forRow:GUI_ROW_SHIPYARD_START]; + } + for (i = 0; i < (n_ships - skip) && (int)i < n_rows; i++) + { + NSDictionary* ship_info = [shipyard dictionaryAtIndex:i + skip]; + OOCreditsQuantity ship_price = [ship_info unsignedLongLongForKey:SHIPYARD_KEY_PRICE]; + [gui setColor:[OOColor yellowColor] forRow:start_row + i]; + [gui setArray:[NSArray arrayWithObjects: + [NSString stringWithFormat:@" %@ ",[[ship_info dictionaryForKey:SHIPYARD_KEY_SHIP] stringForKey:@"display_name" defaultValue:[[ship_info dictionaryForKey:SHIPYARD_KEY_SHIP] stringForKey:KEY_NAME]]], + OOIntCredits(ship_price), + nil] + forRow:start_row + i]; + [gui setKey:(NSString*)[ship_info objectForKey:SHIPYARD_KEY_ID] forRow:start_row + i]; + } + if (i < n_ships - skip) + { + [gui setColor:[OOColor greenColor] forRow:start_row + i]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-more"), @" --> ", nil] forRow:start_row + i]; + [gui setKey:[NSString stringWithFormat:@"More:%d", n_rows + skip] forRow:start_row + i]; + i++; + } + + [gui setSelectableRange:NSMakeRange( GUI_ROW_SHIPYARD_START, i + start_row - GUI_ROW_SHIPYARD_START)]; + [self showShipyardInfoForSelection]; + } + else + { + [gui setText:DESC(@"shipyard-no-ships-available-for-purchase") forRow:GUI_ROW_NO_SHIPS align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_NO_SHIPS]; + + [gui setNoSelectedRow]; + } + + [self showTradeInInformationFooter]; + + [gui setShowTextCursor:NO]; + } + + gui_screen = GUI_SCREEN_SHIPYARD; + + // the following are necessary... + + [self setShowDemoShips: (n_ships > 0)]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) showShipyardInfoForSelection +{ + unsigned i; + GuiDisplayGen *gui = [UNIVERSE gui]; + OOGUIRow sel_row = [gui selectedRow]; + + if (sel_row <= 0) + return; + + NSMutableArray* row_info = [NSMutableArray arrayWithArray:(NSArray*)[gui objectForRow:GUI_ROW_SHIPYARD_LABELS]]; + while ([row_info count] < 4) + [row_info addObject:@""]; + + NSString* key = [gui keyForRow:sel_row]; + + NSDictionary* info = (NSDictionary *)[currentShipyard objectForKey:key]; + + // clean up the display ready for the newly-selected ship (if there is one) + [row_info replaceObjectAtIndex:2 withObject:@""]; + [row_info replaceObjectAtIndex:3 withObject:@""]; + for (i = GUI_ROW_SHIPYARD_INFO_START; i < GUI_ROW_MARKET_CASH - 1; i++) + { + [gui setText:@"" forRow:i]; + [gui setColor:[OOColor greenColor] forRow:i]; + } + [UNIVERSE removeDemoShips]; + + if (info) + { + // the key is a particular ship - show the details + NSString *sales_pitch = (NSString*)[info objectForKey:KEY_SHORT_DESCRIPTION]; + NSDictionary *shipDict = [info dictionaryForKey:SHIPYARD_KEY_SHIP]; + + int cargo_rating = [shipDict intForKey:@"max_cargo"]; + int cargo_extra; + cargo_extra = [shipDict intForKey:@"extra_cargo" defaultValue:15]; + float speed_rating = 0.001 * [shipDict intForKey:@"max_flight_speed"]; + + NSArray *ship_extras = [info arrayForKey:KEY_EQUIPMENT_EXTRAS]; + for (i = 0; i < [ship_extras count]; i++) + { + if ([[ship_extras stringAtIndex:i] isEqualToString:@"EQ_CARGO_BAY"]) + cargo_rating += cargo_extra; + else if ([[ship_extras stringAtIndex:i] isEqualToString:@"EQ_PASSENGER_BERTH"]) + cargo_rating -= 5; + } + + [row_info replaceObjectAtIndex:2 withObject:[NSString stringWithFormat:DESC(@"shipyard-cargo-d-tc"), cargo_rating]]; + [row_info replaceObjectAtIndex:3 withObject:[NSString stringWithFormat:DESC(@"shipyard-speed-f-ls"), speed_rating]]; + + if ([gui addLongText:sales_pitch startingAtRow:GUI_ROW_SHIPYARD_INFO_START align:GUI_ALIGN_LEFT] < GUI_ROW_MARKET_CASH - 1) + { + [self showTradeInInformationFooter]; + } + + // now display the ship + [self showShipyardModel:shipDict]; + } + else + { + // the key is a particular model of ship which we must expand... + // build an array from the entries for that model in the currentShipyard TODO + // + } + + [gui setArray:[NSArray arrayWithArray:row_info] forRow:GUI_ROW_SHIPYARD_LABELS]; +} + + +- (void) showTradeInInformationFooter +{ + GuiDisplayGen *gui = [UNIVERSE gui]; + OOCreditsQuantity tradeIn = [self tradeInValue]; + [gui setText:[NSString stringWithFormat:DESC(@"shipyard-your-@-trade-in-value-@"), [self displayName], OOCredits(tradeIn)] forRow: GUI_ROW_MARKET_CASH - 1]; + [gui setText:[NSString stringWithFormat:DESC(@"shipyard-total-available-%@-%@-plus-%@-trade"), OOCredits(credits + tradeIn), OOCredits(credits), OOCredits(tradeIn)] forRow: GUI_ROW_MARKET_CASH]; +} + + +- (void) showShipyardModel: (NSDictionary *)shipDict +{ + ShipEntity *ship; + + if (!dockedStation) + return; + + Quaternion q2 = { (GLfloat)0.707f, (GLfloat)0.707f, (GLfloat)0.0f, (GLfloat)0.0f }; + + ship = [[ShipEntity alloc] initWithDictionary:shipDict]; + [ship wasAddedToUniverse]; + + GLfloat cr = [ship collisionRadius]; + OOLog(kOOLogNoteShowShipyardModel, @"::::: showShipyardModel:'%@'.", [ship name]); + [ship setOrientation: q2]; + + [ship setPositionX:1.2 * cr y:0.8 * cr z:6.4 * cr]; + [ship setStatus: STATUS_COCKPIT_DISPLAY]; + [ship setScanClass: CLASS_NO_DRAW]; + [ship setRoll: M_PI/10.0]; + [ship setPitch: M_PI/25.0]; + if([ship pendingEscortCount] > 0) [ship setPendingEscortCount:0]; + [UNIVERSE addEntity: ship]; + [[ship getAI] setStateMachine: @"nullAI.plist"]; + + [ship release]; + +} + + +- (OOCreditsQuantity) tradeInValue +{ + // returns down to ship_trade_in_factor% of the full credit value of your ship + + /* FIXME: the trade-in value can be more than the sale value, and + ship_trade_in_factor starts at 100%, so it can be profitable to sit + and buy the same ship over and over again. This bug predates Oolite + 1.65. + Partial fix: make effective trade-in value 75% * ship_trade_in_factor% + of the "raw" trade-in value. This still allows profitability! A better + solution would be to unify the price calculation for trade-in and + for-sale ships. + -- Ahruman 20070707, fix applied 20070708 + */ + unsigned long long value = [UNIVERSE tradeInValueForCommanderDictionary:[self commanderDataDictionary]]; + value = ((value * 75 * ship_trade_in_factor) + 5000) / 10000; // Multiply by two percentages, divide by 100*100. The +5000 is to get normal rounding. + return value * 10; +} + + +- (BOOL) buySelectedShip +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + int sel_row = [gui selectedRow]; + + if (sel_row <= 0) + return NO; + + NSString* key = [gui keyForRow:sel_row]; + + if ([key hasPrefix:@"More:"]) + { + int from_ship = [[[key componentsSeparatedByString:@":"] objectAtIndex:1] intValue]; + if (from_ship < 0) from_ship = 0; + + [self setGuiToShipyardScreen:from_ship]; + if ([[UNIVERSE gui] selectedRow] < 0) + [[UNIVERSE gui] setSelectedRow:GUI_ROW_SHIPYARD_START]; + if (from_ship == 0) + [[UNIVERSE gui] setSelectedRow:GUI_ROW_SHIPYARD_START + MAX_ROWS_SHIPS_FOR_SALE - 1]; + return YES; + } + + // first check you can afford it! + NSDictionary* ship_info = [currentShipyard dictionaryForKey:key]; + OOCreditsQuantity price = [ship_info unsignedLongLongForKey:SHIPYARD_KEY_PRICE]; + OOCreditsQuantity trade_in = [self tradeInValue]; + + if (credits + trade_in < price * 10) + return NO; // you can't afford it! + + // sell all the commodities carried + unsigned i; + for (i = 0; i < [shipCommodityData count]; i++) + { + [self trySellingCommodity:i all:YES]; + } + + // We tried to sell everything. If there are still items present in our inventory, it + // means that the market got saturated (quantity in station > 127 t) before we could sell + // it all. Everything that could not be sold will be lost. -- Nikos 20083012 + if (current_cargo) + { + // Zero out our manifest. + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + for (i = 0; i < [manifest count]; i++) + { + NSMutableArray* manifest_commodity = [NSMutableArray arrayWithArray:[manifest arrayAtIndex:i]]; + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:0]]; + [manifest replaceObjectAtIndex:i withObject:manifest_commodity]; + } + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + current_cargo = 0; + } + + // drop all passengers + [passengers removeAllObjects]; + + // contracts stay the same, so if you default - tough! + // okay we need to switch the model used, lots of the stats, and add all the extras + // pay over the mazoolah + credits -= 10 * price - trade_in; + + // change ship_desc + // TODO: detect brokenness here. + if (ship_desc) [ship_desc release]; + ship_desc = [[ship_info stringForKey:SHIPYARD_KEY_SHIPDATA_KEY] copy]; + NSDictionary *shipDict = [ship_info dictionaryForKey:SHIPYARD_KEY_SHIP]; + + // get a full tank for free + [self setFuel:[self fuelCapacity]]; + + // this ship has a clean record + legalStatus = 0; + + // get forward_weapon aft_weapon port_weapon starboard_weapon from ship_info + aft_weapon = EquipmentStringToWeaponTypeSloppy([shipDict stringForKey:@"aft_weapon_type"]); + port_weapon = EquipmentStringToWeaponTypeSloppy([shipDict stringForKey:@"port_weapon_type"]); + starboard_weapon = EquipmentStringToWeaponTypeSloppy([shipDict stringForKey:@"starboard_weapon_type"]); + forward_weapon = EquipmentStringToWeaponTypeSloppy([shipDict stringForKey:@"forward_weapon_type"]); + + // get basic max_cargo + max_cargo = [UNIVERSE maxCargoForShip:ship_desc]; + + // ensure all missiles are tidied up and start at pylon 0 + [self tidyMissilePylons]; + + // get missiles from ship_info + missiles = [shipDict unsignedIntForKey:@"missiles"]; + + // reset max_passengers + max_passengers = 0; + + // reset and refill extra_equipment then set flags from it + + // keep track of portable equipment.. + + NSMutableSet *portable_equipment = [NSMutableSet set]; + NSEnumerator *eqEnum = nil; + NSString *eq_desc = nil; + OOEquipmentType *item = nil; + + for (eqEnum = [self equipmentEnumerator]; (eq_desc = [eqEnum nextObject]);) + { + item = [OOEquipmentType equipmentTypeWithIdentifier:eq_desc]; + if ([item isPortableBetweenShips]) [portable_equipment addObject:eq_desc]; + } + + // remove ALL + [self removeAllEquipment]; + + // restore portable equipment + for (eqEnum = [portable_equipment objectEnumerator]; (eq_desc = [eqEnum nextObject]); ) + { + [self addEquipmentItem:eq_desc]; + } + + // refill from ship_info + NSArray* extras = [ship_info arrayForKey:KEY_EQUIPMENT_EXTRAS]; + for (i = 0; i < [extras count]; i++) + { + NSString* eq_key = [extras stringAtIndex:i]; + if ([eq_key isEqualToString:@"EQ_PASSENGER_BERTH"]) + { + max_passengers++; + max_cargo -= 5; + } + else + { + [self addEquipmentItem:eq_key]; + } + } + + // add bought ship to shipyard_record + [shipyard_record setObject:ship_desc forKey:[ship_info objectForKey:SHIPYARD_KEY_ID]]; + + // remove the ship from the localShipyard + [[dockedStation localShipyard] removeObjectAtIndex:sel_row - GUI_ROW_SHIPYARD_START]; + + // perform the transformation + NSDictionary* cmdr_dict = [self commanderDataDictionary]; // gather up all the info + if (![self setCommanderDataFromDictionary:cmdr_dict]) return NO; + + [self setStatus:STATUS_DOCKED]; + + // adjust the clock forward by an hour + ship_clock_adjust += 3600.0; + + // finally we can get full hock if we sell it back + ship_trade_in_factor = 100; + + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + + return YES; +} + + +@end + diff --git a/src/Core/Entities/PlayerEntityControls.h b/src/Core/Entities/PlayerEntityControls.h new file mode 100644 index 00000000..d4b518a8 --- /dev/null +++ b/src/Core/Entities/PlayerEntityControls.h @@ -0,0 +1,37 @@ +/* + +PlayerEntityControls.h + +Input management methods. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#include "PlayerEntity.h" + + +@interface PlayerEntity (Controls) + +- (void) initControls; + +- (void) pollControls:(double)delta_t; +- (BOOL) handleGUIUpDownArrowKeys; + +@end diff --git a/src/Core/Entities/PlayerEntityControls.m b/src/Core/Entities/PlayerEntityControls.m new file mode 100644 index 00000000..dea34d80 --- /dev/null +++ b/src/Core/Entities/PlayerEntityControls.m @@ -0,0 +1,3041 @@ +/* + +PlayerEntityControls.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntityControls.h" +#import "PlayerEntityContracts.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "PlayerEntityScriptMethods.h" +#import "PlayerEntitySound.h" +#import "PlayerEntityLoadSave.h" +#import "PlayerEntityStickMapper.h" + +#import "ShipEntityAI.h" +#import "StationEntity.h" +#import "Universe.h" +#import "GameController.h" +#import "AI.h" +#import "MyOpenGLView.h" +#import "OOSound.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" +#import "ResourceManager.h" +#import "HeadUpDisplay.h" +#import "OOConstToString.h" +#import "OOLoggingExtended.h" +#import "OOMusicController.h" + +#import "JoystickHandler.h" + +#if OOLITE_MAC_OS_X +#import "Groolite.h" +#endif + + +static BOOL jump_pressed; +static BOOL hyperspace_pressed; +static BOOL galhyperspace_pressed; +static BOOL pause_pressed; +static BOOL compass_mode_pressed; +static BOOL next_target_pressed; +static BOOL previous_target_pressed; +static BOOL next_missile_pressed; +static BOOL fire_missile_pressed; +static BOOL target_missile_pressed; +#if TARGET_INCOMING_MISSILES +static BOOL target_incoming_missile_pressed; +#endif +static BOOL ident_pressed; +static BOOL safety_pressed; +static BOOL cloak_pressed; +static BOOL rotateCargo_pressed; +static BOOL autopilot_key_pressed; +static BOOL fast_autopilot_key_pressed; +static BOOL target_autopilot_key_pressed; +#if DOCKING_CLEARANCE_ENABLED +static BOOL docking_clearance_request_key_pressed; +#endif +#ifndef NDEBUG +static BOOL dump_target_state_pressed; +#endif +static BOOL f_key_pressed; +static BOOL m_key_pressed; +static BOOL taking_snapshot; +static BOOL pling_pressed; +static BOOL cursor_moving; +static BOOL disc_operation_in_progress; +static BOOL switching_resolution; +static BOOL wait_for_key_up; +static BOOL upDownKeyPressed; +static BOOL leftRightKeyPressed; +static BOOL musicModeKeyPressed; +static BOOL enterSelectKeyPressed; +static BOOL volumeControlPressed; +static BOOL shaderSelectKeyPressed; +static BOOL selectPressed; +static BOOL queryPressed; +static BOOL spacePressed; +static BOOL switching_chart_screens; +static BOOL switching_status_screens; +static BOOL switching_market_screens; +static BOOL switching_equipship_screens; +static BOOL zoom_pressed; +static BOOL customView_pressed; + +static unsigned searchStringLength; +static double timeLastKeyPress; +static OOGUIRow oldSelection; +static int saved_view_direction; +static double saved_script_time; +static int saved_gui_screen; +static NSTimeInterval time_last_frame; + + +@interface PlayerEntity (OOControlsPrivate) + +- (void) pollFlightControls:(double) delta_t; +- (void) pollFlightArrowKeyControls:(double)delta_t; +- (void) pollGuiArrowKeyControls:(double)delta_t; +- (void) handleGameOptionsScreenKeys; +- (void) pollApplicationControls; +- (void) pollViewControls; +- (void) pollGuiScreenControls; +- (void) pollGameOverControls:(double) delta_t; +- (void) pollAutopilotControls:(double) delta_t; +- (void) pollDockedControls:(double) delta_t; +- (void) pollDemoControls:(double) delta_t; + +@end + + +@implementation PlayerEntity (Controls) + +- (void) initControls +{ + NSMutableDictionary *kdic = [NSMutableDictionary dictionaryWithDictionary:[ResourceManager dictionaryFromFilesNamed:@"keyconfig.plist" inFolder:@"Config" mergeMode:MERGE_BASIC cache:NO]]; + + // pre-process kdic - replace any strings with an integer representing the ASCII value of the first character + + unsigned i; + NSArray *keys = nil; + id key = nil, value = nil; + int iValue; + unsigned char keychar; + NSString *keystring = nil; + +#if OOLITE_WINDOWS + // override windows keyboard autoselect + [[UNIVERSE gameView] setKeyboardTo:[kdic stringForKey:@"windows_keymap" defaultValue:@"auto"]]; +#endif + + keys = [kdic allKeys]; + for (i = 0; i < [keys count]; i++) + { + key = [keys objectAtIndex:i]; + value = [kdic objectForKey: key]; + iValue = [value intValue]; + + // for '0' '1' '2' '3' '4' '5' '6' '7' '8' '9' - we want to interpret those as strings - not numbers + // alphabetical characters and symbols will return an intValue of 0. + + if ([value isKindOfClass:[NSString class]] && (iValue < 10)) + { + keystring = value; + if ([keystring length] == 1 || (iValue == 0 && [keystring length] != 0)) + { + keychar = [keystring characterAtIndex: 0] & 0x00ff; // uses lower byte of unichar + } + else if (iValue <= 0xFF) keychar = iValue; + else continue; + + [kdic setObject:[NSNumber numberWithUnsignedChar:keychar] forKey:key]; + } + } + + // set default keys. +#define LOAD_KEY_SETTING(name, default) name = [kdic unsignedShortForKey:@#name defaultValue:default] + + LOAD_KEY_SETTING(key_roll_left, gvArrowKeyLeft ); + LOAD_KEY_SETTING(key_roll_right, gvArrowKeyRight ); + LOAD_KEY_SETTING(key_pitch_forward, gvArrowKeyUp ); + LOAD_KEY_SETTING(key_pitch_back, gvArrowKeyDown ); + LOAD_KEY_SETTING(key_yaw_left, ',' ); + LOAD_KEY_SETTING(key_yaw_right, '.' ); + + LOAD_KEY_SETTING(key_increase_speed, 'w' ); + LOAD_KEY_SETTING(key_decrease_speed, 's' ); + LOAD_KEY_SETTING(key_inject_fuel, 'i' ); + + LOAD_KEY_SETTING(key_fire_lasers, 'a' ); + LOAD_KEY_SETTING(key_launch_missile, 'm' ); + LOAD_KEY_SETTING(key_next_missile, 'y' ); + LOAD_KEY_SETTING(key_ecm, 'e' ); + + LOAD_KEY_SETTING(key_target_missile, 't' ); + LOAD_KEY_SETTING(key_untarget_missile, 'u' ); +#if TARGET_INCOMING_MISSILES + LOAD_KEY_SETTING(key_target_incoming_missile, 'T' ); +#endif + LOAD_KEY_SETTING(key_ident_system, 'r' ); + + LOAD_KEY_SETTING(key_scanner_zoom, 'z' ); + LOAD_KEY_SETTING(key_scanner_unzoom, 'Z' ); + + LOAD_KEY_SETTING(key_launch_escapepod, 27 /* esc */ ); + LOAD_KEY_SETTING(key_energy_bomb, '\t' ); + + LOAD_KEY_SETTING(key_galactic_hyperspace, 'g' ); + LOAD_KEY_SETTING(key_hyperspace, 'h' ); + LOAD_KEY_SETTING(key_jumpdrive, 'j' ); + + LOAD_KEY_SETTING(key_dump_cargo, 'd' ); + LOAD_KEY_SETTING(key_rotate_cargo, 'R' ); + + LOAD_KEY_SETTING(key_autopilot, 'c' ); + LOAD_KEY_SETTING(key_autopilot_target, 'C' ); + LOAD_KEY_SETTING(key_autodock, 'D' ); +#if DOCKING_CLEARANCE_ENABLED + LOAD_KEY_SETTING(key_docking_clearance_request, 'L' ); +#endif + + LOAD_KEY_SETTING(key_snapshot, '*' ); + LOAD_KEY_SETTING(key_docking_music, 's' ); + + LOAD_KEY_SETTING(key_advanced_nav_array, '^' ); + LOAD_KEY_SETTING(key_map_home, gvHomeKey ); + LOAD_KEY_SETTING(key_map_info, 'i' ); + + LOAD_KEY_SETTING(key_pausebutton, 'p' ); + LOAD_KEY_SETTING(key_show_fps, 'F' ); + LOAD_KEY_SETTING(key_mouse_control, 'M' ); + + LOAD_KEY_SETTING(key_comms_log, '`' ); + LOAD_KEY_SETTING(key_next_compass_mode, '\\' ); + + LOAD_KEY_SETTING(key_cloaking_device, '0' ); + + LOAD_KEY_SETTING(key_contract_info, '\?' ); + + LOAD_KEY_SETTING(key_next_target, '+' ); + LOAD_KEY_SETTING(key_previous_target, '-' ); + + LOAD_KEY_SETTING(key_custom_view, 'v' ); + +#ifndef NDEBUG + LOAD_KEY_SETTING(key_dump_target_state, 'H' ); +#endif + + if (key_yaw_left == key_roll_left && key_yaw_left == ',') key_yaw_left = 0; + if (key_yaw_right == key_roll_right && key_yaw_right == '.') key_yaw_right = 0; + + // other keys are SET and cannot be varied + + // Enable polling + pollControls=YES; +} + + +- (void) pollControls:(double)delta_t +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + + NS_DURING + if (gameView) + { + // poll the gameView keyboard things + [self pollApplicationControls]; // quit command-f etc. + switch ([self status]) + { + case STATUS_WITCHSPACE_COUNTDOWN: + case STATUS_IN_FLIGHT: + [self pollFlightControls:delta_t]; + break; + + case STATUS_DEAD: + [self pollGameOverControls:delta_t]; + break; + + case STATUS_AUTOPILOT_ENGAGED: + [self pollAutopilotControls:delta_t]; + break; + + case STATUS_DOCKED: + [self pollDockedControls:delta_t]; + break; + + case STATUS_START_GAME: + [self pollDemoControls:delta_t]; + break; + + default: + break; + } + } + NS_HANDLER + OOLog(kOOLogException, @"***** Exception checking controls: %@ : %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + +// DJS + aegidian: Moved from the big switch/case block in pollGuiArrowKeyControls +- (BOOL) handleGUIUpDownArrowKeys +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + GuiDisplayGen *gui = [UNIVERSE gui]; + BOOL result = NO; + BOOL arrow_up = [gameView isDown:gvArrowKeyUp]; + BOOL arrow_down = [gameView isDown:gvArrowKeyDown]; + BOOL mouse_click = [gameView isDown:gvMouseLeftButton]; + + if (arrow_down) + { + if ((!upDownKeyPressed) || (script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([gui setNextRow: +1]) + { + [self playMenuNavigationDown]; + result = YES; + } + else + { + [self playMenuNavigationNot]; + } + timeLastKeyPress = script_time; + } + } + + if (arrow_up) + { + if ((!upDownKeyPressed) || (script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([gui setNextRow: -1]) + { + [self playMenuNavigationUp]; + result = YES; + } + else + { + [self playMenuNavigationNot]; + } + timeLastKeyPress = script_time; + } + } + + if (mouse_click) + { + if (!upDownKeyPressed) + { + int click_row = 0; + if (UNIVERSE) + click_row = UNIVERSE->cursor_row; + if ([gui setSelectedRow:click_row]) + { + result = YES; + } + } + } + + upDownKeyPressed = (arrow_up || arrow_down || mouse_click); + + return result; +} + +@end + + +@implementation PlayerEntity (OOControlsPrivate) + +- (void) pollApplicationControls +{ + if(!pollControls) + return; + + // does fullscreen / quit / snapshot + MyOpenGLView *gameView = [UNIVERSE gameView]; + + // command-key controls + if (([gameView isCommandDown])&&([[gameView gameController] inFullScreenMode])) + { + if (([gameView isCommandDown])&&([gameView isDown:102])) // command f + { + [[gameView gameController] exitFullScreenMode]; + if (mouse_control_on) + [UNIVERSE addMessage:DESC(@"mouse-off") forCount:3.0]; + mouse_control_on = NO; + } + + if (([gameView isCommandDown])&&([gameView isDown:113])) // command q + { + [[gameView gameController] pauseFullScreenModeToPerform:@selector(exitApp) onTarget:[gameView gameController]]; + } + } + +#if OOLITE_WINDOWS + if ( ([gameView isDown:'Q']) ) + { + [[gameView gameController] exitApp]; + exit(0); // Force it + } +#endif + + // handle pressing Q or [esc] in error-handling mode + if ([self status] == STATUS_HANDLING_ERROR) + { + if ([gameView isDown:113]||[gameView isDown:81]||[gameView isDown:27]) // 'q' | 'Q' | esc + { + [[gameView gameController] exitApp]; + } + } + + // snapshot + if ([gameView isDown:key_snapshot]) // '*' key + { + if (!taking_snapshot) + { + taking_snapshot = YES; + [gameView snapShot]; + } + } + else + { + taking_snapshot = NO; + } + + // FPS display + if ([gameView isDown:key_show_fps]) // 'F' key + { + if (!f_key_pressed) [UNIVERSE setDisplayFPS:![UNIVERSE displayFPS]]; + f_key_pressed = YES; + } + else + { + f_key_pressed = NO; + } + + // Mouse control + BOOL allowMouseControl; +#if OO_DEBUG + allowMouseControl = YES; +#else + allowMouseControl = [[gameView gameController] inFullScreenMode]; +#endif + + if (allowMouseControl) + { + if ([gameView isDown:key_mouse_control]) // 'M' key + { + if (!m_key_pressed) + { + mouse_control_on = !mouse_control_on; + if (mouse_control_on) + { + [UNIVERSE addMessage:DESC(@"mouse-on") forCount:3.0]; + /* Ensure the keyboard pitch override (intended to lock + out the joystick if the player runs to the keyboard) + is reset */ + keyboardRollPitchOverride = NO; + keyboardYawOverride = NO; + } + else + { + [UNIVERSE addMessage:DESC(@"mouse-off") forCount:3.0]; + } + } + m_key_pressed = YES; + } + else + { + m_key_pressed = NO; + } + } + else + { + if (mouse_control_on) + { + mouse_control_on = NO; + [UNIVERSE addMessage:DESC(@"mouse-off") forCount:3.0]; + } + } +} + + +- (void) pollFlightControls:(double)delta_t +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + + // DJS: TODO: Sort where SDL keeps its stuff. + if(!stickHandler) + { + stickHandler=[gameView getStickHandler]; + } + const BOOL *joyButtonState = [stickHandler getAllButtonStates]; + + BOOL paused = [[gameView gameController] gameIsPaused]; + double speed_delta = 5.0 * thrust; + + if (!paused) + { + // arrow keys + if ([UNIVERSE displayGUI]) + [self pollGuiArrowKeyControls:delta_t]; + else + [self pollFlightArrowKeyControls:delta_t]; + + // view keys + [self pollViewControls]; + + if (![UNIVERSE displayCursor]) + { + if ((joyButtonState[BUTTON_FUELINJECT] || [gameView isDown:key_inject_fuel]) && + [self hasFuelInjection] && + !hyperspeed_engaged) + { + if (fuel > 0 && !afterburner_engaged) + { + [UNIVERSE addMessage:DESC(@"fuel-inject-on") forCount:1.5]; + afterburner_engaged = YES; + [self startAfterburnerSound]; + } + else + { + if (fuel <= 0.0) + [UNIVERSE addMessage:DESC(@"fuel-out") forCount:1.5]; + } + afterburner_engaged = (fuel > 0); + } + else + afterburner_engaged = NO; + + if ((!afterburner_engaged)&&(afterburnerSoundLooping)) + [self stopAfterburnerSound]; + +#if OOLITE_HAVE_JOYSTICK + // DJS: Thrust can be an axis or a button. Axis takes precidence. + double reqSpeed=[stickHandler getAxisState: AXIS_THRUST]; + if(reqSpeed == STICK_AXISUNASSIGNED || [stickHandler getNumSticks] == 0) + { + // DJS: original keyboard code + if (([gameView isDown:key_increase_speed] || joyButtonState[BUTTON_INCTHRUST])&&(flightSpeed < maxFlightSpeed)&&(!afterburner_engaged)) + { + flightSpeed += speed_delta * delta_t; + } + + // ** tgape ** - decrease obviously means no hyperspeed + if (([gameView isDown:key_decrease_speed] || joyButtonState[BUTTON_DECTHRUST])&&(!afterburner_engaged)) + { + flightSpeed -= speed_delta * delta_t; + + // ** tgape ** - decrease obviously means no hyperspeed + hyperspeed_engaged = NO; + } + } // DJS: STICK_NOFUNCTION else...a joystick axis is assigned to thrust. + else + { + if (flightSpeed < maxFlightSpeed * reqSpeed) + { + flightSpeed += speed_delta * delta_t; + } + if (flightSpeed > maxFlightSpeed * reqSpeed) + { + flightSpeed -= speed_delta * delta_t; + } + } // DJS: end joystick thrust axis +#else + if (([gameView isDown:key_increase_speed])&&(flightSpeed < maxFlightSpeed)&&(!afterburner_engaged)) + { + flightSpeed += speed_delta * delta_t; + } + + if (([gameView isDown:key_decrease_speed])&&(!afterburner_engaged)) + { + flightSpeed -= speed_delta * delta_t; + // ** tgape ** - decrease obviously means no hyperspeed + hyperspeed_engaged = NO; + } +#endif + if (!afterburner_engaged && ![self atHyperspeed] && !hyperspeed_engaged) + { + flightSpeed = OOClamp_0_max_f(flightSpeed, maxFlightSpeed); + } + + // hyperspeed controls + if ([gameView isDown:key_jumpdrive] || joyButtonState[BUTTON_HYPERSPEED]) // 'j' + { + if (!jump_pressed) + { + if (!hyperspeed_engaged) + { + hyperspeed_locked = [self massLocked]; + hyperspeed_engaged = !hyperspeed_locked; + if (hyperspeed_locked) + { + [self playJumpMassLocked]; + [UNIVERSE addMessage:DESC(@"jump-mass-locked") forCount:1.5]; + } + } + else + { + hyperspeed_engaged = NO; + } + } + jump_pressed = YES; + } + else + { + jump_pressed = NO; + } + + // shoot 'a' + if ((([gameView isDown:key_fire_lasers])||((mouse_control_on)&&([gameView isDown:gvMouseLeftButton]))||joyButtonState[BUTTON_FIRE])&&(shot_time > weapon_reload_time)) + + { + if ([self fireMainWeapon]) + { + [self playLaserHit:target_laser_hit != NO_TARGET]; + } + } + + // shoot 'm' // launch missile + if ([gameView isDown:key_launch_missile] || joyButtonState[BUTTON_LAUNCHMISSILE]) + { + // launch here + if (!fire_missile_pressed) + { + [self fireMissile]; + fire_missile_pressed = YES; + } + } + else fire_missile_pressed = NO; + + // shoot 'y' // next missile + if ([gameView isDown:key_next_missile] || joyButtonState[BUTTON_CYCLEMISSILE]) + { + if ((!ident_engaged)&&(!next_missile_pressed)) + { + [self playNextMissileSelected]; + [self selectNextMissile]; + } + next_missile_pressed = YES; + } + else next_missile_pressed = NO; + + // '+' // next target + if ([gameView isDown:key_next_target]) + { + if ((!next_target_pressed)&&([self hasEquipmentItem:@"EQ_TARGET_MEMORY"])) + { + [self moveTargetMemoryBy:+1]; + } + next_target_pressed = YES; + } + else next_target_pressed = NO; + + // '-' // previous target + if ([gameView isDown:key_previous_target]) + { + if ((!previous_target_pressed)&&([self hasEquipmentItem:@"EQ_TARGET_MEMORY"])) + { + [self moveTargetMemoryBy:-1]; + } + previous_target_pressed = YES; + } + else previous_target_pressed = NO; + + // shoot 'r' // switch on ident system + if ([gameView isDown:key_ident_system] || joyButtonState[BUTTON_ID]) + { + // ident 'on' here + if (!ident_pressed) + { + // Clear current target if we're already in Ident mode + if (ident_engaged) + { + primaryTarget = NO_TARGET; + } + [self safeAllMissiles]; + ident_engaged = YES; + if ([self primaryTargetID] == NO_TARGET) + { + [self playIdentOn]; + [UNIVERSE addMessage:DESC(@"ident-on") forCount:2.0]; + } + else + { + [self playIdentLockedOn]; + [self printIdentLockedOnForMissile:NO]; + } + } + ident_pressed = YES; + } + else ident_pressed = NO; +#if TARGET_INCOMING_MISSILES + // target nearest incoming missile 'T' - useful for quickly giving a missile target to turrets + if ([gameView isDown:key_target_incoming_missile] || joyButtonState[BUTTON_TARGETINCOMINGMISSILE]) + { + if (!target_incoming_missile_pressed) + { + [self targetNearestIncomingMissile]; + } + target_incoming_missile_pressed = YES; + } + else target_incoming_missile_pressed = NO; +#endif + + // shoot 't' // switch on missile targetting + if (([gameView isDown:key_target_missile] || joyButtonState[BUTTON_ARMMISSILE])&&(missile_entity[activeMissile])) + { + // targetting 'on' here + if (!target_missile_pressed) + { + // Clear current target if we're already in Missile Targetting mode + if (missile_status != MISSILE_STATUS_SAFE) + { + primaryTarget = NO_TARGET; + } + + // Arm missile and check for missile lock + missile_status = MISSILE_STATUS_ARMED; + if ([missile_entity[activeMissile] isMissile]) + { + if ([[self primaryTarget] isShip]) + { + missile_status = MISSILE_STATUS_TARGET_LOCKED; + [missile_entity[activeMissile] addTarget:[self primaryTarget]]; + [self printIdentLockedOnForMissile:YES]; + [self playMissileLockedOn]; + } + else + { + [self removeTarget:nil]; + [missile_entity[activeMissile] removeTarget:nil]; + [UNIVERSE addMessage:DESC(@"missile-armed") forCount:2.0]; + [self playMissileArmed]; + } + } + else if ([missile_entity[activeMissile] isMine]) + { + [UNIVERSE addMessage:DESC(@"mine-armed") forCount:4.5]; + [self playMineArmed]; + } + ident_engaged = NO; + } + target_missile_pressed = YES; + } + else target_missile_pressed = NO; + + // shoot 'u' // disarm missile targetting + if ([gameView isDown:key_untarget_missile] || joyButtonState[BUTTON_UNARM]) + { + if (!safety_pressed) + { + //targetting off in both cases! + primaryTarget = NO_TARGET; + [self safeAllMissiles]; + if (!ident_engaged) + { + [UNIVERSE addMessage:DESC(@"missile-safe") forCount:2.0]; + [self playMissileSafe]; + } + else + { + [UNIVERSE addMessage:DESC(@"ident-off") forCount:2.0]; + [self playIdentOff]; + } + ident_engaged = NO; + } + safety_pressed = YES; + } + else safety_pressed = NO; + + // shoot 'e' // ECM + if (([gameView isDown:key_ecm] || joyButtonState[BUTTON_ECM]) && [self hasECM]) + { + if (!ecm_in_operation) + { + if ([self fireECM]) + { + [self playFiredECMSound]; + [UNIVERSE addMessage:DESC(@"ecm-on") forCount:3.0]; + } + } + } + + // shoot 'tab' // Energy bomb + if (([gameView isDown:key_energy_bomb] || joyButtonState[BUTTON_ENERGYBOMB]) && [self hasEnergyBomb]) + { + // original energy bomb routine + [self fireEnergyBomb]; + [self removeEquipmentItem:@"EQ_ENERGY_BOMB"]; + } + + // shoot 'escape' // Escape pod launch + if (([gameView isDown:key_launch_escapepod] || joyButtonState[BUTTON_ESCAPE]) && [self hasEscapePod] && [UNIVERSE station] != nil) + + { + found_target = [self launchEscapeCapsule]; + } + + // shoot 'd' // Dump Cargo + if (([gameView isDown:key_dump_cargo] || joyButtonState[BUTTON_JETTISON]) && [cargo count] > 0) + { + [self dumpCargo]; + } + + // shoot 'R' // Rotate Cargo + if ([gameView isDown:key_rotate_cargo]) + { + if ((!rotateCargo_pressed)&&([cargo count] > 0)) + [self rotateCargo]; + rotateCargo_pressed = YES; + } + else + rotateCargo_pressed = NO; + + // autopilot 'c' + if ([gameView isDown:key_autopilot] || joyButtonState[BUTTON_DOCKCPU]) // look for the 'c' key + { + if ([self hasDockingComputer] && !autopilot_key_pressed) // look for the 'c' key + { + BOOL isUsingDockingAI = [[shipAI name] isEqual: PLAYER_DOCKING_AI_NAME]; + BOOL isOkayToUseAutopilot = YES; + + if (isUsingDockingAI) + { + if ([self checkForAegis] != AEGIS_IN_DOCKING_RANGE) + { + isOkayToUseAutopilot = NO; + [self playAutopilotOutOfRange]; + [UNIVERSE addMessage:DESC(@"autopilot-out-of-range") forCount:4.5]; + } + } + + if (isOkayToUseAutopilot) + { + primaryTarget = NO_TARGET; + targetStation = [[UNIVERSE station] universalID]; + autopilot_engaged = YES; + ident_engaged = NO; + [self safeAllMissiles]; + velocity = kZeroVector; + [self setStatus:STATUS_AUTOPILOT_ENGAGED]; + [shipAI setState:@"GLOBAL"]; // reboot the AI + [self playAutopilotOn]; +#if DOCKING_CLEARANCE_ENABLED + [self setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; +#endif + [UNIVERSE addMessage:DESC(@"autopilot-on") forCount:4.5]; + [self doScriptEvent:@"playerStartedAutoPilot"]; + + [[OOMusicController sharedController] playDockingMusic]; + + if (afterburner_engaged) + { + afterburner_engaged = NO; + if (afterburnerSoundLooping) + [self stopAfterburnerSound]; + } + } + } + autopilot_key_pressed = YES; + } + else + autopilot_key_pressed = NO; + + // autopilot 'C' - dock with target + if ([gameView isDown:key_autopilot_target]) // look for the 'C' key + { + if ([self hasDockingComputer] && (!target_autopilot_key_pressed)) + { + Entity* primeTarget = [self primaryTarget]; + BOOL primeTargetIsHostile = [self hasHostileTarget]; + if ((primeTarget) && (primeTarget->isStation) && + [primeTarget isKindOfClass:[StationEntity class]] && + !primeTargetIsHostile) + { + targetStation = primaryTarget; + primaryTarget = NO_TARGET; + autopilot_engaged = YES; + ident_engaged = NO; + [self safeAllMissiles]; + velocity = kZeroVector; + [self setStatus:STATUS_AUTOPILOT_ENGAGED]; + [shipAI setState:@"GLOBAL"]; // restart the AI + [self playAutopilotOn]; +#if DOCKING_CLEARANCE_ENABLED + [self setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; +#endif + [UNIVERSE addMessage:DESC(@"autopilot-on") forCount:4.5]; + [self doScriptEvent:@"playerStartedAutoPilot"]; + + [[OOMusicController sharedController] playDockingMusic]; + + if (afterburner_engaged) + { + afterburner_engaged = NO; + if (afterburnerSoundLooping) + [self stopAfterburnerSound]; + } + } + else + { + [self playAutopilotCannotDockWithTarget]; + if (primeTargetIsHostile && [primeTarget isStation]) + { + [UNIVERSE addMessage:DESC(@"autopilot-target-docking-instructions-denied") forCount:4.5]; + } + else + { + [UNIVERSE addMessage:DESC(@"autopilot-cannot-dock-with-target") forCount:4.5]; + } + } + } + target_autopilot_key_pressed = YES; + } + else + target_autopilot_key_pressed = NO; + + // autopilot 'D' + if ([gameView isDown:key_autodock] || joyButtonState[BUTTON_DOCKCPUFAST]) // look for the 'D' key + { + if ([self hasDockingComputer] && (!fast_autopilot_key_pressed)) // look for the 'D' key + { + if ([self checkForAegis] == AEGIS_IN_DOCKING_RANGE) + { + StationEntity *the_station = [UNIVERSE station]; + if (the_station) + { + if (legalStatus > 50) + { + [self setStatus:STATUS_AUTOPILOT_ENGAGED]; + [self interpretAIMessage:@"DOCKING_REFUSED"]; + } + else + { + if (legalStatus > 0) + { + // there's a slight chance you'll be fined for your past offences when autodocking + int fine_chance = ranrot_rand() & 0x03ff; // 0..1023 + int government = 1 + [[UNIVERSE currentSystemData] intForKey:KEY_GOVERNMENT]; // 1..8 + if ([UNIVERSE inInterstellarSpace]) government = 2; // equivalent to Feudal. I'm assuming any station in interstellar space is military. -- Ahruman 2008-05-29 + fine_chance /= government; + if (fine_chance < legalStatus) + { + [self markForFines]; + } + } +#if DOCKING_CLEARANCE_ENABLED + [self setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; +#endif + ship_clock_adjust = 1200.0; // 20 minutes penalty to enter dock + ident_engaged = NO; + [self safeAllMissiles]; + [UNIVERSE setViewDirection:VIEW_FORWARD]; + [self enterDock:the_station]; + } + } + } + else + { + [self playAutopilotOutOfRange]; + [UNIVERSE addMessage:DESC(@"autopilot-out-of-range") forCount:4.5]; + } + } + fast_autopilot_key_pressed = YES; + } + else + fast_autopilot_key_pressed = NO; + +#if DOCKING_CLEARANCE_ENABLED + // docking clearance request 'L', not available in strict mode + if ([gameView isDown:key_docking_clearance_request] && ![UNIVERSE strict]) + { + if (!docking_clearance_request_key_pressed) + { + Entity *primeTarget = [self primaryTarget]; + if ((primeTarget)&&(primeTarget->isStation)&&[primeTarget isKindOfClass:[StationEntity class]]) + { + NSString *stationDockingClearanceStatus = [(StationEntity*)primeTarget acceptDockingClearanceRequestFrom:self]; + if (stationDockingClearanceStatus != nil) + { + [self doScriptEvent:@"playerRequestedDockingClearance" withArgument:stationDockingClearanceStatus]; + } + } + } + docking_clearance_request_key_pressed = YES; + } + else + docking_clearance_request_key_pressed = NO; +#endif + + // hyperspace 'h' + if ([gameView isDown:key_hyperspace] || joyButtonState[BUTTON_HYPERDRIVE]) // look for the 'h' key + { + if (!hyperspace_pressed) + { + float dx = target_system_seed.d - galaxy_coordinates.x; + float dy = target_system_seed.b - galaxy_coordinates.y; + double distance = distanceBetweenPlanetPositions(target_system_seed.d,target_system_seed.b,galaxy_coordinates.x,galaxy_coordinates.y); + BOOL jumpOK = YES; + + if ((dx == 0) && (dy == 0) && equal_seeds(target_system_seed, system_seed)) + { + [self playHyperspaceNoTarget]; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-no-target") forCount:3.0]; + jumpOK = NO; + } + + if (distance > 7) + { + [self playHyperspaceNoFuel]; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-too-far") forCount:3.0]; + jumpOK = NO; + } + else if ((10.0 * distance > fuel)||(fuel == 0)) + { + [self playHyperspaceNoFuel]; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-no-fuel") forCount:3.0]; + jumpOK = NO; + } + + if ([self status] == STATUS_WITCHSPACE_COUNTDOWN) + { + // abort! + jumpOK = NO; + galactic_witchjump = NO; + [self setStatus:STATUS_IN_FLIGHT]; + [self playHyperspaceAborted]; + // say it! + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-user-abort") forCount:3.0]; + + [self doScriptEvent:@"playerCancelledJumpCountdown"]; + } + + if (jumpOK) + { + galactic_witchjump = NO; + witchspaceCountdown = hyperspaceMotorSpinTime; + [self setStatus:STATUS_WITCHSPACE_COUNTDOWN]; + [self playStandardHyperspace]; + // say it! + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"witch-to-@-in-f-seconds"), [UNIVERSE getSystemName:target_system_seed], witchspaceCountdown] forCount:1.0]; + + [self doScriptEvent:@"playerStartedJumpCountdown" + withArguments:[NSArray arrayWithObjects:@"standard", [NSNumber numberWithFloat:witchspaceCountdown], nil]]; + } + } + hyperspace_pressed = YES; + } + else + hyperspace_pressed = NO; + + // Galactic hyperspace 'g' + if (([gameView isDown:key_galactic_hyperspace] || joyButtonState[BUTTON_GALACTICDRIVE]) && + ([self hasEquipmentItem:@"EQ_GAL_DRIVE"]))// look for the 'g' key + { + if (!galhyperspace_pressed) + { + BOOL jumpOK = YES; + + if ([self status] == STATUS_WITCHSPACE_COUNTDOWN) + { + // abort! + jumpOK = NO; + galactic_witchjump = NO; + [self setStatus:STATUS_IN_FLIGHT]; + [self playHyperspaceAborted]; + // say it! + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"witch-user-abort") forCount:3.0]; + + [self doScriptEvent:@"playerCancelledJumpCountdown"]; + } + + if (jumpOK) + { + galactic_witchjump = YES; + witchspaceCountdown = hyperspaceMotorSpinTime; + [self setStatus:STATUS_WITCHSPACE_COUNTDOWN]; + [self playGalacticHyperspace]; + // say it! + [UNIVERSE addMessage:[NSString stringWithFormat:DESC(@"witch-galactic-in-f-seconds"), witchspaceCountdown] forCount:1.0]; + + [self doScriptEvent:@"playerStartedJumpCountdown" + withArguments:[NSArray arrayWithObjects:@"galactic", [NSNumber numberWithFloat:witchspaceCountdown], nil]]; + } + } + galhyperspace_pressed = YES; + } + else + galhyperspace_pressed = NO; + + // shoot '0' // Cloaking Device + if (([gameView isDown:key_cloaking_device] || joyButtonState[BUTTON_CLOAK]) && [self hasCloakingDevice]) + { + if (!cloak_pressed) + { + if (!cloaking_device_active) + { + if ([self activateCloakingDevice]) + { + [UNIVERSE addMessage:DESC(@"cloak-on") forCount:2]; + [self playCloakingDeviceOn]; + } + else + { + [UNIVERSE addMessage:DESC(@"cloak-low-juice") forCount:3]; + [self playCloakingDeviceInsufficientEnergy]; + } + } + else + { + [self deactivateCloakingDevice]; + [UNIVERSE addMessage:DESC(@"cloak-off") forCount:2]; + [self playCloakingDeviceOff]; + } + } + cloak_pressed = YES; + } + else + cloak_pressed = NO; + + } + +#ifndef NDEBUG + if ([gameView isDown:key_dump_target_state]) + { + if (!dump_target_state_pressed) + { + dump_target_state_pressed = YES; + id target = [self primaryTarget]; + if (target == nil) target = self; + [target dumpState]; + } + } + else dump_target_state_pressed = NO; +#endif + + + // text displays + [self pollGuiScreenControls]; + } + else + { + // game is paused + + // check options menu request + if ((([gameView isDown:gvFunctionKey2])||([gameView isDown:gvNumberKey2]))&&(gui_screen != GUI_SCREEN_OPTIONS)) + { + [gameView clearKeys]; + [self setGuiToLoadSaveScreen]; + } + + if (gui_screen == GUI_SCREEN_OPTIONS || gui_screen == GUI_SCREEN_GAMEOPTIONS || gui_screen == GUI_SCREEN_STICKMAPPER) + { + [[UNIVERSE message_gui] leaveLastLine]; + NSTimeInterval time_this_frame = [NSDate timeIntervalSinceReferenceDate]; + OOTimeDelta time_delta; + if (![[GameController sharedController] gameIsPaused]) + { + time_delta = time_this_frame - time_last_frame; + time_last_frame = time_this_frame; + time_delta = OOClamp_0_max_d(time_delta, MINIMUM_GAME_TICK); + } + else + { + time_delta = 0.0; + } + + script_time += time_delta; + [self pollGuiArrowKeyControls:time_delta]; + } + +#ifndef NDEBUG + // look for debugging keys + if ([gameView isDown:48])// look for the '0' key + { + if (!cloak_pressed) + { + [UNIVERSE obj_dump]; // dump objects + gDebugFlags = 0; + [UNIVERSE addMessage:@"Entity List dumped. Debugging OFF" forCount:3]; + } + cloak_pressed = YES; + } + else + cloak_pressed = NO; + + // look for debugging keys + if ([gameView isDown:'d'])// look for the 'd' key + { + gDebugFlags = DEBUG_ALL; + [UNIVERSE addMessage:@"Full debug ON" forCount:3]; + } + + if ([gameView isDown:'b'])// look for the 'b' key + { + gDebugFlags |= DEBUG_COLLISIONS; + [UNIVERSE addMessage:@"Collision debug ON" forCount:3]; + } + + if ([gameView isDown:'x'])// look for the 'x' key + { + gDebugFlags |= DEBUG_BOUNDING_BOXES; + [UNIVERSE addMessage:@"Bounding box debug ON" forCount:3]; + } + + if ([gameView isDown:'c'])// look for the 'c' key + { + gDebugFlags |= DEBUG_OCTREE; + [UNIVERSE addMessage:@"Octree debug ON" forCount:3]; + } + +#endif + + if ([gameView isDown:'s'])// look for the 's' key + { + OOLogSetDisplayMessagesInClass(@"$shaderDebugOn", YES); + [UNIVERSE addMessage:@"Shader debug ON" forCount:3]; + } + +#ifndef NDEBUG + if ([gameView isDown:'o'])// look for the 'o' key + { + gDebugFlags |= DEBUG_HIDE_HUD; + } +#endif + + if (([gameView isDown:gvArrowKeyLeft] || [gameView isDown:gvArrowKeyRight]) && gui_screen != GUI_SCREEN_GAMEOPTIONS) + { + if (!leftRightKeyPressed) + { + float newTimeAccelerationFactor = [gameView isDown:gvArrowKeyLeft] ? + OOMax_f([UNIVERSE timeAccelerationFactor] / 2.0f, TIME_ACCELERATION_FACTOR_MIN) : + OOMin_f([UNIVERSE timeAccelerationFactor] * 2.0f, TIME_ACCELERATION_FACTOR_MAX); + [UNIVERSE setTimeAccelerationFactor:newTimeAccelerationFactor]; + } + leftRightKeyPressed = YES; + } + else + leftRightKeyPressed = NO; + + + if ([gameView isDown:'n'])// look for the 'n' key + { +#ifndef NDEBUG + gDebugFlags = 0; + [UNIVERSE addMessage:@"All debug flags OFF" forCount:3]; +#else + [UNIVERSE addMessage:@"Shader debug OFF" forCount:3]; +#endif // NDEBUG + OOLogSetDisplayMessagesInClass(@"$shaderDebugOn", NO); + } + } + + // Pause game 'p' + if ([gameView isDown:key_pausebutton])// look for the 'p' key + { + if (!pause_pressed) + { + if (paused) + { + int previousGuiScreen = gui_screen; + script_time = saved_script_time; + // Reset to correct GUI screen, if we are unpausing from one. + gui_screen = saved_gui_screen; + switch (gui_screen) + { + case GUI_SCREEN_STATUS: + [self setGuiToStatusScreen]; + break; + case GUI_SCREEN_SHORT_RANGE_CHART: + [self setGuiToShortRangeChartScreen]; + break; + case GUI_SCREEN_LONG_RANGE_CHART: + [self setGuiToLongRangeChartScreen]; + break; + case GUI_SCREEN_MANIFEST: + [self setGuiToManifestScreen]; + break; + case GUI_SCREEN_MARKET: + [self setGuiToMarketScreen]; + break; + case GUI_SCREEN_SYSTEM_DATA: + // Do not reset planet rotation if we are already in + // the system info screen - looks kind of ugly. + if (previousGuiScreen != GUI_SCREEN_SYSTEM_DATA) + [self setGuiToSystemDataScreen]; + break; + default: + break; + } + [gameView allowStringInput:NO]; + [UNIVERSE setDisplayCursor:NO]; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE setViewDirection:saved_view_direction]; + [[gameView gameController] unpause_game]; + } + else + { + saved_view_direction = [UNIVERSE viewDirection]; + saved_script_time = script_time; + saved_gui_screen = gui_screen; + [UNIVERSE addMessage:DESC(@"game-paused") forCount:1.0]; + [[gameView gameController] pause_game]; + } + } + pause_pressed = YES; + } + else + { + pause_pressed = NO; + } +} + + +- (void) pollGuiArrowKeyControls:(double)delta_t +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + BOOL moving = NO; + double cursor_speed = 10.0; + NSString *commanderFile; + GameController *controller = [UNIVERSE gameController]; + GuiDisplayGen *gui = [UNIVERSE gui]; + GUI_ROW_INIT(gui); + + // deal with string inputs as necessary + if (gui_screen == GUI_SCREEN_LONG_RANGE_CHART) + { + [gameView setStringInput: gvStringInputAlpha]; + } + else if (gui_screen == GUI_SCREEN_SAVE) + { + [gameView setStringInput: gvStringInputAll]; + } + else + { + [gameView allowStringInput: NO]; + } + + switch (gui_screen) + { + case GUI_SCREEN_LONG_RANGE_CHART: + if ([gameView isDown:key_advanced_nav_array]) // '^' key + { + if (!pling_pressed) + { + if ([self hasEquipmentItem:@"EQ_ADVANCED_NAVIGATIONAL_ARRAY"]) [gui setShowAdvancedNavArray:YES]; + pling_pressed = YES; + } + } + else + { + if (pling_pressed) + { + [gui setShowAdvancedNavArray:NO]; + pling_pressed = NO; + } + } + if ([[gameView typedString] length]) + { + planetSearchString = [gameView typedString]; + NSPoint search_coords = [UNIVERSE findSystemCoordinatesWithPrefix:planetSearchString withGalaxySeed:galaxy_seed]; + if ((search_coords.x >= 0.0)&&(search_coords.y >= 0.0)) + { + moving = ((cursor_coordinates.x != search_coords.x)||(cursor_coordinates.y != search_coords.y)); + cursor_coordinates = search_coords; + } + else + { + [gameView resetTypedString]; + } + } + else + { + [UNIVERSE findSystemCoordinatesWithPrefix:@"" withGalaxySeed:galaxy_seed]; + planetSearchString = nil; + } + + moving |= (searchStringLength != [[gameView typedString] length]); + searchStringLength = [[gameView typedString] length]; + + case GUI_SCREEN_SHORT_RANGE_CHART: + + show_info_flag = ([gameView isDown:key_map_info] && ![UNIVERSE strict]); + + // If we have entered this screen with the injectors key pressed, make sure + // that injectors switch off when we release it - Nikos. + if (afterburner_engaged && ![gameView isDown:key_inject_fuel]) + { + afterburner_engaged = NO; + } + + if ([self status] != STATUS_WITCHSPACE_COUNTDOWN) + { + if ([gameView isDown:gvMouseLeftButton]) + { + NSPoint maus = [gameView virtualJoystickPosition]; + if (gui_screen == GUI_SCREEN_SHORT_RANGE_CHART) + { + double vadjust = 51; + double hscale = 4.0 * MAIN_GUI_PIXEL_WIDTH / 256.0; + double vscale = 4.0 * MAIN_GUI_PIXEL_HEIGHT / 512.0; + cursor_coordinates.x = galaxy_coordinates.x + (maus.x * MAIN_GUI_PIXEL_WIDTH) / hscale; + cursor_coordinates.y = galaxy_coordinates.y + (maus.y * MAIN_GUI_PIXEL_HEIGHT + vadjust) / vscale; + } + if (gui_screen == GUI_SCREEN_LONG_RANGE_CHART) + { + double vadjust = 211; + double hadjust = MAIN_GUI_PIXEL_WIDTH / 2.0; + double hscale = MAIN_GUI_PIXEL_WIDTH / 256.0; + double vscale = MAIN_GUI_PIXEL_HEIGHT / 512.0; + cursor_coordinates.x = (maus.x * MAIN_GUI_PIXEL_WIDTH + hadjust)/ hscale; + cursor_coordinates.y = (maus.y * MAIN_GUI_PIXEL_HEIGHT + vadjust) / vscale; + } + [gameView resetTypedString]; + moving = YES; + } + if ([gameView isDown:gvMouseDoubleClick]) + { + [gameView clearMouse]; + [UNIVERSE findSystemCoordinatesWithPrefix:@"" withGalaxySeed:galaxy_seed]; + [self setGuiToSystemDataScreen]; + } + if ([gameView isDown:key_map_home]) + { + [gameView resetTypedString]; + cursor_coordinates = galaxy_coordinates; + moving = YES; + } + if ([gameView isDown:gvArrowKeyLeft]) + { + [gameView resetTypedString]; + cursor_coordinates.x -= cursor_speed*delta_t; + if (cursor_coordinates.x < 0.0) cursor_coordinates.x = 0.0; + moving = YES; + } + if ([gameView isDown:gvArrowKeyRight]) + { + [gameView resetTypedString]; + cursor_coordinates.x += cursor_speed*delta_t; + if (cursor_coordinates.x > 256.0) cursor_coordinates.x = 256.0; + moving = YES; + } + if ([gameView isDown:gvArrowKeyDown]) + { + [gameView resetTypedString]; + cursor_coordinates.y += cursor_speed*delta_t*2.0; + if (cursor_coordinates.y > 256.0) cursor_coordinates.y = 256.0; + moving = YES; + } + if ([gameView isDown:gvArrowKeyUp]) + { + [gameView resetTypedString]; + cursor_coordinates.y -= cursor_speed*delta_t*2.0; + if (cursor_coordinates.y < 0.0) cursor_coordinates.y = 0.0; + moving = YES; + } + if ((cursor_moving)&&(!moving)) + { + target_system_seed = [UNIVERSE findSystemAtCoords:cursor_coordinates withGalaxySeed:galaxy_seed]; + cursor_coordinates.x = target_system_seed.d; + cursor_coordinates.y = target_system_seed.b; + if (gui_screen == GUI_SCREEN_LONG_RANGE_CHART) [self setGuiToLongRangeChartScreen]; + if (gui_screen == GUI_SCREEN_SHORT_RANGE_CHART) [self setGuiToShortRangeChartScreen]; + } + cursor_moving = moving; + if ((cursor_moving)&&(gui_screen == GUI_SCREEN_LONG_RANGE_CHART)) [self setGuiToLongRangeChartScreen]; // update graphics + if ((cursor_moving)&&(gui_screen == GUI_SCREEN_SHORT_RANGE_CHART)) [self setGuiToShortRangeChartScreen]; // update graphics + } + + case GUI_SCREEN_SYSTEM_DATA: + if ([self status] == STATUS_DOCKED && [gameView isDown:key_contract_info]) // '?' toggle between maps/info and contract screen + { + if (!queryPressed) + { + [self setGuiToContractsScreen]; + if ((oldSelection >= (int)[gui selectableRange].location)&&(oldSelection < (int)[gui selectableRange].location + (int)[gui selectableRange].length)) + [gui setSelectedRow:oldSelection]; + [self setGuiToContractsScreen]; + } + queryPressed = YES; + } + else + queryPressed = NO; + break; + + // DJS: Farm off load/save screen options to LoadSave.m + case GUI_SCREEN_LOAD: + commanderFile = [self commanderSelector]; + if(commanderFile) + { + [self loadPlayerFromFile:commanderFile]; + //[self setGuiToStatusScreen]; //already called inside loadPlayerFromFile + } + break; + case GUI_SCREEN_SAVE: + [self saveCommanderInputHandler]; + break; + + case GUI_SCREEN_SAVE_OVERWRITE: + [self overwriteCommanderInputHandler]; + break; + +#if OOLITE_HAVE_JOYSTICK + case GUI_SCREEN_STICKMAPPER: + [self stickMapperInputHandler: gui view: gameView]; + + leftRightKeyPressed = [gameView isDown:gvArrowKeyRight]|[gameView isDown:gvArrowKeyLeft]; + if (leftRightKeyPressed) + { + NSString* key = [gui keyForRow: [gui selectedRow]]; + if ([gameView isDown:gvArrowKeyRight]) + { + key = [gui keyForRow: GUI_ROW_FUNCEND]; + } + if ([gameView isDown:gvArrowKeyLeft]) + { + key = [gui keyForRow: GUI_ROW_FUNCSTART]; + } + int from_function = [[[key componentsSeparatedByString:@":"] objectAtIndex: 1] intValue]; + if (from_function < 0) from_function = 0; + + [self setGuiToStickMapperScreen:from_function]; + if ([[UNIVERSE gui] selectedRow] < 0) + [[UNIVERSE gui] setSelectedRow: GUI_ROW_FUNCSTART]; + if (from_function == 0) + [[UNIVERSE gui] setSelectedRow: GUI_ROW_FUNCSTART + MAX_ROWS_FUNCTIONS - 1]; + } + break; +#endif + + case GUI_SCREEN_GAMEOPTIONS: + [self handleGameOptionsScreenKeys]; + break; + + case GUI_SCREEN_OPTIONS: + [self handleGUIUpDownArrowKeys]; + int guiSelectedRow = [gui selectedRow]; + BOOL selectKeyPress = ([gameView isDown:13]||[gameView isDown:gvMouseDoubleClick]); + + if (selectKeyPress) // 'enter' + { + if ((guiSelectedRow == GUI_ROW(,QUICKSAVE))&&(!disc_operation_in_progress)) + { + NS_DURING + disc_operation_in_progress = YES; + [self quicksavePlayer]; + NS_HANDLER + OOLog(kOOLogException, @"\n\n***** Handling localException: %@ : %@ *****\n\n",[localException name], [localException reason]); + if ([[localException name] isEqual:@"GameNotSavedException"]) // try saving game instead + { + OOLog(kOOLogException, @"\n\n***** Trying a normal save instead *****\n\n"); + if ([controller inFullScreenMode]) + [controller pauseFullScreenModeToPerform:@selector(savePlayer) onTarget:self]; + else + [self savePlayer]; + } + else + { + [localException raise]; + } + NS_ENDHANDLER + } + if ((guiSelectedRow == GUI_ROW(,SAVE))&&(!disc_operation_in_progress)) + { + disc_operation_in_progress = YES; + [self savePlayer]; + } + if ((guiSelectedRow == GUI_ROW(,LOAD))&&(!disc_operation_in_progress)) + { + disc_operation_in_progress = YES; + if (![self loadPlayer]) + { + disc_operation_in_progress = NO; + [self setGuiToStatusScreen]; + } + } + + + if ((guiSelectedRow == GUI_ROW(,BEGIN_NEW))&&(!disc_operation_in_progress)) + { + disc_operation_in_progress = YES; + [UNIVERSE reinit]; + } + + if ([gameView isDown:gvMouseDoubleClick]) + [gameView clearMouse]; + } + else + { + disc_operation_in_progress = NO; + } + +#if OOLITE_SDL + // quit only appears in GNUstep as users aren't + // used to Cmd-Q equivs. Same goes for window + // vs fullscreen. + if ((guiSelectedRow == GUI_ROW(,QUIT)) && selectKeyPress) + { + [[gameView gameController] exitApp]; + } +#endif + + if ((guiSelectedRow == GUI_ROW(,GAMEOPTIONS)) && selectKeyPress) + { + [gameView clearKeys]; + [self setGuiToGameOptionsScreen]; + } + + /* TODO: Investigate why this has to be handled last (if the + quit item and this are swapped, the game crashes if + strict mode is selected with SIGSEGV in the ObjC runtime + system. The stack trace shows it crashes when it hits + the if statement, trying to send the message to one of + the things contained.) */ + if ((guiSelectedRow == GUI_ROW(,STRICT))&& selectKeyPress) + { + [UNIVERSE setStrict:![UNIVERSE strict]]; + } + + break; + + case GUI_SCREEN_EQUIP_SHIP: + if ([self handleGUIUpDownArrowKeys]) + { + [self showInformationForSelectedUpgrade]; + } + + if ([gameView isDown:gvArrowKeyLeft]) + { + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([[gui keyForRow:GUI_ROW_EQUIPMENT_START] hasPrefix:@"More:"]) + { + [self playMenuPagePrevious]; + [gui setSelectedRow:GUI_ROW_EQUIPMENT_START]; + [self buySelectedItem]; + } + timeLastKeyPress = script_time; + } + } + if ([gameView isDown:gvArrowKeyRight]) + { + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([[gui keyForRow:GUI_ROW_EQUIPMENT_START + GUI_MAX_ROWS_EQUIPMENT - 1] hasPrefix:@"More:"]) + { + [self playMenuPageNext]; + [gui setSelectedRow:GUI_ROW_EQUIPMENT_START + GUI_MAX_ROWS_EQUIPMENT - 1]; + [self buySelectedItem]; + } + timeLastKeyPress = script_time; + } + } + leftRightKeyPressed = [gameView isDown:gvArrowKeyRight]|[gameView isDown:gvArrowKeyLeft]; + + if ([gameView isDown:13] || [gameView isDown:gvMouseDoubleClick]) // 'enter' + { + if ([gameView isDown:gvMouseDoubleClick]) + { + selectPressed = NO; + [gameView clearMouse]; + } + if ((!selectPressed)&&([gui selectedRow] > -1)) + { + [self buySelectedItem]; + selectPressed = YES; + } + } + else + { + selectPressed = NO; + } + break; + + case GUI_SCREEN_MARKET: + if ([self status] == STATUS_DOCKED) + { + [self handleGUIUpDownArrowKeys]; + + if (([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft])||([gameView isDown:13]||[gameView isDown:gvMouseDoubleClick])) + { + if ([gameView isDown:gvArrowKeyRight]) // --> + { + if (!wait_for_key_up) + { + int item = [(NSString *)[gui selectedRowKey] intValue]; + if ([self tryBuyingCommodity:item all:[gameView isShiftDown]]) + { + [self playBuyCommodity]; + [self setGuiToMarketScreen]; + } + else + { + [self playCantBuyCommodity]; + } + wait_for_key_up = YES; + } + } + if ([gameView isDown:gvArrowKeyLeft]) // <-- + { + if (!wait_for_key_up) + { + int item = [(NSString *)[gui selectedRowKey] intValue]; + if ([self trySellingCommodity:item all:[gameView isShiftDown]]) + { + [self playSellCommodity]; + [self setGuiToMarketScreen]; + } + else + { + [self playCantSellCommodity]; + } + wait_for_key_up = YES; + } + } + if ([gameView isDown:13]||[gameView isDown:gvMouseDoubleClick]) // 'enter' + { + if ([gameView isDown:gvMouseDoubleClick]) + { + wait_for_key_up = NO; + [gameView clearMouse]; + } + if (!wait_for_key_up) + { + int item = [(NSString *)[gui selectedRowKey] intValue]; + int yours = [[shipCommodityData arrayAtIndex:item] intAtIndex:1]; + if ([gameView isShiftDown] && [self tryBuyingCommodity:item all:YES]) // buy as much as possible (with Shift) + { + [self playBuyCommodity]; + [self setGuiToMarketScreen]; + } + else if ((yours > 0) && [self trySellingCommodity:item all:YES]) // sell all you can + { + [self playSellCommodity]; + [self setGuiToMarketScreen]; + } + else if ([self tryBuyingCommodity:item all:YES]) // buy as much as possible + { + [self playBuyCommodity]; + [self setGuiToMarketScreen]; + } + else + { + [self playCantBuyCommodity]; + } + wait_for_key_up = YES; + } + } + } + else + { + wait_for_key_up = NO; + } + } + break; + + case GUI_SCREEN_CONTRACTS: + if ([self status] == STATUS_DOCKED) + { + if ([self handleGUIUpDownArrowKeys]) + [self setGuiToContractsScreen]; + + if ([self status] == STATUS_DOCKED && ([gameView isDown:13] || [gameView isDown:gvMouseDoubleClick])) // 'enter' | doubleclick + { + if ([gameView isDown:gvMouseDoubleClick]) + [gameView clearMouse]; + if (!selectPressed) + { + if ([self pickFromGuiContractsScreen]) + { + [self playBuyCommodity]; + [self setGuiToContractsScreen]; + } + else + { + [self playCantBuyCommodity]; + } + } + selectPressed = YES; + } + else + { + selectPressed = NO; + } + if ([gameView isDown:key_contract_info]) // '?' toggle between contracts screen and map + { + if (!queryPressed) + { + oldSelection = [gui selectedRow]; + [self highlightSystemFromGuiContractsScreen]; + } + queryPressed = YES; + } + else + queryPressed = NO; + } + break; + + case GUI_SCREEN_REPORT: + if ([gameView isDown:32]) // spacebar + { + if (!spacePressed) + { + BOOL reportEnded = ([dockingReport length] == 0); + [self playDismissedReportScreen]; + [self setGuiToStatusScreen]; + if(reportEnded) [self doScriptEvent:@"reportScreenEnded"]; // last report given. Screen is now free for missionscreens. + } + spacePressed = YES; + } + else + spacePressed = NO; + break; + case GUI_SCREEN_STATUS: + [self handleGUIUpDownArrowKeys]; + if ([gameView isDown:gvArrowKeyLeft]) + { + + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([[gui keyForRow:STATUS_EQUIPMENT_FIRST_ROW] isEqual:GUI_KEY_OK]) + { + [gui setSelectedRow:STATUS_EQUIPMENT_FIRST_ROW]; + [self playMenuPagePrevious]; + [gui setStatusPage:-1]; + [self setGuiToStatusScreen]; + } + timeLastKeyPress = script_time; + } + } + if ([gameView isDown:gvArrowKeyRight]) + { + + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([[gui keyForRow:STATUS_EQUIPMENT_FIRST_ROW + STATUS_EQUIPMENT_MAX_ROWS] isEqual:GUI_KEY_OK]) + { + [gui setSelectedRow:STATUS_EQUIPMENT_FIRST_ROW + STATUS_EQUIPMENT_MAX_ROWS]; + [self playMenuPageNext]; + [gui setStatusPage:+1]; + [self setGuiToStatusScreen]; + } + timeLastKeyPress = script_time; + } + } + leftRightKeyPressed = [gameView isDown:gvArrowKeyRight]|[gameView isDown:gvArrowKeyLeft]; + + if ([gameView isDown:13] || [gameView isDown:gvMouseDoubleClick]) // 'enter' + { + if ([gameView isDown:gvMouseDoubleClick]) + { + selectPressed = NO; + [gameView clearMouse]; + } + if ((!selectPressed)&&([gui selectedRow] > -1)) + { + [gui setStatusPage:([gui selectedRow] == STATUS_EQUIPMENT_FIRST_ROW ? -1 : +1)]; + [self setGuiToStatusScreen]; + + selectPressed = YES; + } + } + else + { + selectPressed = NO; + } + + break; + case GUI_SCREEN_SHIPYARD: + if ([self handleGUIUpDownArrowKeys]) + { + [self showShipyardInfoForSelection]; + } + + if ([gameView isDown:gvArrowKeyLeft]) + { + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([[gui keyForRow:GUI_ROW_SHIPYARD_START] hasPrefix:@"More:"]) + { + [self playMenuPagePrevious]; + [gui setSelectedRow:GUI_ROW_SHIPYARD_START]; + [self buySelectedShip]; + } + timeLastKeyPress = script_time; + } + } + if ([gameView isDown:gvArrowKeyRight]) + { + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([[gui keyForRow:GUI_ROW_SHIPYARD_START + MAX_ROWS_SHIPS_FOR_SALE - 1] hasPrefix:@"More:"]) + { + [self playMenuPageNext]; + [gui setSelectedRow:GUI_ROW_SHIPYARD_START + MAX_ROWS_SHIPS_FOR_SALE - 1]; + [self buySelectedShip]; + } + timeLastKeyPress = script_time; + } + } + leftRightKeyPressed = [gameView isDown:gvArrowKeyRight]|[gameView isDown:gvArrowKeyLeft]; + + if ([gameView isDown:13]) // 'enter' NOT double-click + { + if (!selectPressed) + { + // try to buy the ship! + OOCreditsQuantity money = credits; + if ([self buySelectedShip]) + { + if (money != credits) // money == credits means we skipped to another page, don't do anything + { + [UNIVERSE removeDemoShips]; + [self setGuiToStatusScreen]; + [self playBuyShip]; + [self doScriptEvent:@"playerBoughtNewShip" withArgument:self]; // some equipment.oxp might want to know everything has changed. + } + } + else + { + [self playCantBuyShip]; + } + } + selectPressed = YES; + } + else + { + selectPressed = NO; + } + break; + + default: + break; + } + + // damp any rotations we entered with + if (flightRoll > 0.0) + { + if (flightRoll > delta_t) [self decrease_flight_roll:delta_t]; + else flightRoll = 0.0; + } + if (flightRoll < 0.0) + { + if (flightRoll < -delta_t) [self increase_flight_roll:delta_t]; + else flightRoll = 0.0; + } + if (flightPitch > 0.0) + { + if (flightPitch > delta_t) [self decrease_flight_pitch:delta_t]; + else flightPitch = 0.0; + } + if (flightPitch < 0.0) + { + if (flightPitch < -delta_t) [self increase_flight_pitch:delta_t]; + else flightPitch = 0.0; + } + if (flightYaw > 0.0) + { + if (flightYaw > delta_t) [self decrease_flight_yaw:delta_t]; + else flightYaw = 0.0; + } + if (flightYaw < 0.0) + { + if (flightYaw < -delta_t) [self increase_flight_yaw:delta_t]; + else flightYaw = 0.0; + } +} + + +- (void) handleGameOptionsScreenKeys +{ + GameController *controller = [UNIVERSE gameController]; + NSArray *modes = [controller displayModes]; + MyOpenGLView *gameView = [UNIVERSE gameView]; + GuiDisplayGen *gui = [UNIVERSE gui]; + GUI_ROW_INIT(gui); + + [self handleGUIUpDownArrowKeys]; + int guiSelectedRow = [gui selectedRow]; + BOOL selectKeyPress = ([gameView isDown:13]||[gameView isDown:gvMouseDoubleClick]); + if ([gameView isDown:gvMouseDoubleClick]) + [gameView clearMouse]; + + +#if OOLITE_HAVE_JOYSTICK + if ((guiSelectedRow == GUI_ROW(GAME,STICKMAPPER)) && selectKeyPress) + { + selFunctionIdx = 0; + [self setGuiToStickMapperScreen: 0]; + } +#endif + + if (!switching_resolution && + guiSelectedRow == GUI_ROW(GAME,DISPLAY) && + ([gameView isDown:gvArrowKeyRight] || [gameView isDown:gvArrowKeyLeft])) + { + int direction = ([gameView isDown:gvArrowKeyRight]) ? 1 : -1; + OOInteger displayModeIndex = [controller indexOfCurrentDisplayMode]; + if (displayModeIndex == NSNotFound) + { + OOLogWARN(@"graphics.mode.notFound", @"couldn't find current fullscreen setting, switching to default."); + displayModeIndex = 0; + } + else + { + displayModeIndex = displayModeIndex + direction; + int count = [modes count]; + if (displayModeIndex < 0) + displayModeIndex = count - 1; + if (displayModeIndex >= count) + displayModeIndex = 0; + } + NSDictionary *mode = [modes objectAtIndex:displayModeIndex]; + int modeWidth = [mode intForKey:kOODisplayWidth]; + int modeHeight = [mode intForKey:kOODisplayHeight]; + int modeRefresh = [mode intForKey:kOODisplayRefreshRate]; + [controller setDisplayWidth:modeWidth Height:modeHeight Refresh:modeRefresh]; + + NSString *displayModeString = [self screenModeStringForWidth:modeWidth height:modeHeight refreshRate:modeRefresh]; + + [self playChangedOption]; + [gui setText:displayModeString forRow:GUI_ROW(GAME,DISPLAY) align:GUI_ALIGN_CENTER]; + switching_resolution = YES; +#if OOLITE_HAVE_APPKIT + if ([controller inFullScreenMode]) [controller goFullscreen:(id)YES]; // changes fullscreen mode immediately +#elif OOLITE_SDL + /* TODO: The gameView for the SDL game currently holds and + sets the actual screen resolution (controller just stores + it). This probably ought to change. */ + [gameView setScreenSize: displayModeIndex]; // also changes fullscreen mode immediately +#endif + } + if (switching_resolution && ![gameView isDown:gvArrowKeyRight] && ![gameView isDown:gvArrowKeyLeft] && !selectKeyPress) + { + switching_resolution = NO; + } + +#if OOLITE_SPEECH_SYNTH + if ((guiSelectedRow == GUI_ROW(GAME,SPEECH))&&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft]))) + { + if ([gameView isDown:gvArrowKeyRight] != isSpeechOn) + [self playChangedOption]; + isSpeechOn = [gameView isDown:gvArrowKeyRight]; + NSString *message = DESC(isSpeechOn ? @"gameoptions-spoken-messages-yes" : @"gameoptions-spoken-messages-no"); + [gui setText:message forRow:GUI_ROW(GAME,SPEECH) align:GUI_ALIGN_CENTER]; + if (isSpeechOn) + { + [UNIVERSE stopSpeaking]; + [UNIVERSE startSpeakingString:message]; + } + } +#if OOLITE_ESPEAK + if (guiSelectedRow == GUI_ROW(GAME,SPEECH_LANGUAGE)) + { + if ([gameView isDown:gvArrowKeyRight] || [gameView isDown:gvArrowKeyLeft]) + { + if (!leftRightKeyPressed && script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL) + { + [self playChangedOption]; + if ([gameView isDown:gvArrowKeyRight]) + voice_no = [UNIVERSE nextVoice: voice_no]; + else + voice_no = [UNIVERSE prevVoice: voice_no]; + [UNIVERSE setVoice: voice_no withGenderM:voice_gender_m]; + NSString *message = [NSString stringWithFormat:DESC(@"gameoptions-voice-@"), [UNIVERSE voiceName: voice_no]]; + [gui setText:message forRow:GUI_ROW(GAME,SPEECH_LANGUAGE) align:GUI_ALIGN_CENTER]; + if (isSpeechOn) + { + [UNIVERSE stopSpeaking]; + [UNIVERSE startSpeakingString:[UNIVERSE voiceName: voice_no]]; + } + } + leftRightKeyPressed = YES; + } + else + leftRightKeyPressed = NO; + } + + if (guiSelectedRow == GUI_ROW(GAME,SPEECH_GENDER)) + { + if ([gameView isDown:gvArrowKeyRight] || [gameView isDown:gvArrowKeyLeft]) + { + if (!leftRightKeyPressed && script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL) + { + [self playChangedOption]; + BOOL m = [gameView isDown:gvArrowKeyRight]; + if (m != voice_gender_m) + { + voice_gender_m = m; + [UNIVERSE setVoice:voice_no withGenderM:voice_gender_m]; + NSString *message = [NSString stringWithFormat:DESC(voice_gender_m ? @"gameoptions-voice-M" : @"gameoptions-voice-F")]; + [gui setText:message forRow:GUI_ROW(GAME,SPEECH_GENDER) align:GUI_ALIGN_CENTER]; + if (isSpeechOn) + { + [UNIVERSE stopSpeaking]; + [UNIVERSE startSpeakingString:[UNIVERSE voiceName: voice_no]]; + } + } + } + leftRightKeyPressed = YES; + } + else + leftRightKeyPressed = NO; + } +#endif +#endif + + if ((guiSelectedRow == GUI_ROW(GAME,MUSIC))&&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft]))) + { + if (!musicModeKeyPressed) + { + OOMusicController *musicController = [OOMusicController sharedController]; + int initialMode = [musicController mode]; + int mode = initialMode; + + if ([gameView isDown:gvArrowKeyRight]) mode++; + if ([gameView isDown:gvArrowKeyLeft]) mode--; + + [musicController setMode:MAX(mode, 0)]; + + if ((int)[musicController mode] != initialMode) + { + [self playChangedOption]; + NSString *message = [NSString stringWithFormat:DESC(@"gameoptions-music-mode-@"), [UNIVERSE descriptionForArrayKey:@"music-mode" index:mode]]; + [gui setText:message forRow:GUI_ROW(GAME,MUSIC) align:GUI_ALIGN_CENTER]; + } + } + musicModeKeyPressed = YES; + } + else musicModeKeyPressed = NO; + + if ((guiSelectedRow == GUI_ROW(GAME,AUTOSAVE))&&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft]))) + { + if ([gameView isDown:gvArrowKeyRight] != [UNIVERSE autoSave]) + [self playChangedOption]; + [UNIVERSE setAutoSave:[gameView isDown:gvArrowKeyRight]]; + if ([UNIVERSE autoSave]) + [gui setText:DESC(@"gameoptions-autosave-yes") forRow:GUI_ROW(GAME,AUTOSAVE) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-autosave-no") forRow:GUI_ROW(GAME,AUTOSAVE) align:GUI_ALIGN_CENTER]; + } + + if ((guiSelectedRow == GUI_ROW(GAME,VOLUME)) + &&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft])) + &&[OOSound respondsToSelector:@selector(masterVolume)]) + { + if ((!volumeControlPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + BOOL rightKeyDown = [gameView isDown:gvArrowKeyRight]; + BOOL leftKeyDown = [gameView isDown:gvArrowKeyLeft]; + int volume = 100 * [OOSound masterVolume]; + volume += (((rightKeyDown && (volume < 100)) ? 5 : 0) - ((leftKeyDown && (volume > 0)) ? 5 : 0)); + if (volume > 100) volume = 100; + if (volume < 0) volume = 0; + [OOSound setMasterVolume: 0.01 * volume]; + [self playChangedOption]; + if (volume > 0) + { + NSString* soundVolumeWordDesc = DESC(@"gameoptions-sound-volume"); + NSString* v1_string = @"|||||||||||||||||||||||||"; + NSString* v0_string = @"........................."; + v1_string = [v1_string substringToIndex:volume / 5]; + v0_string = [v0_string substringToIndex:20 - volume / 5]; + [gui setText:[NSString stringWithFormat:@"%@%@%@ ", soundVolumeWordDesc, v1_string, v0_string] forRow:GUI_ROW(GAME,VOLUME) align:GUI_ALIGN_CENTER]; + } + else + [gui setText:DESC(@"gameoptions-sound-volume-mute") forRow:GUI_ROW(GAME,VOLUME) align:GUI_ALIGN_CENTER]; + timeLastKeyPress = script_time; + } + volumeControlPressed = YES; + } + else + volumeControlPressed = NO; + +#if OOLITE_MAC_OS_X + if ((guiSelectedRow == GUI_ROW(GAME,GROWL))&&([gameView isDown:gvArrowKeyRight]||[gameView isDown:gvArrowKeyLeft])) + { + if ((!leftRightKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + NSUserDefaults* prefs = [NSUserDefaults standardUserDefaults]; + BOOL rightKeyDown = [gameView isDown:gvArrowKeyRight]; + BOOL leftKeyDown = [gameView isDown:gvArrowKeyLeft]; + int growl_min_priority = 3; + if ([prefs objectForKey:@"groolite-min-priority"]) + growl_min_priority = [prefs integerForKey:@"groolite-min-priority"]; + int new_priority = growl_min_priority; + if (rightKeyDown) + new_priority--; + if (leftKeyDown) + new_priority++; + if (new_priority < kGroolitePriorityMinimum) // sanity check values -2 .. 3 + new_priority = kGroolitePriorityMinimum; + if (new_priority > kGroolitePriorityMaximum) + new_priority = kGroolitePriorityMaximum; + if (new_priority != growl_min_priority) + { + growl_min_priority = new_priority; + NSString* growl_priority_desc = [Groolite priorityDescription:growl_min_priority]; + [gui setText:[NSString stringWithFormat:DESC(@"gameoptions-show-growl-messages-@"), growl_priority_desc] + forRow:GUI_ROW(GAME,GROWL) align:GUI_ALIGN_CENTER]; + [self playChangedOption]; + [prefs setInteger:growl_min_priority forKey:@"groolite-min-priority"]; + } + timeLastKeyPress = script_time; + } + leftRightKeyPressed = YES; + } + else + leftRightKeyPressed = NO; +#endif + + if ((guiSelectedRow == GUI_ROW(GAME,WIREFRAMEGRAPHICS))&&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft]))) + { + if ([gameView isDown:gvArrowKeyRight] != [UNIVERSE wireframeGraphics]) + [self playChangedOption]; + [UNIVERSE setWireframeGraphics:[gameView isDown:gvArrowKeyRight]]; + if ([UNIVERSE wireframeGraphics]) + [gui setText:DESC(@"gameoptions-wireframe-graphics-yes") forRow:GUI_ROW(GAME,WIREFRAMEGRAPHICS) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-wireframe-graphics-no") forRow:GUI_ROW(GAME,WIREFRAMEGRAPHICS) align:GUI_ALIGN_CENTER]; + } + +#if ALLOW_PROCEDURAL_PLANETS + if ((guiSelectedRow == GUI_ROW(GAME,PROCEDURALLYTEXTUREDPLANETS))&&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft]))) + { + if ([gameView isDown:gvArrowKeyRight] != [UNIVERSE doProcedurallyTexturedPlanets]) + { + [UNIVERSE setDoProcedurallyTexturedPlanets:[gameView isDown:gvArrowKeyRight]]; + [self playChangedOption]; + if ([UNIVERSE planet]) + { + [UNIVERSE setUpPlanet]; + } + } + if ([UNIVERSE doProcedurallyTexturedPlanets]) + [gui setText:DESC(@"gameoptions-procedurally-textured-planets-yes") forRow:GUI_ROW(GAME,PROCEDURALLYTEXTUREDPLANETS) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-procedurally-textured-planets-no") forRow:GUI_ROW(GAME,PROCEDURALLYTEXTUREDPLANETS) align:GUI_ALIGN_CENTER]; + } +#endif + + if ((guiSelectedRow == GUI_ROW(GAME,DETAIL))&&(([gameView isDown:gvArrowKeyRight])||([gameView isDown:gvArrowKeyLeft]))) + { + if ([gameView isDown:gvArrowKeyRight] != [UNIVERSE reducedDetail]) + [self playChangedOption]; + [UNIVERSE setReducedDetail:[gameView isDown:gvArrowKeyRight]]; + if ([UNIVERSE reducedDetail]) + [gui setText:DESC(@"gameoptions-reduced-detail-yes") forRow:GUI_ROW(GAME,DETAIL) align:GUI_ALIGN_CENTER]; + else + [gui setText:DESC(@"gameoptions-reduced-detail-no") forRow:GUI_ROW(GAME,DETAIL) align:GUI_ALIGN_CENTER]; + } + + + if (guiSelectedRow == GUI_ROW(GAME,SHADEREFFECTS) && ([gameView isDown:gvArrowKeyRight] || [gameView isDown:gvArrowKeyLeft])) + { + if (!shaderSelectKeyPressed || (script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + int direction = ([gameView isDown:gvArrowKeyRight]) ? 1 : -1; + int shaderEffects = [UNIVERSE shaderEffectsLevel]; + shaderEffects = shaderEffects + direction; + if (shaderEffects < SHADERS_MIN) + shaderEffects = SHADERS_MIN; + if (shaderEffects > SHADERS_MAX) + shaderEffects = SHADERS_MAX; + [UNIVERSE setShaderEffectsLevel:shaderEffects]; + [gui setText:[NSString stringWithFormat:DESC(@"gameoptions-shaderfx-@"), ShaderSettingToDisplayString(shaderEffects)] + forRow:GUI_ROW(GAME,SHADEREFFECTS) + align:GUI_ALIGN_CENTER]; + timeLastKeyPress = script_time; + } + shaderSelectKeyPressed = YES; + } + else shaderSelectKeyPressed = NO; + +#if OOLITE_SDL + if ((guiSelectedRow == GUI_ROW(GAME,DISPLAYSTYLE)) && selectKeyPress) + { + [gameView toggleScreenMode]; + // redraw GUI + [self setGuiToGameOptionsScreen]; + } +#endif + + if ((guiSelectedRow == GUI_ROW(GAME,BACK)) && selectKeyPress) + { + [gameView clearKeys]; + [self setGuiToLoadSaveScreen]; + } +} + + +- (void) switchToMainView +{ + gui_screen = GUI_SCREEN_MAIN; + if (showDemoShips) + { + [self setShowDemoShips: NO]; + [UNIVERSE removeDemoShips]; + } + [(MyOpenGLView *)[UNIVERSE gameView] allowStringInput:NO]; + [UNIVERSE setDisplayCursor:NO]; +} + + +- (void) pollViewControls +{ + if(!pollControls) + return; + + MyOpenGLView *gameView = [UNIVERSE gameView]; + + NSPoint virtualView = NSZeroPoint; + double view_threshold = 0.5; + + if (!stickHandler) + { + stickHandler = [gameView getStickHandler]; + } + + if ([stickHandler getNumSticks]) + { + virtualView = [stickHandler getViewAxis]; + if (virtualView.y == STICK_AXISUNASSIGNED) + virtualView.y = 0.0; + if (virtualView.x == STICK_AXISUNASSIGNED) + virtualView.x = 0.0; + if (fabs(virtualView.y) >= fabs(virtualView.x)) + virtualView.x = 0.0; // forward/aft takes precedence + else + virtualView.y = 0.0; + } + + const BOOL *joyButtonState = [stickHandler getAllButtonStates]; + + // view keys + if (([gameView isDown:gvFunctionKey1])||([gameView isDown:gvNumberKey1])||(virtualView.y < -view_threshold)||joyButtonState[BUTTON_VIEWFORWARD] || (([gameView isDown:key_hyperspace] || joyButtonState[BUTTON_HYPERDRIVE]) && [UNIVERSE displayGUI])) + { + if ([UNIVERSE displayGUI]) + [self switchToMainView]; + [UNIVERSE setViewDirection:VIEW_FORWARD]; + currentWeaponFacing = VIEW_FORWARD; + } + if (([gameView isDown:gvFunctionKey2])||([gameView isDown:gvNumberKey2])||(virtualView.y > view_threshold)||joyButtonState[BUTTON_VIEWAFT]) + { + if ([UNIVERSE displayGUI]) + [self switchToMainView]; + [UNIVERSE setViewDirection:VIEW_AFT]; + currentWeaponFacing = VIEW_AFT; + } + if (([gameView isDown:gvFunctionKey3])||([gameView isDown:gvNumberKey3])||(virtualView.x < -view_threshold)||joyButtonState[BUTTON_VIEWPORT]) + { + if ([UNIVERSE displayGUI]) + [self switchToMainView]; + [UNIVERSE setViewDirection:VIEW_PORT]; + currentWeaponFacing = VIEW_PORT; + } + if (([gameView isDown:gvFunctionKey4])||([gameView isDown:gvNumberKey4])||(virtualView.x > view_threshold)||joyButtonState[BUTTON_VIEWSTARBOARD]) + { + if ([UNIVERSE displayGUI]) + [self switchToMainView]; + [UNIVERSE setViewDirection:VIEW_STARBOARD]; + currentWeaponFacing = VIEW_STARBOARD; + } + + if ([gameView isDown:key_custom_view]) + { + if (!customView_pressed && [_customViews count] != 0 && ![UNIVERSE displayCursor]) + { + if ([UNIVERSE viewDirection] == VIEW_CUSTOM) // already in custom view mode + { + // rotate the custom views + _customViewIndex = (_customViewIndex + 1) % [_customViews count]; + } + + [self setCustomViewDataFromDictionary:[_customViews dictionaryAtIndex:_customViewIndex]]; + + if ([UNIVERSE displayGUI]) + [self switchToMainView]; + [UNIVERSE setViewDirection:VIEW_CUSTOM]; + } + customView_pressed = YES; + } + else + customView_pressed = NO; + + // Zoom scanner 'z' + if ([gameView isDown:key_scanner_zoom] && ([gameView allowingStringInput] == gvStringInputNo)) // look for the 'z' key + { + if (!scanner_zoom_rate) + { + if ([hud scanner_zoom] < 5.0) + { + if (([hud scanner_zoom] > 1.0)||(!zoom_pressed)) + scanner_zoom_rate = SCANNER_ZOOM_RATE_UP; + } + else + { + if (!zoom_pressed) // must release and re-press zoom to zoom back down.. + scanner_zoom_rate = SCANNER_ZOOM_RATE_DOWN; + } + } + zoom_pressed = YES; + } + else + zoom_pressed = NO; + + // Unzoom scanner 'Z' + if ([gameView isDown:key_scanner_unzoom] && ([gameView allowingStringInput] == gvStringInputNo)) // look for the 'Z' key + { + if ((!scanner_zoom_rate)&&([hud scanner_zoom] > 1.0)) + scanner_zoom_rate = SCANNER_ZOOM_RATE_DOWN; + } + + // Compass mode '\' + if ([gameView isDown:key_next_compass_mode]) // look for the '\' key + { + if ((!compass_mode_pressed)&&(compassMode != COMPASS_MODE_BASIC)) + [self setNextCompassMode]; + compass_mode_pressed = YES; + } + else + { + compass_mode_pressed = NO; + } + + // show comms log '`' + if ([gameView isDown:key_comms_log]) + { + [UNIVERSE showCommsLog: 1.5]; + [hud refreshLastTransmitter]; + } +} + + +- (void) pollFlightArrowKeyControls:(double)delta_t +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + NSPoint virtualStick = NSZeroPoint; + double reqYaw = 0.0; + double deadzone; + + // TODO: Rework who owns the stick. + if(!stickHandler) + { + stickHandler=[gameView getStickHandler]; + } + numSticks=[stickHandler getNumSticks]; + deadzone = STICK_DEADZONE / [stickHandler getSensitivity]; + + /* DJS: Handle inputs on the joy roll/pitch axis. + Mouse control on takes precidence over joysticks. + We have to assume the player has a reason for switching mouse + control on if they have a joystick - let them do it. */ + if(mouse_control_on) + { + virtualStick=[gameView virtualJoystickPosition]; + double sensitivity = 2.0; + virtualStick.x *= sensitivity; + virtualStick.y *= sensitivity; + } + else if(numSticks) + { + virtualStick=[stickHandler getRollPitchAxis]; + if((virtualStick.x == STICK_AXISUNASSIGNED || + virtualStick.y == STICK_AXISUNASSIGNED) || + (fabs(virtualStick.x) < deadzone && + fabs(virtualStick.y) < deadzone)) + { + // Not assigned or deadzoned - set to zero. + virtualStick.x=0; + virtualStick.y=0; + } + else if(virtualStick.x != 0 || + virtualStick.y != 0) + { + // cancel keyboard override, stick has been waggled + keyboardRollPitchOverride=NO; + } + // handle yaw separately from pitch/roll + reqYaw = [stickHandler getAxisState: AXIS_YAW]; + if((reqYaw == STICK_AXISUNASSIGNED) || fabs(reqYaw) < deadzone) + { + // Not assigned or deadzoned - set to zero. + reqYaw=0; + } + else if(reqYaw != 0) + { + // cancel keyboard override, stick has been waggled + keyboardYawOverride=NO; + } + } + + double roll_dampner = ROLL_DAMPING_FACTOR * delta_t; + double pitch_dampner = PITCH_DAMPING_FACTOR * delta_t; + double yaw_dampner = YAW_DAMPING_FACTOR * delta_t; + + rolling = NO; + if (!mouse_control_on ) + { + if ([gameView isDown:key_roll_left]) + { + keyboardRollPitchOverride=YES; + if (flightRoll > 0.0) flightRoll = 0.0; + [self decrease_flight_roll:delta_t*roll_delta]; + rolling = YES; + } + if ([gameView isDown:key_roll_right]) + { + keyboardRollPitchOverride=YES; + if (flightRoll < 0.0) flightRoll = 0.0; + [self increase_flight_roll:delta_t*roll_delta]; + rolling = YES; + } + } + if((mouse_control_on || numSticks) && !keyboardRollPitchOverride) + { + double stick_roll = max_flight_roll * virtualStick.x; + if (flightRoll < stick_roll) + { + [self increase_flight_roll:delta_t*roll_delta]; + if (flightRoll > stick_roll) + flightRoll = stick_roll; + } + if (flightRoll > stick_roll) + { + [self decrease_flight_roll:delta_t*roll_delta]; + if (flightRoll < stick_roll) + flightRoll = stick_roll; + } + rolling = (fabs(virtualStick.x) >= deadzone); + } + if (!rolling) + { + if (flightRoll > 0.0) + { + if (flightRoll > roll_dampner) [self decrease_flight_roll:roll_dampner]; + else flightRoll = 0.0; + } + if (flightRoll < 0.0) + { + if (flightRoll < -roll_dampner) [self increase_flight_roll:roll_dampner]; + else flightRoll = 0.0; + } + } + + pitching = NO; + if (!mouse_control_on) + { + if ([gameView isDown:key_pitch_back]) + { + keyboardRollPitchOverride=YES; + if (flightPitch < 0.0) flightPitch = 0.0; + [self increase_flight_pitch:delta_t*pitch_delta]; + pitching = YES; + } + if ([gameView isDown:key_pitch_forward]) + { + keyboardRollPitchOverride=YES; + if (flightPitch > 0.0) flightPitch = 0.0; + [self decrease_flight_pitch:delta_t*pitch_delta]; + pitching = YES; + } + } + if((mouse_control_on || numSticks) && !keyboardRollPitchOverride) + { + double stick_pitch = max_flight_pitch * virtualStick.y; + if (flightPitch < stick_pitch) + { + [self increase_flight_pitch:delta_t*roll_delta]; + if (flightPitch > stick_pitch) + flightPitch = stick_pitch; + } + if (flightPitch > stick_pitch) + { + [self decrease_flight_pitch:delta_t*roll_delta]; + if (flightPitch < stick_pitch) + flightPitch = stick_pitch; + } + pitching = (fabs(virtualStick.y) >= deadzone); + } + if (!pitching) + { + if (flightPitch > 0.0) + { + if (flightPitch > pitch_dampner) [self decrease_flight_pitch:pitch_dampner]; + else flightPitch = 0.0; + } + if (flightPitch < 0.0) + { + if (flightPitch < -pitch_dampner) [self increase_flight_pitch:pitch_dampner]; + else flightPitch = 0.0; + } + } + + if (![UNIVERSE strict]) + { + yawing = NO; + if ([gameView isDown:key_yaw_left]) + { + keyboardYawOverride=YES; + if (flightYaw < 0.0) flightYaw = 0.0; + [self increase_flight_yaw:delta_t*yaw_delta]; + yawing = YES; + } + else if ([gameView isDown:key_yaw_right]) + { + keyboardYawOverride=YES; + if (flightYaw > 0.0) flightYaw = 0.0; + [self decrease_flight_yaw:delta_t*yaw_delta]; + yawing = YES; + } + if(numSticks && !keyboardRollPitchOverride && !keyboardYawOverride) + { + // I think yaw is handled backwards in the code, + // which is why the negative sign is here. + double stick_yaw = max_flight_yaw * (-reqYaw); + if (flightYaw < stick_yaw) + { + [self increase_flight_yaw:delta_t*yaw_delta]; + if (flightYaw > stick_yaw) + flightYaw = stick_yaw; + } + if (flightYaw > stick_yaw) + { + [self decrease_flight_yaw:delta_t*yaw_delta]; + if (flightYaw < stick_yaw) + flightYaw = stick_yaw; + } + yawing = (fabs(reqYaw) >= deadzone); + } + if (!yawing) + { + if (flightYaw > 0.0) + { + if (flightYaw > yaw_dampner) [self decrease_flight_yaw:yaw_dampner]; + else flightYaw = 0.0; + } + if (flightYaw < 0.0) + { + if (flightYaw < -yaw_dampner) [self increase_flight_yaw:yaw_dampner]; + else flightYaw = 0.0; + } + } + } +} + + +- (void) pollGuiScreenControls +{ + if(!pollControls) + return; + + GuiDisplayGen *gui = [UNIVERSE gui]; + MyOpenGLView *gameView = [UNIVERSE gameView]; + BOOL docked_okay = ([self status] == STATUS_DOCKED); + + // text displays + if (([gameView isDown:gvFunctionKey5])||([gameView isDown:gvNumberKey5])) + { + if (!switching_status_screens) + { + switching_status_screens = YES; + if ((gui_screen == GUI_SCREEN_STATUS)&&(![UNIVERSE strict])) + [self setGuiToManifestScreen]; + else + [self setGuiToStatusScreen]; + [self checkScript]; + } + } + else + { + switching_status_screens = NO; + } + + if (([gameView isDown:gvFunctionKey6])||([gameView isDown:gvNumberKey6])) + { + if (!switching_chart_screens) + { + switching_chart_screens = YES; + if (gui_screen == GUI_SCREEN_SHORT_RANGE_CHART) + [self setGuiToLongRangeChartScreen]; + else + [self setGuiToShortRangeChartScreen]; + } + } + else + { + switching_chart_screens = NO; + } + + if (([gameView isDown:gvFunctionKey7])||([gameView isDown:gvNumberKey7])) + { + if (gui_screen != GUI_SCREEN_SYSTEM_DATA) + { + [self setGuiToSystemDataScreen]; + } + } + + + if (docked_okay) + { + if ((([gameView isDown:gvFunctionKey2])||([gameView isDown:gvNumberKey2]))&&(gui_screen != GUI_SCREEN_OPTIONS)) + { + [gameView clearKeys]; + [self setGuiToLoadSaveScreen]; + } + + if (([gameView isDown:gvFunctionKey3])||([gameView isDown:gvNumberKey3])) + { + if (!switching_equipship_screens) + { + if (!dockedStation) dockedStation = [UNIVERSE station]; + OOGUIScreenID oldScreen = gui_screen; + + if ((gui_screen == GUI_SCREEN_EQUIP_SHIP)&&[dockedStation hasShipyard]) + { + [gameView clearKeys]; + [self setGuiToShipyardScreen:0]; + [gui setSelectedRow:GUI_ROW_SHIPYARD_START]; + [self showShipyardInfoForSelection]; + } + else + { + [gameView clearKeys]; + [self setGuiToEquipShipScreen:0]; + [gui setSelectedRow:GUI_ROW_EQUIPMENT_START]; + } + + [self noteGuiChangeFrom:oldScreen to:gui_screen]; + } + switching_equipship_screens = YES; + } + else + { + switching_equipship_screens = NO; + } + + if (([gameView isDown:gvFunctionKey8])||([gameView isDown:gvNumberKey8])) + { + if (!switching_market_screens) + { + if ((gui_screen == GUI_SCREEN_MARKET)&&(dockedStation == [UNIVERSE station])&&(![UNIVERSE strict])) + { + [gameView clearKeys]; + [self setGuiToContractsScreen]; + [gui setSelectedRow:GUI_ROW_PASSENGERS_START]; + } + else + { + [gameView clearKeys]; + [self setGuiToMarketScreen]; + [gui setSelectedRow:GUI_ROW_MARKET_START]; + } + } + switching_market_screens = YES; + } + else + { + switching_market_screens = NO; + } + } + else + { + if (([gameView isDown:gvFunctionKey8])||([gameView isDown:gvNumberKey8])) + { + if (!switching_market_screens) + { + [self setGuiToMarketScreen]; + [gui setSelectedRow:GUI_ROW_MARKET_START]; + } + switching_market_screens = YES; + } + else + { + switching_market_screens = NO; + } + } +} + + +- (void) pollGameOverControls:(double)delta_t +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + if ([gameView isDown:32]) // look for the spacebar + { + if (!spacePressed) + { + [UNIVERSE displayMessage:@"" forCount:1.0]; + shot_time = 31.0; // force restart + } + spacePressed = YES; + } + else + spacePressed = NO; +} + + +static BOOL toggling_music; + +- (void) pollAutopilotControls:(double)delta_t +{ + // controls polled while the autopilot is active + + MyOpenGLView *gameView = [UNIVERSE gameView]; + + // view keys + [self pollViewControls]; + + // text displays + [self pollGuiScreenControls]; + + if ([UNIVERSE displayGUI]) + [self pollGuiArrowKeyControls:delta_t]; + + if ([gameView isDown:key_autopilot]) // look for the 'c' key + { + if ([self hasDockingComputer] && (!autopilot_key_pressed)) // look for the 'c' key + { + [self abortDocking]; // let the station know that you are no longer on approach + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + autopilot_engaged = NO; + primaryTarget = NO_TARGET; + targetStation = NO_TARGET; + [self setStatus:STATUS_IN_FLIGHT]; + [self playAutopilotOff]; + [UNIVERSE addMessage:DESC(@"autopilot-off") forCount:4.5]; +#if DOCKING_CLEARANCE_ENABLED + [self setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; +#endif + [self doScriptEvent:@"playerCancelledAutoPilot"]; + + [[OOMusicController sharedController] stopDockingMusic]; + } + autopilot_key_pressed = YES; + } + else + autopilot_key_pressed = NO; + + if (([gameView isDown:key_docking_music])) // look for the 's' key + { + if (!toggling_music) + { + [[OOMusicController sharedController] toggleDockingMusic]; + } + toggling_music = YES; + } + else + { + toggling_music = NO; + } +} + + +- (void) pollDockedControls:(double)delta_t +{ + StationEntity *station = nil; + MyOpenGLView *gameView = nil; + + if(pollControls) + { + gameView = [UNIVERSE gameView]; + if (([gameView isDown:gvFunctionKey1])||([gameView isDown:gvNumberKey1])) // look for the f1 key + { + // FIXME: should this not be in leaveDock:? (Note: leaveDock: is also called from script method launchFromStation and -[StationEntity becomeExplosion]) -- Ahruman 20080308 + [UNIVERSE setUpUniverseFromStation]; // launch! + if (!dockedStation) dockedStation = [UNIVERSE station]; + station = dockedStation; // leaveDock will clear dockedStation. + + //don't autosave immediately after a load + if (station == [UNIVERSE station] && [UNIVERSE autoSaveNow]) [self autosavePlayer]; + if ([UNIVERSE autoSave]) [UNIVERSE setAutoSaveNow:YES]; + + [self leaveDock:dockedStation]; + } + } + + // text displays + // mission screens + if (gui_screen == GUI_SCREEN_MISSION) + [self pollDemoControls: delta_t]; + else + [self pollGuiScreenControls]; // don't switch away from mission screens + + [self pollGuiArrowKeyControls:delta_t]; +} + + +- (void) pollDemoControls:(double)delta_t +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + GuiDisplayGen *gui = [UNIVERSE gui]; + + switch (gui_screen) + { + case GUI_SCREEN_INTRO1: + if(0) {} // Dummy statement so compiler does not complain. + + // In order to support multiple languages, the Y/N response cannot be hardcoded. We get the keys + // corresponding to Yes/No from descriptions.plist and if they are not found there, we set them + // by default to [yY] and [nN] respectively. + id valueYes = [[UNIVERSE descriptions] stringForKey:@"load-previous-commander-yes" defaultValue:@"y"]; + id valueNo = [[UNIVERSE descriptions] stringForKey:@"load-previous-commander-no" defaultValue:@"n"]; + unsigned char loadPreviousCommanderYes, loadPreviousCommanderNo; + + loadPreviousCommanderYes = [valueYes characterAtIndex: 0] & 0x00ff; // Use lower byte of unichar. + loadPreviousCommanderNo = [valueNo characterAtIndex: 0] & 0x00ff; // Use lower byte of unichar. + + if (!disc_operation_in_progress) + { + if (([gameView isDown:loadPreviousCommanderYes]) || ([gameView isDown:loadPreviousCommanderYes - 32])) + { + [[OOMusicController sharedController] stopThemeMusic]; + disc_operation_in_progress = YES; + [self setStatus:STATUS_DOCKED]; + [UNIVERSE removeDemoShips]; + [gui clearBackground]; + if (![self loadPlayer]) + { + [self setGuiToIntroFirstGo:NO]; + } + } + } + if (([gameView isDown:loadPreviousCommanderNo]) || ([gameView isDown:loadPreviousCommanderNo - 32])) + { + [self setGuiToIntroFirstGo:NO]; + } + + break; + + case GUI_SCREEN_INTRO2: + if ([gameView isDown:' ']) // '' + { + [self setStatus: STATUS_DOCKED]; + [UNIVERSE removeDemoShips]; + [gui clearBackground]; + [self setGuiToStatusScreen]; + [[OOMusicController sharedController] stopThemeMusic]; + } + if ([gameView isDown:gvArrowKeyLeft]) // '<--' + { + if (!upDownKeyPressed) + [UNIVERSE selectIntro2Previous]; + } + if ([gameView isDown:gvArrowKeyRight]) // '-->' + { + if (!upDownKeyPressed) + [UNIVERSE selectIntro2Next]; + } + upDownKeyPressed = (([gameView isDown:gvArrowKeyLeft])||([gameView isDown:gvArrowKeyRight])); + break; + + case GUI_SCREEN_MISSION: + if ([[gui keyForRow:21] isEqual:@"spacebar"]) + { + if ([gameView isDown:32]) // '' + { + if (!spacePressed) + { + [self setStatus:STATUS_DOCKED]; + [UNIVERSE removeDemoShips]; + [gui clearBackground]; + [self setGuiToStatusScreen]; + [[OOMusicController sharedController] stopMissionMusic]; + + [self doScriptEvent:@"missionScreenEnded"]; + } + spacePressed = YES; + } + else + spacePressed = NO; + } + else + { + int guiSelectedRow = [gui selectedRow]; + if ([gameView isDown:gvArrowKeyDown]) + { + if ((!upDownKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([gui setSelectedRow:guiSelectedRow + 1]) + { + [self playMenuNavigationDown]; + } + else + { + [self playMenuNavigationNot]; + } + timeLastKeyPress = script_time; + } + } + if ([gameView isDown:gvArrowKeyUp]) + { + if ((!upDownKeyPressed)||(script_time > timeLastKeyPress + KEY_REPEAT_INTERVAL)) + { + if ([gui setSelectedRow:guiSelectedRow - 1]) + { + [self playMenuNavigationUp]; + } + else + { + [self playMenuNavigationNot]; + } + timeLastKeyPress = script_time; + } + } + upDownKeyPressed = (([gameView isDown:gvArrowKeyUp])||([gameView isDown:gvArrowKeyDown])); + + if ([gameView isDown:13]) // '' + { + if (!enterSelectKeyPressed) + { + [self setMissionChoice:[gui selectedRowKey]]; + + [UNIVERSE removeDemoShips]; + [gui clearBackground]; + [self setGuiToStatusScreen]; + [[OOMusicController sharedController] stopMissionMusic]; + [self playDismissedMissionScreen]; + + [self doScriptEvent:@"missionScreenEnded"]; + [self checkScript]; + } + enterSelectKeyPressed = YES; + } + else + { + enterSelectKeyPressed = NO; + } + } + break; + + default: + break; + } +} + +@end diff --git a/src/Core/Entities/PlayerEntityLegacyScriptEngine.h b/src/Core/Entities/PlayerEntityLegacyScriptEngine.h new file mode 100644 index 00000000..3b4cb399 --- /dev/null +++ b/src/Core/Entities/PlayerEntityLegacyScriptEngine.h @@ -0,0 +1,254 @@ +/* + +PlayerEntityLegacyScriptEngine.h + +Various utility methods used for scripting. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" + + +@class OOScript; + + +typedef enum +{ + COMPARISON_EQUAL, + COMPARISON_NOTEQUAL, + COMPARISON_LESSTHAN, + COMPARISON_GREATERTHAN, + COMPARISON_ONEOF, + COMPARISON_UNDEFINED +} OOComparisonType; + + +typedef enum +{ + OP_STRING, + OP_NUMBER, + OP_BOOL, + OP_MISSION_VAR, + OP_LOCAL_VAR, + OP_FALSE, + + OP_INVALID // Must be last. +} OOOperationType; + + +@interface PlayerEntity (Scripting) + +- (void) checkScript; + +- (void) setScriptTarget:(ShipEntity *)ship; +- (ShipEntity*) scriptTarget; + +- (void) runScriptActions:(NSArray *)sanitizedActions withContextName:(NSString *)contextName forTarget:(ShipEntity *)target; +- (void) runUnsanitizedScriptActions:(NSArray *)unsanitizedActions allowingAIMethods:(BOOL)allowAIMethods withContextName:(NSString *)contextName forTarget:(ShipEntity *)target; + +// Test (sanitized) legacy script conditions array. +- (BOOL) scriptTestConditions:(NSArray *)array; + +- (NSDictionary*) missionVariables; + +- (NSString *)missionVariableForKey:(NSString *)key; +- (void)setMissionVariable:(NSString *)value forKey:(NSString *)key; + +- (NSMutableDictionary *)localVariablesForMission:(NSString *)missionKey; +- (NSString *)localVariableForKey:(NSString *)variableName andMission:(NSString *)missionKey; +- (void)setLocalVariable:(NSString *)value forKey:(NSString *)variableName andMission:(NSString *)missionKey; + +/*-----------------------------------------------------*/ + +- (NSString *) mission_string; +- (NSString *) status_string; +- (NSString *) gui_screen_string; +- (NSNumber *) galaxy_number; +- (NSNumber *) planet_number; +- (NSNumber *) score_number; +- (NSNumber *) credits_number; +- (NSNumber *) scriptTimer_number; +- (NSNumber *) shipsFound_number; + +- (NSNumber *) d100_number; +- (NSNumber *) pseudoFixedD100_number; +- (NSNumber *) d256_number; +- (NSNumber *) pseudoFixedD256_number; + +- (NSNumber *) clock_number; // returns the game time in seconds +- (NSNumber *) clock_secs_number; // returns the game time in seconds +- (NSNumber *) clock_mins_number; // returns the game time in minutes +- (NSNumber *) clock_hours_number; // returns the game time in hours +- (NSNumber *) clock_days_number; // returns the game time in days + +- (NSNumber *) fuelLevel_number; // returns the fuel level in LY + +- (NSString *) dockedAtMainStation_bool; +- (NSString *) foundEquipment_bool; + +- (NSString *) sunWillGoNova_bool; // returns whether the sun is going to go nova +- (NSString *) sunGoneNova_bool; // returns whether the sun has gone nova + +- (NSString *) missionChoice_string; // returns nil or the key for the chosen option + +- (NSNumber *) dockedTechLevel_number; +- (NSString *) dockedStationName_string; // returns 'NONE' if the player isn't docked, [station name] if it is, 'UNKNOWN' otherwise + +- (NSNumber *) systemGovernment_number; +- (NSString *) systemGovernment_string; +- (NSNumber *) systemEconomy_number; +- (NSString *) systemEconomy_string; +- (NSNumber *) systemTechLevel_number; +- (NSNumber *) systemPopulation_number; +- (NSNumber *) systemProductivity_number; + +- (NSString *) commanderName_string; +- (NSString *) commanderRank_string; +- (NSString *) commanderShip_string; +- (NSString *) commanderShipDisplayName_string; +- (NSString *) commanderLegalStatus_string; +- (NSNumber *) commanderLegalStatus_number; + +/*-----------------------------------------------------*/ + +- (NSArray *) missionsList; + +- (void) setMissionDescription:(NSString *)textKey; +- (void) clearMissionDescription; +- (void) setMissionDescription:(NSString *)textKey forMission:(NSString *)key; +- (void) clearMissionDescriptionForMission:(NSString *)key; + +- (void) commsMessage:(NSString *)valueString; +- (void) commsMessageByUnpiloted:(NSString *)valueString; // Enabled 02-May-2008 - Nikos. Same as commsMessage, but + // can be used by scripts to have unpiloted ships sending + // commsMessages, if we want to. + +- (void) consoleMessage3s:(NSString *)valueString; +- (void) consoleMessage6s:(NSString *)valueString; + +- (void) setLegalStatus:(NSString *)valueString; +- (void) awardCredits:(NSString *)valueString; +- (void) awardShipKills:(NSString *)valueString; +- (void) awardEquipment:(NSString *)equipString; //eg. EQ_NAVAL_ENERGY_UNIT +- (void) removeEquipment:(NSString *)equipString; //eg. EQ_NAVAL_ENERGY_UNIT + +- (void) setPlanetinfo:(NSString *)key_valueString; // uses key=value format +- (void) setSpecificPlanetInfo:(NSString *)key_valueString; // uses galaxy#=planet#=key=value + +- (void) awardCargo:(NSString *)amount_typeString; +- (void) removeAllCargo; +- (void) removeAllCargo:(BOOL)forceRemoval; + +- (void) useSpecialCargo:(NSString *)descriptionString; + +- (void) testForEquipment:(NSString *)equipString; //eg. EQ_NAVAL_ENERGY_UNIT + +- (void) awardFuel:(NSString *)valueString; // add to fuel up to 7.0 LY + +- (void) messageShipAIs:(NSString *)roles_message; +- (void) ejectItem:(NSString *)item_key; +- (void) addShips:(NSString *)roles_number; +- (void) addSystemShips:(NSString *)roles_number_position; +- (void) addShipsAt:(NSString *)roles_number_system_x_y_z; +- (void) addShipsAtPrecisely:(NSString *)roles_number_system_x_y_z; +- (void) addShipsWithinRadius:(NSString *)roles_number_system_x_y_z_r; +- (void) spawnShip:(NSString *)ship_key; +- (void) set:(NSString *)missionvariable_value; +- (void) reset:(NSString *)missionvariable; +/* + set:missionvariable_value + add:missionvariable_value + subtract:missionvariable_value + + the value may be a string constant or one of the above calls + ending in _bool, _number, or _string + + egs. + set: mission_my_mission_status MISSION_START + set: mission_my_mission_value 12.345 + set: mission_my_mission_clock clock_number + add: mission_my_mission_clock 86400 + subtract: mission_my_mission_clock d100_number +*/ + +- (void) increment:(NSString *)missionVariableString; +- (void) decrement:(NSString *)missionVariableString; + +- (void) add:(NSString *)missionVariableString_value; +- (void) subtract:(NSString *)missionVariableString_value; + +- (void) checkForShips: (NSString *)roleString; +- (void) resetScriptTimer; +- (void) addMissionText: (NSString *)textKey; +- (void) addLiteralMissionText: (NSString *)text; + +- (void) setMissionChoices:(NSString *)choicesKey; // choicesKey is a key for a dictionary of + // choices/choice phrases in missiontext.plist and also.. +- (void) resetMissionChoice; // resets MissionChoice to nil + +- (void) clearMissionScreen; + +- (void) addMissionDestination:(NSString *)destinations; // mark a system on the star charts +- (void) removeMissionDestination:(NSString *)destinations; // stop a system being marked on star charts + +- (void) showShipModel: (NSString *)shipKey; +- (void) setMissionMusic: (NSString *)value; +- (void) setMissionImage: (NSString *)value; + +- (void) setFuelLeak: (NSString *)value; +- (NSNumber *)fuelLeakRate_number; +- (void) setSunNovaIn: (NSString *)time_value; +- (void) launchFromStation; +- (void) blowUpStation; +- (void) sendAllShipsAway; + +- (void) addPlanet: (NSString *)planetKey; +- (void) addMoon: (NSString *)moonKey; + +- (void) debugOn; +- (void) debugOff; +- (void) debugMessage:(NSString *)args; + +- (NSString*) replaceVariablesInString:(NSString*) args; + +- (void) playSound:(NSString *) soundName; + +- (BOOL) mapKey:(NSString *) keycode toOXP:(OOScript *)oxp; +- (void) targetNearestHostile; +#if TARGET_INCOMING_MISSILES +- (void) targetNearestIncomingMissile; +#endif + +- (void) setGalacticHyperspaceBehaviourTo:(NSString *) galacticHyperspaceBehaviourString; +- (void) setGalacticHyperspaceFixedCoordsTo:(NSString *) galacticHyperspaceFixedCoordsString; + +/*-----------------------------------------------------*/ + +- (void) setGuiToMissionScreen; +- (void) setBackgroundFromDescriptionsKey:(NSString*) d_key; +- (void) addScene:(NSArray *) items atOffset:(Vector) off; +- (BOOL) processSceneDictionary:(NSDictionary *) couplet atOffset:(Vector) off; +- (BOOL) processSceneString:(NSString*) item atOffset:(Vector) off; + +@end + + +NSString *OOComparisonTypeToString(OOComparisonType type) CONST_FUNC; diff --git a/src/Core/Entities/PlayerEntityLegacyScriptEngine.m b/src/Core/Entities/PlayerEntityLegacyScriptEngine.m new file mode 100644 index 00000000..80a182cf --- /dev/null +++ b/src/Core/Entities/PlayerEntityLegacyScriptEngine.m @@ -0,0 +1,2705 @@ +/* + +PlayerEntityLegacyScriptEngine.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntityLegacyScriptEngine.h" +#import "PlayerEntityScriptMethods.h" +#import "PlayerEntitySound.h" +#import "GuiDisplayGen.h" +#import "Universe.h" +#import "ResourceManager.h" +#import "AI.h" +#import "ShipEntityAI.h" +#import "ShipEntityScriptMethods.h" +#import "OOScript.h" +#import "OOMusicController.h" +#import "OOColor.h" +#import "OOStringParsing.h" +#import "OOConstToString.h" +#import "OOTexture.h" +#import "OOCollectionExtractors.h" +#import "OOLoggingExtended.h" +#import "OOSound.h" +#import "PlanetEntity.h" +#import "ParticleEntity.h" +#import "StationEntity.h" +#import "Comparison.h" +#import "OOLegacyScriptWhitelist.h" + +#define kOOLogUnconvertedNSLog @"unclassified.PlayerEntityLegacyScriptEngine" + + +#define SUPPORT_TRACE_MESSAGES (!defined NDEBUG) + +// Trace messages are very verbose debug messages in the script mechanism, +// disabled in logcontrol.plist by default and disabled here in release builds +// for performance reasons. +#if SUPPORT_TRACE_MESSAGES +#define TraceLog OOLog +#else +#define TraceLog(...) do {} while (0) +#endif + + +#define TRACE_AND_RETURN(x) do { BOOL r = (x); TraceLog(kOOLogTraceTestConditionResult, @" Result: %@", r ? @"YES" : @"NO"); return r; } while (0) + + +static NSString * const kOOLogScriptAddShipsFailed = @"script.addShips.failed"; +static NSString * const kOOLogScriptMissionDescNoText = @"script.missionDescription.noMissionText"; +static NSString * const kOOLogScriptMissionDescNoKey = @"script.missionDescription.noMissionKey"; + +static NSString * const kOOLogDebug = @"script.debug"; +static NSString * const kOOLogDebugOnMetaClass = @"$scriptDebugOn"; + NSString * const kOOLogDebugMessage = @"script.debug.message"; +static NSString * const kOOLogDebugOnOff = @"script.debug.onOff"; +static NSString * const kOOLogDebugAddPlanet = @"script.debug.addPlanet"; +static NSString * const kOOLogDebugReplaceVariablesInString = @"script.debug.replaceVariablesInString"; +static NSString * const kOOLogDebugProcessSceneStringAddScene = @"script.debug.processSceneString.addScene"; +static NSString * const kOOLogDebugProcessSceneStringAddModel = @"script.debug.processSceneString.addModel"; +static NSString * const kOOLogDebugProcessSceneStringAddMiniPlanet = @"script.debug.processSceneString.addMiniPlanet"; +static NSString * const kOOLogDebugProcessSceneStringAddBillboard = @"script.debug.processSceneString.addBillboard"; + +static NSString * const kOOLogTraceScriptAction = @"script.debug.trace.scriptAction"; +static NSString * const kOOLogTraceTestCondition = @"script.debug.trace.testCondition"; +static NSString * const kOOLogTraceTestConditionCheckingVariable = @"script.debug.trace.testCondition.checkingVariable"; +static NSString * const kOOLogTraceTestConditionValues = @"script.debug.trace.testCondition.testValues"; +static NSString * const kOOLogTraceTestConditionResult = @"script.debug.trace.testCondition.testResult"; +static NSString * const kOOLogTraceTestConditionOneOf = @"script.debug.trace.testCondition.oneOf"; + +static NSString * const kOOLogNoteRemoveAllCargo = @"script.debug.note.removeAllCargo"; +static NSString * const kOOLogNoteUseSpecialCargo = @"script.debug.note.useSpecialCargo"; + NSString * const kOOLogNoteAddShips = @"script.debug.note.addShips"; +static NSString * const kOOLogNoteSet = @"script.debug.note.set"; +static NSString * const kOOLogNoteShowShipModel = @"script.debug.note.showShipModel"; +static NSString * const kOOLogNoteFuelLeak = @"script.debug.note.setFuelLeak"; +static NSString * const kOOLogNoteAddPlanet = @"script.debug.note.addPlanet"; +static NSString * const kOOLogNoteProcessSceneString = @"script.debug.note.processSceneString"; + +static NSString * const kOOLogSyntaxBadConditional = @"script.debug.syntax.badConditional"; +static NSString * const kOOLogSyntaxNoAction = @"script.debug.syntax.action.noneSpecified"; +static NSString * const kOOLogSyntaxBadAction = @"script.debug.syntax.action.badSelector"; +static NSString * const kOOLogSyntaxNoScriptCondition = @"script.debug.syntax.scriptCondition.noneSpecified"; +static NSString * const kOOLogSyntaxBadScriptCondition = @"script.debug.syntax.scriptCondition.badSelector"; +static NSString * const kOOLogSyntaxSetPlanetInfo = @"script.debug.syntax.setPlanetInfo"; +static NSString * const kOOLogSyntaxAwardCargo = @"script.debug.syntax.awardCargo"; +static NSString * const kOOLogSyntaxAwardEquipment = @"script.debug.syntax.awardEquipment"; +static NSString * const kOOLogSyntaxMessageShipAIs = @"script.debug.syntax.messageShipAIs"; + NSString * const kOOLogSyntaxAddShips = @"script.debug.syntax.addShips"; +static NSString * const kOOLogSyntaxSet = @"script.debug.syntax.set"; +static NSString * const kOOLogSyntaxReset = @"script.debug.syntax.reset"; +static NSString * const kOOLogSyntaxIncrement = @"script.debug.syntax.increment"; +static NSString * const kOOLogSyntaxDecrement = @"script.debug.syntax.decrement"; +static NSString * const kOOLogSyntaxAdd = @"script.debug.syntax.add"; +static NSString * const kOOLogSyntaxSubtract = @"script.debug.syntax.subtract"; +static NSString * const kOOLogInvalidComparison = @"script.debug.syntax.badComparison"; + +static NSString * const kOOLogRemoveAllCargoNotDocked = @"script.error.removeAllCargo.notDocked"; + + +#define ACTIONS_TEMP_PREFIX "__oolite_actions_temp" +static NSString * const kActionTempPrefix = @ ACTIONS_TEMP_PREFIX; +static NSString * const kActionTempFormat = @ ACTIONS_TEMP_PREFIX ".%u"; + + +static NSString *sMissionStringValue = nil; +static NSString *sCurrentMissionKey = nil; +static ShipEntity *scriptTarget = nil; + + +@interface PlayerEntity (ScriptingPrivate) + +- (BOOL) scriptTestCondition:(NSArray *)scriptCondition; +- (NSString *) expandScriptRightHandSide:(NSArray *)rhsComponents; + +- (void) scriptActions:(NSArray *)actions forTarget:(ShipEntity *)target missionKey:(NSString *)missionKey; + +@end + + +@implementation PlayerEntity (Scripting) + + +static NSString *CurrentScriptNameOr(NSString *alternative) +{ + if (sCurrentMissionKey != nil && ![sCurrentMissionKey hasPrefix:kActionTempPrefix]) + { + return [NSString stringWithFormat:@"\"%@\"", sCurrentMissionKey]; + } + return alternative; +} + + +OOINLINE NSString *CurrentScriptName(void) +{ + return CurrentScriptNameOr(nil); +} + + +OOINLINE NSString *CurrentScriptDesc(void) +{ + return CurrentScriptNameOr(@""); +} + + +static void PerformScriptActions(NSArray *actions, Entity *target); +static void PerformConditionalStatment(NSArray *actions, Entity *target); +static void PerformActionStatment(NSArray *statement, Entity *target); +static BOOL TestScriptConditions(NSArray *conditions); + + +static void PerformScriptActions(NSArray *actions, Entity *target) +{ + unsigned i, count; + NSArray *statement = nil; + + count = [actions count]; + for (i = 0; i < count; i++) + { + statement = [actions objectAtIndex:i]; + if ([[statement objectAtIndex:0] boolValue]) + { + PerformConditionalStatment(statement, target); + } + else + { + PerformActionStatment(statement, target); + } + } +} + + +static void PerformConditionalStatment(NSArray *statement, Entity *target) +{ + /* A sanitized conditional statement takes the form of an array: + (true, conditions, trueActions, falseActions) + The first element is always true. The second is an array of conditions. + The third and four elements are actions to perform if the conditions + evaluate to true or false, respectively. + */ + + NSArray *conditions = nil; + NSArray *actions = nil; + + conditions = [statement objectAtIndex:1]; + + if (TestScriptConditions(conditions)) + { + actions = [statement objectAtIndex:2]; + } + else + { + actions = [statement objectAtIndex:3]; + } + + PerformScriptActions(actions, target); +} + + +static void PerformActionStatment(NSArray *statement, Entity *target) +{ + /* A sanitized action statement takes the form of an array: + (false, selector [, argument]) + The first element is always false. The second is the method selector + (as a string). If the method takes an argument, the third argument is + the argument string. + + The sanitizer is responsible for ensuring that there is an argument, + even if it's the empty string, for any selector with a colon at the + end, and no arguments for selectors without colons. The runner can + therefore use the list's element count as a flag without examining the + selector. + */ + + NSString *selectorString = nil; + NSString *argumentString = nil; + NSString *expandedString = nil; + SEL selector = NULL; + NSMutableDictionary *locals = nil; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + + selectorString = [statement objectAtIndex:1]; + if ([statement count] > 2) argumentString = [statement objectAtIndex:2]; + + TraceLog(kOOLogTraceScriptAction, @"script action: \"%@%@\"", selectorString, argumentString ? (NSString *)[@" " stringByAppendingString:argumentString] : (NSString *)@""); + + selector = NSSelectorFromString(selectorString); + + if (target == nil || ![target respondsToSelector:selector]) + { + target = player; + } + + if (argumentString != nil) + { + // Method with argument; substitute [description] expressions. + locals = [player localVariablesForMission:sCurrentMissionKey]; + expandedString = ExpandDescriptionsWithLocalsForCurrentSystem(argumentString, locals); + +#if SUPPORT_TRACE_MESSAGES + if (![expandedString isEqualToString:argumentString]) + { + OOLogIndent(); + TraceLog(kOOLogTraceScriptAction, @"script action after expansion: \"%@ %@\"", selectorString, argumentString); + OOLogOutdent(); + } +#endif + + [target performSelector:selector withObject:expandedString]; + } + else + { + // Method without argument. + [target performSelector:selector]; + } +} + + +static BOOL TestScriptConditions(NSArray *conditions) +{ + NSEnumerator *condEnum = nil; + NSArray *condition = nil; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + + for (condEnum = [conditions objectEnumerator]; (condition = [condEnum nextObject]); ) + { + if (![player scriptTestCondition:condition]) return NO; + } + + return YES; +} + + +- (void) setScriptTarget:(ShipEntity *)ship +{ + scriptTarget = ship; +} + + +- (ShipEntity*) scriptTarget +{ + return scriptTarget; +} + + +OOINLINE OOEntityStatus RecursiveRemapStatus(OOEntityStatus status) +{ + // Some player stutuses should only be seen once per "event". + // This remaps them to something innocuous in case of recursion. + if (status == STATUS_DOCKING || + status == STATUS_LAUNCHING || + status == STATUS_ENTERING_WITCHSPACE || + status == STATUS_EXITING_WITCHSPACE) + { + return STATUS_IN_FLIGHT; + } + else + { + return status; + } +} + + +static BOOL sRunningScript = NO; + + +- (void) checkScript +{ + BOOL wasRunningScript = sRunningScript; + OOEntityStatus status, restoreStatus; + + [self setScriptTarget:self]; + + OOLogPushIndent(); + OOLog(@"script.trace.runWorld", @"----- Running world script with state %@", [self status_string]); + OOLogIndentIf(@"script.trace.runWorld"); + + /* World scripts can potentially be invoked recursively, through + scriptActionOnTarget: and possibly other mechanisms. This is bad, but + that's the way it is. Legacy world scripts rely on only seeing certain + player statuses once per "event". To ensure this, we must lie about + the player's status when invoked recursively. + + Of course, there are also methods in the game that rely on status not + lying. However, I don't believe any that rely on these particular + statuses can be legitimately invoked by scripts. The alternative would + be to track the "status-as-seen-by-scripts" separately from the "real" + status, which'd risk synchronization problems. + + In summary, scriptActionOnTarget: is bad, and calling it from scripts + rather than AIs is very bad. + -- Ahruman, 20080302 + + Addendum: scriptActionOnTarget: is currently not in the whitelist for + script methods. Let's hope this doesn't turn out to be a problem. + -- Ahruman, 20090208 + */ + status = [self status]; + restoreStatus = status; + NS_DURING + if (sRunningScript) + { + status = RecursiveRemapStatus(status); + [self setStatus:status]; + if (RecursiveRemapStatus(status) != restoreStatus) + { + OOLog(@"script.trace.runWorld.recurse.lying", @"----- Running world script recursively and temporarily changing player status from %@ to %@.", EntityStatusToString(restoreStatus), EntityStatusToString(status)); + } + else + { + OOLog(@"script.trace.runWorld.recurse", @"----- Running world script recursively.", EntityStatusToString(restoreStatus), EntityStatusToString(status)); + } + } + sRunningScript = YES; + + // After all that, actually running the scripts is trivial. + [[worldScripts allValues] makeObjectsPerformSelector:@selector(runWithTarget:) withObject:self]; + NS_HANDLER + OOLog(kOOLogException, @"***** Exception running world scripts: %@ : %@", [localException name], [localException reason]); + NS_ENDHANDLER + + // Restore anti-recursion measures. + sRunningScript = wasRunningScript; + if (status != restoreStatus) [self setStatus:restoreStatus]; + + OOLogPopIndent(); +} + + +- (void)runScriptActions:(NSArray *)actions withContextName:(NSString *)contextName forTarget:(ShipEntity *)target +{ + NSAutoreleasePool *pool = nil; + NSString *oldMissionKey = nil; + NSString * volatile theMissionKey = contextName; // Work-around for silly exception macros + + pool = [[NSAutoreleasePool alloc] init]; + + // FIXME: does this actually make sense in the context of non-missions? + oldMissionKey = sCurrentMissionKey; + sCurrentMissionKey = theMissionKey; + [self setScriptTarget:target]; + + NS_DURING + PerformScriptActions(actions, target); + NS_HANDLER + OOLog(@"script.error.exception", + @"***** EXCEPTION %@: %@ while handling legacy script actions for %@", + [localException name], + [localException reason], + [theMissionKey hasPrefix:kActionTempPrefix] ? [target shortDescription] : theMissionKey); + // Suppress exception + NS_ENDHANDLER + + sCurrentMissionKey = oldMissionKey; + [pool release]; +} + + +- (void) runUnsanitizedScriptActions:(NSArray *)actions allowingAIMethods:(BOOL)allowAIMethods withContextName:(NSString *)contextName forTarget:(ShipEntity *)target +{ + [self runScriptActions:OOSanitizeLegacyScript(actions, contextName, allowAIMethods) + withContextName:contextName + forTarget:target]; +} + + +- (BOOL) scriptTestConditions:(NSArray *)array +{ + BOOL result = NO; + + NS_DURING + result = TestScriptConditions(array); + NS_HANDLER + OOLog(@"script.error.exception", + @"***** EXCEPTION %@: %@ while testing legacy script conditions.", + [localException name], + [localException reason]); + // Suppress exception + NS_ENDHANDLER + + return result; +} + + +- (BOOL) scriptTestCondition:(NSArray *)scriptCondition +{ + /* Test a script condition sanitized by OOLegacyScriptWhitelist. + + A sanitized condition is an array of the form: + (opType, rawString, selector, comparisonType, operandArray). + + opType and comparisonType are NSNumbers containing OOOperationType and + OOComparisonType enumerators, respectively. + + rawString is the original textual representation of the condition for + display purposes. + + selector is a string, either a method selector or a mission/local + variable name. + + operandArray is an array of operands. Each operand is itself an array + of two items: a boolean indicating whether it's a method selector + (true) or a string (false), and a string. + + The special opType OP_FALSE doesn't require any other elements in the + array. All other valid opTypes require the array to have five elements. + + For performance reasons, this method assumes the script condition will + have been generated by OOSanitizeLegacyScriptConditions() and doesn't + perform extensive validity checks. + */ + + OOOperationType opType; + NSString *selectorString = nil; + SEL selector = NULL; + OOComparisonType comparator; + NSArray *operandArray = nil; + NSString *lhsString = nil; + NSString *expandedRHS = nil; + NSArray *rhsComponents = nil; + NSString *rhsItem = nil; + OOUInteger i, count; + NSCharacterSet *whitespace = nil; + double lhsValue, rhsValue; + BOOL lhsFlag, rhsFlag; + + opType = [[scriptCondition objectAtIndex:0] unsignedIntValue]; + if (opType == OP_FALSE) return NO; + + selectorString = [scriptCondition objectAtIndex:2]; + comparator = [[scriptCondition objectAtIndex:3] unsignedIntValue]; + operandArray = [scriptCondition objectAtIndex:4]; + + TraceLog(kOOLogTraceTestCondition, @"scriptTestCondition [%@]: \"%@\"", CurrentScriptDesc(), [scriptCondition objectAtIndex:1]); + + // Transform mission/local var ops into string ops. + if (opType == OP_MISSION_VAR) + { + sMissionStringValue = [mission_variables objectForKey:selectorString]; + selector = @selector(mission_string); + opType = OP_STRING; + } + else if (opType == OP_LOCAL_VAR) + { + sMissionStringValue = [[self localVariablesForMission:sCurrentMissionKey] objectForKey:selectorString]; + selector = @selector(mission_string); + opType = OP_STRING; + } + else + { + selector = NSSelectorFromString(selectorString); + } + + expandedRHS = [self expandScriptRightHandSide:operandArray]; + + if (opType == OP_STRING) + { + lhsString = [self performSelector:selector]; + TraceLog(kOOLogTraceTestConditionValues, @"..... comparing %@ (from %@) to \"%@\" with operator %@", + lhsString ? (NSString *)[NSString stringWithFormat:@"\"%@\"", lhsString] : (NSString *)@"nil", + selectorString, + expandedRHS ? expandedRHS: (NSString *)(comparator == COMPARISON_UNDEFINED ? @"undefined" : @"nil"), + OOComparisonTypeToString(comparator)); + + switch (comparator) + { + case COMPARISON_UNDEFINED: + TRACE_AND_RETURN(lhsString == nil); + + case COMPARISON_EQUAL: + TRACE_AND_RETURN([lhsString isEqualToString:expandedRHS]); + + case COMPARISON_NOTEQUAL: + TRACE_AND_RETURN(![lhsString isEqualToString:expandedRHS]); + + case COMPARISON_LESSTHAN: + TRACE_AND_RETURN([lhsString doubleValue] < [expandedRHS doubleValue]); + + case COMPARISON_GREATERTHAN: + TRACE_AND_RETURN([lhsString doubleValue] > [expandedRHS doubleValue]); + + case COMPARISON_ONEOF: + { + rhsComponents = [expandedRHS componentsSeparatedByString:@","]; + count = [rhsComponents count]; + + TraceLog(kOOLogTraceTestConditionOneOf, @"performing a ONEOF comparison with %u elements: is %@ ONEOF %@ ?", count, lhsString, expandedRHS); + + whitespace = [NSCharacterSet whitespaceCharacterSet]; + lhsString = [lhsString stringByTrimmingCharactersInSet:whitespace]; + + for (i = 0; i < count; i++) + { + rhsItem = [[rhsComponents objectAtIndex:i] stringByTrimmingCharactersInSet:whitespace]; + if ([lhsString isEqualToString:rhsItem]) + { + TraceLog(kOOLogTraceTestConditionOneOf, @"found a match (\"%@\") in ONEOF!", rhsItem); + TRACE_AND_RETURN(YES); + } + } + } + TRACE_AND_RETURN(NO); + } + } + else if (opType == OP_NUMBER) + { + lhsValue = [[self performSelector:selector] doubleValue]; + + TraceLog(kOOLogTraceTestConditionValues, @"..... comparing %g (from %@) to \"%@\" with operator %@", + lhsValue, + selectorString, + expandedRHS ? expandedRHS : (NSString *)@"nil", + OOComparisonTypeToString(comparator)); + + if (comparator == COMPARISON_ONEOF) + { + rhsComponents = [expandedRHS componentsSeparatedByString:@","]; + count = [rhsComponents count]; + + TraceLog(kOOLogTraceTestConditionOneOf, @"performing a ONEOF comparison with %u elements: is %@ ONEOF %@ ?", count, lhsString, expandedRHS); + + for (i = 0; i < count; i++) + { + rhsItem = [rhsComponents objectAtIndex:i]; + rhsValue = [rhsItem doubleValue]; + + if (lhsValue == rhsValue) + { + TraceLog(kOOLogTraceTestConditionOneOf, @"found a match (%@) in ONEOF!", rhsItem); + TRACE_AND_RETURN(YES); + } + } + + TraceLog(kOOLogTraceTestConditionOneOf, @"No match in ONEOF"); + TRACE_AND_RETURN(NO); + } + else + { + rhsValue = [expandedRHS doubleValue]; + + switch (comparator) + { + case COMPARISON_EQUAL: + TRACE_AND_RETURN(lhsValue == rhsValue); + + case COMPARISON_NOTEQUAL: + TRACE_AND_RETURN(lhsValue != rhsValue); + + case COMPARISON_LESSTHAN: + TRACE_AND_RETURN(lhsValue < rhsValue); + + case COMPARISON_GREATERTHAN: + TRACE_AND_RETURN(lhsValue > rhsValue); + + case COMPARISON_UNDEFINED: + case COMPARISON_ONEOF: + // "Can't happen" - undefined should have been caught by the sanitizer, oneof is handled above. + OOLog(@"script.error.unexpectedOperator", @"***** SCRIPT ERROR: in %@, operator %@ is not valid for numbers, evaluating to false.", CurrentScriptDesc(), OOComparisonTypeToString(comparator)); + TRACE_AND_RETURN(NO); + } + } + } + else if (opType == OP_BOOL) + { + lhsFlag = [[self performSelector:selector] isEqual:@"YES"]; + rhsFlag = [expandedRHS isEqual:@"YES"]; + + switch (comparator) + { + case COMPARISON_EQUAL: + TRACE_AND_RETURN(lhsFlag == rhsFlag); + + case COMPARISON_NOTEQUAL: + TRACE_AND_RETURN(lhsFlag != rhsFlag); + + case COMPARISON_LESSTHAN: + case COMPARISON_GREATERTHAN: + case COMPARISON_UNDEFINED: + case COMPARISON_ONEOF: + // "Can't happen" - should have been caught by the sanitizer. + OOLog(@"script.error.unexpectedOperator", @"***** SCRIPT ERROR: in %@, operator %@ is not valid for booleans, evaluating to false.", CurrentScriptDesc(), OOComparisonTypeToString(comparator)); + TRACE_AND_RETURN(NO); + } + } + + // What are we doing here? + OOLog(@"script.error.fallthrough", @"***** SCRIPT ERROR: in %@, unhandled condition '%@' (%@). This is an internal error, please report it.", CurrentScriptDesc(), [scriptCondition objectAtIndex:1], scriptCondition); + return NO; +} + + +- (NSString *) expandScriptRightHandSide:(NSArray *)rhsComponents +{ + NSMutableArray *result = nil; + NSEnumerator *componentEnum = nil; + NSArray *component = nil; + NSString *value = nil; + + result = [NSMutableArray arrayWithCapacity:[rhsComponents count]]; + + for (componentEnum = [rhsComponents objectEnumerator]; (component = [componentEnum nextObject]); ) + { + /* Each component is a two-element array. The second element is a + string. The first element is a boolean indicating whether the + string is a selector (true) or a literal (false). + + All valid selectors return a string or an NSNumber; in either + case, -description gives us a useful value to substitute into + the expanded string. + */ + + value = [component objectAtIndex:1]; + + if ([[component objectAtIndex:0] boolValue]) + { + value = [[self performSelector:NSSelectorFromString(value)] description]; + if (value == nil) value = @"(null)"; // for backwards compatibility + } + + [result addObject:value]; + } + + return [result componentsJoinedByString:@" "]; +} + + +- (NSDictionary *) missionVariables +{ + return mission_variables; +} + + +- (NSString *)missionVariableForKey:(NSString *)key +{ + NSString *result = nil; + if (key != nil) result = [mission_variables objectForKey:key]; + return result; +} + + +- (void)setMissionVariable:(NSString *)value forKey:(NSString *)key +{ + if (key != nil) + { + if (value != nil) [mission_variables setObject:value forKey:key]; + else [mission_variables removeObjectForKey:key]; + } +} + + +- (NSMutableDictionary *)localVariablesForMission:(NSString *)missionKey +{ + NSMutableDictionary *result = nil; + + if (missionKey == nil) return nil; + + result = [localVariables objectForKey:missionKey]; + if (result == nil) + { + result = [NSMutableDictionary dictionary]; + [localVariables setObject:result forKey:missionKey]; + } + + return result; +} + + +- (NSString *)localVariableForKey:(NSString *)variableName andMission:(NSString *)missionKey +{ + return [[localVariables dictionaryForKey:missionKey] objectForKey:variableName]; +} + + +- (void)setLocalVariable:(NSString *)value forKey:(NSString *)variableName andMission:(NSString *)missionKey +{ + NSMutableDictionary *locals = nil; + + if (variableName != nil && missionKey != nil) + { + locals = [self localVariablesForMission:missionKey]; + if (value != nil) + { + [locals setObject:value forKey:variableName]; + } + else + { + [locals removeObjectForKey:variableName]; + } + } +} + + +- (NSArray *) missionsList +{ + NSEnumerator *scriptEnum = nil; + NSString *scriptName = nil; + NSString *vars = nil; + NSMutableArray *result = nil; + + result = [NSMutableArray array]; + + for (scriptEnum = [worldScripts keyEnumerator]; (scriptName = [scriptEnum nextObject]); ) + { + vars = [mission_variables objectForKey:scriptName]; + + if (vars != nil) + { + [result addObject:[NSString stringWithFormat:@"\t%@", vars]]; + } + } + return result; +} + + +- (NSString*) replaceVariablesInString:(NSString*) args +{ + NSMutableDictionary *locals = [self localVariablesForMission:sCurrentMissionKey]; + NSMutableString *resultString = [NSMutableString stringWithString: args]; + NSString *valueString; + unsigned i; + NSMutableArray *tokens = ScanTokensFromString(args); + + for (i = 0; i < [tokens count]; i++) + { + valueString = [tokens objectAtIndex:i]; + + if ([mission_variables objectForKey:valueString]) + { + [resultString replaceOccurrencesOfString:valueString withString:[mission_variables objectForKey:valueString] options:NSLiteralSearch range:NSMakeRange(0, [resultString length])]; + } + else if ([locals objectForKey:valueString]) + { + [resultString replaceOccurrencesOfString:valueString withString:[locals objectForKey:valueString] options:NSLiteralSearch range:NSMakeRange(0, [resultString length])]; + } + else if (([valueString hasSuffix:@"_number"])||([valueString hasSuffix:@"_bool"])||([valueString hasSuffix:@"_string"])) + { + SEL valueselector = NSSelectorFromString(valueString); + if ([self respondsToSelector:valueselector]) + { + [resultString replaceOccurrencesOfString:valueString withString:[NSString stringWithFormat:@"%@", [self performSelector:valueselector]] options:NSLiteralSearch range:NSMakeRange(0, [resultString length])]; + } + } + else if ([valueString hasPrefix:@"["]&&[valueString hasSuffix:@"]"]) + { + NSString* replaceString = ExpandDescriptionForCurrentSystem(valueString); + [resultString replaceOccurrencesOfString:valueString withString:replaceString options:NSLiteralSearch range:NSMakeRange(0, [resultString length])]; + } + } + + OOLog(kOOLogDebugReplaceVariablesInString, @"EXPANSION: \"%@\" becomes \"%@\"", args, resultString); + + return [NSString stringWithString: resultString]; +} + +/*-----------------------------------------------------*/ + + +- (void) setMissionDescription:(NSString *)textKey +{ + NSString *text = [[UNIVERSE missiontext] stringForKey:textKey]; + if (!text) + { + OOLog(kOOLogScriptMissionDescNoText, @"***** SCRIPT ERROR: in %@, no missiontext set for key '%@' [UNIVERSE missiontext] is:\n%@ ", CurrentScriptDesc(), textKey, [UNIVERSE missiontext]); + return; + } + if (!sCurrentMissionKey) + { + OOLog(kOOLogScriptMissionDescNoKey, @"***** SCRIPT ERROR: in %@, sCurrentMissionKey not set", CurrentScriptDesc()); + return; + } + text = ExpandDescriptionForCurrentSystem(text); + text = [self replaceVariablesInString: text]; + + [mission_variables setObject:text forKey:sCurrentMissionKey]; +} + + +- (void) clearMissionDescription +{ + if (!sCurrentMissionKey) + { + OOLog(kOOLogScriptMissionDescNoText, @"***** SCRIPT ERROR: in %@, sCurrentMissionKey not set", CurrentScriptDesc()); + return; + } + if (![mission_variables objectForKey:sCurrentMissionKey]) + return; + [mission_variables removeObjectForKey:sCurrentMissionKey]; +} + + +- (void) setMissionDescription:(NSString *)textKey forMission:(NSString *)key +{ + NSString *old_sCurrentMissionKey = sCurrentMissionKey; + sCurrentMissionKey = key; + [self setMissionDescription:textKey]; + sCurrentMissionKey = old_sCurrentMissionKey; +} + + +- (void) clearMissionDescriptionForMission:(NSString *)key +{ + NSString *old_sCurrentMissionKey = sCurrentMissionKey; + sCurrentMissionKey = key; + [self clearMissionDescription]; + sCurrentMissionKey = old_sCurrentMissionKey; +} + + +- (NSString *) mission_string +{ + return sMissionStringValue; +} + + +- (NSString *) status_string +{ + return EntityStatusToString([self status]); +} + + +- (NSString *) gui_screen_string +{ + return GUIScreenIDToString(gui_screen); +} + + +- (NSNumber *) galaxy_number +{ + return [NSNumber numberWithInt:[self currentGalaxyID]]; +} + + +- (NSNumber *) planet_number +{ + return [NSNumber numberWithInt:[self currentSystemID]]; +} + + +- (NSNumber *) score_number +{ + return [NSNumber numberWithUnsignedInt:[self score]]; +} + + +- (NSNumber *) credits_number +{ + return [NSNumber numberWithDouble:[self creditBalance]]; +} + + +- (NSNumber *) scriptTimer_number +{ + return [NSNumber numberWithDouble:[self scriptTimer]]; +} + + +static int shipsFound; +- (NSNumber *) shipsFound_number +{ + return [NSNumber numberWithInt:shipsFound]; +} + + +- (NSNumber *) commanderLegalStatus_number +{ + return [NSNumber numberWithInt:[self legalStatus]]; +} + + +- (void) setLegalStatus:(NSString *)valueString +{ + legalStatus = [valueString intValue]; +} + + +- (NSString *) commanderLegalStatus_string +{ + return LegalStatusToString(legalStatus); +} + + +static int scriptRandomSeed = -1; // ensure proper random function +- (NSNumber *) d100_number +{ + if (scriptRandomSeed == -1) scriptRandomSeed = floor(1301 * ship_clock); // stop predictable sequences + ranrot_srand(scriptRandomSeed); + scriptRandomSeed = ranrot_rand(); + int d100 = ranrot_rand() % 100; + return [NSNumber numberWithInt:d100]; +} + + +- (NSNumber *) pseudoFixedD100_number +{ + return [NSNumber numberWithInt:[self systemPseudoRandom100]]; +} + + +- (NSNumber *) d256_number +{ + if (scriptRandomSeed == -1) scriptRandomSeed = floor(1301 * ship_clock); // stop predictable sequences + ranrot_srand(scriptRandomSeed); + scriptRandomSeed = ranrot_rand(); + int d256 = ranrot_rand() % 256; + return [NSNumber numberWithInt:d256]; +} + + +- (NSNumber *) pseudoFixedD256_number +{ + return [NSNumber numberWithInt:[self systemPseudoRandom256]]; +} + + +- (NSNumber *) clock_number // returns the game time in seconds +{ + return [NSNumber numberWithDouble:ship_clock]; +} + + +- (NSNumber *) clock_secs_number // returns the game time in seconds +{ + return [NSNumber numberWithUnsignedLongLong:ship_clock]; +} + + +- (NSNumber *) clock_mins_number // returns the game time in minutes +{ + return [NSNumber numberWithUnsignedLongLong:ship_clock / 60.0]; +} + + +- (NSNumber *) clock_hours_number // returns the game time in hours +{ + return [NSNumber numberWithUnsignedLongLong:ship_clock / 3600.0]; +} + + +- (NSNumber *) clock_days_number // returns the game time in days +{ + return [NSNumber numberWithUnsignedLongLong:ship_clock / 86400.0]; +} + + +- (NSNumber *) fuelLevel_number // returns the fuel level in LY +{ + return [NSNumber numberWithFloat:floor(0.1 * fuel)]; +} + + +- (NSString *) dockedAtMainStation_bool +{ + if ([self dockedAtMainStation]) return @"YES"; + else return @"NO"; +} + + +- (NSString *) foundEquipment_bool +{ + return (found_equipment)? @"YES" : @"NO"; +} + + +- (NSString *) sunWillGoNova_bool // returns whether the sun is going to go nova +{ + return ([[UNIVERSE sun] willGoNova])? @"YES" : @"NO"; +} + + +- (NSString *) sunGoneNova_bool // returns whether the sun has gone nova +{ + return ([[UNIVERSE sun] goneNova])? @"YES" : @"NO"; +} + + +- (NSString *) missionChoice_string // returns nil or the key for the chosen option +{ + return missionChoice; +} + + +- (NSNumber *) dockedTechLevel_number +{ + if (!dockedStation) + { + return [self systemTechLevel_number]; + } + return [NSNumber numberWithInt:[dockedStation equivalentTechLevel]]; +} + +- (NSString *) dockedStationName_string // returns 'NONE' if the player isn't docked, [station name] if it is, 'UNKNOWN' otherwise (?) +{ + NSString *result = nil; + if ([self status] != STATUS_DOCKED) return @"NONE"; + + result = [self dockedStationName]; + if (result == nil) result = @"UNKNOWN"; + return result; +} + + +- (NSString *) systemGovernment_string +{ + int government = [[self systemGovernment_number] intValue]; // 0 .. 7 (0 anarchic .. 7 most stable) + NSString *result = GovernmentToString(government); + if (result == nil) result = @"UNKNOWN"; + + return result; +} + + +- (NSNumber *) systemGovernment_number +{ + NSDictionary *systeminfo = [UNIVERSE generateSystemData:system_seed]; + return [systeminfo objectForKey:KEY_GOVERNMENT]; +} + + +- (NSString *) systemEconomy_string +{ + int economy = [[self systemEconomy_number] intValue]; // 0 .. 7 (0 rich industrial .. 7 poor agricultural) + NSString *result = EconomyToString(economy); + if (result == nil) result = @"UNKNOWN"; + + return result; +} + + +- (NSNumber *) systemEconomy_number +{ + NSDictionary *systeminfo = [UNIVERSE generateSystemData:system_seed]; + return [systeminfo objectForKey:KEY_ECONOMY]; +} + + +- (NSNumber *) systemTechLevel_number +{ + NSDictionary *systeminfo = [UNIVERSE generateSystemData:system_seed]; + return [systeminfo objectForKey:KEY_TECHLEVEL]; +} + + +- (NSNumber *) systemPopulation_number +{ + NSDictionary *systeminfo = [UNIVERSE generateSystemData:system_seed]; + return [systeminfo objectForKey:KEY_POPULATION]; +} + + +- (NSNumber *) systemProductivity_number +{ + NSDictionary *systeminfo = [UNIVERSE generateSystemData:system_seed]; + return [systeminfo objectForKey:KEY_PRODUCTIVITY]; +} + + +- (NSString *) commanderName_string +{ + return [self playerName]; +} + + +- (NSString *) commanderRank_string +{ + return KillCountToRatingString([self score]); +} + + +- (NSString *) commanderShip_string +{ + return [self name]; +} + + +- (NSString *) commanderShipDisplayName_string +{ + return [self displayName]; +} + +/*-----------------------------------------------------*/ + +- (void) commsMessage:(NSString *)valueString +{ + Random_Seed very_random_seed; + very_random_seed.a = rand() & 255; + very_random_seed.b = rand() & 255; + very_random_seed.c = rand() & 255; + very_random_seed.d = rand() & 255; + very_random_seed.e = rand() & 255; + very_random_seed.f = rand() & 255; + seed_RNG_only_for_planet_description(very_random_seed); + NSString* expandedMessage = ExpandDescriptionForCurrentSystem(valueString); + expandedMessage = [self replaceVariablesInString: expandedMessage]; + + [UNIVERSE addCommsMessage:expandedMessage forCount:4.5]; +} + + +// Enabled on 02-May-2008 - Nikos +// This method does the same as -commsMessage, (which in fact calls), the difference being that scripts can use this +// method to have unpiloted ship entities sending comms messages. +- (void) commsMessageByUnpiloted:(NSString *)valueString +{ + [self commsMessage:valueString]; +} + + +- (void) consoleMessage3s:(NSString *)valueString +{ + Random_Seed very_random_seed; + very_random_seed.a = rand() & 255; + very_random_seed.b = rand() & 255; + very_random_seed.c = rand() & 255; + very_random_seed.d = rand() & 255; + very_random_seed.e = rand() & 255; + very_random_seed.f = rand() & 255; + seed_RNG_only_for_planet_description(very_random_seed); + NSString* expandedMessage = ExpandDescriptionForCurrentSystem(valueString); + expandedMessage = [self replaceVariablesInString: expandedMessage]; + + [UNIVERSE addMessage: expandedMessage forCount: 3]; +} + + +- (void) consoleMessage6s:(NSString *)valueString +{ + Random_Seed very_random_seed; + very_random_seed.a = rand() & 255; + very_random_seed.b = rand() & 255; + very_random_seed.c = rand() & 255; + very_random_seed.d = rand() & 255; + very_random_seed.e = rand() & 255; + very_random_seed.f = rand() & 255; + seed_RNG_only_for_planet_description(very_random_seed); + NSString* expandedMessage = ExpandDescriptionForCurrentSystem(valueString); + expandedMessage = [self replaceVariablesInString: expandedMessage]; + + [UNIVERSE addMessage: expandedMessage forCount: 6]; +} + + +- (void) awardCredits:(NSString *)valueString +{ + if (scriptTarget != self) return; + + int award = 10 * [valueString intValue]; + if (award < 0 && credits < (unsigned)-award) credits = 0; + else credits += award; +} + + +- (void) awardShipKills:(NSString *)valueString +{ + if (scriptTarget != self) return; + + int value = [valueString intValue]; + if (0 < value) ship_kills += value; +} + + +- (void) awardEquipment:(NSString *)equipString //eg. EQ_NAVAL_ENERGY_UNIT +{ + if (scriptTarget != self) return; + + if ([equipString isEqual:@"EQ_FUEL"]) + { + [self setFuel:[self fuelCapacity]]; + return; + } + + if ([equipString hasSuffix:@"MISSILE"]||[equipString hasSuffix:@"MINE"]) + { + [self mountMissile:[[UNIVERSE newShipWithRole:equipString] autorelease]]; + return; + } + if([equipString hasPrefix:@"EQ_WEAPON"] && ![equipString hasSuffix:@"_DAMAGED"]) + { + OOLog(kOOLogSyntaxAwardEquipment, @"***** SCRIPT ERROR: in %@, CANNOT award undamaged weapon:'%@'. Damaged weapons can be awarded instead.", CurrentScriptDesc(), equipString); + return; + } + if ([equipString hasSuffix:@"_DAMAGED"] && [self hasEquipmentItem:[equipString substringToIndex:[equipString length] - [@"_DAMAGED" length]]]) + { + OOLog(kOOLogSyntaxAwardEquipment, @"***** SCRIPT ERROR: in %@, CANNOT award damaged equipment:'%@'. Undamaged version already equipped.", CurrentScriptDesc(), equipString); + } + else if (![self hasEquipmentItem:equipString]) + { + [self addEquipmentItem:equipString]; + } + +} + + +- (void) removeEquipment:(NSString *)equipString //eg. EQ_NAVAL_ENERGY_UNIT +{ + NSString* eq_type = equipString; + + if (scriptTarget != self) return; + + if ([eq_type isEqual:@"EQ_FUEL"]) + { + fuel = 0; + return; + } + + if ([self hasEquipmentItem:eq_type]) + { + [self removeEquipmentItem:eq_type]; + } + +} + + +- (void) setPlanetinfo:(NSString *)key_valueString // uses key=value format +{ + NSArray * tokens = [key_valueString componentsSeparatedByString:@"="]; + NSString* keyString = nil; + NSString* valueString = nil; + + if ([tokens count] != 2) + { + OOLog(kOOLogSyntaxSetPlanetInfo, @"***** SCRIPT ERROR: in %@, CANNOT setPlanetinfo: '%@' (bad parameter count)", CurrentScriptDesc(), key_valueString); + return; + } + + keyString = [[tokens objectAtIndex:0] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + valueString = [[tokens objectAtIndex:1] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + [UNIVERSE setSystemDataKey:keyString value:valueString]; + +} + + +- (void) setSpecificPlanetInfo:(NSString *)key_valueString // uses galaxy#=planet#=key=value +{ + NSArray * tokens = [key_valueString componentsSeparatedByString:@"="]; + NSString* keyString = nil; + NSString* valueString = nil; + int gnum, pnum; + + if ([tokens count] != 4) + { + OOLog(kOOLogSyntaxSetPlanetInfo, @"***** SCRIPT ERROR: in %@, CANNOT setSpecificPlanetInfo: '%@' (bad parameter count)", CurrentScriptDesc(), key_valueString); + return; + } + + gnum = [tokens intAtIndex:0]; + pnum = [tokens intAtIndex:1]; + keyString = [[tokens objectAtIndex:2] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + valueString = [[tokens objectAtIndex:3] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + [UNIVERSE setSystemDataForGalaxy:gnum planet:pnum key:keyString value:valueString]; +} + + +- (void) awardCargo:(NSString *)amount_typeString +{ + if (scriptTarget != self) return; + + NSArray *tokens = ScanTokensFromString(amount_typeString); + NSString *typeString = nil; + OOCargoQuantityDelta amount; + OOCargoType type; + OOMassUnit unit; + NSArray *commodityArray = nil; + + if ([tokens count] != 2) + { + OOLog(kOOLogSyntaxAwardCargo, @"***** SCRIPT ERROR: in %@, CANNOT awardCargo: '%@' (%@)", CurrentScriptDesc(), amount_typeString, @"bad parameter count"); + return; + } + + typeString = [tokens objectAtIndex:1]; + type = [UNIVERSE commodityForName:typeString]; + if (type == CARGO_UNDEFINED) type = [typeString intValue]; + + commodityArray = [UNIVERSE commodityDataForType:type]; + + if (commodityArray == nil) + { + OOLog(kOOLogSyntaxAwardCargo, @"***** SCRIPT ERROR: in %@, CANNOT awardCargo: '%@' (%@)", CurrentScriptDesc(), amount_typeString, @"unknown type"); + return; + } + + amount = [tokens intAtIndex:0]; + if (amount < 0) + { + OOLog(kOOLogSyntaxAwardCargo, @"***** SCRIPT ERROR: in %@, CANNOT awardCargo: '%@' (%@)", CurrentScriptDesc(), amount_typeString, @"negative quantity"); + return; + } + + unit = [UNIVERSE unitsForCommodity:type]; + if (specialCargo && unit == UNITS_TONS) + { + OOLog(kOOLogSyntaxAwardCargo, @"***** SCRIPT ERROR: in %@, CANNOT awardCargo: '%@' (%@)", CurrentScriptDesc(), amount_typeString, @"cargo hold full with special cargo"); + return; + } + + [self awardCargoType:type amount:amount]; +} + + +- (void) removeAllCargo +{ + [self removeAllCargo:NO]; +} + +- (void) removeAllCargo:(BOOL)forceRemoval +{ + OOCargoType type; + OOMassUnit unit; + + if (scriptTarget != self) return; + + if ([self status] != STATUS_DOCKED && !forceRemoval) + { + OOLog(kOOLogRemoveAllCargoNotDocked, @"***** SCRIPT ERROR: in %@, removeAllCargo: only works when docked.", CurrentScriptDesc()); + return; + } + + if (forceRemoval) + { + OOLog(kOOLogNoteRemoveAllCargo, @"Forcing removeAllCargo:"); + } + else + { + OOLog(kOOLogNoteRemoveAllCargo, @"Going to removeAllCargo:"); + } + + NSMutableArray *manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + for (type = 0; type < (OOCargoType)[manifest count]; type++) + { + NSMutableArray *manifest_commodity = [NSMutableArray arrayWithArray:[manifest arrayAtIndex:type]]; + unit = [manifest_commodity intAtIndex:MARKET_UNITS]; + if (unit == UNITS_TONS) + { + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:0]]; + [manifest replaceObjectAtIndex:type withObject:[NSArray arrayWithArray:manifest_commodity]]; + } + } + + if(forceRemoval && [self status] != STATUS_DOCKED) + { + int i; + for (i = [cargo count]-1; i >=0; i--) + { + ShipEntity* canister = [cargo objectAtIndex:i]; + if (!canister) break; + unit = [UNIVERSE unitsForCommodity:[canister commodityType]]; + if (unit == UNITS_TONS) + [cargo removeObjectAtIndex:i]; + } + } + + [shipCommodityData release]; + shipCommodityData = [manifest mutableCopy]; + + [specialCargo release]; + specialCargo = nil; +} + + +- (void) useSpecialCargo:(NSString *)descriptionString; +{ + if (scriptTarget != self) return; + + OOLog(kOOLogNoteUseSpecialCargo, @"Going to useSpecialCargo:'%@'", descriptionString); + + [self removeAllCargo:YES]; + specialCargo = [ExpandDescriptionForCurrentSystem(descriptionString) retain]; +} + + +- (void) testForEquipment:(NSString *)equipString //eg. EQ_NAVAL_ENERGY_UNIT +{ + found_equipment = [self hasEquipmentItem:equipString]; +} + + +- (void) awardFuel:(NSString *)valueString // add to fuel up to 7.0 LY +{ + int delta = 10 * [valueString floatValue]; + OOFuelQuantity scriptTargetFuelBeforeAward = [scriptTarget fuel]; + + if (delta < 0 && scriptTargetFuelBeforeAward < (unsigned)-delta) [scriptTarget setFuel:0]; + else + { + [scriptTarget setFuel:(scriptTargetFuelBeforeAward + delta)]; + } +} + + +- (void) messageShipAIs:(NSString *)roles_message +{ + NSMutableArray* tokens = ScanTokensFromString(roles_message); + NSString* roleString = nil; + NSString* messageString = nil; + + if ([tokens count] < 2) + { + OOLog(kOOLogSyntaxMessageShipAIs, @"***** SCRIPT ERROR: in %@, CANNOT messageShipAIs: '%@' (bad parameter count)", CurrentScriptDesc(), roles_message); + return; + } + + roleString = [tokens objectAtIndex:0]; + [tokens removeObjectAtIndex:0]; + messageString = [tokens componentsJoinedByString:@" "]; + + [UNIVERSE sendShipsWithPrimaryRole:roleString messageToAI:messageString]; +} + + +- (void) ejectItem:(NSString *)itemKey +{ + if (scriptTarget == nil) scriptTarget = self; + [scriptTarget ejectShipOfType:itemKey]; +} + + +- (void) addShips:(NSString *)roles_number +{ + NSMutableArray* tokens = ScanTokensFromString(roles_number); + NSString* roleString = nil; + NSString* numberString = nil; + + if ([tokens count] != 2) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@, CANNOT addShips: '%@' (expected )", CurrentScriptDesc(), roles_number); + return; + } + + roleString = [tokens objectAtIndex:0]; + numberString = [tokens objectAtIndex:1]; + + int number = [numberString intValue]; + if (number < 0) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@, can't add %i ships -- that's less than zero, y'know..", CurrentScriptDesc(), number); + return; + } + + OOLog(kOOLogNoteAddShips, @"DEBUG: Going to add %d ships with role '%@'", number, roleString); + + while (number--) + [UNIVERSE witchspaceShipWithPrimaryRole:roleString]; +} + + +- (void) addSystemShips:(NSString *)roles_number_position +{ + NSMutableArray* tokens = ScanTokensFromString(roles_number_position); + NSString* roleString = nil; + NSString* numberString = nil; + NSString* positionString = nil; + + if ([tokens count] != 3) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@, CANNOT addSystemShips: '%@' (expected )", CurrentScriptDesc(), roles_number_position); + return; + } + + roleString = [tokens objectAtIndex:0]; + numberString = [tokens objectAtIndex:1]; + positionString = [tokens objectAtIndex:2]; + + int number = [numberString intValue]; + double posn = [positionString doubleValue]; + if (number < 0) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@, can't add %i ships -- that's less than zero, y'know..", CurrentScriptDesc(), number); + return; + } + + OOLog(kOOLogNoteAddShips, @"DEBUG: Going to add %d ships with role '%@' at a point %.3f along route1", number, roleString, posn); + + while (number--) + [UNIVERSE addShipWithRole:roleString nearRouteOneAt:posn]; +} + + +- (void) addShipsAt:(NSString *)roles_number_system_x_y_z +{ + NSMutableArray* tokens = ScanTokensFromString(roles_number_system_x_y_z); + + NSString* roleString = nil; + NSString* numberString = nil; + NSString* systemString = nil; + NSString* xString = nil; + NSString* yString = nil; + NSString* zString = nil; + + if ([tokens count] != 6) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@, CANNOT addShipsAt: '%@' (expected )", CurrentScriptDesc(), roles_number_system_x_y_z); + return; + } + + roleString = [tokens objectAtIndex:0]; + numberString = [tokens objectAtIndex:1]; + systemString = [tokens objectAtIndex:2]; + xString = [tokens objectAtIndex:3]; + yString = [tokens objectAtIndex:4]; + zString = [tokens objectAtIndex:5]; + + Vector posn = make_vector( [xString floatValue], [yString floatValue], [zString floatValue]); + + int number = [numberString intValue]; + if (number < 1) + { + OOLog(kOOLogSyntaxAddShips, @"----- WARNING in %@ Tried to add %i ships -- no ship added.", CurrentScriptDesc(), number); + return; + } + + OOLog(kOOLogNoteAddShips, @"DEBUG: Going to add %d ship(s) with role '%@' at point (%.3f, %.3f, %.3f) using system %@", number, roleString, posn.x, posn.y, posn.z, systemString); + + if (![UNIVERSE addShips: number withRole:roleString nearPosition: posn withCoordinateSystem: systemString]) + { + OOLog(kOOLogScriptAddShipsFailed, @"***** SCRIPT ERROR: in %@, %@ could not add %u ships with role \"%@\"", CurrentScriptDesc(), @"addShipsAt:", number, roleString); + } +} + + +- (void) addShipsAtPrecisely:(NSString *)roles_number_system_x_y_z +{ + NSMutableArray* tokens = ScanTokensFromString(roles_number_system_x_y_z); + + NSString* roleString = nil; + NSString* numberString = nil; + NSString* systemString = nil; + NSString* xString = nil; + NSString* yString = nil; + NSString* zString = nil; + + if ([tokens count] != 6) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@,* CANNOT addShipsAtPrecisely: '%@' (expected )", CurrentScriptDesc(), roles_number_system_x_y_z); + return; + } + + roleString = [tokens objectAtIndex:0]; + numberString = [tokens objectAtIndex:1]; + systemString = [tokens objectAtIndex:2]; + xString = [tokens objectAtIndex:3]; + yString = [tokens objectAtIndex:4]; + zString = [tokens objectAtIndex:5]; + + Vector posn = make_vector( [xString floatValue], [yString floatValue], [zString floatValue]); + + int number = [numberString intValue]; + if (number < 1) + { + OOLog(kOOLogSyntaxAddShips, @"----- WARNING: in %@, Can't add %i ships -- no ship added.", CurrentScriptDesc(), number); + return; + } + + OOLog(kOOLogNoteAddShips, @"DEBUG: Going to add %d ship(s) with role '%@' precisely at point (%.3f, %.3f, %.3f) using system %@", number, roleString, posn.x, posn.y, posn.z, systemString); + + if (![UNIVERSE addShips: number withRole:roleString atPosition: posn withCoordinateSystem: systemString]) + { + OOLog(kOOLogScriptAddShipsFailed, @"***** SCRIPT ERROR: in %@, %@ could not add %u ships with role '%@'", CurrentScriptDesc(), @"addShipsAtPrecisely:", number, roleString); + } +} + + +- (void) addShipsWithinRadius:(NSString *)roles_number_system_x_y_z_r +{ + NSMutableArray* tokens = ScanTokensFromString(roles_number_system_x_y_z_r); + + if ([tokens count] != 7) + { + OOLog(kOOLogSyntaxAddShips, @"***** SCRIPT ERROR: in %@, CANNOT 'addShipsWithinRadius: %@' (expected ))", CurrentScriptDesc(), roles_number_system_x_y_z_r); + return; + } + + NSString* roleString = [tokens objectAtIndex:0]; + int number = [[tokens objectAtIndex:1] intValue]; + NSString* systemString = [tokens objectAtIndex:2]; + GLfloat x = [[tokens objectAtIndex:3] floatValue]; + GLfloat y = [[tokens objectAtIndex:4] floatValue]; + GLfloat z = [[tokens objectAtIndex:5] floatValue]; + GLfloat r = [[tokens objectAtIndex:6] floatValue]; + Vector posn = make_vector( x, y, z); + + if (number < 1) + { + OOLog(kOOLogSyntaxAddShips, @"----- WARNING: in %@, can't add %i ships -- no ship added.", CurrentScriptDesc(), number); + return; + } + + OOLog(kOOLogNoteAddShips, @"DEBUG: Going to add %d ship(s) with role '%@' within %.2f radius about point (%.3f, %.3f, %.3f) using system %@", number, roleString, r, x, y, z, systemString); + + if (![UNIVERSE addShips:number withRole: roleString nearPosition: posn withCoordinateSystem: systemString withinRadius: r]) + { + OOLog(kOOLogScriptAddShipsFailed, @"***** SCRIPT ERROR :in %@, %@ could not add %u ships with role \"%@\"", CurrentScriptDesc(), @"addShipsWithinRadius:", number, roleString); + } +} + + +- (void) spawnShip:(NSString *)ship_key +{ + if ([UNIVERSE spawnShip:ship_key]) + { + OOLog(kOOLogNoteAddShips, @"DEBUG: Spawned ship with shipdata key '%@'.", ship_key); + } + else + { + OOLog(kOOLogScriptAddShipsFailed, @"***** SCRIPT ERROR: in %@, could not spawn ship with shipdata key '%@'.", CurrentScriptDesc(), ship_key); + } +} + + +- (void) set:(NSString *)missionvariable_value +{ + NSMutableArray *tokens = ScanTokensFromString(missionvariable_value); + NSString *missionVariableString = nil; + NSString *valueString = nil; + BOOL hasMissionPrefix, hasLocalPrefix; + + if ([tokens count] < 2) + { + OOLog(kOOLogSyntaxSet, @"***** SCRIPT ERROR: in %@, CANNOT SET '%@' (expected mission_variable or local_variable followed by value expression)", CurrentScriptDesc(), missionvariable_value); + return; + } + + missionVariableString = [tokens objectAtIndex:0]; + [tokens removeObjectAtIndex:0]; + valueString = [tokens componentsJoinedByString:@" "]; + + hasMissionPrefix = [missionVariableString hasPrefix:@"mission_"]; + hasLocalPrefix = [missionVariableString hasPrefix:@"local_"]; + + if (!hasMissionPrefix && !hasLocalPrefix) + { + OOLog(kOOLogSyntaxSet, @"***** SCRIPT ERROR: in %@, IDENTIFIER '%@' DOES NOT BEGIN WITH 'mission_' or 'local_'", CurrentScriptDesc(), missionVariableString); + return; + } + + OOLog(kOOLogNoteSet, @"DEBUG: script %@ is set to %@", missionVariableString, valueString); + + if (hasMissionPrefix) + { + [self setMissionVariable:valueString forKey:missionVariableString]; + } + else + { + [self setLocalVariable:valueString forKey:missionVariableString andMission:sCurrentMissionKey]; + } +} + + +- (void) reset:(NSString *)missionvariable +{ + NSString* missionVariableString = [missionvariable stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + BOOL hasMissionPrefix, hasLocalPrefix; + + hasMissionPrefix = [missionVariableString hasPrefix:@"mission_"]; + hasLocalPrefix = [missionVariableString hasPrefix:@"local_"]; + + if (hasMissionPrefix) + { + [self setMissionVariable:nil forKey:missionVariableString]; + } + else if (hasLocalPrefix) + { + [self setLocalVariable:nil forKey:missionVariableString andMission:sCurrentMissionKey]; + } + else + { + OOLog(kOOLogSyntaxReset, @"***** SCRIPT ERROR: in %@, IDENTIFIER '%@' DOES NOT BEGIN WITH 'mission_' or 'local_'", CurrentScriptDesc(), missionVariableString); + } +} + + +- (void) increment:(NSString *)missionVariableString +{ + BOOL hasMissionPrefix, hasLocalPrefix; + int value = 0; + + hasMissionPrefix = [missionVariableString hasPrefix:@"mission_"]; + hasLocalPrefix = [missionVariableString hasPrefix:@"local_"]; + + if (hasMissionPrefix) + { + value = [[self missionVariableForKey:missionVariableString] intValue]; + value++; + [self setMissionVariable:[NSString stringWithFormat:@"%d", value] forKey:missionVariableString]; + } + else if (hasLocalPrefix) + { + value = [[self localVariableForKey:missionVariableString andMission:sCurrentMissionKey] intValue]; + value++; + [self setLocalVariable:[NSString stringWithFormat:@"%d", value] forKey:missionVariableString andMission:sCurrentMissionKey]; + } + else + { + OOLog(kOOLogSyntaxIncrement, @"***** SCRIPT ERROR: in %@, IDENTIFIER '%@' DOES NOT BEGIN WITH 'mission_' or 'local_'", CurrentScriptDesc(), missionVariableString); + } +} + + +- (void) decrement:(NSString *)missionVariableString +{ + BOOL hasMissionPrefix, hasLocalPrefix; + int value = 0; + + hasMissionPrefix = [missionVariableString hasPrefix:@"mission_"]; + hasLocalPrefix = [missionVariableString hasPrefix:@"local_"]; + + if (hasMissionPrefix) + { + value = [[self missionVariableForKey:missionVariableString] intValue]; + value--; + [self setMissionVariable:[NSString stringWithFormat:@"%d", value] forKey:missionVariableString]; + } + else if (hasLocalPrefix) + { + value = [[self localVariableForKey:missionVariableString andMission:sCurrentMissionKey] intValue]; + value--; + [self setLocalVariable:[NSString stringWithFormat:@"%d", value] forKey:missionVariableString andMission:sCurrentMissionKey]; + } + else + { + OOLog(kOOLogSyntaxDecrement, @"***** SCRIPT ERROR: in %@, IDENTIFIER '%@' DOES NOT BEGIN WITH 'mission_' or 'local_'", CurrentScriptDesc(), missionVariableString); + } +} + + +- (void) add:(NSString *)missionVariableString_value +{ + NSString* missionVariableString = nil; + NSString* valueString; + double value; + NSMutableArray* tokens = ScanTokensFromString(missionVariableString_value); + BOOL hasMissionPrefix, hasLocalPrefix; + + if ([tokens count] < 2) + { + OOLog(kOOLogSyntaxAdd, @"***** SCRIPT ERROR: in %@, CANNOT ADD: '%@'", CurrentScriptDesc(), missionVariableString_value); + return; + } + + missionVariableString = [tokens objectAtIndex:0]; + [tokens removeObjectAtIndex:0]; + valueString = [tokens componentsJoinedByString:@" "]; + + hasMissionPrefix = [missionVariableString hasPrefix:@"mission_"]; + hasLocalPrefix = [missionVariableString hasPrefix:@"local_"]; + + if (hasMissionPrefix) + { + value = [[self missionVariableForKey:missionVariableString] doubleValue]; + value += [valueString doubleValue]; + [self setMissionVariable:[NSString stringWithFormat:@"%f", value] forKey:missionVariableString]; + } + else if (hasLocalPrefix) + { + value = [[self localVariableForKey:missionVariableString andMission:sCurrentMissionKey] doubleValue]; + value += [valueString doubleValue]; + [self setLocalVariable:[NSString stringWithFormat:@"%f", value] forKey:missionVariableString andMission:sCurrentMissionKey]; + } + else + { + OOLog(kOOLogSyntaxAdd, @"***** SCRIPT ERROR: in %@, CANNOT ADD: '%@' -- IDENTIFIER '%@' DOES NOT BEGIN WITH 'mission_' or 'local_'", CurrentScriptDesc(), missionVariableString_value); + } +} + + +- (void) subtract:(NSString *)missionVariableString_value +{ + NSString* missionVariableString = nil; + NSString* valueString; + double value; + NSMutableArray* tokens = ScanTokensFromString(missionVariableString_value); + BOOL hasMissionPrefix, hasLocalPrefix; + + if ([tokens count] < 2) + { + OOLog(@"script.debug.syntax.subtract", @"***** SCRIPT ERROR: in %@, CANNOT SUBTRACT: '%@'", CurrentScriptDesc(), missionVariableString_value); + return; + } + + missionVariableString = [tokens objectAtIndex:0]; + [tokens removeObjectAtIndex:0]; + valueString = [tokens componentsJoinedByString:@" "]; + + hasMissionPrefix = [missionVariableString hasPrefix:@"mission_"]; + hasLocalPrefix = [missionVariableString hasPrefix:@"local_"]; + + if (hasMissionPrefix) + { + value = [[self missionVariableForKey:missionVariableString] doubleValue]; + value -= [valueString doubleValue]; + [self setMissionVariable:[NSString stringWithFormat:@"%f", value] forKey:missionVariableString]; + } + else if (hasLocalPrefix) + { + value = [[self localVariableForKey:missionVariableString andMission:sCurrentMissionKey] doubleValue]; + value -= [valueString doubleValue]; + [self setLocalVariable:[NSString stringWithFormat:@"%f", value] forKey:missionVariableString andMission:sCurrentMissionKey]; + } + else + { + OOLog(kOOLogSyntaxAdd, @"***** SCRIPT ERROR: in %@, CANNOT ADD: '%@' -- IDENTIFIER '%@' DOES NOT BEGIN WITH 'mission_' or 'local_'", CurrentScriptDesc(), missionVariableString_value); + } +} + + +- (void) checkForShips:(NSString *)roleString +{ + shipsFound = [UNIVERSE countShipsWithPrimaryRole:roleString]; +} + + +- (void) resetScriptTimer +{ + script_time = 0.0; + script_time_check = SCRIPT_TIMER_INTERVAL; + script_time_interval = SCRIPT_TIMER_INTERVAL; +} + + +- (void) addMissionText: (NSString *)textKey +{ + NSString *text = nil; + NSArray *paras = nil; + + if ([textKey isEqual:lastTextKey]) return; // don't repeatedly add the same text + [lastTextKey release]; + lastTextKey = [textKey copy]; + + // Replace literal \n in strings with line breaks and perform expansions. + text = [[UNIVERSE missiontext] stringForKey:textKey]; + if (text == nil) return; + text = ExpandDescriptionForCurrentSystem(text); + paras = [text componentsSeparatedByString:@"\\n"]; + text = [paras componentsJoinedByString:@"\n"]; + text = [self replaceVariablesInString:text]; + + [self addLiteralMissionText:text]; +} + + +- (void) addLiteralMissionText:(NSString *)text +{ + GuiDisplayGen *gui = [UNIVERSE gui]; + NSArray *paras = [text componentsSeparatedByString:@"\n"]; + unsigned i, count; + + if (text != nil) + { + count = [paras count]; + for (i = 0; i < count; i++) + { + missionTextRow = [gui addLongText:[paras objectAtIndex:i] startingAtRow:missionTextRow align:GUI_ALIGN_LEFT]; + } + } +} + + +- (void) setMissionChoices:(NSString *)choicesKey // choicesKey is a key for a dictionary of +{ // choices/choice phrases in missiontext.plist and also.. + GuiDisplayGen* gui = [UNIVERSE gui]; + // TODO: MORE STUFF HERE + // must find list of choices in missiontext.plist + // add them to gui setting the key for each line to the key in the dict of choices + // and the text of the line to the value in the dict of choices + // and also set the selectable range + // ++ change the mission screen's response to wait for a choice + // and only if the selectable range is not present ask: + // Press Space Commander... + // + NSDictionary *choices_dict = [[UNIVERSE missiontext] dictionaryForKey:choicesKey]; + if ([choices_dict count] == 0) + { + return; + } + + NSArray *choice_keys = [[choices_dict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + + [gui setText:@"" forRow:21]; // clears out the 'Press spacebar' message + [gui setKey:@"" forRow:21]; // clears the key to enable pollDemoControls to check for a selection + [gui setSelectableRange:NSMakeRange(0,0)]; // clears the selectable range + + int choices_row = 22 - [choice_keys count]; + NSEnumerator *choiceEnum = nil; + NSString *choiceKey = nil; + NSString *choiceText = nil; + + for (choiceEnum = [choice_keys objectEnumerator]; (choiceKey = [choiceEnum nextObject]); ) + { + choiceText = [NSString stringWithFormat:@" %@ ",[choices_dict objectForKey:choiceKey]]; + choiceText = ExpandDescriptionForCurrentSystem(choiceText); + choiceText = [self replaceVariablesInString:choiceText]; + [gui setText:choiceText forRow:choices_row align: GUI_ALIGN_CENTER]; + [gui setKey:choiceKey forRow:choices_row]; + [gui setColor:[OOColor yellowColor] forRow:choices_row]; + choices_row++; + } + + [gui setSelectableRange:NSMakeRange(22 - [choice_keys count], [choice_keys count])]; + [gui setSelectedRow: 22 - [choice_keys count]]; + + [self resetMissionChoice]; +} + + +- (void) resetMissionChoice +{ + [self setMissionChoice:nil]; +} + + +- (void) clearMissionScreen +{ + [self setMissionImage:nil]; + [self setMissionMusic:nil]; + [self showShipModel:nil]; +} + + +- (void) addMissionDestination:(NSString *)destinations +{ + unsigned i, j; + int pnum, dest; + NSMutableArray* tokens = ScanTokensFromString(destinations); + BOOL addDestination; + + for (j = 0; j < [tokens count]; j++) + { + dest = [tokens intAtIndex:j]; + if (dest < 0 || dest > 255) + continue; + + addDestination = YES; + for (i = 0; i < [missionDestinations count]; i++) + { + pnum = [missionDestinations intAtIndex:i]; + if (pnum == dest) + { + addDestination = NO; + break; + } + } + + if (addDestination == YES) + [missionDestinations addObject:[NSNumber numberWithUnsignedInt:dest]]; + } +} + + +- (void) removeMissionDestination:(NSString *)destinations +{ + unsigned i, j; + int pnum, dest; + NSMutableArray *tokens = ScanTokensFromString(destinations); + BOOL removeDestination; + + for (j = 0; j < [tokens count]; j++) + { + dest = [[tokens objectAtIndex:j] intValue]; + if (dest < 0 || dest > 255) continue; + + removeDestination = NO; + for (i = 0; i < [missionDestinations count]; i++) + { + pnum = [missionDestinations intAtIndex:i]; + if (pnum == dest) + { + removeDestination = YES; + break; + } + } + + if (removeDestination == YES) + { + [missionDestinations removeObjectAtIndex:i]; + } + } +} + + +- (void) showShipModel:(NSString *)shipKey +{ + ShipEntity *ship; + + if (!dockedStation) return; + + [UNIVERSE removeDemoShips]; // get rid of any pre-existing models on display + if ([shipKey isEqualToString:@"none"] || [shipKey length] == 0) return; + + [[PlayerEntity sharedPlayer] setShowDemoShips: YES]; + + Quaternion q2 = { (GLfloat)0.707, (GLfloat)0.707, (GLfloat)0.0, (GLfloat)0.0}; + + ship = [UNIVERSE newShipWithRole: shipKey]; // retain count = 1 + if (ship) + { + double cr = ship->collision_radius; + OOLog(kOOLogNoteShowShipModel, @"::::: showShipModel:'%@' (%@) (%@)", shipKey, ship, [ship name]); + [ship setOrientation: q2]; + [ship setPositionX:0.0f y:0.0f z:3.6f * cr]; + [ship setScanClass:CLASS_NO_DRAW]; + [ship setRoll:M_PI/5.0]; + [ship setPitch:M_PI/10.0]; + [[ship getAI] setStateMachine:@"nullAI.plist"]; + [ship setPendingEscortCount:0]; + [UNIVERSE addEntity:ship]; + [ship setStatus:STATUS_COCKPIT_DISPLAY]; + + [ship release]; + } +} + + +- (void) setMissionMusic:(NSString *)value +{ + if ([value length] == 0 || [[value lowercaseString] isEqual:@"none"]) + { + value = nil; + } + [[OOMusicController sharedController] setMissionMusic:value]; +} + + +- (void) setMissionImage:(NSString *)value +{ + [missionBackgroundTexture release]; + missionBackgroundTexture = nil; + + if ([value length] != 0 && ![[value lowercaseString] isEqual:@"none"]) + { + missionBackgroundTexture = [OOTexture textureWithName:value inFolder:@"Images"]; + [missionBackgroundTexture retain]; + } +} + + +- (void) setFuelLeak:(NSString *)value +{ + if (scriptTarget != self) + { + [scriptTarget setFuel:0]; + return; + } + + fuel_leak_rate = [value doubleValue]; + if (fuel_leak_rate > 0) + { + [self playFuelLeak]; + [UNIVERSE addMessage:DESC(@"danger-fuel-leak") forCount:6]; + OOLog(kOOLogNoteFuelLeak, @"FUEL LEAK activated!"); + } +} + + +- (NSNumber *) fuelLeakRate_number +{ + return [NSNumber numberWithFloat:[self fuelLeakRate]]; +} + + +- (void) setSunNovaIn:(NSString *)time_value +{ + double time_until_nova = [time_value doubleValue]; + [[UNIVERSE sun] setGoingNova:YES inTime: time_until_nova]; +} + + +- (void) launchFromStation +{ + [self leaveDock:dockedStation]; +} + + +- (void) blowUpStation +{ + StationEntity *mainStation = nil; + + mainStation = [UNIVERSE station]; + if (mainStation != nil) + { + [UNIVERSE unMagicMainStation]; + [mainStation takeEnergyDamage:500000000.0 from:nil becauseOf:nil]; // 500 million should do it! + } +} + + +- (void) sendAllShipsAway +{ + if (!UNIVERSE) + return; + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + Entity* my_entities[ent_count]; + int i; + for (i = 0; i < ent_count; i++) + my_entities[i] = [uni_entities[i] retain]; // retained + + for (i = 1; i < ent_count; i++) + { + Entity* e1 = my_entities[i]; + if (e1->isShip) + { + ShipEntity* se1 = (ShipEntity*)e1; + int e_class = e1->scanClass; + if ((e_class == CLASS_NEUTRAL)||(e_class == CLASS_POLICE)||(e_class == CLASS_MILITARY)||(e_class == CLASS_THARGOID)) + { + AI* se1AI = [se1 getAI]; + [se1 setFuel:MAX(PLAYER_MAX_FUEL, [se1 fuelCapacity])]; + [se1AI setStateMachine:@"exitingTraderAI.plist"]; + [se1AI setState:@"EXIT_SYSTEM"]; + // FIXME: I don't think the following line does anything meaningful. -- Ahruman + [se1AI reactToMessage:[NSString stringWithFormat:@"pauseAI: %d", 3 + (ranrot_rand() & 15)]]; + [se1 setPrimaryRole:@"none"]; // prevents new ship from appearing at witchpoint when this one leaves! + } + } + } + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // released +} + + +- (void) addPlanet: (NSString *)planetKey +{ + OOLog(kOOLogNoteAddPlanet, @"addPlanet: %@", planetKey); + + if (!UNIVERSE) + return; + NSDictionary* dict = [[UNIVERSE planetInfo] dictionaryForKey:planetKey]; + if (!dict) + { + OOLog(@"script.error.addPlanet.keyNotFound", @"***** ERROR: could not find an entry in planetinfo.plist for '%@'", planetKey); + return; + } + + /*- add planet -*/ + OOLog(kOOLogDebugAddPlanet, @"DEBUG: initPlanetFromDictionary: %@", dict); + PlanetEntity* planet = [[[PlanetEntity alloc] initPlanetFromDictionary:dict] autorelease]; + [planet setStatus:STATUS_ACTIVE]; + + Quaternion planetOrientation; + if (ScanQuaternionFromString([dict objectForKey:@"orientation"], &planetOrientation)) + { + [planet setOrientation:planetOrientation]; + } + + if (![dict objectForKey:@"position"]) + { + OOLog(@"script.error.addPlanet.noPosition", @"***** ERROR: you must specify a position for scripted planet '%@' before it can be created", planetKey); + return; + } + + NSString *positionString = [dict objectForKey:@"position"]; + if([positionString hasPrefix:@"abs "] && ([UNIVERSE planet] != nil || [UNIVERSE sun] !=nil)) + { + OOLogWARN(@"script.deprecated", @"setting %@ for %@ '%@' in 'abs' inside .plists can cause compatibility issues across Oolite versions. Use coordinates relative to main system objects instead.",@"position",@"planet",planetKey); + } + + Vector posn = [UNIVERSE coordinatesFromCoordinateSystemString:positionString]; + if (posn.x || posn.y || posn.z) + { + OOLog(kOOLogDebugAddPlanet, @"planet position (%.2f %.2f %.2f) derived from %@", posn.x, posn.y, posn.z, positionString); + } + else + { + ScanVectorFromString(positionString, &posn); + OOLog(kOOLogDebugAddPlanet, @"planet position (%.2f %.2f %.2f) derived from %@", posn.x, posn.y, posn.z, positionString); + } + [planet setPosition: posn]; + + [UNIVERSE addEntity:planet]; +} + + +- (void) addMoon: (NSString *)moonKey +{ + OOLog(kOOLogNoteAddPlanet, @"DEBUG: addMoon '%@'", moonKey); + + if (!UNIVERSE) + return; + NSDictionary* dict = [[UNIVERSE planetInfo] dictionaryForKey:moonKey]; + if (!dict) + { + OOLog(@"script.error.addPlanet.keyNotFound", @"***** ERROR: could not find an entry in planetinfo.plist for '%@'", moonKey); + return; + } + + OOLog(kOOLogDebugAddPlanet, @"DEBUG: initMoonFromDictionary: %@", dict); + PlanetEntity* planet = [[[PlanetEntity alloc] initMoonFromDictionary:dict] autorelease]; + [planet setStatus:STATUS_ACTIVE]; + + Quaternion planetOrientation; + if (ScanQuaternionFromString([dict objectForKey:@"orientation"], &planetOrientation)) + { + [planet setOrientation:planetOrientation]; + } + + if (![dict objectForKey:@"position"]) + { + OOLog(@"script.error.addPlanet.noPosition", @"***** ERROR: you must specify a position for scripted moon '%@' before it can be created", moonKey); + return; + } + + NSString *positionString = [dict objectForKey:@"position"]; + if([positionString hasPrefix:@"abs "] && ([UNIVERSE planet] != nil || [UNIVERSE sun] !=nil)) + { + OOLogWARN(@"script.deprecated", @"setting %@ for %@ '%@' in 'abs' inside .plists can cause compatibility issues across Oolite versions. Use coordinates relative to main system objects instead.",@"position",@"moon",moonKey); + } + Vector posn = [UNIVERSE coordinatesFromCoordinateSystemString:positionString]; + if (posn.x || posn.y || posn.z) + { + OOLog(kOOLogDebugAddPlanet, @"moon position (%.2f %.2f %.2f) derived from %@", posn.x, posn.y, posn.z, positionString); + } + else + { + ScanVectorFromString(positionString, &posn); + OOLog(kOOLogDebugAddPlanet, @"moon position (%.2f %.2f %.2f) derived from %@", posn.x, posn.y, posn.z, positionString); + } + [planet setPosition: posn]; + + [UNIVERSE addEntity:planet]; +} + + +- (void) debugOn +{ + OOLogSetDisplayMessagesInClass(kOOLogDebugOnMetaClass, YES); + OOLog(kOOLogDebugOnOff, @"SCRIPT debug messages ON"); +} + + +- (void) debugOff +{ + OOLog(kOOLogDebugOnOff, @"SCRIPT debug messages OFF"); + OOLogSetDisplayMessagesInClass(kOOLogDebugOnMetaClass, NO); +} + + +- (void) debugMessage:(NSString *)args +{ + OOLog(kOOLogDebugMessage, @"SCRIPT debugMessage: %@", args); +} + + +- (void) playSound:(NSString *) soundName +{ + [self playLegacyScriptSound:soundName]; +} + +/*-----------------------------------------------------*/ + + +- (void) setGuiToMissionScreen +{ + GuiDisplayGen* gui = [UNIVERSE gui]; + + // GUI stuff + { + [gui clear]; + [gui setTitle:DESC(@"mission-information")]; + // + [gui setText:DESC(@"press-space-commander") forRow:21 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor yellowColor] forRow:21]; + [gui setKey:@"spacebar" forRow:21]; + // + [gui setSelectableRange:NSMakeRange(0,0)]; + [gui setBackgroundTexture:missionBackgroundTexture]; + + [gui setShowTextCursor:NO]; + } + /* ends */ + + missionTextRow = 1; + + if (gui) + gui_screen = GUI_SCREEN_MISSION; + + if (lastTextKey) + { + [lastTextKey release]; + lastTextKey = nil; + } + + [[OOMusicController sharedController] playMissionMusic]; + + // the following are necessary... + [UNIVERSE setDisplayText:YES]; + [UNIVERSE setViewDirection:VIEW_GUI_DISPLAY]; +} + + +- (void) setBackgroundFromDescriptionsKey:(NSString*) d_key +{ + NSArray * items = (NSArray *)[[UNIVERSE descriptions] objectForKey:d_key]; + // + if (!items) + return; + // + [self addScene: items atOffset: kZeroVector]; + // + [self setShowDemoShips: YES]; +} + + +- (void) addScene:(NSArray *)items atOffset:(Vector)off +{ + unsigned i; + + if (items == nil) return; + + for (i = 0; i < [items count]; i++) + { + id item = [items objectAtIndex:i]; + if ([item isKindOfClass:[NSString class]]) + { + [self processSceneString:item atOffset: off]; + } + else if ([item isKindOfClass:[NSArray class]]) + { + [self addScene:item atOffset: off]; + } + else if ([item isKindOfClass:[NSDictionary class]]) + { + [self processSceneDictionary:item atOffset: off]; + } + } +} + + +- (BOOL) processSceneDictionary:(NSDictionary *) couplet atOffset:(Vector) off +{ + NSArray *conditions = [couplet objectForKey:@"conditions"]; + NSArray *actions = nil; + if ([couplet objectForKey:@"do"]) + actions = [NSArray arrayWithObject: [couplet objectForKey:@"do"]]; + NSArray *else_actions = nil; + if ([couplet objectForKey:@"else"]) + else_actions = [NSArray arrayWithObject: [couplet objectForKey:@"else"]]; + BOOL success = YES; + if (conditions == nil) + { + OOLog(@"script.scene.couplet.badConditions", @"***** SCENE ERROR: no 'conditions' in %@ - returning YES and performing 'do' actions.", [couplet description]); + } + else + { + if (![conditions isKindOfClass:[NSArray class]]) + { + OOLog(@"script.scene.couplet.badConditions", @"***** SCENE ERROR: \"conditions = %@\" is not an array - returning NO.", [conditions description]); + return NO; + } + } + + // check conditions.. + success = TestScriptConditions(OOSanitizeLegacyScriptConditions(conditions, @"")); + + // perform successful actions... + if ((success) && (actions) && [actions count]) + [self addScene: actions atOffset: off]; + + // perform unsuccessful actions + if ((!success) && (else_actions) && [else_actions count]) + [self addScene: else_actions atOffset: off]; + + return success; +} + + +- (BOOL) processSceneString:(NSString*) item atOffset:(Vector) off +{ + Vector model_p0; + Quaternion model_q; + + if (!item) + return NO; + NSArray * i_info = ScanTokensFromString(item); + if (!i_info) + return NO; + NSString* i_key = [(NSString*)[i_info objectAtIndex:0] lowercaseString]; + + OOLog(kOOLogNoteProcessSceneString, @"..... processing %@ (%@)", i_info, i_key); + + // + // recursively add further scenes: + // + if ([i_key isEqual:@"scene"]) + { + if ([i_info count] != 5) // must be scene_key_x_y_z + return NO; // 0.... 1.. 2 3 4 + NSString* scene_key = (NSString*)[i_info objectAtIndex: 1]; + Vector scene_offset = {0}; + ScanVectorFromString([[i_info subarrayWithRange:NSMakeRange(2, 3)] componentsJoinedByString:@" "], &scene_offset); + scene_offset.x += off.x; scene_offset.y += off.y; scene_offset.z += off.z; + NSArray * scene_items = (NSArray *)[[UNIVERSE descriptions] objectForKey:scene_key]; + OOLog(kOOLogDebugProcessSceneStringAddScene, @"::::: adding scene: '%@'", scene_key); + // + if (scene_items) + { + [self addScene: scene_items atOffset: scene_offset]; + return YES; + } + else + return NO; + } + // + // Add ship models: + // + if ([i_key isEqual:@"ship"]||[i_key isEqual:@"model"]||[i_key isEqual:@"role"]) + { + if ([i_info count] != 10) // must be item_name_x_y_z_W_X_Y_Z_align + { + return NO; // 0... 1... 2 3 4 5 6 7 8 9.... + } + + ShipEntity* ship = nil; + + if ([i_key isEqual:@"ship"]||[i_key isEqual:@"model"]) + { + ship = [UNIVERSE newShipWithName:[i_info stringAtIndex: 1]]; + } + else if ([i_key isEqual:@"role"]) + { + ship = [UNIVERSE newShipWithRole:[i_info stringAtIndex: 1]]; + } + if (!ship) + return NO; + + ScanVectorAndQuaternionFromString([[i_info subarrayWithRange:NSMakeRange(2, 7)] componentsJoinedByString:@" "], &model_p0, &model_q); + + Vector model_offset = positionOffsetForShipInRotationToAlignment(ship, model_q, [i_info stringAtIndex:9]); + model_p0 = vector_add(model_p0, vector_subtract(off, model_offset)); + + OOLog(kOOLogDebugProcessSceneStringAddModel, @"::::: adding model to scene:'%@'", ship); + [ship setOrientation: model_q]; + [ship setPosition: model_p0]; + [ship setStatus: STATUS_COCKPIT_DISPLAY]; + [ship setScanClass: CLASS_NO_DRAW]; + [UNIVERSE addEntity: ship]; + [[ship getAI] setStateMachine: @"nullAI.plist"]; + [ship setRoll: 0.0]; + [ship setPitch: 0.0]; + [ship setVelocity: kZeroVector]; + [ship setBehaviour: BEHAVIOUR_STOP_STILL]; + + [ship release]; + return YES; + } + // + // Add player ship model: + // + if ([i_key isEqual:@"player"]) + { + if ([i_info count] != 9) // must be player_x_y_z_W_X_Y_Z_align + return NO; // 0..... 1 2 3 4 5 6 7 8.... + + ShipEntity* doppelganger = [UNIVERSE newShipWithName: ship_desc]; // retain count = 1 + if (!doppelganger) + return NO; + + ScanVectorAndQuaternionFromString([[i_info subarrayWithRange:NSMakeRange( 1, 7)] componentsJoinedByString:@" "], &model_p0, &model_q); + + Vector model_offset = positionOffsetForShipInRotationToAlignment( doppelganger, model_q, (NSString*)[i_info objectAtIndex:8]); + model_p0.x += off.x - model_offset.x; + model_p0.y += off.y - model_offset.y; + model_p0.z += off.z - model_offset.z; + + OOLog(kOOLogDebugProcessSceneStringAddModel, @"::::: adding model to scene:'%@'", doppelganger); + [doppelganger setOrientation: model_q]; + [doppelganger setPosition: model_p0]; + [doppelganger setStatus: STATUS_COCKPIT_DISPLAY]; + [doppelganger setScanClass: CLASS_NO_DRAW]; + [UNIVERSE addEntity: doppelganger]; + [[doppelganger getAI] setStateMachine: @"nullAI.plist"]; + [doppelganger setRoll: 0.0]; + [doppelganger setPitch: 0.0]; + [doppelganger setVelocity: kZeroVector]; + [doppelganger setBehaviour: BEHAVIOUR_STOP_STILL]; + + [doppelganger release]; + return YES; + } + // + // Add planet model: selected via gui-scene-show-planet/-local-planet + // + if ([i_key isEqual:@"local-planet"] || [i_key isEqual:@"target-planet"]) + { + if ([i_info count] != 4) // must be xxxxx-planet_x_y_z + return NO; // 0........... 1 2 3 + PlanetEntity* doppelganger=nil; + BOOL procGen = NO; +#if ALLOW_PROCEDURAL_PLANETS + procGen = [UNIVERSE doProcedurallyTexturedPlanets]; + if ([i_key isEqual:@"local-planet"] && procGen && [UNIVERSE sun]) + { + // can safely show retextured planets! + doppelganger = [[PlanetEntity alloc] initMiniatureFromPlanet:[UNIVERSE planet]]; + } + else +#endif + { + doppelganger = [[PlanetEntity alloc] initWithSeed:target_system_seed]; + if (doppelganger) + [doppelganger miniaturize]; + } + if (!doppelganger) + return NO; + + ScanVectorFromString([[i_info subarrayWithRange:NSMakeRange(1, 3)] componentsJoinedByString:@" "], &model_p0); + Quaternion model_q = { 0.707, 0.707, 0.0, 0.0 }; + if (procGen) + { + model_q = make_quaternion( 0.707, 0.314, 0.707, 0.0 ); + } + model_p0 = vector_add(model_p0, off); + + OOLog(kOOLogDebugProcessSceneStringAddMiniPlanet, @"::::: adding %@ to scene:'%@'", i_key, doppelganger); + [doppelganger setOrientation: model_q]; + [doppelganger setPosition: model_p0]; + [UNIVERSE addEntity: doppelganger]; + + [doppelganger autorelease]; + return YES; + } + + // + // Add billboard model: + // + if ([i_key isEqual:@"billboard"]) + { + if ([i_info count] != 6) // must be billboard_imagefile_x_y_w_h + return NO; // 0........ 1........ 2 3 4 5 + + NSString* texturefile = (NSString*)[i_info objectAtIndex:1]; + NSSize billSize = NSMakeSize([[i_info objectAtIndex:4] floatValue], [[i_info objectAtIndex:5] floatValue]); + Vector model_p0; + model_p0.x = [[i_info objectAtIndex:2] floatValue] + off.x; + model_p0.y = [[i_info objectAtIndex:3] floatValue] + off.y; + model_p0.z = off.z; + + ParticleEntity* billboard = [[ParticleEntity alloc] initBillboard:billSize withTexture:texturefile]; + if (!billboard) + return NO; + + [billboard setPosition:vector_add([billboard position], model_p0)]; + + [billboard setStatus: STATUS_COCKPIT_DISPLAY]; + + OOLog(kOOLogDebugProcessSceneStringAddBillboard, @"::::: adding billboard:'%@' to scene.", billboard); + + [UNIVERSE addEntity: billboard]; + + [billboard release]; + return YES; + } + // + // fall through.. + return NO; +} + + +- (BOOL) mapKey:(NSString *)keycode toOXP:(OOScript *)oxp +{ + OOScript *s = [oxpKeys objectForKey:keycode]; + if (s == nil) + { + if (oxpKeys == nil) oxpKeys = [[NSMutableDictionary alloc] init]; + + [oxpKeys setObject:oxp forKey:keycode]; + return YES; + } + + return NO; +} + + +- (void) targetNearestHostile +{ + [self scanForHostiles]; + if (found_target != NO_TARGET) + { + Entity *ent = [UNIVERSE entityForUniversalID:found_target]; + if (ent != 0x00) + { + ident_engaged = YES; + missile_status = MISSILE_STATUS_TARGET_LOCKED; + [self addTarget:ent]; + } + } +} + + +#if TARGET_INCOMING_MISSILES +- (void) targetNearestIncomingMissile +{ + [self scanForNearestIncomingMissile]; + Entity *ent = [UNIVERSE entityForUniversalID:found_target]; + if (ent != nil) + { + ident_engaged = YES; + missile_status = MISSILE_STATUS_TARGET_LOCKED; + [self addTarget:ent]; + } + [self doScriptEvent:@"playerTargetedMissile" withArgument:ent]; +} +#endif + + +- (void) setGalacticHyperspaceBehaviourTo:(NSString *)galacticHyperspaceBehaviourString +{ + OOGalacticHyperspaceBehaviour ghBehaviour = StringToGalacticHyperspaceBehaviour(galacticHyperspaceBehaviourString); + if (ghBehaviour == GALACTIC_HYPERSPACE_BEHAVIOUR_UNKNOWN) + { + OOLog(@"player.setGalacticHyperspaceBehaviour.invalidInput", + @"setGalacticHyperspaceBehaviourTo: called with unknown behaviour %@.", galacticHyperspaceBehaviourString); + } + [self setGalacticHyperspaceBehaviour:ghBehaviour]; +} + + +- (void) setGalacticHyperspaceFixedCoordsTo:(NSString *)galacticHyperspaceFixedCoordsString +{ + NSArray *coord_vals = ScanTokensFromString(galacticHyperspaceFixedCoordsString); + if ([coord_vals count] < 2) // Will be 0 if string is nil + { + OOLog(@"player.setGalacticHyperspaceFixedCoords.invalidInput", + @"setGalacticHyperspaceFixedCoords: called with bad specifier. Defaulting to Oolite standard."); + galacticHyperspaceFixedCoords.x = galacticHyperspaceFixedCoords.y = 0x60; + } + + [self setGalacticHyperspaceFixedCoordsX:[coord_vals unsignedCharAtIndex:0] + y:[coord_vals unsignedCharAtIndex:1]]; +} + +@end + + +NSString *OOComparisonTypeToString(OOComparisonType type) +{ + switch (type) + { + case COMPARISON_EQUAL: return @"equal"; + case COMPARISON_NOTEQUAL: return @"notequal"; + case COMPARISON_LESSTHAN: return @"lessthan"; + case COMPARISON_GREATERTHAN: return @"greaterthan"; + case COMPARISON_ONEOF: return @"oneof"; + case COMPARISON_UNDEFINED: return @"undefined"; + } + return @""; +} diff --git a/src/Core/Entities/PlayerEntityLoadSave.h b/src/Core/Entities/PlayerEntityLoadSave.h new file mode 100644 index 00000000..4eea8cdb --- /dev/null +++ b/src/Core/Entities/PlayerEntityLoadSave.h @@ -0,0 +1,64 @@ +/* + +PlayerEntityLoadSave.h + +Created for the Oolite-Linux project (but is portable) + +LoadSave has been separated out into a separate category because +PlayerEntity.m has gotten far too big and is in danger of becoming +the whole general mish mash. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" +#import "GuiDisplayGen.h" +#import "MyOpenGLView.h" +#import "Universe.h" + +#define LABELROW 1 +#define BACKROW 2 +#define STARTROW 3 +#define ENDROW 16 +#define MOREROW 16 +#define NUMROWS 13 +#define COLUMNS 2 +#define INPUTROW 21 +#define CDRDESCROW 18 +#define SAVE_OVERWRITE_WARN_ROW 5 +#define SAVE_OVERWRITE_YES_ROW 8 +#define SAVE_OVERWRITE_NO_ROW 9 + +@interface PlayerEntity (LoadSave) + +- (BOOL) loadPlayer; // Returns NO on immediate failure, i.e. when using an OS X modal open panel which is cancelled. +- (void) savePlayer; +- (void) quicksavePlayer; +- (void) autosavePlayer; + +- (NSString *) commanderSelector; +- (void) saveCommanderInputHandler; +- (void) overwriteCommanderInputHandler; + +- (BOOL) loadPlayerFromFile:(NSString *)fileToOpen; + + +@end + diff --git a/src/Core/Entities/PlayerEntityLoadSave.m b/src/Core/Entities/PlayerEntityLoadSave.m new file mode 100644 index 00000000..c9c90ac7 --- /dev/null +++ b/src/Core/Entities/PlayerEntityLoadSave.m @@ -0,0 +1,1010 @@ +/* + + PlayerEntityLoadSave.m + + Oolite + Copyright (C) 2004-2008 Giles C Williams and contributors + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + MA 02110-1301, USA. + + */ + +#import "PlayerEntityLoadSave.h" +#import "PlayerEntityContracts.h" +#import "PlayerEntityControls.h" +#import "PlayerEntitySound.h" + +#import "NSFileManagerOOExtensions.h" +#import "GameController.h" +#import "PlayerEntityControls.h" +#import "OOXMLExtensions.h" +#import "OOSound.h" +#import "OOColor.h" +#import "OOStringParsing.h" +#import "OOPListParsing.h" +#import "StationEntity.h" +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" +#import "OOShipRegistry.h" + +#define kOOLogUnconvertedNSLog @"unclassified.PlayerEntityLoadSave" + + +// Set to 1 to use custom load/save dialogs in windowed mode on Macs in debug builds. No effect on other platforms. +#define USE_CUSTOM_LOAD_SAVE_ON_MAC_DEBUG 0 + +#if USE_CUSTOM_LOAD_SAVE_ON_MAC_DEBUG && OO_DEBUG && defined(OOLITE_USE_APPKIT_LOAD_SAVE) +#undef OOLITE_USE_APPKIT_LOAD_SAVE +#endif + + +// Name of modifier key used to issue commands. See also -isCommandModifierKeyDown. +#if OOLITE_MAC_OS_X +#define COMMAND_MODIFIER_KEY "command" +#else +#define COMMAND_MODIFIER_KEY "Ctrl" +#endif + + +@interface MyOpenGLView (OOLoadSaveExtensions) + +- (BOOL)isCommandModifierKeyDown; + +@end + + +@interface PlayerEntity (OOLoadSavePrivate) + +#if OOLITE_USE_APPKIT_LOAD_SAVE + +- (BOOL) loadPlayerWithPanel; +- (void) savePlayerWithPanel; + +#endif + +- (void) setGuiToLoadCommanderScreen; +- (void) setGuiToSaveCommanderScreen: (NSString *)cdrName; +- (void) setGuiToOverwriteScreen: (NSString *)cdrName; +- (void) lsCommanders: (GuiDisplayGen *)gui directory: (NSString*)directory pageNumber: (int)page highlightName: (NSString *)highlightName; +- (void) writePlayerToPath:(NSString *)path; +- (void) nativeSavePlayer: (NSString *)cdrName; +- (BOOL) existingNativeSave: (NSString *)cdrName; +- (void) showCommanderShip: (int)cdrArrayIndex; +- (int) findIndexOfCommander: (NSString *)cdrName; + +@end + + +@implementation PlayerEntity (LoadSave) + +- (BOOL)loadPlayer +{ + BOOL OK = YES; + +#if OOLITE_USE_APPKIT_LOAD_SAVE + // OS X: use system open/save dialogs in windowed mode, custom interface in full-screen. + if ([[UNIVERSE gameController] inFullScreenMode]) + { + [self setGuiToLoadCommanderScreen]; + } + else + { + OK = [self loadPlayerWithPanel]; + } +#else + // Other platforms: use custom interface all the time. + [self setGuiToLoadCommanderScreen]; +#endif + return OK; +} + + +- (void)savePlayer +{ +#if OOLITE_USE_APPKIT_LOAD_SAVE + // OS X: use system open/save dialogs in windowed mode, custom interface in full-screen. + if ([[UNIVERSE gameController] inFullScreenMode]) + { + [self setGuiToSaveCommanderScreen:player_name]; + } + else + { + [self savePlayerWithPanel]; + } +#else + // Other platforms: use custom interface all the time. + [self setGuiToSaveCommanderScreen:player_name]; +#endif +} + +- (void) autosavePlayer +{ + NSString *tmp_path = nil; + NSString *tmp_name = nil; + NSString *dir = [[UNIVERSE gameController] playerFileDirectory]; + + tmp_name = player_name; + tmp_path = save_path; + + NSString *saveName = DESC(@"autosave-commander-name"); + NSString *savePath = [dir stringByAppendingPathComponent:[saveName stringByAppendingString:@".oolite-save"]]; + + [player_name autorelease]; + player_name = [saveName copy]; + + [self writePlayerToPath:savePath]; + + if (tmp_path != nil) + { + [save_path autorelease]; + save_path = [tmp_path copy]; + } + [player_name autorelease]; + player_name = [tmp_name copy]; +} + + +- (void) quicksavePlayer +{ + NSString *path = nil; + + path = save_path; + if (!path) path = [[[UNIVERSE gameView] gameController] playerFileToLoad]; + if (!path) + { + OOLog(@"quickSave.failed.noName", @"ERROR no file name returned by [[[UNIVERSE gameView] gameController] playerFileToLoad]"); + [NSException raise:@"OoliteGameNotSavedException" + format:@"ERROR no file name returned by [[UNIVERSE gameView] gameController] playerFileToLoad]"]; + } + + [self writePlayerToPath:path]; + + [self setGuiToStatusScreen]; +} + + +- (NSString *)commanderSelector +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + GuiDisplayGen *gui = [UNIVERSE gui]; + NSString *dir = [[UNIVERSE gameController] playerFileDirectory]; + + int idx; + if([self handleGUIUpDownArrowKeys]) + { + int guiSelectedRow=[gui selectedRow]; + idx=(guiSelectedRow - STARTROW) + (currentPage * NUMROWS); + if (guiSelectedRow != MOREROW && guiSelectedRow != BACKROW) + { + [self showCommanderShip: idx]; + } + } + else + { + idx=([gui selectedRow] - STARTROW) + (currentPage * NUMROWS); + } + + // handle page <-- and page --> keys + if ([gameView isDown:gvArrowKeyLeft] && [[gui keyForRow:BACKROW] isEqual: GUI_KEY_OK]) + { + currentPage--; + [self playMenuPagePrevious]; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + } + // + if ([gameView isDown:gvArrowKeyRight] && [[gui keyForRow:MOREROW] isEqual: GUI_KEY_OK]) + { + currentPage++; + [self playMenuPageNext]; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + } + + // Enter pressed - find the commander name underneath. + if ([gameView isDown:13]||[gameView isDown:gvMouseDoubleClick]) + { + NSDictionary *cdr; + switch ([gui selectedRow]) + { + case BACKROW: + currentPage--; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + break; + case MOREROW: + currentPage++; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + break; + default: + cdr=[cdrDetailArray objectAtIndex: idx]; + if ([cdr boolForKey:@"isSavedGame"]) + return [cdr stringForKey:@"saved_game_path"]; + else + { + if ([gameView isCommandModifierKeyDown]||[gameView isDown:gvMouseDoubleClick]) + { + // change directory to the selected path + NSString* newDir = [cdr stringForKey:@"saved_game_path"]; + [[UNIVERSE gameController] setPlayerFileDirectory: newDir]; + dir = newDir; + currentPage = 0; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + } + } + } + } + + if([gameView isDown: 27]) + { + [self setGuiToStatusScreen]; + } + return nil; +} + + +- (void) saveCommanderInputHandler +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + GuiDisplayGen *gui = [UNIVERSE gui]; + NSString *dir = [[UNIVERSE gameController] playerFileDirectory]; + + if ([self handleGUIUpDownArrowKeys]) + { + int guiSelectedRow=[gui selectedRow]; + int idx = (guiSelectedRow - STARTROW) + (currentPage * NUMROWS); + if (guiSelectedRow != MOREROW && guiSelectedRow != BACKROW) + { + [self showCommanderShip: idx]; + if ([(NSDictionary *)[cdrDetailArray objectAtIndex:idx] boolForKey:@"isSavedGame"]) // don't show things that aren't saved games + commanderNameString = [[cdrDetailArray dictionaryAtIndex:idx] stringForKey:@"player_name"]; + else + commanderNameString = [gameView typedString]; + } + } + else + commanderNameString = [gameView typedString]; + [gameView setTypedString: commanderNameString]; + + [gui setText: + [NSString stringWithFormat:DESC(@"savescreen-commander-name-@"), commanderNameString] + forRow: INPUTROW]; + [gui setColor:[OOColor cyanColor] forRow:INPUTROW]; + + // handle page <-- and page --> keys + if ([gameView isDown:gvArrowKeyLeft] && [[gui keyForRow:BACKROW] isEqual: GUI_KEY_OK]) + { + currentPage--; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + } + // + if ([gameView isDown:gvArrowKeyRight] && [[gui keyForRow:MOREROW] isEqual: GUI_KEY_OK]) + { + currentPage++; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + } + + if(([gameView isDown: 13]||[gameView isDown:gvMouseDoubleClick]) && [commanderNameString length]) + { + if ([gameView isCommandModifierKeyDown]||[gameView isDown:gvMouseDoubleClick]) + { + int guiSelectedRow=[gui selectedRow]; + int idx = (guiSelectedRow - STARTROW) + (currentPage * NUMROWS); + NSDictionary* cdr = [cdrDetailArray objectAtIndex:idx]; + if (![cdr boolForKey:@"isSavedGame"]) // don't open saved games + { + // change directory to the selected path + NSString* newDir = [cdr stringForKey:@"saved_game_path"]; + [[UNIVERSE gameController] setPlayerFileDirectory: newDir]; + dir = newDir; + currentPage = 0; + [self lsCommanders: gui directory: dir pageNumber: currentPage highlightName: nil]; + [gameView supressKeysUntilKeyUp]; + } + } + else + { + pollControls=YES; + if ([self existingNativeSave: commanderNameString]) + { + [gameView supressKeysUntilKeyUp]; + [self setGuiToOverwriteScreen: commanderNameString]; + } + else + { + [self nativeSavePlayer: commanderNameString]; + [self setGuiToStatusScreen]; + } + } + } + + if([gameView isDown: 27]) + { + // esc was pressed - get out of here + pollControls=YES; + [self setGuiToStatusScreen]; + } +} + + +- (void) overwriteCommanderInputHandler +{ + MyOpenGLView *gameView = [UNIVERSE gameView]; + GuiDisplayGen *gui = [UNIVERSE gui]; + + [self handleGUIUpDownArrowKeys]; + + if (([gameView isDown: 13] && ([gui selectedRow] == SAVE_OVERWRITE_YES_ROW))||[gameView isDown: 121]||[gameView isDown: 89]) + { + pollControls=YES; + [self nativeSavePlayer: commanderNameString]; + [self setGuiToStatusScreen]; + + [self playSaveOverwriteYes]; + } + + if (([gameView isDown: 13] && ([gui selectedRow] == SAVE_OVERWRITE_NO_ROW))||[gameView isDown: 27]||[gameView isDown: 110]||[gameView isDown: 78]) + { + // esc or NO was pressed - get out of here + // FIXME: should return to save screen instead. + pollControls=YES; + [self setGuiToStatusScreen]; + + [self playSaveOverwriteNo]; + } +} + + +- (BOOL) loadPlayerFromFile:(NSString *)fileToOpen +{ + /* TODO: it would probably be better to load by creating a new + PlayerEntity, verifying that's OK, then replacing the global player. + + Actually, it'd be better to separate PlayerEntity into OOPlayer and + OOPlayerShipEntity. And then move most of OOPlayerShipEntity into + ShipEntity, and make NPC ships behave more like player ships. + -- Ahruman + */ + + BOOL loadedOK = YES; + NSDictionary *fileDic = nil; + NSString *fail_reason = nil; + + if (fileToOpen == nil) + { + fail_reason = DESC(@"loadfailed-no-file-specified"); + loadedOK = NO; + } + + if (loadedOK) + { + fileDic = OODictionaryFromFile(fileToOpen); + if (fileDic == nil) + { + fail_reason = DESC(@"loadfailed-could-not-load-file"); + loadedOK = NO; + } + } + + if (loadedOK) + { + // Check that player ship exists + NSString *shipKey = nil; + NSDictionary *shipDict = nil; + + shipKey = [fileDic stringForKey:@"ship_desc"]; + shipDict = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipKey]; + + if (shipDict == nil) + { + loadedOK = NO; + if (shipKey != nil) fail_reason = [NSString stringWithFormat:DESC(@"loadfailed-could-not-find-ship-type-@-please-reinstall-the-appropriate-OXP"), shipKey]; + else fail_reason = DESC(@"loadfailed-invalid-saved-game-no-ship-specified"); + } + } + + if (loadedOK) + { + [self set_up:NO]; //don't run reset yet + if ([self setCommanderDataFromDictionary:fileDic]) + { + [self doScriptEvent:@"reset"]; //after the mission variables are loaded from the save game + } + else + { + fail_reason = DESC(@"loadfailed-could-not-set-up-player-ship"); + loadedOK = NO; + } + } + + if (loadedOK) + { + if (save_path) [save_path autorelease]; + save_path = [fileToOpen retain]; + + [[[UNIVERSE gameView] gameController] setPlayerFileToLoad:fileToOpen]; + [[[UNIVERSE gameView] gameController] setPlayerFileDirectory:fileToOpen]; + } + else + { + OOLog(@"load.failed", @"***** FILE LOADING ERROR!! *****"); + [[UNIVERSE gameController] setPlayerFileToLoad:nil]; + [UNIVERSE game_over]; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:DESC(@"loadfailed-saved-game-failed-to-load") forCount: 9.0]; + if (fail_reason != nil) [UNIVERSE addMessage: fail_reason forCount: 9.0]; + return NO; + } + + [UNIVERSE setTimeAccelerationFactor:TIME_ACCELERATION_FACTOR_DEFAULT]; + [UNIVERSE setSystemTo:system_seed]; + [UNIVERSE removeAllEntitiesExceptPlayer:NO]; + [UNIVERSE setUpSpace]; + [UNIVERSE setAutoSaveNow:NO]; + + [self setStatus:STATUS_DOCKED]; + [UNIVERSE setViewDirection:VIEW_GUI_DISPLAY]; + + dockedStation = [UNIVERSE station]; + if (dockedStation) + { + position = [dockedStation position]; + [self setOrientation: kIdentityQuaternion]; + v_forward = vector_forward_from_quaternion(orientation); + v_right = vector_right_from_quaternion(orientation); + v_up = vector_up_from_quaternion(orientation); + } + + flightRoll = 0.0; + flightPitch = 0.0; + flightYaw = 0.0; + flightSpeed = 0.0; + + if (![dockedStation localMarket]) + { + NSArray *market = [fileDic arrayForKey:@"localMarket"]; + if (market != nil) [dockedStation setLocalMarket:market]; + else [dockedStation initialiseLocalMarketWithSeed:system_seed andRandomFactor:market_rnd]; + } + [self calculateCurrentCargo]; + [UNIVERSE setGalaxy_seed: galaxy_seed andReinit:YES]; // set overridden planet names on long range map + [self setGuiToStatusScreen]; + return loadedOK; +} + +@end + + +@implementation PlayerEntity (OOLoadSavePrivate) + +#if OOLITE_USE_APPKIT_LOAD_SAVE + +- (BOOL)loadPlayerWithPanel +{ + int result; + NSArray *fileTypes = nil; + NSOpenPanel *oPanel = nil; + + fileTypes = [NSArray arrayWithObject:@"oolite-save"]; + oPanel = [NSOpenPanel openPanel]; + + [oPanel setAllowsMultipleSelection:NO]; + result = [oPanel runModalForDirectory:nil file:nil types:fileTypes]; + if (result == NSOKButton) + { + return [self loadPlayerFromFile:[oPanel filename]]; + } + else + { + return NO; + } +} + + +- (void) savePlayerWithPanel +{ + NSSavePanel *sp; + int runResult; + + sp = [NSSavePanel savePanel]; + [sp setRequiredFileType:@"oolite-save"]; + [sp setCanSelectHiddenExtension:YES]; + + // display the NSSavePanel + runResult = [sp runModalForDirectory:nil file:player_name]; + + // if successful, save file under designated name + // TODO: break actual writing into a separate method to avoid redundancy. -- Ahruman + if (runResult == NSOKButton) + { + NSArray* path_components = [[sp filename] pathComponents]; + NSString* new_name = [[path_components objectAtIndex:[path_components count]-1] stringByDeletingPathExtension]; + + [player_name release]; + player_name = [new_name copy]; + + [self writePlayerToPath:[sp filename]]; + } + [self setGuiToStatusScreen]; +} + +#endif + + +- (void) writePlayerToPath:(NSString *)path +{ + NSString *errDesc = nil; + NSDictionary *dict = nil; + BOOL didSave = NO; + + if (!path) + { + OOLog(@"save.failed", @"***** SAVE ERROR: %s called with nil path.", __PRETTY_FUNCTION__); + return; + } + + dict = [self commanderDataDictionary]; + if (dict == nil) errDesc = @"could not construct commander data dictionary."; + else didSave = [dict writeOOXMLToFile:path atomically:YES errorDescription:&errDesc]; + if (didSave) + { + + [UNIVERSE clearPreviousMessage]; // allow this to be given time and again + [UNIVERSE addMessage:DESC(@"game-saved") forCount:2]; + [save_path autorelease]; + save_path = [path copy]; + [[UNIVERSE gameController] setPlayerFileToLoad:save_path]; + [[UNIVERSE gameController] setPlayerFileDirectory:save_path]; + } + else + { + OOLog(@"save.failed", @"***** SAVE ERROR: %@", errDesc); + [NSException raise:@"OoliteException" + format:@"Attempt to save game to file '%@' failed: %@", errDesc]; + } + + [self setGuiToStatusScreen]; +} + + +- (void)nativeSavePlayer:(NSString *)cdrName +{ + NSString* dir = [[UNIVERSE gameController] playerFileDirectory]; + + NSString *savePath = [dir stringByAppendingPathComponent:[cdrName stringByAppendingPathExtension:@"oolite-save"]]; + + [player_name release]; + player_name = [cdrName copy]; + + [self writePlayerToPath:savePath]; +} + + + +- (void) setGuiToLoadCommanderScreen +{ + GuiDisplayGen *gui=[UNIVERSE gui]; + NSString* dir = [[UNIVERSE gameController] playerFileDirectory]; + + gui_screen = GUI_SCREEN_LOAD; + + [gui clear]; + [gui setTitle:DESC(@"loadscreen-title")]; + + currentPage = 0; + [self lsCommanders:gui directory:dir pageNumber: currentPage highlightName:nil]; + + [[UNIVERSE gameView] supressKeysUntilKeyUp]; + + [self setShowDemoShips: YES]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) setGuiToSaveCommanderScreen: (NSString *)cdrName +{ + GuiDisplayGen *gui=[UNIVERSE gui]; + MyOpenGLView* gameView = [UNIVERSE gameView]; + NSString* dir = [[UNIVERSE gameController] playerFileDirectory]; + + pollControls = NO; + gui_screen = GUI_SCREEN_SAVE; + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"savescreen-title")]]; + + currentPage = 0; + [self lsCommanders:gui directory:dir pageNumber: currentPage highlightName:nil]; + + [gui setText:DESC(@"savescreen-commander-name") forRow: INPUTROW]; + [gui setColor:[OOColor cyanColor] forRow:INPUTROW]; + [gui setShowTextCursor: YES]; + [gui setCurrentRow: INPUTROW]; + + [gameView setTypedString: cdrName]; + [gameView supressKeysUntilKeyUp]; + + [self setShowDemoShips: YES]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: YES]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) setGuiToOverwriteScreen: (NSString *)cdrName +{ + GuiDisplayGen *gui=[UNIVERSE gui]; + MyOpenGLView* gameView = [UNIVERSE gameView]; + + // Don't poll controls + pollControls=NO; + + gui_screen = GUI_SCREEN_SAVE_OVERWRITE; + + [gui clear]; + [gui setTitle:[NSString stringWithFormat:DESC(@"overwrite-save-commander-@"), cdrName]]; + + [gui setText:[NSString stringWithFormat:DESC(@"overwritescreen-commander-@-already-exists-overwrite-query"), cdrName] + forRow:SAVE_OVERWRITE_WARN_ROW align: GUI_ALIGN_CENTER]; + + [gui setText:DESC(@"overwritescreen-yes") forRow: SAVE_OVERWRITE_YES_ROW align: GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow: SAVE_OVERWRITE_YES_ROW]; + + [gui setText:DESC(@"overwritescreen-no") forRow: SAVE_OVERWRITE_NO_ROW align: GUI_ALIGN_CENTER]; + [gui setKey:GUI_KEY_OK forRow: SAVE_OVERWRITE_NO_ROW]; + + [gui setSelectableRange: NSMakeRange(SAVE_OVERWRITE_YES_ROW, 2)]; + [gui setSelectedRow: SAVE_OVERWRITE_NO_ROW]; + + [self setShowDemoShips: NO]; + [UNIVERSE setDisplayText: YES]; + [UNIVERSE setDisplayCursor: NO]; + [gameView setStringInput: gvStringInputNo]; + [UNIVERSE setViewDirection: VIEW_GUI_DISPLAY]; +} + + +- (void) lsCommanders: (GuiDisplayGen *)gui + directory: (NSString*) directory + pageNumber: (int)page + highlightName: (NSString *)highlightName +{ + NSFileManager *cdrFileManager=[NSFileManager defaultManager]; + int rangeStart=STARTROW; + unsigned lastIndex; + unsigned i; + int row=STARTROW; + + // cdrArray defined in PlayerEntity.h + NSArray *cdrArray=[cdrFileManager commanderContentsOfPath: directory]; + + // get commander details so a brief rundown of the commander's details may + // be displayed. + if (!cdrDetailArray) + cdrDetailArray=[[NSMutableArray alloc] init]; // alloc retains this so the retain further on in the code was unnecessary + else + [cdrDetailArray removeAllObjects]; + + [cdrDetailArray addObject:[NSMutableDictionary dictionaryWithObjectsAndKeys: + @"YES", @"isParentFolder", + [directory stringByDeletingLastPathComponent], @"saved_game_path", nil]]; + + for(i = 0; i < [cdrArray count]; i++) + { + NSString* path = [cdrArray objectAtIndex:i]; + BOOL exists, isDirectory = NO; + + exists = [cdrFileManager fileExistsAtPath:path isDirectory:&isDirectory]; + + if (exists) + { + if (!isDirectory && [[[path pathExtension] lowercaseString] isEqualToString:@"oolite-save"]) + { + NSDictionary *cdr = OODictionaryFromFile(path); + if(cdr) + { + // okay use the same dictionary but add a 'saved_game_path' attribute + NSMutableDictionary* cdr1 = [NSMutableDictionary dictionaryWithDictionary:cdr]; + [cdr1 setObject: @"YES" forKey:@"isSavedGame"]; + [cdr1 setObject: path forKey:@"saved_game_path"]; + [cdrDetailArray addObject: cdr1]; + } + } + if (isDirectory && ![[path lastPathComponent] hasPrefix:@"."]) + { + [cdrDetailArray addObject: [NSDictionary dictionaryWithObjectsAndKeys: @"YES", @"isFolder", path, @"saved_game_path", nil]]; + } + } + } + + if(![cdrDetailArray count]) + { + // Empty directory; tell the user and exit immediately. + [gui setText:DESC(@"loadsavescreen-no-commanders-found") forRow:STARTROW align:GUI_ALIGN_CENTER]; + return; + } + + // Do we need to highlight a name? + int highlightRowOnPage=STARTROW; + int highlightIdx=0; + if(highlightName) + { + highlightIdx=[self findIndexOfCommander: highlightName]; + if(highlightIdx < 0) + { + OOLog(@"save.list.commanders.commanderNotFound", @"Commander %@ doesn't exist, very bad", highlightName); + highlightIdx=0; + } + + // figure out what page we need to be on + page=highlightIdx/NUMROWS; + highlightRowOnPage=highlightIdx % NUMROWS + STARTROW; + } + + // We now know for certain what page we're on - + // set the first index of the first commander on this page. + unsigned firstIndex=page * NUMROWS; + + // Set up the GUI. + OOGUITabSettings tabStop; + tabStop[0]=0; + tabStop[1]=160; + tabStop[2]=270; + [gui setTabStops: tabStop]; + [gui setColor: [OOColor greenColor] forRow: LABELROW]; + [gui setArray: [NSArray arrayWithObjects: DESC(@"loadsavescreen-commander-name"), DESC(@"loadsavescreen-rating"), nil] + forRow:LABELROW]; + + // clear text lines here + for (i = STARTROW - 1; i < ENDROW + 1; i++) + { + [gui setText:@"" forRow:i align:GUI_ALIGN_LEFT]; + [gui setColor: [OOColor yellowColor] forRow: i]; + [gui setKey:GUI_KEY_SKIP forRow:i]; + } + + if (page) + { + [gui setColor:[OOColor greenColor] forRow:STARTROW-1]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-back"), @" <-- ", nil] + forRow:STARTROW-1]; + [gui setKey:GUI_KEY_OK forRow:STARTROW-1]; + rangeStart=STARTROW-1; + if(!highlightIdx) + highlightIdx=firstIndex; + } + + if (firstIndex + NUMROWS >= [cdrDetailArray count]) + { + lastIndex=[cdrDetailArray count]; + [gui setSelectableRange: NSMakeRange(rangeStart, lastIndex)]; + } + else + { + lastIndex=(page * NUMROWS) + NUMROWS; + [gui setColor:[OOColor greenColor] forRow:ENDROW]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-more"), @" --> ", nil] + forRow:ENDROW]; + [gui setKey:GUI_KEY_OK forRow:ENDROW]; + [gui setSelectableRange: NSMakeRange(rangeStart, MOREROW)]; + } + + for (i=firstIndex; i < lastIndex; i++) + { + NSDictionary *cdr=[cdrDetailArray objectAtIndex: i]; + if ([cdr boolForKey:@"isSavedGame"]) + { + NSString *ratingDesc = KillCountToRatingString([cdr unsignedIntForKey:@"ship_kills"]); + [gui setArray:[NSArray arrayWithObjects: + [NSString stringWithFormat:@" %@ ",[cdr stringForKey:@"player_name"]], + [NSString stringWithFormat:@" %@ ",ratingDesc], + nil] + forRow:row]; + if ([player_name isEqualToString:[cdr stringForKey:@"player_name"]]) + highlightRowOnPage = row; + + [gui setKey:GUI_KEY_OK forRow:row]; + row++; + } + if ([cdr boolForKey:@"isParentFolder"]) + { + [gui setArray:[NSArray arrayWithObjects: + [NSString stringWithFormat:@" (..) %@ ", [[cdr stringForKey:@"saved_game_path"] lastPathComponent]], + @"", + nil] + forRow:row]; + [gui setColor: [OOColor orangeColor] forRow: row]; + [gui setKey:GUI_KEY_OK forRow:row]; + row++; + } + if ([cdr boolForKey:@"isFolder"]) + { + [gui setArray:[NSArray arrayWithObjects: + [NSString stringWithFormat:@" >> %@ ", [[cdr stringForKey:@"saved_game_path"] lastPathComponent]], + @"", + nil] + forRow:row]; + [gui setColor: [OOColor orangeColor] forRow: row]; + [gui setKey:GUI_KEY_OK forRow:row]; + row++; + } + } + [gui setSelectedRow: highlightRowOnPage]; + highlightIdx = (highlightRowOnPage - STARTROW) + (currentPage * NUMROWS); + // show the first ship, this will be the selected row + [self showCommanderShip: highlightIdx]; +} + + +// check for an existing saved game... +- (BOOL) existingNativeSave: (NSString *)cdrName +{ + NSString* dir = [[UNIVERSE gameController] playerFileDirectory]; + + NSString *savePath=[dir stringByAppendingPathComponent:[cdrName stringByAppendingPathExtension:@"oolite-save"]]; + return [[NSFileManager defaultManager] fileExistsAtPath:savePath]; +} + + +// Get some brief details about the commander file. +- (void) showCommanderShip:(int)cdrArrayIndex +{ + GuiDisplayGen *gui=[UNIVERSE gui]; + [UNIVERSE removeDemoShips]; + NSDictionary *cdr=[cdrDetailArray objectAtIndex: cdrArrayIndex]; + + [gui setText:@"" forRow:CDRDESCROW align:GUI_ALIGN_LEFT]; + [gui setText:@"" forRow:CDRDESCROW + 1 align:GUI_ALIGN_LEFT]; + [gui setText:@"" forRow:CDRDESCROW + 2 align:GUI_ALIGN_LEFT]; + + if ([cdr boolForKey:@"isFolder"]) + { + NSString *folderDesc=[NSString stringWithFormat: DESC(@"loadsavescreen-hold-@-and-press-return-to-open-folder-@"), @COMMAND_MODIFIER_KEY, [[cdr stringForKey:@"saved_game_path"] lastPathComponent]]; + [gui setColor: [OOColor orangeColor] forRow: CDRDESCROW]; + [gui addLongText: folderDesc startingAtRow: CDRDESCROW align: GUI_ALIGN_LEFT]; + return; + } + + if ([cdr boolForKey:@"isParentFolder"]) + { + NSString *folderDesc=[NSString stringWithFormat: DESC(@"loadsavescreen-hold-@-and-press-return-to-open-parent-folder-@"), @COMMAND_MODIFIER_KEY, [[cdr stringForKey:@"saved_game_path"] lastPathComponent]]; + [gui setColor: [OOColor orangeColor] forRow: CDRDESCROW]; + [gui addLongText: folderDesc startingAtRow: CDRDESCROW align: GUI_ALIGN_LEFT]; + return; + } + [gui setColor: [OOColor yellowColor] forRow: CDRDESCROW]; + + if (![cdr boolForKey:@"isSavedGame"]) // don't show things that aren't saved games + return; + + if(!dockedStation) dockedStation = [UNIVERSE station]; + + // Display the commander's ship. + NSString *shipDesc = [cdr stringForKey:@"ship_desc"]; + NSString *shipName = nil; + NSDictionary *shipDict = nil; + NSString *rating = nil; + + shipDict = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipDesc]; + if(shipDict != nil) + { + [self showShipyardModel:shipDict]; + shipName = [shipDict stringForKey:@"display_name"]; + if (shipName == nil) shipName = [shipDict stringForKey:KEY_NAME]; + } + else + { + [self showShipyardModel:[[OOShipRegistry sharedRegistry] shipInfoForKey:@"oolite-unknown-ship"]]; + shipName = [cdr stringForKey:@"ship_name" defaultValue:@"unknown"]; + shipName = [shipName stringByAppendingString:@" - OXP not installed"]; + } + + // Make a short description of the commander + NSString *legalDesc = nil; + OOCreditsQuantity money; + + legalDesc = LegalStatusToString([cdr intForKey:@"legal_status"]); + + rating = KillCountToRatingAndKillString([cdr unsignedIntForKey:@"ship_kills"]); + money = [cdr unsignedLongLongForKey:@"credits"]; + + // Nikos - Add some more information in the load game screen (current location, galaxy number and timestamp). + //------------------------------------------------------------------------------------------------------------------------- + + // Store the current galaxy seed because findSystemNumberAtCoords may alter it in a while. + PlayerEntity *player = [PlayerEntity sharedPlayer]; + Random_Seed player_galaxy_seed = [player galaxy_seed]; + + int galNumber; + NSString *timeStamp = nil; + NSString *locationName = [cdr stringForKey:@"current_system_name"]; + + // If there is no key containing the name of the current system in the savefile, fall back to + // extracting the name from the galaxy seed and coordinates information. + if (locationName == nil) + { + Random_Seed gal_seed; + NSPoint gal_coords; + int locationNumber; + + gal_coords = PointFromString([cdr stringForKey:@"galaxy_coordinates"]); + gal_seed = RandomSeedFromString([cdr stringForKey:@"galaxy_seed"]); + locationNumber = [UNIVERSE findSystemNumberAtCoords:gal_coords withGalaxySeed:gal_seed]; + locationName = [UNIVERSE systemNameIndex:locationNumber]; + } + + galNumber = [cdr intForKey:@"galaxy_number"] + 1; // Galaxy numbering starts at 0. + + timeStamp = ClockToString([cdr doubleForKey:@"ship_clock" defaultValue:PLAYER_SHIP_CLOCK_START], NO); + + //------------------------------------------------------------------------------------------------------------------------- + + NSString *cdrDesc = nil; + + cdrDesc = [NSString stringWithFormat:DESC(@"loadsavescreen-commander-@-rated-@-has-@-legal-status-@-ship-@-location-@-g-@-timestamp-@"), + [cdr stringForKey:@"player_name"], + rating, + OOCredits(money), + legalDesc, + shipName, + locationName, + galNumber, + timeStamp]; + + [gui addLongText:cdrDesc startingAtRow:CDRDESCROW align:GUI_ALIGN_LEFT]; + + // Restore the seed of the galaxy the player is currently in. + [UNIVERSE setGalaxy_seed: player_galaxy_seed]; +} + + +- (int) findIndexOfCommander: (NSString *)cdrName +{ + unsigned i; + for (i=0; i < [cdrDetailArray count]; i++) + { + NSString *currentName = [[cdrDetailArray dictionaryAtIndex: i] stringForKey:@"player_name"]; + if([cdrName compare: currentName] == NSOrderedSame) + { + return i; + } + } + + // not found! + return -1; +} + +@end + + +@implementation MyOpenGLView (OOLoadSaveExtensions) + +- (BOOL)isCommandModifierKeyDown +{ +#if OOLITE_MAC_OS_X + return [self isCommandDown]; +#else + return [self isCtrlDown]; +#endif +} + +@end diff --git a/src/Core/Entities/PlayerEntityScriptMethods.h b/src/Core/Entities/PlayerEntityScriptMethods.h new file mode 100644 index 00000000..7e1b66a8 --- /dev/null +++ b/src/Core/Entities/PlayerEntityScriptMethods.h @@ -0,0 +1,62 @@ +/* + +PlayerEntityScriptMethods.h + +Methods for use by scripting mechanisms. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" + + +@interface PlayerEntity (ScriptMethods) + +- (NSString *) playerName; + +- (unsigned) score; +- (void) setScore:(unsigned)value; + +- (double) creditBalance; +- (void) setCreditBalance:(double)value; + +- (float)fuelLeakRate; +- (void)setFuelLeakRate:(float)value; + +- (NSString *) dockedStationName; +- (NSString *) dockedStationDisplayName; +- (BOOL) dockedAtMainStation; + +- (BOOL) canAwardCargoType:(OOCargoType)type amount:(OOCargoQuantity)amount; +- (void) awardCargoType:(OOCargoType)type amount:(OOCargoQuantity)amount; + +- (OOGalaxyID) currentGalaxyID; +- (OOSystemID) currentSystemID; + +- (void) setMissionChoice:(NSString *)newChoice; + +- (OOTimeDelta) scriptTimer; + +- (unsigned) systemPseudoRandom100; +- (unsigned) systemPseudoRandom256; +- (double) systemPseudoRandomFloat; + +@end diff --git a/src/Core/Entities/PlayerEntityScriptMethods.m b/src/Core/Entities/PlayerEntityScriptMethods.m new file mode 100644 index 00000000..9f3c43fa --- /dev/null +++ b/src/Core/Entities/PlayerEntityScriptMethods.m @@ -0,0 +1,276 @@ +/* + +PlayerEntityScriptMethods.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntityScriptMethods.h" + +#import "Universe.h" +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" + + +@implementation PlayerEntity (ScriptMethods) + +- (NSString *) playerName +{ + return [[player_name retain] autorelease]; +} + + +- (unsigned) score +{ + return ship_kills; +} + + +- (void) setScore:(unsigned)value +{ + ship_kills = value; +} + + +- (double)creditBalance +{ + return 0.1 * (double)credits; +} + + +- (void)setCreditBalance:(double)value +{ + value = round(value * 10.0); + credits = OOClamp_0_max_d(value, (double)ULONG_MAX); +} + + +- (float)fuelLeakRate +{ + return fuel_leak_rate; +} + + +- (void)setFuelLeakRate:(float)value +{ + fuel_leak_rate = OOMax_f(value, 0.0f); +} + + +- (NSString *) dockedStationName +{ + return [(ShipEntity *)dockedStation name]; +} + + +- (NSString *) dockedStationDisplayName +{ + return [(ShipEntity *)dockedStation displayName]; +} + + +- (BOOL) dockedAtMainStation +{ + return [self status] == STATUS_DOCKED && dockedStation == [UNIVERSE station]; +} + + +- (BOOL) canAwardCargoType:(OOCargoType)type amount:(OOCargoQuantity)amount +{ + if (type == CARGO_NOT_CARGO) return NO; + if ([UNIVERSE unitsForCommodity:type] == UNITS_TONS) + { + if ([self specialCargo] != nil) return NO; + if (amount > [self availableCargoSpace]) return NO; + } + + return YES; +} + + +- (void) awardCargoType:(OOCargoType)type amount:(OOCargoQuantity)amount +{ + OOMassUnit unit; + NSArray *commodityArray = nil; + + commodityArray = [UNIVERSE commodityDataForType:type]; + if (commodityArray == nil) return; + + OOLog(@"script.debug.note.awardCargo", @"Going to award cargo: %d x '%@'", amount, CommodityDisplayNameForCommodityArray(commodityArray)); + + unit = [commodityArray intAtIndex:MARKET_UNITS]; + + if ([self status] != STATUS_DOCKED) + { + // in-flight + while (amount) + { + if (unit != UNITS_TONS) + { + if (specialCargo) + { + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + NSMutableArray* manifest_commodity = [NSMutableArray arrayWithArray:(NSArray *)[manifest objectAtIndex:type]]; + int manifest_quantity = [(NSNumber *)[manifest_commodity objectAtIndex:MARKET_QUANTITY] intValue]; + manifest_quantity += amount; + amount = 0; + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:manifest_quantity]]; + [manifest replaceObjectAtIndex:type withObject:[NSArray arrayWithArray:manifest_commodity]]; + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + } + else + { + int amount_per_container = (unit == UNITS_KILOGRAMS)? 1000 : 1000000; + while (amount > 0) + { + int smaller_quantity = 1 + ((amount - 1) % amount_per_container); + if ([cargo count] < max_cargo) + { + ShipEntity* container = [UNIVERSE newShipWithRole:@"cargopod"]; + if (container) + { + // Shouldn't there be a [UNIVERSE addEntity:] here? -- Ahruman + [container wasAddedToUniverse]; + [container setScanClass: CLASS_CARGO]; + [container setCommodity:type andAmount:smaller_quantity]; + [cargo addObject:container]; + [container release]; + } + } + amount -= smaller_quantity; + } + } + } + else + { + // put each ton in a separate container + while (amount) + { + if ([cargo count] < max_cargo) + { + ShipEntity* container = [UNIVERSE newShipWithRole:@"cargopod"]; + if (container) + { + // Shouldn't there be a [UNIVERSE addEntity:] here? -- Ahruman + [container wasAddedToUniverse]; + [container setScanClass: CLASS_CARGO]; + [container setStatus:STATUS_IN_HOLD]; + [container setCommodity:type andAmount:1]; + [cargo addObject:container]; + [container release]; + } + } + amount--; + } + } + } + } + else + { // docked + // like purchasing a commodity + NSMutableArray* manifest = [NSMutableArray arrayWithArray:shipCommodityData]; + NSMutableArray* manifest_commodity = [NSMutableArray arrayWithArray:(NSArray *)[manifest objectAtIndex:type]]; + int manifest_quantity = [(NSNumber *)[manifest_commodity objectAtIndex:MARKET_QUANTITY] intValue]; + while ((amount)&&(current_cargo < max_cargo)) + { + manifest_quantity++; + amount--; + if (unit == UNITS_TONS) + current_cargo++; + } + [manifest_commodity replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:manifest_quantity]]; + [manifest replaceObjectAtIndex:type withObject:[NSArray arrayWithArray:manifest_commodity]]; + [shipCommodityData release]; + shipCommodityData = [[NSArray arrayWithArray:manifest] retain]; + } +} + + +- (OOGalaxyID) currentGalaxyID +{ + return galaxy_number; +} + + +- (OOSystemID) currentSystemID +{ + if ([UNIVERSE sun] == nil) return -1; // Interstellar space + return [UNIVERSE currentSystemID]; +} + + +- (void) setMissionChoice:(NSString *)newChoice +{ + BOOL equal = [newChoice isEqualToString:missionChoice] || (newChoice == missionChoice); // Catch both being nil as well + if (!equal) + { + if (newChoice == nil) + { + NSString *oldChoice = missionChoice; + [missionChoice autorelease]; + missionChoice = nil; + + [self doScriptEvent:@"missionChoiceWasReset" withArgument:oldChoice]; + } + else + { + [missionChoice release]; + missionChoice = [newChoice copy]; + } + } +} + + +- (OOTimeDelta) scriptTimer +{ + return script_time; +} + + +- (unsigned) systemPseudoRandom100 +{ + seed_RNG_only_for_planet_description(system_seed); + return (gen_rnd_number() * 256 + gen_rnd_number()) % 100; +} + + +- (unsigned) systemPseudoRandom256 +{ + seed_RNG_only_for_planet_description(system_seed); + return gen_rnd_number(); +} + + +- (double) systemPseudoRandomFloat +{ + Random_Seed seed = system_seed; + seed_RNG_only_for_planet_description(system_seed); + unsigned a = gen_rnd_number(); + unsigned b = gen_rnd_number(); + unsigned c = gen_rnd_number(); + system_seed = seed; + + a = (a << 16) | (b << 8) | c; + return (double)a / (double)0x01000000; + +} + +@end diff --git a/src/Core/Entities/PlayerEntitySound.h b/src/Core/Entities/PlayerEntitySound.h new file mode 100644 index 00000000..39956ac1 --- /dev/null +++ b/src/Core/Entities/PlayerEntitySound.h @@ -0,0 +1,136 @@ +/* + +PlayerEntitySound.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" + + +@interface PlayerEntity (Sound) + +- (void) setUpSound; +- (void) destroySound; + +// Interface sounds; only one at a time +- (BOOL) isBeeping; +- (void) playIdentOn; +- (void) playIdentOff; +- (void) playIdentLockedOn; +- (void) playMissileArmed; +- (void) playMineArmed; +- (void) playMissileSafe; +- (void) playMissileLockedOn; +- (void) playNextMissileSelected; +- (void) playCargoJettisioned; +- (void) playAutopilotOn; +- (void) playAutopilotOff; +- (void) playAutopilotOutOfRange; +- (void) playAutopilotCannotDockWithTarget; +- (void) playSaveOverwriteYes; +- (void) playSaveOverwriteNo; +- (void) playHoldFull; +- (void) playJumpMassLocked; +- (void) playTargetLost; +- (void) playNoTargetInMemory; +- (void) playTargetSwitched; +- (void) playCloakingDeviceOn; +- (void) playCloakingDeviceOff; +- (void) playCloakingDeviceInsufficientEnergy; +- (void) playMenuNavigationUp; +- (void) playMenuNavigationDown; +- (void) playMenuNavigationNot; +- (void) playMenuPagePrevious; +- (void) playMenuPageNext; +- (void) playDismissedReportScreen; +- (void) playDismissedMissionScreen; +- (void) playChangedOption; + +- (void) updateFuelScoopSoundWithInterval:(OOTimeDelta)delta_t; + +- (void) startAfterburnerSound; +- (void) stopAfterburnerSound; + +// Buy/sell get their own source. +- (void) playBuyCommodity; +- (void) playBuyShip; +- (void) playSellCommodity; +- (void) playCantBuyCommodity; +- (void) playCantSellCommodity; +- (void) playCantBuyShip; + +// Hyperspace alert sounds; logically hyperspace sounds, but played on the interface sound source. +- (void) playHyperspaceNoTarget; +- (void) playHyperspaceNoFuel; +- (void) playHyperspaceBlocked; + + +/* Hyperspace sounds; only one at a time. These get their own pool since + people might want something longer than beeps and boops (e.g. the existing + hyperspace countdown one). Hyperspace-related alert sounds are with the + normal interface sounds. +*/ +- (void) playStandardHyperspace; +- (void) playGalacticHyperspace; +- (void) playHyperspaceAborted; + +// ECM; only one at a time +- (void) playHitByECMSound; +- (void) playFiredECMSound; + +- (void) playLaunchFromStation; +- (void) playDockWithStation; +- (void) playExitWitchspace; + +// Warning sounds +- (void) playHostileWarning; +- (void) playAlertConditionRed; +- (void) playIncomingMissile; +- (void) playEnergyLow; +- (void) playDockingDenied; +- (void) playWitchjumpFailure; +- (void) playWitchjumpMisjump; +- (void) playWitchjumpBlocked; +- (void) playWitchjumpDistanceTooGreat; +- (void) playWitchjumpInsufficientFuel; +- (void) playFuelLeak; + +// Damage sounds +- (void) playShieldHit; +- (void) playDirectHit; +- (void) playScrapeDamage; + +// Weapon sounds +- (void) playLaserHit:(BOOL)hit; +- (void) playWeaponOverheated; +- (void) playMissileLaunched; +- (void) playMineLaunched; +- (void) playEnergyBombFired; + +// Miscellaneous sounds +- (void) playEscapePodScooped; +- (void) playAegisCloseToPlanet; +- (void) playAegisCloseToStation; +- (void) playGameOver; + +- (void) playLegacyScriptSound:(NSString *)key; + +@end diff --git a/src/Core/Entities/PlayerEntitySound.m b/src/Core/Entities/PlayerEntitySound.m new file mode 100644 index 00000000..cf50379d --- /dev/null +++ b/src/Core/Entities/PlayerEntitySound.m @@ -0,0 +1,613 @@ +/* + +PlayerEntitySound.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntitySound.h" +#import "OOSound.h" +#import "ResourceManager.h" +#import "Universe.h" +#import "OOSoundSourcePool.h" + + +// Sizes of sound source pools +enum +{ + kWarningPoolSize = 6, + kWeaponPoolSize = 3, + kDamagePoolSize = 4, + kMiscPoolSize = 2 +}; + + +static OOSoundSourcePool *sWarningSoundPool; +static OOSoundSourcePool *sWeaponSoundPool; +static OOSoundSourcePool *sDamageSoundPool; +static OOSoundSourcePool *sMiscSoundPool; +static OOSoundSource *sHyperspaceSoundSource; +static OOSoundSource *sInterfaceBeepSource; +static OOSoundSource *sEcmSource; +static OOSoundSource *sBreakPatternSource; +static OOSoundSource *sBuySellSource; +static OOSoundSource *sAfterburnerSources[2]; + + +@implementation PlayerEntity (Sound) + +- (void) setUpSound +{ + [self destroySound]; + + refPoint = [[OOSoundReferencePoint alloc] init]; + + sInterfaceBeepSource = [[OOSoundSource alloc] init]; + sBreakPatternSource = [[OOSoundSource alloc] init]; + sEcmSource = [[OOSoundSource alloc] init]; + sHyperspaceSoundSource = [[OOSoundSource alloc] init]; + sBuySellSource = [[OOSoundSource alloc] init]; + + sWarningSoundPool = [[OOSoundSourcePool alloc] initWithCount:kWarningPoolSize minRepeatTime:0.0]; + sWeaponSoundPool = [[OOSoundSourcePool alloc] initWithCount:kWeaponPoolSize minRepeatTime:0.0]; + sDamageSoundPool = [[OOSoundSourcePool alloc] initWithCount:kDamagePoolSize minRepeatTime:0.1]; // Repeat time limit is to avoid playing a scrape sound every frame on glancing scrapes. This does limit the number of laser hits that can be played in a furrball, though; maybe lasers and scrapes should use different pools. + sMiscSoundPool = [[OOSoundSourcePool alloc] initWithCount:kMiscPoolSize minRepeatTime:0.0]; + + // Two sources with the same sound are used to simulate looping. + OOSound *afterburnerSound = [ResourceManager ooSoundNamed:@"afterburner1.ogg" inFolder:@"Sounds"]; + sAfterburnerSources[0] = [[OOSoundSource alloc] initWithSound:afterburnerSound]; + sAfterburnerSources[1] = [[OOSoundSource alloc] initWithSound:afterburnerSound]; +} + + +- (void) destroySound +{ + [refPoint release]; + refPoint = nil; + + [sInterfaceBeepSource release]; + sInterfaceBeepSource = nil; + [sBreakPatternSource release]; + sBreakPatternSource = nil; + [sEcmSource release]; + sEcmSource = nil; + [sHyperspaceSoundSource release]; + sHyperspaceSoundSource = nil; + [sBuySellSource release]; + sBuySellSource = nil; + + [sWarningSoundPool release]; + sWarningSoundPool = nil; + [sWeaponSoundPool release]; + sWeaponSoundPool = nil; + [sDamageSoundPool release]; + sDamageSoundPool = nil; +} + + +- (void) playInterfaceBeep:(NSString *)beepKey +{ + [sInterfaceBeepSource playSound:[OOSound soundWithCustomSoundKey:beepKey]]; +} + + +- (BOOL) isBeeping +{ + return [sInterfaceBeepSource isPlaying]; +} + + +- (void) boop +{ + [self playInterfaceBeep:@"[general-boop]"]; +} + + +- (void) playIdentOn +{ + [self playInterfaceBeep:@"[ident-on]"]; +} + + +- (void) playIdentOff +{ + [self playInterfaceBeep:@"[ident-off]"]; +} + + +- (void) playIdentLockedOn +{ + [self playInterfaceBeep:@"[ident-locked-on]"]; +} + + +- (void) playMissileArmed +{ + [self playInterfaceBeep:@"[missile-armed]"]; +} + + +- (void) playMineArmed +{ + [self playInterfaceBeep:@"[mine-armed]"]; +} + + +- (void) playMissileSafe +{ + [self playInterfaceBeep:@"[missile-safe]"]; +} + + +- (void) playMissileLockedOn +{ + [self playInterfaceBeep:@"[missile-locked-on]"]; +} + + +- (void) playNextMissileSelected +{ + [self playInterfaceBeep:@"[next-missile-selected]"]; +} + + +- (void) playCargoJettisioned +{ + [self playInterfaceBeep:@"[cargo-jettisoned]"]; +} + + +- (void) playAutopilotOn +{ + [self playInterfaceBeep:@"[autopilot-on]"]; +} + + +- (void) playAutopilotOff +{ + [self playInterfaceBeep:@"[autopilot-off]"]; +} + + +- (void) playAutopilotOutOfRange +{ + [self playInterfaceBeep:@"[autopilot-out-of-range]"]; +} + + +- (void) playAutopilotCannotDockWithTarget +{ + [self playInterfaceBeep:@"[autopilot-cannot-dock-with-target]"]; +} + + +- (void) playSaveOverwriteYes +{ + [self playInterfaceBeep:@"[save-overwrite-yes]"]; +} + + +- (void) playSaveOverwriteNo +{ + [self playInterfaceBeep:@"[save-overwrite-no]"]; +} + + +- (void) playHoldFull +{ + [self playInterfaceBeep:@"[hold-full]"]; +} + + +- (void) playJumpMassLocked +{ + [self playInterfaceBeep:@"[jump-mass-locked]"]; +} + + +- (void) playTargetLost +{ + [self playInterfaceBeep:@"[target-lost]"]; +} + + +- (void) playNoTargetInMemory +{ + [self playInterfaceBeep:@"[no-target-in-memory]"]; +} + + +- (void) playTargetSwitched +{ + [self playInterfaceBeep:@"[target-switched]"]; +} + + +- (void) playHyperspaceNoTarget +{ + [self playInterfaceBeep:@"[witch-no-target]"]; +} + + +- (void) playHyperspaceNoFuel +{ + [self playInterfaceBeep:@"[witch-no-fuel]"]; +} + + +- (void) playHyperspaceBlocked +{ + [self playInterfaceBeep:@"[witch-blocked-by-@]"]; +} + + +- (void) playCloakingDeviceOn +{ + [self playInterfaceBeep:@"[cloaking-device-on]"]; +} + + +- (void) playCloakingDeviceOff +{ + [self playInterfaceBeep:@"[cloaking-device-off]"]; +} + + +- (void) playMenuNavigationUp +{ + [self playInterfaceBeep:@"[menu-navigation-up]"]; +} + + +- (void) playMenuNavigationDown +{ + [self playInterfaceBeep:@"[menu-navigation-down]"]; +} + + +- (void) playMenuNavigationNot +{ + [self playInterfaceBeep:@"[menu-navigation-not]"]; +} + + +- (void) playMenuPagePrevious +{ + [self playInterfaceBeep:@"[menu-next-page]"]; +} + + +- (void) playMenuPageNext +{ + [self playInterfaceBeep:@"[menu-previous-page]"]; +} + + +- (void) playDismissedReportScreen +{ + [self playInterfaceBeep:@"[dismissed-report-screen]"]; +} + + +- (void) playDismissedMissionScreen +{ + [self playInterfaceBeep:@"[dismissed-mission-screen]"]; +} + + +- (void) playChangedOption +{ + [self playInterfaceBeep:@"[changed-option]"]; +} + + +- (void) updateFuelScoopSoundWithInterval:(OOTimeDelta)delta_t +{ + static double scoopSoundPlayTime = 0.0; + scoopSoundPlayTime -= delta_t; + if (scoopSoundPlayTime < 0.0) + { + [self playInterfaceBeep:@"[scoop]"]; + scoopSoundPlayTime = 0.5; + } +} + + +// time delay method for playing afterburner sounds +// this overlaps two sounds each 2 seconds long, but with a .5s +// crossfade +- (void) updateAfterburnerSound +{ + static uint8_t which = 0; + + if (!afterburner_engaged) // end the loop cycle + { + afterburnerSoundLooping = NO; + } + + if (afterburnerSoundLooping) + { + [sAfterburnerSources[which] play]; + which = !which; + + [self performSelector:@selector(updateAfterburnerSound) + withObject:NULL + afterDelay:1.25]; // and swap sounds in 1.25s time + } +} + + +- (void) startAfterburnerSound +{ + if (!afterburnerSoundLooping) + { + afterburnerSoundLooping = YES; + [self updateAfterburnerSound]; + } +} + + +- (void) stopAfterburnerSound +{ + // Do nothing, stop is detected in updateAfterburnerSound +} + + +- (void) playCloakingDeviceInsufficientEnergy +{ + [self playInterfaceBeep:@"[cloaking-device-insufficent-energy]"]; +} + + +- (void) playBuyCommodity +{ + [sBuySellSource playCustomSoundWithKey:@"[buy-commodity]"]; +} + + +- (void) playBuyShip +{ + [sBuySellSource playCustomSoundWithKey:@"[buy-ship]"]; +} + + +- (void) playSellCommodity +{ + [sBuySellSource playCustomSoundWithKey:@"[sell-commodity]"]; +} + + +- (void) playCantBuyCommodity +{ + [sBuySellSource playCustomSoundWithKey:@"[could-not-buy-commodity]"]; +} + + +- (void) playCantSellCommodity +{ + [sBuySellSource playCustomSoundWithKey:@"[could-not-sell-commodity]"]; +} + + +- (void) playCantBuyShip +{ + [sBuySellSource playCustomSoundWithKey:@"[could-not-buy-ship]"]; +} + + +- (void) playStandardHyperspace +{ + [sHyperspaceSoundSource playCustomSoundWithKey:@"[hyperspace-countdown-begun]"]; +} + + +- (void) playGalacticHyperspace +{ + [sHyperspaceSoundSource playCustomSoundWithKey:@"[galactic-hyperspace-countdown-begun]"]; +} + + +- (void) playHyperspaceAborted +{ + [sHyperspaceSoundSource playCustomSoundWithKey:@"[hyperspace-countdown-aborted]"]; +} + + +- (void) playHitByECMSound +{ + if (![sEcmSource isPlaying]) [sEcmSource playCustomSoundWithKey:@"[player-hit-by-ecm]"]; +} + + +- (void) playFiredECMSound +{ + if (![sEcmSource isPlaying]) [sEcmSource playCustomSoundWithKey:@"[player-fired-ecm]"]; +} + + +- (void) playLaunchFromStation +{ + [sBreakPatternSource playCustomSoundWithKey:@"[player-launch-from-station]"]; +} + + +- (void) playDockWithStation +{ + [sBreakPatternSource playCustomSoundWithKey:@"[player-dock-with-station]"]; +} + + +- (void) playExitWitchspace +{ + [sBreakPatternSource playCustomSoundWithKey:@"[player-exit-witchspace]"]; +} + + +- (void) playHostileWarning +{ + [sWarningSoundPool playSoundWithKey:@"[hostile-warning]" priority:1]; +} + + +- (void) playAlertConditionRed +{ + [sWarningSoundPool playSoundWithKey:@"[alert-condition-red]" priority:2]; +} + + +- (void) playIncomingMissile +{ + [sWarningSoundPool playSoundWithKey:@"[incoming-missile]" priority:3]; +} + + +- (void) playEnergyLow +{ + [sWarningSoundPool playSoundWithKey:@"[energy-low]" priority:0.5]; +} + + +- (void) playDockingDenied +{ + [sWarningSoundPool playSoundWithKey:@"[autopilot-denied]" priority:1]; +} + + +- (void) playWitchjumpFailure +{ + [sWarningSoundPool playSoundWithKey:@"[witchdrive-failure]" priority:1.5]; +} + + +- (void) playWitchjumpMisjump +{ + [sWarningSoundPool playSoundWithKey:@"[witchdrive-malfunction]" priority:1.5]; +} + + +- (void) playWitchjumpBlocked +{ + [sWarningSoundPool playSoundWithKey:@"[witch-blocked-by-@]" priority:1.3]; +} + + +- (void) playWitchjumpDistanceTooGreat +{ + [sWarningSoundPool playSoundWithKey:@"[witch-too-far]" priority:1.3]; +} + + +- (void) playWitchjumpInsufficientFuel +{ + [sWarningSoundPool playSoundWithKey:@"[witch-no-fuel]" priority:1.3]; +} + + +- (void) playFuelLeak +{ + [sWarningSoundPool playSoundWithKey:@"[fuel-leak]" priority:0.5]; +} + + +- (void) playShieldHit +{ + [sDamageSoundPool playSoundWithKey:@"[player-hit-by-weapon]"]; +} + + +- (void) playDirectHit +{ + [sDamageSoundPool playSoundWithKey:@"[player-direct-hit]"]; +} + + +- (void) playScrapeDamage +{ + [sDamageSoundPool playSoundWithKey:@"[player-scrape-damage]"]; +} + + +- (void) playLaserHit:(BOOL)hit +{ + if (hit) + { + [sWeaponSoundPool playSoundWithKey:@"[player-laser-hit]" priority:1 expiryTime:0.05]; + } + else + { + [sWeaponSoundPool playSoundWithKey:@"[player-laser-miss]" priority:1 expiryTime:0.05]; + } +} + + +- (void) playWeaponOverheated +{ + [sWeaponSoundPool playSoundWithKey:@"[weapon-overheat]"]; +} + + +- (void) playMissileLaunched +{ + [sWeaponSoundPool playSoundWithKey:@"[missile-launched]"]; +} + + +- (void) playMineLaunched +{ + [sWeaponSoundPool playSoundWithKey:@"[mine-launched]"]; +} + + +- (void) playEnergyBombFired +{ + [sWeaponSoundPool playSoundWithKey:@"[energy-bomb-fired]" priority:2.0]; +} + + +- (void) playEscapePodScooped +{ + [sMiscSoundPool playSoundWithKey:@"[escape-pod-scooped]"]; +} + + +- (void) playAegisCloseToPlanet +{ + [sMiscSoundPool playSoundWithKey:@"[aegis-planet]"]; +} + + +- (void) playAegisCloseToStation +{ + [sMiscSoundPool playSoundWithKey:@"[aegis-station]"]; +} + + +- (void) playGameOver +{ + [sMiscSoundPool playSoundWithKey:@"[game-over]"]; +} + + +- (void) playLegacyScriptSound:(NSString *)key +{ + [sMiscSoundPool playSoundWithKey:key priority:1.1]; +} + +@end diff --git a/src/Core/Entities/PlayerEntityStickMapper.h b/src/Core/Entities/PlayerEntityStickMapper.h new file mode 100644 index 00000000..0c18cfbf --- /dev/null +++ b/src/Core/Entities/PlayerEntityStickMapper.h @@ -0,0 +1,69 @@ +/* + +PlayerEntityStickMapper.h + +Joystick support for SDL implementation of Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntity.h" +#import "GuiDisplayGen.h" +#import "MyOpenGLView.h" +#import "Universe.h" + +#define MAX_ROWS_FUNCTIONS 12 + +#define GUI_ROW_STICKNAME 1 +#define GUI_ROW_HEADING 3 +#define GUI_ROW_FUNCSTART 4 +#define GUI_ROW_FUNCEND (GUI_ROW_FUNCSTART + MAX_ROWS_FUNCTIONS - 1) +#define GUI_ROW_INSTRUCT 17 + +// Dictionary keys +#define KEY_GUIDESC @"guiDesc" +#define KEY_ALLOWABLE @"allowable" +#define KEY_AXISFN @"axisfunc" +#define KEY_BUTTONFN @"buttonfunc" + +@interface PlayerEntity (StickMapper) + + - (void) setGuiToStickMapperScreen: (unsigned)skip; + - (void) stickMapperInputHandler: (GuiDisplayGen *)gui + view: (MyOpenGLView *)gameView; + // Callback method + - (void) updateFunction: (NSDictionary *)hwDict; + + // internal methods + - (void) removeFunction: (int)selFunctionIdx; + - (NSArray *)getStickFunctionList; + - (void)displayFunctionList: (GuiDisplayGen *)gui + skip: (unsigned) skip; + - (NSString *)describeStickDict: (NSDictionary *)stickDict; + - (NSString *)hwToString: (int)hwFlags; + + // Future: populate via plist + - (NSDictionary *)makeStickGuiDict: (NSString *)what + allowable: (int)allowable + axisfn: (int)axisfn + butfn: (int)butfn; + +@end + diff --git a/src/Core/Entities/PlayerEntityStickMapper.m b/src/Core/Entities/PlayerEntityStickMapper.m new file mode 100644 index 00000000..8fd31f4e --- /dev/null +++ b/src/Core/Entities/PlayerEntityStickMapper.m @@ -0,0 +1,507 @@ +/* + +PlayerEntityStickMapper.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "PlayerEntityStickMapper.h" +#import "PlayerEntityControls.h" +#import "JoystickHandler.h" + +@implementation PlayerEntity (StickMapper) + +- (void) setGuiToStickMapperScreen: (unsigned)skip +{ + GuiDisplayGen *gui=[UNIVERSE gui]; + NSArray *stickList=[stickHandler listSticks]; + unsigned i; + OOGUITabStop tabStop[GUI_MAX_COLUMNS]; + tabStop[0]=50; + tabStop[1]=210; + tabStop[2]=320; + [gui setTabStops: tabStop]; + + gui_screen=GUI_SCREEN_STICKMAPPER; + [gui clear]; + [gui setTitle:[NSString stringWithFormat:@"Configure Joysticks"]]; + + for(i=0; i < [stickList count]; i++) + { + [gui setArray:[NSArray arrayWithObjects: + [NSString stringWithFormat: @"Stick %d", i+1], + [stickList objectAtIndex: i], + nil] + forRow:i + GUI_ROW_STICKNAME]; + } + + [self displayFunctionList: gui skip: skip]; + + [gui setArray:[NSArray arrayWithObject:@"Select a function and press Enter to modify or 'u' to unset."] + forRow:GUI_ROW_INSTRUCT]; + + [gui setSelectedRow: selFunctionIdx + GUI_ROW_FUNCSTART]; + [[UNIVERSE gameView] supressKeysUntilKeyUp]; + +} + +- (void) stickMapperInputHandler: (GuiDisplayGen *)gui + view: (MyOpenGLView *)gameView +{ + // Don't do anything if the user is supposed to be selecting + // a function - other than look for Escape. + if(waitingForStickCallback) + { + if([gameView isDown: 27]) + { + [stickHandler clearCallback]; + [gui setArray: [NSArray arrayWithObjects: + [NSString stringWithString: @"Function setting aborted."], nil] + forRow: GUI_ROW_INSTRUCT]; + waitingForStickCallback=NO; + } + + // Break out now. + return; + } + + [self handleGUIUpDownArrowKeys]; + + NSString* key = [gui keyForRow: [gui selectedRow]]; + if ([key hasPrefix:@"Index:"]) + selFunctionIdx=[[[key componentsSeparatedByString:@":"] objectAtIndex: 1] intValue]; + else + selFunctionIdx=-1; + + if([gameView isDown: 13]) + { + if ([key hasPrefix:@"More:"]) + { + int from_function = [[[key componentsSeparatedByString:@":"] objectAtIndex: 1] intValue]; + if (from_function < 0) from_function = 0; + + [self setGuiToStickMapperScreen:from_function]; + if ([[UNIVERSE gui] selectedRow] < 0) + [[UNIVERSE gui] setSelectedRow: GUI_ROW_FUNCSTART]; + if (from_function == 0) + [[UNIVERSE gui] setSelectedRow: GUI_ROW_FUNCSTART + MAX_ROWS_FUNCTIONS - 1]; + return; + } + + NSDictionary *entry=[stickFunctions objectAtIndex: selFunctionIdx]; + int hw=[(NSNumber *)[entry objectForKey: KEY_ALLOWABLE] intValue]; + [stickHandler setCallback: @selector(updateFunction:) + object: self + hardware: hw]; + + // Print instructions + NSString *instructions; + switch(hw) + { + case HW_AXIS: + instructions = @"Fully deflect the axis you want to use for this function. Esc aborts."; + break; + case HW_BUTTON: + instructions = @"Press the button you want to use for this function. Esc aborts."; + break; + default: + instructions = @"Press the button or deflect the axis you want to use for this function."; + } + [gui setArray: [NSArray arrayWithObjects: instructions, nil] forRow: GUI_ROW_INSTRUCT]; + waitingForStickCallback=YES; + } + + if([gameView isDown: 'u']) + { + if (selFunctionIdx >= 0) [self removeFunction: selFunctionIdx]; + } +} + +// Callback function, called by JoystickHandler when the callback +// is set. The dictionary contains the thing that was pressed/moved. +- (void) updateFunction: (NSDictionary *)hwDict +{ + waitingForStickCallback=NO; + + // Right time and the right place? + if(gui_screen != GUI_SCREEN_STICKMAPPER) + { + NSLog(@"updateFunction: Oops, we weren't expecting that callback"); + return; + } + + // What moved? + int function; + NSDictionary *entry=[stickFunctions objectAtIndex: selFunctionIdx]; + if([(NSNumber *)[hwDict objectForKey: STICK_ISAXIS] boolValue] == YES) + { + function=[(NSNumber *)[entry objectForKey: KEY_AXISFN] intValue]; + if (function == AXIS_THRUST) + { + [stickHandler unsetButtonFunction: BUTTON_INCTHRUST]; + [stickHandler unsetButtonFunction: BUTTON_DECTHRUST]; + } + if (function == AXIS_VIEWX) + { + [stickHandler unsetButtonFunction: BUTTON_VIEWPORT]; + [stickHandler unsetButtonFunction: BUTTON_VIEWSTARBOARD]; + } + if (function == AXIS_VIEWY) + { + [stickHandler unsetButtonFunction: BUTTON_VIEWFORWARD]; + [stickHandler unsetButtonFunction: BUTTON_VIEWAFT]; + } + } + else + { + function=[(NSNumber *)[entry objectForKey: KEY_BUTTONFN] intValue]; + if (function == BUTTON_INCTHRUST || function == BUTTON_DECTHRUST) + [stickHandler unsetAxisFunction: AXIS_THRUST]; + if (function == BUTTON_VIEWPORT || function == BUTTON_VIEWSTARBOARD) + [stickHandler unsetAxisFunction: AXIS_VIEWX]; + if (function == BUTTON_VIEWFORWARD || function == BUTTON_VIEWAFT) + [stickHandler unsetAxisFunction: AXIS_VIEWY]; + } + [stickHandler setFunction: function withDict: hwDict]; + [stickHandler saveStickSettings]; + + // Update the GUI (this will refresh the function list). + unsigned skip; + if (selFunctionIdx < MAX_ROWS_FUNCTIONS) + skip = 0; + else + skip = ((selFunctionIdx - 1) / (MAX_ROWS_FUNCTIONS - 2)) * (MAX_ROWS_FUNCTIONS - 2) + 1; + + [self setGuiToStickMapperScreen: skip]; +} + +- (void) removeFunction: (int)idx +{ + selFunctionIdx=idx; + NSDictionary *entry=[stickFunctions objectAtIndex: idx]; + NSNumber *butfunc=[entry objectForKey: KEY_BUTTONFN]; + NSNumber *axfunc=[entry objectForKey: KEY_AXISFN]; + + // Some things can have either axis or buttons - make sure we clear + // both! + if(butfunc) + { + [stickHandler unsetButtonFunction: [butfunc intValue]]; + } + if(axfunc) + { + [stickHandler unsetAxisFunction: [axfunc intValue]]; + } + [stickHandler saveStickSettings]; + + unsigned skip; + if (selFunctionIdx < MAX_ROWS_FUNCTIONS) + skip = 0; + else + skip = ((selFunctionIdx - 1) / (MAX_ROWS_FUNCTIONS - 2)) * (MAX_ROWS_FUNCTIONS - 2) + 1; + [self setGuiToStickMapperScreen: skip]; +} + +- (void) displayFunctionList: (GuiDisplayGen *)gui + skip: (unsigned) skip +{ + unsigned i; + [gui setColor: [OOColor greenColor] forRow: GUI_ROW_HEADING]; + [gui setArray: [NSArray arrayWithObjects: + @"Function", @"Assigned to", @"Type", nil] + forRow: GUI_ROW_HEADING]; + + if(!stickFunctions) + { + stickFunctions=[self getStickFunctionList]; + } + NSDictionary *assignedAxes=[stickHandler getAxisFunctions]; + NSDictionary *assignedButs=[stickHandler getButtonFunctions]; + + unsigned n_functions = [stickFunctions count]; + int n_rows, start_row, previous = 0; + + if (skip >= n_functions) + skip = n_functions - 1; + + if (n_functions < MAX_ROWS_FUNCTIONS) + { + skip = 0; + previous = 0; + n_rows = MAX_ROWS_FUNCTIONS; + start_row = GUI_ROW_FUNCSTART; + } + else + { + n_rows = MAX_ROWS_FUNCTIONS - 1; + start_row = GUI_ROW_FUNCSTART; + if (skip > 0) + { + n_rows -= 1; + start_row += 1; + if (skip > MAX_ROWS_FUNCTIONS) + previous = skip - (MAX_ROWS_FUNCTIONS - 2); + else + previous = 0; + } + } + + if (n_functions > 0) + { + if (skip > 0) + { + [gui setColor:[OOColor greenColor] forRow:GUI_ROW_FUNCSTART]; + [gui setArray:[NSArray arrayWithObjects:DESC(@"gui-back"), @" <-- ", nil] forRow:GUI_ROW_FUNCSTART]; + [gui setKey:[NSString stringWithFormat:@"More:%d", previous] forRow:GUI_ROW_FUNCSTART]; + } + + for(i=0; i < (n_functions - skip) && (int)i < n_rows; i++) + { + NSString *allowedThings; + NSString *assignment; + NSDictionary *entry=[stickFunctions objectAtIndex: i + skip]; + NSString *axFuncKey=[(NSNumber *)[entry objectForKey: KEY_AXISFN] stringValue]; + NSString *butFuncKey=[(NSNumber *)[entry objectForKey: KEY_BUTTONFN] stringValue]; + int allowable=[(NSNumber *)[entry objectForKey: KEY_ALLOWABLE] intValue]; + switch(allowable) + { + case HW_AXIS: + allowedThings=[NSString stringWithString: @"Axis"]; + assignment=[self describeStickDict: + [assignedAxes objectForKey: axFuncKey]]; + break; + case HW_BUTTON: + allowedThings=[NSString stringWithString: @"Button"]; + assignment=[self describeStickDict: + [assignedButs objectForKey: butFuncKey]]; + break; + default: + allowedThings=[NSString stringWithString: @"Axis/Button"]; + + // axis has priority + assignment=[self describeStickDict: + [assignedAxes objectForKey: axFuncKey]]; + if(!assignment) + assignment=[self describeStickDict: + [assignedButs objectForKey: butFuncKey]]; + } + + // Find out what's assigned for this function currently. + if(!assignment) + assignment=[NSString stringWithString: @" - "]; + + [gui setArray: [NSArray arrayWithObjects: + [entry objectForKey: KEY_GUIDESC], assignment, allowedThings, nil] + forRow: i + start_row]; + //[gui setKey: GUI_KEY_OK forRow: i + start_row]; + [gui setKey: [NSString stringWithFormat: @"Index:%d", i + skip] forRow: i + start_row]; + } + if (i < n_functions - skip) + { + [gui setColor: [OOColor greenColor] forRow: start_row + i]; + [gui setArray: [NSArray arrayWithObjects: DESC(@"gui-more"), @" --> ", nil] forRow: start_row + i]; + [gui setKey: [NSString stringWithFormat: @"More:%d", n_rows + skip] forRow: start_row + i]; + i++; + } + + [gui setSelectableRange: NSMakeRange(GUI_ROW_FUNCSTART, i + start_row - GUI_ROW_FUNCSTART)]; + } + +} + +- (NSString *) describeStickDict: (NSDictionary *)stickDict +{ + NSString *desc=nil; + if(stickDict) + { + int thingNumber=[(NSNumber *)[stickDict objectForKey: STICK_AXBUT] + intValue]; + int stickNumber=[(NSNumber *)[stickDict objectForKey: STICK_NUMBER] + intValue]; + // Button or axis? + if([(NSNumber *)[stickDict objectForKey: STICK_ISAXIS] boolValue]) + { + desc=[NSString stringWithFormat: @"Stick %d axis %d", + stickNumber+1, thingNumber+1]; + } + else if(thingNumber >= MAX_REAL_BUTTONS) + { + static const char dir[][6] = { "up", "right", "down", "left" }; + desc=[NSString stringWithFormat: @"Stick %d hat %d %s", + stickNumber+1, (thingNumber - MAX_REAL_BUTTONS) / 4 + 1, + dir[thingNumber & 3]]; + } + else + { + desc=[NSString stringWithFormat: @"Stick %d button %d", + stickNumber+1, thingNumber+1]; + } + } + return desc; +} + +- (NSString *)hwToString: (int)hwFlags +{ + NSString *hwString; + switch(hwFlags) + { + case HW_AXIS: + hwString=[NSString stringWithString: @"axis"]; + break; + case HW_BUTTON: + hwString=[NSString stringWithString: @"button"]; + break; + default: + hwString=[NSString stringWithString: @"axis/button"]; + } + return hwString; +} + +// TODO: This data could be put into a plist (i18n or just modifiable by +// the user). It is otherwise an ugly method, but it'll do for testing. +- (NSArray *)getStickFunctionList +{ + NSMutableArray *funcList=[[NSMutableArray alloc] init]; + + [funcList addObject: + [self makeStickGuiDict: @"Roll" + allowable: HW_AXIS + axisfn: AXIS_ROLL + butfn: STICK_NOFUNCTION]]; + [funcList addObject: + [self makeStickGuiDict: @"Pitch" + allowable: HW_AXIS + axisfn: AXIS_PITCH + butfn: STICK_NOFUNCTION]]; + [funcList addObject: + [self makeStickGuiDict: @"Yaw" + allowable: HW_AXIS + axisfn: AXIS_YAW + butfn: STICK_NOFUNCTION]]; + [funcList addObject: + [self makeStickGuiDict: @"Increase thrust" + allowable: HW_AXIS|HW_BUTTON + axisfn: AXIS_THRUST + butfn: BUTTON_INCTHRUST]]; + [funcList addObject: + [self makeStickGuiDict: @"Decrease thrust" + allowable: HW_AXIS|HW_BUTTON + axisfn: AXIS_THRUST + butfn: BUTTON_DECTHRUST]]; + [funcList addObject: + [self makeStickGuiDict: @"Primary weapon" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_FIRE]]; + [funcList addObject: + [self makeStickGuiDict: @"Secondary weapon" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_LAUNCHMISSILE]]; + [funcList addObject: + [self makeStickGuiDict: @"Arm secondary" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_ARMMISSILE]]; + [funcList addObject: + [self makeStickGuiDict: @"Disarm secondary" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_UNARM]]; +#if TARGET_INCOMING_MISSILES + [funcList addObject: + [self makeStickGuiDict: @"Target nearest incoming missile" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_TARGETINCOMINGMISSILE]]; +#endif + [funcList addObject: + [self makeStickGuiDict: @"Cycle secondary" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_CYCLEMISSILE]]; + [funcList addObject: + [self makeStickGuiDict: @"ECM" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_ECM]]; + [funcList addObject: + [self makeStickGuiDict: @"Toggle ID" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_ID]]; + [funcList addObject: + [self makeStickGuiDict: @"Fuel Injection" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_FUELINJECT]]; + [funcList addObject: + [self makeStickGuiDict: @"Hyperspeed" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_HYPERSPEED]]; + [funcList addObject: + [self makeStickGuiDict: @"Roll/pitch precision toggle" + allowable: HW_BUTTON + axisfn: STICK_NOFUNCTION + butfn: BUTTON_PRECISION]]; + [funcList addObject: + [self makeStickGuiDict: @"View Forward" + allowable: HW_AXIS|HW_BUTTON + axisfn: AXIS_VIEWY + butfn: BUTTON_VIEWFORWARD]]; + [funcList addObject: + [self makeStickGuiDict: @"View Aft" + allowable: HW_AXIS|HW_BUTTON + axisfn: AXIS_VIEWY + butfn: BUTTON_VIEWAFT]]; + [funcList addObject: + [self makeStickGuiDict: @"View Port" + allowable: HW_AXIS|HW_BUTTON + axisfn: AXIS_VIEWX + butfn: BUTTON_VIEWPORT]]; + [funcList addObject: + [self makeStickGuiDict: @"View Starboard" + allowable: HW_AXIS|HW_BUTTON + axisfn: AXIS_VIEWX + butfn: BUTTON_VIEWSTARBOARD]]; + return funcList; +} + +- (NSDictionary *)makeStickGuiDict: (NSString *)what + allowable: (int)allowable + axisfn: (int)axisfn + butfn: (int)butfn +{ + NSMutableDictionary *guiDict=[[NSMutableDictionary alloc] init]; + [guiDict setObject: what forKey: KEY_GUIDESC]; + [guiDict setObject: [NSNumber numberWithInt: allowable] + forKey: KEY_ALLOWABLE]; + if(axisfn >= 0) + [guiDict setObject: [NSNumber numberWithInt: axisfn] + forKey: KEY_AXISFN]; + if(butfn >= 0) + [guiDict setObject: [NSNumber numberWithInt: butfn] + forKey: KEY_BUTTONFN]; + return guiDict; +} + +@end + diff --git a/src/Core/Entities/RingEntity.h b/src/Core/Entities/RingEntity.h new file mode 100644 index 00000000..90e26fa8 --- /dev/null +++ b/src/Core/Entities/RingEntity.h @@ -0,0 +1,40 @@ +/* + +RingEntity.h + +Entity implementing tunnel effect for hyperspace and stations. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOSelfDrawingEntity.h" + + +#define RING_SPEED 200.0 + + +@interface RingEntity : OOSelfDrawingEntity +{ + double lifetime; +} + +- (void) setLifetime:(double) amount; + +@end diff --git a/src/Core/Entities/RingEntity.m b/src/Core/Entities/RingEntity.m new file mode 100644 index 00000000..d7add151 --- /dev/null +++ b/src/Core/Entities/RingEntity.m @@ -0,0 +1,158 @@ +/* + +RingEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "RingEntity.h" + +#import "Universe.h" + +@implementation RingEntity + +// the vertex array data... +typedef struct +{ + Vector vertex_array[64]; + GLfloat color_array[4*64]; + GLuint triangle_index_array[3*64]; +} Ringdata; + +Ringdata ringentity; + + +- (void) setLifetime:(double) amount +{ + lifetime = amount; +} + +- (id) init +{ + self = [super init]; + + [self setModelName:@"ring.dat"]; + + // initialise the vertex arrays + // + int i; + int ti = 0; + GLfloat amb_diff1[] = { 1.0, 0.0, 0.0, 0.5}; + GLfloat amb_diff2[] = { 0.0, 0.0, 1.0, 0.25}; + for (i = 0; i < 64; i++) + { + ringentity.vertex_array[i] = vertices[i]; + ringentity.triangle_index_array[ti++] = faces[i].vertex[0]; + ringentity.triangle_index_array[ti++] = faces[i].vertex[1]; + ringentity.triangle_index_array[ti++] = faces[i].vertex[2]; + if (vertices[i].z < -20.0) + { + ringentity.color_array[i*4+0] = amb_diff1[0]; + ringentity.color_array[i*4+1] = amb_diff1[1]; + ringentity.color_array[i*4+2] = amb_diff1[2]; + ringentity.color_array[i*4+3] = amb_diff1[3]; + } + else + { + ringentity.color_array[i*4+0] = amb_diff2[0]; + ringentity.color_array[i*4+1] = amb_diff2[1]; + ringentity.color_array[i*4+2] = amb_diff2[2]; + ringentity.color_array[i*4+3] = amb_diff2[3]; + } + } + + lifetime = 50.0; + [self setStatus:STATUS_EFFECT]; + + velocity.x = 0.0; + velocity.y = 0.0; + velocity.z = 1.0; + + isRing = YES; + isImmuneToBreakPatternHide = YES; + + return self; +} + +- (void) update:(OOTimeDelta) delta_t +{ + [super update:delta_t]; + + double movement = RING_SPEED * delta_t; + position = vector_subtract(position, vector_multiply_scalar(velocity, movement)); // swap out for setting a velocity vector + lifetime -= movement; + if (lifetime < 0.0) + { + [UNIVERSE removeEntity:self]; + } +} + +- (void) drawEntity:(BOOL)immediate :(BOOL)translucent +{ + glShadeModel(GL_SMOOTH); + glDisable(GL_LIGHTING); + + if (translucent || immediate) + { + if (basefile) + { + if (immediate) + { + glEnableClientState(GL_VERTEX_ARRAY); + glVertexPointer( 3, GL_FLOAT, 0, ringentity.vertex_array); + // 3 coords per vertex + // of type GL_FLOAT + // 0 stride (tightly packed) + // pointer to first vertex + + glEnableClientState(GL_COLOR_ARRAY); + glColorPointer( 4, GL_FLOAT, 0, ringentity.color_array); + // 4 values per vertex color + // of type GL_FLOAT + // 0 stride (tightly packed) + // pointer to quadruplet + + glDisableClientState(GL_NORMAL_ARRAY); + glDisableClientState(GL_INDEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_EDGE_FLAG_ARRAY); + + glDrawElements( GL_TRIANGLES, 3 * 64, GL_UNSIGNED_INT, ringentity.triangle_index_array); + } + else + { + if (displayListName != 0) + glCallList(displayListName); + else + [self generateDisplayList]; + } + } + } + glEnable(GL_LIGHTING); + CheckOpenGLErrors(@"RingEntity after drawing %@", self); +} + + +- (BOOL) canCollide +{ + return NO; +} + +@end diff --git a/src/Core/Entities/ShipEntity.h b/src/Core/Entities/ShipEntity.h new file mode 100644 index 00000000..04d7ee3f --- /dev/null +++ b/src/Core/Entities/ShipEntity.h @@ -0,0 +1,814 @@ +/* + +ShipEntity.h + +Entity subclass representing a ship, or various other flying things like cargo +pods and stations (a subclass). + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOEntityWithDrawable.h" + +@class OOColor, StationEntity, ParticleEntity, PlanetEntity, WormholeEntity, + AI, Octree, OOMesh, OOScript, OORoleSet, OOShipGroup, OOEquipmentType; + +#ifdef OO_BRAIN_AI +@class OOBrain; +#endif + + +#define MAX_TARGETS 24 +#define RAIDER_MAX_CARGO 5 +#define MERCHANTMAN_MAX_CARGO 125 + +#define LAUNCH_DELAY 2.0f + +#define PIRATES_PREFER_PLAYER YES + +#define TURRET_MINIMUM_COS 0.20f + +#define AFTERBURNER_BURNRATE 0.25f +#define AFTERBURNER_NPC_BURNRATE 1.0f +#define AFTERBURNER_TIME_PER_FUEL 4.0f + +#define CLOAKING_DEVICE_ENERGY_RATE 12.8f +#define CLOAKING_DEVICE_MIN_ENERGY 128 +#define CLOAKING_DEVICE_START_ENERGY 0.75f + +#define MILITARY_JAMMER_ENERGY_RATE 3 +#define MILITARY_JAMMER_MIN_ENERGY 128 + +#define COMBAT_IN_RANGE_FACTOR 0.035f +#define COMBAT_OUT_RANGE_FACTOR 0.500f +#define COMBAT_WEAPON_RANGE_FACTOR 1.200f + +#define SHIP_COOLING_FACTOR 1.0f +#define SHIP_INSULATION_FACTOR 0.00175f +#define SHIP_MAX_CABIN_TEMP 256.0f +#define SHIP_MIN_CABIN_TEMP 60.0f +#define EJECTA_TEMP_FACTOR 0.85f // Ejected items have 85% of parent's temperature +#define DEFAULT_HYPERSPACE_SPIN_TIME 15.0f + +#define SUN_TEMPERATURE 1250.0f + +#define MAX_ESCORTS 16 +#define ESCORT_SPACING_FACTOR 3.0 + +#define SHIPENTITY_MAX_MISSILES 16 + +#define TURRET_SHOT_SPEED 2000.0f + +#define TRACTOR_FORCE 2500.0f + +#define AIMS_AGGRESSOR_SWITCHED_TARGET @"AGGRESSOR_SWITCHED_TARGET" + +// number of vessels considered when scanning around +#define MAX_SCAN_NUMBER 16 + +#define BASELINE_SHIELD_LEVEL 128.0f // Max shield level with no boosters. + +#define MIN_FUEL 0 // minimum fuel required for afterburner use + +@interface ShipEntity: OOEntityWithDrawable +{ +@public + // derived variables + OOTimeDelta shot_time; // time elapsed since last shot was fired + + // navigation + Vector v_forward, v_up, v_right; // unit vectors derived from the direction faced + + // variables which are controlled by instincts/AI + Vector destination; // for flying to/from a set point + OOUniversalID primaryTarget; // for combat or rendezvous + GLfloat desired_range; // range to which to journey/scan + GLfloat desired_speed; // speed at which to travel + OOBehaviour behaviour; // ship's behavioural state + + BoundingBox totalBoundingBox; // records ship configuration + +@protected + //set-up + NSDictionary *shipinfoDictionary; + + Quaternion subentityRotationalVelocity; + + //scripting + OOScript *script; + + //docking instructions + NSDictionary *dockingInstructions; + + OOUniversalID last_escort_target; // last target an escort was deployed after + unsigned found_hostiles; // number of hostiles found + + OOColor *laser_color; + + // per ship-type variables + // + GLfloat maxFlightSpeed; // top speed (160.0 for player) (200.0 for fast raider) + GLfloat max_flight_roll; // maximum roll rate (2.0 for player) (3.0 for fast raider) + GLfloat max_flight_pitch; // maximum pitch rate (1.0 for player) (1.5 for fast raider) also radians/sec for (* turrets *) + GLfloat max_flight_yaw; + GLfloat cruiseSpeed; // 80% of top speed + + GLfloat thrust; // acceleration + float hyperspaceMotorSpinTime; // duration of hyperspace countdown + + // TODO: stick all equipment in a list, and move list from playerEntity to shipEntity. -- Ahruman + unsigned military_jammer_active: 1, // military_jammer + + docking_match_rotation: 1, + + + pitching_over: 1, // set to YES if executing a sharp loop + reportAIMessages: 1, // normally NO, suppressing AI message reporting + + being_mined: 1, // normally NO, set to Yes when fired on by mining laser + + being_fined: 1, + + isHulk: 1, // This is used to distinguish abandoned ships from cargo + trackCloseContacts: 1, + + isNearPlanetSurface: 1, // check for landing on planet + isFrangible: 1, // frangible => subEntities can be damaged individually + cloaking_device_active: 1, // cloaking_device + cloakPassive: 1, // cloak deactivates when main weapons or missiles are fired + canFragment: 1, // Can it break into wreckage? + suppressExplosion: 1, // Avoid exploding on death (script hook) + suppressAegisMessages: 1, // No script/AI messages sent by -checkForAegis, + isMissile: 1, // Whether this was launched by fireMissile (used to track submunitions). + isUnpiloted: 1, // Is meant to not have crew + + // scripting + haveExecutedSpawnAction: 1, + noRocks: 1; + + OOFuelQuantity fuel; // witch-space fuel + GLfloat fuel_accumulator; + + OOCargoQuantity likely_cargo; // likely amount of cargo (for merchantmen, this is what is spilled as loot) + OOCargoQuantity max_cargo; // capacity of cargo hold + OOCargoQuantity extra_cargo; // capacity of cargo hold extension (if any) + OOCargoType cargo_type; // if this is scooped, this is indicates contents + OOCargoFlag cargo_flag; // indicates contents for merchantmen + OOCreditsQuantity bounty; // bounty (if any) + + GLfloat energy_recharge_rate; // recharge rate for energy banks + + OOWeaponType forward_weapon_type; // type of forward weapon (allows lasers, plasma cannon, others) + OOWeaponType aft_weapon_type; // type of aft weapon (allows lasers, plasma cannon, others) + GLfloat weapon_energy; // energy used/delivered by weapon + GLfloat weaponRange; // range of the weapon (in meters) + + GLfloat scannerRange; // typically 25600 + + unsigned missiles; // number of on-board missiles + NSString *missileRole; + +#ifdef OO_BRAIN_AI + OOBrain *brain; // brain controlling ship, could be a character brain or the autopilot +#endif + AI *shipAI; // ship's AI system + + NSString *name; // descriptive name + NSString *displayName; // name shown on screen + OORoleSet *roleSet; // Roles a ship can take, eg. trader, hunter, police, pirate, scavenger &c. + NSString *primaryRole; // "Main" role of the ship. + + // AI stuff + Vector jink; // x and y set factors for offsetting a pursuing ship's position + Vector coordinates; // for flying to/from a set point + Vector reference; // a direction vector of magnitude 1 (* turrets *) + OOUniversalID primaryAggressor; // recorded after an attack + OOUniversalID targetStation; // for docking + OOUniversalID found_target; // from scans + OOUniversalID target_laser_hit; // u-id for the entity hit by the last laser shot + OOUniversalID owner_id; // u-id for the controlling owner of this entity (* turrets *) + double launch_time; // time at which launched + + GLfloat frustration, // degree of dissatisfaction with the current behavioural state, factor used to test this + success_factor; + + int patrol_counter; // keeps track of where the ship is along a patrol route + + OOUniversalID proximity_alert; // id of a ShipEntity within 2x collision_radius + NSMutableDictionary *previousCondition; // restored after collision avoidance + + // derived variables + float weapon_recharge_rate; // time between shots + int shot_counter; // number of shots fired + double cargo_dump_time; // time cargo was last dumped + + NSMutableArray *cargo; // cargo containers go in here + + int commodity_type; // type of commodity in a container + int commodity_amount; // 1 if unit is TONNES (0), possibly more if precious metals KILOGRAMS (1) + // or gem stones GRAMS (2) + + // navigation + GLfloat flightSpeed; // current speed + GLfloat flightRoll; // current roll rate + GLfloat flightPitch; // current pitch rate + GLfloat flightYaw; // current yaw rate + + float accuracy; + float pitch_tolerance; + + OOAegisStatus aegis_status; // set to YES when within the station's protective zone + + + double messageTime; // counts down the seconds a radio message is active for + + double next_spark_time; // time of next spark when throwing sparks + + int thanked_ship_id; // last ship thanked + + Vector collision_vector; // direction of colliding thing. + + // beacons + NSString *beaconCode; + char beaconChar; // character displayed for this beacon + OOUniversalID nextBeaconID; // next beacon in sequence + + //position of gun ports + Vector forwardWeaponOffset, + aftWeaponOffset, + portWeaponOffset, + starboardWeaponOffset; + + // crew (typically one OOCharacter - the pilot) + NSArray *crew; + + // close contact / collision tracking + NSMutableDictionary *closeContactsInfo; + + NSString *lastRadioMessage; + + // scooping... + Vector tractor_position; + + // from player entity moved here now we're doing more complex heat stuff + float ship_temperature; + + // for advanced scanning etc. + ShipEntity* scanned_ships[MAX_SCAN_NUMBER + 1]; + GLfloat distance2_scanned_ships[MAX_SCAN_NUMBER + 1]; + unsigned n_scanned_ships; + + // advanced navigation + Vector navpoints[32]; + unsigned next_navpoint_index; + unsigned number_of_navpoints; + + // Collision detection + Octree *octree; + +#ifndef NDEBUG + // DEBUGGING + OOBehaviour debugLastBehaviour; +#endif + + uint16_t entity_personality; // Per-entity random number. Exposed to shaders and scripts. + NSDictionary *scriptInfo; // script_info dictionary from shipdata.plist, exposed to scripts. + + NSMutableArray *subEntities; + +@private + OOWeakReference *_subEntityTakingDamage; // frangible => subEntities can be damaged individually + + NSMutableSet *_equipment; + float _heatInsulation; + + OOWeakReference *_lastPlanet; // remember last aegis planet + + OOShipGroup *_group; + OOShipGroup *_escortGroup; + uint8_t _maxEscortCount; + uint8_t _pendingEscortCount; +} + +// ship brains +- (void) setStateMachine:(NSString *) ai_desc; +- (void) setAI:(AI *) ai; +- (AI *) getAI; +- (void) setShipScript:(NSString *) script_name; +- (OOScript *) shipScript; +- (double) frustration; + +- (void) interpretAIMessage:(NSString *)message; + +#ifdef OO_BRAIN_AI +- (OOBrain *)brain; +- (void)setBrain:(OOBrain*) aBrain; +#endif + +- (OOMesh *)mesh; +- (void)setMesh:(OOMesh *)mesh; + +- (NSArray *)subEntities; +- (unsigned) subEntityCount; +- (BOOL) hasSubEntity:(ShipEntity *)sub; + +- (NSEnumerator *)subEntityEnumerator; +- (NSEnumerator *)shipSubEntityEnumerator; +- (NSEnumerator *)particleSubEntityEnumerator; +- (NSEnumerator *)flasherEnumerator; +- (NSEnumerator *)exhaustEnumerator; + +- (ShipEntity *) subEntityTakingDamage; +- (void) setSubEntityTakingDamage:(ShipEntity *)sub; + +- (void) clearSubEntities; // Releases and clears subentity array, after making sure subentities don't think ship is owner. + +// octree collision hunting +- (GLfloat)doesHitLine:(Vector) v0: (Vector) v1; +- (GLfloat)doesHitLine:(Vector) v0: (Vector) v1 :(ShipEntity**) hitEntity; +- (GLfloat)doesHitLine:(Vector) v0: (Vector) v1 withPosition:(Vector) o andIJK:(Vector) i :(Vector) j :(Vector) k; // for subentities + +- (BoundingBox) findBoundingBoxRelativeToPosition:(Vector)opv InVectors:(Vector) _i :(Vector) _j :(Vector) _k; + +- (Vector)absoluteTractorPosition; + +// beacons +- (NSString *)beaconCode; +- (void)setBeaconCode:(NSString *)bcode; +- (BOOL)isBeacon; +- (char)beaconChar; +- (int)nextBeaconID; +- (void)setNextBeacon:(ShipEntity*) beaconShip; + +- (void) setUpEscorts; + +- (id)initWithDictionary:(NSDictionary *) dict; +- (BOOL)setUpShipFromDictionary:(NSDictionary *) dict; +- (BOOL)setUpSubEntities:(NSDictionary *) shipDict; +- (NSDictionary *)shipInfoDictionary; + +- (void) setDefaultWeaponOffsets; + +- (BOOL)isFrangible; + +- (void)respondToAttackFrom:(Entity *)from becauseOf:(Entity *)other; + +// Equipment +- (BOOL) hasEquipmentItem:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons; // This can take a string or an set or array of strings. If a collection, returns YES if ship has _any_ of the specified equipment. If includeWeapons is NO, missiles and primary weapons are not checked. +- (BOOL) hasEquipmentItem:(id)equipmentKeys; // Short for hasEquipmentItem:foo includeWeapons:NO +- (BOOL) hasAllEquipment:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons; // Like hasEquipmentItem:includeWeapons:, but requires _all_ elements in collection. +- (BOOL) hasAllEquipment:(id)equipmentKeys; // Short for hasAllEquipment:foo includeWeapons:NO +- (BOOL) canAddEquipment:(NSString *)equipmentKey; // Test ability to add equipment, taking equipment-specific constriants into account. +- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey; // Actual test if equipment satisfies validation criteria. +- (void) addEquipmentItem:(NSString *)equipmentKey; +- (void) addEquipmentItem:(NSString *)equipmentKey withValidation:(BOOL)validateAddition; +/* NOTE: for legacy reasons, canAddEquipment: returns YES if given a missile + or mine type, but addEquipmentItem: does nothing in those cases. This + should probably be cleaned up by making addEquipmentItem: mount stores. +*/ +- (NSEnumerator *) equipmentEnumerator; +- (unsigned) equipmentCount; +- (void) removeEquipmentItem:(NSString *)equipmentKey; +- (void) removeAllEquipment; + +// Internal, subject to change. Use the methods above instead. +- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeMissiles:(BOOL)includeMissiles; +- (BOOL) hasPrimaryWeapon:(OOWeaponType)weaponType; +- (void) removeExternalStore:(OOEquipmentType *)eqType; + +// Passengers - not supported for NPCs, but interface is here for genericity. +- (unsigned) passengerCount; +- (unsigned) passengerCapacity; + +- (unsigned) missileCount; +- (unsigned) missileCapacity; + +// Tests for the various special-cased equipment items +- (BOOL) hasScoop; +- (BOOL) hasECM; +- (BOOL) hasCloakingDevice; +- (BOOL) hasMilitaryScannerFilter; +- (BOOL) hasMilitaryJammer; +- (BOOL) hasExpandedCargoBay; +- (BOOL) hasShieldBooster; +- (BOOL) hasMilitaryShieldEnhancer; +- (BOOL) hasHeatShield; +- (BOOL) hasFuelInjection; +- (BOOL) hasEnergyBomb; +- (BOOL) hasEscapePod; +- (BOOL) hasDockingComputer; +- (BOOL) hasGalacticHyperdrive; + +// Shield information derived from equipment. NPCs can't have shields, but that should change at some point. +- (float) shieldBoostFactor; +- (float) maxForwardShieldLevel; +- (float) maxAftShieldLevel; +- (float) shieldRechargeRate; + +- (float) afterburnerFactor; + +// Behaviours +- (void) behaviour_stop_still:(double) delta_t; +- (void) behaviour_idle:(double) delta_t; +- (void) behaviour_tumble:(double) delta_t; +- (void) behaviour_tractored:(double) delta_t; +- (void) behaviour_track_target:(double) delta_t; +- (void) behaviour_intercept_target:(double) delta_t; +- (void) behaviour_attack_target:(double) delta_t; +- (void) behaviour_fly_to_target_six:(double) delta_t; +- (void) behaviour_attack_mining_target:(double) delta_t; +- (void) behaviour_attack_fly_to_target:(double) delta_t; +- (void) behaviour_attack_fly_from_target:(double) delta_t; +- (void) behaviour_running_defense:(double) delta_t; +- (void) behaviour_flee_target:(double) delta_t; +- (void) behaviour_fly_range_from_destination:(double) delta_t; +- (void) behaviour_face_destination:(double) delta_t; +- (void) behaviour_formation_form_up:(double) delta_t; +- (void) behaviour_fly_to_destination:(double) delta_t; +- (void) behaviour_fly_from_destination:(double) delta_t; +- (void) behaviour_avoid_collision:(double) delta_t; +- (void) behaviour_track_as_turret:(double) delta_t; +- (void) behaviour_fly_thru_navpoints:(double) delta_t; + + +- (void) resetTracking; + +- (GLfloat *) scannerDisplayColorForShip:(ShipEntity*)otherShip :(BOOL)isHostile :(BOOL)flash; + +- (BOOL)isCloaked; +- (void)setCloaked:(BOOL)cloak; + +- (BOOL) isJammingScanning; + +- (void) addSubEntity:(Entity *) subent; +- (void) addExhaust:(ParticleEntity *) exhaust; +- (void) addFlasher:(ParticleEntity *) flasher; + +- (void) applyThrust:(double) delta_t; + +- (void) avoidCollision; +- (void) resumePostProximityAlert; + +- (double) messageTime; +- (void) setMessageTime:(double) value; + +//- (int) groupID; +//- (void) setGroupID:(int) value; + +- (OOShipGroup *) group; +- (void) setGroup:(OOShipGroup *)group; + +- (OOShipGroup *) escortGroup; +- (OOShipGroup *) stationGroup; // should probably be defined in stationEntity.m + +- (BOOL) hasEscorts; +- (NSEnumerator *) escortEnumerator; +- (NSArray *) escortArray; + +- (uint8_t) escortCount; + +// Pending escort count: number of escorts to set up "later". +- (uint8_t) pendingEscortCount; +- (void) setPendingEscortCount:(uint8_t)count; + +- (ShipEntity *) proximity_alert; +- (void) setProximity_alert:(ShipEntity*) other; + +- (NSString *) name; +- (NSString *) displayName; +- (void) setName:(NSString *)inName; +- (void) setDisplayName:(NSString *)inName; +- (NSString *) identFromShip:(ShipEntity*) otherShip; // name displayed to other ships + +- (BOOL) hasRole:(NSString *)role; +- (OORoleSet *)roleSet; + +- (NSString *)primaryRole; +- (void)setPrimaryRole:(NSString *)role; +- (BOOL)hasPrimaryRole:(NSString *)role; + +- (BOOL)isPolice; // Scan class is CLASS_POLICE +- (BOOL)isThargoid; // Scan class is CLASS_THARGOID +- (BOOL)isTrader; // Primary role is "trader" || isPlayer +- (BOOL)isPirate; // Primary role is "pirate" +- (BOOL)isMissile; // Primary role has suffix "MISSILE" +- (BOOL)isMine; // Primary role has suffix "MINE" +- (BOOL)isWeapon; // isMissile || isWeapon +- (BOOL)isEscort; // Primary role is "escort" or "wingman" +- (BOOL)isShuttle; // Primary role is "shuttle" +- (BOOL)isPirateVictim; // Primary role is listed in pirate-victim-roles.plist +- (BOOL)isUnpiloted; // Has unpiloted = yes in its shipdata.plist entry + +- (BOOL) hasHostileTarget; + +- (GLfloat) weaponRange; +- (void) setWeaponRange:(GLfloat) value; +- (void) setWeaponDataFromType:(OOWeaponType)weapon_type; +- (float) weaponRechargeRate; +- (void) setWeaponRechargeRate:(float)value; + +- (GLfloat) scannerRange; +- (void) setScannerRange: (GLfloat) value; + +- (Vector) reference; +- (void) setReference:(Vector) v; + +- (BOOL) reportAIMessages; +- (void) setReportAIMessages:(BOOL) yn; + +- (void) transitionToAegisNone; +- (PlanetEntity *) findNearestPlanet; +- (PlanetEntity *) findNearestStellarBody; // NOTE: includes sun. +- (PlanetEntity *) findNearestPlanetExcludingMoons; +- (OOAegisStatus) checkForAegis; +- (BOOL) withinStationAegis; + +- (NSArray*) crew; +- (void) setCrew: (NSArray*) crewArray; + +// Fuel and capacity in tenths of light-years. +- (OOFuelQuantity) fuel; +- (void) setFuel:(OOFuelQuantity) amount; +- (OOFuelQuantity) fuelCapacity; + +- (void) setRoll:(double) amount; +- (void) setPitch:(double) amount; + +- (void)setThrustForDemo:(float)factor; + +- (void) setBounty:(OOCreditsQuantity) amount; +- (OOCreditsQuantity) bounty; + +- (int) legalStatus; + +- (void) setCommodity:(OOCargoType)co_type andAmount:(OOCargoQuantity)co_amount; +- (OOCargoType) commodityType; +- (OOCargoQuantity) commodityAmount; + +- (OOCargoQuantity) maxCargo; +- (OOCargoQuantity) availableCargoSpace; +- (OOCargoType) cargoType; +- (NSMutableArray *) cargo; +- (void) setCargo:(NSArray *) some_cargo; + +- (OOCargoFlag) cargoFlag; +- (void) setCargoFlag:(OOCargoFlag) flag; + +- (void) setSpeed:(double) amount; +- (double) desiredSpeed; +- (void) setDesiredSpeed:(double) amount; + +- (void) increase_flight_speed:(double) delta; +- (void) decrease_flight_speed:(double) delta; +- (void) increase_flight_roll:(double) delta; +- (void) decrease_flight_roll:(double) delta; +- (void) increase_flight_pitch:(double) delta; +- (void) decrease_flight_pitch:(double) delta; +- (void) increase_flight_yaw:(double) delta; +- (void) decrease_flight_yaw:(double) delta; + +- (GLfloat) flightRoll; +- (GLfloat) flightPitch; +- (GLfloat) flightYaw; +- (GLfloat) flightSpeed; +- (GLfloat) maxFlightSpeed; +- (GLfloat) speedFactor; + +- (GLfloat) temperature; +- (void) setTemperature:(GLfloat) value; +- (GLfloat) heatInsulation; +- (void) setHeatInsulation:(GLfloat) value; + +// the percentage of damage taken (100 is destroyed, 0 is fine) +- (int) damage; + +- (void) dealEnergyDamageWithinDesiredRange; +- (void) dealMomentumWithinDesiredRange:(double)amount; + +- (void) getDestroyedBy:(Entity *)whom context:(NSString *)why; +- (void) becomeExplosion; +- (void) becomeLargeExplosion:(double) factor; +- (void) becomeEnergyBlast; +Vector randomPositionInBoundingBox(BoundingBox bb); + +- (Vector) positionOffsetForAlignment:(NSString*) align; +Vector positionOffsetForShipInRotationToAlignment(ShipEntity* ship, Quaternion q, NSString* align); + +- (void) collectBountyFor:(ShipEntity *)other; + +- (BoundingBox) findSubentityBoundingBox; + +- (Vector) absolutePositionForSubentity; +- (Vector) absolutePositionForSubentityOffset:(Vector) offset; + +- (Triangle) absoluteIJKForSubentity; + +- (void) addSolidSubentityToCollisionRadius:(ShipEntity *)subent; + +ShipEntity *doOctreesCollide(ShipEntity *prime, ShipEntity *other); + +- (NSComparisonResult) compareBeaconCodeWith:(ShipEntity *)other; + +- (GLfloat)laserHeatLevel; +- (GLfloat)hullHeatLevel; +- (GLfloat)entityPersonality; +- (GLint)entityPersonalityInt; + +- (void)setSuppressExplosion:(BOOL)suppress; + +/*----------------------------------------- + + AI piloting methods + +-----------------------------------------*/ + +BOOL class_masslocks(int some_class); +- (BOOL) checkTorusJumpClear; + +- (void) checkScanner; +- (ShipEntity**) scannedShips; +- (int) numberOfScannedShips; + +- (void) setFound_target:(Entity *) targetEntity; +- (void) setPrimaryAggressor:(Entity *) targetEntity; +- (void) addTarget:(Entity *) targetEntity; +- (void) removeTarget:(Entity *) targetEntity; +- (id) primaryTarget; +- (int) primaryTargetID; + +- (void) noteLostTarget; +- (void) noteTargetDestroyed:(ShipEntity *)target; + +- (OOBehaviour) behaviour; +- (void) setBehaviour:(OOBehaviour) cond; + +- (void) trackOntoTarget:(double) delta_t withDForward: (GLfloat) dp; + +- (double) ballTrackTarget:(double) delta_t; +- (double) ballTrackLeadingTarget:(double) delta_t; + +- (GLfloat) rollToMatchUp:(Vector) up_vec rotating:(GLfloat) match_roll; + +- (GLfloat) rangeToDestination; +- (double) trackDestination:(double) delta_t :(BOOL) retreat; +//- (double) trackPosition:(Vector) track_pos :(double) delta_t :(BOOL) retreat; + +- (Vector) destination; +- (Vector) distance_six: (GLfloat) dist; +- (Vector) distance_twelve: (GLfloat) dist; + +- (double) trackPrimaryTarget:(double) delta_t :(BOOL) retreat; +- (double) missileTrackPrimaryTarget:(double) delta_t; + +//return 0.0 if there is no primary target +- (double) rangeToPrimaryTarget; +- (BOOL) onTarget:(BOOL) fwd_weapon; + +- (OOTimeDelta) shotTime; +- (void) resetShotTime; + +- (BOOL) fireMainWeapon:(double) range; +- (BOOL) fireAftWeapon:(double) range; +- (BOOL) fireTurretCannon:(double) range; +- (void) setLaserColor:(OOColor *) color; +- (OOColor *)laserColor; +- (BOOL) fireSubentityLaserShot: (double) range; +- (BOOL) fireDirectLaserShot; +- (BOOL) fireLaserShotInDirection: (OOViewID) direction; +- (BOOL) firePlasmaShot:(double) offset :(double) speed :(OOColor *) color; +- (BOOL) fireMissile; +- (BOOL) isMissileFlagSet; +- (void) setIsMissileFlag:(BOOL)newValue; +- (BOOL) fireECM; +- (BOOL) activateCloakingDevice; +- (void) deactivateCloakingDevice; +- (BOOL) launchEnergyBomb; +- (OOUniversalID) launchEscapeCapsule; +- (OOCargoType) dumpCargo; +- (ShipEntity *) dumpCargoItem; +- (OOCargoType) dumpItem: (ShipEntity*) jetto; + +- (void) manageCollisions; +- (BOOL) collideWithShip:(ShipEntity *)other; +- (void) adjustVelocity:(Vector) xVel; +- (void) addImpactMoment:(Vector) moment fraction:(GLfloat) howmuch; +- (BOOL) canScoop:(ShipEntity *)other; +- (void) getTractoredBy:(ShipEntity *)other; +- (void) scoopIn:(ShipEntity *)other; +- (void) scoopUp:(ShipEntity *)other; +- (void) takeScrapeDamage:(double) amount from:(Entity *) ent; + +- (void) takeHeatDamage:(double) amount; + +- (void) enterDock:(StationEntity *)station; +- (void) leaveDock:(StationEntity *)station; + +- (void) enterWormhole:(WormholeEntity *) w_hole; +- (void) enterWormhole:(WormholeEntity *) w_hole replacing:(BOOL)replacing; +- (void) enterWitchspace; +- (void) leaveWitchspace; + +- (void) markAsOffender:(int)offence_value; + +- (void) switchLightsOn; +- (void) switchLightsOff; + +- (void) setDestination:(Vector) dest; +- (void) setEscortDestination:(Vector) dest; + +- (BOOL) canAcceptEscort:(ShipEntity *)potentialEscort; +- (BOOL) acceptAsEscort:(ShipEntity *) other_ship; +- (Vector) coordinatesForEscortPosition:(int) f_pos; +- (void) deployEscorts; +- (void) dockEscorts; + +- (void) setTargetToNearestStation; +- (void) setTargetToSystemStation; + +- (void) landOnPlanet:(PlanetEntity *)planet; + +- (void) abortDocking; + +- (void) broadcastThargoidDestroyed; + +- (void) broadcastHitByLaserFrom:(ShipEntity*) aggressor_ship; + +- (void) sendExpandedMessage:(NSString *) message_text toShip:(ShipEntity*) other_ship; +- (void) broadcastAIMessage:(NSString *) ai_message; +// Unpiloted ships cannot broadcast messages, unless the unpilotedOverride is set to YES. +- (void) broadcastMessage:(NSString *) message_text withUnpilotedOverride:(BOOL) unpilotedOverride; +- (void) setCommsMessageColor; +- (void) receiveCommsMessage:(NSString *) message_text; +- (void) commsMessage:(NSString *)valueString withUnpilotedOverride:(BOOL)unpilotedOverride; + +- (BOOL) markForFines; + +- (BOOL) isMining; + +- (void) spawn:(NSString *)roles_number; + +- (int) checkShipsInVicinityForWitchJumpExit; + +- (BOOL) trackCloseContacts; +- (void) setTrackCloseContacts:(BOOL) value; + +/* + * Changes a ship to a hulk, for example when the pilot ejects. + * Aso unsets hulkiness for example when a new pilot gets in. + */ +- (void) setHulk:(BOOL) isNowHulk; +- (BOOL) isHulk; +- (void) claimAsSalvage; +- (void) sendCoordinatesToPilot; +- (void) pilotArrived; + +- (OOScript *)script; +- (NSDictionary *)scriptInfo; + +- (Entity *)entityForShaderProperties; + +// *** Script events. +// For NPC ships, these call doEvent: on the ship script. +// For the player, they do that and also call doWorldScriptEvent:. +- (void) doScriptEvent:(NSString *)message; +- (void) doScriptEvent:(NSString *)message withArgument:(id)argument; +- (void) doScriptEvent:(NSString *)message withArgument:(id)argument1 andArgument:(id)argument2; +- (void) doScriptEvent:(NSString *)message withArguments:(NSArray *)arguments; + +- (void) reactToAIMessage:(NSString *)message; // Immediate message +- (void) sendAIMessage:(NSString *)message; // Queued message +- (void) doScriptEvent:(NSString *)scriptEvent andReactToAIMessage:(NSString *)aiMessage; +- (void) doScriptEvent:(NSString *)scriptEvent withArgument:(id)argument andReactToAIMessage:(NSString *)aiMessage; + +@end + + +// For the common case of testing whether foo is a ship, bar is a ship, bar is a subentity of foo and this relationship is represented sanely. +@interface Entity (SubEntityRelationship) + +- (BOOL) isShipWithSubEntityShip:(Entity *)other; + +@end + + +BOOL ship_canCollide (ShipEntity* ship); + + +NSDictionary *DefaultShipShaderMacros(void); diff --git a/src/Core/Entities/ShipEntity.m b/src/Core/Entities/ShipEntity.m new file mode 100644 index 00000000..58c0f600 --- /dev/null +++ b/src/Core/Entities/ShipEntity.m @@ -0,0 +1,8842 @@ +/* + +ShipEntity.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntity.h" +#import "ShipEntityAI.h" +#import "ShipEntityScriptMethods.h" + +#import "OOMaths.h" +#import "Universe.h" +#import "OOShaderMaterial.h" +#import "OOOpenGLExtensionManager.h" + +#import "ResourceManager.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" +#import "NSScannerOOExtensions.h" +#import "OOFilteringEnumerator.h" +#import "OORoleSet.h" +#import "OOShipGroup.h" +#import "OOExcludeObjectEnumerator.h" + +#import "OOCharacter.h" +#import "AI.h" +#ifdef OO_BRAIN_AI +#import "OOBrain.h" +#endif + +#import "OOMesh.h" +#import "Geometry.h" +#import "Octree.h" +#import "OOColor.h" + +#import "ParticleEntity.h" +#import "StationEntity.h" +#import "PlanetEntity.h" +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "PlayerEntitySound.h" +#import "WormholeEntity.h" +#import "GuiDisplayGen.h" +#import "HeadUpDisplay.h" +#import "OOEntityFilterPredicate.h" +#import "OOEquipmentType.h" + +#import "OODebugGLDrawing.h" + +#import "OOScript.h" + + +#define kOOLogUnconvertedNSLog @"unclassified.ShipEntity" + + +extern NSString * const kOOLogSyntaxAddShips; +static NSString * const kOOLogEntityBehaviourChanged = @"entity.behaviour.changed"; + + +@interface ShipEntity (Private) + +- (void) drawSubEntity:(BOOL) immediate :(BOOL) translucent; + +- (void)subEntityDied:(ShipEntity *)sub; +- (void)subEntityReallyDied:(ShipEntity *)sub; + +#ifndef NDEBUG +- (void) drawDebugStuff; +#endif + +- (void) rescaleBy:(GLfloat)factor; + +- (BOOL) setUpOneSubentity:(NSDictionary *) subentDict; +- (BOOL) setUpOneFlasher:(NSDictionary *) subentDict; +- (BOOL) setUpOneStandardSubentity:(NSDictionary *) subentDict asTurret:(BOOL)asTurret; + +- (PlanetEntity *) lastPlanet; +- (void) setLastPlanet:(PlanetEntity *)lastPlanet; + +@end + + +@implementation ShipEntity + +- (id) init +{ + /* -init used to set up a bunch of defaults that were different from + those in -reinit and -setUpShipFromDictionary:. However, it seems that + no ships are ever used which are not -setUpShipFromDictionary: (which + is as it should be), so these different defaults were meaningless. + */ + return [self initWithDictionary:nil]; +} + + +// Designated initializer +- (id) initWithDictionary:(NSDictionary *) dict +{ + if (dict == nil && ![self isKindOfClass:[PlayerEntity class]]) + { + // Is there any reason we should allow nil dictionary here? I think not. --Ahruman 2008-04-27 + // Yes, the player ship uses -init. Any others? --Ahruman 2008-04-28 + OOLog(@"ship.sanityCheck.nilDict", @"Ship created with nil dictionary!"); + } + + self = [super init]; + + isShip = YES; + entity_personality = ranrot_rand() & 0x7FFF; + [self setStatus:STATUS_IN_FLIGHT]; + + zero_distance = SCANNER_MAX_RANGE2 * 2.0; + weapon_recharge_rate = 6.0; + shot_time = 100000.0; + ship_temperature = 60.0; + + if (![self setUpShipFromDictionary:dict]) + { + [self release]; + self = nil; + } + + // Problem observed in testing -- Ahruman + if (self != nil && !isfinite(maxFlightSpeed)) + { + OOLog(@"ship.sanityCheck.failed", @"Ship %@ generated with infinite top speed, clamped to 300!", self); + maxFlightSpeed = 300; + } + return self; +} + + +- (BOOL) setUpShipFromDictionary:(NSDictionary *) dict +{ + NSDictionary *shipDict = dict; + + orientation = kIdentityQuaternion; + rotMatrix = kIdentityMatrix; + v_forward = kBasisZVector; + v_up = kBasisYVector; + v_right = kBasisXVector; + reference = v_forward; // reference vector for (* turrets *) + + isShip = YES; + + // In order for default values to work and float values to not be junk, + // replace nil with empty dictionary. -- Ahruman 2008-04-28 + if (shipDict == nil) shipDict = [NSDictionary dictionary]; + + // All like_ship references should have been resolved in -[Universe getDictionaryForShip:recursionLimit:] + if ([shipDict objectForKey:@"like_ship"] != nil) + { + OOLog(@"ship.setUp.like_ship", @"***** Error: like_ship found in ship dictionary in -[ShipEntity setUpShipFromDictionary:], when it should have been resolved already. This is an internal error, please report it."); + return NO; + } + + shipinfoDictionary = [shipDict copy]; + shipDict = shipinfoDictionary; // TEMP: ensure no mutation + + // set things from dictionary from here out + maxFlightSpeed = [shipDict floatForKey:@"max_flight_speed"]; + max_flight_roll = [shipDict floatForKey:@"max_flight_roll"]; + max_flight_pitch = [shipDict floatForKey:@"max_flight_pitch"]; + max_flight_yaw = [shipDict floatForKey:@"max_flight_yaw" defaultValue:max_flight_pitch]; // Note by default yaw == pitch + cruiseSpeed = maxFlightSpeed*0.8; + + thrust = [shipDict floatForKey:@"thrust"]; + + maxEnergy = [shipDict floatForKey:@"max_energy"]; + energy_recharge_rate = [shipDict floatForKey:@"energy_recharge_rate"]; + + forward_weapon_type = StringToWeaponType([shipDict stringForKey:@"forward_weapon_type" defaultValue:@"WEAPON_NONE"]); + aft_weapon_type = StringToWeaponType([shipDict stringForKey:@"aft_weapon_type" defaultValue:@"WEAPON_NONE"]); + [self setWeaponDataFromType:forward_weapon_type]; + // no front laser? It's probably a missile - no limits to weapon_energy. + if (weapon_energy == 0.0) weapon_energy = [shipDict floatForKey:@"weapon_energy"]; + + scannerRange = [shipDict floatForKey:@"scanner_range" defaultValue:25600.0]; + missiles = [shipDict intForKey:@"missiles"]; + missileRole = [shipDict stringForKey:@"missile_role"]; + + // upgrades: + if ([shipDict fuzzyBooleanForKey:@"has_ecm"]) [self addEquipmentItem:@"EQ_ECM"]; + if ([shipDict fuzzyBooleanForKey:@"has_scoop"]) [self addEquipmentItem:@"EQ_FUEL_SCOOPS"]; + if ([shipDict fuzzyBooleanForKey:@"has_escape_pod"]) [self addEquipmentItem:@"EQ_ESCAPE_POD"]; + if ([shipDict fuzzyBooleanForKey:@"has_energy_bomb"]) [self addEquipmentItem:@"EQ_ENERGY_BOMB"]; + if ([shipDict fuzzyBooleanForKey:@"has_cloaking_device"]) [self addEquipmentItem:@"EQ_CLOAKING_DEVICE"]; + if (![UNIVERSE strict]) + { + // These items are not available in strict mode. + if ([shipDict fuzzyBooleanForKey:@"has_fuel_injection"]) [self addEquipmentItem:@"EQ_FUEL_INJECTION"]; + if ([shipDict fuzzyBooleanForKey:@"has_military_jammer"]) [self addEquipmentItem:@"EQ_MILITARY_JAMMER"]; + if ([shipDict fuzzyBooleanForKey:@"has_military_scanner_filter"]) [self addEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER"]; + } + + canFragment = [shipDict fuzzyBooleanForKey:@"fragment_chance" defaultValue:0.9]; + + // Each new ship should start in seemingly good operating condition, unless specifically told not to + [self setThrowSparks:[shipDict boolForKey:@"throw_sparks" defaultValue:NO]]; + + cloaking_device_active = NO; + military_jammer_active = NO; + cloakPassive = [shipDict boolForKey:@"cloak_passive" defaultValue:NO]; + + // FIXME: give NPCs shields instead. + if ([shipDict fuzzyBooleanForKey:@"has_shield_booster"]) + { + maxEnergy += 256.0f; + } + if ([shipDict fuzzyBooleanForKey:@"has_shield_enhancer"]) + { + maxEnergy += 256.0f; + energy_recharge_rate *= 1.5; + } + + // Moved here from above upgrade loading so that ships start with full energy banks. -- Ahruman + energy = maxEnergy; + + fuel = [shipDict unsignedShortForKey:@"fuel"]; // Does it make sense that this defaults to 0? Should it not be 70? -- Ahruman + fuel_accumulator = 1.0; + + if (![UNIVERSE strict]) + { + hyperspaceMotorSpinTime = [shipDict floatForKey:@"hyperspace_motor_spin_time" defaultValue:DEFAULT_HYPERSPACE_SPIN_TIME]; + } + else + { + hyperspaceMotorSpinTime = DEFAULT_HYPERSPACE_SPIN_TIME; + } + + bounty = [shipDict unsignedIntForKey:@"bounty"]; + + [shipAI autorelease]; + shipAI = [[AI alloc] init]; + [shipAI setStateMachine:[shipDict stringForKey:@"ai_type" defaultValue:@"nullAI.plist"]]; + + max_cargo = [shipDict unsignedIntForKey:@"max_cargo"]; + likely_cargo = [shipDict unsignedIntForKey:@"likely_cargo"]; + extra_cargo = [shipDict unsignedIntForKey:@"extra_cargo" defaultValue:15]; + if ([shipDict fuzzyBooleanForKey:@"no_boulders"]) noRocks = YES; + + NSString *cargoString = [shipDict stringForKey:@"cargo_carried"]; + if (cargoString != nil) + { + cargo_flag = CARGO_FLAG_FULL_UNIFORM; + + OOCargoType c_commodity = CARGO_UNDEFINED; + int c_amount = 1; + NSScanner *scanner = [NSScanner scannerWithString:cargoString]; + if ([scanner scanInt:&c_amount]) + { + [scanner ooliteScanCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:NULL]; // skip whitespace + c_commodity = [UNIVERSE commodityForName: [[scanner string] substringFromIndex:[scanner scanLocation]]]; + } + else + { + c_amount = 1; + c_commodity = [UNIVERSE commodityForName: [shipDict stringForKey:@"cargo_carried"]]; + } + + if (c_commodity != CARGO_UNDEFINED) [self setCommodity:c_commodity andAmount:c_amount]; + } + + cargoString = [shipDict stringForKey:@"cargo_type"]; + if (cargoString) + { + cargo_type = StringToCargoType(cargoString); + + [cargo autorelease]; + cargo = [[NSMutableArray alloc] initWithCapacity:max_cargo]; // alloc retains; + } + + // Load the model (must be before subentities) + NSString *modelName = [shipDict stringForKey:@"model"]; + if (modelName != nil) + { + OOMesh *mesh = [OOMesh meshWithName:modelName + materialDictionary:[shipDict dictionaryForKey:@"materials"] + shadersDictionary:[shipDict dictionaryForKey:@"shaders"] + smooth:[shipDict boolForKey:@"smooth"] + shaderMacros:DefaultShipShaderMacros() + shaderBindingTarget:self]; + if (mesh == nil) return NO; + [self setMesh:mesh]; + } + + float density = [shipDict floatForKey:@"density" defaultValue:1.0]; + if (octree) mass = density * 20.0 * [octree volume]; + + [name autorelease]; + name = [[shipDict stringForKey:@"name" defaultValue:name] copy]; + + [displayName autorelease]; + displayName = [[shipDict stringForKey:@"display_name" defaultValue:name] copy]; + + [roleSet release]; + roleSet = [[[OORoleSet roleSetWithString:[shipDict stringForKey:@"roles"]] roleSetWithRemovedRole:@"player"] retain]; + [primaryRole release]; + primaryRole = nil; + + [self setOwner:self]; + + [self setHulk:[shipDict boolForKey:@"is_hulk"]]; + + if (![self setUpSubEntities: shipDict]) + { + return NO; + } + + isFrangible = [shipDict boolForKey:@"frangible" defaultValue:YES]; + + OOColor *color = [OOColor brightColorWithDescription:[shipDict objectForKey:@"laser_color"]]; + if (color == nil) color = [OOColor redColor]; + [self setLaserColor:color]; + +#if 0 + //scan class settings. 'scanClass' is in common usage, but we could also have a more standard 'scan_class' key with higher precedence. Kaks 20090810 + //let's see if scan_class is set... + scanClass = StringToScanClass([shipDict stringForKey:@"scan_class" defaultValue:@"CLASS_NOT_SET"]); + + //try.'scanClass' if 'scan_class' isn't set. + if (scanClass == CLASS_NOT_SET) + { + scanClass = StringToScanClass([shipDict stringForKey:@"scanClass" defaultValue:@"CLASS_NOT_SET"]); + } +#else + // scan class. NOTE: non-standard capitalization is documented and entrenched. + scanClass = StringToScanClass([shipDict stringForKey:@"scanClass" defaultValue:@"CLASS_NOT_SET"]); +#endif + + // accuracy. Must come after scanClass, because we are using scanClass to determine if this is a missile. + accuracy = [shipDict floatForKey:@"accuracy" defaultValue:-100.0f]; // Out-of-range default + if (accuracy >= -5.0f && accuracy <= 10.0f) + { + pitch_tolerance = 0.01 * (85.0f + accuracy); + } + else + { + pitch_tolerance = 0.01 * (80 + (randf() * 15.0f)); + } + + // If this entity is a missile, clamp its accuracy within range from 0.0 to 10.0. + // Otherwise, just make sure that the accuracy value does not fall below 1.0. + // Using a switch statement, in case accuracy for other scan classes need be considered in the future. + switch (scanClass) + { + case CLASS_MISSILE : + accuracy = OOClamp_0_max_f(accuracy, 10.0f); + break; + default : + if (accuracy < 1.0f) accuracy = 1.0f; + break; + } + + // escorts + _pendingEscortCount = _maxEscortCount = [shipDict unsignedIntForKey:@"escorts"]; + + // beacons + [self setBeaconCode:[shipDict stringForKey:@"beacon"]]; + + // rotating subentities + subentityRotationalVelocity = kIdentityQuaternion; + ScanQuaternionFromString([shipDict objectForKey:@"rotational_velocity"], &subentityRotationalVelocity); + + // contact tracking entities + if ([shipDict objectForKey:@"track_contacts"]) + { + [self setTrackCloseContacts:[shipDict boolForKey:@"track_contacts"]]; + } + else + { + [self setTrackCloseContacts:NO]; + } + + // set weapon offsets + [self setDefaultWeaponOffsets]; + + ScanVectorFromString([shipDict objectForKey:@"weapon_position_forward"], &forwardWeaponOffset); + ScanVectorFromString([shipDict objectForKey:@"weapon_position_aft"], &aftWeaponOffset); + ScanVectorFromString([shipDict objectForKey:@"weapon_position_port"], &portWeaponOffset); + ScanVectorFromString([shipDict objectForKey:@"weapon_position_starboard"], &starboardWeaponOffset); + + // fuel scoop destination position (where cargo gets sucked into) + tractor_position = kZeroVector; + ScanVectorFromString([shipDict objectForKey:@"scoop_position"], &tractor_position); + + // ship skin insulation factor (1.0 is normal) + [self setHeatInsulation:[shipDict floatForKey:@"heat_insulation" defaultValue:[self hasHeatShield] ? 2.0 : 1.0]]; + + // crew and passengers + NSDictionary* cdict = [[UNIVERSE characters] objectForKey:[shipDict stringForKey:@"pilot"]]; + if (cdict != nil) + { + OOCharacter *pilot = [OOCharacter characterWithDictionary:cdict]; + [self setCrew:[NSArray arrayWithObject:pilot]]; + } + + // unpiloted (like missiles asteroids etc.) + if ((isUnpiloted = [shipDict fuzzyBooleanForKey:@"unpiloted"])) [self setCrew:nil]; + + // Get scriptInfo dictionary, containing arbitrary stuff scripts might be interested in. + scriptInfo = [[shipDict dictionaryForKey:@"script_info" defaultValue:nil] retain]; + + [self setShipScript:[shipDict stringForKey:@"script"]]; + + return YES; +} + + +- (BOOL) setUpSubEntities: (NSDictionary *) shipDict +{ + unsigned int i; + NSArray *plumes = [shipDict arrayForKey:@"exhaust"]; + + for (i = 0; i < [plumes count]; i++) + { + ParticleEntity *exhaust = [[ParticleEntity alloc] initExhaustFromShip:self details:[plumes objectAtIndex:i]]; + [self addExhaust:exhaust]; + [exhaust release]; + } + + NSArray *subs = [shipDict arrayForKey:@"subentities"]; + + for (i = 0; i < [subs count]; i++) + { + [self setUpOneSubentity:[subs dictionaryAtIndex:i]]; + } + return YES; +} + + +- (BOOL) setUpOneSubentity:(NSDictionary *) subentDict +{ + NSString *type = nil; + + type = [subentDict stringForKey:@"type"]; + if ([type isEqualToString:@"flasher"]) + { + return [self setUpOneFlasher:subentDict]; + } + else + { + return [self setUpOneStandardSubentity:subentDict asTurret:[type isEqualToString:@"ball_turret"]]; + } +} + + +- (BOOL) setUpOneFlasher:(NSDictionary *) subentDict +{ + ParticleEntity *flasher = nil; + float size, frequency, phase; + + size = [subentDict floatForKey:@"size"]; + frequency = [subentDict floatForKey:@"frequency"] * 2.0; + phase = [subentDict floatForKey:@"phase"]; + + flasher = [[ParticleEntity alloc] initFlasherWithSize:size frequency:frequency phase:phase]; + [flasher setColor:[OOColor brightColorWithDescription:[[subentDict arrayForKey:@"colors"] objectAtIndex:0]]]; + [flasher setPosition:[subentDict vectorForKey:@"position"]]; + if ([subentDict boolForKey:@"initially_on"]) [flasher setStatus:STATUS_EFFECT]; + + [self addFlasher:flasher]; + [flasher release]; + + return YES; +} + + +- (BOOL) setUpOneStandardSubentity:(NSDictionary *) subentDict asTurret:(BOOL)asTurret +{ + ShipEntity *subentity = nil; + NSString *subentKey = nil; + Vector subPosition; + Quaternion subOrientation; + + subentKey = [subentDict stringForKey:@"subentity_key"]; + if (subentKey == nil) return NO; + + subentity = [UNIVERSE newShipWithName:subentKey]; + if (subentity == nil) return NO; + + subPosition = [subentDict vectorForKey:@"position"]; + subOrientation = [subentDict quaternionForKey:@"orientation"]; + + if (!asTurret && [self isStation] && [subentDict boolForKey:@"is_dock"]) + { + [(StationEntity *)self setDockingPortModel:subentity :subPosition :subOrientation]; + } + + [subentity setPosition:subPosition]; + [subentity setOrientation:subOrientation]; + [subentity setReference:vector_forward_from_quaternion(subOrientation)]; + + if (asTurret) + { + [subentity setBehaviour:BEHAVIOUR_TRACK_AS_TURRET]; + [subentity setWeaponRechargeRate:[subentDict floatForKey:@"fire_rate"]]; + [subentity setStatus: STATUS_ACTIVE]; + } + else + { + [subentity setStatus:STATUS_INACTIVE]; + } + + [self addSolidSubentityToCollisionRadius:subentity]; + + [self addSubEntity:subentity]; + [subentity release]; + + return YES; +} + + +- (void) dealloc +{ + [self setTrackCloseContacts:NO]; // deallocs tracking dictionary + [[self parentEntity] subEntityReallyDied:self]; // Will do nothing if we're not really a subentity + [self clearSubEntities]; + + [shipinfoDictionary release]; + [shipAI release]; + [cargo release]; + [name release]; + [displayName release]; + [roleSet release]; + [primaryRole release]; + [laser_color release]; + [script release]; + + [previousCondition release]; + + [dockingInstructions release]; + + [crew release]; + + [lastRadioMessage autorelease]; + + [octree autorelease]; + + [self setSubEntityTakingDamage:nil]; + [self removeAllEquipment]; + + [_group removeShip:self]; + [_group release]; + [_escortGroup removeShip:self]; + [_escortGroup release]; + + [_lastPlanet release]; + + [super dealloc]; +} + + +- (void) clearSubEntities +{ + [subEntities makeObjectsPerformSelector:@selector(setOwner:) withObject:nil]; // Ensure backlinks are broken + [subEntities release]; + subEntities = nil; +} + + +- (NSString *)descriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\" %@", [self name], [super descriptionComponents]]; +} + +- (NSString *) shortDescriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\"", [self name]]; +} + + +- (OOMesh *)mesh +{ + return (OOMesh *)[self drawable]; +} + + +- (void)setMesh:(OOMesh *)mesh +{ + if (mesh != [self mesh]) + { + [self setDrawable:mesh]; + [octree autorelease]; + octree = [[mesh octree] retain]; + } +} + + +- (NSArray *)subEntities +{ + return [[subEntities copy] autorelease]; +} + + +- (unsigned) subEntityCount +{ + return [subEntities count]; +} + + +- (BOOL) hasSubEntity:(ShipEntity *)sub +{ + return [subEntities containsObject:sub]; +} + +- (NSEnumerator *)subEntityEnumerator +{ + return [[self subEntities] objectEnumerator]; +} + + +- (NSEnumerator *)shipSubEntityEnumerator +{ + return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isShip)]; +} + + +- (NSEnumerator *)particleSubEntityEnumerator +{ + return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isParticle)]; +} + + +- (NSEnumerator *)flasherEnumerator +{ + return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isFlasher)]; +} + + +- (NSEnumerator *)exhaustEnumerator +{ + return [[self subEntities] objectEnumeratorFilteredWithSelector:@selector(isExhaust)]; +} + + +- (ShipEntity *) subEntityTakingDamage +{ + ShipEntity *result = [_subEntityTakingDamage weakRefUnderlyingObject]; + +#ifndef NDEBUG + // Sanity check - there have been problems here, see fireLaserShotInDirection: + // -parentEntity will take care of reporting insanity. + if ([result parentEntity] != self) result = nil; +#endif + + // Clear the weakref if the subentity is dead. + if (result == nil) [self setSubEntityTakingDamage:nil]; + + return result; +} + + +- (void) setSubEntityTakingDamage:(ShipEntity *)sub +{ +#ifndef NDEBUG + // Sanity checks: sub must be a ship subentity of self, or nil. + if (sub != nil) + { + if (![self hasSubEntity:sub]) + { + OOLog(@"ship.subentity.sanityCheck.failed.details", @"Attempt to set subentity taking damage of %@ to %@, which is not a subentity.", [self shortDescription], sub); + sub = nil; + } + else if (![sub isShip]) + { + OOLog(@"ship.subentity.sanityCheck.failed", @"Attempt to set subentity taking damage of %@ to %@, which is not a ship.", [self shortDescription], sub); + sub = nil; + } + } +#endif + + [_subEntityTakingDamage release]; + _subEntityTakingDamage = [sub weakRetain]; +} + + +- (OOScript *)shipScript +{ + return script; +} + + +- (BoundingBox)findBoundingBoxRelativeToPosition:(Vector)opv InVectors:(Vector) _i :(Vector) _j :(Vector) _k +{ + return [[self mesh] findBoundingBoxRelativeToPosition:opv + basis:_i :_j :_k + selfPosition:position + selfBasis:v_right :v_up :v_forward]; +} + + +#ifdef OO_BRAIN_AI +// ship's brains! +- (OOBrain *)brain +{ + return brain; +} + + +- (void)setBrain:(OOBrain *)aBrain +{ + brain = aBrain; +} +#endif + + +- (GLfloat)doesHitLine:(Vector)v0: (Vector)v1; +{ + Vector u0 = vector_between(position, v0); // relative to origin of model / octree + Vector u1 = vector_between(position, v1); + Vector w0 = make_vector(dot_product(u0, v_right), dot_product(u0, v_up), dot_product(u0, v_forward)); // in ijk vectors + Vector w1 = make_vector(dot_product(u1, v_right), dot_product(u1, v_up), dot_product(u1, v_forward)); + return [octree isHitByLine:w0 :w1]; +} + + +- (GLfloat) doesHitLine:(Vector)v0: (Vector)v1 :(ShipEntity **)hitEntity; +{ + if (hitEntity) + hitEntity[0] = (ShipEntity*)nil; + Vector u0 = vector_between(position, v0); // relative to origin of model / octree + Vector u1 = vector_between(position, v1); + Vector w0 = make_vector(dot_product(u0, v_right), dot_product(u0, v_up), dot_product(u0, v_forward)); // in ijk vectors + Vector w1 = make_vector(dot_product(u1, v_right), dot_product(u1, v_up), dot_product(u1, v_forward)); + GLfloat hit_distance = [octree isHitByLine:w0 :w1]; + if (hit_distance) + { + if (hitEntity) + hitEntity[0] = self; + } + + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); ) + { + Vector p0 = [se absolutePositionForSubentity]; + Triangle ijk = [se absoluteIJKForSubentity]; + u0 = vector_between(p0, v0); + u1 = vector_between(p0, v1); + w0 = resolveVectorInIJK(u0, ijk); + w1 = resolveVectorInIJK(u1, ijk); + + GLfloat hitSub = [se->octree isHitByLine:w0 :w1]; + if (hitSub && (hit_distance == 0 || hit_distance > hitSub)) + { + hit_distance = hitSub; + if (hitEntity) + { + *hitEntity = se; + } + } + } + + return hit_distance; +} + + +- (GLfloat)doesHitLine:(Vector)v0: (Vector)v1 withPosition:(Vector)o andIJK:(Vector)i :(Vector)j :(Vector)k; +{ + Vector u0 = vector_between(o, v0); // relative to origin of model / octree + Vector u1 = vector_between(o, v1); + Vector w0 = make_vector(dot_product(u0, i), dot_product(u0, j), dot_product(u0, k)); // in ijk vectors + Vector w1 = make_vector(dot_product(u1, j), dot_product(u1, j), dot_product(u1, k)); + return [octree isHitByLine:w0 :w1]; +} + + +- (void) wasAddedToUniverse +{ + [super wasAddedToUniverse]; + + // if we have a universal id then we can proceed to set up any + // stuff that happens when we get added to the UNIVERSE + if (universalID != NO_TARGET) + { + // set up escorts + if (([self status] == STATUS_IN_FLIGHT || [self status] == STATUS_LAUNCHING) && _pendingEscortCount != 0) // just popped into existence + { + [self setUpEscorts]; + } + else + { + /* Should be zero already, but make sure just in case. (Escorts + aren't set up here if we're launched from a station, but in + that case the station sets pending count to zero.) + STATUS_LAUNCHING also is valid for replacement ships for + wormholing ships. In that case escorts still must be set up. + */ + if (_pendingEscortCount != 0) + { + OOLog(@"ship.escortSetup.wtf", @"Pending escort count for %@ is %u, expected 0. This is an internal error, please report it.", self, _pendingEscortCount); + } + _pendingEscortCount = 0; + } + } + + // Tell subentities, too + [subEntities makeObjectsPerformSelector:@selector(wasAddedToUniverse)]; + + [self resetTracking]; // resets stuff for tracking/exhausts +} + + +- (void)wasRemovedFromUniverse +{ + [subEntities makeObjectsPerformSelector:@selector(wasRemovedFromUniverse)]; +} + + +- (Vector)absoluteTractorPosition +{ + Vector result = position; + result.x += v_right.x * tractor_position.x + v_up.x * tractor_position.y + v_forward.x * tractor_position.z; + result.y += v_right.y * tractor_position.x + v_up.y * tractor_position.y + v_forward.y * tractor_position.z; + result.z += v_right.z * tractor_position.x + v_up.z * tractor_position.y + v_forward.z * tractor_position.z; + return result; +} + + +- (NSString *)beaconCode +{ + return beaconCode; +} + + +- (void)setBeaconCode:(NSString *)bcode +{ + if ([beaconCode length] == 0) beaconCode = nil; + + if (beaconCode != bcode) + { + [beaconCode release]; + beaconCode = [bcode copy]; + if (beaconCode != nil) + { + beaconChar = [bcode cStringUsingOoliteEncodingAndRemapping][0]; + } + else + { + beaconChar = '\0'; + } + } +} + + +- (BOOL)isBeacon +{ + return (beaconChar != 0); +} + + +- (char)beaconChar +{ + return beaconChar; +} + + +- (int)nextBeaconID +{ + return nextBeaconID; +} + + +- (void)setNextBeacon:(ShipEntity *)beaconShip +{ + if (beaconShip == nil) nextBeaconID = NO_TARGET; + else nextBeaconID = [beaconShip universalID]; +} + + +- (void) setUpEscorts +{ + NSString *defaultRole = @"escort"; + NSString *escortRole = nil; + NSString *escortShipKey = nil; + NSString *autoAI = nil; + NSDictionary *autoAIMap = nil; + NSDictionary *escortShipDict = nil; + AI *escortAI = nil; + + // Ensure that we do not try to create escorts if we are an escort ship ourselves. + // This could lead to circular reference memory overflows (e.g. "boa-mk2" trying to create 4 "boa-mk2" + // escorts or the case of two ships specifying eachother as escorts) - Nikos 20090510 + if ([self isEscort]) + { + OOLogWARN(@"shipEntity.setupEscorts.escortShipCircularReference", + @"Ship %@ requested escorts, when it is an escort ship itself. Avoiding possible circular reference overflow by ignoring escort setup.", self); + return; + } + + if (_pendingEscortCount == 0) return; + + if ([self isPolice]) defaultRole = @"wingman"; + + escortRole = [shipinfoDictionary stringForKey:@"escort-role" defaultValue:defaultRole]; + if (![escortRole isEqualToString: defaultRole]) + { + if (![[UNIVERSE newShipWithRole:escortRole] autorelease]) + { + escortRole = defaultRole; + } + } + + escortShipKey = [shipinfoDictionary stringForKey:@"escort-ship"]; + if (escortShipKey != nil) + { + if (![[UNIVERSE newShipWithName:escortShipKey] autorelease]) + { + escortShipKey = nil; + } + } + + OOShipGroup *escortGroup = [self escortGroup]; + if ([self group] == nil) + { + [self setGroup:escortGroup]; + } + [escortGroup setLeader:self]; + + while (_pendingEscortCount > 0) + { + Vector ex_pos = [self coordinatesForEscortPosition:_pendingEscortCount - 1]; + + ShipEntity *escorter = nil; + + if (escortShipKey) + { + escorter = [UNIVERSE newShipWithName:escortShipKey]; // retained + } + else + { + escorter = [UNIVERSE newShipWithRole:escortRole]; // retained + } + + if (escorter == nil) break; + + if ([escorter crew] == nil) + { + [escorter setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"hunter" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + } + + // spread them around a little randomly + double dd = escorter->collision_radius; + ex_pos.x += dd * 6.0 * (randf() - 0.5); + ex_pos.y += dd * 6.0 * (randf() - 0.5); + ex_pos.z += dd * 6.0 * (randf() - 0.5); + + [escorter setPosition:ex_pos]; + [escorter setStatus:STATUS_IN_FLIGHT]; + [escorter setPrimaryRole:defaultRole]; //for mothership + [escorter setScanClass:scanClass]; // you are the same as I + if ([self bounty] == 0) [escorter setBounty:0]; // Avoid dirty escorts for clean mothers + + [UNIVERSE addEntity:escorter]; + + escortShipDict = [escorter shipInfoDictionary]; + autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES]; + autoAI = [autoAIMap stringForKey:defaultRole]; + if (autoAI==nil) // no 'wingman' defined in autoAImap? + { + autoAI = [autoAIMap stringForKey:@"escort" defaultValue:@"nullAI.plist"]; + } + if (escortShipKey && [escortShipDict fuzzyBooleanForKey:@"auto_ai" defaultValue:YES]) //setAITo only once! + { + [escorter setAITo:autoAI]; + } + + escortAI = [escorter getAI]; + if ([[escortAI name] isEqualToString: @"nullAI.plist"] && ![autoAI isEqualToString:@"nullAI.plist"]) + { + [escortAI setStateMachine:autoAI]; // must happen after adding to the UNIVERSE! + } + + [escorter setGroup:escortGroup]; + [escorter setOwner:self]; // make self group leader + + if([escorter heatInsulation] < [self heatInsulation]) [escorter setHeatInsulation:[self heatInsulation]]; // give escorts same protection as mother. + if(([escorter maxFlightSpeed] < cruiseSpeed) && ([escorter maxFlightSpeed] > cruiseSpeed * 0.3)) + cruiseSpeed = [escorter maxFlightSpeed] * 0.99; // adapt patrolSpeed to the slowest escort but ignore the very slow ones. + + [escortAI setState:@"FLYING_ESCORT"]; // Begin escort flight. (If the AI doesn't define FLYING_ESCORT, this has no effect.) + [escorter doScriptEvent:@"spawnedAsEscort" withArgument:self]; + + if (bounty) + { + int extra = 1 | (ranrot_rand() & 15); + bounty += extra; // obviously we're dodgier than we thought! + [escorter setBounty: extra]; + } + else + { + [escorter setBounty:0]; + } + [escorter release]; + _pendingEscortCount--; + } +} + + +- (NSDictionary *)shipInfoDictionary +{ + return shipinfoDictionary; +} + + +- (void) setDefaultWeaponOffsets +{ + forwardWeaponOffset = kZeroVector; + aftWeaponOffset = kZeroVector; + portWeaponOffset = kZeroVector; + starboardWeaponOffset = kZeroVector; +} + + +- (BOOL)isFrangible +{ + return isFrangible; +} + + +- (OOScanClass) scanClass +{ + if (cloaking_device_active) + return CLASS_NO_DRAW; + else + return scanClass; +} + +////////////////////////////////////////////// + +BOOL ship_canCollide (ShipEntity* ship) +{ + int s_status = [ship status]; + int s_scan_class = ship->scanClass; + if ((s_status == STATUS_COCKPIT_DISPLAY)||(s_status == STATUS_DEAD)||(s_status == STATUS_BEING_SCOOPED)) + return NO; + if ((s_scan_class == CLASS_MISSILE) && ([ship shotTime] < 0.25)) // not yet fused + return NO; + return YES; +} + + +- (BOOL) canCollide +{ + return ship_canCollide(self); +} + +ShipEntity* doOctreesCollide(ShipEntity* prime, ShipEntity* other) +{ + // octree check + Octree *prime_octree = prime->octree; + Octree *other_octree = other->octree; + + Vector prime_position = [prime absolutePositionForSubentity]; + Triangle prime_ijk = [prime absoluteIJKForSubentity]; + Vector other_position = [other absolutePositionForSubentity]; + Triangle other_ijk = [other absoluteIJKForSubentity]; + + Vector relative_position_of_other = resolveVectorInIJK(vector_between(prime_position, other_position), prime_ijk); + Triangle relative_ijk_of_other; + relative_ijk_of_other.v[0] = resolveVectorInIJK(other_ijk.v[0], prime_ijk); + relative_ijk_of_other.v[1] = resolveVectorInIJK(other_ijk.v[1], prime_ijk); + relative_ijk_of_other.v[2] = resolveVectorInIJK(other_ijk.v[2], prime_ijk); + + // check hull octree against other hull octree + if ([prime_octree isHitByOctree:other_octree + withOrigin:relative_position_of_other + andIJK:relative_ijk_of_other]) + { + return other; + } + + // check prime subentities against the other's hull + NSArray* prime_subs = prime->subEntities; + if (prime_subs) + { + int i; + int n_subs = [prime_subs count]; + for (i = 0; i < n_subs; i++) + { + Entity* se = [prime_subs objectAtIndex:i]; + if ([se isShip] && [se canCollide] && doOctreesCollide((ShipEntity*)se, other)) + return other; + } + } + + // check prime hull against the other's subentities + NSArray* other_subs = other->subEntities; + if (other_subs) + { + int i; + int n_subs = [other_subs count]; + for (i = 0; i < n_subs; i++) + { + Entity* se = [other_subs objectAtIndex:i]; + if ([se isShip] && [se canCollide] && doOctreesCollide(prime, (ShipEntity*)se)) + return (ShipEntity*)se; + } + } + + // check prime subenties against the other's subentities + if ((prime_subs)&&(other_subs)) + { + int i; + int n_osubs = [other_subs count]; + for (i = 0; i < n_osubs; i++) + { + Entity* oe = [other_subs objectAtIndex:i]; + if ([oe isShip] && [oe canCollide]) + { + int j; + int n_psubs = [prime_subs count]; + for (j = 0; j < n_psubs; j++) + { + Entity* pe = [prime_subs objectAtIndex:j]; + if ([pe isShip] && [pe canCollide] && doOctreesCollide((ShipEntity*)pe, (ShipEntity*)oe)) + return (ShipEntity*)oe; + } + } + } + } + + // fall through => no collision + return nil; +} + + +- (BOOL) checkCloseCollisionWith:(Entity *)other +{ + if (other == nil) return NO; + if ([collidingEntities containsObject:other]) return NO; // we know about this already! + + ShipEntity *otherShip = nil; + if ([other isShip]) otherShip = (ShipEntity *)other; + + if ([self canScoop:otherShip]) return YES; // quick test - could this improve scooping for small ships? I think so! + + if (otherShip != nil && trackCloseContacts) + { + // in update we check if close contacts have gone out of touch range (origin within our collision_radius) + // here we check if something has come within that range + Vector otherPos = [otherShip position]; + OOUniversalID otherID = [otherShip universalID]; + NSString *other_key = [NSString stringWithFormat:@"%d", otherID]; + + if (![closeContactsInfo objectForKey:other_key] && + distance2(position, otherPos) < collision_radius * collision_radius) + { + // calculate position with respect to our own position and orientation + Vector dpos = vector_between(position, otherPos); + Vector rpos = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward)); + [closeContactsInfo setObject:[NSString stringWithFormat:@"%f %f %f", rpos.x, rpos.y, rpos.z] forKey: other_key]; + + // send AI a message about the touch + OOUniversalID temp_id = primaryTarget; + primaryTarget = otherID; + [self doScriptEvent:@"shipCloseContact" withArgument:otherShip andReactToAIMessage:@"CLOSE CONTACT"]; + primaryTarget = temp_id; + } + } + + if (zero_distance > CLOSE_COLLISION_CHECK_MAX_RANGE2) // don't work too hard on entities that are far from the player + return YES; + + if (otherShip != nil) + { + // check hull octree versus other hull octree + collider = doOctreesCollide(self, otherShip); + return (collider != nil); + } + + // default at this stage is to say YES they've collided! + collider = other; + return YES; +} + + +- (BoundingBox)findSubentityBoundingBox +{ + return [[self mesh] findSubentityBoundingBoxWithPosition:position rotMatrix:rotMatrix]; +} + + +- (Vector) absolutePositionForSubentity +{ + return [self absolutePositionForSubentityOffset:kZeroVector]; +} + + +- (Vector) absolutePositionForSubentityOffset:(Vector) offset +{ + Vector abspos = vector_add(position, OOVectorMultiplyMatrix(offset, rotMatrix)); + Entity *last = nil; + Entity *father = [self parentEntity]; + OOMatrix r_mat; + + while ((father)&&(father != last) && (father != NO_TARGET)) + { + r_mat = [father drawRotationMatrix]; + abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]); + last = father; + if (![last isSubEntity]) break; + father = [father owner]; + } + return abspos; +} + + +- (Triangle) absoluteIJKForSubentity +{ + Triangle result = {{ kBasisXVector, kBasisYVector, kBasisZVector, kZeroVector }}; + Entity *last = nil; + Entity *father = self; + OOMatrix r_mat; + + while ((father)&&(father != last) && (father != NO_TARGET)) + { + r_mat = [father drawRotationMatrix]; + result.v[0] = OOVectorMultiplyMatrix(result.v[0], r_mat); + result.v[1] = OOVectorMultiplyMatrix(result.v[1], r_mat); + result.v[2] = OOVectorMultiplyMatrix(result.v[2], r_mat); + + last = father; + if (![last isSubEntity]) break; + father = [father owner]; + } + return result; +} + + +- (void) addSolidSubentityToCollisionRadius:(ShipEntity*) subent +{ + if (!subent) + return; + + double distance = sqrt(magnitude2(subent->position)) + [subent findCollisionRadius]; + if (distance > collision_radius) + collision_radius = distance; + + mass += 20.0 * [subent->octree volume]; +} + + +- (BOOL) validForAddToUniverse +{ + if (shipinfoDictionary == nil) + { + OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self); + return NO; + } + return YES; +} + + +- (void) update:(OOTimeDelta)delta_t +{ + if (shipinfoDictionary == nil) + { + OOLog(@"shipEntity.notDict", @"Ship %@ was not set up from dictionary.", self); + [UNIVERSE removeEntity:self]; + return; + } + + if (!isfinite(maxFlightSpeed)) + { + OOLog(@"ship.sanityCheck.failed", @"Ship %@ has infinite top speed!", self); + maxFlightSpeed = 300; + } + + // + // deal with collisions + // + [self manageCollisions]; + [self saveToLastFrame]; + + // + // reset any inadvertant legal mishaps + // + if (scanClass == CLASS_POLICE) + { + if (bounty > 0) + bounty = 0; + ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget]; + if ((target)&&(target->scanClass == CLASS_POLICE)) + { + [self noteLostTarget]; + } + } + + if (trackCloseContacts) + { + // in checkCloseCollisionWith: we check if some thing has come within touch range (origin within our collision_radius) + // here we check if it has gone outside that range + NSEnumerator *contactEnum = nil; + NSString *other_key = nil; + + for (contactEnum = [closeContactsInfo keyEnumerator]; (other_key = [contactEnum nextObject]); ) + { + ShipEntity* other = [UNIVERSE entityForUniversalID:[other_key intValue]]; + if ((other != nil) && (other->isShip)) + { + if (distance2(position, other->position) > collision_radius * collision_radius) // moved beyond our sphere! + { + // calculate position with respect to our own position and orientation + Vector dpos = vector_between(position, other->position); + Vector pos1 = make_vector(dot_product(dpos, v_right), dot_product(dpos, v_up), dot_product(dpos, v_forward)); + Vector pos0 = {0, 0, 0}; + ScanVectorFromString([closeContactsInfo objectForKey: other_key], &pos0); + // send AI messages about the contact + int temp_id = primaryTarget; + primaryTarget = other->universalID; + if ((pos0.x < 0.0)&&(pos1.x > 0.0)) + { + [self doScriptEvent:@"shipTraversePositiveX" withArgument:other andReactToAIMessage:@"POSITIVE X TRAVERSE"]; + } + if ((pos0.x > 0.0)&&(pos1.x < 0.0)) + { + [self doScriptEvent:@"shipTraverseNegativeX" withArgument:other andReactToAIMessage:@"NEGATIVE X TRAVERSE"]; + } + if ((pos0.y < 0.0)&&(pos1.y > 0.0)) + { + [self doScriptEvent:@"shipTraversePositiveY" withArgument:other andReactToAIMessage:@"POSITIVE Y TRAVERSE"]; + } + if ((pos0.y > 0.0)&&(pos1.y < 0.0)) + { + [self doScriptEvent:@"shipTraverseNegativeY" withArgument:other andReactToAIMessage:@"NEGATIVE Y TRAVERSE"]; + } + if ((pos0.z < 0.0)&&(pos1.z > 0.0)) + { + [self doScriptEvent:@"shipTraversePositiveZ" withArgument:other andReactToAIMessage:@"POSITIVE Z TRAVERSE"]; + } + if ((pos0.z > 0.0)&&(pos1.z < 0.0)) + { + [self doScriptEvent:@"shipTraverseNegativeZ" withArgument:other andReactToAIMessage:@"NEGATIVE Z TRAVERSE"]; + } + primaryTarget = temp_id; + [closeContactsInfo removeObjectForKey: other_key]; + } + } + else + { + [closeContactsInfo removeObjectForKey: other_key]; + } + } + } + + // think! +#ifdef OO_BRAIN_AI + [brain update:delta_t]; +#endif + + // super update + [super update:delta_t]; + +#ifndef NDEBUG + // DEBUGGING + if (reportAIMessages && (debugLastBehaviour != behaviour)) + { + OOLog(kOOLogEntityBehaviourChanged, @"%@ behaviour is now %@", self, BehaviourToString(behaviour)); + debugLastBehaviour = behaviour; + } +#endif + + // update time between shots + shot_time += delta_t; + + // handle radio message effects + if (messageTime > 0.0) + { + messageTime -= delta_t; + if (messageTime < 0.0) + messageTime = 0.0; + } + + // temperature factors + double external_temp = 0.0; + PlanetEntity *sun = [UNIVERSE sun]; + if (sun != nil) + { + // set the ambient temperature here + double sun_zd = magnitude2(vector_between(position, sun->position)); // square of distance + double sun_cr = sun->collision_radius; + double alt1 = sun_cr * sun_cr / sun_zd; + external_temp = SUN_TEMPERATURE * alt1; + if ([sun goneNova]) external_temp *= 100; + } + + // work on the ship temperature + // + if (external_temp > ship_temperature) + ship_temperature += (external_temp - ship_temperature) * delta_t * SHIP_INSULATION_FACTOR / [self heatInsulation]; + else + { + if (ship_temperature > SHIP_MIN_CABIN_TEMP) + ship_temperature += (external_temp - ship_temperature) * delta_t * SHIP_COOLING_FACTOR / [self heatInsulation]; + } + + if (ship_temperature > SHIP_MAX_CABIN_TEMP) + [self takeHeatDamage: delta_t * ship_temperature]; + + // are we burning due to low energy + if ((energy < maxEnergy * 0.20)&&(energy_recharge_rate > 0.0)) // prevents asteroid etc. from burning + throw_sparks = YES; + + // burning effects + // + if (throw_sparks) + { + next_spark_time -= delta_t; + if (next_spark_time < 0.0) + { + [self throwSparks]; + throw_sparks = NO; // until triggered again + } + } + + // cloaking device + if ([self hasCloakingDevice]) + { + if (cloaking_device_active) + { + energy -= delta_t * CLOAKING_DEVICE_ENERGY_RATE; + if (energy < CLOAKING_DEVICE_MIN_ENERGY) + [self deactivateCloakingDevice]; + } + else + { + if (energy < maxEnergy) + { + energy += delta_t * CLOAKING_DEVICE_ENERGY_RATE; + if (energy > maxEnergy) + { + energy = maxEnergy; + [shipAI message:@"ENERGY_FULL"]; + } + } + } + } + + // military_jammer + if ([self hasMilitaryJammer]) + { + if (military_jammer_active) + { + energy -= delta_t * MILITARY_JAMMER_ENERGY_RATE; + if (energy < MILITARY_JAMMER_MIN_ENERGY) + military_jammer_active = NO; + } + else + { + if (energy > 1.5 * MILITARY_JAMMER_MIN_ENERGY) + military_jammer_active = YES; + } + } + + // check outside factors + // + aegis_status = [self checkForAegis]; // is a station or something nearby?? + + //scripting + if (!haveExecutedSpawnAction && script != nil && [self status] == STATUS_IN_FLIGHT) + { + [[PlayerEntity sharedPlayer] setScriptTarget:self]; + [self doScriptEvent:@"shipSpawned"]; + haveExecutedSpawnAction = YES; + } + + // behaviours according to status and behaviour + // + if ([self status] == STATUS_LAUNCHING) + { + if ([UNIVERSE getTime] > launch_time + LAUNCH_DELAY) // move for while before thinking + { + [self setStatus:STATUS_IN_FLIGHT]; + [self doScriptEvent:@"shipLaunchedFromStation"]; + [shipAI reactToMessage: @"LAUNCHED OKAY"]; + } + else + { + // ignore behaviour just keep moving... + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; + if (energy < maxEnergy) + { + energy += energy_recharge_rate * delta_t; + if (energy > maxEnergy) + { + energy = maxEnergy; + [self doScriptEvent:@"shipEnergyBecameFull"]; + [shipAI message:@"ENERGY_FULL"]; + } + } + + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); ) + { + [se update:delta_t]; + } + return; + } + } + // + // double check scooped behaviour + // + if ([self status] == STATUS_BEING_SCOOPED) + { + //if we are being tractored, but we have no owner, then we have a problem + if (behaviour != BEHAVIOUR_TRACTORED || [self owner] == nil || [self owner] == self || [self owner] == NO_TARGET) + { + // escaped tractor beam + [self setStatus:STATUS_IN_FLIGHT]; // should correct 'uncollidable objects' bug + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + } + } + + if ([self status] == STATUS_COCKPIT_DISPLAY) + { + [self applyRoll: delta_t * flightRoll andClimb: delta_t * flightPitch]; + GLfloat range2 = 0.1 * distance2(position, destination) / (collision_radius * collision_radius); + if ((range2 > 1.0)||(velocity.z > 0.0)) range2 = 1.0; + position = vector_add(position, vector_multiply_scalar(velocity, range2 * delta_t)); + } + else + { + double target_speed = maxFlightSpeed; + + ShipEntity *target = [UNIVERSE entityForUniversalID:primaryTarget]; + + if (target == nil || [target scanClass] == CLASS_NO_DRAW || ![target isShip] || [target isCloaked]) + { + // It's no longer a parrot, it has ceased to be, it has joined the choir invisible... + if (primaryTarget != NO_TARGET) + { + if ([target isShip] && [target isCloaked]) + { + [self doScriptEvent:@"shipTargetCloaked" andReactToAIMessage:@"TARGET_CLOAKED"]; + } + [self noteLostTarget]; + } + else + { + target_speed = [target flightSpeed]; + if (target_speed < maxFlightSpeed) + { + target_speed += maxFlightSpeed; + target_speed /= 2.0; + } + } + } + + switch (behaviour) + { + case BEHAVIOUR_TUMBLE : + [self behaviour_tumble: delta_t]; + break; + + case BEHAVIOUR_STOP_STILL : + case BEHAVIOUR_STATION_KEEPING : + [self behaviour_stop_still: delta_t]; + break; + + case BEHAVIOUR_IDLE : + [self behaviour_idle: delta_t]; + break; + + case BEHAVIOUR_TRACTORED : + [self behaviour_tractored: delta_t]; + break; + + case BEHAVIOUR_TRACK_TARGET : + [self behaviour_track_target: delta_t]; + break; + + case BEHAVIOUR_INTERCEPT_TARGET : + case BEHAVIOUR_COLLECT_TARGET : + [self behaviour_intercept_target: delta_t]; + break; + + case BEHAVIOUR_ATTACK_TARGET : + [self behaviour_attack_target: delta_t]; + break; + + case BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX : + case BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE : + [self behaviour_fly_to_target_six: delta_t]; + break; + + case BEHAVIOUR_ATTACK_MINING_TARGET : + [self behaviour_attack_mining_target: delta_t]; + break; + + case BEHAVIOUR_ATTACK_FLY_TO_TARGET : + [self behaviour_attack_fly_to_target: delta_t]; + break; + + case BEHAVIOUR_ATTACK_FLY_FROM_TARGET : + [self behaviour_attack_fly_from_target: delta_t]; + break; + + case BEHAVIOUR_RUNNING_DEFENSE : + [self behaviour_running_defense: delta_t]; + break; + + case BEHAVIOUR_FLEE_TARGET : + [self behaviour_flee_target: delta_t]; + break; + + case BEHAVIOUR_FLY_RANGE_FROM_DESTINATION : + [self behaviour_fly_range_from_destination: delta_t]; + break; + + case BEHAVIOUR_FACE_DESTINATION : + [self behaviour_face_destination: delta_t]; + break; + + case BEHAVIOUR_FORMATION_FORM_UP : + [self behaviour_formation_form_up: delta_t]; + break; + + case BEHAVIOUR_FLY_TO_DESTINATION : + [self behaviour_fly_to_destination: delta_t]; + break; + + case BEHAVIOUR_FLY_FROM_DESTINATION : + case BEHAVIOUR_FORMATION_BREAK : + [self behaviour_fly_from_destination: delta_t]; + break; + + case BEHAVIOUR_AVOID_COLLISION : + [self behaviour_avoid_collision: delta_t]; + break; + + case BEHAVIOUR_TRACK_AS_TURRET : + [self behaviour_track_as_turret: delta_t]; + break; + + case BEHAVIOUR_FLY_THRU_NAVPOINTS : + [self behaviour_fly_thru_navpoints: delta_t]; + break; + + case BEHAVIOUR_ENERGY_BOMB_COUNTDOWN: + // Do nothing + break; + } + + // manage energy + if (energy < maxEnergy) + { + energy += energy_recharge_rate * delta_t; + if (energy > maxEnergy) + { + energy = maxEnergy; + [shipAI message:@"ENERGY_FULL"]; + } + } + + // update destination position for escorts + if ([self hasEscorts]) + { + NSEnumerator *escortEnum = nil; + ShipEntity *escort = nil; + unsigned i = 0; + + // Note: works on escortArray rather than escortEnumerator because escorts may be mutated. + for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); ) + { + [escort setEscortDestination:[self coordinatesForEscortPosition:i++]]; + } + } + if ([self escortGroup] != nil) + { + ShipEntity *leader = [[self escortGroup] leader]; + if (leader != nil && ([leader scanClass] != [self scanClass])) { + OOLog(@"ship.sanityCheck.failed", @"Ship %@ escorting %@ with wrong scanclass!", self, leader); + [[self escortGroup] removeShip:self]; + [[self escortGroup] release]; + } + } + } + + // subentity rotation + if (!quaternion_equal(subentityRotationalVelocity, kIdentityQuaternion) && + !quaternion_equal(subentityRotationalVelocity, kZeroQuaternion)) + { + Quaternion qf = subentityRotationalVelocity; + qf.w *= (1.0 - delta_t); + qf.x *= delta_t; + qf.y *= delta_t; + qf.z *= delta_t; + orientation = quaternion_multiply(qf, orientation); + } + + // reset totalBoundingBox + totalBoundingBox = boundingBox; + + // update subentities + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); ) + { + [se update:delta_t]; + if ([se isShip]) + { + BoundingBox sebb = [se findSubentityBoundingBox]; + bounding_box_add_vector(&totalBoundingBox, sebb.max); + bounding_box_add_vector(&totalBoundingBox, sebb.min); + } + } + +} + + +// override Entity version... +// +- (double) speed +{ + return sqrt(velocity.x * velocity.x + velocity.y * velocity.y + velocity.z * velocity.z + flightSpeed * flightSpeed); +} + + + +- (void)respondToAttackFrom:(Entity *)from becauseOf:(Entity *)other +{ + Entity *source = nil; + + if ([other isKindOfClass:[ShipEntity class]]) + { + source = other; + + ShipEntity *hunter = (ShipEntity *)other; + //if we are in the same group, then we have to be careful about how we handle things + if ([self isPolice] && [hunter isPolice]) + { + //police never get into a fight with each other + return; + } + + OOShipGroup *group = [self group]; + + if (group != nil && group == [hunter group]) + { + //we are in the same group, do we forgive you? + //criminals are less likely to forgive + if (randf() < (0.8 - (bounty/100))) + { + //it was an honest mistake, lets get on with it + return; + } + + ShipEntity *groupLeader = [group leader]; + if (hunter == groupLeader) + { + //oops we were attacked by our leader, desert him + [self setGroup:nil]; + } + else + { + //evict them from our group + [hunter setGroup:nil]; + + [groupLeader setFound_target:other]; + [groupLeader setPrimaryAggressor:hunter]; + [groupLeader respondToAttackFrom:from becauseOf:other]; + } + } + } + else + { + source = from; + } + [self doScriptEvent:@"shipBeingAttacked" withArgument:source andReactToAIMessage:@"ATTACKED"]; +} + + +// Equipment + +- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeWeapons:(BOOL)includeWeapons +{ + if ([self hasOneEquipmentItem:itemKey includeMissiles:includeWeapons]) return YES; + if (includeWeapons) + { + // Check for primary weapon + OOWeaponType weaponType = EquipmentStringToWeaponTypeStrict(itemKey); + if (weaponType != WEAPON_NONE) + { + if ([self hasPrimaryWeapon:weaponType]) return YES; + } + } + + return NO; +} + + +- (BOOL) hasOneEquipmentItem:(NSString *)itemKey includeMissiles:(BOOL)includeMissiles +{ + if ([_equipment containsObject:itemKey]) return YES; + + if (includeMissiles && missiles != 0) + { + /* Note: this is slightly misleading. + When an NPC ship doesn't specify a missile_role, the actual missile + to fire is chosen at random when a missile is fired. There is a + 90 % chance "EQ_MISSILE" will be used, and a 10 % chance that + "missile" will be used. A ship for which hasEquipmentItem:@"EQ_MISSILE" + returns YES could fire missiles that are not of the EQ_MISSILE + role. This technicality is unlikely to matter most of the time, + but someone will probably come along with a need to differentiate + specific missiles at which point our reply will have to be "tough". + -- Ahruman 2009-06-21 + */ + NSString *mRole = missileRole; + if (mRole == nil) mRole = @"EQ_MISSILE"; + if ([itemKey isEqual:mRole]) return YES; + } + + return NO; +} + + +- (BOOL) hasPrimaryWeapon:(OOWeaponType)weaponType +{ + NSEnumerator *subEntEnum = nil; + ShipEntity *subEntity = nil; + + if (forward_weapon_type == weaponType || aft_weapon_type == weaponType) return YES; + + for (subEntEnum = [self shipSubEntityEnumerator]; (subEntity = [subEntEnum nextObject]); ) + { + if ([subEntity hasPrimaryWeapon:weaponType]) return YES; + } + + return NO; +} + + +- (BOOL) hasEquipmentItem:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons +{ + NSEnumerator *keyEnum = nil; + id key = nil; + + if (!includeWeapons && _equipment == nil) return NO; + + // Make sure it's an array or set, using a single-object set if it's a string. + if ([equipmentKeys isKindOfClass:[NSString class]]) equipmentKeys = [NSArray arrayWithObject:equipmentKeys]; + else if (![equipmentKeys isKindOfClass:[NSArray class]] && ![equipmentKeys isKindOfClass:[NSSet class]]) return NO; + + for (keyEnum = [equipmentKeys objectEnumerator]; (key = [keyEnum nextObject]); ) + { + if ([self hasOneEquipmentItem:key includeWeapons:includeWeapons]) return YES; + } + + return NO; +} + + +- (BOOL) hasEquipmentItem:(id)equipmentKeys +{ + return [self hasEquipmentItem:equipmentKeys includeWeapons:NO]; +} + + +- (BOOL) hasAllEquipment:(id)equipmentKeys includeWeapons:(BOOL)includeWeapons +{ + NSEnumerator *keyEnum = nil; + id key = nil; + + if (_equipment == nil) return NO; + + // Make sure it's an array or set, using a single-object set if it's a string. + if ([equipmentKeys isKindOfClass:[NSString class]]) equipmentKeys = [NSArray arrayWithObject:equipmentKeys]; + else if (![equipmentKeys isKindOfClass:[NSArray class]] && ![equipmentKeys isKindOfClass:[NSSet class]]) return NO; + + for (keyEnum = [equipmentKeys objectEnumerator]; (key = [keyEnum nextObject]); ) + { + if (![self hasOneEquipmentItem:key includeWeapons:includeWeapons]) return NO; + } + + return YES; +} + + +- (BOOL) hasAllEquipment:(id)equipmentKeys +{ + return [self hasAllEquipment:equipmentKeys includeWeapons:NO]; +} + + +- (BOOL) canAddEquipment:(NSString *)equipmentKey +{ + if ([equipmentKey hasSuffix:@"_DAMAGED"]) + { + equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]]; + } + + // FIXME: deal with special handling of missiles and mines. + + if ([self hasEquipmentItem:equipmentKey]) return NO; + if (![self equipmentValidToAdd:equipmentKey]) return NO; + + return YES; +} + + +- (BOOL) equipmentValidToAdd:(NSString *)equipmentKey +{ + OOEquipmentType *eqType = nil; + + if ([equipmentKey hasSuffix:@"_DAMAGED"]) + { + equipmentKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]]; + } + + eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey]; + if (eqType == nil) return NO; + + if ([eqType requiresEmptyPylon] && [self missileCount] >= [self missileCapacity]) return NO; + if ([eqType requiresMountedPylon] && [self missileCount] == 0) return NO; + if ([self availableCargoSpace] < [eqType requiredCargoSpace]) return NO; + if ([eqType requiresEquipment] != nil && ![self hasAllEquipment:[eqType requiresEquipment] includeWeapons:YES]) return NO; + if ([eqType requiresAnyEquipment] != nil && ![self hasEquipmentItem:[eqType requiresAnyEquipment] includeWeapons:YES]) return NO; + if ([eqType incompatibleEquipment] != nil && [self hasEquipmentItem:[eqType incompatibleEquipment] includeWeapons:YES]) return NO; + if ([eqType requiresCleanLegalRecord] && [self legalStatus] != 0) return NO; + if ([eqType requiresNonCleanLegalRecord] && [self legalStatus] == 0) return NO; + if ([eqType requiresFreePassengerBerth] && [self passengerCount] >= [self passengerCapacity]) return NO; + if ([eqType requiresFullFuel] && [self fuel] < [self fuelCapacity]) return NO; + if ([eqType requiresNonFullFuel] && [self fuel] >= [self fuelCapacity]) return NO; + + return YES; +} + + +- (void) addEquipmentItem:(NSString *)equipmentKey +{ + [self addEquipmentItem:equipmentKey withValidation:YES]; +} + + +- (void) addEquipmentItem:(NSString *)equipmentKey withValidation:(BOOL)validateAddition +{ + OOEquipmentType *eqType = nil; + + if (validateAddition == YES && ![self canAddEquipment:equipmentKey]) return; + eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey]; + + // FIXME: deal with special handling of missiles and mines. + if ([eqType isMissileOrMine]) return; + + if (_equipment == nil) _equipment = [[NSMutableSet alloc] init]; + + // if we heve one of these with a differen damage status - remove it first + NSString *alterKey = nil; + if ([equipmentKey hasSuffix:@"_DAMAGED"]) + { + alterKey = [equipmentKey substringToIndex:[equipmentKey length] - [@"_DAMAGED" length]]; + } + else + { + alterKey = [equipmentKey stringByAppendingString:@"_DAMAGED"]; + } + [_equipment removeObject:alterKey]; + + // add the equipment + [_equipment addObject:equipmentKey]; +} + + +- (NSEnumerator *) equipmentEnumerator +{ + return [_equipment objectEnumerator]; +} + + +- (unsigned) equipmentCount +{ + return [_equipment count]; +} + + +- (void) removeEquipmentItem:(NSString *)equipmentKey +{ + OOEquipmentType *eqType = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey]; + if (eqType == nil) return; + + if ([eqType isMissileOrMine]) + { + [self removeExternalStore:eqType]; + } + else + { + [_equipment removeObject:equipmentKey]; + if ([_equipment count] == 0) [self removeAllEquipment]; + if (isPlayer) + { + if([equipmentKey isEqualToString:@"EQ_ADVANCED_COMPASS"]) [(PlayerEntity*)self setCompassMode:COMPASS_MODE_BASIC]; + } + } +} + + +- (void) removeExternalStore:(OOEquipmentType *)eqType +{ + NSString *identifier = [eqType identifier]; + + // If we have missiles, and equipment identifier matches our missile role, decrement missile count. + if (missiles > 0) + { + if ([missileRole isEqualToString:identifier] || (missileRole == nil && [identifier isEqualToString:@"EQ_MISSILE"])) + { + missiles--; + } + } +} + + +- (void) removeAllEquipment +{ + [_equipment release]; + _equipment = nil; +} + + +- (unsigned) passengerCount +{ + return 0; +} + + +- (unsigned) passengerCapacity +{ + return 0; +} + + +- (unsigned) missileCount +{ + return missiles; +} + + +- (unsigned) missileCapacity +{ + // FIXME: need useful maximum from shipdata key max_missiles (or missiles if no max specified). + return missiles; +} + + +- (BOOL) hasScoop +{ + return [self hasEquipmentItem:@"EQ_FUEL_SCOOPS"]; +} + + +- (BOOL) hasECM +{ + return [self hasEquipmentItem:@"EQ_ECM"]; +} + + +- (BOOL) hasCloakingDevice +{ + return [self hasEquipmentItem:@"EQ_CLOAKING_DEVICE"]; +} + + +- (BOOL) hasMilitaryScannerFilter +{ + return [self hasEquipmentItem:@"EQ_MILITARY_SCANNER_FILTER"]; +} + + +- (BOOL) hasMilitaryJammer +{ + return [self hasEquipmentItem:@"EQ_MILITARY_JAMMER"]; +} + + +- (BOOL) hasExpandedCargoBay +{ + return [self hasEquipmentItem:@"EQ_CARGO_BAY"]; +} + + +- (BOOL) hasShieldBooster +{ + return [self hasEquipmentItem:@"EQ_SHIELD_BOOSTER"]; +} + + +- (BOOL) hasMilitaryShieldEnhancer +{ + return [self hasEquipmentItem:@"EQ_NAVAL_SHIELD_BOOSTER"]; +} + + +- (BOOL) hasHeatShield +{ + return [self hasEquipmentItem:@"EQ_HEAT_SHIELD"]; +} + + +- (BOOL) hasFuelInjection +{ + return [self hasEquipmentItem:@"EQ_FUEL_INJECTION"]; +} + + +- (BOOL) hasEnergyBomb +{ + return [self hasEquipmentItem:@"EQ_ENERGY_BOMB"]; +} + + +- (BOOL) hasEscapePod +{ + return [self hasEquipmentItem:@"EQ_ESCAPE_POD"]; +} + + +- (BOOL) hasDockingComputer +{ + return [self hasEquipmentItem:@"EQ_DOCK_COMP"]; +} + + +- (BOOL) hasGalacticHyperdrive +{ + return [self hasEquipmentItem:@"EQ_GAL_DRIVE"]; +} + + +- (float) shieldBoostFactor +{ + float boostFactor = 1.0f; + if ([self hasShieldBooster]) boostFactor += 1.0f; + if ([self hasMilitaryShieldEnhancer]) boostFactor += 1.0f; + + return boostFactor; +} + + +- (float) maxForwardShieldLevel +{ + return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor]; +} + + +- (float) maxAftShieldLevel +{ + return BASELINE_SHIELD_LEVEL * [self shieldBoostFactor]; +} + + +- (float) shieldRechargeRate +{ + return [self hasMilitaryShieldEnhancer] ? 3.0f : 2.0f; +} + + +- (float) afterburnerFactor +{ + return 7.0f; +} + + +//////////////// +// // +// behaviours // +// // +- (void) behaviour_stop_still:(double) delta_t +{ + double damping = 0.5 * delta_t; + // damp roll + if (flightRoll < 0) + flightRoll += (flightRoll < -damping) ? damping : -flightRoll; + if (flightRoll > 0) + flightRoll -= (flightRoll > damping) ? damping : flightRoll; + // damp pitch + if (flightPitch < 0) + flightPitch += (flightPitch < -damping) ? damping : -flightPitch; + if (flightPitch > 0) + flightPitch -= (flightPitch > damping) ? damping : flightPitch; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_idle:(double) delta_t +{ + double damping = 0.5 * delta_t; + if ((!isStation)&&(scanClass != CLASS_BUOY)) + { + // damp roll + if (flightRoll < 0) + flightRoll += (flightRoll < -damping) ? damping : -flightRoll; + if (flightRoll > 0) + flightRoll -= (flightRoll > damping) ? damping : flightRoll; + } + if (scanClass != CLASS_BUOY) + { + // damp pitch + if (flightPitch < 0) + flightPitch += (flightPitch < -damping) ? damping : -flightPitch; + if (flightPitch > 0) + flightPitch -= (flightPitch > damping) ? damping : flightPitch; + } + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_tumble:(double) delta_t +{ + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_tractored:(double) delta_t +{ + double distance = [self rangeToDestination]; + desired_range = collision_radius * 2.0; + ShipEntity* hauler = (ShipEntity*)[self owner]; + if ((hauler)&&([hauler isShip])) + { + if (distance < desired_range) + { + behaviour = BEHAVIOUR_TUMBLE; + [self setStatus:STATUS_IN_FLIGHT]; + [hauler scoopUp:self]; + return; + } + GLfloat tf = TRACTOR_FORCE / mass; + destination = [hauler absoluteTractorPosition]; + // adjust for difference in velocity (spring rule) + Vector dv = vector_between([self velocity], [hauler velocity]); + GLfloat moment = delta_t * 0.25 * tf; + velocity.x += moment * dv.x; + velocity.y += moment * dv.y; + velocity.z += moment * dv.z; + // acceleration = force / mass + // force proportional to distance (spring rule) + Vector dp = vector_between(position, destination); + moment = delta_t * 0.5 * tf; + velocity.x += moment * dp.x; + velocity.y += moment * dp.y; + velocity.z += moment * dp.z; + // force inversely proportional to distance + GLfloat d2 = magnitude2(dp); + moment = (d2 > 0.0)? delta_t * 5.0 * tf / d2 : 0.0; + if (d2 > 0.0) + { + velocity.x += moment * dp.x; + velocity.y += moment * dp.y; + velocity.z += moment * dp.z; + } + // + if ([self status] == STATUS_BEING_SCOOPED) + { + BOOL lost_contact = (distance > hauler->collision_radius + collision_radius + 250.0f); // 250m range for tractor beam + if ([hauler isPlayer]) + { + switch ([(PlayerEntity*)hauler dialFuelScoopStatus]) + { + case SCOOP_STATUS_NOT_INSTALLED : + case SCOOP_STATUS_FULL_HOLD : + lost_contact = YES; // don't draw + break; + } + } + // + if (lost_contact) // 250m range for tractor beam + { + // escaped tractor beam + [self setStatus:STATUS_IN_FLIGHT]; + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + [shipAI exitStateMachineWithMessage:nil]; // exit nullAI.plist + } + else if ([hauler isPlayer]) + { + [(PlayerEntity*)hauler setScoopsActive]; + } + } + } + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + desired_speed = 0.0; + thrust = 25.0; // used to damp velocity (must be less than hauler thrust) + [self applyThrust:delta_t]; + thrust = 0.0; // must reset thrust now +} + + +- (void) behaviour_track_target:(double) delta_t +{ + [self trackPrimaryTarget:delta_t:NO]; + if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget)) + { + [self avoidCollision]; + } + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_intercept_target:(double) delta_t +{ + double range = [self rangeToPrimaryTarget]; + if (behaviour == BEHAVIOUR_INTERCEPT_TARGET) + { + desired_speed = maxFlightSpeed; + if (range < desired_range) + { + [shipAI reactToMessage:@"DESIRED_RANGE_ACHIEVED"]; + } + desired_speed = maxFlightSpeed * [self trackPrimaryTarget:delta_t:NO]; + } + else + { + ShipEntity* target = [self primaryTarget]; + double target_speed = [target speed]; + double eta = range / (flightSpeed - target_speed); + double last_success_factor = success_factor; + double last_distance = last_success_factor; + double distance = [self rangeToDestination]; + success_factor = distance; + // + double slowdownTime = 96.0 / thrust; // more thrust implies better slowing + double minTurnSpeedFactor = 0.005 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds + + if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor)) + desired_speed = flightSpeed * 0.75; // cut speed by 50% to a minimum minTurnSpeedFactor of speed + else + desired_speed = maxFlightSpeed; + + if (desired_speed < target_speed) + { + desired_speed += target_speed; + if (target_speed > maxFlightSpeed) + { + [self noteLostTarget]; + } + } + // + if (target) // check introduced to stop crash at next line + { + destination = target->position; + desired_range = 0.5 * target->collision_radius; + [self trackDestination: delta_t : NO]; + } + // + if (distance < last_distance) // improvement + { + frustration -= delta_t; + if (frustration < 0.0) + frustration = 0.0; + } + else + { + frustration += delta_t; + if (frustration > 10.0) // 10s of frustration + { + [shipAI reactToMessage:@"FRUSTRATED"]; + frustration -= 5.0; //repeat after another five seconds' frustration + } + } + } + if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget)) + [self avoidCollision]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_attack_target:(double) delta_t +{ + BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL); + float max_available_speed = maxFlightSpeed; + double range = [self rangeToPrimaryTarget]; + if (canBurn) max_available_speed *= [self afterburnerFactor]; + + [self activateCloakingDevice]; + + desired_speed = max_available_speed; + if (range < 0.035 * weaponRange) + behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET; + else + if (universalID & 1) // 50% of ships are smart S.M.R.T. smart! + { + if (randf() < 0.75) + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX; + else + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE; + } + else + { + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; + } + frustration = 0.0; // behaviour changed, so reset frustration + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_fly_to_target_six:(double) delta_t +{ + BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL); + float max_available_speed = maxFlightSpeed; + double range = [self rangeToPrimaryTarget]; + if (canBurn) max_available_speed *= [self afterburnerFactor]; + + // deal with collisions and lost targets + if (proximity_alert != NO_TARGET) + { + [self avoidCollision]; + } + if (range > SCANNER_MAX_RANGE || primaryTarget == NO_TARGET) + { + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + [self noteLostTarget]; + } + + // control speed + BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed); + double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0); + ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget]; + double target_speed = [target speed]; + double last_success_factor = success_factor; // EW + double distance = [self rangeToDestination]; + success_factor = distance; // EW + + if (range < slow_down_range) + { + desired_speed = OOMax_d(target_speed, 0.4 * maxFlightSpeed); + + // avoid head-on collision + if ((range < 0.5 * distance)&&(behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX)) + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE; + } + else + desired_speed = max_available_speed; // use afterburner to approach + + + // if within 0.75km of the target's six or twelve then vector in attack + if (distance < 750.0) + { + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; + frustration = 0.0; + desired_speed = OOMax_d(target_speed, 0.4 * maxFlightSpeed); // within the weapon's range don't use afterburner + } + + // target-six + if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX) + { + // head for a point weapon-range * 0.5 to the six of the target + // + destination = [target distance_six:0.5 * weaponRange]; + } + // target-twelve + if (behaviour == BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE) + { + // head for a point 1.25km above the target + // + destination = [target distance_twelve:1250]; + } + + double confidenceFactor = [self trackDestination:delta_t :NO]; + + if(success_factor > last_success_factor || confidenceFactor < 0.85) frustration += delta_t; // EW + else if(frustration > 0.0) frustration -= delta_t; + if(frustration > 10) // EW + { + frustration = 0.0; + if (randf() < 0.4) + { + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; + } + else + { + if (randf() < 0.5) + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX; + else + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE; + } + } + + // use weaponry + int missile_chance = 0; + int rhs = 3.2 / delta_t; + if (rhs) missile_chance = 1 + (ranrot_rand() % rhs); + + double hurt_factor = 16 * pow(energy/maxEnergy, 4.0); + if (missiles > missile_chance * hurt_factor) + { + [self fireMissile]; + } + [self activateCloakingDevice]; + [self fireMainWeapon:range]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_attack_mining_target:(double) delta_t +{ + double range = [self rangeToPrimaryTarget]; + if (primaryTarget == NO_TARGET || range > SCANNER_MAX_RANGE) + { + [self noteLostTarget]; + desired_speed = maxFlightSpeed * 0.375; + return; + } + else if ((range < 650) || (proximity_alert != NO_TARGET)) + { + if (proximity_alert == NO_TARGET) + { + desired_speed = range * maxFlightSpeed / (650.0 * 16.0); + } + else + { + [self avoidCollision]; + } + } + else + { + //we have a target, its within scanner range, and outside 650 + desired_speed = maxFlightSpeed * 0.375; + } + + [self trackPrimaryTarget:delta_t:NO]; + [self fireMainWeapon:range]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_attack_fly_to_target:(double) delta_t +{ + BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL); + float max_available_speed = maxFlightSpeed; + double range = [self rangeToPrimaryTarget]; + if (canBurn) max_available_speed *= [self afterburnerFactor]; + if (primaryTarget == NO_TARGET) + { + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + [self noteLostTarget]; + return; + } + if ((range < COMBAT_IN_RANGE_FACTOR * weaponRange)||(proximity_alert != NO_TARGET)) + { + if (proximity_alert == NO_TARGET) + { + if (aft_weapon_type == WEAPON_NONE) + { + jink.x = (ranrot_rand() % 256) - 128.0; + jink.y = (ranrot_rand() % 256) - 128.0; + jink.z = 1000.0; + behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET; + frustration = 0.0; + desired_speed = max_available_speed; + } + else + { + // entering running defense mode + jink = kZeroVector; + behaviour = BEHAVIOUR_RUNNING_DEFENSE; + frustration = 0.0; + desired_speed = maxFlightSpeed; + } + } + else + { + [self avoidCollision]; + } + } + else + { + if (range > SCANNER_MAX_RANGE) + { + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + [self noteLostTarget]; + } + } + + // control speed + // + BOOL isUsingAfterburner = canBurn && (flightSpeed > maxFlightSpeed); + double slow_down_range = weaponRange * COMBAT_WEAPON_RANGE_FACTOR * ((isUsingAfterburner)? 3.0 * [self afterburnerFactor] : 1.0); + ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget]; + double target_speed = [target speed]; + if (range <= slow_down_range) + desired_speed = OOMax_d(target_speed, 0.25 * maxFlightSpeed); // within the weapon's range match speed + else + desired_speed = max_available_speed; // use afterburner to approach + + double last_success_factor = success_factor; + success_factor = [self trackPrimaryTarget:delta_t:NO]; // do the actual piloting + if ((success_factor > 0.999)||(success_factor > last_success_factor)) + { + frustration -= delta_t; + if (frustration < 0.0) + frustration = 0.0; + } + else + { + frustration += delta_t; + if (frustration > 3.0) // 3s of frustration + { + [shipAI reactToMessage:@"FRUSTRATED"]; + // THIS IS HERE AS A TEST ONLY + // BREAK OFF + jink.x = (ranrot_rand() % 256) - 128.0; + jink.y = (ranrot_rand() % 256) - 128.0; + jink.z = 1000.0; + behaviour = BEHAVIOUR_ATTACK_FLY_FROM_TARGET; + frustration = 0.0; + desired_speed = maxFlightSpeed; + } + } + + int missile_chance = 0; + int rhs = 3.2 / delta_t; + if (rhs) missile_chance = 1 + (ranrot_rand() % rhs); + + double hurt_factor = 16 * pow(energy/maxEnergy, 4.0); + if (missiles > missile_chance * hurt_factor) + { + [self fireMissile]; + } + [self activateCloakingDevice]; + [self fireMainWeapon:range]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_attack_fly_from_target:(double) delta_t +{ + double range = [self rangeToPrimaryTarget]; + if (primaryTarget == NO_TARGET) + { + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + [self noteLostTarget]; + return; + } + if (range > COMBAT_OUT_RANGE_FACTOR * weaponRange + 15.0 * jink.x) + { + jink.x = 0.0; + jink.y = 0.0; + jink.z = 0.0; + behaviour = BEHAVIOUR_ATTACK_TARGET; + frustration = 0.0; + } + [self trackPrimaryTarget:delta_t:YES]; + + int missile_chance = 0; + int rhs = 3.2 / delta_t; + if (rhs) missile_chance = 1 + (ranrot_rand() % rhs); + + double hurt_factor = 16 * pow(energy/maxEnergy, 4.0); + if (missiles > missile_chance * hurt_factor) + { + [self fireMissile]; + } + [self activateCloakingDevice]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_running_defense:(double) delta_t +{ + double range = [self rangeToPrimaryTarget]; + if (range > weaponRange || range == 0) + { + jink.x = 0.0; + jink.y = 0.0; + jink.z = 0.0; + behaviour = BEHAVIOUR_ATTACK_FLY_TO_TARGET; + frustration = 0.0; + } + [self trackPrimaryTarget:delta_t:YES]; + [self fireAftWeapon:range]; + [self activateCloakingDevice]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_flee_target:(double) delta_t +{ + BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL); + float max_available_speed = maxFlightSpeed; + double range = [self rangeToPrimaryTarget]; + if (canBurn) max_available_speed *= [self afterburnerFactor]; + + if (range > desired_range || range == 0) + [shipAI message:@"REACHED_SAFETY"]; + else + desired_speed = max_available_speed; + [self trackPrimaryTarget:delta_t:YES]; + + int missile_chance = 0; + int rhs = 3.2 / delta_t; + if (rhs) missile_chance = 1 + (ranrot_rand() % rhs); + + if (([self hasEnergyBomb]) && (range < 10000.0) && canBurn) + { + float qbomb_chance = 0.01 * delta_t; + if (randf() < qbomb_chance) + { + [self launchEnergyBomb]; + } + } + + double hurt_factor = 16 * pow(energy/maxEnergy, 4.0); + if (([(ShipEntity *)[self primaryTarget] primaryTarget] == self)&&(missiles > missile_chance * hurt_factor)) + [self fireMissile]; + [self activateCloakingDevice]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_fly_range_from_destination:(double) delta_t +{ + double distance = [self rangeToDestination]; + if (distance < desired_range) + { + behaviour = BEHAVIOUR_FLY_FROM_DESTINATION; + desired_speed = maxFlightSpeed; // Not all AI define speed when flying away. Start with max speed to stay compatible with such AI's. + } + else + { + behaviour = BEHAVIOUR_FLY_TO_DESTINATION; + } + if (proximity_alert != NO_TARGET) + { + [self avoidCollision]; + } + frustration = 0.0; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_face_destination:(double) delta_t +{ + double max_cos = 0.995; + double distance = [self rangeToDestination]; + double old_pitch = flightPitch; + desired_speed = 0.0; + if (desired_range > 1.0 && distance > desired_range) + max_cos = sqrt(1 - 0.90 * desired_range*desired_range/(distance * distance)); // Head for a point within 95% of desired_range (must match the value in trackDestination) + else + max_cos = 0.995; // 0.995 - cos(5 degrees) is close enough + double confidenceFactor = [self trackDestination:delta_t:NO]; + if (confidenceFactor >= max_cos) + { + // desired facing achieved + [shipAI message:@"FACING_DESTINATION"]; + frustration = 0.0; + if(docking_match_rotation) // IDLE stops rotating while docking + { + behaviour = BEHAVIOUR_FLY_TO_DESTINATION; + } + else + { + behaviour = BEHAVIOUR_IDLE; + } + } + + if(flightSpeed == 0) frustration += delta_t; + if (frustration > 15.0 / max_flight_pitch) // allow more time for slow ships. + { + frustration = 0.0; + [shipAI reactToMessage:@"FRUSTRATED"]; + if(flightPitch == old_pitch) flightPitch = 0.5 * max_flight_pitch; // hack to get out of frustration. + } + + /* 2009-7-18 Eric: the condition check below is intended to eliminate the flippering between two positions for fast turning ships + during low FPS conditions. This flippering is particular frustrating on slow computers during docking. But with my current computer I can't + induce those low FPS conditions so I can't properly test if it helps. + I did try with the TAF time acceleration that also generated larger frame jumps and than it seemed to help. + */ + if(flightSpeed == 0 && frustration > 5 && confidenceFactor > 0.5 && ((flightPitch > 0 && old_pitch < 0) || (flightPitch < 0 && old_pitch > 0))) + { + flightPitch += 0.5 * old_pitch; // damping with last pitch value. + } + + if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget)) + [self avoidCollision]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_formation_form_up:(double) delta_t +{ + // destination for each escort is set in update() from owner. + ShipEntity* leadShip = [self owner]; + double distance = [self rangeToDestination]; + double eta = (distance - desired_range) / flightSpeed; + if(eta < 0) eta = 0; + if ((eta < 5.0)&&(leadShip)&&(leadShip->isShip)) + desired_speed = [leadShip flightSpeed] * (1 + eta * 0.05); // EW: This code works better, specialy at low speeds. + // desired_speed = [leadShip flightSpeed] * 1.25; // EW Original code, escorts always fly 25% to fast or drop speed 50% later on. + // this speed dropping does not work well at low speeds leading to escorts "waggling". + else + desired_speed = maxFlightSpeed; + + double last_distance = success_factor; + success_factor = distance; + + // do the actual piloting!! + [self trackDestination:delta_t: NO]; + + eta = eta / 0.51; // 2% safety margin assuming an average of half current speed + GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / thrust : 4.0; + GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds + + if ((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor)) + desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed + + if (distance < last_distance) // improvement + { + frustration -= 0.25 * delta_t; + if (frustration < 0.0) + frustration = 0.0; + } + else + { + frustration += delta_t; + if (frustration > 15.0) + { + if (!leadShip) [shipAI reactToMessage:@"FRUSTRATED"]; // escorts never reach their destination when following leader. + else if (distance > 0.5 * scannerRange) + { + flightPitch = max_flight_pitch; // hack to get out of frustration. + } + frustration = 0; + } + } + if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget)) + [self avoidCollision]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_fly_to_destination:(double) delta_t +{ + double distance = [self rangeToDestination]; + if (distance < desired_range)// + collision_radius) + { + // desired range achieved + [shipAI message:@"DESIRED_RANGE_ACHIEVED"]; + if(!docking_match_rotation) // IDLE stops rotating while docking + { + behaviour = BEHAVIOUR_IDLE; + desired_speed = 0.0; + } + frustration = 0.0; + } + else + { + double last_distance = success_factor; + success_factor = distance; + + // do the actual piloting!! + double confidenceFactor = [self trackDestination:delta_t: NO]; + if(confidenceFactor < 0.2) confidenceFactor = 0.2; // don't allow small or negative values. + + /* 2009-07-19 Eric: Estimated Time of Arrival (eta) should also take the "angle to target" into account (confidenceFactor = cos(angle to target)) + and should not fuss about that last meter and use "distance + 1" instead of just "distance". + trackDestination already did pitch regulation, use confidence here only for cutting down to high speeds. + This should prevent ships crawling to their destination when they try to pull up close to their destination. + + To prevent ships circling around their target without reaching destination I added a limitation based on turnrate, + speed and distance to target. Formula based on satelite orbit: + orbitspeed = turnrate (rad/sec) * radius (m) or flightSpeed = max_flight_pitch * 2 Pi * distance + Speed must be significant lower when not flying in direction of target (low confidenceFactor) or it can never approach its destination + and the ships runs the risk flying in circles around the target. (exclude active escorts) + */ + GLfloat eta = ((distance + 1) - desired_range) / (0.51 * flightSpeed * confidenceFactor); // 2% safety margin assuming an average of half current speed + GLfloat slowdownTime = (thrust > 0.0)? flightSpeed / thrust : 4.0; + GLfloat minTurnSpeedFactor = 0.05 * max_flight_pitch * max_flight_roll; // faster turning implies higher speeds + + if (((eta < slowdownTime)&&(flightSpeed > maxFlightSpeed * minTurnSpeedFactor)) || (flightSpeed > max_flight_pitch * 5 * confidenceFactor * distance)) + desired_speed = flightSpeed * 0.50; // cut speed by 50% to a minimum minTurnSpeedFactor of speed + + if (distance < last_distance) // improvement + { + frustration -= 0.25 * delta_t; + if (frustration < 0.0) + frustration = 0.0; + } + else + { + frustration += delta_t; + if ((frustration > slowdownTime * 10.0 && slowdownTime > 0)||(frustration > 15.0)) // 10x slowdownTime or 15s of frustration + { + [shipAI reactToMessage:@"FRUSTRATED"]; + frustration -= slowdownTime * 5.0; //repeat after another five units of frustration + } + } + } + if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget)) + [self avoidCollision]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_fly_from_destination:(double) delta_t +{ + double distance = [self rangeToDestination]; + if (distance > desired_range) + { + // desired range achieved + [shipAI message:@"DESIRED_RANGE_ACHIEVED"]; + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + desired_speed = 0.0; + } + + [self trackDestination:delta_t:YES]; + if ((proximity_alert != NO_TARGET)&&(proximity_alert != primaryTarget)) + [self avoidCollision]; + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_avoid_collision:(double) delta_t +{ + double distance = [self rangeToDestination]; + if (distance > desired_range) + { + [self resumePostProximityAlert]; + } + else + { + ShipEntity* prox_ship = [self proximity_alert]; + if (prox_ship) + { + desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE; + destination = prox_ship->position; + } + double dq = [self trackDestination:delta_t:YES]; + if (dq >= 0) + dq = 0.5 * dq + 0.5; + else + dq = 0.0; + desired_speed = maxFlightSpeed * dq; + } + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + [self applyThrust:delta_t]; +} + + +- (void) behaviour_track_as_turret:(double) delta_t +{ + double aim = [self ballTrackLeadingTarget:delta_t]; + ShipEntity* turret_owner = (ShipEntity *)[self owner]; + ShipEntity* turret_target = (ShipEntity *)[turret_owner primaryTarget]; + // + if ((turret_owner)&&(turret_target)&&[turret_owner hasHostileTarget]) + { + Vector p1 = turret_target->position; + Vector p0 = turret_owner->position; + double cr = turret_owner->collision_radius; + p1.x -= p0.x; p1.y -= p0.y; p1.z -= p0.z; + if (aim > .95) + [self fireTurretCannon: sqrt(magnitude2(p1)) - cr]; + } +} + + +- (void) behaviour_fly_thru_navpoints:(double) delta_t +{ + int navpoint_plus_index = (next_navpoint_index + 1) % number_of_navpoints; + Vector d1 = navpoints[ next_navpoint_index]; // head for this one + Vector d2 = navpoints[ navpoint_plus_index]; // but be facing this one + + Vector rel = vector_between(d1, position); // vector from d1 to position + Vector ref = vector_between(d2, d1); // vector from d2 to d1 + ref = vector_normal(ref); + + Vector xp = make_vector(ref.y * rel.z - ref.z * rel.y, ref.z * rel.x - ref.x * rel.z, ref.x * rel.y - ref.y * rel.x); + + GLfloat v0 = 0.0; + + GLfloat r0 = dot_product(rel, ref); // proportion of rel in direction ref + + // if r0 is negative then we're the wrong side of things + + GLfloat r1 = sqrtf(magnitude2(xp)); // distance of position from line + + BOOL in_cone = (r0 > 0.5 * r1); + + if (!in_cone) // are we in the approach cone ? + r1 = 25.0 * flightSpeed; // aim a few km out! + else + r1 *= 2.0; + + GLfloat dist2 = magnitude2(rel); + + if (dist2 < desired_range * desired_range) + { + // desired range achieved + [self doScriptEvent:@"shipReachedNavPoint" andReactToAIMessage:@"NAVPOINT_REACHED"]; + if (navpoint_plus_index == 0) + { + [self doScriptEvent:@"shipReachedEndPoint" andReactToAIMessage:@"ENDPOINT_REACHED"]; + behaviour = BEHAVIOUR_IDLE; + } + next_navpoint_index = navpoint_plus_index; // loop as required + } + else + { + double last_success_factor = success_factor; + double last_dist2 = last_success_factor; + success_factor = dist2; + + // set destination spline point from r1 and ref + destination = make_vector(d1.x + r1 * ref.x, d1.y + r1 * ref.y, d1.z + r1 * ref.z); + + // do the actual piloting!! + // + // aim to within 1m + GLfloat temp = desired_range; + if (in_cone) + desired_range = 1.0; + else + desired_range = 100.0; + v0 = [self trackDestination:delta_t: NO]; + desired_range = temp; + + if (dist2 < last_dist2) // improvement + { + frustration -= 0.25 * delta_t; + if (frustration < 0.0) + frustration = 0.0; + } + else + { + frustration += delta_t; + if (frustration > 15.0) // 15s of frustration + { + [shipAI reactToMessage:@"FRUSTRATED"]; + frustration -= 15.0; //repeat after another 15s of frustration + } + } + } + + [self applyRoll:delta_t*flightRoll andClimb:delta_t*flightPitch]; + GLfloat temp = desired_speed; + desired_speed *= v0 * v0; + [self applyThrust:delta_t]; + desired_speed = temp; +} + + +- (void) saveToLastFrame +{ + double t_now = [UNIVERSE getTime]; + + if (t_now >= trackTime + 0.1) // update at most every 1/10 of a second + { + // save previous data + Quaternion qrot = [self normalOrientation]; + trackTime = t_now; + track[trackIndex].position = position; + track[trackIndex].orientation = qrot; + track[trackIndex].timeframe = trackTime; + track[trackIndex].k = v_forward; + + // Update exhaust + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + Frame thisFrame = { trackTime, kZeroVector, qrot, v_forward }; + + for (subEnum = [self exhaustEnumerator]; (se = [subEnum nextObject]); ) + { + Vector sepos = [se position]; + thisFrame.position = make_vector( + position.x + v_right.x * sepos.x + v_up.x * sepos.y + v_forward.x * sepos.z, + position.y + v_right.y * sepos.x + v_up.y * sepos.y + v_forward.y * sepos.z, + position.z + v_right.z * sepos.x + v_up.z * sepos.y + v_forward.z * sepos.z); + [se saveFrame:thisFrame atIndex:trackIndex]; // syncs subentity trackIndex to this entity + } + + trackIndex = (trackIndex + 1 ) & 0xff; + } +} + + +// reset position tracking +- (void) resetTracking +{ + Quaternion qrot = [self normalOrientation]; + Vector vi = vector_right_from_quaternion(qrot); + Vector vj = vector_up_from_quaternion(qrot); + Vector vk = vector_forward_from_quaternion(qrot); + Frame resetFrame = { 0, position, qrot, vk }; + + Vector vel = vector_multiply_scalar(vk, flightSpeed); + + [self resetFramesFromFrame:resetFrame withVelocity:vel]; + + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + + for (subEnum = [self exhaustEnumerator]; (se = [subEnum nextObject]); ) + { + Vector sepos = [se position]; + resetFrame.position = make_vector( + position.x + vi.x * sepos.x + vj.x * sepos.y + vk.x * sepos.z, + position.y + vi.y * sepos.x + vj.y * sepos.y + vk.y * sepos.z, + position.z + vi.z * sepos.x + vj.z * sepos.y + vk.z * sepos.z); + [se resetFramesFromFrame:resetFrame withVelocity:vel]; + } +} + + +- (void)drawEntity:(BOOL)immediate :(BOOL)translucent +{ + NSEnumerator *subEntityEnum = nil; + ShipEntity *subEntity = nil; + + if ((no_draw_distance < zero_distance) || // Done redundantly to skip subentities + (cloaking_device_active && randf() > 0.10)) + { + // Don't draw. + return; + } + + // Draw self. + [super drawEntity:immediate :translucent]; + +#ifndef NDEBUG + // Draw bounding boxes if we have to before going for the subentities. + // TODO: the translucent flag here makes very little sense. Something's wrong with the matrices. + if (translucent) [self drawDebugStuff]; + else if (gDebugFlags & DEBUG_BOUNDING_BOXES && ![self isSubEntity]) + { + OODebugDrawBoundingBox([self boundingBox]); + OODebugDrawColoredBoundingBox(totalBoundingBox, [OOColor purpleColor]); + } +#endif + + // Draw subentities. + if (!immediate) // TODO: is this relevant any longer? + { + for (subEntityEnum = [self subEntityEnumerator]; (subEntity = [subEntityEnum nextObject]); ) + { + [subEntity setOwner:self]; // refresh ownership + [subEntity drawSubEntity:immediate :translucent]; + } + } +} + + +#ifndef NDEBUG +- (void) drawDebugStuff +{ + + if (reportAIMessages) + { + OODebugDrawPoint(destination, [OOColor blueColor]); + OODebugDrawColoredLine([self position], destination, [OOColor colorWithCalibratedWhite:0.15 alpha:1.0]); + + Entity *pTarget = [self primaryTarget]; + if (pTarget != nil) + { + OODebugDrawPoint([pTarget position], [OOColor redColor]); + OODebugDrawColoredLine([self position], [pTarget position], [OOColor colorWithCalibratedRed:0.2 green:0.0 blue:0.0 alpha:1.0]); + } + + Entity *sTarget = [UNIVERSE entityForUniversalID:targetStation]; + if (sTarget != pTarget && [sTarget isStation]) + { + OODebugDrawPoint([sTarget position], [OOColor cyanColor]); + } + + Entity *fTarget = [UNIVERSE entityForUniversalID:found_target]; + if (fTarget != nil && fTarget != pTarget && fTarget != sTarget) + { + OODebugDrawPoint([fTarget position], [OOColor magentaColor]); + } + } +} +#endif + + +- (void) drawSubEntity:(BOOL) immediate :(BOOL) translucent +{ + Entity* my_owner = [self owner]; + if (my_owner) + { + // this test provides an opportunity to do simple LoD culling + zero_distance = [my_owner zeroDistance]; + if (zero_distance > no_draw_distance) + { + return; // TOO FAR AWAY + } + } + + if ([self status] == STATUS_ACTIVE) + { + Vector abspos = position; // STATUS_ACTIVE means it is in control of it's own orientation + Entity *last = nil; + Entity *father = my_owner; + OOMatrix r_mat; + + while ((father)&&(father != last) &&father != NO_TARGET) + { + r_mat = [father drawRotationMatrix]; + abspos = vector_add(OOVectorMultiplyMatrix(abspos, r_mat), [father position]); + last = father; + if (![last isSubEntity]) break; + father = [father owner]; + } + + GLLoadOOMatrix([UNIVERSE viewMatrix]); + glPopMatrix(); + glPushMatrix(); + GLTranslateOOVector(abspos); + GLMultOOMatrix(rotMatrix); + + [self drawEntity:immediate :translucent]; + } + else + { + glPushMatrix(); + + GLTranslateOOVector(position); + GLMultOOMatrix(rotMatrix); + + [self drawEntity:immediate :translucent]; + + glPopMatrix(); + } + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_BOUNDING_BOXES) + { + OODebugDrawBoundingBox([self boundingBox]); + } +#endif + +} + + +static GLfloat cargo_color[4] = { 0.9, 0.9, 0.9, 1.0}; // gray +static GLfloat hostile_color[4] = { 1.0, 0.25, 0.0, 1.0}; // red/orange +static GLfloat neutral_color[4] = { 1.0, 1.0, 0.0, 1.0}; // yellow +static GLfloat friendly_color[4] = { 0.0, 1.0, 0.0, 1.0}; // green +static GLfloat missile_color[4] = { 0.0, 1.0, 1.0, 1.0}; // cyan +static GLfloat police_color1[4] = { 0.5, 0.0, 1.0, 1.0}; // purpley-blue +static GLfloat police_color2[4] = { 1.0, 0.0, 0.5, 1.0}; // purpley-red +static GLfloat jammed_color[4] = { 0.0, 0.0, 0.0, 0.0}; // clear black +static GLfloat mascem_color1[4] = { 0.3, 0.3, 0.3, 1.0}; // dark gray +static GLfloat mascem_color2[4] = { 0.4, 0.1, 0.4, 1.0}; // purple + +- (GLfloat *) scannerDisplayColorForShip:(ShipEntity*)otherShip :(BOOL)isHostile :(BOOL)flash +{ + + if ([self isJammingScanning]) + { + if (![otherShip hasMilitaryScannerFilter]) + return jammed_color; + else + { + if (flash) + return mascem_color1; + else + { + if (isHostile) + return hostile_color; + else + return mascem_color2; + } + } + } + + switch (scanClass) + { + case CLASS_ROCK : + case CLASS_CARGO : + return cargo_color; + case CLASS_THARGOID : + if (flash) + return hostile_color; + else + return friendly_color; + case CLASS_MISSILE : + return missile_color; + case CLASS_STATION : + return friendly_color; + case CLASS_BUOY : + if (flash) + return friendly_color; + else + return neutral_color; + case CLASS_POLICE : + case CLASS_MILITARY : + if ((isHostile)&&(flash)) + return police_color2; + else + return police_color1; + case CLASS_MINE : + if (flash) + return neutral_color; + else + return hostile_color; + default : + if (isHostile) + return hostile_color; + } + return neutral_color; +} + + +- (BOOL)isCloaked +{ + return cloaking_device_active; +} + + +- (void)setCloaked:(BOOL)cloak +{ + if (cloak) [self activateCloakingDevice]; + else [self deactivateCloakingDevice]; +} + + +- (BOOL) isJammingScanning +{ + return ([self hasMilitaryJammer] && military_jammer_active); +} + + +- (void) addSubEntity:(Entity *)sub +{ + if (sub == nil) return; + + if (subEntities == nil) subEntities = [[NSMutableArray alloc] init]; + sub->isSubEntity = YES; + // Order matters - need consistent state in setOwner:. -- Ahruman 2008-04-20 + [subEntities addObject:sub]; + [sub setOwner:self]; +} + + +- (void) setOwner:(Entity *)who_owns_entity +{ + [super setOwner:who_owns_entity]; + + /* Reset shader binding target so that bind-to-super works. + This is necessary since we don't know about the owner in + setUpShipFromDictionary:, when the mesh is initially set up. + -- Ahruman 2008-04-19 + */ + OODrawable *drawable_ = [self drawable]; + if (isSubEntity) + { + [drawable_ setBindingTarget:self]; + } +} + + +- (void) addExhaust:(ParticleEntity *)exhaust +{ + [self addSubEntity:exhaust]; +} + + +- (void) addFlasher:(ParticleEntity *)flasher +{ + [self addSubEntity:flasher]; +} + + +- (void) applyThrust:(double) delta_t +{ + GLfloat dt_thrust = thrust * delta_t; + BOOL canBurn = [self hasFuelInjection] && (fuel > MIN_FUEL); + float max_available_speed = maxFlightSpeed; + if (canBurn) max_available_speed *= [self afterburnerFactor]; + + position = vector_add(position, vector_multiply_scalar(velocity, delta_t)); + + if (thrust) + { + GLfloat velmag = sqrtf(magnitude2(velocity)); + if (velmag) + { + GLfloat vscale = (velmag - dt_thrust) / velmag; + if (vscale < 0.0) + vscale = 0.0; + scale_vector(&velocity, vscale); + } + } + + if (behaviour == BEHAVIOUR_TUMBLE) return; + + // check for speed + if (desired_speed > max_available_speed) + desired_speed = max_available_speed; + + if (flightSpeed > desired_speed) + { + [self decrease_flight_speed: dt_thrust]; + if (flightSpeed < desired_speed) flightSpeed = desired_speed; + } + if (flightSpeed < desired_speed) + { + [self increase_flight_speed: dt_thrust]; + if (flightSpeed > desired_speed) flightSpeed = desired_speed; + } + [self moveForward: delta_t*flightSpeed]; + + // burn fuel at the appropriate rate + if ((flightSpeed > maxFlightSpeed) && canBurn) + { + fuel_accumulator -= delta_t * AFTERBURNER_NPC_BURNRATE; + while (fuel_accumulator < 0.0) + { + if (fuel-- <= MIN_FUEL) + max_available_speed = maxFlightSpeed; + fuel_accumulator += 1.0; + } + } +} + + +- (void) orientationChanged +{ + [super orientationChanged]; + + v_forward = vector_forward_from_quaternion(orientation); + v_up = vector_up_from_quaternion(orientation); + v_right = vector_right_from_quaternion(orientation); +} + + +- (void) applyRoll:(GLfloat) roll1 andClimb:(GLfloat) climb1 +{ + Quaternion q1 = kIdentityQuaternion; + + if (!roll1 && !climb1 && !hasRotated) return; + + if (roll1) quaternion_rotate_about_z(&q1, -roll1); + if (climb1) quaternion_rotate_about_x(&q1, -climb1); + + orientation = quaternion_multiply(q1, orientation); + [self orientationChanged]; +} + + +- (void) avoidCollision +{ + if (scanClass == CLASS_MISSILE) + return; // missiles are SUPPOSED to collide! + + ShipEntity* prox_ship = [self proximity_alert]; + + if (prox_ship) + { + if (previousCondition) + { + [previousCondition release]; + previousCondition = nil; + } + + previousCondition = [[NSMutableDictionary dictionaryWithCapacity:5] retain]; + + [previousCondition setInteger:behaviour forKey:@"behaviour"]; + [previousCondition setInteger:primaryTarget forKey:@"primaryTarget"]; + [previousCondition setFloat:desired_range forKey:@"desired_range"]; + [previousCondition setFloat:desired_speed forKey:@"desired_speed"]; + [previousCondition setVector:destination forKey:@"destination"]; + + destination = [prox_ship position]; + destination = OOVectorInterpolate(position, [prox_ship position], 0.5); // point between us and them + + desired_range = prox_ship->collision_radius * PROXIMITY_AVOID_DISTANCE; + + behaviour = BEHAVIOUR_AVOID_COLLISION; + } +} + + +- (void) resumePostProximityAlert +{ + if (!previousCondition) return; + + behaviour = [previousCondition intForKey:@"behaviour"]; + primaryTarget = [previousCondition intForKey:@"primaryTarget"]; + desired_range = [previousCondition floatForKey:@"desired_range"]; + desired_speed = [previousCondition floatForKey:@"desired_speed"]; + destination = [previousCondition vectorForKey:@"destination"]; + + [previousCondition release]; + previousCondition = nil; + frustration = 0.0; + + proximity_alert = NO_TARGET; + + //[shipAI message:@"RESTART_DOCKING"]; // if docking, start over, other AIs will ignore this message +} + + +- (double) messageTime +{ + return messageTime; +} + + +- (void) setMessageTime:(double) value +{ + messageTime = value; +} + + +- (OOShipGroup *) group +{ + return _group; +} + + +- (void) setGroup:(OOShipGroup *)group +{ + if (group != _group) + { + if (self == [_group leader]) [_group setLeader:nil]; + [_group removeShip:self]; + [_group release]; + [group addShip:self]; + _group = [group retain]; + } +} + + +- (OOShipGroup *) escortGroup +{ + if (_escortGroup == nil) + { + _escortGroup = [[OOShipGroup alloc] initWithName:@"escort group"]; + [_escortGroup setLeader:self]; + } + + return _escortGroup; +} + + +- (OOShipGroup *) stationGroup +{ + if (_group == nil) + { + _group = [[OOShipGroup alloc] initWithName:@"station group"]; + [_group setLeader:self]; + } + + return _group; +} + + +- (BOOL) hasEscorts +{ + if (_escortGroup == nil) return NO; + return [_escortGroup count] > 1; // If only one memeber, it's self. +} + + +- (NSEnumerator *) escortEnumerator +{ + if (_escortGroup == nil) return [[NSArray array] objectEnumerator]; + return [[_escortGroup objectEnumerator] ooExcludingObject:self]; +} + + +- (NSArray *) escortArray +{ + if (_escortGroup == nil) return [NSArray array]; + return [[self escortEnumerator] allObjects]; +} + + +- (uint8_t) escortCount +{ + if (_escortGroup == nil) return 0; + return [_escortGroup count] - 1; +} + + +- (uint8_t) pendingEscortCount +{ + return _pendingEscortCount; +} + + +- (void) setPendingEscortCount:(uint8_t)count +{ + _pendingEscortCount = count; +} + + +- (ShipEntity*) proximity_alert +{ + return [UNIVERSE entityForUniversalID:proximity_alert]; +} + + +- (void) setProximity_alert:(ShipEntity*) other +{ + if (!other) + { + proximity_alert = NO_TARGET; + return; + } + + if (isStation || (other->isStation)) // don't be alarmed close to stations -- is this sensible? we dont mind crashing into carriers? + return; + + if ((scanClass == CLASS_CARGO)||(scanClass == CLASS_BUOY)||(scanClass == CLASS_MISSILE)||(scanClass == CLASS_ROCK)) // rocks and stuff don't get alarmed easily + return; + + // check vectors + Vector vdiff = vector_between(position, other->position); + GLfloat d_forward = dot_product(vdiff, v_forward); + GLfloat d_up = dot_product(vdiff, v_up); + GLfloat d_right = dot_product(vdiff, v_right); + if ((d_forward > 0.0)&&(flightSpeed > 0.0)) // it's ahead of us and we're moving forward + d_forward *= 0.25 * maxFlightSpeed / flightSpeed; // extend the collision zone forward up to 400% + double d2 = d_forward * d_forward + d_up * d_up + d_right * d_right; + double cr2 = collision_radius * 2.0 + other->collision_radius; cr2 *= cr2; // check with twice the combined radius + + if (d2 > cr2) // we're okay + return; + + if (behaviour == BEHAVIOUR_AVOID_COLLISION) // already avoiding something + { + ShipEntity* prox = [UNIVERSE entityForUniversalID:proximity_alert]; + if ((prox)&&(prox != other)) + { + // check which subtends the greatest angle + GLfloat sa_prox = prox->collision_radius * prox->collision_radius / distance2(position, prox->position); + GLfloat sa_other = other->collision_radius * other->collision_radius / distance2(position, other->position); + if (sa_prox < sa_other) return; + } + } + proximity_alert = [other universalID]; +} + + +- (NSString *) name +{ + return name; +} + + +- (NSString *) displayName +{ + if (displayName == nil) return name; + return displayName; +} + + +- (void) setName:(NSString *)inName +{ + [name release]; + name = [inName copy]; +} + + +- (void) setDisplayName:(NSString *)inName +{ + [displayName release]; + displayName = [inName copy]; +} + + +- (NSString *) identFromShip:(ShipEntity*) otherShip +{ + if ([self isJammingScanning] && ![otherShip hasMilitaryScannerFilter]) + { + return DESC(@"unknown-target"); + } + return displayName; +} + + +- (BOOL) hasRole:(NSString *)role +{ + return [roleSet hasRole:role] || [role isEqual:primaryRole]; +} + + +- (OORoleSet *)roleSet +{ + if (roleSet == nil) roleSet = [[OORoleSet alloc] initWithRoleString:primaryRole]; + return [roleSet roleSetWithAddedRoleIfNotSet:primaryRole probability:1.0]; +} + + +- (NSString *)primaryRole +{ + if (primaryRole == nil) + { + primaryRole = [roleSet anyRole]; + if (primaryRole == nil) primaryRole = @"trader"; + [primaryRole retain]; + OOLog(@"ship.noPrimaryRole", @"%@ had no primary role, randomly selected \"%@\".", [self name], primaryRole); + } + + return primaryRole; +} + + +- (void)setPrimaryRole:(NSString *)role +{ + if (![role isEqual:primaryRole]) + { + [primaryRole release]; + primaryRole = [role copy]; + } +} + + +- (BOOL)hasPrimaryRole:(NSString *)role +{ + return [[self primaryRole] isEqual:role]; +} + + +- (BOOL)isPolice +{ + //bounty hunters have a police role, but are not police, so we must test by scan class, not by role + return [self scanClass] == CLASS_POLICE; +} + +- (BOOL)isThargoid +{ + return [self scanClass] == CLASS_THARGOID; +} + + +- (BOOL)isTrader +{ + return isPlayer || [self hasPrimaryRole:@"trader"]; +} + + +- (BOOL)isPirate +{ + return [self hasPrimaryRole:@"pirate"]; +} + + +- (BOOL)isMissile +{ + return ([[self primaryRole] hasSuffix:@"MISSILE"] || [self hasPrimaryRole:@"missile"]); +} + + +- (BOOL)isMine +{ + return [[self primaryRole] hasSuffix:@"MINE"]; +} + + +- (BOOL)isWeapon +{ + return [self isMissile] || [self isMine]; +} + + +- (BOOL)isEscort +{ + return [self hasPrimaryRole:@"escort"] || [self hasPrimaryRole:@"wingman"]; +} + + +- (BOOL)isShuttle +{ + return [self hasPrimaryRole:@"shuttle"]; +} + + +- (BOOL)isPirateVictim +{ + return [UNIVERSE roleIsPirateVictim:[self primaryRole]]; +} + + +- (BOOL)isUnpiloted +{ + return isUnpiloted; +} + + +- (BOOL) hasHostileTarget +{ + if (primaryTarget == NO_TARGET) + return NO; + if ([self isMissile]) + return YES; // missiles are always fired against a hostile target + if ((behaviour == BEHAVIOUR_AVOID_COLLISION)&&(previousCondition)) + { + int old_behaviour = [previousCondition intForKey:@"behaviour"]; + return IsBehaviourHostile(old_behaviour); + } + return IsBehaviourHostile(behaviour); +} + + +- (GLfloat) weaponRange +{ + return weaponRange; +} + + +- (void) setWeaponRange: (GLfloat) value +{ + weaponRange = value; +} + + +- (void) setWeaponDataFromType: (OOWeaponType) weapon_type +{ + switch (weapon_type) + { + case WEAPON_PLASMA_CANNON : + weapon_energy = 6.0; + weapon_recharge_rate = 0.25; + weaponRange = 5000; + break; + case WEAPON_PULSE_LASER : + weapon_energy = 15.0; + weapon_recharge_rate = 0.33; + weaponRange = 12500; + break; + case WEAPON_BEAM_LASER : + weapon_energy = 15.0; + weapon_recharge_rate = 0.25; + weaponRange = 15000; + break; + case WEAPON_MINING_LASER : + weapon_energy = 50.0; + weapon_recharge_rate = 0.5; + weaponRange = 12500; + break; + case WEAPON_THARGOID_LASER : // omni directional lasers FRIGHTENING! + weapon_energy = 12.5; + weapon_recharge_rate = 0.5; + weaponRange = 17500; + break; + case WEAPON_MILITARY_LASER : + weapon_energy = 23.0; + weapon_recharge_rate = 0.20; + weaponRange = 30000; + break; + case WEAPON_NONE : + weapon_energy = 0.0; // indicating no weapon! + weapon_recharge_rate = 0.20; // maximum rate + weaponRange = 32000; + break; + } +} + + +- (float) weaponRechargeRate +{ + return weapon_recharge_rate; +} + + +- (void) setWeaponRechargeRate:(float)value +{ + weapon_recharge_rate = value; +} + + +- (GLfloat) scannerRange +{ + return scannerRange; +} + + +- (void) setScannerRange: (GLfloat) value +{ + scannerRange = value; +} + + +- (Vector) reference +{ + return reference; +} + + +- (void) setReference:(Vector) v +{ + reference = v; +} + + +- (BOOL) reportAIMessages +{ + return reportAIMessages; +} + + +- (void) setReportAIMessages:(BOOL) yn +{ + reportAIMessages = yn; +} + + +- (void) transitionToAegisNone +{ + if (!suppressAegisMessages && aegis_status != AEGIS_NONE) + { + PlanetEntity *lastPlanet = [self lastPlanet]; + if (lastPlanet != nil) + { + [self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:lastPlanet]; + + if (lastPlanet == [UNIVERSE sun]) + { + [shipAI message:@"AWAY_FROM_SUN"]; + } + else + { + [shipAI message:@"AWAY_FROM_PLANET"]; + } + [self setLastPlanet:nil]; + } + + if (aegis_status != AEGIS_CLOSE_TO_ANY_PLANET) + { + [shipAI message:@"AEGIS_NONE"]; + } + } + aegis_status = AEGIS_NONE; +} + + +NSComparisonResult ComparePlanetsBySurfaceDistance(id i1, id i2, void* context) +{ + Vector p = [(ShipEntity*) context position]; + PlanetEntity* e1 = i1; + PlanetEntity* e2 = i2; + //fx: empirical value used to help determine proximity when non-nested planets are close to each other + float fx=1.35; + float r; + + float p1 = magnitude2(vector_subtract([e1 position], p)); + float p2 = magnitude2(vector_subtract([e2 position], p)); + r = [e1 radius]; + p1 -= fx*r*r; + r = [e2 radius]; + p2 -= fx*r*r; + + if (p1 < p2) return NSOrderedAscending; + if (p1 > p2) return NSOrderedDescending; + + return NSOrderedSame; +} + + +- (PlanetEntity *) findNearestPlanet +{ + PlanetEntity *result = nil; + NSArray *planets = nil; + + planets = [UNIVERSE planets]; + if ([planets count] == 0) return nil; + + planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self]; + result = [planets objectAtIndex:0]; + + // ignore miniature planets when determining nearest planet - Nikos 20090313 + if ([result planetType] == PLANET_TYPE_MINIATURE) + { + if ([UNIVERSE sun]) // if we are not in witchspace give us the next in the list, else nothing + { + result = [planets objectAtIndex:1]; + } + else + { + result = nil; + } + } + + return result; +} + + +- (PlanetEntity *) findNearestStellarBody +{ + PlanetEntity *result = nil; + NSArray *planets = nil; + PlanetEntity *sun = nil; + + planets = [UNIVERSE planets]; + sun = [UNIVERSE sun]; + if (sun != nil) + { + planets = [planets arrayByAddingObject:sun]; + } + if ([planets count] == 0) return nil; + + planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self]; + result = [planets objectAtIndex:0]; + + // ignore miniature planets when determining nearest stellar body - Nikos 20090313 + if ([result planetType] == PLANET_TYPE_MINIATURE) + { + if (sun) // if we are not in witchspace give us the next in the list, else nothing + { + result = [planets objectAtIndex:1]; + } + else + { + result = nil; + } + } + + return result; +} + + +- (PlanetEntity *) findNearestPlanetExcludingMoons +{ + PlanetEntity *result = nil; + PlanetEntity *planet = nil; + NSArray *bodies = nil; + NSArray *planets = nil; + unsigned i; + + bodies = [UNIVERSE planets]; + planets = [NSMutableArray arrayWithCapacity:[bodies count]]; + + for (i=0; i < [bodies count]; i++) + { + planet = [bodies objectAtIndex:i]; + if([planet planetType] == PLANET_TYPE_GREEN) + planets = [planets arrayByAddingObject:planet]; + } + + if ([planets count] == 0) return nil; + + planets = [planets sortedArrayUsingFunction:ComparePlanetsBySurfaceDistance context:self]; + result = [planets objectAtIndex:0]; + + return result; +} + + +- (OOAegisStatus) checkForAegis +{ + PlanetEntity *the_planet = [self findNearestStellarBody]; + BOOL sunGoneNova = [[UNIVERSE sun] goneNova]; + + if (the_planet == nil) + { + if (aegis_status != AEGIS_NONE) + { + // Planet disappeared! + [self transitionToAegisNone]; + } + return AEGIS_NONE; + } + + // check planet + float cr = [the_planet collisionRadius]; + float cr2 = cr * cr; + OOAegisStatus result = AEGIS_NONE; + float d2; + + d2 = magnitude2(vector_subtract([the_planet position], [self position])); + + // check if nearing surface + BOOL wasNearPlanetSurface = isNearPlanetSurface; + isNearPlanetSurface = (d2 - cr2) < (250000.0f + 1000.0f * cr); //less than 500m from the surface: (a+b)*(a+b) = a*a+b*b +2*a*b + if (!suppressAegisMessages) + { + if (!wasNearPlanetSurface && isNearPlanetSurface) + { + [self doScriptEvent:@"shipApproachingPlanetSurface" withArgument:the_planet]; + [shipAI reactToMessage:@"APPROACHING_SURFACE"]; + } + if (wasNearPlanetSurface && !isNearPlanetSurface) + { + [self doScriptEvent:@"shipLeavingPlanetSurface" withArgument:the_planet]; + [shipAI reactToMessage:@"LEAVING_SURFACE"]; + } + } + + if (d2 < cr2 * 9.0f) //to 3x radius of any planet/moon + { + result = AEGIS_CLOSE_TO_ANY_PLANET; + } + + if (!sunGoneNova) + { + d2 = magnitude2(vector_subtract([[UNIVERSE planet] position], [self position])); + cr2 = [[UNIVERSE planet] collisionRadius]; + cr2 *= cr2; + if (d2 < cr2 * 9.0f) // to 3x radius of main planet + { + result = AEGIS_CLOSE_TO_MAIN_PLANET; + } + } + + // check station + StationEntity *the_station = [UNIVERSE station]; + if (the_station) + { + d2 = magnitude2(vector_subtract([the_station position], [self position])); + if (d2 < SCANNER_MAX_RANGE2 * 4.0f) // double scanner range + { + result = AEGIS_IN_DOCKING_RANGE; + } + } + + /* Rewrote aegis stuff and tested it against redux.oxp that adds multiple planets and moons. + Made sure AI scripts can differentiate between MAIN and NON-MAIN planets so they can decide + if they can dock at the systemStation or just any station. + Added sun detection so route2Patrol can turn before they heat up in the sun. + -- Eric 2009-07-11 + */ + if (!suppressAegisMessages) + { + // script/AI messages on change in status + // approaching.. + if ((aegis_status == AEGIS_NONE || aegis_status == AEGIS_CLOSE_TO_ANY_PLANET)&&(result == AEGIS_CLOSE_TO_MAIN_PLANET)) + { + if(aegis_status == AEGIS_CLOSE_TO_ANY_PLANET) + { + [self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:[self lastPlanet]]; + [shipAI message:@"AWAY_FROM_PLANET"]; // fires for all planets and moons. + } + [self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:[UNIVERSE planet]]; + [self setLastPlanet:[UNIVERSE planet]]; + [shipAI message:@"CLOSE_TO_PLANET"]; // fires for all planets and moons. + [shipAI message:@"AEGIS_CLOSE_TO_PLANET"]; // fires only for main planets, keep for compatibility with pre-1.72 AI plists. + [shipAI message:@"AEGIS_CLOSE_TO_MAIN_PLANET"]; // fires only for main planet. + } + if ((aegis_status == AEGIS_NONE || aegis_status == AEGIS_CLOSE_TO_MAIN_PLANET)&&(result == AEGIS_CLOSE_TO_ANY_PLANET)) + { + if(aegis_status == AEGIS_CLOSE_TO_MAIN_PLANET) + { + [self doScriptEvent:@"shipExitedPlanetaryVicinity" withArgument:[UNIVERSE planet]]; + [shipAI message:@"AWAY_FROM_PLANET"]; + } + [self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:the_planet]; + [self setLastPlanet:the_planet]; + if(the_planet == [UNIVERSE sun]) + { + [shipAI message:@"CLOSE_TO_SUN"]; + } + else + { + [shipAI message:@"CLOSE_TO_PLANET"]; + if ([the_planet planetType] == PLANET_TYPE_MOON) + { + [shipAI message:@"CLOSE_TO_MOON"]; + } + else + { + [shipAI message:@"CLOSE_TO_SECONDARY_PLANET"]; + } + } + } + if ((aegis_status != AEGIS_IN_DOCKING_RANGE)&&(result == AEGIS_IN_DOCKING_RANGE)) + { + [self doScriptEvent:@"shipEnteredStationAegis" withArgument:the_station]; + [shipAI message:@"AEGIS_IN_DOCKING_RANGE"]; + + if([self lastPlanet] == nil && !sunGoneNova) // With small main planets the station aegis can come before planet aegis + { + [self doScriptEvent:@"shipEnteredPlanetaryVicinity" withArgument:[UNIVERSE planet]]; + [self setLastPlanet:[UNIVERSE planet]]; + } + } + // leaving.. + if ((aegis_status == AEGIS_IN_DOCKING_RANGE)&&(result != AEGIS_IN_DOCKING_RANGE)) + { + [self doScriptEvent:@"shipExitedStationAegis" withArgument:the_station]; + [shipAI message:@"AEGIS_LEAVING_DOCKING_RANGE"]; + } + if ((aegis_status != AEGIS_NONE)&&(result == AEGIS_NONE)) + { + if([self lastPlanet] == nil && !sunGoneNova) + { + [self setLastPlanet:[UNIVERSE planet]]; // in case of a first launch. + } + [self transitionToAegisNone]; + } + } + + aegis_status = result; // put this here + return result; +} + + +- (BOOL) withinStationAegis +{ + return aegis_status == AEGIS_IN_DOCKING_RANGE; +} + + +- (PlanetEntity *) lastPlanet +{ + PlanetEntity *planet = [_lastPlanet weakRefUnderlyingObject]; + if (planet == nil) + { + [_lastPlanet release]; + _lastPlanet = nil; + } + + return planet; +} + + +- (void) setLastPlanet:(PlanetEntity *)lastPlanet +{ + [_lastPlanet release]; + _lastPlanet = [lastPlanet weakRetain]; +} + + +- (void) setStatus:(OOEntityStatus) stat +{ + [super setStatus:stat]; + if (stat == STATUS_LAUNCHING) + { + launch_time = [UNIVERSE getTime]; + } +} + + +- (NSArray*) crew +{ + return crew; +} + + +- (void) setCrew: (NSArray*) crewArray +{ + if (isUnpiloted) + { + //unpiloted ships cannot have crew + return; + } + //do not set to hulk here when crew is nill (or 0). Some things like missiles have no crew. + [crew autorelease]; + crew = [crewArray copy]; +} + + +- (void) setStateMachine:(NSString *) ai_desc +{ + [shipAI setStateMachine: ai_desc]; +} + + +- (void) setAI:(AI *) ai +{ + [ai retain]; + if (shipAI) + { + [shipAI clearAllData]; + [shipAI autorelease]; + } + shipAI = ai; +} + + +- (AI *) getAI +{ + return shipAI; +} + + +- (void) setShipScript:(NSString *)script_name +{ + NSMutableDictionary *properties = nil; + NSArray *actions = nil; + + properties = [NSMutableDictionary dictionary]; + [properties setObject:self forKey:@"ship"]; + + [script autorelease]; + script = [OOScript nonLegacyScriptFromFileNamed:script_name properties:properties]; + + if (script == nil) + { + actions = [shipinfoDictionary arrayForKey:@"launch_actions"]; + if (actions) [properties setObject:actions forKey:@"legacy_launchActions"]; + actions = [shipinfoDictionary arrayForKey:@"script_actions"]; + if (actions) [properties setObject:actions forKey:@"legacy_scriptActions"]; + actions = [shipinfoDictionary arrayForKey:@"death_actions"]; + if (actions) [properties setObject:actions forKey:@"legacy_deathActions"]; + actions = [shipinfoDictionary arrayForKey:@"setup_actions"]; + if (actions) [properties setObject:actions forKey:@"legacy_setupActions"]; + + script = [OOScript nonLegacyScriptFromFileNamed:@"oolite-default-ship-script.js" + properties:properties]; + } + [script retain]; +} + + +- (double)frustration +{ + return frustration; +} + + +- (OOFuelQuantity) fuel +{ + return fuel; +} + + +- (void) setFuel:(OOFuelQuantity) amount +{ + if (amount > [self fuelCapacity]) amount = [self fuelCapacity]; + + fuel = amount; +} + + +- (OOFuelQuantity) fuelCapacity +{ + // FIXME: shipdata.plist can allow greater fuel quantities (without extending hyperspace range). Need some consistency here. + return PLAYER_MAX_FUEL; +} + + +- (void) setRoll:(double) amount +{ + flightRoll = amount * M_PI / 2.0; +} + + +- (void) setPitch:(double) amount +{ + flightPitch = amount * M_PI / 2.0; +} + + +- (void)setThrustForDemo:(float)factor +{ + flightSpeed = factor * maxFlightSpeed; +} + + +- (void) setBounty:(OOCreditsQuantity) amount +{ + bounty = amount; +} + + +- (OOCreditsQuantity) bounty +{ + return bounty; +} + + +- (int) legalStatus +{ + if (scanClass == CLASS_THARGOID) + return 5 * collision_radius; + if (scanClass == CLASS_ROCK) + return 0; + return bounty; +} + + +- (void) setCommodity:(OOCargoType)co_type andAmount:(OOCargoQuantity)co_amount +{ + if (co_type != CARGO_UNDEFINED) + { + commodity_type = co_type; + commodity_amount = co_amount; + } +} + + +- (OOCargoType) commodityType +{ + return commodity_type; +} + + +- (OOCargoQuantity) commodityAmount +{ + return commodity_amount; +} + + +- (OOCargoQuantity) maxCargo +{ + return max_cargo; +} + + +- (OOCargoQuantity) availableCargoSpace +{ + return [self maxCargo] - [[self cargo] count]; +} + + +- (OOCargoType) cargoType +{ + return cargo_type; +} + + +- (NSMutableArray*) cargo +{ + return cargo; +} + + +- (void) setCargo:(NSArray *) some_cargo +{ + [cargo removeAllObjects]; + [cargo addObjectsFromArray:some_cargo]; +} + + +- (OOCargoFlag) cargoFlag +{ + return cargo_flag; +} + + +- (void) setCargoFlag:(OOCargoFlag) flag +{ + cargo_flag = flag; +} + + +- (void) setSpeed:(double) amount +{ + flightSpeed = amount; +} + + +- (void) setDesiredSpeed:(double) amount +{ + desired_speed = amount; +} + + +- (double) desiredSpeed +{ + return desired_speed; +} + + +- (void) increase_flight_speed:(double) delta +{ + double factor = 1.0; + if (desired_speed > maxFlightSpeed && [self hasFuelInjection] && fuel > MIN_FUEL) factor = [self afterburnerFactor]; + + if (flightSpeed < maxFlightSpeed * factor) + flightSpeed += delta * factor; + else + flightSpeed = maxFlightSpeed * factor; +} + + +- (void) decrease_flight_speed:(double) delta +{ + if (flightSpeed > delta) + flightSpeed -= delta; + else + flightSpeed = 0; +} + + +- (void) increase_flight_roll:(double) delta +{ + if (flightRoll < max_flight_roll) + flightRoll += delta; + if (flightRoll > max_flight_roll) + flightRoll = max_flight_roll; +} + + +- (void) decrease_flight_roll:(double) delta +{ + if (flightRoll > -max_flight_roll) + flightRoll -= delta; + if (flightRoll < -max_flight_roll) + flightRoll = -max_flight_roll; +} + + +- (void) increase_flight_pitch:(double) delta +{ + if (flightPitch < max_flight_pitch) + flightPitch += delta; + if (flightPitch > max_flight_pitch) + flightPitch = max_flight_pitch; +} + + +- (void) decrease_flight_pitch:(double) delta +{ + if (flightPitch > -max_flight_pitch) + flightPitch -= delta; + if (flightPitch < -max_flight_pitch) + flightPitch = -max_flight_pitch; +} + + +- (void) increase_flight_yaw:(double) delta +{ + if (flightYaw < max_flight_yaw) + flightYaw += delta; + if (flightYaw > max_flight_yaw) + flightYaw = max_flight_yaw; +} + + +- (void) decrease_flight_yaw:(double) delta +{ + if (flightYaw > -max_flight_yaw) + flightYaw -= delta; + if (flightYaw < -max_flight_yaw) + flightYaw = -max_flight_yaw; +} + + +- (GLfloat) flightRoll +{ + return flightRoll; +} + + +- (GLfloat) flightPitch +{ + return flightPitch; +} + + +- (GLfloat) flightYaw +{ + return flightYaw; +} + + +- (GLfloat) flightSpeed +{ + return flightSpeed; +} + + +- (GLfloat) maxFlightSpeed +{ + return maxFlightSpeed; +} + + +- (GLfloat) speedFactor +{ + if (maxFlightSpeed <= 0.0) return 0.0; + return flightSpeed / maxFlightSpeed; +} + + +- (GLfloat) temperature +{ + return ship_temperature; +} + + +- (void) setTemperature:(GLfloat) value +{ + ship_temperature = value; +} + + +- (GLfloat) heatInsulation +{ + return _heatInsulation; +} + + +- (void) setHeatInsulation:(GLfloat) value +{ + _heatInsulation = value; +} + + +- (int) damage +{ + return (int)(100 - (100 * energy / maxEnergy)); +} + + +// Exposed to AI +- (void) dealEnergyDamageWithinDesiredRange +{ + NSArray* targets = [UNIVERSE getEntitiesWithinRange:desired_range ofEntity:self]; + if ([targets count] > 0) + { + unsigned i; + for (i = 0; i < [targets count]; i++) + { + Entity *e2 = [targets objectAtIndex:i]; + Vector p2 = vector_subtract([e2 position], position); + double ecr = [e2 collisionRadius]; + double d2 = magnitude2(p2) - ecr * ecr; + double damage = weapon_energy*desired_range/d2; + [e2 takeEnergyDamage:damage from:self becauseOf:[self owner]]; + } + } +} + + +- (void) dealMomentumWithinDesiredRange:(double)amount +{ + NSArray* targets = [UNIVERSE getEntitiesWithinRange:desired_range ofEntity:self]; + if ([targets count] > 0) + { + unsigned i; + for (i = 0; i < [targets count]; i++) + { + ShipEntity *e2 = (ShipEntity*)[targets objectAtIndex:i]; + if ([e2 isShip]) + { + Vector p2 = vector_subtract([e2 position], position); + double ecr = [e2 collisionRadius]; + double d2 = magnitude2(p2) - ecr * ecr; + while (d2 <= 0.0) + { + p2 = OOVectorRandomSpatial(1.0); + d2 = magnitude2(p2); + } + double moment = amount*desired_range/d2; + [e2 addImpactMoment:vector_normal(p2) fraction:moment]; + } + } + } +} + + +- (BOOL) isHulk +{ + return isHulk; +} + +- (void) setHulk:(BOOL)isNowHulk +{ + isHulk = isNowHulk; +} + +- (void) getDestroyedBy:(Entity *)whom context:(NSString *)context +{ + if (whom == nil) whom = (id)[NSNull null]; + + // Is this safe to do here? The script actions will be executed before the status has been set to + // STATUS_DEAD, which is the opposite of what was happening inside becomeExplosion - Nikos. + if (script != nil) + { + [[PlayerEntity sharedPlayer] setScriptTarget:self]; + [self doScriptEvent:@"shipDied" withArguments:[NSArray arrayWithObjects:whom, context, nil]]; + } + + [self becomeExplosion]; +} + + +- (void) rescaleBy:(GLfloat)factor +{ + // rescale mesh (and collision detection stuff) + [self setMesh:[[self mesh] meshRescaledBy:factor]]; + + // rescale positions of subentities + NSEnumerator *subEnum = nil; + Entity *se = nil; + for (subEnum = [self subEntityEnumerator]; (se = [subEnum nextObject]); ) + { + se->position = vector_multiply_scalar([se position], factor); + + // rescale ship subentities + if ([se isShip]) [(ShipEntity*)se rescaleBy:factor]; + + // rescale particle subentities + if (se->isParticle) + { + ParticleEntity* pe = (ParticleEntity*)se; + NSSize sz = [pe size]; + sz.width *= factor; + sz.height *= factor; + [pe setSize: sz]; + } + } + + // rescale mass + mass *= factor * factor * factor; +} + + +- (void) becomeExplosion +{ + OOCargoQuantity cargo_to_go; + + // check if we're destroying a subentity + ShipEntity *parent = [self parentEntity]; + if (parent != nil) + { + ShipEntity* this_ship = [self retain]; + Vector this_pos = [self absolutePositionForSubentity]; + + // remove this ship from its parent's subentity list + [parent subEntityDied:self]; + [UNIVERSE addEntity:this_ship]; + [this_ship setPosition:this_pos]; + [this_ship release]; + } + + Vector xposition = position; + ParticleEntity *fragment; + int i; + Vector v; + Quaternion q; + int speed_low = 200; + int n_alloys = floor(sqrtf(sqrtf(mass / 25000.0))); + + if ([self status] == STATUS_DEAD) + { + [UNIVERSE removeEntity:self]; + return; + } + [self setStatus:STATUS_DEAD]; + + if ([self isThargoid]) [self broadcastThargoidDestroyed]; + + if (!suppressExplosion) + { + if ((mass > 500000.0f)&&(randf() < 0.25f)) // big! + { + // draw an expanding ring + ParticleEntity *ring = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained + Vector ring_vel = [self velocity]; + ring_vel.x *= 0.25; ring_vel.y *= 0.25; ring_vel.z *= 0.25; // quarter velocity + [ring setVelocity:ring_vel]; + [UNIVERSE addEntity:ring]; + [ring release]; + } + + // several parts to the explosion: + // 1. fast sparks + fragment = [[ParticleEntity alloc] initFragburstSize:collision_radius fromPosition:xposition]; + [UNIVERSE addEntity:fragment]; + [fragment release]; + // 2. slow clouds + fragment = [[ParticleEntity alloc] initBurst2Size:collision_radius fromPosition:xposition]; + [UNIVERSE addEntity:fragment]; + [fragment release]; + // 3. flash + fragment = [[ParticleEntity alloc] initFlashSize:collision_radius fromPosition:xposition]; + [UNIVERSE addEntity:fragment]; + [fragment release]; + + BOOL add_more_explosion = YES; + if (UNIVERSE) + { + add_more_explosion &= (UNIVERSE->n_entities < 0.95 * UNIVERSE_MAX_ENTITIES); // + add_more_explosion &= ([UNIVERSE getTimeDelta] < 0.125); // FPS > 8 + } + // quick - check if UNIVERSE is nearing limit for entities - if it is don't add to it! + // + if (add_more_explosion) + { + // we need to throw out cargo at this point. + NSArray *jetsam = nil; // this will contain the stuff to get thrown out + unsigned cargo_chance = 10; + if ([[name lowercaseString] rangeOfString:@"medical"].location != NSNotFound) + { + cargo_to_go = max_cargo * cargo_chance / 100; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + [self setCargo:[UNIVERSE getContainersOfDrugs:cargo_to_go]]; + cargo_chance = 100; // chance of any given piece of cargo surviving decompression + cargo_flag = CARGO_FLAG_CANISTERS; + } + + cargo_to_go = max_cargo * cargo_chance / 100; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + cargo_chance = 100; // chance of any given piece of cargo surviving decompression + switch (cargo_flag) + { + case CARGO_FLAG_NONE: + case CARGO_FLAG_FULL_PASSENGERS: + break; + + case CARGO_FLAG_FULL_UNIFORM : + { + NSString* commodity_name = [shipinfoDictionary stringForKey:@"cargo_carried"]; + jetsam = [UNIVERSE getContainersOfCommodity:commodity_name :cargo_to_go]; + } + break; + + case CARGO_FLAG_FULL_PLENTIFUL : + jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:NO]; + break; + + case CARGO_FLAG_PIRATE : + cargo_to_go = likely_cargo; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + cargo_chance = 65; // 35% chance of spoilage + jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:YES]; + break; + + case CARGO_FLAG_FULL_SCARCE : + jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:YES]; + break; + + case CARGO_FLAG_CANISTERS: + jetsam = [NSArray arrayWithArray:cargo]; // what the ship is carrying + [cargo removeAllObjects]; // dispense with it! + break; + } + + // Throw out cargo + if (jetsam) + { + int n_jetsam = [jetsam count]; + // + for (i = 0; i < n_jetsam; i++) + { + if (Ranrot() % 100 < cargo_chance) // chance of any given piece of cargo surviving decompression + { + ShipEntity* container = [jetsam objectAtIndex:i]; + Vector rpos = xposition; + Vector rrand = randomPositionInBoundingBox(boundingBox); + rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z; + rpos.x += (ranrot_rand() % 7) - 3; + rpos.y += (ranrot_rand() % 7) - 3; + rpos.z += (ranrot_rand() % 7) - 3; + [container setPosition:rpos]; + v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2); + v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2); + v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2); + [container setVelocity:v]; + quaternion_set_random(&q); + [container setOrientation:q]; + [container setStatus:STATUS_IN_FLIGHT]; + [container setScanClass: CLASS_CARGO]; + [UNIVERSE addEntity:container]; + [[container getAI] setState:@"GLOBAL"]; + } + } + } + + // Throw out rocks and alloys to be scooped up + if ([self hasRole:@"asteroid"]) + { + if (!noRocks && (being_mined || randf() < 0.20)) + { + int n_rocks = 2 + (Ranrot() % (likely_cargo + 1)); + + for (i = 0; i < n_rocks; i++) + { + ShipEntity* rock = [UNIVERSE newShipWithRole:@"boulder"]; // retain count = 1 + if (rock) + { + Vector rpos = xposition; + int r_speed = 20.0 * [rock maxFlightSpeed]; + int cr = 3 * rock->collision_radius; + rpos.x += (ranrot_rand() % cr) - cr/2; + rpos.y += (ranrot_rand() % cr) - cr/2; + rpos.z += (ranrot_rand() % cr) - cr/2; + [rock setPosition:rpos]; + v.x = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2); + v.y = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2); + v.z = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2); + [rock setVelocity:v]; + quaternion_set_random(&q); + [rock setOrientation:q]; + [rock setStatus:STATUS_IN_FLIGHT]; + [rock setScanClass: CLASS_ROCK]; + [UNIVERSE addEntity:rock]; + [[rock getAI] setState:@"GLOBAL"]; + [rock release]; + } + } + } + [UNIVERSE removeEntity:self]; + return; // don't do anything more + } + + if ([self hasRole:@"boulder"]) + { + if ((being_mined)||(ranrot_rand() % 100 < 20)) + { + int n_rocks = 2 + (ranrot_rand() % 5); + // + for (i = 0; i < n_rocks; i++) + { + ShipEntity* rock = [UNIVERSE newShipWithRole:@"splinter"]; // retain count = 1 + if (rock) + { + Vector rpos = xposition; + int r_speed = 20.0 * [rock maxFlightSpeed]; + int cr = 3 * rock->collision_radius; + rpos.x += (ranrot_rand() % cr) - cr/2; + rpos.y += (ranrot_rand() % cr) - cr/2; + rpos.z += (ranrot_rand() % cr) - cr/2; + [rock setPosition:rpos]; + v.x = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2); + v.y = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2); + v.z = 0.1 *((ranrot_rand() % r_speed) - r_speed / 2); + [rock setBounty: 0]; + [rock setCommodity:[UNIVERSE commodityForName:@"Minerals"] andAmount: 1]; + [rock setVelocity:v]; + quaternion_set_random(&q); + [rock setOrientation:q]; + [rock setStatus:STATUS_IN_FLIGHT]; + [rock setScanClass: CLASS_CARGO]; + [UNIVERSE addEntity:rock]; + [[rock getAI] setState:@"GLOBAL"]; + [rock release]; + } + } + } + [UNIVERSE removeEntity:self]; + return; // don't do anything more + } + + // throw out burning chunks of wreckage + // + if (n_alloys && canFragment) + { + int n_wreckage = (n_alloys < 3)? n_alloys : 3; + + // quick - check if UNIVERSE is nearing limit for entities - if it is don't make wreckage + // + add_more_explosion &= (UNIVERSE->n_entities < 0.50 * UNIVERSE_MAX_ENTITIES); + if (!add_more_explosion) + n_wreckage = 0; + // + //// + + for (i = 0; i < n_wreckage; i++) + { + ShipEntity* wreck = [UNIVERSE newShipWithRole:@"wreckage"]; // retain count = 1 + if (wreck) + { + GLfloat expected_mass = 0.1f * mass * (0.75 + 0.5 * randf()); + GLfloat wreck_mass = [wreck mass]; + GLfloat scale_factor = powf(expected_mass / wreck_mass, 0.33333333f); // cube root of volume ratio + [wreck rescaleBy: scale_factor]; + + Vector r1 = randomFullNodeFrom([octree octreeDetails], kZeroVector); + Vector rpos = make_vector (v_right.x * r1.x + v_up.x * r1.y + v_forward.x * r1.z, + v_right.y * r1.x + v_up.y * r1.y + v_forward.y * r1.z, + v_right.z * r1.x + v_up.z * r1.y + v_forward.z * r1.z); + rpos.x += xposition.x; + rpos.y += xposition.y; + rpos.z += xposition.z; + [wreck setPosition:rpos]; + + [wreck setVelocity:[self velocity]]; + + quaternion_set_random(&q); + [wreck setOrientation:q]; + + [wreck setTemperature: 1000.0]; // take 1000e heat damage per second + [wreck setHeatInsulation: 1.0e7]; // very large! so it won't cool down + [wreck setEnergy: 750.0 * randf() + 250.0 * i + 100.0]; // burn for 0.25s -> 1.25s + + [wreck setStatus:STATUS_IN_FLIGHT]; + [UNIVERSE addEntity:wreck]; + [wreck performTumble]; + [wreck rescaleBy: 1.0/scale_factor]; + [wreck release]; + } + } + n_alloys = ranrot_rand() % n_alloys; + } + + // Throw out scrap metal + // + for (i = 0; i < n_alloys; i++) + { + ShipEntity* plate = [UNIVERSE newShipWithRole:@"alloy"]; // retain count = 1 + if (plate) + { + Vector rpos = xposition; + Vector rrand = randomPositionInBoundingBox(boundingBox); + rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z; + rpos.x += (ranrot_rand() % 7) - 3; + rpos.y += (ranrot_rand() % 7) - 3; + rpos.z += (ranrot_rand() % 7) - 3; + [plate setPosition:rpos]; + v.x = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2); + v.y = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2); + v.z = 0.1 *((ranrot_rand() % speed_low) - speed_low / 2); + [plate setVelocity:v]; + quaternion_set_random(&q); + [plate setOrientation:q]; + [plate setScanClass: CLASS_CARGO]; + [plate setCommodity:[UNIVERSE commodityForName:@"Alloys"] andAmount:1]; + [UNIVERSE addEntity:plate]; + [plate setStatus:STATUS_IN_FLIGHT]; + [plate setTemperature:[self temperature] * EJECTA_TEMP_FACTOR]; + [[plate getAI] setState:@"GLOBAL"]; + [plate release]; + } + } + } + } + + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); ) + { + [se setSuppressExplosion:suppressExplosion]; + [se setPosition:[se absolutePositionForSubentity]]; + [UNIVERSE addEntity:se]; + [se becomeExplosion]; + } + [self clearSubEntities]; + + // momentum from explosions + desired_range = collision_radius * 2.5; + [self dealMomentumWithinDesiredRange: 0.125 * mass]; + + if (self != [PlayerEntity sharedPlayer]) // was if !isPlayer - but I think this may cause ghosts (Who's "I"? -- Ahruman) + { + if (isPlayer) + { +#ifndef NDEBUG + OOLog(@"becomeExplosion.suspectedGhost.confirm", @"Ship spotted with isPlayer set when not actually the player."); +#endif + isPlayer = NO; + } + [UNIVERSE removeEntity:self]; + } +} + + +// Exposed to AI +- (void) becomeEnergyBlast +{ + ParticleEntity* blast = [[ParticleEntity alloc] initEnergyMineFromShip:self]; + [UNIVERSE addEntity:blast]; + [blast setOwner: [self owner]]; + [blast release]; + [UNIVERSE removeEntity:self]; +} + + +- (void)subEntityDied:(ShipEntity *)sub +{ + if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil]; + + [sub setOwner:nil]; + [subEntities removeObject:sub]; +} + + +- (void)subEntityReallyDied:(ShipEntity *)sub +{ + NSMutableArray *newSubs = nil; + unsigned i, count; + id element; + + if ([self subEntityTakingDamage] == sub) [self setSubEntityTakingDamage:nil]; + + if ([self hasSubEntity:sub]) + { + OOLogERR(@"shipEntity.bug.subEntityRetainUnderflow", @"Subentity died while still in subentity list! This is bad. Leaking subentity list to avoid crash. This is an internal error, please report it."); + + count = [subEntities count]; + if (count != 1) + { + newSubs = [[NSMutableArray alloc] initWithCapacity:count - 1]; + for (i = 0; i != count; ++i) + { + element = [subEntities objectAtIndex:i]; + if (element != sub) + { + [newSubs addObject:element]; + [element release]; // Let it die later, even though there's a reference in the leaked array. + } + } + } + + // Leak old array, replace with new. + subEntities = newSubs; + } +} + + +Vector randomPositionInBoundingBox(BoundingBox bb) +{ + Vector result; + result.x = bb.min.x + randf() * (bb.max.x - bb.min.x); + result.y = bb.min.y + randf() * (bb.max.y - bb.min.y); + result.z = bb.min.z + randf() * (bb.max.z - bb.min.z); + return result; +} + + +- (Vector) positionOffsetForAlignment:(NSString*) align +{ + NSString* padAlign = [NSString stringWithFormat:@"%@---", align]; + Vector result = kZeroVector; + switch ([padAlign characterAtIndex:0]) + { + case (unichar)'c': + case (unichar)'C': + result.x = 0.5 * (boundingBox.min.x + boundingBox.max.x); + break; + case (unichar)'M': + result.x = boundingBox.max.x; + break; + case (unichar)'m': + result.x = boundingBox.min.x; + break; + } + switch ([padAlign characterAtIndex:1]) + { + case (unichar)'c': + case (unichar)'C': + result.y = 0.5 * (boundingBox.min.y + boundingBox.max.y); + break; + case (unichar)'M': + result.y = boundingBox.max.y; + break; + case (unichar)'m': + result.y = boundingBox.min.y; + break; + } + switch ([padAlign characterAtIndex:2]) + { + case (unichar)'c': + case (unichar)'C': + result.z = 0.5 * (boundingBox.min.z + boundingBox.max.z); + break; + case (unichar)'M': + result.z = boundingBox.max.z; + break; + case (unichar)'m': + result.z = boundingBox.min.z; + break; + } + return result; +} + + +Vector positionOffsetForShipInRotationToAlignment(ShipEntity* ship, Quaternion q, NSString* align) +{ + NSString* padAlign = [NSString stringWithFormat:@"%@---", align]; + Vector i = vector_right_from_quaternion(q); + Vector j = vector_up_from_quaternion(q); + Vector k = vector_forward_from_quaternion(q); + BoundingBox arbb = [ship findBoundingBoxRelativeToPosition: make_vector(0,0,0) InVectors: i : j : k]; + Vector result = kZeroVector; + switch ([padAlign characterAtIndex:0]) + { + case (unichar)'c': + case (unichar)'C': + result.x = 0.5 * (arbb.min.x + arbb.max.x); + break; + case (unichar)'M': + result.x = arbb.max.x; + break; + case (unichar)'m': + result.x = arbb.min.x; + break; + } + switch ([padAlign characterAtIndex:1]) + { + case (unichar)'c': + case (unichar)'C': + result.y = 0.5 * (arbb.min.y + arbb.max.y); + break; + case (unichar)'M': + result.y = arbb.max.y; + break; + case (unichar)'m': + result.y = arbb.min.y; + break; + } + switch ([padAlign characterAtIndex:2]) + { + case (unichar)'c': + case (unichar)'C': + result.z = 0.5 * (arbb.min.z + arbb.max.z); + break; + case (unichar)'M': + result.z = arbb.max.z; + break; + case (unichar)'m': + result.z = arbb.min.z; + break; + } + return result; +} + + +- (void) becomeLargeExplosion:(double) factor +{ + Vector xposition = position; + ParticleEntity *fragment; + OOCargoQuantity n_cargo = (ranrot_rand() % (likely_cargo + 1)); + OOCargoQuantity cargo_to_go; + + if ([self status] == STATUS_DEAD) return; + [self setStatus:STATUS_DEAD]; + + //scripting + if (script != nil) + { + [[PlayerEntity sharedPlayer] setScriptTarget:self]; + [self doScriptEvent:@"shipDied"]; // FIXME: params missing + } + + // two parts to the explosion: + // 1. fast sparks + float how_many = factor; + while (how_many > 0.5f) + { + fragment = [[ParticleEntity alloc] initFragburstSize: collision_radius fromPosition:xposition]; + [UNIVERSE addEntity:fragment]; + [fragment release]; + how_many -= 1.0f; + } + // 2. slow clouds + how_many = factor; + while (how_many > 0.5f) + { + fragment = [[ParticleEntity alloc] initBurst2Size: collision_radius fromPosition:xposition]; + [UNIVERSE addEntity:fragment]; + [fragment release]; + how_many -= 1.0f; + } + + + // we need to throw out cargo at this point. + unsigned cargo_chance = 10; + if ([[name lowercaseString] rangeOfString:@"medical"].location != NSNotFound) + { + cargo_to_go = max_cargo * cargo_chance / 100; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + [self setCargo:[UNIVERSE getContainersOfDrugs:cargo_to_go]]; + cargo_chance = 100; // chance of any given piece of cargo surviving decompression + cargo_flag = CARGO_FLAG_CANISTERS; + } + if (cargo_flag == CARGO_FLAG_FULL_PLENTIFUL) + { + cargo_to_go = max_cargo / 10; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + [self setCargo:[UNIVERSE getContainersOfGoods:cargo_to_go scarce:NO]]; + cargo_chance = 100; + } + if (cargo_flag == CARGO_FLAG_FULL_SCARCE) + { + cargo_to_go = max_cargo / 10; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + [self setCargo:[UNIVERSE getContainersOfGoods:cargo_to_go scarce:NO]]; + cargo_chance = 100; + } + while ([cargo count] > 0) + { + if (Ranrot() % 100 < cargo_chance) // 10% chance of any given piece of cargo surviving decompression + { + ShipEntity* container = [[cargo objectAtIndex:0] retain]; + Vector rpos = xposition; + Vector rrand = randomPositionInBoundingBox(boundingBox); + rpos.x += rrand.x; rpos.y += rrand.y; rpos.z += rrand.z; + rpos.x += (ranrot_rand() % 7) - 3; + rpos.y += (ranrot_rand() % 7) - 3; + rpos.z += (ranrot_rand() % 7) - 3; + [container setPosition:rpos]; + [container setScanClass: CLASS_CARGO]; + [UNIVERSE addEntity:container]; + [[container getAI] setState:@"GLOBAL"]; + [container setStatus:STATUS_IN_FLIGHT]; + [container release]; + if (n_cargo > 0) + n_cargo--; // count down extra cargo + } + [cargo removeObjectAtIndex:0]; + } + + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); ) + { + [se setSuppressExplosion:suppressExplosion]; + [se setPosition:[se absolutePositionForSubentity]]; + [UNIVERSE addEntity:se]; + [se becomeExplosion]; + } + [self clearSubEntities]; + + if (!isPlayer) [UNIVERSE removeEntity:self]; +} + + +- (void) collectBountyFor:(ShipEntity *)other +{ + if ([self isPirate]) bounty += [other bounty]; +} + + +- (NSComparisonResult) compareBeaconCodeWith:(ShipEntity*) other +{ + return [[self beaconCode] compare:[other beaconCode] options: NSCaseInsensitiveSearch]; +} + + +- (GLfloat)laserHeatLevel +{ + float result = (weapon_recharge_rate - [self shotTime]) / weapon_recharge_rate; + return OOClamp_0_1_f(result); +} + + +- (GLfloat)hullHeatLevel +{ + return ship_temperature / (GLfloat)SHIP_MAX_CABIN_TEMP; +} + + +- (GLfloat)entityPersonality +{ + return entity_personality / (float)0x7FFF; +} + + +- (GLint)entityPersonalityInt +{ + return entity_personality; +} + + +- (void)setSuppressExplosion:(BOOL)suppress +{ + suppressExplosion = suppress != NO; +} + +/*----------------------------------------- + + AI piloting methods + +-----------------------------------------*/ + +BOOL class_masslocks(int some_class) +{ + switch (some_class) + { + case CLASS_BUOY: + case CLASS_ROCK: + case CLASS_CARGO: + case CLASS_MINE: + case CLASS_NO_DRAW: + return NO; + + case CLASS_THARGOID: + case CLASS_MISSILE: + case CLASS_STATION: + case CLASS_POLICE: + case CLASS_MILITARY: + case CLASS_WORMHOLE: + return YES; + } + return NO; +} + + +- (BOOL) checkTorusJumpClear +{ + Entity* scan; + // + scan = z_previous; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_previous; // skip non-mass-locking + while ((scan)&&(scan->position.z > position.z - scannerRange)) + { + if (class_masslocks(scan->scanClass) && (distance2(position, scan->position) < SCANNER_MAX_RANGE2)) + return NO; + scan = scan->z_previous; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_previous; + } + scan = z_next; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_next; // skip non-mass-locking + while ((scan)&&(scan->position.z < position.z + scannerRange)) + { + if (class_masslocks(scan->scanClass) && (distance2(position, scan->position) < SCANNER_MAX_RANGE2)) + return NO; + scan = scan->z_previous; while ((scan)&&(!class_masslocks(scan->scanClass))) scan = scan->z_previous; + } + return YES; +} + + +- (void) checkScanner +{ + Entity* scan; + n_scanned_ships = 0; + // + scan = z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous; // skip non-ships + while ((scan)&&(scan->position.z > position.z - scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER)) + { + if (scan->isShip) + { + distance2_scanned_ships[n_scanned_ships] = distance2(position, scan->position); + if (distance2_scanned_ships[n_scanned_ships] < SCANNER_MAX_RANGE2) + scanned_ships[n_scanned_ships++] = (ShipEntity*)scan; + } + scan = scan->z_previous; while ((scan)&&(scan->isShip == NO)) scan = scan->z_previous; + } + // + scan = z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships + while ((scan)&&(scan->position.z < position.z + scannerRange)&&(n_scanned_ships < MAX_SCAN_NUMBER)) + { + if (scan->isShip) + { + distance2_scanned_ships[n_scanned_ships] = distance2(position, scan->position); + if (distance2_scanned_ships[n_scanned_ships] < SCANNER_MAX_RANGE2) + scanned_ships[n_scanned_ships++] = (ShipEntity*)scan; + } + scan = scan->z_next; while ((scan)&&(scan->isShip == NO)) scan = scan->z_next; // skip non-ships + } + // + scanned_ships[n_scanned_ships] = nil; // terminate array +} + + +- (ShipEntity**) scannedShips +{ + scanned_ships[n_scanned_ships] = nil; // terminate array + return scanned_ships; +} + + +- (int) numberOfScannedShips +{ + return n_scanned_ships; +} + + +- (void) setFound_target:(Entity *) targetEntity +{ + if (targetEntity) + found_target = [targetEntity universalID]; +} + + +- (void) setPrimaryAggressor:(Entity *) targetEntity +{ + if (targetEntity) + primaryAggressor = [targetEntity universalID]; +} + + +- (void) addTarget:(Entity *) targetEntity +{ + if (targetEntity == self) return; + if (targetEntity != nil) primaryTarget = [targetEntity universalID]; + + [[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(addTarget:) withObject:targetEntity]; +} + + +- (void) removeTarget:(Entity *) targetEntity +{ + [self noteLostTarget]; + + [[self shipSubEntityEnumerator] makeObjectsPerformSelector:@selector(removeTarget:) withObject:targetEntity]; +} + + +- (id) primaryTarget +{ + return [UNIVERSE entityForUniversalID:primaryTarget]; +} + + +- (int) primaryTargetID +{ + return primaryTarget; +} + + +- (void) noteLostTarget +{ + if (primaryTarget != NO_TARGET) + { + ShipEntity* target = [UNIVERSE entityForUniversalID:primaryTarget]; + primaryTarget = NO_TARGET; + [self doScriptEvent:@"shipLostTarget" withArgument:(target && target->isShip) ? (id)target : nil]; + [shipAI reactToMessage:@"TARGET_LOST"]; + } +} + + +- (void) noteTargetDestroyed:(ShipEntity *)target +{ + [self collectBountyFor:(ShipEntity *)target]; + if ([self primaryTarget] == target) + { + [self removeTarget:target]; + [self doScriptEvent:@"shipDestroyedTarget" withArgument:target]; + [shipAI message:@"TARGET_DESTROYED"]; + } +} + + +- (OOBehaviour) behaviour +{ + return behaviour; +} + + +- (void) setBehaviour:(OOBehaviour) cond +{ + if (cond != behaviour) + { + frustration = 0.0; // change is a GOOD thing + behaviour = cond; + } +} + + +- (Vector) destination +{ + return destination; +} + + +- (Vector) distance_six: (GLfloat) dist +{ + Vector six = position; + six.x -= dist * v_forward.x; six.y -= dist * v_forward.y; six.z -= dist * v_forward.z; + return six; +} + + +- (Vector) distance_twelve: (GLfloat) dist +{ + Vector twelve = position; + twelve.x += dist * v_up.x; twelve.y += dist * v_up.y; twelve.z += dist * v_up.z; + return twelve; +} + + +- (double) ballTrackTarget:(double) delta_t +{ + Vector vector_to_target; + Vector axis_to_track_by; + Vector my_position = position; // position relative to parent + Vector my_aim = vector_forward_from_quaternion(orientation); + Vector my_ref = reference; + double aim_cos, ref_cos; + + Entity *target = [self primaryTarget]; + + Entity *last = nil; + Entity *father = [self parentEntity]; + OOMatrix r_mat; + + while ((father)&&(father != last) && father != NO_TARGET) + { + r_mat = [father drawRotationMatrix]; + my_position = vector_add(OOVectorMultiplyMatrix(my_position, r_mat), [father position]); + my_ref = OOVectorMultiplyMatrix(my_ref, r_mat); + last = father; + if (![last isSubEntity]) break; + father = [father owner]; + } + + if (target) + { + vector_to_target = vector_subtract([target position], my_position); + vector_to_target = vector_normal_or_fallback(vector_to_target, kBasisZVector); + + // do the tracking! + aim_cos = dot_product(vector_to_target, my_aim); + ref_cos = dot_product(vector_to_target, my_ref); + } + else + { + aim_cos = 0.0; + ref_cos = -1.0; + } + + if (ref_cos > TURRET_MINIMUM_COS) // target is forward of self + { + axis_to_track_by = cross_product(vector_to_target, my_aim); + } + else + { + aim_cos = 0.0; + axis_to_track_by = cross_product(my_ref, my_aim); // return to center + } + + quaternion_rotate_about_axis(&orientation, axis_to_track_by, thrust * delta_t); + [self orientationChanged]; + + [self setStatus:STATUS_ACTIVE]; + + return aim_cos; +} + + +- (void) trackOntoTarget:(double) delta_t withDForward: (GLfloat) dp +{ + Vector vector_to_target; + Quaternion q_minarc; + // + Entity* target = [self primaryTarget]; + // + if (!target) + return; + + vector_to_target = target->position; + vector_to_target.x -= position.x; vector_to_target.y -= position.y; vector_to_target.z -= position.z; + // + GLfloat range2 = magnitude2(vector_to_target); + GLfloat targetRadius = 0.75 * target->collision_radius; + GLfloat max_cos = sqrt(1 - targetRadius*targetRadius/range2); + + if (dp > max_cos) + return; // ON TARGET! + + if (vector_to_target.x||vector_to_target.y||vector_to_target.z) + vector_to_target = vector_normal(vector_to_target); + else + vector_to_target.z = 1.0; + + q_minarc = quaternion_rotation_between(v_forward, vector_to_target); + + orientation = quaternion_multiply(q_minarc, orientation); + [self orientationChanged]; + + flightRoll = 0.0; + flightPitch = 0.0; +} + + +- (double) ballTrackLeadingTarget:(double) delta_t +{ + Vector vector_to_target; + Vector axis_to_track_by; + Vector my_position = position; // position relative to parent + Vector my_aim = vector_forward_from_quaternion(orientation); + Vector my_ref = reference; + double aim_cos, ref_cos; + Entity *target = [self primaryTarget]; + Vector leading = [target velocity]; + Entity *last = nil; + Entity *father = [self parentEntity]; + OOMatrix r_mat; + + while ((father)&&(father != last) && (father != NO_TARGET)) + { + r_mat = [father drawRotationMatrix]; + my_position = vector_add(OOVectorMultiplyMatrix(my_position, r_mat), [father position]); + my_ref = OOVectorMultiplyMatrix(my_ref, r_mat); + last = father; + if (![last isSubEntity]) break; + father = [father owner]; + } + + if (target) + { + vector_to_target = vector_subtract([target position], my_position); + float lead = magnitude(vector_to_target) / TURRET_SHOT_SPEED; + + vector_to_target = vector_add(vector_to_target, vector_multiply_scalar(leading, lead)); + vector_to_target = vector_normal_or_fallback(vector_to_target, kBasisZVector); + + // do the tracking! + aim_cos = dot_product(vector_to_target, my_aim); + ref_cos = dot_product(vector_to_target, my_ref); + } + else + { + aim_cos = 0.0; + ref_cos = -1.0; + } + + if (ref_cos > TURRET_MINIMUM_COS) // target is forward of self + { + axis_to_track_by = cross_product(vector_to_target, my_aim); + } + else + { + aim_cos = 0.0; + axis_to_track_by = cross_product(my_ref, my_aim); // return to center + } + + quaternion_rotate_about_axis(&orientation, axis_to_track_by, thrust * delta_t); + [self orientationChanged]; + + [self setStatus:STATUS_ACTIVE]; + + return aim_cos; +} + + +- (double) trackPrimaryTarget:(double) delta_t :(BOOL) retreat +{ + Entity* target = [self primaryTarget]; + + if (!target) // leave now! + { + [self noteLostTarget]; // NOTE: was AI message: rather than reactToMessage: + return 0.0; + } + + if (scanClass == CLASS_MISSILE) + return [self missileTrackPrimaryTarget: delta_t]; + + GLfloat d_forward, d_up, d_right; + + Vector relPos = vector_subtract(target->position, position); + double range2 = magnitude2(relPos); + + if (range2 > SCANNER_MAX_RANGE2) + { + [self noteLostTarget]; // NOTE: was AI message: rather than reactToMessage: + return 0.0; + } + + //jink if retreating + if (retreat && (range2 > 250000.0)) // don't jink if closer than 500m - just RUN + { + Vector vx, vy, vz; + if (target->isShip) + { + ShipEntity* targetShip = (ShipEntity*)target; + vx = targetShip->v_right; + vy = targetShip->v_up; + vz = targetShip->v_forward; + } + else + { + Quaternion q = target->orientation; + vx = vector_right_from_quaternion(q); + vy = vector_up_from_quaternion(q); + vz = vector_forward_from_quaternion(q); + } + relPos.x += jink.x * vx.x + jink.y * vy.x + jink.z * vz.x; + relPos.y += jink.x * vx.y + jink.y * vy.y + jink.z * vz.y; + relPos.z += jink.x * vx.z + jink.y * vy.z + jink.z * vz.z; + } + + if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos); + else relPos.z = 1.0; + + double targetRadius = 0.75 * target->collision_radius; + + double max_cos = sqrt(1 - targetRadius*targetRadius/range2); + + double rate2 = 4.0 * delta_t; + double rate1 = 2.0 * delta_t; + + double stick_roll = 0.0; //desired roll and pitch + double stick_pitch = 0.0; + + double reverse = (retreat)? -1.0: 1.0; + + double min_d = 0.004; + + d_right = dot_product(relPos, v_right); + d_up = dot_product(relPos, v_up); + d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target + + if (d_forward * reverse > max_cos) // on_target! + return d_forward; + + // begin rule-of-thumb manoeuvres + stick_pitch = 0.0; + stick_roll = 0.0; + + + if ((reverse * d_forward < -0.5) && !pitching_over) // we're going the wrong way! + pitching_over = YES; + + if (pitching_over) + { + if (reverse * d_up > 0) // pitch up + stick_pitch = -max_flight_pitch; + else + stick_pitch = max_flight_pitch; + pitching_over = (reverse * d_forward < 0.707); + } + + // check if we are flying toward the destination.. + if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls.. + { + if (d_forward < -max_cos) // hack to avoid just flying away from the destination + { + d_up = min_d * 2.0; + } + + if (d_up > min_d) + { + int factor = sqrt(fabs(d_right) / fabs(min_d)); + if (factor > 8) + factor = 8; + if (d_right > min_d) + stick_roll = - max_flight_roll * reverse * 0.125 * factor; + if (d_right < -min_d) + stick_roll = + max_flight_roll * reverse * 0.125 * factor; + } + if (d_up < -min_d) + { + int factor = sqrt(fabs(d_right) / fabs(min_d)); + if (factor > 8) + factor = 8; + if (d_right > min_d) + stick_roll = + max_flight_roll * reverse * 0.125 * factor; + if (d_right < -min_d) + stick_roll = - max_flight_roll * reverse * 0.125 * factor; + } + + if (stick_roll == 0.0) + { + int factor = sqrt(fabs(d_up) / fabs(min_d)); + if (factor > 8) + factor = 8; + if (d_up > min_d) + stick_pitch = - max_flight_pitch * reverse * 0.125 * factor; + if (d_up < -min_d) + stick_pitch = + max_flight_pitch * reverse * 0.125 * factor; + } + } + + // end rule-of-thumb manoeuvres + + // apply 'quick-stop' to roll and pitch adjustments + if (((stick_roll > 0.0)&&(flightRoll < 0.0))||((stick_roll < 0.0)&&(flightRoll > 0.0))) + rate1 *= 4.0; // much faster correction + if (((stick_pitch > 0.0)&&(flightPitch < 0.0))||((stick_pitch < 0.0)&&(flightPitch > 0.0))) + rate2 *= 4.0; // much faster correction + + // apply stick movement limits + if (flightRoll < stick_roll - rate1) + stick_roll = flightRoll + rate1; + if (flightRoll > stick_roll + rate1) + stick_roll = flightRoll - rate1; + if (flightPitch < stick_pitch - rate2) + stick_pitch = flightPitch + rate2; + if (flightPitch > stick_pitch + rate2) + stick_pitch = flightPitch - rate2; + + // apply stick to attitude control + flightRoll = stick_roll; + flightPitch = stick_pitch; + + if (retreat) + d_forward *= d_forward; // make positive AND decrease granularity + + if (d_forward < 0.0) + return 0.0; + + if ((!flightRoll)&&(!flightPitch)) // no correction + return 1.0; + + return d_forward; +} + + +- (double) missileTrackPrimaryTarget:(double) delta_t +{ + Vector relPos; + GLfloat d_forward, d_up, d_right, range2; + Entity *target = [self primaryTarget]; + + if (!target) // leave now! + return 0.0; + + double damping = 0.5 * delta_t; + double rate2 = 4.0 * delta_t; + double rate1 = 2.0 * delta_t; + + double stick_roll = 0.0; //desired roll and pitch + double stick_pitch = 0.0; + + relPos = vector_subtract(target->position, position); + + + // Adjust missile course by taking into account target's velocity and missile + // accuracy. Modification on original code contributed by Cmdr James. + + float missileSpeed = (float)[self speed]; + + // Avoid getting ourselves in a divide by zero situation by setting a missileSpeed + // low threshold. Arbitrarily chosen 0.01, since it seems to work quite well. + // Missile accuracy is already clamped within the 0.0 to 10.0 range at initialization, + // but doing these calculations every frame when accuracy equals 0.0 just wastes cycles. + if (missileSpeed > 0.01f && accuracy > 0.0f) + { + Vector leading = [target velocity]; + float lead = magnitude(relPos) / missileSpeed; + + // Adjust where we are going to take into account target's velocity. + // Use accuracy value to determine how well missile will track target. + relPos.x += (lead * leading.x * (accuracy / 10.0f)); + relPos.y += (lead * leading.y * (accuracy / 10.0f)); + relPos.z += (lead * leading.z * (accuracy / 10.0f)); + } + + + range2 = magnitude2(relPos); + + if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos); + else relPos.z = 1.0; + + d_right = dot_product(relPos, v_right); // = cosine of angle between angle to target and v_right + d_up = dot_product(relPos, v_up); // = cosine of angle between angle to target and v_up + d_forward = dot_product(relPos, v_forward); // = cosine of angle between angle to target and v_forward + + // begin rule-of-thumb manoeuvres + + stick_roll = 0.0; + + if (pitching_over) + pitching_over = (stick_pitch != 0.0); + + if ((d_forward < -pitch_tolerance) && (!pitching_over)) + { + pitching_over = YES; + if (d_up >= 0) + stick_pitch = -max_flight_pitch; + if (d_up < 0) + stick_pitch = max_flight_pitch; + } + + if (pitching_over) + { + pitching_over = (d_forward < 0.5); + } + else + { + stick_pitch = -max_flight_pitch * d_up; + stick_roll = -max_flight_roll * d_right; + } + + // end rule-of-thumb manoeuvres + + // apply damping + if (flightRoll < 0) + flightRoll += (flightRoll < -damping) ? damping : -flightRoll; + if (flightRoll > 0) + flightRoll -= (flightRoll > damping) ? damping : flightRoll; + if (flightPitch < 0) + flightPitch += (flightPitch < -damping) ? damping : -flightPitch; + if (flightPitch > 0) + flightPitch -= (flightPitch > damping) ? damping : flightPitch; + + // apply stick movement limits + if (flightRoll + rate1 < stick_roll) + stick_roll = flightRoll + rate1; + if (flightRoll - rate1 > stick_roll) + stick_roll = flightRoll - rate1; + if (flightPitch + rate2 < stick_pitch) + stick_pitch = flightPitch + rate2; + if (flightPitch - rate2 > stick_pitch) + stick_pitch = flightPitch - rate2; + + // apply stick to attitude + flightRoll = stick_roll; + flightPitch = stick_pitch; + + // + // return target confidence 0.0 .. 1.0 + // + if (d_forward < 0.0) + return 0.0; + return d_forward; +} + + +- (double) trackDestination:(double) delta_t :(BOOL) retreat +{ + Vector relPos; + GLfloat d_forward, d_up, d_right; + + BOOL we_are_docking = (nil != dockingInstructions); + + double rate2 = 4.0 * delta_t; + double rate1 = 2.0 * delta_t; + + double stick_roll = 0.0; //desired roll and pitch + double stick_pitch = 0.0; + + double reverse = 1.0; + + double min_d = 0.004; + double max_cos = 0.995; // was 0.85; should match default value of max_cos in behaviour_fly_to_destination! + + if (retreat) + reverse = -reverse; + + if (isPlayer) + reverse = -reverse; + + relPos = vector_subtract(destination, position); + double range2 = magnitude2(relPos); + double desired_range2 = desired_range*desired_range; + + /* 2009-7-18 Eric: We need to aim well inide the desired_range sphere round the target and not at the surface of the sphere. + Because of the framerate most ships normally overshoot the target and they end up flying clearly on a path + through the sphere. Those ships give no problems, but ships with a very low turnrate will aim close to the surface and will than + have large trouble with reaching their destination. When those ships enter the slowdown range, they have almost no speed vector + in the direction of the target. I now used 95% of desired_range to aim at, but a smaller value might even be better. + */ + if (range2 > desired_range2) max_cos = sqrt(1 - 0.90 * desired_range2/range2); // Head for a point within 95% of desired_range. + + if (!vector_equal(relPos, kZeroVector)) relPos = vector_normal(relPos); + else relPos.z = 1.0; + + d_right = dot_product(relPos, v_right); + d_up = dot_product(relPos, v_up); + d_forward = dot_product(relPos, v_forward); // == cos of angle between v_forward and vector to target + + // begin rule-of-thumb manoeuvres + stick_pitch = 0.0; + stick_roll = 0.0; + + // check if we are flying toward the destination.. + if ((d_forward < max_cos)||(retreat)) // not on course so we must adjust controls.. + { + + if (d_forward <= -max_cos) // hack to avoid just flying away from the destination + { + d_up = min_d * 2.0; + } + + if (d_up > min_d) + { + int factor = sqrt(fabs(d_right) / fabs(min_d)); + if (factor > 8) + factor = 8; + if (d_right > min_d) + stick_roll = - max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse; + if (d_right < -min_d) + stick_roll = + max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse; + } + if (d_up < -min_d) + { + int factor = sqrt(fabs(d_right) / fabs(min_d)); + if (factor > 8) + factor = 8; + if (d_right > min_d) + stick_roll = + max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse; + if (d_right < -min_d) + stick_roll = - max_flight_roll * reverse * 0.125 * factor; //roll_roll * reverse; + } + + if (stick_roll == 0.0) + { + int factor = sqrt(fabs(d_up) / fabs(min_d)); + if (factor > 8) + factor = 8; + if (d_up > min_d) + stick_pitch = - max_flight_pitch * reverse * 0.125 * factor; //pitch_pitch * reverse; + if (d_up < -min_d) + stick_pitch = + max_flight_pitch * reverse * 0.125 * factor; //pitch_pitch * reverse; + } + } + + if (we_are_docking && docking_match_rotation && (d_forward > max_cos)) + { + /* we are docking and need to consider the rotation/orientation of the docking port */ + StationEntity* station_for_docking = (StationEntity*)[UNIVERSE entityForUniversalID:targetStation]; + + if ((station_for_docking)&&(station_for_docking->isStation)) + { + stick_roll = [self rollToMatchUp:[station_for_docking portUpVectorForShipsBoundingBox: boundingBox] rotating:[station_for_docking flightRoll]]; + } + } + + // end rule-of-thumb manoeuvres + + // apply 'quick-stop' to roll and pitch adjustments + if (((stick_roll > 0.0)&&(flightRoll < 0.0))||((stick_roll < 0.0)&&(flightRoll > 0.0))) + rate1 *= 4.0; // much faster correction + if (((stick_pitch > 0.0)&&(flightPitch < 0.0))||((stick_pitch < 0.0)&&(flightPitch > 0.0))) + rate2 *= 4.0; // much faster correction + + // apply stick movement limits + if (flightRoll < stick_roll - rate1) + stick_roll = flightRoll + rate1; + if (flightRoll > stick_roll + rate1) + stick_roll = flightRoll - rate1; + if (flightPitch < stick_pitch - rate2) + stick_pitch = flightPitch + rate2; + if (flightPitch > stick_pitch + rate2) + stick_pitch = flightPitch - rate2; + + // apply stick to attitude control + flightRoll = stick_roll; + flightPitch = stick_pitch; + + if (retreat) + d_forward *= d_forward; // make positive AND decrease granularity + + if (d_forward < 0.0) + return 0.0; + + if ((!flightRoll)&&(!flightPitch)) // no correction + return 1.0; + + return d_forward; +} + + +- (GLfloat) rollToMatchUp:(Vector) up_vec rotating:(GLfloat) match_roll; +{ + GLfloat cosTheta = dot_product(up_vec, v_up); // == cos of angle between up vectors + GLfloat sinTheta = dot_product(up_vec, v_right); + + if (!isPlayer) + { + match_roll = -match_roll; // make necessary corrections for a different viewpoint + sinTheta = -sinTheta; + } + + if (cosTheta < 0.0f) + { + cosTheta = -cosTheta; + sinTheta = -sinTheta; + } + + if (sinTheta > 0.0f) + { + // increase roll rate + return cosTheta * cosTheta * match_roll + sinTheta * sinTheta * max_flight_roll; + } + else + { + // decrease roll rate + return cosTheta * cosTheta * match_roll - sinTheta * sinTheta * max_flight_roll; + } +} + + +- (GLfloat) rangeToDestination +{ + return sqrtf(distance2(position, destination)); +} + + +- (double) rangeToPrimaryTarget +{ + double dist; + Vector delta; + Entity *target = [self primaryTarget]; + if (target == nil) // leave now! + return 0.0; + delta = target->position; + delta.x -= position.x; + delta.y -= position.y; + delta.z -= position.z; + dist = sqrt(delta.x*delta.x + delta.y*delta.y + delta.z*delta.z); + dist -= target->collision_radius; + dist -= collision_radius; + return dist; +} + + +- (BOOL) onTarget:(BOOL) fwd_weapon +{ + GLfloat d2, radius, dq, astq; + Vector rel_pos, urp; + int weapon_type = (fwd_weapon)? forward_weapon_type : aft_weapon_type; + if (weapon_type == WEAPON_THARGOID_LASER) + { + return (randf() < 0.05); // one in twenty shots on target + } + + Entity *target = [self primaryTarget]; + if (target == nil) return NO; + if ([target status] == STATUS_DEAD) return NO; + + if (isSunlit && (target->isSunlit == NO) && (randf() < 0.75)) + return NO; // 3/4 of the time you can't see from a lit place into a darker place + radius = target->collision_radius; + rel_pos = target->position; + rel_pos.x -= position.x; + rel_pos.y -= position.y; + rel_pos.z -= position.z; + d2 = magnitude2(rel_pos); + if (d2) + urp = vector_normal(rel_pos); + else + urp = make_vector(0, 0, 1); + dq = dot_product(urp, v_forward); // cosine of angle between v_forward and unit relative position + if (((fwd_weapon)&&(dq < 0.0)) || ((!fwd_weapon)&&(dq > 0.0))) + return NO; + + astq = sqrt(1.0 - radius * radius / d2); // cosine of half angle subtended by target + + return (fabs(dq) >= astq); +} + + +- (BOOL) fireMainWeapon:(double) range +{ + // + // set the values for the forward weapon + // + [self setWeaponDataFromType:forward_weapon_type]; + weapon_energy = OOClamp_0_max_f([shipinfoDictionary floatForKey:@"weapon_energy" defaultValue:weapon_energy],50.0); + + if ([self shotTime] < weapon_recharge_rate) + return NO; + + if (range > randf() * weaponRange * accuracy) + return NO; + if (range > weaponRange) + return NO; + if (![self onTarget:YES]) + return NO; + + // + + BOOL fired = NO; + switch (forward_weapon_type) + { + case WEAPON_PLASMA_CANNON : + [self firePlasmaShot: 0.0: 1500.0: [OOColor yellowColor]]; + fired = YES; + break; + + case WEAPON_PULSE_LASER : + case WEAPON_BEAM_LASER : + case WEAPON_MINING_LASER : + case WEAPON_MILITARY_LASER : + [self fireLaserShotInDirection: VIEW_FORWARD]; + fired = YES; + break; + + case WEAPON_THARGOID_LASER : + [self fireDirectLaserShot]; + fired = YES; + break; + + case WEAPON_NONE: + // Do nothing + break; + } + + //can we fire lasers from our subentities? + NSEnumerator *subEnum = nil; + ShipEntity *se = nil; + for (subEnum = [self shipSubEntityEnumerator]; (se = [subEnum nextObject]); ) + { + if ([se fireSubentityLaserShot:range]) fired = YES; + } + + if (fired && cloaking_device_active && cloakPassive) + { + [self deactivateCloakingDevice]; + } + + return fired; +} + + +- (BOOL) fireAftWeapon:(double) range +{ + BOOL result = YES; + // + // save the existing weapon values + // + double weapon_energy1 = weapon_energy; + double weapon_recharge_rate1 = weapon_recharge_rate; + double weapon_range1 = weaponRange; + // + // set new values from aft_weapon_type + // + [self setWeaponDataFromType:aft_weapon_type]; + + if ([self shotTime] < weapon_recharge_rate) + return NO; + if (![self onTarget:NO]) + return NO; + if (range > randf() * weaponRange) + return NO; + + if (result) + { + switch (aft_weapon_type) + { + case WEAPON_PULSE_LASER : + case WEAPON_BEAM_LASER : + case WEAPON_MINING_LASER : + case WEAPON_MILITARY_LASER : + [self fireLaserShotInDirection:VIEW_AFT]; + break; + case WEAPON_THARGOID_LASER : + [self fireDirectLaserShot]; + return YES; + break; + + case WEAPON_PLASMA_CANNON: // FIXME: NPCs can't have rear plasma cannons, for no obvious reason. + case WEAPON_NONE: + // do nothing + break; + } + } + + // restore previous values + weapon_energy = weapon_energy1; + weapon_recharge_rate = weapon_recharge_rate1; + weaponRange = weapon_range1; + // + return result; +} + + +- (OOTimeDelta) shotTime +{ + return shot_time; +} + + +- (void) resetShotTime +{ + shot_time = 0.0; +} + + +- (BOOL) fireTurretCannon:(double) range +{ + if ([self shotTime] < weapon_recharge_rate) + return NO; + if (range > 5050) //50 more than max range - open up just slightly early + return NO; + + ParticleEntity *shot = nil; + Vector origin = position; + Entity *last = nil; + Entity *father = [self parentEntity]; + OOMatrix r_mat; + Vector vel; + + while ((father)&&(father != last) && (father != NO_TARGET)) + { + r_mat = [father drawRotationMatrix]; + origin = vector_add(OOVectorMultiplyMatrix(origin, r_mat), [father position]); + last = father; + if (![last isSubEntity]) break; + father = [father owner]; + } + + vel = vector_forward_from_quaternion(orientation); // Facing + origin = vector_add(origin, vector_multiply_scalar(vel, collision_radius + 0.5)); // Start just outside collision sphere + vel = vector_multiply_scalar(vel, TURRET_SHOT_SPEED); // Shot velocity + + shot = [[ParticleEntity alloc] initPlasmaShotAt:origin + velocity:vel + energy:weapon_energy + duration:3.0 + color:laser_color]; + [shot autorelease]; + [UNIVERSE addEntity:shot]; + [shot setOwner:[self owner]]; // has to be done AFTER adding shot to the UNIVERSE + + [self resetShotTime]; + return YES; +} + + +- (void) setLaserColor:(OOColor *) color +{ + if (color) + { + [laser_color release]; + laser_color = [color retain]; + } +} + + +- (OOColor *)laserColor +{ + return [[laser_color retain] autorelease]; +} + + +- (BOOL) fireSubentityLaserShot: (double) range +{ + ParticleEntity *shot; + int direction = VIEW_FORWARD; + GLfloat hit_at_range; + target_laser_hit = NO_TARGET; + + if (forward_weapon_type == WEAPON_NONE) + return NO; + [self setWeaponDataFromType:forward_weapon_type]; + + ShipEntity* parent = (ShipEntity*)[self owner]; + + if ([self shotTime] < weapon_recharge_rate) + return NO; + + if (range > weaponRange) + return NO; + + hit_at_range = weaponRange; + target_laser_hit = [UNIVERSE getFirstEntityHitByLaserFromEntity:self inView:direction offset: make_vector(0,0,0) rangeFound: &hit_at_range]; + + shot = [[ParticleEntity alloc] initLaserFromShip:self view:direction offset:kZeroVector]; + [shot setColor:laser_color]; + [shot setScanClass: CLASS_NO_DRAW]; + ShipEntity *victim = [UNIVERSE entityForUniversalID:target_laser_hit]; + if ([victim isShip]) + { + ShipEntity *subent = [victim subEntityTakingDamage]; + if (subent && [victim isFrangible]) + { + // do 1% bleed-through damage... + [victim takeEnergyDamage: 0.01 * weapon_energy from:subent becauseOf: parent]; + victim = subent; + } + + if (hit_at_range < weaponRange) + { + [victim takeEnergyDamage:weapon_energy from:self becauseOf: parent]; // a very palpable hit + + [shot setCollisionRadius: hit_at_range]; + Vector flash_pos = [shot position]; + Vector vd = vector_forward_from_quaternion([shot orientation]); + flash_pos.x += vd.x * hit_at_range; flash_pos.y += vd.y * hit_at_range; flash_pos.z += vd.z * hit_at_range; + ParticleEntity* laserFlash = [[ParticleEntity alloc] initFlashSize:1.0 fromPosition: flash_pos color:laser_color]; + [laserFlash setVelocity:[victim velocity]]; + [UNIVERSE addEntity:laserFlash]; + [laserFlash release]; + } + } + [UNIVERSE addEntity:shot]; + [shot release]; + + [self resetShotTime]; + + return YES; +} + + +- (BOOL) fireDirectLaserShot +{ + GLfloat hit_at_range; + Entity* my_target = [self primaryTarget]; + if (!my_target) + return NO; + ParticleEntity* shot; + double range_limit2 = weaponRange*weaponRange; + Vector r_pos = my_target->position; + r_pos.x -= position.x; r_pos.y -= position.y; r_pos.z -= position.z; + if (r_pos.x||r_pos.y||r_pos.z) + r_pos = vector_normal(r_pos); + else + r_pos.z = 1.0; + + Quaternion q_laser = quaternion_rotation_between(r_pos, make_vector(0.0f,0.0f,1.0f)); + q_laser.x += 0.01 * (randf() - 0.5); // randomise aim a little (+/- 0.005) + q_laser.y += 0.01 * (randf() - 0.5); + q_laser.z += 0.01 * (randf() - 0.5); + quaternion_normalize(&q_laser); + + Quaternion q_save = orientation; // save rotation + orientation = q_laser; // face in direction of laser + target_laser_hit = [UNIVERSE getFirstEntityHitByLaserFromEntity:self inView:VIEW_FORWARD offset: make_vector(0,0,0) rangeFound: &hit_at_range]; + orientation = q_save; // restore rotation + + Vector vel = make_vector(v_forward.x * flightSpeed, v_forward.y * flightSpeed, v_forward.z * flightSpeed); + + // do special effects laser line + shot = [[ParticleEntity alloc] initLaserFromShip:self view:VIEW_FORWARD offset:kZeroVector]; + [shot setColor:laser_color]; + [shot setScanClass: CLASS_NO_DRAW]; + [shot setPosition: position]; + [shot setOrientation: q_laser]; + [shot setVelocity: vel]; + ShipEntity *victim = [UNIVERSE entityForUniversalID:target_laser_hit]; + if ([victim isShip]) + { + ShipEntity *subent = [victim subEntityTakingDamage]; + if (subent != nil && [victim isFrangible]) + { + // do 1% bleed-through damage... + [victim takeEnergyDamage: 0.01 * weapon_energy from:subent becauseOf:self]; + victim = subent; + } + + if (hit_at_range * hit_at_range < range_limit2) + { + [victim takeEnergyDamage:weapon_energy from:self becauseOf:self]; // a very palpable hit + + [shot setCollisionRadius: hit_at_range]; + Vector flash_pos = shot->position; + Vector vd = vector_forward_from_quaternion(shot->orientation); + flash_pos.x += vd.x * hit_at_range; flash_pos.y += vd.y * hit_at_range; flash_pos.z += vd.z * hit_at_range; + ParticleEntity* laserFlash = [[ParticleEntity alloc] initFlashSize:1.0 fromPosition: flash_pos color:laser_color]; + [laserFlash setVelocity:[victim velocity]]; + [UNIVERSE addEntity:laserFlash]; + [laserFlash release]; + } + } + [UNIVERSE addEntity:shot]; + [shot release]; + + [self resetShotTime]; + + // random laser over-heating for AI ships + if ((!isPlayer)&&((ranrot_rand() & 255) < weapon_energy)&&(![self isMining])) + { + shot_time -= (randf() * weapon_energy); + } + + return YES; +} + + +- (BOOL) fireLaserShotInDirection: (OOViewID) direction +{ + ParticleEntity *shot; + double range_limit2 = weaponRange*weaponRange; + GLfloat hit_at_range; + Vector vel; + target_laser_hit = NO_TARGET; + + vel.x = v_forward.x * flightSpeed; + vel.y = v_forward.y * flightSpeed; + vel.z = v_forward.z * flightSpeed; + + Vector laserPortOffset; + + switch(direction) + { + case VIEW_AFT: + laserPortOffset = aftWeaponOffset; + break; + case VIEW_PORT: + laserPortOffset = portWeaponOffset; + break; + case VIEW_STARBOARD: + laserPortOffset = starboardWeaponOffset; + break; + default: + laserPortOffset = forwardWeaponOffset; + } + + target_laser_hit = [UNIVERSE getFirstEntityHitByLaserFromEntity:self inView:direction offset:laserPortOffset rangeFound: &hit_at_range]; + + shot = [[ParticleEntity alloc] initLaserFromShip:self view:direction offset:laserPortOffset]; // alloc retains! + + [shot setColor:laser_color]; + [shot setScanClass: CLASS_NO_DRAW]; + [shot setVelocity: vel]; + ShipEntity *victim = [UNIVERSE entityForUniversalID:target_laser_hit]; + if ([victim isShip]) + { + /* CRASH in [victim->sub_entities containsObject:subent] here (1.69, OS X/x86). + Analysis: Crash is in _freedHandler called from CFEqual, indicating either a dead + object in victim->sub_entities or dead victim->subentity_taking_damage. I suspect + the latter. Probable solution: dying subentities must cause parent to clean up + properly. This was probably obscured by the entity recycling scheme in the past. + Fix: made subentity_taking_damage a weak reference accessed via a method. + -- Ahruman 20070706, 20080304 + */ + ShipEntity *subent = [victim subEntityTakingDamage]; + if (subent != nil && [victim isFrangible]) + { + // do 1% bleed-through damage... + [victim takeEnergyDamage: 0.01 * weapon_energy from:subent becauseOf:self]; + victim = subent; + } + + if (hit_at_range * hit_at_range < range_limit2) + { + [victim takeEnergyDamage:weapon_energy from:self becauseOf:self]; // a very palpable hit + + [shot setCollisionRadius: hit_at_range]; + Vector flash_pos = shot->position; + Vector vd = vector_forward_from_quaternion(shot->orientation); + flash_pos.x += vd.x * hit_at_range; flash_pos.y += vd.y * hit_at_range; flash_pos.z += vd.z * hit_at_range; + ParticleEntity* laserFlash = [[ParticleEntity alloc] initFlashSize:1.0 fromPosition: flash_pos color:laser_color]; + [laserFlash setVelocity:[victim velocity]]; + [UNIVERSE addEntity:laserFlash]; + [laserFlash release]; + } + } + [UNIVERSE addEntity:shot]; + [shot release]; //release + + [self resetShotTime]; + + // random laser over-heating for AI ships + if ((!isPlayer)&&((ranrot_rand() & 255) < weapon_energy)&&(![self isMining])) + { + shot_time -= (randf() * weapon_energy); + } + + return YES; +} + + +- (void) throwSparks +{ + ParticleEntity* spark; + Vector vel; + Vector origin = position; + + GLfloat lr = randf() * (boundingBox.max.x - boundingBox.min.x) + boundingBox.min.x; + GLfloat ud = randf() * (boundingBox.max.y - boundingBox.min.y) + boundingBox.min.y; + GLfloat fb = randf() * boundingBox.max.z + boundingBox.min.z; // rear section only + + origin.x += fb * v_forward.x; + origin.y += fb * v_forward.y; + origin.z += fb * v_forward.z; + + origin.x += ud * v_up.x; + origin.y += ud * v_up.y; + origin.z += ud * v_up.z; + + origin.x += lr * v_right.x; + origin.y += lr * v_right.y; + origin.z += lr * v_right.z; + + float w = boundingBox.max.x - boundingBox.min.x; + float h = boundingBox.max.y - boundingBox.min.y; + float m = (w < h) ? 0.25 * w: 0.25 * h; + + float sz = m * (1 + randf() + randf()); // half minimum dimension on average + + vel = make_vector(2.0 * (origin.x - position.x), 2.0 * (origin.y - position.y), 2.0 * (origin.z - position.z)); + + OOColor *color = [OOColor colorWithCalibratedHue:0.08 + 0.17 * randf() saturation:1.0 brightness:1.0 alpha:1.0]; + + spark = [[ParticleEntity alloc] initSparkAt:origin + velocity:vel + duration:2.0 + 3.0 * randf() + size:sz + color:color]; + [spark setOwner:self]; + [UNIVERSE addEntity:spark]; + [spark release]; + + next_spark_time = randf(); +} + + +- (BOOL) firePlasmaShot:(double) offset :(double) speed :(OOColor *) color +{ + ParticleEntity *shot; + Vector vel, rt; + Vector origin = position; + double start = collision_radius + 0.5; + + speed += flightSpeed; + + if (++shot_counter % 2) + offset = -offset; + + vel = v_forward; + rt = v_right; + + if (isPlayer) // player can fire into multiple views! + { + switch ([UNIVERSE viewDirection]) + { + case VIEW_AFT : + vel = v_forward; + vel.x = -vel.x; vel.y = -vel.y; vel.z = -vel.z; // reverse + rt = v_right; + rt.x = -rt.x; rt.y = -rt.y; rt.z = -rt.z; // reverse + break; + case VIEW_STARBOARD : + vel = v_right; + rt = v_forward; + rt.x = -rt.x; rt.y = -rt.y; rt.z = -rt.z; // reverse + break; + case VIEW_PORT : + vel = v_right; + vel.x = -vel.x; vel.y = -vel.y; vel.z = -vel.z; // reverse + rt = v_forward; + break; + + default: + break; + } + } + + origin.x += vel.x * start; + origin.y += vel.y * start; + origin.z += vel.z * start; + + origin.x += rt.x * offset; + origin.y += rt.y * offset; + origin.z += rt.z * offset; + + vel.x *= speed; + vel.y *= speed; + vel.z *= speed; + + shot = [[ParticleEntity alloc] initPlasmaShotAt:origin + velocity:vel + energy:weapon_energy + duration:5.0 + color:color]; + + [shot setOwner:self]; + [UNIVERSE addEntity:shot]; + [shot release]; + + [self resetShotTime]; + + return YES; +} + + +- (BOOL) fireMissile +{ + ShipEntity *missile = nil; + Vector vel; + Vector origin = position; + Vector start, v_eject; + Entity *target = nil; + ShipEntity *target_ship = nil; + + // default launching position + start.x = 0.0; // in the middle + start.y = boundingBox.min.y - 4.0; // 4m below bounding box + start.z = boundingBox.max.z + 1.0; // 1m ahead of bounding box + // custom launching position + ScanVectorFromString([shipinfoDictionary objectForKey:@"missile_launch_position"], &start); + + double throw_speed = 250.0; + Quaternion q1 = orientation; + target = [self primaryTarget]; + + if ((missiles <= 0)||(target == nil)||(target->scanClass == CLASS_NO_DRAW)) // no missile lock! + return NO; + + if ([target isShip]) + { + target_ship = (ShipEntity*)target; + if ([target_ship isCloaked]) return NO; + if (![self hasMilitaryScannerFilter] && [target_ship isJammingScanning]) return NO; + } + + // custom missiles + if (missileRole != nil) missile = [UNIVERSE newShipWithRole:missileRole]; + if (missile == nil) // no custom role + { + if (randf() < 0.90) // choose a standard missile 90% of the time + { + missile = [UNIVERSE newShipWithRole:@"EQ_MISSILE"]; // retained + } + else // otherwise choose any with the role 'missile' - which may include alternative weapons + { + missile = [UNIVERSE newShipWithRole:@"missile"]; // retained + } + } + + if (missile == nil) return NO; + + missiles--; + + double mcr = missile->collision_radius; + + v_eject = vector_normal(start); + + vel = kZeroVector; // starting velocity + + // check if start is within bounding box... + while ( (start.x > boundingBox.min.x - mcr)&&(start.x < boundingBox.max.x + mcr)&& + (start.y > boundingBox.min.y - mcr)&&(start.y < boundingBox.max.y + mcr)&& + (start.z > boundingBox.min.z - mcr)&&(start.z < boundingBox.max.z + mcr)) + { + start.x += mcr * v_eject.x; start.y += mcr * v_eject.y; start.z += mcr * v_eject.z; + vel.x += 10.0f * mcr * v_eject.x; vel.y += 10.0f * mcr * v_eject.y; vel.z += 10.0f * mcr * v_eject.z; // throw it outward a bit harder + } + + if (isPlayer) + q1.w = -q1.w; // player view is reversed remember! + + vel.x += (flightSpeed + throw_speed) * v_forward.x; + vel.y += (flightSpeed + throw_speed) * v_forward.y; + vel.z += (flightSpeed + throw_speed) * v_forward.z; + + origin.x = position.x + v_right.x * start.x + v_up.x * start.y + v_forward.x * start.z; + origin.y = position.y + v_right.y * start.x + v_up.y * start.y + v_forward.y * start.z; + origin.z = position.z + v_right.z * start.x + v_up.z * start.y + v_forward.z * start.z; + + if (![self isMissileFlagSet]) [missile setOwner:self]; + else [missile setOwner:[self owner]]; + + [missile addTarget:target]; + [missile setGroup:[self group]]; + [missile setPosition:origin]; + [missile setOrientation:q1]; + [missile setVelocity:vel]; + [missile setSpeed:150.0]; + [missile setDistanceTravelled:0.0]; + [missile setStatus:STATUS_IN_FLIGHT]; // necessary to get it going! + [missile setIsMissileFlag:YES]; + [missile resetShotTime]; + + [UNIVERSE addEntity:missile]; + + [missile release]; //release + + if ([missile scanClass] == CLASS_MISSILE) + { + [target_ship setPrimaryAggressor:self]; + [target_ship doScriptEvent:@"shipAttackedWithMissile" withArgument:missile andArgument:self]; + [target_ship reactToAIMessage:@"INCOMING_MISSILE"]; + } + + if (cloaking_device_active && cloakPassive) + { + [self deactivateCloakingDevice]; + } + + return YES; +} + + +- (BOOL) isMissileFlagSet +{ + return isMissile; // were we created using fireMissile? (for tracking submunitions) +} + + +- (void) setIsMissileFlag:(BOOL)newValue +{ + isMissile = !!newValue; // set the isMissile flag, used for tracking submunitions +} + + +// Exposed to AI +- (BOOL) fireECM +{ + if (![self hasECM]) return NO; + + ParticleEntity *ecmDevice = [[ParticleEntity alloc] initECMMineFromShip:self]; // retained + [UNIVERSE addEntity:ecmDevice]; + [ecmDevice release]; + return YES; +} + + +- (BOOL) activateCloakingDevice +{ + if (![self hasCloakingDevice]) return NO; + + if (!cloaking_device_active) cloaking_device_active = (energy > CLOAKING_DEVICE_START_ENERGY * maxEnergy); + return cloaking_device_active; +} + + +- (void) deactivateCloakingDevice +{ + cloaking_device_active = NO; +} + + +- (BOOL) launchEnergyBomb +{ + if (![self hasEnergyBomb]) return NO; + [self setSpeed: maxFlightSpeed + 300]; + ShipEntity* bomb = [UNIVERSE newShipWithRole:@"energy-bomb"]; + if (bomb == nil) return NO; + + [self removeEquipmentItem:@"EQ_ENERGY_BOMB"]; + + double start = collision_radius + bomb->collision_radius; + Quaternion random_direction; + Vector vel; + Vector rpos; + double random_roll = randf() - 0.5; // -0.5 to +0.5 + double random_pitch = randf() - 0.5; // -0.5 to +0.5 + quaternion_set_random(&random_direction); + + rpos = vector_subtract([self position], vector_multiply_scalar(v_forward, start)); + + double eject_speed = -800.0; + vel = vector_multiply_scalar(v_forward, [self flightSpeed] + eject_speed); + eject_speed *= 0.5 * (randf() - 0.5); // -0.25x .. +0.25x + vel = vector_add(vel, vector_multiply_scalar(v_up, eject_speed)); + eject_speed *= 0.5 * (randf() - 0.5); // -0.0625x .. +0.0625x + vel = vector_add(vel, vector_multiply_scalar(v_right, eject_speed)); + + [bomb setPosition:rpos]; + [bomb setOrientation:random_direction]; + [bomb setRoll:random_roll]; + [bomb setPitch:random_pitch]; + [bomb setVelocity:vel]; + [bomb setScanClass:CLASS_MINE]; // TODO should be CLASS_ENERGY_BOMB + [bomb setStatus:STATUS_IN_FLIGHT]; + [bomb setEnergy:5.0]; // 5 second countdown + [bomb setBehaviour:BEHAVIOUR_ENERGY_BOMB_COUNTDOWN]; + [bomb setOwner:self]; + [UNIVERSE addEntity:bomb]; + [[bomb getAI] setState:@"GLOBAL"]; + [bomb release]; + + if (self != [PlayerEntity sharedPlayer]) // get the heck out of here + { + [self addTarget:bomb]; + [self setBehaviour:BEHAVIOUR_FLEE_TARGET]; + frustration = 0.0; + } + return YES; +} + + +- (OOUniversalID)launchEscapeCapsule +{ + OOUniversalID result = NO_TARGET; + ShipEntity *mainPod = nil, *pod = nil; + unsigned n_pods; + + /* BUG: player can't launch escape pod in interstellar space (because + there is no standard place for ressurection), but NPCs can. + FIX: don't let NPCs do it either. Submitted by Cmdr James. + -- Ahruman 20070822 + */ + if ([UNIVERSE station] == nil) return NO_TARGET; + + // check number of pods aboard -- require at least one. + n_pods = [shipinfoDictionary unsignedIntForKey:@"has_escape_pod"]; + + pod = [UNIVERSE newShipWithRole:[shipinfoDictionary stringForKey:@"escape_pod_model" defaultValue:@"escape-capsule"]]; + mainPod = pod; + + if (pod) + { + [pod setOwner:self]; + [pod setScanClass: CLASS_CARGO]; + [pod setCommodity:[UNIVERSE commodityForName:@"Slaves"] andAmount:1]; + if (crew) // transfer crew + { + // make sure crew inherit any legalStatus + unsigned i; + for (i = 0; i < [crew count]; i++) + { + OOCharacter *ch = (OOCharacter*)[crew objectAtIndex:i]; + [ch setLegalStatus: [self legalStatus] | [ch legalStatus]]; + } + [pod setCrew:crew]; + [self setCrew:nil]; + [self setHulk:YES]; //CmdrJames experiment with fixing ejection behaviour + } + [[pod getAI] setStateMachine:@"homeAI.plist"]; + [self dumpItem:pod]; + [[pod getAI] setState:@"GLOBAL"]; + [pod release]; //release + result = [pod universalID]; + } + // launch other pods (passengers) + unsigned i; + for (i = 1; i < n_pods; i++) + { + pod = [UNIVERSE newShipWithRole:@"escape-capsule"]; + if (pod) + { + Random_Seed orig = [UNIVERSE systemSeedForSystemNumber:gen_rnd_number()]; + [pod setOwner:self]; + [pod setScanClass: CLASS_CARGO]; + [pod setCommodity:[UNIVERSE commodityForName:@"Slaves"] andAmount:1]; + [pod setCrew:[NSArray arrayWithObject:[OOCharacter randomCharacterWithRole:@"passenger" andOriginalSystem:orig]]]; + [[pod getAI] setStateMachine:@"homeAI.plist"]; + [self dumpItem:pod]; + [[pod getAI] setState:@"GLOBAL"]; + [pod release]; //release + } + } + + [self doScriptEvent:@"shipLaunchedEscapePod" withArgument:mainPod]; + + return result; +} + + +// This is a documented AI method; do not change semantics. (Note: AIs don't have access to the return value.) +- (OOCargoType) dumpCargo +{ + ShipEntity *jetto = [self dumpCargoItem]; + if (jetto != nil) return [jetto commodityType]; + else return CARGO_NOT_CARGO; +} + + +- (ShipEntity *) dumpCargoItem +{ + ShipEntity *jetto = nil; + + if (([cargo count] > 0)&&([UNIVERSE getTime] - cargo_dump_time > 0.5)) // space them 0.5s or 10m apart + { + jetto = [[[cargo objectAtIndex:0] retain] autorelease]; + if (jetto != nil) + { + [self dumpItem:jetto]; + [cargo removeObjectAtIndex:0]; + } + } + + return jetto; +} + + +- (OOCargoType) dumpItem: (ShipEntity*) jetto +{ + if (!jetto) + return 0; + int result = [jetto cargoType]; + Vector start; + + double eject_speed = 20.0; + double eject_reaction = -eject_speed * [jetto mass] / [self mass]; + double jcr = jetto->collision_radius; + + Quaternion random_direction; + Vector vel, v_eject; + Vector rpos = position; + double random_roll = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5 + double random_pitch = ((ranrot_rand() % 1024) - 512.0)/1024.0; // -0.5 to +0.5 + quaternion_set_random(&random_direction); + + // default launching position + start.x = 0.0; // in the middle + start.y = 0.0; // + start.z = boundingBox.min.z - jcr; // 1m behind of bounding box + + // custom launching position + ScanVectorFromString([shipinfoDictionary objectForKey:@"aft_eject_position"], &start); + + v_eject = vector_normal(start); + + // check if start is within bounding box... + while ( (start.x > boundingBox.min.x - jcr)&&(start.x < boundingBox.max.x + jcr)&& + (start.y > boundingBox.min.y - jcr)&&(start.y < boundingBox.max.y + jcr)&& + (start.z > boundingBox.min.z - jcr)&&(start.z < boundingBox.max.z + jcr)) + { + start = vector_add(start, vector_multiply_scalar(v_eject, jcr)); + } + + v_eject = make_vector( v_right.x * start.x + v_up.x * start.y + v_forward.x * start.z, + v_right.y * start.x + v_up.y * start.y + v_forward.y * start.z, + v_right.z * start.x + v_up.z * start.y + v_forward.z * start.z); + + rpos = vector_add(rpos, v_eject); + v_eject = vector_normal(v_eject); + + v_eject.x += (randf() - randf())/eject_speed; + v_eject.y += (randf() - randf())/eject_speed; + v_eject.z += (randf() - randf())/eject_speed; + + vel = vector_add(vector_multiply_scalar(v_forward, flightSpeed), vector_multiply_scalar(v_eject, eject_speed)); + velocity = vector_add(velocity, vector_multiply_scalar(v_eject, eject_reaction)); + + [jetto setPosition:rpos]; + [jetto setOrientation:random_direction]; + [jetto setRoll:random_roll]; + [jetto setPitch:random_pitch]; + [jetto setVelocity:vel]; + [jetto setScanClass: CLASS_CARGO]; + [jetto setStatus: STATUS_IN_FLIGHT]; + [jetto setTemperature:[self temperature] * EJECTA_TEMP_FACTOR]; + [UNIVERSE addEntity:jetto]; + [[jetto getAI] setState:@"GLOBAL"]; + cargo_dump_time = [UNIVERSE getTime]; + return result; +} + + +- (void) manageCollisions +{ + // deal with collisions + // + Entity* ent; + ShipEntity* other_ship; + + while ([collidingEntities count] > 0) + { + ent = [(Entity *)[collidingEntities objectAtIndex:0] retain]; + [collidingEntities removeObjectAtIndex:0]; + if (ent) + { + if (ent->isShip) + { + other_ship = (ShipEntity *)ent; + [self collideWithShip:other_ship]; + } + if (ent->isPlanet) + { + [self getDestroyedBy:ent context:@"hit a planet"]; + if (self == [PlayerEntity sharedPlayer]) [self retain]; + } + if (ent->isWormhole) + { + WormholeEntity* whole = (WormholeEntity*)ent; + if (isPlayer) + { + [(PlayerEntity*)self enterWormhole: whole]; + return; + } + else + { + [whole suckInShip: self]; + } + } + [ent release]; + } + } +} + + +- (BOOL) collideWithShip:(ShipEntity *)other +{ + Vector loc; + double inc1, dam1, dam2; + + if (!other) + return NO; + + ShipEntity* otherParent = [other parentEntity]; + BOOL otherIsStation = other == [UNIVERSE station]; + // calculate line of centers using centres + loc = vector_normal_or_zbasis(vector_subtract([other absolutePositionForSubentity], position)); + + inc1 = dot_product(v_forward, loc); + + if ([self canScoop:other]) + { + [self scoopIn:other]; + return NO; + } + if ([other canScoop:self]) + { + [other scoopIn:self]; + return NO; + } + if (universalID == NO_TARGET) + return NO; + if (other->universalID == NO_TARGET) + return NO; + + // find velocity along line of centers + // + // momentum = mass x velocity + // ke = mass x velocity x velocity + // + GLfloat m1 = mass; // mass of self + GLfloat m2 = [other mass]; // mass of other + + // starting velocities: + Vector v, vel1b = [self velocity]; + + if (otherParent != nil) + { + // Subentity + /* TODO: if the subentity is rotating (subentityRotationalVelocity is + not 1 0 0 0) we should calculate the tangential velocity from the + other's position relative to our absolute position and add that in. + */ + v = [otherParent velocity]; + } + else + { + v = [other velocity]; + } + + v = vector_subtract(vel1b, v); + + GLfloat v2b = dot_product(v, loc); // velocity of other along loc before collision + + GLfloat v1a = sqrt(v2b * v2b * m2 / m1); // velocity of self along loc after elastic collision + if (v2b < 0.0f) v1a = -v1a; // in same direction as v2b + + // are they moving apart at over 1m/s already? + if (v2b < 0.0f) + { + if (v2b < -1.0f) return NO; + else + { + position = vector_subtract(position, loc); // adjust self position + v = kZeroVector; // go for the 1m/s solution + } + } + + // convert change in velocity into damage energy (KE) + dam1 = m2 * v2b * v2b / 50000000; + dam2 = m1 * v2b * v2b / 50000000; + + // calculate adjustments to velocity after collision + Vector vel1a = vector_multiply_scalar(loc, -v1a); + Vector vel2a = vector_multiply_scalar(loc, v2b); + + if (magnitude2(v) <= 0.1) // virtually no relative velocity - we must provide at least 1m/s to avoid conjoined objects + { + vel1a = vector_multiply_scalar(loc, -1); + vel2a = loc; + } + + // apply change in velocity + if (otherParent != nil) + { + [otherParent adjustVelocity:vel2a]; // move the otherParent not the subentity + } + else + { + [other adjustVelocity:vel2a]; + } + + [self adjustVelocity:vel1a]; + + BOOL selfDestroyed = (dam1 > energy); + BOOL otherDestroyed = (dam2 > [other energy]) && !otherIsStation; + + if (dam1 > 0.05) + { + [self takeScrapeDamage: dam1 from:other]; + if (selfDestroyed) // inelastic! - take xplosion velocity damage instead + { + vel2a = vector_multiply_scalar(vel2a, -1); + [other adjustVelocity:vel2a]; + } + } + + if (dam2 > 0.05) + { + if (otherParent != nil && ![otherParent isFrangible]) + { + [otherParent takeScrapeDamage: dam2 from:self]; + } + else + { + [other takeScrapeDamage: dam2 from:self]; + } + + if (otherDestroyed) // inelastic! - take explosion velocity damage instead + { + vel2a = vector_multiply_scalar(vel1a, -1); + [self adjustVelocity:vel1a]; + } + } + + if (!selfDestroyed && !otherDestroyed) + { + float t = 10.0 * [UNIVERSE getTimeDelta]; // 10 ticks + + Vector pos1a = vector_add([self position], vector_multiply_scalar(loc, t * v1a)); + [self setPosition:pos1a]; + + if (!otherIsStation) + { + Vector pos2a = vector_add([other position], vector_multiply_scalar(loc, t * v2b)); + [other setPosition:pos2a]; + } + } + + // remove self from other's collision list + [[other collisionArray] removeObject:self]; + + [self doScriptEvent:@"shipCollided" withArgument:other andReactToAIMessage:@"COLLISION"]; + + return YES; +} + + +- (Vector) velocity // overrides Entity velocity +{ + return vector_add(velocity, vector_multiply_scalar(v_forward, flightSpeed)); +} + + +- (void) adjustVelocity:(Vector) xVel +{ + velocity = vector_add(velocity, xVel); +} + + +- (void) addImpactMoment:(Vector) moment fraction:(GLfloat) howmuch +{ + velocity = vector_add(velocity, vector_multiply_scalar(moment, howmuch / mass)); +} + + +- (BOOL) canScoop:(ShipEntity*)other +{ + if (other == nil) return NO; + if (![self hasScoop]) return NO; + if ([cargo count] >= max_cargo) return NO; + if (scanClass == CLASS_CARGO) return NO; // we have no power so we can't scoop + if ([other scanClass] != CLASS_CARGO) return NO; + if ([other cargoType] == CARGO_NOT_CARGO) return NO; + + if ([other isStation]) return NO; + + Vector loc = vector_between(position, [other position]); + + if (dot_product(v_forward, loc) < 0.0f) return NO; // Must be in front of us + if ([self isPlayer] && dot_product(v_up, loc) > 0.0f) return NO; // player has to scoop on underside, give more flexibility to NPCs + + return YES; +} + + +- (void) getTractoredBy:(ShipEntity *)other +{ + desired_speed = 0.0; + [self setAITo:@"nullAI.plist"]; // prevent AI from changing status or behaviour + behaviour = BEHAVIOUR_TRACTORED; + [self setStatus:STATUS_BEING_SCOOPED]; + [self addTarget:other]; + [self setOwner:other]; +} + + +- (void) scoopIn:(ShipEntity *)other +{ + [other getTractoredBy:self]; +} + + +- (void) suppressTargetLost +{ + +} + + +- (void) scoopUp:(ShipEntity *)other +{ + if (other == nil) return; + + OOCargoType co_type; + OOCargoQuantity co_amount; + + switch ([other cargoType]) + { + case CARGO_RANDOM: + co_type = [other commodityType]; + co_amount = [other commodityAmount]; + break; + + case CARGO_SLAVES: + co_amount = 1; + co_type = [UNIVERSE commodityForName:@"Slaves"]; + break; + + case CARGO_ALLOY: + co_amount = 1; + co_type = [UNIVERSE commodityForName:@"Alloys"]; + break; + + case CARGO_MINERALS: + co_amount = 1; + co_type = [UNIVERSE commodityForName:@"Minerals"]; + break; + + case CARGO_THARGOID: + co_amount = 1; + co_type = [UNIVERSE commodityForName:@"Alien Items"]; + break; + + case CARGO_SCRIPTED_ITEM: + { + //scripting + PlayerEntity *player = [PlayerEntity sharedPlayer]; + [player setScriptTarget:self]; + [other doScriptEvent:@"shipWasScooped" withArgument:self]; + [self doScriptEvent:@"shipScoopedOther" withArgument:other]; + + if (isPlayer) + { + NSString* scoopedMS = [NSString stringWithFormat:DESC(@"@-scooped"), [other displayName]]; + [UNIVERSE clearPreviousMessage]; + [UNIVERSE addMessage:scoopedMS forCount:4]; + } + } + + default : + co_amount = 0; + co_type = 0; + break; + } + + /* Bug: docking failed due to NSRangeException while looking for element + NSNotFound of cargo mainfest in -[PlayerEntity unloadCargoPods]. + Analysis: bad cargo pods being generated due to + -[Universe commodityForName:] looking in wrong place for names. + Fix 1: fix -[Universe commodityForName:]. + Fix 2: catch NSNotFound here and substitute random cargo type. + -- Ahruman 20070714 + */ + if (co_type == CARGO_UNDEFINED) + { + co_type = [UNIVERSE getRandomCommodity]; + co_amount = [UNIVERSE getRandomAmountOfCommodity:co_type]; + } + + if (co_amount > 0) + { + [other setCommodity:co_type andAmount:co_amount]; // belt and braces setting this! + cargo_flag = CARGO_FLAG_CANISTERS; + + if (isPlayer) + { + [UNIVERSE clearPreviousMessage]; + if ([other crew]) + { + unsigned i; + for (i = 0; i < [[other crew] count]; i++) + { + OOCharacter *rescuee = [[other crew] objectAtIndex:i]; + if ([rescuee legalStatus]) + { + [UNIVERSE addMessage: [NSString stringWithFormat:DESC(@"scoop-captured-@"), [rescuee name]] forCount: 4.5]; + } + else if ([rescuee insuranceCredits]) + { + [UNIVERSE addMessage: [NSString stringWithFormat:DESC(@"scoop-rescued-@"), [rescuee name]] forCount: 4.5]; + } + else + { + [UNIVERSE addMessage: DESC(@"scoop-got-slave") forCount: 4.5]; + } + [(PlayerEntity *)self playEscapePodScooped]; + } + } + else + { + [UNIVERSE addMessage:[UNIVERSE describeCommodity:co_type amount:co_amount] forCount:4.5]; + } + } + [cargo insertObject: other atIndex: 0]; // places most recently scooped object at eject position + [other setStatus:STATUS_IN_HOLD]; + [other setBehaviour:BEHAVIOUR_TUMBLE]; + [shipAI message:@"CARGO_SCOOPED"]; + if (max_cargo && [cargo count] == max_cargo) [shipAI message:@"HOLD_FULL"]; + } + [[other collisionArray] removeObject:self]; // so it can't be scooped twice! + [self suppressTargetLost]; + [UNIVERSE removeEntity:other]; +} + + +- (void) takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other +{ + if ([self status] == STATUS_DEAD) return; + if (amount <= 0.0) return; + + // If it's an energy mine... + if (ent && ent->isParticle && ent->scanClass == CLASS_MINE) + { + switch (scanClass) + { + case CLASS_WORMHOLE : + case CLASS_ROCK : + case CLASS_CARGO : + case CLASS_BUOY : + // does not normally cascade + if ((fuel > MIN_FUEL) || isStation) + { + //we have fuel onboard so we can still go pop, or we are a station which can + } + else break; + case CLASS_STATION : + case CLASS_MINE : + case CLASS_PLAYER : + case CLASS_POLICE : + case CLASS_MILITARY : + case CLASS_THARGOID : + case CLASS_MISSILE : + case CLASS_NOT_SET : + case CLASS_NO_DRAW : + case CLASS_NEUTRAL : + case CLASS_TARGET : + // ...start a chain reaction, if we're dying and have a non-trivial amount of energy. + if (energy < amount && energy > 10) + { + ParticleEntity *chainReaction = [[ParticleEntity alloc] initEnergyMineFromShip:self]; + [UNIVERSE addEntity:chainReaction]; + [chainReaction setOwner:[ent owner]]; + [chainReaction release]; + } + break; + //no default thanks, we want the compiler to tell us if we missed a case. + } + } + + energy -= amount; + being_mined = NO; + ShipEntity *hunter = nil; + + if ([other isShip] && [self owner] != other) // our owner could be the same entity as the one responsible for our taking damage in the case of submunitions + { + hunter = (ShipEntity *)other; + if ([hunter isCloaked]) + { + [self doScriptEvent:@"shipBeingAttackedByCloaked" andReactToAIMessage:@"ATTACKED_BY_CLOAKED"]; + + // lose it! + other = nil; + hunter = nil; + } + } + + // if the other entity is a ship note it as an aggressor + if (hunter != nil) + { + BOOL iAmTheLaw = [self isPolice]; + BOOL uAreTheLaw = [hunter isPolice]; + + last_escort_target = NO_TARGET; // we're being attacked, escorts can scramble! + + primaryAggressor = [hunter universalID]; + found_target = primaryAggressor; + + // firing on an innocent ship is an offence + [self broadcastHitByLaserFrom: hunter]; + + // tell ourselves we've been attacked + if (energy > 0) + { + [self respondToAttackFrom:ent becauseOf:other]; + } + + // tell our group we've been attacked + OOShipGroup *group = [self group]; + if (group != nil && group != [hunter group] && !(iAmTheLaw || uAreTheLaw)) + { + if ([self isTrader] || [self isEscort]) + { + ShipEntity *groupLeader = [group leader]; + [groupLeader setFound_target:hunter]; + [groupLeader setPrimaryAggressor:hunter]; + [groupLeader respondToAttackFrom:ent becauseOf:hunter]; + //unsetting group leader for carriers can break stuff + } + if ([self isPirate]) + { + NSEnumerator *groupEnum = nil; + ShipEntity *otherPirate = nil; + + for (groupEnum = [group objectEnumerator]; (otherPirate = [groupEnum nextObject]); ) + { + if (randf() < 0.5) // 50% chance they'll help + { + [otherPirate setFound_target:hunter]; + [otherPirate setPrimaryAggressor:hunter]; + [otherPirate respondToAttackFrom:ent becauseOf:hunter]; + } + } + } + else if (iAmTheLaw) + { + NSEnumerator *groupEnum = nil; + ShipEntity *otherPolice = nil; + + for (groupEnum = [group objectEnumerator]; (otherPolice = [groupEnum nextObject]); ) + { + [otherPolice setFound_target:hunter]; + [otherPolice setPrimaryAggressor:hunter]; + [otherPolice respondToAttackFrom:ent becauseOf:hunter]; + } + } + } + + // if I'm a copper and you're not, then mark the other as an offender! + if (iAmTheLaw && !uAreTheLaw) + { + [hunter markAsOffender:64]; + } + + if ([hunter group] == group || (iAmTheLaw && uAreTheLaw)) + { + // avoid shooting each other + if ([hunter behaviour] == BEHAVIOUR_ATTACK_FLY_TO_TARGET) // avoid me please! + { + [hunter setBehaviour:BEHAVIOUR_ATTACK_FLY_FROM_TARGET]; + [hunter setDesiredSpeed:[hunter maxFlightSpeed]]; + } + } + + if ((other)&&(other->isShip)) + being_mined = [(ShipEntity *)other isMining]; + } + // die if I'm out of energy + if (energy <= 0.0) + { + if (hunter != nil) [hunter noteTargetDestroyed:self]; + [self getDestroyedBy:other context:suppressExplosion ? @"energy damage" : @"removed"]; + } + else + { + // warn if I'm low on energy + if (energy < maxEnergy * 0.25) + { + [self doScriptEvent:@"shipEnergyIsLow" andReactToAIMessage:@"ENERGY_LOW"]; + } + if (energy < maxEnergy *0.125 && [self hasEscapePod] && (ranrot_rand() & 3) == 0) // 25% chance he gets to an escape pod + { + // TODO: abandoning ship should be split out into a separate method. + if ([self launchEscapeCapsule] != NO_TARGET) + { + [self removeEquipmentItem:@"EQ_ESCAPE_POD"]; + + [shipAI setStateMachine:@"nullAI.plist"]; + [shipAI setState:@"GLOBAL"]; + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; + [self setScanClass: CLASS_CARGO]; // we're unmanned now! + thrust = thrust * 0.5; + desired_speed = 0.0; + // maxFlightSpeed = 0.0; + [self setHulk:YES]; + } + } + } +} + + +- (void) takeScrapeDamage:(double) amount from:(Entity *) ent +{ + if ([self status] == STATUS_DEAD) return; + + if ([self status] == STATUS_LAUNCHING) // no collisions during launches please + return; + if ([ent status] == STATUS_LAUNCHING) // no collisions during launches please + return; + + energy -= amount; + // oops we hit too hard!!! + if (energy <= 0.0) + { + being_mined = YES; // same as using a mining laser + if ([ent isShip]) + { + [(ShipEntity *)ent noteTargetDestroyed:self]; + } + [self getDestroyedBy:ent context:@"scrape damage"]; + } + else + { + // warn if I'm low on energy + if (energy < maxEnergy * 0.25) + { + [self doScriptEvent:@"shipEnergyIsLow" andReactToAIMessage:@"ENERGY_LOW"]; + } + } +} + + +- (void) takeHeatDamage:(double) amount +{ + if ([self status] == STATUS_DEAD) // it's too late for this one! + return; + + if (amount < 0.0) + return; + + energy -= amount; + + throw_sparks = YES; + + // oops we're burning up! + if (energy <= 0.0) + [self getDestroyedBy:nil context:@"heat damage"]; + else + { + // warn if I'm low on energy + if (energy < maxEnergy * 0.25) + { + [self doScriptEvent:@"shipEnergyIsLow" andReactToAIMessage:@"ENERGY_LOW"]; + } + } +} + + +- (void) enterDock:(StationEntity *)station +{ + // throw these away now we're docked... + if (dockingInstructions != nil) + { + [dockingInstructions autorelease]; + dockingInstructions = nil; + } + + [self doScriptEvent:@"shipWillDockWithStation" withArgument:station]; + [self doScriptEvent:@"shipDockedWithStation" withArgument:station]; + [shipAI message:@"DOCKED"]; + [station noteDockedShip:self]; + [UNIVERSE removeEntity:self]; +} + + +- (void) leaveDock:(StationEntity *)station +{ + if (station == nil) return; + + Vector stat_f = vector_forward_from_quaternion([station orientation]); + [self setPosition:vector_add([station position], vector_multiply_scalar(stat_f, 500.0f))]; + + [self setOrientation:[station orientation]]; + flightRoll = [station flightRoll]; + flightPitch = 0.0; + flightSpeed = maxFlightSpeed * 0.5; + + [self setStatus:STATUS_LAUNCHING]; + + [self doScriptEvent:@"shipWillLaunchFromStation" withArgument:station]; + [shipAI message:@"LAUNCHED"]; + [UNIVERSE addEntity:self]; +} + + +- (void) enterWormhole:(WormholeEntity *) w_hole +{ + [self enterWormhole:w_hole replacing:YES]; +} + + +- (void) enterWormhole:(WormholeEntity *) w_hole replacing:(BOOL)replacing +{ + if (replacing && ![[UNIVERSE sun] willGoNova] && [UNIVERSE sun] != nil) + { + /* Add a new ship to maintain quantities of standard ships, unless + there's a nova in the works, the AI asked us not to, or we're in + interstellar space. + */ + [UNIVERSE witchspaceShipWithPrimaryRole:[self primaryRole]]; + } + + [w_hole suckInShip: self]; // removes ship from universe +} + + +- (void) enterWitchspace +{ + // witchspace entry effects here + ParticleEntity *ring1 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained + [UNIVERSE addEntity:ring1]; + [ring1 release]; + ParticleEntity *ring2 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained + [ring2 setSize:NSMakeSize([ring2 size].width * -2.5 ,[ring2 size].height * -2.0 )]; // shrinking! + [UNIVERSE addEntity:ring2]; + [ring2 release]; + + [shipAI message:@"ENTERED_WITCHSPACE"]; + + if (![[UNIVERSE sun] willGoNova]) // if the sun's not going nova + [UNIVERSE witchspaceShipWithPrimaryRole:[self primaryRole]]; // then add a new ship like this one leaving! + + [UNIVERSE removeEntity:self]; +} + +int w_space_seed = 1234567; +- (void) leaveWitchspace +{ + Vector pos = [UNIVERSE getWitchspaceExitPosition]; + Quaternion q_rtn = [UNIVERSE getWitchspaceExitRotation]; + + // try to ensure healthy random numbers + // + ranrot_srand(w_space_seed); + w_space_seed = ranrot_rand(); + + position = pos; + double d1 = SCANNER_MAX_RANGE * (randf() - randf()); + if (abs(d1) < 500.0) // no closer than 500m + d1 += ((d1 > 0.0)? 500.0: -500.0); + Quaternion q1 = q_rtn; + quaternion_set_random(&q1); + Vector v1 = vector_forward_from_quaternion(q1); + position.x += v1.x * d1; // randomise exit position + position.y += v1.y * d1; + position.z += v1.z * d1; + orientation = q_rtn; + flightRoll = 0.0; + flightPitch = 0.0; + flightSpeed = maxFlightSpeed * 0.25; + [self setStatus:STATUS_LAUNCHING]; + [shipAI message:@"EXITED_WITCHSPACE"]; + [UNIVERSE addEntity:self]; + + // witchspace exit effects here + ParticleEntity *ring1 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained + [UNIVERSE addEntity:ring1]; + [ring1 release]; + ParticleEntity *ring2 = [[ParticleEntity alloc] initHyperringFromShip:self]; // retained + [ring2 setSize:NSMakeSize([ring2 size].width * -2.5 ,[ring2 size].height * -2.0 )]; // shrinking! + [UNIVERSE addEntity:ring2]; + [ring2 release]; +} + + +- (void) markAsOffender:(int)offence_value +{ + if (scanClass != CLASS_POLICE) bounty |= offence_value; +} + + +// Exposed to AI +- (void) switchLightsOn +{ + NSEnumerator *subEnum = nil; + ParticleEntity *se = nil; + ShipEntity *sub = nil; + + for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); ) + { + [se setStatus:STATUS_EFFECT]; + } + for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); ) + { + [sub switchLightsOn]; + } +} + +// Exposed to AI +- (void) switchLightsOff +{ + NSEnumerator *subEnum = nil; + ParticleEntity *se = nil; + ShipEntity *sub = nil; + + for (subEnum = [self flasherEnumerator]; (se = [subEnum nextObject]); ) + { + [se setStatus:STATUS_INACTIVE]; + } + for (subEnum = [self shipSubEntityEnumerator]; (sub = [subEnum nextObject]); ) + { + [sub switchLightsOff]; + } +} + + +- (void) setDestination:(Vector) dest +{ + destination = dest; + frustration = 0.0; // new destination => no frustration! +} + + +- (void) setEscortDestination:(Vector) dest +{ + destination = dest; // don't reset frustration for escorts. +} + + +- (BOOL) canAcceptEscort:(ShipEntity *)potentialEscort +{ + //this condition has to be checked first! + if (![self isEscort] && ([self hasRole:@"police"] || [self hasRole:@"interceptor"])) + { + return [potentialEscort hasRole:@"wingman"]; + } + if ([self bounty] == 0 && [potentialEscort bounty] != 0) // clean mothers can only accept clean escorts + { + return NO; + } + if (![self isEscort]) + { + return [potentialEscort hasRole:@"escort"]; + } + return NO; +} + + +- (BOOL) acceptAsEscort:(ShipEntity *) other_ship +{ + // can't pair with self + if (self == other_ship) return NO; + + //increased stack depth at which it can accept escorts to avoid rejections at this stage. + //doesn't seem to have any adverse effect for now. - Kaks. + if ([shipAI stackDepth] > 3) + { + OOLog(@"ship.escort.reject", @"%@ rejecting escort %@ because AI stack depth is %u.",self, other_ship, [shipAI stackDepth]); + return NO; + } + + if ([self canAcceptEscort:other_ship]) + { + OOShipGroup *escortGroup = [self escortGroup]; + + if ([escortGroup containsShip:other_ship]) return YES; + + // check total number acceptable + unsigned maxEscorts = _maxEscortCount; + unsigned escortCount = [escortGroup count] - 1; + + //however the system's patrols don't have escorts inside their dictionary + if (maxEscorts == 0 && ([self hasRole:@"police"] || [self hasRole:@"interceptor"] || [self hasRole:@"hunter"])) + { + maxEscorts = MAX_ESCORTS; + } + else if (maxEscorts > MAX_ESCORTS) + { + maxEscorts = MAX_ESCORTS; + } + + if (escortCount < maxEscorts) + { + [escortGroup addShip:other_ship]; + [other_ship setGroup:escortGroup]; + + if(([other_ship maxFlightSpeed] < cruiseSpeed) && ([other_ship maxFlightSpeed] > cruiseSpeed * 0.3)) + cruiseSpeed = [other_ship maxFlightSpeed] * 0.99; + + OOLog(@"ship.escort.accept", @"Accepting existing escort %@.", other_ship); + + [self doScriptEvent:@"shipAcceptedEscort" withArgument:other_ship]; + [other_ship doScriptEvent:@"escortAccepted" withArgument:self]; + return YES; + } + else if (maxEscorts > 0) + { + OOLog(@"ship.escort.reject", @" %@ already got max escorts(%d). Escort rejected: %@.",self, escortCount, other_ship); + } + } + return NO; +} + + +- (Vector) coordinatesForEscortPosition:(int) f_pos +{ + int f_hi = 1 + (f_pos >> 2); + int f_lo = f_pos & 3; + + int fp = f_lo * 3; + int escort_positions[12] = { -2,0,-1, 2,0,-1, -3,0,-3, 3,0,-3 }; // V-shape escort pattern + // int escort_positions[12] = { -2,0,+2, 2,0,+2, -3,0,-3, 3,0,-3 }; // X-shape escort pattern + Vector pos = position; + double spacing = collision_radius * ESCORT_SPACING_FACTOR; + double xx = f_hi * spacing * escort_positions[fp++]; + double yy = f_hi * spacing * escort_positions[fp++]; + double zz = f_hi * spacing * escort_positions[fp]; + pos.x += v_right.x * xx; pos.y += v_right.y * xx; pos.z += v_right.z * xx; + pos.x += v_up.x * yy; pos.y += v_up.y * yy; pos.z += v_up.z * yy; + pos.x += v_forward.x * zz; pos.y += v_forward.y * zz; pos.z += v_forward.z * zz; + + return pos; +} + + +// Exposed to AI +- (void) deployEscorts +{ + NSEnumerator *escortEnum = nil; + ShipEntity *escort = nil; + ShipEntity *target = nil; + NSMutableSet *idleEscorts = nil; + unsigned deployCount; + + if ([self primaryTarget] == nil || _escortGroup == nil) return; + + OOShipGroup *escortGroup = [self escortGroup]; + unsigned escortCount = [escortGroup count] - 1; // escorts minus leader. + if (escortCount == 0) return; + + if ([self group] == nil) [self setGroup:escortGroup]; + + if (primaryTarget == last_escort_target) + { + // already deployed escorts onto this target! + return; + } + + last_escort_target = primaryTarget; + + // Find idle escorts + idleEscorts = [NSMutableSet set]; + for (escortEnum = [self escortEnumerator]; (escort = [escortEnum nextObject]); ) + { + if (![[[escort getAI] name] isEqualToString:@"interceptAI.plist"]) + { + [idleEscorts addObject:escort]; + } + } + + escortCount = [idleEscorts count]; + if (escortCount == 0) return; + + deployCount = ranrot_rand() % escortCount + 1; + + // Deply deployCount idle escorts. + target = [self primaryTarget]; + for (escortEnum = [idleEscorts objectEnumerator]; (escort = [escortEnum nextObject]); ) + { + [escort addTarget:target]; + [escort setStateMachine:@"interceptAI.plist"]; + [escort doScriptEvent:@"escortAttack" withArgument:target]; + + if (--deployCount == 0) break; + } +} + + +// Exposed to AI +- (void) dockEscorts +{ + if (![self hasEscorts]) return; + + OOShipGroup *escortGroup = [self escortGroup]; + NSEnumerator *escortEnum = nil; + ShipEntity *escort = nil; + unsigned i = 0; + // Note: works on escortArray rather than escortEnumerator because escorts may be mutated. + for (escortEnum = [[self escortArray] objectEnumerator]; (escort = [escortEnum nextObject]); ) + { + float delay = i++ * 3.0 + 1.5; // send them off at three second intervals + AI *ai = [escort getAI]; + + // act individually now! + if ([escort group] == escortGroup) [escort setGroup:nil]; + if ([escort owner] == self) [escort setOwner:nil]; + + [ai setStateMachine:@"dockingAI.plist" afterDelay:delay]; + [ai setState:@"ABORT" afterDelay:delay + 0.25]; + [escort doScriptEvent:@"escortDock" withArgument:[NSNumber numberWithFloat:delay]]; + } + + // We now have no escorts. + [_escortGroup release]; + _escortGroup = nil; +} + + +// Exosed to AI +- (void) setTargetToNearestStation +{ + // check if the groupID (parent ship) points to a station... + Entity* mother = [[self group] leader]; + if ([mother isStation]) + { + primaryTarget = [mother universalID]; + targetStation = primaryTarget; + return; // head for mother! + } + + /*- selects the nearest station it can find -*/ + if (!UNIVERSE) + return; + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + Entity* my_entities[ent_count]; + int i; + int station_count = 0; + for (i = 0; i < ent_count; i++) + if (uni_entities[i]->isStation) + my_entities[station_count++] = [uni_entities[i] retain]; // retained + // + StationEntity* station = nil; + double nearest2 = SCANNER_MAX_RANGE2 * 1000000.0; // 1000x scanner range (25600 km), squared. + for (i = 0; i < station_count; i++) + { + StationEntity* thing = (StationEntity*)my_entities[i]; + double range2 = distance2(position, thing->position); + if (range2 < nearest2) + { + station = (StationEntity *)thing; + nearest2 = range2; + } + } + for (i = 0; i < station_count; i++) + [my_entities[i] release]; // released + // + if (station) + { + primaryTarget = [station universalID]; + targetStation = primaryTarget; + } + else + { + [shipAI message:@"NO_STATION_FOUND"]; + } +} + + +// Exosed to AI +- (void) setTargetToSystemStation +{ + StationEntity* system_station = [UNIVERSE station]; + + if (!system_station) + { + [shipAI message:@"NOTHING_FOUND"]; + [shipAI message:@"NO_STATION_FOUND"]; + primaryTarget = NO_TARGET; + targetStation = NO_TARGET; + return; + } + + if (!system_station->isStation) + { + [shipAI message:@"NOTHING_FOUND"]; + [shipAI message:@"NO_STATION_FOUND"]; + primaryTarget = NO_TARGET; + targetStation = NO_TARGET; + return; + } + + primaryTarget = [system_station universalID]; + targetStation = primaryTarget; + return; +} + + +- (void) landOnPlanet:(PlanetEntity *)planet +{ + if (planet) + { + [planet welcomeShuttle:self]; // 10km from the surface + } + [shipAI message:@"LANDED_ON_PLANET"]; + +#ifndef NDEBUG + if ([self reportAIMessages]) + { + OOLog(@"planet.collide.shuttleLanded", @"DEBUG: %@ landed on planet %@", self, planet); + } +#endif + + [UNIVERSE removeEntity:self]; +} + + +// Exposed to AI +- (void) abortDocking +{ + [[UNIVERSE findEntitiesMatchingPredicate:IsStationPredicate + parameter:nil + inRange:-1 + ofEntity:nil] + makeObjectsPerformSelector:@selector(abortDockingForShip:) withObject:self]; +} + + +- (void) broadcastThargoidDestroyed +{ + [[UNIVERSE findShipsMatchingPredicate:HasRolePredicate + parameter:@"tharglet" + inRange:SCANNER_MAX_RANGE2 + ofEntity:self] + makeObjectsPerformSelector:@selector(sendAIMessage:) withObject:@"THARGOID_DESTROYED"]; +} + + +static BOOL AuthorityPredicate(Entity *entity, void *parameter) +{ + ShipEntity *victim = parameter; + + // Select main station, if victim is in aegis + if (entity == [UNIVERSE station] && [victim withinStationAegis]) + { + return YES; + } + + // Select police units in scanner range + if ([entity scanClass] == CLASS_POLICE && + distance2([victim position], [entity position]) < SCANNER_MAX_RANGE2) + { + return YES; + } + + // Reject others + return NO; +} + + +- (void) broadcastHitByLaserFrom:(ShipEntity *) aggressor_ship +{ + /*-- If you're clean, locates all police and stations in range and tells them OFFENCE_COMMITTED --*/ + if (!UNIVERSE) return; + if ([self bounty]) return; + if (!aggressor_ship) return; + + if ( (scanClass == CLASS_NEUTRAL)|| + (scanClass == CLASS_STATION)|| + (scanClass == CLASS_BUOY)|| + (scanClass == CLASS_POLICE)|| + (scanClass == CLASS_MILITARY)|| + (scanClass == CLASS_PLAYER)) // only for active ships... + { + NSArray *authorities = nil; + NSEnumerator *authEnum = nil; + ShipEntity *auth = nil; + + authorities = [UNIVERSE findShipsMatchingPredicate:AuthorityPredicate + parameter:self + inRange:-1 + ofEntity:nil]; + authEnum = [authorities objectEnumerator]; + while ((auth = [authEnum nextObject])) + { + [auth setFound_target:aggressor_ship]; + [auth doScriptEvent:@"offenceCommittedNearby" withArgument:aggressor_ship andArgument:self]; + [auth reactToAIMessage:@"OFFENCE_COMMITTED"]; + } + } +} + + +- (void) sendExpandedMessage:(NSString *) message_text toShip:(ShipEntity*) other_ship +{ + if (!other_ship) + return; + if (!crew) + return; // nobody to send the signal + if ((lastRadioMessage) && (messageTime > 0.0) && [message_text isEqual:lastRadioMessage]) + return; // don't send the same message too often + [lastRadioMessage autorelease]; + lastRadioMessage = [message_text retain]; + Vector delta = other_ship->position; + delta.x -= position.x; delta.y -= position.y; delta.z -= position.z; + double d2 = delta.x*delta.x + delta.y*delta.y + delta.z*delta.z; + if (d2 > scannerRange * scannerRange) + return; // out of comms range + if (!other_ship) + return; + NSMutableString *localExpandedMessage = [NSMutableString stringWithString:message_text]; + [localExpandedMessage replaceOccurrencesOfString:@"[self:name]" + withString:[self displayName] + options:NSLiteralSearch range:NSMakeRange(0, [localExpandedMessage length])]; + [localExpandedMessage replaceOccurrencesOfString:@"[target:name]" + withString:[other_ship identFromShip: self] + options:NSLiteralSearch range:NSMakeRange(0, [localExpandedMessage length])]; + Random_Seed very_random_seed; + very_random_seed.a = rand() & 255; + very_random_seed.b = rand() & 255; + very_random_seed.c = rand() & 255; + very_random_seed.d = rand() & 255; + very_random_seed.e = rand() & 255; + very_random_seed.f = rand() & 255; + seed_RNG_only_for_planet_description(very_random_seed); + NSString* expandedMessage = ExpandDescriptionForCurrentSystem(localExpandedMessage); + [self setCommsMessageColor]; + [other_ship receiveCommsMessage:[NSString stringWithFormat:@"%@:\n %@", displayName, expandedMessage]]; + if (other_ship->isPlayer) + messageTime = 6.0; + [UNIVERSE resetCommsLogColor]; +} + + +- (void) broadcastAIMessage:(NSString *) ai_message +{ + NSString* expandedMessage = ExpandDescriptionForCurrentSystem(ai_message); + + [self checkScanner]; + unsigned i; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity* ship = scanned_ships[i]; + [[ship getAI] message: expandedMessage]; + } +} + + +- (void) broadcastMessage:(NSString *) message_text withUnpilotedOverride:(BOOL) unpilotedOverride +{ + NSString* expandedMessage = [NSString stringWithFormat:@"%@:\n %@", displayName, ExpandDescriptionForCurrentSystem(message_text)]; + + if (!crew && !unpilotedOverride) + return; // nobody to send the signal and no override for unpiloted craft is set + + [self setCommsMessageColor]; + [self checkScanner]; + unsigned i; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity* ship = scanned_ships[i]; + [ship receiveCommsMessage: expandedMessage]; + if ([ship isPlayer]) + messageTime = 6.0; + } + [UNIVERSE resetCommsLogColor]; +} + + +- (void) setCommsMessageColor +{ + float hue = 0.0625 * (universalID & 15); + [[UNIVERSE comm_log_gui] setTextColor:[OOColor colorWithCalibratedHue:hue saturation:0.375 brightness:1.0 alpha:1.0]]; + if (scanClass == CLASS_THARGOID) + [[UNIVERSE comm_log_gui] setTextColor:[OOColor greenColor]]; + if (scanClass == CLASS_POLICE) + [[UNIVERSE comm_log_gui] setTextColor:[OOColor cyanColor]]; +} + + +- (void) receiveCommsMessage:(NSString *) message_text +{ + // ignore messages for now +} + + +- (void) commsMessage:(NSString *)valueString withUnpilotedOverride:(BOOL)unpilotedOverride +{ + Random_Seed very_random_seed; + very_random_seed.a = rand() & 255; + very_random_seed.b = rand() & 255; + very_random_seed.c = rand() & 255; + very_random_seed.d = rand() & 255; + very_random_seed.e = rand() & 255; + very_random_seed.f = rand() & 255; + seed_RNG_only_for_planet_description(very_random_seed); + + [self broadcastMessage:valueString withUnpilotedOverride:unpilotedOverride]; +} + + +- (BOOL) markForFines +{ + if (being_fined) + return NO; // can't mark twice + being_fined = ([self legalStatus] > 0); + return being_fined; +} + + +- (BOOL) isMining +{ + return ((behaviour == BEHAVIOUR_ATTACK_MINING_TARGET)&&(forward_weapon_type == WEAPON_MINING_LASER)); +} + + +- (void) interpretAIMessage:(NSString *)ms +{ + if ([ms hasPrefix:AIMS_AGGRESSOR_SWITCHED_TARGET]) + { + // if I'm under attack send a thank-you message to the rescuer + // + NSArray* tokens = ScanTokensFromString(ms); + int switcher_id = [(NSString*)[tokens objectAtIndex:1] intValue]; + Entity* switcher = [UNIVERSE entityForUniversalID:switcher_id]; + int rescuer_id = [(NSString*)[tokens objectAtIndex:2] intValue]; + Entity* rescuer = [UNIVERSE entityForUniversalID:rescuer_id]; + if ((switcher_id == primaryAggressor)&&(switcher_id == primaryTarget)&&(switcher)&&(rescuer)&&(rescuer->isShip)&&(thanked_ship_id != rescuer_id)&&(scanClass != CLASS_THARGOID)) + { + if (scanClass == CLASS_POLICE) + [self sendExpandedMessage:@"[police-thanks-for-assist]" toShip:(ShipEntity*)rescuer]; + else + [self sendExpandedMessage:@"[thanks-for-assist]" toShip:(ShipEntity*)rescuer]; + thanked_ship_id = rescuer_id; + [(ShipEntity*)switcher setBounty:[(ShipEntity*)switcher bounty] + 5 + (ranrot_rand() & 15)]; // reward + } + } +} + + +- (BoundingBox) findBoundingBoxRelativeTo:(Entity *)other InVectors:(Vector) _i :(Vector) _j :(Vector) _k +{ + Vector opv = other ? other->position : position; + return [self findBoundingBoxRelativeToPosition:opv InVectors:_i :_j :_k]; +} + + +// Exposed to AI and legacy scripts. +- (void) spawn:(NSString *)roles_number +{ + NSArray *tokens = ScanTokensFromString(roles_number); + NSString *roleString = nil; + NSString *numberString = nil; + OOUInteger number; + + if ([tokens count] != 2) + { + OOLog(kOOLogSyntaxAddShips, @"***** Could not spawn: '%@' (must be two tokens, role and number)",roles_number); + return; + } + + roleString = [tokens stringAtIndex:0]; + numberString = [tokens stringAtIndex:1]; + + number = [numberString intValue]; + + [self spawnShipsWithRole:roleString count:number]; +} + + +- (int) checkShipsInVicinityForWitchJumpExit +{ + // checks if there are any large masses close by + // since we want to place the space station at least 10km away + // the formula we'll use is K x m / d2 < 1.0 + // (m = mass, d2 = distance squared) + // coriolis station is mass 455,223,200 + // 10km is 10,000m, + // 10km squared is 100,000,000 + // therefore K is 0.22 (approx) + + int result = NO_TARGET; + + GLfloat k = 0.1; + + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + ShipEntity* my_entities[ent_count]; + int i; + + int ship_count = 0; + for (i = 0; i < ent_count; i++) + if ((uni_entities[i]->isShip)&&(uni_entities[i] != self)) + my_entities[ship_count++] = (ShipEntity*)[uni_entities[i] retain]; // retained + // + for (i = 0; (i < ship_count)&&(result == NO_TARGET) ; i++) + { + ShipEntity* ship = my_entities[i]; + Vector delta = vector_between(position, ship->position); + GLfloat d2 = magnitude2(delta); + if ((k * [ship mass] > d2)&&(d2 < SCANNER_MAX_RANGE2)) // if you go off scanner from a blocker - it ceases to block + result = [ship universalID]; + } + for (i = 0; i < ship_count; i++) + [my_entities[i] release]; // released + + return result; +} + + +- (BOOL) trackCloseContacts +{ + return trackCloseContacts; +} + + +- (void) setTrackCloseContacts:(BOOL) value +{ + if (value == trackCloseContacts) return; + + trackCloseContacts = value; + [closeContactsInfo release]; + + if (trackCloseContacts) + { + closeContactsInfo = [[NSMutableDictionary alloc] init]; + } + else + { + closeContactsInfo = nil; + } +} + + +- (void) claimAsSalvage +{ + // Create a bouy and beacon where the hulk is. + // Get the main GalCop station to launch a pilot boat to deliver a pilot to the hulk. + OOLog(@"claimAsSalvage.called", @"claimAsSalvage called on %@ %@", [self name], [self roleSet]); + + // Not an abandoned hulk, so don't allow the salvage + if (![self isHulk]) + { + OOLog(@"claimAsSalvage.failed.notHulk", @"claimAsSalvage failed because not a hulk"); + return; + } + + // Set target to main station, and return now if it can't be found + [self setTargetToSystemStation]; + if (primaryTarget == NO_TARGET) + { + OOLog(@"claimAsSalvage.failed.noStation", @"claimAsSalvage failed because did not find a station"); + return; + } + + // Get the station to launch a pilot boat to bring a pilot out to the hulk (use a viper for now) + StationEntity *station = (StationEntity *)[UNIVERSE entityForUniversalID:primaryTarget]; + OOLog(@"claimAsSalvage.requestingPilot", @"claimAsSalvage asking station to launch a pilot boat"); + [station launchShipWithRole:@"pilot"]; + [self setReportAIMessages:YES]; + OOLog(@"claimAsSalvage.success", @"claimAsSalvage setting own state machine to capturedShipAI.plist"); + [self setStateMachine:@"capturedShipAI.plist"]; +} + + +- (void) sendCoordinatesToPilot +{ + Entity *scan; + ShipEntity *scanShip, *pilot; + + n_scanned_ships = 0; + scan = z_previous; + OOLog(@"ship.pilotage", @"searching for pilot boat"); + while (scan &&(scan->isShip == NO)) + { + scan = scan->z_previous; // skip non-ships + } + + pilot = nil; + while (scan) + { + if (scan->isShip) + { + scanShip = (ShipEntity *)scan; + + if ([self hasRole:@"pilot"] == YES) + { + if ([scanShip primaryTargetID] == NO_TARGET) + { + OOLog(@"ship.pilotage", @"found pilot boat with no target, will use this one"); + pilot = scanShip; + [pilot setPrimaryRole:@"pilot"]; + break; + } + } + } + scan = scan->z_previous; + while (scan && (scan->isShip == NO)) + { + scan = scan->z_previous; + } + } + + if (pilot != nil) + { + OOLog(@"ship.pilotage", @"becoming pilot target and setting AI"); + [pilot setReportAIMessages:YES]; + [pilot addTarget:self]; + [pilot setStateMachine:@"pilotAI.plist"]; + [self reactToAIMessage:@"FOUND_PILOT"]; + } +} + + +- (void) pilotArrived +{ + [self setHulk:NO]; + [self reactToAIMessage:@"PILOT_ARRIVED"]; +} + + +#ifndef NDEBUG +- (void)dumpSelfState +{ + NSMutableArray *flags = nil; + NSString *flagsString = nil; + + [super dumpSelfState]; + + OOLog(@"dumpState.shipEntity", @"Name: %@", name); + OOLog(@"dumpState.shipEntity", @"Display Name: %@", displayName); + OOLog(@"dumpState.shipEntity", @"Roles: %@", [self roleSet]); + OOLog(@"dumpState.shipEntity", @"Primary role: %@", primaryRole); + OOLog(@"dumpState.shipEntity", @"Script: %@", script); + OOLog(@"dumpState.shipEntity", @"Subentity count: %u", [self subEntityCount]); + OOLog(@"dumpState.shipEntity", @"Behaviour: %@", BehaviourToString(behaviour)); + if (primaryTarget != NO_TARGET) OOLog(@"dumpState.shipEntity", @"Target: %@", [self primaryTarget]); + OOLog(@"dumpState.shipEntity", @"Destination: %@", VectorDescription(destination)); + OOLog(@"dumpState.shipEntity", @"Other destination: %@", VectorDescription(coordinates)); + OOLog(@"dumpState.shipEntity", @"Waypoint count: %u", number_of_navpoints); + OOLog(@"dumpState.shipEntity", @"Desired speed: %g", desired_speed); + if ([self escortCount] != 0) OOLog(@"dumpState.shipEntity", @"Escort count: %u", [self escortCount]); + OOLog(@"dumpState.shipEntity", @"Fuel: %i", fuel); + OOLog(@"dumpState.shipEntity", @"Fuel accumulator: %g", fuel_accumulator); + OOLog(@"dumpState.shipEntity", @"Missile count: %u", missiles); + +#ifdef OO_BRAIN_AI + if (brain != nil && OOLogWillDisplayMessagesInClass(@"dumpState.shipEntity.brain")) + { + OOLog(@"dumpState.shipEntity.brain", @"Brain:"); + OOLogPushIndent(); + OOLogIndent(); + NS_DURING + [brain dumpState]; + NS_HANDLER + NS_ENDHANDLER + OOLogPopIndent(); + } +#endif + + if (shipAI != nil && OOLogWillDisplayMessagesInClass(@"dumpState.shipEntity.ai")) + { + OOLog(@"dumpState.shipEntity.ai", @"AI:"); + OOLogPushIndent(); + OOLogIndent(); + NS_DURING + [shipAI dumpState]; + NS_HANDLER + NS_ENDHANDLER + OOLogPopIndent(); + } + OOLog(@"dumpState.shipEntity", @"Frustration: %g", frustration); + OOLog(@"dumpState.shipEntity", @"Success factor: %g", success_factor); + OOLog(@"dumpState.shipEntity", @"Shots fired: %u", shot_counter); + OOLog(@"dumpState.shipEntity", @"Time since shot: %g", [self shotTime]); + OOLog(@"dumpState.shipEntity", @"Spawn time: %g (%g seconds ago)", [self spawnTime], [self timeElapsedSinceSpawn]); + if (beaconChar != '\0') + { + OOLog(@"dumpState.shipEntity", @"Beacon character: '%c'", beaconChar); + } + OOLog(@"dumpState.shipEntity", @"Hull temperature: %g", ship_temperature); + OOLog(@"dumpState.shipEntity", @"Heat insulation: %g", [self heatInsulation]); + + flags = [NSMutableArray array]; + #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; } + ADD_FLAG_IF_SET(military_jammer_active); + ADD_FLAG_IF_SET(docking_match_rotation); + ADD_FLAG_IF_SET(pitching_over); + ADD_FLAG_IF_SET(reportAIMessages); + ADD_FLAG_IF_SET(being_mined); + ADD_FLAG_IF_SET(being_fined); + ADD_FLAG_IF_SET(isHulk); + ADD_FLAG_IF_SET(trackCloseContacts); + ADD_FLAG_IF_SET(isNearPlanetSurface); + ADD_FLAG_IF_SET(isFrangible); + ADD_FLAG_IF_SET(cloaking_device_active); + ADD_FLAG_IF_SET(canFragment); + ADD_FLAG_IF_SET(proximity_alert); + flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none"; + OOLog(@"dumpState.shipEntity", @"Flags: %@", flagsString); +} +#endif + + +- (OOScript *)script +{ + return script; +} + + +- (NSDictionary *)scriptInfo +{ + return (scriptInfo != nil) ? scriptInfo : (NSDictionary *)[NSDictionary dictionary]; +} + + +- (Entity *)entityForShaderProperties +{ + return [self rootShipEntity]; +} + + +// *** Script event dispatch. +// For ease of overriding, these all go through doScriptEvent:withArguments:. +- (void) doScriptEvent:(NSString *)message +{ + [self doScriptEvent:message withArguments:nil]; +} + + +- (void) doScriptEvent:(NSString *)message withArgument:(id)argument +{ + NSArray *arguments = nil; + + if (argument == nil) + [self doScriptEvent:message]; + else + { + arguments = [NSArray arrayWithObject:argument]; + [self doScriptEvent:message withArguments:arguments]; + } +} + + +- (void) doScriptEvent:(NSString *)message + withArgument:(id)argument1 + andArgument:(id)argument2 +{ + NSArray *arguments = nil; + + if (argument1 == nil) argument1 = [NSNull null]; + if (argument2 == nil) argument2 = [NSNull null]; + arguments = [NSArray arrayWithObjects:argument1, argument2, nil]; + + NS_DURING + [self doScriptEvent:message withArguments:arguments]; + NS_HANDLER + OOLog(kOOLogException, @"***** Exception while performing script event %@ for %@: %@ : %@", message, [self shortDescription], [localException name], [localException reason]); + NS_ENDHANDLER +} + + +- (void) doScriptEvent:(NSString *)message withArguments:(NSArray *)arguments +{ + [script doEvent:message withArguments:arguments]; +} + + +- (void) reactToAIMessage:(NSString *)message +{ + [shipAI reactToMessage:message]; +} + + +- (void) sendAIMessage:(NSString *)message +{ + [shipAI message:message]; +} + + +- (void) doScriptEvent:(NSString *)scriptEvent andReactToAIMessage:(NSString *)aiMessage +{ + [self doScriptEvent:scriptEvent]; + [self reactToAIMessage:aiMessage]; +} + + +- (void) doScriptEvent:(NSString *)scriptEvent withArgument:(id)argument andReactToAIMessage:(NSString *)aiMessage +{ + [self doScriptEvent:scriptEvent withArgument:argument]; + [self reactToAIMessage:aiMessage]; +} + + +// Exposed to AI and scripts. +- (void) doNothing +{ + +} + +@end + + +@implementation Entity (SubEntityRelationship) + +- (BOOL) isShipWithSubEntityShip:(Entity *)other +{ + return NO; +} + +@end + + +@implementation ShipEntity (SubEntityRelationship) + +- (BOOL) isShipWithSubEntityShip:(Entity *)other +{ + assert ([self isShip]); + + if (![other isShip]) return NO; + if (![other isSubEntity]) return NO; + if ([other owner] != self) return NO; + +#ifndef NDEBUG + // Sanity check; this should always be true. + if (![self hasSubEntity:(ShipEntity *)other]) + { + OOLogERR(@"ship.subentity.sanityCheck.failed", @"%@ thinks it's a subentity of %@, but the supposed parent does not agree. This is an internal error, please report it.", [other shortDescription], [self shortDescription]); + [other setOwner:nil]; + return NO; + } +#endif + + return YES; +} + +@end + + +NSDictionary *DefaultShipShaderMacros(void) +{ + static NSDictionary *macros = nil; + NSDictionary *materialDefaults = nil; + + if (macros == nil) + { + materialDefaults = [ResourceManager dictionaryFromFilesNamed:@"material-defaults.plist" inFolder:@"Config" andMerge:YES]; + macros = [[materialDefaults dictionaryForKey:@"ship-prefix-macros" defaultValue:[NSDictionary dictionary]] retain]; + } + + return macros; +} + + +BOOL OOUniformBindingPermitted(NSString *propertyName, id bindingTarget) +{ + static NSSet *entityWhitelist = nil; + static NSSet *shipWhitelist = nil; + static NSSet *playerShipWhitelist = nil; + + if (entityWhitelist == nil) + { + NSDictionary *wlDict = [ResourceManager whitelistDictionary]; + entityWhitelist = [[NSSet alloc] initWithArray:[wlDict arrayForKey:@"shader_entity_binding_methods"]]; + shipWhitelist = [[NSSet alloc] initWithArray:[wlDict arrayForKey:@"shader_ship_binding_methods"]]; + playerShipWhitelist = [[NSSet alloc] initWithArray:[wlDict arrayForKey:@"shader_player_ship_binding_methods"]]; + } + + if ([bindingTarget isKindOfClass:[Entity class]]) + { + if ([entityWhitelist containsObject:propertyName]) return YES; + } + if ([bindingTarget isKindOfClass:[ShipEntity class]]) + { + if ([shipWhitelist containsObject:propertyName]) return YES; + } + if ([bindingTarget isKindOfClass:[PlayerEntity class]]) + { + if ([playerShipWhitelist containsObject:propertyName]) return YES; + } + + return NO; +} diff --git a/src/Core/Entities/ShipEntityAI.h b/src/Core/Entities/ShipEntityAI.h new file mode 100644 index 00000000..a76c8adb --- /dev/null +++ b/src/Core/Entities/ShipEntityAI.h @@ -0,0 +1,46 @@ +/* + +ShipEntityAI.h + +Additional methods relating to behaviour/artificial intelligence. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntity.h" + +@class AI, Universe, PlanetEntity; + +@interface ShipEntity (AI) + +// AI methods also used in other code. + +- (void) setAITo:(NSString *)aiString; +- (void) switchAITo:(NSString *)aiString; + +- (void) scanForHostiles; +- (void) performTumble; +- (void) performStop; +#if TARGET_INCOMING_MISSILES +- (void) scanForNearestIncomingMissile; +#endif + +@end diff --git a/src/Core/Entities/ShipEntityAI.m b/src/Core/Entities/ShipEntityAI.m new file mode 100644 index 00000000..367ff620 --- /dev/null +++ b/src/Core/Entities/ShipEntityAI.m @@ -0,0 +1,2370 @@ +/* + +ShipEntityAI.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntityAI.h" +#import "OOMaths.h" +#import "Universe.h" +#import "AI.h" + +#import "StationEntity.h" +#import "PlanetEntity.h" +#import "WormholeEntity.h" +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "OOJSFunction.h" +#import "OOShipGroup.h" + +#import "OOStringParsing.h" +#import "OOEntityFilterPredicate.h" +#import "OOConstToString.h" +#import "OOCollectionExtractors.h" + +#define kOOLogUnconvertedNSLog @"unclassified.ShipEntityAI" + + +@interface ShipEntity (OOAIPrivate) + +- (void)performHyperSpaceExitReplace:(BOOL)replace; + +- (void)scanForNearestShipWithPredicate:(EntityFilterPredicate)predicate parameter:(void *)parameter; +- (void)scanForNearestShipWithNegatedPredicate:(EntityFilterPredicate)predicate parameter:(void *)parameter; + +- (void) acceptDistressMessageFrom:(ShipEntity *)other; + +@end + + +@interface StationEntity (OOAIPrivate) + +- (void) acceptDistressMessageFrom:(ShipEntity *)other; + +@end + + +@interface ShipEntity (PureAI) + +// Methods used only by AI. + +- (void) pauseAI:(NSString *)intervalString; + +- (void) randomPauseAI:(NSString *)intervalString; + +- (void) dropMessages:(NSString *)messageString; + +- (void) debugDumpPendingMessages; + +- (void) setDestinationToCurrentLocation; + +- (void) setDesiredRangeTo:(NSString *)rangeString; + +- (void) setSpeedTo:(NSString *)speedString; + +- (void) setSpeedFactorTo:(NSString *)speedString; + +- (void) setSpeedToCruiseSpeed; + +- (void) performFlyToRangeFromDestination; + +- (void) performIdle; + +- (void) performHold; + +- (void) setTargetToPrimaryAggressor; + +- (void) performAttack; + +- (void) scanForNearestMerchantmen; +- (void) scanForRandomMerchantmen; + +- (void) scanForLoot; + +- (void) scanForRandomLoot; + +- (void) setTargetToFoundTarget; + +- (void) checkForFullHold; + +- (void) performCollect; + +- (void) performIntercept; + +- (void) performFlee; + +- (void) requestDockingCoordinates; + +- (void) getWitchspaceEntryCoordinates; + +- (void) setDestinationFromCoordinates; + +- (void) performFaceDestination; + +- (void) fightOrFleeMissile; + +- (void) setCourseToPlanet; +- (void) setTakeOffFromPlanet; +- (void) landOnPlanet; + +- (void) checkTargetLegalStatus; +- (void) checkOwnLegalStatus; + +- (void) exitAIWithMessage:(NSString *)message; + +- (void) setDestinationToTarget; +- (void) setDestinationWithinTarget; + +- (void) checkCourseToDestination; + +- (void) scanForOffenders; + +- (void) setCourseToWitchpoint; + +- (void) setDestinationToWitchpoint; +- (void) setDestinationToStationBeacon; + +- (void) performHyperSpaceExit; +- (void) performHyperSpaceExitWithoutReplacing; +- (void) wormholeEscorts; +- (void) wormholeGroup; +- (void) wormholeEntireGroup; + +- (void) commsMessage:(NSString *)valueString; +- (void) commsMessageByUnpiloted:(NSString *)valueString; +- (void) broadcastDistressMessage; + +- (void) ejectCargo; + +- (void) scanForThargoid; +- (void) scanForNonThargoid; +- (void) becomeUncontrolledThargon; + +- (void) checkDistanceTravelled; + +- (void) fightOrFleeHostiles; + +- (void) suggestEscort; + +- (void) escortCheckMother; + +- (void) performEscort; + +- (void) checkGroupOddsVersusTarget; + +- (void) groupAttackTarget; + +- (void) scanForFormationLeader; + +- (void) messageMother:(NSString *)msgString; + +- (void) setPlanetPatrolCoordinates; + +- (void) setSunSkimStartCoordinates; + +- (void) setSunSkimEndCoordinates; + +- (void) setSunSkimExitCoordinates; + +- (void) patrolReportIn; + +- (void) checkForMotherStation; + +- (void) sendTargetCommsMessage:(NSString *) message; + +- (void) markTargetForFines; + +- (void) markTargetForOffence:(NSString *) valueString; + +- (void) scanForRocks; + +- (void) performMining; + +- (void) setDestinationToDockingAbort; + +- (void) requestNewTarget; + +- (void) rollD:(NSString *) die_number; + +- (void) scanForNearestShipWithPrimaryRole:(NSString *)scanRole; +- (void) scanForNearestShipHavingRole:(NSString *)scanRole; +- (void) scanForNearestShipWithAnyPrimaryRole:(NSString *)scanRoles; +- (void) scanForNearestShipHavingAnyRole:(NSString *)scanRoles; +- (void) scanForNearestShipWithScanClass:(NSString *)scanScanClass; + +- (void) scanForNearestShipWithoutPrimaryRole:(NSString *)scanRole; +- (void) scanForNearestShipNotHavingRole:(NSString *)scanRole; +- (void) scanForNearestShipWithoutAnyPrimaryRole:(NSString *)scanRoles; +- (void) scanForNearestShipNotHavingAnyRole:(NSString *)scanRoles; +- (void) scanForNearestShipWithoutScanClass:(NSString *)scanScanClass; + +- (void) setCoordinates:(NSString *)system_x_y_z; + +- (void) checkForNormalSpace; + +- (void) recallDockingInstructions; + +- (void) addFuel:(NSString *) fuel_number; + +- (void) enterTargetWormhole; + +- (void) scriptActionOnTarget:(NSString *) action; + +- (void) sendScriptMessage:(NSString *)message; + +- (void) ai_throwSparks; + +// racing code. +- (void) targetFirstBeaconWithCode:(NSString *) code; +- (void) targetNextBeaconWithCode:(NSString *) code; +- (void) setRacepointsFromTarget; +- (void) performFlyRacepoints; + +@end + + +@implementation ShipEntity (AI) + +- (void) setAITo:(NSString *)aiString +{ + [[self getAI] setStateMachine:aiString]; +} + + +- (void) switchAITo:(NSString *)aiString +{ + [[self getAI] setStateMachine:aiString]; + [[self getAI] clearStack]; +} + + +- (void) scanForHostiles +{ + /*-- Locates all the ships in range targetting the receiver and chooses the nearest --*/ + found_target = NO_TARGET; + found_hostiles = 0; + + [self checkScanner]; + unsigned i; + GLfloat found_d2 = scannerRange * scannerRange; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity *thing = scanned_ships[i]; + GLfloat d2 = distance2_scanned_ships[i]; + if ((d2 < found_d2) && ([thing isThargoid] || (([thing primaryTarget] == self) && [thing hasHostileTarget]))) + { + found_target = [thing universalID]; + found_d2 = d2; + found_hostiles++; + } + } + + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else [shipAI message:@"NOTHING_FOUND"]; +} + + +#if TARGET_INCOMING_MISSILES +- (void) scanForNearestIncomingMissile +{ + BinaryOperationPredicateParameter param = + { + HasScanClassPredicate, [NSNumber numberWithInt:CLASS_MISSILE], + IsHostileAgainstTargetPredicate, self + }; + [self scanForNearestShipWithPredicate:ANDPredicate parameter:¶m]; +} +#endif + + +- (void) performTumble +{ + flightRoll = max_flight_roll*2.0*(randf() - 0.5); + flightPitch = max_flight_pitch*2.0*(randf() - 0.5); + behaviour = BEHAVIOUR_TUMBLE; + frustration = 0.0; +} + + +- (void) performStop +{ + behaviour = BEHAVIOUR_STOP_STILL; + desired_speed = 0.0; + frustration = 0.0; +} + +@end + + +@implementation ShipEntity (PureAI) + +- (void) pauseAI:(NSString *)intervalString +{ + [shipAI setNextThinkTime:[UNIVERSE getTime] + [intervalString doubleValue]]; +} + + +- (void) randomPauseAI:(NSString *)intervalString +{ + NSArray* tokens = ScanTokensFromString(intervalString); + double start, end; + + if ([tokens count] != 2) + { + OOLog(@"ai.syntax.randomPauseAI", @"***** ERROR: cannot read min and max value for randomPauseAI:, needs 2 values: '%@'.", intervalString); + return; + } + + start = [tokens doubleAtIndex:0]; + end = [tokens doubleAtIndex:1]; + + [shipAI setNextThinkTime:[UNIVERSE getTime] + (start + (end - start)*randf())]; +} + + +- (void) dropMessages:(NSString *)messageString +{ + NSArray *messages = nil; + NSEnumerator *messageEnum = nil; + NSString *message = nil; + + messages = ScanTokensFromString(messageString); + for (messageEnum = [messages objectEnumerator]; (message = [messageEnum nextObject]); ) + { + [shipAI dropMessage:messageString]; + } +} + + +- (void) debugDumpPendingMessages +{ + [shipAI debugDumpPendingMessages]; +} + + +- (void) setDestinationToCurrentLocation +{ + // randomly add a .5m variance + destination = vector_add(position, OOVectorRandomSpatial(0.5)); +} + + +- (void) setDesiredRangeTo:(NSString *)rangeString +{ + desired_range = [rangeString doubleValue]; +} + + +- (void) performFlyToRangeFromDestination +{ + behaviour = BEHAVIOUR_FLY_RANGE_FROM_DESTINATION; + frustration = 0.0; +} + + +- (void) setSpeedTo:(NSString *)speedString +{ + desired_speed = [speedString doubleValue]; +} + + +- (void) setSpeedFactorTo:(NSString *)speedString +{ + desired_speed = maxFlightSpeed * [speedString doubleValue]; +} + +- (void) setSpeedToCruiseSpeed +{ + desired_speed = cruiseSpeed; +} + +- (void) performIdle +{ + behaviour = BEHAVIOUR_IDLE; + frustration = 0.0; +} + + +- (void) performHold +{ + desired_speed = 0.0; + behaviour = BEHAVIOUR_TRACK_TARGET; + frustration = 0.0; +} + + +- (void) setTargetToPrimaryAggressor +{ + if (![UNIVERSE entityForUniversalID:primaryAggressor]) + return; + if (primaryTarget == primaryAggressor) + return; + + // a more considered approach here: + // if we're already busy attacking a target we don't necessarily want to break off + // + switch (behaviour) + { + case BEHAVIOUR_ATTACK_FLY_FROM_TARGET: + case BEHAVIOUR_ATTACK_FLY_TO_TARGET: + if (randf() < 0.75) // if I'm attacking, ignore 75% of new aggressor's attacks + return; + break; + + default: + break; + } + + // inform our old target of our new target + // + Entity *primeTarget = [UNIVERSE entityForUniversalID:primaryTarget]; + if ((primeTarget)&&(primeTarget->isShip)) + { + ShipEntity *currentShip = [UNIVERSE entityForUniversalID:primaryTarget]; + [[currentShip getAI] message:[NSString stringWithFormat:@"%@ %d %d", AIMS_AGGRESSOR_SWITCHED_TARGET, universalID, primaryAggressor]]; + } + + // okay, so let's now target the aggressor + [self addTarget:[UNIVERSE entityForUniversalID:primaryAggressor]]; +} + + +- (void) performAttack +{ + behaviour = BEHAVIOUR_ATTACK_TARGET; + desired_range = 1250 * randf() + 750; // 750 til 2000 + frustration = 0.0; +} + + +- (void) scanForNearestMerchantmen +{ + float d2, found_d2; + unsigned i; + ShipEntity *ship = nil; + + //-- Locates the nearest merchantman in range. + [self checkScanner]; + + found_d2 = scannerRange * scannerRange; + found_target = NO_TARGET; + + for (i = 0; i < n_scanned_ships ; i++) + { + ship = scanned_ships[i]; + if ([ship isPirateVictim] && ([ship status] != STATUS_DEAD) && ([ship status] != STATUS_DOCKED)) + { + d2 = distance2_scanned_ships[i]; + if (PIRATES_PREFER_PLAYER && (d2 < desired_range * desired_range) && ship->isPlayer && [self isPirate]) + { + d2 = 0.0; + } + else d2 = distance2_scanned_ships[i]; + if (d2 < found_d2) + { + found_d2 = d2; + found_target = [ship universalID]; + } + } + } + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) scanForRandomMerchantmen +{ + unsigned n_found, i; + + //-- Locates one of the merchantman in range. + [self checkScanner]; + OOUniversalID ids_found[n_scanned_ships]; + + n_found = 0; + found_target = NO_TARGET; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity *ship = scanned_ships[i]; + if (([ship status] != STATUS_DEAD) && ([ship status] != STATUS_DOCKED) && [ship isPirateVictim]) + ids_found[n_found++] = ship->universalID; + } + if (n_found == 0) + { + [shipAI message:@"NOTHING_FOUND"]; + } + else + { + i = ranrot_rand() % n_found; // pick a number from 0 -> (n_found - 1) + found_target = ids_found[i]; + [shipAI message:@"TARGET_FOUND"]; + } +} + + +- (void) scanForLoot +{ + /*-- Locates the nearest debris in range --*/ + if (!isStation) + { + if (![self hasScoop]) + { + [shipAI message:@"NOTHING_FOUND"]; //can't collect loot if you have no scoop! + return; + } + if ([cargo count] >= max_cargo) + { + if (max_cargo) [shipAI message:@"HOLD_FULL"]; //can't collect loot if holds are full! + [shipAI message:@"NOTHING_FOUND"]; //can't collect loot if holds are full! + return; + } + } + else + { + if (magnitude2([self velocity])) + { + [shipAI message:@"NOTHING_FOUND"]; //can't collect loot if you're a moving station + return; + } + } + + [self checkScanner]; + + double found_d2 = scannerRange * scannerRange; + found_target = NO_TARGET; + unsigned i; + for (i = 0; i < n_scanned_ships; i++) + { + ShipEntity *other = (ShipEntity *)scanned_ships[i]; + if ((other->scanClass == CLASS_CARGO)&&([other cargoType] != CARGO_NOT_CARGO)) + { + if ((![self isPolice]) || ([other commodityType] == 3)) // police only rescue lifepods and slaves + { + GLfloat d2 = distance2_scanned_ships[i]; + if (d2 < found_d2) + { + found_d2 = d2; + found_target = other->universalID; + } + } + } + } + if (found_target != NO_TARGET) + { + [shipAI message:@"TARGET_FOUND"]; + } + else + { + [shipAI message:@"NOTHING_FOUND"]; + } +} + + +- (void) scanForRandomLoot +{ + /*-- Locates the all debris in range and chooses a piece at random from the first sixteen found --*/ + if (![self isStation] && ![self hasScoop]) + { + [shipAI message:@"NOTHING_FOUND"]; //can't collect loot if you have no scoop! + return; + } + // + [self checkScanner]; + // + OOUniversalID thing_uids_found[16]; + unsigned things_found = 0; + found_target = NO_TARGET; + unsigned i; + for (i = 0; (i < n_scanned_ships)&&(things_found < 16) ; i++) + { + ShipEntity *other = scanned_ships[i]; + if ((other->scanClass == CLASS_CARGO)&&([other cargoType] != CARGO_NOT_CARGO)) + { + found_target = [other universalID]; + thing_uids_found[things_found++] = found_target; + } + } + + if (things_found != 0) + { + found_target = thing_uids_found[ranrot_rand() % things_found]; + [shipAI message:@"TARGET_FOUND"]; + } + else + [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) setTargetToFoundTarget +{ + if ([UNIVERSE entityForUniversalID:found_target]) + [self addTarget:[UNIVERSE entityForUniversalID:found_target]]; +} + + +- (void) checkForFullHold +{ + if (!max_cargo) + { + [shipAI message:@"NO_CARGO_BAY"]; + } + else if ([cargo count] >= max_cargo) + { + [shipAI message:@"HOLD_FULL"]; + } + else + { + [shipAI message:@"HOLD_NOT_FULL"]; + } +} + + +- (void) performCollect +{ + behaviour = BEHAVIOUR_COLLECT_TARGET; + frustration = 0.0; +} + + +- (void) performIntercept +{ + behaviour = BEHAVIOUR_INTERCEPT_TARGET; + frustration = 0.0; +} + + +- (void) performFlee +{ + behaviour = BEHAVIOUR_FLEE_TARGET; + frustration = 0.0; +} + + +- (void) getWitchspaceEntryCoordinates +{ + /*- calculates coordinates from the nearest station it can find, or just fly 10s forward -*/ + if (!UNIVERSE) + { + coordinates = position; + coordinates.x += v_forward.x * maxFlightSpeed * 10.0; + coordinates.y += v_forward.y * maxFlightSpeed * 10.0; + coordinates.z += v_forward.z * maxFlightSpeed * 10.0; + return; + } + // + // find the nearest station... + // + [self checkScanner]; + // + StationEntity *station = nil; + double nearest2 = SCANNER_MAX_RANGE2 * 1000000.0; // 1000x scanner range (25600 km), squared. + unsigned i; + for (i = 0; i < n_scanned_ships; i++) + { + if (scanned_ships[i]->isStation) + { + StationEntity *thing = (StationEntity *)scanned_ships[i]; + GLfloat range2 = distance2_scanned_ships[i]; + if (range2 < nearest2) + { + station = thing; + nearest2 = range2; + } + } + } + + if (station) + { + coordinates = station->position; + Vector vr = vector_right_from_quaternion(station->orientation); + coordinates.x += 10000 * vr.x; // 10km from station + coordinates.y += 10000 * vr.y; + coordinates.z += 10000 * vr.z; + } + else + { + coordinates = position; + coordinates.x += v_forward.x * maxFlightSpeed * 10.0; + coordinates.y += v_forward.y * maxFlightSpeed * 10.0; + coordinates.z += v_forward.z * maxFlightSpeed * 10.0; + } +} + + +- (void) setDestinationFromCoordinates +{ + destination = coordinates; +} + + +- (void) performFaceDestination +{ + behaviour = BEHAVIOUR_FACE_DESTINATION; + frustration = 0.0; +} + + +- (void) fightOrFleeMissile +{ + // find an incoming missile... + // + ShipEntity *missile = nil; + unsigned i; + NSEnumerator *escortEnum = nil; + ShipEntity *escort = nil; + ShipEntity *target = nil; + + [self checkScanner]; + for (i = 0; (i < n_scanned_ships)&&(missile == nil); i++) + { + ShipEntity *thing = scanned_ships[i]; + if (thing->scanClass == CLASS_MISSILE) + { + target = [thing primaryTarget]; + + if (target == self) + { + missile = thing; + } + else + { + for (escortEnum = [self escortEnumerator]; (escort = [escortEnum nextObject]); ) + { + if (target == escort) + { + missile = thing; + } + } + } + } + } + + if (missile == nil) return; + + [self addTarget:missile]; + + // Notify own ship script that we are being attacked. + ShipEntity *hunter = [missile owner]; + [self doScriptEvent:@"beingAttacked" withArgument:hunter]; + + if ([self isPolice]) + { + // Notify other police in group of attacker. + // Note: prior to 1.73 this was done only if we had ECM. + NSEnumerator *policeEnum = nil; + ShipEntity *police = nil; + + for (policeEnum = [[self group] objectEnumerator]; (police = [policeEnum nextObject]); ) + { + [police setFound_target:hunter]; + [police setPrimaryAggressor:hunter]; + } + } + + if ([self hasECM]) + { + // use the ECM and battle on + + [self setPrimaryAggressor:hunter]; // lets get them now for that! + found_target = primaryAggressor; + + // if I'm a copper and you're not, then mark the other as an offender! + if ([self isPolice] && ![hunter isPolice]) [hunter markAsOffender:64]; + + [self fireECM]; + return; + } + + // RUN AWAY !! + jink = make_vector(0.0f, 0.0f, 1000.0f); + desired_range = 10000; + [self performFlee]; + [shipAI message:@"FLEEING"]; +} + + +- (void) setCourseToPlanet +{ + /*- selects the nearest planet it can find -*/ + PlanetEntity *the_planet = [self findNearestPlanetExcludingMoons]; + // PlanetEntity *the_planet = [self findNearestPlanet]; + if (the_planet) + { + Vector p_pos = the_planet->position; + double p_cr = the_planet->collision_radius; // 200m above the surface + Vector p1 = vector_between(p_pos, position); + p1 = vector_normal(p1); // vector towards ship + p1.x += 0.5 * (randf() - 0.5); + p1.y += 0.5 * (randf() - 0.5); + p1.z += 0.5 * (randf() - 0.5); + p1 = vector_normal(p1); + destination = make_vector(p_pos.x + p1.x * p_cr, p_pos.y + p1.y * p_cr, p_pos.z + p1.z * p_cr); // on surface + desired_range = collision_radius + 50.0; // +50m from the destination + } +} + + +- (void) setTakeOffFromPlanet +{ + /*- selects the nearest planet it can find -*/ + PlanetEntity *the_planet = [self findNearestPlanet]; + if (the_planet) + { + destination = vector_add([the_planet position], vector_multiply_scalar( + vector_normal(vector_subtract([the_planet position],position)),-10000.0-the_planet->collision_radius));// 10km straight up + desired_range = 50.0; + } + else + { + OOLog(@"ai.setTakeOffFromPlanet.noPlanet", @"***** Error. Planet not found during take off!"); + } +} + + +- (void) landOnPlanet +{ + // Selects the nearest planet it can find. + [self landOnPlanet:[self findNearestPlanet]]; +} + + +- (void) checkTargetLegalStatus +{ + ShipEntity *other_ship = [UNIVERSE entityForUniversalID:primaryTarget]; + if (!other_ship) + { + [shipAI message:@"NO_TARGET"]; + return; + } + else + { + int ls = [other_ship legalStatus]; + if (ls > 50) + { + [shipAI message:@"TARGET_FUGITIVE"]; + return; + } + if (ls > 20) + { + [shipAI message:@"TARGET_OFFENDER"]; + return; + } + if (ls > 0) + { + [shipAI message:@"TARGET_MINOR_OFFENDER"]; + return; + } + [shipAI message:@"TARGET_CLEAN"]; + } +} + + +- (void) checkOwnLegalStatus +{ + if (scanClass == CLASS_THARGOID) + { + [shipAI message:@"SELF_THARGOID"]; + return; + } + int ls = [self legalStatus]; + if (ls > 50) + { + [shipAI message:@"SELF_FUGITIVE"]; + return; + } + if (ls > 20) + { + [shipAI message:@"SELF_OFFENDER"]; + return; + } + if (ls > 0) + { + [shipAI message:@"SELF_MINOR_OFFENDER"]; + return; + } + [shipAI message:@"SELF_CLEAN"]; +} + + +- (void) exitAIWithMessage:(NSString *)message; +{ + if ([message length] == 0) message = @"RESTARTED"; + [shipAI exitStateMachineWithMessage:message]; +} + + +- (void) setDestinationToTarget +{ + Entity *the_target = [UNIVERSE entityForUniversalID:primaryTarget]; + if (the_target) + destination = the_target->position; +} + + +- (void) setDestinationWithinTarget +{ + Entity *the_target = [UNIVERSE entityForUniversalID:primaryTarget]; + if (the_target) + { + Vector pos = the_target->position; + Quaternion q; quaternion_set_random(&q); + Vector v = vector_forward_from_quaternion(q); + GLfloat d = (randf() - randf()) * the_target->collision_radius; + destination = make_vector(pos.x + d * v.x, pos.y + d * v.y, pos.z + d * v.z); + } +} + + +- (void) checkCourseToDestination +{ + Entity *hazard = [UNIVERSE hazardOnRouteFromEntity: self toDistance: desired_range fromPoint: destination]; + + if (!hazard || (hazard->isShip && sqrtf(distance2(position, hazard->position)) > scannerRange) || (hazard->isPlanet && aegis_status == AEGIS_NONE)) + [shipAI message:@"COURSE_OK"]; // Avoid going into a waypoint.plist for far away objects, it cripples the main AI a bit in its funtionality. + else + { + if ((hazard->isShip)&&(weapon_energy * 24.0 > [hazard energy])) + [shipAI reactToMessage:@"HAZARD_CAN_BE_DESTROYED"]; + + destination = [UNIVERSE getSafeVectorFromEntity:self toDistance:desired_range fromPoint:destination]; + [shipAI message:@"WAYPOINT_SET"]; + } +} + + +- (void) scanForOffenders +{ + /*-- Locates all the ships in range and compares their legal status or bounty against ranrot_rand() & 255 - chooses the worst offender --*/ + NSDictionary *systeminfo = [UNIVERSE currentSystemData]; + float gov_factor = 0.4 * [(NSNumber *)[systeminfo objectForKey:KEY_GOVERNMENT] intValue]; // 0 .. 7 (0 anarchic .. 7 most stable) --> [0.0, 0.4, 0.8, 1.2, 1.6, 2.0, 2.4, 2.8] + // + if (![UNIVERSE sun]) + gov_factor = 1.0; + // + found_target = NO_TARGET; + + // find the worst offender on the scanner + // + [self checkScanner]; + unsigned i; + float worst_legal_factor = 0; + GLfloat found_d2 = scannerRange * scannerRange; + OOShipGroup *group = [self group]; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity *ship = scanned_ships[i]; + if ((ship->scanClass != CLASS_CARGO)&&([ship status] != STATUS_DEAD)&&([ship status] != STATUS_DOCKED)) + { + GLfloat d2 = distance2_scanned_ships[i]; + float legal_factor = [ship legalStatus] * gov_factor; + int random_factor = ranrot_rand() & 255; // 25% chance of spotting a fugitive in 15s + if ((d2 < found_d2)&&(random_factor < legal_factor)&&(legal_factor > worst_legal_factor)) + { + if (group == nil || group != [ship group]) // fellows with bounty can't be offenders + { + found_target = [ship universalID]; + worst_legal_factor = legal_factor; + } + } + } + } + + if (found_target != NO_TARGET) + [shipAI message:@"TARGET_FOUND"]; + else + [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) setCourseToWitchpoint +{ + if (UNIVERSE) + { + destination = [UNIVERSE getWitchspaceExitPosition]; + desired_range = 10000.0; // 10km away + } +} + + +- (void) setDestinationToWitchpoint +{ + destination = [UNIVERSE getWitchspaceExitPosition]; +} + + +- (void) setDestinationToStationBeacon +{ + if ([UNIVERSE station]) + destination = [[UNIVERSE station] getBeaconPosition]; +} + + +static WormholeEntity *whole = nil; +// +- (void) performHyperSpaceExit +{ + [self performHyperSpaceExitReplace:YES]; +} + + +- (void) performHyperSpaceExitWithoutReplacing +{ + [self performHyperSpaceExitReplace:NO]; +} + + +// FIXME: resolve this stuff. +- (void) wormholeEscorts +{ + NSEnumerator *shipEnum = nil; + ShipEntity *ship = nil; + + if (whole == nil) return; + + for (shipEnum = [self escortEnumerator]; (ship = [shipEnum nextObject]); ) + { + [ship addTarget:whole]; + [ship reactToAIMessage:@"ENTER WORMHOLE"]; + } + + // We now have no escorts.. + [_escortGroup release]; + _escortGroup = nil; + +} + + +- (void) wormholeGroup +{ + NSEnumerator *shipEnum = nil; + ShipEntity *ship = nil; + + for (shipEnum = [[self group] objectEnumerator]; (ship = [shipEnum nextObject]); ) + { + [ship addTarget:whole]; + [ship reactToAIMessage:@"ENTER WORMHOLE"]; + } +} + + +- (void) wormholeEntireGroup +{ + [self wormholeGroup]; + [self wormholeEscorts]; +} + + +- (void) commsMessage:(NSString *)valueString +{ + [self commsMessage:valueString withUnpilotedOverride:NO]; +} + + +- (void) commsMessageByUnpiloted:(NSString *)valueString +{ + [self commsMessage:valueString withUnpilotedOverride:YES]; +} + + +- (void) broadcastDistressMessage +{ + /*-- Locates all the stations, bounty hunters and police ships in range and tells them that you are under attack --*/ + + [self checkScanner]; + // + GLfloat d2; + GLfloat found_d2 = SCANNER_MAX_RANGE2; + NSString* distress_message; + found_target = NO_TARGET; + BOOL is_buoy = (scanClass == CLASS_BUOY); + + if (messageTime > 2.0 * randf()) + return; // don't send too many distress messages at once, space them out semi-randomly + + if (is_buoy) + distress_message = @"[buoy-distress-call]"; + else + distress_message = @"[distress-call]"; + + unsigned i; + for (i = 0; i < n_scanned_ships; i++) + { + ShipEntity* ship = scanned_ships[i]; + d2 = distance2_scanned_ships[i]; + if (d2 < found_d2) + { + // tell it! // + if (ship->isPlayer) + { + if ((primaryAggressor == [ship universalID])&&(energy < 0.375 * maxEnergy)&&(!is_buoy)) + { + [self sendExpandedMessage:ExpandDescriptionForCurrentSystem(@"[beg-for-mercy]") toShip:ship]; + [self ejectCargo]; + [self performFlee]; + } + else + [self sendExpandedMessage:ExpandDescriptionForCurrentSystem(distress_message) toShip:ship]; + // reset the thanked_ship_id + // + thanked_ship_id = NO_TARGET; + } + if ([self bounty] == 0) // Only clean ships can have their distress calls accepted + { + if (ship->isStation) + [ship acceptDistressMessageFrom:self]; + if ([ship hasPrimaryRole:@"police"]) // Not isPolice because we don't want wingmen shooting off... but what about interceptors? + [ship acceptDistressMessageFrom:self]; + if ([ship hasPrimaryRole:@"hunter"]) + [ship acceptDistressMessageFrom:self]; + } + } + } +} + + +- (void) ejectCargo +{ + unsigned i; + if ((cargo_flag == CARGO_FLAG_FULL_PLENTIFUL)||(cargo_flag == CARGO_FLAG_FULL_SCARCE)) + { + NSArray* jetsam; + int cargo_to_go = 0.1 * max_cargo; + while (cargo_to_go > 15) + cargo_to_go = ranrot_rand() % cargo_to_go; + + jetsam = [UNIVERSE getContainersOfGoods:cargo_to_go scarce:cargo_flag == CARGO_FLAG_FULL_SCARCE]; + + if (!cargo) + cargo = [[NSMutableArray alloc] initWithCapacity:max_cargo]; + [cargo addObjectsFromArray:jetsam]; + cargo_flag = CARGO_FLAG_CANISTERS; + } + [self dumpCargo]; + for (i = 1; i < [cargo count]; i++) + { + [self performSelector:@selector(dumpCargo) withObject:nil afterDelay:0.75 * i]; // drop 3 canisters per 2 seconds + } +} + + +- (void) scanForThargoid +{ + return [self scanForNearestShipWithPrimaryRole:@"thargoid"]; +} + + +- (void) scanForNonThargoid +{ + /*-- Locates all the non thargoid ships in range and chooses the nearest --*/ + found_target = NO_TARGET; + + [self checkScanner]; + unsigned i; + GLfloat found_d2 = scannerRange * scannerRange; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity *thing = scanned_ships[i]; + GLfloat d2 = distance2_scanned_ships[i]; + if (([thing scanClass] != CLASS_CARGO) && ([thing status] != STATUS_DOCKED) && ![thing isThargoid] && (d2 < found_d2)) + { + found_target = [thing universalID]; + if ([thing isPlayer]) d2 = 0.0; // prefer the player + found_d2 = d2; + } + } + + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) becomeUncontrolledThargon +{ + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + int i; + for (i = 0; i < ent_count; i++) if (uni_entities[i]->isShip) + { + ShipEntity *other = (ShipEntity*)uni_entities[i]; + if ([other primaryTarget] == self) + { + [other removeTarget:self]; + } + } + // now we're just a bunch of alien artefacts! + scanClass = CLASS_CARGO; + reportAIMessages = NO; + [shipAI setStateMachine:@"dumbAI.plist"]; + primaryTarget = NO_TARGET; + [self setSpeed: 0.0]; +} + + +- (void) checkDistanceTravelled +{ + if (distanceTravelled > desired_range) + [shipAI message:@"GONE_BEYOND_RANGE"]; +} + + +- (void) fightOrFleeHostiles +{ + if ([self hasEscorts]) + { + if (found_target == last_escort_target) + { + [shipAI message:@"FLEEING"]; + return; + } + + primaryAggressor = found_target; + primaryTarget = found_target; + [self deployEscorts]; + [shipAI message:@"DEPLOYING_ESCORTS"]; + [shipAI message:@"FLEEING"]; + return; + } + + // consider launching a missile + if (missiles > 2) // keep a reserve + { + if (randf() < 0.50) + { + primaryAggressor = found_target; + primaryTarget = found_target; + [self fireMissile]; + [shipAI message:@"FLEEING"]; + return; + } + } + + // consider fighting + if (energy > maxEnergy * 0.80) + { + primaryAggressor = found_target; + //[self performAttack]; + [shipAI message:@"FIGHTING"]; + return; + } + + [shipAI message:@"FLEEING"]; +} + + +- (void) suggestEscort +{ + ShipEntity *mother = [UNIVERSE entityForUniversalID:primaryTarget]; + if (mother) + { +#ifndef NDEBUG + if (reportAIMessages) + { + OOLog(@"ai.suggestEscort", @"DEBUG: %@ suggests escorting %@", self, mother); + } +#endif + + if ([mother acceptAsEscort:self]) + { + // copy legal status across + if (([mother legalStatus] > 0)&&(bounty <= 0)) + { + int extra = 1 | (ranrot_rand() & 15); + [mother setBounty: [mother legalStatus] + extra]; + bounty += extra; // obviously we're dodgier than we thought! + } + + [self setOwner:mother]; + [self setGroup:[mother escortGroup]]; + [shipAI message:@"ESCORTING"]; + return; + } + +#ifndef NDEBUG + if (reportAIMessages) + { + OOLog(@"ai.suggestEscort.refused", @"DEBUG: %@ refused by %@", self, mother); + } +#endif + + } + [self setOwner:NULL]; + [shipAI message:@"NOT_ESCORTING"]; +} + + +- (void) escortCheckMother +{ + ShipEntity *mother = [self owner]; + + if ([mother acceptAsEscort:self]) + { + [self setOwner:mother]; + [self setGroup:[mother escortGroup]]; + [shipAI message:@"ESCORTING"]; + } + else + { + [self setOwner:self]; + if ([self group] == [mother escortGroup]) [self setGroup:nil]; + [shipAI message:@"NOT_ESCORTING"]; + } +} + + +- (void) performEscort +{ + if(behaviour != BEHAVIOUR_FORMATION_FORM_UP) + { + behaviour = BEHAVIOUR_FORMATION_FORM_UP; + frustration = 0.0; // behavior changed, reset frustration. + } +} + + +- (void) checkGroupOddsVersusTarget +{ + OOUInteger ownGroupCount = [[self group] count] + (ranrot_rand() & 3); // add a random fudge factor + OOUInteger targetGroupCount = [[[self primaryTarget] group] count] + (ranrot_rand() & 3); // add a random fudge factor + + if (ownGroupCount == targetGroupCount) + { + [shipAI message:@"ODDS_LEVEL"]; + } + else if (ownGroupCount > targetGroupCount) + { + [shipAI message:@"ODDS_GOOD"]; + } + else + { + [shipAI message:@"ODDS_BAD"]; + } +} + + +- (void) groupAttackTarget +{ + NSEnumerator *shipEnum = nil; + ShipEntity *target = nil, *ship = nil; + + if ([self group] == nil) // ship is alone! + { + found_target = primaryTarget; + [shipAI reactToMessage:@"GROUP_ATTACK_TARGET"]; + return; + } + + target = [self primaryTarget]; + + for (shipEnum = [[self group] objectEnumerator]; (ship = [shipEnum nextObject]); ) + { + [ship setFound_target:target]; + [ship reactToAIMessage:@"GROUP_ATTACK_TARGET"]; + } +} + + +- (void) scanForFormationLeader +{ + //-- Locates the nearest suitable formation leader in range --// + found_target = NO_TARGET; + [self checkScanner]; + unsigned i; + GLfloat found_d2 = scannerRange * scannerRange; + for (i = 0; i < n_scanned_ships; i++) + { + ShipEntity *ship = scanned_ships[i]; + if ((ship != self) && (!ship->isPlayer) && (ship->scanClass == scanClass)) // look for alike + { + GLfloat d2 = distance2_scanned_ships[i]; + if ((d2 < found_d2) && [ship canAcceptEscort:self]) + { + found_d2 = d2; + found_target = ship->universalID; + } + } + } + + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else + { + [shipAI message:@"NOTHING_FOUND"]; + if ([self hasPrimaryRole:@"wingman"]) + { + // become free-lance police :) + [shipAI setStateMachine:@"route1patrolAI.plist"]; // use this to avoid referencing a released AI + [self setPrimaryRole:@"police"]; + } + } + +} + + +- (void) messageMother:(NSString *)msgString +{ + ShipEntity *mother = [self owner]; + if (mother != nil && mother != self) + { + [mother reactToAIMessage:msgString]; + } +} + + +- (void) messageSelf:(NSString *)msgString +{ + [self sendAIMessage:msgString]; +} + + +- (void) setPlanetPatrolCoordinates +{ + // check we've arrived near the last given coordinates + Vector r_pos = vector_subtract(position, coordinates); + if (magnitude2(r_pos) < 1000000 || patrol_counter == 0) + { + Entity *the_sun = [UNIVERSE sun]; + Entity *the_station = [self owner]; // was: [UNIVERSE station]; // let it work for any station. + if(!the_station) the_station = [UNIVERSE station]; + if ((!the_sun)||(!the_station)) + return; + Vector sun_pos = the_sun->position; + Vector stn_pos = the_station->position; + Vector sun_dir = make_vector(sun_pos.x - stn_pos.x, sun_pos.y - stn_pos.y, sun_pos.z - stn_pos.z); + Vector vSun = make_vector(0, 0, 1); + if (sun_dir.x||sun_dir.y||sun_dir.z) + vSun = vector_normal(sun_dir); + Vector v0 = vector_forward_from_quaternion(the_station->orientation); + Vector v1 = cross_product(v0, vSun); + Vector v2 = cross_product(v0, v1); + switch (patrol_counter) + { + case 0: // first go to 5km ahead of the station + coordinates = make_vector(stn_pos.x + 5000 * v0.x, stn_pos.y + 5000 * v0.y, stn_pos.z + 5000 * v0.z); + desired_range = 250.0; + break; + case 1: // go to 25km N of the station + coordinates = make_vector(stn_pos.x + 25000 * v1.x, stn_pos.y + 25000 * v1.y, stn_pos.z + 25000 * v1.z); + desired_range = 250.0; + break; + case 2: // go to 25km E of the station + coordinates = make_vector(stn_pos.x + 25000 * v2.x, stn_pos.y + 25000 * v2.y, stn_pos.z + 25000 * v2.z); + desired_range = 250.0; + break; + case 3: // go to 25km S of the station + coordinates = make_vector(stn_pos.x - 25000 * v1.x, stn_pos.y - 25000 * v1.y, stn_pos.z - 25000 * v1.z); + desired_range = 250.0; + break; + case 4: // go to 25km W of the station + coordinates = make_vector(stn_pos.x - 25000 * v2.x, stn_pos.y - 25000 * v2.y, stn_pos.z - 25000 * v2.z); + desired_range = 250.0; + break; + default: // We should never come here + coordinates = make_vector(stn_pos.x + 5000 * v0.x, stn_pos.y + 5000 * v0.y, stn_pos.z + 5000 * v0.z); + desired_range = 250.0; + break; + } + patrol_counter++; + if (patrol_counter > 4) + { + if (randf() < .25) + { + // consider docking + [self setAITo:@"dockingAI.plist"]; + } + else + { + // go around again + patrol_counter = 1; + } + } + } + [shipAI message:@"APPROACH_COORDINATES"]; +} + + +- (void) setSunSkimStartCoordinates +{ + Vector v0 = [UNIVERSE getSunSkimStartPositionForShip:self]; + + if ((v0.x != 0.0)||(v0.y != 0.0)||(v0.z != 0.0)) + { + coordinates = v0; + [shipAI message:@"APPROACH_COORDINATES"]; + } + else + { + [shipAI message:@"WAIT_FOR_SUN"]; + } +} + + +- (void) setSunSkimEndCoordinates +{ + coordinates = [UNIVERSE getSunSkimEndPositionForShip:self]; + [shipAI message:@"APPROACH_COORDINATES"]; +} + + +- (void) setSunSkimExitCoordinates +{ + Entity *the_sun = [UNIVERSE sun]; + if (!the_sun) + return; + Vector v1 = [UNIVERSE getSunSkimEndPositionForShip:self]; + Vector vs = the_sun->position; + Vector vout = make_vector(v1.x - vs.x, v1.y - vs.y, v1.z - vs.z); + if (vout.x||vout.y||vout.z) + vout = vector_normal(vout); + else + vout.z = 1.0; + v1.x += 10000 * vout.x; v1.y += 10000 * vout.y; v1.z += 10000 * vout.z; + coordinates = v1; + [shipAI message:@"APPROACH_COORDINATES"]; +} + + +- (void) patrolReportIn +{ + [[UNIVERSE station] acceptPatrolReportFrom:self]; +} + + +- (void) checkForMotherStation +{ + StationEntity *motherStation = [self owner]; + if ((!motherStation) || (!(motherStation->isStation))) + { + [shipAI message:@"NOTHING_FOUND"]; + return; + } + Vector v0 = motherStation->position; + Vector rpos = make_vector(position.x - v0.x, position.y - v0.y, position.z - v0.z); + double found_d2 = scannerRange * scannerRange; + if (magnitude2(rpos) > found_d2) + { + [shipAI message:@"NOTHING_FOUND"]; + return; + } + [shipAI message:@"STATION_FOUND"]; +} + + +- (void) sendTargetCommsMessage:(NSString*) message +{ + ShipEntity *ship = [self primaryTarget]; + if ((ship == nil) || ([ship status] == STATUS_DEAD) || ([ship status] == STATUS_DOCKED)) + { + [self noteLostTarget]; + return; + } + [self sendExpandedMessage:message toShip:[self primaryTarget]]; +} + + +- (void) markTargetForFines +{ + ShipEntity *ship = [self primaryTarget]; + if ((ship == nil) || ([ship status] == STATUS_DEAD) || ([ship status] == STATUS_DOCKED)) + { + [self noteLostTarget]; + return; + } + if ([ship markForFines]) [shipAI message:@"TARGET_MARKED"]; +} + + +- (void) markTargetForOffence:(NSString*) valueString +{ + if ((isStation)||(scanClass == CLASS_POLICE)) + { + ShipEntity *ship = [self primaryTarget]; + if ((ship == nil) || ([ship status] == STATUS_DEAD) || ([ship status] == STATUS_DOCKED)) + { + [self noteLostTarget]; + return; + } + NSString* finalValue = ExpandDescriptionForCurrentSystem(valueString); // expand values + [ship markAsOffender:[finalValue intValue]]; + } +} + + +- (void) scanForRocks +{ + /*-- Locates the all boulders and asteroids in range and selects nearest --*/ + + // find boulders then asteroids within range + // + found_target = NO_TARGET; + [self checkScanner]; + unsigned i; + GLfloat found_d2 = scannerRange * scannerRange; + for (i = 0; i < n_scanned_ships; i++) + { + ShipEntity *thing = scanned_ships[i]; + if ([thing hasRole:@"boulder"]) + { + GLfloat d2 = distance2_scanned_ships[i]; + if (d2 < found_d2) + { + found_target = thing->universalID; + found_d2 = d2; + } + } + } + if (found_target == NO_TARGET) + { + for (i = 0; i < n_scanned_ships; i++) + { + ShipEntity *thing = scanned_ships[i]; + if ([thing hasRole:@"asteroid"]) + { + GLfloat d2 = distance2_scanned_ships[i]; + if (d2 < found_d2) + { + found_target = thing->universalID; + found_d2 = d2; + } + } + } + } + + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) performMining +{ + behaviour = BEHAVIOUR_ATTACK_MINING_TARGET; + frustration = 0.0; +} + + +- (void) setDestinationToDockingAbort +{ + Entity *the_target = [self primaryTarget]; + double bo_distance = 8000; // 8km back off + Vector v0 = position; + Vector d0 = (the_target) ? the_target->position : kZeroVector; + v0.x += (randf() - 0.5)*collision_radius; v0.y += (randf() - 0.5)*collision_radius; v0.z += (randf() - 0.5)*collision_radius; + v0.x -= d0.x; v0.y -= d0.y; v0.z -= d0.z; + v0 = vector_normal_or_fallback(v0, make_vector(0, 0, -1)); + + v0.x *= bo_distance; v0.y *= bo_distance; v0.z *= bo_distance; + v0.x += d0.x; v0.y += d0.y; v0.z += d0.z; + coordinates = v0; + destination = v0; +} + + +- (void) requestNewTarget +{ + ShipEntity *mother = [self owner]; + if (mother == nil) mother = [[self group] leader]; + if (mother == nil) + { + [shipAI message:@"MOTHER_LOST"]; + return; + } + + /*-- Locates all the ships in range targetting the mother ship and chooses the nearest/biggest --*/ + found_target = NO_TARGET; + found_hostiles = 0; + [self checkScanner]; + unsigned i; + GLfloat found_d2 = scannerRange * scannerRange; + GLfloat max_e = 0; + for (i = 0; i < n_scanned_ships ; i++) + { + ShipEntity *thing = scanned_ships[i]; + GLfloat d2 = distance2_scanned_ships[i]; + GLfloat e1 = [thing energy]; + if ((d2 < found_d2) && ([thing isThargoid] || (([thing primaryTarget] == mother) && [thing hasHostileTarget]))) + { + if (e1 > max_e) + { + found_target = thing->universalID; + max_e = e1; + } + found_hostiles++; + } + } + + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) rollD:(NSString*) die_number +{ + int die_sides = [die_number intValue]; + if (die_sides > 0) + { + int die_roll = 1 + (ranrot_rand() % die_sides); + NSString* result = [NSString stringWithFormat:@"ROLL_%d", die_roll]; + [shipAI reactToMessage: result]; + } + else + { + OOLog(@"ai.rollD.invalidValue", @"***** ERROR: invalid value supplied to rollD: '%@'.", die_number); + } +} + + +- (void) scanForNearestShipWithPrimaryRole:(NSString *)scanRole +{ + [self scanForNearestShipWithPredicate:HasPrimaryRolePredicate parameter:scanRole]; +} + + +- (void) scanForNearestShipHavingRole:(NSString *)scanRole +{ + [self scanForNearestShipWithPredicate:HasRolePredicate parameter:scanRole]; +} + + +- (void) scanForNearestShipWithAnyPrimaryRole:(NSString *)scanRoles +{ + NSSet *set = [NSSet setWithArray:ScanTokensFromString(scanRoles)]; + [self scanForNearestShipWithPredicate:HasPrimaryRoleInSetPredicate parameter:set]; +} + + +- (void) scanForNearestShipHavingAnyRole:(NSString *)scanRoles +{ + NSSet *set = [NSSet setWithArray:ScanTokensFromString(scanRoles)]; + [self scanForNearestShipWithPredicate:HasRoleInSetPredicate parameter:set]; +} + + +- (void) scanForNearestShipWithScanClass:(NSString *)scanScanClass +{ + NSNumber *parameter = [NSNumber numberWithInt:StringToScanClass(scanScanClass)]; + [self scanForNearestShipWithPredicate:HasScanClassPredicate parameter:parameter]; +} + + +- (void) scanForNearestShipWithoutPrimaryRole:(NSString *)scanRole +{ + [self scanForNearestShipWithNegatedPredicate:HasPrimaryRolePredicate parameter:scanRole]; +} + + +- (void) scanForNearestShipNotHavingRole:(NSString *)scanRole +{ + [self scanForNearestShipWithNegatedPredicate:HasRolePredicate parameter:scanRole]; +} + + +- (void) scanForNearestShipWithoutAnyPrimaryRole:(NSString *)scanRoles +{ + NSSet *set = [NSSet setWithArray:ScanTokensFromString(scanRoles)]; + [self scanForNearestShipWithNegatedPredicate:HasPrimaryRoleInSetPredicate parameter:set]; +} + + +- (void) scanForNearestShipNotHavingAnyRole:(NSString *)scanRoles +{ + NSSet *set = [NSSet setWithArray:ScanTokensFromString(scanRoles)]; + [self scanForNearestShipWithNegatedPredicate:HasRoleInSetPredicate parameter:set]; +} + + +- (void) scanForNearestShipWithoutScanClass:(NSString *)scanScanClass +{ + NSNumber *parameter = [NSNumber numberWithInt:StringToScanClass(scanScanClass)]; + [self scanForNearestShipWithNegatedPredicate:HasScanClassPredicate parameter:parameter]; +} + + +- (void) scanForNearestShipMatchingPredicate:(NSString *)predicateExpression +{ + /* Takes a boolean-valued JS expression where "ship" is the ship being + evaluated and "this" is our ship's ship script. the expression is + turned into a JS function of the form: + + function _oo_AIScanPredicate(ship) + { + return $expression; + } + + Examples of expressions: + ship.isWeapon + this.someComplicatedPredicate(ship) + function (ship) { ...do something complicated... } () + */ + + static NSMutableDictionary *scriptCache = nil; + NSString *aiName = nil; + NSString *key = nil; + OOJSFunction *function = nil; + JSContext *context = NULL; + + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + if (predicateExpression == nil) predicateExpression = @"false"; + + aiName = [[self getAI] name]; +#ifndef NDEBUG + /* In debug/test release builds, scripts are cached per AI in order to be + able to report errors correctly. For end-user releases, we only cache + one copy of each predicate, potentially leading to error messages for + the wrong AI. + */ + key = [NSString stringWithFormat:@"%@\n%@", aiName, predicateExpression]; +#else + key = predicateExpression; +#endif + + // Look for cached function + function = [scriptCache objectForKey:key]; + if (function == nil) + { + NSString *predicateCode = nil; + const char *argNames[] = { "ship" }; + + // Stuff expression in a function. + predicateCode = [NSString stringWithFormat:@"return %@;", predicateExpression]; + function = [[OOJSFunction alloc] initWithName:@"_oo_AIScanPredicate" + scope:NULL + code:predicateCode + argumentCount:1 + argumentNames:argNames + fileName:aiName + lineNumber:0 + context:context]; + [function autorelease]; + + // Cache function. + if (function != nil) + { + if (scriptCache == nil) scriptCache = [[NSMutableDictionary alloc] init]; + [scriptCache setObject:function forKey:key]; + } + } + + if (function != nil) + { + JSFunctionPredicateParameter param = { context, [function function], JSVAL_TO_OBJECT([self javaScriptValueInContext:context]), NO }; + [self scanForNearestShipWithPredicate:JSFunctionPredicate parameter:¶m]; + } + else + { + // Report error (once per occurrence) + static NSMutableSet *errorCache = nil; + + if (![errorCache containsObject:key]) + { + OOLog(@"ai.scanForNearestShipMatchingPredicate.compile.failed", @"Could not compile JavaScript predicate \"%@\" for AI %@.", predicateExpression, [[self getAI] name]); + if (errorCache == nil) errorCache = [[NSMutableSet alloc] init]; + [errorCache addObject:key]; + } + + // Select nothing + found_target = NO_TARGET; + [[self getAI] message:@"NOTHING_FOUND"]; + } + + JS_ReportPendingException(context); + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; +} + + +- (void) setCoordinates:(NSString *)system_x_y_z +{ + NSArray* tokens = ScanTokensFromString(system_x_y_z); + NSString* systemString = nil; + NSString* xString = nil; + NSString* yString = nil; + NSString* zString = nil; + + if ([tokens count] != 4) + { + OOLog(@"ai.syntax.setCoordinates", @"***** ERROR: cannot setCoordinates: '%@'.",system_x_y_z); + return; + } + + systemString = (NSString *)[tokens objectAtIndex:0]; + xString = (NSString *)[tokens objectAtIndex:1]; + if ([xString hasPrefix:@"rand:"]) + xString = [NSString stringWithFormat:@"%.3f", bellf([(NSString*)[[xString componentsSeparatedByString:@":"] objectAtIndex:1] intValue])]; + yString = (NSString *)[tokens objectAtIndex:2]; + if ([yString hasPrefix:@"rand:"]) + yString = [NSString stringWithFormat:@"%.3f", bellf([(NSString*)[[yString componentsSeparatedByString:@":"] objectAtIndex:1] intValue])]; + zString = (NSString *)[tokens objectAtIndex:3]; + if ([zString hasPrefix:@"rand:"]) + zString = [NSString stringWithFormat:@"%.3f", bellf([(NSString*)[[zString componentsSeparatedByString:@":"] objectAtIndex:1] intValue])]; + + Vector posn = make_vector([xString floatValue], [yString floatValue], [zString floatValue]); + GLfloat scalar = 1.0; + + coordinates = [UNIVERSE coordinatesForPosition:posn withCoordinateSystem:systemString returningScalar:&scalar]; + + [shipAI message:@"APPROACH_COORDINATES"]; +} + + +- (void) checkForNormalSpace +{ + if ([UNIVERSE sun] && [UNIVERSE planet]) + [shipAI message:@"NORMAL_SPACE"]; + else + [shipAI message:@"INTERSTELLAR_SPACE"]; +} + + +- (void) requestDockingCoordinates +{ + /*- requests coordinates from the target station + if the target station can't be found + then use the nearest it can find (which may be a rock hermit) -*/ + + StationEntity *station = nil; + Entity *targStation = nil; + NSString *message = nil; + double distanceToStation2 = 0.0; + + targStation = [UNIVERSE entityForUniversalID:targetStation]; + if ([targStation isStation]) + { + station = (StationEntity*)targStation; + } + else + { + station = [UNIVERSE nearestShipMatchingPredicate:IsStationPredicate + parameter:nil + relativeToEntity:self]; + } + + distanceToStation2 = distance2( [station position], [self position] ); + + // Player check for being inside the aegis already exists in PlayerEntityControls. We just + // check here that distance to station is less than 2.5 times scanner range to avoid problems with + // NPC ships getting stuck with a dockingAI while just outside the aegis - Nikos 20090630, as proposed by Eric + if (station != nil && distanceToStation2 < SCANNER_MAX_RANGE2 * 6.25) + { + // remember the instructions + [dockingInstructions release]; + dockingInstructions = [[station dockingInstructionsForShip:self] retain]; + + [self recallDockingInstructions]; + + message = [dockingInstructions objectForKey:@"ai_message"]; + if (message != nil) [shipAI message:message]; + message = [dockingInstructions objectForKey:@"comms_message"]; + if (message != nil) [station sendExpandedMessage:message toShip:self]; + } + else + { + [shipAI message:@"NO_STATION_FOUND"]; + } +} + + +- (void) recallDockingInstructions +{ + if (dockingInstructions != nil) + { + destination = [dockingInstructions vectorForKey:@"destination"]; + desired_speed = fminf([dockingInstructions floatForKey:@"speed"], maxFlightSpeed); + desired_range = [dockingInstructions floatForKey:@"range"]; + if ([dockingInstructions objectForKey:@"station_id"]) + { + primaryTarget = [dockingInstructions intForKey:@"station_id"]; + targetStation = primaryTarget; + } + docking_match_rotation = [dockingInstructions boolForKey:@"match_rotation"]; + } +} + + +- (void) addFuel:(NSString*) fuel_number +{ + [self setFuel:[self fuel] + [fuel_number intValue] * 10]; +} + + +- (void) enterTargetWormhole +{ + WormholeEntity *whole = nil; + + // locate nearest wormhole + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + WormholeEntity* wormholes[ent_count]; + int i; + int wh_count = 0; + for (i = 0; i < ent_count; i++) + if (uni_entities[i]->isWormhole) + wormholes[wh_count++] = [uni_entities[i] retain]; // retained + // + double found_d2 = scannerRange * scannerRange; + for (i = 0; i < wh_count ; i++) + { + WormholeEntity *wh = wormholes[i]; + double d2 = distance2(position, wh->position); + if (d2 < found_d2) + { + whole = wh; + found_d2 = d2; + } + [wh release]; // released + } + + if (!whole) + return; + if (!whole->isWormhole) + return; + [whole suckInShip:self]; +} + + +- (void) scriptActionOnTarget:(NSString *)action +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + ShipEntity *targEnt = [self primaryTarget]; + ShipEntity *oldTarget = nil; + +#ifndef NDEBUG + static BOOL deprecationWarning = NO; + + if (!deprecationWarning) + { + deprecationWarning = YES; + OOLog(@"script.deprecated.scriptActionOnTarget", @"----- WARNING in AI %@: the AI method scriptActionOnTarget: is deprecated and should not be used. It is slow and has unpredictable side effects. The recommended alternative is to use sendScriptMessage: to call a function in a ship's JavaScript ship script instead. scriptActionOnTarget: should not be used at all from scripts. An alternative is safeScriptActionOnTarget:, which is similar to scriptActionOnTarget: but has less side effects.", [AI currentlyRunningAIDescription]); + } + else + { + OOLog(@"script.deprecated.scriptActionOnTarget.repeat", @"----- WARNING in AI %@: the AI method scriptActionOnTarget: is deprecated and should not be used.", [AI currentlyRunningAIDescription]); + } +#endif + + if ([targEnt isShip]) + { + oldTarget = [player scriptTarget]; + [player setScriptTarget:(ShipEntity*)targEnt]; + [player runUnsanitizedScriptActions:[NSArray arrayWithObject:action] + allowingAIMethods:YES + withContextName:[NSString stringWithFormat:@"AI \"%@\" state %@", [[self getAI] name], [[self getAI] state]] + forTarget:targEnt]; + [player checkScript]; // react immediately to any changes this makes + [player setScriptTarget:oldTarget]; + } +} + + +- (void) safeScriptActionOnTarget:(NSString *)action +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + ShipEntity *targEnt = [self primaryTarget]; + ShipEntity *oldTarget = nil; + + if ([targEnt isShip]) + { + oldTarget = [player scriptTarget]; + [player setScriptTarget:(ShipEntity*)targEnt]; + [player runUnsanitizedScriptActions:[NSArray arrayWithObject:action] + allowingAIMethods:YES + withContextName:[NSString stringWithFormat:@"AI \"%@\" state %@", [[self getAI] name], [[self getAI] state]] + forTarget:targEnt]; + [player setScriptTarget:oldTarget]; + } +} + + +// Send own ship script a message. +- (void) sendScriptMessage:(NSString *)message +{ + NSArray *components = ScanTokensFromString(message); + if ([components count] == 1) + { + [self doScriptEvent:message]; + } + else + { + NSString *function = [components objectAtIndex:0]; + components = [components subarrayWithRange:NSMakeRange(1, [components count] - 1)]; + [self doScriptEvent:function withArgument:components]; + } +} + + +- (void) ai_throwSparks +{ + [self setThrowSparks:YES]; +} + + +// racing code TODO +- (void) targetFirstBeaconWithCode:(NSString*) code +{ + NSArray *all_beacons = [UNIVERSE listBeaconsWithCode: code]; + if ([all_beacons count]) + { + primaryTarget = [(ShipEntity*)[all_beacons objectAtIndex:0] universalID]; + [shipAI message:@"TARGET_FOUND"]; + } + else + [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void) targetNextBeaconWithCode:(NSString*) code +{ + NSArray *all_beacons = [UNIVERSE listBeaconsWithCode: code]; + ShipEntity *current_beacon = [UNIVERSE entityForUniversalID:primaryTarget]; + + if ((!current_beacon)||(![current_beacon isBeacon])) + { + [shipAI message:@"NO_CURRENT_BEACON"]; + [shipAI message:@"NOTHING_FOUND"]; + return; + } + + // find the current beacon in the list.. + OOUInteger i = [all_beacons indexOfObject:current_beacon]; + + if (i == NSNotFound) + { + [shipAI message:@"NOTHING_FOUND"]; + return; + } + + i++; // next index + + if (i < [all_beacons count]) + { + // locate current target in list + primaryTarget = [(ShipEntity*)[all_beacons objectAtIndex:i] universalID]; + [shipAI message:@"TARGET_FOUND"]; + } + else + { + [shipAI message:@"LAST_BEACON"]; + [shipAI message:@"NOTHING_FOUND"]; + } +} + + +- (void) setRacepointsFromTarget +{ + // two point - one at z - cr one at z + cr + ShipEntity *ship = [UNIVERSE entityForUniversalID:primaryTarget]; + if (ship == nil) + { + [shipAI message:@"NOTHING_FOUND"]; + return; + } + Vector k = ship->v_forward; + GLfloat c = ship->collision_radius; + Vector o = ship->position; + navpoints[0] = make_vector(o.x - c * k.x, o.y - c * k.y, o.z - c * k.z); + navpoints[1] = make_vector(o.x + c * k.x, o.y + c * k.y, o.z + c * k.z); + navpoints[2] = make_vector(o.x + 2.0 * c * k.x, o.y + 2.0 * c * k.y, o.z + 2.0 * c * k.z); + number_of_navpoints = 2; + next_navpoint_index = 0; + destination = navpoints[0]; + [shipAI message:@"RACEPOINTS_SET"]; +} + + +- (void) performFlyRacepoints +{ + next_navpoint_index = 0; + desired_range = collision_radius; + behaviour = BEHAVIOUR_FLY_THRU_NAVPOINTS; +} + +@end + + +@implementation ShipEntity (OOAIPrivate) + +- (void)performHyperSpaceExitReplace:(BOOL)replace +{ + // The [UNIVERSE nearbyDestinationsWithinRange:] method is very expensive, so cache + // its results. + static NSArray *sDests = nil; + + whole = nil; + + // get a list of destinations within range + if (sDests == nil) + { + sDests = [[UNIVERSE nearbyDestinationsWithinRange: 0.1 * fuel] copy]; + } + + int n_dests = [sDests count]; + + + // if none available report to the AI and exit + if (!n_dests) + { + [shipAI reactToMessage:@"WITCHSPACE UNAVAILABLE"]; + + // If no systems exist near us, the AI is switched to a different state, so we do not need + // the nearby destinations array anymore. + [sDests release]; + sDests = nil; + + return; + } + + // check if we're clear of nearby masses + ShipEntity *blocker = [UNIVERSE entityForUniversalID:[self checkShipsInVicinityForWitchJumpExit]]; + if (blocker) + { + found_target = [blocker universalID]; + [shipAI reactToMessage:@"WITCHSPACE BLOCKED"]; + return; + } + + // select one at random + int i = 0; + if (n_dests > 1) + i = ranrot_rand() % n_dests; + + NSString* systemSeedKey = [(NSDictionary*)[sDests objectAtIndex:i] objectForKey:@"system_seed"]; + Random_Seed targetSystem = RandomSeedFromString(systemSeedKey); + fuel -= 10 * [[(NSDictionary*)[sDests objectAtIndex:i] objectForKey:@"distance"] doubleValue]; + + // create wormhole + whole = [[[WormholeEntity alloc] initWormholeTo: targetSystem fromShip: self] autorelease]; + [UNIVERSE addEntity: whole]; + + // tell the ship we're about to jump (so it can inform escorts etc). + primaryTarget = [whole universalID]; + found_target = primaryTarget; + [shipAI reactToMessage:@"WITCHSPACE OKAY"]; // must be a reaction, the ship is about to disappear + + [self enterWormhole:whole replacing:replace]; // TODO + + // If we have reached this code, it means that the ship has already entered hyperspace, + // the destinations array is therefore no longer required and can be released. + [sDests release]; + sDests = nil; +} + + +- (void)scanForNearestShipWithPredicate:(EntityFilterPredicate)predicate parameter:(void *)parameter +{ + // Locates all the ships in range for which predicate returns YES, and chooses the nearest. + unsigned i; + ShipEntity *candidate; + float d2, found_d2 = scannerRange * scannerRange; + + found_target = NO_TARGET; + [self checkScanner]; + + if (predicate == NULL) return; + + for (i = 0; i < n_scanned_ships ; i++) + { + candidate = scanned_ships[i]; + d2 = distance2_scanned_ships[i]; + if ((d2 < found_d2) && (candidate->scanClass != CLASS_CARGO) && ([candidate status] != STATUS_DOCKED) && predicate(candidate, parameter)) + { + found_target = candidate->universalID; + found_d2 = d2; + } + } + + if (found_target != NO_TARGET) [shipAI message:@"TARGET_FOUND"]; + else [shipAI message:@"NOTHING_FOUND"]; +} + + +- (void)scanForNearestShipWithNegatedPredicate:(EntityFilterPredicate)predicate parameter:(void *)parameter +{ + ChainedEntityPredicateParameter param = { predicate, parameter }; + [self scanForNearestShipWithPredicate:NOTPredicate parameter:¶m]; +} + + +- (void) acceptDistressMessageFrom:(ShipEntity *)other +{ + found_target = [[other primaryTarget] universalID]; + switch (behaviour) + { + case BEHAVIOUR_ATTACK_TARGET : + case BEHAVIOUR_ATTACK_FLY_TO_TARGET : + case BEHAVIOUR_ATTACK_FLY_FROM_TARGET : + // busy - ignore the request + break; + + case BEHAVIOUR_FLEE_TARGET : + // scared - ignore the request; + break; + + default: + if ([self isPolice]) + [[UNIVERSE entityForUniversalID:found_target] markAsOffender:8]; // you have been warned!! + [shipAI reactToMessage:@"ACCEPT_DISTRESS_CALL"]; + break; + } +} + +@end + + +@implementation StationEntity (OOAIPrivate) + +- (void) acceptDistressMessageFrom:(ShipEntity *)other +{ + if (self != [UNIVERSE station]) return; + + int old_target = primaryTarget; + primaryTarget = [[other primaryTarget] universalID]; + [(ShipEntity *)[other primaryTarget] markAsOffender:8]; // mark their card + [self launchDefenseShip]; + primaryTarget = old_target; + +} + +@end + + +@implementation ShipEntity (OOAIStationStubs) + +// AI methods for stations, have no effect on normal ships. + +#define STATION_STUB_BASE(PROTO, NAME) PROTO { OOLog(@"ai.invalid.notAStation", @"Attempt to use station AI method \"%s\" on non-station %@.", NAME, self); } +#define STATION_STUB_NOARG(NAME) STATION_STUB_BASE(- (void) NAME, #NAME) +#define STATION_STUB_ARG(NAME) STATION_STUB_BASE(- (void) NAME (NSString *)param, #NAME) + +STATION_STUB_NOARG(increaseAlertLevel) +STATION_STUB_NOARG(decreaseAlertLevel) +STATION_STUB_NOARG(launchPolice) +STATION_STUB_NOARG(launchDefenseShip) +STATION_STUB_NOARG(launchScavenger) +STATION_STUB_NOARG(launchMiner) +STATION_STUB_NOARG(launchPirateShip) +STATION_STUB_NOARG(launchShuttle) +STATION_STUB_NOARG(launchTrader) +STATION_STUB_NOARG(launchEscort) +- (BOOL) launchPatrol +{ + OOLog(@"ai.invalid.notAStation", @"Attempt to use station AI method \"%s\" on non-station %@.", "launchPatrol", self); + return NO; +} +STATION_STUB_ARG(launchShipWithRole:) +STATION_STUB_NOARG(abortAllDockings) + +@end + diff --git a/src/Core/Entities/ShipEntityScriptMethods.h b/src/Core/Entities/ShipEntityScriptMethods.h new file mode 100644 index 00000000..15a26cc1 --- /dev/null +++ b/src/Core/Entities/ShipEntityScriptMethods.h @@ -0,0 +1,38 @@ +/* + +ShipEntityScriptMethods.h + +Methods for use by scripting mechanisms. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntity.h" + + +@interface ShipEntity (ScriptMethods) + +- (ShipEntity *) ejectShipOfType:(NSString *)shipKey; // Note: ship type, not role. +- (ShipEntity *) ejectShipOfRole:(NSString *)role; + +- (NSArray *) spawnShipsWithRole:(NSString *)role count:(OOUInteger)count; + +@end diff --git a/src/Core/Entities/ShipEntityScriptMethods.m b/src/Core/Entities/ShipEntityScriptMethods.m new file mode 100644 index 00000000..6b0179b2 --- /dev/null +++ b/src/Core/Entities/ShipEntityScriptMethods.m @@ -0,0 +1,95 @@ +/* + +ShipEntityScriptMethods.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntityScriptMethods.h" +#import "Universe.h" +#import "OOCollectionExtractors.h" + + +extern NSString * const kOOLogNoteAddShips; + + +@implementation ShipEntity (ScriptMethods) + +- (ShipEntity *) ejectShipOfType:(NSString *)shipKey +{ + ShipEntity *item = nil; + + if (shipKey != nil) + { + item = [UNIVERSE newShipWithName:shipKey]; + if (item != nil) [self dumpItem:item]; + } + + return item; +} + + +- (ShipEntity *) ejectShipOfRole:(NSString *)role +{ + ShipEntity *item = nil; + + if (role != nil) + { + item = [UNIVERSE newShipWithRole:role]; + if (item != nil) [self dumpItem:item]; + } + + return item; +} + + +- (NSArray *) spawnShipsWithRole:(NSString *)role count:(OOUInteger)count +{ + ShipEntity *spawned = nil; + NSMutableArray *result = nil; + + if (count == 0) return [NSArray array]; + + OOLog(kOOLogNoteAddShips, @"Spawning %d x '%@' near %@ %d", count, role, [self shortDescription], [self universalID]); + + result = [NSMutableArray arrayWithCapacity:count]; + + do + { + spawned = [UNIVERSE spawnShipWithRole:role near:self]; + if (spawned != nil) + { + [spawned setTemperature:[self temperature] * EJECTA_TEMP_FACTOR]; + if ([self isMissileFlagSet] && [[spawned shipInfoDictionary] boolForKey:@"is_submunition"]) + { + [spawned setOwner:[self owner]]; + [spawned addTarget:[self primaryTarget]]; + [spawned setIsMissileFlag:YES]; + } + [result addObject:spawned]; + } + } + while (--count); + + return result; +} + +@end diff --git a/src/Core/Entities/SkyEntity.h b/src/Core/Entities/SkyEntity.h new file mode 100644 index 00000000..7ba5881c --- /dev/null +++ b/src/Core/Entities/SkyEntity.h @@ -0,0 +1,42 @@ +/* + +SkyEntity.h + +Entity subclass implementing the game backdrop of stars and nebulae. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOEntityWithDrawable.h" + +@class OOColor; + + +@interface SkyEntity: OOEntityWithDrawable +{ + OOColor *skyColor; +} + +- (id) initWithColors:(OOColor *)col1 :(OOColor *)col2 andSystemInfo:(NSDictionary *)systemInfo; +- (BOOL) changeProperty:(NSString *)key withDictionary:(NSDictionary*) dict; + +- (OOColor *)skyColor; + +@end diff --git a/src/Core/Entities/SkyEntity.m b/src/Core/Entities/SkyEntity.m new file mode 100644 index 00000000..01a8e278 --- /dev/null +++ b/src/Core/Entities/SkyEntity.m @@ -0,0 +1,238 @@ +/* + +SkyEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "SkyEntity.h" +#import "OOSkyDrawable.h" +#import "PlayerEntity.h" + +#import "OOMaths.h" +#import "Universe.h" +#import "MyOpenGLView.h" +#import "OOColor.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" +#import "OOMaterial.h" + + +#define SKY_MAX_STARS 4800 +#define SKY_MAX_BLOBS 1280 +#define SKY_clusterChance 0.80 +#define SKY_alpha 0.10 +#define SKY_scale 10.0 + + +@interface SkyEntity (OOPrivate) + +- (void)readColor1:(OOColor **)ioColor1 andColor2:(OOColor **)ioColor2 fromDictionary:(NSDictionary *)dictionary; + +@end + + +@implementation SkyEntity + +- (id) initWithColors:(OOColor *) col1:(OOColor *) col2 andSystemInfo:(NSDictionary *) systemInfo +{ + OOSkyDrawable *skyDrawable; + float clusterChance, + alpha, + scale; + signed starCount, // Need to be able to hold -1... + nebulaCount; + unsigned starCountMultiplier, + nebulaCountMultiplier; + + self = [super init]; + if (self == nil) return nil; + + // Load colours + [self readColor1:&col1 andColor2:&col2 fromDictionary:systemInfo]; + + skyColor = [[OOColor colorWithDescription:[systemInfo objectForKey:@"sun_color"]] retain]; + if (skyColor == nil) + skyColor = [[col2 blendedColorWithFraction:0.5 ofColor:col1] retain]; + + // Load distribution values + clusterChance = [systemInfo floatForKey:@"sky_blur_cluster_chance" defaultValue:SKY_clusterChance]; + alpha = [systemInfo floatForKey:@"sky_blur_alpha" defaultValue:SKY_alpha]; + scale = [systemInfo floatForKey:@"sky_blur_scale" defaultValue:SKY_scale]; + + // Load star count + starCount = [systemInfo floatForKey:@"sky_n_stars" defaultValue:-1]; + starCountMultiplier = [systemInfo unsignedIntForKey:@"star_count_multiplier" defaultValue:1]; + nebulaCountMultiplier = [systemInfo unsignedIntForKey:@"nebula_count_multiplier" defaultValue:1]; + if (0 <= starCount) + { + starCount = MIN(SKY_MAX_STARS, starCount); + } + else + { + starCount = starCountMultiplier * SKY_MAX_STARS * 0.5 * randf() * randf(); + } + + // ...and sky count. (Note: simplifying this would change the appearance of stars/blobs.) + nebulaCount = [systemInfo floatForKey:@"sky_n_blurs" defaultValue:-1]; + if (0 <= nebulaCount) + { + nebulaCount = MIN(SKY_MAX_BLOBS, nebulaCount); + } + else + { + nebulaCount = nebulaCountMultiplier * SKY_MAX_BLOBS * 0.5 * randf() * randf(); + } + + if ([UNIVERSE reducedDetail]) starCount /= 2; // Halve the number of stars in the reduced detail setting. + + skyDrawable = [[OOSkyDrawable alloc] + initWithColor1:col1 + Color2:col2 + starCount:starCount + nebulaCount:nebulaCount + clusterFactor:clusterChance + alpha:alpha + scale:scale]; + [self setDrawable:skyDrawable]; + [skyDrawable release]; + + [self setStatus:STATUS_EFFECT]; + isSky = YES; + + return self; +} + + +- (void) dealloc +{ + [skyColor release]; + + [super dealloc]; +} + + +- (OOColor *) skyColor +{ + return skyColor; +} + + +- (BOOL) changeProperty:(NSString *)key withDictionary:(NSDictionary*) dict +{ + id object = [dict objectForKey:key]; + + // TODO: properties requiring reInit? + if ([key isEqualToString:@"sun_color"]) + { + OOColor *col=[[OOColor colorWithDescription:object] retain]; + if (col != nil) + { + [skyColor release]; + skyColor = [col copy]; + [col release]; + [UNIVERSE setLighting]; + } + } + else + { + OOLogWARN(@"script.warning", @"Change to property '%@' not applied, will apply only on leaving and re-entering this system.",key); + return NO; + } + return YES; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + zero_distance = MAX_CLEAR_DEPTH * MAX_CLEAR_DEPTH; + if (player != nil) position = player->position; +} + + +- (BOOL) canCollide +{ + return NO; +} + + +- (void) drawEntity:(BOOL) immediate :(BOOL) translucent +{ + if ([UNIVERSE breakPatternHide]) return; + + [super drawEntity:immediate :translucent]; + + CheckOpenGLErrors(@"SkyEntity after drawing %@", self); +} + +@end + + +@implementation SkyEntity (OOPrivate) + +- (void)readColor1:(OOColor **)ioColor1 andColor2:(OOColor **)ioColor2 fromDictionary:(NSDictionary *)dictionary +{ + NSString *string = nil; + NSArray *tokens = nil; + id colorDesc = nil; + OOColor *color = nil; + + assert(ioColor1 != NULL && ioColor2 != NULL); + + string = [dictionary stringForKey:@"sky_rgb_colors"]; + if (string != nil) + { + tokens = ScanTokensFromString(string); + + if ([tokens count] == 6) + { + float r1 = OOClamp_0_1_f([tokens floatAtIndex:0]); + float g1 = OOClamp_0_1_f([tokens floatAtIndex:1]); + float b1 = OOClamp_0_1_f([tokens floatAtIndex:2]); + float r2 = OOClamp_0_1_f([tokens floatAtIndex:3]); + float g2 = OOClamp_0_1_f([tokens floatAtIndex:4]); + float b2 = OOClamp_0_1_f([tokens floatAtIndex:5]); + *ioColor1 = [OOColor colorWithCalibratedRed:r1 green:g1 blue:b1 alpha:1.0]; + *ioColor2 = [OOColor colorWithCalibratedRed:r2 green:g2 blue:b2 alpha:1.0]; + } + else + { + OOLogWARN(@"sky.fromDict", @"could not interpret \"%@\" as two RGB colours (must be six numbers).", string); + } + } + colorDesc = [dictionary objectForKey:@"sky_color_1"]; + if (colorDesc != nil) + { + color = [[OOColor colorWithDescription:colorDesc] premultipliedColor]; + if (color != nil) *ioColor1 = color; + else OOLogWARN(@"sky.fromDict", @"could not interpret \"%@\" as a colour.", colorDesc); + } + colorDesc = [dictionary objectForKey:@"sky_color_2"]; + if (colorDesc != nil) + { + color = [[OOColor colorWithDescription:colorDesc] premultipliedColor]; + if (color != nil) *ioColor2 = color; + else OOLogWARN(@"sky.fromDict", @"could not interpret \"%@\" as a colour.", colorDesc); + } +} + +@end diff --git a/src/Core/Entities/StationEntity.h b/src/Core/Entities/StationEntity.h new file mode 100644 index 00000000..52ee913a --- /dev/null +++ b/src/Core/Entities/StationEntity.h @@ -0,0 +1,198 @@ +/* + +StationEntity.h + +ShipEntity subclass representing a space station or dockable ship. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntity.h" +#import "legacy_random.h" + +typedef enum +{ + STATION_ALERT_LEVEL_GREEN = ALERT_CONDITION_GREEN, + STATION_ALERT_LEVEL_YELLOW = ALERT_CONDITION_YELLOW, + STATION_ALERT_LEVEL_RED = ALERT_CONDITION_RED +} OOStationAlertLevel; + +#define STATION_MAX_POLICE 8 + +#define STATION_DELAY_BETWEEN_LAUNCHES 6.0 + +#define MAX_DOCKING_STAGES 16 + +#define DOCKING_CLEARANCE_WINDOW 126.0 + +@interface StationEntity: ShipEntity +{ + + NSMutableDictionary *shipsOnApproach; + NSMutableDictionary *shipsOnHold; + NSMutableArray *launchQueue; + double last_launch_time; + double approach_spacing; + OOStationAlertLevel alertLevel; + + OOUniversalID id_lock[MAX_DOCKING_STAGES]; // ship id's or NO_TARGET's + + unsigned max_police; // max no. of police ships allowed + unsigned max_defense_ships; // max no. of defense ships allowed + unsigned police_launched; + + unsigned max_scavengers; // max no. of scavenger ships allowed + unsigned scavengers_launched; + + OOTechLevelID equivalentTechLevel; + float equipmentPriceFactor; + + Vector port_position; + Quaternion port_orientation; + Vector port_dimensions; + ShipEntity *port_model; + + unsigned no_docking_while_launching: 1, + hasNPCTraffic: 1; + + OOUniversalID planet; + + NSMutableArray *localMarket; + NSMutableArray *localPassengers; + NSMutableArray *localContracts; + NSMutableArray *localShipyard; + + unsigned docked_shuttles; + double last_shuttle_launch_time; + double shuttle_launch_interval; + + unsigned docked_traders; + double last_trader_launch_time; + double trader_launch_interval; + + double last_patrol_report_time; + double patrol_launch_interval; +#if DOCKING_CLEARANCE_ENABLED + BOOL requiresDockingClearance; +#endif + +} + +- (void) setDockingPortModel:(ShipEntity*) dock_model :(Vector) dock_pos :(Quaternion) dock_q; + +- (NSMutableArray *) localMarket; +- (void) setLocalMarket:(NSArray *) some_market; +- (NSMutableArray *) localPassengers; +- (void) setLocalPassengers:(NSArray *) some_market; +- (NSMutableArray *) localContracts; +- (void) setLocalContracts:(NSArray *) some_market; +- (NSMutableArray *) localShipyard; +- (void) setLocalShipyard:(NSArray *) some_market; + +- (NSMutableArray *) initialiseLocalMarketWithSeed: (Random_Seed) s_seed andRandomFactor: (int) random_factor; +- (NSMutableArray *) initialiseLocalPassengersWithSeed: (Random_Seed) s_seed andRandomFactor: (int) random_factor; +- (NSMutableArray *) initialiseLocalContractsWithSeed: (Random_Seed) s_seed andRandomFactor: (int) random_factor; + +- (OOTechLevelID) equivalentTechLevel; +- (void) setEquivalentTechLevel:(OOTechLevelID) value; + +- (double) port_radius; + +- (Vector) getPortPosition; + +- (Vector) getBeaconPosition; + +- (float) equipmentPriceFactor; + +- (void) setPlanet:(PlanetEntity *)planet_entity; + +- (PlanetEntity *) planet; + +- (void) sanityCheckShipsOnApproach; + +- (void) autoDockShipsOnApproach; + +- (NSDictionary *) dockingInstructionsForShip:(ShipEntity *) ship; +- (void) addShipToShipsOnApproach:(ShipEntity *) ship; + +- (Vector) portUpVector; +- (Vector) portUpVectorForShipsBoundingBox:(BoundingBox) bb; + +- (BOOL) shipIsInDockingCorridor:(ShipEntity*) ship; + +- (BOOL) dockingCorridorIsEmpty; + +- (void) clearDockingCorridor; + +- (void) clear; + + +- (void) abortAllDockings; + +- (void) abortDockingForShip:(ShipEntity *) ship; + +- (void) addShipToLaunchQueue:(ShipEntity *) ship; + +- (unsigned) countShipsInLaunchQueueWithPrimaryRole:(NSString *)role; + +- (void) launchShip:(ShipEntity *) ship; + +- (void) launchIndependentShip:(NSString*) role; + +- (void) launchIndependentShipWithRole:(NSString*) role; + +- (void) noteDockedShip:(ShipEntity *) ship; + +- (BOOL)hasNPCTraffic; +- (void)setHasNPCTraffic:(BOOL)flag; + +- (OOStationAlertLevel) alertLevel; +- (void) setAlertLevel:(OOStationAlertLevel)level signallingScript:(BOOL)signallingScript; + +////////////////////////////////////////////////////////////// AI methods... + +- (void) increaseAlertLevel; +- (void) decreaseAlertLevel; + +- (void) launchPolice; +- (void) launchDefenseShip; +- (void) launchScavenger; +- (void) launchMiner; +/**Lazygun** added the following line*/ +- (void) launchPirateShip; +- (void) launchShuttle; +- (void) launchTrader; +- (void) launchEscort; +- (BOOL) launchPatrol; + +- (void) launchShipWithRole:(NSString*) role; + +- (void) acceptPatrolReportFrom:(ShipEntity*) patrol_ship; + +#if DOCKING_CLEARANCE_ENABLED +- (NSString *) acceptDockingClearanceRequestFrom:(ShipEntity *)other; +- (BOOL) requiresDockingClearance; +- (void) setRequiresDockingClearance:(BOOL)newValue; +#endif + +- (BOOL) isRotatingStation; +- (BOOL) hasShipyard; + +@end diff --git a/src/Core/Entities/StationEntity.m b/src/Core/Entities/StationEntity.m new file mode 100644 index 00000000..4079422a --- /dev/null +++ b/src/Core/Entities/StationEntity.m @@ -0,0 +1,2169 @@ +/* + +StationEntity.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "StationEntity.h" +#import "ShipEntityAI.h" +#import "OOCollectionExtractors.h" +#import "OOStringParsing.h" + +#import "Universe.h" +#import "HeadUpDisplay.h" + +#import "PlayerEntityLegacyScriptEngine.h" +#import "PlanetEntity.h" +#import "ParticleEntity.h" +#import "OOShipGroup.h" + +#import "AI.h" +#import "OOCharacter.h" + +#import "OOScript.h" +#import "OODebugGLDrawing.h" + +#define kOOLogUnconvertedNSLog @"unclassified.StationEntity" + + +static NSDictionary* instructions(int station_id, Vector coords, float speed, float range, NSString* ai_message, NSString* comms_message, BOOL match_rotation); + + +@implementation StationEntity + +- (OOTechLevelID) equivalentTechLevel +{ + return equivalentTechLevel; +} + + +- (void) setEquivalentTechLevel:(OOTechLevelID) value +{ + equivalentTechLevel = value; +} + + +- (double) port_radius +{ + return magnitude(port_position); +} + + +- (Vector) getPortPosition +{ + Vector result = position; + result.x += port_position.x * v_right.x + port_position.y * v_up.x + port_position.z * v_forward.x; + result.y += port_position.x * v_right.y + port_position.y * v_up.y + port_position.z * v_forward.y; + result.z += port_position.x * v_right.z + port_position.y * v_up.z + port_position.z * v_forward.z; + return result; +} + + +- (Vector) getBeaconPosition +{ + double buoy_distance = 10000.0; // distance from station entrance + Vector result = position; + Vector v_f = vector_forward_from_quaternion(orientation); + result.x += buoy_distance * v_f.x; + result.y += buoy_distance * v_f.y; + result.z += buoy_distance * v_f.z; + return result; +} + + +- (float) equipmentPriceFactor +{ + return equipmentPriceFactor; +} + + +- (NSMutableArray *) localMarket +{ + return localMarket; +} + + +- (void) setLocalMarket:(NSArray *) some_market +{ + if (localMarket) + [localMarket release]; + localMarket = [[NSMutableArray alloc] initWithArray:some_market]; +} + + +- (NSMutableArray *) localPassengers +{ + return localPassengers; +} + + +- (void) setLocalPassengers:(NSArray *) some_market +{ + if (localPassengers) + [localPassengers release]; + localPassengers = [[NSMutableArray alloc] initWithArray:some_market]; +} + + +- (NSMutableArray *) localContracts +{ + return localContracts; +} + + +- (void) setLocalContracts:(NSArray *) some_market +{ + if (localContracts) + [localContracts release]; + localContracts = [[NSMutableArray alloc] initWithArray:some_market]; +} + + +- (NSMutableArray *) localShipyard +{ + return localShipyard; +} + + +- (void) setLocalShipyard:(NSArray *) some_market +{ + if (localShipyard) + [localShipyard release]; + localShipyard = [[NSMutableArray alloc] initWithArray:some_market]; +} + + +- (NSMutableArray *) initialiseLocalMarketWithSeed: (Random_Seed) s_seed andRandomFactor: (int) random_factor +{ + int rf = (random_factor ^ universalID) & 0xff; + int economy = [[UNIVERSE generateSystemData:s_seed] intForKey:KEY_ECONOMY]; + if (localMarket) + [localMarket release]; + localMarket = [[NSMutableArray alloc] initWithArray:[UNIVERSE commodityDataForEconomy:economy andStation:self andRandomFactor:rf]]; + return localMarket; +} + + +- (NSMutableArray *) initialiseLocalPassengersWithSeed: (Random_Seed) s_seed andRandomFactor: (int) random_factor +{ + if (localPassengers) + [localPassengers release]; + localPassengers = [[NSMutableArray alloc] initWithArray:[UNIVERSE passengersForSystem:s_seed atTime:[[[PlayerEntity sharedPlayer] clock_number] intValue]]]; + return localPassengers; +} + + +- (NSMutableArray *) initialiseLocalContractsWithSeed: (Random_Seed) s_seed andRandomFactor: (int) random_factor +{ + if (localContracts) + [localContracts release]; + localContracts = [[NSMutableArray alloc] initWithArray:[UNIVERSE contractsForSystem:s_seed atTime:[[[PlayerEntity sharedPlayer] clock_number] intValue]]]; + return localContracts; +} + + +- (void) setPlanet:(PlanetEntity *)planet_entity +{ + if (planet_entity) + planet = [planet_entity universalID]; + else + planet = NO_TARGET; +} + + +- (PlanetEntity *) planet +{ + return [UNIVERSE entityForUniversalID:planet]; +} + + +- (void) sanityCheckShipsOnApproach +{ + unsigned i; + NSArray* ships = [shipsOnApproach allKeys]; + + // Remove dead entities. + // No enumerator because we mutate the dictionary. + for (i = 0; i < [ships count]; i++) + { + int sid = [[ships objectAtIndex:i] intValue]; + if ((sid == NO_TARGET)||(![UNIVERSE entityForUniversalID:sid])) + { + [shipsOnApproach removeObjectForKey:[ships objectAtIndex:i]]; + if ([shipsOnApproach count] == 0) + [shipAI message:@"DOCKING_COMPLETE"]; + } + } + + if ([shipsOnApproach count] == 0) + { + if (last_launch_time < [UNIVERSE getTime]) + { + last_launch_time = [UNIVERSE getTime]; + } + approach_spacing = 0.0; + } + + ships = [shipsOnHold allKeys]; + for (i = 0; i < [ships count]; i++) + { + int sid = [[ships objectAtIndex:i] intValue]; + if ((sid == NO_TARGET)||(![UNIVERSE entityForUniversalID:sid])) + { + [shipsOnHold removeObjectForKey:[ships objectAtIndex:i]]; + } + } +} + + +// Exposed to AI +- (void) abortAllDockings +{ + unsigned i; + NSArray* ships = [shipsOnApproach allKeys]; + for (i = 0; i < [ships count]; i++) + { + int sid = [[ships objectAtIndex:i] intValue]; + if ([UNIVERSE entityForUniversalID:sid]) + [[[UNIVERSE entityForUniversalID:sid] getAI] message:@"DOCKING_ABORTED"]; + } + [shipsOnApproach removeAllObjects]; + +#if DOCKING_CLEARANCE_ENABLED + PlayerEntity *player = [PlayerEntity sharedPlayer]; + BOOL isDockingStation = (self == [player getTargetDockStation]); + if (isDockingStation && player && [player status] == STATUS_IN_FLIGHT && + [player getDockingClearanceStatus] >= DOCKING_CLEARANCE_STATUS_REQUESTED) + { + [self sendExpandedMessage:DESC(@"docking-clearance-abort-cancelled") toShip:player]; + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; + } +#endif + + ships = [shipsOnHold allKeys]; + for (i = 0; i < [ships count]; i++) + { + int sid = [[ships objectAtIndex:i] intValue]; + if ([UNIVERSE entityForUniversalID:sid]) + [[[UNIVERSE entityForUniversalID:sid] getAI] message:@"DOCKING_ABORTED"]; + } + [shipsOnHold removeAllObjects]; + + [shipAI message:@"DOCKING_COMPLETE"]; + last_launch_time = [UNIVERSE getTime]; + approach_spacing = 0.0; +} + + +- (void) autoDockShipsOnApproach +{ + unsigned i; + NSArray* ships = [shipsOnApproach allKeys]; + for (i = 0; i < [ships count]; i++) + { + int sid = [(NSString *)[ships objectAtIndex:i] intValue]; + if ([UNIVERSE entityForUniversalID:sid]) + [[UNIVERSE entityForUniversalID:sid] enterDock:self]; + } + [shipsOnApproach removeAllObjects]; + + ships = [shipsOnHold allKeys]; + for (i = 0; i < [ships count]; i++) + { + int sid = [(NSString *)[ships objectAtIndex:i] intValue]; + if ([UNIVERSE entityForUniversalID:sid]) + [[UNIVERSE entityForUniversalID:sid] enterDock:self]; + } + [shipsOnHold removeAllObjects]; + + [shipAI message:@"DOCKING_COMPLETE"]; +} + +static NSDictionary* instructions(int station_id, Vector coords, float speed, float range, NSString* ai_message, NSString* comms_message, BOOL match_rotation) +{ + NSMutableDictionary* acc = [NSMutableDictionary dictionaryWithCapacity:8]; + [acc setObject:[NSString stringWithFormat:@"%.2f %.2f %.2f", coords.x, coords.y, coords.z] forKey:@"destination"]; + [acc setObject:[NSNumber numberWithFloat:speed] forKey:@"speed"]; + [acc setObject:[NSNumber numberWithFloat:range] forKey:@"range"]; + [acc setObject:[NSNumber numberWithInt:station_id] forKey:@"station_id"]; + [acc setObject:[NSNumber numberWithBool:match_rotation] forKey:@"match_rotation"]; + if (ai_message) + [acc setObject:ai_message forKey:@"ai_message"]; + if (comms_message) + [acc setObject:comms_message forKey:@"comms_message"]; + // + return [NSDictionary dictionaryWithDictionary:acc]; +} + +// this routine does more than set coordinates - it provides a whole set of docking instructions and messages at each stage.. +// +- (NSDictionary *) dockingInstructionsForShip:(ShipEntity *) ship +{ + Vector coords; + + int ship_id = [ship universalID]; + NSNumber *shipID = [NSNumber numberWithUnsignedShort:ship_id]; + + Vector launchVector = vector_forward_from_quaternion(quaternion_multiply(port_orientation, orientation)); + Vector temp = (fabsf(launchVector.x) < 0.8)? make_vector(1,0,0) : make_vector(0,1,0); + temp = cross_product(launchVector, temp); // 90 deg to launchVector & temp + Vector vi = cross_product(launchVector, temp); + Vector vj = cross_product(launchVector, vi); + Vector vk = launchVector; + + if (!ship) + return nil; + + if ((ship->isPlayer)&&([ship legalStatus] > 50)) // note: non-player fugitives dock as normal + { + // refuse docking to the fugitive player + return instructions(universalID, ship->position, 0, 100, @"DOCKING_REFUSED", @"[station-docking-refused-to-fugitive]", NO); + } + + if (no_docking_while_launching) + { + return instructions(universalID, ship->position, 0, 100, @"TRY_AGAIN_LATER", nil, NO); + } + +#if DOCKING_CLEARANCE_ENABLED + // If the ship is not on its docking approach and the player has + // requested or even been granted docking clearance, then tell the + // ship to wait. + PlayerEntity *player = [PlayerEntity sharedPlayer]; + BOOL isDockingStation = self == [player getTargetDockStation]; + if (isDockingStation && ![shipsOnApproach objectForKey:shipID] && + player && [player status] == STATUS_IN_FLIGHT && + [player getDockingClearanceStatus] >= DOCKING_CLEARANCE_STATUS_REQUESTED) + { + return instructions(universalID, ship->position, 0, 100, @"TRY_AGAIN_LATER", nil, NO); + } +#endif + + [shipAI reactToMessage:@"DOCKING_REQUESTED"]; // react to the request + + if (magnitude2([self velocity]) > 1.0) // no docking while moving + { + if (![shipsOnHold objectForKey:shipID]) + [self sendExpandedMessage: @"[station-acknowledges-hold-position]" toShip: ship]; + [shipsOnHold setObject: shipID forKey: shipID]; + [self performStop]; + return instructions(universalID, ship->position, 0, 100, @"HOLD_POSITION", nil, NO); + } + + if (fabs(flightPitch) > 0.01) // no docking while pitching + { + if (![shipsOnHold objectForKey:shipID]) + [self sendExpandedMessage: @"[station-acknowledges-hold-position]" toShip: ship]; + [shipsOnHold setObject: shipID forKey: shipID]; + [self performStop]; + return instructions(universalID, ship->position, 0, 100, @"HOLD_POSITION", nil, NO); + } + + // rolling is okay for some + if (fabs(flightPitch) > 0.01) // rolling + { + Vector portPos = [self getPortPosition]; + Vector portDir = vector_forward_from_quaternion(port_orientation); + BOOL isOffCentre = (fabs(portPos.x) + fabs(portPos.y) > 0.0f)|(fabs(portDir.x) + fabs(portDir.y) > 0.0f); + BOOL isRotatingStation = [shipinfoDictionary boolForKey:@"rotating" defaultValue:NO]; + if ((!isRotatingStation)&&(isOffCentre)) + { + if (![shipsOnHold objectForKey:shipID]) + [self sendExpandedMessage: @"[station-acknowledges-hold-position]" toShip: ship]; + [shipsOnHold setObject: shipID forKey: shipID]; + [self performStop]; + return instructions(universalID, ship->position, 0, 100, @"HOLD_POSITION", nil, NO); + } + } + + // we made it thorugh holding! + // + if ([shipsOnHold objectForKey:shipID]) + [shipsOnHold removeObjectForKey:shipID]; + + // check if this is a new ship on approach + // + if (![shipsOnApproach objectForKey:shipID]) + { + Vector delta = ship->position; + delta.x -= position.x; delta.y -= position.y; delta.z -= position.z; + float ship_distance = sqrt(magnitude2(delta)); + + [self addShipToShipsOnApproach: ship]; + + if (ship_distance < 1000.0 + collision_radius + ship->collision_radius) // too close - back off + return instructions(universalID, position, 0, 5000, @"BACK_OFF", nil, NO); + + if (ship_distance > 12500.0) // long way off - approach more closely + return instructions(universalID, position, 0, 10000, @"APPROACH", nil, NO); + } + + if (![shipsOnApproach objectForKey:shipID]) + { + // some error has occurred - log it, and send the try-again message + OOLogERR(@"station.issueDockingInstructions.failed", @"couldn't addShipToShipsOnApproach:%@ in %@, retrying later -- shipsOnApproach:\n%@", ship, self, shipsOnApproach); + // + return instructions(universalID, ship->position, 0, 100, @"TRY_AGAIN_LATER", nil, NO); + } + + + // shipsOnApproach now has an entry for the ship. + // + NSMutableArray* coordinatesStack = [shipsOnApproach objectForKey:shipID]; + + if ([coordinatesStack count] == 0) + { + OOLogERR(@"station.issueDockingInstructions.failed", @" -- coordinatesStack = %@", coordinatesStack); + + return instructions(universalID, ship->position, 0, 100, @"HOLD_POSITION", nil, NO); + } + + // get the docking information from the instructions + NSMutableDictionary* nextCoords = (NSMutableDictionary *)[coordinatesStack objectAtIndex:0]; + int docking_stage = [(NSNumber *)[nextCoords objectForKey:@"docking_stage"] intValue]; + float speedAdvised = [(NSNumber *)[nextCoords objectForKey:@"speed"] floatValue]; + float rangeAdvised = [(NSNumber *)[nextCoords objectForKey:@"range"] floatValue]; + BOOL match_rotation = ([nextCoords objectForKey:@"match_rotation"] != nil); + NSString* comms_message = [nextCoords stringForKey:@"comms_message"]; + + // calculate world coordinates from relative coordinates + Vector rel_coords; + rel_coords.x = [(NSNumber *)[nextCoords objectForKey:@"rx"] floatValue]; + rel_coords.y = [(NSNumber *)[nextCoords objectForKey:@"ry"] floatValue]; + rel_coords.z = [(NSNumber *)[nextCoords objectForKey:@"rz"] floatValue]; + coords = [self getPortPosition]; + coords.x += rel_coords.x * vi.x + rel_coords.y * vj.x + rel_coords.z * vk.x; + coords.y += rel_coords.x * vi.y + rel_coords.y * vj.y + rel_coords.z * vk.y; + coords.z += rel_coords.x * vi.z + rel_coords.y * vj.z + rel_coords.z * vk.z; + + // check if the ship is at the control point + double max_allowed_range = 2.0 * rangeAdvised + ship->collision_radius; // maximum distance permitted from control point - twice advised range + Vector delta = ship->position; + delta.x -= coords.x; delta.y -= coords.y; delta.z -= coords.z; + + if (magnitude2(delta) > max_allowed_range * max_allowed_range) // too far from the coordinates - do not remove them from the stack! + { + if ((docking_stage == 1) &&(magnitude2(delta) < 1000000.0)) // 1km*1km + speedAdvised *= 0.5; // half speed + + return instructions(universalID, coords, speedAdvised, rangeAdvised, @"APPROACH_COORDINATES", nil, NO); + } + else + { + // reached the current coordinates okay.. + + // get the NEXT coordinates + nextCoords = (NSMutableDictionary *)[coordinatesStack objectAtIndex:1]; + docking_stage = [(NSNumber *)[nextCoords objectForKey:@"docking_stage"] intValue]; + speedAdvised = [(NSNumber *)[nextCoords objectForKey:@"speed"] floatValue]; + rangeAdvised = [(NSNumber *)[nextCoords objectForKey:@"range"] floatValue]; + match_rotation = ([nextCoords objectForKey:@"match_rotation"] != nil); + comms_message = [nextCoords stringForKey:@"comms_message"]; + + if (comms_message) + [self sendExpandedMessage: comms_message toShip: ship]; + + // calculate world coordinates from relative coordinates + rel_coords.x = [(NSNumber *)[nextCoords objectForKey:@"rx"] floatValue]; + rel_coords.y = [(NSNumber *)[nextCoords objectForKey:@"ry"] floatValue]; + rel_coords.z = [(NSNumber *)[nextCoords objectForKey:@"rz"] floatValue]; + coords = [self getPortPosition]; + coords.x += rel_coords.x * vi.x + rel_coords.y * vj.x + rel_coords.z * vk.x; + coords.y += rel_coords.x * vi.y + rel_coords.y * vj.y + rel_coords.z * vk.y; + coords.z += rel_coords.x * vi.z + rel_coords.y * vj.z + rel_coords.z * vk.z; + + if ((id_lock[docking_stage] == NO_TARGET) + &&(id_lock[docking_stage + 1] == NO_TARGET) + &&(id_lock[docking_stage + 2] == NO_TARGET)) // check three stages ahead + { + // approach is clear - move to next position + // + int i; // clear any previously owned docking stages + for (i = 1; i < MAX_DOCKING_STAGES; i++) + if ((id_lock[i] == ship_id)||([UNIVERSE entityForUniversalID:id_lock[i]] == nil)) + id_lock[i] = NO_TARGET; + + if (docking_stage > 1) // don't claim first docking stage + id_lock[docking_stage] = ship_id; // otherwise - claim this docking stage + + //remove the previous stage from the stack + [coordinatesStack removeObjectAtIndex:0]; + + return instructions(universalID, coords, speedAdvised, rangeAdvised, @"APPROACH_COORDINATES", nil, match_rotation); + } + else + { + // approach isn't clear - hold position.. + // + [[ship getAI] message:@"HOLD_POSITION"]; + + if (![nextCoords objectForKey:@"hold_message_given"]) + { + // COMM-CHATTER + [UNIVERSE clearPreviousMessage]; + [self sendExpandedMessage: @"[station-hold-position]" toShip: ship]; + [nextCoords setObject:@"YES" forKey:@"hold_message_given"]; + } + + return instructions(universalID, ship->position, 0, 100, @"HOLD_POSITION", nil, NO); + } + } + + // we should never reach here. + return instructions(universalID, coords, 50, 10, @"APPROACH_COORDINATES", nil, NO); +} + + +- (void) addShipToShipsOnApproach:(ShipEntity *) ship +{ + int corridor_distance[] = { -1, 1, 3, 5, 7, 9, 11, 12, 12}; + int corridor_offset[] = { 0, 0, 0, 0, 0, 0, 1, 3, 12}; + int corridor_speed[] = { 48, 48, 48, 48, 36, 48, 64, 128, 512}; // how fast to approach the next point + int corridor_range[] = { 24, 12, 6, 4, 4, 6, 15, 38, 96}; // how close you have to get to the target point + int corridor_rotate[] = { 1, 1, 1, 1, 0, 0, 0, 0, 0}; // whether to match the station rotation + int corridor_count = 9; + int corridor_final_approach = 3; + + NSNumber *shipID = [NSNumber numberWithUnsignedShort:[ship universalID]]; + + Vector launchVector = vector_forward_from_quaternion(quaternion_multiply(port_orientation, orientation)); + Vector temp = (fabsf(launchVector.x) < 0.8)? make_vector(1,0,0) : make_vector(0,1,0); + temp = cross_product(launchVector, temp); // 90 deg to launchVector & temp + Vector rightVector = cross_product(launchVector, temp); + Vector upVector = cross_product(launchVector, rightVector); + + // will select a direction for offset based on the entity personality (was ship ID) + int offset_id = [ship entityPersonalityInt] & 0xf; // 16 point compass + double c = cos(offset_id * M_PI * ONE_EIGHTH); + double s = sin(offset_id * M_PI * ONE_EIGHTH); + + // test if this points at the ship + Vector point1 = [self getPortPosition]; + point1.x += launchVector.x * corridor_offset[corridor_count - 1]; + point1.y += launchVector.x * corridor_offset[corridor_count - 1]; + point1.z += launchVector.x * corridor_offset[corridor_count - 1]; + Vector alt1 = point1; + point1.x += c * upVector.x * corridor_offset[corridor_count - 1] + s * rightVector.x * corridor_offset[corridor_count - 1]; + point1.y += c * upVector.y * corridor_offset[corridor_count - 1] + s * rightVector.y * corridor_offset[corridor_count - 1]; + point1.z += c * upVector.z * corridor_offset[corridor_count - 1] + s * rightVector.z * corridor_offset[corridor_count - 1]; + alt1.x -= c * upVector.x * corridor_offset[corridor_count - 1] + s * rightVector.x * corridor_offset[corridor_count - 1]; + alt1.y -= c * upVector.y * corridor_offset[corridor_count - 1] + s * rightVector.y * corridor_offset[corridor_count - 1]; + alt1.z -= c * upVector.z * corridor_offset[corridor_count - 1] + s * rightVector.z * corridor_offset[corridor_count - 1]; + if (distance2(alt1, ship->position) < distance2(point1, ship->position)) + { + s = -s; + c = -c; // turn 180 degrees + } + + // + NSMutableArray* coordinatesStack = [NSMutableArray arrayWithCapacity: MAX_DOCKING_STAGES]; + double port_depth = 250; // 250m deep standard port + // + int i; + for (i = corridor_count - 1; i >= 0; i--) + { + NSMutableDictionary* nextCoords = [NSMutableDictionary dictionaryWithCapacity:3]; + + int offset = corridor_offset[i]; + + // space out first coordinate further if there are many ships + if ((i == corridor_count - 1) && offset) + offset += approach_spacing / port_depth; + + [nextCoords setObject:[NSNumber numberWithInt: corridor_count - i] forKey:@"docking_stage"]; + + [nextCoords setObject:[NSNumber numberWithFloat: s * port_depth * offset] forKey:@"rx"]; + [nextCoords setObject:[NSNumber numberWithFloat: c * port_depth * offset] forKey:@"ry"]; + [nextCoords setObject:[NSNumber numberWithFloat: port_depth * corridor_distance[i]] forKey:@"rz"]; + + [nextCoords setObject:[NSNumber numberWithFloat: corridor_speed[i]] forKey:@"speed"]; + + [nextCoords setObject:[NSNumber numberWithFloat: corridor_range[i]] forKey:@"range"]; + + if (corridor_rotate[i]) + [nextCoords setObject:@"YES" forKey:@"match_rotation"]; + + if (i == corridor_final_approach) + { + if (self == [UNIVERSE station]) + [nextCoords setObject:@"[station-begin-final-aproach]" forKey:@"comms_message"]; + else + [nextCoords setObject:@"[docking-begin-final-aproach]" forKey:@"comms_message"]; + } + + [coordinatesStack addObject:nextCoords]; + } + + [shipsOnApproach setObject:coordinatesStack forKey:shipID]; + + approach_spacing += 500; // space out incoming ships by 500m + + // COMM-CHATTER + if (self == [UNIVERSE station]) + [self sendExpandedMessage: @"[station-welcome]" toShip: ship]; + else + [self sendExpandedMessage: @"[docking-welcome]" toShip: ship]; + +} + + +- (void) abortDockingForShip:(ShipEntity *) ship +{ + int ship_id = [ship universalID]; + NSNumber *shipID = [NSNumber numberWithUnsignedShort:ship_id]; + if ([UNIVERSE entityForUniversalID:[ship universalID]]) + [[[UNIVERSE entityForUniversalID:[ship universalID]] getAI] message:@"DOCKING_ABORTED"]; + + if ([shipsOnHold objectForKey:shipID]) + [shipsOnHold removeObjectForKey:shipID]; + + if ([shipsOnApproach objectForKey:shipID]) + { + [shipsOnApproach removeObjectForKey:shipID]; + if ([shipsOnApproach count] == 0) + [shipAI message:@"DOCKING_COMPLETE"]; + } + + int i; // clear any previously owned docking stages + for (i = 1; i < MAX_DOCKING_STAGES; i++) + if ((id_lock[i] == ship_id)||([UNIVERSE entityForUniversalID:id_lock[i]] == nil)) + id_lock[i] = NO_TARGET; + +} + + +- (Vector) portUpVector +{ + if (port_dimensions.x > port_dimensions.y) + { + return vector_up_from_quaternion(quaternion_multiply(port_orientation, orientation)); + } + else + { + return vector_right_from_quaternion(quaternion_multiply(port_orientation, orientation)); + } +} + + +- (Vector) portUpVectorForShipsBoundingBox:(BoundingBox) bb +{ + BOOL twist = ((port_dimensions.x < port_dimensions.y) ^ (bb.max.x - bb.min.x < bb.max.y - bb.min.y)); + + if (!twist) + { + return vector_up_from_quaternion(quaternion_multiply(port_orientation, orientation)); + } + else + { + return vector_right_from_quaternion(quaternion_multiply(port_orientation, orientation)); + } +} + + +//////////////////////////////////////////////// from superclass + +- (id) initWithDictionary:(NSDictionary *) dict +{ + isStation = YES; + + shipsOnApproach = [[NSMutableDictionary alloc] init]; + shipsOnHold = [[NSMutableDictionary alloc] init]; + launchQueue = [[NSMutableArray alloc] init]; + + self = [super initWithDictionary:dict]; + + return self; +} + + +- (void) dealloc +{ + [shipsOnApproach release]; + [shipsOnHold release]; + [launchQueue release]; + + [localMarket release]; + [localPassengers release]; + [localContracts release]; + [localShipyard release]; + + [super dealloc]; +} + + +- (BOOL) setUpShipFromDictionary:(NSDictionary *) dict +{ + + isShip = YES; + isStation = YES; + alertLevel = STATION_ALERT_LEVEL_GREEN; + + double port_radius = [dict nonNegativeDoubleForKey:@"port_radius" defaultValue:500.0]; + port_position = make_vector(0, 0, port_radius); + port_orientation = kIdentityQuaternion; + + // port_dimensions can be set for rock-hermits and other specials + port_dimensions = make_vector(69, 69, 250); + NSString *portDimensionsStr = [dict stringForKey:@"port_dimensions"]; + if (portDimensionsStr != nil) // this can be set for rock-hermits and other specials + { + NSArray* tokens = [portDimensionsStr componentsSeparatedByString:@"x"]; + if ([tokens count] == 3) + { + port_dimensions = make_vector([[tokens objectAtIndex:0] floatValue], + [[tokens objectAtIndex:1] floatValue], + [[tokens objectAtIndex:2] floatValue]); + } + } + + if (![super setUpShipFromDictionary:dict]) return NO; + + equivalentTechLevel = [dict unsignedIntegerForKey:@"equivalent_tech_level" defaultValue:NSNotFound]; + max_scavengers = [dict unsignedIntForKey:@"max_scavengers" defaultValue:3]; + max_defense_ships = [dict unsignedIntForKey:@"max_defense_ships" defaultValue:3]; + max_police = [dict unsignedIntForKey:@"max_police" defaultValue:STATION_MAX_POLICE]; + equipmentPriceFactor = [dict nonNegativeFloatForKey:@"equipment_price_factor" defaultValue:1.0]; + equipmentPriceFactor = OOMax_f(equipmentPriceFactor, 0.5f); + hasNPCTraffic = [dict fuzzyBooleanForKey:@"has_npc_traffic" defaultValue:YES]; +#if DOCKING_CLEARANCE_ENABLED + // Non main stations may have requiresDockingClearance set to yes as a result of the code below, + // but this variable should be irrelevant for them, as they do not make use of it anyway. + NSDictionary *universalInfo = [[UNIVERSE planetInfo] dictionaryForKey:PLANETINFO_UNIVERSAL_KEY]; + requiresDockingClearance = [dict boolForKey:@"requires_docking_clearance" defaultValue: + universalInfo != nil ? [universalInfo boolForKey:@"stations_require_docking_clearance" defaultValue:NO] : NO]; +#endif + + double unitime = [UNIVERSE getTime]; + + if ([self isRotatingStation] && [self hasNPCTraffic]) + { + docked_shuttles = ranrot_rand() & 3; // 0..3; + shuttle_launch_interval = 15.0 * 60.0; // every 15 minutes + last_shuttle_launch_time = unitime - (ranrot_rand() & 63) * shuttle_launch_interval / 60.0; + + docked_traders = 3 + (ranrot_rand() & 7); // 1..3; + trader_launch_interval = 3600.0 / docked_traders; // every few minutes + last_trader_launch_time = unitime + 60.0 - trader_launch_interval; // in one minute's time + } + else + { + docked_shuttles = 0; + docked_traders = 0; // 1..3; + } + + patrol_launch_interval = 300.0; // 5 minutes + last_patrol_report_time = unitime - patrol_launch_interval; + + [self setCrew:[NSArray arrayWithObject:[OOCharacter characterWithRole:@"police" andOriginalSystem:[UNIVERSE systemSeed]]]]; + + if ([self group] == nil) + { + [self setGroup:[self stationGroup]]; + } + return YES; +} + + +- (void) setDockingPortModel:(ShipEntity*) dock_model :(Vector) dock_pos :(Quaternion) dock_q +{ + port_model = dock_model; + + port_position = dock_pos; + port_orientation = dock_q; + + BoundingBox bb = [port_model boundingBox]; + port_dimensions = make_vector(bb.max.x - bb.min.x, bb.max.y - bb.min.y, bb.max.z - bb.min.z); + + if (bb.max.z > 0.0) + { + Vector vk = vector_forward_from_quaternion(dock_q); + port_position.x += bb.max.z * vk.x; + port_position.y += bb.max.z * vk.y; + port_position.z += bb.max.z * vk.z; + } + +} + + +- (BOOL) shipIsInDockingCorridor:(ShipEntity*) ship +{ + if ((!ship)||(!ship->isShip)) + return NO; + + Quaternion q0 = quaternion_multiply(port_orientation, orientation); + Vector vi = vector_right_from_quaternion(q0); + Vector vj = vector_up_from_quaternion(q0); + Vector vk = vector_forward_from_quaternion(q0); + + Vector port_pos = [self getPortPosition]; + + BoundingBox shipbb = [ship boundingBox]; + BoundingBox arbb = [ship findBoundingBoxRelativeToPosition: port_pos InVectors: vi : vj : vk]; + + // port dimensions.. + GLfloat ww = port_dimensions.x; + GLfloat hh = port_dimensions.y; + GLfloat dd = port_dimensions.z; + + while (shipbb.max.x - shipbb.min.x > ww * 0.90) ww *= 1.25; + while (shipbb.max.y - shipbb.min.y > hh * 0.90) hh *= 1.25; + + ww *= 0.5; + hh *= 0.5; + +#ifndef NDEBUG + if ((ship->isPlayer)&&(gDebugFlags & DEBUG_DOCKING)) + { + BOOL inLane; + float range; + unsigned laneFlags = 0; + + if (arbb.max.x < ww) laneFlags |= 1; + if (arbb.min.x > -ww) laneFlags |= 2; + if (arbb.max.y < hh) laneFlags |= 4; + if (arbb.min.y > -hh) laneFlags |= 8; + inLane = laneFlags == 0xF; + range = 0.90 * arbb.max.z + 0.10 * arbb.min.z; + + OOLog(@"docking.debug", @"Normalised port dimensions are %g x %g x %g. Player bounding box is at %@-%@ -- %s (%X), range: %g", + ww * 2.0, hh * 2.0, dd, + VectorDescription(arbb.min), VectorDescription(arbb.max), + inLane ? "in lane" : "out of lane", laneFlags, + range); + } +#endif + + if (arbb.max.z < -dd) + return NO; + + if ((arbb.max.x < ww)&&(arbb.min.x > -ww)&&(arbb.max.y < hh)&&(arbb.min.y > -hh)) + { + // in lane + if (0.90 * arbb.max.z + 0.10 * arbb.min.z < 0.0) // we're 90% in docking position! + [ship enterDock:self]; + // + return YES; + // + } + + if ([ship status] == STATUS_LAUNCHING) + return YES; + + // if close enough (within 50%) correct and add damage + // + if ((arbb.min.x > -1.5 * ww)&&(arbb.max.x < 1.5 * ww)&&(arbb.min.y > -1.5 * hh)&&(arbb.max.y < 1.5 * hh)) + { + if (arbb.min.z < 0.0) // got our nose inside + { + GLfloat correction_factor = -arbb.min.z / (arbb.max.z - arbb.min.z); // proportion of ship inside + + // damage the ship according to velocity but don't collide + [ship takeScrapeDamage: 5 * [UNIVERSE getTimeDelta]*[ship flightSpeed] from:self]; + + Vector delta; + delta.x = 0.5 * (arbb.max.x + arbb.min.x) * correction_factor; + delta.y = 0.5 * (arbb.max.y + arbb.min.y) * correction_factor; + + if ((arbb.max.x < ww)&&(arbb.min.x > -ww)) // x is okay - no need to correct + delta.x = 0; + if ((arbb.max.y > hh)&&(arbb.min.x > -hh)) // y is okay - no need to correct + delta.y = 0; + + // adjust the ship back to the center of the port + Vector pos = ship->position; + pos.x -= delta.y * vj.x + delta.x * vi.x; + pos.y -= delta.y * vj.y + delta.x * vi.y; + pos.z -= delta.y * vj.z + delta.x * vi.z; + [ship setPosition:pos]; + } + + // if far enough in - dock + if (0.90 * arbb.max.z + 0.10 * arbb.min.z < 0.0) + [ship enterDock:self]; + + return YES; // okay NOW we're in the docking corridor! + } + // + // + return NO; +} + + +- (BOOL) dockingCorridorIsEmpty +{ + if (!UNIVERSE) + return NO; + + double unitime = [UNIVERSE getTime]; + + if (unitime < last_launch_time + STATION_DELAY_BETWEEN_LAUNCHES) // leave sufficient pause between launches + return NO; + + // check against all ships + BOOL isEmpty = YES; + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + Entity* my_entities[ent_count]; + int i; + int ship_count = 0; + for (i = 0; i < ent_count; i++) + //on red alert, launch even if the player is trying block the corridor + if ([uni_entities[i] isShip] && (alertLevel < STATION_ALERT_LEVEL_RED || ![uni_entities[i] isPlayer])) + my_entities[ship_count++] = [uni_entities[i] retain]; // retained + + for (i = 0; (i < ship_count)&&(isEmpty); i++) + { + ShipEntity* ship = (ShipEntity*)my_entities[i]; + double d2 = distance2(position, ship->position); + if ((ship != self)&&(d2 < 25000000)&&([ship status] != STATUS_DOCKED)) // within 5km + { + Vector ppos = [self getPortPosition]; + d2 = distance2(ppos, ship->position); + if (d2 < 4000000) // within 2km of the port entrance + { + Quaternion q1 = orientation; + q1 = quaternion_multiply(port_orientation, q1); + // + Vector v_out = vector_forward_from_quaternion(q1); + Vector r_pos = make_vector(ship->position.x - ppos.x, ship->position.y - ppos.y, ship->position.z - ppos.z); + if (r_pos.x||r_pos.y||r_pos.z) + r_pos = vector_normal(r_pos); + else + r_pos.z = 1.0; + // + double vdp = dot_product(v_out, r_pos); //== cos of the angle between r_pos and v_out + // + if (vdp > 0.86) + { + isEmpty = NO; + last_launch_time = unitime; + } + } + } + } + + for (i = 0; i < ship_count; i++) + [my_entities[i] release]; //released + + return isEmpty; +} + + +- (void) clearDockingCorridor +{ + if (!UNIVERSE) + return; + + // check against all ships + BOOL isClear = YES; + int ent_count = UNIVERSE->n_entities; + Entity** uni_entities = UNIVERSE->sortedEntities; // grab the public sorted list + Entity* my_entities[ent_count]; + int i; + int ship_count = 0; + for (i = 0; i < ent_count; i++) + if (uni_entities[i]->isShip) + my_entities[ship_count++] = [uni_entities[i] retain]; // retained + + for (i = 0; i < ship_count; i++) + { + ShipEntity* ship = (ShipEntity*)my_entities[i]; + double d2 = distance2(position, ship->position); + if ((ship != self)&&(d2 < 25000000)&&([ship status] != STATUS_DOCKED)) // within 5km + { + Vector ppos = [self getPortPosition]; + float time_out = -15.00; // 15 secs + do + { + isClear = YES; + d2 = distance2(ppos, ship->position); + if (d2 < 4000000) // within 2km of the port entrance + { + Quaternion q1 = orientation; + q1 = quaternion_multiply(port_orientation, q1); + // + Vector v_out = vector_forward_from_quaternion(q1); + Vector r_pos = make_vector(ship->position.x - ppos.x, ship->position.y - ppos.y, ship->position.z - ppos.z); + if (r_pos.x||r_pos.y||r_pos.z) + r_pos = vector_normal(r_pos); + else + r_pos.z = 1.0; + // + double vdp = dot_product(v_out, r_pos); //== cos of the angle between r_pos and v_out + // + if (vdp > 0.86) + { + isClear = NO; + + // okay it's in the way .. give it a wee nudge (0.25s) + [ship update: 0.25]; + time_out += 0.25; + } + if (time_out > 0) + { + Vector v1 = vector_forward_from_quaternion(port_orientation); + Vector spos = ship->position; + spos.x += 3000.0 * v1.x; spos.y += 3000.0 * v1.y; spos.z += 3000.0 * v1.z; + [ship setPosition:spos]; // move 3km out of the way + } + } + } while (!isClear); + } + } + + for (i = 0; i < ship_count; i++) + [my_entities[i] release]; //released + + return; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + BOOL isRockHermit = (scanClass == CLASS_ROCK); + BOOL isMainStation = (self == [UNIVERSE station]); + + double unitime = [UNIVERSE getTime]; + + [super update:delta_t]; + +#if DOCKING_CLEARANCE_ENABLED + PlayerEntity *player = [PlayerEntity sharedPlayer]; + BOOL isDockingStation = (self == [player getTargetDockStation]); + if (isDockingStation && [player status] == STATUS_IN_FLIGHT && + [player getDockingClearanceStatus] >= DOCKING_CLEARANCE_STATUS_GRANTED) + { + if (last_launch_time-20 < unitime && [player getDockingClearanceStatus] != DOCKING_CLEARANCE_STATUS_TIMING_OUT) + { + [self sendExpandedMessage:DESC(@"docking-clearance-about-to-expire") toShip:player]; + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_TIMING_OUT]; + } + else if (last_launch_time < unitime) + { + [self sendExpandedMessage:DESC(@"docking-clearance-expired") toShip:player]; + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; // Docking clearance for player has expired. + } + } + // TODO: If player is waiting for docking clearance, send him an update + // every X seconds telling him where he's at in the queue. + if (isDockingStation && [player status] == STATUS_IN_FLIGHT && [player getDockingClearanceStatus] == DOCKING_CLEARANCE_STATUS_REQUESTED && + [shipsOnApproach count] == 0 && [launchQueue count] == 0) + { + last_launch_time = unitime + DOCKING_CLEARANCE_WINDOW; + [self sendExpandedMessage:[NSString stringWithFormat: + DESC(@"you-are-cleared-to-dock-please-proceed-clearance-expires-at-@"), + ClockToString([player clockTime] + DOCKING_CLEARANCE_WINDOW, NO)] + toShip:player]; + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; + } +#endif + + if (([launchQueue count] > 0)&&([shipsOnApproach count] == 0)&&[self dockingCorridorIsEmpty]) + { + ShipEntity *se=(ShipEntity *)[launchQueue objectAtIndex:0]; + [self launchShip:se]; +#if 0 + // This code is most likely causing many ships to leave the station with the wrong AI + // (route1PatrolAi.plist). Disabling it for now - Nikos + if([se groupID] == universalID) //defender - might have lost its state machine while queueing... + { + if ([se hasPrimaryRole:@"pirate"]) + [se setAITo:@"pirateAI.plist"]; + else + [se setAITo:@"route1PatrolAI.plist"]; + } +#endif + [launchQueue removeObjectAtIndex:0]; + [self doScriptEvent:@"stationLaunchedShip" withArgument:se]; + } + if (([launchQueue count] == 0)&&(no_docking_while_launching)) + no_docking_while_launching = NO; // launching complete + if (approach_spacing > 0.0) + { + approach_spacing -= delta_t * 10.0; // reduce by 10 m/s + if (approach_spacing < 0.0) approach_spacing = 0.0; + } + if ((docked_shuttles > 0)&&(!isRockHermit)) + { + if (unitime > last_shuttle_launch_time + shuttle_launch_interval) + { + if (([self hasNPCTraffic])&&(aegis_status != AEGIS_NONE)) + { + [self launchShuttle]; + docked_shuttles--; + } + last_shuttle_launch_time = unitime; + } + } + + if ((docked_traders > 0)&&(!isRockHermit)) + { + if (unitime > last_trader_launch_time + trader_launch_interval) + { + if ([self hasNPCTraffic]) + { + [self launchTrader]; + docked_traders--; + } + last_trader_launch_time = unitime; + } + } + + // testing patrols + if ((unitime > (last_patrol_report_time + patrol_launch_interval))&&(isMainStation)) + { + if (![self hasNPCTraffic] || [self launchPatrol]) + last_patrol_report_time = unitime; + } +} + + +- (void) clear +{ + if (launchQueue) + [launchQueue removeAllObjects]; + if (shipsOnApproach) + [shipsOnApproach removeAllObjects]; + if (shipsOnHold) + [shipsOnHold removeAllObjects]; +} + + +- (void) addShipToLaunchQueue:(ShipEntity *) ship +{ + [self sanityCheckShipsOnApproach]; + if (!launchQueue) + launchQueue = [[NSMutableArray alloc] init]; // retained + if (ship) + [launchQueue addObject:ship]; +} + + +- (unsigned) countShipsInLaunchQueueWithPrimaryRole:(NSString *)role +{ + unsigned i, count, result = 0; + count = [launchQueue count]; + + for (i = 0; i < [launchQueue count]; i++) + { + if ([[launchQueue objectAtIndex:i] hasPrimaryRole:role]) + result++; + } + return result; +} + + +- (void) launchShip:(ShipEntity *) ship +{ + if (![ship isShip]) return; + + Vector launchPos = position; + Vector launchVel = velocity; + double launchSpeed = 0.5 * [ship maxFlightSpeed]; + if (maxFlightSpeed > 0 && flightSpeed > 0) + { + launchSpeed = 0.5 * [ship maxFlightSpeed] * (1.0 + flightSpeed/maxFlightSpeed); + } + Quaternion q1 = orientation; + q1 = quaternion_multiply(port_orientation, q1); + Vector launchVector = vector_forward_from_quaternion(q1); + BoundingBox bb = [ship boundingBox]; + if ((port_dimensions.x < port_dimensions.y) ^ (bb.max.x - bb.min.x < bb.max.y - bb.min.y)) + { + quaternion_rotate_about_axis(&q1, launchVector, M_PI*0.5); // to account for the slot being at 90 degrees to vertical + } + [ship setOrientation:q1]; + // launch position + launchPos.x += port_position.x * v_right.x + port_position.y * v_up.x + port_position.z * v_forward.x; + launchPos.y += port_position.x * v_right.y + port_position.y * v_up.y + port_position.z * v_forward.y; + launchPos.z += port_position.x * v_right.z + port_position.y * v_up.z + port_position.z * v_forward.z; + [ship setPosition:launchPos]; + if([ship pendingEscortCount] > 0) [ship setPendingEscortCount:0]; // Make sure no extra escorts are added after launch. (e.g. for miners etc.) + // launch speed + launchVel = vector_add(launchVel, vector_multiply_scalar(launchVector, launchSpeed)); + [ship setSpeed:sqrt(magnitude2(launchVel))]; + [ship setVelocity:launchVel]; + // orientation + [ship setRoll:flightRoll]; + [ship setPitch:0.0]; + [ship setStatus: STATUS_LAUNCHING]; + [[ship getAI] reactToMessage:@"pauseAI: 2.0"]; // pause while launching + [UNIVERSE addEntity:ship]; + last_launch_time = [UNIVERSE getTime]; + [ship resetTracking]; // resets stuff for tracking/exhausts +} + + +- (void) noteDockedShip:(ShipEntity *) ship +{ + if (ship == nil) return; + + // set last launch time to avoid clashes with outgoing ships + last_launch_time = [UNIVERSE getTime]; + if ([ship isShuttle]) docked_shuttles++; + else if ([ship isTrader] && ![ship isPlayer]) docked_traders++; + else if (([ship isPolice] && ![ship isEscort]) || [ship hasPrimaryRole:@"hermit-ship"] || [ship hasPrimaryRole:@"defense_ship"]) + { + if (0 < police_launched) police_launched--; + } + else if ([ship hasPrimaryRole:@"scavenger"] || [ship hasPrimaryRole:@"miner"]) // treat miners and scavengers alike! + { + if (0 < scavengers_launched) scavengers_launched--; + } + + OOUniversalID ship_id = [ship universalID]; + NSNumber *shipID = [NSNumber numberWithUnsignedShort:ship_id]; + + [shipsOnApproach removeObjectForKey:shipID]; + if ([shipsOnApproach count] == 0) + [shipAI message:@"DOCKING_COMPLETE"]; + + unsigned i; // clear any previously owned docking stages + for (i = 0; i < MAX_DOCKING_STAGES; i++) + if ((id_lock[i] == ship_id)||([UNIVERSE entityForUniversalID:id_lock[i]] == nil)) + id_lock[i] = NO_TARGET; + + [script doEvent:@"otherShipDocked" withArgument:ship]; +} + + +- (BOOL)hasNPCTraffic +{ + return hasNPCTraffic; +} + + +- (void)setHasNPCTraffic:(BOOL)flag +{ + hasNPCTraffic = flag != NO; +} + + +- (BOOL) collideWithShip:(ShipEntity *)other +{ + [self abortAllDockings]; + return [super collideWithShip:other]; +} + + +- (BOOL) hasHostileTarget +{ + if (primaryTarget == NO_TARGET) + return NO; + if ((behaviour == BEHAVIOUR_AVOID_COLLISION)&&(previousCondition)) + { + int old_behaviour = [(NSNumber*)[previousCondition objectForKey:@"behaviour"] intValue]; + return IsBehaviourHostile(old_behaviour); + } + return IsBehaviourHostile(behaviour)||(alertLevel == STATION_ALERT_LEVEL_YELLOW)||(alertLevel == STATION_ALERT_LEVEL_RED); +} + +- (void)takeEnergyDamage:(double)amount from:(Entity *)ent becauseOf:(Entity *)other +{ + //stations must ignore friendly fire, otherwise the defenders' AI gets stuck. + BOOL isFriend = NO; + OOShipGroup *group = [self group]; + + if ([other isShip] && group != nil) + { + OOShipGroup *otherGroup = [(ShipEntity *)other group]; + isFriend = otherGroup == group || [otherGroup leader] == self; + } + + // If this is the system's main station... + if (self == [UNIVERSE station] && !isFriend) + { + //...get angry + + BOOL isEnergyMine = (ent && [ent isParticle] && [ent scanClass] == CLASS_MINE); + unsigned b=isEnergyMine ? 96 : 64; + if ([(ShipEntity*)other bounty] >= b) //already a hardened criminal? + { + b *= 1.5; //bigger bounty! + } + [(ShipEntity*)other markAsOffender:b]; + + [self setPrimaryAggressor:other]; + found_target = primaryAggressor; + [self launchPolice]; + + if (isEnergyMine) //don't blow up! + { + [self increaseAlertLevel]; + [self respondToAttackFrom:ent becauseOf:other]; + return; + } + } + // Stop damage if main station & close to death! + if (!isFriend && (self != [UNIVERSE station] || amount < energy) ) + { + // Handle damage like a ship. + [super takeEnergyDamage:amount from:ent becauseOf:other]; + } +} + +- (void) adjustVelocity:(Vector) xVel +{ + if (self != [UNIVERSE station]) [super adjustVelocity:xVel]; //dont get moved +} + +- (void)takeScrapeDamage:(double)amount from:(Entity *)ent +{ + // Stop damage if main station + if (self != [UNIVERSE station]) [super takeScrapeDamage:amount from:ent]; +} + + +- (void) takeHeatDamage:(double)amount +{ + // Stop damage if main station + if (self != [UNIVERSE station]) [super takeHeatDamage:amount]; +} + + +- (OOStationAlertLevel) alertLevel +{ + return alertLevel; +} + + +- (void) setAlertLevel:(OOStationAlertLevel)level signallingScript:(BOOL)signallingScript +{ + if (level < STATION_ALERT_LEVEL_GREEN) level = STATION_ALERT_LEVEL_GREEN; + if (level > STATION_ALERT_LEVEL_RED) level = STATION_ALERT_LEVEL_RED; + + if (alertLevel != level) + { + OOStationAlertLevel oldLevel = alertLevel; + alertLevel = level; + if (signallingScript) + { + [self doScriptEvent:@"alertConditionChanged" + withArgument:[NSNumber numberWithUnsignedInt:level] + andArgument:[NSNumber numberWithUnsignedInt:oldLevel]]; + } + switch (level) + { + case STATION_ALERT_LEVEL_GREEN: + [shipAI reactToMessage:@"GREEN_ALERT"]; + break; + + case STATION_ALERT_LEVEL_YELLOW: + [shipAI reactToMessage:@"YELLOW_ALERT"]; + break; + + case STATION_ALERT_LEVEL_RED: + [shipAI reactToMessage:@"RED_ALERT"]; + break; + } + } +} + + +- (void) launchIndependentShip:(NSString*) role +{ + BOOL trader = [role isEqualToString:@"trader"]; + BOOL sunskimmer = ([role isEqualToString:@"sunskim-trader"]); + ShipEntity *ship = nil; + NSString *defaultRole = @"escort"; + NSString *escortRole = nil; + NSString *escortShipKey = nil; + NSDictionary *traderDict = nil; + + if((trader && (randf() < 0.1)) || sunskimmer) + { + ship = [UNIVERSE newShipWithRole:@"sunskim-trader"]; + sunskimmer = true; + trader = true; + role = @"trader"; // make sure also sunskimmers get trader role. + } + else + { + ship = [UNIVERSE newShipWithRole:role]; + } + + if (ship) + { + traderDict = [ship shipInfoDictionary]; + if (![ship crew]) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: role + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [ship setPrimaryRole:role]; + + if(trader || ship->scanClass == CLASS_NOT_SET) [ship setScanClass: CLASS_NEUTRAL]; // keep defined scanclasses for non-traders. + + if (trader) + { + [ship setBounty:0]; + [ship setCargoFlag:CARGO_FLAG_FULL_PLENTIFUL]; + if (sunskimmer) + { + [[ship getAI] setStateMachine:@"route2sunskimAI.plist"]; + if([ship heatInsulation] < 7) [ship setHeatInsulation:7]; + // even with this value the slow sunskim-anaconda will burn in a big sun. + } + else + { + [[ship getAI] setStateMachine:@"exitingTraderAI.plist"]; + if([ship fuel] == 0) [ship setFuel:70]; + } + } + + [self addShipToLaunchQueue:ship]; + + OOShipGroup *escortGroup = [ship escortGroup]; + if ([ship group] == nil) [ship setGroup:escortGroup]; + // Eric: Escorts are defined both as _group and as _escortGroup because friendly attacks are only handled withing _group. + [escortGroup setLeader:ship]; + + // add escorts to the trader + unsigned escorts = [ship pendingEscortCount]; + if(escorts > 0) + { + escortRole = [traderDict stringForKey:@"escort-role" defaultValue:defaultRole]; + if (![escortRole isEqualToString: defaultRole]) + { + if (![[UNIVERSE newShipWithRole:escortRole] autorelease]) + { + escortRole = defaultRole; + } + } + + escortShipKey = [traderDict stringForKey:@"escort-ship"]; + if (escortShipKey != nil) + { + if (![[UNIVERSE newShipWithName:escortShipKey] autorelease]) + { + escortShipKey = nil; + } + } + + while (escorts--) + { + ShipEntity *escort_ship; + + if (escortShipKey) + { + escort_ship = [UNIVERSE newShipWithName:escortShipKey]; // retained + } + else + { + escort_ship = [UNIVERSE newShipWithRole:escortRole]; // retained + } + + if (escort_ship) + { + if (![escort_ship crew] && ![escort_ship isUnpiloted]) + [escort_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"hunter" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [escort_ship setScanClass: CLASS_NEUTRAL]; + [escort_ship setCargoFlag: CARGO_FLAG_FULL_PLENTIFUL]; + [escort_ship setPrimaryRole:@"escort"]; + if (sunskimmer && [escort_ship heatInsulation] < [ship heatInsulation]) + [escort_ship setHeatInsulation:[ship heatInsulation]]; + + [escort_ship setGroup:escortGroup]; + [escort_ship setOwner:ship]; + + [[escort_ship getAI] setStateMachine:@"escortAI.plist"]; + [self addShipToLaunchQueue:escort_ship]; + + [escort_ship release]; + } + } + } + + [ship setPendingEscortCount:0]; + [ship release]; + } +} + + +//////////////////////////////////////////////// extra AI routines + + +// Exposed to AI +- (void) increaseAlertLevel +{ + [self setAlertLevel:[self alertLevel] + 1 signallingScript:YES]; +} + + +// Exposed to AI +- (void) decreaseAlertLevel +{ + [self setAlertLevel:[self alertLevel] - 1 signallingScript:YES]; +} + + +// Exposed to AI +- (void) launchPolice +{ + OOUniversalID police_target = primaryTarget; + unsigned i; + OOTechLevelID techlevel = [self equivalentTechLevel]; + if (techlevel == NSNotFound) techlevel = 6; + + for (i = 0; (i < 4)&&(police_launched < max_police) ; i++) + { + ShipEntity *police_ship = nil; + if (![UNIVERSE entityForUniversalID:police_target]) + { + [self noteLostTarget]; + return; + } + + if ((Ranrot() & 7) + 6 <= techlevel) + { + police_ship = [UNIVERSE newShipWithRole:@"interceptor"]; // retain count = 1 + } + else + { + police_ship = [UNIVERSE newShipWithRole:@"police"]; // retain count = 1 + } + + if (police_ship) + { + if (![police_ship crew]) + { + [police_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"police" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + } + + [police_ship setGroup:[self stationGroup]]; // who's your Daddy + [police_ship setPrimaryRole:@"police"]; + [police_ship addTarget:[UNIVERSE entityForUniversalID:police_target]]; + [police_ship setScanClass:CLASS_POLICE]; + [police_ship setBounty:0]; + [[police_ship getAI] setStateMachine:@"policeInterceptAI.plist"]; + [self addShipToLaunchQueue:police_ship]; + [police_ship release]; + police_launched++; + } + } + no_docking_while_launching = YES; + [self abortAllDockings]; +} + + +// Exposed to AI +- (void) launchDefenseShip +{ + OOUniversalID defense_target = primaryTarget; + ShipEntity *defense_ship = nil; + NSString *defense_ship_key = nil, + *defense_ship_role = nil, + *default_defense_ship_role = nil; + NSString *defense_ship_ai = @"policeInterceptAI.plist"; + + OOTechLevelID techlevel; + + techlevel = [self equivalentTechLevel]; + if (techlevel == NSNotFound) techlevel = 6; + if ((Ranrot() & 7) + 6 <= techlevel) + default_defense_ship_role = @"interceptor"; + else + default_defense_ship_role = @"police"; + + if (police_launched >= max_defense_ships) // shuttles are to rockhermits what police ships are to stations + return; + + if (![UNIVERSE entityForUniversalID:defense_target]) + { + [self noteLostTarget]; + return; + } + + defense_ship_key = [shipinfoDictionary stringForKey:@"defense_ship"]; + if (defense_ship_key != nil) + { + defense_ship = [UNIVERSE newShipWithName:defense_ship_key]; + } + if (!defense_ship) + { + defense_ship_role = [shipinfoDictionary stringForKey:@"defense_ship_role" defaultValue:default_defense_ship_role]; + defense_ship = [UNIVERSE newShipWithRole:defense_ship_role]; + } + + if (!defense_ship && default_defense_ship_role != defense_ship_role) + defense_ship = [UNIVERSE newShipWithRole:default_defense_ship_role]; + if (!defense_ship) + return; + + [defense_ship setPrimaryRole:@"defense_ship"]; + + police_launched++; + + if (![defense_ship crew]) + { + [defense_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"hunter" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + } + + [defense_ship setOwner: self]; + if ([self group] == nil) + { + [self setGroup:[self stationGroup]]; + } + [defense_ship setGroup:[self stationGroup]]; // who's your Daddy + + if ([defense_ship isPolice]) + { + [defense_ship setStateMachine:defense_ship_ai]; + } + + [defense_ship addTarget:[UNIVERSE entityForUniversalID:defense_target]]; + + if ((scanClass != CLASS_ROCK)&&(scanClass != CLASS_STATION)) + [defense_ship setScanClass: scanClass]; // same as self + + [self addShipToLaunchQueue:defense_ship]; + [defense_ship release]; + no_docking_while_launching = YES; + [self abortAllDockings]; +} + + +// Exposed to AI +- (void) launchScavenger +{ + ShipEntity *scavenger_ship; + + unsigned scavs = [UNIVERSE countShipsWithRole:@"scavenger" inRange:SCANNER_MAX_RANGE ofEntity:self] + [self countShipsInLaunchQueueWithPrimaryRole:@"scavenger"]; + + if (scavs >= max_scavengers) return; + if (scavengers_launched >= max_scavengers) return; + + scavengers_launched++; + + scavenger_ship = [UNIVERSE newShipWithRole:@"scavenger"]; // retain count = 1 + if (scavenger_ship) + { + if (![scavenger_ship crew]) + [scavenger_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"hunter" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [scavenger_ship setScanClass: CLASS_NEUTRAL]; + [scavenger_ship setGroup:[self stationGroup]]; // who's your Daddy -- FIXME: should we have a separate group for non-escort auxiliaires? + [[scavenger_ship getAI] setStateMachine:@"scavengerAI.plist"]; + [self addShipToLaunchQueue:scavenger_ship]; + [scavenger_ship release]; + } +} + + +// Exposed to AI +- (void) launchMiner +{ + ShipEntity *miner_ship; + + int n_miners = [UNIVERSE countShipsWithRole:@"miner" inRange:SCANNER_MAX_RANGE ofEntity:self] + [self countShipsInLaunchQueueWithPrimaryRole:@"miner"]; + + if (n_miners >= 1) // just the one + return; + + // count miners as scavengers... + if (scavengers_launched >= max_scavengers) return; + + miner_ship = [UNIVERSE newShipWithRole:@"miner"]; // retain count = 1 + if (miner_ship) + { + if (![miner_ship crew]) + [miner_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"miner" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + scavengers_launched++; + [miner_ship setScanClass:CLASS_NEUTRAL]; + [miner_ship setGroup:[self stationGroup]]; // who's your Daddy -- FIXME: should we have a separate group for non-escort auxiliaires? + [[miner_ship getAI] setStateMachine:@"minerAI.plist"]; + [self addShipToLaunchQueue:miner_ship]; + [miner_ship release]; + } +} + +/**Lazygun** added the following method. A complete rip-off of launchDefenseShip. + */ +// Exposed to AI +- (void) launchPirateShip +{ + //Pirate ships are launched from the same pool as defence ships. + OOUniversalID defense_target = primaryTarget; + ShipEntity *pirate_ship = nil; + + if (police_launched >= max_defense_ships) return; // shuttles are to rockhermits what police ships are to stations + + if (![UNIVERSE entityForUniversalID:defense_target]) + { + [self noteLostTarget]; + return; + } + + police_launched++; + + // Yep! The standard hermit defence ships, even if they're the aggressor. + pirate_ship = [UNIVERSE newShipWithRole:@"pirate"]; // retain count = 1 + // Nope, use standard pirates in a generic method. + + if (pirate_ship) + { + if (![pirate_ship crew]) + { + [pirate_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"pirate" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + } + + // set the owner of the ship to the station so that it can check back for docking later + [pirate_ship setOwner:self]; + [pirate_ship setGroup:[self stationGroup]]; // who's your Daddy + [pirate_ship setPrimaryRole:@"defense_ship"]; + [pirate_ship addTarget:[UNIVERSE entityForUniversalID:defense_target]]; + [pirate_ship setScanClass: CLASS_NEUTRAL]; + //**Lazygun** added 30 Nov 04 to put a bounty on those pirates' heads. + [pirate_ship setBounty: 10 + floor(randf() * 20)]; // modified for variety + + [self addShipToLaunchQueue:pirate_ship]; + [pirate_ship release]; + no_docking_while_launching = YES; + [self abortAllDockings]; + } +} + + +// Exposed to AI +- (void) launchShuttle +{ + ShipEntity *shuttle_ship; + + shuttle_ship = [UNIVERSE newShipWithRole:@"shuttle"]; // retain count = 1 + + if (shuttle_ship) + { + if (![shuttle_ship crew]) + [shuttle_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"trader" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [shuttle_ship setScanClass: CLASS_NEUTRAL]; + [shuttle_ship setCargoFlag:CARGO_FLAG_FULL_SCARCE]; + [[shuttle_ship getAI] setStateMachine:@"fallingShuttleAI.plist"]; + [self addShipToLaunchQueue:shuttle_ship]; + + [shuttle_ship release]; + } +} + +// Exposed to AI +- (void) launchTrader +{ + [self launchIndependentShip:@"trader"]; +} + +// Exposed to AI +- (void) launchIndependentShipWithRole:(NSString*) role +{ + [self launchIndependentShip:role]; +} + +// Exposed to AI +- (void) launchEscort +{ + ShipEntity *escort_ship; + + escort_ship = [UNIVERSE newShipWithRole:@"escort"]; // retain count = 1 + + if (escort_ship) + { + if (![escort_ship crew] && ![escort_ship isUnpiloted]) + [escort_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"hunter" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [escort_ship setScanClass: CLASS_NEUTRAL]; + [escort_ship setCargoFlag: CARGO_FLAG_FULL_PLENTIFUL]; + [[escort_ship getAI] setStateMachine:@"escortAI.plist"]; + [self addShipToLaunchQueue:escort_ship]; + + [escort_ship release]; + } +} + + +// Exposed to AI +- (BOOL) launchPatrol +{ + if (police_launched < max_police) + { + ShipEntity *patrol_ship = nil; + OOTechLevelID techlevel; + + techlevel = [self equivalentTechLevel]; + if (techlevel == NSNotFound) + techlevel = 6; + + police_launched++; + + if ((Ranrot() & 7) + 6 <= techlevel) + patrol_ship = [UNIVERSE newShipWithRole:@"interceptor"]; // retain count = 1 + else + patrol_ship = [UNIVERSE newShipWithRole:@"police"]; // retain count = 1 + if (patrol_ship) + { + if (![patrol_ship crew]) + [patrol_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: @"police" + andOriginalSystem: [UNIVERSE systemSeed]]]]; + + [patrol_ship switchLightsOff]; + [patrol_ship setScanClass: CLASS_POLICE]; + [patrol_ship setPrimaryRole:@"police"]; + [patrol_ship setBounty:0]; + [patrol_ship setGroup:[self stationGroup]]; // who's your Daddy + [[patrol_ship getAI] setStateMachine:@"planetPatrolAI.plist"]; + [self addShipToLaunchQueue:patrol_ship]; + [self acceptPatrolReportFrom:patrol_ship]; + [patrol_ship release]; + return YES; + } + } + return NO; +} + + +// Exposed to AI +- (void) launchShipWithRole:(NSString*) role +{ + ShipEntity *ship = [UNIVERSE newShipWithRole: role]; // retain count = 1 + if (ship) + { + if (![ship crew]) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: role + andOriginalSystem: [UNIVERSE systemSeed]]]]; + if (ship->scanClass == CLASS_NOT_SET) [ship setScanClass: CLASS_NEUTRAL]; + [ship setPrimaryRole:role]; + [ship setGroup:[self stationGroup]]; // who's your Daddy + [self addShipToLaunchQueue:ship]; + [ship release]; + } +} + + +// Exposed to AI +- (void) becomeExplosion +{ + if (self == [UNIVERSE station]) return; + + // launch docked ships if possible + PlayerEntity* player = [PlayerEntity sharedPlayer]; + if ((player)&&([player status] == STATUS_DOCKED)&&([player dockedStation] == self)) + { + // undock the player! + [player leaveDock:self]; + [UNIVERSE setViewDirection:VIEW_FORWARD]; + [UNIVERSE setDisplayCursor:NO]; + [player warnAboutHostiles]; // sound a klaxon + } + + if (scanClass == CLASS_ROCK) // ie we're a rock hermit or similar + { + // set the role so that we break up into rocks! + [self setPrimaryRole:@"asteroid"]; + being_mined = YES; + } + + // finally bite the bullet + [super becomeExplosion]; +} + + +// Exposed to AI +- (void) becomeEnergyBlast +{ + if (self == [UNIVERSE station]) return; + [super becomeEnergyBlast]; +} + + +- (void) becomeLargeExplosion:(double) factor +{ + if (self == [UNIVERSE station]) return; + [super becomeLargeExplosion:factor]; +} + + +- (void) acceptPatrolReportFrom:(ShipEntity*) patrol_ship +{ + last_patrol_report_time = [UNIVERSE getTime]; +} + + +#if DOCKING_CLEARANCE_ENABLED +- (NSString *) acceptDockingClearanceRequestFrom:(ShipEntity *)other +{ + NSString *result = nil; + double timeNow = [UNIVERSE getTime]; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + + [UNIVERSE clearPreviousMessage]; + + // Docking clearance not required - clear it just in case it's been + // set for another nearby station. + if (![self requiresDockingClearance]) + { + // TODO: We're potentially cancelling docking at another station, so + // ensure we clear the timer to allow NPC traffic. If we + // don't, normal traffic will resume once the timer runs out. + [self sendExpandedMessage:DESC(@"station-does-not-require-docking-clearance") toShip:other]; + if ([other isPlayer]) + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; + result = @"DOCKING_CLEARANCE_NOT_REQUIRED"; + } + + // Docking clearance already granted for this station - check for + // time-out or cancellation (but only for the Player). + if( result == nil && [other isPlayer] && self == [player getTargetDockStation]) + { + switch( [player getDockingClearanceStatus] ) + { + case DOCKING_CLEARANCE_STATUS_TIMING_OUT: + last_launch_time = timeNow + DOCKING_CLEARANCE_WINDOW; + [self sendExpandedMessage:[NSString stringWithFormat: + DESC(@"your-docking-clearance-has-been-extended-until-@"), + ClockToString([player clockTime] + DOCKING_CLEARANCE_WINDOW, NO)] + toShip:other]; + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; + result = @"DOCKING_CLEARANCE_EXTENDED"; + break; + case DOCKING_CLEARANCE_STATUS_REQUESTED: + case DOCKING_CLEARANCE_STATUS_GRANTED: + last_launch_time = timeNow; + [self sendExpandedMessage:DESC(@"docking-clearance-cancelled") toShip:other]; + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; + result = @"DOCKING_CLEARANCE_CANCELLED"; + break; + case DOCKING_CLEARANCE_STATUS_NONE: + case DOCKING_CLEARANCE_STATUS_NOT_REQUIRED: + break; + } + } + + // First we must set the status to REQUESTED to avoid problems when + // switching docking targets - even if we later set it back to NONE. + if (result == nil && [other isPlayer] && self != [player getTargetDockStation]) + { + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_REQUESTED]; + } + + // Deny docking for fugitives at the main station + // TODO: Should this be another key in shipdata.plist and/or should this + // apply to all stations? + if (result == nil && self == [UNIVERSE station] && [other bounty] > 50) // do not grant docking clearance to fugitives + { + [self sendExpandedMessage:DESC(@"H-station-refuses-to-grant-docking-clearance") toShip:other]; + if ([other isPlayer]) + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_NONE]; + result = @"DOCKING_CLEARANCE_DENIED_SHIP_FUGITIVE"; + } + + // Put ship in queue if we've got incoming or outgoing traffic + if (result == nil && [shipsOnApproach count] && last_launch_time < timeNow) + { + [self sendExpandedMessage:[NSString stringWithFormat: + DESC(@"please-wait-until-d-ships-have-completed-their-approach"), + [shipsOnApproach count]+1] toShip:other]; + // No need to set status to REQUESTED as we've already done that earlier. + result = @"DOCKING_CLEARANCE_DENIED_TRAFFIC_INBOUND"; + } + if (result == nil && [launchQueue count] && last_launch_time < timeNow) + { + [self sendExpandedMessage:[NSString stringWithFormat: + DESC(@"please-wait-until-d-launching-ships-have-cleared-station"), + [launchQueue count]+1] toShip:other]; + // No need to set status to REQUESTED as we've already done that earlier. + result = @"DOCKING_CLEARANCE_DENIED_TRAFFIC_OUTBOUND"; + } + + // Ship has passed all checks - grant docking! + // TODO: We probably don't need to test against last_launch_time anymore, + // since we cancel existing launch requests above. + //if (result == nil && last_launch_time < timeNow) + if (result == nil) + { + last_launch_time = timeNow + DOCKING_CLEARANCE_WINDOW; + [self sendExpandedMessage:[NSString stringWithFormat: + DESC(@"you-are-cleared-to-dock-please-proceed-clearance-expires-at-@"), + ClockToString([player clockTime] + DOCKING_CLEARANCE_WINDOW, NO)] + toShip:other]; + if ([other isPlayer]) + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; + result = @"DOCKING_CLEARANCE_GRANTED"; + } + return result; +} + + +- (BOOL) requiresDockingClearance +{ + return requiresDockingClearance; +} + + +- (void) setRequiresDockingClearance:(BOOL)newValue +{ + requiresDockingClearance = !!newValue; // Ensure yes or no +} +#endif + + +- (BOOL) isRotatingStation +{ + if ([shipinfoDictionary boolForKey:@"rotating" defaultValue:NO]) return YES; + return [[shipinfoDictionary objectForKey:@"roles"] rangeOfString:@"rotating-station"].location != NSNotFound; // legacy +} + + +- (BOOL) hasShipyard +{ + if ([UNIVERSE strict]) + return NO; + if ([UNIVERSE station] == self) + return YES; + + // NOTE: non-standard capitalization is documented and entrenched. + if ([shipinfoDictionary objectForKey:@"hasShipyard"]) + { + id determinant = [shipinfoDictionary objectForKey:@"hasShipyard"]; + + if ([determinant isKindOfClass:[NSArray class]]) + { + + return [[PlayerEntity sharedPlayer] scriptTestConditions:determinant]; + } + else + { + return OOFuzzyBooleanFromObject(determinant, 0.0f); + } + } + else + { + return NO; + } +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\" %@", name, [super descriptionComponents]]; +} + + +- (void)dumpSelfState +{ + NSMutableArray *flags = nil; + NSString *flagsString = nil; + NSString *alertString = nil; + + [super dumpSelfState]; + + switch (alertLevel) + { + case STATION_ALERT_LEVEL_GREEN: + alertString = @"green"; + break; + + case STATION_ALERT_LEVEL_YELLOW: + alertString = @"yellow"; + break; + + case STATION_ALERT_LEVEL_RED: + alertString = @"red"; + break; + + default: + alertString = @"*** ERROR: UNKNOWN ALERT LEVEL ***"; + } + + OOLog(@"dumpState.stationEntity", @"Alert level: %@", alertString); + OOLog(@"dumpState.stationEntity", @"Max police: %u", max_police); + OOLog(@"dumpState.stationEntity", @"Max defense ships: %u", max_defense_ships); + OOLog(@"dumpState.stationEntity", @"Police launched: %u", police_launched); + OOLog(@"dumpState.stationEntity", @"Max scavengers: %u", max_scavengers); + OOLog(@"dumpState.stationEntity", @"Scavengers launched: %u", scavengers_launched); + OOLog(@"dumpState.stationEntity", @"Docked shuttles: %u", docked_shuttles); + OOLog(@"dumpState.stationEntity", @"Docked traders: %u", docked_traders); + OOLog(@"dumpState.stationEntity", @"Equivalent tech level: %i", equivalentTechLevel); + OOLog(@"dumpState.stationEntity", @"Equipment price factor: %g", equipmentPriceFactor); + + flags = [NSMutableArray array]; + #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; } + ADD_FLAG_IF_SET(no_docking_while_launching); + if ([self isRotatingStation]) { [flags addObject:@"rotatingStation"]; } + flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none"; + OOLog(@"dumpState.stationEntity", @"Flags: %@", flagsString); +} + +@end + + +#ifndef NDEBUG + +@implementation StationEntity (OOWireframeDockingBox) + + +- (void)drawEntity:(BOOL)immediate :(BOOL)translucent +{ + Vector adjustedPosition; + Vector halfDimensions; + + [super drawEntity:immediate:translucent]; + + if (gDebugFlags & DEBUG_BOUNDING_BOXES) + { + OODebugDrawBasisAtOrigin(50.0f); + + OOMatrix matrix; + matrix = OOMatrixForQuaternionRotation(port_orientation); + glPushMatrix(); + GLMultOOMatrix(matrix); + + halfDimensions = vector_multiply_scalar(port_dimensions, 0.5f); + adjustedPosition = port_position; + adjustedPosition.z -= halfDimensions.z; + + OODebugDrawColoredBoundingBoxBetween(vector_subtract(adjustedPosition, halfDimensions), vector_add(adjustedPosition, halfDimensions), [OOColor redColor]); + OODebugDrawBasisAtOrigin(30.0f); + + glPopMatrix(); + } +} + +@end + +#endif diff --git a/src/Core/Entities/WormholeEntity.h b/src/Core/Entities/WormholeEntity.h new file mode 100644 index 00000000..e52c0419 --- /dev/null +++ b/src/Core/Entities/WormholeEntity.h @@ -0,0 +1,91 @@ +/* + +WormholeEntity.h + +Entity subclass representing a wormhole between systems. (This is -- to use +technical terminology -- the blue blobby thing you see hanging in space. The +purple tunnel is RingEntity.) + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Entity.h" + +#define WORMHOLE_EXPIRES_TIMEINTERVAL 900.0 +#define WORMHOLE_SHRINK_RATE 4000.0 + +@class ShipEntity, Universe; + +typedef enum +{ + WH_SCANINFO_NONE = 0, + WH_SCANINFO_SCANNED, + WH_SCANINFO_COLLAPSE_TIME, + WH_SCANINFO_ARRIVAL_TIME, + WH_SCANINFO_DESTINATION, + WH_SCANINFO_SHIP, +} WORMHOLE_SCANINFO; + +@interface WormholeEntity: Entity +{ + double expiry_time; // Time when wormhole entrance closes + double travel_time; // Time taken for a ship to traverse the wormhole + double arrival_time; // Time when wormhole exit opens + double scan_time; // Time when wormhole was scanned + + Random_Seed origin; + Random_Seed destination; + + NSMutableArray *shipsInTransit; + + double witch_mass; + + WORMHOLE_SCANINFO scan_info; + BOOL hasExitPosition; +} + +- (WormholeEntity*) initWithDict:(NSDictionary*)dict; +- (WormholeEntity*) initWormholeTo:(Random_Seed) s_seed fromShip:(ShipEntity *) ship; + +- (BOOL) suckInShip:(ShipEntity *) ship; +- (void) disgorgeShips; + +- (Random_Seed) origin; +- (Random_Seed) destination; + +- (double) expiryTime; // Time at which the wormholes entrance closes +- (double) arrivalTime; // Time at which the wormholes exit opens +- (double) travelTime; // Time needed for a ship to traverse the wormhole +- (double) scanTime; // Time when wormhole was scanned +- (void) setScannedAt:(double)time; + +- (BOOL) isScanned; // True if the wormhole has been scanned by the player +- (WORMHOLE_SCANINFO) scanInfo; // Stage of scanning +- (void)setScanInfo:(WORMHOLE_SCANINFO) scanInfo; + +- (NSArray*) shipsInTransit; + +#if WORMHOLE_SCANNER +- (NSString *) identFromShip:(ShipEntity*) ship; +#endif + +- (NSDictionary *)getDict; + +@end diff --git a/src/Core/Entities/WormholeEntity.m b/src/Core/Entities/WormholeEntity.m new file mode 100644 index 00000000..d6cffb69 --- /dev/null +++ b/src/Core/Entities/WormholeEntity.m @@ -0,0 +1,557 @@ +/* + +WormholeEntity.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "WormholeEntity.h" + +#import "ParticleEntity.h" +#import "ShipEntity.h" +#import "PlanetEntity.h" +#import "PlayerEntity.h" + +#import "Universe.h" +#import "AI.h" +#import "OORoleSet.h" +#import "OOShipRegistry.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" + +// Hidden interface +@interface WormholeEntity (Private) + +-(id) init; + +@end + +// Static local functions +static void DrawWormholeCorona(GLfloat inner_radius, GLfloat outer_radius, int step, GLfloat z_distance, GLfloat *col4v1); + + +@implementation WormholeEntity (Private) + +-(id) init +{ + if ((self = [super init])) + { + witch_mass = 0.0; + shipsInTransit = [[NSMutableArray arrayWithCapacity:4] retain]; + collision_radius = 0.0; + [self setStatus:STATUS_EFFECT]; + scanClass = CLASS_WORMHOLE; + isWormhole = YES; + scan_info = WH_SCANINFO_NONE; + scan_time = 0; + hasExitPosition = NO; + } + return self; +} + +@end // Private interface implementation + + +// +// Public Wormhole Implementation +// + +@implementation WormholeEntity + +- (WormholeEntity*)initWithDict:(NSDictionary*)dict +{ + assert(dict != nil); + + if ([self init]) + { + NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; + + origin = RandomSeedFromString([dict stringForKey:@"origin_seed"]); + destination = RandomSeedFromString([dict stringForKey:@"dest_seed"]); + + // We only ever init from dictionary if we're loaded by the player, so + // by definition we have been scanned + scan_info = WH_SCANINFO_SCANNED; + + // Remember, times are stored as Ship Clock - but anything + // saving/restoring wormholes from dictionaries should know this! + expiry_time = [dict doubleForKey:@"expiry_time"]; + arrival_time = [dict doubleForKey:@"arrival_time"]; + NSDictionary * posDict = [dict objectForKey:@"position"]; + position.x = [posDict floatForKey:@"x"]; + position.y = [posDict floatForKey:@"y"]; + position.z = [posDict floatForKey:@"z"]; + + // Setup shipsInTransit + NSArray * shipDictsArray = [dict arrayForKey:@"ships"]; + NSEnumerator * shipDicts = [shipDictsArray objectEnumerator]; + NSDictionary * currShipDict; + [shipsInTransit removeAllObjects]; + while ((currShipDict = [shipDicts nextObject]) != nil) + { + double time = [currShipDict doubleForKey:@"time_delta"]; + NSDictionary * myShipDict = [currShipDict objectForKey:@"ship"]; + ShipEntity * ship = [ShipEntity alloc]; + ship = [ship initWithDictionary:myShipDict]; + // MKW 20090815 - Check to make sure the ship loaded ok - if not, let's try to load + // a compatible alternative. + if( !ship ) + { + OOLog(@"wormhole.load.warning", @"Ship '%@' failed to initialize - missing OXP? Attempting to replace with random ship using roles '%@'.", + [myShipDict stringForKey:@"name"], [myShipDict stringForKey:@"roles"]); + OORoleSet * roleSet = [OORoleSet roleSetWithString:[myShipDict stringForKey:@"roles"]]; + NSString * shipRole = [roleSet anyRole]; + NSString * shipKey = [[OOShipRegistry sharedRegistry] randomShipKeyForRole:shipRole]; + if( shipKey ) + { + ship = [ShipEntity alloc]; + myShipDict = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipKey]; + ship = [ship initWithDictionary:myShipDict]; + } + if( ship ) + { + OOLog(@"wormhole.load.warning", @"Loaded alternative ship '%@' with role '%@'.", + [ship name], shipRole); + } + else + { + OOLog(@"wormhole.load.warning", @"Failed to load alternative ship - skipping Wormhole ship."); + } + } + + if( ship ) + { + [shipsInTransit addObject:[NSDictionary dictionaryWithObjectsAndKeys: + ship, @"ship", + [NSNumber numberWithDouble:time], @"time", + nil]]; + [ship release]; + } + /* + [shipsInTransit addObject:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithDouble:[currShipDict doubleForKey:@"time_delta"]], @"time", + [[ShipEntity alloc] initWithDictionary:[currShipDict objectForKey:@"ship"]], @"ship", + nil]]; + */ + } + [pool release]; + } + return self; +} + +- (WormholeEntity*) initWormholeTo:(Random_Seed) s_seed fromShip:(ShipEntity *) ship +{ + assert(ship != nil); + + if ((self = [self init])) + { + double now = [[PlayerEntity sharedPlayer] clockTimeAdjusted]; + double distance; + + origin = [UNIVERSE systemSeed]; + destination = s_seed; + distance = distanceBetweenPlanetPositions(destination.d, destination.b, origin.d, origin.b); + witch_mass = [ship mass]; + expiry_time = now + (witch_mass / WORMHOLE_SHRINK_RATE); + travel_time = (distance * distance * 3600); // Taken from PlayerEntity.h + arrival_time = now + travel_time; + position = [ship position]; + zero_distance = distance2([[PlayerEntity sharedPlayer] position], position); + } + return self; +} + + +- (BOOL) suckInShip:(ShipEntity *) ship +{ + if (equal_seeds(destination, [UNIVERSE systemSeed])) + return NO; // far end of the wormhole! + + if (!ship) + return NO; + + double now = [[PlayerEntity sharedPlayer] clockTimeAdjusted]; + [shipsInTransit addObject:[NSDictionary dictionaryWithObjectsAndKeys: + ship, @"ship", + [NSNumber numberWithDouble: now + travel_time - arrival_time], @"time", + nil]]; + witch_mass += [ship mass]; + expiry_time = now + (witch_mass / WORMHOLE_SHRINK_RATE); + collision_radius = 0.5 * M_PI * pow(witch_mass, 1.0/3.0); + + // witchspace entry effects here + ParticleEntity *ring = [[ParticleEntity alloc] initHyperringFromShip:ship]; // retained + [UNIVERSE addEntity:ring]; + [ring release]; + ring = [[ParticleEntity alloc] initHyperringFromShip:ship]; // retained + [ring setSize:NSMakeSize([ring size].width * -2.5 ,[ring size].height * -2.0 )]; // shrinking! + [UNIVERSE addEntity:ring]; + [ring release]; + + // Should probably pass the wormhole, but they have no JS representation + [ship doScriptEvent:@"shipWillEnterWormhole"]; + [[ship getAI] message:@"ENTERED_WITCHSPACE"]; + + [UNIVERSE removeEntity:ship]; + [[ship getAI] clearStack]; // get rid of any preserved states + + return YES; +} + + +- (void) disgorgeShips +{ + double now = [[PlayerEntity sharedPlayer] clockTimeAdjusted]; + int n_ships = [shipsInTransit count]; + NSMutableArray * shipsStillInTransit = [[NSMutableArray alloc] initWithCapacity:n_ships]; + + int i; + for (i = 0; i < n_ships; i++) + { + ShipEntity* ship = (ShipEntity*)[(NSDictionary*)[shipsInTransit objectAtIndex:i] objectForKey:@"ship"]; + double ship_arrival_time = arrival_time + [(NSNumber*)[(NSDictionary*)[shipsInTransit objectAtIndex:i] objectForKey:@"time"] doubleValue]; + double time_passed = now - ship_arrival_time; + + if (ship_arrival_time > now) + { + [shipsStillInTransit addObject:[shipsInTransit objectAtIndex:i]]; + } + else + { + // Only calculate exit position once so that all ships arrive from the same point + if (!hasExitPosition) + //if ( vector_equal(position, kZeroVector) ) + { + hasExitPosition = YES; + position = [UNIVERSE getWitchspaceExitPosition]; + Quaternion q1; + quaternion_set_random(&q1); + double d1 = SCANNER_MAX_RANGE*((ranrot_rand() % 256)/256.0 - 0.5); + if (abs(d1) < 500.0) // no closer than 500m + d1 += ((d1 > 0.0)? 500.0: -500.0); + Vector v1 = vector_forward_from_quaternion(q1); + position.x += v1.x * d1; // randomise exit position + position.y += v1.y * d1; + position.z += v1.z * d1; + } + [ship setPosition: position]; + [ship setOrientation: [UNIVERSE getWitchspaceExitRotation]]; + [ship setPitch: 0.0]; + [ship setRoll: 0.0]; + + [ship setBounty:[ship bounty]/2]; // adjust legal status for new system + + if ([ship cargoFlag] == CARGO_FLAG_FULL_PLENTIFUL) + [ship setCargoFlag: CARGO_FLAG_FULL_SCARCE]; + + [UNIVERSE addEntity:ship]; + + // Should probably pass the wormhole, but they have no JS representation + [ship doScriptEvent:@"shipExitedWormhole" andReactToAIMessage:@"EXITED WITCHSPACE"]; + + // update the ships's position + [ship update: time_passed]; + } + } + [shipsInTransit release]; + shipsInTransit = shipsStillInTransit; +} + + +- (Random_Seed) origin +{ + return origin; +} + +- (Random_Seed) destination +{ + return destination; +} + +- (double) expiryTime +{ + return expiry_time; +} + +- (double) arrivalTime +{ + return arrival_time; +} + +- (double) travelTime +{ + return travel_time; +} + +- (double) scanTime +{ + return scan_time; +} + +- (BOOL) isScanned +{ + return scan_info > WH_SCANINFO_NONE; +} + +- (void) setScannedAt:(double)p_scanTime +{ + if( scan_info == WH_SCANINFO_NONE ) + { + scan_time = p_scanTime; + scan_info = WH_SCANINFO_SCANNED; + } + // else we previously scanned this wormhole +} + +- (WORMHOLE_SCANINFO) scanInfo +{ + return scan_info; +} + +- (void) setScanInfo:(WORMHOLE_SCANINFO)p_scanInfo +{ + scan_info = p_scanInfo; +} + +- (NSArray*) shipsInTransit +{ + return shipsInTransit; +} + +- (void) dealloc +{ + [shipsInTransit release]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + double now = [UNIVERSE getTime]; + return [NSString stringWithFormat:@"destination: %@ ttl: %.2fs arrival: %@", + [UNIVERSE getSystemName:destination], + WORMHOLE_EXPIRES_TIMEINTERVAL - now, + ClockToString(arrival_time, false)]; +} + +#if WORMHOLE_SCANNER +- (NSString *) identFromShip:(ShipEntity*)ship +{ + assert([ship hasEquipmentItem:@"EQ_WORMHOLE_SCANNER"]); + if ([self scanInfo] >= WH_SCANINFO_DESTINATION) + return [NSString stringWithFormat:DESC(@"wormhole-to-@"), [UNIVERSE getSystemName:destination]]; + else + return DESC(@"wormhole-desc"); +} +#endif + + +- (BOOL) canCollide +{ + if (equal_seeds(destination, [UNIVERSE systemSeed])) + { + return NO; // far end of the wormhole! + } + return (witch_mass > 0.0); +} + + +- (BOOL) checkCloseCollisionWith:(Entity *)other +{ + return !other->isParticle; +} + + +- (void) update:(OOTimeDelta) delta_t +{ + [super update:delta_t]; + + PlayerEntity *player = [PlayerEntity sharedPlayer]; + assert(player != nil); + rotMatrix = OOMatrixForBillboard(position, [player position]); + double now = [player clockTimeAdjusted]; + + if (witch_mass > 0.0) + { + witch_mass -= WORMHOLE_SHRINK_RATE * delta_t; + if (witch_mass < 0.0) + witch_mass = 0.0; + collision_radius = 0.5 * M_PI * pow(witch_mass, 1.0/3.0); + no_draw_distance = collision_radius * collision_radius * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR; + } + + scanClass = (witch_mass > 0.0)? CLASS_WORMHOLE : CLASS_NO_DRAW; + + if (now > expiry_time) + { + //position.x = position.y = position.z = 0; + position = kZeroVector; + [UNIVERSE removeEntity: self]; + } +} + + +- (void) drawEntity:(BOOL) immediate :(BOOL) translucent +{ + if (!UNIVERSE) + return; + + if ([UNIVERSE breakPatternHide]) + return; // DON'T DRAW DURING BREAK PATTERN + + if (zero_distance > no_draw_distance) + return; // TOO FAR AWAY TO SEE + + if (witch_mass < 0.0) + return; + + if (collision_radius <= 0.0) + return; + + if (translucent) + { + // for now, a simple copy of the energy bomb draw routine + double srzd = sqrt(zero_distance); + + GLfloat color_fv[4] = { 0.0, 0.0, 1.0, 0.25}; + + glDisable(GL_CULL_FACE); // face culling + glDisable(GL_TEXTURE_2D); + + glColor4fv(color_fv); + glBegin(GL_TRIANGLE_FAN); + // + GLDrawBallBillboard(collision_radius, 4, srzd); + // + glEnd(); + + DrawWormholeCorona(0.67 * collision_radius, collision_radius, 4, srzd, color_fv); + + glEnable(GL_CULL_FACE); // face culling + } + CheckOpenGLErrors(@"WormholeEntity after drawing %@", self); +} + + +static void DrawWormholeCorona(GLfloat inner_radius, GLfloat outer_radius, int step, GLfloat z_distance, GLfloat *col4v1) +{ + if (outer_radius >= z_distance) // inside the sphere + return; + int i; + + NSRange activity = { 0.34, 1.0 }; + + GLfloat s0, c0, s1, c1; + + GLfloat r0, r1; + GLfloat rv0, rv1, q; + + GLfloat theta, delta, halfStep; + + r0 = outer_radius * z_distance / sqrt(z_distance * z_distance - outer_radius * outer_radius); + r1 = inner_radius * z_distance / sqrt(z_distance * z_distance - inner_radius * inner_radius); + + delta = step * M_PI / 180.0f; + halfStep = 0.5f * delta; + theta = 0.0f; + + glBegin(GL_TRIANGLE_STRIP); + for (i = 0; i < 360; i += step ) + { + theta += delta; + + rv0 = randf(); + rv1 = randf(); + + q = activity.location + rv0 * activity.length; + + s0 = r0 * sinf(theta); + c0 = r0 * cosf(theta); + glColor4f(col4v1[0] * q, col4v1[1] * q, col4v1[2] * q, col4v1[3] * rv0); + glVertex3f(s0, c0, 0.0); + + s1 = r1 * sinf(theta - halfStep) * 0.5 * (1.0 + rv1); + c1 = r1 * cosf(theta - halfStep) * 0.5 * (1.0 + rv1); + glColor4f(col4v1[0], col4v1[1], col4v1[2], 0.0); + glVertex3f(s1, c1, 0.0); + + } + // repeat last values to close + rv0 = randf(); + rv1 = randf(); + + q = activity.location + rv0 * activity.length; + + s0 = 0.0f; // r0 * sinf(0); + c0 = r0; // r0 * cosf(0); + glColor4f(col4v1[0] * q, col4v1[1] * q, col4v1[2] * q, col4v1[3] * rv0); + glVertex3f(s0, c0, 0.0); + + s1 = r1 * sinf(halfStep) * 0.5 * (1.0 + rv1); + c1 = r1 * cosf(halfStep) * 0.5 * (1.0 + rv1); + glColor4f(col4v1[0], col4v1[1], col4v1[2], 0.0); + glVertex3f(s1, c1, 0.0); + + glEnd(); +} + +- (NSDictionary *)getDict +{ + NSMutableDictionary * myDict = [[NSMutableDictionary dictionary] retain]; + NSString * str = nil; + + str = [NSString stringWithFormat:@"%d %d %d %d %d %d",origin.a, origin.b, origin.c, origin.d, origin.e, origin.f]; + [myDict setObject:str forKey:@"origin_seed"]; + str = [NSString stringWithFormat:@"%d %d %d %d %d %d",destination.a, destination.b, destination.c, destination.d, destination.e, destination.f]; + [myDict setObject:str forKey:@"dest_seed"]; + // Anything converting a wormhole to a dictionary should already have + // modified its time to shipClock time + [myDict setFloat:(expiry_time) forKey:@"expiry_time"]; + [myDict setFloat:(arrival_time) forKey:@"arrival_time"]; + [myDict setObject:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithFloat:position.x], @"x", + [NSNumber numberWithFloat:position.y], @"y", + [NSNumber numberWithFloat:position.z], @"z", + nil] forKey:@"position"]; + + NSMutableArray * shipArray = [NSMutableArray arrayWithCapacity:[shipsInTransit count]]; + NSEnumerator * ships = [shipsInTransit objectEnumerator]; + NSDictionary * currShipDict = nil; + while ((currShipDict = [ships nextObject]) != nil) + { + /* + NSMutableDictionary * myShipDict = [NSMutableDictionary dictionary]; + [myShipDict setFloat:([currShipDict doubleForKey:@"time"]) forKey:@"time_delta"]; + ShipEntity * currShip = (ShipEntity*)[currShipDict objectForKey:@"ship"]; + [myShipDict setObject:[currShip shipInfoDictionary] forKey:@"ship"]; + [shipArray addObject:myShipDict]; + */ + [shipArray addObject:[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithDouble:[currShipDict doubleForKey:@"time"]], @"time_delta", + [[currShipDict objectForKey:@"ship"] shipInfoDictionary], @"ship", + nil]]; + } + [myDict setObject:shipArray forKey:@"ships"]; + + return myDict; +} + +@end diff --git a/src/Core/GameController.h b/src/Core/GameController.h new file mode 100644 index 00000000..35c04fac --- /dev/null +++ b/src/Core/GameController.h @@ -0,0 +1,166 @@ +/* + +GameController.h + +Main application controller class. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOCocoa.h" + +#define MODE_WINDOWED 100 +#define MODE_FULL_SCREEN 200 + +#define DISPLAY_MIN_COLOURS 32 +#define DISPLAY_MIN_WIDTH 640 +#define DISPLAY_MIN_HEIGHT 480 + +#ifndef GNUSTEP +/* OS X apps are permitted to assume 800x600 screens. Under OS X, we always + start up in windowed mode. Therefore, the default size fits an 800x600 + screen and leaves space for the menu bar and title bar. +*/ +#define DISPLAY_DEFAULT_WIDTH 800 +#define DISPLAY_DEFAULT_HEIGHT 540 +#define DISPLAY_DEFAULT_REFRESH 75 +#endif + +#define DISPLAY_MAX_WIDTH 2400 +#define DISPLAY_MAX_HEIGHT 1800 + +#define MINIMUM_GAME_TICK 0.25 +// * reduced from 0.5s for tgape * // + + +@class MyOpenGLView, OOProgressBar; + + +#if OOLITE_MAC_OS_X +#define kOODisplayWidth ((NSString *)kCGDisplayWidth) +#define kOODisplayHeight ((NSString *)kCGDisplayHeight) +#define kOODisplayRefreshRate ((NSString *)kCGDisplayRefreshRate) +#define kOODisplayBitsPerPixel ((NSString *)kCGDisplayBitsPerPixel) +#define kOODisplayIOFlags ((NSString *)kCGDisplayIOFlags) +#else +#define kOODisplayWidth (@"Width") +#define kOODisplayHeight (@"Height") +#define kOODisplayRefreshRate (@"RefreshRate") +#endif + + +@interface GameController : NSObject +{ +#if OOLITE_HAVE_APPKIT + IBOutlet NSTextField *splashProgressTextField; + IBOutlet NSView *splashView; + IBOutlet NSWindow *gameWindow; + IBOutlet NSTextView *helpView; + IBOutlet OOProgressBar *progressBar; +#endif + +#if OOLITE_SDL + NSRect fsGeometry; + MyOpenGLView *switchView; +#endif + IBOutlet MyOpenGLView *gameView; + + NSTimeInterval last_timeInterval; + double delta_t; + + int my_mouse_x, my_mouse_y; + + NSString *playerFileDirectory; + NSString *playerFileToLoad; + NSMutableArray *expansionPathsToInclude; + + NSTimer *timer; + + /* GDC example code */ + + NSMutableArray *displayModes; + + unsigned int width, height; + unsigned int refresh; + BOOL fullscreen; + NSDictionary *originalDisplayMode; + NSDictionary *fullscreenDisplayMode; + +#if OOLITE_MAC_OS_X + NSOpenGLContext *fullScreenContext; +#endif + + BOOL stayInFullScreenMode; + + /* end of GDC */ + + SEL pauseSelector; + NSObject *pauseTarget; + + BOOL gameIsPaused; +} + ++ (id)sharedController; + +- (void) applicationDidFinishLaunching: (NSNotification *)notification; +- (BOOL) gameIsPaused; +- (void) pause_game; +- (void) unpause_game; + +#if OOLITE_HAVE_APPKIT +- (IBAction) goFullscreen:(id)sender; +#elif OOLITE_SDL +- (void) setFullScreenMode:(BOOL)fsm; +#endif +- (void) exitFullScreenMode; +- (BOOL) inFullScreenMode; + +- (void) pauseFullScreenModeToPerform:(SEL) selector onTarget:(id) target; +- (void) exitApp; + +- (BOOL) setDisplayWidth:(unsigned int) d_width Height:(unsigned int)d_height Refresh:(unsigned int) d_refresh; +- (NSDictionary *) findDisplayModeForWidth:(unsigned int)d_width Height:(unsigned int) d_height Refresh:(unsigned int) d_refresh; +- (NSArray *) displayModes; +- (OOUInteger) indexOfCurrentDisplayMode; + +- (NSString *) playerFileToLoad; +- (void) setPlayerFileToLoad:(NSString *)filename; + +- (NSString *) playerFileDirectory; +- (void) setPlayerFileDirectory:(NSString *)filename; + +- (void) loadPlayerIfRequired; + +- (void) beginSplashScreen; +- (void) logProgress:(NSString*) message; +- (void) setProgressBarValue:(float)value; // Negative for hidden +- (void) endSplashScreen; + +- (void) startAnimationTimer; +- (void) stopAnimationTimer; + +- (MyOpenGLView *) gameView; +- (void) setGameView:(MyOpenGLView *)view; + +- (void)windowDidResize:(NSNotification *)aNotification; + +@end + diff --git a/src/Core/GameController.m b/src/Core/GameController.m new file mode 100644 index 00000000..c1d7b16b --- /dev/null +++ b/src/Core/GameController.m @@ -0,0 +1,1028 @@ +/* + +GameController.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "GameController.h" +#import "Universe.h" +#import "ResourceManager.h" +#import "MyOpenGLView.h" +#import "OOSound.h" +#import "OOOpenGL.h" +#import "PlayerEntityLoadSave.h" +#import +#import "OOCollectionExtractors.h" +#import "OOOXPVerifier.h" +#import "OOLoggingExtended.h" +#import "NSFileManagerOOExtensions.h" + +#define kOOLogUnconvertedNSLog @"unclassified.GameController" + + +static GameController *sSharedController = nil; + + +@interface GameController (OOPrivate) + +- (void) getDisplayModes; + +- (void)reportUnhandledStartupException:(NSException *)exception; + +- (void)performGameTick:(id)userInfo; +- (void)doPerformGameTick; + +@end + + +@implementation GameController + ++ (id)sharedController +{ + if (sSharedController == nil) [[self alloc] init]; + return sSharedController; +} + + +- (id) init +{ + if (sSharedController != nil) + { + [self release]; + [NSException raise:NSInternalInconsistencyException format:@"%s: expected only one GameController to exist at a time.", __FUNCTION__]; + } + + self = [super init]; + sSharedController = self; + + last_timeInterval = [NSDate timeIntervalSinceReferenceDate]; + delta_t = 0.01; // one hundredth of a second + // + my_mouse_x = my_mouse_y = 0; + // + playerFileToLoad = nil; + playerFileDirectory = nil; + expansionPathsToInclude = nil; + pauseSelector = (SEL)nil; + pauseTarget = nil; + gameIsPaused = NO; + + return self; +} + + +- (void) dealloc +{ +#if OOLITE_HAVE_APPKIT + [[[NSWorkspace sharedWorkspace] notificationCenter] removeObserver:UNIVERSE]; +#endif + + [timer release]; + [gameView release]; + [UNIVERSE release]; + + [playerFileToLoad release]; + [playerFileDirectory release]; + [expansionPathsToInclude release]; + + [super dealloc]; +} + + +- (BOOL) gameIsPaused +{ + return gameIsPaused; +} + + +- (void) pause_game +{ + gameIsPaused = YES; +} + + +- (void) unpause_game +{ + gameIsPaused = NO; +} + + +- (BOOL) setDisplayWidth:(unsigned int) d_width Height:(unsigned int) d_height Refresh:(unsigned int) d_refresh +{ + NSDictionary *d_mode = [self findDisplayModeForWidth: d_width Height: d_height Refresh: d_refresh]; + if (d_mode) + { + width = d_width; + height = d_height; + refresh = d_refresh; + fullscreenDisplayMode = d_mode; + + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + + [userDefaults setInteger:width forKey:@"display_width"]; + [userDefaults setInteger:height forKey:@"display_height"]; + [userDefaults setInteger:refresh forKey:@"display_refresh"]; + + // Manual synchronization is required for SDL And doesn't hurt much for OS X. + [userDefaults synchronize]; + + return YES; + } + return NO; +} + + +- (OOUInteger) indexOfCurrentDisplayMode +{ + NSDictionary *mode; + + mode = [self findDisplayModeForWidth: width Height: height Refresh: refresh]; + if (mode == nil) + return NSNotFound; + else + return [displayModes indexOfObject:mode]; + + return NSNotFound; +} + + +- (NSArray *) displayModes +{ + return [NSArray arrayWithArray:displayModes]; +} + + +- (MyOpenGLView *) gameView +{ + return gameView; +} + + +- (void) setGameView:(MyOpenGLView *)view +{ + [gameView release]; + gameView = [view retain]; + [gameView setGameController:self]; + [UNIVERSE setGameView:gameView]; +} + + +- (void) applicationDidFinishLaunching:(NSNotification *)notification +{ + NSAutoreleasePool *pool = nil; + unsigned i; + + pool = [[NSAutoreleasePool alloc] init]; + + NS_DURING + // ensures the gameView is drawn to: OpenGL is initialised and so textures can initialse. + [self beginSplashScreen]; + +#if OO_OXP_VERIFIER_ENABLED + if ([OOOXPVerifier runVerificationIfRequested]) + { + [self exitApp]; + } +#endif + + //[self logProgress:@"Getting display modes..."]; //cannot localise strings before loading OXPs + [self logProgress:@"..."]; //language neutral + [self getDisplayModes]; + + // moved to before the Universe is created + if (expansionPathsToInclude) + { + //[self logProgress:@"Loading selected expansion packs..."]; //cannot localise strings before loading OXPs + [self logProgress:@".........."]; //language neutral + for (i = 0; i < [expansionPathsToInclude count]; i++) + { + [ResourceManager addExternalPath: (NSString*)[expansionPathsToInclude objectAtIndex: i]]; + } + } + + // moved here to try to avoid initialising this before having an Open GL context + //[self logProgress:DESC(@"Initialising universe")]; // DESC expansions only possible after the next line! + [[Universe alloc] initWithGameView:gameView]; + + [self logProgress:DESC(@"loading-player")]; + [self loadPlayerIfRequired]; + + [self logProgress:@""]; + + // get the run loop and add the call to performGameTick: + [self startAnimationTimer]; + + [self endSplashScreen]; + NS_HANDLER + [self reportUnhandledStartupException:localException]; + exit(EXIT_FAILURE); + NS_ENDHANDLER + + // Release anything allocated above that is not required. + [pool release]; + +#if !OOLITE_HAVE_APPKIT + [[NSRunLoop currentRunLoop] run]; +#endif +} + + +- (void) loadPlayerIfRequired +{ + if (playerFileToLoad) + { + PlayerEntity *player = [PlayerEntity sharedPlayer]; + [player loadPlayerFromFile:playerFileToLoad]; + [player setStatus:STATUS_DOCKED]; + [player setGuiToStatusScreen]; + } +} + + +- (void) beginSplashScreen +{ +#if !OOLITE_HAVE_APPKIT + + if(!gameView){ + gameView = [MyOpenGLView alloc]; + [gameView init]; + [gameView setGameController:self]; + [gameView initSplashScreen]; + } + +#else + + [gameView updateScreen]; + +#endif +} + + +#if OOLITE_HAVE_APPKIT + +- (void) performGameTick:(id)userInfo +{ + [self doPerformGameTick]; +} + +#else + +- (void) performGameTick:(id)sender +{ + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + [gameView pollControls]; + [self doPerformGameTick]; + + [pool release]; + +} + +#endif + + +- (void)doPerformGameTick +{ + if (gameIsPaused) + delta_t = 0.0; // no movement! + else + { + delta_t = [NSDate timeIntervalSinceReferenceDate] - last_timeInterval; + last_timeInterval += delta_t; + if (delta_t > MINIMUM_GAME_TICK) + delta_t = MINIMUM_GAME_TICK; // peg the maximum pause (at 0.5->1.0 seconds) to protect against when the machine sleeps + } + + [UNIVERSE update:delta_t]; + [OOSound update]; + +#if OOLITE_HAVE_APPKIT + if (fullscreen) + { + [UNIVERSE drawUniverse]; + return; + } +#endif + + if (gameView != nil) [gameView display]; + else OOLog(kOOLogInconsistentState, @"***** gameView not set : delta_t %f",(float)delta_t); +} + + +- (void) startAnimationTimer +{ + if (timer == nil) + { + NSTimeInterval ti = 0.01; + timer = [[NSTimer timerWithTimeInterval:ti target:self selector:@selector(performGameTick:) userInfo:nil repeats:YES] retain]; + [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; + } +} + + +- (void) stopAnimationTimer +{ + if (timer != nil) + { + [timer invalidate]; + [timer release]; + timer = nil; + } +} + + +#if OOLITE_MAC_OS_X && !OOLITE_SDL +static NSComparisonResult CompareDisplayModes(id arg1, id arg2, void *context) +{ + // TODO: If fullscreen mode is practical in GNUstep + NSDictionary *mode1 = (NSDictionary *)arg1; + NSDictionary *mode2 = (NSDictionary *)arg2; + int size1, size2; + + // Sort first on pixel count + size1 = [[mode1 objectForKey:kOODisplayWidth] intValue] * + [[mode1 objectForKey:kOODisplayHeight] intValue]; + size2 = [[mode2 objectForKey:kOODisplayWidth] intValue] * + [[mode2 objectForKey:kOODisplayHeight] intValue]; + + // Then on refresh rate + if (size1 == size2) + { + size1 = (int)[[mode1 objectForKey:kOODisplayRefreshRate] intValue]; + size2 = (int)[[mode2 objectForKey:kOODisplayRefreshRate] intValue]; + } + + return (size1 < size2) ? NSOrderedAscending + : (size1 > size2) ? NSOrderedDescending + : NSOrderedSame; +} + + +- (void) getDisplayModes +{ + unsigned modeIndex, modeCount; + NSArray *modes = nil; + NSDictionary *mode = nil, *mode2 = nil; + unsigned modeWidth, modeHeight, color; + unsigned modeWidth2, modeHeight2, color2; + BOOL stretched, stretched2, interlaced, interlaced2; + float modeRefresh, modeRefresh2; + NSUserDefaults *userDefaults = nil; + BOOL deleteFirst; + + // Load preferences. + userDefaults = [NSUserDefaults standardUserDefaults]; + width = [userDefaults intForKey:@"display_width" defaultValue:DISPLAY_DEFAULT_WIDTH]; + height = [userDefaults intForKey:@"display_height" defaultValue:DISPLAY_DEFAULT_HEIGHT]; + refresh = [userDefaults intForKey:@"display_refresh" defaultValue:DISPLAY_DEFAULT_REFRESH]; + + // Get the list of all available modes + modes = (NSArray *)CGDisplayAvailableModes(kCGDirectMainDisplay); + + // Filter out modes that we don't want + displayModes = [[NSMutableArray alloc] init]; + modeCount = [modes count]; + for (modeIndex = 0; modeIndex < modeCount; modeIndex++) + { + mode = [modes objectAtIndex: modeIndex]; + modeWidth = [mode unsignedIntForKey:kOODisplayWidth]; + modeHeight = [mode unsignedIntForKey:kOODisplayHeight]; + color = [mode unsignedIntForKey:kOODisplayBitsPerPixel]; + modeRefresh = [mode floatForKey:kOODisplayRefreshRate]; + + if (color < DISPLAY_MIN_COLOURS || + modeWidth < DISPLAY_MIN_WIDTH || + modeWidth > DISPLAY_MAX_WIDTH || + modeHeight < DISPLAY_MIN_HEIGHT || + modeHeight > DISPLAY_MAX_HEIGHT) + continue; + [displayModes addObject: mode]; + } + + // Sort the filtered modes + [displayModes sortUsingFunction:CompareDisplayModes context:NULL]; + + // ***JESTER_START*** 11/08/04 + // Powerbooks return several "identical modes" CGDisplayAvailableModes doesn't appear + // to pick up refresh rates. Logged as Radar 3759831. + // In order to deal with this, we'll just edit out the duplicates. + /* + Bug 011893: restoring old display filtering code because my previous + assumption that using a set would filter out "duplicates" was broken. + The modes in question are not actually duplicates. For instance, + stretched modes look like "duplicates" from Oolite's perspective. The + Right Thing is to handle stretched modes properly. Also, the bug that + having "duplicates" causes (bad behaviour in config screen, see bug + 011893) is really down to not tracking the selected display mode index + explictly. + Basically, this needs redoing, but shouldn't hold up 1.70. + -- Ahruman + */ + unsigned int mode2Index = 0; + for (modeIndex = 0; modeIndex + 1 < [displayModes count]; modeIndex++) + { + mode = [displayModes objectAtIndex:modeIndex]; + modeWidth = [mode unsignedIntForKey:kOODisplayWidth]; + modeHeight = [mode unsignedIntForKey:kOODisplayHeight]; + modeRefresh = [mode floatForKey:kOODisplayRefreshRate]; + color = [mode unsignedIntForKey:kOODisplayBitsPerPixel]; + stretched = [mode boolForKey:(NSString *)kCGDisplayModeIsStretched]; + interlaced = [mode boolForKey:(NSString *)kCGDisplayModeIsInterlaced]; + + for (mode2Index = modeIndex + 1; mode2Index < [displayModes count]; ++mode2Index) + { + mode2 = [displayModes objectAtIndex:mode2Index]; + modeWidth2 = [mode2 unsignedIntForKey:kOODisplayWidth]; + modeHeight2 = [mode2 unsignedIntForKey:kOODisplayHeight]; + modeRefresh2 = [mode2 floatForKey:kOODisplayRefreshRate]; + color2 = [mode unsignedIntForKey:kOODisplayBitsPerPixel]; + stretched2 = [mode2 boolForKey:(NSString *)kCGDisplayModeIsStretched]; + interlaced2 = [mode2 boolForKey:(NSString *)kCGDisplayModeIsInterlaced]; + + if (modeWidth == modeWidth2 && + modeHeight == modeHeight2 && + modeRefresh == modeRefresh2) + { + /* Modes are "duplicates" from Oolite's perspective, so one + needs to be removed. If one has higher colour depth, use + that one. Otherwise, If one is stretched and the other + isn't, remove the stretched one. Otherwise, if one is + interlaced and the other isn't, remove the interlaced one. + Otherwise, remove the one that comes later in the list. + */ + deleteFirst = NO; + if (color < color2) deleteFirst = YES; + else if (color == color2) + { + if (stretched && !stretched2) deleteFirst = YES; + else if (stretched == stretched2) + { + if (interlaced && !interlaced2) deleteFirst = YES; + } + } + if (deleteFirst) + { + [displayModes removeObjectAtIndex:modeIndex]; + modeIndex--; + break; + } + else + { + [displayModes removeObjectAtIndex:mode2Index]; + mode2Index--; + } + } + } + } + + if ([displayModes count] == 0) + { + [NSException raise:@"OoliteNoDisplayModes" + format:@"No acceptable display modes could be found!"]; + } + + fullscreenDisplayMode = [self findDisplayModeForWidth:width Height:height Refresh:refresh]; + if (fullscreenDisplayMode == nil) + { + // set full screen mode to first available mode + fullscreenDisplayMode = [displayModes objectAtIndex:0]; + width = [[fullscreenDisplayMode objectForKey:kOODisplayWidth] intValue]; + height = [[fullscreenDisplayMode objectForKey:kOODisplayHeight] intValue]; + refresh = [[fullscreenDisplayMode objectForKey:kOODisplayRefreshRate] intValue]; + } +} + + +- (IBAction) goFullscreen:(id) sender +{ + CGLContextObj cglContext; + CGDisplayErr err; + GLint oldSwapInterval; + GLint newSwapInterval; + CGMouseDelta mouse_dx, mouse_dy; + + // empty the event queue and strip all keys - stop problems with hangover keys + { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + while ([NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES] != NULL) {} + + [pool release]; + [gameView clearKeys]; + } + + pauseTarget = sender; + + my_mouse_x = my_mouse_y = 0; + + while (pauseTarget) + { + CGPoint centerOfScreen = CGPointMake(width/2.0F,height/2.0F); + + pauseTarget = nil; + + //get the appropriate display mode for the selected values + + fullscreenDisplayMode = [self findDisplayModeForWidth:width Height:height Refresh:refresh]; + if (fullscreenDisplayMode == nil) + { + OOLog(@"display.mode.noneFound", @"***** unable to find suitable full screen mode"); + return; + } + + originalDisplayMode = (NSDictionary *)CGDisplayCurrentMode(kCGDirectMainDisplay); + + // Pixel Format Attributes for the FullScreen NSOpenGLContext + NSOpenGLPixelFormatAttribute attrs[] = { + + // Specify that we want a full-screen OpenGL context. + NSOpenGLPFAFullScreen, +// // and that we want a windowed OpenGL context. +// NSOpenGLPFAWindow, + + // We may be on a multi-display system (and each screen may be driven by a different renderer), so we need to specify which screen we want to take over. + // For this demo, we'll specify the main screen. + NSOpenGLPFAScreenMask, CGDisplayIDToOpenGLDisplayMask(kCGDirectMainDisplay), + + // Specifying "NoRecovery" gives us a context that cannot fall back to the software renderer. + //This makes the View-based context a compatible with the fullscreen context, enabling us to use the "shareContext" + // feature to share textures, display lists, and other OpenGL objects between the two. + NSOpenGLPFANoRecovery, + + // Attributes Common to FullScreen and non-FullScreen + NSOpenGLPFACompliant, + + NSOpenGLPFAColorSize, 32, + NSOpenGLPFADepthSize, 32, + NSOpenGLPFADoubleBuffer, + NSOpenGLPFAAccelerated, +#if FSAA + // Need a preference or other sane way to activate this + NSOpenGLPFASampleBuffers, 1, + NSOpenGLPFASamples,4, +#endif + 0 + }; + GLint rendererID; + + // Create the FullScreen NSOpenGLContext with the attributes listed above. + NSOpenGLPixelFormat *pixelFormat = [[NSOpenGLPixelFormat alloc] initWithAttributes:attrs]; + + // Just as a diagnostic, report the renderer ID that this pixel format binds to. CGLRenderers.h contains a list of known renderers and their corresponding RendererID codes. + [pixelFormat getValues:&rendererID forAttribute:NSOpenGLPFARendererID forVirtualScreen:0]; + + // Create an NSOpenGLContext with the FullScreen pixel format. By specifying the non-FullScreen context as our "shareContext", we automatically inherit all of the textures, display lists, and other OpenGL objects it has defined. + fullScreenContext = [[NSOpenGLContext alloc] initWithFormat:pixelFormat shareContext:[gameView openGLContext]]; + [pixelFormat release]; + pixelFormat = nil; + + if (fullScreenContext == nil) + { + OOLog(@"display.context.create.failed", @"***** Failed to create fullScreenContext"); + return; + } + + + // Pause animation in the OpenGL view. While we're in full-screen mode, we'll drive the animation actively + // instead of using a timer callback. + if (timer) + [self stopAnimationTimer]; + + // Take control of the display where we're about to go FullScreen. + // this stops windows from being shuffled around. + err = CGCaptureAllDisplays(); + if (err != CGDisplayNoErr) + { + [fullScreenContext release]; + fullScreenContext = nil; + return; + } + + // switch resolution! + err = CGDisplaySwitchToMode(kCGDirectMainDisplay, (CFDictionaryRef)fullscreenDisplayMode); + if (err != CGDisplayNoErr) + { + OOLog(@"display.mode.switch.failed", @"***** Unable to change display mode."); + return; + } + + // Hide the cursor + CGDisplayMoveCursorToPoint(kCGDirectMainDisplay,centerOfScreen); + CGDisplayHideCursor(kCGDirectMainDisplay); + + // Enter FullScreen mode and make our FullScreen context the active context for OpenGL commands. + [fullScreenContext setFullScreen]; + [fullScreenContext makeCurrentContext]; + + // Save the current swap interval so we can restore it later, and then set the new swap interval to lock us to the display's refresh rate. + cglContext = CGLGetCurrentContext(); + CGLGetParameter(cglContext, kCGLCPSwapInterval, &oldSwapInterval); + newSwapInterval = 1; + CGLSetParameter(cglContext, kCGLCPSwapInterval, &newSwapInterval); + + // Tell the scene the dimensions of the area it's going to render to, so it can set up an appropriate viewport and viewing transformation. + [gameView initialiseGLWithSize:NSMakeSize(width,height)]; + + // Now that we've got the screen, we enter a loop in which we alternately process input events and computer and render the next frame of our animation. + // The shift here is from a model in which we passively receive events handed to us by the AppKit to one in which we are actively driving event processing. + stayInFullScreenMode = YES; + + fullscreen = YES; + + BOOL past_first_mouse_delta = NO; + + while (stayInFullScreenMode) + { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + + // Check for and process input events. + NSEvent *event; + while ((event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])) + { + switch ([event type]) + { + case NSLeftMouseDown: + [gameView mouseDown:event]; + break; + + case NSRightMouseDown: + my_mouse_x = my_mouse_y = 0; // center mouse + past_first_mouse_delta = NO; + [gameView setVirtualJoystick:0.0 :0.0]; + break; + + case NSLeftMouseUp: + [gameView mouseUp:event]; + break; + + case NSMouseMoved: + case NSLeftMouseDragged: +// case NSRightMouseDragged: // avoid conflict with NSRightMouseDown + case NSOtherMouseDragged: + CGGetLastMouseDelta(&mouse_dx, &mouse_dy); + if (past_first_mouse_delta) + { + my_mouse_x += mouse_dx; + my_mouse_y += mouse_dy; + } + else past_first_mouse_delta =YES; + + [gameView setVirtualJoystick:(double)my_mouse_x/width :(double)my_mouse_y/height]; + CGDisplayMoveCursorToPoint(kCGDirectMainDisplay,centerOfScreen); + break; + + case NSKeyDown: + [gameView keyDown:event]; + break; + + case NSFlagsChanged: + [gameView flagsChanged:event]; + break; + + case NSKeyUp: + [gameView keyUp:event]; + break; + + default: + break; + } + } + + // Update our stuff. + [self performGameTick:self]; + + [fullScreenContext flushBuffer]; + + // Clean up any autoreleased objects that were created this time through the loop. + [pool release]; + } + + // Clear the front and back framebuffers before switching out of FullScreen mode. + // (This is not strictly necessary, but avoids an untidy flash of garbage.) + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + [fullScreenContext flushBuffer]; + glClear(GL_COLOR_BUFFER_BIT); + [fullScreenContext flushBuffer]; + + // Restore the previously set swap interval. + CGLSetParameter(cglContext, kCGLCPSwapInterval, &oldSwapInterval); + + // Exit fullscreen mode and release our FullScreen NSOpenGLContext. + [NSOpenGLContext clearCurrentContext]; + [fullScreenContext clearDrawable]; + [fullScreenContext release]; + fullScreenContext = nil; + + // switch resolution back! + err = CGDisplaySwitchToMode(kCGDirectMainDisplay, (CFDictionaryRef)originalDisplayMode); + if (err != CGDisplayNoErr) + { + OOLog(@"display.mode.switch.failed", @"***** Unable to change display mode."); + return; + } + + // show the cursor + CGDisplayShowCursor(kCGDirectMainDisplay); + + // Release control of the displays. + CGReleaseAllDisplays(); + + fullscreen = NO; + + // Resume animation timer firings. + [self startAnimationTimer]; + + // Mark our view as needing drawing. (The animation has advanced while we were in FullScreen mode, so its current contents are stale.) + [gameView setNeedsDisplay:YES]; + + if (pauseTarget) + { + [pauseTarget performSelector:pauseSelector]; + } + + } +} + + +- (BOOL) inFullScreenMode +{ + return fullscreen; +} + +#elif OOLITE_SDL + +- (void) getDisplayModes +{ + NSArray *modes = [gameView getScreenSizeArray]; + NSDictionary *mode = nil; + unsigned int modeIndex, modeCount; + unsigned int modeWidth, modeHeight; + + displayModes = [[NSMutableArray alloc] init]; + modeCount = [modes count]; + for (modeIndex = 0; modeIndex < modeCount; modeIndex++) + { + mode = [modes objectAtIndex: modeIndex]; + modeWidth = [[mode objectForKey: kOODisplayWidth] intValue]; + modeHeight = [[mode objectForKey: kOODisplayHeight] intValue]; + + if (modeWidth < DISPLAY_MIN_WIDTH || + modeWidth > DISPLAY_MAX_WIDTH || + modeHeight < DISPLAY_MIN_HEIGHT || + modeHeight > DISPLAY_MAX_HEIGHT) + continue; + [displayModes addObject: mode]; + } + + NSSize fsmSize = [gameView currentScreenSize]; + width = fsmSize.width; + height = fsmSize.height; +} + + +- (void) setFullScreenMode:(BOOL)fsm +{ + fullscreen = fsm; +} + + +- (BOOL) inFullScreenMode +{ + return [gameView inFullScreenMode]; +} + +#else + #error Unknown environment! +#endif + + +#if OOLITE_HAVE_APPKIT + +- (void) logProgress:(NSString *)message +{ + [splashProgressTextField setStringValue:message]; [splashProgressTextField display]; +} + + +- (void) setProgressBarValue:(float)value +{ + [progressBar setDoubleValue:value]; + [progressBar display]; +} + + +- (void) endSplashScreen +{ + [gameWindow setAcceptsMouseMovedEvents:YES]; + [gameWindow setContentView:gameView]; + [gameWindow makeFirstResponder:gameView]; +} + + +// NIB methods +- (void)awakeFromNib +{ + NSString *path = nil; + + // Set contents of Help window + path = [[NSBundle mainBundle] pathForResource:@"ReadMe" ofType:@"rtfd"]; + if (path != nil) + { + [helpView readRTFDFromFile:path]; + } +} + + +// delegate methods +- (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename +{ + if ([[filename pathExtension] isEqual:@"oolite-save"]) + { + [self setPlayerFileToLoad:filename]; + [self setPlayerFileDirectory:filename]; + return YES; + } + if ([[filename pathExtension] isEqualToString:@"oxp"] || + [[filename pathExtension] isEqual:@"oolite_expansion_pack"]) + { + BOOL dir_test; + [[NSFileManager defaultManager] fileExistsAtPath:filename isDirectory:&dir_test]; + if (dir_test) + { + if (!expansionPathsToInclude) + expansionPathsToInclude = [[NSMutableArray alloc] initWithCapacity: 4]; // retained + [expansionPathsToInclude addObject: filename]; + return YES; + } + } + return NO; +} + + +- (void) exitApp +{ +#if OOLITE_GNUSTEP + [[NSNotificationCenter defaultCenter] postNotificationName:@"ApplicationWillTerminate" object:self]; +#endif + [NSApp terminate:self]; +} + + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender +{ + OOLoggingTerminate(); + return NSTerminateNow; +} + +#elif OOLITE_SDL + +- (void) logProgress:(NSString *)message +{} + +- (void) setProgressBarValue:(float)value +{} + +- (void) endSplashScreen +{ + [gameView endSplashScreen]; +} +- (void) exitApp +{ + [[NSUserDefaults standardUserDefaults] synchronize]; + OOLog(@"gameController.exitApp",@".GNUstepDefaults synchronized."); + OOLoggingTerminate(); + SDL_Quit(); + exit(0); +} + +#else + #error Unknown environment! +#endif + +- (NSDictionary *) findDisplayModeForWidth:(unsigned int) d_width Height:(unsigned int) d_height Refresh:(unsigned int) d_refresh +{ + int i, modeCount; + NSDictionary *mode; + unsigned int modeWidth, modeHeight, modeRefresh; + + modeCount = [displayModes count]; + + for (i = 0; i < modeCount; i++) + { + mode = [displayModes objectAtIndex: i]; + modeWidth = [[mode objectForKey:kOODisplayWidth] intValue]; + modeHeight = [[mode objectForKey:kOODisplayHeight] intValue]; + modeRefresh = [[mode objectForKey:kOODisplayRefreshRate] intValue]; + if ((modeWidth == d_width)&&(modeHeight == d_height)&&(modeRefresh == d_refresh)) + { + return mode; + } + } + return nil; +} + +- (void) exitFullScreenMode +{ + stayInFullScreenMode = NO; +} + + +- (void) pauseFullScreenModeToPerform:(SEL) selector onTarget:(id) target +{ + pauseSelector = selector; + pauseTarget = target; + stayInFullScreenMode = NO; +} + + +- (void)windowDidResize:(NSNotification *)aNotification +{ + [gameView updateScreen]; +} + + +- (NSString *) playerFileToLoad +{ + return playerFileToLoad; +} + + +- (void) setPlayerFileToLoad:(NSString *)filename +{ + if (playerFileToLoad) + [playerFileToLoad autorelease]; + playerFileToLoad = nil; + if ([[[filename pathExtension] lowercaseString] isEqual:@"oolite-save"]) + playerFileToLoad = [[NSString stringWithString:filename] retain]; +} + + +- (NSString *) playerFileDirectory +{ + if (playerFileDirectory == nil) + { + playerFileDirectory = [[NSUserDefaults standardUserDefaults] stringForKey:@"save-directory"]; + if (playerFileDirectory != nil && ![[NSFileManager defaultManager] fileExistsAtPath:playerFileDirectory]) + { + playerFileDirectory = nil; + } + if (playerFileDirectory == nil) playerFileDirectory = [[NSFileManager defaultManager] defaultCommanderPath]; + + [playerFileDirectory retain]; + } + + return playerFileDirectory; +} + + +- (void) setPlayerFileDirectory:(NSString *)filename +{ + if (playerFileDirectory != nil) + { + [playerFileDirectory autorelease]; + playerFileDirectory = nil; + } + + if ([[[filename pathExtension] lowercaseString] isEqual:@"oolite-save"]) + { + filename = [filename stringByDeletingLastPathComponent]; + } + + playerFileDirectory = [filename retain]; + [[NSUserDefaults standardUserDefaults] setObject:filename forKey:@"save-directory"]; +} + + +- (void)reportUnhandledStartupException:(NSException *)exception +{ + OOLog(@"startup.exception", @"***** Unhandled exception during startup: %@ (%@).", [exception name], [exception reason]); + + #if OOLITE_MAC_OS_X + // Display an error alert. + // TODO: provide better information on reporting bugs in the manual, and refer to it here. + NSRunCriticalAlertPanel(@"Oolite failed to start up, because an unhandled exception occurred.", @"An exception of type %@ occurred. If this problem persists, please file a bug report.", @"OK", NULL, NULL, [exception name]); + #endif +} + +@end + + diff --git a/src/Core/Geometry.h b/src/Core/Geometry.h new file mode 100644 index 00000000..333df501 --- /dev/null +++ b/src/Core/Geometry.h @@ -0,0 +1,66 @@ +/* + +Geometry.h + +Class for reasoning about triangle meshes, in particular for the creation of +octtrees for collision-detection purposes. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + +#import "OOMaths.h" + +@class ShipEntity, Octree; + +@interface Geometry : NSObject +{ + // a geometry essentially consists of a whole bunch of Triangles. + // Note: simply making these unsigned will break octree generation. If trying it, don't forget to flush the cache. -- Ahruman 20081101 + OOInteger n_triangles; // how many triangles in the geometry + OOInteger max_triangles; // how many triangles are allowed in the geometry before expansion + Triangle *triangles; // pointer to an array of triangles which we'll grow as necessary... + BOOL isConvex; // set at initialisation to NO +} + +- (id) initWithCapacity:(OOUInteger)amount; + +- (BOOL) isConvex; +- (void) setConvex:(BOOL) value; + +- (void) addTriangle:(Triangle) tri; + +- (BOOL) testHasGeometry; +- (BOOL) testIsConvex; +- (BOOL) testCornersWithinGeometry:(GLfloat) corner; +- (GLfloat) findMaxDimensionFromOrigin; + +- (Octree*) findOctreeToDepth: (int) depth; +- (id) octreeWithinRadius:(GLfloat) octreeRadius toDepth: (int) depth; + +- (void) translate:(Vector) offset; +- (void) scale:(GLfloat) scalar; + +- (void) x_axisSplitBetween:(Geometry*) g_plus :(Geometry*) g_minus :(GLfloat) x; +- (void) y_axisSplitBetween:(Geometry*) g_plus :(Geometry*) g_minus :(GLfloat) y; +- (void) z_axisSplitBetween:(Geometry*) g_plus :(Geometry*) g_minus :(GLfloat) z; + +@end diff --git a/src/Core/Geometry.m b/src/Core/Geometry.m new file mode 100644 index 00000000..77d5e58a --- /dev/null +++ b/src/Core/Geometry.m @@ -0,0 +1,733 @@ +/* + +Geometry.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Geometry.h" + +#import "OOMaths.h" +#import "Octree.h" +#import "OOLogging.h" + + +@implementation Geometry + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"%u triangles, %@", n_triangles, [self testIsConvex]?@"convex":@"not convex"]; +} + +- (id)initWithCapacity:(OOUInteger)amount +{ + if (amount < 1) + return nil; + self = [super init]; + +// OOLog(@"geometry.init", @"Geometry inited with capacity %i", amount); + + max_triangles = amount; + triangles = malloc(max_triangles * sizeof(Triangle)); // allocate the required space + n_triangles = 0; + isConvex = NO; + + return self; +} + +- (void) dealloc +{ + free(triangles); // free up the allocated space + [super dealloc]; +} + +- (BOOL) isConvex +{ + return isConvex; +} + +- (void) setConvex:(BOOL) value +{ + isConvex = value; +} + + +// SLOW_CODE This is a relatively costly method. A lot of growing is done despite the 1 + capacity * 2 growth rate. +- (void) addTriangle:(Triangle) tri +{ + // check for degenerate triangles + if ((tri.v[0].x == tri.v[1].x)&&(tri.v[0].y == tri.v[1].y)&&(tri.v[0].z == tri.v[1].z)) // v0 == v1 -> return + return; + if ((tri.v[1].x == tri.v[2].x)&&(tri.v[1].y == tri.v[2].y)&&(tri.v[1].z == tri.v[2].z)) // v1 == v2 -> return + return; + if ((tri.v[2].x == tri.v[0].x)&&(tri.v[2].y == tri.v[0].y)&&(tri.v[2].z == tri.v[0].z)) // v2 == v0 -> return + return; + // clear! + // + // check for no-more-room + if (EXPECT_NOT(n_triangles == max_triangles)) + { + // create more space by doubling the capacity of this geometry... + // OOLog(@"geometry.grow", @"Expanding geometry %p from %u to %u triangles.", self, max_triangles, 1 + max_triangles * 2); + max_triangles = 1 + max_triangles * 2; + triangles = realloc(triangles, max_triangles * sizeof(Triangle)); + if (EXPECT_NOT(triangles == NULL)) + { + OOLog(kOOLogAllocationFailure, @"!!!!! Ran out of memory to allocate more geometry!"); + exit(-1); + } + } + triangles[n_triangles++] = tri; +} + +- (BOOL) testHasGeometry +{ + return (n_triangles > 0); +} + +- (BOOL) testIsConvex +{ + // enumerate over triangles + // calculate normal for each one + // then enumerate over vertices relative to a vertex on the triangle + // and check if they are on the forwardside or coplanar with the triangle + // if a vertex is on the backside of any triangle then return NO; + OOInteger i, j; + for (i = 0; i < n_triangles; i++) + { + Vector v0 = triangles[i].v[0]; + Vector vn = calculateNormalForTriangle(&triangles[i]); + // + for (j = 0; j < n_triangles; j++) + { + if (j != i) + { + if ((dot_product(vector_between(v0, triangles[j].v[0]), vn) < -0.001)|| + (dot_product(vector_between(v0, triangles[j].v[1]), vn) < -0.001)|| + (dot_product(vector_between(v0, triangles[j].v[2]), vn) < -0.001)) // within 1mm tolerance + { + isConvex = NO; + return NO; + } + } + } + } + isConvex = YES; + return YES; +} + +- (BOOL) testCornersWithinGeometry:(GLfloat) corner; +{ + // enumerate over triangles + // calculate normal for each one + // then enumerate over corners relative to a vertex on the triangle + // and check if they are on the forwardside or coplanar with the triangle + // if a corner is on the backside of any triangle then return NO; + OOInteger i, x, y, z; + for (i = 0; i < n_triangles; i++) + { + Vector v0 = triangles[i].v[0]; + Vector vn = calculateNormalForTriangle(&triangles[i]); + // + for (z = -1; z < 2; z += 2) for (y = -1; y < 2; y += 2) for (x = -1; x < 2; x += 2) + { + Vector vc = make_vector(corner * x, corner * y, corner * z); + if (dot_product(vector_between(v0, vc), vn) < -0.001) + return NO; + } + } + return YES; +} + +- (GLfloat) findMaxDimensionFromOrigin +{ + // enumerate over triangles + GLfloat result = 0; + OOInteger i, j; + for (i = 0; i < n_triangles; i++) for (j = 0; j < 3; j++) + { + Vector v = triangles[i].v[j]; + if (fabs(v.x) > result) + result = (float)fabs(v.x); + if (fabs(v.y) > result) + result = (float)fabs(v.y); + if (fabs(v.z) > result) + result = (float)fabs(v.z); + } + return result; +} + +static int leafcount; +static float volumecount; +- (Octree*) findOctreeToDepth: (int) depth +{ + // + leafcount = 0; + volumecount = 0.0f; + // + GLfloat foundRadius = 0.5f + [self findMaxDimensionFromOrigin]; // pad out from geometry by a half meter + // + NSObject* foundOctree = [self octreeWithinRadius:foundRadius toDepth:depth]; + // + Octree* octreeRepresentation = [[Octree alloc] initWithRepresentationOfOctree:foundRadius :foundOctree :leafcount]; + // + return [octreeRepresentation autorelease]; +} + +- (id) octreeWithinRadius:(GLfloat) octreeRadius toDepth: (int) depth; +{ + // + GLfloat offset = 0.5f * octreeRadius; + // + if (![self testHasGeometry]) + { + leafcount++; // nil or zero or 0 + return [NSNumber numberWithBool:NO]; // empty octree + } + // there is geometry! + // + if ((octreeRadius <= OCTREE_MIN_RADIUS)||(depth <= 0)) // maximum resolution + { + leafcount++; // partially full or -1 + volumecount += octreeRadius * octreeRadius * octreeRadius * 0.5f; + return [NSNumber numberWithBool:YES]; // at least partially full octree + } + // + if (!isConvex) + [self testIsConvex]; // check! + // + if (isConvex) // we're convex! + { + if ([self testCornersWithinGeometry: octreeRadius]) // all eight corners inside or on! + { + leafcount++; // full or -1 + volumecount += octreeRadius * octreeRadius * octreeRadius; + return [NSNumber numberWithBool:YES]; // full octree + } + } + // + // SLOW_CODE -- all these allocations and deallocations are quite costly. Would a bucket allocator work? --ahruman + Geometry* g_000 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_001 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_010 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_011 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_100 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_101 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_110 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_111 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + // + Geometry* g_xx1 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_xx0 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + // + [self z_axisSplitBetween:g_xx1 :g_xx0 : offset]; + if ([g_xx0 testHasGeometry]) + { + Geometry* g_x00 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_x10 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + // + [g_xx0 y_axisSplitBetween: g_x10 : g_x00 : offset]; + if ([g_x00 testHasGeometry]) + { + [g_x00 x_axisSplitBetween:g_100 :g_000 : offset]; + [g_000 setConvex: isConvex]; + [g_100 setConvex: isConvex]; + } + if ([g_x10 testHasGeometry]) + { + [g_x10 x_axisSplitBetween:g_110 :g_010 : offset]; + [g_010 setConvex: isConvex]; + [g_110 setConvex: isConvex]; + } + [g_x00 release]; + [g_x10 release]; + } + if ([g_xx1 testHasGeometry]) + { + Geometry* g_x01 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + Geometry* g_x11 = [(Geometry *)[Geometry alloc] initWithCapacity:n_triangles]; + // + [g_xx1 y_axisSplitBetween: g_x11 : g_x01 :offset]; + if ([g_x01 testHasGeometry]) + { + [g_x01 x_axisSplitBetween:g_101 :g_001 :offset]; + [g_001 setConvex: isConvex]; + [g_101 setConvex: isConvex]; + } + if ([g_x11 testHasGeometry]) + { + [g_x11 x_axisSplitBetween:g_111 :g_011 :offset]; + [g_011 setConvex: isConvex]; + [g_111 setConvex: isConvex]; + } + [g_x01 release]; + [g_x11 release]; + } + [g_xx0 release]; + [g_xx1 release]; + + leafcount++; // pointer to array + NSObject* result = [NSArray arrayWithObjects: + [g_000 octreeWithinRadius: offset toDepth:depth - 1], + [g_001 octreeWithinRadius: offset toDepth:depth - 1], + [g_010 octreeWithinRadius: offset toDepth:depth - 1], + [g_011 octreeWithinRadius: offset toDepth:depth - 1], + [g_100 octreeWithinRadius: offset toDepth:depth - 1], + [g_101 octreeWithinRadius: offset toDepth:depth - 1], + [g_110 octreeWithinRadius: offset toDepth:depth - 1], + [g_111 octreeWithinRadius: offset toDepth:depth - 1], + nil]; + [g_000 release]; + [g_001 release]; + [g_010 release]; + [g_011 release]; + [g_100 release]; + [g_101 release]; + [g_110 release]; + [g_111 release]; + // + return result; +} + +- (void) translate:(Vector) offset +{ + OOInteger i; + for (i = 0; i < n_triangles; i++) + { + triangles[i].v[0].x += offset.x; + triangles[i].v[1].x += offset.x; + triangles[i].v[2].x += offset.x; + + triangles[i].v[0].y += offset.y; + triangles[i].v[1].y += offset.y; + triangles[i].v[2].y += offset.y; + + triangles[i].v[0].z += offset.z; + triangles[i].v[1].z += offset.z; + triangles[i].v[2].z += offset.z; + } +} + +- (void) scale:(GLfloat) scalar +{ + OOInteger i; + for (i = 0; i < n_triangles; i++) + { + triangles[i].v[0].x *= scalar; + triangles[i].v[1].x *= scalar; + triangles[i].v[2].x *= scalar; + triangles[i].v[0].y *= scalar; + triangles[i].v[1].y *= scalar; + triangles[i].v[2].y *= scalar; + triangles[i].v[0].z *= scalar; + triangles[i].v[1].z *= scalar; + triangles[i].v[2].z *= scalar; + } +} + +- (void) x_axisSplitBetween:(Geometry*) g_plus :(Geometry*) g_minus :(GLfloat) x; +{ + // test each triangle splitting against x == 0.0 + // + OOInteger i; + for (i = 0; i < n_triangles; i++) + { + BOOL done_tri = NO; + Vector v0 = triangles[i].v[0]; + Vector v1 = triangles[i].v[1]; + Vector v2 = triangles[i].v[2]; + + if ((v0.x >= 0.0)&&(v1.x >= 0.0)&&(v2.x >= 0.0)) + { + [g_plus addTriangle: triangles[i]]; + done_tri = YES; + } + if ((v0.x <= 0.0)&&(v1.x <= 0.0)&&(v2.x <= 0.0)) + { + [g_minus addTriangle: triangles[i]]; + done_tri = YES; + } + if (!done_tri) // triangle must cross y == 0.0 + { + GLfloat i01, i12, i20; + if (v0.x == v1.x) + i01 = -1.0f; + else + i01 = v0.x / (v0.x - v1.x); + if (v1.x == v2.x) + i12 = -1.0f; + else + i12 = v1.x / (v1.x - v2.x); + if (v2.x == v0.x) + i20 = -1.0f; + else + i20 = v2.x / (v2.x - v0.x); + Vector v01 = make_vector(0.0f, i01 * (v1.y - v0.y) + v0.y, i01 * (v1.z - v0.z) + v0.z); + Vector v12 = make_vector(0.0f, i12 * (v2.y - v1.y) + v1.y, i12 * (v2.z - v1.z) + v1.z); + Vector v20 = make_vector(0.0f, i20 * (v0.y - v2.y) + v2.y, i20 * (v0.z - v2.z) + v2.z); + + // cases where a vertex is on the split.. + if (v0.x == 0.0) + { + if (v1.x > 0) + { + [g_plus addTriangle:make_triangle(v0, v1, v12)]; + [g_minus addTriangle:make_triangle(v0, v12, v2)]; + } + else + { + [g_minus addTriangle:make_triangle(v0, v1, v12)]; + [g_plus addTriangle:make_triangle(v0, v12, v2)]; + } + } + if (v1.x == 0.0) + { + if (v2.x > 0) + { + [g_plus addTriangle:make_triangle(v1, v2, v20)]; + [g_minus addTriangle:make_triangle(v1, v20, v0)]; + } + else + { + [g_minus addTriangle:make_triangle(v1, v2, v20)]; + [g_plus addTriangle:make_triangle(v1, v20, v0)]; + } + } + if (v2.x == 0.0) + { + if (v0.x > 0) + { + [g_plus addTriangle:make_triangle(v2, v0, v01)]; + [g_minus addTriangle:make_triangle(v2, v01, v1)]; + } + else + { + [g_minus addTriangle:make_triangle(v2, v0, v01)]; + [g_plus addTriangle:make_triangle(v2, v01, v1)]; + } + } + + if ((v0.x > 0.0)&&(v1.x > 0.0)&&(v2.x < 0.0)) + { + [g_plus addTriangle:make_triangle(v0, v12, v20)]; + [g_plus addTriangle:make_triangle(v0, v1, v12)]; + [g_minus addTriangle:make_triangle(v20, v12, v2)]; + } + + if ((v0.x > 0.0)&&(v1.x < 0.0)&&(v2.x > 0.0)) + { + [g_plus addTriangle:make_triangle(v2, v01, v12)]; + [g_plus addTriangle:make_triangle(v2, v0, v01)]; + [g_minus addTriangle:make_triangle(v12, v01, v1)]; + } + + if ((v0.x > 0.0)&&(v1.x < 0.0)&&(v2.x < 0.0)) + { + [g_plus addTriangle:make_triangle(v20, v0, v01)]; + [g_minus addTriangle:make_triangle(v2, v20, v1)]; + [g_minus addTriangle:make_triangle(v20, v01, v1)]; + } + + if ((v0.x < 0.0)&&(v1.x > 0.0)&&(v2.x > 0.0)) + { + [g_minus addTriangle:make_triangle(v01, v20, v0)]; + [g_plus addTriangle:make_triangle(v1, v20, v01)]; + [g_plus addTriangle:make_triangle(v1, v2, v20)]; + } + + if ((v0.x < 0.0)&&(v1.x > 0.0)&&(v2.x < 0.0)) + { + [g_plus addTriangle:make_triangle(v01, v1, v12)]; + [g_minus addTriangle:make_triangle(v0, v01, v2)]; + [g_minus addTriangle:make_triangle(v01, v12, v2)]; + } + + if ((v0.x < 0.0)&&(v1.x < 0.0)&&(v2.x > 0.0)) + { + [g_plus addTriangle:make_triangle(v12, v2, v20)]; + [g_minus addTriangle:make_triangle(v1, v12, v0)]; + [g_minus addTriangle:make_triangle(v12, v20, v0)]; + } + + } + } + [g_plus translate: make_vector(-x, 0.0f, 0.0f)]; + [g_minus translate: make_vector(x, 0.0f, 0.0f)]; +} + +- (void) y_axisSplitBetween:(Geometry*) g_plus :(Geometry*) g_minus :(GLfloat) y; +{ + // test each triangle splitting against y == 0.0 + // + OOInteger i; + for (i = 0; i < n_triangles; i++) + { + BOOL done_tri = NO; + Vector v0 = triangles[i].v[0]; + Vector v1 = triangles[i].v[1]; + Vector v2 = triangles[i].v[2]; + + if ((v0.y >= 0.0)&&(v1.y >= 0.0)&&(v2.y >= 0.0)) + { + [g_plus addTriangle: triangles[i]]; + done_tri = YES; + } + if ((v0.y <= 0.0)&&(v1.y <= 0.0)&&(v2.y <= 0.0)) + { + [g_minus addTriangle: triangles[i]]; + done_tri = YES; + } + if (!done_tri) // triangle must cross y == 0.0 + { + GLfloat i01, i12, i20; + if (v0.y == v1.y) + i01 = -1.0f; + else + i01 = v0.y / (v0.y - v1.y); + if (v1.y == v2.y) + i12 = -1.0f; + else + i12 = v1.y / (v1.y - v2.y); + if (v2.y == v0.y) + i20 = -1.0f; + else + i20 = v2.y / (v2.y - v0.y); + Vector v01 = make_vector(i01 * (v1.x - v0.x) + v0.x, 0.0f, i01 * (v1.z - v0.z) + v0.z); + Vector v12 = make_vector(i12 * (v2.x - v1.x) + v1.x, 0.0f, i12 * (v2.z - v1.z) + v1.z); + Vector v20 = make_vector(i20 * (v0.x - v2.x) + v2.x, 0.0f, i20 * (v0.z - v2.z) + v2.z); + + // cases where a vertex is on the split.. + if (v0.y == 0.0) + { + if (v1.y > 0) + { + [g_plus addTriangle:make_triangle(v0, v1, v12)]; + [g_minus addTriangle:make_triangle(v0, v12, v2)]; + } + else + { + [g_minus addTriangle:make_triangle(v0, v1, v12)]; + [g_plus addTriangle:make_triangle(v0, v12, v2)]; + } + } + if (v1.y == 0.0) + { + if (v2.y > 0) + { + [g_plus addTriangle:make_triangle(v1, v2, v20)]; + [g_minus addTriangle:make_triangle(v1, v20, v0)]; + } + else + { + [g_minus addTriangle:make_triangle(v1, v2, v20)]; + [g_plus addTriangle:make_triangle(v1, v20, v0)]; + } + } + if (v2.y == 0.0) + { + if (v0.y > 0) + { + [g_plus addTriangle:make_triangle(v2, v0, v01)]; + [g_minus addTriangle:make_triangle(v2, v01, v1)]; + } + else + { + [g_minus addTriangle:make_triangle(v2, v0, v01)]; + [g_plus addTriangle:make_triangle(v2, v01, v1)]; + } + } + + if ((v0.y > 0.0)&&(v1.y > 0.0)&&(v2.y < 0.0)) + { + [g_plus addTriangle:make_triangle(v0, v12, v20)]; + [g_plus addTriangle:make_triangle(v0, v1, v12)]; + [g_minus addTriangle:make_triangle(v20, v12, v2)]; + } + + if ((v0.y > 0.0)&&(v1.y < 0.0)&&(v2.y > 0.0)) + { + [g_plus addTriangle:make_triangle(v2, v01, v12)]; + [g_plus addTriangle:make_triangle(v2, v0, v01)]; + [g_minus addTriangle:make_triangle(v12, v01, v1)]; + } + + if ((v0.y > 0.0)&&(v1.y < 0.0)&&(v2.y < 0.0)) + { + [g_plus addTriangle:make_triangle(v20, v0, v01)]; + [g_minus addTriangle:make_triangle(v2, v20, v1)]; + [g_minus addTriangle:make_triangle(v20, v01, v1)]; + } + + if ((v0.y < 0.0)&&(v1.y > 0.0)&&(v2.y > 0.0)) + { + [g_minus addTriangle:make_triangle(v01, v20, v0)]; + [g_plus addTriangle:make_triangle(v1, v20, v01)]; + [g_plus addTriangle:make_triangle(v1, v2, v20)]; + } + + if ((v0.y < 0.0)&&(v1.y > 0.0)&&(v2.y < 0.0)) + { + [g_plus addTriangle:make_triangle(v01, v1, v12)]; + [g_minus addTriangle:make_triangle(v0, v01, v2)]; + [g_minus addTriangle:make_triangle(v01, v12, v2)]; + } + + if ((v0.y < 0.0)&&(v1.y < 0.0)&&(v2.y > 0.0)) + { + [g_plus addTriangle:make_triangle(v12, v2, v20)]; + [g_minus addTriangle:make_triangle(v1, v12, v0)]; + [g_minus addTriangle:make_triangle(v12, v20, v0)]; + } + } + } + [g_plus translate: make_vector(0.0f, -y, 0.0f)]; + [g_minus translate: make_vector(0.0f, y, 0.0f)]; +} + +- (void) z_axisSplitBetween:(Geometry*) g_plus :(Geometry*) g_minus :(GLfloat) z +{ + // test each triangle splitting against z == 0.0 + // + OOInteger i; + for (i = 0; i < n_triangles; i++) + { + BOOL done_tri = NO; + Vector v0 = triangles[i].v[0]; + Vector v1 = triangles[i].v[1]; + Vector v2 = triangles[i].v[2]; + + if ((v0.z >= 0.0)&&(v1.z >= 0.0)&&(v2.z >= 0.0)) + { + [g_plus addTriangle: triangles[i]]; + done_tri = YES; + } + if ((v0.z <= 0.0)&&(v1.z <= 0.0)&&(v2.z <= 0.0)) + { + [g_minus addTriangle: triangles[i]]; + done_tri = YES; + } + if (!done_tri) // triangle must cross y == 0.0 + { + GLfloat i01, i12, i20; + if (v0.z == v1.z) + i01 = -1.0f; + else + i01 = v0.z / (v0.z - v1.z); + if (v1.z == v2.z) + i12 = -1.0f; + else + i12 = v1.z / (v1.z - v2.z); + if (v2.z == v0.z) + i20 = -1.0f; + else + i20 = v2.z / (v2.z - v0.z); + Vector v01 = make_vector(i01 * (v1.x - v0.x) + v0.x, i01 * (v1.y - v0.y) + v0.y, 0.0f); + Vector v12 = make_vector(i12 * (v2.x - v1.x) + v1.x, i12 * (v2.y - v1.y) + v1.y, 0.0f); + Vector v20 = make_vector(i20 * (v0.x - v2.x) + v2.x, i20 * (v0.y - v2.y) + v2.y, 0.0f); + + // cases where a vertex is on the split.. + if (v0.z == 0.0) + { + if (v1.z > 0) + { + [g_plus addTriangle:make_triangle(v0, v1, v12)]; + [g_minus addTriangle:make_triangle(v0, v12, v2)]; + } + else + { + [g_minus addTriangle:make_triangle(v0, v1, v12)]; + [g_plus addTriangle:make_triangle(v0, v12, v2)]; + } + } + if (v1.z == 0.0) + { + if (v2.z > 0) + { + [g_plus addTriangle:make_triangle(v1, v2, v20)]; + [g_minus addTriangle:make_triangle(v1, v20, v0)]; + } + else + { + [g_minus addTriangle:make_triangle(v1, v2, v20)]; + [g_plus addTriangle:make_triangle(v1, v20, v0)]; + } + } + if (v2.z == 0.0) + { + if (v0.z > 0) + { + [g_plus addTriangle:make_triangle(v2, v0, v01)]; + [g_minus addTriangle:make_triangle(v2, v01, v1)]; + } + else + { + [g_minus addTriangle:make_triangle(v2, v0, v01)]; + [g_plus addTriangle:make_triangle(v2, v01, v1)]; + } + } + + if ((v0.z > 0.0)&&(v1.z > 0.0)&&(v2.z < 0.0)) + { + [g_plus addTriangle:make_triangle(v0, v12, v20)]; + [g_plus addTriangle:make_triangle(v0, v1, v12)]; + [g_minus addTriangle:make_triangle(v20, v12, v2)]; + } + + if ((v0.z > 0.0)&&(v1.z < 0.0)&&(v2.z > 0.0)) + { + [g_plus addTriangle:make_triangle(v2, v01, v12)]; + [g_plus addTriangle:make_triangle(v2, v0, v01)]; + [g_minus addTriangle:make_triangle(v12, v01, v1)]; + } + + if ((v0.z > 0.0)&&(v1.z < 0.0)&&(v2.z < 0.0)) + { + [g_plus addTriangle:make_triangle(v20, v0, v01)]; + [g_minus addTriangle:make_triangle(v2, v20, v1)]; + [g_minus addTriangle:make_triangle(v20, v01, v1)]; + } + + if ((v0.z < 0.0)&&(v1.z > 0.0)&&(v2.z > 0.0)) + { + [g_minus addTriangle:make_triangle(v01, v20, v0)]; + [g_plus addTriangle:make_triangle(v1, v20, v01)]; + [g_plus addTriangle:make_triangle(v1, v2, v20)]; + } + + if ((v0.z < 0.0)&&(v1.z > 0.0)&&(v2.z < 0.0)) + { + [g_plus addTriangle:make_triangle(v01, v1, v12)]; + [g_minus addTriangle:make_triangle(v0, v01, v2)]; + [g_minus addTriangle:make_triangle(v01, v12, v2)]; + } + + if ((v0.z < 0.0)&&(v1.z < 0.0)&&(v2.z > 0.0)) + { + [g_plus addTriangle:make_triangle(v12, v2, v20)]; + [g_minus addTriangle:make_triangle(v1, v12, v0)]; + [g_minus addTriangle:make_triangle(v12, v20, v0)]; + } + + } + } + [g_plus translate: make_vector(0.0f, 0.0f, -z)]; + [g_minus translate: make_vector(0.0f, 0.0f, z)]; +} + +@end diff --git a/src/Core/GuiDisplayGen.h b/src/Core/GuiDisplayGen.h new file mode 100644 index 00000000..d8cc46c9 --- /dev/null +++ b/src/Core/GuiDisplayGen.h @@ -0,0 +1,216 @@ +/* + +GuiDisplayGen.h + +Class handling interface elements, primarily text, that are not part of the 3D +game world, together with GuiDisplayGen. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOMaths.h" +#import "OOTypes.h" + + +#define GUI_DEFAULT_COLUMNS 6 +#define GUI_DEFAULT_ROWS 30 + +#define GUI_MAX_ROWS 64 +#define GUI_MAX_COLUMNS 40 +#define MAIN_GUI_PIXEL_HEIGHT 480 +#define MAIN_GUI_PIXEL_WIDTH 480 +#define MAIN_GUI_ROW_HEIGHT 16 +#define MAIN_GUI_ROW_WIDTH 16 +#define MAIN_GUI_PIXEL_ROW_START 40 + + +typedef enum +{ + GUI_ALIGN_LEFT, + GUI_ALIGN_RIGHT, + GUI_ALIGN_CENTER +} OOGUIAlignment; + +#define GUI_KEY_OK @"OK" +#define GUI_KEY_SKIP @"SKIP-ROW" + + +@class OOSound, OOColor, OOTexture, OpenGLSprite, HeadUpDisplay; + + +typedef int OOGUIRow; // -1 for none +typedef int OOGUITabStop; // negative value = right align text +typedef OOGUITabStop OOGUITabSettings[GUI_MAX_COLUMNS]; + + +@interface GuiDisplayGen: NSObject +{ + NSSize size_in_pixels; + unsigned n_columns; + unsigned n_rows; + int pixel_row_center; + unsigned pixel_row_height; + int pixel_row_start; + NSSize pixel_text_size; + + BOOL showAdvancedNavArray; + + NSSize pixel_title_size; + + OOColor *backgroundColor; + OOColor *textColor; + + OpenGLSprite *backgroundSprite; + + NSString *title; + + NSMutableArray *rowText; + NSMutableArray *rowKey; + NSMutableArray *rowColor; + + Vector drawPosition; + + NSPoint rowPosition[GUI_MAX_ROWS]; + OOGUIAlignment rowAlignment[GUI_MAX_ROWS]; + float rowFadeTime[GUI_MAX_ROWS]; + + OOGUITabSettings tabStops; + + NSRange rowRange; + + OOGUIRow selectedRow; + NSRange selectableRange; + + BOOL showTextCursor; + OOGUIRow currentRow; + + GLfloat fade_alpha; // for fade-in / fade-out + OOTimeDelta fade_duration; // period + OOTimeAbsolute fade_from_time; // from [universe getTime] + GLfloat fade_sign; // -1.0 to 1.0 + int statusPage; // status screen: paging equipped items +} + +- (id) init; +- (id) initWithPixelSize:(NSSize)gui_size + columns:(int)gui_cols + rows:(int)gui_rows + rowHeight:(int)gui_row_height + rowStart:(int)gui_row_start + title:(NSString*)gui_title; + +- (void) resizeWithPixelSize:(NSSize)gui_size + columns:(int)gui_cols + rows:(int)gui_rows + rowHeight:(int)gui_row_height + rowStart:(int)gui_row_start + title:(NSString*) gui_title; +- (void) resizeTo:(NSSize)gui_size + characterHeight:(int)csize + title:(NSString*)gui_title; +- (NSSize)size; +- (unsigned)columns; +- (unsigned)rows; +- (unsigned)rowHeight; +- (int)rowStart; + +- (NSString *)title; +- (void) setTitle:(NSString *)str; + +- (void) dealloc; + +- (void) setDrawPosition:(Vector) vector; +- (Vector) drawPosition; + +- (void) fadeOutFromTime:(OOTimeAbsolute) now_time overDuration:(OOTimeDelta) duration; + +- (GLfloat) alpha; +- (void) setAlpha:(GLfloat) an_alpha; + +- (void) setBackgroundColor:(OOColor*) color; + +- (void) setTextColor:(OOColor*) color; + +- (void) setCharacterSize:(NSSize) character_size; + +- (void)setShowAdvancedNavArray:(BOOL)inFlag; + +- (void) setColor:(OOColor *)color forRow:(OOGUIRow)row; + +- (id) objectForRow:(OOGUIRow)row; +- (NSString*) keyForRow:(OOGUIRow)row; +- (int) selectedRow; +- (BOOL) setSelectedRow:(OOGUIRow)row; +- (BOOL) setNextRow:(int) direction; +- (BOOL) setFirstSelectableRow; +- (void) setNoSelectedRow; +- (NSString *) selectedRowText; +- (NSString *) selectedRowKey; + +- (void) setShowTextCursor:(BOOL) yesno; +- (void) setCurrentRow:(OOGUIRow) value; + +- (NSRange) selectableRange; +- (void) setSelectableRange:(NSRange) range; + +- (void) setTabStops:(OOGUITabSettings)stops; + +- (void) clear; + +- (void) setKey:(NSString *)str forRow:(OOGUIRow)row; +- (void) setText:(NSString *)str forRow:(OOGUIRow)row; +- (void) setText:(NSString *)str forRow:(OOGUIRow)row align:(OOGUIAlignment)alignment; +- (int) addLongText:(NSString *)str + startingAtRow:(OOGUIRow)row + align:(OOGUIAlignment)alignment; +- (void) printLongText:(NSString *)str + align:(OOGUIAlignment)alignment + color:(OOColor *)text_color + fadeTime:(float)text_fade + key:(NSString *)text_key + addToArray:(NSMutableArray *)text_array; +- (void) printLineNoScroll:(NSString *)str + align:(OOGUIAlignment)alignment + color:(OOColor *)text_color + fadeTime:(float)text_fade + key:(NSString *)text_key + addToArray:(NSMutableArray *)text_array; + +- (void) setArray:(NSArray *)arr forRow:(OOGUIRow)row; + +- (void) insertItemsFromArray:(NSArray *)items + withKeys:(NSArray *)item_keys + intoRow:(OOGUIRow)row + color:(OOColor *)text_color; + +///////////////////////////////////////////////////// + +- (void) scrollUp:(int) how_much; + +- (void)setBackgroundTexture:(OOTexture *)backgroundTexture; +- (void)clearBackground; + +- (void)leaveLastLine; + +- (int) drawGUI:(GLfloat) alpha drawCursor:(BOOL) drawCursor; +- (void) setStatusPage:(int) pageNum; + +@end diff --git a/src/Core/GuiDisplayGen.m b/src/Core/GuiDisplayGen.m new file mode 100644 index 00000000..1a99be1e --- /dev/null +++ b/src/Core/GuiDisplayGen.m @@ -0,0 +1,1450 @@ +/* + +GuiDisplayGen.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "GuiDisplayGen.h" +#import "Universe.h" +#import "PlayerEntity.h" +#import "OpenGLSprite.h" +#import "ResourceManager.h" +#import "OOSound.h" +#import "OOStringParsing.h" +#import "HeadUpDisplay.h" +#import "OOCollectionExtractors.h" + + +OOINLINE BOOL RowInRange(OOGUIRow row, NSRange range) +{ + return ((int)range.location <= row && row < (int)(range.location + range.length)); +} + + +@interface GuiDisplayGen (Internal) + +- (void) drawGLDisplay:(GLfloat)x :(GLfloat)y :(GLfloat)z :(GLfloat) alpha; + +- (void) drawStarChart:(GLfloat)x :(GLfloat)y :(GLfloat)z :(GLfloat) alpha; +- (void) drawGalaxyChart:(GLfloat)x :(GLfloat)y :(GLfloat)z :(GLfloat) alpha; +- (void) drawEqptList: (NSArray *)eqptList z:(GLfloat)z; +- (void) drawAdvancedNavArrayAtX:(float)x y:(float)y z:(float)z alpha:(float)alpha; + +@end + + +@implementation GuiDisplayGen + +- (id) init +{ + self = [super init]; + + size_in_pixels = NSMakeSize(MAIN_GUI_PIXEL_WIDTH, MAIN_GUI_PIXEL_HEIGHT); + n_columns = GUI_DEFAULT_COLUMNS; + n_rows = GUI_DEFAULT_ROWS; + pixel_row_center = size_in_pixels.width / 2; + pixel_row_height = MAIN_GUI_ROW_HEIGHT; + pixel_row_start = MAIN_GUI_PIXEL_ROW_START; // first position down the page... + + pixel_text_size = NSMakeSize(0.9f * pixel_row_height, pixel_row_height); // main gui has 18x20 characters + + pixel_title_size = NSMakeSize(pixel_row_height * 1.75f, pixel_row_height * 1.5f); + + int stops[6] = {0, 192, 256, 320, 384, 448}; + unsigned i; + + rowRange = NSMakeRange(0,n_rows); + + rowText = [[NSMutableArray alloc] initWithCapacity:n_rows]; // alloc retains + rowKey = [[NSMutableArray alloc] initWithCapacity:n_rows]; // alloc retains + rowColor = [[NSMutableArray alloc] initWithCapacity:n_rows]; // alloc retains + + for (i = 0; i < n_rows; i++) + { + [rowText addObject:@"."]; + [rowKey addObject:[NSString stringWithFormat:@"%d",i]]; + [rowColor addObject:[OOColor yellowColor]]; + rowPosition[i].x = 0.0f; + rowPosition[i].y = size_in_pixels.height - (pixel_row_start + i * pixel_row_height); + rowAlignment[i] = GUI_ALIGN_LEFT; + } + + for (i = 0; i < n_columns; i++) + { + tabStops[i] = stops[i]; + } + + title = @""; + + textColor = [[OOColor yellowColor] retain]; + + drawPosition = make_vector(0.0f, 0.0f, 640.0f); + + return self; +} + + +- (id) initWithPixelSize:(NSSize)gui_size + columns:(int)gui_cols + rows:(int)gui_rows + rowHeight:(int)gui_row_height + rowStart:(int)gui_row_start + title:(NSString*)gui_title +{ + self = [super init]; + + size_in_pixels = gui_size; + n_columns = gui_cols; + n_rows = gui_rows; + pixel_row_center = size_in_pixels.width / 2; + pixel_row_height = gui_row_height; + pixel_row_start = gui_row_start; // first position down the page... + + pixel_text_size = NSMakeSize(pixel_row_height, pixel_row_height); + + pixel_title_size = NSMakeSize(pixel_row_height * 1.75f, pixel_row_height * 1.5f); + + unsigned i; + + rowRange = NSMakeRange(0,n_rows); + + rowText = [[NSMutableArray alloc] initWithCapacity:n_rows]; // alloc retains + rowKey = [[NSMutableArray alloc] initWithCapacity:n_rows]; // alloc retains + rowColor = [[NSMutableArray alloc] initWithCapacity:n_rows]; // alloc retains + + for (i = 0; i < n_rows; i++) + { + [rowText addObject:@""]; + [rowKey addObject:@""]; + [rowColor addObject:[OOColor greenColor]]; + rowPosition[i].x = 0.0f; + rowPosition[i].y = size_in_pixels.height - (pixel_row_start + i * pixel_row_height); + rowAlignment[i] = GUI_ALIGN_LEFT; + } + + title = [gui_title retain]; + + textColor = [[OOColor yellowColor] retain]; + + return self; +} + + +- (void) dealloc +{ + [backgroundSprite release]; + [backgroundColor release]; + [textColor release]; + [title release]; + [rowText release]; + [rowKey release]; + [rowColor release]; + + [super dealloc]; +} + + +- (void) resizeWithPixelSize:(NSSize)gui_size + columns:(int)gui_cols + rows:(int)gui_rows + rowHeight:(int)gui_row_height + rowStart:(int)gui_row_start + title:(NSString*) gui_title +{ + [self clear]; + // + size_in_pixels = gui_size; + n_columns = gui_cols; + n_rows = gui_rows; + pixel_row_center = size_in_pixels.width / 2; + pixel_row_height = gui_row_height; + pixel_row_start = gui_row_start; // first position down the page... + + pixel_text_size = NSMakeSize(pixel_row_height, pixel_row_height); + pixel_title_size = NSMakeSize(pixel_row_height * 1.75f, pixel_row_height * 1.5f); + + rowRange = NSMakeRange(0,n_rows); + [self clear]; + // + [self setTitle: gui_title]; +} + + +- (void) resizeTo:(NSSize)gui_size + characterHeight:(int)csize + title:(NSString*)gui_title +{ + [self clear]; + // + size_in_pixels = gui_size; + n_columns = gui_size.width / csize; + n_rows = (int)gui_size.height / csize; + + [self setTitle: gui_title]; + + pixel_row_center = gui_size.width / 2; + pixel_row_height = csize; + currentRow = n_rows - 1; // first position down the page... + + if (title != nil) + pixel_row_start = 2.75f * csize + 0.5f * (gui_size.height - n_rows * csize); + else + pixel_row_start = csize + 0.5f * (gui_size.height - n_rows * csize); + + [rowText removeAllObjects]; + [rowKey removeAllObjects]; + [rowColor removeAllObjects]; + + unsigned i; + for (i = 0; i < n_rows; i++) + { + [rowText addObject:@""]; + [rowKey addObject:@""]; + [rowColor addObject:[OOColor greenColor]]; + rowPosition[i].x = 0.0f; + rowPosition[i].y = size_in_pixels.height - (pixel_row_start + i * pixel_row_height); + rowAlignment[i] = GUI_ALIGN_LEFT; + } + + pixel_text_size = NSMakeSize(csize, csize); + pixel_title_size = NSMakeSize(csize * 1.75f, csize * 1.5f); + + OOLog(@"gui.reset", @"gui %@ reset to rows:%d columns:%d start:%d", self, n_rows, n_columns, pixel_row_start); + + rowRange = NSMakeRange(0,n_rows); + [self clear]; +} + + +- (NSSize)size +{ + return size_in_pixels; +} + + +- (unsigned)columns +{ + return n_columns; +} + + +- (unsigned)rows +{ + return n_rows; +} + + +- (unsigned)rowHeight +{ + return pixel_row_height; +} + + +- (int)rowStart +{ + return pixel_row_start; +} + + +- (NSString *)title +{ + return title; +} + + +- (void) setTitle:(NSString *)str +{ + if (str != title) + { + [title release]; + if ([str length] == 0) str = nil; + title = [str copy]; + } +} + + +- (void) setDrawPosition:(Vector) vector +{ + drawPosition = vector; +} + + +- (Vector) drawPosition +{ + return drawPosition; +} + + +- (void) fadeOutFromTime:(OOTimeAbsolute) now_time overDuration:(OOTimeDelta) duration +{ + if (fade_alpha <= 0.0f) + { + return; + } + fade_sign = (float)(-fade_alpha / duration); + fade_from_time = now_time; + fade_duration = duration; +} + + +- (GLfloat) alpha +{ + return fade_alpha; +} + + +- (void) setAlpha:(GLfloat) an_alpha +{ + fade_alpha = an_alpha; +} + + +- (void) setBackgroundColor:(OOColor*) color +{ + [backgroundColor release]; + backgroundColor = [color retain]; +} + + +- (void) setTextColor:(OOColor*) color +{ + [textColor release]; + if (color == nil) color = [[OOColor yellowColor] retain]; + textColor = [color retain]; +} + + +- (void) setCharacterSize:(NSSize) character_size +{ + pixel_text_size = character_size; +} + + +- (void)setShowAdvancedNavArray:(BOOL)inFlag +{ + showAdvancedNavArray = inFlag; +} + + +- (void) setColor:(OOColor *) color forRow:(OOGUIRow)row +{ + if (RowInRange(row, rowRange)) + [rowColor replaceObjectAtIndex:row withObject:color]; +} + + +- (id) objectForRow:(OOGUIRow)row +{ + if (RowInRange(row, rowRange)) + return [rowText objectAtIndex:row]; + else + return NULL; +} + + +- (NSString*) keyForRow:(OOGUIRow)row +{ + if (RowInRange(row, rowRange)) + return [rowKey objectAtIndex:row]; + else + return NULL; +} + + +- (int) selectedRow +{ + if (RowInRange(selectedRow, selectableRange)) + return selectedRow; + else + return -1; +} + + +- (BOOL) setSelectedRow:(OOGUIRow)row +{ + if ((row == selectedRow)&&RowInRange(row, selectableRange)) + return YES; + if (RowInRange(row, selectableRange)) + { + if (![[rowKey objectAtIndex:row] isEqual:GUI_KEY_SKIP]) + { + selectedRow = row; + return YES; + } + } + return NO; +} + + +- (BOOL) setNextRow:(int) direction +{ + int row = selectedRow + direction; + while (RowInRange(row, selectableRange)) + { + if (![[rowKey objectAtIndex:row] isEqual:GUI_KEY_SKIP]) + { + selectedRow = row; + return YES; + } + row += direction; + } + return NO; +} + + +- (BOOL) setFirstSelectableRow +{ + int row = selectableRange.location; + while (RowInRange(row, selectableRange)) + { + if (![[rowKey objectAtIndex:row] isEqual:GUI_KEY_SKIP]) + { + selectedRow = row; + return YES; + } + row++; + } + selectedRow = -1; + return NO; +} + + +- (void) setNoSelectedRow +{ + selectedRow = -1; +} + + +- (NSString *) selectedRowText +{ + if ([[rowText objectAtIndex:selectedRow] isKindOfClass:[NSString class]]) + return (NSString *)[rowText objectAtIndex:selectedRow]; + if ([[rowText objectAtIndex:selectedRow] isKindOfClass:[NSArray class]]) + return (NSString *)[[rowText objectAtIndex:selectedRow] objectAtIndex:0]; + return NULL; +} + + +- (NSString *) selectedRowKey +{ + if ((selectedRow < 0)||((unsigned)selectedRow > [rowKey count])) + return nil; + else + return (NSString *)[rowKey objectAtIndex:selectedRow]; +} + + +- (void) setShowTextCursor:(BOOL) yesno +{ + showTextCursor = yesno; +} + + +- (void) setCurrentRow:(OOGUIRow) value +{ + if ((value < 0)||((unsigned)value >= n_rows)) + { + showTextCursor = NO; + currentRow = -1; + } + else + { + currentRow = value; + } +} + + +- (NSRange) selectableRange +{ + return selectableRange; +} + + +- (void) setSelectableRange:(NSRange) range +{ + selectableRange = range; +} + + +- (void) setTabStops:(OOGUITabSettings)stops +{ + if (stops != NULL) memmove(tabStops, stops, sizeof tabStops); +} + + +- (void) clear +{ + unsigned i; + [self setTitle: nil]; + for (i = 0; i < n_rows; i++) + { + [self setText:@"" forRow:i align:GUI_ALIGN_LEFT]; + [self setColor:textColor forRow:i]; + // + [self setKey:GUI_KEY_SKIP forRow:i]; + // + rowFadeTime[i] = 0.0f; + } + [self setShowTextCursor:NO]; + [self setSelectableRange:NSMakeRange(0,0)]; +} + + +- (void) setKey:(NSString *)str forRow:(OOGUIRow)row +{ + if (RowInRange(row, rowRange)) + [rowKey replaceObjectAtIndex:row withObject:str]; +} + + +- (void) setText:(NSString *)str forRow:(OOGUIRow)row +{ + if (RowInRange(row, rowRange)) + { + [rowText replaceObjectAtIndex:row withObject:str]; + } +} + + +- (void) setText:(NSString *)str forRow:(OOGUIRow)row align:(OOGUIAlignment)alignment +{ + if (str != nil && RowInRange(row, rowRange)) + { + [rowText replaceObjectAtIndex:row withObject:str]; + rowAlignment[row] = alignment; + } +} + + +- (int) addLongText:(NSString *)str + startingAtRow:(OOGUIRow)row + align:(OOGUIAlignment)alignment +{ + NSSize chSize = pixel_text_size; + NSSize strsize = OORectFromString(str, 0.0f, 0.0f, chSize).size; + if (strsize.width < size_in_pixels.width) + { + [self setText:str forRow:row align:alignment]; + return row + 1; + } + else + { + NSMutableArray *words = ScanTokensFromString(str); + NSMutableString *string1 = [NSMutableString stringWithCapacity:256]; + NSMutableString *string2 = [NSMutableString stringWithCapacity:256]; + strsize.width = 0.0f; + while ((strsize.width < size_in_pixels.width)&&([words count] > 0)) + { + [string1 appendString:(NSString *)[words objectAtIndex:0]]; + [string1 appendString:@" "]; + [words removeObjectAtIndex:0]; + strsize = OORectFromString(string1, 0.0f, 0.0f, chSize).size; + if ([words count] > 0) + strsize.width += OORectFromString((NSString *)[words objectAtIndex:0], 0.0f, 0.0f, chSize).size.width; + } + [string2 appendString:[words componentsJoinedByString:@" "]]; + [self setText:string1 forRow:row align:alignment]; + return [self addLongText:string2 startingAtRow:row+1 align:alignment]; + } +} + +- (void) leaveLastLine +{ + unsigned i; + for (i=0; i < n_rows-1; i++) + { + [rowText replaceObjectAtIndex:i withObject:@""]; + [rowColor replaceObjectAtIndex:i withObject:textColor]; + [rowKey replaceObjectAtIndex:i withObject:@""]; + rowAlignment[i] = GUI_ALIGN_LEFT; + rowFadeTime[i] = 0.0f; + } + rowFadeTime[i] = 0.4f; // fade the last line... +} + +- (void) printLongText:(NSString *)str + align:(OOGUIAlignment) alignment + color:(OOColor *)text_color + fadeTime:(float)text_fade + key:(NSString *)text_key + addToArray:(NSMutableArray *)text_array +{ + // print a multi-line message + // + if ([str rangeOfString:@"\n"].location != NSNotFound) + { + NSArray *lines = [str componentsSeparatedByString:@"\n"]; + unsigned i; + for (i = 0; i < [lines count]; i++) + [self printLongText:[lines stringAtIndex:i] align:alignment color:text_color fadeTime:text_fade key:text_key addToArray:text_array]; + return; + } + + OOGUIRow row = currentRow; + if (row == (OOGUIRow)n_rows - 1) + [self scrollUp:1]; + NSSize chSize = pixel_text_size; + NSSize strsize = OORectFromString(str, 0.0f, 0.0f, chSize).size; + if (strsize.width < size_in_pixels.width) + { + [self setText:str forRow:row align:alignment]; + if (text_color) + [self setColor:text_color forRow:row]; + if (text_key) + [self setKey:text_key forRow:row]; + rowFadeTime[row] = text_fade; + if (currentRow < (OOGUIRow)n_rows - 1) + currentRow++; + } + else + { + NSMutableArray *words = ScanTokensFromString(str); + NSMutableString *string1 = [NSMutableString stringWithCapacity:256]; + NSMutableString *string2 = [NSMutableString stringWithCapacity:256]; + strsize.width = 0.0f; + while ((strsize.width < size_in_pixels.width)&&([words count] > 0)) + { + [string1 appendString:(NSString *)[words objectAtIndex:0]]; + [string1 appendString:@" "]; + [words removeObjectAtIndex:0]; + strsize = OORectFromString(string1, 0.0f, 0.0f, chSize).size; + if ([words count] > 0) + strsize.width += OORectFromString([words stringAtIndex:0], 0.0f, 0.0f, chSize).size.width; + } + [string2 appendString:[words componentsJoinedByString:@" "]]; + [self setText:string1 forRow:row align:alignment]; + if (text_color) + [self setColor:text_color forRow:row]; + if (text_key) + [self setKey:text_key forRow:row]; + if (text_array) + [text_array addObject:string1]; + rowFadeTime[row] = text_fade; + [self printLongText:string2 align:alignment color:text_color fadeTime:text_fade key:text_key addToArray:text_array]; + } +} + + +- (void) printLineNoScroll:(NSString *)str + align:(OOGUIAlignment)alignment + color:(OOColor *)text_color + fadeTime:(float)text_fade + key:(NSString *)text_key + addToArray:(NSMutableArray *)text_array +{ + [self setText:str forRow:currentRow align:alignment]; + if (text_color) + [self setColor:text_color forRow:currentRow]; + if (text_key) + [self setKey:text_key forRow:currentRow]; + if (text_array) + [text_array addObject:str]; + rowFadeTime[currentRow] = text_fade; +} + + +- (void) setArray:(NSArray *)arr forRow:(OOGUIRow)row +{ + if (RowInRange(row, rowRange)) + [rowText replaceObjectAtIndex:row withObject:arr]; +} + + + +- (void) insertItemsFromArray:(NSArray *)items + withKeys:(NSArray *)item_keys + intoRow:(OOGUIRow)row + color:(OOColor *)text_color +{ + if (!items) + return; + if([items count] == 0) + return; + + unsigned n_items = [items count]; + if ((item_keys)&&([item_keys count] != n_items)) + { + // throw exception + [NSException raise:@"ArrayLengthMismatchException" + format:@"The NSArray sent as 'item_keys' to insertItemsFromArray::: must contain the same number of objects as the NSArray 'items'"]; + } + + unsigned i; + for (i = n_rows; i >= row + n_items ; i--) + { + [self setKey:[self keyForRow:i - n_items] forRow:i]; + id old_row_info = [self objectForRow:i - n_items]; + if ([old_row_info isKindOfClass:[NSArray class]]) + [self setArray:old_row_info forRow:i]; + if ([old_row_info isKindOfClass:[NSString class]]) + [self setText:(NSString *)old_row_info forRow:i]; + } + for (i = 0; i < n_items; i++) + { + id new_row_info = [items objectAtIndex:i]; + if (text_color) + [self setColor:text_color forRow: row + i]; + else + [self setColor:textColor forRow: row + i]; + if ([new_row_info isKindOfClass:[NSArray class]]) + [self setArray:new_row_info forRow: row + i]; + if ([new_row_info isKindOfClass:[NSString class]]) + [self setText:(NSString *)new_row_info forRow: row + i]; + if (item_keys) + [self setKey:[item_keys objectAtIndex:i] forRow: row + i]; + else + [self setKey:@"" forRow: row + i]; + } +} + + +- (void) scrollUp:(int) how_much +{ + unsigned i; + for (i = 0; i + how_much < n_rows; i++) + { + [rowText replaceObjectAtIndex:i withObject:[rowText objectAtIndex: i + how_much]]; + [rowColor replaceObjectAtIndex:i withObject:[rowColor objectAtIndex: i + how_much]]; + [rowKey replaceObjectAtIndex:i withObject:[rowKey objectAtIndex: i + how_much]]; + rowAlignment[i] = rowAlignment[i + how_much]; + rowFadeTime[i] = rowFadeTime[i + how_much]; + } + for (; i < n_rows; i++) + { + [rowText replaceObjectAtIndex:i withObject:@""]; + [rowColor replaceObjectAtIndex:i withObject:textColor]; + [rowKey replaceObjectAtIndex:i withObject:@""]; + rowAlignment[i] = GUI_ALIGN_LEFT; + rowFadeTime[i] = 0.0f; + } +} + + +- (void)setBackgroundTexture:(OOTexture *)backgroundTexture +{ + [backgroundSprite release]; + backgroundSprite = nil; + + if (backgroundTexture != nil) + { + backgroundSprite = [[OpenGLSprite alloc] initWithTexture:backgroundTexture]; + } +} + + +- (void)clearBackground +{ + [self setBackgroundTexture:nil]; +} + + +- (void) setStatusPage:(int)pageNum +{ + if (pageNum==0) + statusPage=1; + else + statusPage += pageNum; +} + + +- (void) drawEqptList:(NSArray *)eqptList z:(GLfloat)z +{ + if (eqptList == nil) return; + + int first_row = STATUS_EQUIPMENT_FIRST_ROW; + int items_per_column = STATUS_EQUIPMENT_MAX_ROWS; + + int first_y = 40; // first_row =10 :-> 40 - first_row=11 -> 24 etc... + int items_count = [eqptList count]; + int page_count=1; + int i, start; + NSArray *info = nil; + NSString *name = nil; + BOOL damaged; + + // Paging calculations. Assuming 10 lines we get - one page:20 items per page (ipp) + // two pages: 18 ipp - three+ pages: 1st & last 18pp, middle pages 16ipp + + i = items_per_column * 2 + 2; + if (items_count > i) // don't fit in one page? + { + [UNIVERSE setDisplayCursor: YES]; + i = items_per_column * 4; // total items in the first and last pages + items_per_column--; // for all the middle pages. + if (items_count <= i) // two pages + { + page_count++; + if (statusPage == 1) start=0; + else + { + statusPage = 2; + start = i/statusPage; // for the for loop + } + } + else // three or more + { + page_count = ceil((float)(items_count-i)/(items_per_column*2)) + 2; + statusPage = OOClampInteger(statusPage, 1, page_count); + start = (statusPage == 1) ? 0 : (statusPage-1) * items_per_column * 2 + 2; + } + } + else + { + statusPage=page_count; // one page + start=0; + } + + [self setSelectableRange:NSMakeRange(first_row, first_row + STATUS_EQUIPMENT_MAX_ROWS)]; + if (statusPage > 1) + { + [self setColor:[OOColor greenColor] forRow:first_row]; + [self setArray:[NSArray arrayWithObjects:DESC(@"gui-back"), @"", @" <-- ",nil] forRow:first_row]; + [self setKey:GUI_KEY_OK forRow:first_row]; + first_y -= 16; // start 1 row down! + if (statusPage == page_count) [self setSelectedRow:first_row]; + } + if (statusPage < page_count) + { + [self setColor:[OOColor greenColor] forRow:first_row + STATUS_EQUIPMENT_MAX_ROWS]; + [self setArray:[NSArray arrayWithObjects:DESC(@"gui-more"), @"", @" --> ",nil] forRow:first_row + STATUS_EQUIPMENT_MAX_ROWS]; + [self setKey:GUI_KEY_OK forRow:first_row + STATUS_EQUIPMENT_MAX_ROWS]; + if (statusPage == 1) [self setSelectedRow:first_row + STATUS_EQUIPMENT_MAX_ROWS]; + } + + if (statusPage == 1 || statusPage == page_count) items_per_column++; + items_count = OOClampInteger(items_count, 1, start + items_per_column * 2); + for (i=start; i < items_count; i++) + { + info = [eqptList objectAtIndex:i]; + name = [info stringAtIndex:0]; + damaged = ![info boolAtIndex:1]; + if (damaged) glColor4f (1.0f, 0.5f, 0.0f, 1.0f); // Damaged items show up orange. + else glColor4f (1.0f, 1.0f, 0.0f, 1.0f); // Normal items in yellow. + if([name length] > 42) name =[[name substringToIndex:40] stringByAppendingString:@"..."]; + if (i - start < items_per_column) + { + OODrawString (name, -220, first_y - 16 * (i - start), z, NSMakeSize(15, 15)); + } + else + { + OODrawString (name, 50, first_y - 16 * (i - items_per_column - start), z, NSMakeSize(15, 15)); + } + } +} + + +- (int) drawGUI:(GLfloat) alpha drawCursor:(BOOL) drawCursor +{ + GLfloat x = drawPosition.x; + GLfloat y = drawPosition.y; + GLfloat z = [[UNIVERSE gameView] display_z]; + + if (alpha > 0.05f) + { + + PlayerEntity* player = [PlayerEntity sharedPlayer]; + + glEnable(GL_LINE_SMOOTH); + + if (self == [UNIVERSE gui]) + { + if ([player guiScreen] == GUI_SCREEN_SHORT_RANGE_CHART) + [self drawStarChart:x - 0.5f * size_in_pixels.width :y - 0.5f * size_in_pixels.height :z :alpha]; + if ([player guiScreen] == GUI_SCREEN_LONG_RANGE_CHART) + { + [self drawGalaxyChart:x - 0.5f * size_in_pixels.width :y - 0.5f * size_in_pixels.height :z :alpha]; + } + if ([player guiScreen] == GUI_SCREEN_STATUS) + { + [self drawEqptList:[player equipmentList] z:z ]; + } + } + [self drawGLDisplay:x - 0.5f * size_in_pixels.width :y - 0.5f * size_in_pixels.height :z :alpha]; + + if (fade_sign) + { + fade_alpha += (float)(fade_sign * [UNIVERSE getTimeDelta]); + if (fade_alpha < 0.05f) // done fading out + { + fade_alpha = 0.0f; + fade_sign = 0.0f; + } + if (fade_alpha > 1.0f) // done fading in + { + fade_alpha = 1.0f; + fade_sign = 0.0f; + } + } + } + + int cursor_row = 0; + + if (drawCursor) + { + NSPoint vjpos = [[UNIVERSE gameView] virtualJoystickPosition]; + double cursor_x = size_in_pixels.width * vjpos.x; + if (cursor_x < -size_in_pixels.width * 0.5) cursor_x = -size_in_pixels.width * 0.5f; + if (cursor_x > size_in_pixels.width * 0.5) cursor_x = size_in_pixels.width * 0.5f; + double cursor_y = -size_in_pixels.height * vjpos.y; + if (cursor_y < -size_in_pixels.height * 0.5) cursor_y = -size_in_pixels.height * 0.5f; + if (cursor_y > size_in_pixels.height * 0.5) cursor_y = size_in_pixels.height * 0.5f; + + cursor_row = 1 + (float)floor((0.5f * size_in_pixels.height - pixel_row_start - cursor_y) / pixel_row_height); + + GLfloat h1 = 3.0f; + GLfloat h3 = 9.0f; + glColor4f(0.2f, 0.2f, 1.0f, 0.5f); + glLineWidth(2.0f); + + cursor_x += x; + cursor_y += y; + [[UNIVERSE gameView] setVirtualJoystick:cursor_x/size_in_pixels.width :-cursor_y/size_in_pixels.height]; + + glBegin(GL_LINES); + glVertex3f((float)cursor_x - h1, (float)cursor_y, z); glVertex3f((float)cursor_x - h3, (float)cursor_y, z); + glVertex3f((float)cursor_x + h1, (float)cursor_y, z); glVertex3f((float)cursor_x + h3, (float)cursor_y, z); + glVertex3f((float)cursor_x, (float)cursor_y - h1, z); glVertex3f((float)cursor_x, (float)cursor_y - h3, z); + glVertex3f((float)cursor_x, (float)cursor_y + h1, z); glVertex3f((float)cursor_x, (float)cursor_y + h3, z); + glEnd(); + glLineWidth(1.0f); + + } + + return cursor_row; +} + + +- (void) drawGLDisplay:(GLfloat)x :(GLfloat)y :(GLfloat)z :(GLfloat) alpha +{ + NSSize strsize; + unsigned i; + OOTimeDelta delta_t = [UNIVERSE getTimeDelta]; + NSSize characterSize = pixel_text_size; + NSSize titleCharacterSize = pixel_title_size; + + // do backdrop + // + if (backgroundColor) + { + glColor4f([backgroundColor redComponent], [backgroundColor greenComponent], [backgroundColor blueComponent], alpha * [backgroundColor alphaComponent]); + glBegin(GL_QUADS); + glVertex3f(x + 0.0f, y + 0.0f, z); + glVertex3f(x + size_in_pixels.width, y + 0.0f, z); + glVertex3f(x + size_in_pixels.width, y + size_in_pixels.height, z); + glVertex3f(x + 0.0f, y + size_in_pixels.height, z); + glEnd(); + } + + // show background image... + // + if (backgroundSprite) + { + [backgroundSprite blitCentredToX:x + 0.5f * size_in_pixels.width Y:y + 0.5f * size_in_pixels.height Z:z alpha:alpha]; + } + + if (!RowInRange(selectedRow, selectableRange)) + selectedRow = -1; // out of Range; + + //// + // drawing operations here + + if (title != nil) + { + // + // draw the title + // + strsize = OORectFromString(title, 0.0f, 0.0f, titleCharacterSize).size; + glColor4f(1.0f, 0.0f, 0.0f, alpha); // red + OODrawString(title, x + pixel_row_center - strsize.width/2.0, y + size_in_pixels.height - pixel_title_size.height, z, titleCharacterSize); + + // draw a horizontal divider + // + glColor4f(0.75f, 0.75f, 0.75f, alpha); // 75% gray + glBegin(GL_QUADS); + glVertex3f(x + 0, y + size_in_pixels.height - pixel_title_size.height + 4, z); + glVertex3f(x + size_in_pixels.width, y + size_in_pixels.height - pixel_title_size.height + 4, z); + glVertex3f(x + size_in_pixels.width, y + size_in_pixels.height - pixel_title_size.height + 2, z); + glVertex3f(x + 0, y + size_in_pixels.height - pixel_title_size.height + 2, z); + glEnd(); + } + + // draw each row of text + // + for (i = 0; i < n_rows; i++) + { + OOColor* row_color = (OOColor *)[rowColor objectAtIndex:i]; + GLfloat row_alpha = alpha; + if (rowFadeTime[i] > 0.0f) + { + rowFadeTime[i] -= (float)delta_t; + if (rowFadeTime[i] <= 0.0f) + { + [rowText replaceObjectAtIndex:i withObject:@""]; + rowFadeTime[i] = 0.0f; + } + if ((rowFadeTime[i] > 0.0f)&&(rowFadeTime[i] < 1.0)) + row_alpha *= rowFadeTime[i]; + } + glColor4f([row_color redComponent], [row_color greenComponent], [row_color blueComponent], row_alpha); + + if ([[rowText objectAtIndex:i] isKindOfClass:[NSString class]]) + { + NSString* text = (NSString *)[rowText objectAtIndex:i]; + if (![text isEqual:@""]) + { + strsize = OORectFromString(text, 0.0f, 0.0f, characterSize).size; + switch (rowAlignment[i]) + { + case GUI_ALIGN_LEFT : + rowPosition[i].x = 0.0f; + break; + case GUI_ALIGN_RIGHT : + rowPosition[i].x = size_in_pixels.width - strsize.width; + break; + case GUI_ALIGN_CENTER : + rowPosition[i].x = (size_in_pixels.width - strsize.width)/2.0f; + break; + } + if (i == (unsigned)selectedRow) + { + NSRect block = OORectFromString(text, x + rowPosition[i].x + 2, y + rowPosition[i].y + 2, characterSize); + glColor4f(1.0f, 0.0f, 0.0f, row_alpha); // red + glBegin(GL_QUADS); + glVertex3f(block.origin.x, block.origin.y, z); + glVertex3f(block.origin.x + block.size.width, block.origin.y, z); + glVertex3f(block.origin.x + block.size.width, block.origin.y + block.size.height, z); + glVertex3f(block.origin.x, block.origin.y + block.size.height, z); + glEnd(); + glColor4f(0.0f, 0.0f, 0.0f, row_alpha); // black + } + OODrawString(text, x + rowPosition[i].x, y + rowPosition[i].y, z, characterSize); + + // draw cursor at end of current Row + // + if ((showTextCursor)&&(i == (unsigned)currentRow)) + { + NSRect tr = OORectFromString(text, 0.0f, 0.0f, characterSize); + NSPoint cu = NSMakePoint(x + rowPosition[i].x + tr.size.width + 0.2f * characterSize.width, y + rowPosition[i].y); + tr.origin = cu; + tr.size.width = 0.5f * characterSize.width; + GLfloat g_alpha = 0.5f * (1.0f + (float)sin(6 * [UNIVERSE getTime])); + glColor4f(1.0f, 0.0f, 0.0f, row_alpha * g_alpha); // red + glBegin(GL_QUADS); + glVertex3f(tr.origin.x, tr.origin.y, z); + glVertex3f(tr.origin.x + tr.size.width, tr.origin.y, z); + glVertex3f(tr.origin.x + tr.size.width, tr.origin.y + tr.size.height, z); + glVertex3f(tr.origin.x, tr.origin.y + tr.size.height, z); + glEnd(); + } + } + } + if ([[rowText objectAtIndex:i] isKindOfClass:[NSArray class]]) + { + unsigned j; + NSArray* array = (NSArray *)[rowText objectAtIndex:i]; + unsigned max_columns=[array count] < n_columns ? [array count] : n_columns; + BOOL isLeftAligned; + + for (j = 0; j < max_columns ; j++) + { + NSString* text = [array stringAtIndex:j]; + if ([text length] != 0) + { + isLeftAligned=tabStops[j]>=0; + rowPosition[i].x = abs(tabStops[j]); + NSRect block = OORectFromString(text, x + rowPosition[i].x + 2, y + rowPosition[i].y + 2, characterSize); + if(!isLeftAligned) + { + rowPosition[i].x -=block.size.width+6; + block = OORectFromString(text, x + rowPosition[i].x, y + rowPosition[i].y + 2, characterSize); + block.size.width+=2; + } + if (i == (unsigned)selectedRow) + { + glColor4f(1.0f, 0.0f, 0.0f, row_alpha); // red + glBegin(GL_QUADS); + glVertex3f(block.origin.x, block.origin.y, z); + glVertex3f(block.origin.x + block.size.width, block.origin.y, z); + glVertex3f(block.origin.x + block.size.width, block.origin.y + block.size.height, z); + glVertex3f(block.origin.x, block.origin.y + block.size.height, z); + glEnd(); + glColor4f(0.0f, 0.0f, 0.0f, row_alpha); // black + } + OODrawString(text, x + rowPosition[i].x, y + rowPosition[i].y, z, characterSize); + } + } + } + } +} + + +- (void) drawStarChart:(GLfloat)x :(GLfloat)y :(GLfloat)z :(GLfloat) alpha +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + + if (!player) + return; + + NSPoint galaxy_coordinates = [player galaxy_coordinates]; + NSPoint cursor_coordinates = [player cursor_coordinates]; + NSPoint cu; + + double fuel = 35.0 * [player dialFuel]; + + Random_Seed g_seed; + double hcenter = size_in_pixels.width/2.0; + double vcenter = 160.0f; + double hscale = 4.0 * size_in_pixels.width / 256.0; + double vscale = -4.0 * size_in_pixels.height / 512.0; + double hoffset = hcenter - galaxy_coordinates.x*hscale; + double voffset = size_in_pixels.height - pixel_title_size.height - 5 - vcenter - galaxy_coordinates.y*vscale; + int i; + NSPoint star; + + if ((abs(cursor_coordinates.x-galaxy_coordinates.x)>=20)||(abs(cursor_coordinates.y-galaxy_coordinates.y)>=38)) + cursor_coordinates = galaxy_coordinates; // home + + // get a list of systems marked as contract destinations + NSArray* markedDestinations = [player markedDestinations]; + + // draw fuel range circle + // + glColor4f(0.0f, 1.0f, 0.0f, alpha); // green + glLineWidth(2.0f); + cu = NSMakePoint((float)(hscale*galaxy_coordinates.x+hoffset),(float)(vscale*galaxy_coordinates.y+voffset)); + GLDrawOval(x + cu.x, y + cu.y, z, NSMakeSize((float)(fuel*hscale), 2*(float)(fuel*vscale)), 5); + + // draw marks and stars + // + glLineWidth(1.5f); + glColor4f(1.0f, 1.0f, 0.75f, alpha); // pale yellow + + for (i = 0; i < 256; i++) + { + g_seed = [UNIVERSE systemSeedForSystemNumber:i]; + + int dx, dy; + float blob_size = 4.0f + 0.5f * (g_seed.f & 15); + + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + + dx = abs(galaxy_coordinates.x - g_seed.d); + dy = abs(galaxy_coordinates.y - g_seed.b); + + if ((dx < 20)&&(dy < 38)) + { + if ([markedDestinations boolAtIndex:i]) // is marked + { + GLfloat mark_size = 0.5f * blob_size + 2.5f; + glColor4f(1.0f, 0.0f, 0.0f, alpha); // red + glBegin(GL_LINES); + glVertex3f(x + star.x - mark_size, y + star.y - mark_size, z); + glVertex3f(x + star.x + mark_size, y + star.y + mark_size, z); + glVertex3f(x + star.x - mark_size, y + star.y + mark_size, z); + glVertex3f(x + star.x + mark_size, y + star.y - mark_size, z); + glEnd(); + glColor4f(1.0f, 1.0f, 0.75f, alpha); // pale yellow + } + GLDrawFilledOval(x + star.x, y + star.y, z, NSMakeSize(blob_size,blob_size), 15); + } + } + + // draw names + // + glColor4f(1.0f, 1.0f, 0.0f, alpha); // yellow + for (i = 0; i < 256; i++) + { + g_seed = [UNIVERSE systemSeedForSystemNumber:i]; + + int dx, dy; + + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + + dx = abs(galaxy_coordinates.x - g_seed.d); + dy = abs(galaxy_coordinates.y - g_seed.b); + + if ((dx < 20)&&(dy < 38)) + { + NSDictionary* sys_info = [UNIVERSE generateSystemData:g_seed]; + int tec = [sys_info intForKey:KEY_TECHLEVEL]; + int eco = [sys_info intForKey:KEY_ECONOMY]; + int gov = [sys_info intForKey:KEY_GOVERNMENT]; + NSString* p_name = [sys_info stringForKey:KEY_NAME]; + if (![player showInfoFlag]) + { + OODrawString(p_name, x + star.x, y + star.y, z, NSMakeSize(pixel_row_height,pixel_row_height)); + } + else + { + OODrawPlanetInfo(gov, eco, tec, x + star.x + 2.0, y + star.y + 2.0, z, NSMakeSize(pixel_row_height,pixel_row_height)); + } + } + } + + // draw cross-hairs over current location + // + glColor4f(0.0f, 1.0f, 0.0f, alpha); // green + glBegin(GL_QUADS); + glVertex3f(x + cu.x - 1, y + cu.y - 14, z); + glVertex3f(x + cu.x + 1, y + cu.y - 14, z); + glVertex3f(x + cu.x + 1, y + cu.y + 14, z); + glVertex3f(x + cu.x - 1, y + cu.y + 14, z); + glVertex3f(x + cu.x - 14, y + cu.y - 1, z); + glVertex3f(x + cu.x + 14, y + cu.y - 1, z); + glVertex3f(x + cu.x + 14, y + cu.y + 1, z); + glVertex3f(x + cu.x - 14, y + cu.y + 1, z); + glEnd(); + + // draw cross hairs over cursor + // + glColor4f(1.0f, 0.0f, 0.0f, alpha); // red + cu = NSMakePoint((float)(hscale*cursor_coordinates.x+hoffset),(float)(vscale*cursor_coordinates.y+voffset)); + glBegin(GL_QUADS); + glVertex3f(x + cu.x - 1, y + cu.y - 7, z); + glVertex3f(x + cu.x + 1, y + cu.y - 7, z); + glVertex3f(x + cu.x + 1, y + cu.y + 7, z); + glVertex3f(x + cu.x - 1, y + cu.y + 7, z); + glVertex3f(x + cu.x - 7, y + cu.y - 1, z); + glVertex3f(x + cu.x + 7, y + cu.y - 1, z); + glVertex3f(x + cu.x + 7, y + cu.y + 1, z); + glVertex3f(x + cu.x - 7, y + cu.y + 1, z); + glEnd(); +} + + +- (void) drawGalaxyChart:(GLfloat)x :(GLfloat)y :(GLfloat)z :(GLfloat) alpha +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + + NSPoint galaxy_coordinates = [player galaxy_coordinates]; + NSPoint cursor_coordinates = [player cursor_coordinates]; + + double fuel = 35.0 * [player dialFuel]; + + // get a list of systems marked as contract destinations + NSArray* markedDestinations = [player markedDestinations]; + + BOOL* systems_found = [UNIVERSE systems_found]; + + NSPoint star, cu; + + Random_Seed g_seed; + double hscale = size_in_pixels.width / 256.0; + double vscale = -1.0 * size_in_pixels.height / 512.0; + double hoffset = 0.0f; + double voffset = size_in_pixels.height - pixel_title_size.height - 5; + int i; + + if (showAdvancedNavArray && ![UNIVERSE strict] && [player hasEquipmentItem:@"EQ_ADVANCED_NAVIGATIONAL_ARRAY"]) + { + [self drawAdvancedNavArrayAtX:x y:y z:z alpha:alpha]; + } + + // draw fuel range circle + // + glColor4f(0.0f, 1.0f, 0.0f, alpha); // green + glLineWidth(2.0f); + cu = NSMakePoint((float)(hscale*galaxy_coordinates.x+hoffset),(float)(vscale*galaxy_coordinates.y+voffset)); + GLDrawOval(x + cu.x, y + cu.y, z, NSMakeSize((float)(fuel*hscale), 2*(float)(fuel*vscale)), 5); + + // draw cross-hairs over current location + // + glBegin(GL_QUADS); + glVertex3f(x + cu.x - 1, y + cu.y - 14, z); + glVertex3f(x + cu.x + 1, y + cu.y - 14, z); + glVertex3f(x + cu.x + 1, y + cu.y + 14, z); + glVertex3f(x + cu.x - 1, y + cu.y + 14, z); + glVertex3f(x + cu.x - 14, y + cu.y - 1, z); + glVertex3f(x + cu.x + 14, y + cu.y - 1, z); + glVertex3f(x + cu.x + 14, y + cu.y + 1, z); + glVertex3f(x + cu.x - 14, y + cu.y + 1, z); + glEnd(); + + // draw cross hairs over cursor + // + glColor4f(1.0f, 0.0f, 0.0f, alpha); // red + cu = NSMakePoint((float)(hscale*cursor_coordinates.x+hoffset),(float)(vscale*cursor_coordinates.y+voffset)); + glBegin(GL_QUADS); + glVertex3f(x + cu.x - 1, y + cu.y - 7, z); + glVertex3f(x + cu.x + 1, y + cu.y - 7, z); + glVertex3f(x + cu.x + 1, y + cu.y + 7, z); + glVertex3f(x + cu.x - 1, y + cu.y + 7, z); + glVertex3f(x + cu.x - 7, y + cu.y - 1, z); + glVertex3f(x + cu.x + 7, y + cu.y - 1, z); + glVertex3f(x + cu.x + 7, y + cu.y + 1, z); + glVertex3f(x + cu.x - 7, y + cu.y + 1, z); + glEnd(); + + // draw marks + // + glLineWidth(1.5f); + glColor4f(1.0f, 0.0f, 0.0f, alpha); + for (i = 0; i < 256; i++) + { + g_seed = [UNIVERSE systemSeedForSystemNumber:i]; + BOOL mark = [(NSNumber*)[markedDestinations objectAtIndex:i] boolValue]; + if (mark) + { + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + glBegin(GL_LINES); + glVertex3f(x + star.x - 2.5f, y + star.y - 2.5f, z); + glVertex3f(x + star.x + 2.5f, y + star.y + 2.5f, z); + glVertex3f(x + star.x - 2.5f, y + star.y + 2.5f, z); + glVertex3f(x + star.x + 2.5f, y + star.y - 2.5f, z); + glEnd(); + } + } + + // draw stars + // + glColor4f(1.0f, 1.0f, 1.0f, alpha); + glBegin(GL_QUADS); + for (i = 0; i < 256; i++) + { + g_seed = [UNIVERSE systemSeedForSystemNumber:i]; + + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + + float sz = (4.0f + 0.5f * (0x03 | (g_seed.f & 0x0f))) / 7.0f; + + glVertex3f(x + star.x, y + star.y + sz, z); + glVertex3f(x + star.x + sz, y + star.y, z); + glVertex3f(x + star.x, y + star.y - sz, z); + glVertex3f(x + star.x - sz, y + star.y, z); + } + glEnd(); + + // draw found stars and captions + // + glLineWidth(1.5f); + glColor4f(0.0f, 1.0f, 0.0f, alpha); + int n_matches=0; + for (i = 0; i < 256; i++) if (systems_found[i]) n_matches++; + BOOL drawNames=n_matches<4; + for (i = 0; i < 256; i++) + { + BOOL mark = systems_found[i]; + g_seed = [UNIVERSE systemSeedForSystemNumber:i]; + if (mark) + { + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + glBegin(GL_LINE_LOOP); + glVertex3f(x + star.x - 2.0f, y + star.y - 2.0f, z); + glVertex3f(x + star.x + 2.0f, y + star.y - 2.0f, z); + glVertex3f(x + star.x + 2.0f, y + star.y + 2.0f, z); + glVertex3f(x + star.x - 2.0f, y + star.y + 2.0f, z); + glEnd(); + if (drawNames) + OODrawString([UNIVERSE systemNameIndex:i] , x + star.x + 2.0, y + star.y - 10.0f, z, NSMakeSize(10,10)); + } + } + + // draw bottom horizontal divider + // + glColor4f(0.75f, 0.75f, 0.75f, alpha); // 75% gray + glBegin(GL_QUADS); + glVertex3f(x + 0, (float)(y + voffset + 260.0f*vscale + 0), z); + glVertex3f(x + size_in_pixels.width, y + (float)(voffset + 260.0f*vscale + 0), z); + glVertex3f(x + size_in_pixels.width, (float)(y + voffset + 260.0f*vscale - 2), z); + glVertex3f(x + 0, (float)(y + voffset + 260.0f*vscale - 2), z); + glEnd(); + +} + + +// Advanced Navigation Array -- galactic chart route mapping - contributed by Nikos Barkas (another_commander). +- (void) drawAdvancedNavArrayAtX:(float)x y:(float)y z:(float)z alpha:(float)alpha +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + NSPoint galaxy_coordinates = [player galaxy_coordinates]; + NSPoint cursor_coordinates = [player cursor_coordinates]; + Random_Seed galaxy_seed = [player galaxy_seed]; + Random_Seed g_seed, g_seed2; + int i, j; + double hscale = size_in_pixels.width / 256.0; + double vscale = -1.0 * size_in_pixels.height / 512.0; + double hoffset = 0.0f; + double voffset = size_in_pixels.height - pixel_title_size.height - 5; + NSPoint star, star2 = NSZeroPoint; + + glColor4f(0.25f, 0.25f, 0.25f, alpha); + + glBegin(GL_LINES ); + for (i = 0; i < 256; i++) for (j = i + 1; j < 256; j++) + { + g_seed = [UNIVERSE systemSeedForSystemNumber:i]; + g_seed2 = [UNIVERSE systemSeedForSystemNumber:j]; + + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + star2.x = (float)(g_seed2.d * hscale + hoffset); + star2.y = (float)(g_seed2.b * vscale + voffset); + double d = distanceBetweenPlanetPositions(g_seed.d, g_seed.b, g_seed2.d, g_seed2.b); + + if (d <= ([player fuelCapacity] / 10.0f)) // another_commander - Default to 7.0 LY. + { + glVertex3f(x+star.x, y+star.y, z); + glVertex3f(x+star2.x, y+star2.y, z); + } + } + glEnd(); + + // Draw route from player position to currently selected destination. + int planetNumber = [UNIVERSE findSystemNumberAtCoords:galaxy_coordinates withGalaxySeed:galaxy_seed]; + int destNumber = [UNIVERSE findSystemNumberAtCoords:cursor_coordinates withGalaxySeed:galaxy_seed]; + NSDictionary* routeInfo = [UNIVERSE routeFromSystem:planetNumber toSystem:destNumber]; + + if ((destNumber != planetNumber) && routeInfo) + { + int route_hops = [(NSArray *)[routeInfo objectForKey:@"route"] count] -1; + + glColor4f (1.0f, 1.0f, 0.0f, alpha); // Yellow for plotting routes. + for (i = 0; i < route_hops; i++) + { + int loc = [(NSNumber *)[[routeInfo objectForKey:@"route"] objectAtIndex:i] intValue]; + int loc2 = [(NSNumber *)[[routeInfo objectForKey:@"route"] objectAtIndex:(i+1)] intValue]; + + g_seed = [UNIVERSE systemSeedForSystemNumber:loc]; + g_seed2 = [UNIVERSE systemSeedForSystemNumber:(loc2)]; + star.x = (float)(g_seed.d * hscale + hoffset); + star.y = (float)(g_seed.b * vscale + voffset); + star2.x = (float)(g_seed2.d * hscale + hoffset); + star2.y = (float)(g_seed2.b * vscale + voffset); + + glBegin (GL_LINES); + glVertex3f (x+star.x, y+star.y, z); + glVertex3f (x+star2.x, y+star2.y, z); + glEnd(); + + // Label the route. + OODrawString([UNIVERSE systemNameIndex:loc] , x + star.x + 2.0, y + star.y - 6.0, z, NSMakeSize(8,8)); + } + // Label the destination, which was not included in the above loop. + OODrawString([UNIVERSE systemNameIndex:destNumber] , x + star2.x + 2.0, y + star2.y - 6.0, z, NSMakeSize(8,8)); + } +} + +@end diff --git a/src/Core/HeadUpDisplay.h b/src/Core/HeadUpDisplay.h new file mode 100644 index 00000000..aa93cc7c --- /dev/null +++ b/src/Core/HeadUpDisplay.h @@ -0,0 +1,260 @@ +/* + +HeadUpDisplay.h + +Class handling the player ship’s heads-up display, and 2D drawing functions. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOOpenGL.h" + +#import "OOTypes.h" +#import "OOMaths.h" +#import "MyOpenGLView.h" + +@class OOCrosshairs, OOColor; + + +#define SCANNER_CENTRE_X 0 +#define SCANNER_CENTRE_Y -180 +#define SCANNER_SCALE 256 +#define SCANNER_WIDTH 288 +#define SCANNER_HEIGHT 72 + +#define SCANNER_MAX_ZOOM 5.0 +#define SCANNER_ZOOM_LEVELS 5 +#define ZOOM_INDICATOR_CENTRE_X 108 +#define ZOOM_INDICATOR_CENTRE_Y -216 +#define ZOOM_INDICATOR_WIDTH 11.0f +#define ZOOM_INDICATOR_HEIGHT 14.0f +#define ZOOM_LEVELS_IMAGE @"zoom.png" + +#define COMPASS_IMAGE @"compass.png" +#define COMPASS_CENTRE_X 132 +#define COMPASS_CENTRE_Y -216 +#define COMPASS_SIZE 56 +#define COMPASS_HALF_SIZE 28 +#define COMPASS_REDDOT_IMAGE @"reddot.png" +#define COMPASS_GREENDOT_IMAGE @"greendot.png" +#define COMPASS_DOT_SIZE 16 +#define COMPASS_HALF_DOT_SIZE 8 + +#define AEGIS_IMAGE @"aegis.png" +#define AEGIS_CENTRE_X -132 +#define AEGIS_CENTRE_Y -216 +#define AEGIS_WIDTH 24 +#define AEGIS_HEIGHT 24 + +#define SPEED_BAR_CENTRE_X 200 +#define SPEED_BAR_CENTRE_Y -145 +#define SPEED_BAR_WIDTH 80 +#define SPEED_BAR_HEIGHT 8 +#define SPEED_BAR_DRAW_SURROUND YES + +#define ROLL_BAR_CENTRE_X 200 +#define ROLL_BAR_CENTRE_Y -160 +#define ROLL_BAR_WIDTH 80 +#define ROLL_BAR_HEIGHT 8 +#define ROLL_BAR_DRAW_SURROUND YES + +#define PITCH_BAR_CENTRE_X 200 +#define PITCH_BAR_CENTRE_Y -170 +#define PITCH_BAR_WIDTH 80 +#define PITCH_BAR_HEIGHT 8 +#define PITCH_BAR_DRAW_SURROUND YES + +#define ENERGY_GAUGE_CENTRE_X 200 +#define ENERGY_GAUGE_CENTRE_Y -205 +#define ENERGY_GAUGE_WIDTH 80 +#define ENERGY_GAUGE_HEIGHT 48 +#define ENERGY_GAUGE_DRAW_SURROUND YES + +#define FORWARD_SHIELD_BAR_CENTRE_X -200 +#define FORWARD_SHIELD_BAR_CENTRE_Y -146 +#define FORWARD_SHIELD_BAR_WIDTH 80 +#define FORWARD_SHIELD_BAR_HEIGHT 8 +#define FORWARD_SHIELD_BAR_DRAW_SURROUND YES + +#define AFT_SHIELD_BAR_CENTRE_X -200 +#define AFT_SHIELD_BAR_CENTRE_Y -162 +#define AFT_SHIELD_BAR_WIDTH 80 +#define AFT_SHIELD_BAR_HEIGHT 8 +#define AFT_SHIELD_BAR_DRAW_SURROUND YES + +#define FUEL_BAR_CENTRE_X -200 +#define FUEL_BAR_CENTRE_Y -179 +#define FUEL_BAR_WIDTH 80 +#define FUEL_BAR_HEIGHT 8 + +#define CABIN_TEMP_BAR_CENTRE_X -200 +#define CABIN_TEMP_BAR_CENTRE_Y -189 +#define CABIN_TEMP_BAR_WIDTH 80 +#define CABIN_TEMP_BAR_HEIGHT 8 + +#define WEAPON_TEMP_BAR_CENTRE_X -200 +#define WEAPON_TEMP_BAR_CENTRE_Y -199 +#define WEAPON_TEMP_BAR_WIDTH 80 +#define WEAPON_TEMP_BAR_HEIGHT 8 + +#define ALTITUDE_BAR_CENTRE_X -200 +#define ALTITUDE_BAR_CENTRE_Y -209 +#define ALTITUDE_BAR_WIDTH 80 +#define ALTITUDE_BAR_HEIGHT 8 + +#define MISSILES_DISPLAY_X -228 +#define MISSILES_DISPLAY_Y -224 +#define MISSILES_DISPLAY_SPACING 16 +#define MISSILE_ICON_WIDTH 12 +#define MISSILE_ICON_HEIGHT MISSILE_ICON_WIDTH + +#define CLOCK_DISPLAY_X -44 +#define CLOCK_DISPLAY_Y -234 +#define CLOCK_DISPLAY_WIDTH 12 +#define CLOCK_DISPLAY_HEIGHT 12 + +#define FPSINFO_DISPLAY_X -300 +#define FPSINFO_DISPLAY_Y 220 +#define FPSINFO_DISPLAY_WIDTH 12 +#define FPSINFO_DISPLAY_HEIGHT 12 + +#define STATUS_LIGHT_CENTRE_X -108 +#define STATUS_LIGHT_CENTRE_Y -216 +#define STATUS_LIGHT_WIDTH 8 +#define STATUS_LIGHT_HEIGHT 8 + +#define HIT_INDICATOR_CENTRE_X 200 +#define HIT_INDICATOR_CENTRE_Y 0 + +#define SCOOPSTATUS_CENTRE_X -132 +#define SCOOPSTATUS_CENTRE_Y -152 +#define SCOOPSTATUS_WIDTH 16.0 +#define SCOOPSTATUS_HEIGHT 16.0 + +#define DIALS_KEY @"dials" +#define LEGENDS_KEY @"legends" +#define X_KEY @"x" +#define Y_KEY @"y" +#define SPACING_KEY @"spacing" +#define ALPHA_KEY @"alpha" +#define SELECTOR_KEY @"selector" +#define IMAGE_KEY @"image" +#define WIDTH_KEY @"width" +#define HEIGHT_KEY @"height" +#define SPRITE_KEY @"sprite" +#define DRAW_SURROUND_KEY @"draw_surround" +#define EQUIPMENT_REQUIRED_KEY @"equipment_required" +#define LABELLED_KEY @"labelled" +#define TEXT_KEY @"text" +#define RGB_COLOR_KEY @"rgb_color" +#define COLOR_KEY @"color" +#define N_BARS_KEY @"n_bars" + +#define ROWS_KEY @"rows" +#define COLUMNS_KEY @"columns" +#define ROW_HEIGHT_KEY @"row_height" +#define ROW_START_KEY @"row_start" +#define TITLE_KEY @"title" +#define BACKGROUND_RGBA_KEY @"background_rgba" +#define OVERALL_ALPHA_KEY @"overall_alpha" + +#define Z1 [(MyOpenGLView *)[[player universe] gameView] display_z] + +#define ONE_EIGHTH 0.125 + + + +@class Entity, PlayerEntity, OpenGLSprite; + + +@interface HeadUpDisplay: NSObject +{ +@private + NSMutableArray *legendArray; + NSMutableArray *dialArray; + + // zoom level + GLfloat scanner_zoom; + + //where to draw it + GLfloat z1; + GLfloat line_width; + + GLfloat overallAlpha; + + BOOL reticleTargetSensitive; + + BOOL cloakIndicatorOnStatusLight; + + int last_transmitter; + + // Crosshairs + OOCrosshairs *_crosshairs; + OOWeaponType _lastWeaponType; + GLfloat _lastOverallAlpha; + NSDictionary *_crosshairOverrides; + OOColor *_crosshairColor; + GLfloat _crosshairScale; + GLfloat _crosshairWidth; +} + +- (id) initWithDictionary:(NSDictionary *) hudinfo; + +- (void) resizeGuis:(NSDictionary*) info; + +- (double) scanner_zoom; +- (void) setScannerZoom:(double) value; + +- (GLfloat) overallAlpha; +- (void) setOverallAlpha:(GLfloat) newAlphaValue; + +- (BOOL) reticleTargetSensitive; +- (void) setReticleTargetSensitive:(BOOL) newReticleTargetSensitiveValue; + +- (void) addLegend:(NSDictionary *) info; +- (void) addDial:(NSDictionary *) info; + +- (void) renderHUD; + +- (void) refreshLastTransmitter; + +- (void) setLine_width:(GLfloat) value; +- (GLfloat) line_width; + +- (void) drawWatermarkString:(NSString *)watermarkString; + +@end + + +@interface NSString (OODisplayEncoding) + +// Return a C string in the 8-bit encoding used for display. +- (const char *) cStringUsingOoliteEncoding; + +// Return a C string in the 8-bit encoding used for display, with substitutions performed. +- (const char *) cStringUsingOoliteEncodingAndRemapping; + +@end + + +void OODrawString(NSString *text, double x, double y, double z, NSSize siz); +void OODrawPlanetInfo(int gov, int eco, int tec, double x, double y, double z, NSSize siz); +NSRect OORectFromString(NSString *text, double x, double y, NSSize siz); diff --git a/src/Core/HeadUpDisplay.m b/src/Core/HeadUpDisplay.m new file mode 100644 index 00000000..d7201e9f --- /dev/null +++ b/src/Core/HeadUpDisplay.m @@ -0,0 +1,2619 @@ +/* + +HeadUpDisplay.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "HeadUpDisplay.h" +#import "ResourceManager.h" +#import "PlayerEntity.h" +#import "PlanetEntity.h" +#import "StationEntity.h" +#import "Universe.h" +#import "OOTrumble.h" +#import "OOColor.h" +#import "GuiDisplayGen.h" +#import "OOTexture.h" +#import "OpenGLSprite.h" +#import "OOCollectionExtractors.h" +#import "OOEncodingConverter.h" +#import "OOCrosshairs.h" +#import "OOConstToString.h" +#import "OOStringParsing.h" + + +#define kOOLogUnconvertedNSLog @"unclassified.HeadUpDisplay" + + +#define ONE_SIXTEENTH 0.0625 +#define ONE_SIXTYFOURTH 0.015625 +#define DEFAULT_OVERALL_ALPHA 0.75 + + +static void DrawSpecialOval(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step, GLfloat* color4v); + +static void GetRGBAArrayFromInfo(NSDictionary *info, GLfloat ioColor[4]); + +static void hudDrawIndicatorAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz, double amount); +static void hudDrawMarkerAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz, double amount); +static void hudDrawBarAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz, double amount); +static void hudDrawSurroundAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz); +static void hudDrawSpecialIconAt(NSArray* ptsArray, int x, int y, int z, NSSize siz); +static void hudDrawMineIconAt(int x, int y, int z, NSSize siz); +static void hudDrawMissileIconAt(int x, int y, int z, NSSize siz); +static void hudDrawStatusIconAt(int x, int y, int z, NSSize siz); +static void hudDrawReticleOnTarget(Entity* target, PlayerEntity* player1, GLfloat z1, GLfloat overallAlpha, BOOL reticleTargetSensitive); +static void drawScannerGrid(double x, double y, double z, NSSize siz, int v_dir, GLfloat thickness, double zoom); + + +static OOTexture *sFontTexture = nil; +static OOEncodingConverter *sEncodingCoverter = nil; + + +enum +{ + kFontTextureOptions = kOOTextureMinFilterMipMap | kOOTextureMagFilterLinear | kOOTextureNoShrink | kOOTextureAlphaMask +}; + + +@interface HeadUpDisplay (Private) + +- (void) drawCrosshairs; +- (void) drawLegends; +- (void) drawDials; + +- (void) drawLegend:(NSDictionary *) info; +- (void) drawHUDItem:(NSDictionary *) info; + +- (void) drawScanner:(NSDictionary *) info; +- (void) drawScannerZoomIndicator:(NSDictionary *) info; + +- (void) drawCompass:(NSDictionary *) info; +- (void) drawCompassPlanetBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha; +- (void) drawCompassStationBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha; +- (void) drawCompassSunBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha; +- (void) drawCompassTargetBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha; +- (void) drawCompassWitchpointBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha; +- (void) drawCompassBeaconBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha; + +- (void) drawAegis:(NSDictionary *) info; +- (void) drawSpeedBar:(NSDictionary *) info; +- (void) drawRollBar:(NSDictionary *) info; +- (void) drawPitchBar:(NSDictionary *) info; +- (void) drawYawBar:(NSDictionary *) info; +- (void) drawEnergyGauge:(NSDictionary *) info; +- (void) drawForwardShieldBar:(NSDictionary *) info; +- (void) drawAftShieldBar:(NSDictionary *) info; +- (void) drawFuelBar:(NSDictionary *) info; +- (void) drawCabinTempBar:(NSDictionary *) info; +- (void) drawWeaponTempBar:(NSDictionary *) info; +- (void) drawAltitudeBar:(NSDictionary *) info; +- (void) drawMissileDisplay:(NSDictionary *) info; +- (void) drawTargetReticle:(NSDictionary *) info; +- (void) drawStatusLight:(NSDictionary *) info; +- (void) drawDirectionCue:(NSDictionary *) info; +- (void) drawClock:(NSDictionary *) info; +- (void) drawFPSInfoCounter:(NSDictionary *) info; +- (void) drawScoopStatus:(NSDictionary *) info; + +- (void) drawGreenSurround:(NSDictionary *) info; +- (void) drawYellowSurround:(NSDictionary *) info; + +- (void) drawTrumbles:(NSDictionary *) info; + +- (NSArray *) crosshairDefinitionForWeaponType:(OOWeaponType)weapon; + +@end + + +@implementation HeadUpDisplay + +GLfloat red_color[4] = {1.0, 0.0, 0.0, 1.0}; +GLfloat redplus_color[4] = {1.0, 0.0, 0.5, 1.0}; +GLfloat yellow_color[4] = {1.0, 1.0, 0.0, 1.0}; +GLfloat green_color[4] = {0.0, 1.0, 0.0, 1.0}; +GLfloat darkgreen_color[4] ={0.0, 0.75, 0.0, 1.0}; +GLfloat cyan_color[4] = {0.0, 1.0, 1.0, 1.0}; +GLfloat blue_color[4] = {0.0, 0.0, 1.0, 1.0}; +GLfloat black_color[4] = {0.0, 0.0, 0.0, 1.0}; +GLfloat lightgray_color[4] = {0.25, 0.25, 0.25, 1.0}; + +static float sGlyphWidths[256]; + + +static double drawCharacterQuad(uint8_t chr, double x, double y, double z, NSSize siz); + +static void InitTextEngine(void); + + +OOINLINE void GLColorWithOverallAlpha(GLfloat *color, GLfloat alpha) +{ + glColor4f(color[0], color[1], color[2], color[3] * alpha); +} + + +- (id) initWithDictionary:(NSDictionary *) hudinfo +{ + unsigned i; + BOOL areTrumblesToBeDrawn = NO; + + self = [super init]; + + line_width = 1.0; + + if (sFontTexture == nil) InitTextEngine(); + + // init arrays + dialArray = [[NSMutableArray alloc] initWithCapacity:16]; // alloc retains + legendArray = [[NSMutableArray alloc] initWithCapacity:16]; // alloc retains + + // populate arrays + NSArray *dials = [hudinfo arrayForKey:DIALS_KEY]; + for (i = 0; i < [dials count]; i++) + { + NSDictionary *dial_info = [dials dictionaryAtIndex:i]; + if (!areTrumblesToBeDrawn && [[dial_info stringForKey:SELECTOR_KEY] isEqualToString:@"drawTrumbles:"]) areTrumblesToBeDrawn = YES; + [self addDial:dial_info]; + } + + if (!areTrumblesToBeDrawn) // naughty - a hud with no built-in drawTrumbles: - one must be added! + { + NSDictionary *trumble_dial_info = [NSDictionary dictionaryWithObjectsAndKeys: @"drawTrumbles:", SELECTOR_KEY, nil]; + [self addDial:trumble_dial_info]; + } + + NSArray *legends = [hudinfo arrayForKey:LEGENDS_KEY]; + for (i = 0; i < [legends count]; i++) + { + [self addLegend:[legends dictionaryAtIndex:i]]; + } + + overallAlpha = [hudinfo floatForKey:@"overall_alpha" defaultValue:DEFAULT_OVERALL_ALPHA]; + + reticleTargetSensitive = [hudinfo boolForKey:@"reticle_target_sensitive" defaultValue:NO]; + + cloakIndicatorOnStatusLight = [hudinfo boolForKey:@"cloak_indicator_on_status_light" defaultValue:NO]; + + last_transmitter = NO_TARGET; + + _crosshairOverrides = [[hudinfo dictionaryForKey:@"crosshairs"] retain]; + id crosshairColor = [hudinfo objectForKey:@"crosshair_color" defaultValue:@"greenColor"]; + _crosshairColor = [[OOColor colorWithDescription:crosshairColor] retain]; + _crosshairScale = [hudinfo floatForKey:@"crosshair_scale" defaultValue:32.0f]; + _crosshairWidth = [hudinfo floatForKey:@"crosshair_width" defaultValue:1.5f]; + + return self; +} + + +- (void) dealloc +{ + [legendArray release]; + [dialArray release]; + + [super dealloc]; +} + +//------------------------------------------------------------------------------------// + + +- (void) resizeGuis:(NSDictionary*) info +{ + // check for entries in hud plist for comm_log_gui and message_gui + // resize and reposition them accordingly + + GuiDisplayGen* message_gui = [UNIVERSE message_gui]; + if ((message_gui)&&([info objectForKey:@"message_gui"])) + { + NSDictionary* gui_info = (NSDictionary*)[info objectForKey:@"message_gui"]; + Vector pos = [message_gui drawPosition]; + if ([gui_info objectForKey:X_KEY]) + pos.x = [[gui_info objectForKey:X_KEY] floatValue]; + if ([gui_info objectForKey:Y_KEY]) + pos.y = [[gui_info objectForKey:Y_KEY] floatValue]; + [message_gui setDrawPosition:pos]; + NSSize siz = [message_gui size]; + int rht = [message_gui rowHeight]; + NSString* title = [message_gui title]; + if ([gui_info objectForKey:WIDTH_KEY]) + siz.width = [[gui_info objectForKey:WIDTH_KEY] floatValue]; + if ([gui_info objectForKey:HEIGHT_KEY]) + siz.height = [[gui_info objectForKey:HEIGHT_KEY] floatValue]; + if ([gui_info objectForKey:ROW_HEIGHT_KEY]) + rht = [[gui_info objectForKey:ROW_HEIGHT_KEY] intValue]; + if ([gui_info objectForKey:TITLE_KEY]) + title = [NSString stringWithFormat:@"%@", [gui_info objectForKey:TITLE_KEY]]; + [message_gui resizeTo:siz characterHeight:rht title:title]; + if ([gui_info objectForKey:ALPHA_KEY]) + [message_gui setAlpha: [[gui_info objectForKey:ALPHA_KEY] floatValue]]; + else + [message_gui setAlpha: 1.0]; + if ([gui_info objectForKey:BACKGROUND_RGBA_KEY]) + [message_gui setBackgroundColor:[OOColor colorFromString:[gui_info stringForKey:BACKGROUND_RGBA_KEY]]]; + } + + GuiDisplayGen* comm_log_gui = [UNIVERSE comm_log_gui]; + if ((comm_log_gui)&&([info objectForKey:@"comm_log_gui"])) + { + NSDictionary* gui_info = (NSDictionary*)[info objectForKey:@"comm_log_gui"]; + Vector pos = [comm_log_gui drawPosition]; + if ([gui_info objectForKey:X_KEY]) + pos.x = [[gui_info objectForKey:X_KEY] floatValue]; + if ([gui_info objectForKey:Y_KEY]) + pos.y = [[gui_info objectForKey:Y_KEY] floatValue]; + [comm_log_gui setDrawPosition:pos]; + NSSize siz = [comm_log_gui size]; + int rht = [comm_log_gui rowHeight]; + NSString* title = [comm_log_gui title]; + if ([gui_info objectForKey:WIDTH_KEY]) + siz.width = [[gui_info objectForKey:WIDTH_KEY] floatValue]; + if ([gui_info objectForKey:HEIGHT_KEY]) + siz.height = [[gui_info objectForKey:HEIGHT_KEY] floatValue]; + if ([gui_info objectForKey:ROW_HEIGHT_KEY]) + rht = [[gui_info objectForKey:ROW_HEIGHT_KEY] intValue]; + if ([gui_info objectForKey:TITLE_KEY]) + title = [NSString stringWithFormat:@"%@", [gui_info objectForKey:TITLE_KEY]]; + [comm_log_gui resizeTo:siz characterHeight:rht title:title]; + if ([gui_info objectForKey:ALPHA_KEY]) + [comm_log_gui setAlpha: [[gui_info objectForKey:ALPHA_KEY] floatValue]]; + else + [comm_log_gui setAlpha: 1.0]; + if ([gui_info objectForKey:BACKGROUND_RGBA_KEY]) + [comm_log_gui setBackgroundColor:[OOColor colorFromString:[gui_info stringForKey:BACKGROUND_RGBA_KEY]]]; + } + + +} + + +- (double) scanner_zoom +{ + return scanner_zoom; +} + + +- (void) setScannerZoom:(double) value +{ + scanner_zoom = value; +} + +- (GLfloat) overallAlpha +{ + return overallAlpha; +} + + +- (void) setOverallAlpha:(GLfloat) newAlphaValue +{ + overallAlpha = OOClamp_0_1_f(newAlphaValue); +} + + +- (BOOL) reticleTargetSensitive +{ + return reticleTargetSensitive; +} + + +- (void) setReticleTargetSensitive:(BOOL) newReticleTargetSensitiveValue +{ + reticleTargetSensitive = !!newReticleTargetSensitiveValue; // ensure YES or NO. +} + + +- (void) addLegend:(NSDictionary *) info +{ + NSString *imageName = nil; + OOTexture *texture = nil; + NSSize imageSize; + OpenGLSprite *legendSprite = nil; + NSMutableDictionary *legendDict = nil; + + imageName = [info stringForKey:IMAGE_KEY]; + if (imageName != nil) + { + texture = [OOTexture textureWithName:imageName inFolder:@"Images"]; + if (texture == nil) + { + OOLog(kOOLogFileNotFound, @"***** ERROR: HeadUpDisplay couldn't get an image texture name for %@", imageName); + return; + } + + imageSize = [texture dimensions]; + imageSize.width = [info floatForKey:WIDTH_KEY defaultValue:imageSize.width]; + imageSize.height = [info floatForKey:HEIGHT_KEY defaultValue:imageSize.height]; + + legendSprite = [[OpenGLSprite alloc] initWithTexture:texture size:imageSize]; + + legendDict = [info mutableCopy]; + [legendDict setObject:legendSprite forKey:SPRITE_KEY]; + [legendArray addObject:legendDict]; + [legendDict release]; + [legendSprite release]; + } + else if ([info stringForKey:TEXT_KEY] != nil) + { + [legendArray addObject:info]; + } +} + + +- (void) addDial:(NSDictionary *) info +{ + if ([info stringForKey:SELECTOR_KEY] != nil) + { + SEL _selector = NSSelectorFromString([info stringForKey:SELECTOR_KEY]); + if ([self respondsToSelector:_selector]) [dialArray addObject:info]; + } +} + + +- (void) renderHUD +{ + glLineWidth(_crosshairWidth * line_width); + [self drawCrosshairs]; + CheckOpenGLErrors(@"HeadUpDisplay after drawCrosshairs"); + + glLineWidth(line_width); + [self drawLegends]; + CheckOpenGLErrors(@"HeadUpDisplay after drawLegends"); + + [self drawDials]; + CheckOpenGLErrors(@"HeadUpDisplay after drawDials"); +} + + +- (void) drawLegends +{ + unsigned i; + + z1 = [[UNIVERSE gameView] display_z]; + for (i = 0; i < [legendArray count]; i++) + { + [self drawLegend:[legendArray dictionaryAtIndex:i]]; + } +} + + +// SLOW_CODE - HUD drawing is taking up a ridiculous 30%-40% of frame time. Much of this seems to be spent in string processing. String caching is needed. -- ahruman +- (void) drawDials +{ + unsigned i; + + z1 = [[UNIVERSE gameView] display_z]; + for (i = 0; i < [dialArray count]; i++) + { + [self drawHUDItem:[dialArray dictionaryAtIndex:i]]; + } +} + +- (void) drawCrosshairs +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + OOViewID viewID = [UNIVERSE viewDirection]; + OOWeaponType weapon = [player weaponForView:viewID]; + NSArray *points = nil; + + if (viewID == VIEW_CUSTOM || + overallAlpha == 0.0f || + !([player status] == STATUS_IN_FLIGHT || [player status] == STATUS_WITCHSPACE_COUNTDOWN) || + [UNIVERSE displayGUI] + ) + { + // Don't draw crosshairs + return; + } + + if (weapon != _lastWeaponType || overallAlpha != _lastOverallAlpha) + { + [_crosshairs release]; + _crosshairs = nil; + } + + if (_crosshairs == nil) + { + // Make new crosshairs object + points = [self crosshairDefinitionForWeaponType:weapon]; + + _crosshairs = [[OOCrosshairs alloc] initWithPoints:points + scale:_crosshairScale + color:_crosshairColor + overallAlpha:overallAlpha]; + + _lastWeaponType = weapon; + _lastOverallAlpha = overallAlpha; + } + + [_crosshairs render]; +} + + +- (NSArray *) crosshairDefinitionForWeaponType:(OOWeaponType)weapon +{ + NSString *weaponName = nil; + static NSDictionary *crosshairDefs = nil; + NSArray *result = nil; + + /* Search order: + (hud.plist).crosshairs.WEAPON_NAME + (hud.plist).crosshairs.OTHER + (crosshairs.plist).WEAPON_NAME + (crosshairs.plist).OTHER + */ + + weaponName = WeaponTypeToString(weapon); + result = [_crosshairOverrides arrayForKey:weaponName]; + if (result == nil) result = [_crosshairOverrides arrayForKey:@"OTHER"]; + if (result == nil) + { + if (crosshairDefs == nil) + { + crosshairDefs = [ResourceManager dictionaryFromFilesNamed:@"crosshairs.plist" + inFolder:@"Config" + andMerge:YES]; + [crosshairDefs retain]; + } + + result = [crosshairDefs arrayForKey:weaponName]; + if (result == nil) result = [crosshairDefs arrayForKey:@"OTHER"]; + } + + return result; +} + + +- (void) drawLegend:(NSDictionary *) info +{ + OpenGLSprite *legendSprite = nil; + NSString *legendText = nil; + float x, y; + NSSize size; + + x = [info floatForKey:X_KEY]; + y = [info floatForKey:Y_KEY]; + + legendSprite = [info objectForKey:SPRITE_KEY]; + if (legendSprite != nil) + { + float alpha = [info floatForKey:ALPHA_KEY] * overallAlpha; + [legendSprite blitCentredToX:x Y:y Z:z1 alpha:alpha]; + } + else + { + legendText = [info stringForKey:TEXT_KEY]; + if (legendText != nil) + { + size.width = [info floatForKey:WIDTH_KEY]; + size.height = [info floatForKey:HEIGHT_KEY]; + GLColorWithOverallAlpha(green_color, overallAlpha); + OODrawString(legendText, x, y, z1, size); + } + } +} + + +- (void) drawHUDItem:(NSDictionary *) info +{ + NSString *equipment = [info stringForKey:EQUIPMENT_REQUIRED_KEY]; + if (equipment != nil && ![[PlayerEntity sharedPlayer] hasEquipmentItem:equipment]) + return; + + if ([info stringForKey:SELECTOR_KEY] != nil) + { + SEL _selector = NSSelectorFromString([info stringForKey:SELECTOR_KEY]); + if ([self respondsToSelector:_selector]) + [self performSelector:_selector withObject:info]; + else + OOLog(@"hud.unknownSelector", @"DEBUG HeadUpDisplay does not respond to '%@'",[info objectForKey:SELECTOR_KEY]); + } + + CheckOpenGLErrors(@"HeadUpDisplay after drawHUDItem %@", info); +} + +//---------------------------------------------------------------------// + +static BOOL hostiles; +- (void) drawScanner:(NSDictionary *) info +{ + int x; + int y; + NSSize siz; + GLfloat scanner_color[4] = { 1.0, 0.0, 0.0, 1.0 }; + + x = [info intForKey:X_KEY defaultValue:SCANNER_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:SCANNER_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:SCANNER_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:SCANNER_HEIGHT]; + GetRGBAArrayFromInfo(info, scanner_color); + + scanner_color[3] *= overallAlpha; + float alpha = scanner_color[3]; + + double z_factor = siz.height / siz.width; // approx 1/4 + double y_factor = 1.0 - sqrt(z_factor); // approx 1/2 + + int i; + int scanner_cx = x; + int scanner_cy = y; + double mass_lock_range2 = 25600.0*25600.0; + + int scanner_scale = SCANNER_MAX_RANGE * 2.5 / siz.width; + + double max_zoomed_range2 = SCANNER_SCALE*SCANNER_SCALE*10000.0/(scanner_zoom*scanner_zoom); + + GLfloat max_zoomed_range = sqrtf(max_zoomed_range2); + + BOOL isHostile = NO; + BOOL foundHostiles = NO; + BOOL mass_locked = NO; + + Vector position, relativePosition; + OOMatrix rotMatrix; + int flash = ((int)([UNIVERSE getTime] * 4))&1; + + Universe *uni = UNIVERSE; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + if (player == nil) return; + + // use a non-mutable copy so this can't be changed under us. + int ent_count = uni->n_entities; + Entity **uni_entities = uni->sortedEntities; // grab the public sorted list + Entity *my_entities[ent_count]; + + for (i = 0; i < ent_count; i++) + { + my_entities[i] = [uni_entities[i] retain]; // retained + } + + Entity *drawthing = nil; + + GLfloat col[4] = { 1.0, 1.0, 1.0, overallAlpha }; // can be manipulated + + position = [player position]; + rotMatrix = [player rotationMatrix]; + + glColor4fv(scanner_color); + drawScannerGrid(x, y, z1, siz, [UNIVERSE viewDirection], line_width, scanner_zoom); + + GLfloat off_scope2 = (siz.width > siz.height) ? siz.width * siz.width : siz.height * siz.height; + + OOEntityStatus p_status = [player status]; + + if ((p_status == STATUS_IN_FLIGHT)||(p_status == STATUS_AUTOPILOT_ENGAGED)||(p_status == STATUS_LAUNCHING)||(p_status == STATUS_WITCHSPACE_COUNTDOWN)) + { + double upscale = scanner_zoom*1.25/scanner_scale; + off_scope2 /= upscale * upscale; + double max_blip = 0.0; + + for (i = 0; i < ent_count; i++) // scanner lollypops + { + drawthing = my_entities[i]; + + int drawClass = drawthing->scanClass; + if (drawClass == CLASS_PLAYER) drawClass = CLASS_NO_DRAW; + if (drawthing->isShip) + { + ShipEntity* ship = (ShipEntity*)drawthing; + if ([ship isCloaked]) drawClass = CLASS_NO_DRAW; + } + + // consider large bodies for mass_lock + if (drawthing->isPlanet) + { + PlanetEntity* planet = (PlanetEntity *)drawthing; + if ([planet planetType] != PLANET_TYPE_MINIATURE) + { + double dist = planet->zero_distance; + double rad = planet->collision_radius; + double factor = ([planet planetType] == PLANET_TYPE_SUN) ? 2.0 : 4.0; + // mass lock when 25 km or less from the surface - dist is a square distance so needs to be compared to (rad+25000) * (rad+25000)! + if (dist< rad*rad +50000*rad+625000000 || dist < rad*rad*factor) + { + mass_locked = YES; + } + } + } + + if (drawClass != CLASS_NO_DRAW) + { + GLfloat x1,y1,y2; + float ms_blip = 0.0; + + if (drawthing->zero_distance <= mass_lock_range2) + { + switch (drawClass) + { + case CLASS_BUOY : + case CLASS_ROCK : + case CLASS_CARGO : + case CLASS_MINE : + break; + case CLASS_THARGOID : + case CLASS_MISSILE : + case CLASS_STATION : + case CLASS_POLICE : + case CLASS_MILITARY : + case CLASS_WORMHOLE : + default : + mass_locked = YES; + break; + } + } + + [player setAlertFlag:ALERT_FLAG_MASS_LOCK to:mass_locked]; + + if (isnan(drawthing->zero_distance)) + continue; + + // exit if it's too far away + GLfloat act_dist = sqrtf(drawthing->zero_distance); + GLfloat lim_dist = act_dist - drawthing->collision_radius; + + if (lim_dist > max_zoomed_range) + continue; + + // has it sent a recent message + // + if (drawthing->isShip) + ms_blip = 2.0 * [(ShipEntity *)drawthing messageTime]; + if (ms_blip > max_blip) + { + max_blip = ms_blip; + last_transmitter = [drawthing universalID]; + } + ms_blip -= floor(ms_blip); + + relativePosition = drawthing->relativePosition; + Vector rp = relativePosition; + + if (act_dist > max_zoomed_range) + scale_vector(&relativePosition, max_zoomed_range / act_dist); + + // rotate the view + relativePosition = OOVectorMultiplyMatrix(relativePosition, rotMatrix); + // scale the view + scale_vector(&relativePosition, upscale); + + x1 = relativePosition.x; + y1 = z_factor * relativePosition.z; + y2 = y1 + y_factor * relativePosition.y; + + isHostile = NO; + if ([drawthing isShip]) + { + ShipEntity* ship = (ShipEntity *)drawthing; + double wr = [ship weaponRange]; + isHostile = (([ship hasHostileTarget])&&([ship primaryTarget] == player)&&(drawthing->zero_distance < wr*wr)); + GLfloat* base_col = [ship scannerDisplayColorForShip:player :isHostile :flash]; + col[0] = base_col[0]; col[1] = base_col[1]; col[2] = base_col[2]; col[3] = alpha * base_col[3]; + } + + if ([drawthing isWormhole]) + { + col[0] = blue_color[0]; col[1] = (flash)? 1.0 : blue_color[1]; col[2] = blue_color[2]; col[3] = alpha * blue_color[3]; + } + + // position the scanner + x1 += scanner_cx; y1 += scanner_cy; y2 += scanner_cy; + + switch (drawClass) + { + case CLASS_THARGOID : + foundHostiles = YES; + break; + case CLASS_ROCK : + case CLASS_CARGO : + case CLASS_MISSILE : + case CLASS_STATION : + case CLASS_BUOY : + case CLASS_POLICE : + case CLASS_MILITARY : + case CLASS_MINE : + case CLASS_WORMHOLE : + default : + if (isHostile) + foundHostiles = YES; + break; + } + + if ([drawthing isShip]) + { + ShipEntity* ship = (ShipEntity*)drawthing; + if (ship->collision_radius * upscale > 4.5) + { + Vector bounds[6]; + BoundingBox bb = ship->totalBoundingBox; + bounds[0] = ship->v_forward; scale_vector(&bounds[0], bb.max.z); + bounds[1] = ship->v_forward; scale_vector(&bounds[1], bb.min.z); + bounds[2] = ship->v_right; scale_vector(&bounds[2], bb.max.x); + bounds[3] = ship->v_right; scale_vector(&bounds[3], bb.min.x); + bounds[4] = ship->v_up; scale_vector(&bounds[4], bb.max.y); + bounds[5] = ship->v_up; scale_vector(&bounds[5], bb.min.y); + // rotate the view + int i; + for (i = 0; i < 6; i++) + { + bounds[i] = OOVectorMultiplyMatrix(vector_add(bounds[i], rp), rotMatrix); + scale_vector(&bounds[i], upscale); + bounds[i] = make_vector(bounds[i].x + scanner_cx, bounds[i].z * z_factor + bounds[i].y * y_factor + scanner_cy, z1 ); + } + // draw the diamond + // + glBegin(GL_QUADS); + glColor4f(col[0], col[1], col[2], 0.33333 * col[3]); + glVertex3f(bounds[0].x, bounds[0].y, bounds[0].z); glVertex3f(bounds[4].x, bounds[4].y, bounds[4].z); + glVertex3f(bounds[1].x, bounds[1].y, bounds[1].z); glVertex3f(bounds[5].x, bounds[5].y, bounds[5].z); + glVertex3f(bounds[2].x, bounds[2].y, bounds[2].z); glVertex3f(bounds[4].x, bounds[4].y, bounds[4].z); + glVertex3f(bounds[3].x, bounds[3].y, bounds[3].z); glVertex3f(bounds[5].x, bounds[5].y, bounds[5].z); + glVertex3f(bounds[2].x, bounds[2].y, bounds[2].z); glVertex3f(bounds[0].x, bounds[0].y, bounds[0].z); + glVertex3f(bounds[3].x, bounds[3].y, bounds[3].z); glVertex3f(bounds[1].x, bounds[1].y, bounds[1].z); + glEnd(); + } + } + + + if (ms_blip > 0.0) + { + DrawSpecialOval(x1 - 0.5, y2 + 1.5, z1, NSMakeSize(16.0 * (1.0 - ms_blip), 8.0 * (1.0 - ms_blip)), 30, col); + } + if ((drawthing->isParticle)&&(drawClass == CLASS_MINE)) + { + double r1 = 2.5 + drawthing->collision_radius * upscale; + double l2 = r1*r1 - relativePosition.y*relativePosition.y; + double r0 = (l2 > 0)? sqrt(l2): 0; + if (r0 > 0) + { + glColor4f(1.0, 0.5, 1.0, alpha); + GLDrawOval(x1 - 0.5, y1 + 1.5, z1, NSMakeSize(r0, r0 * siz.height / siz.width), 20); + } + glColor4f(0.5, 0.0, 1.0, 0.33333 * alpha); + GLDrawFilledOval(x1 - 0.5, y2 + 1.5, z1, NSMakeSize(r1, r1), 15); + } + else + { + glBegin(GL_QUADS); + glColor4fv(col); + glVertex3f(x1-3, y2, z1); glVertex3f(x1+2, y2, z1); glVertex3f(x1+2, y2+3, z1); glVertex3f(x1-3, y2+3, z1); + col[3] *= 0.3333; // one third the alpha + glColor4fv(col); + glVertex3f(x1, y1, z1); glVertex3f(x1+2, y1, z1); glVertex3f(x1+2, y2, z1); glVertex3f(x1, y2, z1); + glEnd(); + } + } + } + + [player setAlertFlag:ALERT_FLAG_HOSTILES to:foundHostiles]; + + if ((foundHostiles)&&(!hostiles)) + { + hostiles = YES; + } + if ((!foundHostiles)&&(hostiles)) + { + hostiles = NO; // there are now no hostiles on scope, relax + } + } + + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // released + +} + + +- (void) refreshLastTransmitter +{ + Entity* lt = [UNIVERSE entityForUniversalID:last_transmitter]; + if ((lt == nil)||(!(lt->isShip))) + return; + ShipEntity* st = (ShipEntity*)lt; + if ([st messageTime] <= 0.0) + [st setMessageTime:2.5]; +} + + +- (void) drawScannerZoomIndicator:(NSDictionary *) info +{ + int x; + int y; + NSSize siz; + GLfloat zoom_color[] = { 1.0f, 0.1f, 0.0f, 1.0f }; + + x = [info intForKey:X_KEY defaultValue:ZOOM_INDICATOR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:ZOOM_INDICATOR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:ZOOM_INDICATOR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:ZOOM_INDICATOR_HEIGHT]; + GetRGBAArrayFromInfo(info, zoom_color); + + GLfloat cx = x - 0.3 * siz.width; + GLfloat cy = y - 0.75 * siz.height; + + int zl = scanner_zoom; + if (zl < 1) zl = 1; + if (zl > SCANNER_ZOOM_LEVELS) zl = SCANNER_ZOOM_LEVELS; + if (zl == 1) zoom_color[3] *= 0.75; + GLColorWithOverallAlpha(zoom_color, overallAlpha); + glEnable(GL_TEXTURE_2D); + [sFontTexture apply]; + glBegin(GL_QUADS); + drawCharacterQuad(48 + zl, cx - 0.4 * siz.width, cy, z1, siz); + drawCharacterQuad(58, cx, cy, z1, siz); + drawCharacterQuad(49, cx + 0.3 * siz.width, cy, z1, siz); + glEnd(); + [OOTexture applyNone]; + glDisable(GL_TEXTURE_2D); +} + + +- (void) drawCompass:(NSDictionary *) info +{ + int x; + int y; + NSSize siz; + float alpha; + + x = [info intForKey:X_KEY defaultValue:COMPASS_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:COMPASS_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:COMPASS_HALF_SIZE]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:COMPASS_HALF_SIZE]; + alpha = [info nonNegativeFloatForKey:ALPHA_KEY defaultValue:1.0] * overallAlpha; + + // draw the compass + OOMatrix rotMatrix; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + Vector position = [player position]; + + rotMatrix = [player rotationMatrix]; + + GLfloat h1 = siz.height * 0.125; + GLfloat h3 = siz.height * 0.375; + GLfloat w1 = siz.width * 0.125; + GLfloat w3 = siz.width * 0.375; + glLineWidth(2.0 * line_width); // thicker + glColor4f(0.0f, 0.0f, 1.0f, alpha); + GLDrawOval(x, y, z1, siz, 12); + glColor4f(0.0f, 0.0f, 1.0f, 0.5f * alpha); + glBegin(GL_LINES); + glVertex3f(x - w1, y, z1); glVertex3f(x - w3, y, z1); + glVertex3f(x + w1, y, z1); glVertex3f(x + w3, y, z1); + glVertex3f(x, y - h1, z1); glVertex3f(x, y - h3, z1); + glVertex3f(x, y + h1, z1); glVertex3f(x, y + h3, z1); + glEnd(); + glLineWidth(line_width); // thinner + + PlanetEntity *the_sun = [UNIVERSE sun]; + PlanetEntity *the_planet = [UNIVERSE planet]; + StationEntity *the_station = [UNIVERSE station]; + Entity *the_target = [player primaryTarget]; + Entity *the_next_beacon = [UNIVERSE entityForUniversalID:[player nextBeaconID]]; + OOEntityStatus p_status = [player status]; + if (((p_status == STATUS_IN_FLIGHT) + ||(p_status == STATUS_AUTOPILOT_ENGAGED) + ||(p_status == STATUS_LAUNCHING) + ||(p_status == STATUS_WITCHSPACE_COUNTDOWN)) // be in the right mode + &&(the_sun) + &&(the_planet) // and be in a system + && ![the_sun goneNova]) // and the system has not been novabombed + { + Entity *reference = nil; + + switch ([player compassMode]) + { + case COMPASS_MODE_BASIC: + + if ([player checkForAegis] != AEGIS_NONE && the_station) + { + reference = the_station; + } + else + { + reference = the_planet; + } + break; + + case COMPASS_MODE_PLANET: + reference = the_planet; + break; + + case COMPASS_MODE_STATION: + reference = the_station; + break; + + case COMPASS_MODE_SUN: + reference = the_sun; + break; + + case COMPASS_MODE_TARGET: + reference = the_target; + break; + + case COMPASS_MODE_BEACONS: + reference = the_next_beacon; + break; + } + + if (reference == nil) + { + [player setCompassMode:COMPASS_MODE_PLANET]; + reference = the_planet; + } + + // translate and rotate the view + Vector relativePosition = vector_subtract([reference position], position); + relativePosition = OOVectorMultiplyMatrix(relativePosition, rotMatrix); + relativePosition = vector_normal_or_fallback(relativePosition, kBasisZVector); + + relativePosition.x *= siz.width * 0.4; + relativePosition.y *= siz.height * 0.4; + relativePosition.x += x; + relativePosition.y += y; + + NSSize sz = siz; + sz.width *= 0.2; + sz.height *= 0.2; + glLineWidth(2.0); + switch ([player compassMode]) + { + case COMPASS_MODE_BASIC: + [self drawCompassPlanetBlipAt:relativePosition Size:NSMakeSize(6, 6) Alpha:alpha]; + break; + + case COMPASS_MODE_PLANET: + [self drawCompassPlanetBlipAt:relativePosition Size:sz Alpha:alpha]; + break; + case COMPASS_MODE_STATION: + [self drawCompassStationBlipAt:relativePosition Size:sz Alpha:alpha]; + break; + case COMPASS_MODE_SUN: + [self drawCompassSunBlipAt:relativePosition Size:sz Alpha:alpha]; + break; + case COMPASS_MODE_TARGET: + [self drawCompassTargetBlipAt:relativePosition Size:sz Alpha:alpha]; + break; + case COMPASS_MODE_BEACONS: + [self drawCompassBeaconBlipAt:relativePosition Size:sz Alpha:alpha]; + NSArray *icon = [[UNIVERSE descriptions] arrayForKey:[(ShipEntity*)the_next_beacon primaryRole]]; + if (icon == nil) + OODrawString([NSString stringWithFormat:@"%c", [(ShipEntity*)the_next_beacon beaconChar]], + x - 2.5 * sz.width, y - 3.0 * sz.height, z1, NSMakeSize(sz.width * 2, sz.height * 2)); + else + { + glBegin(GL_POLYGON); + hudDrawSpecialIconAt(icon, + x - sz.width, y - 1.5 * sz.height, z1, NSMakeSize(sz.width, sz.height)); + glEnd(); + glColor4f(0.0, 0.0, 0.0, 0.5 * alpha); + glBegin(GL_LINE_LOOP); + hudDrawSpecialIconAt(icon, + x - sz.width, y - 1.5 * sz.height, z1, NSMakeSize(sz.width, sz.height)); + glEnd(); + } + break; + } + } +} + + +- (void) drawCompassPlanetBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha +{ + if (relativePosition.z >= 0) + { + glColor4f(0.0,1.0,0.0,0.75 * alpha); + GLDrawFilledOval(relativePosition.x, relativePosition.y, z1, siz, 30); + glColor4f(0.0,1.0,0.0,alpha); + GLDrawOval(relativePosition.x, relativePosition.y, z1, siz, 30); + } + else + { + glColor4f(1.0,0.0,0.0,alpha); + GLDrawOval(relativePosition.x, relativePosition.y, z1, siz, 30); + } +} + + +- (void) drawCompassStationBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha +{ + if (relativePosition.z >= 0) + { + glColor4f(0.0,1.0,0.0,alpha); + } + else + { + glColor4f(1.0,0.0,0.0,alpha); + } + glBegin(GL_LINE_LOOP); + glVertex3f(relativePosition.x - 0.5 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x + 0.5 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x + 0.5 * siz.width, relativePosition.y + 0.5 * siz.height, z1); + glVertex3f(relativePosition.x - 0.5 * siz.width, relativePosition.y + 0.5 * siz.height, z1); + glEnd(); +} + + +- (void) drawCompassSunBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha +{ + glColor4f(1.0,1.0,0.0,0.75 * alpha); + GLDrawFilledOval(relativePosition.x, relativePosition.y, z1, siz, 30); + if (relativePosition.z >= 0) + { + glColor4f(0.0,1.0,0.0,alpha); + GLDrawOval(relativePosition.x, relativePosition.y, z1, siz, 30); + } + else + { + glColor4f(1.0,0.0,0.0,alpha); + GLDrawOval(relativePosition.x, relativePosition.y, z1, siz, 30); + } +} + + +- (void) drawCompassTargetBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha +{ + if (relativePosition.z >= 0) + { + glColor4f(0.0,1.0,0.0,alpha); + } + else + { + glColor4f(1.0,0.0,0.0,alpha); + } + glBegin(GL_LINES); + glVertex3f(relativePosition.x - siz.width, relativePosition.y, z1); + glVertex3f(relativePosition.x + siz.width, relativePosition.y, z1); + glVertex3f(relativePosition.x, relativePosition.y - siz.height, z1); + glVertex3f(relativePosition.x, relativePosition.y + siz.height, z1); + glEnd(); + GLDrawOval(relativePosition.x, relativePosition.y, z1, siz, 30); +} + + +- (void) drawCompassWitchpointBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha +{ + if (relativePosition.z >= 0) + { + glColor4f(0.0,1.0,0.0,alpha); + } + else + { + glColor4f(1.0,0.0,0.0,alpha); + } + glBegin(GL_LINES); + + glVertex3f(relativePosition.x - 0.5 * siz.width, relativePosition.y + 0.5 * siz.height, z1); + glVertex3f(relativePosition.x - 0.25 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + + glVertex3f(relativePosition.x - 0.25 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x, relativePosition.y, z1); + + glVertex3f(relativePosition.x, relativePosition.y, z1); + glVertex3f(relativePosition.x + 0.25 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + + glVertex3f(relativePosition.x + 0.25 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x + 0.5 * siz.width, relativePosition.y + 0.5 * siz.height, z1); + glEnd(); +} + + +- (void) drawCompassBeaconBlipAt:(Vector) relativePosition Size:(NSSize) siz Alpha:(GLfloat) alpha +{ + if (relativePosition.z >= 0) + { + glColor4f(0.0,1.0,0.0,alpha); + } + else + { + glColor4f(1.0,0.0,0.0,alpha); + } + glBegin(GL_LINES); + + glVertex3f(relativePosition.x - 0.5 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x, relativePosition.y + 0.5 * siz.height, z1); + + glVertex3f(relativePosition.x + 0.5 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x, relativePosition.y + 0.5 * siz.height, z1); + + glVertex3f(relativePosition.x - 0.5 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + glVertex3f(relativePosition.x + 0.5 * siz.width, relativePosition.y - 0.5 * siz.height, z1); + + glEnd(); +} + + +- (void) drawAegis:(NSDictionary *) info +{ + if (([UNIVERSE viewDirection] == VIEW_GUI_DISPLAY)||([UNIVERSE sun] == nil)||([[PlayerEntity sharedPlayer] checkForAegis] != AEGIS_IN_DOCKING_RANGE)) + return; // don't draw + + int x; + int y; + NSSize siz; + GLfloat alpha = 0.5f; + + x = [info intForKey:X_KEY defaultValue:AEGIS_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:AEGIS_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:AEGIS_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:AEGIS_HEIGHT]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:AEGIS_HEIGHT]; + alpha *= [info nonNegativeFloatForKey:ALPHA_KEY defaultValue:1.0f] * overallAlpha; + + // draw the aegis indicator + // + GLfloat w = siz.width / 16.0; + GLfloat h = siz.height / 16.0; + + GLfloat strip[] = { -7,8, -6,5, 5,8, 3,5, 7,2, 4,2, 6,-1, 4,2, -4,-1, -6,2, -4,-1, -7,-1, -3,-4, -5,-7, 6,-4, 7,-7 }; + + glColor4f(0.0f, 1.0f, 0.0f, alpha); + glBegin(GL_QUAD_STRIP); + int i; + for (i = 0; i < 32; i += 2) + glVertex3f(x + w * strip[i], y - h * strip[i + 1], z1); + glEnd(); + +} + + +- (void) drawSpeedBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround; + + x = [info intForKey:X_KEY defaultValue:SPEED_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:SPEED_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:SPEED_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:SPEED_BAR_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:SPEED_BAR_DRAW_SURROUND]; + + double ds = [player dialSpeed]; + + if (draw_surround) + { + // draw speed surround + GLColorWithOverallAlpha(green_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + // draw speed bar + if (ds > .25) + GLColorWithOverallAlpha(yellow_color, overallAlpha); + if (ds > .80) + GLColorWithOverallAlpha(red_color, overallAlpha); + hudDrawBarAt(x, y, z1, siz, ds); + +} + + +- (void) drawRollBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround; + + x = [info intForKey:X_KEY defaultValue:ROLL_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:ROLL_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:ROLL_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:ROLL_BAR_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:ROLL_BAR_DRAW_SURROUND]; + + if (draw_surround) + { + // draw ROLL surround + GLColorWithOverallAlpha(green_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + // draw ROLL bar + GLColorWithOverallAlpha(yellow_color, overallAlpha); + hudDrawIndicatorAt(x, y, z1, siz, [player dialRoll]); +} + + +- (void) drawPitchBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround; + + x = [info intForKey:X_KEY defaultValue:PITCH_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:PITCH_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:PITCH_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:PITCH_BAR_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:PITCH_BAR_DRAW_SURROUND]; + + if (draw_surround) + { + // draw PITCH surround + GLColorWithOverallAlpha(green_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + // draw PITCH bar + GLColorWithOverallAlpha(yellow_color, overallAlpha); + hudDrawIndicatorAt(x, y, z1, siz, [player dialPitch]); +} + + +- (void) drawYawBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround; + + // YAW does not exist in strict mode + if ([UNIVERSE strict]) return; + + x = [info intForKey:X_KEY defaultValue:PITCH_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:PITCH_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:PITCH_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:PITCH_BAR_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:PITCH_BAR_DRAW_SURROUND]; + if (draw_surround) + { + // draw YAW surround + GLColorWithOverallAlpha(green_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + // draw YAW bar + GLColorWithOverallAlpha(yellow_color, overallAlpha); + hudDrawIndicatorAt(x, y, z1, siz, [player dialYaw]); +} + + +- (void) drawEnergyGauge:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround, labelled; + + x = [info intForKey:X_KEY defaultValue:ENERGY_GAUGE_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:ENERGY_GAUGE_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:ENERGY_GAUGE_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:ENERGY_GAUGE_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:ENERGY_GAUGE_DRAW_SURROUND]; + labelled = [info boolForKey:LABELLED_KEY defaultValue:YES]; + + int n_bars = [player dialMaxEnergy]/64.0; + n_bars = [info unsignedIntForKey:N_BARS_KEY defaultValue:n_bars]; + if (n_bars < 1) n_bars = 1; + if (n_bars > 8) labelled = NO; + + if (draw_surround) + { + // draw energy surround + GLColorWithOverallAlpha(yellow_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + + // draw energy banks + { + int qy = siz.height / n_bars; + NSSize dial_size = NSMakeSize(siz.width,qy - 2); + int cy = y - (n_bars - 1) * qy / 2; + double energy = [player dialEnergy]*n_bars; + [player setAlertFlag:ALERT_FLAG_ENERGY to:((energy < 1.0)&&([player status] == STATUS_IN_FLIGHT))]; + int i; + for (i = 0; i < n_bars; i++) + { + GLColorWithOverallAlpha(yellow_color, overallAlpha); + if (energy > 1.0) + hudDrawBarAt(x, cy, z1, dial_size, 1.0); + if ((energy > 0.0)&&(energy <= 1.0)) + hudDrawBarAt(x, cy, z1, dial_size, energy); + if (labelled) + { + GLColorWithOverallAlpha(green_color, overallAlpha); + OODrawString([NSString stringWithFormat:@"E%x",n_bars - i], x + 0.5 * dial_size.width + 2, cy - 0.5 * qy, z1, NSMakeSize(9, (qy < 18)? qy : 18 )); + } + energy -= 1.0; + cy += qy; + } + } + +} + + +- (void) drawForwardShieldBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround; + + x = [info intForKey:X_KEY defaultValue:FORWARD_SHIELD_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:FORWARD_SHIELD_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:FORWARD_SHIELD_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:FORWARD_SHIELD_BAR_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:FORWARD_SHIELD_BAR_DRAW_SURROUND]; + + double shield = [player dialForwardShield]; + if (draw_surround) + { + // draw forward_shield surround + GLColorWithOverallAlpha(green_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + // draw forward_shield bar + GLColorWithOverallAlpha(green_color, overallAlpha); + if (shield < .80) + GLColorWithOverallAlpha(yellow_color, overallAlpha); + if (shield < .25) + GLColorWithOverallAlpha(red_color, overallAlpha); + hudDrawBarAt(x, y, z1, siz, shield); +} + + +- (void) drawAftShieldBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL draw_surround; + + x = [info intForKey:X_KEY defaultValue:AFT_SHIELD_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:AFT_SHIELD_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:AFT_SHIELD_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:AFT_SHIELD_BAR_HEIGHT]; + draw_surround = [info boolForKey:DRAW_SURROUND_KEY defaultValue:AFT_SHIELD_BAR_DRAW_SURROUND]; + + double shield = [player dialAftShield]; + if (draw_surround) + { + // draw aft_shield surround + GLColorWithOverallAlpha(green_color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); + } + // draw aft_shield bar + GLColorWithOverallAlpha(green_color, overallAlpha); + if (shield < .80) + GLColorWithOverallAlpha(yellow_color, overallAlpha); + if (shield < .25) + GLColorWithOverallAlpha(red_color, overallAlpha); + hudDrawBarAt(x, y, z1, siz, shield); +} + + +- (void) drawFuelBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + float fu, hr; + + x = [info intForKey:X_KEY defaultValue:FUEL_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:FUEL_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:FUEL_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:FUEL_BAR_HEIGHT]; + + fu = [player dialFuel]; + hr = [player dialHyperRange]; + + // draw fuel bar + GLColorWithOverallAlpha(yellow_color, overallAlpha); + hudDrawBarAt(x, y, z1, siz, fu); + + // draw range indicator + if ((hr > 0)&&(hr <= 1.0)) + { + GLColorWithOverallAlpha((fu < hr) ? red_color : green_color, overallAlpha); + hudDrawMarkerAt(x, y, z1, siz, hr); + } +} + + +- (void) drawCabinTempBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + + x = [info intForKey:X_KEY defaultValue:CABIN_TEMP_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:CABIN_TEMP_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:CABIN_TEMP_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:CABIN_TEMP_BAR_HEIGHT]; + + double temp = [player hullHeatLevel]; + int flash = (int)([UNIVERSE getTime] * 4); + flash &= 1; + // draw ship_temperature bar + GLColorWithOverallAlpha(green_color, overallAlpha); + if (temp > .25) + GLColorWithOverallAlpha(yellow_color, overallAlpha); + if (temp > .80) + GLColorWithOverallAlpha(red_color, overallAlpha); + if ((flash)&&(temp > .90)) + GLColorWithOverallAlpha(redplus_color, overallAlpha); + [player setAlertFlag:ALERT_FLAG_TEMP to:((temp > .90)&&([player status] == STATUS_IN_FLIGHT))]; + hudDrawBarAt(x, y, z1, siz, temp); +} + + +- (void) drawWeaponTempBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + + x = [info intForKey:X_KEY defaultValue:WEAPON_TEMP_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:WEAPON_TEMP_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:WEAPON_TEMP_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:WEAPON_TEMP_BAR_HEIGHT]; + + double temp = [player laserHeatLevel]; + // draw weapon_temp bar + GLColorWithOverallAlpha(green_color, overallAlpha); + if (temp > .25) + GLColorWithOverallAlpha(yellow_color, overallAlpha); + if (temp > .80) + GLColorWithOverallAlpha(red_color, overallAlpha); + hudDrawBarAt(x, y, z1, siz, temp); +} + + +- (void) drawAltitudeBar:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + + x = [info intForKey:X_KEY defaultValue:ALTITUDE_BAR_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:ALTITUDE_BAR_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:ALTITUDE_BAR_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:ALTITUDE_BAR_HEIGHT]; + + GLfloat alt = [player dialAltitude]; + int flash = (int)([UNIVERSE getTime] * 4); + flash &= 1; + + // draw altitude bar + if ((flash)&&(alt < .10)) GLColorWithOverallAlpha(redplus_color, overallAlpha); + else if (alt < .25) GLColorWithOverallAlpha(red_color, overallAlpha); + else if (alt < .75) GLColorWithOverallAlpha(yellow_color, overallAlpha); + else GLColorWithOverallAlpha(green_color, overallAlpha); + + hudDrawBarAt(x, y, z1, siz, alt); + + [player setAlertFlag:ALERT_FLAG_ALT to:((alt < .10)&&([player status] == STATUS_IN_FLIGHT))]; +} + + +- (void) drawMissileDisplay:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + int sp; + + x = [info intForKey:X_KEY defaultValue:MISSILES_DISPLAY_X]; + y = [info intForKey:Y_KEY defaultValue:MISSILES_DISPLAY_Y]; + sp = [info unsignedIntForKey:SPACING_KEY defaultValue:MISSILES_DISPLAY_SPACING]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:MISSILE_ICON_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:MISSILE_ICON_HEIGHT]; + + if (![player dialIdentEngaged]) + { + unsigned n_mis = [player dialMaxMissiles]; + unsigned i; + for (i = 0; i < n_mis; i++) + { + if ([player missileForStation:i]) + { + // TODO: copy icon data into missile object instead of looking it up each time. Possibly make weapon stores a ShipEntity subclass? + NSString *miss_roles = [[player missileForStation:i] primaryRole]; + NSArray *miss_icon = [[UNIVERSE descriptions] arrayForKey:miss_roles]; + if (i == [player activeMissile]) + { + GLColorWithOverallAlpha(yellow_color, overallAlpha); + glBegin(GL_POLYGON); + if (miss_icon) + { + hudDrawSpecialIconAt(miss_icon, x + i * sp + 2, y + 1, z1, NSMakeSize(siz.width + 4, siz.height + 4)); + } + else + { + if ([miss_roles hasSuffix:@"MISSILE"]) + hudDrawMissileIconAt(x + i * sp + 2, y + 1, z1, NSMakeSize(siz.width + 4, siz.height + 4)); + if ([miss_roles hasSuffix:@"MINE"]) + hudDrawMineIconAt(x + i * sp + 2, y + 1, z1, NSMakeSize(siz.width + 4, siz.height + 4)); + } + glEnd(); + + // Draw black backing, so outline colour isn’t blended into missile colour. + GLColorWithOverallAlpha(black_color, overallAlpha); + glBegin(GL_POLYGON); + if (miss_icon) + { + hudDrawSpecialIconAt(miss_icon, x + i * sp, y, z1, siz); + } + else + { + if ([miss_roles hasSuffix:@"MISSILE"]) + hudDrawMissileIconAt(x + i * sp, y, z1, siz); + if ([miss_roles hasSuffix:@"MINE"]) + hudDrawMineIconAt(x + i * sp, y, z1, siz); + } + glEnd(); + + switch ([player dialMissileStatus]) + { + case MISSILE_STATUS_SAFE : + GLColorWithOverallAlpha(green_color, overallAlpha); break; + case MISSILE_STATUS_ARMED : + GLColorWithOverallAlpha(yellow_color, overallAlpha); break; + case MISSILE_STATUS_TARGET_LOCKED : + GLColorWithOverallAlpha(red_color, overallAlpha); break; + } + } + else + { + if ([[player missileForStation:i] primaryTarget]) + GLColorWithOverallAlpha(red_color, overallAlpha); + else + GLColorWithOverallAlpha(green_color, overallAlpha); + } + glBegin(GL_POLYGON); + if (miss_icon) + { + hudDrawSpecialIconAt(miss_icon, x + i * sp, y, z1, siz); + } + else + { + if ([miss_roles hasSuffix:@"MISSILE"]) + hudDrawMissileIconAt(x + i * sp, y, z1, siz); + if ([miss_roles hasSuffix:@"MINE"]) + hudDrawMineIconAt(x + i * sp, y, z1, siz); + } + glEnd(); + if (i != [player activeMissile]) + { + GLColorWithOverallAlpha(green_color, overallAlpha); + glBegin(GL_LINE_LOOP); + if (miss_icon) + { + hudDrawSpecialIconAt(miss_icon, x + i * sp, y, z1, siz); + } + else + { + if ([miss_roles hasSuffix:@"MISSILE"]) + hudDrawMissileIconAt(x + i * sp, y, z1, siz); + if ([miss_roles hasSuffix:@"MINE"]) + hudDrawMineIconAt(x + i * sp, y, z1, siz); + } + glEnd(); + } + } + else + { + GLColorWithOverallAlpha(lightgray_color, overallAlpha); + glBegin(GL_LINE_LOOP); + hudDrawMissileIconAt(x + i * sp, y, z1, siz); + glEnd(); + } + } + } + else + { + x -= siz.width; + y -= siz.height * 0.75; + siz.width *= 0.80; + sp *= 0.75; + switch ([player dialMissileStatus]) + { + case MISSILE_STATUS_SAFE : + GLColorWithOverallAlpha(green_color, overallAlpha); break; + case MISSILE_STATUS_ARMED : + GLColorWithOverallAlpha(yellow_color, overallAlpha); break; + case MISSILE_STATUS_TARGET_LOCKED : + GLColorWithOverallAlpha(red_color, overallAlpha); break; + } + glBegin(GL_QUADS); + glVertex3i(x , y, z1); + glVertex3i(x + siz.width, y, z1); + glVertex3i(x + siz.width, y + siz.height, z1); + glVertex3i(x , y + siz.height, z1); + glEnd(); + GLColorWithOverallAlpha(green_color, overallAlpha); + OODrawString([player dialTargetName], x + sp, y, z1, NSMakeSize(siz.width, siz.height)); + } + +} + + +- (void) drawTargetReticle:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + + if ([player primaryTargetID] != NO_TARGET) + { + hudDrawReticleOnTarget([player primaryTarget], player, z1, overallAlpha, reticleTargetSensitive); + [self drawDirectionCue:info]; + } +} + + +- (void) drawStatusLight:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + BOOL blueAlert = cloakIndicatorOnStatusLight && [player isCloaked]; + + x = [info intForKey:X_KEY defaultValue:STATUS_LIGHT_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:STATUS_LIGHT_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:STATUS_LIGHT_HEIGHT]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:STATUS_LIGHT_HEIGHT]; + + GLfloat status_color[4] = { 0.25, 0.25, 0.25, 1.0}; + int alertCondition = [player alertCondition]; + double flash_alpha = 0.333 * (2.0 + sin([UNIVERSE getTime] * 2.5 * alertCondition)); + + switch(alertCondition) + { + case ALERT_CONDITION_RED : + status_color[0] = red_color[0]; + status_color[1] = red_color[1]; + status_color[2] = blueAlert ? blue_color[2] : red_color[2]; + break; + case ALERT_CONDITION_GREEN : + status_color[0] = green_color[0]; + status_color[1] = green_color[1]; + status_color[2] = blueAlert ? blue_color[2] : green_color[2]; + break; + case ALERT_CONDITION_YELLOW : + status_color[0] = yellow_color[0]; + status_color[1] = yellow_color[1]; + status_color[2] = blueAlert ? blue_color[2] : yellow_color[2]; + break; + default : + case ALERT_CONDITION_DOCKED : + break; + } + status_color[3] = flash_alpha; + GLColorWithOverallAlpha(status_color, overallAlpha); + glBegin(GL_POLYGON); + hudDrawStatusIconAt(x, y, z1, siz); + glEnd(); + glColor4f(0.25, 0.25, 0.25, overallAlpha); + glBegin(GL_LINE_LOOP); + hudDrawStatusIconAt(x, y, z1, siz); + glEnd(); +} + + +- (void) drawDirectionCue:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + NSString *equipment = nil; + + // the direction cue is an advanced option + // so we need to check for its extra equipment flag first + equipment = [info stringForKey:EQUIPMENT_REQUIRED_KEY]; + if (equipment != nil && ![player hasEquipmentItem:equipment]) + return; + + if ([UNIVERSE displayGUI]) + return; + + GLfloat clear_color[4] = {0.0, 1.0, 0.0, 0.0}; + Entity *target = [player primaryTarget]; + if (!target) + return; + + // draw the direction cue + OOMatrix rotMatrix; + Vector position = [player position]; + + rotMatrix = [player rotationMatrix]; + + if ([UNIVERSE viewDirection] != VIEW_GUI_DISPLAY) + { + GLfloat siz1 = CROSSHAIR_SIZE * (1.0 - ONE_EIGHTH); + GLfloat siz0 = CROSSHAIR_SIZE * ONE_EIGHTH; + GLfloat siz2 = CROSSHAIR_SIZE * (1.0 + ONE_EIGHTH); + + // Transform the view + Vector rpn = vector_subtract([target position], position); + rpn = OOVectorMultiplyMatrix(rpn, rotMatrix); + + switch ([UNIVERSE viewDirection]) + { + case VIEW_AFT: + rpn.x = - rpn.x; + break; + case VIEW_PORT: + rpn.x = rpn.z; + break; + case VIEW_STARBOARD: + rpn.x = -rpn.z; + break; + case VIEW_CUSTOM: + rpn = OOVectorMultiplyMatrix(rpn, [player customViewMatrix]); + break; + + default: + break; + } + rpn.z = 0; // flatten vector + if (rpn.x||rpn.y) + { + rpn = vector_normal(rpn); + glBegin(GL_LINES); + glColor4fv(clear_color); + glVertex3f(rpn.x * siz1 - rpn.y * siz0, rpn.y * siz1 + rpn.x * siz0, z1); + GLColorWithOverallAlpha(green_color, overallAlpha); + glVertex3f(rpn.x * siz2, rpn.y * siz2, z1); + glColor4fv(clear_color); + glVertex3f(rpn.x * siz1 + rpn.y * siz0, rpn.y * siz1 - rpn.x * siz0, z1); + GLColorWithOverallAlpha(green_color, overallAlpha); + glVertex3f(rpn.x * siz2, rpn.y * siz2, z1); + glEnd(); + } + } +} + + +- (void) drawClock:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + + x = [info intForKey:X_KEY defaultValue:CLOCK_DISPLAY_X]; + y = [info intForKey:Y_KEY defaultValue:CLOCK_DISPLAY_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:CLOCK_DISPLAY_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:CLOCK_DISPLAY_HEIGHT]; + + GLColorWithOverallAlpha(green_color, overallAlpha); + OODrawString([player dial_clock], x, y, z1, siz); +} + + +- (void) drawFPSInfoCounter:(NSDictionary *) info +{ + if (![UNIVERSE displayFPS]) return; + + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + + x = [info intForKey:X_KEY defaultValue:FPSINFO_DISPLAY_X]; + y = [info intForKey:Y_KEY defaultValue:FPSINFO_DISPLAY_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:FPSINFO_DISPLAY_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:FPSINFO_DISPLAY_HEIGHT]; + + NSString* positionInfo = [UNIVERSE expressPosition:player->position inCoordinateSystem:@"pwm"]; + NSString* collDebugInfo = [NSString stringWithFormat:@"%@ - %@", [player dial_objinfo], [UNIVERSE collisionDescription]]; + NSString* timeAccelerationFactorInfo = [NSString stringWithFormat:@"TAF: x%.2f", [UNIVERSE timeAccelerationFactor]]; + + NSSize siz08 = NSMakeSize(0.8 * siz.width, 0.8 * siz.width); + + glColor4f(0.0, 1.0, 0.0, 1.0); + OODrawString([player dial_fpsinfo], x, y, z1, siz); + OODrawString(collDebugInfo, x, y - siz.height, z1, siz); + + OODrawString(positionInfo, x, y - 1.8 * siz.height, z1, siz08); + OODrawString(timeAccelerationFactorInfo, x, y - 3.2 * siz08.height, z1, siz08); +} + + +- (void) drawScoopStatus:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + int x; + int y; + NSSize siz; + GLfloat alpha; + + x = [info intForKey:X_KEY defaultValue:SCOOPSTATUS_CENTRE_X]; + y = [info intForKey:Y_KEY defaultValue:SCOOPSTATUS_CENTRE_Y]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:SCOOPSTATUS_WIDTH]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:SCOOPSTATUS_HEIGHT]; + alpha = [info nonNegativeFloatForKey:ALPHA_KEY defaultValue:0.75f]; + + GLfloat* s0_color = red_color; + GLfloat s1c[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + GLfloat s2c[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + GLfloat s3c[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + int scoop_status = [player dialFuelScoopStatus]; + double t = [UNIVERSE getTime]; + GLfloat a1 = alpha * 0.5f * (1.0f + sin(t * 8.0f)); + GLfloat a2 = alpha * 0.5f * (1.0f + sin(t * 8.0f - 1.0f)); + GLfloat a3 = alpha * 0.5f * (1.0f + sin(t * 8.0f - 2.0f)); + + switch (scoop_status) + { + case SCOOP_STATUS_NOT_INSTALLED : + return; // don't draw + case SCOOP_STATUS_FULL_HOLD : + s0_color = darkgreen_color; + alpha *= 0.75; + break; + case SCOOP_STATUS_ACTIVE : + case SCOOP_STATUS_OKAY : + s0_color = green_color; + break; + } + int i; + for (i = 0; i < 3; i++) + { + s1c[i] = s0_color[i]; + s2c[i] = s0_color[i]; + s3c[i] = s0_color[i]; + } + if (scoop_status == SCOOP_STATUS_FULL_HOLD) + { + s3c[0] = red_color[0]; + s3c[1] = red_color[1]; + s3c[2] = red_color[2]; + } + if (scoop_status == SCOOP_STATUS_ACTIVE) + { + s1c[3] = alpha * a1; + s2c[3] = alpha * a2; + s3c[3] = alpha * a3; + } + else + { + s1c[3] = alpha; + s2c[3] = alpha; + s3c[3] = alpha; + } + + GLfloat w1 = siz.width / 8.0; + GLfloat w2 = 2.0 * w1; +// GLfloat w3 = 3.0 * w1; + GLfloat w4 = 4.0 * w1; + GLfloat h1 = siz.height / 8.0; + GLfloat h2 = 2.0 * h1; + GLfloat h3 = 3.0 * h1; + GLfloat h4 = 4.0 * h1; + + glDisable(GL_TEXTURE_2D); + glBegin(GL_QUADS); + // section 1 + GLColorWithOverallAlpha(s1c, overallAlpha); + glVertex3f(x, y + h1, z1); glVertex3f(x - w2, y + h2, z1); glVertex3f(x, y + h3, z1); glVertex3f(x + w2, y + h2, z1); + // section 2 + GLColorWithOverallAlpha(s2c, overallAlpha); + glVertex3f(x, y - h1, z1); glVertex3f(x - w4, y + h1, z1); glVertex3f(x - w4, y + h2, z1); glVertex3f(x, y, z1); + glVertex3f(x, y - h1, z1); glVertex3f(x + w4, y + h1, z1); glVertex3f(x + w4, y + h2, z1); glVertex3f(x, y, z1); + // section 3 + GLColorWithOverallAlpha(s3c, overallAlpha); + glVertex3f(x, y - h4, z1); glVertex3f(x - w2, y - h2, z1); glVertex3f(x - w2, y - h1, z1); glVertex3f(x, y - h2, z1); + glVertex3f(x, y - h4, z1); glVertex3f(x + w2, y - h2, z1); glVertex3f(x + w2, y - h1, z1); glVertex3f(x, y - h2, z1); + glEnd(); + +} + + +- (void) drawSurround:(NSDictionary *)info color:(GLfloat[4])color +{ + OOInteger x; + OOInteger y; + NSSize siz; + + x = [info integerForKey:X_KEY defaultValue:NSNotFound]; + y = [info integerForKey:Y_KEY defaultValue:NSNotFound]; + siz.width = [info nonNegativeFloatForKey:WIDTH_KEY defaultValue:NAN]; + siz.height = [info nonNegativeFloatForKey:HEIGHT_KEY defaultValue:NAN]; + + if (x == NSNotFound || y == NSNotFound || isnan(siz.width) || isnan(siz.height)) return; + + // draw green surround + GLColorWithOverallAlpha(color, overallAlpha); + hudDrawSurroundAt(x, y, z1, siz); +} + + +- (void) drawGreenSurround:(NSDictionary *) info +{ + [self drawSurround:info color:green_color]; +} + + +- (void) drawYellowSurround:(NSDictionary *) info +{ + [self drawSurround:info color:yellow_color]; +} + + +- (void) drawTrumbles:(NSDictionary *) info +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + + OOTrumble** trumbles = [player trumbleArray]; + int i; + for (i = [player trumbleCount]; i > 0; i--) + { + OOTrumble* trum = trumbles[i - 1]; + [trum drawTrumble: z1]; + } +} + + +- (void) drawWatermarkString:(NSString *)watermarkString +{ + NSSize watermarkStringSize = OORectFromString(watermarkString, 0.0f, 0.0f, NSMakeSize(10, 10)).size; + + glColor4f(0.0, 1.0, 0.0, 1.0); + // position the watermark string on the top right hand corner of the game window and right-align it + OODrawString(watermarkString, MAIN_GUI_PIXEL_WIDTH / 2 - watermarkStringSize.width + 80, + MAIN_GUI_PIXEL_HEIGHT / 2 - watermarkStringSize.height, z1, NSMakeSize(10,10)); +} + +//---------------------------------------------------------------------// + +static void hudDrawIndicatorAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz, double amount) +{ + if (siz.width > siz.height) + { + GLfloat dial_oy = y - siz.height/2; + GLfloat position = x + amount * siz.width / 2; + glBegin(GL_QUADS); + glVertex3f(position, dial_oy, z); + glVertex3f(position+2, y, z); + glVertex3f(position, dial_oy+siz.height, z); + glVertex3f(position-2, y, z); + glEnd(); + } + else + { + GLfloat dial_ox = x - siz.width/2; + GLfloat position = y + amount * siz.height / 2; + glBegin(GL_QUADS); + glVertex3f(dial_ox, position, z); + glVertex3f(x, position+2, z); + glVertex3f(dial_ox + siz.width, position, z); + glVertex3f(x, position-2, z); + glEnd(); + } +} + + +static void hudDrawMarkerAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz, double amount) +{ + if (siz.width > siz.height) + { + GLfloat dial_oy = y - siz.height/2; + GLfloat position = x + amount * siz.width - siz.width/2; + glBegin(GL_QUADS); + glVertex3f(position+1, dial_oy+1, z); + glVertex3f(position+1, dial_oy+siz.height-1, z); + glVertex3f(position-1, dial_oy+siz.height-1, z); + glVertex3f(position-1, dial_oy+1, z); + glEnd(); + } + else + { + GLfloat dial_ox = x - siz.width/2; + GLfloat position = y + amount * siz.height - siz.height/2; + glBegin(GL_QUADS); + glVertex3f(dial_ox+1, position+1, z); + glVertex3f(dial_ox + siz.width-1, position+1, z); + glVertex3f(dial_ox + siz.width-1, position-1, z); + glVertex3f(dial_ox+1, position-1, z); + glEnd(); + } +} + + +static void hudDrawBarAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz, double amount) +{ + GLfloat dial_ox = x - siz.width/2; + GLfloat dial_oy = y - siz.height/2; + if (fabs(siz.width) > fabs(siz.height)) + { + GLfloat position = dial_ox + amount * siz.width; + + glBegin(GL_QUADS); + glVertex3f(dial_ox, dial_oy, z); + glVertex3f(position, dial_oy, z); + glVertex3f(position, dial_oy+siz.height, z); + glVertex3f(dial_ox, dial_oy+siz.height, z); + glEnd(); + } + else + { + GLfloat position = dial_oy + amount * siz.height; + + glBegin(GL_QUADS); + glVertex3f(dial_ox, dial_oy, z); + glVertex3f(dial_ox, position, z); + glVertex3f(dial_ox+siz.width, position, z); + glVertex3f(dial_ox+siz.width, dial_oy, z); + glEnd(); + } +} + + +static void hudDrawSurroundAt(GLfloat x, GLfloat y, GLfloat z, NSSize siz) +{ + GLfloat dial_ox = x - siz.width/2; + GLfloat dial_oy = y - siz.height/2; + + glBegin(GL_LINE_LOOP); + glVertex3f(dial_ox-2, dial_oy-2, z); + glVertex3f(dial_ox+siz.width+2, dial_oy-2, z); + glVertex3f(dial_ox+siz.width+2, dial_oy+siz.height+2, z); + glVertex3f(dial_ox-2, dial_oy+siz.height+2, z); + glEnd(); +} + + +static void hudDrawSpecialIconAt(NSArray* ptsArray, int x, int y, int z, NSSize siz) +{ + if (!ptsArray) + return; + int ox = x - siz.width / 2.0; + int oy = y - siz.height / 2.0; + int w = siz.width / 4.0; + int h = siz.height / 4.0; + int i = 0; + int npts = [ptsArray count] & 0xfffe; // make sure it's an even number + while (i < npts) + { + int x = [ptsArray intAtIndex:i++]; + int y = [ptsArray intAtIndex:i++]; + glVertex3i(ox + x * w, oy + y * h, z); + } +} + + +static void hudDrawMissileIconAt(int x, int y, int z, NSSize siz) +{ + int ox = x - siz.width / 2.0; + int oy = y - siz.height / 2.0; + int w = siz.width / 4.0; + int h = siz.height / 4.0; + + glVertex3i(ox, oy + 3 * h, z); + glVertex3i(ox + 2 * w, oy, z); + glVertex3i(ox + w, oy, z); + glVertex3i(ox + w, oy - 2 * h, z); + glVertex3i(ox - w, oy - 2 * h, z); + glVertex3i(ox - w, oy, z); + glVertex3i(ox - 2 * w, oy, z); +} + + +static void hudDrawMineIconAt(int x, int y, int z, NSSize siz) +{ + int ox = x - siz.width / 2.0; + int oy = y - siz.height / 2.0; + int w = siz.width / 4.0; + int h = siz.height / 4.0; + + glVertex3i(ox, oy + 2 * h, z); + glVertex3i(ox + w, oy + h, z); + glVertex3i(ox + w, oy - h, z); + glVertex3i(ox, oy - 2 * h, z); + glVertex3i(ox - w, oy - h, z); + glVertex3i(ox - w, oy + h, z); +} + + +static void hudDrawStatusIconAt(int x, int y, int z, NSSize siz) +{ + int ox = x - siz.width / 2.0; + int oy = y - siz.height / 2.0; + int w = siz.width / 4.0; + int h = siz.height / 4.0; + + glVertex3i(ox, oy + h, z); + glVertex3i(ox, oy + 3 * h, z); + glVertex3i(ox + w, oy + 4 * h, z); + glVertex3i(ox + 3 * w, oy + 4 * h, z); + glVertex3i(ox + 4 * w, oy + 3 * h, z); + glVertex3i(ox + 4 * w, oy + h, z); + glVertex3i(ox + 3 * w, oy, z); + glVertex3i(ox + w, oy, z); +} + + +static void hudDrawReticleOnTarget(Entity* target, PlayerEntity* player1, GLfloat z1, GLfloat overallAlpha, BOOL reticleTargetSensitive) +{ + ShipEntity *target_ship = nil; + NSString *legal_desc = nil; + if ((!target)||(!player1)) + return; + + if ([target isShip]) + target_ship = (ShipEntity*)target; + + if ([target_ship isCloaked]) return; + + switch ([target scanClass]) + { + case CLASS_NEUTRAL: + { + int target_legal = [target_ship legalStatus]; + int legal_i = 0; + if (target_legal > 0) + legal_i = (target_legal <= 50) ? 1 : 2; + legal_desc = [[[UNIVERSE descriptions] arrayForKey:@"legal_status"] stringAtIndex:legal_i]; + } + break; + + case CLASS_THARGOID: + legal_desc = DESC(@"legal-desc-alien"); + break; + + case CLASS_POLICE: + legal_desc = DESC(@"legal-desc-system-vessel"); + break; + + case CLASS_MILITARY: + legal_desc = DESC(@"legal-desc-military-vessel"); + break; + + default: + break; + } + + if ([player1 guiScreen] != GUI_SCREEN_MAIN) // don't draw on text screens + return; + + OOMatrix back_mat; + Quaternion back_q = [player1 orientation]; + back_q.w = -back_q.w; // invert + Vector v1 = vector_up_from_quaternion(back_q); + Vector p1; + + p1 = vector_subtract([target position], [player1 viewpointPosition]); + + double rdist = fast_magnitude(p1); + double rsize = [target collisionRadius]; + + if (rsize < rdist * ONE_SIXTYFOURTH) + rsize = rdist * ONE_SIXTYFOURTH; + + GLfloat rs0 = rsize; + GLfloat rs2 = rsize * 0.50; + + glPushMatrix(); + + // deal with view directions + Vector view_dir, view_up = kBasisYVector; + switch ([UNIVERSE viewDirection]) + { + default: + case VIEW_FORWARD : + view_dir.x = 0.0; view_dir.y = 0.0; view_dir.z = 1.0; + break; + case VIEW_AFT : + view_dir.x = 0.0; view_dir.y = 0.0; view_dir.z = -1.0; + quaternion_rotate_about_axis(&back_q, v1, M_PI); + break; + case VIEW_PORT : + view_dir.x = -1.0; view_dir.y = 0.0; view_dir.z = 0.0; + quaternion_rotate_about_axis(&back_q, v1, 0.5 * M_PI); + break; + case VIEW_STARBOARD : + view_dir.x = 1.0; view_dir.y = 0.0; view_dir.z = 0.0; + quaternion_rotate_about_axis(&back_q, v1, -0.5 * M_PI); + break; + case VIEW_CUSTOM : + view_dir = [player1 customViewForwardVector]; + view_up = [player1 customViewUpVector]; + back_q = quaternion_multiply([player1 customViewQuaternion], back_q); + break; + } + gluLookAt(view_dir.x, view_dir.y, view_dir.z, 0.0, 0.0, 0.0, view_up.x, view_up.y, view_up.z); + + back_mat = OOMatrixForQuaternionRotation(back_q); + + // rotate the view + GLMultOOMatrix([player1 rotationMatrix]); + // translate the view + glTranslatef(p1.x, p1.y, p1.z); + //rotate to face player1 + GLMultOOMatrix(back_mat); + // draw the reticle +#if WORMHOLE_SCANNER + // Draw reticle cyan for Wormholes + if ([target isWormhole] ) + { + GLColorWithOverallAlpha(cyan_color, overallAlpha); + } + else +#endif + { + // If reticle is target sensitive, draw target box in red when target passes through crosshairs. + if (reticleTargetSensitive && [UNIVERSE getFirstEntityTargetedByPlayer] == [player1 primaryTarget]) + { + GLColorWithOverallAlpha(red_color, overallAlpha); + } + else + { + GLColorWithOverallAlpha(green_color, overallAlpha); + } + } + glBegin(GL_LINES); + glVertex2f(rs0,rs2); glVertex2f(rs0,rs0); + glVertex2f(rs0,rs0); glVertex2f(rs2,rs0); + + glVertex2f(rs0,-rs2); glVertex2f(rs0,-rs0); + glVertex2f(rs0,-rs0); glVertex2f(rs2,-rs0); + + glVertex2f(-rs0,rs2); glVertex2f(-rs0,rs0); + glVertex2f(-rs0,rs0); glVertex2f(-rs2,rs0); + + glVertex2f(-rs0,-rs2); glVertex2f(-rs0,-rs0); + glVertex2f(-rs0,-rs0); glVertex2f(-rs2,-rs0); + + glEnd(); + + // add text for reticle here + float range = (sqrtf(target->zero_distance) - target->collision_radius) * 0.001f; + NSSize textsize = NSMakeSize(rdist * ONE_SIXTYFOURTH, rdist * ONE_SIXTYFOURTH); + float line_height = rdist * ONE_SIXTYFOURTH; + NSString* info = (legal_desc == nil)? [NSString stringWithFormat:@"%0.3f km", range] : [NSString stringWithFormat:@"%0.3f km (%@)", range, legal_desc]; + // no need to set color - tis green already! + OODrawString([player1 dialTargetName], rs0, 0.5 * rs2, 0, textsize); + OODrawString(info, rs0, 0.5 * rs2 - line_height, 0, textsize); + +#if WORMHOLE_SCANNER + if ([target isWormhole]) + { + // Note: No break statements in the following switch() since every case + // falls through to the next. Cases arranged in reverse order. + switch([(WormholeEntity *)target scanInfo]) + { + case WH_SCANINFO_SHIP: + // TOOD: Render anything on the HUD for this? + case WH_SCANINFO_DESTINATION: + // Rendered above in dialTargetName, so no need to do anything here + // unless we want a separate line Destination: XXX ? + case WH_SCANINFO_ARRIVAL_TIME: + { + NSString *wormholeETA = [NSString stringWithFormat:DESC(@"wormhole-ETA-@"), ClockToString([(WormholeEntity *)target arrivalTime], NO)]; + OODrawString(wormholeETA, rs0, 0.5 * rs2 - 3 * line_height, 0, textsize); + } + case WH_SCANINFO_COLLAPSE_TIME: + { + double timeForCollapsing = [(WormholeEntity *)target expiryTime] - [player1 clockTimeAdjusted]; + int minutesToCollapse = floor (timeForCollapsing / 60.0); + int secondsToCollapse = (int)timeForCollapsing % 60; + + NSString *wormholeExpiringIn = [NSString stringWithFormat:DESC(@"wormhole-collapsing-in-mm:ss"), minutesToCollapse, secondsToCollapse]; + OODrawString(wormholeExpiringIn, rs0, 0.5 * rs2 - 2 * line_height, 0, textsize); + } + case WH_SCANINFO_SCANNED: + case WH_SCANINFO_NONE: + break; + } + } +#endif + + glPopMatrix(); +} + + +static void InitTextEngine(void) +{ + NSDictionary *fontSpec = nil; + NSArray *widths = nil; + NSString *texName = nil; + unsigned i, count; + + fontSpec = [ResourceManager dictionaryFromFilesNamed:@"oolite-font.plist" + inFolder:@"Config" + andMerge:NO]; + + texName = [fontSpec stringForKey:@"texture" defaultValue:@"oolite-font.png"]; + sFontTexture = [OOTexture textureWithName:texName + inFolder:@"Textures" + options:kFontTextureOptions + anisotropy:0.0f + lodBias:-0.75f]; + [sFontTexture retain]; + + sEncodingCoverter = [[OOEncodingConverter alloc] initWithFontPList:fontSpec]; + widths = [fontSpec arrayForKey:@"widths"]; + count = [widths count]; + if (count > 256) count = 256; + for (i = 0; i != count; ++i) + { + sGlyphWidths[i] = [widths floatAtIndex:i] * 0.13; // 0.13 is an inherited magic number + } +} + + +static double drawCharacterQuad(uint8_t chr, double x, double y, double z, NSSize siz) +{ + GLfloat texture_x = ONE_SIXTEENTH * (chr & 0x0f); + GLfloat texture_y = ONE_SIXTEENTH * (chr >> 4); + if (chr > 32) y += ONE_EIGHTH * siz.height; // Adjust for baseline offset change in 1.71 (needed to keep accented characters in box) + + glTexCoord2f(texture_x, texture_y + ONE_SIXTEENTH); + glVertex3f(x, y, z); + glTexCoord2f(texture_x + ONE_SIXTEENTH, texture_y + ONE_SIXTEENTH); + glVertex3f(x + siz.width, y, z); + glTexCoord2f(texture_x + ONE_SIXTEENTH, texture_y); + glVertex3f(x + siz.width, y + siz.height, z); + glTexCoord2f(texture_x, texture_y); + glVertex3f(x, y + siz.height, z); + + return siz.width * sGlyphWidths[chr]; +} + + +NSRect OORectFromString(NSString *text, double x, double y, NSSize siz) +{ + unsigned i; + double w = 0; + NSData *data = nil; + const uint8_t *bytes = NULL; + unsigned length; + + data = [sEncodingCoverter convertString:text]; + bytes = [data bytes]; + length = [data length]; + + for (i = 0; i < length; i++) + { + w += siz.width * sGlyphWidths[bytes[i]]; + } + + return NSMakeRect(x, y, w, siz.height); +} + + +void OODrawString(NSString *text, double x, double y, double z, NSSize siz) +{ + unsigned i; + double cx = x; + unsigned length; + NSData *data = nil; + const uint8_t *bytes = NULL; + + glEnable(GL_TEXTURE_2D); + [sFontTexture apply]; + + data = [sEncodingCoverter convertString:text]; + length = [data length]; + bytes = [data bytes]; + + glBegin(GL_QUADS); + for (i = 0; i < length; i++) + { + cx += drawCharacterQuad(bytes[i], cx, y, z, siz); + } + glEnd(); + + [OOTexture applyNone]; + glDisable(GL_TEXTURE_2D); +} + + +void OODrawPlanetInfo(int gov, int eco, int tec, double x, double y, double z, NSSize siz) +{ + GLfloat govcol[] = { 0.5, 0.0, 0.7, + 0.7, 0.5, 0.3, + 0.0, 1.0, 0.3, + 1.0, 0.8, 0.1, + 1.0, 0.0, 0.0, + 0.1, 0.5, 1.0, + 0.7, 0.7, 0.7, + 0.7, 1.0, 1.0}; + + double cx = x; + int tl = tec + 1; + GLfloat ce1 = 1.0 - 0.125 * eco; + + glEnable(GL_TEXTURE_2D); + [sFontTexture apply]; + + glBegin(GL_QUADS); + + glColor4f(ce1, 1.0, 0.0, 1.0); + cx += drawCharacterQuad(23 - eco, cx, y, z, siz); // characters 16..23 are economy symbols + glColor3fv(&govcol[gov * 3]); + cx += drawCharacterQuad(gov, cx, y, z, siz) - 1.0; // charcters 0..7 are government symbols + glColor4f(0.5, 1.0, 1.0, 1.0); + if (tl > 9) + cx += drawCharacterQuad(49, cx, y - 2, z, siz) - 2.0; + cx += drawCharacterQuad(48 + (tl % 10), cx, y - 2, z, siz); + glEnd(); + + [OOTexture applyNone]; + glDisable(GL_TEXTURE_2D); + +} + + +static void drawScannerGrid(double x, double y, double z, NSSize siz, int v_dir, GLfloat thickness, double zoom) +{ + GLfloat w1, h1; + GLfloat ww = 0.5 * siz.width; + GLfloat hh = 0.5 * siz.height; + + GLfloat w2 = 0.250 * siz.width; + GLfloat h2 = 0.250 * siz.height; + + GLfloat km_scan = 0.001 * SCANNER_MAX_RANGE / zoom; // calculate kilometer divisions + GLfloat hdiv = 0.5 * siz.height / km_scan; + GLfloat wdiv = 0.25 * siz.width / km_scan; + + int i, ii; + + if (wdiv < 4.0) + { + wdiv *= 2.0; + ii = 5; + } + else + { + ii = 1; + } + + glLineWidth(2.0 * thickness); + + GLDrawOval(x, y, z, siz, 4); + + glLineWidth(thickness); + + glBegin(GL_LINES); + glVertex3f(x, y - hh, z); glVertex3f(x, y + hh, z); + glVertex3f(x - ww, y, z); glVertex3f(x + ww, y, z); + + for (i = ii; 2.0 * hdiv * i < siz.height; i += ii) + { + h1 = i * hdiv; + w1 = wdiv; + if (i % 5 == 0) + w1 = w1 * 2.5; + if (i % 10 == 0) + w1 = w1 * 2.0; + if (w1 > 3.5) // don't draw tiny marks + { + glVertex3f(x - w1, y + h1, z); glVertex3f(x + w1, y + h1, z); + glVertex3f(x - w1, y - h1, z); glVertex3f(x + w1, y - h1, z); + } + } + + switch (v_dir) + { + case VIEW_BREAK_PATTERN : + case VIEW_GUI_DISPLAY : + case VIEW_FORWARD : + case VIEW_NONE : + glVertex3f(x, y, z); glVertex3f(x - w2, y + hh, z); + glVertex3f(x, y, z); glVertex3f(x + w2, y + hh, z); + break; + case VIEW_AFT : + glVertex3f(x, y, z); glVertex3f(x - w2, y - hh, z); + glVertex3f(x, y, z); glVertex3f(x + w2, y - hh, z); + break; + case VIEW_PORT : + glVertex3f(x, y, z); glVertex3f(x - ww, y + h2, z); + glVertex3f(x, y, z); glVertex3f(x - ww, y - h2, z); + break; + case VIEW_STARBOARD : + glVertex3f(x, y, z); glVertex3f(x + ww, y + h2, z); + glVertex3f(x, y, z); glVertex3f(x + ww, y - h2, z); + break; + } + glEnd(); +} + + +static void DrawSpecialOval(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step, GLfloat* color4v) +{ + GLfloat ww = 0.5 * siz.width; + GLfloat hh = 0.5 * siz.height; + GLfloat theta; + GLfloat delta; + GLfloat s; + + delta = step * M_PI / 180.0f; + + glEnable(GL_LINE_SMOOTH); + glBegin(GL_LINE_LOOP); + for (theta = 0.0f; theta < (2.0f * M_PI); theta += delta) + { + s = sinf(theta); + glColor4f(color4v[0], color4v[1], color4v[2], fabsf(s * color4v[3])); + glVertex3f(x + ww * s, y + hh * cosf(theta), z); + } + glEnd(); +} + + +- (void) setLine_width:(GLfloat) value +{ + line_width = value; +} + + +- (GLfloat) line_width +{ + return line_width; +} + +@end + + +@implementation NSString (OODisplayEncoding) + +- (const char *) cStringUsingOoliteEncoding +{ + if (sEncodingCoverter == nil) InitTextEngine(); + + // Note: the data will be autoreleased, so the bytes behave as though they're autoreleased too. + return [[self dataUsingEncoding:[sEncodingCoverter encoding] allowLossyConversion:YES] bytes]; +} + + +- (const char *) cStringUsingOoliteEncodingAndRemapping +{ + if (sEncodingCoverter == nil) InitTextEngine(); + + // Note: the data will be autoreleased, so the bytes behave as though they're autoreleased too. + return [[sEncodingCoverter convertString:self] bytes]; +} + +@end + + +static void GetRGBAArrayFromInfo(NSDictionary *info, GLfloat ioColor[4]) +{ + id colorDesc = nil; + OOColor *color = nil; + + // First, look for general colour specifier. + colorDesc = [info objectForKey:RGB_COLOR_KEY]; + if (colorDesc != nil) + { + color = [OOColor colorWithDescription:colorDesc]; + if (color != nil) + { + [color getGLRed:&ioColor[0] green:&ioColor[1] blue:&ioColor[2] alpha:&ioColor[3]]; + return; + } + } + + // Failing that, look for rgb_color and alpha. + colorDesc = [info arrayForKey:RGB_COLOR_KEY]; + if (colorDesc != nil && [colorDesc count] == 3) + { + ioColor[0] = [colorDesc nonNegativeFloatAtIndex:0]; + ioColor[1] = [colorDesc nonNegativeFloatAtIndex:1]; + ioColor[2] = [colorDesc nonNegativeFloatAtIndex:2]; + } + ioColor[3] = [info nonNegativeFloatForKey:ALPHA_KEY defaultValue:ioColor[3]]; +} diff --git a/src/Core/Materials/OOBasicMaterial.h b/src/Core/Materials/OOBasicMaterial.h new file mode 100644 index 00000000..78e2ed53 --- /dev/null +++ b/src/Core/Materials/OOBasicMaterial.h @@ -0,0 +1,121 @@ +/* + +OOBasicMaterial.h + +Material using basic OpenGL properties. Normal materials +(OOSingleTextureMaterial, OOShaderMaterial) are subclasses of this. It may be +desireable to have a material which does not use normal GL material +properties, in which case it should be based on OOMaterial directly. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOMaterial.h" +#import "OOColor.h" + + +@interface OOBasicMaterial: OOMaterial +{ + NSString *materialName; + + // Colours + GLfloat diffuse[4], + specular[4], + ambient[4], + emission[4]; + + // Specular exponent + uint8_t shininess; // Default: 0.0 +} + +/* Initialize with default values (historical Olite defaults, not GL defaults): + diffuse { 1.0, 1.0, 1.0, 1.0 } + specular { 0.0, 0.0, 0.0, 1.0 } + ambient { 1.0, 1.0, 1.0, 1.0 } + emission { 0.0, 0.0, 0.0, 1.0 } + shininess 0 +*/ +- (id)initWithName:(NSString *)name; + +/* Initialize with dictionary. Accepted keys: + diffuse colour description + specular colour description + ambient colour description + emission colour description + shininess integer + + "Colour description" refers to anything +[OOColor colorWithDescription:] + will accept. +*/ +- (id)initWithName:(NSString *)name configuration:(NSDictionary *)configuration; + +- (OOColor *)diffuseColor; +- (void)setDiffuseColor:(OOColor *)color; +- (void)setAmbientAndDiffuseColor:(OOColor *)color; +- (OOColor *)specularColor; +- (void)setSpecularColor:(OOColor *)color; +- (OOColor *)ambientColor; +- (void)setAmbientColor:(OOColor *)color; +- (OOColor *)emmisionColor; +- (void)setEmissionColor:(OOColor *)color; + +- (void)getDiffuseComponents:(GLfloat[4])outComponents; +- (void)setDiffuseComponents:(const GLfloat[4])components; +- (void)setAmbientAndDiffuseComponents:(const GLfloat[4])components; +- (void)getSpecularComponents:(GLfloat[4])outComponents; +- (void)setSpecularComponents:(const GLfloat[4])components; +- (void)getAmbientComponents:(GLfloat[4])outComponents; +- (void)setAmbientComponents:(const GLfloat[4])components; +- (void)getEmissionComponents:(GLfloat[4])outComponents; +- (void)setEmissionComponents:(const GLfloat[4])components; + +- (void)setDiffuseRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a; +- (void)setAmbientAndDiffuseRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a; +- (void)setSpecularRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a; +- (void)setAmbientRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a; +- (void)setEmissionRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a; + +- (uint8_t)shininess; +- (void)setShininess:(uint8_t)value; // Clamped to [0, 128] + +@end diff --git a/src/Core/Materials/OOBasicMaterial.m b/src/Core/Materials/OOBasicMaterial.m new file mode 100644 index 00000000..ec8274e7 --- /dev/null +++ b/src/Core/Materials/OOBasicMaterial.m @@ -0,0 +1,353 @@ +/* + +OOBasicMaterial.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOBasicMaterial.h" +#import "OOCollectionExtractors.h" +#import "OOFunctionAttributes.h" +#import "Universe.h" + + +static OOBasicMaterial *sDefaultMaterial = nil; + + +#define FACE GL_FRONT_AND_BACK + + +@implementation OOBasicMaterial + +- (id)initWithName:(NSString *)name +{ + self = [super init]; + if (EXPECT_NOT(self == nil)) return nil; + + materialName = [name copy]; + + [self setDiffuseRed:1.0f green:1.0f blue:1.0f alpha:1.0f]; + [self setAmbientRed:1.0f green:1.0f blue:1.0f alpha:1.0f]; + specular[3] = 1.0; + emission[3] = 1.0; + + return self; +} + + +- (id)initWithName:(NSString *)name configuration:(NSDictionary *)configuration +{ + id colorDesc = nil; + int shininessVal; + + self = [self initWithName:name]; + if (EXPECT_NOT(self == nil)) return nil; + + if (configuration == nil) configuration = [NSDictionary dictionary]; + + colorDesc = [configuration objectForKey:@"diffuse"]; + if (colorDesc != nil) [self setDiffuseColor:[OOColor colorWithDescription:colorDesc]]; + + colorDesc = [configuration objectForKey:@"ambient"]; + if (colorDesc != nil) [self setAmbientColor:[OOColor colorWithDescription:colorDesc]]; + else [self setAmbientColor:[self diffuseColor]]; + + colorDesc = [configuration objectForKey:@"emission"]; + if (colorDesc != nil) [self setEmissionColor:[OOColor colorWithDescription:colorDesc]]; + + shininessVal = [configuration intForKey:@"shininess" defaultValue:-1]; + if (shininessVal != 0 && ![UNIVERSE reducedDetail]) + { + colorDesc = [configuration objectForKey:@"specular"]; + if (shininessVal < 0) + { + shininessVal = 10; + if (colorDesc == nil) colorDesc = @"0.2 0.2 0.2 1.0"; + } + [self setShininess:shininessVal]; + if (colorDesc != nil) [self setSpecularColor:[OOColor colorWithDescription:colorDesc]]; + } + + return self; +} + + +- (void)dealloc +{ + [super willDealloc]; + [materialName release]; + + [super dealloc]; +} + + +- (NSString *)name +{ + return materialName; +} + + +- (BOOL)doApply +{ + glMaterialfv(FACE, GL_DIFFUSE, diffuse); + glMaterialfv(FACE, GL_SPECULAR, specular); + glMaterialfv(FACE, GL_AMBIENT, ambient); + glMaterialfv(FACE, GL_EMISSION, emission); + glMateriali(FACE, GL_SHININESS, shininess); + + return YES; +} + + +- (void)unapplyWithNext:(OOMaterial *)next +{ + if (![next isKindOfClass:[OOBasicMaterial class]]) + { + if (EXPECT_NOT(sDefaultMaterial == nil)) sDefaultMaterial = [[OOBasicMaterial alloc] initWithName:@""]; + [sDefaultMaterial doApply]; + } +} + + +- (OOColor *)diffuseColor +{ + return [OOColor colorWithCalibratedRed:diffuse[0] + green:diffuse[1] + blue:diffuse[2] + alpha:diffuse[3]]; +} + + +- (void)setDiffuseColor:(OOColor *)color +{ + if (color != nil) + { + [self setDiffuseRed:[color redComponent] + green:[color greenComponent] + blue:[color blueComponent] + alpha:[color alphaComponent]]; + } +} + + +- (void)setAmbientAndDiffuseColor:(OOColor *)color +{ + [self setAmbientColor:color]; + [self setDiffuseColor:color]; +} + + +- (OOColor *)specularColor +{ + return [OOColor colorWithCalibratedRed:specular[0] + green:specular[1] + blue:specular[2] + alpha:specular[3]]; +} + + +- (void)setSpecularColor:(OOColor *)color +{ + if (color != nil) + { + [self setSpecularRed:[color redComponent] + green:[color greenComponent] + blue:[color blueComponent] + alpha:[color alphaComponent]]; + } +} + + +- (OOColor *)ambientColor +{ + return [OOColor colorWithCalibratedRed:ambient[0] + green:ambient[1] + blue:ambient[2] + alpha:ambient[3]]; +} + + +- (void)setAmbientColor:(OOColor *)color +{ + if (color != nil) + { + [self setAmbientRed:[color redComponent] + green:[color greenComponent] + blue:[color blueComponent] + alpha:[color alphaComponent]]; + } +} + + +- (OOColor *)emmisionColor +{ + return [OOColor colorWithCalibratedRed:emission[0] + green:emission[1] + blue:emission[2] + alpha:emission[3]]; +} + + +- (void)setEmissionColor:(OOColor *)color +{ + if (color != nil) + { + [self setEmissionRed:[color redComponent] + green:[color greenComponent] + blue:[color blueComponent] + alpha:[color alphaComponent]]; + } +} + + +- (void)getDiffuseComponents:(GLfloat[4])outComponents +{ + memcpy(outComponents, diffuse, 4 * sizeof *outComponents); +} + + +- (void)setDiffuseComponents:(const GLfloat[4])components +{ + memcpy(diffuse, components, 4 * sizeof *components); +} + + +- (void)setAmbientAndDiffuseComponents:(const GLfloat[4])components +{ + [self setAmbientComponents:components]; + [self setDiffuseComponents:components]; +} + + +- (void)getSpecularComponents:(GLfloat[4])outComponents +{ + memcpy(outComponents, specular, 4 * sizeof *outComponents); +} + + +- (void)setSpecularComponents:(const GLfloat[4])components +{ + memcpy(specular, components, 4 * sizeof *components); +} + + +- (void)getAmbientComponents:(GLfloat[4])outComponents +{ + memcpy(outComponents, ambient, 4 * sizeof *outComponents); +} + + +- (void)setAmbientComponents:(const GLfloat[4])components +{ + memcpy(ambient, components, 4 * sizeof *components); +} + + +- (void)getEmissionComponents:(GLfloat[4])outComponents +{ + memcpy(outComponents, emission, 4 * sizeof *outComponents); +} + + +- (void)setEmissionComponents:(const GLfloat[4])components +{ + memcpy(emission, components, 4 * sizeof *components); +} + + +- (void)setDiffuseRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a +{ + diffuse[0] = r; + diffuse[1] = g; + diffuse[2] = b; + diffuse[3] = a; +} + + +- (void)setAmbientAndDiffuseRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a +{ + [self setAmbientRed:r green:g blue:b alpha:a]; + [self setDiffuseRed:r green:g blue:b alpha:a]; +} + + +- (void)setSpecularRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a +{ + specular[0] = r; + specular[1] = g; + specular[2] = b; + specular[3] = a; +} + + +- (void)setAmbientRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a +{ + ambient[0] = r; + ambient[1] = g; + ambient[2] = b; + ambient[3] = a; +} + + +- (void)setEmissionRed:(GLfloat)r green:(GLfloat)g blue:(GLfloat)b alpha:(GLfloat)a +{ + emission[0] = r; + emission[1] = g; + emission[2] = b; + emission[3] = a; +} + + + +- (uint8_t)shininess +{ + return shininess; +} + + +- (void)setShininess:(uint8_t)value +{ + shininess = MIN(value, 128); +} + +@end diff --git a/src/Core/Materials/OOMaterial.h b/src/Core/Materials/OOMaterial.h new file mode 100644 index 00000000..ee022919 --- /dev/null +++ b/src/Core/Materials/OOMaterial.h @@ -0,0 +1,131 @@ +/* + +OOMaterial.h + +A material which can be applied to an OpenGL object, or more accurately, to +the current OpenGL render state. + +This is an abstract class; actual materials should be subclasses. + +Currently, only shader materials are supported. Direct use of textures should +also be replaced with an OOMaterial subclass. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOOpenGL.h" +#import "OOWeakReference.h" + + +@interface OOMaterial: NSObject + +// Called once at startup (by -[Universe init]). ++ (void)setUp; + + +- (NSString *)name; + +// Make this the current material. +- (void)apply; + +/* Make no material the current material, tearing down anything set up by the + current material. +*/ ++ (void)applyNone; + +/* Get current material. +*/ ++ (OOMaterial *)current; + +/* Ensure material is ready to be used in a display list. This is not + required before using a material directly. +*/ +- (void)ensureFinishedLoading; + +// Only used by shader material, but defined for all materials for convenience. +- (void)setBindingTarget:(id)target; + +@end + + +@interface OOMaterial (OOConvenienceCreators) + +/* Get a material based on configuration. The result will be an + OOBasicMaterial, OOSingleTextureMaterial or OOShaderMaterial (the latter + only if shaders are available). modelName is used for caching of synthesized + shader materials; nil may be passed for no caching. +*/ ++ (id)materialWithName:(NSString *)name + forModelNamed:(NSString *)modelName + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)object + forSmoothedMesh:(BOOL)smooth; + +/* Select an appropriate material description (based on availability of + shaders and content of dictionaries, which may be nil) and call + +materialWithName:forModelNamed:configuration:macros:bindTarget:forSmoothedMesh:. +*/ ++ (id)materialWithName:(NSString *)name + forModelNamed:(NSString *)modelName + materialDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + macros:(NSDictionary *)macros + bindingTarget:(id)object + forSmoothedMesh:(BOOL)smooth; + +@end + + +@interface OOMaterial (OOSubclassInterface) + +// Subclass responsibilities - don't call directly. +- (BOOL)doApply; // Override instead of -apply +- (void)unapplyWithNext:(OOMaterial *)next; + +// Call at top of dealloc +- (void)willDealloc; + +@end diff --git a/src/Core/Materials/OOMaterial.m b/src/Core/Materials/OOMaterial.m new file mode 100644 index 00000000..72507f20 --- /dev/null +++ b/src/Core/Materials/OOMaterial.m @@ -0,0 +1,468 @@ +/* + +OOMaterial.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOMaterial.h" +#import "OOFunctionAttributes.h" +#import "OOLogging.h" + +#import "OOOpenGLExtensionManager.h" +#import "OOShaderMaterial.h" +#import "OOSingleTextureMaterial.h" +#import "OOCollectionExtractors.h" +#import "Universe.h" +#import "OOCacheManager.h" + + +static OOMaterial *sActiveMaterial = nil; + + +@implementation OOMaterial + ++ (void)setUp +{ + // I thought we'd need this, but the stuff I needed it for turned out to be problematic. Maybe in future. -- Ahruman +} + + +- (void)dealloc +{ + // Ensure cleanup happens; doing it more than once is safe. + [self willDealloc]; + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{%@}", [self className], self, [self name]]; +} + + +- (NSString *)name +{ + OOLogGenericParameterError(); + return nil; +} + + +// Make this the current GL shader program. +- (void)apply +{ + [sActiveMaterial unapplyWithNext:self]; + [sActiveMaterial release]; + sActiveMaterial = nil; + + if ([self doApply]) + { + sActiveMaterial = [self retain]; + } +} + + ++ (void)applyNone +{ + [sActiveMaterial unapplyWithNext:nil]; + [sActiveMaterial release]; + sActiveMaterial = nil; +} + + ++ (OOMaterial *)current +{ + return [[sActiveMaterial retain] autorelease]; +} + + +- (void)ensureFinishedLoading +{ + +} + + +- (void)setBindingTarget:(id)target +{ + +} + +@end + + +static void SetUniform(NSMutableDictionary *uniforms, NSString *key, NSString *type, id value) +{ + [uniforms setObject:[NSDictionary dictionaryWithObjectsAndKeys:type, @"type", value, @"value", nil] forKey:key]; +} + + +static void SetUniformFloat(NSMutableDictionary *uniforms, NSString *key, float value) +{ + SetUniform(uniforms, key, @"float", [NSNumber numberWithFloat:value]); +} + + +static void AddTexture(NSMutableDictionary *uniforms, NSMutableArray *textures, NSString *key, NSString *fileName) +{ + SetUniform(uniforms, key, @"texture", [NSNumber numberWithInt:[textures count]]); + [textures addObject:fileName]; +} + + +@implementation OOMaterial (OOConvenienceCreators) + ++ (NSDictionary *)synthesizeMaterialDictionaryWithName:(NSString *)name + forModelNamed:(NSString *)modelName + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros +{ + OOColor *ambient = nil, + *diffuse = nil, + *specular = nil, + *emission = nil; + int shininess = 0; + NSString *diffuseMap = nil, + *specularMap = nil, + *emissionMap = nil, + *emissionAndIlluminationMap = nil, + *illuminationMap = nil, + *normalMap = nil, + *normalAndParallaxMap = nil; + float parallaxScale, + parallaxBias; + NSMutableDictionary *modifiedMacros = nil; + NSMutableArray *textures = nil; + NSMutableDictionary *newConfig = nil; + NSMutableDictionary *uniforms = nil; + NSNumber *one = [NSNumber numberWithInt:1]; + + if (configuration == nil) configuration = [NSDictionary dictionary]; // If it's nil, lookups will always give 0/nil results regardless of defaultValue:. + ambient = [OOColor colorWithDescription:[configuration objectForKey:@"ambient"]]; + diffuse = [OOColor colorWithDescription:[configuration objectForKey:@"diffuse"]]; + specular = [OOColor colorWithDescription:[configuration objectForKey:@"specular"]]; + emission = [OOColor colorWithDescription:[configuration objectForKey:@"emission"]]; + shininess = [configuration intForKey:@"shininess" defaultValue:-1]; + diffuseMap = [configuration stringForKey:@"diffuse_map"]; + specularMap = [configuration stringForKey:@"specular_map"]; + emissionMap = [configuration stringForKey:@"emission_map"]; + emissionAndIlluminationMap = [configuration stringForKey:@"emission_and_illumination_map"]; + illuminationMap = [configuration stringForKey:@"illumination_map"]; + normalMap = [configuration stringForKey:@"normal_map"]; + normalAndParallaxMap = [configuration stringForKey:@"normal_and_parallax_map"]; + parallaxScale = [configuration floatForKey:@"parallax_scale" defaultValue:0.01]; + parallaxBias = [configuration floatForKey:@"parallax_bias" defaultValue:0.00]; + + if (diffuseMap == nil) diffuseMap = name; + + if (diffuse == nil) diffuse = [OOColor whiteColor]; + if (emissionAndIlluminationMap != nil && illuminationMap != nil) + { + // Can't have both emissionAndIlluminationMap and illuminationMap + if (emissionMap == nil) emissionMap = emissionAndIlluminationMap; + emissionAndIlluminationMap = nil; + } + + // If there's a parallax map, it's always part of the one and only normal map + if (normalAndParallaxMap != nil) normalMap = normalAndParallaxMap; + + // Shininess 0 or nil/black specular colour means no specular. + if (shininess == 0 || [specular isBlack]) + { + specular = nil; + } + + // No specular means no specular map. + if (specular == nil) specularMap = nil; + + if ([emission isBlack]) emission = nil; + + modifiedMacros = macros ? [[macros mutableCopy] autorelease] : [NSMutableDictionary dictionaryWithCapacity:8]; + + // Create a synthetic configuration dictionary. + textures = [NSMutableArray arrayWithCapacity:4]; + newConfig = [NSMutableDictionary dictionaryWithCapacity:16]; + uniforms = [NSMutableDictionary dictionaryWithCapacity:6]; + + [newConfig setObject:[NSNumber numberWithBool:YES] forKey:@"_oo_is_synthesized_config"]; + [newConfig setObject:@"oolite-tangent-space-vertex.vertex" forKey:@"vertex_shader"]; + [newConfig setObject:@"oolite-default-shader.fragment" forKey:@"fragment_shader"]; + + if (ambient != nil) [newConfig setObject:[ambient normalizedArray] forKey:@"ambient"]; + if (diffuse != nil) [newConfig setObject:[diffuse normalizedArray] forKey:@"diffuse"]; + if (emission != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_EMISSION"]; + [newConfig setObject:[emission normalizedArray] forKey:@"emission"]; + } + if (shininess != 0) + { + [modifiedMacros setObject:one forKey:@"OOSTD_SPECULAR"]; + if (specular != nil) [newConfig setObject:[specular normalizedArray] forKey:@"specular"]; + if (shininess > 0) [newConfig setObject:[NSNumber numberWithUnsignedInt:shininess] forKey:@"shininess"]; + if (specularMap != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_SPECULAR_MAP"]; + AddTexture(uniforms, textures, @"uSpecularMap", specularMap); + } + } + [newConfig setObject:diffuseMap forKey:@"diffuse_map"]; + if (![diffuseMap isEqualToString:@""]) // empty string, not nil, means no diffuse map + { + [modifiedMacros setObject:one forKey:@"OOSTD_DIFFUSE_MAP"]; + AddTexture(uniforms, textures, @"uDiffuseMap", diffuseMap); + } + if (emissionMap != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_EMISSION_MAP"]; + AddTexture(uniforms, textures, @"uEmissionMap", emissionMap); + } + else if (emissionAndIlluminationMap != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_EMISSION_AND_ILLUMINATION_MAP"]; + AddTexture(uniforms, textures, @"uEmissionMap", emissionAndIlluminationMap); + } + if (illuminationMap != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_ILLUMINATION_MAP"]; + AddTexture(uniforms, textures, @"uIlluminationMap", illuminationMap); + } + if (normalMap != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_NORMAL_MAP"]; + AddTexture(uniforms, textures, @"uNormalMap", normalMap); + if (normalAndParallaxMap != nil) + { + [modifiedMacros setObject:one forKey:@"OOSTD_NORMAL_AND_PARALLAX_MAP"]; + SetUniformFloat(uniforms, @"uParallaxScale", parallaxScale); + SetUniformFloat(uniforms, @"uParallaxBias", parallaxBias); + } + } + if ([UNIVERSE shaderEffectsLevel] == SHADERS_FULL) + { + // Add uniforms required for hull heat glow + [uniforms setObject:@"hullHeatLevel" forKey:@"uHullHeatLevel"]; + [uniforms setObject:@"timeElapsedSinceSpawn" forKey:@"uTime"]; + } + + if ([textures count] != 0) [newConfig setObject:textures forKey:@"textures"]; + if ([uniforms count] != 0) [newConfig setObject:uniforms forKey:@"uniforms"]; + + [newConfig setObject:modifiedMacros forKey:@"_oo_synthesized_material_macros"]; + return newConfig; +} + + ++ (OOMaterial *)defaultShaderMaterialWithName:(NSString *)name + forModelNamed:(NSString *)modelName + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)target +{ + OOCacheManager *cache = nil; + NSString *cacheKey = nil; + NSDictionary *synthesizedConfig = nil; + OOMaterial *result = nil; + + // Avoid looping (can happen if shader fails to compile). + if ([configuration objectForKey:@"_oo_is_synthesized_config"] != nil) + { + OOLog(@"material.synthesize.loop", @"Synthesis loop for material %@.", name); + return nil; + } + + if (modelName != nil) + { + cache = [OOCacheManager sharedCache]; + cacheKey = [NSString stringWithFormat:@"%@/%@", modelName, name]; + synthesizedConfig = [cache objectForKey:cacheKey inCache:@"synthesized shader materials"]; + } + + if (synthesizedConfig == nil) + { + synthesizedConfig = [self synthesizeMaterialDictionaryWithName:name + forModelNamed:modelName + configuration:configuration + macros:macros]; + if (synthesizedConfig != nil && modelName != nil) + { + [cache setObject:synthesizedConfig + forKey:cacheKey + inCache:@"synthesized shader materials"]; + } + } + + if (synthesizedConfig != nil) + { + result = [self materialWithName:name + forModelNamed:modelName + configuration:synthesizedConfig + macros:[synthesizedConfig objectForKey:@"_oo_synthesized_material_macros"] + bindingTarget:target + forSmoothedMesh:YES]; + } + + return result; +} + + ++ (id)materialWithName:(NSString *)name + forModelNamed:(NSString *)modelName + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)object + forSmoothedMesh:(BOOL)smooth +{ + id result = nil; + +#ifndef NO_SHADERS + if ([UNIVERSE useShaders]) + { + if ([OOShaderMaterial configurationDictionarySpecifiesShaderMaterial:configuration]) + { + result = [OOShaderMaterial shaderMaterialWithName:name + configuration:configuration + macros:macros + bindingTarget:object]; + } + + // Use default shader if smoothing is on, or shader detail is full, or material uses an effect map. + if (result == nil && + (smooth || + [UNIVERSE shaderEffectsLevel] == SHADERS_FULL || + [configuration objectForKey:@"specular_map"] != nil || + [configuration objectForKey:@"emission_map"] != nil || + [configuration objectForKey:@"illumination_map"] != nil || + [configuration objectForKey:@"emission_and_illumination_map"] != nil + )) + { + result = [self defaultShaderMaterialWithName:name + forModelNamed:modelName + configuration:configuration + macros:macros + bindingTarget:(id)object]; + } + } +#endif + + if (result == nil) + { + if ([[configuration objectForKey:@"diffuse_map"] isEqual:@""]) + { + result = [[OOBasicMaterial alloc] initWithName:name configuration:configuration]; + } + else + { + result = [[OOSingleTextureMaterial alloc] initWithName:name configuration:configuration]; + } + if (result == nil) + { + result = [[OOBasicMaterial alloc] initWithName:name configuration:configuration]; + } + [result autorelease]; + } + return result; +} + + ++ (id)materialWithName:(NSString *)name + forModelNamed:(NSString *)modelName + materialDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + macros:(NSDictionary *)macros + bindingTarget:(id)object + forSmoothedMesh:(BOOL)smooth +{ + NSDictionary *configuration = nil; + +#ifndef NO_SHADERS + if ([UNIVERSE useShaders]) + { + configuration = [shadersDict dictionaryForKey:name]; + } +#endif + + if (configuration == nil) + { + configuration = [materialDict dictionaryForKey:name]; + } + + return [self materialWithName:name + forModelNamed:modelName + configuration:configuration + macros:macros + bindingTarget:object + forSmoothedMesh:smooth]; +} + +@end + + +@implementation OOMaterial (OOSubclassInterface) + +- (BOOL)doApply +{ + OOLogGenericSubclassResponsibility(); + return NO; +} + + +- (void)unapplyWithNext:(OOMaterial *)next; +{ + // Do nothing. +} + + +- (void)willDealloc +{ + if (EXPECT_NOT(sActiveMaterial == self)) + { + OOLog(@"shader.dealloc.imbalance", @"***** Material deallocated while active, indicating a retain/release imbalance."); + [self unapplyWithNext:nil]; + sActiveMaterial = nil; + } +} + +@end diff --git a/src/Core/Materials/OONullTexture.h b/src/Core/Materials/OONullTexture.h new file mode 100644 index 00000000..d9cf8dcb --- /dev/null +++ b/src/Core/Materials/OONullTexture.h @@ -0,0 +1,59 @@ +/* + +OONullTexture.h + +Singleton subclass of OOTexture representing the empty texture. Applying +OONullTexture is equivalent to [OOTexture applyNone]. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOTexture.h" + + +@interface OONullTexture: OOTexture + ++ (id) sharedNullTexture; + +@end diff --git a/src/Core/Materials/OONullTexture.m b/src/Core/Materials/OONullTexture.m new file mode 100644 index 00000000..cddb2311 --- /dev/null +++ b/src/Core/Materials/OONullTexture.m @@ -0,0 +1,125 @@ +/* + +OONullTexture.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OONullTexture.h" +#import "OOCocoa.h" + + +static OONullTexture *sSingleton = nil; + + +@implementation OONullTexture + ++ (id) sharedNullTexture +{ + // NOTE: assumes single-threaded access. + if (sSingleton == nil) + { + [[self alloc] init]; + } + + return sSingleton; +} + + +- (void) apply +{ + [OOTexture applyNone]; +} + +@end + + +@implementation OONullTexture (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +nullTexture above. + + NOTE: assumes single-threaded access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end diff --git a/src/Core/Materials/OOPNGTextureLoader.h b/src/Core/Materials/OOPNGTextureLoader.h new file mode 100644 index 00000000..1fbfadfd --- /dev/null +++ b/src/Core/Materials/OOPNGTextureLoader.h @@ -0,0 +1,63 @@ +/* + +OOPNGTextureLoader.h + +It's a texture loader. Which loads PNGs. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOTextureLoader.h" + + +@interface OOPNGTextureLoader: OOTextureLoader +{ + struct png_struct_def *png; + struct png_info_struct *pngInfo; + struct png_info_struct *pngEndInfo; + NSData *fileData; + size_t length; + size_t offset; +} + +@end diff --git a/src/Core/Materials/OOPNGTextureLoader.m b/src/Core/Materials/OOPNGTextureLoader.m new file mode 100644 index 00000000..2832988a --- /dev/null +++ b/src/Core/Materials/OOPNGTextureLoader.m @@ -0,0 +1,257 @@ +/* + +OOPNGTextureLoader.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "png.h" +#import "OOPNGTextureLoader.h" +#import "OOFunctionAttributes.h" +#import "OOLogging.h" + + +void png_error(png_structp, png_const_charp) NO_RETURN_FUNC; + + +static void PNGError(png_structp png, png_const_charp message); +static void PNGWarning(png_structp png, png_const_charp message); +static void PNGRead(png_structp png, png_bytep bytes, png_size_t size); + + +@interface OOPNGTextureLoader (OOPrivate) + +- (void)doLoadTexture; +- (void)readBytes:(png_bytep)bytes count:(png_size_t)count; + +@end + + +@implementation OOPNGTextureLoader + +- (void)loadTexture +{ + // Get data from file + fileData = [[NSData alloc] initWithContentsOfMappedFile:path]; + if (fileData == nil) return; + length = [fileData length]; + + [self doLoadTexture]; + + [fileData release]; + fileData = nil; +} + + +- (void)dealloc +{ + [fileData release]; + if (png != NULL) + { + png_destroy_read_struct(&png, &pngInfo, &pngEndInfo); + } + + [super dealloc]; +} + +@end + + +@implementation OOPNGTextureLoader (OOPrivate) + +- (void)doLoadTexture +{ + png_bytepp rows = NULL; + png_uint_32 pngWidth, + pngHeight; + int depth, + colorType; + uint32_t i; + BOOL grayscale; + uint8_t planes; + + // Set up PNG decoding + png = png_create_read_struct(PNG_LIBPNG_VER_STRING, self, PNGError, PNGWarning); + if (png == NULL) + { + OOLog(@"texture.load.png.setup.failed", @"***** Error preparing to read %@.", path); + goto FAIL; + } + + pngInfo = png_create_info_struct(png); + if (pngInfo == NULL) + { + OOLog(@"texture.load.png.setup.failed", @"***** Error preparing to read %@.", path); + goto FAIL; + } + + pngEndInfo = png_create_info_struct(png); + if (pngInfo == NULL) + { + OOLog(@"texture.load.png.setup.failed", @"***** Error preparing to read %@.", path); + goto FAIL; + } + + if (EXPECT_NOT(setjmp(png_jmpbuf(png)))) + { + // libpng will jump here on error. + if (data) + { + free(data); + data = NULL; + } + goto FAIL; + } + + png_set_read_fn(png, self, PNGRead); + + png_read_info(png, pngInfo); + // Read header, get format info and check that it meets our expectations. + if (EXPECT_NOT(!png_get_IHDR(png, pngInfo, &pngWidth, &pngHeight, &depth, &colorType, NULL, NULL, NULL))) + { + OOLog(@"texture.load.png.failed", @"Failed to get metadata from PNG %@", path); + goto FAIL; + } + png_set_strip_16(png); // 16 bits per channel -> 8 bpc + if (depth < 8 || colorType == PNG_COLOR_TYPE_PALETTE) + { + png_set_expand(png); // Paletted -> RGB, greyscale -> 8 bpc + } + if (colorType == PNG_COLOR_TYPE_GRAY) + { + // TODO: what about PNG_COLOR_TYPE_GRAY_ALPHA ? + grayscale = YES; + planes = 1; + format = kOOTextureDataGrayscale; + + // png_set_invert_mono(png); + } + else + { + grayscale = NO; + planes = 4; + format = kOOTextureDataRGBA; + + png_set_bgr(png); + png_set_swap_alpha(png); // RGBA->ARGB + + png_set_filler(png, 0xFF, PNG_FILLER_BEFORE); + } + + png_read_update_info(png, pngInfo); + + // Metadata is acceptable; load data. + width = pngWidth; + height = pngHeight; + rowBytes = png_get_rowbytes(png, pngInfo); + + // png_read_png + rows = malloc(sizeof *rows * height); + data = malloc(rowBytes * height); + if (EXPECT_NOT(rows == NULL || data == NULL)) + { + if (rows != NULL) free(rows); + if (data != NULL) + { + free(data); + data = NULL; + } + OOLog(kOOLogAllocationFailure, @"Failed to allocate space (%u bytes) for texture %@", rowBytes * height, path); + goto FAIL; + } + + for (i = 0; i != height; ++i) + { + rows[i] = ((png_bytep)data) + i * rowBytes; + } + png_read_image(png, rows); + png_read_end(png, pngEndInfo); + +FAIL: + free(rows); + png_destroy_read_struct(&png, &pngInfo, &pngEndInfo); +} + + +- (void)readBytes:(png_bytep)bytes count:(png_size_t)count +{ + // Check that we're within the file's bounds + if (EXPECT_NOT(length - offset < count)) + { + NSString *message = [NSString stringWithFormat:@"attempt to read beyond end of file (%@), file may be truncated.", path]; + png_error(png, [message UTF8String]); // Will not return + } + + assert(bytes != NULL); + + // Copy bytes + memcpy(bytes, [fileData bytes] + offset, count); + offset += count; +} + +@end + + +static void PNGError(png_structp png, png_const_charp message) +{ + OOPNGTextureLoader *loader = nil; + + loader = png->error_ptr; + OOLog(@"texture.load.png.error", @"***** A PNG loading error occurred for %@: %s", [loader path], message); +} + + +static void PNGWarning(png_structp png, png_const_charp message) +{ + OOPNGTextureLoader *loader = nil; + + loader = png->error_ptr; + OOLog(@"texture.load.png.warning", @"***** A PNG loading warning occurred for %@: %s", [loader path], message); +} + + +static void PNGRead(png_structp png, png_bytep bytes, png_size_t size) +{ + OOPNGTextureLoader *loader = png_get_io_ptr(png); + [loader readBytes:bytes count:size]; +} diff --git a/src/Core/Materials/OOShaderMaterial.h b/src/Core/Materials/OOShaderMaterial.h new file mode 100644 index 00000000..ef003b57 --- /dev/null +++ b/src/Core/Materials/OOShaderMaterial.h @@ -0,0 +1,201 @@ +/* + +OOShaderMaterial.h + +Managers a combination of a shader program, textures and uniforms. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOBasicMaterial.h" +#import "OOWeakReference.h" +#import "OOMaths.h" + + +#ifndef NO_SHADERS + +@class OOShaderProgram, OOTexture; + + +enum +{ + // Conversion settings for uniform bindings + kOOUniformConvertClamp = 0x0001U, + kOOUniformConvertNormalize = 0x0002U, + kOOUniformConvertToMatrix = 0x0004U, + kOOUniformBindToSuperTarget = 0x0008U, + + kOOUniformConvertDefaults = kOOUniformConvertToMatrix | kOOUniformBindToSuperTarget +}; +typedef uint16_t OOUniformConvertOptions; + + +@interface OOShaderMaterial: OOBasicMaterial +{ + OOShaderProgram *shaderProgram; + NSMutableDictionary *uniforms; + + uint32_t texCount; + OOTexture **textures; + + OOWeakReference *bindingTarget; +} + ++ (BOOL)configurationDictionarySpecifiesShaderMaterial:(NSDictionary *)configuration; + +/* Set up an OOShaderMaterial. + + Configuration should be a dictionary equivalent to an entry in a + shipdata.plist "shaders" dictionary. Specifically, keys OOShaderMaterial + will look for are currently: + textures array of texture file names. + vertex_shader name of vertex shader file. + fragment_shader name of fragment shader file. + uniforms dictionary of uniforms. Values are either reals or + dictionaries containing: + type "int", "texture" or "float" + value number + + Macros is a dictionary which is converted to macro definitions and + prepended to shader source code. It should be used to specify the + availability if uniforms you tend to register, and other macros such as + bug fix identifiers. For example, the + dictionary: + { "OO_ENGINE_LEVEL" = 1; } + + will be transformed into: + #define OO_ENGINE_LEVEL 1 +*/ ++ (id)shaderMaterialWithName:(NSString *)name + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)target; + +- (id)initWithName:(NSString *)name + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)target; + +/* Bind a uniform to a property of an object. + + SelectorName should specify a method of source which returns the desired + value; it will be called every time -apply is, assuming uniformName is + used in the shader. (If not, OOShaderMaterial will not track the binding.) + + A bound method must not take any parameters, and must return one of the + following types: + * Any integer or float type. + * NSNumber. + * Vector. + * Quaternion. + * OOMatrix. + * OOColor. + + The "convert" flag has different meanings for different types: + * For int, float or NSNumber, it clamps to the range [0..1]. + * For Vector, it normalizes. + * For Quaternion, it converts to a rotation matrix (instead of a vector). + + NOTE: this method *does not* check against the whitelist. See + -bindSafeUniform:toObject:propertyNamed:convertOptions: below. +*/ +- (BOOL)bindUniform:(NSString *)uniformName + toObject:(id)target + property:(SEL)selector + convertOptions:(OOUniformConvertOptions)options; + +/* Bind a uniform to a property of an object. + + This is similar to -bindUniform:toObject:property:convertOptions:, except + that it checks against OOUniformBindingPermitted(). +*/ +- (BOOL)bindSafeUniform:(NSString *)uniformName + toObject:(id)target + propertyNamed:(NSString *)property + convertOptions:(OOUniformConvertOptions)options; + +/* Set a uniform value. +*/ +- (void)setUniform:(NSString *)uniformName intValue:(int)value; +- (void)setUniform:(NSString *)uniformName floatValue:(float)value; +- (void)setUniform:(NSString *)uniformName vectorValue:(Vector)value; +- (void)setUniform:(NSString *)uniformName quaternionValue:(Quaternion)value asMatrix:(BOOL)asMatrix; + +/* Add constant uniforms. Same format as uniforms dictionary of configuration + parameter to -initWithConfiguration:macros:. The target parameter is used + for bindings. +*/ +-(void)addUniformsFromDictionary:(NSDictionary *)uniformDefs withBindingTarget:(id)target; + +@end + + +@interface NSObject (ShaderBindingHierarchy) + +/* Informal protocol for objects to "forward" their shader bindings up a + hierarchy (for instance, subentities to parent entities). +*/ +- (id) superShaderBindingTarget; + +@end + + +enum +{ + /* ID of vertex attribute used for tangents. A fixed ID is used for + simplicty. + NOTE: on Nvidia hardware, attribute 15 is aliased to + gl_MultiTexCoord7. This is not expected to become a problem. + */ + kTangentAttributeIndex = 15 +}; + + +/* OOUniformBindingPermitted() + + Predicate determining whether a given property may be used as a binding. + Client code is responsible for implementing this. +*/ +BOOL OOUniformBindingPermitted(NSString *propertyName, id bindingTarget); + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderMaterial.m b/src/Core/Materials/OOShaderMaterial.m new file mode 100644 index 00000000..9b614b14 --- /dev/null +++ b/src/Core/Materials/OOShaderMaterial.m @@ -0,0 +1,657 @@ +/* + +OOShaderMaterial.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef NO_SHADERS +#import "ResourceManager.h" +#import "OOShaderMaterial.h" +#import "OOShaderUniform.h" +#import "OOFunctionAttributes.h" +#import "OOCollectionExtractors.h" +#import "OOShaderProgram.h" +#import "OOTexture.h" +#import "OOOpenGLExtensionManager.h" +#import "OOMacroOpenGL.h" +#import "Universe.h" +#import "OOIsNumberLiteral.h" +#import "OOLogging.h" + + +static NSString *MacrosToString(NSDictionary *macros); + + +@interface OOShaderMaterial (OOPrivate) + +- (void)addTexturesFromArray:(NSArray *)textureNames unitCount:(GLuint)max; + +@end + + +@implementation OOShaderMaterial + ++ (BOOL)configurationDictionarySpecifiesShaderMaterial:(NSDictionary *)configuration +{ + if (configuration == nil) return NO; + + if ([configuration stringForKey:@"vertex_shader"] != nil) return YES; + if ([configuration stringForKey:@"fragment_shader"] != nil) return YES; + + return NO; +} + + ++ (id)shaderMaterialWithName:(NSString *)name + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)target +{ + return [[[self alloc] initWithName:name configuration:configuration macros:macros bindingTarget:target] autorelease]; +} + + +- (id)initWithName:(NSString *)name + configuration:(NSDictionary *)configuration + macros:(NSDictionary *)macros + bindingTarget:(id)target +{ + BOOL OK = YES; + NSDictionary *uniformDefs = nil; + NSArray *textureDefs = nil; + NSString *macroString = nil; + NSString *vertexShader = nil; + NSString *fragmentShader = nil; + GLint textureUnits; + NSMutableDictionary *modifiedMacros = nil; + + if (configuration == nil) OK = NO; + + OO_ENTER_OPENGL(); + + self = [super initWithName:name configuration:configuration]; + if (self == nil) OK = NO; + + if (OK) + { + modifiedMacros = macros ? [macros mutableCopy] : [[NSMutableDictionary alloc] init]; + + glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB, &textureUnits); + [modifiedMacros setObject:[NSNumber numberWithInt:textureUnits] forKey:@"OO_TEXTURE_UNIT_COUNT"]; + + if ([UNIVERSE shaderEffectsLevel] == SHADERS_SIMPLE) + { + [modifiedMacros setObject:[NSNumber numberWithInt:1] forKey:@"OO_REDUCED_COMPLEXITY"]; + } + + macroString = MacrosToString(modifiedMacros); + [modifiedMacros release]; + } + + if (OK) + { + vertexShader = [configuration stringForKey:@"vertex_shader"]; + fragmentShader = [configuration stringForKey:@"fragment_shader"]; + + if (vertexShader != nil || fragmentShader != nil) + { + static NSDictionary *attributeBindings = nil; + if (attributeBindings == nil) + { + attributeBindings = [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:kTangentAttributeIndex] + forKey:@"tangent"]; + [attributeBindings retain]; + } + + shaderProgram = [OOShaderProgram shaderProgramWithVertexShaderName:vertexShader + fragmentShaderName:fragmentShader + prefix:macroString + attributeBindings:attributeBindings]; + } + else + { + OOLog(@"shader.load.noShader", @"***** Error: no vertex or fragment shader specified specified in shader dictionary:\n%@", configuration); + } + + OK = (shaderProgram != nil); + if (OK) [shaderProgram retain]; + } + + if (OK) + { + // Load uniforms + uniformDefs = [configuration dictionaryForKey:@"uniforms"]; + textureDefs = [configuration arrayForKey:@"textures"]; + + uniforms = [[NSMutableDictionary alloc] initWithCapacity:[uniformDefs count] + [textureDefs count]]; + [self addUniformsFromDictionary:uniformDefs withBindingTarget:target]; + // ...and textures, which are a flavour of uniform for our purpose. + [self addTexturesFromArray:textureDefs unitCount:textureUnits]; + } + + if (!OK) + { + [self release]; + self = nil; + } + return self; +} + + +- (void)dealloc +{ + uint32_t i; + + [self willDealloc]; + + [shaderProgram release]; + [uniforms release]; + + if (textures != NULL) + { + for (i = 0; i != texCount; ++i) + { + [textures[i] release]; + } + free(textures); + } + + [bindingTarget release]; + + [super dealloc]; +} + + +- (BOOL)bindUniform:(NSString *)uniformName + toObject:(id)source + property:(SEL)selector + convertOptions:(OOUniformConvertOptions)options +{ + OOShaderUniform *uniform = nil; + + if (uniformName == nil) return NO; + + uniform = [[OOShaderUniform alloc] initWithName:uniformName + shaderProgram:shaderProgram + boundToObject:source + property:selector + convertOptions:options]; + if (uniform != nil) + { + OOLog(@"shader.uniform.set", @"Set up uniform %@", uniform); + [uniforms setObject:uniform forKey:uniformName]; + [uniform release]; + return YES; + } + else + { + OOLog(@"shader.uniform.unSet", @"Did not set uniform \"%@\"", uniformName); + [uniforms removeObjectForKey:uniformName]; + return NO; + } +} + + +- (BOOL)bindSafeUniform:(NSString *)uniformName + toObject:(id)target + propertyNamed:(NSString *)property + convertOptions:(OOUniformConvertOptions)options +{ + SEL selector = NULL; + + selector = NSSelectorFromString(property); + + if (selector != NULL && OOUniformBindingPermitted(property, target)) + { + return [self bindUniform:uniformName + toObject:target + property:selector + convertOptions:options]; + } + else + { + OOLog(@"shader.uniform.unpermittedMethod", @"Did not bind uniform \"%@\" to property -[%@ %@] - unpermitted method.", uniformName, [target class], property); + } + + return NO; +} + + +- (void)setUniform:(NSString *)uniformName intValue:(int)value +{ + OOShaderUniform *uniform = nil; + + if (uniformName == nil) return; + + uniform = [[OOShaderUniform alloc] initWithName:uniformName + shaderProgram:shaderProgram + intValue:value]; + if (uniform != nil) + { + OOLog(@"shader.uniform.set", @"Set up uniform %@", uniform); + [uniforms setObject:uniform forKey:uniformName]; + [uniform release]; + } + else + { + OOLog(@"shader.uniform.unSet", @"Did not set uniform \"%@\"", uniformName); + [uniforms removeObjectForKey:uniformName]; + } +} + + +- (void)setUniform:(NSString *)uniformName floatValue:(float)value +{ + OOShaderUniform *uniform = nil; + + if (uniformName == nil) return; + + uniform = [[OOShaderUniform alloc] initWithName:uniformName + shaderProgram:shaderProgram + floatValue:value]; + if (uniform != nil) + { + OOLog(@"shader.uniform.set", @"Set up uniform %@", uniform); + [uniforms setObject:uniform forKey:uniformName]; + [uniform release]; + } + else + { + OOLog(@"shader.uniform.unSet", @"Did not set uniform \"%@\"", uniformName); + [uniforms removeObjectForKey:uniformName]; + } +} + + +- (void)setUniform:(NSString *)uniformName vectorValue:(Vector)value +{ + OOShaderUniform *uniform = nil; + + if (uniformName == nil) return; + + uniform = [[OOShaderUniform alloc] initWithName:uniformName + shaderProgram:shaderProgram + vectorValue:value]; + if (uniform != nil) + { + OOLog(@"shader.uniform.set", @"Set up uniform %@", uniform); + [uniforms setObject:uniform forKey:uniformName]; + [uniform release]; + } + else + { + OOLog(@"shader.uniform.unSet", @"Did not set uniform \"%@\"", uniformName); + [uniforms removeObjectForKey:uniformName]; + } +} + + +- (void)setUniform:(NSString *)uniformName quaternionValue:(Quaternion)value asMatrix:(BOOL)asMatrix +{ + OOShaderUniform *uniform = nil; + + if (uniformName == nil) return; + + uniform = [[OOShaderUniform alloc] initWithName:uniformName + shaderProgram:shaderProgram + quaternionValue:value + asMatrix:asMatrix]; + if (uniform != nil) + { + OOLog(@"shader.uniform.set", @"Set up uniform %@", uniform); + [uniforms setObject:uniform forKey:uniformName]; + [uniform release]; + } + else + { + OOLog(@"shader.uniform.unSet", @"Did not set uniform \"%@\"", uniformName); + [uniforms removeObjectForKey:uniformName]; + } +} + + +-(void)addUniformsFromDictionary:(NSDictionary *)uniformDefs withBindingTarget:(id)target +{ + NSEnumerator *uniformEnum = nil; + NSString *name = nil; + id definition = nil; + id value = nil; + NSString *binding = nil; + NSString *type = nil; + GLfloat floatValue; + BOOL gotValue; + OOShaderUniform *uniform = nil; + OOUniformConvertOptions convertOptions; + BOOL quatAsMatrix = YES; + GLfloat scale = 1.0; + + for (uniformEnum = [uniformDefs keyEnumerator]; (name = [uniformEnum nextObject]); ) + { + gotValue = NO; + uniform = nil; + definition = [uniformDefs objectForKey:name]; + + type = nil; + value = nil; + binding = nil; + + if ([definition isKindOfClass:[NSDictionary class]]) + { + value = [(NSDictionary *)definition objectForKey:@"value"]; + binding = [(NSDictionary *)definition stringForKey:@"binding"]; + type = [(NSDictionary *)definition stringForKey:@"type"]; + scale = [(NSDictionary *)definition floatForKey:@"scale" defaultValue:1.0]; + if (type == nil) + { + if (value == nil && binding != nil) type = @"binding"; + else type = @"float"; + } + } + else if ([definition isKindOfClass:[NSNumber class]]) + { + value = definition; + type = @"float"; + } + else if ([definition isKindOfClass:[NSString class]]) + { + if (OOIsNumberLiteral(definition, NO)) + { + value = definition; + type = @"float"; + } + else + { + binding = definition; + type = @"binding"; + } + } + else if ([definition isKindOfClass:[NSArray class]]) + { + binding = definition; + type = @"vector"; + } + + // Transform random values to concrete values + if ([type isEqualToString:@"randomFloat"]) + { + type = @"float"; + value = [NSNumber numberWithFloat:randf() * scale]; + } + else if ([type isEqualToString:@"randomUnitVector"]) + { + type = @"vector"; + value = OOPropertyListFromVector(vector_multiply_scalar(OORandomUnitVector(), scale)); + } + else if ([type isEqualToString:@"randomVectorSpatial"]) + { + type = @"vector"; + value = OOPropertyListFromVector(OOVectorRandomSpatial(scale)); + } + else if ([type isEqualToString:@"randomVectorRadial"]) + { + type = @"vector"; + value = OOPropertyListFromVector(OOVectorRandomRadial(scale)); + } + else if ([type isEqualToString:@"randomQuaternion"]) + { + type = @"quaternion"; + value = OOPropertyListFromQuaternion(OORandomQuaternion()); + } + + if ([type isEqualToString:@"float"] || [type isEqualToString:@"real"]) + { + gotValue = YES; + if ([value respondsToSelector:@selector(floatValue)]) floatValue = [value floatValue]; + else if ([value respondsToSelector:@selector(doubleValue)]) floatValue = [value doubleValue]; + else if ([value respondsToSelector:@selector(intValue)]) floatValue = [value intValue]; + else gotValue = NO; + + if (gotValue) + { + [self setUniform:name floatValue:floatValue]; + } + } + else if ([type isEqualToString:@"int"] || [type isEqualToString:@"integer"] || [type isEqualToString:@"texture"]) + { + /* "texture" is allowed as a synonym for "int" because shader + uniforms are mapped to texture units by specifying an integer + index. + uniforms = { diffuseMap = { type = texture; value = 0; }; }; + means "bind uniform diffuseMap to texture unit 0" (which will + have the first texture in the textures array). + */ + if ([value respondsToSelector:@selector(intValue)]) + { + [self setUniform:name intValue:[value intValue]]; + gotValue = YES; + } + } + else if ([type isEqualToString:@"vector"]) + { + [self setUniform:name vectorValue:OOVectorFromObject(value, kZeroVector)]; + gotValue = YES; + } + else if ([type isEqualToString:@"quaternion"]) + { + if ([definition isKindOfClass:[NSDictionary class]]) + { + quatAsMatrix = [definition boolForKey:@"asMatrix" defaultValue:quatAsMatrix]; + } + [self setUniform:name + quaternionValue:OOQuaternionFromObject(value, kIdentityQuaternion) + asMatrix:quatAsMatrix]; + gotValue = YES; + } + else if (target != nil && [type isEqualToString:@"binding"]) + { + if ([definition isKindOfClass:[NSDictionary class]]) + { + convertOptions = 0; + if ([definition boolForKey:@"clamped" defaultValue:NO]) convertOptions |= kOOUniformConvertClamp; + if ([definition boolForKey:@"normalized" defaultValue:[definition boolForKey:@"normalised" defaultValue:NO]]) + { + convertOptions |= kOOUniformConvertNormalize; + } + if ([definition boolForKey:@"asMatrix" defaultValue:YES]) convertOptions |= kOOUniformConvertToMatrix; + if (![definition boolForKey:@"bindToSubentity" defaultValue:NO]) convertOptions |= kOOUniformBindToSuperTarget; + } + else + { + convertOptions = kOOUniformConvertDefaults; + } + + [self bindSafeUniform:name toObject:target propertyNamed:binding convertOptions:convertOptions]; + gotValue = YES; + } + + if (!gotValue) + { + OOLog(@"shader.uniform.badDescription", @"----- Warning: could not bind uniform \"%@\" for target %@ -- could not interpret definition:\n%@", name, target, definition); + } + } +} + + +- (BOOL)doApply +{ + NSEnumerator *uniformEnum = nil; + OOShaderUniform *uniform = nil; + uint32_t i; + + OO_ENTER_OPENGL(); + + [super doApply]; + [shaderProgram apply]; + + for (i = 0; i != texCount; ++i) + { + glActiveTextureARB(GL_TEXTURE0_ARB + i); + [textures[i] apply]; + } + glActiveTextureARB(GL_TEXTURE0_ARB); + + NS_DURING + for (uniformEnum = [uniforms objectEnumerator]; (uniform = [uniformEnum nextObject]); ) + { + [uniform apply]; + } + NS_HANDLER + /* Supress exceptions during application of bound uniforms. We use a + single exception handler around all uniforms because ObjC + exceptions have some overhead. + */ + NS_ENDHANDLER + + return YES; +} + + +- (void)ensureFinishedLoading +{ + uint32_t i; + + if (textures != NULL) + { + for (i = 0; i != texCount; ++i) + { + [textures[i] ensureFinishedLoading]; + } + } +} + + +- (void)unapplyWithNext:(OOMaterial *)next +{ + uint32_t i, count; + + if (![next isKindOfClass:[OOShaderMaterial class]]) // Avoid redundant state change + { + OO_ENTER_OPENGL(); + [OOShaderProgram applyNone]; + + /* BUG: unapplyWithNext: was failing to clear texture state. If a + shader material was followed by a basic material (with no texture), + the shader's #0 texture would be used. + It is necessary to clear at least one texture for the case where a + shader material with textures is followed by a shader material + without textures, then a basic material. + -- Ahruman 2007-08-13 + */ + count = texCount ? texCount : 1; + for (i = 0; i != count; ++i) + { + glActiveTextureARB(GL_TEXTURE0_ARB + i); + glBindTexture(GL_TEXTURE_2D, 0); + } + glActiveTextureARB(GL_TEXTURE0_ARB); + } +} + + +- (void)setBindingTarget:(id)target +{ + [[uniforms allValues] makeObjectsPerformSelector:@selector(setBindingTarget:) withObject:target]; + [bindingTarget release]; + bindingTarget = [target weakRetain]; +} + +@end + + +@implementation OOShaderMaterial (OOPrivate) + +- (void)addTexturesFromArray:(NSArray *)textureNames unitCount:(GLuint)max +{ + id textureDef = nil; + unsigned i = 0; + + // Allocate space for texture object name array + texCount = MAX(MIN(max, [textureNames count]), 0U); + if (texCount == 0) return; + + textures = malloc(texCount * sizeof *textures); + if (textures == NULL) + { + texCount = 0; + return; + } + + // Set up texture object names and appropriate uniforms + for (i = 0; i != texCount; ++i) + { + [self setUniform:[NSString stringWithFormat:@"tex%u", i] intValue:i]; + + textureDef = [textureNames objectAtIndex:i]; + textures[i] = [OOTexture textureWithConfiguration:textureDef]; + if (textures[i] == nil) textures[i] = [OOTexture nullTexture]; + [textures[i] retain]; + } +} + +@end + + +static NSString *MacrosToString(NSDictionary *macros) +{ + NSMutableString *result = nil; + NSEnumerator *macroEnum = nil; + id key = nil, value = nil; + + if (macros == nil) return nil; + + result = [NSMutableString string]; + for (macroEnum = [macros keyEnumerator]; (key = [macroEnum nextObject]); ) + { + if (![key isKindOfClass:[NSString class]]) continue; + value = [macros objectForKey:key]; + + [result appendFormat:@"#define %@ %@\n", key, value]; + } + + if ([result length] == 0) return nil; + [result appendString:@"\n\n"]; + return result; +} + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderProgram.h b/src/Core/Materials/OOShaderProgram.h new file mode 100644 index 00000000..f3b15899 --- /dev/null +++ b/src/Core/Materials/OOShaderProgram.h @@ -0,0 +1,78 @@ +/* + +OOShaderProgram.h + +Encapsulates a vertex + fragment shader combo. In general, this should only be +used though OOShaderMaterial. The point of this separation is that more than +one OOShaderMaterial can use the same OOShaderProgram (as long as the shaders +are defined in external files, not strings). + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef NO_SHADERS + +#import +#import "OOOpenGL.h" + + +@interface OOShaderProgram: NSObject +{ + GLhandleARB program; + NSString *key; +} + +// Loads a shader from a file, caching and sharing shader program instances. ++ (id) shaderProgramWithVertexShaderName:(NSString *)vertexShaderName + fragmentShaderName:(NSString *)fragmentShaderName + prefix:(NSString *)prefixString // String prepended to program source (both vs and fs) + attributeBindings:(NSDictionary *)attributeBindings; // Maps vertex attribute names to "locations". + +- (void) apply; ++ (void) applyNone; + +- (GLhandleARB) program; + +@end + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderProgram.m b/src/Core/Materials/OOShaderProgram.m new file mode 100644 index 00000000..aef36870 --- /dev/null +++ b/src/Core/Materials/OOShaderProgram.m @@ -0,0 +1,381 @@ +/* + +OOShaderProgram.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef NO_SHADERS + +#import "OOShaderProgram.h" +#import "OOFunctionAttributes.h" +#import "OOStringParsing.h" +#import "ResourceManager.h" +#import "OOOpenGLExtensionManager.h" +#import "OOMacroOpenGL.h" +#import "OOCollectionExtractors.h" + + +static NSMutableDictionary *sShaderCache = nil; +static OOShaderProgram *sActiveProgram = nil; + + +static BOOL GetShaderSource(NSString *fileName, NSString *shaderType, NSString *prefix, NSString **outResult); +static NSString *GetGLSLInfoLog(GLhandleARB shaderObject); + + +@interface OOShaderProgram (OOPrivate) + +- (id)initWithVertexShaderSource:(NSString *)vertexSource + fragmentShaderSource:(NSString *)fragmentSource + prefixString:(NSString *)prefixString + vertexName:(NSString *)vertexName + fragmentName:(NSString *)fragmentName + attributeBindings:(NSDictionary *)attributeBindings + key:(NSString *)key; + +- (void) bindAttributes:(NSDictionary *)attributeBindings; + +@end + + +@implementation OOShaderProgram + ++ (id)shaderProgramWithVertexShaderName:(NSString *)vertexShaderName + fragmentShaderName:(NSString *)fragmentShaderName + prefix:(NSString *)prefixString + attributeBindings:(NSDictionary *)attributeBindings +{ + NSString *cacheKey = nil; + OOShaderProgram *result = nil; + NSString *vertexSource = nil; + NSString *fragmentSource = nil; + + if ([prefixString length] == 0) prefixString = nil; + + // Use cache to avoid creating duplicate shader programs -- saves on GPU resources and potentially state changes. + // FIXME: probably needs to respond to graphics resets. + cacheKey = [NSString stringWithFormat:@"vertex:%@\nfragment:%@\n----\n%@", vertexShaderName, fragmentShaderName, prefixString ?: (NSString *)@""]; + result = [[sShaderCache objectForKey:cacheKey] pointerValue]; + + if (result == nil) + { + // No cached program; create one... + if (!GetShaderSource(vertexShaderName, @"vertex", prefixString, &vertexSource)) return nil; + if (!GetShaderSource(fragmentShaderName, @"fragment", prefixString, &fragmentSource)) return nil; + result = [[OOShaderProgram alloc] initWithVertexShaderSource:vertexSource + fragmentShaderSource:fragmentSource + prefixString:prefixString + vertexName:vertexShaderName + fragmentName:fragmentShaderName + attributeBindings:attributeBindings + key:cacheKey]; + + if (result != nil) + { + // ...and add it to the cache + [result autorelease]; + if (sShaderCache == nil) sShaderCache = [[NSMutableDictionary alloc] init]; + [sShaderCache setObject:[NSValue valueWithPointer:result] forKey:cacheKey]; // Use NSValue so dictionary doesn't retain program + } + } + + return result; +} + + +- (void)dealloc +{ + OO_ENTER_OPENGL(); + +#ifndef NDEBUG + if (EXPECT_NOT(sActiveProgram == self)) + { + OOLog(@"shader.dealloc.imbalance", @"***** OOShaderProgram deallocated while active, indicating a retain/release imbalance. Expect imminent crash."); + [OOShaderProgram applyNone]; + } +#endif + + if (key != nil) + { + [sShaderCache removeObjectForKey:key]; + [key release]; + } + + glDeleteObjectARB(program); + + [super dealloc]; +} + + +- (void)apply +{ + OO_ENTER_OPENGL(); + + if (sActiveProgram != self) + { + [sActiveProgram release]; + sActiveProgram = [self retain]; + glUseProgramObjectARB(program); + } +} + + ++ (void)applyNone +{ + OO_ENTER_OPENGL(); + + if (sActiveProgram != nil) + { + [sActiveProgram release]; + sActiveProgram = nil; + glUseProgramObjectARB(NULL_SHADER); + } +} + + +- (GLhandleARB)program +{ + return program; +} + +@end + + +@implementation OOShaderProgram (OOPrivate) + +- (id)initWithVertexShaderSource:(NSString *)vertexSource + fragmentShaderSource:(NSString *)fragmentSource + prefixString:(NSString *)prefixString + vertexName:(NSString *)vertexName + fragmentName:(NSString *)fragmentName + attributeBindings:(NSDictionary *)attributeBindings + key:(NSString *)inKey +{ + BOOL OK = YES; + const GLcharARB *sourceStrings[2] = { "", NULL }; + GLint compileStatus; + GLhandleARB vertexShader = NULL_SHADER; + GLhandleARB fragmentShader = NULL_SHADER; + + OO_ENTER_OPENGL(); + + self = [super init]; + if (self == nil) OK = NO; + + if (OK && vertexSource == nil && fragmentSource == nil) OK = NO; // Must have at least one shader! + + if (OK && prefixString != nil) + { + sourceStrings[0] = [prefixString UTF8String]; + } + + if (OK && vertexSource != nil) + { + // Compile vertex shader. + vertexShader = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB); + if (vertexShader != NULL_SHADER) + { + sourceStrings[1] = [vertexSource UTF8String]; + glShaderSourceARB(vertexShader, 2, sourceStrings, NULL); + glCompileShaderARB(vertexShader); + + glGetObjectParameterivARB(vertexShader, GL_OBJECT_COMPILE_STATUS_ARB, &compileStatus); + if (compileStatus != GL_TRUE) + { + OOLog(@"shader.compile.vertex.failure", @"***** GLSL %s shader compilation failed for %@:\n>>>>> GLSL log:\n%@\n", "vertex", vertexName, GetGLSLInfoLog(vertexShader)); + OK = NO; + } + } + else OK = NO; + } + + if (OK && fragmentSource != nil) + { + // Compile fragment shader. + fragmentShader = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB); + if (fragmentShader != NULL_SHADER) + { + sourceStrings[1] = [fragmentSource UTF8String]; + glShaderSourceARB(fragmentShader, 2, sourceStrings, NULL); + glCompileShaderARB(fragmentShader); + + glGetObjectParameterivARB(fragmentShader, GL_OBJECT_COMPILE_STATUS_ARB, &compileStatus); + if (compileStatus != GL_TRUE) + { + OOLog(@"shader.compile.fragment.failure", @"***** GLSL %s shader compilation failed for %@:\n>>>>> GLSL log:\n%@\n", "fragment", fragmentName, GetGLSLInfoLog(fragmentShader)); + OK = NO; + } + } + else OK = NO; + } + + if (OK) + { + // Link shader. + program = glCreateProgramObjectARB(); + if (program != NULL_SHADER) + { + if (vertexShader != NULL_SHADER) glAttachObjectARB(program, vertexShader); + if (fragmentShader != NULL_SHADER) glAttachObjectARB(program, fragmentShader); + [self bindAttributes:attributeBindings]; + glLinkProgramARB(program); + + glGetObjectParameterivARB(program, GL_OBJECT_LINK_STATUS_ARB, &compileStatus); + if (compileStatus != GL_TRUE) + { + OOLog(@"shader.link.failure", @"***** GLSL shader linking failed:\n>>>>> GLSL log:\n%@\n", GetGLSLInfoLog(program)); + OK = NO; + } + } + else OK = NO; + } + + if (OK) + { + key = [inKey copy]; + } + + if (vertexShader != NULL_SHADER) glDeleteObjectARB(vertexShader); + if (fragmentShader != NULL_SHADER) glDeleteObjectARB(fragmentShader); + + if (!OK) + { + if (program != NULL_SHADER) glDeleteObjectARB(program); + + [self release]; + self = nil; + } + return self; +} + + +- (void) bindAttributes:(NSDictionary *)attributeBindings +{ + OO_ENTER_OPENGL(); + + NSString *attrKey = nil; + NSEnumerator *keyEnum = nil; + + for (keyEnum = [attributeBindings keyEnumerator]; (attrKey = [keyEnum nextObject]); ) + { + glBindAttribLocationARB(program, [attributeBindings unsignedIntForKey:attrKey], [attrKey UTF8String]); + } +} + +@end + + +/* Attempt to load fragment or vertex shader source from a file. + Returns YES if source was loaded or no shader was specified, and NO if an + external shader was specified but could not be found. +*/ +static BOOL GetShaderSource(NSString *fileName, NSString *shaderType, NSString *prefix, NSString **outResult) +{ + NSString *result = nil; + NSArray *extensions = nil; + NSEnumerator *extEnum = nil; + NSString *extension = nil; + NSString *nameWithExtension = nil; + + if (fileName == nil) return YES; // It's OK for one or the other of the shaders to be undefined. + + result = [ResourceManager stringFromFilesNamed:fileName inFolder:@"Shaders"]; + if (result == nil) + { + extensions = [NSArray arrayWithObjects:shaderType, [shaderType substringToIndex:4], nil]; // vertex and vert, or fragment and frag + + // Futureproofing -- in future, we may wish to support automatic selection between supported shader languages. + if (![fileName pathHasExtensionInArray:extensions]) + { + for (extEnum = [extensions objectEnumerator]; (extension = [extEnum nextObject]); ) + { + nameWithExtension = [fileName stringByAppendingPathExtension:extension]; + result = [ResourceManager stringFromFilesNamed:nameWithExtension + inFolder:@"Shaders"]; + if (result != nil) break; + } + } + if (result == nil) + { + OOLog(kOOLogFileNotFound, @"GLSL ERROR: failed to find fragment program %@.", fileName); + return NO; + } + } + /* + if (result != nil && prefix != nil) + { + result = [prefix stringByAppendingString:result]; + } + */ + if (outResult != NULL) *outResult = result; + return YES; +} + + +static NSString *GetGLSLInfoLog(GLhandleARB shaderObject) +{ + GLint length; + GLcharARB *log = NULL; + NSString *result = nil; + + OO_ENTER_OPENGL(); + + if (EXPECT_NOT(shaderObject == NULL_SHADER)) return nil; + + glGetObjectParameterivARB(shaderObject, GL_OBJECT_INFO_LOG_LENGTH_ARB, &length); + log = malloc(length); + if (log == NULL) + { + length = 1024; + log = malloc(length); + if (log == NULL) return @""; + } + glGetInfoLogARB(shaderObject, length, NULL, log); + + result = [NSString stringWithUTF8String:log]; + if (result == nil) result = [[[NSString alloc] initWithBytes:log length:length - 1 encoding:NSISOLatin1StringEncoding] autorelease]; + return result; +} + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderUniform.h b/src/Core/Materials/OOShaderUniform.h new file mode 100644 index 00000000..7a47a02f --- /dev/null +++ b/src/Core/Materials/OOShaderUniform.h @@ -0,0 +1,109 @@ +/* + +OOShaderUniform.h + +Manages a uniform variable for OOShaderMaterial. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef NO_SHADERS + +#import "OOShaderMaterial.h" +#import "OOMaths.h" + +@class OOColor; + + +@interface OOShaderUniform: NSObject +{ + NSString *name; + GLint location; + uint8_t isBinding: 1, + // flags that apply only to bindings: + isActiveBinding: 1, + convertClamp: 1, + convertNormalize: 1, + convertToMatrix: 1, + bindToSuper: 1; + uint8_t type; + union + { + GLint constInt; + GLfloat constFloat; + GLfloat constVector[4]; + OOMatrix constMatrix; + struct + { + OOWeakReference *object; + SEL selector; + IMP method; + } binding; + } value; +} + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram intValue:(GLint)constValue; +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram floatValue:(GLfloat)constValue; +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram vectorValue:(Vector)constValue; +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram colorValue:(OOColor *)constValue; // Converted to vector +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram quaternionValue:(Quaternion)constValue asMatrix:(BOOL)asMatrix; // Converted to vector (in xyzw order, not wxyz!) or rotation matrix. +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram matrixValue:(OOMatrix)constValue; + +/* "Convert" has different meanings for different types. + For float and int types, it clamps to the range [0, 1]. + For vector types, it normalizes. + For quaternions, it converts to rotation matrix (instead of vec4). +*/ +- (id)initWithName:(NSString *)uniformName + shaderProgram:(OOShaderProgram *)shaderProgram + boundToObject:(id)target + property:(SEL)selector + convertOptions:(OOUniformConvertOptions)options; + +- (void)apply; + +- (void)setBindingTarget:(id)target; + +@end + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderUniform.m b/src/Core/Materials/OOShaderUniform.m new file mode 100644 index 00000000..f636aeca --- /dev/null +++ b/src/Core/Materials/OOShaderUniform.m @@ -0,0 +1,648 @@ +/* + +OOShaderUniform.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef NO_SHADERS + +#import "OOShaderUniform.h" +#import "OOShaderProgram.h" +#import "OOFunctionAttributes.h" +#import +#import "OOMaths.h" +#import "OOOpenGLExtensionManager.h" +#import "OOShaderUniformMethodType.h" + + +typedef char (*CharReturnMsgSend)(id, SEL); +typedef short (*ShortReturnMsgSend)(id, SEL); +typedef int (*IntReturnMsgSend)(id, SEL); +typedef long (*LongReturnMsgSend)(id, SEL); +typedef float (*FloatReturnMsgSend)(id, SEL); +typedef double (*DoubleReturnMsgSend)(id, SEL); +typedef Vector (*VectorReturnMsgSend)(id, SEL); +typedef Quaternion (*QuaternionReturnMsgSend)(id, SEL); +typedef OOMatrix (*MatrixReturnMsgSend)(id, SEL); +typedef NSPoint (*PointReturnMsgSend)(id, SEL); + + +OOINLINE BOOL ValidBindingType(OOShaderUniformType type) +{ + return kOOShaderUniformTypeInt <= type && type <= kOOShaderUniformTypeDouble; +} + + +@interface OOShaderUniform (OOPrivate) + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram; + +- (void)applySimple; +- (void)applyBinding; + +@end + + +@implementation OOShaderUniform + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram intValue:(GLint)constValue +{ + self = [self initWithName:uniformName shaderProgram:shaderProgram]; + if (self != nil) + { + type = kOOShaderUniformTypeInt; + value.constInt = constValue; + } + + return self; +} + + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram floatValue:(GLfloat)constValue +{ + self = [self initWithName:uniformName shaderProgram:shaderProgram]; + if (self != nil) + { + type = kOOShaderUniformTypeFloat; + value.constFloat = constValue; + } + + return self; +} + + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram vectorValue:(Vector)constValue +{ + self = [self initWithName:uniformName shaderProgram:shaderProgram]; + if (self != nil) + { + type = kOOShaderUniformTypeVector; + value.constVector[0] = constValue.x; + value.constVector[1] = constValue.y; + value.constVector[2] = constValue.z; + value.constVector[3] = 1.0f; + } + + return self; +} + + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram colorValue:(OOColor *)constValue +{ + if (EXPECT_NOT(constValue == nil)) + { + [self release]; + return nil; + } + + self = [self initWithName:uniformName shaderProgram:shaderProgram]; + if (self != nil) + { + type = kOOShaderUniformTypeVector; + value.constVector[0] = [constValue redComponent]; + value.constVector[1] = [constValue greenComponent]; + value.constVector[2] = [constValue blueComponent]; + value.constVector[3] = [constValue alphaComponent]; + } + + return self; +} + + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram quaternionValue:(Quaternion)constValue asMatrix:(BOOL)asMatrix +{ + self = [self initWithName:uniformName shaderProgram:shaderProgram]; + if (self != nil) + { + if (asMatrix) + { + type = kOOShaderUniformTypeMatrix; + value.constMatrix = OOMatrixForQuaternionRotation(constValue); + } + else + { + type = kOOShaderUniformTypeVector; + value.constVector[0] = constValue.x; + value.constVector[1] = constValue.y; + value.constVector[2] = constValue.z; + value.constVector[3] = constValue.w; + } + } + + return self; +} + + +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram matrixValue:(OOMatrix)constValue +{ + self = [self initWithName:uniformName shaderProgram:shaderProgram]; + if (self != nil) + { + type = kOOShaderUniformTypeMatrix; + value.constMatrix = constValue; + } + + return self; +} + + +- (id)initWithName:(NSString *)uniformName + shaderProgram:(OOShaderProgram *)shaderProgram + boundToObject:(id)target + property:(SEL)selector + convertOptions:(OOUniformConvertOptions)options +{ + BOOL OK = YES; + + if (EXPECT_NOT(uniformName == NULL || shaderProgram == NULL || selector == NULL)) OK = NO; + + if (OK) + { + self = [super init]; + if (self == nil) OK = NO; + } + + if (OK) + { + location = glGetUniformLocationARB([shaderProgram program], [uniformName UTF8String]); + if (location == -1) + { + OK = NO; + OOLog(@"shader.uniform.bind.failed", @"Could not bind uniform \"%@\" to -[%@ %@] (no uniform of that name could be found).", uniformName, [target class], NSStringFromSelector(selector)); + } + } + + // If we're still OK, it's a bindable method. + if (OK) + { + name = [uniformName retain]; + isBinding = YES; + value.binding.selector = selector; + + convertClamp = (options & kOOUniformConvertClamp) != 0; + convertNormalize = (options & kOOUniformConvertNormalize) != 0; + convertToMatrix = (options & kOOUniformConvertToMatrix) != 0; + bindToSuper = (options & kOOUniformBindToSuperTarget) != 0; + + if (target != nil) [self setBindingTarget:target]; + } + + if (!OK) + { + [self release]; + self = nil; + } + return self; +} + + +- (void)dealloc +{ + [name release]; + if (isBinding) [value.binding.object release]; + + [super dealloc]; +} + + +- (NSString *)description +{ + NSString *valueDesc = nil; + NSString *valueType = nil; + id object; + + if (isBinding) + { + object = [value.binding.object weakRefUnderlyingObject]; + if (object != nil) + { + valueDesc = [NSString stringWithFormat:@"[<%@ %p> %@]", [object class], value.binding.object, NSStringFromSelector(value.binding.selector)]; + } + else + { + valueDesc = @"0"; + } + } + else + { + switch (type) + { + case kOOShaderUniformTypeInt: + valueDesc = [NSString stringWithFormat:@"%i", value.constInt]; + break; + + case kOOShaderUniformTypeFloat: + valueDesc = [NSString stringWithFormat:@"%g", value.constFloat]; + break; + + case kOOShaderUniformTypeVector: + { + Vector v = { value.constVector[0], value.constVector[1], value.constVector[2] }; + valueDesc = VectorDescription(v); + } + break; + + case kOOShaderUniformTypeMatrix: + valueDesc = OOMatrixDescription(value.constMatrix); + break; + } + } + + switch (type) + { + case kOOShaderUniformTypeChar: + case kOOShaderUniformTypeUnsignedChar: + case kOOShaderUniformTypeShort: + case kOOShaderUniformTypeUnsignedShort: + case kOOShaderUniformTypeInt: + case kOOShaderUniformTypeUnsignedInt: + case kOOShaderUniformTypeLong: + case kOOShaderUniformTypeUnsignedLong: + valueType = @"int"; + break; + + case kOOShaderUniformTypeFloat: + case kOOShaderUniformTypeDouble: + valueType = @"float"; + break; + + case kOOShaderUniformTypeVector: + valueType = @"vec4"; + break; + + case kOOShaderUniformTypeQuaternion: + valueType = @"vec4 (quaternion)"; + break; + + case kOOShaderUniformTypeMatrix: + valueType = @"matrix"; + break; + + case kOOShaderUniformTypePoint: + valueType = @"vec2"; + break; + + case kOOShaderUniformTypeObject: + valueType = @"object-binding"; + break; + + } + if (valueType == nil) valueDesc = @"INVALID"; + if (valueDesc == nil) valueDesc = @"INVALID"; + + /* Examples: + {1: int tex1 = 1;} + {3: float laser_heat_level = [ laserHeatLevel];} + */ + return [NSString stringWithFormat:@"<%@ %p>{%i: %@ %@ = %@;}", [self class], self, location, valueType, name, valueDesc]; +} + + +- (void)apply +{ + + if (isBinding) + { + if (isActiveBinding) [self applyBinding]; + } + else [self applySimple]; +} + + +- (void)setBindingTarget:(id)target +{ + BOOL OK = YES; + NSMethodSignature *signature = nil; + unsigned argCount; + NSString *methodProblem = nil; + id superCandidate = nil; + + if (!isBinding) return; + + // Resolve "supertarget" if applicable + if (bindToSuper) + { + for (;;) + { + if (![target respondsToSelector:@selector(superShaderBindingTarget)]) break; + + superCandidate = [(id)target superShaderBindingTarget]; + if (superCandidate == nil || superCandidate == target) break; + target = superCandidate; + } + } + + /* Bad optimization - we need to be able to rebind entities when their + owner changes. + -- Ahruman 2008-04-19 + */ +#if 0 + if (EXPECT_NOT([value.binding.object weakRefUnderlyingObject] == [(id)target weakRefUnderlyingObject])) return; +#endif + + [value.binding.object release]; + value.binding.object = [target weakRetain]; + + if (target == nil) + { + isActiveBinding = NO; + return; + } + + if (OK) + { + if (![target respondsToSelector:value.binding.selector]) + { + methodProblem = @"target does not respond to selector"; + OK = NO; + } + } + + if (OK) + { + value.binding.method = [(id)target methodForSelector:value.binding.selector]; + if (value.binding.method == NULL) + { + methodProblem = @"could not retrieve method implementation"; + OK = NO; + } + } + + if (OK) + { + signature = [(id)target methodSignatureForSelector:value.binding.selector]; + if (signature == nil) + { + methodProblem = @"could not retrieve method signature"; + OK = NO; + } + } + + if (OK) + { + argCount = [signature numberOfArguments]; + if (argCount != 2) // "no-arguments" methods actually take two arguments, self and _msg. + { + methodProblem = @"only methods which do not require arguments may be bound to"; + OK = NO; + } + } + + if (OK) + { + type = OOShaderUniformTypeFromMethodSignature(signature); + if (type == kOOShaderUniformTypeInvalid) + { + OK = NO; + methodProblem = [NSString stringWithFormat:@"unsupported type \"%s\"", [signature methodReturnType]]; + } + } + + isActiveBinding = OK; + if (!OK) OOLog(@"shader.uniform.bind.failed", @"Shader could not bind uniform \"%@\" to -[%@ %@] (%@).", name, [target class], NSStringFromSelector(value.binding.selector), methodProblem); +} + +@end + + +@implementation OOShaderUniform (OOPrivate) + +// Designated initializer. +- (id)initWithName:(NSString *)uniformName shaderProgram:(OOShaderProgram *)shaderProgram +{ + BOOL OK = YES; + + if (EXPECT_NOT(uniformName == NULL || shaderProgram == NULL)) OK = NO; + + if (OK) + { + self = [super init]; + if (self == nil) OK = NO; + } + + if (OK) + { + location = glGetUniformLocationARB([shaderProgram program], [uniformName UTF8String]); + if (location == -1) OK = NO; + } + + if (OK) + { + name = [uniformName copy]; + } + + if (!OK) + { + [self release]; + self = nil; + } + return self; +} + +- (void)applySimple +{ + switch (type) + { + case kOOShaderUniformTypeInt: + glUniform1iARB(location, value.constInt); + break; + + case kOOShaderUniformTypeFloat: + glUniform1fARB(location, value.constFloat); + break; + + case kOOShaderUniformTypeVector: + glUniform4fvARB(location, 1, value.constVector); + break; + + case kOOShaderUniformTypeMatrix: + GLUniformMatrix(location, value.constMatrix); + } +} + + +- (void)applyBinding +{ + + id object = nil; + GLint iVal; + GLfloat fVal; + Vector vVal; + GLfloat expVVal[4]; + OOMatrix mVal; + Quaternion qVal; + NSPoint pVal = {0}; + BOOL isInt = NO, isFloat = NO, isVector = NO, isMatrix = NO, isPoint = NO; + id objVal = nil; + + /* Design note: if the object has been dealloced, or an exception occurs, + do nothing. Shaders can specify a default value for uniforms, which + will be used when no setting has been provided by the host program. + + I considered clearing value.binding.object if the underlying object is + gone, but adding code to save a small amount of spacein a case that + shouldn't occur in normal usage is silly. + */ + object = [value.binding.object weakRefUnderlyingObject]; + if (object == nil) return; + + switch (type) + { + case kOOShaderUniformTypeChar: + case kOOShaderUniformTypeUnsignedChar: + iVal = ((CharReturnMsgSend)value.binding.method)(object, value.binding.selector); + isInt = YES; + break; + + case kOOShaderUniformTypeShort: + case kOOShaderUniformTypeUnsignedShort: + iVal = ((ShortReturnMsgSend)value.binding.method)(object, value.binding.selector); + isInt = YES; + break; + + case kOOShaderUniformTypeInt: + case kOOShaderUniformTypeUnsignedInt: + iVal = ((IntReturnMsgSend)value.binding.method)(object, value.binding.selector); + isInt = YES; + break; + + case kOOShaderUniformTypeLong: + case kOOShaderUniformTypeUnsignedLong: + iVal = ((LongReturnMsgSend)value.binding.method)(object, value.binding.selector); + isInt = YES; + break; + + case kOOShaderUniformTypeFloat: + fVal = ((FloatReturnMsgSend)value.binding.method)(object, value.binding.selector); + isFloat = YES; + break; + + case kOOShaderUniformTypeDouble: + fVal = ((DoubleReturnMsgSend)value.binding.method)(object, value.binding.selector); + isFloat = YES; + break; + + case kOOShaderUniformTypeVector: + vVal = ((VectorReturnMsgSend)value.binding.method)(object, value.binding.selector); + if (convertNormalize) vVal = vector_normal(vVal); + expVVal[0] = vVal.x; + expVVal[1] = vVal.y; + expVVal[2] = vVal.z; + expVVal[3] = 1.0f; + isVector = YES; + break; + + case kOOShaderUniformTypeQuaternion: + qVal = ((QuaternionReturnMsgSend)value.binding.method)(object, value.binding.selector); + if (convertToMatrix) + { + mVal = OOMatrixForQuaternionRotation(qVal); + isMatrix = YES; + } + else + { + expVVal[0] = qVal.x; + expVVal[1] = qVal.y; + expVVal[2] = qVal.z; + expVVal[3] = qVal.w; + isVector = YES; + } + break; + + case kOOShaderUniformTypeMatrix: + mVal = ((MatrixReturnMsgSend)value.binding.method)(object, value.binding.selector); + isMatrix = YES; + break; + + case kOOShaderUniformTypePoint: + pVal = ((PointReturnMsgSend)value.binding.method)(object, value.binding.selector); + isPoint = YES; + break; + + case kOOShaderUniformTypeObject: + objVal = value.binding.method(object, value.binding.selector); + if ([objVal isKindOfClass:[NSNumber class]]) + { + fVal = [objVal floatValue]; + isFloat = YES; + } + else if ([objVal isKindOfClass:[OOColor class]]) + { + expVVal[0] = [objVal redComponent]; + expVVal[1] = [objVal greenComponent]; + expVVal[2] = [objVal blueComponent]; + expVVal[3] = [objVal alphaComponent]; + isVector = YES; + } + break; + } + + if (isFloat) + { + if (convertClamp) fVal = OOClamp_0_1_f(fVal); + glUniform1fARB(location, fVal); + } + else if (isInt) + { + if (convertClamp) iVal = iVal ? 1 : 0; + glUniform1iARB(location, iVal); + } + else if (isPoint) + { + GLfloat v2[2] = { pVal.x, pVal.y }; + glUniform2fvARB(location, 1, v2); + } + else if (isVector) + { + glUniform4fvARB(location, 1, expVVal); + } + else if (isMatrix) + { + GLUniformMatrix(location, mVal); + } +} + +@end + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderUniformMethodType.h b/src/Core/Materials/OOShaderUniformMethodType.h new file mode 100644 index 00000000..f78ea0d9 --- /dev/null +++ b/src/Core/Materials/OOShaderUniformMethodType.h @@ -0,0 +1,82 @@ +/* + +OOShaderUniformMethodType.h + +Type code declarations and OpenStep implementation agnostic method type +matching for uniform bindings. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef NO_SHADERS + +#import "OOCocoa.h" + + +typedef enum +{ + kOOShaderUniformTypeInvalid, // Not valid for bindings or constants + + kOOShaderUniformTypeChar, // Binding only + kOOShaderUniformTypeUnsignedChar, // Binding only + kOOShaderUniformTypeShort, // Binding only + kOOShaderUniformTypeUnsignedShort, // Binding only + kOOShaderUniformTypeInt, // Binding or constant + kOOShaderUniformTypeUnsignedInt, // Binding only + kOOShaderUniformTypeLong, // Binding only + kOOShaderUniformTypeUnsignedLong, // Binding only + kOOShaderUniformTypeFloat, // Binding or constant + kOOShaderUniformTypeDouble, // Binding only + kOOShaderUniformTypeVector, // Binding or constant + kOOShaderUniformTypeQuaternion, // Binding or constant + kOOShaderUniformTypeMatrix, // Binding or constant + kOOShaderUniformTypePoint, // Binding only + kOOShaderUniformTypeObject, // Binding only + + kOOShaderUniformTypeCount // Not valid for bindings or constants +} OOShaderUniformType; + + +OOShaderUniformType OOShaderUniformTypeFromMethodSignature(NSMethodSignature *signature); + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOShaderUniformMethodType.m b/src/Core/Materials/OOShaderUniformMethodType.m new file mode 100644 index 00000000..a1cbca6b --- /dev/null +++ b/src/Core/Materials/OOShaderUniformMethodType.m @@ -0,0 +1,256 @@ +/* + +OOShaderUniformMethodType.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +/* + For shader uniform binding to work, it is necessary to be able to tell the + return type of a method. This is done by comparing the methodReturnType of + a method's NSMethodSignature to those of methods in a template class, one + method for each supported return type. + + Under OS X, the methodReturnType for a type foo is simply @encode(foo), + but under the GNU runtime, it is the @encode() string for the entire + method signature. In general, this is platform-defined. In order to + maintain an implementation-agnostic approach, we get the signature from a + known method of each time at runtime. + + NOTE: the GNU runtime's approach means that the methodReturnType differs + between different method signatures with the same return type. For + instance, a method -(id)foo:(int) will have a different methodReturnType + than a method -(id)foo. As far as I can see this is a bug, but Oolite only + supports binding to methods with no parameters, so this is not a problem. +*/ + +#ifndef NO_SHADERS + +#import "OOShaderUniformMethodType.h" +#import "OOMaths.h" + +static BOOL sInited = NO; +static const char *sTemplates[kOOShaderUniformTypeCount]; + +static void InitTemplates(void); +static const char *CopyTemplateForSelector(SEL selector); + + +OOShaderUniformType OOShaderUniformTypeFromMethodSignature(NSMethodSignature *signature) +{ + unsigned i; + const char *typeCode = NULL; + + if (EXPECT_NOT(sInited == NO)) InitTemplates(); + + typeCode = [signature methodReturnType]; + if (EXPECT_NOT(typeCode == NULL)) return kOOShaderUniformTypeInvalid; + + for (i = kOOShaderUniformTypeInvalid + 1; i != kOOShaderUniformTypeCount; ++i) + { + if (sTemplates[i] != NULL && strcmp(sTemplates[i], typeCode) == 0) return i; + } + + return kOOShaderUniformTypeInvalid; +} + + +@interface OOShaderUniformTypeMethodSignatureTemplateClass: NSObject + +- (float)floatMethod; +- (double)doubleMethod; +- (signed char)signedCharMethod; +- (unsigned char)unsignedCharMethod; +- (signed short)signedShortMethod; +- (unsigned short)unsignedShortMethod; +- (signed int)signedIntMethod; +- (unsigned int)unsignedIntMethod; +- (signed long)signedLongMethod; +- (unsigned long)unsignedLongMethod; +- (Vector)vectorMethod; +- (Quaternion)quaternionMethod; +- (OOMatrix)matrixMethod; +- (NSPoint)pointMethod; +- (id)idMethod; + +@end + + +static void InitTemplates(void) +{ + #define GET_TEMPLATE(enumValue, sel) do { \ + sTemplates[enumValue] = CopyTemplateForSelector(@selector(sel)); \ + } while (0) + + GET_TEMPLATE(kOOShaderUniformTypeChar, signedCharMethod); + GET_TEMPLATE(kOOShaderUniformTypeUnsignedChar, unsignedCharMethod); + GET_TEMPLATE(kOOShaderUniformTypeShort, signedShortMethod); + GET_TEMPLATE(kOOShaderUniformTypeUnsignedShort, unsignedShortMethod); + GET_TEMPLATE(kOOShaderUniformTypeInt, signedIntMethod); + GET_TEMPLATE(kOOShaderUniformTypeUnsignedInt, unsignedIntMethod); + GET_TEMPLATE(kOOShaderUniformTypeLong, signedLongMethod); + GET_TEMPLATE(kOOShaderUniformTypeUnsignedLong, unsignedLongMethod); + GET_TEMPLATE(kOOShaderUniformTypeFloat, floatMethod); + GET_TEMPLATE(kOOShaderUniformTypeDouble, doubleMethod); + GET_TEMPLATE(kOOShaderUniformTypeVector, vectorMethod); + GET_TEMPLATE(kOOShaderUniformTypeQuaternion, quaternionMethod); + GET_TEMPLATE(kOOShaderUniformTypeMatrix, matrixMethod); + GET_TEMPLATE(kOOShaderUniformTypePoint, pointMethod); + GET_TEMPLATE(kOOShaderUniformTypeObject, idMethod); + + sInited = YES; +} + + +static const char *CopyTemplateForSelector(SEL selector) +{ + NSMethodSignature *signature = nil; + const char *typeCode = NULL; + + signature = [OOShaderUniformTypeMethodSignatureTemplateClass instanceMethodSignatureForSelector:selector]; + typeCode = [signature methodReturnType]; + + /* typeCode is *probably* a constant, but this isn't formally guaranteed + as far as I'm aware, so we make a copy of it. + */ + return typeCode ? strdup(typeCode) : NULL; +} + + +@implementation OOShaderUniformTypeMethodSignatureTemplateClass: NSObject + +- (signed char)signedCharMethod +{ + return 0; +} + + +- (unsigned char)unsignedCharMethod +{ + return 0; +} + + +- (signed short)signedShortMethod +{ + return 0; +} + + +- (unsigned short)unsignedShortMethod +{ + return 0; +} + + +- (signed int)signedIntMethod +{ + return 0; +} + + +- (unsigned int)unsignedIntMethod +{ + return 0; +} + + +- (signed long)signedLongMethod +{ + return 0; +} + + +- (unsigned long)unsignedLongMethod +{ + return 0; +} + + +- (float)floatMethod +{ + return 0.0f; +} + + +- (double)doubleMethod +{ + return 0.0; +} + + +- (Vector)vectorMethod +{ + Vector v = {0}; + return v; +} + + +- (Quaternion)quaternionMethod +{ + Quaternion q = {0}; + return q; +} + + +- (OOMatrix)matrixMethod +{ + return kZeroMatrix; +} + + +- (NSPoint)pointMethod +{ + return NSZeroPoint; +} + + +- (id)idMethod +{ + return nil; +} + +@end + +#endif // NO_SHADERS diff --git a/src/Core/Materials/OOSingleTextureMaterial.h b/src/Core/Materials/OOSingleTextureMaterial.h new file mode 100644 index 00000000..eed71c20 --- /dev/null +++ b/src/Core/Materials/OOSingleTextureMaterial.h @@ -0,0 +1,73 @@ +/* + +OOSingleTextureMaterial.h + +A material with a single texture (and no shaders). + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOBasicMaterial.h" + +@class OOTexture; + + +@interface OOSingleTextureMaterial: OOBasicMaterial +{ + OOTexture *_texture; +} + +/* In addition to OOBasicMateral configuration keys, an OOTexture + configuration dictionary may be used. If there is a "texture" entry, it + will be used; otherwise, if there is a "textures" array, its first member + will be used. + + If the found OOTexture config dictionary contains a "name" key, it will be + used in preference to the name parameter. +*/ +- (id)initWithName:(NSString *)name configuration:(NSDictionary *)configuration; + +// Designated initializer +- (id) initWithName:(NSString *)name texture:(OOTexture *)texture configuration:(NSDictionary *)configuration; + +@end diff --git a/src/Core/Materials/OOSingleTextureMaterial.m b/src/Core/Materials/OOSingleTextureMaterial.m new file mode 100644 index 00000000..a0c729b5 --- /dev/null +++ b/src/Core/Materials/OOSingleTextureMaterial.m @@ -0,0 +1,132 @@ +/* + +OOSingleTextureMaterial.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSingleTextureMaterial.h" +#import "OOTexture.h" +#import "OOCollectionExtractors.h" +#import "OOFunctionAttributes.h" + + +@implementation OOSingleTextureMaterial + +- (id)initWithName:(NSString *)name configuration:(NSDictionary *)configuration +{ + id texSpec = nil; + + if (configuration != nil) + { + texSpec = [configuration textureSpecifierForKey:@"diffuse_map" defaultName:name]; + } + else + { + texSpec = name; + } + + return [self initWithName:name + texture:[OOTexture textureWithConfiguration:texSpec] + configuration:configuration]; +} + + +- (id) initWithName:(NSString *)name texture:(OOTexture *)texture configuration:(NSDictionary *)configuration +{ + if (name != nil) + { + self = [super initWithName:name configuration:configuration]; + if (self != nil) + { + _texture = [texture retain]; + } + } + + if (_texture == nil) + { + [self release]; + return nil; + } + + return self; +} + + +- (void)dealloc +{ + [self willDealloc]; + [_texture release]; + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{%@}", [self class], self, _texture]; +} + + +- (BOOL)doApply +{ + if (EXPECT_NOT(![super doApply])) return NO; + + [_texture apply]; + return YES; +} + + +- (void)unapplyWithNext:(OOMaterial *)next +{ + if (![next isKindOfClass:[OOSingleTextureMaterial class]]) [OOTexture applyNone]; + [super unapplyWithNext:next]; +} + + +- (void)ensureFinishedLoading +{ + [_texture ensureFinishedLoading]; +} + +@end diff --git a/src/Core/Materials/OOTexture.h b/src/Core/Materials/OOTexture.h new file mode 100644 index 00000000..78661dd1 --- /dev/null +++ b/src/Core/Materials/OOTexture.h @@ -0,0 +1,253 @@ +/* + +OOTexture.h + +Load, track and manage textures. In general, this should be used through an +OOMaterial. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + +#import "OOOpenGL.h" +#import "OOWeakReference.h" + +@class OOTextureLoader; + + +enum +{ + kOOTextureMinFilterDefault = 0x0000UL, + kOOTextureMinFilterNearest = 0x0001UL, + kOOTextureMinFilterLinear = 0x0002UL, + kOOTextureMinFilterMipMap = 0x0003UL, + + kOOTextureMagFilterNearest = 0x0000UL, + kOOTextureMagFilterLinear = 0x0004UL, + + kOOTextureNoShrink = 0x0010UL, + kOOTextureRepeatS = 0x0020UL, + kOOTextureRepeatT = 0x0040UL, + kOOTextureAllowRectTexture = 0x0080UL, // Indicates that GL_TEXTURE_RECTANGLE_EXT may be used instead of GL_TEXTURE_2D. See -texCoordsScale for a discussion of rectangle textures. + kOOTextureNoFNFMessage = 0x0100UL, // Don't log file not found error + kOOTextureNeverScale = 0x0200UL, // Don't rescale texture, even if rect textures are not available. This *must not* be used for regular textures, but may be passed to OOTextureLoader when being used for other purposes. + kOOTextureAlphaMask = 0x0400UL, // Single-channel texture should be GL_ALPHA, not GL_LUMINANCE. No effect for multi-channel textures. + + kOOTextureMinFilterMask = 0x0003UL, + kOOTextureMagFilterMask = 0x0004UL, + kOOTextureFlagsMask = ~(kOOTextureMinFilterMask | kOOTextureMagFilterMask), + + kOOTextureDefaultOptions = kOOTextureMinFilterDefault | kOOTextureMagFilterLinear, + + kOOTextureDefinedFlags = kOOTextureMinFilterMask | kOOTextureMagFilterMask + | kOOTextureNoShrink +#if GL_EXT_texture_rectangle + | kOOTextureAllowRectTexture +#endif + | kOOTextureRepeatS + | kOOTextureRepeatT + | kOOTextureNoFNFMessage + | kOOTextureNeverScale + | kOOTextureAlphaMask, + + kOOTextureFlagsAllowedForRectangleTexture = + kOOTextureDefinedFlags & ~(kOOTextureRepeatS | kOOTextureRepeatT) +}; + + +#define kOOTextureDefaultAnisotropy 0.5 +#define kOOTextureDefaultLODBias -0.25 + + +typedef enum +{ + kOOTextureDataInvalid, + + kOOTextureDataRGBA, // GL_RGBA, GL_UNSIGNED_INT_8_8_8_8 little-endian/GL_UNSIGNED_INT_8_8_8_8_REV big-endian. + kOOTextureDataGrayscale +} OOTextureDataFormat; + + +@interface OOTexture: OOWeakRefObject +{ + NSString *_key; + uint8_t _loaded: 1, + _uploaded: 1, +#if GL_EXT_texture_rectangle + _isRectTexture: 1, +#endif + _valid: 1; + uint8_t _mipLevels; + + OOTextureLoader *_loader; + + void *_bytes; + GLuint _textureName; + uint32_t _width, + _height; + + OOTextureDataFormat _format; + uint32_t _options; +#if GL_EXT_texture_lod_bias + GLfloat _lodBias; +#endif +#if GL_EXT_texture_filter_anisotropic + float _anisotropy; +#endif +} + +/* Load a texture, looking in Textures directories. + + NOTE: anisotropy is normalized to the range [0, 1]. 1 means as high an + anisotropy setting as the hardware supports. + + This method may change; +textureWithConfiguration is generally more + appropriate. +*/ ++ (id)textureWithName:(NSString *)name + inFolder:(NSString*)directory + options:(uint32_t)options + anisotropy:(GLfloat)anisotropy + lodBias:(GLfloat)lodBias; + +/* Equivalent to textureWithName:name + inFolder:directory + options:kOOTextureDefaultOptions + anisotropy:kOOTextureDefaultAnisotropy + lodBias:kOOTextureDefaultLODBias +*/ ++ (id)textureWithName:(NSString *)name + inFolder:(NSString*)directory; + +/* Load a texure, looking in Textures directories, using configuration + dictionary or name. (That is, configuration may be either an NSDictionary + or an NSString.) + + Supported keys: + name (string, required) + min_filter (string, one of "default", "nearest", "linear", "mipmap") + max_filter (string, one of "default", "nearest", "linear") + noShrink (boolean) + repeat_s (boolean) + repeat_t (boolean) + anisotropy (real) + texture_LOD_bias (real) +*/ ++ (id)textureWithConfiguration:(id)configuration; + +/* Return the "null texture", a texture object representing an empty texture. + Applying the null texture is equivalent to calling [OOTexture applyNone]. +*/ ++ (id) nullTexture; + + +/* Bind the texture to the current texture unit. + This will block until loading is completed. +*/ +- (void)apply; + ++ (void)applyNone; + +/* Ensure texture is loaded. This is required because setting up textures + inside display lists isn't allowed. +*/ +- (void)ensureFinishedLoading; + +/* Dimensions in pixels. + This will block until loading is completed. +*/ +- (NSSize)dimensions; + + +/* Dimensions in texture coordinates. + + If kOOTextureAllowRectTexture is set, and GL_EXT_texture_rectangle is + available, textures whose dimensions are not powers of two will be loaded + as rectangle textures. Rectangle textures use unnormalized co-ordinates; + that is, co-oridinates range from 0 to the actual size of the texture + rather than 0 to 1. Thus, for rectangle textures, -texCoordsScale returns + -dimensions (with the required wait for loading) for a rectangle texture. + For non-rectangle textures, (1, 1) is returned without delay. If the + texture has power-of-two dimensions, it will be loaded as a normal + texture. + + Rectangle textures have additional limitations: kOOTextureMinFilterMipMap + is not supported (kOOTextureMinFilterLinear will be used instead), and + kOOTextureRepeatS/kOOTextureRepeatT will be ignored. + + Note that 'rectangle texture' is a misnomer; non-rectangle textures may + be rectangular, as long as their sides are powers of two. Non-power-of-two + textures would be more descriptive, but this phrase is used for the + extension that allows 'normal' textures to have non-power-of-two sides + without additional restrictions. It is intended that OOTexture should + support this in future, but this shouldn’t affect the interface, only + avoid the scaling-to-power-of-two stage. +*/ +- (NSSize)texCoordsScale; + +/* OpenGL texture name. + Not reccomended, but required for legacy TextureStore. +*/ +- (GLint)glTextureName; + +// Forget all cached textures so new texture objects will reload. ++ (void)clearCache; + +// Called by OOGraphicsResetManager as necessary. ++ (void)rebindAllTextures; + +@end + + +@interface NSDictionary (OOTextureConveniences) +// Returns either a dictionary or a string. +- (id)textureSpecifierForKey:(id)key defaultName:(NSString *)name; +@end + +@interface NSArray (OOTextureConveniences) +// Returns either a dictionary or a string. +- (id)textureSpecifierAtIndex:(unsigned)index defaultName:(NSString *)name; +@end + +id OOTextureSpecFromObject(id object, NSString *defaultName); diff --git a/src/Core/Materials/OOTexture.m b/src/Core/Materials/OOTexture.m new file mode 100644 index 00000000..f7246bea --- /dev/null +++ b/src/Core/Materials/OOTexture.m @@ -0,0 +1,737 @@ +/* + + OOTexture.m + + Oolite + Copyright (C) 2004-2008 Giles C Williams and contributors + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + MA 02110-1301, USA. + + + This file may also be distributed under the MIT/X11 license: + + Copyright (C) 2007 Jens Ayton + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ + +#import "OOTexture.h" +#import "OONullTexture.h" +#import "OOTextureLoader.h" +#import "OOCollectionExtractors.h" +#import "Universe.h" +#import "ResourceManager.h" +#import "OOOpenGLExtensionManager.h" +#import "OOMacroOpenGL.h" +#import "OOCPUInfo.h" +#import "OOCache.h" + + +/* Texture caching: + two parallel caching mechanisms are used. sInUseTextures tracks all live + texture objects, without retaining them (using NSValues to refer to the + objects). sRecentTextures tracks up to kRecentTexturesCount textures which + have been used recently, and retains them. + + This means that the number of live texture objects will never fall below + 80% of kRecentTexturesCount (80% comes from the behaviour of OOCache), but + old textures will eventually be released. If the number of active textures + exceeds kRecentTexturesCount, all of them will be reusable through + sInUseTextures, but only a most-recently-fetched subset will be kept + around by the cache when the number drops. + + Note that any texture in sRecentTextures will also be in sInUseTextures, + but not necessarily vice versa. + */ +enum +{ + kRecentTexturesCount = 50 +}; + +static NSMutableDictionary *sInUseTextures = nil; +static OOCache *sRecentTextures = nil; + + +static BOOL sCheckedExtensions = NO; + + +#if OOLITE_BIG_ENDIAN +#define RGBA_IMAGE_TYPE GL_UNSIGNED_INT_8_8_8_8_REV +#elif OOLITE_LITTLE_ENDIAN +#define RGBA_IMAGE_TYPE GL_UNSIGNED_INT_8_8_8_8 +#else +#error Neither OOLITE_BIG_ENDIAN nor OOLITE_LITTLE_ENDIAN is defined as nonzero! +#endif + + +// Anisotropic filtering +#if GL_EXT_texture_filter_anisotropic +static BOOL sAnisotropyAvailable; +static float sAnisotropyScale; // Scale of anisotropy values +#else +#define sAnisotropyAvailable (NO) +#define sAnisotropyScale (0) +#warning GL_EXT_texture_filter_anisotropic unavailble -- are you using an up-to-date glext.h? +#endif + + +// CLAMP_TO_EDGE (OK, requiring OpenGL 1.2 wouln't be _that_ big a deal...) +#if !defined(GL_CLAMP_TO_EDGE) && GL_SGIS_texture_edge_clamp +#define GL_CLAMP_TO_EDGE GL_CLAMP_TO_EDGE_SGIS +#endif + +#ifdef GL_CLAMP_TO_EDGE +static BOOL sClampToEdgeAvailable; +#else +#warning GL_CLAMP_TO_EDGE (OpenGL 1.2) and GL_SGIS_texture_edge_clamp are unavialble -- are you using an up-to-date gl.h? +#define sClampToEdgeAvailable (NO) +#define GL_CLAMP_TO_EDGE GL_CLAMP +#endif + + +// Client storage: reduce copying by requiring the app to keep data around +#if GL_APPLE_client_storage + +#define OO_GL_CLIENT_STORAGE (1) +static inline void EnableClientStorage(void) +{ + OO_ENTER_OPENGL(); + glPixelStorei(GL_UNPACK_CLIENT_STORAGE_APPLE, GL_TRUE); +} +// #elif in any equivalents on other platforms here +#else +#define OO_GL_CLIENT_STORAGE (0) +#define EnableClientStorage() do {} while (0) +#endif + +#if OO_GL_CLIENT_STORAGE +static BOOL sClientStorageAvialable; +#else +#define sClientStorageAvialable (NO) +#endif + + +#if GL_EXT_texture_lod_bias +static BOOL sTextureLODBiasAvailable; +#else +#define sTextureLODBiasAvailable (NO) +#endif + + +#if GL_EXT_texture_rectangle +static BOOL sRectangleTextureAvailable; +#else +#define sRectangleTextureAvailable (NO) +#endif + + +@interface OOTexture (OOPrivate) + +- (id)initWithPath:(NSString *)path key:(NSString *)key options:(uint32_t)options anisotropy:(float)anisotropy lodBias:(GLfloat)lodBias; +- (void)setUpTexture; +- (void)uploadTexture; +- (void)uploadTextureDataWithMipMap:(BOOL)mipMap format:(OOTextureDataFormat)format; + +- (void)forceRebind; + ++ (void)checkExtensions; + +@end + + +@implementation OOTexture + ++ (id)textureWithName:(NSString *)name + inFolder:(NSString*)directory + options:(uint32_t)options + anisotropy:(GLfloat)anisotropy + lodBias:(GLfloat)lodBias +{ + NSString *key = nil; + OOTexture *result = nil; + NSString *path = nil; + BOOL noFNF; + + if (EXPECT_NOT(name == nil)) return nil; + if (EXPECT_NOT(!sCheckedExtensions)) [self checkExtensions]; + + // Set default flags if needed + if ((options & kOOTextureMinFilterMask) == kOOTextureMinFilterDefault) + { + if ([UNIVERSE reducedDetail]) + { + options |= kOOTextureMinFilterLinear; + } + else + { + options |= kOOTextureMinFilterMipMap; + } + } + + if (options & kOOTextureAllowRectTexture) + { + // Apply rectangle texture restrictions (regardless of whether rectangle textures are available, for consistency) + options &= kOOTextureFlagsAllowedForRectangleTexture; + if ((options & kOOTextureMinFilterMask) == kOOTextureMinFilterMipMap) + { + options = (kOOTextureMinFilterMask & ~kOOTextureMinFilterMask) | kOOTextureMinFilterLinear; + } + +#if GL_EXT_texture_rectangle + if (!sRectangleTextureAvailable) + { + options &= ~kOOTextureAllowRectTexture; + } +#endif // Else, options &= kOOTextureDefinedFlags below will clear the flag + } + + options &= kOOTextureDefinedFlags; + + if (!sAnisotropyAvailable || (options & kOOTextureMinFilterMask) != kOOTextureMinFilterMipMap) + { + anisotropy = 0.0f; + } + if (!sTextureLODBiasAvailable || (options & kOOTextureMinFilterMask) != kOOTextureMinFilterMipMap) + { + lodBias = 0.0f; + } + + noFNF = (options & kOOTextureNoFNFMessage) != 0; + options &= ~kOOTextureNoFNFMessage; + + // Look for existing texture + key = [NSString stringWithFormat:@"%@%@%@:0x%.4X/%g/%g", directory ? directory : (NSString *)@"", directory ? @"/" : @"", name, options, anisotropy, lodBias]; + result = (id)[[sInUseTextures objectForKey:key] pointerValue]; + if (result == nil) + { + // OOLog(@"texture.caching", @":::------::: key NOT found in cache : '%@'.", key); + path = [ResourceManager pathForFileNamed:name inFolder:directory]; + if (path == nil) + { + if (!noFNF) OOLog(kOOLogFileNotFound, @"----- WARNING: Could not find texture file \"%@\". Used default no textures material instead.", name); + return nil; + } + + // No existing texture, load texture... + result = [[[OOTexture alloc] initWithPath:path key:key options:options anisotropy:anisotropy lodBias:lodBias] autorelease]; + +#ifndef OOTEXTURE_NO_CACHE + if (result != nil) + { + // ...and remember it. Use an NSValue so sInUseTextures doesn't retain the texture. + if (sInUseTextures == nil) sInUseTextures = [[NSMutableDictionary alloc] init]; + [sInUseTextures setObject:[NSValue valueWithPointer:result] forKey:key]; + } +#endif + } + + + return result; +} + + ++ (id)textureWithName:(NSString *)name + inFolder:(NSString*)directory +{ + return [self textureWithName:name + inFolder:directory + options:kOOTextureDefaultOptions + anisotropy:kOOTextureDefaultAnisotropy + lodBias:kOOTextureDefaultLODBias]; +} + + ++ (id)textureWithConfiguration:(id)configuration +{ + NSString *name = nil; + NSString *filterString = nil; + uint32_t options = 0; + GLfloat anisotropy; + GLfloat lodBias; + + if ([configuration isKindOfClass:[NSString class]]) + { + name = configuration; + if ([name isEqual:@""]) return nil; + options = kOOTextureDefaultOptions; + anisotropy = kOOTextureDefaultAnisotropy; + lodBias = kOOTextureDefaultLODBias; + } + else if ([configuration isKindOfClass:[NSDictionary class]]) + { + name = [(NSDictionary *)configuration stringForKey:@"name"]; + if (name == nil) + { + OOLog(@"texture.load.noName", @"Invalid texture configuration dictionary (must specify name):\n%@", configuration); + return nil; + } + + filterString = [configuration stringForKey:@"min_filter" defaultValue:@"default"]; + if ([filterString isEqualToString:@"nearest"]) options |= kOOTextureMinFilterNearest; + else if ([filterString isEqualToString:@"linear"]) options |= kOOTextureMinFilterLinear; + else if ([filterString isEqualToString:@"mipmap"]) options |= kOOTextureMinFilterMipMap; + else options |= kOOTextureMinFilterDefault; // Covers "default" + + filterString = [configuration stringForKey:@"mag_filter" defaultValue:@"default"]; + if ([filterString isEqualToString:@"nearest"]) options |= kOOTextureMagFilterNearest; + else options |= kOOTextureMagFilterLinear; // Covers "default" and "linear" + + if ([configuration boolForKey:@"no_shrink" defaultValue:NO]) options |= kOOTextureNoShrink; + if ([configuration boolForKey:@"repeat_s" defaultValue:NO]) options |= kOOTextureRepeatS; + if ([configuration boolForKey:@"repeat_t" defaultValue:NO]) options |= kOOTextureRepeatT; + anisotropy = [configuration floatForKey:@"anisotropy" defaultValue:kOOTextureDefaultAnisotropy]; + lodBias = [configuration floatForKey:@"texture_LOD_bias" defaultValue:kOOTextureDefaultLODBias]; + } + else + { + // Bad type + if (configuration != nil) OOLog(kOOLogParameterError, @"%s: expected string or dictionary, got %@.", __PRETTY_FUNCTION__, [configuration class]); + return nil; + } + + return [self textureWithName:name inFolder:@"Textures" options:options anisotropy:anisotropy lodBias:lodBias]; +} + + ++ (id) nullTexture +{ + return [OONullTexture sharedNullTexture]; +} + + +- (void)dealloc +{ + OOLog(@"texture.dealloc", @"Deallocating and uncaching texture %@", self); + + if (_key != nil) + { + [sInUseTextures removeObjectForKey:_key]; + //assert([sRecentTextures objectForKey:_key] != self); //miscount in autorelease + [sRecentTextures removeObjectForKey:_key]; // make sure there's no reference left inside sRecentTexture + [_key release]; + } + + if (_loaded) + { + if (_textureName != 0) GLRecycleTextureName(_textureName, _mipLevels); + free(_bytes); + } + + [_loader release]; + + [super dealloc]; +} + + +- (NSString *)description +{ + NSString *stateDesc = nil; + + if (_loaded) + { + if (_valid) + { + stateDesc = [NSString stringWithFormat:@"%u x %u", _width, _height]; + } + else + { + stateDesc = @"LOAD ERROR"; + } + } + else + { + stateDesc = @"loading"; + } + + return [NSString stringWithFormat:@"<%@ %p [%u]>{%@, %@}", [self className], self, [self retainCount], _key, stateDesc]; +} + + +- (void)apply +{ + OO_ENTER_OPENGL(); + + if (EXPECT_NOT(!_loaded)) [self setUpTexture]; + else if (EXPECT_NOT(!_uploaded)) [self uploadTexture]; + else glBindTexture(GL_TEXTURE_2D, _textureName); + +#if GL_EXT_texture_lod_bias + if (sTextureLODBiasAvailable) glTexEnvf(GL_TEXTURE_FILTER_CONTROL_EXT, GL_TEXTURE_LOD_BIAS_EXT, _lodBias); +#endif +} + + ++ (void)applyNone +{ + OO_ENTER_OPENGL(); + + glBindTexture(GL_TEXTURE_2D, 0); +} + + +- (void)ensureFinishedLoading +{ + if (EXPECT_NOT(!_loaded)) [self setUpTexture]; +} + + +- (NSSize)dimensions +{ + [self ensureFinishedLoading]; + + return NSMakeSize(_width, _height); +} + + +- (NSSize)texCoordsScale +{ +#if GL_EXT_texture_rectangle + if (_loaded) + { + if (!_isRectTexture) + { + return NSMakeSize(1.0f, 1.0f); + } + else + { + return NSMakeSize(_width, _height); + } + } + else + { + // Not loaded + if (!_options & kOOTextureAllowRectTexture) + { + return NSMakeSize(1.0f, 1.0f); + } + else + { + // Finishing may clear the rectangle texture flag (if the texture turns out to be POT) + [self ensureFinishedLoading]; + return [self texCoordsScale]; + } + } +#else + return NSMakeSize(1.0f, 1.0f); +#endif +} + + +- (GLint)glTextureName +{ + [self ensureFinishedLoading]; + + return _textureName; +} + + ++ (void)clearCache +{ + [sInUseTextures autorelease]; + sInUseTextures = nil; + [sRecentTextures autorelease]; + sRecentTextures = nil; +} + + ++ (void)rebindAllTextures +{ + NSEnumerator *textureEnum = nil; + id texture = nil; + + for (textureEnum = [sInUseTextures objectEnumerator]; (texture = [[textureEnum nextObject] pointerValue]); ) + { + [texture forceRebind]; + } +} + +@end + + +@implementation OOTexture (OOPrivate) + +- (id)initWithPath:(NSString *)path key:(NSString *)inKey options:(uint32_t)options anisotropy:(float)anisotropy lodBias:(GLfloat)inLodBias +{ + self = [super init]; + if (EXPECT_NOT(self == nil)) return nil; + + _loader = [[OOTextureLoader loaderWithPath:path options:options] retain]; + if (EXPECT_NOT(_loader == nil)) + { + [self release]; + return nil; + } + + _options = options; + +#if GL_EXT_texture_filter_anisotropic + _anisotropy = OOClamp_0_1_f(anisotropy) * sAnisotropyScale; +#endif +#if GL_EXT_texture_lod_bias + _lodBias = inLodBias; +#endif + + _key = [inKey copy]; + +#ifndef OOTEXTURE_NO_CACHE + // Add self to recent textures cache + if (EXPECT_NOT(sRecentTextures == nil)) + { + sRecentTextures = [[OOCache alloc] init]; + [sRecentTextures setName:@"recent textures"]; + [sRecentTextures setAutoPrune:YES]; + [sRecentTextures setPruneThreshold:kRecentTexturesCount]; + } + [sRecentTextures setObject:self forKey:_key]; +#endif + + return self; +} + + +- (void)setUpTexture +{ + // This will block until loading is completed, if necessary. + if ([_loader getResult:&_bytes format:&_format width:&_width height:&_height]) + { + [self uploadTexture]; + } + else + { + _textureName = 0; + _valid = NO; + _uploaded = YES; + } + + _loaded = YES; + + [_loader release]; + _loader = nil; +} + + +- (void)uploadTexture +{ + GLint clampMode; + GLint filter; + BOOL mipMap = NO; + + OO_ENTER_OPENGL(); + + if (!_uploaded) + { + _textureName = GLAllocateTextureName(); + glBindTexture(GL_TEXTURE_2D, _textureName); + + // Select wrap mode + clampMode = sClampToEdgeAvailable ? GL_CLAMP_TO_EDGE : GL_CLAMP; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, (_options & kOOTextureRepeatS) ? GL_REPEAT : clampMode); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, (_options & kOOTextureRepeatT) ? GL_REPEAT : clampMode); + + // Select min filter + filter = _options & kOOTextureMinFilterMask; + if (filter == kOOTextureMinFilterNearest) filter = GL_NEAREST; + else if (filter == kOOTextureMinFilterMipMap) + { + mipMap = YES; + filter = GL_LINEAR_MIPMAP_LINEAR; + } + else filter = GL_LINEAR; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); + +#if GL_EXT_texture_filter_anisotropic + if (sAnisotropyAvailable && mipMap && 1.0 < _anisotropy) + { + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, _anisotropy); + } +#endif + + // Select mag filter + filter = _options & kOOTextureMagFilterMask; + if (filter == kOOTextureMagFilterNearest) filter = GL_NEAREST; + else filter = GL_LINEAR; + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter); + + if (sClientStorageAvialable) EnableClientStorage(); + + [self uploadTextureDataWithMipMap:mipMap format:_format]; + + OOLog(@"texture.upload", @"Uploaded texture %u (%ux%u pixels, %@)", _textureName, _width, _height, _key); + + _valid = YES; + _uploaded = YES; + } +} + + +- (void)uploadTextureDataWithMipMap:(BOOL)mipMap format:(OOTextureDataFormat)format +{ + GLint glFormat, internalFormat, type; + unsigned w = _width, + h = _height, + level = 0; + char *bytes = _bytes; + uint8_t planes = OOTexturePlanesForFormat(format); + + OO_ENTER_OPENGL(); + + switch (format) + { + case kOOTextureDataRGBA: + glFormat = GL_RGBA; + internalFormat = GL_RGBA; + type = RGBA_IMAGE_TYPE; + break; + + case kOOTextureDataGrayscale: + if (_options & kOOTextureAlphaMask) + { + glFormat = GL_ALPHA8; + internalFormat = GL_ALPHA; + } + else + { + glFormat = GL_LUMINANCE8; + internalFormat = GL_LUMINANCE; + } + type = GL_UNSIGNED_BYTE; + break; + + default: + OOLog(kOOLogParameterError, @"Unexpected texture format %u.", format); + return; + } + + while (1 < w && 1 < h) + { + glTexImage2D(GL_TEXTURE_2D, level++, glFormat, w, h, 0, internalFormat, type, bytes); + if (!mipMap) return; + bytes += w * planes * h; + w >>= 1; + h >>= 1; + } + + _mipLevels = level - 1; + + // FIXME: GL_TEXTURE_MAX_LEVEL requires OpenGL 1.2. This should be fixed by generating all mip-maps for non-square textures so we don't need to use it. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, _mipLevels); +} + + +- (void)forceRebind +{ + if (_loaded && _uploaded && _valid) + { + _uploaded = NO; + GLRecycleTextureName(_textureName, _mipLevels); + _textureName = 0; + } +} + + ++ (void)checkExtensions +{ + OO_ENTER_OPENGL(); + + sCheckedExtensions = YES; + + OOOpenGLExtensionManager *extMgr = [OOOpenGLExtensionManager sharedManager]; + +#if GL_EXT_texture_filter_anisotropic + sAnisotropyAvailable = [extMgr haveExtension:@"GL_EXT_texture_filter_anisotropic"]; + glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &sAnisotropyScale); + sAnisotropyScale *= OOClamp_0_1_f([[NSUserDefaults standardUserDefaults] floatForKey:@"texture-anisotropy-scale" defaultValue:0.5]); +#endif + +#ifdef GL_CLAMP_TO_EDGE + // GL_CLAMP_TO_EDGE requires OpenGL 1.2 or later. Oolite probably does too... + sClampToEdgeAvailable = (2 < [extMgr minorVersionNumber]) || [extMgr haveExtension:@"GL_SGIS_texture_edge_clamp"]; +#endif + +#if GL_APPLE_client_storage + sClientStorageAvialable = [extMgr haveExtension:@"GL_APPLE_client_storage"]; +#endif + +#if GL_EXT_texture_lod_bias + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"use-texture-lod-bias" defaultValue:YES]) + { + sTextureLODBiasAvailable = [extMgr haveExtension:@"GL_EXT_texture_lod_bias"]; + } + else + { + sTextureLODBiasAvailable = NO; + } +#endif + +#if GL_EXT_texture_rectangle + sRectangleTextureAvailable = [extMgr haveExtension:@"GL_EXT_texture_rectangle"]; +#endif +} + +@end + + +@implementation NSDictionary (OOTextureConveniences) + +- (id)textureSpecifierForKey:(id)key defaultName:(NSString *)name +{ + return OOTextureSpecFromObject([self objectForKey:key], name); +} + +@end + +@implementation NSArray (OOTextureConveniences) + +- (id)textureSpecifierAtIndex:(unsigned)index defaultName:(NSString *)name +{ + return OOTextureSpecFromObject([self objectAtIndex:index], name); +} + +@end + +id OOTextureSpecFromObject(id object, NSString *defaultName) +{ + NSMutableDictionary *mutableResult = nil; + + if (object == nil) return [[defaultName copy] autorelease]; + if ([object isKindOfClass:[NSString class]]) return [[object copy] autorelease]; + if (![object isKindOfClass:[NSDictionary class]]) return nil; + + // If we're here, it's a dictionary. + if (defaultName == nil || [mutableResult objectForKey:@"name"] != nil) return object; + + // If we get here, there's no "name" key and there is a default, so we fill it in: + mutableResult = [object mutableCopy]; + [mutableResult setObject:defaultName forKey:@"name"]; + return [mutableResult autorelease]; +} diff --git a/src/Core/Materials/OOTextureLoader.h b/src/Core/Materials/OOTextureLoader.h new file mode 100644 index 00000000..a0e62acd --- /dev/null +++ b/src/Core/Materials/OOTextureLoader.h @@ -0,0 +1,131 @@ +/* + +OOTextureLoader.h + +Manage asynchronous (threaded) loading of textures. In general, this should be +used through OOTexture. + +Note: interface is likely to change in future to support other buffer (like +S3TC/DXT#). + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOTexture.h" + +#define INSTRUMENT_TEXTURE_LOADING (!defined NDEBUG) + + +@interface OOTextureLoader: NSObject +{ + NSString *path; + + uint8_t generateMipMaps: 1, + scaleAsNormalMap: 1, + avoidShrinking: 1, + noScalingWhatsoever: 1, +#if INSTRUMENT_TEXTURE_LOADING + debugHasLoaded: 1, +#endif + ready: 1; + OOTextureDataFormat format; + + void *data; + uint32_t width, + height, + rowBytes; +} + ++ (id)loaderWithPath:(NSString *)path options:(uint32_t)options; + +- (BOOL)isReady; + +/* Return value indicates success. This may only be called once (subsequent + attempts will return failure), and only on the main thread. +*/ +- (BOOL)getResult:(void **)outData + format:(OOTextureDataFormat *)outFormat + width:(uint32_t *)outWidth + height:(uint32_t *)outHeight; + + +/*** Subclass interface; do not use on pain of pain. Unless you're subclassing. ***/ + +// Subclasses shouldn't do much on init, because of the whole asynchronous thing. +- (id)initWithPath:(NSString *)path options:(uint32_t)options; + +- (NSString *)path; + +/* Load data, setting up data, width, and height, and rowBytes if it's not + width * 4. + + Thread-safety concerns: this will be called in a worker thread, and there + may be several worker threads. The caller takes responsibility for + autorelease pools and exception safety. + + Data must be little-endian ARGB (FIXME: is this correct?) + + Superclass will handle scaling and mip-map generation. Data must be + allocated with malloc() family. +*/ +- (void)loadTexture; + +@end + + +static inline uint8_t OOTexturePlanesForFormat(OOTextureDataFormat format) +{ + switch (format) + { + case kOOTextureDataRGBA: + return 4; + + case kOOTextureDataGrayscale: + return 1; + + case kOOTextureDataInvalid: + break; + } + + return 0; +} diff --git a/src/Core/Materials/OOTextureLoader.m b/src/Core/Materials/OOTextureLoader.m new file mode 100644 index 00000000..98dd63b6 --- /dev/null +++ b/src/Core/Materials/OOTextureLoader.m @@ -0,0 +1,506 @@ +/* + +OOTextureLoader.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOTextureLoader.h" +#import "OOPNGTextureLoader.h" +#import "OOFunctionAttributes.h" +#import "OOCollectionExtractors.h" +#import "OOMaths.h" +#import "Universe.h" +#import "OOTextureScaling.h" +#import "OOCPUInfo.h" +#import +#import "OOAsyncQueue.h" +#import "NSThreadOOExtensions.h" + + +static OOAsyncQueue *sLoadQueue, + *sReadyQueue; +static unsigned sGLMaxSize; +static uint32_t sUserMaxSize; +static BOOL sReducedDetail; +static BOOL sHaveNPOTTextures = NO; // TODO: support "true" non-power-of-two textures. +static BOOL sHaveSetUp = NO; + + +enum +{ + kMaxWorkThreads = 4U +}; + + +@interface OOTextureLoader (OOPrivate) + ++ (void)setUp; + +@end + + +@interface OOTextureLoader (OOTextureLoadingThread) + ++ (void)queueTask:(NSNumber *)threadNumber; +- (void)performLoad; +- (void)applySettings; +- (void)getDesiredWidth:(uint32_t *)outDesiredWidth andHeight:(uint32_t *)outDesiredHeight; + +@end + + +@interface OOTextureLoader (OOCompletionNotification) + +- (void)waitForCompletion; + +@end + + +@implementation OOTextureLoader + ++ (id)loaderWithPath:(NSString *)inPath options:(uint32_t)options +{ + NSString *extension = nil; + id result = nil; + + if (inPath == nil) return nil; + if (!sHaveSetUp) [self setUp]; + + // Get reduced detail setting (every time, in case it changes; we don't want to call through to Universe on the loading thread in case the implementation becomes non-trivial). + sReducedDetail = [UNIVERSE reducedDetail]; + + // Get a suitable loader. FIXME -- this should sniff the data instead of relying on extensions. + extension = [[inPath pathExtension] lowercaseString]; + if ([extension isEqualToString:@"png"]) + { + result = [[OOPNGTextureLoader alloc] initWithPath:inPath options:options]; + [result autorelease]; + } + else + { + OOLog(@"textureLoader.unknownType", @"Can't use %@ as a texture - extension \"%@\" does not identify a known type.", inPath, extension); + } + + if (result != nil) + { + if (![sLoadQueue enqueue:result]) result = nil; + } + + return result; +} + + +- (id)initWithPath:(NSString *)inPath options:(uint32_t)options +{ + self = [super init]; + if (self == nil) return nil; + + path = [inPath copy]; + if (EXPECT_NOT(path == nil)) + { + [self release]; + return nil; + } + + generateMipMaps = (options & kOOTextureMinFilterMask) == kOOTextureMinFilterMipMap; + avoidShrinking = (options & kOOTextureNoShrink) != 0; + noScalingWhatsoever = (options & kOOTextureNeverScale) != 0; + + return self; +} + + +- (void)dealloc +{ + [path autorelease]; + if (data != NULL) free(data); + + [super dealloc]; +} + + +- (NSString *)descriptionComponents +{ + NSString *state = nil; + + if (ready) + { + if (data != NULL) state = @"ready"; + else state = @"failed"; + } + else + { + state = @"loading"; +#if INSTRUMENT_TEXTURE_LOADING + if (debugHasLoaded) state = @"loaded"; +#endif + } + + return [NSString stringWithFormat:@"{%@ -- %@}", path, state]; +} + + +- (NSString *)shortDescriptionComponents +{ + return [path lastPathComponent]; +} + + +- (NSString *)path +{ + return path; +} + + +- (BOOL)isReady +{ + return ready; +} + + +- (BOOL)getResult:(void **)outData + format:(OOTextureDataFormat *)outFormat + width:(uint32_t *)outWidth + height:(uint32_t *)outHeight +{ + if (!ready) + { + [self waitForCompletion]; + } + + if (data != NULL) + { + if (outData != NULL) *outData = data; + if (outFormat != NULL) *outFormat = format; + if (outWidth != NULL) *outWidth = width; + if (outHeight != NULL) *outHeight = height; + + data = NULL; + return YES; + } + else + { + if (outData != NULL) *outData = NULL; + if (outFormat != NULL) *outFormat = kOOTextureDataInvalid; + if (outWidth != NULL) *outWidth = 0; + if (outHeight != NULL) *outHeight = 0; + + return NO; + } +} + + +- (void)loadTexture +{ + OOLog(kOOLogSubclassResponsibility, @"%s is a subclass responsibility!", __PRETTY_FUNCTION__); +} + + +@end + + +@implementation OOTextureLoader (OOPrivate) + ++ (void)setUp +{ + int threadCount, threadNumber = 1; + GLint maxSize; + + sLoadQueue = [[OOAsyncQueue alloc] init]; + sReadyQueue = [[OOAsyncQueue alloc] init]; + if (sLoadQueue == nil || sReadyQueue == nil) + { + OOLog(@"textureLoader.createQueues.failed", @"***** FATAL ERROR: could not set up texture loader queues!"); + exit(EXIT_FAILURE); + } + + // Set up loading threads. +#if OO_DEBUG + threadCount = kMaxWorkThreads; +#else + threadCount = MIN(OOCPUCount(), (unsigned)kMaxWorkThreads); +#endif + do + { + [NSThread detachNewThreadSelector:@selector(queueTask:) toTarget:self withObject:[NSNumber numberWithInt:threadNumber++]]; + } while (--threadCount > 0); + + // Load two maximum sizes - graphics hardware limit and user-specified limit. + glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxSize); + sGLMaxSize = MAX(maxSize, 64); + + // Why 0x80000000? Because it's the biggest number OORoundUpToPowerOf2() can handle. + sUserMaxSize = [[NSUserDefaults standardUserDefaults] unsignedIntForKey:@"max-texture-size" defaultValue:0x80000000]; + sUserMaxSize = OORoundUpToPowerOf2(sUserMaxSize); + sUserMaxSize = MAX(sUserMaxSize, 64U); + + sHaveSetUp = YES; +} + +@end + + +/*** Methods performed on the loader thread. ***/ + +@implementation OOTextureLoader (OOTextureLoadingThread) + ++ (void)queueTask:(NSNumber *)threadNumber +{ + NSAutoreleasePool *pool = nil; + OOTextureLoader *loader = nil; + + /* Lower thread priority so the loader doesn't go "Hey! This thread's + just woken up, let's give it exclusive use of the CPU for a second or + five!", thus stopping graphics from happening, which is somewhat + against the point. + + This leads to priority inversion when the main thread blocks for + texture load completion. I'm assuming people aren't going to be + running other CPU-hogging tasks at the same time as Oolite, so it + won't be a problem. + -- Ahruman + */ + [NSThread setThreadPriority:0.5]; + pool = [[NSAutoreleasePool alloc] init]; + [NSThread ooSetCurrentThreadName:[NSString stringWithFormat:@"OOTextureLoader loader thread %@", threadNumber]]; + [pool release]; + + for (;;) + { + pool = [[NSAutoreleasePool alloc] init]; + + loader = [sLoadQueue dequeue]; + [loader performLoad]; + + [pool release]; + } +} + + +- (void)performLoad +{ + NS_DURING + OOLog(@"textureLoader.asyncLoad", @"Loading texture %@", [path lastPathComponent]); + + [self loadTexture]; + if (data != NULL) [self applySettings]; + + OOLog(@"textureLoader.asyncLoad.done", @"Loading complete."); + NS_HANDLER + OOLog(@"textureLoader.asyncLoad.exception", @"***** Exception loading texture %@: %@ (%@).", path, [localException name], [localException reason]); + + // Be sure to signal load failure + if (data != NULL) + { + free(data); + data = NULL; + } + NS_ENDHANDLER + +#if INSTRUMENT_TEXTURE_LOADING + debugHasLoaded = YES; +#endif + [sReadyQueue enqueue:self]; +} + + +- (void)applySettings +{ + uint32_t desiredWidth, desiredHeight; + BOOL rescale; + void *newData = NULL; + size_t newSize; + uint8_t planes; + + planes = OOTexturePlanesForFormat(format); + + if (rowBytes == 0) rowBytes = width * planes; + [self getDesiredWidth:&desiredWidth andHeight:&desiredHeight]; + + // Rescale if needed. + rescale = (width != desiredWidth || height != desiredHeight); + if (rescale) + { + data = OOScalePixMap(data, width, height, planes, rowBytes, desiredWidth, desiredHeight, generateMipMaps); + if (EXPECT_NOT(data == NULL)) return; + + width = desiredWidth; + height = desiredHeight; + } + + // Generate mip maps if needed. + if (generateMipMaps && !rescale) + { + // Make space... + newSize = desiredWidth * planes * desiredHeight; + newSize = (newSize * 4) / 3; + newData = realloc(data, newSize); + if (newData != nil) data = newData; + else generateMipMaps = NO; + } + if (generateMipMaps) + { + OOGenerateMipMaps(data, width, height, planes); + } + + // All done. +} + + +- (void)getDesiredWidth:(uint32_t *)outDesiredWidth andHeight:(uint32_t *)outDesiredHeight +{ + uint32_t desiredWidth, desiredHeight; + + // Work out appropriate final size for textures. + if (!noScalingWhatsoever) + { + if (!sHaveNPOTTextures) + { + // Round to nearest power of two. NOTE: this is duplicated in OOTextureVerifierStage.m. + desiredWidth = OORoundUpToPowerOf2((2 * width) / 3); + desiredHeight = OORoundUpToPowerOf2((2 * height) / 3); + } + else + { + desiredWidth = width; + desiredHeight = height; + } + + desiredWidth = MIN(desiredWidth, sGLMaxSize); + desiredHeight = MIN(desiredHeight, sGLMaxSize); + + if (!avoidShrinking) + { + desiredWidth = MIN(desiredWidth, sUserMaxSize); + desiredHeight = MIN(desiredHeight, sUserMaxSize); + + if (sReducedDetail) + { + if (512 < desiredWidth) desiredWidth /= 2; + if (512 < desiredHeight) desiredHeight /= 2; + } + } + else + { + if (sReducedDetail || sUserMaxSize < desiredWidth || sUserMaxSize < desiredWidth) + { + // Permit a bit of shrinking for large textures + if (512 < desiredWidth) desiredWidth /= 2; + if (512 < desiredHeight) desiredHeight /= 2; + } + } + } + else + { + desiredWidth = width; + desiredHeight = height; + } + + if (outDesiredWidth != NULL) *outDesiredWidth = desiredWidth; + if (outDesiredHeight != NULL) *outDesiredHeight = desiredHeight; +} + +@end + + +@implementation OOTextureLoader (OOCompletionNotification) + +/* -waitForCompletion + In order for a texture loader to be considered loaded, it must be pulled + off the "ready queue". Since the order of items in the ready queue is not + necessarily (or generally) the order in which they're used, we keep pulling + texture loaders off and marking them as loaded until we get the one we're + looking for. If the loading isn't actually completed, we'll stall on the + dequeue operation until one of the loader threads pushes a loader. + + If INSTRUMENT_TEXTURE_LOADING, we log whether stalling occurred for each + loader. Rather than detecting this directly, we set a flag in the loader + thread to indicate whether the texture has been queued; if it has not yet + been queued when we start looking for it in the queue, we assume it will + stall and time the operation. NOTE: we could not simply use this flag to + check for completion and bypass the queue in all cases, because we need + to block when a stall occurs (busy-waiting is bad and counter-productive) + and we need to clean out the non-stalled loaders from the queue. + + Because loaders that happen to complete before the one we're waiting for + are dequeued and marked done as a side effect, asynchronous loads end up + being reported in batches. In each batch, the last loader listed is the + one for which -waitForCompletion was called. In many cases it will be + listed as an asynchronous load, because we needed to look in the queue but + there was no stall - it had already been enqueued by the time we started + looking. (This is the if (!debugHasLoaded) check at the top.) +*/ + +- (void)waitForCompletion +{ + OOTextureLoader *loader = nil; + +#if INSTRUMENT_TEXTURE_LOADING + NSTimeInterval start = 0; + if (!debugHasLoaded) start = [NSDate timeIntervalSinceReferenceDate]; +#endif + + do + { + // Dequeue a loader and mark it as done. + loader = [sReadyQueue dequeue]; + loader->ready = YES; + +#if INSTRUMENT_TEXTURE_LOADING + if (loader != self || start == 0) + { + OOLog(@"textureLoader.asyncLoad.notStall", @"Texture %@ loaded asynchronously.", [[loader path] lastPathComponent]); + } +#endif + } while (loader != self); // We don't control order, so keep looking until we get the one we care about. + +#if INSTRUMENT_TEXTURE_LOADING + if (start != 0) + { + OOLog(@"textureLoader.asyncLoad.stall", @"Waited %f seconds for texture %@ to load.", [NSDate timeIntervalSinceReferenceDate] - start, [path lastPathComponent]); + } +#endif +} + +@end diff --git a/src/Core/Materials/pngusr.h b/src/Core/Materials/pngusr.h new file mode 100644 index 00000000..67b57f80 --- /dev/null +++ b/src/Core/Materials/pngusr.h @@ -0,0 +1,45 @@ +/* pngusr.h: customise libpng build */ + +/* We only want to read PNGs */ +#define PNG_NO_WRITE_SUPPORTED + +/* No textures embedded in MNGs for us. */ +#define PNG_NO_MNG_FEATURES + +/* We provide our own libpng error/warning functions */ +#define PNG_NO_STDIO + +/* Read transformations we don't use */ +#define PNG_NO_READ_STRIP_ALPHA +#define PNG_NO_READ_SWAP +#define PNG_NO_READ_PACKSWAP +#define PNG_NO_READ_DITHER +#define PNG_NO_READ_GAMMA +#define PNG_NO_READ_INVERT_ALPHA +#define PNG_NO_READ_STRIP_ALPHA +#define PNG_NO_READ_USER_TRANSFORM +#define PNG_NO_READ_RGB_TO_GRAY +#define PNG_NO_READ_BACKGROUND +#define PNG_NO_READ_SHIFT + +#define PNG_NO_PROGRESSIVE_READ + +/* Real men access the info struct directly */ +#define PNG_NO_EASY_ACCESS + +/* Very definitely do not set PNG_THREAD_UNSAFE_OK */ + +/* Let libpng do its own malloc()ing */ +#define PNG_NO_USER_MEM + +/* Static size limit */ +#define PNG_NO_SET_USER_LIMITS +#define PNG_USER_WIDTH_MAX (65536L) +#define PNG_USER_HEIGHT_MAX PNG_USER_WIDTH_MAX + +/* We don't want any ancillary chunks. */ +#define PNG_NO_READ_TEXT +#define PNG_READ_ANCILLARY_CHUNKS_NOT_SUPPORTED + +/* tRNS chunk support has a vulnerability prior to libpng 1.2.18, and we don't need it anyway. */ +#define PNG_NO_READ_tRNS diff --git a/src/Core/NSDictionaryOOExtensions.h b/src/Core/NSDictionaryOOExtensions.h new file mode 100644 index 00000000..2d4274bb --- /dev/null +++ b/src/Core/NSDictionaryOOExtensions.h @@ -0,0 +1,61 @@ +/* + +NSDictionaryOOExtensions.h + +Extensions to NSDictionary. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@interface NSDictionary (OOExtensions) + +// These all return self if passed nil paramters. They all return a new immutable dictionary if sent to a mutable dictionary. +- (NSDictionary *) dictionaryByAddingObject:(id)object forKey:(id)key; +- (NSDictionary *) dictionaryByRemovingObjectForKey:(id)key; +- (NSDictionary *) dictionaryByAddingEntriesFromDictionary:(NSDictionary *)dictionary; + +@end diff --git a/src/Core/NSDictionaryOOExtensions.m b/src/Core/NSDictionaryOOExtensions.m new file mode 100644 index 00000000..943bb5c9 --- /dev/null +++ b/src/Core/NSDictionaryOOExtensions.m @@ -0,0 +1,95 @@ +/* + +NSDictionaryOOExtensions.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "NSDictionaryOOExtensions.h" + + +@implementation NSDictionary (OOExtensions) + +- (NSDictionary *) dictionaryByAddingObject:(id)object forKey:(id)key +{ + // Note: object lifetime issues aside, we need to copy and autorelease so that the right thing happens for mutable dictionaries. + if (object == nil || key == nil) return [[self copy] autorelease]; + + NSMutableDictionary *temp = [self mutableCopy]; + [temp setObject:object forKey:key]; + NSDictionary *result = [[temp copy] autorelease]; + [temp release]; + + return result; +} + + +- (NSDictionary *) dictionaryByRemovingObjectForKey:(id)key +{ + // Note: object lifetime issues aside, we need to copy and autorelease so that the right thing happens for mutable dictionaries. + if (key == nil) return [[self copy] autorelease]; + + NSMutableDictionary *temp = [self mutableCopy]; + [temp removeObjectForKey:key]; + NSDictionary *result = [[temp copy] autorelease]; + [temp release]; + + return result; +} + + +- (NSDictionary *) dictionaryByAddingEntriesFromDictionary:(NSDictionary *)dictionary +{ + // Note: object lifetime issues aside, we need to copy and autorelease so that the right thing happens for mutable dictionaries. + if (dictionary == nil) return [[self copy] autorelease]; + + NSMutableDictionary *temp = [self mutableCopy]; + [temp addEntriesFromDictionary:dictionary]; + NSDictionary *result = [[temp copy] autorelease]; + [temp release]; + + return result; +} + +@end diff --git a/src/Core/NSFileManagerOOExtensions.h b/src/Core/NSFileManagerOOExtensions.h new file mode 100644 index 00000000..de6f2d34 --- /dev/null +++ b/src/Core/NSFileManagerOOExtensions.h @@ -0,0 +1,42 @@ +/* + +NSFileManagerOOExtensions.m + +This extends NSFileManager and adds some methods to insulate the +main oolite code from the gory details of creating/chdiring to the +commander save directory. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + +#define SAVEDIR "oolite-saves" +#define SNAPSHOTDIR "snapshots" + +@interface NSFileManager (OOExtensions) + +- (NSArray*) commanderContentsOfPath:(NSString*) savePath; +- (NSString*) defaultCommanderPath; +- (BOOL)chdirToSnapshotPath; + +@end + + diff --git a/src/Core/NSFileManagerOOExtensions.m b/src/Core/NSFileManagerOOExtensions.m new file mode 100644 index 00000000..f5dbb465 --- /dev/null +++ b/src/Core/NSFileManagerOOExtensions.m @@ -0,0 +1,143 @@ +/* + +NSFileManagerOOExtensions.m + +This extends NSFileManager and adds some methods to insulate the +main oolite code from the gory details of creating/chdiring to the +commander save directory. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#include +#import "NSFileManagerOOExtensions.h" +#import "ResourceManager.h" +#import "OOPListParsing.h" + +#define kOOLogUnconvertedNSLog @"unclassified.NSFileManagerOOExtensions" + + +@implementation NSFileManager (OOExtensions) + +- (NSArray *) commanderContentsOfPath:(NSString*) savePath +{ + BOOL pathIsDirectory = NO; + if ([[NSFileManager defaultManager] fileExistsAtPath:savePath isDirectory:&pathIsDirectory] && pathIsDirectory) + { + NSMutableArray *contents = [NSMutableArray arrayWithArray:[self directoryContentsAtPath: savePath]]; + + // at this point we should strip out any files not loadable as Oolite saved games + unsigned i; + for (i = 0; i < [contents count]; i++) + { + NSString* path = [savePath stringByAppendingPathComponent: (NSString*)[contents objectAtIndex:i]]; + + // ensure it's not a directory + if (![[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&pathIsDirectory] && pathIsDirectory) + { + + // check file extension + if (![[path pathExtension] isEqual:@"oolite-save"]) + { + [contents removeObjectAtIndex: i--]; + continue; + } + + // check to see if we can parse the file okay + NSDictionary *cdr = OODictionaryFromFile(path); + if (!cdr) + { + OOLog(@"savedGame.read.fail.notDictionary", @">>>> %@ could not be parsed as a saved game.", path); + [contents removeObjectAtIndex: i--]; + continue; + } + } + + // all okay - we can use this path! + [contents replaceObjectAtIndex: i withObject: path]; + + } + + return contents; + } + else + { + OOLogERR(@"savedGame.read.fail.fileNotFound", @"File at path '%@' could not be found.", savePath); + return nil; + } +} + + +- (NSString *) defaultCommanderPath +{ + NSString *savedir = [NSHomeDirectory() stringByAppendingPathComponent:@SAVEDIR]; + BOOL pathIsDirectory = NO; + + // does it exist? + if (![[NSFileManager defaultManager] fileExistsAtPath:savedir isDirectory:&pathIsDirectory]) + { + // it doesn't exist. + if([self createDirectoryAtPath: savedir attributes: nil]) + { + return savedir; + } + else + { + OOLogERR(@"savedGame.defaultPath.create.failed", @"Unable to create '%@'. Saved games will go to the home directory.", savedir); + return NSHomeDirectory(); + } + } + + // is it a directory? + if (!pathIsDirectory) + { + OOLogERR(@"savedGame.defaultPath.notDirectory", @"'%@' is not a directory, saved games will go to the home directory.", savedir); + return NSHomeDirectory(); + } + + return savedir; +} + + +- (BOOL)chdirToSnapshotPath +{ + // default path for snapshots is oolite.app/oolite-saves/snapshots + NSString *savedir = [[NSHomeDirectory() stringByAppendingPathComponent:@SAVEDIR] stringByAppendingPathComponent:@SNAPSHOTDIR]; + if (![self changeCurrentDirectoryPath: savedir]) + { + // it probably doesn't exist. + if (![self createDirectoryAtPath: savedir attributes: nil]) + { + OOLog(@"savedSnapshot.defaultPath.create.failed", @"Unable to create directory %@", savedir); + return NO; + } + if (![self changeCurrentDirectoryPath: savedir]) + { + OOLog(@"savedSnapshot.defaultPath.chdir.failed", @"Created %@ but couldn't make it the current directory.", savedir); + return NO; + } + } + + return YES; +} + +@end + + diff --git a/src/Core/NSMutableDictionaryOOExtensions.h b/src/Core/NSMutableDictionaryOOExtensions.h new file mode 100644 index 00000000..a219d1e5 --- /dev/null +++ b/src/Core/NSMutableDictionaryOOExtensions.h @@ -0,0 +1,33 @@ +/* + +MutableDictionaryExtension.h + +Convenience extensions to NSMutableDictionary. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + +@interface NSMutableDictionary (OOExtensions) + +- (void)mergeEntriesFromDictionary:(NSDictionary *)otherDictionary; + +@end diff --git a/src/Core/NSMutableDictionaryOOExtensions.m b/src/Core/NSMutableDictionaryOOExtensions.m new file mode 100644 index 00000000..4e5fc609 --- /dev/null +++ b/src/Core/NSMutableDictionaryOOExtensions.m @@ -0,0 +1,67 @@ +/* + +NSMutableDictionaryOOExtensions.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "NSMutableDictionaryOOExtensions.h" + +@implementation NSMutableDictionary (OOExtensions) + +- (void)mergeEntriesFromDictionary:(NSDictionary *)otherDictionary +{ + NSEnumerator *otherKeysEnum = nil; + id key = nil; + + for (otherKeysEnum = [otherDictionary keyEnumerator]; (key = [otherKeysEnum nextObject]); ) + { + if (![self objectForKey:key]) + [self setObject:[otherDictionary objectForKey:key] forKey:key]; + else + { + BOOL merged = NO; + id thisObject = [self objectForKey:key]; + id otherObject = [otherDictionary objectForKey:key]; + + if ([thisObject isKindOfClass:[NSDictionary class]]&&[otherObject isKindOfClass:[NSDictionary class]]&&(![thisObject isEqual:otherObject])) + { + NSMutableDictionary* mergeObject = [NSMutableDictionary dictionaryWithDictionary:(NSDictionary*)thisObject]; + [mergeObject mergeEntriesFromDictionary:(NSDictionary*)otherObject]; + [self setObject:mergeObject forKey:key]; + merged = YES; + } + + if ([thisObject isKindOfClass:[NSArray class]]&&[otherObject isKindOfClass:[NSArray class]]&&(![thisObject isEqual:otherObject])) + { + NSMutableArray* mergeObject = [NSMutableArray arrayWithArray:(NSArray*)thisObject]; + [mergeObject addObjectsFromArray:(NSArray*)otherObject]; + [self setObject:mergeObject forKey:key]; + merged = YES; + } + + if (!merged) + [self setObject:otherObject forKey:key]; + } + } +} + +@end diff --git a/src/Core/NSScannerOOExtensions.h b/src/Core/NSScannerOOExtensions.h new file mode 100644 index 00000000..4c8b6982 --- /dev/null +++ b/src/Core/NSScannerOOExtensions.h @@ -0,0 +1,35 @@ +/* + +NSScannerOOExtensions.h + +Additions to NSScanner to work around bugs. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + +@interface NSScanner (OOExtensions) + +- (BOOL) ooliteScanCharactersFromSet:(NSCharacterSet *)set intoString:(NSString **)value; +- (BOOL) ooliteScanUpToCharactersFromSet:(NSCharacterSet *)set intoString:(NSString **)value; + +@end + diff --git a/src/Core/NSScannerOOExtensions.m b/src/Core/NSScannerOOExtensions.m new file mode 100644 index 00000000..90702bb1 --- /dev/null +++ b/src/Core/NSScannerOOExtensions.m @@ -0,0 +1,84 @@ +/* + +NSScannerOOExtensions.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + +#import "NSScannerOOExtensions.h" + +@implementation NSScanner (OOExtensions) + +- (BOOL) ooliteScanCharactersFromSet:(NSCharacterSet *)set intoString:(NSString **)value +{ + unsigned int currentLocation = [self scanLocation]; + NSRange matchedRange = NSMakeRange( currentLocation, 0); + NSString* scanString = [self string]; + unsigned int scanLength = [scanString length]; + + while ((currentLocation < scanLength)&&([set characterIsMember:[scanString characterAtIndex:currentLocation]])) + { + currentLocation++; + } + + [self setScanLocation:currentLocation]; + + matchedRange.length = currentLocation - matchedRange.location; + + if (!matchedRange.length) return NO; + + if (value != NULL) + { + *value = [scanString substringWithRange:matchedRange]; + } + + return YES; +} + + +- (BOOL) ooliteScanUpToCharactersFromSet:(NSCharacterSet *)set intoString:(NSString **)value +{ + unsigned int currentLocation = [self scanLocation]; + NSRange matchedRange = NSMakeRange( currentLocation, 0); + NSString* scanString = [self string]; + unsigned int scanLength = [scanString length]; + + while ((currentLocation < scanLength)&&(![set characterIsMember:[scanString characterAtIndex:currentLocation]])) + { + currentLocation++; + } + + [self setScanLocation:currentLocation]; + + matchedRange.length = currentLocation - matchedRange.location; + + if (!matchedRange.length) return NO; + + if (value != NULL) + { + *value = [scanString substringWithRange:matchedRange]; + } + + return YES; +} + +@end diff --git a/src/Core/NSStringOOExtensions.h b/src/Core/NSStringOOExtensions.h new file mode 100644 index 00000000..72770125 --- /dev/null +++ b/src/Core/NSStringOOExtensions.h @@ -0,0 +1,66 @@ +/* + +NSStringOOExtensions.h + +Convenience extensions to NSString. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + + +@interface NSString (OOExtensions) + +/* +stringWithContentsOfUnicodeFile: + + Like +stringWithContentsOfFile:, but biased towards Unicode encodings and + cross-system consistency. Specifically: + * If the file starts with a UTF-16 BOM, assume UTF-16. + * Otherwise, if the file can be interpreted as UTF-8, assume UTF-8. + * Otherwise, assume ISO-Latin-1. +*/ ++ (id)stringWithContentsOfUnicodeFile:(NSString *)path; + + +/* +stringWithUTF16String: + + Takes a NUL-terminated native-endian UTF-16 string. +*/ ++ (id)stringWithUTF16String:(const unichar *)chars; + + +/* -utf16DataWithBOM: + Convert to native-endian UTF-16 data. +*/ +- (NSData *)utf16DataWithBOM:(BOOL)includeByteOrderMark; + +@end + + +@interface NSMutableString (OOExtensions) + +- (void) appendLine:(NSString *)line; +- (void) appendFormatLine:(NSString *)fmt, ...; +- (void) appendFormatLine:(NSString *)fmt arguments:(va_list)args; + +- (void) deleteCharacterAtIndex:(unsigned long)index; + +@end diff --git a/src/Core/NSStringOOExtensions.m b/src/Core/NSStringOOExtensions.m new file mode 100644 index 00000000..db3ac136 --- /dev/null +++ b/src/Core/NSStringOOExtensions.m @@ -0,0 +1,171 @@ +/* + +NSStringOOExtensions.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "NSStringOOExtensions.h" + + +@implementation NSString (OOExtensions) + ++ (id)stringWithContentsOfUnicodeFile:(NSString *)path +{ + id result = nil; + BOOL OK = YES; + NSData *data = nil; + const uint8_t *bytes = NULL; + size_t length = 0; + const uint8_t *effectiveBytes = NULL; + size_t effectiveLength = 0; + + data = [[NSData alloc] initWithContentsOfFile:path]; + if (data == nil) OK = NO; + + if (OK) + { + length = [data length]; + bytes = [data bytes]; + } + + if (OK && 2 <= length && (length % sizeof(unichar)) == 0) + { + // Could be UTF-16 + unichar firstChar = bytes[0]; + firstChar = (firstChar << 8) | bytes[1]; // Endianism doesn't matter, because we test both orders of BOM. + if (firstChar == 0xFFFE || firstChar == 0xFEFF) + { + // Consider it to be UTF-16. + result = [NSString stringWithCharacters:(unichar *)(bytes + sizeof(unichar)) length:(length / sizeof(unichar)) - 1]; + if (result == nil) OK = NO; + } + } + + if (OK && result == nil) + { + // Not UTF-16. Try UTF-8. + if (3 <= length && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) + { + // File starts with UTF-8 BOM; skip it. + effectiveBytes = bytes + 3; + effectiveLength = length + 3; + } + else + { + effectiveBytes = bytes; + effectiveLength = length; + } + + // Attempt to interpret as UTF-8 + result = [[[NSString alloc] initWithBytes:effectiveBytes length:effectiveLength encoding:NSUTF8StringEncoding] autorelease]; + } + + if (OK && result == nil) + { + // Not UTF-16 or UTF-8. Use ISO-Latin-1 (which should work for any byte sequence). + result = [[[NSString alloc] initWithBytes:effectiveBytes length:effectiveLength encoding:NSISOLatin1StringEncoding] autorelease]; + } + + [data release]; + return result; +} + + ++ (id)stringWithUTF16String:(const unichar *)chars +{ + size_t length; + const unichar *end; + + if (chars == NULL) return nil; + + // Find length of string. + end = chars; + while (*end++) {} + length = end - chars - 1; + + return [NSString stringWithCharacters:chars length:length]; +} + + +- (NSData *)utf16DataWithBOM:(BOOL)includeByteOrderMark +{ + size_t lengthInChars; + size_t lengthInBytes; + unichar *buffer = NULL; + unichar *characters = NULL; + + // Calculate sizes + lengthInChars = [self length]; + lengthInBytes = lengthInChars * sizeof(unichar); + if (includeByteOrderMark) lengthInBytes += sizeof(unichar); + + // Allocate buffer + buffer = malloc(lengthInBytes); + if (buffer == NULL) return nil; + + // write BOM (native-endian) if desired + characters = buffer; + if (includeByteOrderMark) + { + *characters++ = 0xFEFF; + } + + // Get the contents + [self getCharacters:characters]; + + // NSData takes ownership of the buffer. + return [NSData dataWithBytesNoCopy:buffer length:lengthInBytes freeWhenDone:YES]; +} + +@end + + +@implementation NSMutableString (OOExtensions) + +- (void) appendLine:(NSString *)line +{ + [self appendString:line ? [line stringByAppendingString:@"\n"] : (NSString *)@"\n"]; +} + + +- (void) appendFormatLine:(NSString *)fmt, ... +{ + va_list args; + va_start(args, fmt); + [self appendFormatLine:fmt arguments:args]; + va_end(args); +} + + +- (void) appendFormatLine:(NSString *)fmt arguments:(va_list)args +{ + NSString *formatted = [[NSString alloc] initWithFormat:fmt arguments:args]; + [self appendLine:formatted]; + [formatted release]; +} + + +- (void) deleteCharacterAtIndex:(unsigned long)index +{ + [self deleteCharactersInRange:NSMakeRange(index, 1)]; +} + +@end diff --git a/src/Core/NSThreadOOExtensions.h b/src/Core/NSThreadOOExtensions.h new file mode 100644 index 00000000..4a75caa1 --- /dev/null +++ b/src/Core/NSThreadOOExtensions.h @@ -0,0 +1,91 @@ +/* + +NSThreadOOExtensions.h + +Utility methods for NSThread and NS*Lock. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +@interface NSThread (OOExtensions) + +/* Set name of current thread for identification during debugging. Only works + on Mac OS X 10.5 or later, does nothing on other platforms. +*/ ++ (void) ooSetCurrentThreadName:(NSString *)name; + +@end + + +@interface NSLock (OOExtensions) + +/* Set name of lock for identification during debugging. Only works + on Mac OS X 10.5 or later, does nothing on other platforms. +*/ +- (void) ooSetName:(NSString *)name; + +@end + + +@interface NSRecursiveLock (OOExtensions) + +/* Set name of lock for identification during debugging. Only works + on Mac OS X 10.5 or later, does nothing on other platforms. +*/ +- (void) ooSetName:(NSString *)name; + +@end + + +@interface NSConditionLock (OOExtensions) + +/* Set name of lock for identification during debugging. Only works + on Mac OS X 10.5 or later, does nothing on other platforms. +*/ +- (void) ooSetName:(NSString *)name; + +@end diff --git a/src/Core/NSThreadOOExtensions.m b/src/Core/NSThreadOOExtensions.m new file mode 100644 index 00000000..a88b45d3 --- /dev/null +++ b/src/Core/NSThreadOOExtensions.m @@ -0,0 +1,123 @@ +/* + +NSThreadOOExtensions.h + +Utility methods for NSThread. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "NSThreadOOExtensions.h" + + +@interface NSThread (LeopardAdditions) +- (void) setName:(NSString *)name; +@end + +@interface NSLock (LeopardAdditions) +- (void) setName:(NSString *)name; +@end + +@interface NSRecursiveLock (LeopardAdditions) +- (void) setName:(NSString *)name; +@end + +@interface NSConditionLock (LeopardAdditions) +- (void) setName:(NSString *)name; +@end + + +@implementation NSThread (OOExtensions) + ++ (void) ooSetCurrentThreadName:(NSString *)name +{ + NSThread *thread = nil; + + thread = [NSThread currentThread]; + if ([thread respondsToSelector:@selector(setName:)]) + { + [thread setName:name]; + } +} + +@end + + +@implementation NSLock (OOExtensions) + +- (void) ooSetName:(NSString *)name +{ + if ([self respondsToSelector:@selector(setName:)]) + { + [self setName:name]; + } +} + +@end + + +@implementation NSRecursiveLock (OOExtensions) + +- (void) ooSetName:(NSString *)name +{ + if ([self respondsToSelector:@selector(setName:)]) + { + [self setName:name]; + } +} + +@end + + +@implementation NSConditionLock (OOExtensions) + +- (void) ooSetName:(NSString *)name +{ + if ([self respondsToSelector:@selector(setName:)]) + { + [self setName:name]; + } +} + +@end diff --git a/src/Core/OOAsyncQueue.h b/src/Core/OOAsyncQueue.h new file mode 100644 index 00000000..cdfcca68 --- /dev/null +++ b/src/Core/OOAsyncQueue.h @@ -0,0 +1,78 @@ +/* + +OOAsyncQueue.h +By Jens Ayton + +OOAsyncQueue is used to pass messages (in the form of arbitrary objects) +between threads. It is many-to-many capable, i.e. it is safe to send messages +from any number of threads and to read messages from any number of threads. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +@interface OOAsyncQueue: NSObject +{ + NSConditionLock *_lock; + struct OOAsyncQueueElement *_head, + *_tail, + *_pool; + unsigned _elemCount, + _poolCount; +} + +- (BOOL)enqueue:(id)object; // Returns NO on failure, or if object is nil. + +- (id)dequeue; // Blocks until the queue is non-empty. +- (id)tryDequeue; // Returns nil if empty. + +// Due to the asynchronous nature of the queue, these values are immediately out of date. +- (BOOL)empty; +- (unsigned)count; + +- (void)emptyQueue; // Releases all elements. + +@end diff --git a/src/Core/OOAsyncQueue.m b/src/Core/OOAsyncQueue.m new file mode 100644 index 00000000..81a3aa67 --- /dev/null +++ b/src/Core/OOAsyncQueue.m @@ -0,0 +1,315 @@ +/* + +OOAsyncQueue.m +By Jens Ayton + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOAsyncQueue.h" +#import "OOFunctionAttributes.h" +#import "OOLogging.h" +#import "NSThreadOOExtensions.h" +#import + + +enum +{ + kConditionNoData = 1, + kConditionQueuedData, + kConditionDead +}; + + +enum +{ + kMaxPoolElements = 5 +}; + + +typedef struct OOAsyncQueueElement OOAsyncQueueElement; +struct OOAsyncQueueElement +{ + OOAsyncQueueElement *next; + id object; +}; + + +OOINLINE OOAsyncQueueElement *AllocElement(void) +{ + return malloc(sizeof (OOAsyncQueueElement)); +} + + +OOINLINE void FreeElement(OOAsyncQueueElement *element) +{ + free(element); +} + + +@interface OOAsyncQueue (OOPrivate) + +- (void)doEmptyQueueWithAcquiredLock; +- (id)doDequeAndUnlockWithAcquiredLock; +- (void)recycleElementWithAcquiredLock:(OOAsyncQueueElement *)element; + +@end + + +@implementation OOAsyncQueue + +- (id)init +{ + self = [super init]; + if (self != nil) + { + _lock = [[NSConditionLock alloc] initWithCondition:kConditionNoData]; + [_lock ooSetName:@"OOAsyncQueue lock"]; + if (_lock == nil) + { + [self release]; + self = nil; + } + } + + return self; +} + + +- (void)dealloc +{ + OOAsyncQueueElement *element = NULL; + + [_lock lock]; + + if (_elemCount != 0) + { + OOLogWARN(@"asyncQueue.nonEmpty", @"%@ deallocated while non-empty, flushing.", self); + [self doEmptyQueueWithAcquiredLock]; + } + + // Free element pool. + while (_pool != NULL) + { + element = _pool; + _pool = element->next; + free(element); + } + + [_lock unlockWithCondition:kConditionDead]; + [_lock release]; + + [super dealloc]; +} + + +- (NSString *)description +{ + // Don't bother locking, the value would be out of date immediately anyway. + return [NSString stringWithFormat:@"<%@ %p>{%u elements}", [self class], self, _elemCount]; +} + + +- (BOOL)enqueue:(id)object +{ + OOAsyncQueueElement *element = NULL; + BOOL success = NO; + + if (EXPECT_NOT(object == nil)) return NO; + + [_lock lock]; + + // Get an element. + if (_pool != NULL) + { + element = _pool; + _pool = element->next; + --_poolCount; + } + else + { + element = AllocElement(); + if (element == NULL) goto FAIL; + } + + // Set element fields. + element->object = [object retain]; + element->next = NULL; + + // Insert in queue. + if (_head == NULL) + { + // Queue was empty, element is entire queue. + _head = _tail = element; + element->next = NULL; + assert(_elemCount == 0); + _elemCount = 1; + } + else + { + assert(_tail != NULL); + assert(_tail->next == NULL); + assert(_elemCount != 0); + + _tail->next = element; + _tail = element; + ++_elemCount; + } + success = YES; + +FAIL: + [_lock unlockWithCondition:kConditionQueuedData]; + return success; +} + + +- (id)dequeue +{ + [_lock lockWhenCondition:kConditionQueuedData]; + return [self doDequeAndUnlockWithAcquiredLock]; +} + + +- (id)tryDequeue +{ + if (![_lock tryLockWhenCondition:kConditionQueuedData]) return NO; + return [self doDequeAndUnlockWithAcquiredLock]; +} + + +- (BOOL)empty +{ + return _head != NULL; +} + + +- (unsigned)count +{ + return _elemCount; +} + + +- (void)emptyQueue +{ + [_lock lock]; + [self doEmptyQueueWithAcquiredLock]; + + assert(_head == NULL && _tail == NULL && _elemCount == 0); + [_lock unlockWithCondition:kConditionNoData]; +} + +@end + + +@implementation OOAsyncQueue (OOPrivate) + +- (void)doEmptyQueueWithAcquiredLock +{ + OOAsyncQueueElement *element = NULL; + + // Loop over queue. + while (_head != NULL) + { + // Dequeue element. + element = _head; + _head = _head->next; + --_elemCount; + + // We don't need the payload any longer. + [element->object release]; + + // Or the element. + [self recycleElementWithAcquiredLock:element]; + } + + _tail = NULL; +} + + +- (id)doDequeAndUnlockWithAcquiredLock +{ + OOAsyncQueueElement *element = NULL; + id result; + + assert(_head != NULL); + + // Dequeue element. + element = _head; + _head = _head->next; + if (_head == NULL) _tail = NULL; + --_elemCount; + + // Unpack payload. + result = [element->object autorelease]; + + // Recycle element. + [self recycleElementWithAcquiredLock:element]; + + // Ensure sane status. + assert((_head == NULL && _tail == NULL && _elemCount == 0) || (_head != NULL && _tail != NULL && _elemCount != 0)); + + // Unlock with appropriate state. + [_lock unlockWithCondition:(_head == NULL) ? kConditionNoData : kConditionQueuedData]; + + return result; +} + + +- (void)recycleElementWithAcquiredLock:(OOAsyncQueueElement *)element +{ + if (_poolCount < kMaxPoolElements) + { + // Add to pool for reuse. + element->next = _pool; + _pool = element; + ++_poolCount; + } + else + { + // Delete. + FreeElement(element); + } +} + +@end diff --git a/src/Core/OOBasicSoundReferencePoint.h b/src/Core/OOBasicSoundReferencePoint.h new file mode 100644 index 00000000..1f9df0b4 --- /dev/null +++ b/src/Core/OOBasicSoundReferencePoint.h @@ -0,0 +1,39 @@ +/* + +OOBasicSoundReferencePoint.h + +No-op implementation of OOSoundReferencePoint; see OOSound.h for information +about sound architecture. + +Oolite +Copyright (C) 2004-2007 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOMaths.h" + + +@interface OOSoundReferencePoint: NSObject + +// Positional audio attributes are ignored in this implementation +- (void)setPosition:(Vector)inPosition; +- (void)setVelocity:(Vector)inVelocity; +- (void)setOrientation:(Vector)inOrientation; + +@end diff --git a/src/Core/OOBasicSoundReferencePoint.m b/src/Core/OOBasicSoundReferencePoint.m new file mode 100644 index 00000000..05ab9776 --- /dev/null +++ b/src/Core/OOBasicSoundReferencePoint.m @@ -0,0 +1,47 @@ +/* + +OOBasicSoundReferencePoint.m + +Oolite +Copyright (C) 2004-2007 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOBasicSoundReferencePoint.h" + + +@implementation OOSoundReferencePoint + +- (void)setPosition:(Vector)inPosition +{ + +} + + +- (void)setVelocity:(Vector)inVelocity +{ + +} + + +- (void)setOrientation:(Vector)inOrientation +{ + +} + +@end diff --git a/src/Core/OOBoundingBox.h b/src/Core/OOBoundingBox.h new file mode 100644 index 00000000..0e09854a --- /dev/null +++ b/src/Core/OOBoundingBox.h @@ -0,0 +1,105 @@ +/* + +OOBoundingBox.h + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOBoundingBox.h directly; include OOMaths.h. +#else + + +typedef struct +{ + Vector min; + Vector max; +} BoundingBox; + + +extern const BoundingBox kZeroBoundingBox; /* (0, 0, 0), (0, 0, 0) */ + + +/* Extend bounding box to contain specified point. */ +OOINLINE void bounding_box_add_vector(BoundingBox *box, Vector vec) ALWAYS_INLINE_FUNC NONNULL_FUNC; +OOINLINE void bounding_box_add_xyz(BoundingBox *box, GLfloat x, GLfloat y, GLfloat z) ALWAYS_INLINE_FUNC NONNULL_FUNC; + +/* Reset bounding box to kZeroBoundingBox. */ +OOINLINE void bounding_box_reset(BoundingBox *box) NONNULL_FUNC; + +/* Reset bounding box to a zero-sized box surrounding specified vector. */ +OOINLINE void bounding_box_reset_to_vector(BoundingBox *box, Vector vec) ALWAYS_INLINE_FUNC NONNULL_FUNC; + +OOINLINE void bounding_box_get_dimensions(BoundingBox bb, GLfloat *xSize, GLfloat *ySize, GLfloat *zSize) ALWAYS_INLINE_FUNC; + + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE void bounding_box_add_vector(BoundingBox *box, Vector vec) +{ + assert(box != NULL); + box->min.x = OOMin_f(box->min.x, vec.x); + box->max.x = OOMax_f(box->max.x, vec.x); + box->min.y = OOMin_f(box->min.y, vec.y); + box->max.y = OOMax_f(box->max.y, vec.y); + box->min.z = OOMin_f(box->min.z, vec.z); + box->max.z = OOMax_f(box->max.z, vec.z); +} + + +void bounding_box_add_xyz(BoundingBox *box, GLfloat x, GLfloat y, GLfloat z) +{ + assert(box != NULL); + box->min.x = OOMin_f(box->min.x, x); + box->max.x = OOMax_f(box->max.x, x); + box->min.y = OOMin_f(box->min.y, y); + box->max.y = OOMax_f(box->max.y, y); + box->min.z = OOMin_f(box->min.z, z); + box->max.z = OOMax_f(box->max.z, z); +} + + +OOINLINE void bounding_box_reset(BoundingBox *box) +{ + assert(box != NULL); + *box = kZeroBoundingBox; +} + + +OOINLINE void bounding_box_reset_to_vector(BoundingBox *box, Vector vec) +{ + assert(box != NULL); + box->min = vec; + box->max = vec; +} + + +OOINLINE void bounding_box_get_dimensions(BoundingBox bb, GLfloat *xSize, GLfloat *ySize, GLfloat *zSize) +{ + if (xSize != NULL) *xSize = bb.max.x - bb.min.y; + if (ySize != NULL) *ySize = bb.max.y - bb.min.y; + if (zSize != NULL) *zSize = bb.max.z - bb.min.z; +} + +#endif diff --git a/src/Core/OOBrain.h b/src/Core/OOBrain.h new file mode 100644 index 00000000..0da4aee0 --- /dev/null +++ b/src/Core/OOBrain.h @@ -0,0 +1,81 @@ +/* + +OOBrain.h + +Part of NPC behaviour implementation. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#ifdef OO_BRAIN_AI + +#import "OOCocoa.h" +#import "OOTypes.h" + +#define MAX_CONSIDERED_ENTITIES 32 +#define MAX_INSTINCTS 16 + + +@class OOInstinct, Entity, ShipEntity; + +@interface OOBrain : NSObject { + + id owner; + + ShipEntity* ship; + + int n_instincts; + OOInstinct* instincts[MAX_INSTINCTS]; // each considered against the nearby_entities - highest priority_out wins! + + OOInstinct* most_urgent_instinct; + + int n_nearby_entities; + Entity* nearby_entities[MAX_CONSIDERED_ENTITIES + 1]; + + double observe_interval; + double time_until_observation; // countdown + + double action_interval; + double time_until_action; // countdown + +} + +- (void) setOwner:(id) anOwner; +- (void) setShip:(ShipEntity*) aShip; + +- (id) owner; +- (ShipEntity*) ship; + +// each instinct has a NSNumber priority +- (id) initBrainWithInstincts:(NSDictionary*) instinctDictionary forOwner:(id) anOwner andShip:(ShipEntity*) aShip; + +- (void) update:(OOTimeDelta) delta_t; + +- (void) observe; // look around, note ships, wormholes, planets + +- (void) evaluateInstincts; // calculate priority for each instinct + +- (void) actOnInstincts; // set ship behaviour from most urgent instinct + +- (void)dumpState; + +@end + +#endif /* OO_BRAIN_AI */ diff --git a/src/Core/OOBrain.m b/src/Core/OOBrain.m new file mode 100644 index 00000000..53d9af4f --- /dev/null +++ b/src/Core/OOBrain.m @@ -0,0 +1,243 @@ +/* + +OOBrain.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOBrain.h" +#import "OOInstinct.h" + +#import "Universe.h" +#import "OOConstToString.h" +#import "OOCollectionExtractors.h" + +#import "ShipEntity.h" + + +@implementation OOBrain + +- (void)setOwner:(id)anOwner +{ + owner = anOwner; +} + + +- (void)setShip:(ShipEntity *)aShip +{ + ship = aShip; +} + + +- (id)owner +{ + return owner; +} + + +- (ShipEntity *)ship +{ + return ship; +} + + +- (id)initBrainWithInstincts:(NSDictionary*) instinctDictionary forOwner:(id) anOwner andShip:(ShipEntity*) aShip +{ + NSEnumerator *instinctEnum = nil; + NSString *key = nil; + OOInstinct *instinct = nil; + + self = [super init]; + + n_instincts = 0; + + for (instinctEnum = [instinctDictionary keyEnumerator]; (key = [instinctEnum nextObject]); ) + { + OOInstinctID itype = StringToInstinct(key); + + if (itype != INSTINCT_NULL) + { + if (n_instincts == MAX_INSTINCTS) + { + OOLog(@"brain.full", @"Instinct dictionary has too many instincts, giving up at %@.", key); + break; + } + + GLfloat iprio = [instinctDictionary floatForKey:key]; + if (iprio != 0.0f) + { + instinct = [[OOInstinct alloc] initInstinctOfType:itype ofPriority:iprio forOwner:anOwner withShip:aShip]; + if (instinct != nil) + { + instincts[n_instincts++] = instinct; // retained + } + } + } + } + + time_until_action = randf() * 0.5; + action_interval = 0.125; + + time_until_observation = randf() * 0.5; + observe_interval = 0.25; + + return self; +} + + +- (void)update:(OOTimeDelta)delta_t +{ + time_until_observation -= delta_t; + if (time_until_observation < 0.0) + { + time_until_observation += observe_interval; + [self observe]; + } + // + time_until_action -= delta_t; + if (time_until_action < 0.0) + { + time_until_action += action_interval; + [self evaluateInstincts]; + } + // + [self actOnInstincts]; +} + + +- (void)observe // look around, note ships, wormholes, planets +{ + n_nearby_entities = 0; + nearby_entities[0] = nil; // zero list + + if (!ship) + return; + + // note nearby collidables and all planets + // + Entity* scan; + GLfloat d2 = 0.0; + GLfloat scannerRange = SCANNER_MAX_RANGE; + // + scan = ship->z_previous; while ((scan)&&(![scan canCollide])) scan = scan->z_previous; // skip non-collidables + while ((scan)&&(scan->position.z > ship->position.z - scannerRange)&&(n_nearby_entities < MAX_CONSIDERED_ENTITIES)) + { + if ([scan canCollide]) + { + d2 = distance2( ship->position, scan->position); + if (d2 < SCANNER_MAX_RANGE2) + nearby_entities[n_nearby_entities++] = scan; + } + scan = scan->z_previous; while ((scan)&&(![scan canCollide])) scan = scan->z_previous; + } + while ((scan)&&(n_nearby_entities < MAX_CONSIDERED_ENTITIES)) + { + if (scan->isPlanet) + nearby_entities[n_nearby_entities++] = scan; + scan = scan->z_previous; while ((scan)&&(!scan->isPlanet)) scan = scan->z_previous; + } + // + scan = ship->z_next; while ((scan)&&(![scan canCollide])) scan = scan->z_next; // skip non-collidables + while ((scan)&&(scan->position.z < ship->position.z + scannerRange)&&(n_nearby_entities < MAX_CONSIDERED_ENTITIES)) + { + if ([scan canCollide]) + { + d2 = distance2( ship->position, scan->position); + if (d2 < SCANNER_MAX_RANGE2) + nearby_entities[n_nearby_entities++] = scan; + } + scan = scan->z_next; while ((scan)&&(![scan canCollide])) scan = scan->z_next; // skip non-collidables + } + while ((scan)&&(n_nearby_entities < MAX_CONSIDERED_ENTITIES)) + { + if (scan->isPlanet) + nearby_entities[n_nearby_entities++] = scan; + scan = scan->z_next; while ((scan)&&(!scan->isPlanet)) scan = scan->z_next; // skip non-planets + } + // + nearby_entities[n_nearby_entities] = nil; +} + + +- (void)evaluateInstincts // calculate priority for each instinct +{ + int i = 0; + nearby_entities[n_nearby_entities] = nil; + GLfloat most_urgent = -99.9; + for (i = 0; i < n_instincts; i++) + { + OOInstinct* instinct = instincts[i]; + if (instinct) + { + GLfloat urgency = [instinct evaluateInstinctWithEntities: nearby_entities]; + if (urgency > most_urgent) + { + most_urgent = urgency; + most_urgent_instinct = instinct; + } + } + } +} + + +- (void)actOnInstincts // set ship behaviour from most urgent instinct +{ + if (most_urgent_instinct) + { + [most_urgent_instinct setShipVars]; + } + else + { + int i = 0; + GLfloat most_urgent = -99.9; + for (i = 0; i < n_instincts; i++) + { + if (instincts[i]) + { + GLfloat urgency = [instincts[i] priority]; + if (urgency > most_urgent) + { + most_urgent = urgency; + most_urgent_instinct = instincts[i]; + } + } + } + } +} + + +- (void)dumpState +{ + OOLog(@"dumpState.brain", @"Instinct count: %u", n_instincts); + OOLog(@"dumpState.brain", @"Nearby entities: %u", n_nearby_entities); + if (most_urgent_instinct != nil && OOLogWillDisplayMessagesInClass(@"dumpState.brain.instinct")) + { + OOLog(@"dumpState.brain.instinct", @"Most urgent instinct:"); + OOLogPushIndent(); + OOLogIndent(); + NS_DURING + [most_urgent_instinct dumpState]; + NS_HANDLER + NS_ENDHANDLER + OOLogPopIndent(); + } +} + +@end diff --git a/src/Core/OOCPUInfo.h b/src/Core/OOCPUInfo.h new file mode 100644 index 00000000..7447be1b --- /dev/null +++ b/src/Core/OOCPUInfo.h @@ -0,0 +1,166 @@ +/* + +OOCPUInfo.h + +Capabilities and features of CPUs. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import + + +void OOCPUInfoInit(void); + + +/* Number of processors (whether they be individual or cores), used to select + number of threads to use for things like texture loading. +*/ +unsigned OOCPUCount(void); + + +/* Set up OOLITE_BIG_ENDIAN and OOLITE_LITTLE_ENDIAN macros. Exactly one must + be non-zero. If you're porting Oolite to a middle-endian platform, you'll + need to work out what to do with endian-sensitive stuff -- currently, that + means texture loading. (The data cache automatically rejects cached data + of the wrong byte sex.) +*/ +#if !defined(OOLITE_BIG_ENDIAN) && !defined(OOLITE_LITTLE_ENDIAN) + +#if __BIG_ENDIAN__ +#define OOLITE_BIG_ENDIAN 1 +#endif + +#if __LITTLE_ENDIAN__ +#define OOLITE_LITTLE_ENDIAN 1 +#endif + + +#if !defined(OOLITE_BIG_ENDIAN) && !defined(OOLITE_LITTLE_ENDIAN) +#if defined(__i386__) || defined(__amd64__) || defined(__x86_64__) +#define OOLITE_LITTLE_ENDIAN 1 +#endif + +#if defined(__sgi__) || defined(__mips__) +#define OOLITE_BIG_ENDIAN 1 +#endif + +// Do not assume PPC == big endian, it can be either. + +#endif // inner none defined +#endif // outer none defined + + +#ifndef OOLITE_BIG_ENDIAN +#define OOLITE_BIG_ENDIAN 0 +#endif + +#ifndef OOLITE_LITTLE_ENDIAN +#define OOLITE_LITTLE_ENDIAN 0 +#endif + + +#if !OOLITE_BIG_ENDIAN && !OOLITE_LITTLE_ENDIAN +#error Neither OOLITE_BIG_ENDIAN nor OOLITE_LITTLE_ENDIAN is defined as nonzero! + +#undef OOLITE_BIG_ENDIAN +#undef OOLITE_LITTLE_ENDIAN + +// Cause errors where the macros are used +#define OOLITE_BIG_ENDIAN "BUG" +#define OOLITE_LITTLE_ENDIAN "BUG" +#endif + + +/* Set up OOLITE_NATIVE_64_BIT. This is intended for 64-bit optimizations + (see OOTextureScaling.m). It is not set for systems where 64-bitness may + be determined at runtime (such as 32-bit OS X binaries), because I can't + be bothered to do the set-up required to use switch to a 64-bit code path + at runtime while being cross-platform. + -- Ahruman +*/ + +#ifndef OOLITE_NATIVE_64_BIT + +#ifdef _UINT64_T +#ifdef __ppc64__ +#define OOLITE_NATIVE_64_BIT 1 +#elif __amd64__ +#define OOLITE_NATIVE_64_BIT 1 +#elif __x86_64__ +#define OOLITE_NATIVE_64_BIT 1 +#endif +#endif // _UINT64_T + +#ifndef OOLITE_NATIVE_64_BIT +#define OOLITE_NATIVE_64_BIT 0 +#endif + +#endif // defined(OOLITE_NATIVE_64_BIT) + + +/* Set up OOLITE_ALTIVEC -- availability flag for AltiVec/VMX. + If AltiVec instructions are being used, OOAltivecAvailable() should be + used to test for availability. If compiling for PPC systems with AltiVec + only, predefine OOLITE_ALTIVEC_ALWAYS to 1. Availability check is + OS-dependent, currently OS X only; see below. + + NOTE: in its current form, this requires __VEC__ to be defined as nonzero. + This is done by gcc when -maltivec is used, which it won't be by default + in GNUmakefile builds. +*/ +#ifndef OOLITE_ALTIVEC + +#if (defined(__ppc__) || defined(__ppc64__)) && __VEC__ +#define OOLITE_ALTIVEC 1 +#else +#define OOLITE_ALTIVEC 0 +#endif + +#endif + + +#if OOLITE_ALTIVEC +#ifndef __GNUC__ +#warning OOLITE_ALTIVEC is set, but the compiler is not gcc. Altivec support is currenty written with the assumption of gcc and may not work on other compilers. +#endif + +#if OOLITE_ALTIVEC_ALWAYS +#define OOAltiVecAvailable() (1) +#else +#ifndef OOLITE_ALTIVEC_DYNAMIC +#define OOLITE_ALTIVEC_DYNAMIC 1 +#endif +#endif + +#if OOLITE_ALTIVEC_DYNAMIC && !OOLITE_MAC_OS_X +#warning OOLITE_ALTIVEC is set, but Oolite does not know how to detect AltiVec on this platform. Either implement OOAltivecAvailable() or predefine OOLITE_ALTIVEC_ALWAYS to 1 if you know it will always be available. +#undef OOLITE_ALTIVEC_DYNAMIC +#undef OOLITE_ALTIVEC +#define OOLITE_ALTIVEC 0 +#endif + +#if OOLITE_ALTIVEC_DYNAMIC +BOOL OOAltiVecAvailable(void); +#endif +#endif + +// After all this, I haven't got around to implementing Altivec texture scaling. -- Ahruman diff --git a/src/Core/OOCPUInfo.m b/src/Core/OOCPUInfo.m new file mode 100644 index 00000000..9fb05f3a --- /dev/null +++ b/src/Core/OOCPUInfo.m @@ -0,0 +1,142 @@ +/* + +OOCPUInfo.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCPUInfo.h" +#import + +#if OOLITE_MAC_OS_X +#import +#elif (OOLITE_LINUX || OOLITE_WINDOWS) +#import +#endif + + +#if 0 +// Confirm settings +#if OOLITE_BIG_ENDIAN +#warning Big-endian. +#endif +#if OOLITE_LITTLE_ENDIAN +#warning Little-endian. +#endif +#if OOLITE_NATIVE_64_BIT +#warning 64-bit. +#else +#warning 32-bit. +#endif +#if OOLITE_ALTIVEC +#warning AltiVec. +#if OOLITE_ALTIVEC_DYNAMIC +#warning Dynamic AltiVec selection. +#endif +#endif +#endif + + +static BOOL sInited = NO; +#if OOLITE_ALTIVEC_DYNAMIC +static BOOL sAltiVecAvailable = NO; +#endif + + +static unsigned sNumberOfCPUs = 0; // Yes, really 0. + + +void OOCPUInfoInit(void) +{ + if (sInited) return; + + // Verify correctness of endian macros + uint8_t endianTag[4] = {0x12, 0x34, 0x56, 0x78}; + +#if OOLITE_BIG_ENDIAN + if (*(uint32_t*)endianTag != 0x12345678) + { + OOLog(@"cpuInfo.endianTest.failed", @"OOLITE_BIG_ENDIAN is set, but the system is not big-endian -- aborting."); + exit(EXIT_FAILURE); + } +#endif + +#if OOLITE_LITTLE_ENDIAN + if (*(uint32_t*)endianTag != 0x78563412) + { + OOLog(@"cpuInfo.endianTest.failed", @"OOLITE_LITTLE_ENDIAN is set, but the system is not little-endian -- aborting."); + exit(EXIT_FAILURE); + } +#endif + + // Count processors +#if OOLITE_MAC_OS_X + int flag = 0; + size_t size = sizeof flag; + if (sysctlbyname("hw.logicalcpu", &flag, &size, NULL, 0) == 0) + { + if (1 <= flag) sNumberOfCPUs = flag; + } + if (sNumberOfCPUs == 0 && sysctlbyname("hw.ncpu", &flag, &size, NULL, 0) == 0) + { + if (1 <= flag) sNumberOfCPUs = flag; + } +#elif OOLITE_WINDOWS + SYSTEM_INFO sysInfo; + + GetSystemInfo(&sysInfo); + sNumberOfCPUs = sysInfo.dwNumberOfProcessors; +#elif defined _SC_NPROCESSORS_ONLN + sNumberOfCPUs = sysconf(_SC_NPROCESSORS_ONLN); +#else + #warning Do not know how to find number of CPUs on this architecture. +#endif + + // Check for AltiVec if relelevant +#if OOLITE_ALTIVEC_DYNAMIC +#if OOLITE_MAC_OS_X + flag = 0; + size = sizeof flag; + if (sysctlbyname("hw.optional.altivec", &flag, &size, NULL, 0) == 0) + { + if (flag) sAltiVecAvailable = YES; + } +#else + #error OOLITE_ALTIVEC_DYNAMIC is (still) set, but Oolite does not know how to check for AltiVec on this platform. (The Mac version may work on other BSDs, at least; give it a shot.) +#endif +#endif + + sInited = YES; +} + + +unsigned OOCPUCount(void) +{ + if (!sInited) OOCPUInfoInit(); + return (sNumberOfCPUs != 0) ? sNumberOfCPUs : 1; +} + + +#if OOLITE_ALTIVEC_DYNAMIC +BOOL OOAltiVecAvailable(void) +{ + return sAltiVecAvailable; +} +#endif diff --git a/src/Core/OOCache.h b/src/Core/OOCache.h new file mode 100644 index 00000000..4bd37e92 --- /dev/null +++ b/src/Core/OOCache.h @@ -0,0 +1,82 @@ +/* + +OOCache.h +By Jens Ayton + +An OOCache handles storage of a limited number of elements for quick reuse. It +may be used directly for in-memory cache, or indirectly through OOCacheManager +for on-disk cache. + +Every OOCache has a 'prune threshold', which controls how many elements it +contains, and an 'auto-prune' flag, which determines how pruning is managed. + +If auto-pruning is on, the cache will pruned to 80% of the prune threshold +whenever the prune threshold is exceeded. If auto-pruning is off, the cache +can be pruned to the prune threshold by explicitly calling -prune. + +While OOCacheManager-managed caches must have string keys and property list +values, OOCaches used directly may have any keys allowable for a mutable +dictionary (that is, keys should conform to and values may be +arbitrary objects) -- an 'unmanaged' cache is essentially a mutable dictionary +with a prune limit. (Project: with the addition of a -keyEnumerator method and +sutiable NSEnumerator subclass, and a -count method, it could be turned into a +subclass of NSMutableDictionary.) + +Default and minimum prune threshold values are specified in OOCacheManager.h. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + + +@interface OOCache: NSObject +{ +@private + struct OOCacheImpl *cache; + unsigned pruneThreshold; + BOOL autoPrune; + BOOL dirty; +} + +- (id)init; +- (id)initWithPList:(id)pList; +- (id)pListRepresentation; + +- (id)objectForKey:(id)key; +- (void)setObject:(id)value forKey:(id)key; +- (void)removeObjectForKey:(id)key; + +- (void)setPruneThreshold:(unsigned)threshold; +- (unsigned)pruneThreshold; + +- (void)setAutoPrune:(BOOL)flag; +- (BOOL)autoPrune; + +- (void)prune; + +- (BOOL)dirty; +- (void)markClean; + +- (NSString *)name; +- (void)setName:(NSString *)name; + +@end diff --git a/src/Core/OOCache.m b/src/Core/OOCache.m new file mode 100644 index 00000000..b72aa327 --- /dev/null +++ b/src/Core/OOCache.m @@ -0,0 +1,1057 @@ +/* + +OOCache.m +By Jens Ayton + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +/* IMPLEMENTATION NOTES + A cache needs to be able to implement three types of operation + efficiently: + * Retrieving: looking up an element by key. + * Inserting: setting the element associated with a key. + * Deleting: removing a single element. + * Pruning: removing one or more least-recently used elements. + + An NSMutableDictionary performs the first three operations efficiently but + has no support for pruning - specifically no support for finding the + least-recently-accessed element. Using standard Foundation containers, i + would be necessary to use several dictionaries and arrays, which would be + quite inefficient since small NSArrays aren’t very good at head insertion + or deletion. Alternatively, a standard dictionary whose value objects + maintain an age-sorted list could be used. + + I chose instead to implement a custom scheme from scratch. It uses two + parallel data structures: a doubly-linked list sorted by age, and a splay + tree to implement insertion/deletion. The implementation is largely + procedural C. Deserialization, pruning and modification tracking is done + in the ObjC class; everything else is done in C functions. + + A SPLAY TREE is a type of semi-balanced binary search tree with certain + useful properties: + * Simplicity. All look-up and restructuring operations are based on a + single operation, splaying, which brings the node with the desired key + (or the node whose key is "left" of the desired key, if there is no + exact match) to the root, while maintaining the binary search tree + invariant. Splaying itself is sufficient for look-up; insertion and + deletion work by splaying and then manipulating at the root. + * Self-optimization. Because each look-up brings the sought element to + the root, often-used elements tend to stay near the top. (Oolite often + performs sequences of identical look-ups, for instance when creating + an asteroid field, or the racing ring set-up which uses lots of + identical ring segments; during combat, missiles, canisters and hull + plates will be commonly used.) Also, this means that for a retrieve- + attempt/insert sequence, the retrieve attempt will optimize the tree + for the insertion. + * Efficiency. In addition to the self-optimization, splay trees have a + small code size and no storage overhead for flags. + The amortized worst-case cost of splaying (cost averaged over a + worst-case sequence of operations) is O(log n); a single worst-case + splay is O(n), but that worst-case also improves the balance of the + tree, so you can't have two worst cases in a row. Insertion and + deletion are also O(log n), consisting of a splay plus an O(1) + operation. + References for splay trees: + * http://www.cs.cmu.edu/~sleator/papers/self-adjusting.pdf + Original research paper. + * http://www.ibr.cs.tu-bs.de/courses/ss98/audii/applets/BST/SplayTree-Example.html + Java applet demonstrating splaying. + * http://www.link.cs.cmu.edu/link/ftp-site/splaying/top-down-splay.c + Sample implementation by one of the inventors. The TreeSplay(), + TreeInsert() and CacheRemove() functions are based on this. + + The AGE LIST is a doubly-linked list, ordered from oldest to youngest. + Whenever an element is retrieved or inserted, it is promoted to the + youngest end of the age list. Pruning proceeds from the oldest end of the + age list. + + if (autoPrune) + { + PRUNING is batched, handling 20% of the cache at once. This is primarily + because deletion somewhat pessimizes the tree (see "Self-optimization" + below). It also provides a bit of code coherency. To reduce pruning + batches while in flight, pruning is also performed before serialization + (which in turn is done, if the cache has changed, whenever the user + docks). This has the effect that the number of items in the cache on disk + never exceeds 80% of the prune threshold. This is probably not actually + poinful, since pruning should be a very small portion of the per-frame run + time in any case. Premature optimization and all that jazz. + Pruning performs at most 0.2n deletions, and is thus O(n log n). + } + else + { + PRUNING is performed manually by calling -prune. + } + + If the macro OOCACHE_PERFORM_INTEGRITY_CHECKS is set to a non-zero value, + the integrity of the tree and the age list will be checked before and + after each high-level operation. This is an inherently O(n) operation. +*/ + + +#import "OOCache.h" +#import "OOCacheManager.h" +#import "OOStringParsing.h" + + +#ifndef OOCACHE_PERFORM_INTEGRITY_CHECKS +#define OOCACHE_PERFORM_INTEGRITY_CHECKS 0 +#endif + + +// Protocol used internally to squash idiotic warnings in gnu-gcc. +@protocol OOCacheComparable +- (NSComparisonResult) compare:(id)other; +- (id) copy; +@end + + +static NSString * const kOOLogCacheIntegrityCheck = @"dataCache.integrityCheck"; + + +typedef struct OOCacheImpl OOCacheImpl; +typedef struct OOCacheNode OOCacheNode; + + +enum { kCountUnknown = -1UL }; + + +static NSString * const kSerializedEntryKeyKey = @"key"; +static NSString * const kSerializedEntryKeyValue = @"value"; + + +static OOCacheImpl *CacheAllocate(void); +static void CacheFree(OOCacheImpl *cache); + +static BOOL CacheInsert(OOCacheImpl *cache, id key, id value); +static BOOL CacheRemove(OOCacheImpl *cache, id key); +static BOOL CacheRemoveOldest(OOCacheImpl *cache, NSString *logKey); +static id CacheRetrieve(OOCacheImpl *cache, id key); +static unsigned CacheGetCount(OOCacheImpl *cache); +static NSArray *CacheArrayOfNodesByAge(OOCacheImpl *cache); +static NSString *CacheGetName(OOCacheImpl *cache); +static void CacheSetName(OOCacheImpl *cache, NSString *name); + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS + static void CacheCheckIntegrity(OOCacheImpl *cache, NSString *context); + + #define CHECK_INTEGRITY(context) CacheCheckIntegrity(cache, (context)) +#else + #define CHECK_INTEGRITY(context) do {} while (0) +#endif + + +@interface OOCache (Private) + +- (void)loadFromArray:(NSArray *)inArray; + +@end + + +@implementation OOCache + + +- (void)dealloc +{ + CHECK_INTEGRITY(@"dealloc"); + CacheFree(cache); + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{\"%@\", %u elements, prune threshold=%u, auto-prune=%s dirty=%s}", [self class], self, [self name], CacheGetCount(cache), pruneThreshold, autoPrune ? "yes" : "no", dirty ? "yes" : "no"]; +} + + +- (id)init +{ + return [self initWithPList:nil]; +} + + +- (id)initWithPList:(id)pList +{ + BOOL OK = YES; + + self = [super init]; + OK = self != nil; + + if (OK) + { + cache = CacheAllocate(); + if (cache == NULL) OK = NO; + } + + if (pList != nil) + { + if (OK) OK = [pList isKindOfClass:[NSArray class]]; + if (OK) [self loadFromArray:pList]; + } + if (OK) + { + pruneThreshold = kOOCacheDefaultPruneThreshold; + autoPrune = YES; + } + + if (!OK) + { + [self release]; + self = nil; + } + + return self; +} + + +- (id)pListRepresentation +{ + return CacheArrayOfNodesByAge(cache); + + return nil; +} + + +- (id)objectForKey:(id)key +{ + id result = nil; + + CHECK_INTEGRITY(@"objectForKey: before"); + + result = CacheRetrieve(cache, key); + // Note: while reordering the age list technically makes the cache dirty, it's not worth rewriting it just for that, so we don't flag it. + + CHECK_INTEGRITY(@"objectForKey: after"); + + return [[result retain] autorelease]; +} + + +- (void)setObject:inObject forKey:(id)key +{ + CHECK_INTEGRITY(@"setObject:forKey: before"); + + if (CacheInsert(cache, key, inObject)) + { + dirty = YES; + if (autoPrune) [self prune]; + } + + CHECK_INTEGRITY(@"setObject:forKey: after"); +} + + +- (void)removeObjectForKey:(id)key +{ + CHECK_INTEGRITY(@"removeObjectForKey: before"); + + if (CacheRemove(cache, key)) dirty = YES; + + CHECK_INTEGRITY(@"removeObjectForKey: after"); +} + + +- (void)setPruneThreshold:(unsigned)threshold +{ + threshold = MAX(threshold, (unsigned)kOOCacheMinimumPruneThreshold); + if (threshold != pruneThreshold) + { + pruneThreshold = threshold; + if (autoPrune) [self prune]; + } +} + + +- (unsigned)pruneThreshold +{ + return pruneThreshold; +} + + +- (void)setAutoPrune:(BOOL)flag +{ + BOOL prune = (flag != NO); + if (prune != autoPrune) + { + autoPrune = prune; + [self prune]; + } +} + + +- (BOOL)autoPrune +{ + return autoPrune; +} + + +- (void)prune +{ + unsigned pruneCount; + unsigned desiredCount; + unsigned count; + + // Order of operations is to ensure rounding down. + if (autoPrune) desiredCount = (pruneThreshold * 4) / 5; + else desiredCount = pruneThreshold; + + if (pruneThreshold == kOOCacheNoPrune || (count = CacheGetCount(cache)) <= pruneThreshold) return; + + pruneCount = count - desiredCount; + + NSString *logKey = [NSString stringWithFormat:@"dataCache.prune.%@", CacheGetName(cache)]; + OOLog(logKey, @"Pruning cache \"%@\" - removing %u entries", CacheGetName(cache), pruneCount); + OOLogIndentIf(logKey); + + while (pruneCount--) CacheRemoveOldest(cache, logKey); + + OOLogOutdentIf(logKey); +} + + +- (BOOL)dirty +{ + return dirty; +} + + +- (void)markClean +{ + dirty = NO; +} + + +- (NSString *)name +{ + return CacheGetName(cache); +} + + +- (void)setName:(NSString *)name +{ + CacheSetName(cache, name); +} + +@end + + +@implementation OOCache (Private) + +- (void)loadFromArray:(NSArray *)array +{ + NSEnumerator *entryEnum = nil; + NSDictionary *entry = nil; + NSString *key = nil; + id value = nil; + + if (array == nil) return; + + for (entryEnum = [array objectEnumerator]; (entry = [entryEnum nextObject]); ) + { + if ([entry isKindOfClass:[NSDictionary class]]) + { + key = [entry objectForKey:kSerializedEntryKeyKey]; + value = [entry objectForKey:kSerializedEntryKeyValue]; + if ([key isKindOfClass:[NSString class]] && value != nil) + { + [self setObject:value forKey:key]; + } + } + } +} + +@end + + +/***** Most of the implementation. In C. Because I'm inconsistent and slightly mad. *****/ + +struct OOCacheImpl +{ + // Splay tree root + OOCacheNode *root; + + // Ends of age list + OOCacheNode *oldest, *youngest; + + unsigned count; + NSString *name; +}; + + +struct OOCacheNode +{ + // Payload + id key; + id value; + + // Splay tree + OOCacheNode *leftChild, *rightChild; + + // Age list + OOCacheNode *younger, *older; +}; + +static OOCacheNode *CacheNodeAllocate(id key, id value); +static void CacheNodeFree(OOCacheImpl *cache, OOCacheNode *node); +static id CacheNodeGetValue(OOCacheNode *node); +static void CacheNodeSetValue(OOCacheNode *node, id value); + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS +static NSString *CacheNodeGetDescription(OOCacheNode *node); +#endif + +static OOCacheNode *TreeSplay(OOCacheNode **root, id key); +static OOCacheNode *TreeInsert(OOCacheImpl *cache, id key, id value); +static unsigned TreeCountNodes(OOCacheNode *node); + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS +static OOCacheNode *TreeCheckIntegrity(OOCacheImpl *cache, OOCacheNode *node, OOCacheNode *expectedParent, NSString *context); +#endif + +static void AgeListMakeYoungest(OOCacheImpl *cache, OOCacheNode *node); +static void AgeListRemove(OOCacheImpl *cache, OOCacheNode *node); + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS +static void AgeListCheckIntegrity(OOCacheImpl *cache, NSString *context); +#endif + + +/***** CacheImpl functions *****/ + +static OOCacheImpl *CacheAllocate(void) +{ + return calloc(sizeof (OOCacheImpl), 1); +} + + +static void CacheFree(OOCacheImpl *cache) +{ + if (cache == NULL) return; + + CacheNodeFree(cache, cache->root); + [cache->name autorelease]; + free(cache); +} + + +static BOOL CacheInsert(OOCacheImpl *cache, id key, id value) +{ + OOCacheNode *node = NULL; + + if (cache == NULL || key == nil || value == nil) return NO; + + node = TreeInsert(cache, key, value); + if (node != NULL) + { + AgeListMakeYoungest(cache, node); + return YES; + } + else return NO; +} + + +static BOOL CacheRemove(OOCacheImpl *cache, id key) +{ + OOCacheNode *node = NULL, *newRoot = NULL; + + node = TreeSplay(&cache->root, key); + if (node != NULL) + { + if (node->leftChild == NULL) newRoot = node->rightChild; + else + { + newRoot = node->leftChild; + TreeSplay(&newRoot, key); + newRoot->rightChild = node->rightChild; + } + node->leftChild = NULL; + node->rightChild = NULL; + + cache->root = newRoot; + --cache->count; + + AgeListRemove(cache, node); + CacheNodeFree(cache, node); + + return YES; + } + else return NO; +} + + +static BOOL CacheRemoveOldest(OOCacheImpl *cache, NSString *logKey) +{ + // This could be more efficient, but does it need to be? + if (cache == NULL || cache->oldest == NULL) return NO; + + OOLog(logKey, @"Pruning cache \"%@\": removing %@", cache->name, cache->oldest->key); + return CacheRemove(cache, cache->oldest->key); +} + + +static id CacheRetrieve(OOCacheImpl *cache, id key) +{ + OOCacheNode *node = NULL; + id result = nil; + + if (cache == NULL || key == NULL) return nil; + + node = TreeSplay(&cache->root, key); + if (node != NULL) + { + result = CacheNodeGetValue(node); + AgeListMakeYoungest(cache, node); + } + return result; +} + + +static NSArray *CacheArrayOfNodesByAge(OOCacheImpl *cache) +{ + OOCacheNode *node = NULL; + NSMutableArray *result = nil; + + if (cache == NULL || cache->count == 0) return nil; + + result = [NSMutableArray arrayWithCapacity:cache->count]; + + for (node = cache->oldest; node != NULL; node = node->younger) + { + [result addObject:[NSDictionary dictionaryWithObjectsAndKeys:node->key, kSerializedEntryKeyKey, node->value, kSerializedEntryKeyValue, nil]]; + } + return result; +} + + +static NSString *CacheGetName(OOCacheImpl *cache) +{ + return cache->name; +} + + +static void CacheSetName(OOCacheImpl *cache, NSString *name) +{ + [cache->name autorelease]; + cache->name = [name copy]; +} + + +static unsigned CacheGetCount(OOCacheImpl *cache) +{ + return cache->count; +} + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS + +static void CacheCheckIntegrity(OOCacheImpl *cache, NSString *context) +{ + unsigned trueCount; + + cache->root = TreeCheckIntegrity(cache, cache->root, NULL, context); + + trueCount = TreeCountNodes(cache->root); + if (kCountUnknown == cache->count) cache->count = trueCount; + else if (cache->count != trueCount) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): count is %u, but should be %u.", context, cache->name, cache->count, trueCount); + cache->count = trueCount; + } + + AgeListCheckIntegrity(cache, context); +} + +#endif // OOCACHE_PERFORM_INTEGRITY_CHECKS + + +/***** CacheNode functions *****/ + +// CacheNodeAllocate(): create a cache node for a key, value pair, without inserting it in the structures. +static OOCacheNode *CacheNodeAllocate(id key, id value) +{ + OOCacheNode *result = NULL; + + if (key == nil || value == nil) return NULL; + + result = calloc(sizeof *result, 1); + if (result != NULL) + { + result->key = [key copy]; + result->value = [value retain]; + } + + return result; +} + + +// CacheNodeFree(): recursively delete a cache node and its children in the splay tree. To delete an individual node, first clear its child pointers. +static void CacheNodeFree(OOCacheImpl *cache, OOCacheNode *node) +{ + id key, value; + + if (node == NULL) return; + + AgeListRemove(cache, node); + + key = node->key; + node->key = nil; + [key release]; + + value = node->value; + node->value = nil; + [value release]; + + CacheNodeFree(cache, node->leftChild); + CacheNodeFree(cache, node->rightChild); + + free(node); +} + + +// CacheNodeGetValue(): retrieve the value of a cache node +static id CacheNodeGetValue(OOCacheNode *node) +{ + if (node == NULL) return nil; + + return node->value; +} + + +// CacheNodeSetValue(): change the value of a cache node (as when setObject:forKey: is called for an existing key). +static void CacheNodeSetValue(OOCacheNode *node, id value) +{ + if (node == NULL) return; + + [node->value release]; + node->value = [value retain]; +} + + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS +// CacheNodeGetDescription(): get a description of a cache node for debugging purposes. +static NSString *CacheNodeGetDescription(OOCacheNode *node) +{ + if (node == NULL) return @"0[null]"; + + return [NSString stringWithFormat:@"%p[\"%@\"]", node, node->key]; +} +#endif // OOCACHE_PERFORM_INTEGRITY_CHECKS + + +/***** Tree functions *****/ + +/* TreeSplay() + This is the fundamental operation of a splay tree. It searches for a node + with a given key, and rebalances the tree so that the found node becomes + the root. If no match is found, the node moved to the root is the one that + would have been found before the target, and will thus be a neighbour of + the target if the key is subsequently inserted. +*/ +static OOCacheNode *TreeSplay(OOCacheNode **root, id key) +{ + NSComparisonResult order; + OOCacheNode N = { leftChild: NULL, rightChild: NULL }; + OOCacheNode *node = NULL, *temp = NULL, *l = &N, *r = &N; + BOOL exact = NO; + + if (root == NULL || *root == NULL || key == nil) return NULL; + + node = *root; + + for (;;) + { + order = [key compare:node->key]; + if (order == NSOrderedAscending) + { + // Closest match is in left subtree + if (node->leftChild == NULL) break; + if ([key compare:node->leftChild->key] == NSOrderedAscending) + { + // Rotate right + temp = node->leftChild; + node->leftChild = temp->rightChild; + temp->rightChild = node; + node = temp; + if (node->leftChild == NULL) break; + } + // Link right + r->leftChild = node; + r = node; + node = node->leftChild; + } + else if (order == NSOrderedDescending) + { + // Closest match is in right subtree + if (node->rightChild == NULL) break; + if ([key compare:node->rightChild->key] == NSOrderedDescending) + { + // Rotate left + temp = node->rightChild; + node->rightChild = temp->leftChild; + temp->leftChild = node; + node = temp; + if (node->rightChild == NULL) break; + } + // Link left + l->rightChild = node; + l = node; + node = node->rightChild; + } + else + { + // Found exact match + exact = YES; + break; + } + } + + // Assemble + l->rightChild = node->leftChild; + r->leftChild = node->rightChild; + node->leftChild = N.rightChild; + node->rightChild = N.leftChild; + + *root = node; + return exact ? node : NULL; +} + + +static OOCacheNode *TreeInsert(OOCacheImpl *cache, id key, id value) +{ + OOCacheNode *closest = NULL, + *node = NULL; + NSComparisonResult order; + + if (cache == NULL || key == nil || value == nil) return NULL; + + if (cache->root == NULL) + { + node = CacheNodeAllocate(key, value); + cache->root = node; + cache->count = 1; + } + else + { + node = TreeSplay(&cache->root, key); + if (node != NULL) + { + // Exact match: key already exists, reuse its node + CacheNodeSetValue(node, value); + } + else + { + closest = cache->root; + node = CacheNodeAllocate(key, value); + order = [key compare:closest->key]; + + if (order == NSOrderedAscending) + { + // Insert to left + node->leftChild = closest->leftChild; + node->rightChild = closest; + closest->leftChild = NULL; + cache->root = node; + ++cache->count; + } + else if (order == NSOrderedDescending) + { + // Insert to right + node->rightChild = closest->rightChild; + node->leftChild = closest; + closest->rightChild = NULL; + cache->root = node; + ++cache->count; + } + else + { + // Key already exists, which we should have caught above + OOLog(@"dataCache.inconsistency", @"%s() internal inconsistency for cache \"%@\", insertion failed.", __FUNCTION__, cache->name); + CacheNodeFree(cache, node); + return NULL; + } + } + } + + return node; +} + + +static unsigned TreeCountNodes(OOCacheNode *node) +{ + if (node == NULL) return 0; + return 1 + TreeCountNodes(node->leftChild) + TreeCountNodes(node->rightChild); +} + + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS +// TreeCheckIntegrity(): verify the links and contents of a (sub-)tree. If successful, returns the root of the subtree (which could theoretically be changed), otherwise returns NULL. +static OOCacheNode *TreeCheckIntegrity(OOCacheImpl *cache, OOCacheNode *node, OOCacheNode *expectedParent, NSString *context) +{ + NSComparisonResult order; + BOOL OK = YES; + + if (node == NULL) return NULL; + + if (OK && node->key == nil) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\" has nil key; deleting subtree.", context, cache->name, CacheNodeGetDescription(node)); + OK = NO; + } + + if (OK && node->value == nil) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\" has nil value, deleting.", context, cache->name, CacheNodeGetDescription(node)); + OK = NO; + } + if (OK && node->leftChild != NULL) + { + order = [node->key compare:node->leftChild->key]; + if (order != NSOrderedDescending) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node %@'s left child %@ is not correctly ordered. Deleting subtree.", context, cache->name, CacheNodeGetDescription(node), CacheNodeGetDescription(node->leftChild)); + CacheNodeFree(cache, node->leftChild); + node->leftChild = nil; + cache->count = kCountUnknown; + } + else + { + node->leftChild = TreeCheckIntegrity(cache, node->leftChild, node, context); + } + } + if (node->rightChild != NULL) + { + order = [node->key compare:node->rightChild->key]; + if (order != NSOrderedAscending) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\"'s right child \"%@\" is not correctly ordered. Deleting subtree.", context, cache->name, CacheNodeGetDescription(node), CacheNodeGetDescription(node->rightChild)); + CacheNodeFree(cache, node->rightChild); + node->rightChild = nil; + cache->count = kCountUnknown; + } + else + { + node->rightChild = TreeCheckIntegrity(cache, node->rightChild, node, context); + } + } + + if (OK) return node; + else + { + cache->count = kCountUnknown; + CacheNodeFree(cache, node); + return NULL; + } +} +#endif // OOCACHE_PERFORM_INTEGRITY_CHECKS + + +/***** Age list functions *****/ + +// AgeListMakeYoungest(): place a given cache node at the youngest end of the age list. +static void AgeListMakeYoungest(OOCacheImpl *cache, OOCacheNode *node) +{ + if (cache == NULL || node == NULL) return; + + AgeListRemove(cache, node); + node->older = cache->youngest; + if (NULL != cache->youngest) cache->youngest->younger = node; + cache->youngest = node; + if (cache->oldest == NULL) cache->oldest = node; +} + + +// AgeListRemove(): remove a cache node from the age-sorted tree. Does not affect its position in the splay tree. +static void AgeListRemove(OOCacheImpl *cache, OOCacheNode *node) +{ + OOCacheNode *younger = NULL; + OOCacheNode *older = NULL; + + if (node == NULL) return; + + younger = node->younger; + older = node->older; + + if (cache->youngest == node) cache->youngest = older; + if (cache->oldest == node) cache->oldest = younger; + + node->younger = NULL; + node->older = NULL; + + if (younger != NULL) younger->older = older; + if (older != NULL) older->younger = younger; +} + + +#if OOCACHE_PERFORM_INTEGRITY_CHECKS + +static void AgeListCheckIntegrity(OOCacheImpl *cache, NSString *context) +{ + OOCacheNode *node = NULL, *next = NULL; + unsigned seenCount = 0; + + if (cache == NULL || context == NULL) return; + + node = cache->youngest; + + if (node) for (;;) + { + next = node->older; + ++seenCount; + if (next == nil) break; + + if (next->younger != node) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): node \"%@\" has invalid older link (should be \"%@\", is \"%@\"); repairing.", context, cache->name, CacheNodeGetDescription(next), CacheNodeGetDescription(node), CacheNodeGetDescription(next->older)); + next->older = node; + } + node = next; + } + + if (seenCount != cache->count) + { + // This is especially bad since this function is called just after verifying that the count field reflects the number of objects in the tree. + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): expected %u nodes, found %u. Cannot repair; clearing cache.", context, cache->name, cache->count, seenCount); + cache->count = 0; + CacheNodeFree(cache, cache->root); + cache->root = NULL; + cache->youngest = NULL; + cache->oldest = NULL; + return; + } + + if (node != cache->oldest) + { + OOLog(kOOLogCacheIntegrityCheck, @"Integrity check (%@ for \"%@\"): oldest pointer in cache is wrong (should be \"%@\", is \"%@\"); repairing.", context, cache->name, CacheNodeGetDescription(node), CacheNodeGetDescription(cache->oldest)); + cache->oldest = node; + } +} + +#endif // OOCACHE_PERFORM_INTEGRITY_CHECKS + + +#if DEBUG_GRAPHVIZ + +/* NOTE: enabling AGE_LIST can result in graph rendering times of many hours, + because determining paths for non-constraint arcs is NP-hard. In particular, + I gave up on rendering a dump of a fairly minimal cache manager after + three and a half hours. Individual caches were fine. +*/ +#define AGE_LIST 0 + +@implementation OOCache (DebugGraphViz) + +- (void) appendNodesFromSubTree:(OOCacheNode *)subTree toString:(NSMutableString *)ioString +{ + [ioString appendFormat:@"\tn%p [label=\" | %@ | \"];\n", subTree, EscapedGraphVizString([subTree->key description])]; + + if (subTree->leftChild != NULL) + { + [self appendNodesFromSubTree:subTree->leftChild toString:ioString]; + [ioString appendFormat:@"\tn%p:f0 -> n%p:f1;\n", subTree, subTree->leftChild]; + } + if (subTree->rightChild != NULL) + { + [self appendNodesFromSubTree:subTree->rightChild toString:ioString]; + [ioString appendFormat:@"\tn%p:f2 -> n%p:f1;\n", subTree, subTree->rightChild]; + } +} + + +- (NSString *) generateGraphVizBodyWithRootNamed:(NSString *)rootName +{ + NSMutableString *result = nil; + + result = [NSMutableString string]; + + // Root node representing cache + [result appendFormat:@"\t%@ [label=\"Cache \\\"%@\\\"\" shape=box];\n" + "\tnode [shape=record];\n\t\n", rootName, EscapedGraphVizString([self name])]; + + if (cache == NULL) return result; + + // Cache + [self appendNodesFromSubTree:cache->root toString:result]; + + // Arc from cache object to root node + [result appendString:@"\tedge [color=black constraint=true];\n"]; + [result appendFormat:@"\t%@ -> n%p:f1;\n", rootName, cache->root]; + +#if AGE_LIST + OOCacheNode *node = NULL; + // Arcs representing age list + [result appendString:@"\t\n\t// Age-sorted list in blue\n\tedge [color=blue constraint=false];\n"]; + node = cache->oldest; + while (node->younger != NULL) + { + [result appendFormat:@"\tn%p:f2 -> n%p:f0;\n", node, node->younger]; + node = node->younger; + } +#endif + + return result; +} + + +- (NSString *) generateGraphViz +{ + NSMutableString *result = nil; + + result = [NSMutableString string]; + + // Header + [result appendFormat: + @"// OOCache dump\n\n" + "digraph cache\n" + "{\n" + "\tgraph [charset=\"UTF-8\", label=\"OOCache \"%@\" debug dump\", labelloc=t, labeljust=l];\n\t\n", [self name]]; + + [result appendString:[self generateGraphVizBodyWithRootNamed:@"cache"]]; + + [result appendString:@"}\n"]; + + return result; +} + + +- (void) writeGraphVizToURL:(NSURL *)url +{ + NSString *graphViz = nil; + NSData *data = nil; + + graphViz = [self generateGraphViz]; + data = [graphViz dataUsingEncoding:NSUTF8StringEncoding]; + + if (data != nil) + { + [data writeToURL:url atomically:YES]; + } +} + + +- (void) writeGraphVizToPath:(NSString *)path +{ + [self writeGraphVizToURL:[NSURL fileURLWithPath:path]]; +} + +@end +#endif + diff --git a/src/Core/OOCacheManager.h b/src/Core/OOCacheManager.h new file mode 100644 index 00000000..d4ec1481 --- /dev/null +++ b/src/Core/OOCacheManager.h @@ -0,0 +1,71 @@ +/* + +OOCacheManager.h +By Jens Ayton + +Singleton class responsible for handling Olite's data cache. +The cache manager stores arbitrary property lists in separate namespaces +(referred to simply as caches). The cache is emptied if it was created with a +different verison of Oolite, or if it was created on a system with a different +byte sex. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOCache.h" + + +enum +{ + kOOCacheMinimumPruneThreshold = 25U, + kOOCacheDefaultPruneThreshold = 200U, + kOOCacheNoPrune = 0xFFFFFFFFU +}; + + +@interface OOCacheManager: NSObject +{ +@private + NSMutableDictionary *_caches; + BOOL _permitWrites; +} + ++ (id)sharedCache; + +- (id)objectForKey:(NSString *)inKey inCache:(NSString *)inCacheKey; +- (void)setObject:(id)inElement forKey:(NSString *)inKey inCache:(NSString *)inCacheKey; +- (void)removeObjectForKey:(NSString *)inKey inCache:(NSString *)inCacheKey; +- (void)clearCache:(NSString *)inCacheKey; +- (void)clearAllCaches; + +/* Prune thresholds: + when the number of objects in a cache reaches the prune threshold, old + objects are removed until the object count is no more than 80% of the + prune threshold. +*/ +- (void)setPruneThreshold:(unsigned)inThreshold forCache:(NSString *)inCacheKey; +- (unsigned)pruneThresholdForCache:(NSString *)inCacheKey; + +- (void)setAllowCacheWrites:(BOOL)flag; + +- (void)flush; + +@end diff --git a/src/Core/OOCacheManager.m b/src/Core/OOCacheManager.m new file mode 100644 index 00000000..8cc3698b --- /dev/null +++ b/src/Core/OOCacheManager.m @@ -0,0 +1,783 @@ +/* + +OOCacheManager.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCacheManager.h" +#import "OOCache.h" +#import "OOPListParsing.h" + + +#define AUTO_PRUNE 0 +#define PRUNE_BEFORE_FLUSH 0 + + +// Use the (presumed) most efficient plist format for each platform. +#if OOLITE_MAC_OS_X +#define CACHE_PLIST_FORMAT NSPropertyListBinaryFormat_v1_0 +#else +#define CACHE_PLIST_FORMAT NSPropertyListGNUstepBinaryFormat +#endif + + +static NSString * const kOOLogDataCacheFound = @"dataCache.found"; +static NSString * const kOOLogDataCacheNotFound = @"dataCache.notFound"; +static NSString * const kOOLogDataCacheRebuild = @"dataCache.rebuild"; +static NSString * const kOOLogDataCacheWriteSuccess = @"dataCache.write.success"; +static NSString * const kOOLogDataCacheWriteFailed = @"dataCache.write.failed"; +static NSString * const kOOLogDataCacheRetrieveSuccess = @"dataCache.retrieve.success"; +static NSString * const kOOLogDataCacheRetrieveFailed = @"dataCache.retrieve.failed"; +static NSString * const kOOLogDataCacheSetSuccess = @"dataCache.set.success"; +static NSString * const kOOLogDataCacheSetFailed = @"dataCache.set.failed"; +static NSString * const kOOLogDataCacheRemoveSuccess = @"dataCache.remove.success"; +static NSString * const kOOLogDataCacheClearSuccess = @"dataCache.clear.success"; +static NSString * const kOOLogDataCacheParamError = @"general.error.parameterError.OOCacheManager"; +static NSString * const kOOLogDataCacheBuildPathError = @"dataCache.write.buildPath.failed"; +static NSString * const kOOLogDataCacheSerializationError = @"dataCache.write.serialize.failed"; +static NSString * const kOOLogDataCacheRemovedOld = @"dataCache.removedOld"; + +static NSString * const kCacheKeyVersion = @"CFBundleVersion"; // Legacy name +static NSString * const kCacheKeyEndianTag = @"endian tag"; +static NSString * const kCacheKeyFormatVersion = @"format version"; +static NSString * const kCacheKeyCaches = @"caches"; + + +enum +{ + kEndianTagValue = 0x0123456789ABCDEFULL, + kFormatVersionValue = 29 +}; + + +static OOCacheManager *sSingleton = nil; + + +@interface OOCacheManager (Private) + +- (void)loadCache; +- (void)write; +- (void)clear; +- (BOOL)dirty; +- (void)markClean; + +- (void)deleteOldCache; +- (NSDictionary *)loadDict; +- (BOOL)writeDict:(NSDictionary *)inDict; + +- (void)buildCachesFromDictionary:(NSDictionary *)inDict; +- (NSDictionary *)dictionaryOfCaches; + +- (BOOL)directoryExists:(NSString *)inPath create:(BOOL)inCreate; + +@end + + +@interface OOCacheManager (PlatformSpecific) + +- (NSString *)cachePathCreatingIfNecessary:(BOOL)inCreate; +- (NSString *)oldCachePath; + +@end + + +@implementation OOCacheManager + +- (id)init +{ + self = [super init]; + if (self != nil) + { + _permitWrites = YES; + [self deleteOldCache]; + [self loadCache]; + } + return self; +} + + +- (void)dealloc +{ + [self clear]; + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{dirty=%s}", [self class], self, [self dirty] ? "yes" : "no"]; +} + + ++ (id)sharedCache +{ + // NOTE: assumes single-threaded access. + if (sSingleton == nil) + { + [[self alloc] init]; + } + + return sSingleton; +} + + +- (id)objectForKey:(NSString *)inKey inCache:(NSString *)inCacheKey +{ + OOCache *cache = nil; + id result = nil; + + // Sanity check + if (inCacheKey == nil || inKey == nil) + { + OOLog(kOOLogDataCacheParamError, @"Bad parameters -- nil key or cacheKey."); + return nil; + } + + cache = [_caches objectForKey:inCacheKey]; + if (cache != nil) + { + result = [cache objectForKey:inKey]; + if (result != nil) + { + OOLog(kOOLogDataCacheRetrieveSuccess, @"Retrieved \"%@\" cache object %@.", inCacheKey, inKey); + } + else + { + OOLog(kOOLogDataCacheRetrieveFailed, @"Failed to retrieve \"%@\" cache object %@ -- no such entry.", inCacheKey, inKey); + } + } + else + { + OOLog(kOOLogDataCacheRetrieveFailed, @"Failed to retreive\"%@\" cache object %@ -- no such cache.", inCacheKey, inKey); + } + + return result; +} + + + +- (void)setObject:(id)inObject forKey:(NSString *)inKey inCache:(NSString *)inCacheKey +{ + OOCache *cache = nil; + + // Sanity check + if (inObject == nil || inCacheKey == nil || inKey == nil) OOLog(kOOLogDataCacheParamError, @"Bad parameters -- nil object, key or cacheKey."); + + if (_caches == nil) return; + + cache = [_caches objectForKey:inCacheKey]; + if (cache == nil) + { + cache = [[[OOCache alloc] init] autorelease]; + if (cache == nil) + { + OOLog(kOOLogDataCacheSetFailed, @"Failed to create cache for key \"%@\".", inCacheKey); + return; + } + [cache setName:inCacheKey]; + [cache setAutoPrune:AUTO_PRUNE]; + [_caches setObject:cache forKey:inCacheKey]; + } + + [cache setObject:inObject forKey:inKey]; + OOLog(kOOLogDataCacheSetSuccess, @"Updated entry %@ in cache \"%@\".", inKey, inCacheKey); +} + + +- (void)removeObjectForKey:(NSString *)inKey inCache:(NSString *)inCacheKey +{ + OOCache *cache = nil; + + // Sanity check + if (inCacheKey == nil || inKey == nil) OOLog(kOOLogDataCacheParamError, @"Bad parameters -- nil key or cacheKey."); + + cache = [_caches objectForKey:inCacheKey]; + if (cache != nil) + { + if (nil != [cache objectForKey:inKey]) + { + [cache removeObjectForKey:inKey]; + OOLog(kOOLogDataCacheRemoveSuccess, @"Removed entry keyed %@ from cache \"%@\".", inKey, inCacheKey); + } + else + { + OOLog(kOOLogDataCacheRemoveSuccess, @"No need to remove non-existent entry keyed %@ from cache \"%@\".", inKey, inCacheKey); + } + } + else + { + OOLog(kOOLogDataCacheRemoveSuccess, @"No need to remove entry keyed %@ from non-existent cache \"%@\".", inKey, inCacheKey); + } +} + + +- (void)clearCache:(NSString *)inCacheKey +{ + // Sanity check + if (inCacheKey == nil) OOLog(kOOLogDataCacheParamError, @"Bad parameter -- nil cacheKey."); + + if (nil != [_caches objectForKey:inCacheKey]) + { + [_caches removeObjectForKey:inCacheKey]; + OOLog(kOOLogDataCacheClearSuccess, @"Cleared cache \"%@\".", inCacheKey); + } + else + { + OOLog(kOOLogDataCacheClearSuccess, @"No need to clear non-existent cache \"%@\".", inCacheKey); + } +} + + +- (void)clearAllCaches +{ + [_caches release]; + _caches = [[NSMutableDictionary alloc] init]; +} + + +- (void)setPruneThreshold:(unsigned)inThreshold forCache:(NSString *)inCacheKey +{ + OOCache *cache = nil; + + cache = [_caches objectForKey:inCacheKey]; + if (cache != nil) + { + [cache setPruneThreshold:inThreshold]; + } +} + + +- (unsigned)pruneThresholdForCache:(NSString *)inCacheKey +{ + OOCache *cache = nil; + + cache = [_caches objectForKey:inCacheKey]; + if (cache != nil) return [cache pruneThreshold]; + else return kOOCacheDefaultPruneThreshold; +} + + +- (void)flush +{ + if (_permitWrites && [self dirty]) + { + [self write]; + [self markClean]; + } +} + + +- (void)setAllowCacheWrites:(BOOL)flag +{ + _permitWrites = (flag != NO); +} + +@end + + +@implementation OOCacheManager (Private) + +- (void)loadCache +{ + NSDictionary *cache = nil; + NSString *cacheVersion = nil; + NSString *ooliteVersion = nil; + NSData *endianTag = nil; + NSNumber *formatVersion = nil; + BOOL accept = YES; + uint64_t endianTagValue = 0; + + ooliteVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:kCacheKeyVersion]; + + [self clear]; + + cache = [self loadDict]; + if (cache != nil) + { + // We have a cache + OOLog(kOOLogDataCacheFound, @"Found data cache."); + OOLogIndentIf(kOOLogDataCacheFound); + + cacheVersion = [cache objectForKey:kCacheKeyVersion]; + if (![cacheVersion isEqual:ooliteVersion]) + { + OOLog(kOOLogDataCacheRebuild, @"Data cache version (%@) does not match Oolite version (%@), rebuilding cache.", cacheVersion, ooliteVersion); + accept = NO; + } + + formatVersion = [cache objectForKey:kCacheKeyFormatVersion]; + if (accept && [formatVersion unsignedIntValue] != kFormatVersionValue) + { + OOLog(kOOLogDataCacheRebuild, @"Data cache format (%@) is not supported format (%u), rebuilding cache.", formatVersion, kFormatVersionValue); + accept = NO; + } + + if (accept) + { + endianTag = [cache objectForKey:kCacheKeyEndianTag]; + if (![endianTag isKindOfClass:[NSData class]] || [endianTag length] != sizeof endianTagValue) + { + OOLog(kOOLogDataCacheRebuild, @"Data cache endian tag is invalid, rebuilding cache."); + accept = NO; + } + else + { + endianTagValue = *(const uint64_t *)[endianTag bytes]; + if (endianTagValue != kEndianTagValue) + { + OOLog(kOOLogDataCacheRebuild, @"Data cache endianness is inappropriate for this system, rebuilding cache."); + accept = NO; + } + } + } + + if (accept) + { + // We have a cache, and it's the right format. + [self buildCachesFromDictionary:[cache objectForKey:kCacheKeyCaches]]; + } + + OOLogOutdentIf(kOOLogDataCacheFound); + } + else + { + // No cache + OOLog(kOOLogDataCacheNotFound, @"No data cache found, starting from scratch."); + } + + // If loading failed, or there was a version or endianness conflict + if (_caches == nil) _caches = [[NSMutableDictionary alloc] init]; +} + + +- (void)write +{ + NSMutableDictionary *newCache = nil; + NSString *ooliteVersion = nil; + NSData *endianTag = nil; + NSNumber *formatVersion = nil; + NSDictionary *pListRep = nil; + uint64_t endianTagValue = kEndianTagValue; + + if (_caches == nil) return; + +#if PRUNE_BEFORE_FLUSH + [[_caches allValues] makeObjectsPerformSelector:@selector(prune)]; +#endif + + OOLog(@"dataCache.willWrite", @"About to write data cache."); // Added for 1.69 to detect possible write-related crash. -- Ahruman + ooliteVersion = [[[NSBundle mainBundle] infoDictionary] objectForKey:kCacheKeyVersion]; + endianTag = [NSData dataWithBytes:&endianTagValue length:sizeof endianTagValue]; + formatVersion = [NSNumber numberWithUnsignedInt:kFormatVersionValue]; + + pListRep = [self dictionaryOfCaches]; + if (ooliteVersion == nil || endianTag == nil || formatVersion == nil || pListRep == nil) + { + OOLog(@"dataCache.cantWrite", @"Failed to write data cache -- prerequisites not fulfilled. (This is an internal error, please report it.)"); + return; + } + + newCache = [NSMutableDictionary dictionaryWithCapacity:4]; + [newCache setObject:ooliteVersion forKey:kCacheKeyVersion]; + [newCache setObject:formatVersion forKey:kCacheKeyFormatVersion]; + [newCache setObject:endianTag forKey:kCacheKeyEndianTag]; + [newCache setObject:pListRep forKey:kCacheKeyCaches]; + + if ([self writeDict:newCache]) + { + [self markClean]; + OOLog(kOOLogDataCacheWriteSuccess, @"Wrote data cache."); + } + else + { + OOLog(kOOLogDataCacheWriteFailed, @"Failed to write data cache."); + } +} + + +- (void)clear +{ + [_caches release]; + _caches = nil; +} + + +- (BOOL)dirty +{ + NSEnumerator *cacheEnum = nil; + OOCache *cache = nil; + + for (cacheEnum = [_caches objectEnumerator]; (cache = [cacheEnum nextObject]); ) + { + if ([cache dirty]) return YES; + } + return NO; +} + + +- (void)markClean +{ + NSEnumerator *cacheEnum = nil; + OOCache *cache = nil; + + for (cacheEnum = [_caches objectEnumerator]; (cache = [cacheEnum nextObject]); ) + { + [cache markClean]; + } +} + + +- (void)deleteOldCache +{ + // Since the cache location has changed, we delete the old cache (if any). + NSString *path = [self oldCachePath]; + NSFileManager *fmgr; + + fmgr = [NSFileManager defaultManager]; + if ([fmgr fileExistsAtPath:path]) + { + OOLog(kOOLogDataCacheRemovedOld, @"Removed old data cache."); + [fmgr removeFileAtPath:path handler:nil]; + } +} + + +- (NSDictionary *)loadDict +{ + NSString *path = nil; + NSData *data = nil; + NSString *errorString = nil; + id contents = nil; + + path = [self cachePathCreatingIfNecessary:NO]; + if (path == nil) return nil; + + NS_DURING + data = [NSData dataWithContentsOfFile:path]; + if (data == nil) NS_VALUERETURN(nil, NSDictionary *); + + contents = [NSPropertyListSerialization propertyListFromData:data + mutabilityOption:NSPropertyListImmutable + format:NULL + errorDescription:&errorString]; + NS_HANDLER + errorString = [localException reason]; + contents = nil; + NS_ENDHANDLER + + if (errorString != nil) + { + OOLog(@"dataCache.badData", @"Could not read data cache: %@", errorString); +#if OOLITE_RELEASE_PLIST_ERROR_STRINGS + [errorString release]; +#endif + return nil; + } + if (![contents isKindOfClass:[NSDictionary class]]) return nil; + + return contents; +} + + +- (BOOL)writeDict:(NSDictionary *)inDict +{ + NSString *path = nil; + NSData *plist = nil; + NSString *errorDesc = nil; + + path = [self cachePathCreatingIfNecessary:YES]; + if (path == nil) return NO; + + plist = [NSPropertyListSerialization dataFromPropertyList:inDict format:CACHE_PLIST_FORMAT errorDescription:&errorDesc]; + if (plist == nil) + { +#if OOLITE_RELEASE_PLIST_ERROR_STRINGS + [errorDesc autorelease]; +#endif + OOLog(kOOLogDataCacheSerializationError, @"Could not convert data cache to property list data: %@", errorDesc); + return NO; + } + + return [plist writeToFile:path atomically:NO]; +} + + +- (void)buildCachesFromDictionary:(NSDictionary *)inDict +{ + NSEnumerator *keyEnum = nil; + id key = nil; + id value = nil; + OOCache *cache = nil; + + if (inDict == nil ) return; + + [_caches release]; + _caches = [[NSMutableDictionary alloc] initWithCapacity:[inDict count]]; + + for (keyEnum = [inDict keyEnumerator]; (key = [keyEnum nextObject]); ) + { + value = [inDict objectForKey:key]; + cache = [[OOCache alloc] initWithPList:value]; + if (cache != nil) + { + [cache setName:key]; + [_caches setObject:cache forKey:key]; + [cache release]; + } + } +} + + +- (NSDictionary *)dictionaryOfCaches +{ + NSMutableDictionary *dict = nil; + NSEnumerator *keyEnum = nil; + id key = nil; + OOCache *cache = nil; + id pList = nil; + + dict = [NSMutableDictionary dictionaryWithCapacity:[_caches count]]; + for (keyEnum = [_caches keyEnumerator]; (key = [keyEnum nextObject]); ) + { + cache = [_caches objectForKey:key]; + pList = [cache pListRepresentation]; + + if (pList != nil) [dict setObject:pList forKey:key]; + } + + return dict; +} + + +- (BOOL)directoryExists:(NSString *)inPath create:(BOOL)inCreate +{ + BOOL exists, directory; + NSFileManager *fmgr = [NSFileManager defaultManager]; + + exists = [fmgr fileExistsAtPath:inPath isDirectory:&directory]; + + if (exists && !directory) + { + OOLog(kOOLogDataCacheBuildPathError, @"Expected %@ to be a folder, but it is a file.", inPath); + return NO; + } + if (!exists) + { + if (!inCreate) return NO; + if (![fmgr createDirectoryAtPath:inPath attributes:nil]) + { + OOLog(kOOLogDataCacheBuildPathError, @"Could not create folder %@.", inPath); + return NO; + } + } + + return YES; +} + +@end + + +@implementation OOCacheManager (PlatformSpecific) + +#if OOLITE_MAC_OS_X + +- (NSString *)cachePathCreatingIfNecessary:(BOOL)inCreate +{ + NSString *cachePath = nil; + + /* Construct the path for the cache file, which is: + ~/Library/Caches/org.aegidian.oolite/Data Cache.plist + In addition to generally being the right place to put caches, + ~/Library/Caches has the particular advantage of not being indexed by + Spotlight or, in future, backed up by Time Machine. + */ + cachePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + cachePath = [cachePath stringByAppendingPathComponent:@"Caches"]; + if (![self directoryExists:cachePath create:inCreate]) return nil; + cachePath = [cachePath stringByAppendingPathComponent:@"org.aegidian.oolite"]; + if (![self directoryExists:cachePath create:inCreate]) return nil; + cachePath = [cachePath stringByAppendingPathComponent:@"Data Cache.plist"]; + return cachePath; +} + + +- (NSString *)oldCachePath +{ + NSString *path = nil; + + path = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + path = [path stringByAppendingPathComponent:@"Application Support"]; + path = [path stringByAppendingPathComponent:@"Oolite"]; + path = [path stringByAppendingPathComponent:@"cache"]; + + return path; +} + +#else + +- (NSString *)cachePathCreatingIfNecessary:(BOOL)inCreate +{ + NSString *cachePath = nil; + + /* Construct the path for the cache file, which is: + ~/GNUstep/Library/Caches/Oolite-cache.plist + */ + cachePath = [NSHomeDirectory() stringByAppendingPathComponent:@"GNUstep"]; + if (![self directoryExists:cachePath create:inCreate]) return nil; + cachePath = [cachePath stringByAppendingPathComponent:@"Library"]; + if (![self directoryExists:cachePath create:inCreate]) return nil; + cachePath = [cachePath stringByAppendingPathComponent:@"Caches"]; + if (![self directoryExists:cachePath create:inCreate]) return nil; + cachePath = [cachePath stringByAppendingPathComponent:@"Oolite-cache.plist"]; + + return cachePath; +} + + +- (NSString *)oldCachePath +{ + return [[[NSHomeDirectory() stringByAppendingPathComponent:@"GNUstep"] + stringByAppendingPathComponent:@"Library"] + stringByAppendingPathComponent:@"Oolite-cache"]; +} + +#endif + +@end + + +@implementation OOCacheManager (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedCache above. + + NOTE: assumes single-threaded access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end + + +#if DEBUG_GRAPHVIZ +@interface OOCache (DebugGraphViz) +- (NSString *) generateGraphVizBodyWithRootNamed:(NSString *)rootName; +@end + + +@implementation OOCacheManager (DebugGraphViz) + +- (NSString *) generateGraphViz +{ + NSMutableString *result = nil; + NSEnumerator *cacheEnum = nil; + OOCache *cache = nil; + NSString *cacheRootName = nil; + NSString *cacheGraphViz = nil; + + result = [NSMutableString string]; + + // Header + [result appendString: + @"// OOCacheManager dump\n\n" + "digraph caches\n" + "{\n" + "\tgraph [charset=\"UTF-8\", label=\"OOCacheManager debug dump\", labelloc=t, labeljust=l];\n\t\n"]; + + // Node representing cache manager + [result appendString:@"\tcacheManger [label=OOCacheManager shape=Mdiamond];\n"]; + + // Nodes and arcs for individual caches, and arcs from cache manager to each cache. + for (cacheEnum = [_caches objectEnumerator]; (cache = [cacheEnum nextObject]); ) + { + cacheRootName = [NSString stringWithFormat:@"cache_%p", cache]; + cacheGraphViz = [cache generateGraphVizBodyWithRootNamed:cacheRootName]; + [result appendString:@"\t\n"]; + [result appendString:cacheGraphViz]; + [result appendFormat:@"\tcacheManger -> %@ [color=green constraint=true];\n", cacheRootName]; + } + + [result appendString:@"}\n"]; + + return result; +} + + +- (void) writeGraphVizToURL:(NSURL *)url +{ + NSString *graphViz = nil; + NSData *data = nil; + + graphViz = [self generateGraphViz]; + data = [graphViz dataUsingEncoding:NSUTF8StringEncoding]; + + if (data != nil) + { + [data writeToURL:url atomically:YES]; + } +} + + +- (void) writeGraphVizToPath:(NSString *)path +{ + [self writeGraphVizToURL:[NSURL fileURLWithPath:path]]; +} + +@end +#endif diff --git a/src/Core/OOCamera.h b/src/Core/OOCamera.h new file mode 100644 index 00000000..9c4fc62c --- /dev/null +++ b/src/Core/OOCamera.h @@ -0,0 +1,82 @@ +/* + +OOCamera.h + +Class representing cameras/viewpoints. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOMaths.h" +#import "OOSpatialReference.h" + + +@interface OOCamera: NSObject +{ + Vector _position; + Quaternion _orientation; + id _reference; +} + +- (Vector) position; +- (void) setPosition:(Vector)position; + +- (Quaternion) orientation; +- (void) setOrientation:(Quaternion)orientation; + +// If reference is not nil, position and orientation are relative to reference. +- (id )reference; +- (void) setReference:(id )reference; + + +- (void) rotateToHeading:(Vector)heading upVector:(Vector)upVector; + +- (OOMatrix) transformationMatrix; +- (OOMatrix) rotationMatrix; + +- (void) glApply; +- (void) glApplyRelative; + +@end diff --git a/src/Core/OOCamera.m b/src/Core/OOCamera.m new file mode 100644 index 00000000..2d04324b --- /dev/null +++ b/src/Core/OOCamera.m @@ -0,0 +1,163 @@ +/* + +OOCamera.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCamera.h" +#import "OOMacroOpenGL.h" + + +@implementation OOCamera + +- (id) init +{ + self = [super init]; + if (self != nil) + { + _orientation = kIdentityQuaternion; + } + return self; +} + + +- (void) dealloc +{ + [_reference autorelease]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + NSString *refDesc = @""; + if (_reference != nil) refDesc = [NSString stringWithFormat:@"relative to ", [(id)_reference shortDescription]]; + + return [NSString stringWithFormat:@"position: %@ orientation: %@%@", VectorDescription([self position]), QuaternionDescription([self orientation]), refDesc]; +} + + +- (Vector) position +{ + return _position; +} + + +- (void) setPosition:(Vector)position +{ + _position = position; +} + + +- (Quaternion) orientation +{ + return _orientation; +} + + +- (void) setOrientation:(Quaternion)orientation +{ + _orientation = orientation; +} + + +- (id )reference +{ + return _reference; +} + + +- (void) setReference:(id )reference +{ + [_reference autorelease]; + _reference = [reference retain]; +} + + +- (void) rotateToHeading:(Vector)heading upVector:(Vector)upVector +{ + +} + + +- (OOMatrix) transformationMatrix +{ + return OOMatrixForQuaternionRotation([self orientation]); +} + + +- (OOMatrix) rotationMatrix +{ + return OOMatrixTranslate([self rotationMatrix], [self position]); +} + + +- (void) glApply +{ + GLint matrixMode; + + OO_ENTER_OPENGL(); + + glGetIntegerv(GL_MATRIX_MODE, &matrixMode); + glMatrixMode(GL_MODELVIEW); + GLLoadOOMatrix([self rotationMatrix]); // Absolute + glMatrixMode(matrixMode); +} + + +- (void) glApplyRelative +{ + GLint matrixMode; + + OO_ENTER_OPENGL(); + + glGetIntegerv(GL_MATRIX_MODE, &matrixMode); + glMatrixMode(GL_MODELVIEW); + GLMultOOMatrix([self rotationMatrix]); // Relative + glMatrixMode(matrixMode); +} + +@end diff --git a/src/Core/OOCharacter.h b/src/Core/OOCharacter.h new file mode 100644 index 00000000..4dad79da --- /dev/null +++ b/src/Core/OOCharacter.h @@ -0,0 +1,94 @@ +/* + +OOCharacter.h + +Represents an NPC person (as opposed to an NPC ship). + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOTypes.h" +#import "legacy_random.h" + +#ifdef OO_BRAIN_AI +@class OOBrain; +#endif + +@interface OOCharacter : NSObject +{ + NSString *name; + NSString *shortDescription; + NSString *longDescription; + Random_Seed originSystemSeed; + Random_Seed genSeed; + int legalStatus; + OOCreditsQuantity insuranceCredits; + +#ifdef OO_BRAIN_AI + OOBrain *brain; // brain of character +#endif + + NSArray *script_actions; +} + +- (id) initWithGenSeed:(Random_Seed)g_seed andOriginalSystemSeed:(Random_Seed)s_seed; +- (id) initWithRole:(NSString *)role andOriginalSystemSeed:(Random_Seed)s_seed; + ++ (OOCharacter *) characterWithRole:(NSString *)c_role andOriginalSystem:(Random_Seed)o_seed; ++ (OOCharacter *) randomCharacterWithRole:(NSString *)c_role andOriginalSystem:(Random_Seed)o_seed; ++ (OOCharacter *) characterWithDictionary:(NSDictionary *)c_dict; + +- (NSString*) planetOfOrigin; +- (NSString*) species; + +- (void) basicSetUp; +- (BOOL) castInRole:(NSString *)role; + +- (NSString *) name; +- (void) setName:(NSString *)value; + +- (NSString *) shortDescription; +- (void) setShortDescription:(NSString *)value; + +- (NSString *) longDescription; +- (void) setLongDescription:(NSString *)value; + +- (Random_Seed) originSystemSeed; +- (void) setOriginSystemSeed:(Random_Seed)value; + +- (Random_Seed) genSeed; +- (void) setGenSeed:(Random_Seed)value; + +- (int) legalStatus; +- (void) setLegalStatus:(int)value; + +- (OOCreditsQuantity) insuranceCredits; +- (void) setInsuranceCredits:(OOCreditsQuantity)value; + +- (NSArray *) script; +- (void) setScript:(NSArray *)some_actions; + +#ifdef OO_BRAIN_AI +- (OOBrain *) brain; +- (void) setBrain:(OOBrain *)aBrain; +#endif + +@end diff --git a/src/Core/OOCharacter.m b/src/Core/OOCharacter.m new file mode 100644 index 00000000..86a2373c --- /dev/null +++ b/src/Core/OOCharacter.m @@ -0,0 +1,516 @@ +/* + +OOCharacter.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCharacter.h" + +#import "Universe.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" + +#ifdef OO_BRAIN_AI +#import "OOBrain.h" +#endif + + +@interface OOCharacter (Private) + + +- (void) setCharacterFromDictionary:(NSDictionary *)dict; + +@end + + +@implementation OOCharacter + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"%@, %@. bounty: %i insurance: %llu", [self name], [self shortDescription], [self legalStatus], [self insuranceCredits]]; +} + + +- (NSString *) jsClassName +{ + return @"Character"; +} + + +- (void) dealloc +{ + [name release]; + [shortDescription release]; + [longDescription release]; + [script_actions release]; +#ifdef OO_BRAIN_AI + [brain release]; +#endif + + [super dealloc]; +} + + +- (id) initWithGenSeed:(Random_Seed) g_seed andOriginalSystemSeed:(Random_Seed) s_seed +{ + self = [super init]; + + // do character set-up + genSeed = g_seed; + originSystemSeed = s_seed; + + [self basicSetUp]; + + return self; +} + + +- (id) initWithRole:(NSString *) role andOriginalSystemSeed:(Random_Seed) s_seed +{ + self = [super init]; + + // do character set-up + originSystemSeed = s_seed; + make_pseudo_random_seed( &genSeed); + + [self basicSetUp]; + + [self castInRole: role]; + + return self; +} + ++ (OOCharacter *) characterWithRole:(NSString *) c_role andOriginalSystem:(Random_Seed) o_seed +{ + return [[[OOCharacter alloc] initWithRole: c_role andOriginalSystemSeed: o_seed] autorelease]; +} + ++ (OOCharacter *) randomCharacterWithRole:(NSString *) c_role andOriginalSystem:(Random_Seed) o_seed +{ + Random_Seed r_seed; + + r_seed.a = (ranrot_rand() & 0xff); + r_seed.b = (ranrot_rand() & 0xff); + r_seed.c = (ranrot_rand() & 0xff); + r_seed.d = (ranrot_rand() & 0xff); + r_seed.e = (ranrot_rand() & 0xff); + r_seed.f = (ranrot_rand() & 0xff); + + OOCharacter *castmember = [[[OOCharacter alloc] initWithGenSeed: r_seed andOriginalSystemSeed: o_seed] autorelease]; + + if ([castmember castInRole: c_role]) + return castmember; + else + { + return castmember; + } +} + ++ (OOCharacter *) characterWithDictionary:(NSDictionary *) c_dict +{ + OOCharacter *castmember = [[[OOCharacter alloc] init] autorelease]; + [castmember setCharacterFromDictionary: c_dict]; + return castmember; +} + + +- (NSString *) planetOfOrigin +{ + // determine the planet of origin + NSDictionary* originInfo = [UNIVERSE generateSystemData: originSystemSeed]; + return [originInfo objectForKey: KEY_NAME]; +} + + +- (NSString *) species +{ + // determine the character's species + int species = genSeed.f & 0x03; // 0-1 native to home system, 2 human colonial, 3 other + BOOL lowercaseIgnore = [[UNIVERSE descriptions] boolForKey:@"lowercase_ignore"]; // i18n. + NSString* speciesString = (species == 3)? [UNIVERSE getSystemInhabitants: genSeed plural:NO]:[UNIVERSE getSystemInhabitants: originSystemSeed plural:NO]; + if (lowercaseIgnore) + { + return [speciesString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + return [[speciesString lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + + +- (void) basicSetUp +{ + // save random seeds for restoration later + RNG_Seed saved_seed = currentRandomSeed(); + // set RNG to character seed + seed_for_planet_description(genSeed); + + // determine the planet of origin + NSDictionary* originInfo = [UNIVERSE generateSystemData: originSystemSeed]; + NSString* planetName = [originInfo objectForKey: KEY_NAME]; + int government = [[originInfo objectForKey:KEY_GOVERNMENT] intValue]; // 0 .. 7 (0 anarchic .. 7 most stable) + int criminal_tendency = government ^ 0x07; + + // determine the character's species + NSString* speciesString = [self species]; + + // determine the character's name + seed_RNG_only_for_planet_description(genSeed); + NSString* genName; + if ([speciesString hasPrefix:@"human"]) + genName = [NSString stringWithFormat:@"%@ %@", ExpandDescriptionForSeed(@"%R", genSeed), ExpandDescriptionForSeed(@"[nom]", genSeed)]; + else + genName = [NSString stringWithFormat:@"%@ %@", ExpandDescriptionForSeed(@"%R", genSeed), ExpandDescriptionForSeed(@"%R", genSeed)]; + + [self setName: genName]; + + [self setShortDescription: [NSString stringWithFormat:ExpandDescriptionForSeed(@"[character-a-@-from-@]", genSeed), speciesString, planetName]]; + [self setLongDescription: [self shortDescription]]; + + // determine legalStatus for a completely random character + [self setLegalStatus: 0]; // clean + int legal_index = gen_rnd_number() & gen_rnd_number() & 0x03; + while (((gen_rnd_number() & 0xf) < criminal_tendency)&&(legal_index < 3)) + legal_index++; + if (legal_index == 3) // criminal + [self setLegalStatus: criminal_tendency + criminal_tendency * (gen_rnd_number() & 0x03) + (gen_rnd_number() & gen_rnd_number() & 0x7f)]; + legal_index = 0; + if (legalStatus) legal_index = (legalStatus <= 50) ? 1 : 2; + +#if 0 + NSString *legalDesc = nil; + // Never used! What was intended? -- Ahruman 2008-11-10 + switch (legal_index) + { + case 0: + legalDesc = @"clean"; + break; + case 1: + legalDesc = @"an offender"; + break; + case 2: + legalDesc = @"a fugitive"; + break; + default: + // never should get here + legalDesc = @"an unperson"; + } +#endif + + // if clean - determine insurance level (if any) + [self setInsuranceCredits:0]; + if (legal_index == 0) + { + int insurance_index = gen_rnd_number() & gen_rnd_number() & 0x03; + switch (insurance_index) + { + case 1: + [self setInsuranceCredits:125]; + break; + case 2: + [self setInsuranceCredits:250]; + break; + case 3: + [self setInsuranceCredits:500]; + } + } + + // restore random seed + setRandomSeed( saved_seed); +} + + +- (BOOL) castInRole:(NSString *) role +{ + BOOL specialSetUpDone = NO; + NSString *legalDesc; + + role = [role lowercaseString]; + + if ([role isEqual:@"pirate"]) + { + // determine legalStatus for a completely random character + int sins = 0x08 | (genSeed.a & genSeed.b); + [self setLegalStatus: sins & 0x7f]; + int legal_index = (legalStatus <= 50) ? 1 : 2; + switch (legal_index) + { + case 1: + legalDesc = @"offender"; + break; + case 2: + legalDesc = @"fugitive"; + break; + } + [self setLongDescription: + ExpandDescriptionForSeed([NSString stringWithFormat:@"%@ is a [21] %@ from %@", [self name], legalDesc, [self planetOfOrigin]], genSeed)]; + + specialSetUpDone = YES; + } + + if ([role isEqual:@"trader"]) + { + legalDesc = @"clean"; + [self setLegalStatus: 0]; // clean + + int insurance_index = gen_rnd_number() & 0x03; + switch (insurance_index) + { + case 0: + [self setInsuranceCredits:0]; + break; + case 1: + [self setInsuranceCredits:125]; + break; + case 2: + [self setInsuranceCredits:250]; + break; + case 3: + [self setInsuranceCredits:500]; + } + specialSetUpDone = YES; + } + + if ([role isEqual:@"hunter"]) + { + legalDesc = @"clean"; + [self setLegalStatus:0]; // clean + int insurance_index = gen_rnd_number() & 0x03; + if (insurance_index == 3) + [self setInsuranceCredits:500]; + specialSetUpDone = YES; + } + + if ([role isEqual:@"police"]) + { + legalDesc = @"clean"; + [self setLegalStatus:0]; // clean + [self setInsuranceCredits:125]; + specialSetUpDone = YES; + } + + if ([role isEqual:@"miner"]) + { + legalDesc = @"clean"; + [self setLegalStatus:0]; // clean + [self setInsuranceCredits:25]; + specialSetUpDone = YES; + } + + if ([role isEqual:@"passenger"]) + { + legalDesc = @"clean"; + [self setLegalStatus:0]; // clean + int insurance_index = gen_rnd_number() & 0x03; + switch (insurance_index) + { + case 0: + [self setInsuranceCredits:25]; + break; + case 1: + [self setInsuranceCredits:125]; + break; + case 2: + [self setInsuranceCredits:250]; + break; + case 3: + [self setInsuranceCredits:500]; + } + specialSetUpDone = YES; + } + + if ([role isEqual:@"slave"]) + { + legalDesc = @"clean"; + [self setLegalStatus:0]; // clean + [self setInsuranceCredits:0]; + specialSetUpDone = YES; + } + + // do long description here + // + + return specialSetUpDone; +} + + +- (NSString *)name +{ + return name; +} + + +- (NSString *)shortDescription +{ + return shortDescription; +} + + +- (NSString *)longDescription +{ + return longDescription; +} + + +- (Random_Seed)originSystemSeed +{ + return originSystemSeed; +} + + +- (Random_Seed)genSeed +{ + return genSeed; +} + + +- (int)legalStatus +{ + return legalStatus; +} + + +- (OOCreditsQuantity)insuranceCredits +{ + return insuranceCredits; +} + + +- (NSArray *)script +{ + return script_actions; +} + + +#ifdef OO_BRAIN_AI +- (OOBrain *)brain +{ + return brain; +} + + +- (void) setBrain:(OOBrain *)aBrain +{ + if (aBrain != brain) + { + [brain release]; + brain = [aBrain retain]; + if (brain != nil) + { + [brain setOwner:self]; + } + } +} +#endif + + +- (void)setName:(NSString *)value +{ + [name autorelease]; + name = [value copy]; +} + + +- (void)setShortDescription:(NSString *)value +{ + [shortDescription autorelease]; + shortDescription = [value copy]; +} + + +- (void)setLongDescription:(NSString *)value +{ + [longDescription autorelease]; + longDescription = [value copy]; +} + + +- (void)setOriginSystemSeed:(Random_Seed)value +{ + originSystemSeed = value; +} + + +- (void)setGenSeed:(Random_Seed)value +{ + genSeed = value; +} + + +- (void)setLegalStatus:(int)value +{ + legalStatus = value; +} + + +- (void)setInsuranceCredits:(OOCreditsQuantity)value +{ + insuranceCredits = value; +} + + +- (void)setScript:(NSArray *)some_actions +{ + [script_actions autorelease]; + script_actions = [some_actions copy]; +} + + +- (void) setCharacterFromDictionary:(NSDictionary *) dict +{ + id origin = nil; + + origin = [dict objectForKey:@"origin"]; + if ([origin isKindOfClass:[NSNumber class]] || + ([origin respondsToSelector:@selector(intValue)] && ([origin intValue] != 0 || [origin isEqual:@"0"]))) + { + // Number or numerical string + [self setOriginSystemSeed:[UNIVERSE systemSeedForSystemNumber:[origin intValue]]]; + } + else if ([origin isKindOfClass:[NSString class]]) + { + Random_Seed seed = [UNIVERSE systemSeedForSystemName:origin]; + if (is_nil_seed(seed)) + { + OOLogERR(@"character.load.unknownSystem", @"could not find a system named '%@' in this galaxy.", origin); + } + else + { + [self setOriginSystemSeed:seed]; + } + } + + if ([dict objectForKey:@"random_seed"]) + { + Random_Seed g_seed = RandomSeedFromString([dict stringForKey:@"random_seed"]); + [self setGenSeed: g_seed]; + [self basicSetUp]; + } + + if ([dict stringForKey:@"role"]) [self castInRole:[dict stringForKey:@"role"]]; + if ([dict stringForKey:@"name"]) [self setName:[dict stringForKey:@"name"]]; + if ([dict stringForKey:@"short_description"]) [self setShortDescription:[dict stringForKey:@"short_description"]]; + if ([dict stringForKey:@"long_description"]) [self setLongDescription:[dict stringForKey:@"long_description"]]; + if ([dict objectForKey:@"legal_status"]) [self setLegalStatus:[dict intForKey:@"legal_status"]]; + if ([dict objectForKey:@"bounty"]) [self setLegalStatus:[dict intForKey:@"bounty"]]; + if ([dict objectForKey:@"insurance"]) [self setInsuranceCredits:[dict unsignedLongLongForKey:@"insurance"]]; + if ([dict arrayForKey:@"script_actions"]) [self setScript:[dict arrayForKey:@"script_actions"]]; +} + +@end diff --git a/src/Core/OOCocoa.h b/src/Core/OOCocoa.h new file mode 100644 index 00000000..e57fd5e4 --- /dev/null +++ b/src/Core/OOCocoa.h @@ -0,0 +1,353 @@ +/* + +OOCocoa.h + +Import OpenStep main headers and define some Macisms and other compatibility +stuff. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +#ifdef GNUSTEP + #define OOLITE_GNUSTEP 1 +#else + #import + + #define OOLITE_MAC_OS_X 1 + #define OOLITE_HAVE_APPKIT 1 + #ifdef OOLITE_SDL_MAC + #define OOLITE_SDL 1 + #endif + + /* Enforce type-clean use of nil and Nil under OS X. (They are untyped in + Cocoa, apparently for compatibility with legacy Mac OS code, but typed in + GNUstep.) + */ + #undef nil + #define nil ((id)0) + #undef Nil + #define Nil ((Class)nil) + + /* Lots of stuff changed between 10.3.9 SDK used for 32-bit builds and 10.5 + SDK used for 64-bit builds. + */ + #if defined MAC_OS_X_VERSION_10_5 && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 + #define OOLITE_LEOPARD 1 + #else + #define OOLITE_LEOPARD 0 + #endif +#endif + + +#if OOLITE_GNUSTEP && !defined(OOLITE_SDL_MAC) +#include +#include // to get UINT_MAX + + +#define OOLITE_SDL 1 + +#ifdef WIN32 +#define OOLITE_WINDOWS 1 +#endif + +#ifdef LINUX +#define OOLITE_LINUX 1 +#endif + + +#define Boolean unsigned char +#define Byte unsigned char +#define true 1 +#define false 0 + +#if !defined(MAX) + #define MAX(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a > __b ? __a : __b; }) +#endif + +#if !defined(MIN) + #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) +#endif + +#define IBOutlet /**/ +#define IBAction void + +typedef int32_t CGMouseDelta; + +#import "Comparison.h" + +/* Define AppKit constants for events */ +enum { + NSUpArrowFunctionKey = 0xF700, + NSDownArrowFunctionKey = 0xF701, + NSLeftArrowFunctionKey = 0xF702, + NSRightArrowFunctionKey = 0xF703, + NSF1FunctionKey = 0xF704, + NSF2FunctionKey = 0xF705, + NSF3FunctionKey = 0xF706, + NSF4FunctionKey = 0xF707, + NSF5FunctionKey = 0xF708, + NSF6FunctionKey = 0xF709, + NSF7FunctionKey = 0xF70A, + NSF8FunctionKey = 0xF70B, + NSF9FunctionKey = 0xF70C, + NSF10FunctionKey = 0xF70D, + NSF11FunctionKey = 0xF70E, + NSF12FunctionKey = 0xF70F, + NSF13FunctionKey = 0xF710, + NSF14FunctionKey = 0xF711, + NSF15FunctionKey = 0xF712, + NSF16FunctionKey = 0xF713, + NSF17FunctionKey = 0xF714, + NSF18FunctionKey = 0xF715, + NSF19FunctionKey = 0xF716, + NSF20FunctionKey = 0xF717, + NSF21FunctionKey = 0xF718, + NSF22FunctionKey = 0xF719, + NSF23FunctionKey = 0xF71A, + NSF24FunctionKey = 0xF71B, + NSF25FunctionKey = 0xF71C, + NSF26FunctionKey = 0xF71D, + NSF27FunctionKey = 0xF71E, + NSF28FunctionKey = 0xF71F, + NSF29FunctionKey = 0xF720, + NSF30FunctionKey = 0xF721, + NSF31FunctionKey = 0xF722, + NSF32FunctionKey = 0xF723, + NSF33FunctionKey = 0xF724, + NSF34FunctionKey = 0xF725, + NSF35FunctionKey = 0xF726, + NSInsertFunctionKey = 0xF727, + NSDeleteFunctionKey = 0xF728, + NSHomeFunctionKey = 0xF729, + NSBeginFunctionKey = 0xF72A, + NSEndFunctionKey = 0xF72B, + NSPageUpFunctionKey = 0xF72C, + NSPageDownFunctionKey = 0xF72D, + NSPrintScreenFunctionKey = 0xF72E, + NSScrollLockFunctionKey = 0xF72F, + NSPauseFunctionKey = 0xF730, + NSSysReqFunctionKey = 0xF731, + NSBreakFunctionKey = 0xF732, + NSResetFunctionKey = 0xF733, + NSStopFunctionKey = 0xF734, + NSMenuFunctionKey = 0xF735, + NSUserFunctionKey = 0xF736, + NSSystemFunctionKey = 0xF737, + NSPrintFunctionKey = 0xF738, + NSClearLineFunctionKey = 0xF739, + NSClearDisplayFunctionKey = 0xF73A, + NSInsertLineFunctionKey = 0xF73B, + NSDeleteLineFunctionKey = 0xF73C, + NSInsertCharFunctionKey = 0xF73D, + NSDeleteCharFunctionKey = 0xF73E, + NSPrevFunctionKey = 0xF73F, + NSNextFunctionKey = 0xF740, + NSSelectFunctionKey = 0xF741, + NSExecuteFunctionKey = 0xF742, + NSUndoFunctionKey = 0xF743, + NSRedoFunctionKey = 0xF744, + NSFindFunctionKey = 0xF745, + NSHelpFunctionKey = 0xF746, + NSModeSwitchFunctionKey = 0xF747 +}; + +#endif + + +#ifndef OOLITE_GNUSTEP +#define OOLITE_GNUSTEP 0 +#endif + +#ifndef OOLITE_MAC_OS_X +#define OOLITE_MAC_OS_X 0 +#endif + +#ifndef OOLITE_WINDOWS +#define OOLITE_WINDOWS 0 +#endif + +#ifndef OOLITE_LINUX +#define OOLITE_LINUX 0 +#endif + +#ifndef OOLITE_SDL +#define OOLITE_SDL 0 +#endif + +#ifndef OOLITE_HAVE_APPKIT +#define OOLITE_HAVE_APPKIT 0 +#endif + +#define OOLITE_HAVE_JOYSTICK OOLITE_SDL + + +// When Oolite-Linux used AppKit, the load/save dialogs didn't work well with the SDL window, so we use a separate macro for this. +#define OOLITE_USE_APPKIT_LOAD_SAVE OOLITE_MAC_OS_X + + +#import "OOLogging.h" + + +@interface NSObject (OODescriptionComponents) + +/* In order to allow implementations of -description to inherit description + components from superclasses, and to allow implementations of -description + and -javaScriptDescription to share code, both are implemented as wrappers + around -descriptionComponents. -descriptionComponents should provide + information about an object without a class name or surrounding + punctuation. -description will wrap the components like this: + {descriptionComponents} + and -javaScriptDescription will wrap them like this: + [JSClassName descriptionComponents] +*/ +- (NSString *)descriptionComponents; + + +/* A lot of Oolite's -description implementations are rather long, and many + embed other descriptions. -shortDescription provides a truncated + alternative, while -shortDescriptionComponents provides a + -descriptionComponents-like mechanism to simplify implementation. +*/ +- (NSString *) shortDescription; +- (NSString *) shortDescriptionComponents; + +@end + + +/* Under Mac OS X 10.4 and earlier, but not GNUstep, the error string + parameters for the property list parsing primitive methods in + NSPropertyListSerialization would contain unreleased strings. To avoid + leaking, it is necessary to release them. This does not affect programs + linked against the Mac OS X 10.5 SDK. +*/ +#if OOLITE_MAC_OS_X + #if defined OOLITE_LEOPARD + #define OOLITE_RELEASE_PLIST_ERROR_STRINGS 0 + #else + #define OOLITE_RELEASE_PLIST_ERROR_STRINGS 1 + #endif +#else + #define OOLITE_RELEASE_PLIST_ERROR_STRINGS 0 +#endif + + + +/* OOInteger and OOUInteger: int (32-bit) on 32-bit platforms, long (64-bit) + on 64-bit platforms. This is equivalent to NSInteger/NSUInteger in OS X + 10.5 and GNUstep 1.16. + + Why not long in 32-bit? Because we want to avoid "multiple methods" + warnings when using SDKs using int. + + Similarly, CGFloat is 64-bit in 64-bit and 32-bit in 32-bit under OS X. + There is no need to make this distinction in GNUStep. +*/ + +#if OOLITE_MAC_OS_X + #if OOLITE_LEOPARD && NSINTEGER_DEFINED + // If OS X 10.5 SDK, use system definitions. + typedef NSInteger OOInteger; + typedef NSUInteger OOUInteger; + typedef CGFloat OOCGFloat; + #if __LP64__ + #define OOLITE_64_BIT 1 + #endif + #else + // Older SDK, 32-bit only. + typedef int OOInteger; + typedef unsigned int OOUInteger; + typedef float OOCGFloat; + #endif +#elif OOLITE_GNUSTEP + /* MKW 20090414 - GNUStep 1.19 still has the NSInteger bug, so let's revert + * to the older definitions for OOInteger */ + #if GNUSTEP_BASE_MAJOR_VERSION > 1 || GNUSTEP_BASE_MINOR_VERSION >= 20 + typedef NSInteger OOInteger; + typedef NSUInteger OOUInteger; + #else + // Older versions of GNUstep used int on all systems. + typedef int OOInteger; + typedef unsigned int OOUInteger; + #endif + typedef float OOCGFloat; +#endif + +#ifndef OOLITE_64_BIT + #define OOLITE_64_BIT 0 +#endif + + +/* Use native exception handling under OS X. + This would probably work for Linux too, but not Windows as yet. + */ + +#if OOLITE_MAC_OS_X + #undef NS_DURING + #undef NS_HANDLER + #undef NS_ENDHANDLER + #undef NS_VALUERETURN(v,t) + #undef NS_VOIDRETURN + + #define NS_DURING @try { + #define NS_HANDLER } @catch (NSException *localException) { + #define NS_ENDHANDLER } + #define NS_VALUERETURN(v,t) return (v) + #define NS_VOIDRETURN return +#endif + + +/* For some reason, return types for some comparison callbacks are typed + NSInteger/int under OS X but (more sensibly) NSComparisonResult under + GNUstep. +*/ +#if OOLITE_MAC_OS_X + typedef OOInteger OOComparisonResult; +#else + typedef NSComparisonResult OOComparisonResult; +#endif + + +#if OOLITE_MAC_OS_X && OOLITE_LEOPARD + #define OOLITE_FAST_ENUMERATION 1 +#else + #define OOLITE_FAST_ENUMERATION 0 +#endif + + +/* Speech synthesis +*/ +#if OOLITE_MAC_OS_X || defined(HAVE_LIBESPEAK) + #define OOLITE_SPEECH_SYNTH 1 + #if defined(HAVE_LIBESPEAK) + #define OOLITE_ESPEAK 1 + #else + #define OOLITE_ESPEAK 0 + #endif +#else + #define OOLITE_SPEECH_SYNTH 0 +#endif + + +#ifndef OO_DEBUG +#define OO_DEBUG 0 +#endif diff --git a/src/Core/OOCocoa.m b/src/Core/OOCocoa.m new file mode 100644 index 00000000..c73e98db --- /dev/null +++ b/src/Core/OOCocoa.m @@ -0,0 +1,99 @@ +/* + +OOCocoa.m + +Runtime-like methods. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@implementation NSObject (OODescriptionComponents) + +- (NSString *)descriptionComponents +{ + return nil; +} + + +- (NSString *)description +{ + NSString *components = nil; + + components = [self descriptionComponents]; + if (components != nil) + { + return [NSString stringWithFormat:@"<%@ %p>{%@}", [self class], self, components]; + } + else + { + return [NSString stringWithFormat:@"<%@ %p>", [self class], self]; + } +} + + +- (NSString *) shortDescription +{ + NSString *components = nil; + + components = [self shortDescriptionComponents]; + if (components != nil) + { + return [NSString stringWithFormat:@"<%@ %p>{%@}", [self class], self, components]; + } + else + { + return [NSString stringWithFormat:@"<%@ %p>", [self class], self]; + } +} + + +- (NSString *) shortDescriptionComponents +{ + return nil; +} + +@end diff --git a/src/Core/OOCollectionExtractors.h b/src/Core/OOCollectionExtractors.h new file mode 100644 index 00000000..2dbc2e72 --- /dev/null +++ b/src/Core/OOCollectionExtractors.h @@ -0,0 +1,504 @@ +/* + +OOCollectionExtractors.h + +Convenience extensions to Foundation collections to extract optional values. +In addition to being convenient, these perform type checking. Which is, +y'know, good to have. + +Note on types: ideally, stdint.h types would be used for integers. However, +NSNumber doesn't do this, so doing so portably would add new complications. + +Starting with Oolite 1.69.1, the various integer methods will always clamp +values to the range of the return type, rather than truncating like NSNumber. +Before that, they weren't entirely inconsistent. + +The "non-negative float"/"non-negative double" will clamp read values to zero +if negative, but will return a negative defaultValue unchanged. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#import +#import "OOFunctionAttributes.h" +#include + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +#import "OOMaths.h" +#endif + + + +@interface NSArray (OOExtractor) + +- (char)charAtIndex:(OOUInteger)index defaultValue:(char)value; +- (short)shortAtIndex:(OOUInteger)index defaultValue:(short)value; +- (int)intAtIndex:(OOUInteger)index defaultValue:(int)value; +- (long)longAtIndex:(OOUInteger)index defaultValue:(long)value; +- (long long)longLongAtIndex:(OOUInteger)index defaultValue:(long long)value; +- (OOInteger)integerAtIndex:(OOUInteger)index defaultValue:(OOInteger)value; + +- (unsigned char)unsignedCharAtIndex:(OOUInteger)index defaultValue:(unsigned char)value; +- (unsigned short)unsignedShortAtIndex:(OOUInteger)index defaultValue:(unsigned short)value; +- (unsigned int)unsignedIntAtIndex:(OOUInteger)index defaultValue:(unsigned int)value; +- (unsigned long)unsignedLongAtIndex:(OOUInteger)index defaultValue:(unsigned long)value; +- (unsigned long long)unsignedLongLongAtIndex:(OOUInteger)index defaultValue:(unsigned long long)value; +- (OOUInteger)unsignedIntegerAtIndex:(OOUInteger)index defaultValue:(OOUInteger)value; + +- (BOOL)boolAtIndex:(OOUInteger)index defaultValue:(BOOL)value; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanAtIndex:(OOUInteger)index defaultValue:(float)value; // Reads a float in the range [0, 1], and returns YES with that probability. +#endif + +- (float)floatAtIndex:(OOUInteger)index defaultValue:(float)value; +- (double)doubleAtIndex:(OOUInteger)index defaultValue:(double)value; +- (float)nonNegativeFloatAtIndex:(OOUInteger)index defaultValue:(float)value; +- (double)nonNegativeDoubleAtIndex:(OOUInteger)index defaultValue:(double)value; + +- (id)objectAtIndex:(OOUInteger)index defaultValue:(id)value; +- (id)objectOfClass:(Class)class atIndex:(OOUInteger)index defaultValue:(id)value; +- (NSString *)stringAtIndex:(OOUInteger)index defaultValue:(NSString *)value; +- (NSArray *)arrayAtIndex:(OOUInteger)index defaultValue:(NSArray *)value; +- (NSSet *)setAtIndex:(OOUInteger)index defaultValue:(NSSet *)value; +- (NSDictionary *)dictionaryAtIndex:(OOUInteger)index defaultValue:(NSDictionary *)value; +- (NSData *)dataAtIndex:(OOUInteger)index defaultValue:(NSData *)value; + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (Vector)vectorAtIndex:(OOUInteger)index defaultValue:(Vector)value; +- (Quaternion)quaternionAtIndex:(OOUInteger)index defaultValue:(Quaternion)value; +#endif + + +// Default: 0 +- (char)charAtIndex:(OOUInteger)index; +- (short)shortAtIndex:(OOUInteger)index; +- (int)intAtIndex:(OOUInteger)index; +- (long)longAtIndex:(OOUInteger)index; +- (long long)longLongAtIndex:(OOUInteger)index; +- (OOInteger)integerAtIndex:(OOUInteger)index; + +- (unsigned char)unsignedCharAtIndex:(OOUInteger)index; +- (unsigned short)unsignedShortAtIndex:(OOUInteger)index; +- (unsigned int)unsignedIntAtIndex:(OOUInteger)index; +- (unsigned long)unsignedLongAtIndex:(OOUInteger)index; +- (unsigned long long)unsignedLongLongAtIndex:(OOUInteger)index; +- (OOUInteger)unsignedIntegerAtIndex:(OOUInteger)index; + +// Default: NO +- (BOOL)boolAtIndex:(OOUInteger)index; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanAtIndex:(OOUInteger)index; // Reads a float in the range [0, 1], and returns YES with that probability. +#endif + +// Default: 0.0 +- (float)floatAtIndex:(OOUInteger)index; +- (double)doubleAtIndex:(OOUInteger)index; +- (float)nonNegativeFloatAtIndex:(OOUInteger)index; +- (double)nonNegativeDoubleAtIndex:(OOUInteger)index; + +// Default: nil +// - (id)objectAtIndex:(OOUInteger)index; // Already defined +- (id)objectOfClass:(Class)class atIndex:(OOUInteger)index; +- (NSString *)stringAtIndex:(OOUInteger)index; +- (NSArray *)arrayAtIndex:(OOUInteger)index; +- (NSSet *)setAtIndex:(OOUInteger)index; +- (NSDictionary *)dictionaryAtIndex:(OOUInteger)index; +- (NSData *)dataAtIndex:(OOUInteger)index; + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +// Default: kZeroVector +- (Vector)vectorAtIndex:(OOUInteger)index; +// Default: kIdentityQuaternion +- (Quaternion)quaternionAtIndex:(OOUInteger)index; +#endif + +@end + + +@interface NSDictionary (OOExtractor) + +- (char)charForKey:(id)key defaultValue:(char)value; +- (short)shortForKey:(id)key defaultValue:(short)value; +- (int)intForKey:(id)key defaultValue:(int)value; +- (long)longForKey:(id)key defaultValue:(long)value; +- (long long)longLongForKey:(id)key defaultValue:(long long)value; +- (OOInteger)integerForKey:(id)key defaultValue:(OOInteger)value; + +- (unsigned char)unsignedCharForKey:(id)key defaultValue:(unsigned char)value; +- (unsigned short)unsignedShortForKey:(id)key defaultValue:(unsigned short)value; +- (unsigned int)unsignedIntForKey:(id)key defaultValue:(unsigned int)value; +- (unsigned long)unsignedLongForKey:(id)key defaultValue:(unsigned long)value; +- (unsigned long long)unsignedLongLongForKey:(id)key defaultValue:(unsigned long long)value; +- (OOUInteger)unsignedIntegerForKey:(id)key defaultValue:(OOUInteger)value; + +- (BOOL)boolForKey:(id)key defaultValue:(BOOL)value; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key defaultValue:(float)value; // Reads a float in the range [0, 1], and returns YES with that probability. +#endif + +- (float)floatForKey:(id)key defaultValue:(float)value; +- (double)doubleForKey:(id)key defaultValue:(double)value; +- (float)nonNegativeFloatForKey:(id)key defaultValue:(float)value; +- (double)nonNegativeDoubleForKey:(id)key defaultValue:(double)value; + +- (id)objectForKey:(id)key defaultValue:(id)value; +- (id)objectOfClass:(Class)class forKey:(id)key defaultValue:(id)value; +- (NSString *)stringForKey:(id)key defaultValue:(NSString *)value; +- (NSArray *)arrayForKey:(id)key defaultValue:(NSArray *)value; +- (NSSet *)setForKey:(id)key defaultValue:(NSSet *)value; +- (NSDictionary *)dictionaryForKey:(id)key defaultValue:(NSDictionary *)value; +- (NSData *)dataForKey:(id)key defaultValue:(NSData *)value; + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (Vector)vectorForKey:(id)key defaultValue:(Vector)value; +- (Quaternion)quaternionForKey:(id)key defaultValue:(Quaternion)value; +#endif + + +// Default: 0 +- (char)charForKey:(id)key; +- (short)shortForKey:(id)key; +- (int)intForKey:(id)key; +- (long)longForKey:(id)key; +- (long long)longLongForKey:(id)key; +- (OOInteger)integerForKey:(id)key; + +- (unsigned char)unsignedCharForKey:(id)key; +- (unsigned short)unsignedShortForKey:(id)key; +- (unsigned int)unsignedIntForKey:(id)key; +- (unsigned long)unsignedLongForKey:(id)key; +- (unsigned long long)unsignedLongLongForKey:(id)key; +- (OOUInteger)unsignedIntegerForKey:(id)key; + +// Default: NO +- (BOOL)boolForKey:(id)key; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key; // Reads a float in the range [0, 1], and returns YES with that probability. +#endif + +// Default: 0.0 +- (float)floatForKey:(id)key; +- (double)doubleForKey:(id)key; +- (float)nonNegativeFloatForKey:(id)key; +- (double)nonNegativeDoubleForKey:(id)key; + +// Default: nil +// - (id)objectForKey:(id)key; // Already defined +- (id)objectOfClass:(Class)class forKey:(id)key; +- (NSString *)stringForKey:(id)key; +- (NSArray *)arrayForKey:(id)key; +- (NSSet *)setForKey:(id)key; +- (NSDictionary *)dictionaryForKey:(id)key; +- (NSData *)dataForKey:(id)key; + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +// Default: kZeroVector +- (Vector)vectorForKey:(id)key; +// Default: kIdentityQuaternion +- (Quaternion)quaternionForKey:(id)key; +#endif + +@end + + +@interface NSUserDefaults (OOExtractor) + +- (char)charForKey:(id)key defaultValue:(char)value; +- (short)shortForKey:(id)key defaultValue:(short)value; +- (int)intForKey:(id)key defaultValue:(int)value; +- (long)longForKey:(id)key defaultValue:(long)value; +- (long long)longLongForKey:(id)key defaultValue:(long long)value; + +- (unsigned char)unsignedCharForKey:(id)key defaultValue:(unsigned char)value; +- (unsigned short)unsignedShortForKey:(id)key defaultValue:(unsigned short)value; +- (unsigned int)unsignedIntForKey:(id)key defaultValue:(unsigned int)value; +- (unsigned long)unsignedLongForKey:(id)key defaultValue:(unsigned long)value; +- (unsigned long long)unsignedLongLongForKey:(id)key defaultValue:(unsigned long long)value; + +- (BOOL)boolForKey:(id)key defaultValue:(BOOL)value; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key defaultValue:(float)value; // Reads a float in the range [0, 1], and returns YES with that probability. +#endif + +- (float)floatForKey:(id)key defaultValue:(float)value; +- (double)doubleForKey:(id)key defaultValue:(double)value; +- (float)nonNegativeFloatForKey:(id)key defaultValue:(float)value; +- (double)nonNegativeDoubleForKey:(id)key defaultValue:(double)value; + +- (id)objectForKey:(id)key defaultValue:(id)value; +- (id)objectOfClass:(Class)class forKey:(id)key defaultValue:(id)value; +- (NSString *)stringForKey:(id)key defaultValue:(NSString *)value; +- (NSArray *)arrayForKey:(id)key defaultValue:(NSArray *)value; +- (NSSet *)setForKey:(id)key defaultValue:(NSSet *)value; +- (NSDictionary *)dictionaryForKey:(id)key defaultValue:(NSDictionary *)value; +- (NSData *)dataForKey:(id)key defaultValue:(NSData *)value; + + +// Default: 0 +- (char)charForKey:(id)key; +- (short)shortForKey:(id)key; +- (int)intForKey:(id)key; +- (long)longForKey:(id)key; +- (long long)longLongForKey:(id)key; + +- (unsigned char)unsignedCharForKey:(id)key; +- (unsigned short)unsignedShortForKey:(id)key; +- (unsigned int)unsignedIntForKey:(id)key; +- (unsigned long)unsignedLongForKey:(id)key; +- (unsigned long long)unsignedLongLongForKey:(id)key; + +// Default: NO +// - (BOOL)boolForKey:(id)key; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key; // Reads a float in the range [0, 1], and returns YES with that probability. +#endif + +// Default: 0.0 +// - (float)floatForKey:(id)key; +- (double)doubleForKey:(NSString *)key; +- (float)nonNegativeFloatForKey:(id)key; +- (double)nonNegativeDoubleForKey:(id)key; + +// Default: nil +// - (id)objectForKey:(id)key; // Already defined +- (id)objectOfClass:(Class)class forKey:(id)key; +// - (NSString *)stringForKey:(id)key; +// - (NSArray *)arrayForKey:(id)key; +- (NSSet *)setForKey:(id)key; +// - (NSDictionary *)dictionaryForKey:(id)key; +// - (NSData *)dataForKey:(id)key; + +@end + + +@interface NSMutableArray (OOInserter) + +- (void)addInteger:(long)value; +- (void)addUnsignedInteger:(unsigned long)value; +- (void)addFloat:(double)value; +- (void)addBool:(BOOL)value; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)addVector:(Vector)value; +- (void)addQuaternion:(Quaternion)value; +#endif + +- (void)insertInteger:(long)value atIndex:(OOUInteger)index; +- (void)insertUnsignedInteger:(unsigned long)value atIndex:(OOUInteger)index; +- (void)insertFloat:(double)value atIndex:(OOUInteger)index; +- (void)insertBool:(BOOL)value atIndex:(OOUInteger)index; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)insertVector:(Vector)value atIndex:(OOUInteger)index; +- (void)insertQuaternion:(Quaternion)value atIndex:(OOUInteger)index; +#endif + +@end + + +@interface NSMutableDictionary (OOInserter) + +- (void)setInteger:(long)value forKey:(id)key; +- (void)setUnsignedInteger:(unsigned long)value forKey:(id)key; +- (void)setLongLong:(long long)value forKey:(id)key; +- (void)setUnsignedLongLong:(unsigned long long)value forKey:(id)key; +- (void)setFloat:(double)value forKey:(id)key; +- (void)setBool:(BOOL)value forKey:(id)key; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)setVector:(Vector)value forKey:(id)key; +- (void)setQuaternion:(Quaternion)value forKey:(id)key; +#endif + +@end + + +@interface NSMutableSet (OOInserter) + +- (void)addInteger:(long)value; +- (void)addUnsignedInteger:(unsigned long)value; +- (void)addFloat:(double)value; +- (void)addBool:(BOOL)value; +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)addVector:(Vector)value; +- (void)addQuaternion:(Quaternion)value; +#endif + +@end + + +// *** Value extraction utilities *** + +/* Utility function to interpret a boolean. May be an NSNumber or any of the + following strings (case-insensitive): + yes + true + on + + no + false + off +*/ +BOOL OOBooleanFromObject(id object, BOOL defaultValue); + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +/* Utility function to interpret a fuzzy boolean. May be any of the strings + accepted by OOBooleanFromObject(), or a number indicating probability of + a yes (between 0 and 1). +*/ +BOOL OOFuzzyBooleanFromObject(id object, float defaultValue); +#endif + + +float OOFloatFromObject(id object, float defaultValue); +double OODoubleFromObject(id object, double defaultValue); +float OONonNegativeFloatFromObject(id object, float defaultValue); +double OONonNegativeDoubleFromObject(id object, double defaultValue); + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +// These take strings, dictionaries or arrays. +Vector OOVectorFromObject(id object, Vector defaultValue); +Quaternion OOQuaternionFromObject(id object, Quaternion defaultValue); + +NSDictionary *OOPropertyListFromVector(Vector value); +NSDictionary *OOPropertyListFromQuaternion(Quaternion value); +#endif + + +OOINLINE long long OOClampInteger(long long value, long long minValue, long long maxValue) ALWAYS_INLINE_FUNC; +long long OOLongLongFromObject(id object, long long defaultValue); +unsigned long long OOUnsignedLongLongFromObject(id object, unsigned long long defaultValue); + + +OOINLINE long long OOClampInteger(long long value, long long minValue, long long maxValue) +{ + return (minValue < value) ? ((value < maxValue) ? value : maxValue) : minValue; +} + + +/* Define an inline function to clamp a give type and its unsigned + counterpart. Example: + + OO_DEFINE_CLAMP_PAIR(char, Char, CHAR) + + expands to + + OOINLINE char OOCharFromObject(id object, char defaultValue) + { + return OOClampInteger(OOLongLongFromObject(object, defaultValue), CHAR_MIN, CHAR_MAX); + } + OOINLINE unsigned char OOUnsignedCharFromObject(id object, unsigned char defaultValue) + { + return OOClampInteger(OOLongLongFromObject(object, defaultValue), 0, UCHAR_MAX); + } +*/ +#define OO_DEFINE_CLAMP(type, typeName, min, max) \ + OOINLINE type OO ## typeName ## FromObject(id object, type defaultValue) \ + { \ + return OOClampInteger(OOLongLongFromObject(object, defaultValue), min, max); \ + } + +#define OO_DEFINE_CLAMP_PAIR(type, typeName, minMaxSymb) \ + OO_DEFINE_CLAMP(type, typeName, minMaxSymb ## _MIN, minMaxSymb ## _MAX) \ + OO_DEFINE_CLAMP(unsigned type, Unsigned ## typeName, 0, U ## minMaxSymb ## _MAX) + +OO_DEFINE_CLAMP_PAIR(char, Char, CHAR) +OO_DEFINE_CLAMP_PAIR(short, Short, SHRT) + +/* When ints or longs are as large as long longs, we can't do any clamping + because the clamping code will overflow (unless we add pointless complexity). + Instead, we alias the long long versions which don't clamp. Inlines are + used instead of macros so that the functions always have the precise type + they should; this is necessary for stuff that uses @encode, notably the + SenTestingKit framework. +*/ +#define OO_ALIAS_CLAMP_LONG_LONG(type, typeName) \ +static inline type OO##typeName##FromObject(id object, type defaultValue) \ +{ \ + return OOLongLongFromObject(object, defaultValue); \ +} +#define OO_ALIAS_CLAMP_PAIR_LONG_LONG(type, typeName) \ +OO_ALIAS_CLAMP_LONG_LONG(type, typeName) \ +OO_ALIAS_CLAMP_LONG_LONG(unsigned type, Unsigned##typeName) + +#if INT_MAX == LLONG_MAX +// Should never get here under Mac OS X, but may under GNUstep. +OO_ALIAS_CLAMP_PAIR_LONG_LONG(int, Int) +#else +OO_DEFINE_CLAMP_PAIR(int, Int, INT) +#endif + +#if LONG_MAX == LLONG_MAX +OO_ALIAS_CLAMP_PAIR_LONG_LONG(long, Long) +#else +OO_DEFINE_CLAMP_PAIR(long, Long, LONG) +#endif + + +#if OOLITE_64_BIT +OOINLINE OOInteger OOIntegerFromObject(id object, OOInteger defaultValue) +{ + return OOLongLongFromObject(object, defaultValue); +} + +OOINLINE OOInteger OOUIntegerFromObject(id object, OOUInteger defaultValue) +{ + return OOUnsignedLongLongFromObject(object, defaultValue); +} +#else +OOINLINE OOInteger OOIntegerFromObject(id object, OOInteger defaultValue) +{ + return OOLongLongFromObject(object, defaultValue); +} + +OOINLINE OOInteger OOUIntegerFromObject(id object, OOUInteger defaultValue) +{ + return OOUnsignedLongLongFromObject(object, defaultValue); +} +#endif + + +#undef OO_DEFINE_CLAMP +#undef OO_DEFINE_CLAMP_PAIR +#undef OO_ALIAS_CLAMP_LONG_LONG +#undef OO_ALIAS_CLAMP_PAIR_LONG_LONG diff --git a/src/Core/OOCollectionExtractors.m b/src/Core/OOCollectionExtractors.m new file mode 100644 index 00000000..2e6b8a45 --- /dev/null +++ b/src/Core/OOCollectionExtractors.m @@ -0,0 +1,1489 @@ +/* + +OOCollectionExtractors.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOCollectionExtractors.h" +#import +#import "OOMaths.h" +#import "OOStringParsing.h" + + +static NSSet *SetForObject(id object, NSSet *defaultValue); +static NSString *StringForObject(id object, NSString *defaultValue); + + +@implementation NSArray (OOExtractor) + +- (char)charAtIndex:(OOUInteger)index defaultValue:(char)value +{ + return OOCharFromObject([self objectAtIndex:index], value); +} + + +- (short)shortAtIndex:(OOUInteger)index defaultValue:(short)value +{ + return OOShortFromObject([self objectAtIndex:index], value); +} + + +- (int)intAtIndex:(OOUInteger)index defaultValue:(int)value +{ + return OOIntFromObject([self objectAtIndex:index], value); +} + + +- (long)longAtIndex:(OOUInteger)index defaultValue:(long)value +{ + return OOLongFromObject([self objectAtIndex:index], value); +} + + +- (long long)longLongAtIndex:(OOUInteger)index defaultValue:(long long)value +{ + return OOLongLongFromObject([self objectAtIndex:index], value); +} + + +- (OOInteger)integerAtIndex:(OOUInteger)index defaultValue:(OOInteger)value +{ + return OOIntegerFromObject([self objectAtIndex:index], value); +} + + +- (unsigned char)unsignedCharAtIndex:(OOUInteger)index defaultValue:(unsigned char)value +{ + return OOUnsignedCharFromObject([self objectAtIndex:index], value); +} + + +- (unsigned short)unsignedShortAtIndex:(OOUInteger)index defaultValue:(unsigned short)value +{ + return OOUnsignedShortFromObject([self objectAtIndex:index], value); +} + + +- (unsigned int)unsignedIntAtIndex:(OOUInteger)index defaultValue:(unsigned int)value +{ + return OOUnsignedIntFromObject([self objectAtIndex:index], value); +} + + +- (unsigned long)unsignedLongAtIndex:(OOUInteger)index defaultValue:(unsigned long)value +{ + return OOUnsignedLongFromObject([self objectAtIndex:index], value); +} + + +- (unsigned long long)unsignedLongLongAtIndex:(OOUInteger)index defaultValue:(unsigned long long)value +{ + return OOUnsignedLongLongFromObject([self objectAtIndex:index], value); +} + + +- (OOUInteger)unsignedIntegerAtIndex:(OOUInteger)index defaultValue:(OOUInteger)value +{ + return OOUIntegerFromObject([self objectAtIndex:index], value); +} + + +- (BOOL)boolAtIndex:(OOUInteger)index defaultValue:(BOOL)value +{ + return OOBooleanFromObject([self objectAtIndex:index], value); +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanAtIndex:(OOUInteger)index defaultValue:(float)value +{ + return OOFuzzyBooleanFromObject([self objectAtIndex:index], value); +} +#endif + + +- (float)floatAtIndex:(OOUInteger)index defaultValue:(float)value +{ + return OOFloatFromObject([self objectAtIndex:index], value); +} + + +- (double)doubleAtIndex:(OOUInteger)index defaultValue:(double)value +{ + return OODoubleFromObject([self objectAtIndex:index], value); +} + + +- (float)nonNegativeFloatAtIndex:(OOUInteger)index defaultValue:(float)value +{ + return OONonNegativeFloatFromObject([self objectAtIndex:index], value); +} + + +- (double)nonNegativeDoubleAtIndex:(OOUInteger)index defaultValue:(double)value +{ + return OONonNegativeDoubleFromObject([self objectAtIndex:index], value); +} + + +- (id)objectAtIndex:(OOUInteger)index defaultValue:(id)value +{ + id objVal = [self objectAtIndex:index]; + id result; + + if (objVal != nil) result = objVal; + else result = value; + + return result; +} + + +- (id)objectOfClass:(Class)class atIndex:(OOUInteger)index defaultValue:(id)value +{ + id objVal = [self objectAtIndex:index]; + NSString *result; + + if ([objVal isKindOfClass:class]) result = objVal; + else result = value; + + return result; +} + + +- (NSString *)stringAtIndex:(OOUInteger)index defaultValue:(NSString *)value +{ + return StringForObject([self objectAtIndex:index], value); +} + + +- (NSArray *)arrayAtIndex:(OOUInteger)index defaultValue:(NSArray *)value +{ + return [self objectOfClass:[NSArray class] atIndex:index defaultValue:value]; +} + + +- (NSSet *)setAtIndex:(OOUInteger)index defaultValue:(NSSet *)value +{ + return SetForObject([self objectAtIndex:index], value); +} + + +- (NSDictionary *)dictionaryAtIndex:(OOUInteger)index defaultValue:(NSDictionary *)value +{ + return [self objectOfClass:[NSDictionary class] atIndex:index defaultValue:value]; +} + + +- (NSData *)dataAtIndex:(OOUInteger)index defaultValue:(NSData *)value +{ + return [self objectOfClass:[NSData class] atIndex:index defaultValue:value]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (Vector)vectorAtIndex:(OOUInteger)index defaultValue:(Vector)value +{ + return OOVectorFromObject([self objectAtIndex:index], value); +} + + +- (Quaternion)quaternionAtIndex:(OOUInteger)index defaultValue:(Quaternion)value; +{ + return OOQuaternionFromObject([self objectAtIndex:index], value); +} +#endif + + +- (char)charAtIndex:(OOUInteger)index +{ + return [self charAtIndex:index defaultValue:0]; +} + + +- (short)shortAtIndex:(OOUInteger)index +{ + return [self shortAtIndex:index defaultValue:0]; +} + + +- (int)intAtIndex:(OOUInteger)index +{ + return [self intAtIndex:index defaultValue:0]; +} + + +- (long)longAtIndex:(OOUInteger)index +{ + return [self longAtIndex:index defaultValue:0]; +} + + +- (long long)longLongAtIndex:(OOUInteger)index +{ + return [self longLongAtIndex:index defaultValue:0]; +} + + +- (OOInteger)integerAtIndex:(OOUInteger)index +{ + return [self integerAtIndex:index defaultValue:0]; +} + + +- (unsigned char)unsignedCharAtIndex:(OOUInteger)index +{ + return [self unsignedCharAtIndex:index defaultValue:0]; +} + + +- (unsigned short)unsignedShortAtIndex:(OOUInteger)index +{ + return [self unsignedShortAtIndex:index defaultValue:0]; +} + + +- (unsigned int)unsignedIntAtIndex:(OOUInteger)index +{ + return [self unsignedIntAtIndex:index defaultValue:0]; +} + + +- (unsigned long)unsignedLongAtIndex:(OOUInteger)index +{ + return [self unsignedLongAtIndex:index defaultValue:0]; +} + + +- (unsigned long long)unsignedLongLongAtIndex:(OOUInteger)index +{ + return [self unsignedLongLongAtIndex:index defaultValue:0]; +} + + +- (OOUInteger)unsignedIntegerAtIndex:(OOUInteger)index +{ + return [self unsignedIntegerAtIndex:index defaultValue:0]; +} + + +- (BOOL)boolAtIndex:(OOUInteger)index +{ + return [self boolAtIndex:index defaultValue:NO]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanAtIndex:(OOUInteger)index +{ + return [self fuzzyBooleanAtIndex:index defaultValue:0.0f]; +} +#endif + + +- (float)floatAtIndex:(OOUInteger)index +{ + return OOFloatFromObject([self objectAtIndex:index], 0.0f); +} + + +- (double)doubleAtIndex:(OOUInteger)index +{ + return OODoubleFromObject([self objectAtIndex:index], 0.0); +} + + +- (float)nonNegativeFloatAtIndex:(OOUInteger)index +{ + return OONonNegativeFloatFromObject([self objectAtIndex:index], 0.0f); +} + + +- (double)nonNegativeDoubleAtIndex:(OOUInteger)index +{ + return OONonNegativeDoubleFromObject([self objectAtIndex:index], 0.0); +} + + +- (id)objectOfClass:(Class)class atIndex:(OOUInteger)index +{ + return [self objectOfClass:class atIndex:index defaultValue:nil]; +} + + +- (NSString *)stringAtIndex:(OOUInteger)index +{ + return [self stringAtIndex:index defaultValue:nil]; +} + + +- (NSArray *)arrayAtIndex:(OOUInteger)index +{ + return [self arrayAtIndex:index defaultValue:nil]; +} + + +- (NSSet *)setAtIndex:(OOUInteger)index +{ + return [self setAtIndex:index defaultValue:nil]; +} + + +- (NSDictionary *)dictionaryAtIndex:(OOUInteger)index +{ + return [self dictionaryAtIndex:index defaultValue:nil]; +} + + +- (NSData *)dataAtIndex:(OOUInteger)index +{ + return [self dataAtIndex:index defaultValue:nil]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (Vector)vectorAtIndex:(OOUInteger)index +{ + return [self vectorAtIndex:index defaultValue:kZeroVector]; +} + + +- (Quaternion)quaternionAtIndex:(OOUInteger)index +{ + return [self quaternionAtIndex:index defaultValue:kIdentityQuaternion]; +} +#endif + +@end + + +@implementation NSDictionary (OOExtractor) + +- (char)charForKey:(id)key defaultValue:(char)value +{ + return OOCharFromObject([self objectForKey:key], value); +} + + +- (short)shortForKey:(id)key defaultValue:(short)value +{ + return OOShortFromObject([self objectForKey:key], value); +} + + +- (int)intForKey:(id)key defaultValue:(int)value +{ + return OOIntFromObject([self objectForKey:key], value); +} + + +- (long)longForKey:(id)key defaultValue:(long)value +{ + return OOLongFromObject([self objectForKey:key], value); +} + + +- (long long)longLongForKey:(id)key defaultValue:(long long)value +{ + return OOLongLongFromObject([self objectForKey:key], value); +} + + +- (OOInteger)integerForKey:(id)key defaultValue:(OOInteger)value +{ + return OOIntegerFromObject([self objectForKey:key], value); +} + + +- (unsigned char)unsignedCharForKey:(id)key defaultValue:(unsigned char)value +{ + return OOUnsignedCharFromObject([self objectForKey:key], value); +} + + +- (unsigned short)unsignedShortForKey:(id)key defaultValue:(unsigned short)value +{ + return OOUnsignedShortFromObject([self objectForKey:key], value); +} + + +- (unsigned int)unsignedIntForKey:(id)key defaultValue:(unsigned int)value +{ + return OOUnsignedIntFromObject([self objectForKey:key], value); +} + + +- (unsigned long)unsignedLongForKey:(id)key defaultValue:(unsigned long)value +{ + return OOUnsignedLongFromObject([self objectForKey:key], value); +} + + +- (unsigned long long)unsignedLongLongForKey:(id)key defaultValue:(unsigned long long)value +{ + return OOUnsignedLongLongFromObject([self objectForKey:key], value); +} + + +- (OOUInteger)unsignedIntegerForKey:(id)key defaultValue:(OOUInteger)value +{ + return OOUIntegerFromObject([self objectForKey:key], value); +} + + +- (BOOL)boolForKey:(id)key defaultValue:(BOOL)value +{ + return OOBooleanFromObject([self objectForKey:key], value); +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key defaultValue:(float)value +{ + return OOFuzzyBooleanFromObject([self objectForKey:key], value); +} +#endif + + +- (float)floatForKey:(id)key defaultValue:(float)value +{ + return OOFloatFromObject([self objectForKey:key], value); +} + + +- (double)doubleForKey:(id)key defaultValue:(double)value +{ + return OODoubleFromObject([self objectForKey:key], value); +} + + +- (float)nonNegativeFloatForKey:(id)key defaultValue:(float)value +{ + return OONonNegativeFloatFromObject([self objectForKey:key], value); +} + + +- (double)nonNegativeDoubleForKey:(id)key defaultValue:(double)value +{ + return OONonNegativeDoubleFromObject([self objectForKey:key], value); +} + + +- (id)objectForKey:(id)key defaultValue:(id)value +{ + id objVal = [self objectForKey:key]; + id result; + + if (objVal != nil) result = objVal; + else result = value; + + return result; +} + + +- (id)objectOfClass:(Class)class forKey:(id)key defaultValue:(id)value +{ + id objVal = [self objectForKey:key]; + id result; + + if ([objVal isKindOfClass:class]) result = objVal; + else result = value; + + return result; +} + + +- (NSString *)stringForKey:(id)key defaultValue:(NSString *)value +{ + return StringForObject([self objectForKey:key], value); +} + + +- (NSArray *)arrayForKey:(id)key defaultValue:(NSArray *)value +{ + return [self objectOfClass:[NSArray class] forKey:key defaultValue:value]; +} + + +- (NSSet *)setForKey:(id)key defaultValue:(NSSet *)value +{ + return SetForObject([self objectForKey:key], value); +} + + +- (NSDictionary *)dictionaryForKey:(id)key defaultValue:(NSDictionary *)value +{ + return [self objectOfClass:[NSDictionary class] forKey:key defaultValue:value]; +} + + +- (NSData *)dataForKey:(id)key defaultValue:(NSData *)value +{ + return [self objectOfClass:[NSData class] forKey:key defaultValue:value]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (Vector)vectorForKey:(id)key defaultValue:(Vector)value +{ + return OOVectorFromObject([self objectForKey:key], value); +} + + +- (Quaternion)quaternionForKey:(id)key defaultValue:(Quaternion)value +{ + return OOQuaternionFromObject([self objectForKey:key], value); +} +#endif + + +- (char)charForKey:(id)key +{ + return [self charForKey:key defaultValue:0]; +} + + +- (short)shortForKey:(id)key +{ + return [self shortForKey:key defaultValue:0]; +} + + +- (int)intForKey:(id)key +{ + return [self intForKey:key defaultValue:0]; +} + + +- (long)longForKey:(id)key +{ + return [self longForKey:key defaultValue:0]; +} + + +- (long long)longLongForKey:(id)key +{ + return [self longLongForKey:key defaultValue:0]; +} + + +- (OOInteger)integerForKey:(id)key +{ + return [self integerForKey:key defaultValue:0]; +} + + +- (unsigned char)unsignedCharForKey:(id)key +{ + return [self unsignedCharForKey:key defaultValue:0]; +} + + +- (unsigned short)unsignedShortForKey:(id)key +{ + return [self unsignedShortForKey:key defaultValue:0]; +} + + +- (unsigned int)unsignedIntForKey:(id)key +{ + return [self unsignedIntForKey:key defaultValue:0]; +} + + +- (unsigned long)unsignedLongForKey:(id)key +{ + return [self unsignedLongForKey:key defaultValue:0]; +} + + +- (OOUInteger)unsignedIntegerForKey:(id)key +{ + return [self unsignedIntegerForKey:key defaultValue:0]; +} + + +- (unsigned long long)unsignedLongLongForKey:(id)key +{ + return [self unsignedLongLongForKey:key defaultValue:0]; +} + + +- (BOOL)boolForKey:(id)key +{ + return [self boolForKey:key defaultValue:NO]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key +{ + return [self fuzzyBooleanForKey:key defaultValue:0.0f]; +} +#endif + + +- (float)floatForKey:(id)key +{ + return OOFloatFromObject([self objectForKey:key], 0.0f); +} + + +- (double)doubleForKey:(id)key +{ + return OODoubleFromObject([self objectForKey:key], 0.0); +} + + +- (float)nonNegativeFloatForKey:(id)key +{ + return OONonNegativeFloatFromObject([self objectForKey:key], 0.0f); +} + + +- (double)nonNegativeDoubleForKey:(id)key +{ + return OONonNegativeDoubleFromObject([self objectForKey:key], 0.0); +} + + +- (id)objectOfClass:(Class)class forKey:(id)key +{ + return [self objectOfClass:class forKey:key defaultValue:nil]; +} + + +- (NSString *)stringForKey:(id)key +{ + return [self stringForKey:key defaultValue:nil]; +} + + +- (NSArray *)arrayForKey:(id)key +{ + return [self arrayForKey:key defaultValue:nil]; +} + + +- (NSSet *)setForKey:(id)key +{ + return [self setForKey:key defaultValue:nil]; +} + + +- (NSDictionary *)dictionaryForKey:(id)key +{ + return [self dictionaryForKey:key defaultValue:nil]; +} + + +- (NSData *)dataForKey:(id)key +{ + return [self dataForKey:key defaultValue:nil]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (Vector)vectorForKey:(id)key +{ + return [self vectorForKey:key defaultValue:kZeroVector]; +} + + +- (Quaternion)quaternionForKey:(id)key +{ + return [self quaternionForKey:key defaultValue:kIdentityQuaternion]; +} +#endif + +@end + + +@implementation NSUserDefaults (OOExtractor) + +- (char)charForKey:(id)key defaultValue:(char)value +{ + return OOCharFromObject([self objectForKey:key], value); +} + + +- (short)shortForKey:(id)key defaultValue:(short)value +{ + return OOShortFromObject([self objectForKey:key], value); +} + + +- (int)intForKey:(id)key defaultValue:(int)value +{ + return OOIntFromObject([self objectForKey:key], value); +} + + +- (long)longForKey:(id)key defaultValue:(long)value +{ + return OOLongFromObject([self objectForKey:key], value); +} + + +- (long long)longLongForKey:(id)key defaultValue:(long long)value +{ + return OOLongLongFromObject([self objectForKey:key], value); +} + + +- (unsigned char)unsignedCharForKey:(id)key defaultValue:(unsigned char)value +{ + return OOUnsignedCharFromObject([self objectForKey:key], value); +} + + +- (unsigned short)unsignedShortForKey:(id)key defaultValue:(unsigned short)value +{ + return OOUnsignedShortFromObject([self objectForKey:key], value); +} + + +- (unsigned int)unsignedIntForKey:(id)key defaultValue:(unsigned int)value +{ + return OOUnsignedIntFromObject([self objectForKey:key], value); +} + + +- (unsigned long)unsignedLongForKey:(id)key defaultValue:(unsigned long)value +{ + return OOUnsignedLongFromObject([self objectForKey:key], value); +} + + +- (unsigned long long)unsignedLongLongForKey:(id)key defaultValue:(unsigned long long)value +{ + return OOUnsignedLongLongFromObject([self objectForKey:key], value); +} + + +- (BOOL)boolForKey:(id)key defaultValue:(BOOL)value +{ + return OOBooleanFromObject([self objectForKey:key], value); +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key defaultValue:(float)value +{ + return OOFuzzyBooleanFromObject([self objectForKey:key], value); +} +#endif + + +- (float)floatForKey:(id)key defaultValue:(float)value +{ + return OOFloatFromObject([self objectForKey:key], value); +} + + +- (double)doubleForKey:(id)key defaultValue:(double)value +{ + return OODoubleFromObject([self objectForKey:key], value); +} + + +- (float)nonNegativeFloatForKey:(id)key defaultValue:(float)value +{ + return OONonNegativeFloatFromObject([self objectForKey:key], value); +} + + +- (double)nonNegativeDoubleForKey:(id)key defaultValue:(double)value +{ + return OONonNegativeDoubleFromObject([self objectForKey:key], value); +} + + +- (id)objectForKey:(id)key defaultValue:(id)value +{ + id objVal = [self objectForKey:key]; + id result; + + if (objVal != nil) result = objVal; + else result = value; + + return result; +} + + +- (id)objectOfClass:(Class)class forKey:(id)key defaultValue:(id)value +{ + id objVal = [self objectForKey:key]; + id result; + + if ([objVal isKindOfClass:class]) result = objVal; + else result = value; + + return result; +} + + +- (NSString *)stringForKey:(id)key defaultValue:(NSString *)value +{ + return StringForObject([self objectForKey:key], value); +} + + +- (NSArray *)arrayForKey:(id)key defaultValue:(NSArray *)value +{ + return [self objectOfClass:[NSArray class] forKey:key defaultValue:value]; +} + + +- (NSSet *)setForKey:(id)key defaultValue:(NSSet *)value +{ + return SetForObject([self objectForKey:key], value); +} + + +- (NSDictionary *)dictionaryForKey:(id)key defaultValue:(NSDictionary *)value +{ + return [self objectOfClass:[NSDictionary class] forKey:key defaultValue:value]; +} + + +- (NSData *)dataForKey:(id)key defaultValue:(NSData *)value +{ + return [self objectOfClass:[NSData class] forKey:key defaultValue:value]; +} + + +- (char)charForKey:(id)key +{ + return [self charForKey:key defaultValue:0]; +} + + +- (short)shortForKey:(id)key +{ + return [self shortForKey:key defaultValue:0]; +} + + +- (int)intForKey:(id)key +{ + return [self intForKey:key defaultValue:0]; +} + + +- (long)longForKey:(id)key +{ + return [self longForKey:key defaultValue:0]; +} + + +- (long long)longLongForKey:(id)key +{ + return [self longLongForKey:key defaultValue:0]; +} + + +- (unsigned char)unsignedCharForKey:(id)key +{ + return [self unsignedCharForKey:key defaultValue:0]; +} + + +- (unsigned short)unsignedShortForKey:(id)key +{ + return [self unsignedShortForKey:key defaultValue:0]; +} + + +- (unsigned int)unsignedIntForKey:(id)key +{ + return [self unsignedIntForKey:key defaultValue:0]; +} + + +- (unsigned long)unsignedLongForKey:(id)key +{ + return [self unsignedLongForKey:key defaultValue:0]; +} + + +- (unsigned long long)unsignedLongLongForKey:(id)key +{ + return [self unsignedLongLongForKey:key defaultValue:0]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (BOOL)fuzzyBooleanForKey:(id)key +{ + return [self fuzzyBooleanForKey:key defaultValue:0.0f]; +} +#endif + + +- (double)doubleForKey:(NSString *)key +{ + return OODoubleFromObject([self objectForKey:key], 0.0); +} + + +- (float)nonNegativeFloatForKey:(id)key +{ + return OONonNegativeFloatFromObject([self objectForKey:key], 0.0f); +} + + +- (double)nonNegativeDoubleForKey:(id)key +{ + return OONonNegativeDoubleFromObject([self objectForKey:key], 0.0); +} + + +- (id)objectOfClass:(Class)class forKey:(id)key +{ + return [self objectOfClass:class forKey:key defaultValue:nil]; +} + + +- (NSSet *)setForKey:(id)key +{ + return [self setForKey:key defaultValue:nil]; +} + +@end + + +@implementation NSMutableArray (OOInserter) + +- (void)addInteger:(long)value +{ + [self addObject:[NSNumber numberWithLong:value]]; +} + + +- (void)addUnsignedInteger:(unsigned long)value +{ + [self addObject:[NSNumber numberWithUnsignedLong:value]]; +} + + +- (void)addFloat:(double)value +{ + [self addObject:[NSNumber numberWithDouble:value]]; +} + + +- (void)addBool:(BOOL)value +{ + [self addObject:[NSNumber numberWithBool:value]]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)addVector:(Vector)value +{ + [self addObject:OOPropertyListFromVector(value)]; +} + + +- (void)addQuaternion:(Quaternion)value +{ + [self addObject:OOPropertyListFromQuaternion(value)]; +} +#endif + + +- (void)insertInteger:(long)value atIndex:(OOUInteger)index +{ + [self insertObject:[NSNumber numberWithLong:value] atIndex:index]; +} + + +- (void)insertUnsignedInteger:(unsigned long)value atIndex:(OOUInteger)index; +{ + [self insertObject:[NSNumber numberWithUnsignedLong:value] atIndex:index]; +} + + +- (void)insertFloat:(double)value atIndex:(OOUInteger)index +{ + [self insertObject:[NSNumber numberWithDouble:value] atIndex:index]; +} + + +- (void)insertBool:(BOOL)value atIndex:(OOUInteger)index +{ + [self insertObject:[NSNumber numberWithBool:value] atIndex:index]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)insertVector:(Vector)value atIndex:(OOUInteger)index +{ + [self insertObject:OOPropertyListFromVector(value) atIndex:index]; +} + + +- (void)insertQuaternion:(Quaternion)value atIndex:(OOUInteger)index +{ + [self insertObject:OOPropertyListFromQuaternion(value) atIndex:index]; +} +#endif + +@end + + +@implementation NSMutableDictionary (OOInserter) + +- (void)setInteger:(long)value forKey:(id)key +{ + [self setObject:[NSNumber numberWithLong:value] forKey:key]; +} + + +- (void)setUnsignedInteger:(unsigned long)value forKey:(id)key +{ + [self setObject:[NSNumber numberWithUnsignedLong:value] forKey:key]; +} + + +- (void)setLongLong:(long long)value forKey:(id)key +{ + [self setObject:[NSNumber numberWithLongLong:value] forKey:key]; +} + + +- (void)setUnsignedLongLong:(unsigned long long)value forKey:(id)key +{ + [self setObject:[NSNumber numberWithUnsignedLongLong:value] forKey:key]; +} + + +- (void)setFloat:(double)value forKey:(id)key +{ + [self setObject:[NSNumber numberWithDouble:value] forKey:key]; +} + + +- (void)setBool:(BOOL)value forKey:(id)key +{ + [self setObject:[NSNumber numberWithBool:value] forKey:key]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)setVector:(Vector)value forKey:(id)key +{ + [self setObject:OOPropertyListFromVector(value) forKey:key]; +} + + +- (void)setQuaternion:(Quaternion)value forKey:(id)key +{ + [self setObject:OOPropertyListFromQuaternion(value) forKey:key]; +} +#endif + +@end + + +@implementation NSMutableSet (OOInserter) + +- (void)addInteger:(long)value +{ + [self addObject:[NSNumber numberWithLong:value]]; +} + + +- (void)addUnsignedInteger:(unsigned long)value +{ + [self addObject:[NSNumber numberWithUnsignedLong:value]]; +} + + +- (void)addFloat:(double)value +{ + [self addObject:[NSNumber numberWithDouble:value]]; +} + + +- (void)addBool:(BOOL)value +{ + [self addObject:[NSNumber numberWithBool:value]]; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +- (void)addVector:(Vector)value +{ + [self addObject:OOPropertyListFromVector(value)]; +} + + +- (void)addQuaternion:(Quaternion)value +{ + [self addObject:OOPropertyListFromQuaternion(value)]; +} +#endif + +@end + + +long long OOLongLongFromObject(id object, long long defaultValue) +{ + long long llValue; + + if ([object respondsToSelector:@selector(longLongValue)]) llValue = [object longLongValue]; + else if ([object respondsToSelector:@selector(longValue)]) llValue = [object longValue]; + else if ([object respondsToSelector:@selector(intValue)]) llValue = [object intValue]; + else llValue = defaultValue; + + return llValue; +} + + +unsigned long long OOUnsignedLongLongFromObject(id object, unsigned long long defaultValue) +{ + unsigned long long ullValue; + + if ([object respondsToSelector:@selector(unsignedLongLongValue)]) ullValue = [object unsignedLongLongValue]; + else if ([object respondsToSelector:@selector(unsignedLongValue)]) ullValue = [object unsignedLongValue]; + else if ([object respondsToSelector:@selector(unsignedIntValue)]) ullValue = [object unsignedIntValue]; + else if ([object respondsToSelector:@selector(intValue)]) ullValue = [object intValue]; + else ullValue = defaultValue; + + return ullValue; +} + + +static inline BOOL IsSpaceOrTab(int value) +{ + return value == ' ' || value == '\t'; +} + + +static BOOL IsZeroString(NSString *string) +{ + /* I don't particularly like regexps, but there are occasions... + To match NSString's behaviour for intValue etc. with non-zero numbers, + we need to skip any leading spaces or tabs (but not line breaks), get + an optional minus sign, then at least one 0. Any trailing junk is + ignored. It is assumed that this function is called for strings whose + numerical value has already been determined to be 0. + */ + + unsigned long i = 0, count = [string length]; +#define PEEK() ((i >= count) ? -1 : [string characterAtIndex:i]) + + while (IsSpaceOrTab(PEEK())) ++i; // Skip spaces and tabs + if (PEEK() == ' ') ++i; // Skip optional hyphen-minus + return PEEK() == '0'; // If this is a 0, it's a numerical string. + +#undef PEEK +} + + +static BOOL BooleanFromString(NSString *string, BOOL defaultValue) +{ + if (NSOrderedSame == [string caseInsensitiveCompare:@"yes"] || + NSOrderedSame == [string caseInsensitiveCompare:@"true"] || + NSOrderedSame == [string caseInsensitiveCompare:@"on"] || + [string doubleValue] != 0.0) // Floating point is used so values like @"0.1" are treated as nonzero. + { + return YES; + } + else if (NSOrderedSame == [string caseInsensitiveCompare:@"no"] || + NSOrderedSame == [string caseInsensitiveCompare:@"false"] || + NSOrderedSame == [string caseInsensitiveCompare:@"off"] || + IsZeroString(string)) + { + return NO; + } + return defaultValue; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +static float FuzzyBooleanProbabilityFromString(NSString *string, float defaultValue) +{ + if (NSOrderedSame == [string caseInsensitiveCompare:@"yes"] || + NSOrderedSame == [string caseInsensitiveCompare:@"true"] || + NSOrderedSame == [string caseInsensitiveCompare:@"on"] || + [string doubleValue] != 0.0) // Floating point is used so values like @"0.1" are treated as nonzero. + { + return 1.0f; + } + else if (NSOrderedSame == [string caseInsensitiveCompare:@"no"] || + NSOrderedSame == [string caseInsensitiveCompare:@"false"] || + NSOrderedSame == [string caseInsensitiveCompare:@"off"] || + IsZeroString(string)) + { + return 0.0f; + } + return defaultValue; +} +#endif + + +BOOL OOBooleanFromObject(id object, BOOL defaultValue) +{ + BOOL result; + + if ([object isKindOfClass:[NSString class]]) + { + result = BooleanFromString(object, defaultValue); + } + else + { + if ([object respondsToSelector:@selector(boolValue)]) result = [object boolValue]; + else if ([object respondsToSelector:@selector(intValue)]) result = [object intValue] != 0; + else result = defaultValue; + } + + return result; +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +BOOL OOFuzzyBooleanFromObject(id object, float defaultValue) +{ + float probability; + + if ([object isKindOfClass:[NSString class]]) + { + probability = [object floatValue]; + + // If our string represents zero, it might be erroneous input or simply yes/no, + // true/false or on/off valid boolean strings. Act on it. + if (probability == 0.0f && !IsZeroString(object)) + { + probability = FuzzyBooleanProbabilityFromString(object, defaultValue); + } + } + else + { + probability = OOFloatFromObject(object, defaultValue); + } + + /* This will always be NO for negative values and YES for values + greater than 1, as expected. randf() is always less than 1, so + < is the correct operator here. + */ + return randf() < probability; +} +#endif + + +float OOFloatFromObject(id object, float defaultValue) +{ + float result; + + if ([object respondsToSelector:@selector(floatValue)]) + { + result = [object floatValue]; + if (result == 0.0f && [object isKindOfClass:[NSString class]] && !IsZeroString(object)) result = defaultValue; + } + else if ([object respondsToSelector:@selector(doubleValue)]) result = [object doubleValue]; + else if ([object respondsToSelector:@selector(intValue)]) result = [object intValue]; + else result = defaultValue; + + return result; +} + + +double OODoubleFromObject(id object, double defaultValue) +{ + double result; + + if ([object respondsToSelector:@selector(doubleValue)]) + { + result = [object doubleValue]; + if (result == 0.0 && [object isKindOfClass:[NSString class]] && !IsZeroString(object)) result = defaultValue; + } + else if ([object respondsToSelector:@selector(floatValue)]) result = [object floatValue]; + else if ([object respondsToSelector:@selector(intValue)]) result = [object intValue]; + else result = defaultValue; + + return result; +} + + +float OONonNegativeFloatFromObject(id object, float defaultValue) +{ + float result; + + if ([object respondsToSelector:@selector(floatValue)]) result = [object floatValue]; + else if ([object respondsToSelector:@selector(doubleValue)]) result = [object doubleValue]; + else if ([object respondsToSelector:@selector(intValue)]) result = [object intValue]; + else return defaultValue; // Don't clamp default + + return OOMax_f(result, 0.0f); +} + + +double OONonNegativeDoubleFromObject(id object, double defaultValue) +{ + double result; + + if ([object respondsToSelector:@selector(doubleValue)]) result = [object doubleValue]; + else if ([object respondsToSelector:@selector(floatValue)]) result = [object floatValue]; + else if ([object respondsToSelector:@selector(intValue)]) result = [object intValue]; + else return defaultValue; // Don't clamp default + + return OOMax_d(result, 0.0f); +} + + +#ifndef OOCOLLECTIONEXTRACTORS_SIMPLE +Vector OOVectorFromObject(id object, Vector defaultValue) +{ + Vector result = defaultValue; + NSDictionary *dict = nil; + + if ([object isKindOfClass:[NSString class]]) + { + // This will only write result if a valid vector is found, and will write an error message otherwise. + ScanVectorFromString(object, &result); + } + else if ([object isKindOfClass:[NSArray class]] && [object count] == 3) + { + result.x = [object floatAtIndex:0]; + result.y = [object floatAtIndex:1]; + result.z = [object floatAtIndex:2]; + } + else if ([object isKindOfClass:[NSDictionary class]]) + { + dict = object; + // Require at least one of the keys x, y, or z + if ([dict objectForKey:@"x"] != nil || + [dict objectForKey:@"y"] != nil || + [dict objectForKey:@"z"] != nil) + { + // Note: uses 0 for unknown components rather than components of defaultValue. + result.x = [dict floatForKey:@"x" defaultValue:0.0f]; + result.y = [dict floatForKey:@"y" defaultValue:0.0f]; + result.z = [dict floatForKey:@"z" defaultValue:0.0f]; + } + } + + return result; +} + + +Quaternion OOQuaternionFromObject(id object, Quaternion defaultValue) +{ + Quaternion result = defaultValue; + NSDictionary *dict = nil; + + if ([object isKindOfClass:[NSString class]]) + { + // This will only write result if a valid quaternion is found, and will write an error message otherwise. + ScanQuaternionFromString(object, &result); + } + else if ([object isKindOfClass:[NSArray class]] && [object count] == 4) + { + result.w = [object floatAtIndex:0]; + result.x = [object floatAtIndex:1]; + result.y = [object floatAtIndex:2]; + result.z = [object floatAtIndex:3]; + } + else if ([object isKindOfClass:[NSDictionary class]]) + { + dict = object; + // Require at least one of the keys w, x, y, or z + if ([dict objectForKey:@"w"] != nil || + [dict objectForKey:@"x"] != nil || + [dict objectForKey:@"y"] != nil || + [dict objectForKey:@"z"] != nil) + { + // Note: uses 0 for unknown components rather than components of defaultValue. + result.w = [dict floatForKey:@"w" defaultValue:0.0f]; + result.x = [dict floatForKey:@"x" defaultValue:0.0f]; + result.y = [dict floatForKey:@"y" defaultValue:0.0f]; + result.z = [dict floatForKey:@"z" defaultValue:0.0f]; + } + } + + return result; +} + + +NSDictionary *OOPropertyListFromVector(Vector value) +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithFloat:value.x], @"x", + [NSNumber numberWithFloat:value.y], @"y", + [NSNumber numberWithFloat:value.z], @"z", + nil]; +} + + +NSDictionary *OOPropertyListFromQuaternion(Quaternion value) +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithFloat:value.w], @"w", + [NSNumber numberWithFloat:value.x], @"x", + [NSNumber numberWithFloat:value.y], @"y", + [NSNumber numberWithFloat:value.z], @"z", + nil]; +} +#endif + + +static NSSet *SetForObject(id object, NSSet *defaultValue) +{ + if ([object isKindOfClass:[NSArray class]]) return [NSSet setWithArray:object]; + else if ([object isKindOfClass:[NSSet class]]) return [[object copy] autorelease]; + + return defaultValue; +} + + +static NSString *StringForObject(id object, NSString *defaultValue) +{ + if ([object isKindOfClass:[NSString class]]) return object; + else if ([object respondsToSelector:@selector(stringValue)]) return [object stringValue]; + + return defaultValue; +} diff --git a/src/Core/OOColor.h b/src/Core/OOColor.h new file mode 100644 index 00000000..6639a242 --- /dev/null +++ b/src/Core/OOColor.h @@ -0,0 +1,129 @@ +/* + +OOColor.h + +Replacement for NSColor (to avoid AppKit dependencies). Only handles RGBA +colours without colour space correction. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" + + +typedef struct +{ + OOCGFloat r, g, b, a; +} OORGBAComponents; + + +typedef struct +{ + OOCGFloat h, s, b, a; +} OOHSBAComponents; + + +@interface OOColor : NSObject +{ + OOCGFloat rgba[4]; +} + ++ (OOColor *)colorWithCalibratedHue:(float)hue saturation:(float)saturation brightness:(float)brightness alpha:(float)alpha; // Note: hue in 0..1 ++ (OOColor *)colorWithCalibratedRed:(float)red green:(float)green blue:(float)blue alpha:(float)alpha; ++ (OOColor *)colorWithCalibratedWhite:(float)white alpha:(float)alpha; ++ (OOColor *)colorWithRGBAComponents:(OORGBAComponents)components; ++ (OOColor *)colorWithHSBAComponents:(OOHSBAComponents)components; // Note: hue in 0..360 + +// Flexible color creator; takes a selector name, a string with components, or an array. ++ (OOColor *)colorWithDescription:(id)description; + +// Like +colorWithDescription:, but forces brightness of at least 0.5. ++ (OOColor *)brightColorWithDescription:(id)description; + +// Creates a colour given a string with components. ++ (OOColor *)colorFromString:(NSString*) colorFloatString; + ++ (OOColor *)blackColor; /* 0.0 white */ ++ (OOColor *)darkGrayColor; /* 0.333 white */ ++ (OOColor *)lightGrayColor; /* 0.667 white */ ++ (OOColor *)whiteColor; /* 1.0 white */ ++ (OOColor *)grayColor; /* 0.5 white */ ++ (OOColor *)redColor; /* 1.0, 0.0, 0.0 RGB */ ++ (OOColor *)greenColor; /* 0.0, 1.0, 0.0 RGB */ ++ (OOColor *)blueColor; /* 0.0, 0.0, 1.0 RGB */ ++ (OOColor *)cyanColor; /* 0.0, 1.0, 1.0 RGB */ ++ (OOColor *)yellowColor; /* 1.0, 1.0, 0.0 RGB */ ++ (OOColor *)magentaColor; /* 1.0, 0.0, 1.0 RGB */ ++ (OOColor *)orangeColor; /* 1.0, 0.5, 0.0 RGB */ ++ (OOColor *)purpleColor; /* 0.5, 0.0, 0.5 RGB */ ++ (OOColor *)brownColor; /* 0.6, 0.4, 0.2 RGB */ ++ (OOColor *)clearColor; /* 0.0 white, 0.0 alpha */ + +/* Blend using the NSCalibratedRGB color space. Both colors are converted into the calibrated RGB color space, and they are blended by taking fraction of color and 1 - fraction of the receiver. The result is in the calibrated RGB color space. If the colors cannot be converted into the calibrated RGB color space the blending fails and nil is returned. +*/ +- (OOColor *)blendedColorWithFraction:(float)fraction ofColor:(OOColor *)color; + ++ (OOColor *) planetTextureColor:(OOCGFloat) q:(OOColor *) seaColor:(OOColor *) paleSeaColor:(OOColor *) landColor:(OOColor *) paleLandColor; ++ (OOColor *) planetTextureColor:(OOCGFloat) q:(OOCGFloat) impress:(OOCGFloat) bias :(OOColor *) seaColor:(OOColor *) paleSeaColor:(OOColor *) landColor:(OOColor *) paleLandColor; + +/* Get the red, green, or blue components of NSCalibratedRGB or NSDeviceRGB colors. +*/ +- (OOCGFloat)redComponent; +- (OOCGFloat)greenComponent; +- (OOCGFloat)blueComponent; +- (void)getRed:(OOCGFloat *)red green:(OOCGFloat *)green blue:(OOCGFloat *)blue alpha:(OOCGFloat *)alpha; +- (void)getGLRed:(GLfloat *)red green:(GLfloat *)green blue:(GLfloat *)blue alpha:(GLfloat *)alpha; + +- (OORGBAComponents)rgbaComponents; + +- (BOOL)isBlack; + +/* Get the components of NSCalibratedRGB or NSDeviceRGB colors as hue, saturation, or brightness. + + IMPORTANT: for reasons of bugwards compatibility, these return hue values + in the range [0, 360], but +colorWithCalibratedHue:... expects values in + the range [0, 1]. +*/ +- (OOCGFloat)hueComponent; +- (OOCGFloat)saturationComponent; +- (OOCGFloat)brightnessComponent; +- (void)getHue:(OOCGFloat *)hue saturation:(OOCGFloat *)saturation brightness:(OOCGFloat *)brightness alpha:(OOCGFloat *)alpha; + +- (OOHSBAComponents)hsbaComponents; + + +// Get the alpha component. +- (OOCGFloat)alphaComponent; + +// Returns the colour, premultiplied by its alpha channel, and with an alpha of 1.0. If the reciever's alpha is 1.0, it will return itself. +- (OOColor *)premultipliedColor; + +// Multiply r, g and b components of a colour by specified factor, clamped to [0..1]. +- (OOColor *)colorWithBrightnessFactor:(float)factor; + +// r,g,b,a array in 0..1 range. +- (NSArray *)normalizedArray; + +@end + + +NSString *OORGBAComponentsDescription(OORGBAComponents components); +NSString *OOHSBAComponentsDescription(OOHSBAComponents components); diff --git a/src/Core/OOColor.m b/src/Core/OOColor.m new file mode 100644 index 00000000..f7be4afb --- /dev/null +++ b/src/Core/OOColor.m @@ -0,0 +1,539 @@ +/* + +OOColor.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOColor.h" +#import "OOCollectionExtractors.h" +#import "OOMaths.h" + + +@implementation OOColor + +// Set methods are internal, because OOColor is immutable (as seen from outside). +- (void) setRGBA:(OOCGFloat)r:(OOCGFloat)g:(OOCGFloat)b:(OOCGFloat)a +{ + rgba[0] = r; + rgba[1] = g; + rgba[2] = b; + rgba[3] = a; +} + + +- (void) setHSBA:(OOCGFloat)h:(OOCGFloat)s:(OOCGFloat)b:(OOCGFloat)a +{ + rgba[3] = a; + if (s == 0.0f) + { + rgba[0] = rgba[1] = rgba[2] = b; + return; + } + OOCGFloat f, p, q, t; + int i; + h = fmodf(h, 360.0f); + if (h < 0.0) h += 360.0f; + h /= 60.0f; + + i = floor(h); + f = h - i; + p = b * (1.0f - s); + q = b * (1.0f - (s * f)); + t = b * (1.0f - (s * (1.0f - f))); + + switch (i) + { + case 0: + rgba[0] = b; rgba[1] = t; rgba[2] = p; break; + case 1: + rgba[0] = q; rgba[1] = b; rgba[2] = p; break; + case 2: + rgba[0] = p; rgba[1] = b; rgba[2] = t; break; + case 3: + rgba[0] = p; rgba[1] = q; rgba[2] = b; break; + case 4: + rgba[0] = t; rgba[1] = p; rgba[2] = b; break; + case 5: + rgba[0] = b; rgba[1] = p; rgba[2] = q; break; + } +} + + +- (id)copyWithZone:(NSZone *)zone +{ + // Copy is implemented as retain since OOColor is immutable. + return [self retain]; +} + + +/* Create NSCalibratedRGBColorSpace colors. +*/ ++ (OOColor *)colorWithCalibratedHue:(float)hue saturation:(float)saturation brightness:(float)brightness alpha:(float)alpha +{ + OOColor* result = [[OOColor alloc] init]; + [result setHSBA: 360.0 * hue : saturation : brightness : alpha]; + return [result autorelease]; +} + + ++ (OOColor *)colorWithCalibratedRed:(float)red green:(float)green blue:(float)blue alpha:(float)alpha +{ + OOColor* result = [[OOColor alloc] init]; + [result setRGBA:red:green:blue:alpha]; + return [result autorelease]; +} + + ++ (OOColor *)colorWithCalibratedWhite:(float)white alpha:(float)alpha +{ + return [OOColor colorWithCalibratedRed:white green:white blue:white alpha:alpha]; +} + + ++ (OOColor *)colorWithRGBAComponents:(OORGBAComponents)components +{ + return [self colorWithCalibratedRed:components.r + green:components.g + blue:components.b + alpha:components.a]; +} + + ++ (OOColor *)colorWithHSBAComponents:(OOHSBAComponents)components +{ + return [self colorWithCalibratedHue:components.h / 360.0f + saturation:components.s + brightness:components.b + alpha:components.a]; +} + + ++ (OOColor *)colorWithDescription:(id)description +{ + NSDictionary *dict = nil; + + if (description == nil) return nil; + + if ([description isKindOfClass:[OOColor class]]) + { + return [[description copy] autorelease]; + } + else if ([description isKindOfClass:[NSString class]]) + { + if ([description hasSuffix:@"Color"]) + { + // +fooColor selector + SEL selector = NSSelectorFromString(description); + if ([self respondsToSelector:selector]) return [self performSelector:selector]; + } + else + { + // Some other string + return [self colorFromString:description]; + } + } + else if ([description isKindOfClass:[NSArray class]]) + { + return [self colorFromString:[description componentsJoinedByString:@" "]]; + } + else if ([description isKindOfClass:[NSDictionary class]]) + { + dict = description; // Workaround for gnu-gcc's more agressive "multiple methods named..." warnings. + + if ([dict objectForKey:@"hue"] != nil) + { + // Treat as HSB(A) dictionary + float h = [dict floatForKey:@"hue"]; + float s = [dict floatForKey:@"saturation" defaultValue:1.0f]; + float b = [dict floatForKey:@"brightness" defaultValue:-1.0f]; + if (b < 0.0f) b = [dict floatForKey:@"value" defaultValue:1.0f]; + float a = [dict floatForKey:@"alpha" defaultValue:-1.0f]; + if (a < 0.0f) a = [dict floatForKey:@"opacity" defaultValue:1.0f]; + + return [OOColor colorWithCalibratedHue:h / 360.0f saturation:s brightness:b alpha:a]; + } + else + { + // Treat as RGB(A) dictionary + float r = [dict floatForKey:@"red"]; + float g = [dict floatForKey:@"green"]; + float b = [dict floatForKey:@"blue"]; + float a = [dict floatForKey:@"alpha" defaultValue:-1.0f]; + if (a < 0.0f) a = [dict floatForKey:@"opacity" defaultValue:1.0f]; + + return [OOColor colorWithCalibratedRed:r green:g blue:b alpha:a]; + } + } + + return nil; +} + + ++ (OOColor *)brightColorWithDescription:(id)description +{ + OOColor *color = [OOColor colorWithDescription:description]; + if (color == nil || 0.5f <= [color brightnessComponent]) return color; + + return [OOColor colorWithCalibratedHue:[color hueComponent] saturation:[color saturationComponent] brightness:0.5f alpha:1.0f]; +} + + ++ (OOColor *)colorFromString:(NSString*) colorFloatString +{ + float rgbaValue[4] = { 0.0f, 0.0f, 0.0f, 1.0f }; + NSScanner *scanner = [NSScanner scannerWithString:colorFloatString]; + float factor = 1.0f; + int i; + + for (i = 0; i != 4; ++i) + { + if (![scanner scanFloat:&rgbaValue[i]]) + { + // Less than three floats or non-float, can't parse -> quit + if (i < 3) return nil; + + // If we get here, we only got three components. Make sure alpha is at correct scale: + rgbaValue[3] /= factor; + } + if (1.0f < rgbaValue[i]) factor = 1.0f / 255.0f; + } + + return [OOColor colorWithCalibratedRed:rgbaValue[0] * factor green:rgbaValue[1] * factor blue:rgbaValue[2] * factor alpha:rgbaValue[3] * factor]; +} + + ++ (OOColor *)blackColor // 0.0 white +{ + return [OOColor colorWithCalibratedWhite:0.0f alpha:1.0f]; +} + + ++ (OOColor *)darkGrayColor // 0.333 white +{ + return [OOColor colorWithCalibratedWhite:1.0f/3.0f alpha:1.0f]; +} + + ++ (OOColor *)lightGrayColor // 0.667 white +{ + return [OOColor colorWithCalibratedWhite:2.0f/3.0f alpha:1.0f]; +} + + ++ (OOColor *)whiteColor // 1.0 white +{ + return [OOColor colorWithCalibratedWhite:1.0f alpha:1.0f]; +} + + ++ (OOColor *)grayColor // 0.5 white +{ + return [OOColor colorWithCalibratedWhite:0.5f alpha:1.0f]; +} + + ++ (OOColor *)redColor // 1.0, 0.0, 0.0 RGB +{ + return [OOColor colorWithCalibratedRed:1.0f green:0.0f blue:0.0f alpha:1.0f]; +} + + ++ (OOColor *)greenColor // 0.0, 1.0, 0.0 RGB +{ + return [OOColor colorWithCalibratedRed:0.0f green:1.0f blue:0.0f alpha:1.0f]; +} + + ++ (OOColor *)blueColor // 0.0, 0.0, 1.0 RGB +{ + return [OOColor colorWithCalibratedRed:0.0f green:0.0f blue:1.0f alpha:1.0f]; +} + + ++ (OOColor *)cyanColor // 0.0, 1.0, 1.0 RGB +{ + return [OOColor colorWithCalibratedRed:0.0f green:1.0f blue:1.0f alpha:1.0f]; +} + + ++ (OOColor *)yellowColor // 1.0, 1.0, 0.0 RGB +{ + return [OOColor colorWithCalibratedRed:1.0f green:1.0f blue:0.0f alpha:1.0f]; +} + + ++ (OOColor *)magentaColor // 1.0, 0.0, 1.0 RGB +{ + return [OOColor colorWithCalibratedRed:1.0f green:0.0f blue:1.0f alpha:1.0f]; +} + + ++ (OOColor *)orangeColor // 1.0, 0.5, 0.0 RGB +{ + return [OOColor colorWithCalibratedRed:1.0f green:0.5f blue:0.0f alpha:1.0f]; +} + + ++ (OOColor *)purpleColor // 0.5, 0.0, 0.5 RGB +{ + return [OOColor colorWithCalibratedRed:0.5f green:0.0f blue:0.5f alpha:1.0f]; +} + + ++ (OOColor *)brownColor // 0.6, 0.4, 0.2 RGB +{ + return [OOColor colorWithCalibratedRed:0.6f green:0.4f blue:0.2f alpha:1.0f]; +} + + ++ (OOColor *)clearColor // 0.0 white, 0.0 alpha +{ + return [OOColor colorWithCalibratedWhite:0.0f alpha:0.0f]; +} + + +- (OOColor *)blendedColorWithFraction:(float)fraction ofColor:(OOColor *)color +{ + GLfloat rgba1[4]; + [color getGLRed:&rgba1[0] green:&rgba1[1] blue:&rgba1[2] alpha:&rgba1[3]]; + OOColor* result = [[OOColor alloc] init]; + float prime = 1.0f - fraction; + [result setRGBA: prime * rgba[0] + fraction * rgba1[0] : prime * rgba[1] + fraction * rgba1[1] : prime * rgba[2] + fraction * rgba1[2] : prime * rgba[3] + fraction * rgba1[3]]; + return [result autorelease]; +} + + +// find a point on the sea->land scale ++ (OOColor *) planetTextureColor:(OOCGFloat) q:(OOColor *) seaColor:(OOColor *) paleSeaColor:(OOColor *) landColor:(OOColor *) paleLandColor +{ + float hi = 0.33; + float oh = 1.0 / hi; + float ih = 1.0 / (1.0 - hi); + if (q <= 0.0) + return seaColor; + if (q > 1.0) + return [OOColor whiteColor]; + if (q < 0.01) + return [paleSeaColor blendedColorWithFraction: q * 100.0 ofColor: landColor]; + if (q > hi) + return [paleLandColor blendedColorWithFraction: (q - hi) * ih ofColor: [OOColor whiteColor]]; // snow capped peaks + return [paleLandColor blendedColorWithFraction: (hi - q) * oh ofColor: landColor]; +} + + +// find a point on the sea->land scale given impress and bias ++ (OOColor *) planetTextureColor:(OOCGFloat) q:(OOCGFloat) impress:(OOCGFloat) bias :(OOColor *) seaColor:(OOColor *) paleSeaColor:(OOColor *) landColor:(OOColor *) paleLandColor +{ + float maxq = impress + bias; + + float hi = 0.66667 * maxq; + float oh = 1.0 / hi; + float ih = 1.0 / (1.0 - hi); + + if (q <= 0.0) + return seaColor; + if (q > 1.0) + return [OOColor whiteColor]; + if (q < 0.01) + return [paleSeaColor blendedColorWithFraction: q * 100.0 ofColor: landColor]; + if (q > hi) + return [paleLandColor blendedColorWithFraction: (q - hi) * ih ofColor: [OOColor whiteColor]]; // snow capped peaks + return [paleLandColor blendedColorWithFraction: (hi - q) * oh ofColor: landColor]; +} + + +// Get the red, green, or blue components. +- (OOCGFloat)redComponent +{ + return rgba[0]; +} + + +- (OOCGFloat)greenComponent +{ + return rgba[1]; +} + + +- (OOCGFloat)blueComponent +{ + return rgba[2]; +} + + +- (void)getRed:(OOCGFloat *)red green:(OOCGFloat *)green blue:(OOCGFloat *)blue alpha:(OOCGFloat *)alpha +{ + *red = rgba[0]; + *green = rgba[1]; + *blue = rgba[2]; + *alpha = rgba[3]; +} + + +- (void)getGLRed:(GLfloat *)red green:(GLfloat *)green blue:(GLfloat *)blue alpha:(GLfloat *)alpha +{ + *red = rgba[0]; + *green = rgba[1]; + *blue = rgba[2]; + *alpha = rgba[3]; +} + + +- (OORGBAComponents)rgbaComponents +{ + OORGBAComponents c = { rgba[0], rgba[1], rgba[2], rgba[3] }; + return c; +} + + +- (BOOL)isBlack +{ + return rgba[0] == 0.0f && rgba[1] == 0.0f && rgba[2] == 0.0f; +} + + +// Get the components as hue, saturation, or brightness. +- (OOCGFloat)hueComponent +{ + OOCGFloat maxrgb = (rgba[0] > rgba[1])? ((rgba[0] > rgba[2])? rgba[0]:rgba[2]):((rgba[1] > rgba[2])? rgba[1]:rgba[2]); + OOCGFloat minrgb = (rgba[0] < rgba[1])? ((rgba[0] < rgba[2])? rgba[0]:rgba[2]):((rgba[1] < rgba[2])? rgba[1]:rgba[2]); + if (maxrgb == minrgb) + return 0.0; + OOCGFloat delta = maxrgb - minrgb; + OOCGFloat hue = 0.0; + if (rgba[0] == maxrgb) + hue = (rgba[1] - rgba[2]) / delta; + else if (rgba[1] == maxrgb) + hue = 2.0 + (rgba[2] - rgba[0]) / delta; + else if (rgba[2] == maxrgb) + hue = 4.0 + (rgba[0] - rgba[1]) / delta; + hue *= 60.0; + while (hue < 0.0) hue += 360.0; + return hue; +} + +- (OOCGFloat)saturationComponent +{ + OOCGFloat maxrgb = (rgba[0] > rgba[1])? ((rgba[0] > rgba[2])? rgba[0]:rgba[2]):((rgba[1] > rgba[2])? rgba[1]:rgba[2]); + OOCGFloat minrgb = (rgba[0] < rgba[1])? ((rgba[0] < rgba[2])? rgba[0]:rgba[2]):((rgba[1] < rgba[2])? rgba[1]:rgba[2]); + OOCGFloat brightness = 0.5 * (maxrgb + minrgb); + if (maxrgb == minrgb) + return 0.0; + OOCGFloat delta = maxrgb - minrgb; + return (brightness <= 0.5)? (delta / (maxrgb + minrgb)) : (delta / (2.0 - (maxrgb + minrgb))); +} + +- (OOCGFloat)brightnessComponent +{ + OOCGFloat maxrgb = (rgba[0] > rgba[1])? ((rgba[0] > rgba[2])? rgba[0]:rgba[2]):((rgba[1] > rgba[2])? rgba[1]:rgba[2]); + OOCGFloat minrgb = (rgba[0] < rgba[1])? ((rgba[0] < rgba[2])? rgba[0]:rgba[2]):((rgba[1] < rgba[2])? rgba[1]:rgba[2]); + return 0.5 * (maxrgb + minrgb); +} + +- (void)getHue:(OOCGFloat *)hue saturation:(OOCGFloat *)saturation brightness:(OOCGFloat *)brightness alpha:(OOCGFloat *)alpha +{ + *alpha = rgba[3]; + + int maxrgb = (rgba[0] > rgba[1])? ((rgba[0] > rgba[2])? 0:2):((rgba[1] > rgba[2])? 1:2); + int minrgb = (rgba[0] < rgba[1])? ((rgba[0] < rgba[2])? 0:2):((rgba[1] < rgba[2])? 1:2); + *brightness = 0.5 * (rgba[maxrgb] + rgba[minrgb]); + if (rgba[maxrgb] == rgba[minrgb]) + { + *saturation = 0.0; + *hue = 0.0; + return; + } + OOCGFloat delta = rgba[maxrgb] - rgba[minrgb]; + *saturation = (*brightness <= 0.5)? (delta / (rgba[maxrgb] + rgba[minrgb])) : (delta / (2.0 - (rgba[maxrgb] + rgba[minrgb]))); + + if (maxrgb==0) + *hue = (rgba[1] - rgba[2]) / delta; + else if (maxrgb==1) + *hue = 2.0 + (rgba[2] - rgba[0]) / delta; + else if (maxrgb==2) + *hue = 4.0 + (rgba[0] - rgba[1]) / delta; + *hue *= 60.0; + while (*hue < 0.0) *hue += 360.0; +} + + +- (OOHSBAComponents)hsbaComponents +{ + OOHSBAComponents c; + [self getHue:&c.h + saturation:&c.s + brightness:&c.b + alpha:&c.a]; + return c; +} + + +// Get the alpha component. +- (OOCGFloat)alphaComponent +{ + return rgba[3]; +} + + +- (OOColor *)premultipliedColor +{ + if (rgba[3] == 1.0f) return [[self retain] autorelease]; + return [OOColor colorWithCalibratedRed:rgba[0] * rgba[3] + green:rgba[1] * rgba[3] + blue:rgba[2] * rgba[3] + alpha:1.0f]; +} + + +- (OOColor *)colorWithBrightnessFactor:(float)factor +{ + return [OOColor colorWithCalibratedRed:OOClamp_0_1_f(rgba[0] * factor) + green:OOClamp_0_1_f(rgba[1] * factor) + blue:OOClamp_0_1_f(rgba[2] * factor) + alpha:rgba[3]]; +} + + +- (NSArray *)normalizedArray +{ + OOCGFloat r, g, b, a; + [self getRed:&r green:&g blue:&b alpha:&a]; + return [NSArray arrayWithObjects: + [NSNumber numberWithDouble:r], + [NSNumber numberWithDouble:g], + [NSNumber numberWithDouble:b], + [NSNumber numberWithDouble:a], + nil]; +} + +@end + + +NSString *OORGBAComponentsDescription(OORGBAComponents components) +{ + return [NSString stringWithFormat:@"{%.3g, %.3g, %.3g, %.3g}", components.r, components.g, components.b, components.a]; +} + + +NSString *OOHSBAComponentsDescription(OOHSBAComponents components) +{ + return [NSString stringWithFormat:@"{%i, %.3g, %.3g, %.3g}", (int)components.h, components.s, components.b, components.a]; +} diff --git a/src/Core/OOConstToString.h b/src/Core/OOConstToString.h new file mode 100644 index 00000000..7f3475b4 --- /dev/null +++ b/src/Core/OOConstToString.h @@ -0,0 +1,87 @@ +/* + +OOConstToString.h + +Convert various sets of integer constants to strings. +To consider: replacing the integer constants with string constants. + +This has grown beyond "const-to-string" at this point. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOFunctionAttributes.h" +#import "OOTypes.h" + + +NSString *EntityStatusToString(OOEntityStatus status) CONST_FUNC; +OOEntityStatus StringToEntityStatus(NSString *string) PURE_FUNC; + +NSString *ScanClassToString(OOScanClass scanClass) CONST_FUNC; +OOScanClass StringToScanClass(NSString *string) PURE_FUNC; + +NSString *InstinctToString(OOInstinctID instinct) CONST_FUNC; +OOInstinctID StringToInstinct(NSString *string) PURE_FUNC; + +NSString *BehaviourToString(OOBehaviour behaviour) CONST_FUNC; + +NSString *GovernmentToString(OOGovernmentID government); + +NSString *EconomyToString(OOEconomyID economy); + +NSString *JSTypeToString(int /* JSType */ type) CONST_FUNC; + +NSString *WeaponTypeToString(OOWeaponType weapon) CONST_FUNC; +OOWeaponType StringToWeaponType(NSString *string) PURE_FUNC; + +// Weapon strings prefixed with EQ_, used in shipyard.plist. +NSString *WeaponTypeToEquipmentString(OOWeaponType weapon) CONST_FUNC; +OOWeaponType EquipmentStringToWeaponTypeSloppy(NSString *string) PURE_FUNC; // Uses suffix match for backwards compatibility. +OOWeaponType EquipmentStringToWeaponTypeStrict(NSString *string) PURE_FUNC; + +NSString *CargoTypeToString(OOCargoType cargo) CONST_FUNC; +OOCargoType StringToCargoType(NSString *string) PURE_FUNC; + +NSString *EnergyUnitTypeToString(OOEnergyUnitType unit) CONST_FUNC; +OOEnergyUnitType StringToEnergyUnitType(NSString *string) PURE_FUNC; + +NSString *GUIScreenIDToString(OOGUIScreenID screen) CONST_FUNC; +OOEnergyUnitType StringToGUIScreenID(NSString *string) PURE_FUNC; + +NSString *KillCountToRatingString(unsigned kills) CONST_FUNC; +NSString *KillCountToRatingAndKillString(unsigned kills) CONST_FUNC; +NSString *LegalStatusToString(int legalStatus) CONST_FUNC; +NSString *AlertConditionToString(OOAlertCondition alertCondition) CONST_FUNC; + +NSString *ShaderSettingToDisplayString(OOShaderSetting setting) CONST_FUNC; + +NSString *CommodityDisplayNameForSymbolicName(NSString *symbolicName); +NSString *CommodityDisplayNameForCommodityArray(NSArray *commodityDefinition); + +NSString *DisplayStringForMassUnit(OOMassUnit unit); +NSString *DisplayStringForMassUnitForCommodity(OOCargoType commodity); + +OOGalacticHyperspaceBehaviour StringToGalacticHyperspaceBehaviour(NSString *string) PURE_FUNC; +NSString *GalacticHyperspaceBehaviourToString(OOGalacticHyperspaceBehaviour behaviour) CONST_FUNC; + +#if DOCKING_CLEARANCE_ENABLED +NSString *DockingClearanceStatusToString(OODockingClearanceStatus dockingClearanceStatus) PURE_FUNC; +#endif diff --git a/src/Core/OOConstToString.m b/src/Core/OOConstToString.m new file mode 100644 index 00000000..0401a34b --- /dev/null +++ b/src/Core/OOConstToString.m @@ -0,0 +1,623 @@ +/* + +OOConstToString.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version ); +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., ); Franklin Street, Fifth Floor, Boston, +MA );-);, USA. + +*/ + +#import "OOConstToString.h" +#import "Entity.h" +#import "Universe.h" +#import +#import "OOCollectionExtractors.h" + + +#define CASE(foo) case foo: return @#foo; +#define REVERSE_CASE(foo) if ([string isEqualToString:@#foo]) return foo; + + +NSString *EntityStatusToString(OOEntityStatus status) +{ + switch (status) + { + CASE(STATUS_EFFECT); + CASE(STATUS_ACTIVE); + CASE(STATUS_COCKPIT_DISPLAY); + CASE(STATUS_TEST); + CASE(STATUS_INACTIVE); + CASE(STATUS_DEAD); + CASE(STATUS_START_GAME); + CASE(STATUS_IN_FLIGHT); + CASE(STATUS_DOCKED); + CASE(STATUS_AUTOPILOT_ENGAGED); + CASE(STATUS_DOCKING); + CASE(STATUS_LAUNCHING); + CASE(STATUS_WITCHSPACE_COUNTDOWN); + CASE(STATUS_ENTERING_WITCHSPACE); + CASE(STATUS_EXITING_WITCHSPACE); + CASE(STATUS_ESCAPE_SEQUENCE); + CASE(STATUS_IN_HOLD); + CASE(STATUS_BEING_SCOOPED); + CASE(STATUS_HANDLING_ERROR); + } + + return @"UNDEFINED"; +} + + +OOEntityStatus StringToEntityStatus(NSString *string) +{ + REVERSE_CASE(STATUS_EFFECT); + REVERSE_CASE(STATUS_ACTIVE); + REVERSE_CASE(STATUS_COCKPIT_DISPLAY); + REVERSE_CASE(STATUS_TEST); + REVERSE_CASE(STATUS_INACTIVE); + REVERSE_CASE(STATUS_DEAD); + REVERSE_CASE(STATUS_START_GAME); + REVERSE_CASE(STATUS_IN_FLIGHT); + REVERSE_CASE(STATUS_DOCKED); + REVERSE_CASE(STATUS_AUTOPILOT_ENGAGED); + REVERSE_CASE(STATUS_DOCKING); + REVERSE_CASE(STATUS_LAUNCHING); + REVERSE_CASE(STATUS_WITCHSPACE_COUNTDOWN); + REVERSE_CASE(STATUS_ENTERING_WITCHSPACE); + REVERSE_CASE(STATUS_EXITING_WITCHSPACE); + REVERSE_CASE(STATUS_ESCAPE_SEQUENCE); + REVERSE_CASE(STATUS_IN_HOLD); + REVERSE_CASE(STATUS_BEING_SCOOPED); + REVERSE_CASE(STATUS_HANDLING_ERROR); + + return STATUS_INACTIVE; +} + + +NSString *ScanClassToString(OOScanClass scanClass) +{ + switch (scanClass) + { + CASE(CLASS_NOT_SET); + CASE(CLASS_NO_DRAW); + CASE(CLASS_NEUTRAL); + CASE(CLASS_STATION); + CASE(CLASS_TARGET); + CASE(CLASS_CARGO); + CASE(CLASS_MISSILE); + CASE(CLASS_ROCK); + CASE(CLASS_MINE); + CASE(CLASS_THARGOID); + CASE(CLASS_BUOY); + CASE(CLASS_WORMHOLE); + CASE(CLASS_PLAYER); + CASE(CLASS_POLICE); + CASE(CLASS_MILITARY); + } + + return @"UNDEFINED"; +} + + +OOScanClass StringToScanClass(NSString *string) +{ + REVERSE_CASE(CLASS_NOT_SET); + REVERSE_CASE(CLASS_NO_DRAW); + REVERSE_CASE(CLASS_NEUTRAL); + REVERSE_CASE(CLASS_STATION); + REVERSE_CASE(CLASS_TARGET); + REVERSE_CASE(CLASS_CARGO); + REVERSE_CASE(CLASS_MISSILE); + REVERSE_CASE(CLASS_ROCK); + REVERSE_CASE(CLASS_MINE); + REVERSE_CASE(CLASS_THARGOID); + REVERSE_CASE(CLASS_BUOY); + REVERSE_CASE(CLASS_WORMHOLE); + REVERSE_CASE(CLASS_PLAYER); + REVERSE_CASE(CLASS_POLICE); + REVERSE_CASE(CLASS_MILITARY); + + return CLASS_NOT_SET; +} + + +NSString *InstinctToString(OOInstinctID instinct) +{ + switch (instinct) + { + CASE(INSTINCT_ATTACK_PREY); + CASE(INSTINCT_AVOID_PREDATORS); + CASE(INSTINCT_AVOID_HAZARDS); + CASE(INSTINCT_FIGHT_OR_FLIGHT); + CASE(INSTINCT_FLOCK_ALIKE); + CASE(INSTINCT_FOLLOW_AI); + CASE(INSTINCT_NULL); + } + + return @"INSTINCT_UNKNOWN"; +} + + +OOInstinctID StringToInstinct(NSString *string) +{ + REVERSE_CASE(INSTINCT_ATTACK_PREY); + REVERSE_CASE(INSTINCT_AVOID_PREDATORS); + REVERSE_CASE(INSTINCT_AVOID_HAZARDS); + REVERSE_CASE(INSTINCT_FIGHT_OR_FLIGHT); + REVERSE_CASE(INSTINCT_FLOCK_ALIKE); + REVERSE_CASE(INSTINCT_FOLLOW_AI); + + return INSTINCT_NULL; +} + + +NSString *BehaviourToString(OOBehaviour behaviour) +{ + switch (behaviour) + { + CASE(BEHAVIOUR_IDLE); + CASE(BEHAVIOUR_TRACK_TARGET); + // CASE(BEHAVIOUR_FLY_TO_TARGET); + // CASE(BEHAVIOUR_HANDS_OFF); + CASE(BEHAVIOUR_TUMBLE); + CASE(BEHAVIOUR_STOP_STILL); + CASE(BEHAVIOUR_STATION_KEEPING); + CASE(BEHAVIOUR_ATTACK_TARGET); + CASE(BEHAVIOUR_ATTACK_FLY_TO_TARGET); + CASE(BEHAVIOUR_ATTACK_FLY_FROM_TARGET); + CASE(BEHAVIOUR_RUNNING_DEFENSE); + CASE(BEHAVIOUR_FLEE_TARGET); + CASE(BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX); + CASE(BEHAVIOUR_ATTACK_MINING_TARGET); + CASE(BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE); + CASE(BEHAVIOUR_AVOID_COLLISION); + CASE(BEHAVIOUR_TRACK_AS_TURRET); + CASE(BEHAVIOUR_FLY_RANGE_FROM_DESTINATION); + CASE(BEHAVIOUR_FLY_TO_DESTINATION); + CASE(BEHAVIOUR_FLY_FROM_DESTINATION); + CASE(BEHAVIOUR_FACE_DESTINATION); + CASE(BEHAVIOUR_COLLECT_TARGET); + CASE(BEHAVIOUR_INTERCEPT_TARGET); + // CASE(BEHAVIOUR_MISSILE_FLY_TO_TARGET); + CASE(BEHAVIOUR_FORMATION_FORM_UP); + CASE(BEHAVIOUR_FORMATION_BREAK); + CASE(BEHAVIOUR_ENERGY_BOMB_COUNTDOWN); + CASE(BEHAVIOUR_TRACTORED); + CASE(BEHAVIOUR_FLY_THRU_NAVPOINTS); + } + + return @"** BEHAVIOUR UNKNOWN **"; +} + + +NSString *GovernmentToString(OOGovernmentID government) +{ + NSArray *strings = nil; + NSString *value = nil; + + strings = [[UNIVERSE descriptions] objectForKey:@"government"]; + if ([strings isKindOfClass:[NSArray class]] && government < [strings count]) + { + value = [strings objectAtIndex:government]; + if ([value isKindOfClass:[NSString class]]) return value; + } + + return nil; +} + + +NSString *EconomyToString(OOEconomyID economy) +{ + NSArray *strings = nil; + NSString *value = nil; + + strings = [[UNIVERSE descriptions] objectForKey:@"economy"]; + if ([strings isKindOfClass:[NSArray class]] && economy < [strings count]) + { + value = [strings objectAtIndex:economy]; + if ([value isKindOfClass:[NSString class]]) return value; + } + + return nil; +} + + +NSString *JSTypeToString(int /* JSType */ type) +{ + switch ((JSType)type) + { + CASE(JSTYPE_VOID); + CASE(JSTYPE_OBJECT); + CASE(JSTYPE_FUNCTION); + CASE(JSTYPE_STRING); + CASE(JSTYPE_NUMBER); + CASE(JSTYPE_BOOLEAN); + CASE(JSTYPE_NULL); + CASE(JSTYPE_XML); + CASE(JSTYPE_LIMIT); + } + return [NSString stringWithFormat:@"unknown (%u)", type]; +} + + +NSString *WeaponTypeToString(OOWeaponType weapon) +{ + switch (weapon) + { + CASE(WEAPON_NONE); + CASE(WEAPON_PLASMA_CANNON); + CASE(WEAPON_PULSE_LASER); + CASE(WEAPON_BEAM_LASER); + CASE(WEAPON_MINING_LASER); + CASE(WEAPON_MILITARY_LASER); + CASE(WEAPON_THARGOID_LASER); + } + return @"Unknown weapon"; +} + + +OOWeaponType StringToWeaponType(NSString *string) +{ + REVERSE_CASE(WEAPON_PLASMA_CANNON); + REVERSE_CASE(WEAPON_PULSE_LASER); + REVERSE_CASE(WEAPON_BEAM_LASER); + REVERSE_CASE(WEAPON_MINING_LASER); + REVERSE_CASE(WEAPON_MILITARY_LASER); + REVERSE_CASE(WEAPON_THARGOID_LASER); + + return WEAPON_NONE; +} + + +NSString *WeaponTypeToEquipmentString(OOWeaponType weapon) +{ +#define EQ_CASE(foo) case foo: return @"EQ_"#foo; + + switch (weapon) + { +// EQ_CASE(WEAPON_PLASMA_CANNON); + EQ_CASE(WEAPON_PULSE_LASER); + EQ_CASE(WEAPON_BEAM_LASER); + EQ_CASE(WEAPON_MINING_LASER); + EQ_CASE(WEAPON_MILITARY_LASER); + EQ_CASE(WEAPON_THARGOID_LASER); + + case WEAPON_PLASMA_CANNON: + case WEAPON_NONE: + break; + } + return nil; +#undef EQ_CASE +} + + +OOWeaponType EquipmentStringToWeaponTypeSloppy(NSString *string) +{ +#define EQ_REVERSE_CASE(foo) if ([string hasSuffix:@#foo]) return WEAPON_##foo; +// EQ_REVERSE_CASE(PLASMA_CANNON); + EQ_REVERSE_CASE(PULSE_LASER); + EQ_REVERSE_CASE(BEAM_LASER); + EQ_REVERSE_CASE(MINING_LASER); + EQ_REVERSE_CASE(MILITARY_LASER); + EQ_REVERSE_CASE(THARGOID_LASER); + + return WEAPON_NONE; +#undef EQ_REVERSE_CASE +} + + +OOWeaponType EquipmentStringToWeaponTypeStrict(NSString *string) +{ +#define EQ_REVERSE_CASE(foo) if ([string isEqual:@"EQ_WEAPON_" #foo]) return WEAPON_##foo; +// EQ_REVERSE_CASE(PLASMA_CANNON); + EQ_REVERSE_CASE(PULSE_LASER); + EQ_REVERSE_CASE(BEAM_LASER); + EQ_REVERSE_CASE(MINING_LASER); + EQ_REVERSE_CASE(MILITARY_LASER); + EQ_REVERSE_CASE(THARGOID_LASER); + + return WEAPON_NONE; +#undef EQ_REVERSE_CASE +} + + +NSString *CargoTypeToString(OOCargoType cargo) +{ + switch (cargo) + { + CASE(CARGO_NOT_CARGO); + CASE(CARGO_SLAVES); + CASE(CARGO_ALLOY); + CASE(CARGO_MINERALS); + CASE(CARGO_THARGOID); + CASE(CARGO_RANDOM); + CASE(CARGO_SCRIPTED_ITEM); + CASE(CARGO_CHARACTER); + + case CARGO_UNDEFINED: + break; + } + return @"Unknown cargo"; +} + + +OOCargoType StringToCargoType(NSString *string) +{ + REVERSE_CASE(CARGO_NOT_CARGO); + REVERSE_CASE(CARGO_SLAVES); + REVERSE_CASE(CARGO_ALLOY); + REVERSE_CASE(CARGO_MINERALS); + REVERSE_CASE(CARGO_THARGOID); + REVERSE_CASE(CARGO_RANDOM); + REVERSE_CASE(CARGO_SCRIPTED_ITEM); + REVERSE_CASE(CARGO_CHARACTER); + + // Backwards compatibility. + if ([string isEqual:@"CARGO_CARRIED"]) return CARGO_RANDOM; + + return CARGO_UNDEFINED; +} + + +NSString *EnergyUnitTypeToString(OOEnergyUnitType unit) +{ + switch (unit) + { + CASE(ENERGY_UNIT_NONE); + CASE(ENERGY_UNIT_NORMAL); + CASE(ENERGY_UNIT_NAVAL); + CASE(ENERGY_UNIT_NORMAL_DAMAGED); + CASE(ENERGY_UNIT_NAVAL_DAMAGED); + default: + return @"Unsupported energy unit"; + } + + return @"Unknown energy unit"; +} + + +OOEnergyUnitType StringToEnergyUnitType(NSString *string) +{ + REVERSE_CASE(ENERGY_UNIT_NONE); + REVERSE_CASE(ENERGY_UNIT_NORMAL); + REVERSE_CASE(ENERGY_UNIT_NAVAL); + REVERSE_CASE(ENERGY_UNIT_NORMAL_DAMAGED); + REVERSE_CASE(ENERGY_UNIT_NAVAL_DAMAGED); + + return ENERGY_UNIT_NONE; +} + + +NSString *GUIScreenIDToString(OOGUIScreenID screen) +{ + switch (screen) + { + CASE(GUI_SCREEN_MAIN); + CASE(GUI_SCREEN_INTRO1); + CASE(GUI_SCREEN_INTRO2); + CASE(GUI_SCREEN_STATUS); + CASE(GUI_SCREEN_MANIFEST); + CASE(GUI_SCREEN_EQUIP_SHIP); + CASE(GUI_SCREEN_SHIPYARD); + CASE(GUI_SCREEN_LONG_RANGE_CHART); + CASE(GUI_SCREEN_SHORT_RANGE_CHART); + CASE(GUI_SCREEN_SYSTEM_DATA); + CASE(GUI_SCREEN_MARKET); + CASE(GUI_SCREEN_CONTRACTS); + CASE(GUI_SCREEN_OPTIONS); + CASE(GUI_SCREEN_GAMEOPTIONS); + CASE(GUI_SCREEN_LOAD); + CASE(GUI_SCREEN_SAVE); + CASE(GUI_SCREEN_SAVE_OVERWRITE); + CASE(GUI_SCREEN_STICKMAPPER); + CASE(GUI_SCREEN_MISSION); + CASE(GUI_SCREEN_REPORT); + } + + return @"UNDEFINED"; +} + + +OOEnergyUnitType StringToGUIScreenID(NSString *string) +{ + REVERSE_CASE(GUI_SCREEN_MAIN); + REVERSE_CASE(GUI_SCREEN_INTRO1); + REVERSE_CASE(GUI_SCREEN_INTRO2); + REVERSE_CASE(GUI_SCREEN_STATUS); + REVERSE_CASE(GUI_SCREEN_MANIFEST); + REVERSE_CASE(GUI_SCREEN_EQUIP_SHIP); + REVERSE_CASE(GUI_SCREEN_SHIPYARD); + REVERSE_CASE(GUI_SCREEN_LONG_RANGE_CHART); + REVERSE_CASE(GUI_SCREEN_SHORT_RANGE_CHART); + REVERSE_CASE(GUI_SCREEN_SYSTEM_DATA); + REVERSE_CASE(GUI_SCREEN_MARKET); + REVERSE_CASE(GUI_SCREEN_CONTRACTS); + REVERSE_CASE(GUI_SCREEN_OPTIONS); + REVERSE_CASE(GUI_SCREEN_LOAD); + REVERSE_CASE(GUI_SCREEN_SAVE); + REVERSE_CASE(GUI_SCREEN_SAVE_OVERWRITE); + REVERSE_CASE(GUI_SCREEN_STICKMAPPER); + REVERSE_CASE(GUI_SCREEN_MISSION); + REVERSE_CASE(GUI_SCREEN_REPORT); + + return GUI_SCREEN_MAIN; +} + + +OOGalacticHyperspaceBehaviour StringToGalacticHyperspaceBehaviour(NSString *string) +{ + if ([string isEqualToString:@"BEHAVIOUR_STANDARD"] || [string isEqualToString:@"BEHAVIOR_STANDARD"]) + { + return GALACTIC_HYPERSPACE_BEHAVIOUR_STANDARD; + } + if ([string isEqualToString:@"BEHAVIOUR_ALL_SYSTEMS_REACHABLE"] || [string isEqualToString:@"BEHAVIOR_ALL_SYSTEMS_REACHABLE"]) + { + return GALACTIC_HYPERSPACE_BEHAVIOUR_ALL_SYSTEMS_REACHABLE; + } + if ([string isEqualToString:@"BEHAVIOUR_FIXED_COORDINATES"] || [string isEqualToString:@"BEHAVIOR_FIXED_COORDINATES"]) + { + return GALACTIC_HYPERSPACE_BEHAVIOUR_FIXED_COORDINATES; + } + + return GALACTIC_HYPERSPACE_BEHAVIOUR_UNKNOWN; +} + + +NSString *GalacticHyperspaceBehaviourToString(OOGalacticHyperspaceBehaviour behaviour) +{ + switch (behaviour) + { + case GALACTIC_HYPERSPACE_BEHAVIOUR_STANDARD: + return @"BEHAVIOUR_STANDARD"; + + case GALACTIC_HYPERSPACE_BEHAVIOUR_ALL_SYSTEMS_REACHABLE: + return @"BEHAVIOUR_ALL_SYSTEMS_REACHABLE"; + + case GALACTIC_HYPERSPACE_BEHAVIOUR_FIXED_COORDINATES: + return @"BEHAVIOUR_FIXED_COORDINATES"; + + case GALACTIC_HYPERSPACE_BEHAVIOUR_UNKNOWN: + break; + } + + return @"UNDEFINED"; +} + + +NSString *KillCountToRatingString(unsigned kills) +{ + enum { kRatingCount = 9 }; + + NSArray *ratingNames = nil; + const unsigned killThresholds[kRatingCount - 1] = + { + 0x0008, + 0x0010, + 0x0020, + 0x0040, + 0x0080, + 0x0200, + 0x0A00, + 0x1900 + }; + unsigned i; + + ratingNames = [[UNIVERSE descriptions] arrayForKey:@"rating"]; + for (i = 0; i < kRatingCount - 1; ++i) + { + if (kills < killThresholds[i]) return [ratingNames stringAtIndex:i]; + } + + return [ratingNames stringAtIndex:kRatingCount - 1]; +} + + +NSString *KillCountToRatingAndKillString(unsigned kills) +{ + return [NSString stringWithFormat:@"%@ (%u)", KillCountToRatingString(kills), kills]; +} + + +NSString *LegalStatusToString(int legalStatus) +{ + enum { kStatusCount = 3 }; + + NSArray *statusNames = nil; + const int statusThresholds[kStatusCount - 1] = + { + 1, + 51 + }; + unsigned i; + + statusNames = [[UNIVERSE descriptions] arrayForKey:@"legal_status"]; + for (i = 0; i != kStatusCount - 1; ++i) + { + if (legalStatus < statusThresholds[i]) return [statusNames stringAtIndex:i]; + } + + return [statusNames stringAtIndex:kStatusCount - 1]; +} + + +NSString *AlertConditionToString(OOAlertCondition alertCondition) +{ + NSArray *conditionNames = [[UNIVERSE descriptions] arrayForKey:@"condition"]; + return [conditionNames stringAtIndex:alertCondition]; +} + + +NSString *ShaderSettingToDisplayString(OOShaderSetting setting) +{ + switch (setting) + { + case SHADERS_NOT_SUPPORTED: return DESC(@"shaderfx-not-available"); + case SHADERS_OFF: return DESC(@"shaderfx-off"); + case SHADERS_SIMPLE: return DESC(@"shaderfx-simple"); + case SHADERS_FULL: return DESC(@"shaderfx-full"); + } + + return @"??"; +} + + +NSString *CommodityDisplayNameForSymbolicName(NSString *symbolicName) +{ + NSString *key = [@"commodity-name " stringByAppendingString:[symbolicName lowercaseString]]; + return [UNIVERSE descriptionForKey:key]; +} + + +NSString *CommodityDisplayNameForCommodityArray(NSArray *commodityDefinition) +{ + return CommodityDisplayNameForSymbolicName([commodityDefinition stringAtIndex:MARKET_NAME]); +} + + +NSString *DisplayStringForMassUnit(OOMassUnit unit) +{ + switch (unit) + { + case UNITS_TONS: return DESC(@"cargo-tons-symbol"); + case UNITS_KILOGRAMS: return DESC(@"cargo-kilograms-symbol"); + case UNITS_GRAMS: return DESC(@"cargo-grams-symbol"); + } + + return @"??"; +} + + +NSString *DisplayStringForMassUnitForCommodity(OOCargoType commodity) +{ + return DisplayStringForMassUnit([UNIVERSE unitsForCommodity:commodity]); +} + +#if DOCKING_CLEARANCE_ENABLED +NSString *DockingClearanceStatusToString(OODockingClearanceStatus dockingClearanceStatus) +{ + switch (dockingClearanceStatus) + { + CASE(DOCKING_CLEARANCE_STATUS_NONE); + CASE(DOCKING_CLEARANCE_STATUS_REQUESTED); + CASE(DOCKING_CLEARANCE_STATUS_NOT_REQUIRED); + CASE(DOCKING_CLEARANCE_STATUS_GRANTED); + CASE(DOCKING_CLEARANCE_STATUS_TIMING_OUT); + } + + return @"DOCKING_CLEARANCE_STATUS_UNKNOWN"; +} +#endif diff --git a/src/Core/OOConvertSystemDescriptions.h b/src/Core/OOConvertSystemDescriptions.h new file mode 100644 index 00000000..c526893d --- /dev/null +++ b/src/Core/OOConvertSystemDescriptions.h @@ -0,0 +1,26 @@ +#import + + +/* Functions to convert system descriptions between dictionary and array + formats. + + The array format is used in descriptions.plist. Each set of strings is an + array, the sets are stored in a master array, and cross-references are + indices into the master array. + + The dictionary format is more human-friendly, with string sets identified + by name and cross-references using names. + + The indicesToKeys parameter is optional; if it is nil or incomplete, + indices or (index-based) keys will be synthesized. +*/ +NSArray *OOConvertSystemDescriptionsToArrayFormat(NSDictionary *descriptionsInDictionaryFormat, NSDictionary *indicesToKeys); +NSDictionary *OOConvertSystemDescriptionsToDictionaryFormat(NSArray *descriptionsInArrayFormat, NSDictionary *indicesToKeys); + +NSString *OOStringifySystemDescriptionLine(NSString *line, NSDictionary *indicesToKeys, BOOL useFallback); + +// Higher-level functions to drive the entire conversion. +void CompileSystemDescriptions(BOOL asXML); +void ExportSystemDescriptions(BOOL asXML); + + diff --git a/src/Core/OOConvertSystemDescriptions.m b/src/Core/OOConvertSystemDescriptions.m new file mode 100644 index 00000000..dbc786ad --- /dev/null +++ b/src/Core/OOConvertSystemDescriptions.m @@ -0,0 +1,372 @@ +// +// OOConvertSystemDescriptions.m +// systemdescriptiontest +// +// Created by Jens Ayton on 2008-12-14. +// Copyright 2008 Jens Ayton. All rights reserved. +// + +#import "OOCocoa.h" +#import "OOConvertSystemDescriptions.h" +#import "OldSchoolPropertyListWriting.h" +#import "OOCollectionExtractors.h" +#import "ResourceManager.h" +#import "Universe.h" + + +static NSMutableDictionary *InitKeyToIndexDict(NSDictionary *dict, NSMutableSet **outUsedIndices); +static NSString *IndexToKey(OOUInteger index, NSDictionary *indicesToKeys, BOOL useFallback); +static NSArray *ConvertIndicesToKeys(NSArray *entry, NSDictionary *indicesToKeys); +static NSNumber *KeyToIndex(NSString *key, NSMutableDictionary *ioKeysToIndices, NSMutableSet *ioUsedIndicies, OOUInteger *ioSlotCache); +static NSArray *ConvertKeysToIndices(NSArray *entry, NSMutableDictionary *ioKeysToIndices, NSMutableSet *ioUsedIndicies, OOUInteger *ioSlotCache); +static OOUInteger HighestIndex(NSMutableDictionary *sparseArray); // Actually returns highest index + 1, which is fine. + + +void CompileSystemDescriptions(BOOL asXML) +{ + NSDictionary *sysDescDict = nil; + NSArray *sysDescArray = nil; + NSDictionary *keyMap = nil; + NSData *data = nil; + NSString *error = nil; + + sysDescDict = [ResourceManager dictionaryFromFilesNamed:@"sysdesc.plist" + inFolder:@"Config" + andMerge:NO]; + if (sysDescDict == nil) + { + OOLog(@"sysdesc.compile.failed.fileNotFound", @"Could not load a dictionary from sysdesc.plist, ignoring --compile-sysdesc option."); + return; + } + + keyMap = [ResourceManager dictionaryFromFilesNamed:@"sysdesc_key_table.plist" + inFolder:@"Config" + andMerge:NO]; + // keyMap is optional, so no nil check + + sysDescArray = OOConvertSystemDescriptionsToArrayFormat(sysDescDict, keyMap); + if (sysDescArray == nil) + { + OOLog(@"sysdesc.compile.failed.conversion", @"Could not convert sysdesc.plist to descriptions.plist format for some reason."); + return; + } + + sysDescDict = [NSDictionary dictionaryWithObject:sysDescArray forKey:@"system_description"]; + + if (asXML) + { + data = [NSPropertyListSerialization dataFromPropertyList:sysDescDict + format:NSPropertyListXMLFormat_v1_0 + errorDescription:&error]; + } + else + { + data = [sysDescDict oldSchoolPListFormatWithErrorDescription:&error]; + } + + if (data == nil) + { + OOLog(@"sysdesc.compile.failed.XML", @"Could not convert translated sysdesc.plist to property list: %@.", error); + return; + } + + if ([ResourceManager writeDiagnosticData:data toFileNamed:@"sysdesc-compiled.plist"]) + { + OOLog(@"sysdesc.compile.success", @"Wrote translated sysdesc.plist to sysdesc-compiled.plist."); + } + else + { + OOLog(@"sysdesc.compile.failed.writeFailure", @"Could not write translated sysdesc.plist to sysdesc-compiled.plist."); + } +} + + +void ExportSystemDescriptions(BOOL asXML) +{ + NSArray *sysDescArray = nil; + NSDictionary *sysDescDict = nil; + NSDictionary *keyMap = nil; + NSData *data = nil; + NSString *error = nil; + + sysDescArray = [[UNIVERSE descriptions] arrayForKey:@"system_description"]; + + keyMap = [ResourceManager dictionaryFromFilesNamed:@"sysdesc_key_table.plist" + inFolder:@"Config" + andMerge:NO]; + // keyMap is optional, so no nil check + + sysDescDict = OOConvertSystemDescriptionsToDictionaryFormat(sysDescArray, keyMap); + if (sysDescArray == nil) + { + OOLog(@"sysdesc.export.failed.conversion", @"Could not convert system_description do sysdesc.plist format for some reason."); + return; + } + + if (asXML) + { + data = [NSPropertyListSerialization dataFromPropertyList:sysDescDict + format:NSPropertyListXMLFormat_v1_0 + errorDescription:&error]; + } + else + { + data = [sysDescDict oldSchoolPListFormatWithErrorDescription:&error]; + } + + if (data == nil) + { + OOLog(@"sysdesc.export.failed.XML", @"Could not convert translated system_description to XML property list: %@.", error); + return; + } + + if ([ResourceManager writeDiagnosticData:data toFileNamed:@"sysdesc.plist"]) + { + OOLog(@"sysdesc.export.success", @"Wrote translated system_description to sysdesc.plist."); + } + else + { + OOLog(@"sysdesc.export.failed.writeFailure", @"Could not write translated system_description to sysdesc.plist."); + } +} + + +NSArray *OOConvertSystemDescriptionsToArrayFormat(NSDictionary *descriptionsInDictionaryFormat, NSDictionary *indicesToKeys) +{ + NSMutableDictionary *result = nil; + NSAutoreleasePool *pool = nil; + NSString *key = nil; + NSArray *entry = nil; + NSEnumerator *keyEnum = nil; + NSMutableDictionary *keysToIndices = nil; + NSMutableSet *usedIndices = nil; + OOUInteger slotCache = 0; + NSNumber *index = nil; + OOUInteger i, count; + NSMutableArray *realResult = nil; + + pool = [[NSAutoreleasePool alloc] init]; + + // Use a dictionary as a sparse array. + result = [NSMutableDictionary dictionaryWithCapacity:[descriptionsInDictionaryFormat count]]; + + keysToIndices = InitKeyToIndexDict(indicesToKeys, &usedIndices); + + for (keyEnum = [descriptionsInDictionaryFormat keyEnumerator]; (key = [keyEnum nextObject]); ) + { + entry = ConvertKeysToIndices([descriptionsInDictionaryFormat objectForKey:key], keysToIndices, usedIndices, &slotCache); + index = KeyToIndex(key, keysToIndices, usedIndices, &slotCache); + + [result setObject:entry forKey:index]; + } + + count = HighestIndex(result); + realResult = [NSMutableArray arrayWithCapacity:count]; + for (i = 0; i < count; i++) + { + entry = [result objectForKey:[NSNumber numberWithUnsignedInt:i]]; + if (entry == nil) entry = [NSArray array]; + [realResult addObject:entry]; + } + + [realResult retain]; + [pool release]; + return [realResult autorelease]; +} + + +NSDictionary *OOConvertSystemDescriptionsToDictionaryFormat(NSArray *descriptionsInArrayFormat, NSDictionary *indicesToKeys) +{ + NSMutableDictionary *result = nil; + NSAutoreleasePool *pool = nil; + NSArray *entry = nil; + NSEnumerator *entryEnum = nil; + NSString *key = nil; + OOUInteger i = 0; + + result = [NSMutableDictionary dictionaryWithCapacity:[descriptionsInArrayFormat count]]; + pool = [[NSAutoreleasePool alloc] init]; + + for (entryEnum = [descriptionsInArrayFormat objectEnumerator]; (entry = [entryEnum nextObject]); ) + { + entry = ConvertIndicesToKeys(entry, indicesToKeys); + key = IndexToKey(i, indicesToKeys, YES); + ++i; + + [result setObject:entry forKey:key]; + } + + [pool release]; + return result; +} + + +NSString *OOStringifySystemDescriptionLine(NSString *line, NSDictionary *indicesToKeys, BOOL useFallback) +{ + OOUInteger p1, p2; + NSRange searchRange; + NSString *before = nil, *after = nil, *middle = nil; + NSString *key = nil; + + searchRange.location = 0; + searchRange.length = [line length]; + + while ([line rangeOfString:@"[" options:NSLiteralSearch range:searchRange].location != NSNotFound) + { + p1 = [line rangeOfString:@"[" options:NSLiteralSearch range:searchRange].location; + p2 = [line rangeOfString:@"]" options:NSLiteralSearch range:searchRange].location + 1; + + before = [line substringWithRange:NSMakeRange(0, p1)]; + after = [line substringWithRange:NSMakeRange(p2,[line length] - p2)]; + middle = [line substringWithRange:NSMakeRange(p1 + 1 , p2 - p1 - 2)]; + + if ([[middle stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"0123456789"]] isEqual:@""] && ![middle isEqual:@""]) + { + // Found [] around integers only + key = IndexToKey([middle intValue], indicesToKeys, useFallback); + if (key != nil) + { + line = [NSString stringWithFormat:@"%@[#%@]%@", before, key, after]; + } + } + + searchRange.length -= p2 - searchRange.location; + searchRange.location = [line length] - searchRange.length; + } + return line; +} + + +static NSMutableDictionary *InitKeyToIndexDict(NSDictionary *dict, NSMutableSet **outUsedIndices) +{ + NSEnumerator *keyEnum = nil; + NSString *key = nil; + NSNumber *number = nil; + NSMutableDictionary *result = nil; + NSMutableSet *used = nil; + + assert(outUsedIndices != NULL); + + result = [NSMutableDictionary dictionaryWithCapacity:[dict count]]; + used = [NSMutableSet setWithCapacity:[dict count]]; + + for (keyEnum = [dict keyEnumerator]; (key = [keyEnum nextObject]); ) + { + // Convert keys of dict to array indices + number = [NSNumber numberWithInt:[key intValue]]; + [result setObject:number forKey:[dict objectForKey:key]]; + [used addObject:number]; + } + + *outUsedIndices = used; + return result; +} + + +static NSString *IndexToKey(OOUInteger index, NSDictionary *indicesToKeys, BOOL useFallback) +{ + NSString *result = [indicesToKeys objectForKey:[NSString stringWithFormat:@"%u", index]]; + if (result == nil && useFallback) result = [NSString stringWithFormat:@"block_%u", index]; + + return result; +} + + +static NSArray *ConvertIndicesToKeys(NSArray *entry, NSDictionary *indicesToKeys) +{ + NSEnumerator *lineEnum = nil; + NSString *line = nil; + NSMutableArray *result = nil; + + result = [NSMutableArray arrayWithCapacity:[entry count]]; + + for (lineEnum = [entry objectEnumerator]; (line = [lineEnum nextObject]); ) + { + [result addObject:OOStringifySystemDescriptionLine(line, indicesToKeys, YES)]; + } + + return result; +} + + +static NSNumber *KeyToIndex(NSString *key, NSMutableDictionary *ioKeysToIndices, NSMutableSet *ioUsedIndicies, OOUInteger *ioSlotCache) +{ + NSNumber *result = nil; + + assert(ioSlotCache != NULL); + + result = [ioKeysToIndices objectForKey:key]; + if (result == nil) + { + // Search for free index + do + { + result = [NSNumber numberWithUnsignedInt:(*ioSlotCache)++]; + } + while ([ioUsedIndicies containsObject:result]); + + [ioKeysToIndices setObject:result forKey:key]; + [ioUsedIndicies addObject:result]; + OOLog(@"sysdesc.compile.unknownKey", @"Assigning key \"%@\" to index %@.", key, result); + } + + return result; +} + + +static NSArray *ConvertKeysToIndices(NSArray *entry, NSMutableDictionary *ioKeysToIndices, NSMutableSet *ioUsedIndicies, OOUInteger *ioSlotCache) +{ + NSEnumerator *lineEnum = nil; + NSString *line = nil; + OOUInteger p1, p2; + NSRange searchRange; + NSMutableArray *result = nil; + NSString *before = nil, *after = nil, *middle = nil; + + result = [NSMutableArray arrayWithCapacity:[entry count]]; + + for (lineEnum = [entry objectEnumerator]; (line = [lineEnum nextObject]); ) + { + searchRange.location = 0; + searchRange.length = [line length]; + + while ([line rangeOfString:@"[" options:NSLiteralSearch range:searchRange].location != NSNotFound) + { + p1 = [line rangeOfString:@"[" options:NSLiteralSearch range:searchRange].location; + p2 = [line rangeOfString:@"]" options:NSLiteralSearch range:searchRange].location + 1; + + before = [line substringWithRange:NSMakeRange(0, p1)]; + after = [line substringWithRange:NSMakeRange(p2,[line length] - p2)]; + middle = [line substringWithRange:NSMakeRange(p1 + 1 , p2 - p1 - 2)]; + + if ([middle length] > 1 && [middle hasPrefix:@"#"]) + { + // Found [] around key + line = [NSString stringWithFormat:@"%@[%@]%@", before, KeyToIndex([middle substringFromIndex:1], ioKeysToIndices, ioUsedIndicies, ioSlotCache), after]; + } + + searchRange.length -= p2 - searchRange.location; + searchRange.location = [line length] - searchRange.length; + } + + [result addObject:line]; + } + + return result; +} + + +static OOUInteger HighestIndex(NSMutableDictionary *sparseArray) +{ + NSEnumerator *keyEnum = nil; + NSNumber *key = nil; + OOUInteger curr, highest = 0; + + for (keyEnum = [sparseArray keyEnumerator]; (key = [keyEnum nextObject]); ) + { + curr = [key intValue]; + if (highest < curr) highest = curr; + } + + return highest; +} diff --git a/src/Core/OOCrosshairs.h b/src/Core/OOCrosshairs.h new file mode 100644 index 00000000..be0ed2e3 --- /dev/null +++ b/src/Core/OOCrosshairs.h @@ -0,0 +1,29 @@ +// +// OOCrosshairs.h +// Oolite +// +// Created by Jens Ayton on 2008-12-16. +// Copyright 2008 Jens Ayton. All rights reserved. +// + +#import "OOCocoa.h" +#import "OOOpenGL.h" + +@class OOColor; + + +@interface OOCrosshairs: NSObject +{ +@private + unsigned _count; + GLfloat *_data; +} + +- (id) initWithPoints:(NSArray *)points + scale:(GLfloat)scale + color:(OOColor *)color + overallAlpha:(GLfloat)alpha; + +- (void) render; + +@end diff --git a/src/Core/OOCrosshairs.m b/src/Core/OOCrosshairs.m new file mode 100644 index 00000000..addf602e --- /dev/null +++ b/src/Core/OOCrosshairs.m @@ -0,0 +1,185 @@ +// +// OOCrosshairs.m +// Oolite +// +// Created by Jens Ayton on 2008-12-16. +// Copyright 2008 Jens Ayton. All rights reserved. +// + +#import "OOCrosshairs.h" +#import "OOColor.h" +#import "OOCollectionExtractors.h" +#import "Universe.h" +#import "MyOpenGLView.h" + + +@interface OOCrosshairs (Private) + +- (void) setUpDataWithPoints:(NSArray *)points + scale:(GLfloat)scale + color:(OOColor *)color + overallAlpha:(GLfloat)alpha; + +- (void) setUpDataForOnePoint:(NSArray *)pointInfo + scale:(GLfloat)scale + colorComps:(GLfloat[4])colorComps + overallAlpha:(GLfloat)alpha + data:(GLfloat *)ioBuffer; + +@end + + +@implementation OOCrosshairs + +- (id) initWithPoints:(NSArray *)points + scale:(GLfloat)scale + color:(OOColor *)color + overallAlpha:(GLfloat)alpha +{ + if ((self = [super init])) + { + if (alpha > 0.0f && (color == nil || [color alphaComponent] != 0.0f)) + { + [self setUpDataWithPoints:points scale:scale color:color overallAlpha:alpha]; + } + } + + return self; +} + + +- (void) dealloc +{ + free(_data); + + [super dealloc]; +} + + +- (void) render +{ + if (_data != NULL) + { + glPushAttrib(GL_ENABLE_BIT); + glDisable(GL_LIGHTING); + glDisable(GL_TEXTURE_2D); + glEnable(GL_LINE_SMOOTH); + glPushMatrix(); + glTranslatef(0, 0, [[UNIVERSE gameView] display_z]); + +#if 1 + glVertexPointer(2, GL_FLOAT, sizeof (GLfloat) * 6, _data); + glColorPointer(4, GL_FLOAT, sizeof (GLfloat) * 6, _data + 2); + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + + glDrawArrays(GL_LINES, 0, _count * 2); + + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_COLOR_ARRAY); +#else + unsigned i; + GLfloat *data = _data; + glBegin(GL_LINES); + for (i = 0; i < _count * 2; i++) + { + glColor4f(data[2], data[3], data[4], data[5]); + glVertex2f(data[0], data[1]); + data += 6; + } + glEnd(); +#endif + + glPopMatrix(); + glPopAttrib(); + } +} + + +- (void) setUpDataWithPoints:(NSArray *)points + scale:(GLfloat)scale + color:(OOColor *)color + overallAlpha:(GLfloat)alpha +{ + unsigned i; + GLfloat colorComps[4] = { 0.0f, 1.0f, 0.0f, 1.0f }; + GLfloat *data = NULL; + + _count = [points count]; + if (_count == 0) return; + + _data = malloc(sizeof (GLfloat) * 12 * _count); // 2 coordinates, 4 colour components for each endpoint of each line segment + [color getRed:&colorComps[0] green:&colorComps[1] blue:&colorComps[2] alpha:&colorComps[3]]; + + // Turn NSArray into GL-friendly element array + data = _data; + for (i = 0; i < _count; i++) + { + [self setUpDataForOnePoint:[points arrayAtIndex:i] + scale:scale + colorComps:colorComps + overallAlpha:alpha + data:data]; + data += 12; + } +} + + +- (void) setUpDataForOnePoint:(NSArray *)pointInfo + scale:(GLfloat)scale + colorComps:(GLfloat[4])colorComps + overallAlpha:(GLfloat)alpha + data:(GLfloat *)ioBuffer +{ + GLfloat x1, y1, a1, x2, y2, a2; + GLfloat r, g, b, a; + + if ([pointInfo count] >= 6) + { + a1 = [pointInfo floatAtIndex:0]; + x1 = [pointInfo floatAtIndex:1] * scale; + y1 = [pointInfo floatAtIndex:2] * scale; + a2 = [pointInfo floatAtIndex:3]; + x2 = [pointInfo floatAtIndex:4] * scale; + y2 = [pointInfo floatAtIndex:5] * scale; + r = colorComps[0]; + g = colorComps[1]; + b = colorComps[2]; + a = colorComps[3]; + + /* a1/a2 * a is hud.plist and crosshairs.plist - specified alpha, + which must be clamped to 0..1 so the plist-specified alpha can't + "escape" the overall alpha range. The result of scaling this by + overall HUD alpha is then clamped again for robustness. + */ + a1 = OOClamp_0_1_f(OOClamp_0_1_f(a1 * a) * alpha); + a2 = OOClamp_0_1_f(OOClamp_0_1_f(a2 * a) * alpha); + } + else + { + // Bad entry, write red point in middle. + x1 = -0.01f; + x2 = 0.01f; + y1 = y2 = 0.0f; + r = 1.0f; + g = b = 0.0f; + a1 = a2 = 1.0; + } + + *ioBuffer++ = x1; + *ioBuffer++ = y1; + *ioBuffer++ = r; + *ioBuffer++ = g; + *ioBuffer++ = b; + *ioBuffer++ = a1; + + *ioBuffer++ = x2; + *ioBuffer++ = y2; + *ioBuffer++ = r; + *ioBuffer++ = g; + *ioBuffer++ = b; + *ioBuffer++ = a2; +} + +@end diff --git a/src/Core/OODebugGLDrawing.h b/src/Core/OODebugGLDrawing.h new file mode 100644 index 00000000..a1fdaaec --- /dev/null +++ b/src/Core/OODebugGLDrawing.h @@ -0,0 +1,182 @@ +/* + +OODebugDrawing.h + +A set of functions for drawing debug stuff like bounding boxes. These are +drawn in wireframe without Z buffer reads or writes. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOMaths.h" +#import "OOColor.h" + + +#if !defined(OODEBUGLDRAWING_DISABLE) && defined(NDEBUG) +#define OODEBUGLDRAWING_DISABLE 1 +#endif + + +#ifndef OODEBUGLDRAWING_DISABLE + +@class OOMaterial; + +typedef struct +{ + OOMaterial *material; +} OODebugWFState; + + +OODebugWFState OODebugBeginWireframe(BOOL ignoreZ); +void OODebugEndWireframe(OODebugWFState state); + +OOINLINE void OODebugDrawBoundingBox(BoundingBox box); +OOINLINE void OODebugDrawBoundingBoxBetween(Vector min, Vector max); +OOINLINE void OODebugDrawColoredBoundingBox(BoundingBox box, OOColor *color); +void OODebugDrawColoredBoundingBoxBetween(Vector min, Vector max, OOColor *color); + +// Normals are drawn as cyan lines +OOINLINE void OODebugDrawNormal(Vector position, Vector normal, GLfloat scale); +OOINLINE void OODebugDrawNormalAtOrigin(Vector normal, GLfloat scale); + +// Other vectors are drawn as magenta lines by default +OOINLINE void OODebugDrawVector(Vector position, Vector v); +OOINLINE void OODebugDrawColoredVector(Vector position, Vector v, OOColor *color); +OOINLINE void OODebugDrawVectorAtOrigin(Vector v); +OOINLINE void OODebugDrawColoredVectorAtOrigin(Vector v, OOColor *color); + +// Lines are drawn white by default +OOINLINE void OODebugDrawLine(Vector start, Vector end); +void OODebugDrawColoredLine(Vector start, Vector end, OOColor *color); + +// Bases are drawn as one red, one green and one blue vector, representing x, y and z axes in the current coordinate frame. +void OODebugDrawBasis(Vector position, GLfloat scale); +OOINLINE void OODebugDrawBasisAtOrigin(GLfloat scale); + +void OODebugDrawPoint(Vector position, OOColor *color); + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE void OODebugDrawBoundingBoxBetween(Vector min, Vector max) +{ + OODebugDrawColoredBoundingBoxBetween(min, max, [OOColor blueColor]); +} + + +OOINLINE void OODebugDrawBoundingBox(BoundingBox box) +{ + OODebugDrawBoundingBoxBetween(box.min, box.max); +} + + +OOINLINE void OODebugDrawColoredBoundingBox(BoundingBox box, OOColor *color) +{ + OODebugDrawColoredBoundingBoxBetween(box.min, box.max, color); +} + + +OOINLINE void OODebugDrawNormal(Vector position, Vector normal, GLfloat scale) +{ + OODebugDrawColoredVector(position, vector_add(position, vector_multiply_scalar(normal, scale)), [OOColor cyanColor]); +} + + +OOINLINE void OODebugDrawNormalAtOrigin(Vector normal, GLfloat scale) +{ + OODebugDrawNormal(kZeroVector, normal, scale); +} + + +OOINLINE void OODebugDrawColoredVector(Vector position, Vector v, OOColor *color) +{ + OODebugDrawColoredLine(position, vector_add(position, v), color); +} + + +OOINLINE void OODebugDrawVector(Vector position, Vector v) +{ + OODebugDrawColoredVector(position, v, [OOColor magentaColor]); +} + + +OOINLINE void OODebugDrawVectorAtOrigin(Vector v) +{ + OODebugDrawVector(kZeroVector, v); +} + + +OOINLINE void OODebugDrawColoredVectorAtOrigin(Vector v, OOColor *color) +{ + OODebugDrawColoredVector(kZeroVector, v, color); +} + + +OOINLINE void OODebugDrawLine(Vector start, Vector end) +{ + OODebugDrawColoredLine(start, end, [OOColor whiteColor]); +} + + +OOINLINE void OODebugDrawBasisAtOrigin(GLfloat scale) +{ + OODebugDrawBasis(kZeroVector, scale); +} + +#else // OODEBUGLDRAWING_DISABLE + +#define OODRAW_NOOP do {} while (0) + +#define OODebugDrawBoundingBox(box) OODRAW_NOOP +#define OODebugDrawBoundingBoxBetween(min, max) OODRAW_NOOP +#define OODebugDrawNormal(position, normal, scale) OODRAW_NOOP +#define OODebugDrawNormalAtOrigin(normal, scale) OODRAW_NOOP +#define OODebugDrawVector(position, v) OODRAW_NOOP +#define OODebugDrawColoredVector(position, v, color) OODRAW_NOOP +#define OODebugDrawVectorAtOrigin(v) OODRAW_NOOP +#define OODebugDrawColoredVectorAtOrigin(v, color) OODRAW_NOOP +#define OODebugDrawBasis(position, scale) OODRAW_NOOP +#define OODebugDrawBasisAtOrigin(scale) OODRAW_NOOP + +#endif // OODEBUGLDRAWING_DISABLE diff --git a/src/Core/OODebugGLDrawing.m b/src/Core/OODebugGLDrawing.m new file mode 100644 index 00000000..fd70e29b --- /dev/null +++ b/src/Core/OODebugGLDrawing.m @@ -0,0 +1,190 @@ +/* + +OODebugDrawing.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OODebugGLDrawing.h" +#import "OOMacroOpenGL.h" +#import "OOMaterial.h" + + +#ifndef OODEBUGLDRAWING_DISABLE + +OOINLINE void ApplyColor(OOColor *color) +{ + GLfloat r, g, b, a; + + OO_ENTER_OPENGL(); + + if (EXPECT_NOT(color == nil)) color = [OOColor lightGrayColor]; + [color getGLRed:&r green:&g blue:&b alpha:&a]; + glColor4f(r, g, b, a); +} + + +void OODebugDrawColoredBoundingBoxBetween(Vector min, Vector max, OOColor *color) +{ + OODebugWFState state = OODebugBeginWireframe(YES); + OO_ENTER_OPENGL(); + + ApplyColor(color); + glBegin(GL_LINE_LOOP); + glVertex3f(min.x, min.y, min.z); + glVertex3f(max.x, min.y, min.z); + glVertex3f(max.x, max.y, min.z); + glVertex3f(min.x, max.y, min.z); + glVertex3f(min.x, max.y, max.z); + glVertex3f(max.x, max.y, max.z); + glVertex3f(max.x, min.y, max.z); + glVertex3f(min.x, min.y, max.z); + glEnd(); + glBegin(GL_LINES); + glVertex3f(max.x, min.y, min.z); + glVertex3f(max.x, min.y, max.z); + glVertex3f(max.x, max.y, min.z); + glVertex3f(max.x, max.y, max.z); + glVertex3f(min.x, min.y, min.z); + glVertex3f(min.x, max.y, min.z); + glVertex3f(min.x, min.y, max.z); + glVertex3f(min.x, max.y, max.z); + glEnd(); + + OODebugEndWireframe(state); +} + + +void OODebugDrawColoredLine(Vector start, Vector end, OOColor *color) +{ + OODebugWFState state = OODebugBeginWireframe(YES); + OO_ENTER_OPENGL(); + + ApplyColor(color); + + glBegin(GL_LINES); + glVertex3f(start.x, start.y, start.z); + glVertex3f(end.x, end.y, end.z); + glEnd(); + + OODebugEndWireframe(state); +} + + +void OODebugDrawBasis(Vector position, GLfloat scale) +{ + OODebugWFState state = OODebugBeginWireframe(YES); + OO_ENTER_OPENGL(); + + glBegin(GL_LINES); + glColor4f(1.0f, 0.0f, 0.0f, 1.0f); + glVertex3f(position.x, position.y, position.z); + glVertex3f(position.x + scale, position.y, position.z); + + glColor4f(0.0f, 1.0f, 0.0f, 1.0f); + glVertex3f(position.x, position.y, position.z); + glVertex3f(position.x, position.y + scale, position.z); + + glColor4f(0.0f, 0.0f, 1.0f, 1.0f); + glVertex3f(position.x, position.y, position.z); + glVertex3f(position.x, position.y, position.z + scale); + glEnd(); + + OODebugEndWireframe(state); +} + + +void OODebugDrawPoint(Vector position, OOColor *color) +{ + OODebugWFState state = OODebugBeginWireframe(YES); + OO_ENTER_OPENGL(); + + ApplyColor(color); + glPointSize(10); + + glBegin(GL_POINTS); + glVertex3f(position.x, position.y, position.z); + glEnd(); + + OODebugEndWireframe(state); +} + + +OODebugWFState OODebugBeginWireframe(BOOL ignoreZ) +{ + OO_ENTER_OPENGL(); + + OODebugWFState state = { material: [OOMaterial current] }; + [OOMaterial applyNone]; + + glPushAttrib(GL_ENABLE_BIT | GL_DEPTH_BUFFER_BIT | GL_LINE_BIT | GL_POINT_BIT | GL_CURRENT_BIT); + + glDisable(GL_LIGHTING); + glDisable(GL_TEXTURE_2D); + glDisable(GL_FOG); + if (ignoreZ) + { + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + } + else + { + glEnable(GL_DEPTH_TEST); + glDepthMask(GL_TRUE); + } + + glLineWidth(1.0f); + + return state; +} + + +void OODebugEndWireframe(OODebugWFState state) +{ + OO_ENTER_OPENGL(); + glPopAttrib(); + [state.material apply]; +} + +#endif diff --git a/src/Core/OODeepCopy.h b/src/Core/OODeepCopy.h new file mode 100644 index 00000000..dd4ed728 --- /dev/null +++ b/src/Core/OODeepCopy.h @@ -0,0 +1,79 @@ +/* + +OODeepCopy.h + +Informal protocol and utility function for making efficient deep copies of +immutable collections. + +It is implemented in such a way that all objects can be deep copied. Objects +that implement the NSCopying protocol are automatically copied, while others +are retained. The following special cases exist: + * NSStrings and NSValues (including NSNumbers) are uniqued - that is, the + resulting collection will only include one (immutable) copy of any string + or number. + * Arrays, sets and dictionaries deep copy their contents. + +For objects where the mutable/immutable distinction exists, the result should +be expected to be immutable. + +This self-optimizing behaviour is similar to that performed by binary plist +export. + +NOTE: in accordance with Cocoa coding conventions, methods and functions with +Copy in the name return objects owned by the receiver. + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2009 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +id OODeepCopy(id object); + + +@interface NSObject (OODeepCopy) + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects; + +@end diff --git a/src/Core/OODeepCopy.m b/src/Core/OODeepCopy.m new file mode 100644 index 00000000..49f2f92e --- /dev/null +++ b/src/Core/OODeepCopy.m @@ -0,0 +1,337 @@ +/* + +OODeepCopy.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2009 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OODeepCopy.h" + + +id OODeepCopy(id object) +{ + NSAutoreleasePool *pool = nil; + NSMutableSet *objects = nil; + + if (object == nil) return nil; + + pool = [[NSAutoreleasePool alloc] init]; + objects = [NSMutableSet set]; + + object = [object ooDeepCopyWithSharedObjects:objects]; + + [pool release]; + + return object; +} + + +@implementation NSObject (OODeepCopy) + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects +{ + if ([self conformsToProtocol:@protocol(NSCopying)]) + { + return [self copy]; + } + else + { + return [self retain]; + } +} + +@end + + +@implementation NSString (OODeepCopy) + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects +{ + if ([self length] == 0) return [[NSString string] retain]; + + id object = [objects member:self]; + if (object != nil && [object isKindOfClass:[NSString class]]) + { + return [object retain]; + } + else + { + object = [self copy]; + [objects addObject:object]; + return object; + } +} + +@end + + +@implementation NSValue (OODeepCopy) // Includes NSNumber + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects +{ + id object = [objects member:self]; + if (object != nil && [object isKindOfClass:[NSValue class]]) + { + return [object retain]; + } + else + { + object = [self copy]; + [objects addObject:object]; + return object; + } +} + +@end + + +@implementation NSArray (OODeepCopy) + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects +{ + OOUInteger i, count; + id *members = NULL; + NSArray *result = nil; + BOOL tempObjects = NO; + + count = [self count]; + if (count == 0) return [[NSArray array] retain]; + + members = malloc(sizeof *members * count); + if (members == NULL) + { + [NSException raise:NSMallocException format:@"Failed to allocate space for %lu objects in %s.", (unsigned long)count, __PRETTY_FUNCTION__]; + } + + // Ensure there's an objects set even if passed nil. + if (objects == nil) + { + objects = [[NSMutableSet alloc] init]; + tempObjects = YES; + } + + [self getObjects:members]; + NS_DURING + // Deep copy members. + for (i = 0; i < count; i++) + { + members[i] = [members[i] ooDeepCopyWithSharedObjects:objects]; + } + NS_HANDLER + // Clean up and rethrow. + free(members); + if (tempObjects) [objects release]; + [localException raise]; + NS_ENDHANDLER + +#if !OOLITE_MAC_OS_X + // Make NSArray of results. + result = [[NSArray alloc] initWithObjects:members count:count]; + + // Release objects. + for (i = 0; i < count; i++) + { + [members[i] release]; + } +#else + // Use CF to avoid redundant retain and release. + CFArrayCallBacks arrayCB = kCFTypeArrayCallBacks; + arrayCB.version = 0; + arrayCB.retain = NULL; + result = (NSArray *)CFArrayCreate(kCFAllocatorDefault, (const void **)members, count, &arrayCB); +#endif + + free(members); + if (tempObjects) [objects release]; + + // Collections are not reused because comparing them is arbitrarily slow. + return result; +} + +@end + + +@implementation NSSet (OODeepCopy) + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects +{ + OOUInteger i, count; + id *members = NULL; + NSSet *result = nil; + BOOL tempObjects = NO; + NSEnumerator *setEnum = nil; + + count = [self count]; + if (count == 0) return [[NSSet set] retain]; + + members = malloc(sizeof *members * count); + if (members == NULL) + { + [NSException raise:NSMallocException format:@"Failed to allocate space for %lu objects in %s.", (unsigned long)count, __PRETTY_FUNCTION__]; + } + + // Ensure there's an objects set even if passed nil. + if (objects == nil) + { + objects = [[NSMutableSet alloc] init]; + tempObjects = YES; + } + + setEnum = [self objectEnumerator]; + NS_DURING + // Deep copy members. + for (i = 0; i < count; i++) + { + members[i] = [[setEnum nextObject] ooDeepCopyWithSharedObjects:objects]; + } + NS_HANDLER + // Clean up and rethrow. + free(members); + if (tempObjects) [objects release]; + [localException raise]; + NS_ENDHANDLER + +#if !OOLITE_MAC_OS_X + // Make NSArray of results. + result = [[NSSet alloc] initWithObjects:members count:count]; + + // Release objects. + for (i = 0; i < count; i++) + { + [members[i] release]; + } +#else + // Use CF to avoid redundant retain and release. + CFSetCallBacks setCB = kCFTypeSetCallBacks; + setCB.version = 0; + setCB.retain = NULL; + result = (NSSet *)CFSetCreate(kCFAllocatorDefault, (const void **)members, count, &setCB); +#endif + + free(members); + if (tempObjects) [objects release]; + + // Collections are not reused because comparing them is arbitrarily slow. + return result; +} + +@end + + +@implementation NSDictionary (OODeepCopy) + +- (id) ooDeepCopyWithSharedObjects:(NSMutableSet *)objects +{ + OOUInteger i, count; + id *keys = NULL; + id *values = NULL; + NSDictionary *result = nil; + BOOL tempObjects = NO; + NSEnumerator *keyEnum = nil; + + count = [self count]; + if (count == 0) return [[NSSet set] retain]; + + keys = malloc(sizeof *keys * count); + values = malloc(sizeof *values * count); + if (keys == NULL || values == NULL) + { + free(keys); + free(values); + [NSException raise:NSMallocException format:@"Failed to allocate space for %lu objects in %s.", (unsigned long)count, __PRETTY_FUNCTION__]; + } + + // Ensure there's an objects set even if passed nil. + if (objects == nil) + { + objects = [[NSMutableSet alloc] init]; + tempObjects = YES; + } + + keyEnum = [self keyEnumerator]; + NS_DURING + // Deep copy members. + for (i = 0; i < count; i++) + { + keys[i] = [[keyEnum nextObject] ooDeepCopyWithSharedObjects:objects]; + values[i] = [[self objectForKey:keys[i]] ooDeepCopyWithSharedObjects:objects]; + } + NS_HANDLER + // Clean up and rethrow. + free(keys); + free(values); + if (tempObjects) [objects release]; + [localException raise]; + NS_ENDHANDLER + +#if !OOLITE_MAC_OS_X + // Make NSArray of results. + result = [[NSDictionary alloc] initWithObjects:values forKeys:keys count:count]; + + // Release objects. + for (i = 0; i < count; i++) + { + [keys[i] release]; + [values[i] release]; + } +#else + // Use CF to avoid redundant retain and release. + CFDictionaryKeyCallBacks keyCB = kCFTypeDictionaryKeyCallBacks; + CFDictionaryValueCallBacks valueCB = kCFTypeDictionaryValueCallBacks; + keyCB.version = 0; + keyCB.retain = NULL; + valueCB.version = 0; + valueCB.retain = NULL; + result = (NSDictionary *)CFDictionaryCreate(kCFAllocatorDefault, (const void **)keys, (const void **)values, count, &keyCB, &valueCB); +#endif + + free(keys); + free(values); + if (tempObjects) [objects release]; + + // Collections are not reused because comparing them is arbitrarily slow. + return result; +} + +@end diff --git a/src/Core/OODisplay.h b/src/Core/OODisplay.h new file mode 100644 index 00000000..071e4ce0 --- /dev/null +++ b/src/Core/OODisplay.h @@ -0,0 +1,41 @@ +// +// OODisplay.h +// Oolite +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OOCocoa.h" +#import "OODisplayMode.h" + +@class OODisplayMode; + + +@interface OODisplay: NSObject + ++ (NSArray *) allDisplays; // Array of OODisplay ++ (OODisplay *) mainDisplay; + +- (NSString *) name; +- (NSArray *) modes; // Array of OODisplayMode + +- (OODisplayMode *) currentMode; +- (unsigned) indexOfCurrentMode; + +/* Matching. A "matching dictionary" is a dictionary of property list types + used to identify a display. If no match is found, the main display is + returned. +*/ +- (NSDictionary *) matchingDictionary; ++ (id) displayForMatchingDictionary:(NSDictionary *)dictionary; + +@end + + +// Except were noted, each of these is posted with the relevant display as its object. +// Registering with nil as the object is required to get Display Added notifications. +extern NSString * const kOODisplayAddedNotification; +extern NSString * const kOODisplayRemovedNotification; +extern NSString * const kOODisplaySettingsChangedNotification; +extern NSString * const kOODisplayOrderChangedNotification; // Object is nil diff --git a/src/Core/OODisplay.m b/src/Core/OODisplay.m new file mode 100644 index 00000000..e1424065 --- /dev/null +++ b/src/Core/OODisplay.m @@ -0,0 +1,106 @@ +// +// OODisplay.m +// Oolite +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplay.h" + +#if OOLITE_SDL +#import "OODisplaySDL.h" +#define OODisplayImpl OODisplaySDL +#elif OOLITE_MAC_OS_X +#import "OODisplayMacOSX.h" +#define OODisplayImpl OODisplayMacOSX +#else +#error Unknown system, don't know what implementation of OODisplay to use. +#endif + + +NSString * const kOODisplayAddedNotification = @"org.aegidian.oolite OODisplay displayAdded"; +NSString * const kOODisplayRemovedNotification = @"org.aegidian.oolite OODisplay displayRemoved"; +NSString * const kOODisplaySettingsChangedNotification = @"org.aegidian.oolite OODisplay displayConfigurationChanged"; +NSString * const kOODisplayOrderChangedNotification = @"org.aegidian.oolite OODisplay displayOrderChanged"; + + +@implementation OODisplay + +- (void) dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:nil name:nil object:self]; + [super dealloc]; +} + + ++ (NSArray *) allDisplays +{ + return [OODisplayImpl allDisplays]; +} + + ++ (OODisplay *) mainDisplay +{ + NSArray *all = nil; + OODisplay *mainDisplay = nil; + + all = [self allDisplays]; + if ([all count] != 0) mainDisplay = [all objectAtIndex:0]; + return mainDisplay; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\"", [self name]]; +} + + +- (NSString *) name +{ + OOLogGenericSubclassResponsibility(); + return nil; +} + + +- (NSArray *) modes +{ + OOLogGenericSubclassResponsibility(); + return nil; +} + + +- (OODisplayMode *) currentMode +{ + unsigned index = [self indexOfCurrentMode]; + if (index == NSNotFound) return nil; + return [[self modes] objectAtIndex:index]; +} + + +- (unsigned) indexOfCurrentMode +{ + OOLogGenericSubclassResponsibility(); + return NSNotFound; +} + + +- (NSDictionary *) matchingDictionary +{ + OOLogGenericSubclassResponsibility(); + return nil; +} + + ++ (id) displayForMatchingDictionary:(NSDictionary *)dictionary +{ + id result = nil; + + if (dictionary != nil) result = [OODisplayImpl displayForMatchingDictionary:dictionary]; + if (result == nil) result = [self mainDisplay]; + + return nil; +} + +@end diff --git a/src/Core/OODisplayMacOSX.h b/src/Core/OODisplayMacOSX.h new file mode 100644 index 00000000..e9d9854e --- /dev/null +++ b/src/Core/OODisplayMacOSX.h @@ -0,0 +1,32 @@ +// +// OODisplayMacOSX.h +// Oolite +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplay.h" +#import + + +@interface OODisplayMacOSX: OODisplay +{ + CGDirectDisplayID _displayID; + NSString *_name; + NSArray *_modes; + float _aspectRatio; +} + +/* The aspect ratio of the screen, or buest guesstimate, based on aspect + ratio of largest non-stretched mode. Used to calculate pixel aspect ratios + for modes. + + Note: 4:3 CRTs tend to have non-4:3 modes like 1152x870 (192:145). However, + these can typically be adjusted to squarish pixel ratio with screen + controls, so may or may not be stretched. The OS reports these as non- + stretched, so we report an aspect ratio of 1.0. +*/ +- (float) aspectRatio; + +@end diff --git a/src/Core/OODisplayMacOSX.m b/src/Core/OODisplayMacOSX.m new file mode 100644 index 00000000..edee4139 --- /dev/null +++ b/src/Core/OODisplayMacOSX.m @@ -0,0 +1,473 @@ +// +// OODisplayMacOSX.m +// Oolite +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplayMacOSX.h" +#import "OOFunctionAttributes.h" +#import "OODisplayModeMacOSX.h" +#import + + +static NSMutableArray *sDisplayList = nil; +static NSMutableDictionary *sDisplayIDToDisplay = nil; +static BOOL sCallbackInstalled = NO; + + +@interface OODisplayMacOSX (PrivateDisplayManagement) + ++ (void) buildDisplayTable; ++ (void) updateDisplayList; ++ (void) invalidateDisplayList; ++ (void) installCallback; + ++ (id) addDisplayWithDisplayID:(CGDirectDisplayID)displayID; ++ (void) removeDisplay:(OODisplayMacOSX *)display; + ++ (void) displayWithID:(CGDirectDisplayID)displayID reconfiguredWithChangeFlags:(CGDisplayChangeSummaryFlags)flags; + ++ (OODisplayMacOSX *) displayWithDisplayID:(CGDirectDisplayID)ID; + +@end + + +@interface OODisplayMacOSX (Private) + +- (id) initWithDisplayID:(CGDirectDisplayID)displayID; + +- (CGDirectDisplayID) displayID; + +- (NSDictionary *) ioKitInfoWithFlags:(IOOptionBits)flags; +- (NSString *) determineName; + +- (void) buildModeList; + +@end + + +static void DisplayReconfigurationCallBack(CGDirectDisplayID display, CGDisplayChangeSummaryFlags flags, void *userInfo); + +OOINLINE id DisplayIDKey(CGDirectDisplayID displayID) +{ + /// CGDirectDisplayID changed from opaque pointer to 32-bit integer for Leopard + return [NSNumber numberWithUnsignedInt:(unsigned int)displayID]; +} + + +@implementation OODisplayMacOSX + ++ (NSArray *) allDisplays +{ + // If we don't have a list, build one. + if (sDisplayIDToDisplay == nil) [self buildDisplayTable]; + if (sDisplayList == nil) [self updateDisplayList]; + + if (!sCallbackInstalled) + { + [self installCallback]; + } + + return sDisplayList; +} + + +- (void) dealloc +{ + [_name release]; + [_modes makeObjectsPerformSelector:@selector(invalidate)]; // Ensure modes don't have back references to display, in case they're retained somewhere else. + [_modes release]; + + [super dealloc]; +} + + +- (NSString *) name +{ + if (_name == nil) _name = [[self determineName] retain]; + return _name; +} + + +- (NSArray *) modes +{ + if (_modes == nil) [self buildModeList]; + + return _modes; +} + + +- (unsigned) indexOfCurrentMode +{ + NSEnumerator *modeEnum = nil; + OODisplayModeMacOSX *mode = nil; + NSDictionary *modeDict = nil; + unsigned i = 0; + + modeDict = (NSDictionary *)CGDisplayCurrentMode(_displayID); + for (modeEnum = [[self modes] objectEnumerator]; (mode = [modeEnum nextObject]); ) + { + if ([mode matchesModeDictionary:modeDict]) return i; + i++; + } + + return NSNotFound; +} + + +- (NSDictionary *) matchingDictionary +{ + NSMutableDictionary *result = nil; + NSDictionary *ioKitMatchingDict = nil; + + result = [NSMutableDictionary dictionary]; + ioKitMatchingDict = [self ioKitInfoWithFlags:kIODisplayMatchingInfo]; + if (ioKitMatchingDict != nil) [result setObject:ioKitMatchingDict forKey:@"mac-iokit-matching-dict"]; + + return result; +} + + ++ (id) displayForMatchingDictionary:(NSDictionary *)dictionary +{ + return nil; +} + + +- (float) aspectRatio +{ + NSEnumerator *modeEnum = nil; + OODisplayModeMacOSX *mode = nil; + + if (_aspectRatio == 0.0) + { + // Calculate aspect ratio as (total) aspect ratio of largest mode. + for (modeEnum = [[self modes] reverseObjectEnumerator]; (mode = [modeEnum nextObject]); ) + { + if (![mode isStretched]) break; + } + + if (mode != nil) + { + _aspectRatio = (float)[mode width] / (float)[mode height]; + } + else + { + // Fallback, could potentially happen if all modes are stretched (which would be silly, but hey). + _aspectRatio = 1.0; + } + } + + return _aspectRatio; +} + +@end + + +@implementation OODisplayMacOSX (Private) + +- (id) initWithDisplayID:(CGDirectDisplayID)displayID +{ + self = [super init]; + if (self != nil) + { + _displayID = displayID; + } + + return self; +} + + +- (CGDirectDisplayID) displayID +{ + return _displayID; +} + + +- (NSString *) determineName +{ + NSString *result = nil; + NSDictionary *info = nil; + NSDictionary *names = nil; + +#if !defined __LP64__ && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_4 + // Old method, deprecated in 10.4, not available in 64-bit, but I trust it more. + OSStatus err; + Str255 str255Name; + + // Get name of monitor. This cast to a different type from a different API + // has been declared safe on the mac-games-dev list in times of yore. + err = DMGetNameByAVID((DisplayIDType)_displayID, 0, str255Name); + if (err == noErr && str255Name[0] != 0) + { + result = (NSString *)CFStringCreateWithPascalString(kCFAllocatorDefault, str255Name, CFStringGetSystemEncoding()); + [result autorelease]; + } +#endif + + if (result == nil) + { + // kIODisplayOnlyPreferredName = 0x00000200, not declared in 10.3.9 SDK + // Causes best-match name (by language) to be used + info = [self ioKitInfoWithFlags:0x00000200]; + if (info != nil) + { + names = [info objectForKey:@kDisplayProductName]; + if ([names count] > 0) + { + result = [[names allValues] objectAtIndex:0]; + } + } + } + + if (result == nil) + { + result = [NSString stringWithFormat:@"Display ID %u", _displayID]; + } + + return result; +} + + +- (NSDictionary *) ioKitInfoWithFlags:(IOOptionBits)flags +{ + io_service_t servicePort; + NSDictionary *info = nil; + + servicePort = CGDisplayIOServicePort(_displayID); + if (MACH_PORT_VALID(servicePort)) + { + info = (NSDictionary *)IODisplayCreateInfoDictionary(servicePort, flags); + [info autorelease]; + } + + return info; +} + + +- (void) buildModeList +{ + NSArray *modeDicts = nil; + NSEnumerator *modeEnum = nil; + NSDictionary *modeDict = nil; + NSMutableArray *result = nil; + OODisplayModeMacOSX *mode = nil; + + modeDicts = (NSArray *)CGDisplayAvailableModes(_displayID); + result = [NSMutableArray arrayWithCapacity:[modeDict count]]; + + for (modeEnum = [modeDicts objectEnumerator]; (modeDict = [modeEnum nextObject]); ) + { + mode = [[OODisplayModeMacOSX alloc] initForDisplay:self modeDictionary:modeDict]; + if (mode != nil) + { + [result addObject:mode]; + [mode release]; + } + } + + [result sortUsingSelector:@selector(compare:)]; + _modes = [result copy]; +} + +@end + + +@implementation OODisplayMacOSX (PrivateDisplayManagement) + ++ (void) buildDisplayTable +{ + CGDisplayErr err = kCGErrorSuccess; + CGDisplayCount i, count; + + [sDisplayIDToDisplay release]; + sDisplayIDToDisplay = nil; + [self invalidateDisplayList]; + + // Find out how many displays there are. + err = CGGetActiveDisplayList(0, NULL, &count); + if (err == kCGErrorSuccess) + { + // Get the list. + CGDirectDisplayID displayIDs[count]; + err = CGGetActiveDisplayList(count, displayIDs, &count); + + if (err == kCGErrorSuccess) + { + // Build list of OODisplayMacOSXs. + sDisplayIDToDisplay = [[NSMutableDictionary alloc] initWithCapacity:count]; + + for (i = 0; i != count; ++i) + { + [self addDisplayWithDisplayID:displayIDs[i]]; + } + } + } + + if (err != kCGErrorSuccess) + { + OOLog(@"display.buildTable.failed", @"Failed to build display table with CGDirectDisplay error %li.", (long)err); + } +} + + ++ (void) updateDisplayList +{ + /* Create list of displays, in order specified by CGDirectDisplay. + This is a separate method because order is invalidated when a new + display is added. + */ + CGDisplayErr err = kCGErrorSuccess; + CGDisplayCount i, count; + OODisplayMacOSX *display = nil; + + [self invalidateDisplayList]; + if (sDisplayIDToDisplay == nil) return; + + // Find out how many displays there are. + err = CGGetActiveDisplayList(0, NULL, &count); + if (err == kCGErrorSuccess) + { + // Get the list. + CGDirectDisplayID displayIDs[count]; + err = CGGetActiveDisplayList(count, displayIDs, &count); + + if (err == kCGErrorSuccess) + { + // Build list of OODisplayMacOSXs. + sDisplayList = [[NSMutableArray alloc] initWithCapacity:count]; + + for (i = 0; i != count; ++i) + { + display = [self displayWithDisplayID:displayIDs[i]]; + if (display != nil) + { + [sDisplayList addObject:display]; + } + } + } + } + + if (err != kCGErrorSuccess) + { + // Fallback: arbitrary order. + sDisplayList = [[sDisplayIDToDisplay allValues] mutableCopy]; + } +} + + ++ (void) invalidateDisplayList +{ + [sDisplayList release]; + sDisplayList = nil; +} + + ++ (void) installCallback +{ + CGDisplayRegisterReconfigurationCallback(DisplayReconfigurationCallBack, NULL); + sCallbackInstalled = YES; +} + + ++ (id) addDisplayWithDisplayID:(CGDirectDisplayID)displayID +{ + OODisplayMacOSX *display = nil; + + display = [self displayWithDisplayID:displayID]; + if (display != nil) + { + OOLog(@"display.add.inconsistency", @"Internal display management error: attempt to add a display ID that already exists."); + } + else + { + // Update by-ID table + display = [[self alloc] initWithDisplayID:displayID]; + [display autorelease]; + [sDisplayIDToDisplay setObject:display forKey:DisplayIDKey(displayID)]; + + // Force ordered list to be updated lazily + [self invalidateDisplayList]; + } + return display; +} + + ++ (void) removeDisplay:(OODisplayMacOSX *)display +{ + if (display == nil) return; + + if (display != [self displayWithDisplayID:[display displayID]]) + { + OOLog(@"display.add.inconsistency", @"Internal display management error: attempt to remove a display that doesn't exist."); + } + else + { + // Update by-ID table + [sDisplayList removeObject:display]; + + // Update ordered list immediately, since removing doesn't change order of remaining entries. + [sDisplayIDToDisplay removeObjectForKey:DisplayIDKey([display displayID])]; + } +} + + ++ (void) displayWithID:(CGDirectDisplayID)displayID reconfiguredWithChangeFlags:(CGDisplayChangeSummaryFlags)flags +{ + OODisplayMacOSX *display = nil; + + OOLog(@"", @"Display %p (%@) reconfigured with flags: 0x%X", displayID, [self displayWithDisplayID:displayID], flags); + + NS_DURING + if (flags & kCGDisplayAddFlag) + { + display = [self addDisplayWithDisplayID:displayID]; + [[NSNotificationCenter defaultCenter] postNotificationName:kOODisplayAddedNotification + object:display]; + } + if (flags & kCGDisplayRemoveFlag) + { + display = [self displayWithDisplayID:displayID]; + if (display != nil) + { + [[display retain] autorelease]; + [self removeDisplay:display]; + [[NSNotificationCenter defaultCenter] postNotificationName:kOODisplayRemovedNotification + object:display]; + } + } + if (flags & kCGDisplaySetModeFlag) + { + display = [self displayWithDisplayID:displayID]; + if (display != nil) + { + [[NSNotificationCenter defaultCenter] postNotificationName:kOODisplaySettingsChangedNotification + object:display]; + } + } + if (flags & kCGDisplaySetMainFlag) + { + [self invalidateDisplayList]; + [[NSNotificationCenter defaultCenter] postNotificationName:kOODisplayOrderChangedNotification + object:nil]; + } + NS_HANDLER + OOLog(@"display.notify.exception", @"Squelching %@ exception posted during display configuration change notification: %@", [localException name], [localException reason]); + NS_ENDHANDLER +} + + ++ (OODisplayMacOSX *) displayWithDisplayID:(CGDirectDisplayID)displayID +{ + return [sDisplayIDToDisplay objectForKey:DisplayIDKey(displayID)]; +} + +@end + + +static void DisplayReconfigurationCallBack(CGDirectDisplayID displayID, CGDisplayChangeSummaryFlags flags, void *userInfo) +{ + [OODisplayMacOSX displayWithID:displayID reconfiguredWithChangeFlags:flags]; +} diff --git a/src/Core/OODisplayMode.h b/src/Core/OODisplayMode.h new file mode 100644 index 00000000..9896aa98 --- /dev/null +++ b/src/Core/OODisplayMode.h @@ -0,0 +1,46 @@ +// +// OODisplayMode.h +// Oolite +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OOCocoa.h" + +@class OODisplay; + + +@interface OODisplayMode: NSObject + +- (OODisplay *) display; + +- (NSString *) userDescription; + +- (unsigned) width; +- (unsigned) height; +- (unsigned) bitDepth; +- (float) refreshRate; + +- (BOOL) isStretched; +- (float) aspectRatio; // Pixel aspect ratio, or stretch factor. For a non-stretched mode, 1.0. For a 4:3 mode on an 8:5 screen, (8/5)/(4/3) = (8*3)/(4*5) = 24/20 = 6/5. + +- (BOOL) isInterlaced; +- (BOOL) isTV; + +- (BOOL) requiresConfirmation; // if YES, it is not safe to switch to this mode without a confirmation alert and automatic reset dance. + +- (BOOL) isOKForWindowedMode; +- (BOOL) isOKForFullScreenMode; + +@end + + +@interface OODisplayMode (Utilities) + +- (NSComparisonResult) compare:(id)other; + +- (NSSize) dimensions; +- (float) pixelArea; + +@end diff --git a/src/Core/OODisplayMode.m b/src/Core/OODisplayMode.m new file mode 100644 index 00000000..24b813c8 --- /dev/null +++ b/src/Core/OODisplayMode.m @@ -0,0 +1,187 @@ +// +// OODisplayMode.m +// Oolite +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplayMode.h" + + +@interface OODisplayMode (Private) + +- (unsigned) sortFlagBits; + +@end + + +@implementation OODisplayMode + +- (NSString *) descriptionComponents +{ + return [self userDescription]; +} + + +- (OODisplay *) display +{ + OOLogGenericSubclassResponsibility(); + return nil; +} + + +- (NSString *) userDescription +{ + NSMutableString *result = nil; + NSSize dimensions; + float refresh; + NSMutableArray *misc; + + dimensions = [self dimensions]; + result = [NSMutableString stringWithFormat:@"%u x %u", (unsigned)dimensions.width, (unsigned)dimensions.height]; + + refresh = [self refreshRate]; + if (refresh > 0.0f) [result appendFormat:@", %.3g Hz"]; + + misc = [NSMutableArray array]; + if ([self isStretched]) [misc addObject:@"stretched"]; + if ([self isInterlaced]) [misc addObject:@"interlaced"]; + if ([self isTV]) [misc addObject:@"TV"]; + if ([misc count] > 0) [result appendFormat:@" (%@)", [misc componentsJoinedByString:@", "]]; + + return result; +} + + +- (unsigned) width +{ + OOLogGenericSubclassResponsibility(); + return 0; +} + + +- (unsigned) height +{ + OOLogGenericSubclassResponsibility(); + return 0; +} + + +- (unsigned) bitDepth +{ + OOLogGenericSubclassResponsibility(); + return 0; +} + + +- (float) refreshRate +{ + return 0.0f; +} + + +- (BOOL) isStretched +{ + return NO; +} + + +- (float) aspectRatio +{ + return 1.0f; +} + + +- (BOOL) isInterlaced +{ + return NO; +} + + +- (BOOL) isTV +{ + return NO; +} + + +- (BOOL) requiresConfirmation +{ + return NO; +} + + +- (BOOL) isOKForWindowedMode +{ + return YES; +} + + +- (BOOL) isOKForFullScreenMode +{ + return YES; +} + +@end + + +@implementation OODisplayMode (Utilities) + +- (NSComparisonResult) compare:(id)other +{ + if (![other isKindOfClass:[OODisplayMode class]]) return NSOrderedSame; + + // First criterion: area. Screens with larger areas are listed later. + float myArea = [self pixelArea], otherArea = [other pixelArea]; + if (myArea < otherArea) return NSOrderedAscending; + if (myArea > otherArea) return NSOrderedDescending; + + // Second criterion: flags. + unsigned myFlags = [self sortFlagBits], otherFlags = [other sortFlagBits]; + if (myFlags < otherFlags) return NSOrderedAscending; + if (myFlags > otherFlags) return NSOrderedDescending; + + // Third criterion: refresh rate. + float myRefreshRate = [self refreshRate], otherRefreshRate = [other refreshRate]; + if (myRefreshRate < otherRefreshRate) return NSOrderedAscending; + if (myRefreshRate > otherRefreshRate) return NSOrderedDescending; + + // Fouth criterion: display depth. + unsigned myDepth = [self bitDepth], otherDepth = [other bitDepth]; + if (myDepth < otherDepth) return NSOrderedAscending; + if (myDepth > otherDepth) return NSOrderedDescending; + + return NSOrderedSame; +} + + +- (NSSize) dimensions +{ + return NSMakeSize([self width], [self height]); +} + + +- (float) pixelArea +{ + NSSize dimensions = [self dimensions]; + return dimensions.width * dimensions.height; +} + +@end + + +@implementation OODisplayMode (Private) + +- (unsigned) sortFlagBits +{ + // Return flags packed into an int for -compare:. + unsigned result = 0; + + if ([self isStretched]) result |= 0x01; + if ([self isInterlaced]) result |= 0x02; + if ([self isTV]) result |= 0x04; + + return result; +} + +@end diff --git a/src/Core/OODisplayModeMacOSX.h b/src/Core/OODisplayModeMacOSX.h new file mode 100644 index 00000000..9c5ab235 --- /dev/null +++ b/src/Core/OODisplayModeMacOSX.h @@ -0,0 +1,26 @@ +// +// OODisplayModeMacOSX.h +// DisplayTest +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplayMode.h" + +@class OODisplayMacOSX; + + +@interface OODisplayModeMacOSX: OODisplayMode +{ + OODisplayMacOSX *_display; + NSDictionary *_mode; +} + +- (id) initForDisplay:(OODisplayMacOSX *)display modeDictionary:(NSDictionary *)modeDict; +- (void) invalidate; + +// Used to find current mode. +- (BOOL) matchesModeDictionary:(NSDictionary *)modeDict; + +@end diff --git a/src/Core/OODisplayModeMacOSX.m b/src/Core/OODisplayModeMacOSX.m new file mode 100644 index 00000000..4e8fda45 --- /dev/null +++ b/src/Core/OODisplayModeMacOSX.m @@ -0,0 +1,152 @@ +// +// OODisplayModeMacOSX.m +// DisplayTest +// +// Created by Jens Ayton on 2007-12-08. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplayModeMacOSX.h" +#import "OODisplayMacOSX.h" +#import "OOCollectionExtractors.h" + + +@interface OODisplayModeMacOSX (Private) + +- (id) internalModeID; + +@end + + +@implementation OODisplayModeMacOSX + +- (id) initForDisplay:(OODisplayMacOSX *)display modeDictionary:(NSDictionary *)modeDict +{ + self = [super init]; + if (self != nil) + { + _display = display; // Not retained. + _mode = [modeDict retain]; + } + + return self; +} + + +- (void) dealloc +{ + [self invalidate]; + + [super dealloc]; +} + + +- (BOOL) isEqual:(id)other +{ + if (self == other) return YES; + if (![other isKindOfClass:[OODisplayModeMacOSX class]]) return NO; + + if (![[self display] isEqual:[(OODisplayModeMacOSX *)other display]]) return NO; + if (![[self internalModeID] isEqual:[other internalModeID]]) return NO; + + return YES; +} + + +- (OODisplay *) display +{ + return _display; +} + + +- (unsigned) width +{ + return [_mode unsignedIntForKey:(NSString *)kCGDisplayWidth]; +} + + +- (unsigned) height +{ + return [_mode unsignedIntForKey:(NSString *)kCGDisplayHeight]; +} + + +- (unsigned) bitDepth +{ + return [_mode unsignedIntForKey:(NSString *)kCGDisplayBitsPerPixel]; +} + + +- (float) refreshRate +{ + return [_mode floatForKey:(NSString *)kCGDisplayRefreshRate]; +} + + +- (BOOL) isStretched +{ + return [_mode boolForKey:(NSString *)kCGDisplayModeIsStretched]; +} + + +- (float) aspectRatio +{ + return [_display aspectRatio] / ((float)[self width] / (float)[self height]); +} + + +- (BOOL) isInterlaced +{ + return [_mode boolForKey:(NSString *)kCGDisplayModeIsInterlaced]; +} + + +- (BOOL) isTV +{ + return [_mode boolForKey:(NSString *)kCGDisplayModeIsTelevisionOutput]; +} + + +- (BOOL) requiresConfirmation +{ + return ![_mode boolForKey:(NSString *)kCGDisplayModeIsSafeForHardware]; +} + + +- (BOOL) isOKForWindowedMode +{ + return [_mode boolForKey:(NSString *)kCGDisplayModeUsableForDesktopGUI]; +} + + +- (NSDictionary *) modeDictionary +{ + return _mode; +} + + +- (void) invalidate +{ + // _display is not retained. + _display = nil; + [_mode release]; + _mode = nil; +} + + +- (BOOL) matchesModeDictionary:(NSDictionary *)modeDict +{ + return [[self internalModeID] isEqual:[modeDict objectForKey:(NSString *)kCGDisplayMode]]; +} + +@end + + +@implementation OODisplayModeMacOSX (Private) + +- (id) internalModeID +{ + return [_mode objectForKey:(NSString *)kCGDisplayMode]; +} + +@end \ No newline at end of file diff --git a/src/Core/OODisplayModeSDL.h b/src/Core/OODisplayModeSDL.h new file mode 100644 index 00000000..9d866f8d --- /dev/null +++ b/src/Core/OODisplayModeSDL.h @@ -0,0 +1,29 @@ +// +// OODisplayModeSDL.h +// DisplayTest +// +// Created by Jens Ayton on 2007-12-30. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplayMode.h" + +@class OODisplaySDL; + + +@interface OODisplayModeSDL: OODisplayMode +{ + OODisplaySDL *_display; + unsigned _width, + _height, + _depth; +} + +- (id) initWithDisplay:(OODisplaySDL *)display + width:(unsigned)width + height:(unsigned)height + depth:(unsigned)depth; + +- (void) invalidate; + +@end diff --git a/src/Core/OODisplayModeSDL.m b/src/Core/OODisplayModeSDL.m new file mode 100644 index 00000000..4ac1423b --- /dev/null +++ b/src/Core/OODisplayModeSDL.m @@ -0,0 +1,91 @@ +// +// OODisplayModeSDL.m +// DisplayTest +// +// Created by Jens Ayton on 2007-12-30. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplayModeSDL.h" +#import "OODisplaySDL.h" + + +@implementation OODisplayModeSDL + +- (id) initWithDisplay:(OODisplaySDL *)display + width:(unsigned)width + height:(unsigned)height + depth:(unsigned)depth +{ + self = [super init]; + if (self != nil) + { + _display = display; // Not retained + _width = width; + _height = height; + _depth = depth; + } + return self; +} + + +- (void) dealloc +{ + [self invalidate]; + + [super dealloc]; +} + + +- (BOOL) isEqual:(id)other +{ + if (self == other) return YES; + if (![other isKindOfClass:[OODisplayModeSDL class]]) return NO; + + if (![[self display] isEqual:[(OODisplayModeSDL *)other display]]) return NO; + if ([self width] != [other width]) return NO; + if ([self height] != [other height]) return NO; + if ([self bitDepth] != [other bitDepth]) return NO; + + return YES; +} + + +- (OODisplay *) display +{ + return _display; +} + + +- (unsigned) width +{ + return _width; +} + + +- (unsigned) height +{ + return _height; +} + + +- (unsigned) bitDepth +{ + return _depth; +} + + +- (float) refreshRate +{ + return 0; +} + + +- (void) invalidate +{ + // _display is not retained. + _display = nil; + _width = _height = _depth = 0; +} + +@end diff --git a/src/Core/OODisplaySDL.h b/src/Core/OODisplaySDL.h new file mode 100644 index 00000000..46b94a0a --- /dev/null +++ b/src/Core/OODisplaySDL.h @@ -0,0 +1,17 @@ +// +// OODisplaySDL.h +// DisplayTest +// +// Created by Jens Ayton on 2007-12-30. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplay.h" + + +@interface OODisplaySDL: OODisplay +{ + NSArray *_modes; +} + +@end diff --git a/src/Core/OODisplaySDL.m b/src/Core/OODisplaySDL.m new file mode 100644 index 00000000..958a28f4 --- /dev/null +++ b/src/Core/OODisplaySDL.m @@ -0,0 +1,174 @@ +// +// OODisplaySDL.m +// DisplayTest +// +// Created by Jens Ayton on 2007-12-30. +// Copyright 2007 Jens Ayton. All rights reserved. +// + +#import "OODisplaySDL.h" +#import "OODisplayModeSDL.h" +#import "OOCollectionExtractors.h" +#import + + +// SDL doesn't have a concept of multiple displays, so we only need one. +static OODisplaySDL *sDisplay = nil; + + +@interface OODisplaySDL (Private) + +- (void) buildModeList; + +@end + + +@implementation OODisplaySDL + ++ (NSArray *) allDisplays +{ + return [NSArray arrayWithObject:[self mainDisplay]]; +} + + ++ (OODisplay *) mainDisplay +{ + if (sDisplay == nil) sDisplay = [[OODisplaySDL alloc] init]; + return sDisplay; +} + + +- (void) dealloc +{ + if (sDisplay == self) sDisplay = nil; + [_modes makeObjectsPerformSelector:@selector(invalidate)]; // Ensure modes don't have back references to display, in case they're retained somewhere else. + [_modes release]; + + [super dealloc]; +} + + +- (NSString *) name +{ + return @"Display"; +} + + +- (NSArray *) modes +{ + if (_modes == nil) [self buildModeList]; + return _modes; +} + + +- (unsigned) indexOfCurrentMode +{ + NSEnumerator *modeEnum = nil; + OODisplayModeSDL *mode = nil; + const SDL_VideoInfo *info = NULL; + unsigned i = 0; + + info = SDL_GetVideoInfo(); + if (info == NULL) return NSNotFound; + + for (modeEnum = [[self modes] objectEnumerator]; (mode = [modeEnum nextObject]); ) + { + if ([mode width] == info->current_w && + [mode height] == info->current_h && + [mode bitDepth] == info->vfmt->BitsPerPixel) + { + return i; + } + i++; + } + + return NSNotFound; +} + + +- (NSDictionary *) matchingDictionary +{ + return [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:@"sdl-main-display"]; +} + + ++ (id) displayForMatchingDictionary:(NSDictionary *)dictionary +{ + if ([dictionary boolForKey:@"sdl-main-display"]) return [self mainDisplay]; + return nil; +} + +@end + + +@implementation OODisplaySDL (Private) + +- (void) buildModeList +{ + /* SDL doesn't provide a way to enumerate all display modes. Instead, it + provides a way to enumerate rectangles given a pixel format. It also + doesn't provide a way to find valid pixel formats (which are rather + comprehensive descriptions). This is so very, very stupid. + */ + SDL_Rect **rawRects = NULL; + unsigned i, j; + NSMutableArray *modes = nil; + OODisplayModeSDL *mode = nil; + const unsigned depths[] = {8, 16, 32}; + const unsigned depthCount = ARRAY_LENGTH(depths); + const SDL_VideoInfo *info = NULL; + NSMutableArray *rects = nil; + NSSize size; + + modes = [NSMutableArray array]; + + rawRects = SDL_ListModes(NULL, SDL_FULLSCREEN); + if (rawRects != NULL && rawRects != (SDL_Rect **)-1) + { + // First, copy the list because SDL_VideoModeOK() may have clobbered it. + rects = [NSMutableArray array]; + for (i = 0; rawRects[i]; ++i) + { + size = NSMakeSize(rawRects[i]->w, rawRects[i]->h); + [rects addObject:[NSValue valueWithSize:size]]; + } + + for (i = 0; i != [rects count]; ++i) + { + size = [[rects objectAtIndex:i] sizeValue]; + + for (j = 0; j != depthCount; ++j) + { + if (SDL_VideoModeOK(size.width, size.height, depths[j], SDL_FULLSCREEN | SDL_OPENGL)) + { + mode = [[OODisplayModeSDL alloc] initWithDisplay:self + width:size.width + height:size.height + depth:depths[j]]; + [modes addObject:mode]; + [mode release]; + } + } + } + } + + if ([modes count] == 0) + { + // No modes found; assume screen's current settings constitute a mode. + info = SDL_GetVideoInfo(); + if (info != nil) + { + mode = [[OODisplayModeSDL alloc] initWithDisplay:self + width:info->current_w + height:info->current_h + depth:info->vfmt->BitsPerPixel]; + [modes addObject:mode]; + [mode release]; + } + } + + [modes sortUsingSelector:@selector(compare:)]; + _modes = [modes copy]; +} + +@end diff --git a/src/Core/OODrawable.h b/src/Core/OODrawable.h new file mode 100644 index 00000000..56b5fa5e --- /dev/null +++ b/src/Core/OODrawable.h @@ -0,0 +1,76 @@ +/* + +OODrawable.h + +Abstract base class for objects which can draw themselves. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" +#import "OOMaths.h" +#import "OOWeakReference.h" + +@class Geometry; + + +@interface OODrawable: NSObject + +- (void)renderOpaqueParts; +- (void)renderTranslucentParts; +- (BOOL)hasOpaqueParts; +- (BOOL)hasTranslucentParts; + +- (GLfloat)collisionRadius; +- (GLfloat)maxDrawDistance; +- (Geometry *)geometry; + +- (BoundingBox)boundingBox; + +// Passed to all materials. +- (void)setBindingTarget:(id)target; + +- (void)dumpSelfState; + +@end diff --git a/src/Core/OODrawable.m b/src/Core/OODrawable.m new file mode 100644 index 00000000..15be867f --- /dev/null +++ b/src/Core/OODrawable.m @@ -0,0 +1,112 @@ +/* + +OODrawable.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OODrawable.h" + + +@implementation OODrawable + +- (void)renderOpaqueParts +{ + +} + + +- (void)renderTranslucentParts +{ + +} + + +- (BOOL)hasOpaqueParts +{ + return NO; +} + + +- (BOOL)hasTranslucentParts +{ + return NO; +} + + +- (GLfloat)collisionRadius +{ + return 0.0f; +} + + +- (GLfloat)maxDrawDistance +{ + return 0.0f; +} + + +- (Geometry *)geometry +{ + return nil; +} + + +- (BoundingBox)boundingBox +{ + return kZeroBoundingBox; +} + + +- (void)setBindingTarget:(id)target +{ + +} + + +- (void)dumpSelfState +{ + +} + +@end diff --git a/src/Core/OOEncodingConverter.h b/src/Core/OOEncodingConverter.h new file mode 100644 index 00000000..a786afca --- /dev/null +++ b/src/Core/OOEncodingConverter.h @@ -0,0 +1,89 @@ +/* + +OOEncodingConverter.h + +Convert a string to an 8-bit encoding, with some Oolite-specific remappings +specified at init time (currently always from oolite-font.plist). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#ifndef OOENCODINGCONVERTER_EXCLUDE // For the convenience of fonttexgen + +#import "OOCocoa.h" + +@class OOCache; + + +@interface OOEncodingConverter: NSObject +{ + NSStringEncoding _encoding; + OOCache *_cache; + NSDictionary *_substitutions; +} + +- (id) initWithEncoding:(NSStringEncoding)encoding substitutions:(NSDictionary *)substitutions; +- (id) initWithFontPList:(NSDictionary *)fontPList; + +- (NSData *) convertString:(NSString *)string; + +- (NSStringEncoding) encoding; + +@end + +#endif //OOENCODINGCONVERTER_EXCLUDE + + +/* + There are a variety of overlapping naming schemes for text encoding. + We ignore them and use a fixed list: + "windows-latin-1" NSWindowsCP1252StringEncoding + "windows-latin-2" NSWindowsCP1250StringEncoding + "windows-cyrillic" NSWindowsCP1251StringEncoding + "windows-greek" NSWindowsCP1253StringEncoding + "windows-turkish" NSWindowsCP1254StringEncoding +*/ +NSString *StringFromEncoding(NSStringEncoding encoding); // Returns nil for unknown +NSStringEncoding EncodingFromString(NSString *name); // Returns (NSStringEncoding)NSNotFound for unknown diff --git a/src/Core/OOEncodingConverter.m b/src/Core/OOEncodingConverter.m new file mode 100644 index 00000000..da9a2d1f --- /dev/null +++ b/src/Core/OOEncodingConverter.m @@ -0,0 +1,276 @@ +/* + +OOEncodingConverter.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OOENCODINGCONVERTER_EXCLUDE + +#import "OOEncodingConverter.h" +#import "OOCache.h" +#import "OOCollectionExtractors.h" +#import "OOLogging.h" + + +/* Using compatibility mapping - converting strings to Unicode form KC - would + reduce potential complications in localizing Oolite. However, the method to + perform the transformation is not available in GNUstep. I'm currently not + using it under OS X either, for cross-platform consistency. + -- Ahruman 2008-01-27 +*/ +#if OOLITE_MAC_OS_X +#define USE_COMPATIBILITY_MAPPING 0 +#else +#define USE_COMPATIBILITY_MAPPING 0 +#endif + + +#define PROFILE_ENCODING_CONVERTER 0 + + +#if PROFILE_ENCODING_CONVERTER +static OOEncodingConverter *sProfiledConverter = nil; +static NSTimer *sProfileTimer = nil; + +static unsigned sCacheHits = 0; +static unsigned sCacheMisses = 0; +#endif + + +@interface OOEncodingConverter (Private) + +- (NSData *) performConversionForString:(NSString *)string; + +@end + + +@implementation OOEncodingConverter + +- (id) initWithEncoding:(NSStringEncoding)encoding substitutions:(NSDictionary *)substitutions +{ + self = [super init]; + if (self != nil) + { + _cache = [[OOCache alloc] init]; + [_cache setPruneThreshold:100]; + [_cache setName:@"Text encoding"]; + _substitutions = [substitutions copy]; + _encoding = encoding; + +#if PROFILE_ENCODING_CONVERTER + if (sProfiledConverter == nil) + { + sProfiledConverter = self; + sProfileTimer = [NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(profileFire:) userInfo:nil repeats:YES]; + } +#endif + } + + return self; +} + + +- (id) initWithFontPList:(NSDictionary *)fontPList +{ + return [self initWithEncoding:EncodingFromString([fontPList stringForKey:@"encoding"]) substitutions:[fontPList dictionaryForKey:@"substitutions"]]; +} + + +- (void) dealloc +{ + [_cache release]; + [_substitutions release]; + +#if PROFILE_ENCODING_CONVERTER + sProfiledConverter = nil; + [sProfileTimer invalidate]; + sProfileTimer = nil; + sCacheHits = 0; + sCacheMisses = 0; +#endif + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"encoding: %u", _encoding]; +} + + +- (NSData *) convertString:(NSString *)string +{ + NSData *data = nil; + +#if USE_COMPATIBILITY_MAPPING + // Convert to Unicode Normalization Form KC (that is, minimize the use of combining modifiers while avoiding precomposed ligatures) + string = [string precomposedStringWithCompatibilityMapping]; +#endif + + if (string == nil) return [NSData data]; + + data = [_cache objectForKey:string]; + if (data == nil) + { + data = [self performConversionForString:string]; + if (data != nil) [_cache setObject:data forKey:string]; + +#if PROFILE_ENCODING_CONVERTER + ++sCacheMisses; + } + else + { + ++sCacheHits; +#endif + } + + return data; +} + + +- (NSStringEncoding) encoding +{ + return _encoding; +} + +@end + + +@implementation OOEncodingConverter (Private) + +- (NSData *) performConversionForString:(NSString *)string +{ + NSString *subst = nil; + NSEnumerator *substEnum = nil; + NSMutableString *mutable = nil; + + mutable = [[string mutableCopy] autorelease]; + if (mutable == nil) return nil; + + for (substEnum = [_substitutions keyEnumerator]; (subst = [substEnum nextObject]); ) + { + [mutable replaceOccurrencesOfString:subst + withString:[_substitutions objectForKey:subst] + options:0 + range:NSMakeRange(0, [mutable length])]; + } + + return [mutable dataUsingEncoding:_encoding allowLossyConversion:YES]; +} + + +#if PROFILE_ENCODING_CONVERTER +/* + Profiling observations: + * The clock generates one new string per second. + * The trade screens each use over 60 strings, so cache sizes below 70 are + undesireable. + * Cache hit ratio is extremely near 100% at most times. +*/ +- (void) profileFire:(id)junk +{ + float ratio = (float)sCacheHits / (float)(sCacheHits + sCacheMisses); + OOLog(@"strings.encoding.profile", @"Cache hits: %u, misses: %u, ratio: %.2g", sCacheHits, sCacheMisses, ratio); + sCacheHits = sCacheMisses = 0; +} +#endif + +@end + +#endif //OOENCODINGCONVERTER_EXCLUDE + + +/* + There are a variety of overlapping naming schemes for text encoding. + We ignore them and use a fixed list: + "windows-latin-1" NSWindowsCP1252StringEncoding + "windows-latin-2" NSWindowsCP1250StringEncoding + "windows-cyrillic" NSWindowsCP1251StringEncoding + "windows-greek" NSWindowsCP1253StringEncoding + "windows-turkish" NSWindowsCP1254StringEncoding +*/ + +#define kWindowsLatin1Str @"windows-latin-1" +#define kWindowsLatin2Str @"windows-latin-2" +#define kWindowsCyrillicStr @"windows-cyrillic" +#define kWindowsGreekStr @"windows-greek" +#define kWindowsTurkishStr @"windows-turkish" + + +NSString *StringFromEncoding(NSStringEncoding encoding) +{ + switch (encoding) + { + case NSWindowsCP1252StringEncoding: + return kWindowsLatin1Str; + + case NSWindowsCP1250StringEncoding: + return kWindowsLatin2Str; + + case NSWindowsCP1251StringEncoding: + return kWindowsCyrillicStr; + + case NSWindowsCP1253StringEncoding: + return kWindowsGreekStr; + + case NSWindowsCP1254StringEncoding: + return kWindowsTurkishStr; + + default: + return nil; + } +} + + +NSStringEncoding EncodingFromString(NSString *name) +{ + if ([name isEqualToString:kWindowsLatin1Str]) return NSWindowsCP1252StringEncoding; + if ([name isEqualToString:kWindowsLatin2Str]) return NSWindowsCP1250StringEncoding; + if ([name isEqualToString:kWindowsCyrillicStr]) return NSWindowsCP1251StringEncoding; + if ([name isEqualToString:kWindowsGreekStr]) return NSWindowsCP1253StringEncoding; + if ([name isEqualToString:kWindowsTurkishStr]) return NSWindowsCP1254StringEncoding; + return (NSStringEncoding)NSNotFound; +} diff --git a/src/Core/OOEntityFilterPredicate.h b/src/Core/OOEntityFilterPredicate.h new file mode 100644 index 00000000..8ecb485d --- /dev/null +++ b/src/Core/OOEntityFilterPredicate.h @@ -0,0 +1,78 @@ +/* + +OOEntityFilterPredicate.h + +Filters used to select entities in various contexts. Callers are required to +ensure that the "entity" argument is non-nil and the "parameter" argument is +valid and relevant. + +To reduce header spaghetti, the EntityFilterPredicate type is declared in +Universe.h, which is included just about everywhere anyway. This file just +declares a set of widely-useful predicates. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "Universe.h" + + +typedef struct +{ + EntityFilterPredicate predicate; + void *parameter; +} ChainedEntityPredicateParameter; + +typedef struct +{ + EntityFilterPredicate predicate1; + void *parameter1; + EntityFilterPredicate predicate2; + void *parameter2; +} BinaryOperationPredicateParameter; + + +BOOL YESPredicate(Entity *entity, void *parameter); // Parameter: ignored. Always returns YES. +BOOL NOPredicate(Entity *entity, void *parameter); // Parameter: ignored. Always returns NO. + +BOOL NOTPredicate(Entity *entity, void *parameter); // Parameter: ChainedEntityPredicateParameter. Reverses effect of chained predicate. + +BOOL ANDPredicate(Entity *entity, void *parameter); // Parameter: BinaryOperationPredicateParameter. Short-circuiting AND operator. +BOOL ORPredicate(Entity *entity, void *parameter); // Parameter: BinaryOperationPredicateParameter. Short-circuiting OR operator. +BOOL NORPredicate(Entity *entity, void *parameter); // Parameter: BinaryOperationPredicateParameter. Short-circuiting NOR operator. +BOOL XORPredicate(Entity *entity, void *parameter); // Parameter: BinaryOperationPredicateParameter. XOR operator. +BOOL NANDPredicate(Entity *entity, void *parameter); // Parameter: BinaryOperationPredicateParameter. NAND operator. + +BOOL HasScanClassPredicate(Entity *entity, void *parameter); // Parameter: NSNumber (int) +BOOL HasClassPredicate(Entity *entity, void *parameter); // Parameter: Class +BOOL IsShipPredicate(Entity *entity, void *parameter); // Parameter: ignored. Tests isShip and !isSubentity. +BOOL IsStationPredicate(Entity *entity, void *parameter); // Parameter: ignored. Tests isStation. +BOOL IsPlanetPredicate(Entity *entity, void *parameter); // Parameter: ignored. Tests isPlanet and planetType == PLANET_TYPE_GREEN. +BOOL IsSunPredicate(Entity *entity, void *parameter); // Parameter: ignored. Tests isSun. + +// These predicates assume their parameter is a ShipEntity. +BOOL HasRolePredicate(Entity *ship, void *parameter); // Parameter: NSString +BOOL HasPrimaryRolePredicate(Entity *ship, void *parameter); // Parameter: NSString +BOOL HasRoleInSetPredicate(Entity *ship, void *parameter); // Parameter: NSSet +BOOL HasPrimaryRoleInSetPredicate(Entity *ship, void *parameter); // Parameter: NSSet +#if TARGET_INCOMING_MISSILES +BOOL IsHostileAgainstTargetPredicate(Entity *ship, void *parameter); // Parameter: id +#endif diff --git a/src/Core/OOEntityFilterPredicate.m b/src/Core/OOEntityFilterPredicate.m new file mode 100644 index 00000000..9a6f6f4a --- /dev/null +++ b/src/Core/OOEntityFilterPredicate.m @@ -0,0 +1,176 @@ +/* + +OOEntityFilterPredicate.h + +Filters used to select entities in various contexts. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOEntityFilterPredicate.h" +#import "Entity.h" +#import "ShipEntity.h" +#import "PlanetEntity.h" +#import "OORoleSet.h" + + +BOOL YESPredicate(Entity *entity, void *parameter) +{ + return YES; +} + + +BOOL NOPredicate(Entity *entity, void *parameter) +{ + return NO; +} + + +BOOL NOTPredicate(Entity *entity, void *parameter) +{ + ChainedEntityPredicateParameter *param = parameter; + if (param == NULL || param->predicate == NULL) return NO; + + return !param->predicate(entity, param->parameter); +} + + +BOOL ANDPredicate(Entity *entity, void *parameter) +{ + BinaryOperationPredicateParameter *param = parameter; + + if (!param->predicate1(entity, param->parameter1)) return NO; + if (!param->predicate2(entity, param->parameter2)) return NO; + return YES; +} + + +BOOL ORPredicate(Entity *entity, void *parameter) +{ + BinaryOperationPredicateParameter *param = parameter; + + if (param->predicate1(entity, param->parameter1)) return YES; + if (param->predicate2(entity, param->parameter2)) return YES; + return NO; +} + + +BOOL NORPredicate(Entity *entity, void *parameter) +{ + BinaryOperationPredicateParameter *param = parameter; + + if (param->predicate1(entity, param->parameter1)) return NO; + if (param->predicate2(entity, param->parameter2)) return NO; + return YES; +} + + +BOOL XORPredicate(Entity *entity, void *parameter) +{ + BinaryOperationPredicateParameter *param = parameter; + BOOL A, B; + + A = param->predicate1(entity, param->parameter1); + B = param->predicate2(entity, param->parameter2); + + return (A || B) && !(A && B); +} + + +BOOL NANDPredicate(Entity *entity, void *parameter) +{ + BinaryOperationPredicateParameter *param = parameter; + BOOL A, B; + + A = param->predicate1(entity, param->parameter1); + B = param->predicate2(entity, param->parameter2); + + return !(A && B); +} + + +BOOL HasScanClassPredicate(Entity *entity, void *parameter) +{ + return [(id)parameter intValue] == [entity scanClass]; +} + + +BOOL HasClassPredicate(Entity *entity, void *parameter) +{ + return [entity isKindOfClass:(Class)parameter]; +} + + +BOOL IsShipPredicate(Entity *entity, void *parameter) +{ + return [entity isShip] && ![entity isSubEntity]; +} + + +BOOL IsStationPredicate(Entity *entity, void *parameter) +{ + return [entity isStation]; +} + + +BOOL IsPlanetPredicate(Entity *entity, void *parameter) +{ + if (!entity->isPlanet) return NO; + return ([(PlanetEntity *)entity planetType] == PLANET_TYPE_GREEN || [(PlanetEntity *)entity planetType] == PLANET_TYPE_MOON); +} + + +BOOL IsSunPredicate(Entity *entity, void *parameter) +{ + return [entity isSun]; +} + + +BOOL HasRolePredicate(Entity *ship, void *parameter) +{ + return [(ShipEntity *)ship hasRole:(NSString *)parameter]; +} + + +BOOL HasPrimaryRolePredicate(Entity *ship, void *parameter) +{ + return [(ShipEntity *)ship hasPrimaryRole:(NSString *)parameter]; +} + + +BOOL HasRoleInSetPredicate(Entity *ship, void *parameter) +{ + return [[(ShipEntity *)ship roleSet] intersectsSet:(NSSet *)parameter]; +} + + +BOOL HasPrimaryRoleInSetPredicate(Entity *ship, void *parameter) +{ + return [(NSSet *)parameter containsObject:[(ShipEntity *)ship primaryRole]]; +} + + +#if TARGET_INCOMING_MISSILES +BOOL IsHostileAgainstTargetPredicate(Entity *ship, void *parameter) +{ + return [(ShipEntity *)ship hasHostileTarget] && [(ShipEntity *)ship primaryTarget] == (ShipEntity *)parameter; +} +#endif diff --git a/src/Core/OOEquipmentType.h b/src/Core/OOEquipmentType.h new file mode 100644 index 00000000..9761d049 --- /dev/null +++ b/src/Core/OOEquipmentType.h @@ -0,0 +1,123 @@ +/* + +OOEquipmentType.h + +Manage the set of installed ships. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOTypes.h" + + +@interface OOEquipmentType: NSObject +{ +@private + OOTechLevelID _techLevel; + OOCreditsQuantity _price; + NSString *_name; + NSString *_identifier; + NSString *_description; + unsigned _isAvailableToAll: 1, + _requiresEmptyPylon: 1, + _requiresMountedPylon: 1, + _requiresClean: 1, + _requiresNotClean: 1, + _portableBetweenShips: 1, + _requiresFreePassengerBerth: 1, + _requiresFullFuel: 1, + _requiresNonFullFuel: 1, + _isMissileOrMine: 1; + OOCargoQuantity _requiredCargoSpace; + NSSet *_requiresEquipment; + NSSet *_requiresAnyEquipment; + NSSet *_incompatibleEquipment; + NSArray *_conditions; + + struct JSObject *_jsSelf; +} + ++ (void) loadEquipment; // Load equipment data; called on loading and when changing to/from strict mode. + ++ (NSArray *) allEquipmentTypes; ++ (NSEnumerator *) equipmentEnumerator; + ++ (OOEquipmentType *) equipmentTypeWithIdentifier:(NSString *)identifier; + +- (NSString *) identifier; +- (NSString *) damagedIdentifier; +- (NSString *) name; // localized +- (NSString *) descriptiveText; // localized +- (OOTechLevelID) techLevel; +- (OOCreditsQuantity) price; // Tenths of credits + +- (BOOL) isAvailableToAll; +- (BOOL) requiresEmptyPylon; +- (BOOL) requiresMountedPylon; +- (BOOL) requiresCleanLegalRecord; +- (BOOL) requiresNonCleanLegalRecord; +- (BOOL) requiresFreePassengerBerth; +- (BOOL) requiresFullFuel; +- (BOOL) requiresNonFullFuel; +- (BOOL) isPrimaryWeapon; +- (BOOL) isMissileOrMine; +- (BOOL) isPortableBetweenShips; + +- (OOCargoQuantity) requiredCargoSpace; +- (NSSet *) requiresEquipment; // Set of equipment identifiers; all items required +- (NSSet *) requiresAnyEquipment; // Set of equipment identifiers; any item required +- (NSSet *) incompatibleEquipment; // Set of equipment identifiers; all items prohibited + +// FIXME: should have general mechanism to handle scripts or legacy conditions. +- (NSArray *) conditions; + +@end + + +@interface OOEquipmentType (Conveniences) + +- (OOTechLevelID) effectiveTechLevel; + +@end diff --git a/src/Core/OOEquipmentType.m b/src/Core/OOEquipmentType.m new file mode 100644 index 00000000..80ecdcd5 --- /dev/null +++ b/src/Core/OOEquipmentType.m @@ -0,0 +1,412 @@ +/* + +OOEquipmentType.h + +Manage the set of installed ships. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOEquipmentType.h" +#import "Universe.h" +#import "OOCollectionExtractors.h" +#import "OOLegacyScriptWhitelist.h" + + +static NSArray *sEquipmentTypes = nil; +static NSDictionary *sEquipmentTypesByIdentifier = nil; + + +@interface OOEquipmentType (Private) + +- (id) initWithInfo:(NSArray *)info; + +@end + + +@implementation OOEquipmentType + ++ (void) loadEquipment +{ + NSArray *equipmentData = nil; + NSMutableArray *equipmentTypes = nil; + NSMutableDictionary *equipmentTypesByIdentifier = nil; + NSArray *itemInfo = nil; + OOEquipmentType *item = nil; + NSEnumerator *itemEnum = nil; + + equipmentData = [UNIVERSE equipmentData]; + + [sEquipmentTypes release]; + sEquipmentTypes = nil; + equipmentTypes = [NSMutableArray arrayWithCapacity:[equipmentData count]]; + [sEquipmentTypesByIdentifier release]; + sEquipmentTypesByIdentifier = nil; + equipmentTypesByIdentifier = [NSMutableDictionary dictionaryWithCapacity:[equipmentData count]]; + + for (itemEnum = [equipmentData objectEnumerator]; (itemInfo = [itemEnum nextObject]); ) + { + item = [[[OOEquipmentType alloc] initWithInfo:itemInfo] autorelease]; + if (item != nil) + { + [equipmentTypes addObject:item]; + [equipmentTypesByIdentifier setObject:item forKey:[item identifier]]; + } + } + + sEquipmentTypes = [equipmentTypes copy]; + sEquipmentTypesByIdentifier = [equipmentTypesByIdentifier copy]; +} + + ++ (NSArray *) allEquipmentTypes +{ + return sEquipmentTypes; +} + + ++ (NSEnumerator *) equipmentEnumerator +{ + return [sEquipmentTypes objectEnumerator]; +} + + ++ (OOEquipmentType *) equipmentTypeWithIdentifier:(NSString *)identifier +{ + return [sEquipmentTypesByIdentifier objectForKey:identifier]; +} + + +- (id) initWithInfo:(NSArray *)info +{ + BOOL OK = YES; + NSDictionary *extra = nil; + NSArray *conditions = nil; + + self = [super init]; + if (self == nil) OK = NO; + + if (OK && [info count] <= EQUIPMENT_LONG_DESC_INDEX) OK = NO; + + if (OK) + { + // Read required attributes + _techLevel = [info unsignedIntAtIndex:EQUIPMENT_TECH_LEVEL_INDEX]; + _price = [info unsignedIntAtIndex:EQUIPMENT_PRICE_INDEX]; + _name = [[info stringAtIndex:EQUIPMENT_SHORT_DESC_INDEX] retain]; + _identifier = [[info stringAtIndex:EQUIPMENT_KEY_INDEX] retain]; + _description = [[info stringAtIndex:EQUIPMENT_LONG_DESC_INDEX] retain]; + + if (_name == nil || _identifier == nil || _description == nil) + { + OOLog(@"equipment.load", @"***** ERROR: Invalid equipment.plist entry - missing name, identifier or description (\"%@\", %@, \"%@\")", _name, _identifier, _description); + OK = NO; + } + } + + if (OK) + { + // Implied attributes for backwards-compatibility + if ([_identifier hasSuffix:@"_MISSILE"] || [_identifier hasSuffix:@"_MINE"]) + { + _isMissileOrMine = YES; + _requiresEmptyPylon = YES; + } + else if ([_identifier isEqualToString:@"EQ_PASSENGER_BERTH_REMOVAL"]) + { + _requiresFreePassengerBerth = YES; + } + else if ([_identifier isEqualToString:@"EQ_FUEL"]) + { + _requiresNonFullFuel = YES; + } + } + + if (OK && [info count] > EQUIPMENT_EXTRA_INFO_INDEX) + { + // Read extra info dictionary + extra = [info dictionaryAtIndex:EQUIPMENT_EXTRA_INFO_INDEX]; + if (extra != nil) + { + _isAvailableToAll = [extra boolForKey:@"available_to_all" defaultValue:_isAvailableToAll]; + _requiresEmptyPylon = [extra boolForKey:@"requires_empty_pylon" defaultValue:_requiresEmptyPylon]; + _requiresMountedPylon = [extra boolForKey:@"requires_mounted_pylon" defaultValue:_requiresMountedPylon]; + _requiresClean = [extra boolForKey:@"requires_clean" defaultValue:_requiresClean]; + _requiresNotClean = [extra boolForKey:@"requires_not_clean" defaultValue:_requiresNotClean]; + _portableBetweenShips = [extra boolForKey:@"portable_between_ships" defaultValue:_portableBetweenShips]; + _requiresFreePassengerBerth = [extra boolForKey:@"requires_free_passenger_berth" defaultValue:_requiresFreePassengerBerth]; + _requiresFullFuel = [extra boolForKey:@"requires_full_fuel" defaultValue:_requiresFullFuel]; + _requiresNonFullFuel = [extra boolForKey:@"requires_non_full_fuel" defaultValue:_requiresNonFullFuel]; + + _requiredCargoSpace = [extra unsignedIntForKey:@"requires_cargo_space" defaultValue:_requiredCargoSpace]; + + id object = [extra objectForKey:@"requires_equipment"]; + if ([object isKindOfClass:[NSString class]]) _requiresEquipment = [[NSSet setWithObject:object] retain]; + else if ([object isKindOfClass:[NSArray class]]) _requiresEquipment = [[NSSet setWithArray:object] retain]; + else if (object != nil) + { + OOLog(@"equipment.load", @"***** ERROR: %@ for equipment item %@ is not a string or an array.", @"requires_equipment", _identifier); + } + + object = [extra objectForKey:@"requires_any_equipment"]; + if ([object isKindOfClass:[NSString class]]) _requiresAnyEquipment = [[NSSet setWithObject:object] retain]; + else if ([object isKindOfClass:[NSArray class]]) _requiresAnyEquipment = [[NSSet setWithArray:object] retain]; + else if (object != nil) + { + OOLog(@"equipment.load", @"***** ERROR: %@ for equipment item %@ is not a string or an array.", @"requires_any_equipment", _identifier); + } + + object = [extra objectForKey:@"incompatible_with_equipment"]; + if ([object isKindOfClass:[NSString class]]) _incompatibleEquipment = [[NSSet setWithObject:object] retain]; + else if ([object isKindOfClass:[NSArray class]]) _incompatibleEquipment = [[NSSet setWithArray:object] retain]; + else if (object != nil) + { + OOLog(@"equipment.load", @"***** ERROR: %@ for equipment item %@ is not a string or an array.", @"incompatible_with_equipment", _identifier); + } + + object = [extra objectForKey:@"conditions"]; + if ([object isKindOfClass:[NSString class]]) conditions = [NSArray arrayWithObject:object]; + else if ([object isKindOfClass:[NSArray class]]) conditions = object; + else if (object != nil) + { + OOLog(@"equipment.load", @"***** ERROR: %@ for equipment item %@ is not a string or an array.", @"conditions", _identifier); + } + if (conditions != nil) + { + _conditions = OOSanitizeLegacyScriptConditions(conditions, [NSString stringWithFormat:@"equipment type \"%@\"", _name]); + [_conditions retain]; + } + } + } + + if (!OK) + { + [self release]; + self = nil; + } + return self; +} + + +- (void) dealloc +{ + [_name release]; + [_identifier release]; + [_description release]; + [_requiresEquipment release]; + [_requiresAnyEquipment release]; + [_incompatibleEquipment release]; + [_conditions release]; + + [super dealloc]; +} + + +- (id) copyWithZone:(NSZone *)zone +{ + // OOEquipmentTypes are immutable. + return [self retain]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"%@ \"%@\"", _identifier, _name]; +} + + +- (NSString *) identifier +{ + return _identifier; +} + + +- (NSString *) damagedIdentifier +{ + return [_identifier stringByAppendingString:@"_DAMAGED"]; +} + + +- (NSString *) name +{ + return _name; +} + + +- (NSString *) descriptiveText +{ + return _description; +} + + +- (OOTechLevelID) techLevel +{ + return _techLevel; +} + + +- (OOCreditsQuantity) price +{ + return _price; +} + + +- (BOOL) isAvailableToAll +{ + return _isAvailableToAll; +} + + +- (BOOL) requiresEmptyPylon +{ + return _requiresEmptyPylon; +} + + +- (BOOL) requiresMountedPylon +{ + return _requiresMountedPylon; +} + + +- (BOOL) requiresCleanLegalRecord +{ + return _requiresClean; +} + + +- (BOOL) requiresNonCleanLegalRecord +{ + return _requiresNotClean; +} + + +- (BOOL) requiresFreePassengerBerth +{ + return _requiresFreePassengerBerth; +} + + +- (BOOL) requiresFullFuel +{ + return _requiresFullFuel; +} + + +- (BOOL) requiresNonFullFuel +{ + return _requiresNonFullFuel; +} + + +- (BOOL) isPrimaryWeapon +{ + return [[self identifier] hasPrefix:@"EQ_WEAPON"]; +} + + +- (BOOL) isMissileOrMine +{ + return _isMissileOrMine; +} + + +- (BOOL) isPortableBetweenShips +{ + return _portableBetweenShips; +} + + +- (OOCargoQuantity) requiredCargoSpace +{ + return _requiredCargoSpace; +} + + +- (NSSet *) requiresEquipment +{ + return _requiresEquipment; +} + + +- (NSSet *) requiresAnyEquipment +{ + return _requiresAnyEquipment; +} + + +- (NSSet *) incompatibleEquipment +{ + return _incompatibleEquipment; +} + + +- (NSArray *) conditions +{ + return _conditions; +} + +@end + + +#import "PlayerEntityLegacyScriptEngine.h" + +@implementation OOEquipmentType (Conveniences) + +- (OOTechLevelID) effectiveTechLevel +{ + OOTechLevelID tl; + id missionVar = nil; + + tl = [self techLevel]; + if (tl == kOOVariableTechLevel) + { + missionVar = [[PlayerEntity sharedPlayer] missionVariableForKey:[@"mission_TL_FOR_" stringByAppendingString:[self identifier]]]; + tl = OOUIntegerFromObject(missionVar, tl); + } + + return tl; +} + +@end diff --git a/src/Core/OOExcludeObjectEnumerator.h b/src/Core/OOExcludeObjectEnumerator.h new file mode 100644 index 00000000..99aaa3db --- /dev/null +++ b/src/Core/OOExcludeObjectEnumerator.h @@ -0,0 +1,48 @@ +/* + +OOExcludeObjectEnumerator.h + +Simple filter to exclude one object from an NSEnumerator. + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + + +@interface OOExcludeObjectEnumerator: NSEnumerator +{ +@private + NSEnumerator *_enumerator; + id _excludeObject; +} + ++ (id) enumeratorWithEnumerator:(NSEnumerator *)enumerator + excludingObject:(id)object; + +@end + + +@interface NSEnumerator (OOExcludingObject) + +- (id) ooExcludingObject:(id)object; + +@end diff --git a/src/Core/OOExcludeObjectEnumerator.m b/src/Core/OOExcludeObjectEnumerator.m new file mode 100644 index 00000000..0de7385d --- /dev/null +++ b/src/Core/OOExcludeObjectEnumerator.m @@ -0,0 +1,84 @@ +/* + +OOExcludeObjectEnumerator.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOExcludeObjectEnumerator.h" + + +@implementation OOExcludeObjectEnumerator + +- (id) initWithEnumerator:(NSEnumerator *)enumerator + excludingObject:(id)object +{ + if ((self = [super init])) + { + _enumerator = [enumerator retain]; + _excludeObject = [object retain]; + } + + return self; +} + + +- (void) dealloc +{ + [_enumerator release]; + [_excludeObject release]; + + [super dealloc]; +} + + ++ (id) enumeratorWithEnumerator:(NSEnumerator *)enumerator + excludingObject:(id)object +{ + if (object == nil) return enumerator; + if (enumerator == nil) return nil; + + return [[[self alloc] initWithEnumerator:enumerator excludingObject:object] autorelease]; +} + + +- (id) nextObject +{ + id result = nil; + do + { + result = [_enumerator nextObject]; + } while (result == _excludeObject && result != nil); + + return result; +} + +@end + + +@implementation NSEnumerator (OOExcludingObject) + +- (id) ooExcludingObject:(id)object +{ + return [OOExcludeObjectEnumerator enumeratorWithEnumerator:self excludingObject:object]; +} + +@end diff --git a/src/Core/OOFastArithmetic.h b/src/Core/OOFastArithmetic.h new file mode 100644 index 00000000..4fb78049 --- /dev/null +++ b/src/Core/OOFastArithmetic.h @@ -0,0 +1,298 @@ +/* + +OOFastArithmetic.h + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOFastArithmetic.h directly; include OOMaths.h. +#else + + +#ifdef WIN32 + #define FASTINVSQRT_ENABLED 0 /* Doesn't work on Windows (why?) */ +#else + #define FASTINVSQRT_ENABLED 0 /* Disabled due to precision problems. */ +#endif + + +/* OO_PPC: whether to use PowerPC instruction intrinsics like __fsel(). */ +#ifndef OO_PPC + #if defined(__ppc__) || defined(__ppc64__) + #define OO_PPC 1 + #else + #define OO_PPC 0 + #endif +#endif + + +/* (test > 0) ? a : b. Extra fast on PowerPC. */ +OOINLINE double OOSelect_d(double test, double a, double b) INLINE_CONST_FUNC; +OOINLINE float OOSelect_f(float test, float a, float b) INLINE_CONST_FUNC; + +/* Floating point reciprocal estimate, approximation of 1.0f / x. Precise within 1/256th of exact value. */ +OOINLINE float OOReciprocalEstimate(float value) INLINE_CONST_FUNC; + +/* Inverse square root and approximation of same. */ +#if OO_PPC +float OOInvSqrtf(float x) CONST_FUNC; +#else +OOINLINE float OOInvSqrtf(float x) INLINE_CONST_FUNC; +#endif +OOINLINE float OOFastInvSqrtf(float x) INLINE_CONST_FUNC; + +/* Round integer up to nearest power of 2. */ +OOINLINE uint32_t OORoundUpToPowerOf2(uint32_t x) INLINE_CONST_FUNC; + +OOINLINE float OOMin_f(float a, float b) INLINE_CONST_FUNC; +OOINLINE double OOMin_d(double a, double b) INLINE_CONST_FUNC; +OOINLINE float OOMax_f(float a, float b) INLINE_CONST_FUNC; +OOINLINE double OOMax_d(double a, double b) INLINE_CONST_FUNC; + +/* Clamp to range. */ +OOINLINE float OOClamp_0_1_f(float value) INLINE_CONST_FUNC; +OOINLINE double OOClamp_0_1_d(double value) INLINE_CONST_FUNC; +OOINLINE float OOClamp_0_max_f(float value, float max) INLINE_CONST_FUNC; +OOINLINE double OOClamp_0_max_d(double value, double max) INLINE_CONST_FUNC; + + +#if OO_PPC + #ifdef __MWERKS__ + OOINLINE double OOSelect_d(double test, double a, double b) + { + return __fsel(test, a, b); + } + + + OOINLINE float OOSelect_f(float test, float a, float b) + { + return __fsel(test, a, b); + } + + + OOINLINE float OOReciprocalEstimate(float value) + { + return __fres(value); + } + #define OOFASTARITHMETIC_USED_PPC + #elif defined(__GNUC__) + /* Taken from under Mac OS X */ + OOINLINE double OOSelect_d(double test, double a, double b) + { + double result; + __asm__ ("fsel %0,%1,%2,%3" + /* outputs: */ : "=f" (result) + /* inputs: */ : "f" (test), "f" (a), "f" (b)); + return result; + } + + OOINLINE float OOSelect_f(float test, float a, float b) + { + float result; + __asm__ ("fsel %0,%1,%2,%3" + /* outputs: */ : "=f" (result) + /* inputs: */ : "f" (test), "f" (a), "f" (b)); + return result; + } + + OOINLINE float OOReciprocalEstimate(float value) + { + float estimate; + __asm__ ("fres %0,%1" + /* outputs: */ : "=f" (estimate) + /* inputs: */ : "f" (value)); + return estimate; + } + #define OOFASTARITHMETIC_USED_PPC + #else + #warning Unknown compiler - not sure how to use PowerPC intrinsics. Using less efficient methods. + #define OO_PPC_fsel(test, a, b) (((test)>0)?(a):(b)) + #define OO_PPC_fsels(test, a, b) OO_PPC_fsel(test, a, b) + #define OO_PPC_fres(val) (1.0f/(val)) + #endif +#endif + + +#ifndef OOFASTARITHMETIC_USED_PPC + OOINLINE double OOSelect_d(double test, double a, double b) + { + return (test > 0) ? a : b; + } + + + OOINLINE float OOSelect_f(float test, float a, float b) + { + return (test > 0) ? a : b; + } + + + OOINLINE float OOReciprocalEstimate(float value) + { + return 1.0f / value; + } +#endif + + +#if !OO_PPC +OOINLINE float OOInvSqrtf(float x) +{ + return 1.0f/sqrtf(x); +} +#endif + + +OOINLINE float OOFastInvSqrtf(float x) +{ +/* This appears to have been responsible for a lack of laser accuracy, as + well as not working at all under Windows. Disabled for now. + Could probably be made faster on PPC using frsqrte[s], but would need to + ensure precision. +*/ +#if FASTINVSQRT_ENABLED + float xhalf = 0.5f * x; + int i = *(int*)&x; + i = 0x5f375a86 - (i>>1); + x = *(float*)&i; + x = x * (1.5f - xhalf * x * x); + return x; +#elif OO_PPC + return OOInvSqrtf(x); +#else + return OOReciprocalEstimate(sqrt(x)); +#endif +} + + +#ifdef __GNUC__ + OOINLINE uint32_t OORoundUpToPowerOf2(uint32_t value) + { + return 0x80000000 >> (__builtin_clz(value - 1) - 1); + } +#elif OO_PPC && defined(__MWERKS__) + OOINLINE uint32_t OORoundUpToPowerOf2(uint32_t value) + { + return 0x80000000 >> (__cntlzw(value - 1) - 1); + } +#else + OOINLINE uint32_t OORoundUpToPowerOf2(uint32_t value) + { + value -= 1; + value |= (value >> 1); + value |= (value >> 2); + value |= (value >> 4); + value |= (value >> 8); + value |= (value >> 16); + return value + 1; + } +#endif + + +#ifdef OOFASTARITHMETIC_USED_PPC + OOINLINE float OOMin_f(float a, float b) + { + return OOSelect_f(a - b, b, a); + } + + OOINLINE double OOMin_d(double a, double b) + { + return OOSelect_d(a - b, b, a); + } + + OOINLINE float OOMax_f(float a, float b) + { + return OOSelect_f(a - b, a, b); + } + + OOINLINE double OOMax_d(double a, double b) + { + return OOSelect_d(a - b, a, b); + } + + OOINLINE float OOClamp_0_1_f(float value) + { + float clampUpper = OOSelect_f(value - 1.0f, 1.0f, value); + return OOSelect_f(value, clampUpper, 0.0f); + } + + OOINLINE double OOClamp_0_1_d(double value) + { + float clampUpper = OOSelect_d(value - 1.0, 1.0, value); + return OOSelect_d(value, clampUpper, 0.0); + } + + OOINLINE float OOClamp_0_max_f(float value, float max) + { + float clampUpper = OOSelect_f(value - max, max, value); + return OOSelect_f(value, clampUpper, 0.0f); + } + + OOINLINE double OOClamp_0_max_d(double value, double max) + { + double clampUpper = OOSelect_d(value - max, max, value); + return OOSelect_d(value, clampUpper, 0.0); + } +#else + OOINLINE float OOMin_f(float a, float b) + { + return fminf(a, b); + } + + OOINLINE double OOMin_d(double a, double b) + { + return fmin(a, b); + } + + OOINLINE float OOMax_f(float a, float b) + { + return fmaxf(a, b); + } + + OOINLINE double OOMax_d(double a, double b) + { + return fmax(a, b); + } + + OOINLINE float OOClamp_0_1_f(float value) + { + return fmaxf(0.0f, fminf(value, 1.0f)); + } + + OOINLINE double OOClamp_0_1_d(double value) + { + return fmax(0.0f, fmin(value, 1.0f)); + } + + OOINLINE float OOClamp_0_max_f(float value, float max) + { + return fmaxf(0.0f, fminf(value, max)); + } + + OOINLINE double OOClamp_0_max_d(double value, double max) + { + return fmax(0.0, fmin(value, max)); + } +#endif + + +#endif /* INCLUDED_OOMATHS_h */ diff --git a/src/Core/OOFastArithmetic.m b/src/Core/OOFastArithmetic.m new file mode 100644 index 00000000..afd815af --- /dev/null +++ b/src/Core/OOFastArithmetic.m @@ -0,0 +1,138 @@ +/* + +OOFastArithmetic.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOMaths.h" + + +#if OO_PPC + +#ifndef __llvm__ +/* OOInvSqrtf() + Based on G3-otimized fsqrtf() by Conn Clark. The only difference is that + it does not store and multiply by the original value to go from reciprocal + square root to square root. + The original is documented as having full precision if frsqrte has at + least 1/59 precision as on the G3, and an error of no more than 1/4.22e+06 + if frsqrte has 1/32 (the minimum allowable precision) as on the G5. + Informal testing shows errors up to 1.2e-07 on a G5 for this version, at + about an 18% speed increase over 1.0f/sqrtf(x); the performance advantage + is expected to be more significant on G4 and G3 processors (the G5 has + a sqare root instruction). + + The original was found here: + http://sources.redhat.com/ml/libc-alpha/2006-12/txt00004.txt + and is LGPL-licensed. This version may also be distributed under the GNU + Lesser General Public License, in addition the GPL as specified in the + file header. + + This may not build under Linux-PPC; it may need to be modified to use + GCC's crazy asm syntax. +*/ +asm float OOInvSqrtf(float x) +{ + /* start loading some constants for integer comparison */ + lis r3,0x3f00 /* 0.5F equiv as an integer */ + lis r4,0x3FC0 /* 1.5F equiv as an integer */ + stfsu f1,-12(r1) /* store original and get 12 bytes of stack space. */ + stw r3,4(r1) /* store 0.5F on the stack to be loaded by fpu */ + mffs f6 /* store fpu configuration */ + stw r4,8(r1) /* store 1.5F on the stack to be loaded by fpu */ + lis r7, 0x7F80 /* load NAN for testing */ + lfs f2,4(r1) /* load 0.5F into fpu reg 2 */ + lwz r5,0(r1) /* load original value as an int for testing */ + fmr f9,f1 /* copy original value into fpu reg 9 */ + cmpi 0,r5,0x0000 /* test for positive zero */ + rlwinm r12,r5,0,0,1 /* mask off sign bit and store in reg 12*/ + lfs f7,8(r1) /* load 1.5F into fpu reg 7 */ + cmpl 1,r12,r7 /* test for NAN results in cr1 */ + ble neg_number_or_zero /* branch if less than or equal to zero */ + frsqrte f1,f1 /* get recip sqrt estimate (does no harm if input is NaN */ + beq cr1, not_a_number /* branch if original value was not a number */ + fmul f2,f2,f9 /* begin Goldschmidt */ + fmuls f3,f1,f1 /* single-precision saves one clock without affecting accuracy */ + fmul f4,f2,f3 + fnmsubs f3,f2,f3,f7 /* single-precision saves one clock without affecting accuracy */ + fmul f5,f3,f3 + fmul f1,f3,f1 + fnmsubs f3,f4,f5,f7 /* single-precision saves one clock without affecting accuracy */ + fmul f4,f4,f5 + fmul f1,f3,f1 + fmul f5,f3,f3 + fnmsub f3,f4,f5,f7 + fmul f1,f3,f1 + addi r1,r1,12 /* clean up stack */ + mtfsf 0xff,f6 /* restore fpu state */ + frsp f1,f1 /* round result to a float */ + blr /* return */ + +neg_number_or_zero: + lis r9,0x3F80 /* load up equiv of 1.0F */ + stw r9,8(r1) /* store 1.0F to where fpu can load it */ + lis r6, 0x8000 /* negative */ + lfs f2,8(r1) /* load 1.0F into fpu reg 2 */ + beq its_zero /* branch if zero */ + + /* This bit sets FPU status bits for negative number case, and isn't strictly needed for Oolite. */ + cmpl 0,r5,r6 /* test for negative zero */ + beq its_zero /* branch if zero */ + stfs f6,0(r1) /* store fpu status */ + lis r3,0x7FC0 /* load aNaN */ + lis r4,0x2000 /* load FE_INVALID flag */ + lwz r5,0(r1) /* load fpu status in gp register */ + ori r4,r4,0x0200 /* load INV_SQRT flag */ + or r5,r5,r4 /* or FE_INVALID and INV_SQRT flags with fpu status */ + stw r3,4(r1) /* store aNaN on stack */ + stw r5,0(r1) /* store fpu status */ + lfs f6,0(r1) /* load new fpu status */ + lfs f1,4(r1) /* load aNaN to be returned */ + mtfsf 0xff,f6 /* update fpu status to new value */ + +its_zero: + fmuls f1,f1,f2 /* multiply by 1.0 to set appropriate status bits */ + addi r1,r1,12 /* clean up stack */ + blr /* return */ + +not_a_number: + lis r6,0x3F80 /* load up equiv of 1.0F */ + stw r6,8(r1) /* store 1.0F to where fpu can load it */ + lfs f2,8(r1) /* load 1.0F into fpu reg 2 */ + mtfsf 0xff,f6 /* restore fpu status */ + fmuls f1,f1,f2 /* multiply by 1.0 to set appropriate status bits */ + addi r1,r1,12 /* clean up stack */ + blr /* return */ +} + +#else + +#import + +// LLVM-GCC doesn't support asm at the moment. +float OOInvSqrtf(float x) +{ + return 1.0 / sqrtf(x); +} + +#endif +#endif diff --git a/src/Core/OOFilteringEnumerator.h b/src/Core/OOFilteringEnumerator.h new file mode 100644 index 00000000..2e02194f --- /dev/null +++ b/src/Core/OOFilteringEnumerator.h @@ -0,0 +1,137 @@ +/* + +OOFilteringEnumerator.h +By Jens Ayton + +NSEnumerator which takes an existing enumerator and filters out the objects +that return NO from a given method. The method may take 0 or 1 arguments + +Example of use: + NSArray *cats = [self cats]; + NSEnumerator *happyCatEnum = [[cats objectEnumerator] filteredWithSelector:@selector(isHappy)]; + id happyCat = nil; + + while ((happyCat = [happyCatEnum nextObject])) + { + ... + } + +Filters can be trivially chained. For instance, to get happy red cats, use: + NSEnumeratore *happyRedCatEnum = [[[cats objectEnumerator] + filteredWithSelector:@selector(isHappy)] + filteredWithSelector:@selector(hasColor:) + andArgument:[NSColor redColor]]; + +Objects that do not respond to the filter selector are treated as if they had +returned NO. + +Bonus feature: adds NSArray-like (but non-exception-throwing) +makeObjectsPerformSelector: to all enumerators. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@interface OOFilteringEnumerator: NSEnumerator +{ + NSEnumerator *_underlyingEnum; + SEL _selector; + id _argument; + BOOL _takesArgument; +} + ++ (id) filterEnumerator:(NSEnumerator *)enumerator withSelector:(SEL)selector; ++ (id) filterEnumerator:(NSEnumerator *)enumerator withSelector:(SEL)selector andArgument:(id)argument; + +- (id) initWithUnderlyingEnumerator:(NSEnumerator *)enumerator + withSelector:(SEL)selector + takingArgument:(BOOL)takesArgument + argumentValue:(id)argument; + +@end + + +@interface NSEnumerator (OOFilteringEnumerator) + +- (id) filteredWithSelector:(SEL)selector; +- (id) filteredWithSelector:(SEL)selector andArgument:(id)argument; + +@end + + +@interface NSArray (OOFilteringEnumerator) + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector; +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument; + +@end + + +@interface NSSet (OOFilteringEnumerator) + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector; +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument; + +@end + + +@interface NSDictionary (OOFilteringEnumerator) + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector; +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument; + +- (id) keyEnumeratorFilteredWithSelector:(SEL)selector; +- (id) keyEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument; + +@end + + +@interface NSEnumerator (OOMakeObjectsPerformSelector) + +- (void)makeObjectsPerformSelector:(SEL)selector; +- (void)makeObjectsPerformSelector:(SEL)selector withObject:(id)argument; + +@end diff --git a/src/Core/OOFilteringEnumerator.m b/src/Core/OOFilteringEnumerator.m new file mode 100644 index 00000000..553bb3c6 --- /dev/null +++ b/src/Core/OOFilteringEnumerator.m @@ -0,0 +1,280 @@ +/* + +OOFilteringEnumerator.m +By Jens Ayton + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOFilteringEnumerator.h" + + +typedef BOOL (*BoolReturnMsgSend)(id, SEL); +typedef BOOL (*BoolReturnWithParamMsgSend)(id, SEL, id); + + +@implementation OOFilteringEnumerator + ++ (id) filterEnumerator:(NSEnumerator *)enumerator withSelector:(SEL)selector +{ + if (selector == NULL) return [[enumerator retain] autorelease]; + + return [[[self alloc] initWithUnderlyingEnumerator:enumerator + withSelector:selector + takingArgument:NO + argumentValue:nil] + autorelease]; +} + + ++ (id) filterEnumerator:(NSEnumerator *)enumerator withSelector:(SEL)selector andArgument:(id)argument +{ + if (selector == NULL) return [[enumerator retain] autorelease]; + + return [[[self alloc] initWithUnderlyingEnumerator:enumerator + withSelector:selector + takingArgument:YES + argumentValue:argument] + autorelease]; +} + +- (id) initWithUnderlyingEnumerator:(NSEnumerator *)enumerator + withSelector:(SEL)selector + takingArgument:(BOOL)takesArgument + argumentValue:(id)argument +{ + self = [super init]; + if (self != nil) + { + _underlyingEnum = [enumerator retain]; + _selector = selector; + _takesArgument = takesArgument; + if (_takesArgument) + { + _argument = [argument retain]; + } + } + return self; +} + + +- (void) dealloc +{ + [_underlyingEnum release]; + [_argument release]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + NSString *subDesc = NSStringFromSelector(_selector); + if (_takesArgument) + { + subDesc = [subDesc stringByAppendingString:[_argument shortDescription]]; + } + + return [NSString stringWithFormat:@"%@ matching %@", [_underlyingEnum shortDescription], subDesc]; +} + + +- (NSString *) shortDescriptionComponents +{ + return NSStringFromSelector(_selector); +} + + +- (id) nextObject +{ + for (;;) + { + // Get next object + id obj = [_underlyingEnum nextObject]; + BOOL filter; + + if (obj == nil) + { + // End of enumeration + if (_underlyingEnum != nil) + { + [_underlyingEnum release]; + _underlyingEnum = nil; + [_argument release]; + _argument = nil; + } + return nil; + } + + // Check against filter + IMP predicate = [obj methodForSelector:_selector]; + if (predicate != NULL) + { + if (!_takesArgument) + { + filter = ((BoolReturnMsgSend)predicate)(obj, _selector); + } + else + { + filter = ((BoolReturnWithParamMsgSend)predicate)(obj, _selector, _argument); + } + } + else + { + // Unsupported method + filter = NO; + } + + // If object passed, return it. + if (filter) return obj; + } +} + +@end + + +@implementation NSEnumerator (OOFilteringEnumerator) + +- (id) filteredWithSelector:(SEL)selector +{ + return [OOFilteringEnumerator filterEnumerator:self withSelector:selector]; +} + + +- (id) filteredWithSelector:(SEL)selector andArgument:(id)argument +{ + return [OOFilteringEnumerator filterEnumerator:self withSelector:selector andArgument:argument]; +} + +@end + + +@implementation NSArray (OOFilteringEnumerator) + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector +{ + return [[self objectEnumerator] filteredWithSelector:selector]; +} + + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument +{ + return [[self objectEnumerator] filteredWithSelector:selector andArgument:argument]; +} + +@end + + +@implementation NSSet (OOFilteringEnumerator) + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector +{ + return [[self objectEnumerator] filteredWithSelector:selector]; +} + + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument +{ + return [[self objectEnumerator] filteredWithSelector:selector andArgument:argument]; +} + +@end + + +@implementation NSDictionary (OOFilteringEnumerator) + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector +{ + return [[self objectEnumerator] filteredWithSelector:selector]; +} + + +- (id) objectEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument +{ + return [[self objectEnumerator] filteredWithSelector:selector andArgument:argument]; +} + + +- (id) keyEnumeratorFilteredWithSelector:(SEL)selector +{ + return [[self keyEnumerator] filteredWithSelector:selector]; +} + + +- (id) keyEnumeratorFilteredWithSelector:(SEL)selector andArgument:(id)argument +{ + return [[self keyEnumerator] filteredWithSelector:selector andArgument:argument]; +} + +@end + + +@implementation NSEnumerator (OOMakeObjectsPerformSelector) + +- (void)makeObjectsPerformSelector:(SEL)selector +{ + id object = nil; + while ((object = [self nextObject])) + { + if (selector != NULL && [object respondsToSelector:selector]) + { + [object performSelector:selector]; + } + } +} + + +- (void)makeObjectsPerformSelector:(SEL)selector withObject:(id)argument +{ + id object = nil; + while ((object = [self nextObject])) + { + if (selector != NULL && [object respondsToSelector:selector]) + { + [object performSelector:selector withObject:argument]; + } + } +} + +@end diff --git a/src/Core/OOFunctionAttributes.h b/src/Core/OOFunctionAttributes.h new file mode 100644 index 00000000..c1659f05 --- /dev/null +++ b/src/Core/OOFunctionAttributes.h @@ -0,0 +1,42 @@ +#ifndef INCLUDED_OOFUNCTIONATTRIBUTES_h +#define INCLUDED_OOFUNCTIONATTRIBUTES_h + + +#ifndef GCC_ATTR + #ifdef __GNUC__ + #define GCC_ATTR(x) __attribute__(x) + #else + #define GCC_ATTR(x) + #endif +#endif + + +#define OOINLINE static inline + + +#if !OO_DEBUG +#define ALWAYS_INLINE_FUNC GCC_ATTR((always_inline)) // Force inlining of function +#else +#define ALWAYS_INLINE_FUNC // Don't force inlining of function (because gdb is silly) +#endif + +#define PURE_FUNC GCC_ATTR((pure)) // result dependent only on params and globals +#define CONST_FUNC GCC_ATTR((const)) // pure + no pointer dereferences or globals +#define NONNULL_FUNC GCC_ATTR((nonnull)) // Pointer parameters may not be NULL +#define DEPRECATED_FUNC GCC_ATTR((deprecated)) // Warn if this function is used +#define NO_RETURN_FUNC GCC_ATTR((noreturn)) // Function can never return + +#define INLINE_PURE_FUNC ALWAYS_INLINE_FUNC PURE_FUNC +#define INLINE_CONST_FUNC ALWAYS_INLINE_FUNC CONST_FUNC + + +#ifdef __GNUC__ + #define EXPECT(x) __builtin_expect((x), 1) + #define EXPECT_NOT(x) __builtin_expect((x), 0) +#else + #define EXPECT(x) (x) + #define EXPECT_NOT(x) (x) +#endif + + +#endif /* INCLUDED_OOFUNCTIONATTRIBUTES_h */ diff --git a/src/Core/OOGraphicsResetManager.h b/src/Core/OOGraphicsResetManager.h new file mode 100644 index 00000000..3b4c2124 --- /dev/null +++ b/src/Core/OOGraphicsResetManager.h @@ -0,0 +1,80 @@ +/* + +OOGraphicsResetManager.h + +Tracks objects with state that needs to be reset when the graphics context is +modified (for instance, when switching between windowed and full-screen mode +in SDL builds). This means re-uploading all textures, and also resetting any +display lists relying on old texture names. All objects which have display +lists must therefore register with the OOGraphicsResetManager on init, and +unregister on dealloc. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@protocol OOGraphicsResetClient + +- (void)resetGraphicsState; + +@end + + +@interface OOGraphicsResetManager: NSObject +{ + NSMutableSet *clients; +} + ++ (id)sharedManager; + +// Clients are not retained. +- (void)registerClient:(id)client; +- (void)unregisterClient:(id)client; + +// Forwarded to all clients, after resetting textures. +- (void)resetGraphicsState; + +@end diff --git a/src/Core/OOGraphicsResetManager.m b/src/Core/OOGraphicsResetManager.m new file mode 100644 index 00000000..27bdbfeb --- /dev/null +++ b/src/Core/OOGraphicsResetManager.m @@ -0,0 +1,153 @@ +/* + +OOGraphicsResetManager.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOGraphicsResetManager.h" +#import "OOTexture.h" + + +static OOGraphicsResetManager *sSingleton = nil; + + +@implementation OOGraphicsResetManager + +- (void)dealloc +{ + if (sSingleton == self) sSingleton = nil; + [clients release]; + + [super dealloc]; +} + + ++ (id)sharedManager +{ + if (sSingleton == nil) [[self alloc] init]; + return sSingleton; +} + + +- (void)registerClient:(id)client +{ + if (client != nil) + { + if (clients == nil) clients = [[NSMutableSet alloc] init]; + [clients addObject:[NSValue valueWithPointer:client]]; + } +} + + +- (void)unregisterClient:(id)client +{ + [clients removeObject:[NSValue valueWithPointer:client]]; +} + + +- (void)resetGraphicsState +{ + NSEnumerator *clientEnum = nil; + id client = nil; + + [OOTexture rebindAllTextures]; + + for (clientEnum = [clients objectEnumerator]; (client = [[clientEnum nextObject] pointerValue]); ) + { + [client resetGraphicsState]; + } +} + +@end + + +@implementation OOGraphicsResetManager (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedManager above. + + // NOTE: assumes single-threaded first access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end diff --git a/src/Core/OOInstinct.h b/src/Core/OOInstinct.h new file mode 100644 index 00000000..45d298b1 --- /dev/null +++ b/src/Core/OOInstinct.h @@ -0,0 +1,117 @@ +/* + +OOInstinct.h + +Part of NPC behaviour implementation. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#ifdef OO_BRAIN_AI + +#import "OOCocoa.h" +#import "OOMaths.h" +#import "legacy_random.h" +#import "OOTypes.h" + + +@class Entity, ShipEntity, OOCharacter, AI; + +@interface OOInstinct : NSObject { + + // + // associations + // + id owner; // could be a ShipEntity or an OOCharacter + // + ShipEntity *ship; // to exchange information + OOUniversalID ship_id; // to double check ship is within universe + // + OOInstinctID type; // what this instinct IS + + // AI (usually nil) + AI* ai; // will not be used except in INSTINCT_FOLLOW_AI + + // variables which will be controlled by instincts + Vector destination; // for flying to or from a set point, need not be used + OOUniversalID target_id; // was primaryTarget; // for combat or rendezvous, may be NO_TARGET + GLfloat desired_range; // range to which to journey/scan + GLfloat desired_speed; // speed at which to travel, may be much greater than maxFlightSpeed of ship + OOBehaviour behaviour; // ship's intended behavioural state if this instinct is followed + + Vector saved_destination; + OOUniversalID saved_target_id; + GLfloat saved_desired_range; + GLfloat saved_desired_speed; + OOBehaviour saved_behaviour; + + // priorities... + GLfloat priority_in; // how much this matters to the owner + GLfloat priority_out; // how much important it is to follow this instinct +} + + +- (GLfloat) evaluateInstinctWithEntities:(Entity**) entities; // performs necessary calculations for the instinct and returns priority_out +- (GLfloat) priority; // returns priority_out without calculation + +// main instincts +// +// INSTINCT_NULL 0 +- (void) instinct_null; +// +// INSTINCT_AVOID_HAZARDS 101 +- (void) instinct_avoid_hazards:(Entity**) entities; +// +// INSTINCT_FLOCK_ALIKE 102 +- (void) instinct_flock_alike:(Entity**) entities; +// +// INSTINCT_FIGHT_OR_FLIGHT 103 +- (void) instinct_fight_or_flight:(Entity**) entities; +// +// INSTINCT_ATTACK_PREY 105 +- (void) instinct_attack_prey:(Entity**) entities; +// +// INSTINCT_AVOID_PREDATORS 106 +- (void) instinct_avoid_predators:(Entity**) entities; +// +// INSTINCT_FOLLOW_AI 201 +- (void) instinct_follow_ai; + + +- (void) freezeShipVars; +- (void) unfreezeShipVars; + +- (void) setShipVars; +- (void) getShipVars; + +- (void) setDestination:(Vector) value; +- (void) setTargetID:(int) value; +- (void) setDesiredRange:(GLfloat) value; +- (void) setDesiredSpeed:(GLfloat) value; +- (void) setBehaviour:(int) value; +- (void) setPriority:(GLfloat) value; + +- (id) initInstinctOfType:(int) aType ofPriority:(GLfloat)aPriority forOwner:(id) anOwner withShip:(ShipEntity*) aShip; + +- (void)dumpState; + +@end + +#endif /* OO_BRAIN_AI */ diff --git a/src/Core/OOInstinct.m b/src/Core/OOInstinct.m new file mode 100644 index 00000000..23b905ad --- /dev/null +++ b/src/Core/OOInstinct.m @@ -0,0 +1,286 @@ +/* + +OOInstinct.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOInstinct.h" +#import "ShipEntity.h" +#import "Universe.h" +#import "AI.h" +#import "OOConstToString.h" + +#define kOOLogUnconvertedNSLog @"unclassified.OOInstinct" + + +@implementation OOInstinct + + +- (GLfloat) evaluateInstinctWithEntities:(Entity**) entities // performs necessary calculations for the instinct and returns priority_out +{ + // is the ship in the universe? + if (ship->universalID == NO_TARGET) return 0.0f; + + // is the ship still as set when initialised? + if (ship_id != ship->universalID) return 0.0f; + + // does this instinct have any priority? + if (priority_in == 0.0f) return 0.0f; + + priority_out = 0.0f; // reset + // + // todo by type + switch (type) + { + case INSTINCT_NULL: + [self instinct_null]; + break; + + case INSTINCT_FOLLOW_AI: + [self instinct_follow_ai]; + break; + + default: + break; + } + // + priority_out *= priority_in; // factor preference + // + return priority_out; +} + +- (GLfloat) priority // returns priority_out without calculation +{ + return priority_out; +} + +// main instincts +// INSTINCT_NULL 0 +- (void) instinct_null +{ + priority_out = 0.0f; // TODO +} + +// basic behavioural instincts +// +// INSTINCT_AVOID_HAZARDS 101 +- (void) instinct_avoid_hazards:(Entity**) entities +{ + GLfloat max_so_far = 0.0; + Entity* avoid_target = nil; + int entity_index = 0; + Vector u = [ship velocity]; + GLfloat cr = ship->collision_radius; + // + + OOLog(@"instinct.test.avoidHazards", @"\nTESTING instinct_avoid hazards %@", self); + + while (entities[entity_index]) + { + Entity* ent = entities[entity_index++]; + + Vector rp = vector_subtract(ent->position, ship->position); + GLfloat sz = ent->collision_radius + cr; + GLfloat d = sqrtf(magnitude2(rp)) - sz; + if (d < 0.01) + d = 0.01; // 1 cm is suitably small + Vector rv = vector_subtract([ent velocity], u); + GLfloat approach_v = dot_product(rv, vector_normal(rp)); + + GLfloat assessment = sz * approach_v / d; // == size x approach velocity / distance + + OOLog(@"instinct.test.avoidHazards", @"TESTING instinct_avoid hazards %@ assessment of %@ : %.2f", ship, ent, assessment); + + if (assessment > max_so_far) + { + avoid_target = ent; + max_so_far = assessment; + } + } + // + if (!avoid_target) + { + priority_out = 0.0f; + target_id = NO_TARGET; + behaviour = BEHAVIOUR_FLEE_TARGET; + } + else + { + priority_out = max_so_far; + } + priority_out = 0.0f; // TODO +} +// +// INSTINCT_FLOCK_ALIKE 102 +- (void) instinct_flock_alike:(Entity**) entities +{ + priority_out = 0.0f; // TODO +} +// +// INSTINCT_FIGHT_OR_FLIGHT 103 +- (void) instinct_fight_or_flight:(Entity**) entities +{ + priority_out = 0.0f; // TODO +} +// +// INSTINCT_ATTACK_PREY 105 +- (void) instinct_attack_prey:(Entity**) entities +{ + priority_out = 0.0f; // TODO +} +// INSTINCT_AVOID_PREDATORS 106 +- (void) instinct_avoid_predators:(Entity**) entities +{ + priority_out = 0.0f; // TODO +} +// +// INSTINCT_FOLLOW_AI 201 +- (void) instinct_follow_ai +{ + double ut = [UNIVERSE getTime]; + if (ut > [ai nextThinkTime]) + { + [ai think]; + [ai setNextThinkTime: ut + [ai thinkTimeInterval]]; + } + priority_out = 1.0f; // constant - only adjusted by priority_in +} +// +//// + + +- (void) freezeShipVars +{ + if ([ship universalID] != 0) + { + saved_destination = ship->destination; + saved_desired_range = ship->desired_range; + saved_desired_speed = ship->desired_speed; + saved_behaviour = ship->behaviour; + saved_target_id = ship->primaryTarget; + } +} + +- (void) unfreezeShipVars +{ + if ([ship universalID] != 0) + { + ship->destination = saved_destination; + ship->desired_range = saved_desired_range; + ship->desired_speed = saved_desired_speed; + ship->behaviour = saved_behaviour; + ship->primaryTarget = saved_target_id; + } +} + +- (void) setShipVars +{ + if ([ship universalID] != 0) + { + ship->destination = destination; + ship->desired_range = desired_range; + ship->desired_speed = desired_speed; + ship->behaviour = behaviour; + ship->primaryTarget = target_id; + } +} + +- (void) getShipVars +{ + if ([ship universalID] != 0) + { + destination = ship->destination; + desired_range = ship->desired_range; + desired_speed = ship->desired_speed; + behaviour = ship->behaviour; + target_id = ship->primaryTarget; + } +} + + +- (void) setDestination:(Vector) value +{ + destination = value; +} + +- (void) setTargetID:(int) value +{ + target_id = value; +} + +- (void) setDesiredRange:(GLfloat) value +{ + desired_range = value; +} + +- (void) setDesiredSpeed:(GLfloat) value +{ + desired_speed = value; +} + +- (void) setBehaviour:(int) value +{ + behaviour = value; +} + +- (void) setPriority:(GLfloat) value +{ + priority_in = value; + priority_out = 0.0f; +} + +// aShip must be in the universe for the following to work correctly +- (id) initInstinctOfType:(int) aType ofPriority:(GLfloat)aPriority forOwner:(id) anOwner withShip:(ShipEntity*) aShip +{ + self = [super init]; + + type = aType; + owner = anOwner; + ship = aShip; + ship_id = [ship universalID]; + + if (type == INSTINCT_FOLLOW_AI) + { + ai = [ship getAI]; + [ai setRulingInstinct:self]; + } + else + { + ai = (AI*)nil; + } + + priority_in = aPriority; + priority_out = 0.0f; + + return self; +} + + +- (void)dumpState +{ + OOLog(@"dumpState.instinct", @"Instinct type: %@", InstinctToString(type)); + OOLog(@"dumpState.instinct", @"Destination: %@", VectorDescription(destination)); + if (target_id != NO_TARGET) OOLog(@"dumpState.instinct", @"Target: %@", [UNIVERSE entityForUniversalID:target_id]); + OOLog(@"dumpState.instinct", @"Desired speed: %g", desired_speed); + OOLog(@"dumpState.instinct", @"Behaviour: %@", BehaviourToString(behaviour)); +} + +@end diff --git a/src/Core/OOIsNumberLiteral.h b/src/Core/OOIsNumberLiteral.h new file mode 100644 index 00000000..5f1e7dad --- /dev/null +++ b/src/Core/OOIsNumberLiteral.h @@ -0,0 +1,50 @@ +/* + OOISNumberLiteral.h + + Utility function to recognize certain number literals, corresponding to + C-style decimal integer literals and floating point literals without + trailing type suffixes. + + + Copyright (C) 2008 Jens Ayton + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#import + + +/* Recognise C-style decimal integer or float literals, without type suffix. + More formally, it tests against the following grammar: + + number ::= [] [] basicNumber [] [] + whitespace ::= [] + whitespaceChar ::= " " | "\t" + sign ::= "+" | "-" + basicNumber = integer [decimal] | decimal + integer ::= [] + digit ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" + decimal ::= + decimalPoint ::= "." + exponent = [] + e ::= "e" | "E" + + if allowSpaces = NO, the [] terms are excluded. +*/ +BOOL OOIsNumberLiteral(NSString *string, BOOL allowSpaces); diff --git a/src/Core/OOIsNumberLiteral.m b/src/Core/OOIsNumberLiteral.m new file mode 100644 index 00000000..1a8e368c --- /dev/null +++ b/src/Core/OOIsNumberLiteral.m @@ -0,0 +1,129 @@ +/* + OOISNumberLiteral.m + + Copyright (C) 2008 Jens Ayton + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +*/ + +#import "OOIsNumberLiteral.h" + + +#if 0 +#define FAIL(s) do { NSLog(@"OOIsNumberLiteral failed for \"%@\": %@.", string, @s); return NO; } while (0) +#else +#define FAIL(s) do { return NO; } while (0) +#endif + + +BOOL OOIsNumberLiteral(NSString *string, BOOL allowSpaces) +{ + BOOL leadingSpace = allowSpaces, + trailingSpace = NO, + allowSign = YES, + allowE = NO, + hadE = NO, + hadExp = NO, + allowDec = YES, + hadNumber = NO; + unsigned i, count; + + if (string == nil) return NO; + + count = [string length]; + for (i = 0; i != count; ++i) + { + switch ([string characterAtIndex:i]) + { + // + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + leadingSpace = NO; + if (trailingSpace) FAIL("Digit after trailing whitespace"); + if (!hadE) allowE = YES; + else hadExp = YES; + allowSign = NO; + hadNumber = YES; + break; + + // + case ' ': + case '\t': + if (leadingSpace || trailingSpace) break; + if (hadNumber && allowSpaces) + { + trailingSpace = YES; + allowSign = allowE = allowDec = NO; + break; + } + FAIL("Space in unpermitted position"); + + // + case '-': + case '+': + leadingSpace = NO; + if (allowSign) + { + allowSign = NO; + break; + } + FAIL("Sign (+ or -) in unpermitted position"); + + // + case '.': + leadingSpace = NO; + if (allowDec) + { + allowDec = NO; + continue; + } + FAIL("Sign (+ or -) in unpermitted position"); + + // + case 'e': + case 'E': + leadingSpace = NO; + if (allowE) + { + allowE = NO; + allowSign = YES; + allowDec = NO; + hadE = YES; + continue; + } + FAIL("E in unpermitted position"); + + default: + FAIL ("Unpermitted character"); + } + } + + if (hadE && !hadExp) FAIL("E with no exponent"); + if (!hadNumber) FAIL("No digits in string"); + + return YES; +} diff --git a/src/Core/OOLight.h b/src/Core/OOLight.h new file mode 100644 index 00000000..db5455db --- /dev/null +++ b/src/Core/OOLight.h @@ -0,0 +1,114 @@ +/* + +OOLight.h + +Class to manage light sources, and bind them to OpenGL lights. +Currently only handles point lights. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" +#import "OOColor.h" +#import "OOMaths.h" + + +typedef uint8_t OOLightIndex; + +enum +{ + kOOLightNotBound = 0xFF +}; + + +@interface OOLight: NSObject +{ + OORGBAComponents _ambient; + OORGBAComponents _diffuse; + OORGBAComponents _specular; + Vector _position; + BOOL _dirty; + OOLightIndex _bound; +} + +- (id) initWithAmbient:(OOColor *)ambient + diffuse:(OOColor *)diffuse + specular:(OOColor *)specular; + +- (id) initWithAmbientRGBA:(OORGBAComponents)ambient + diffuseRGBA:(OORGBAComponents)diffuse + specularRGBA:(OORGBAComponents)specular; + +// Bind OOLights to GL lights +- (void) bindToLight:(OOLightIndex)lightNumber; +- (BOOL) bound; +- (OOLightIndex) boundLight; +- (void) unbindLight; ++ (void) unbindLight:(OOLightIndex)lightNumber; ++ (OOLight *) boundLight:(OOLightIndex)lightNumber; ++ (void) unbindAllLights; + ++ (void) updateLights; + +// Light modification +- (Vector) position; +- (void) setPosition:(Vector)position; + +- (OOColor *) ambient; +- (OORGBAComponents) ambientRGBA; +- (void) setAmbient:(OOColor *)color; +- (void) setAmbientRGBA:(OORGBAComponents)components; + +- (OOColor *) diffuse; +- (OORGBAComponents) diffuseRGBA; +- (void) setDiffuse:(OOColor *)color; +- (void) setDiffuseRGBA:(OORGBAComponents)components; + +- (OOColor *) specular; +- (OORGBAComponents) specularRGBA; +- (void) setSpecular:(OOColor *)color; +- (void) setSpecularRGBA:(OORGBAComponents)components; + +@end diff --git a/src/Core/OOLight.m b/src/Core/OOLight.m new file mode 100644 index 00000000..2a56b049 --- /dev/null +++ b/src/Core/OOLight.m @@ -0,0 +1,385 @@ +/* + +OOLight.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOLight.h" +#import +#import "OOFunctionAttributes.h" +#import "Universe.h" + + +enum +{ + kLightCount = 8 +}; + + +static OOLight *sBindings[kLightCount] = { nil }; + + +OOINLINE GLenum LightIndexToGLLight(OOLightIndex idx) INLINE_CONST_FUNC; +OOINLINE GLenum LightIndexToGLLight(OOLightIndex idx) +{ + return GL_LIGHT0 + idx; +} + + +OOINLINE BOOL ComponentsEqual(OORGBAComponents a, OORGBAComponents b) +{ + return a.r == b.r && + a.g == b.g && + a.b == b.b && + a.a == b.a; +} + + +@interface OOLight (OOPrivate) + +- (void) updateWithGlobalAmbient:(OORGBAComponents *)ioAmbient; + +@end + + +@implementation OOLight + +- (id) init +{ + return [self initWithAmbient:nil + diffuse:nil + specular:nil]; +} + + +- (id) initWithAmbient:(OOColor *)ambient + diffuse:(OOColor *)diffuse + specular:(OOColor *)specular +{ + OORGBAComponents amb = { 0.0f, 0.0f, 0.0f, 1.0f }, + dif = { 0.8f, 0.8f, 0.8f, 1.0f }, + spc = { 0.8f, 0.8f, 0.8f, 1.0f }; + + if (ambient != nil) amb = [ambient rgbaComponents]; + if (diffuse != nil) dif = [diffuse rgbaComponents]; + if (specular != nil) spc = [specular rgbaComponents]; + + return [self initWithAmbientRGBA:amb + diffuseRGBA:dif + specularRGBA:spc]; +} + +- (id) initWithAmbientRGBA:(OORGBAComponents)ambient + diffuseRGBA:(OORGBAComponents)diffuse + specularRGBA:(OORGBAComponents)specular +{ + self = [super init]; + if (self != nil) + { + _ambient = ambient; + _diffuse = diffuse; + _specular = specular; + _bound = kOOLightNotBound; + } + return self; +} + + +- (void) dealloc +{ + [self unbindLight]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"color: %@, position: %@", OORGBAComponentsDescription(_diffuse), VectorDescription(_position)]; +} + + +- (void) bindToLight:(OOLightIndex)lightNumber +{ + if (lightNumber != _bound) + { + // Can't be bound to more than one light number. + [self unbindLight]; + + if (lightNumber < kLightCount) + { + // Can't have more than one light bound to a light number. + [OOLight unbindLight:lightNumber]; + + // Apply binding. + sBindings[lightNumber] = self; + glEnable(LightIndexToGLLight(_bound)); + _dirty = YES; + _bound = lightNumber; + } + } +} + + +- (BOOL) bound +{ + return _bound != kOOLightNotBound; +} + + +- (OOLightIndex) boundLight +{ + return _bound; +} + + +- (void) unbindLight +{ + const float zerofv[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + + if (_bound != kOOLightNotBound) + { + // Apply non-lighting to light. + // In addition to disabling the light, we set its colours to zero, for shaders. + glDisable(LightIndexToGLLight(_bound)); + glLightfv(LightIndexToGLLight(_bound), GL_AMBIENT, zerofv); + glLightfv(LightIndexToGLLight(_bound), GL_DIFFUSE, zerofv); + glLightfv(LightIndexToGLLight(_bound), GL_SPECULAR, zerofv); + + sBindings[_bound] = nil; + _bound = kOOLightNotBound; + } +} + + ++ (void) unbindLight:(OOLightIndex)lightNumber +{ + if (lightNumber < kLightCount) + { + [sBindings[lightNumber] unbindLight]; + } +} + + ++ (OOLight *) boundLight:(OOLightIndex)lightNumber +{ + if (lightNumber < kLightCount) + { + return sBindings[lightNumber]; + } + else + { + return nil; + } +} + + ++ (void) unbindAllLights +{ + OOLightIndex i; + + for (i = 0; i < kLightCount; i++) + { + [self unbindLight:i]; + } +} + + ++ (void) updateLights +{ + OOLightIndex i; + OORGBAComponents ambient = { 0.0f, 0.0f, 0.0f, 1.0f }; + + // Update all lights with changes. + for (i = 0; i < kLightCount; i++) + { + if (sBindings[i] != nil) + { + /* if (sBindings[i]->_dirty) */ [sBindings[i] updateWithGlobalAmbient:&ambient]; + + ambient.r += sBindings[i]->_ambient.r * sBindings[i]->_ambient.a; + ambient.g += sBindings[i]->_ambient.g * sBindings[i]->_ambient.a; + ambient.b += sBindings[i]->_ambient.b * sBindings[i]->_ambient.a; + } + } + + CheckOpenGLErrors(@"After updating lights."); + + float val[4] = { ambient.r, ambient.g, ambient.b, ambient.a }; + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, val); + + CheckOpenGLErrors(@"After updating ambient lighting."); +} + + +- (Vector) position +{ + return _position; +} + + +- (void) setPosition:(Vector)position +{ + if (!vector_equal(_position, position)) + { + _position = position; + _dirty = YES; + } +} + + +- (OOColor *) ambient +{ + return [OOColor colorWithRGBAComponents:_ambient]; +} + + +- (OORGBAComponents) ambientRGBA +{ + return _ambient; +} + + +- (void) setAmbient:(OOColor *)color +{ + [self setAmbientRGBA:[color rgbaComponents]]; +} + + +- (void) setAmbientRGBA:(OORGBAComponents)components +{ + if (!ComponentsEqual(_ambient, components)) + { + _ambient = components; + _dirty = YES; + } +} + + +- (OOColor *) diffuse +{ + return [OOColor colorWithRGBAComponents:_diffuse]; +} + + +- (OORGBAComponents) diffuseRGBA +{ + return _diffuse; +} + + +- (void) setDiffuse:(OOColor *)color +{ + [self setDiffuseRGBA:[color rgbaComponents]]; +} + + +- (void) setDiffuseRGBA:(OORGBAComponents)components +{ + if (!ComponentsEqual(_diffuse, components)) + { + _diffuse = components; + _dirty = YES; + } +} + + +- (OOColor *) specular +{ + return [OOColor colorWithRGBAComponents:_specular]; +} + + +- (OORGBAComponents) specularRGBA +{ + return _specular; +} + + +- (void) setSpecular:(OOColor *)color +{ + [self setSpecularRGBA:[color rgbaComponents]]; +} + + +- (void) setSpecularRGBA:(OORGBAComponents)components +{ + if (!ComponentsEqual(_specular, components)) + { + _specular = components; + _dirty = YES; + } +} + +@end + + +@implementation OOLight (OOPrivate) + +- (void) updateWithGlobalAmbient:(OORGBAComponents *)ioAmbient +{ + float val[4]; + +#define APPLY_COLOR(n, c) val[0] = c.r; \ + val[1] = c.g; \ + val[2] = c.b; \ + val[3] = c.a; \ + glLightfv(LightIndexToGLLight(_bound), n, val); + + APPLY_COLOR(GL_DIFFUSE, _diffuse); + APPLY_COLOR(GL_SPECULAR, _specular); + + // ModelView matrix? Never heard of it, guv. + Vector pos = _position; +// mult_vector_gl_matrix(&pos, [UNIVERSE activeViewMatrix]); + val[0] = pos.x; + val[1] = pos.y; + val[2] = pos.z; + val[3] = 1.0f; + glLightfv(LightIndexToGLLight(_bound), GL_POSITION, val); + _dirty = NO; +} + +@end diff --git a/src/Core/OOLogHeader.h b/src/Core/OOLogHeader.h new file mode 100644 index 00000000..5cae0308 --- /dev/null +++ b/src/Core/OOLogHeader.h @@ -0,0 +1,56 @@ +/* + +OOLogHeader.h + +Print system info to log, to make us less reliant on users actually telling us +stuff. Called from OOLoggingInit() to ensure it appears near the top of the log. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#import "OOCocoa.h" + + +void OOPrintLogHeader(void); diff --git a/src/Core/OOLogHeader.m b/src/Core/OOLogHeader.m new file mode 100644 index 00000000..89d444e6 --- /dev/null +++ b/src/Core/OOLogHeader.m @@ -0,0 +1,342 @@ +/* + +OOLogHeader.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOLogHeader.h" +#import "OOCPUInfo.h" +#import "OOLogging.h" + + +static NSString *AdditionalLogHeaderInfo(void); + + +void OOPrintLogHeader(void) +{ + // Bunch of string literal macros which are assembled into a CPU info string. + #if defined (__ppc__) + #define CPU_TYPE_STRING "PPC-32" + #elif defined (__ppc64__) + #define CPU_TYPE_STRING "PPC-64" + #elif defined (__i386__) + #define CPU_TYPE_STRING "x86-32" + #elif defined (__x86_64__) + #define CPU_TYPE_STRING "x86-64" + #else + #if OOLITE_BIG_ENDIAN + #define CPU_TYPE_STRING "" + #elif OOLITE_LITTLE_ENDIAN + #define CPU_TYPE_STRING "" + #else + #define CPU_TYPE_STRING "" + #endif + #endif + + #if OOLITE_MAC_OS_X + #define OS_TYPE_STRING "Mac OS X" + #elif OOLITE_WINDOWS + #define OS_TYPE_STRING "Windows" + #elif OOLITE_LINUX + #define OS_TYPE_STRING "Linux" // Hmm, what about other unices? + #elif OOLITE_SDL + #define OS_TYPE_STRING "unknown SDL system" + #elif OOLITE_HAVE_APPKIT + #define OS_TYPE_STRING "unknown AppKit system" + #else + #define OS_TYPE_STRING "unknown system" + #endif + + #if OO_DEBUG + #define RELEASE_VARIANT_STRING " debug" + #elif !defined (NDEBUG) + #define RELEASE_VARIANT_STRING " test release" + #else + #define RELEASE_VARIANT_STRING "" + #endif + + #if ALLOW_PROCEDURAL_PLANETS + #define OPT1_STR " [Procedural Planets]" + #else + #define OPT1_STR "" + #endif + + #if DOCKING_CLEARANCE_ENABLED + #define OPT2_STR " [Docking Clearance]" + #else + #define OPT2_STR "" + #endif + + #if WORMHOLE_SCANNER + #define OPT3_STR " [Wormhole Scanner]" + #else + #define OPT3_STR "" + #endif + + #if TARGET_INCOMING_MISSILES + #define OPT4_STR " [Target Incoming Missiles]" + #else + #define OPT4_STR "" + #endif + + #define OPT_STR OPT1_STR OPT2_STR OPT3_STR OPT4_STR + + // systemString: NSString with system type and possibly version. + #if OOLITE_MAC_OS_X + NSString *systemString = [NSString stringWithFormat:@OS_TYPE_STRING " %@", [[NSProcessInfo processInfo] operatingSystemVersionString]]; + #else + #define systemString @OS_TYPE_STRING + #endif + + NSString *versionString = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; + if (versionString == nil) versionString = @""; + + NSMutableString *miscString = [NSMutableString stringWithFormat:@"Opening log for Oolite version %@ (" CPU_TYPE_STRING RELEASE_VARIANT_STRING ") under %@ at %@.\n", versionString, systemString, [NSDate date]]; + + [miscString appendString:AdditionalLogHeaderInfo()]; + [miscString appendString:@"\nOolite Options:" OPT_STR "\n"]; + + [miscString appendString:@"\nNote that the contents of the log file can be adjusted by editing logcontrol.plist."]; + + OOLog(@"log.header", @"%@\n", miscString); +} + + +// System-specific stuff to append to log header. +#if OOLITE_MAC_OS_X +#import + + +#ifndef CPUFAMILY_INTEL_6_13 +// Copied from OS X 10.5 SDK +#define CPUFAMILY_INTEL_6_13 0xaa33392b +#define CPUFAMILY_INTEL_6_14 0x73d67300 /* "Intel Core Solo" and "Intel Core Duo" (32-bit Pentium-M with SSE3) */ +#define CPUFAMILY_INTEL_6_15 0x426f69ef /* "Intel Core 2 Duo" */ +#define CPUFAMILY_INTEL_6_23 0x78ea4fbc /* Penryn */ +#define CPUFAMILY_INTEL_6_26 0x6b5a4cd2 + +#define CPUFAMILY_INTEL_YONAH CPUFAMILY_INTEL_6_14 +#define CPUFAMILY_INTEL_MEROM CPUFAMILY_INTEL_6_15 +#define CPUFAMILY_INTEL_PENRYN CPUFAMILY_INTEL_6_23 +#define CPUFAMILY_INTEL_NEHALEM CPUFAMILY_INTEL_6_26 + +#define CPUFAMILY_INTEL_CORE CPUFAMILY_INTEL_6_14 +#define CPUFAMILY_INTEL_CORE2 CPUFAMILY_INTEL_6_15 +#endif + +#ifndef CPU_TYPE_ARM +#define CPU_TYPE_ARM ((cpu_type_t) 12) +#define CPU_SUBTYPE_ARM_ALL ((cpu_subtype_t) 0) +#define CPU_SUBTYPE_ARM_V4T ((cpu_subtype_t) 5) +#define CPU_SUBTYPE_ARM_V6 ((cpu_subtype_t) 6) +#define CPUFAMILY_ARM_9 0xe73283ae +#define CPUFAMILY_ARM_11 0x8ff620d8 +#endif + + +static NSString *GetSysCtlString(const char *name); +static unsigned long long GetSysCtlInt(const char *name); +static NSString *GetCPUDescription(void); + +static NSString *AdditionalLogHeaderInfo(void) +{ + NSString *sysModel = nil; + unsigned long long sysPhysMem; + + sysModel = GetSysCtlString("hw.model"); + sysPhysMem = GetSysCtlInt("hw.memsize"); + + return [NSString stringWithFormat:@"Machine type: %@, %llu MiB memory, %@.", sysModel, sysPhysMem >> 20, GetCPUDescription()]; +} + + +static NSString *GetCPUDescription(void) +{ + unsigned long long sysCPUType, sysCPUSubType, sysCPUFamily, + sysCPUFrequency, sysCPUCount; + NSString *typeStr = nil, *subTypeStr = nil; + + sysCPUType = GetSysCtlInt("hw.cputype"); + sysCPUSubType = GetSysCtlInt("hw.cpusubtype"); + sysCPUFamily = GetSysCtlInt("hw.cpufamily"); + sysCPUFrequency = GetSysCtlInt("hw.cpufrequency"); + sysCPUCount = GetSysCtlInt("hw.logicalcpu"); + + /* Note: CPU_TYPE_STRING tells us the build architecture. This gets the + physical CPU type. They may differ, for instance, when running under + Rosetta. The code is written for flexibility, although ruling out + x86 code running on PPC would be entirely reasonable. + */ + switch (sysCPUType) + { + case CPU_TYPE_POWERPC: + typeStr = @"PowerPC"; + switch (sysCPUSubType) + { + case CPU_SUBTYPE_POWERPC_750: + subTypeStr = @" G3 (750)"; + break; + + case CPU_SUBTYPE_POWERPC_7400: + subTypeStr = @" G4 (7400)"; + break; + + case CPU_SUBTYPE_POWERPC_7450: + subTypeStr = @" G4 (7450)"; + break; + + case CPU_SUBTYPE_POWERPC_970: + subTypeStr = @" G5 (970)"; + break; + + default: + subTypeStr = [NSString stringWithFormat:@":%u", sysCPUSubType]; + } + break; + + case CPU_TYPE_I386: + typeStr = @"x86"; + switch (sysCPUFamily) + { + case CPUFAMILY_INTEL_6_13: + subTypeStr = @" (Intel 6:13)"; + break; + + case CPUFAMILY_INTEL_YONAH: + subTypeStr = @" (Core/Yonah)"; + break; + + case CPUFAMILY_INTEL_MEROM: + subTypeStr = @" (Core 2/Merom)"; + break; + + case CPUFAMILY_INTEL_PENRYN: + subTypeStr = @" (Penryn)"; + break; + + case CPUFAMILY_INTEL_NEHALEM: + subTypeStr = @" (Nehalem)"; + break; + + default: + subTypeStr = [NSString stringWithFormat:@" (family %u)", sysCPUFamily]; + } + break; + + case CPU_TYPE_ARM: + typeStr = @"ARM"; + switch (sysCPUSubType) + { + case CPU_SUBTYPE_ARM_V4T: + subTypeStr = @" v4T"; + break; + + case CPU_SUBTYPE_ARM_V6: + subTypeStr = @"v6"; // No space + break; + } + if (subTypeStr == nil) + { + switch (sysCPUFamily) + { + case CPUFAMILY_ARM_9: + subTypeStr = @"9"; // No space + break; + + case CPUFAMILY_ARM_11: + subTypeStr = @"11"; // No space + break; + + default: + subTypeStr = [NSString stringWithFormat:@" (family %u)", sysCPUFamily]; + } + } + } + + if (typeStr == nil) typeStr = [NSString stringWithFormat:@"%u", sysCPUType]; + + return [NSString stringWithFormat:@"%llu x %@%@ @ %llu MHz", sysCPUCount, typeStr, subTypeStr, (sysCPUFrequency + 500000) / 1000000]; +} + + +static NSString *GetSysCtlString(const char *name) +{ + char *buffer = NULL; + size_t size = 0; + + // Get size + sysctlbyname(name, NULL, &size, NULL, 0); + if (size == 0) return nil; + + buffer = alloca(size); + if (sysctlbyname(name, buffer, &size, NULL, 0) != 0) return nil; + return [NSString stringWithUTF8String:buffer]; +} + + +static unsigned long long GetSysCtlInt(const char *name) +{ + unsigned long long llresult = 0; + unsigned int intresult = 0; + size_t size; + + size = sizeof llresult; + if (sysctlbyname(name, &llresult, &size, NULL, 0) != 0) return 0; + if (size == sizeof llresult) return llresult; + + size = sizeof intresult; + if (sysctlbyname(name, &intresult, &size, NULL, 0) != 0) return 0; + if (size == sizeof intresult) return intresult; + + return 0; +} + +#else +static NSString *AdditionalLogHeaderInfo(void) +{ + unsigned cpuCount = OOCPUCount(); + return [NSString stringWithFormat:@"%u processor%@ detected.", cpuCount, cpuCount != 1 ? @"s" : @""]; +} +#endif diff --git a/src/Core/OOLogOutputHandler.h b/src/Core/OOLogOutputHandler.h new file mode 100644 index 00000000..f8c08d8f --- /dev/null +++ b/src/Core/OOLogOutputHandler.h @@ -0,0 +1,71 @@ +/* + +OOLogOutputHandler.h +By Jens Ayton + +Output handler for OOLogging. + +This does two things: +1. It writes log output to ~/Logs/Oolite/Latest.log under Mac OS X or + ~/.Oolite/Logs/Latest.log under Linux, handling thread serialization. +2. It installs a filter to capture NSLogs and convert them to OOLogs. This is + different to the macro in OOLogging.h, which acts at compile time; the + filter catches logging in included frameworks. + +OOLogOutputHandlerPrint() is thread-safe. Other functions are not. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +void OOLogOutputHandlerInit(void); +void OOLogOutputHandlerClose(void); +void OOLogOutputHandlerPrint(NSString *string); + +// This will attempt to ensure the containing directory exists. If it fails, it will return nil. +NSString *OOLogHandlerGetLogPath(void); +NSString *OOLogHandlerGetLogBasePath(void); +void OOLogOutputHandlerChangeLogFile(NSString *newLogName); diff --git a/src/Core/OOLogOutputHandler.m b/src/Core/OOLogOutputHandler.m new file mode 100644 index 00000000..b26bc87a --- /dev/null +++ b/src/Core/OOLogOutputHandler.m @@ -0,0 +1,747 @@ +/* + +OOLogOutputHandler.m +By Jens Ayton + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#define OOLOG_POISON_NSLOG 0 +#define DLOPEN_NO_WARN + +#import "OOLogOutputHandler.h" +#import "OOLogging.h" +#import "OOAsyncQueue.h" +#import +#import +#import "NSThreadOOExtensions.h" + + +#undef NSLog // We need to be able to call the real NSLog. + + +#if OOLITE_MAC_OS_X + +#import + +#ifndef NDEBUG + +#define SET_CRASH_REPORTER_INFO 1 + +// Function to set "Application Specific Information" field in crash reporter log in Leopard. +// Extremely unsupported, so not used in release builds. +static void InitCrashReporterInfo(void); +static void SetCrashReporterInfo(const char *info); +static BOOL sCrashReporterInfoAvailable = NO; + +#endif + + +typedef void (*LogCStringFunctionProc)(const char *string, unsigned length, BOOL withSyslogBanner); +typedef LogCStringFunctionProc (*LogCStringFunctionGetterProc)(void); +typedef void (*LogCStringFunctionSetterProc)(LogCStringFunctionProc); + +static LogCStringFunctionGetterProc _NSLogCStringFunction = NULL; +static LogCStringFunctionSetterProc _NSSetLogCStringFunction = NULL; + +static void LoadLogCStringFunctions(void); +static void OONSLogCStringFunction(const char *string, unsigned length, BOOL withSyslogBanner); + +static NSString *GetAppName(void); + +static LogCStringFunctionProc sDefaultLogCStringFunction = NULL; + +#elif OOLITE_GNUSTEP + +static void OONSLogPrintfHandler(NSString *message); + +#else +#error Unknown platform! +#endif + +static BOOL DirectoryExistCreatingIfNecessary(NSString *path); + + +#define kFlushInterval 2.0 // Lower bound on interval between explicit log file flushes. + + +@interface OOAsyncLogger: NSObject +{ + OOAsyncQueue *messageQueue; + NSConditionLock *threadStateMonitor; + NSFileHandle *logFile; + NSTimer *flushTimer; +} + +- (void)asyncLogMessage:(NSString *)message; +- (void)endLogging; + +- (void)changeFile; + +// Internal +- (BOOL)startLogging; +- (void)loggerThread; +- (void)flushLog; + +@end + + +static BOOL sInited = NO; +static BOOL sWriteToStderr = YES; +static BOOL sSaturated = NO; +static OOAsyncLogger *sLogger = nil; +static NSString *sLogFileName = @"Latest.log"; + + +void OOLogOutputHandlerInit(void) +{ + if (sInited) return; + +#if SET_CRASH_REPORTER_INFO + InitCrashReporterInfo(); +#endif + + sLogger = [[OOAsyncLogger alloc] init]; + sInited = YES; + + if (sLogger != nil) + { + sWriteToStderr = [[NSUserDefaults standardUserDefaults] boolForKey:@"logging-echo-to-stderr"]; + } + else + { + sWriteToStderr = YES; + } + +#if OOLITE_MAC_OS_X + LoadLogCStringFunctions(); + if (_NSSetLogCStringFunction != NULL) + { + sDefaultLogCStringFunction = _NSLogCStringFunction(); + _NSSetLogCStringFunction(OONSLogCStringFunction); + } + else + { + OOLog(@"logging.nsLogFilter.install.failed", @"Failed to install NSLog() filter; system messages will not be logged in log file."); + } +#elif GNUSTEP + NSRecursiveLock *lock = GSLogLock(); + [lock lock]; + _NSLog_printf_handler = OONSLogPrintfHandler; + [lock unlock]; +#endif + + atexit(OOLogOutputHandlerClose); +} + + +void OOLogOutputHandlerClose(void) +{ + if (sInited) + { + sWriteToStderr = YES; + sInited = NO; + + [sLogger endLogging]; + [sLogger release]; + sLogger = nil; + +#if OOLITE_MAC_OS_X + if (sDefaultLogCStringFunction != NULL && _NSSetLogCStringFunction != NULL) + { + _NSSetLogCStringFunction(sDefaultLogCStringFunction); + sDefaultLogCStringFunction = NULL; + } +#elif GNUSTEP + NSRecursiveLock *lock = GSLogLock(); + [lock lock]; + _NSLog_printf_handler = NULL; + [lock unlock]; +#endif + } +} + + +void OOLogOutputHandlerPrint(NSString *string) +{ + if (sInited && sLogger != nil) [sLogger asyncLogMessage:string]; + + BOOL doCStringStuff = sWriteToStderr; +#if SET_CRASH_REPORTER_INFO + doCStringStuff = doCStringStuff || sCrashReporterInfoAvailable; +#endif + + if (doCStringStuff) + { + const char *cStr = [[string stringByAppendingString:@"\n"] UTF8String]; + if (sWriteToStderr) fputs(cStr, stderr); + +#if SET_CRASH_REPORTER_INFO + if (sCrashReporterInfoAvailable) SetCrashReporterInfo(cStr); +#endif + } + +} + + +NSString *OOLogHandlerGetLogPath(void) +{ + return [OOLogHandlerGetLogBasePath() stringByAppendingPathComponent:sLogFileName]; +} + + +void OOLogOutputHandlerChangeLogFile(NSString *newLogName) +{ + if (![sLogFileName isEqual:newLogName]) + { + sLogFileName = [newLogName copy]; + [sLogger changeFile]; + } +} + + +enum +{ + kConditionReadyToDealloc = 1, + kConditionWorking +}; + + +@implementation OOAsyncLogger + +- (id)init +{ + BOOL OK = YES; + NSString *logPath = nil; + NSString *oldPath = nil; + NSFileManager *fmgr = nil; + + // We'll need these for a couple of things. + fmgr = [NSFileManager defaultManager]; + + logPath = OOLogHandlerGetLogPath(); + // If there is an existing file, move it to Previous.log. + if ([fmgr fileExistsAtPath:logPath]) + { + oldPath = [OOLogHandlerGetLogBasePath() stringByAppendingPathComponent:@"Previous.log"]; + [fmgr removeFileAtPath:oldPath handler:nil]; + if (![fmgr movePath:logPath toPath:oldPath handler:nil]) + { + if (![fmgr removeFileAtPath:logPath handler:nil]) + { + NSLog(@"Log setup: could not move or delete existing log at %@, will log to stdout instead.", logPath); + OK = NO; + } + } + } + + if (OK) OK = [self startLogging]; + + if (!OK) + { + [self release]; + self = nil; + } + + return self; +} + + +- (void)dealloc +{ + [messageQueue release]; + [threadStateMonitor release]; + [logFile release]; + [flushTimer invalidate]; + + [super dealloc]; +} + + +- (BOOL)startLogging +{ + BOOL OK = YES; + NSString *logPath = nil; + NSFileManager *fmgr = nil; + + // We'll need these for a couple of things. + fmgr = [NSFileManager defaultManager]; + + self = [super init]; + if (self == nil) OK = NO; + + if (OK) + { + messageQueue = [[OOAsyncQueue alloc] init]; + if (messageQueue == nil) OK = NO; + } + + if (OK) + { + // set up threadStateMonitor -- used as a binary semaphore of sorts to check when the worker thread starts and stops. + threadStateMonitor = [[NSConditionLock alloc] initWithCondition:kConditionReadyToDealloc]; + if (threadStateMonitor == nil) OK = NO; + [threadStateMonitor ooSetName:@"OOLogOutputHandler thread state monitor"]; + } + + if (OK) + { + // Create work thread to actually handle messages. + // This needs to be done early to avoid messy state if something goes wrong. + [NSThread detachNewThreadSelector:@selector(loggerThread) toTarget:self withObject:nil]; + // Wait for it to start. + if (![threadStateMonitor lockWhenCondition:kConditionWorking beforeDate:[NSDate dateWithTimeIntervalSinceNow:5.0]]) + { + // If it doesn't signal a start within five seconds, assume something's wrong. + // Send kill signal, just in case it comes to life... + [messageQueue enqueue:@"die"]; + // ...and stop -dealloc from waiting for thread death + [threadStateMonitor release]; + threadStateMonitor = nil; + OK = NO; + } + [threadStateMonitor unlockWithCondition:kConditionWorking]; + } + + if (OK) + { + logPath = OOLogHandlerGetLogPath(); + OK = (logPath != nil); + } + + if (OK) + { + // Create shiny new log file + OK = [fmgr createFileAtPath:logPath contents:nil attributes:nil]; + if (OK) + { + logFile = [[NSFileHandle fileHandleForWritingAtPath:logPath] retain]; + OK = (logFile != nil); + } + if (!OK) + { + NSLog(@"Log setup: could not open log at %@, will log to stdout instead.", logPath); + OK = NO; + } + } + + return OK; +} + + +- (void)endLogging +{ + NSString *postamble = nil; + + if (messageQueue != nil && threadStateMonitor != nil) + { + // We're fully inited; write postamble, wait for worker thread to terminate cleanly, and close file. + postamble = [NSString stringWithFormat:@"\nClosing log at %@.", [NSDate date]]; + [self asyncLogMessage:postamble]; + [messageQueue enqueue:@"die"]; // Kill message + [threadStateMonitor lockWhenCondition:kConditionReadyToDealloc]; + [threadStateMonitor unlock]; + + [logFile closeFile]; + } +} + + +- (void)changeFile +{ + [self endLogging]; + if (![self startLogging]) sWriteToStderr = YES; +} + + +- (void)asyncLogMessage:(NSString *)message +{ + // Don't log of saturated flag is set. + if (sSaturated) return; + + if (message != nil) + { + message = [message stringByAppendingString:@"\n"]; + +#if OOLITE_WINDOWS + // Convert Unix line endings to Windows ones. + NSArray *messageComponents = [message componentsSeparatedByString:@"\n"]; + message = [messageComponents componentsJoinedByString:@"\r\n"]; +#endif + + [messageQueue enqueue:[message dataUsingEncoding:NSUTF8StringEncoding]]; + + if (flushTimer == nil) + { + // No pending flush + flushTimer = [NSTimer scheduledTimerWithTimeInterval:kFlushInterval target:self selector:@selector(flushLog) userInfo:nil repeats:NO]; + } + } +} + + +- (void)flushLog +{ + flushTimer = nil; + [messageQueue enqueue:@"flush"]; +} + + +- (void)loggerThread +{ + id message = nil; + NSAutoreleasePool *rootPool = nil, *pool = nil; + OOUInteger size = 0; + + rootPool = [[NSAutoreleasePool alloc] init]; + [NSThread ooSetCurrentThreadName:@"OOLogOutputHandler logging thread"]; + + // Signal readiness + [messageQueue retain]; + [threadStateMonitor lock]; + [threadStateMonitor unlockWithCondition:kConditionWorking]; + + NS_DURING + for (;;) + { + pool = [[NSAutoreleasePool alloc] init]; + + message = [messageQueue dequeue]; + + if (!sSaturated && [message isKindOfClass:[NSData class]]) + { + size += [message length]; + if (size > 1 << 30) // 1 GiB + { + sSaturated = YES; +#if OOLITE_WINDOWS + message = @"\r\n\r\n\r\n***** LOG TRUNCATED DUE TO EXCESSIVE LENGTH *****\r\n"; +#else + message = @"\n\n\n***** LOG TRUNCATED DUE TO EXCESSIVE LENGTH *****\n"; +#endif + message = [message dataUsingEncoding:NSUTF8StringEncoding]; + } + + [logFile writeData:message]; + } + else if ([message isEqual:@"flush"]) + { + [logFile synchronizeFile]; + } + else if ([message isEqual:@"die"]) + { + break; + } + + [pool release]; + } + NS_HANDLER + NS_ENDHANDLER + [pool release]; + + // Clean up; after this, ivars are out of bounds. + [messageQueue release]; + [threadStateMonitor lock]; + [threadStateMonitor unlockWithCondition:kConditionReadyToDealloc]; + + [rootPool release]; +} + +@end + + +#if OOLITE_MAC_OS_X + +/* LoadLogCStringFunctions() + + We wish to make NSLogv() call our custom function OONSLogCStringFunction() + rather than printing to stdout, by calling _NSSetLogCStringFunction(). + Additionally, in order to close the logger cleanly, we wish to be able to + restore the standard logger, which requires us to call + _NSLogCStringFunction(). These functions are private. + _NSLogCStringFunction() is undocumented. _NSSetLogCStringFunction() is + documented at http://docs.info.apple.com/article.html?artnum=70081 , + with the warning: + + Be aware that this code references private APIs; this is an + unsupported workaround and users should use these instructions at + their own risk. Apple will not guarantee or provide support for + this procedure. + + The approach taken here is to load the function sdynamically. This makes + us safe in the case of Apple removing the functions. In the unlikely event + that they change the functions' paramters without renaming them, we would + have a problem. +*/ +static void LoadLogCStringFunctions(void) +{ + CFBundleRef foundationBundle = NULL; + LogCStringFunctionGetterProc getter = NULL; + LogCStringFunctionSetterProc setter = NULL; + + foundationBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.Foundation")); + if (foundationBundle != NULL) + { + getter = CFBundleGetFunctionPointerForName(foundationBundle, CFSTR("_NSLogCStringFunction")); + setter = CFBundleGetFunctionPointerForName(foundationBundle, CFSTR("_NSSetLogCStringFunction")); + + if (getter != NULL && setter != NULL) + { + _NSLogCStringFunction = getter; + _NSSetLogCStringFunction = setter; + } + } +} + + +static void OONSLogCStringFunction(const char *string, unsigned length, BOOL withSyslogBanner) +{ + if (OOLogWillDisplayMessagesInClass(@"system")) + { + OOLogWithFunctionFileAndLine(@"system", NULL, NULL, 0, @"%s", string); + } +} + +#elif OOLITE_GNUSTEP + +static void OONSLogPrintfHandler(NSString *message) +{ + if (OOLogWillDisplayMessagesInClass(@"gnustep")) + { + OOLogWithFunctionFileAndLine(@"gnustep", NULL, NULL, 0, @"%@", message); + } +} + +#endif + + +static BOOL DirectoryExistCreatingIfNecessary(NSString *path) +{ + BOOL exists, directory; + NSFileManager *fmgr = [NSFileManager defaultManager]; + + exists = [fmgr fileExistsAtPath:path isDirectory:&directory]; + + if (exists && !directory) + { + NSLog(@"Log setup: expected %@ to be a folder, but it is a file.", path); + return NO; + } + if (!exists) + { + if (![fmgr createDirectoryAtPath:path attributes:nil]) + { + NSLog(@"Log setup: could not create folder %@.", path); + return NO; + } + } + + return YES; +} + + +#if OOLITE_MAC_OS_X + +static void ExcludeFromTimeMachine(NSString *path); + + +NSString *OOLogHandlerGetLogBasePath(void) +{ + static NSString *basePath = nil; + + if (basePath == nil) + { + // ~/Library + basePath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + + // ~/Library/Logs + basePath = [basePath stringByAppendingPathComponent:@"Logs"]; + if (!DirectoryExistCreatingIfNecessary(basePath)) return nil; + + // ~/Library/Logs/Oolite + basePath = [basePath stringByAppendingPathComponent:GetAppName()]; + if (!DirectoryExistCreatingIfNecessary(basePath)) return nil; + ExcludeFromTimeMachine(basePath); + + [basePath retain]; + } + + return basePath; +} + + +static void ExcludeFromTimeMachine(NSString *path) +{ + OSStatus (*CSBackupSetItemExcluded)(NSURL *item, Boolean exclude, Boolean excludeByPath) = NULL; + CFBundleRef carbonCoreBundle = CFBundleGetBundleWithIdentifier(CFSTR("com.apple.CoreServices.CarbonCore")); + if (carbonCoreBundle) + { + CSBackupSetItemExcluded = CFBundleGetFunctionPointerForName(carbonCoreBundle, CFSTR("CSBackupSetItemExcluded")); + if (CSBackupSetItemExcluded != NULL) + { + (void)CSBackupSetItemExcluded([NSURL fileURLWithPath:path], YES, NO); + } + } +} + + +static NSString *GetAppName(void) +{ + static NSString *appName = nil; + NSBundle *bundle = nil; + + if (appName == nil) + { + bundle = [NSBundle mainBundle]; + appName = [bundle objectForInfoDictionaryKey:@"CFBundleName"]; + if (appName == nil) appName = [bundle bundleIdentifier]; + if (appName == nil) appName = @""; + [appName retain]; + } + + return appName; +} + +#elif OOLITE_LINUX + +NSString *OOLogHandlerGetLogBasePath(void) +{ + static NSString *basePath = nil; + + if (basePath == nil) + { + // ~ + basePath = NSHomeDirectory(); + + // ~/.Oolite + basePath = [basePath stringByAppendingPathComponent:@".Oolite"]; + if (!DirectoryExistCreatingIfNecessary(basePath)) return nil; + + // ~/.Oolite/Logs + basePath = [basePath stringByAppendingPathComponent:@"Logs"]; + if (!DirectoryExistCreatingIfNecessary(basePath)) return nil; + + [basePath retain]; + } + + return basePath; +} + +#elif OOLITE_WINDOWS + +NSString *OOLogHandlerGetLogBasePath(void) +{ + static NSString *basePath = nil; + + if (basePath == nil) + { + // \Oolite + basePath = NSHomeDirectory(); + + // \Oolite\Logs + basePath = [basePath stringByAppendingPathComponent:@"Logs"]; + if (!DirectoryExistCreatingIfNecessary(basePath)) return nil; + + [basePath retain]; + } + + return basePath; +} + +#endif + + +#if SET_CRASH_REPORTER_INFO + +static char **sCrashReporterInfo = NULL; +static char *sOldCrashReporterInfo = NULL; +static NSLock *sCrashReporterInfoLock = nil; + +// Evil hackery based on http://www.allocinit.net/blog/2008/01/04/application-specific-information-in-leopard-crash-reports/ +static void InitCrashReporterInfo(void) +{ + sCrashReporterInfo = dlsym(RTLD_DEFAULT, "__crashreporter_info__"); + if (sCrashReporterInfo != NULL) + { + sCrashReporterInfoLock = [[NSLock alloc] init]; + if (sCrashReporterInfoLock != nil) + { + sCrashReporterInfoAvailable = YES; + } + else + { + sCrashReporterInfo = NULL; + } + } +} + +static void SetCrashReporterInfo(const char *info) +{ + char *copy = NULL, *old = NULL; + + /* Don't do anything if setup failed or the string is NULL or empty. + (The NULL and empty checks may not be desirable in other uses.) + */ + if (!sCrashReporterInfoAvailable || info == NULL || *info == '\0') return; + + // Copy the string, which we assume to be dynamic... + copy = strdup(info); + if (copy == NULL) return; + + /* ...and swap it in. + Note that we keep a separate pointer to the old value, in case + something else overwrites __crashreporter_info__. + */ + [sCrashReporterInfoLock lock]; + *sCrashReporterInfo = copy; + old = sOldCrashReporterInfo; + sOldCrashReporterInfo = copy; + [sCrashReporterInfoLock unlock]; + + // Delete our old string. + if (old != NULL) free(old); +} + +#endif diff --git a/src/Core/OOLogging.h b/src/Core/OOLogging.h new file mode 100644 index 00000000..8d39d3f4 --- /dev/null +++ b/src/Core/OOLogging.h @@ -0,0 +1,176 @@ +/* + +OOLogging.h +By Jens Ayton + +More flexible alternative to NSLog(). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import + + +#ifndef OOLOG_POISON_NSLOG + #define OOLOG_POISON_NSLOG 0 +#endif + + +#ifndef OOLOG_FUNCTION_NAME + #if defined (__GNUC__) && __GNUC__ >= 2 + #define OOLOG_FUNCTION_NAME __PRETTY_FUNCTION__ + #elif 199901L <= __STDC_VERSION__ + #define OOLOG_FUNCTION_NAME __func__ + #else + #define OOLOG_FUNCTION_NAME NULL + #endif +#endif + +#ifndef OOLOG_FILE_NAME + #ifdef OOLOG_NO_FILE_NAME + #define OOLOG_FILE_NAME NULL + #else + #define OOLOG_FILE_NAME __FILE__ + #endif +#endif + + +/* OOLOG_SHORT_CIRCUIT: + If nonzero, the test of whether to display a message before evaluating the + other parameters of the call. This saves time, but could cause weird bugs + if the parameters involve calls with side effects. +*/ +#ifndef OOLOG_SHORT_CIRCUIT + #define OOLOG_SHORT_CIRCUIT 1 +#endif + + +/* General usage: + OOLog(messageClass, format, parameters); + is conceptually equivalent to: + NSLog(format, parameters); + except that it will do nothing if logging is disabled for messageClass. + + A message class is a hierarchical string, such as: + @"all.script.debug" + + To determine whether scripting is enabled for this class, a setting for + @"all.script.debug" is looked up in a settings table. If it is not found, + @"all.script" is tried, followed by @"all". + + Message class display settings can be manipulated with + OOLogSetDisplayMessagesInClass() and tested with + OOLogWillDisplayMessagesInClass(). +*/ +#if OOLOG_SHORT_CIRCUIT + #define OOLog(class, format, ...) do { if (OOLogWillDisplayMessagesInClass(class)) { OOLogWithFunctionFileAndLine(class, OOLOG_FUNCTION_NAME, OOLOG_FILE_NAME, __LINE__, format, ## __VA_ARGS__); }} while (0) + #define OOLogWithArgmuents(class, format, args) do { if (OOLogWillDisplayMessagesInClass(class)) { OOLogWithFunctionFileAndLineAndArguments(class, OOLOG_FUNCTION_NAME, OOLOG_FILE_NAME, __LINE__, format, args); }} while (0) +#else + #define OOLog(class, format, ...) OOLogWithFunctionFileAndLine(class, OOLOG_FUNCTION_NAME, OOLOG_FILE_NAME, __LINE__, format, ## __VA_ARGS__) + #define OOLogWithArgmuents(class, format, args) OOLogWithFunctionFileAndLineAndArguments(class, OOLOG_FUNCTION_NAME, OOLOG_FILE_NAME, __LINE__, format, args) +#endif + +BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass); + +void OOLogIndent(void); +void OOLogOutdent(void); + +#if OOLOG_SHORT_CIRCUIT +#define OOLogIndentIf(class) do { if (OOLogWillDisplayMessagesInClass(class)) OOLogIndent(); } while (0) +#define OOLogOutdentIf(class) do { if (OOLogWillDisplayMessagesInClass(class)) OOLogOutdent(); } while (0) +#else +void OOLogIndentIf(NSString *inMessageClass); +void OOLogOutdentIf(NSString *inMessageClass); +#endif + +#define OOLogERR(class, format, ...) OOLogWithPrefix(class, OOLOG_FUNCTION_NAME, OOLOG_FILE_NAME, __LINE__, @"***** ERROR: ",format, ## __VA_ARGS__) +#define OOLogWARN(class, format, ...) OOLogWithPrefix(class, OOLOG_FUNCTION_NAME, OOLOG_FILE_NAME, __LINE__, @"----- WARNING: ",format, ## __VA_ARGS__) + +// Remember/restore indent levels, for cases where an exception may occur while indented. +void OOLogPushIndent(void); +void OOLogPopIndent(void); + +void OOLogWithPrefix(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inPrefix, NSString *inFormat, ...); +void OOLogWithFunctionFileAndLine(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inFormat, ...); +void OOLogWithFunctionFileAndLineAndArguments(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inFormat, va_list inArguments); + +// OOLogGenericParameterError(): general parameter error message, "***** $function_name: bad parameters. (This is an internal programming error, please report it.)" +#define OOLogGenericParameterError() OOLogGenericParameterErrorForFunction(OOLOG_FUNCTION_NAME) +void OOLogGenericParameterErrorForFunction(const char *inFunction); + +// OOLogGenericSubclassResponsibility(): general subclass responsibility message, "***** $function_name is a subclass responsibility. (This is an internal programming error, please report it.)" +#define OOLogGenericSubclassResponsibility() OOLogGenericSubclassResponsibilityForFunction(OOLOG_FUNCTION_NAME) +void OOLogGenericSubclassResponsibilityForFunction(const char *inFunction); + + +#if OOLOG_POISON_NSLOG + #pragma GCC poison NSLog // Use OOLog instead +#elif !OOLOG_NO_HIJACK_NSLOG + // Hijack NSLog. Buahahahaha. + #define NSLog(format, ...) OOLog(kOOLogUnconvertedNSLog, format, ## __VA_ARGS__) + #define NSLogv(format, args) OOLogWithArgmuents(kOOLogUnconvertedNSLog, format, args) +#endif + + +// *** Predefined message classes. +/* These are general coding error types. Generally a subclass should be used + for each instance -- for instance, -[Entity warnAboutHostiles] uses + @"general.error.subclassResponsibility.Entity-warnAboutHostiles". +*/ + +extern NSString * const kOOLogSubclassResponsibility; // @"general.error.subclassResponsibility" +extern NSString * const kOOLogParameterError; // @"general.error.parameterError" +extern NSString * const kOOLogDeprecatedMethod; // @"general.error.deprecatedMethod" +extern NSString * const kOOLogAllocationFailure; // @"general.error.allocationFailure" +extern NSString * const kOOLogInconsistentState; // @"general.error.inconsistentState" +extern NSString * const kOOLogException; // @"exception" + +extern NSString * const kOOLogFileNotFound; // @"files.notfound" +extern NSString * const kOOLogFileNotLoaded; // @"files.notloaded" + +extern NSString * const kOOLogOpenGLError; // @"rendering.opengl.error" + +// Don't use. However, #defining it as @"unclassified.module" can be used as a stepping stone to OOLog support. +extern NSString * const kOOLogUnconvertedNSLog; // @"unclassified" diff --git a/src/Core/OOLogging.m b/src/Core/OOLogging.m new file mode 100644 index 00000000..1bdd879c --- /dev/null +++ b/src/Core/OOLogging.m @@ -0,0 +1,884 @@ +/* + +OOLogging.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#define OOLOG_POISON_NSLOG 0 + +#import "OOLoggingExtended.h" +#import "OOPListParsing.h" +#import "OOFunctionAttributes.h" +#import "ResourceManager.h" +#import "OOCollectionExtractors.h" +#import "NSThreadOOExtensions.h" +#import "OOLogHeader.h" +#import "OOLogOutputHandler.h" + +#undef NSLog // We need to be able to call the real NSLog. + + +#define PER_THREAD_INDENTATION 1 +#ifndef APPNAME +#define APPNAME @"Oolite" +#endif + + +#if PER_THREAD_INDENTATION + #if OOLITE_USE_TLS // Define to use __thread keyword where supported + #define USE_INDENT_GLOBALS 1 + #define THREAD_LOCAL __thread + #else + #define USE_INDENT_GLOBALS 0 + static NSString * const kIndentLevelKey = @"org.aegidian.oolite.oolog.indentLevel"; + static NSString * const kIndentStackKey = @"org.aegidian.oolite.oolog.indentStack"; + #endif +#else + #define USE_INDENT_GLOBALS 1 + #define THREAD_LOCAL +#endif + + +// Control flags for OOLogInternal() - like message classes, but less cool. +#define OOLOG_NOT_INITED 1 +#define OOLOG_SETTING_SET 0 +#define OOLOG_SETTING_RETRIEVE 0 +#define OOLOG_METACLASS_LOOP 1 +#define OOLOG_UNDEFINED_METACLASS 1 +#define OOLOG_BAD_SETTING 1 +#define OOLOG_BAD_DEFAULT_SETTING 1 +#define OOLOG_BAD_POP_INDENT 1 +#define OOLOG_EXCEPTION_IN_LOG 1 + + +// Used to track OOLogPushIndent()/OOLogPopIndent() state. +typedef struct OOLogIndentStackElement OOLogIndentStackElement; +struct OOLogIndentStackElement +{ + OOLogIndentStackElement *link; + unsigned indent; +}; + + +// We could probably use less state variables. +static BOOL sInited = NO; +static NSLock *sLock = nil; +static NSMutableDictionary *sExplicitSettings = nil; +static NSMutableDictionary *sDerivedSettingsCache = nil; +#if USE_INDENT_GLOBALS +static THREAD_LOCAL unsigned sIndentLevel = 0; +static THREAD_LOCAL OOLogIndentStackElement + *sIndentStack = NULL; +#endif +static BOOL sShowFunction = NO; +static BOOL sShowFileAndLine = NO; +static BOOL sShowClass = YES; +static BOOL sDefaultDisplay = YES; +static BOOL sOverrideInEffect = NO; +static BOOL sOverrideValue = NO; + +// These specific values are used for true, false and inherit in the cache and explicitSettings dictionaries so we can use pointer comparison. +static NSString * const kTrueToken = @"on"; +static NSString * const kFalseToken = @"off"; +static NSString * const kInheritToken = @"inherit"; + + +// To avoid recursion/self-dependencies, OOLog gets its own logging function. +#define OOLogInternal(cond, format, ...) do { if ((cond)) { OOLogInternal_(OOLOG_FUNCTION_NAME, format, ## __VA_ARGS__); }} while (0) +static void OOLogInternal_(const char *inFunction, NSString *inFormat, ...); + + +// Functions used internally +static void LoadExplicitSettings(void); +static void LoadExplicitSettingsFromDictionary(NSDictionary *inDict); +static id ResolveDisplaySetting(NSString *inMessageClass); +static id ResolveMetaClassReference(NSString *inMetaClass, NSMutableSet *ioSeenMetaClasses); + +OOINLINE unsigned GetIndentLevel(void) PURE_FUNC; +OOINLINE void SetIndentLevel(unsigned level); + + +#ifndef OOLOG_NO_FILE_NAME +static NSMutableDictionary *sFileNamesCache = nil; +static NSString *AbbreviatedFileName(const char *inName); +#endif + + +// Given a boolean, return the appropriate value for the cache dictionary. +static inline id CacheValue(BOOL inValue) __attribute__((pure)); +static inline id CacheValue(BOOL inValue) +{ + return inValue ? kTrueToken : kFalseToken; +} + + +/* Inited() + Test wether OOLoggingInit() has been called. +*/ +static inline BOOL Inited(void) +{ + if (EXPECT(sInited)) return YES; + OOLogInternal(OOLOG_NOT_INITED, @"***** ERROR: OOLoggingInit() has not been called."); + return NO; +} + + +BOOL OOLogWillDisplayMessagesInClass(NSString *inMessageClass) +{ + id value = nil; + + if (!Inited()) return NO; + + if (sOverrideInEffect) return sOverrideValue; + + [sLock lock]; + + // Look for cached value + value = [sDerivedSettingsCache objectForKey:inMessageClass]; + if (EXPECT_NOT(value == nil)) + { + // No cached value. + value = ResolveDisplaySetting(inMessageClass); + + if (value != nil) + { + if (EXPECT_NOT(sDerivedSettingsCache == nil)) sDerivedSettingsCache = [[NSMutableDictionary alloc] init]; + [sDerivedSettingsCache setObject:value forKey:inMessageClass]; + } + } + [sLock unlock]; + + OOLogInternal(OOLOG_SETTING_RETRIEVE, @"%@ is %s", inMessageClass, (value == kTrueToken) ? "on" : "off"); + return value == kTrueToken; +} + + +void OOLogSetDisplayMessagesInClass(NSString *inClass, BOOL inFlag) +{ + id value = nil; + + if (!Inited()) return; + + [sLock lock]; + value = [sExplicitSettings objectForKey:inClass]; + if (value == nil || value != CacheValue(inFlag)) + { + OOLogInternal(OOLOG_SETTING_SET, @"Setting %@ to %s", inClass, inFlag ? "ON" : "OFF"); + + [sExplicitSettings setObject:CacheValue(inFlag) forKey:inClass]; + + // Clear cache and let it be rebuilt as needed. Cost of rebuilding cache is not sufficient to warrant complexity of a partial clear. + [sDerivedSettingsCache release]; + sDerivedSettingsCache = nil; + } + else + { + OOLogInternal(OOLOG_SETTING_SET, @"Keeping %@ %s", inClass, inFlag ? "ON" : "OFF"); + } + [sLock unlock]; +} + + +NSString *OOLogGetParentMessageClass(NSString *inClass) +{ + NSRange range; + + if (inClass == nil) return nil; + + range = [inClass rangeOfString:@"." options:NSCaseInsensitiveSearch | NSLiteralSearch | NSBackwardsSearch]; // Only NSBackwardsSearch is important, others are optimizations + if (range.location == NSNotFound) return nil; + + return [inClass substringToIndex:range.location]; +} + + +#if !OOLOG_SHORT_CIRCUIT + +void OOLogIndentIf(NSString *inMessageClass) +{ + if (OOLogWillDisplayMessagesInClass(inMessageClass)) OOLogIndent(); +} + + +void OOLogOutdentIf(NSString *inMessageClass) +{ + if (OOLogWillDisplayMessagesInClass(inMessageClass)) OOLogOutdent(); +} + +#endif + + +#if USE_INDENT_GLOBALS + +#if OOLITE_USE_TLS + #define INDENT_LOCK() do {} while (0) + #define INDENT_UNLOCK() do {} while (0) +#else + #define INDENT_LOCK() [sLock lock] + #define INDENT_UNLOCK() [sLock unlock] +#endif + + +OOINLINE unsigned GetIndentLevel(void) +{ + return sIndentLevel; +} + + +OOINLINE void SetIndentLevel(unsigned value) +{ + sIndentLevel = value; +} + + +void OOLogPushIndent(void) +{ + OOLogIndentStackElement *elem = NULL; + + elem = malloc(sizeof *elem); + if (elem != NULL) + { + INDENT_LOCK(); + + elem->indent = sIndentLevel; + elem->link = sIndentStack; + sIndentStack = elem; + + INDENT_UNLOCK(); + } +} + + +void OOLogPopIndent(void) +{ + INDENT_LOCK(); + + OOLogIndentStackElement *elem = sIndentStack; + + if (elem != NULL) + { + sIndentStack = elem->link; + sIndentLevel = elem->indent; + free(elem); + } + else + { + OOLogInternal(OOLOG_BAD_POP_INDENT, @"OOLogPopIndent(): state stack underflow."); + } + INDENT_UNLOCK(); +} + +#else // !USE_INDENT_GLOBALS + +#define INDENT_LOCK() do {} while (0) +#define INDENT_UNLOCK() do {} while (0) + + +OOINLINE unsigned GetIndentLevel(void) +{ + NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary]; + return [[threadDict objectForKey:kIndentLevelKey] unsignedIntValue]; +} + + +OOINLINE void SetIndentLevel(unsigned value) +{ + NSMutableDictionary *threadDict = [[NSThread currentThread] threadDictionary]; + [threadDict setObject:[NSNumber numberWithUnsignedInt:value] forKey:kIndentLevelKey]; +} + + +void OOLogPushIndent(void) +{ + OOLogIndentStackElement *elem = NULL; + NSMutableDictionary *threadDict = nil; + NSValue *val = nil; + + elem = malloc(sizeof *elem); + if (elem != NULL) + { + threadDict = [[NSThread currentThread] threadDictionary]; + val = [threadDict objectForKey:kIndentStackKey]; + + elem->indent = [[threadDict objectForKey:kIndentLevelKey] intValue]; + elem->link = [val pointerValue]; + [threadDict setObject:[NSValue valueWithPointer:elem] forKey:kIndentStackKey]; + } +} + + +void OOLogPopIndent(void) +{ + OOLogIndentStackElement *elem = NULL; + NSMutableDictionary *threadDict = nil; + NSValue *val = nil; + + threadDict = [[NSThread currentThread] threadDictionary]; + val = [threadDict objectForKey:kIndentStackKey]; + + elem = [val pointerValue]; + + if (elem != NULL) + { + [threadDict setObject:[NSNumber numberWithUnsignedInt:elem->indent] forKey:kIndentLevelKey]; + [threadDict setObject:[NSValue valueWithPointer:elem->link] forKey:kIndentStackKey]; + free(elem); + } + else + { + OOLogInternal(OOLOG_BAD_POP_INDENT, @"OOLogPopIndent(): state stack underflow."); + } +} + +#endif // USE_INDENT_GLOBALS + + +void OOLogIndent(void) +{ + INDENT_LOCK(); + + SetIndentLevel(GetIndentLevel() + 1); + + INDENT_UNLOCK(); +} + + +void OOLogOutdent(void) +{ + INDENT_LOCK(); + + unsigned indentLevel = GetIndentLevel(); + if (indentLevel != 0) SetIndentLevel(indentLevel - 1); + + INDENT_UNLOCK(); +} + + +void OOLogWithPrefix(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inPrefix, NSString *inFormat, ...) +{ + if (!OOLogWillDisplayMessagesInClass(inMessageClass)) return; + va_list args; + va_start(args, inFormat); + OOLogWithFunctionFileAndLineAndArguments(inMessageClass, inFunction, inFile, inLine, [inPrefix stringByAppendingString:inFormat], args); + va_end(args); +} + + +void OOLogWithFunctionFileAndLine(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inFormat, ...) +{ + va_list args; + + va_start(args, inFormat); + OOLogWithFunctionFileAndLineAndArguments(inMessageClass, inFunction, inFile, inLine, inFormat, args); + va_end(args); +} + + +void OOLogWithFunctionFileAndLineAndArguments(NSString *inMessageClass, const char *inFunction, const char *inFile, unsigned long inLine, NSString *inFormat, va_list inArguments) +{ + NSAutoreleasePool *pool = nil; + NSString *formattedMessage = nil; + unsigned indentLevel; + + if (inFormat == nil) return; + +#if !OOLOG_SHORT_CIRCUIT + if (!OOLogWillDisplayMessagesInClass(inMessageClass)) return; +#endif + + pool = [[NSAutoreleasePool alloc] init]; + NS_DURING + // Do argument substitution + formattedMessage = [[[NSString alloc] initWithFormat:inFormat arguments:inArguments] autorelease]; + + // Apply various prefix options + #ifndef OOLOG_NO_FILE_NAME + if (sShowFileAndLine && inFile != NULL) + { + if (sShowFunction) + { + formattedMessage = [NSString stringWithFormat:@"%s (%@:%u): %@", inFunction, AbbreviatedFileName(inFile), inLine, formattedMessage]; + } + else + { + formattedMessage = [NSString stringWithFormat:@"%@:%u: %@", AbbreviatedFileName(inFile), inLine, formattedMessage]; + } + } + else + #endif + { + if (sShowFunction) + { + formattedMessage = [NSString stringWithFormat:@"%s: %@", inFunction, formattedMessage]; + } + } + + if (sShowClass) + { + if (sShowFunction || sShowFileAndLine) + { + formattedMessage = [NSString stringWithFormat:@"[%@] %@", inMessageClass, formattedMessage]; + } + else + { + formattedMessage = [NSString stringWithFormat:@"[%@]: %@", inMessageClass, formattedMessage]; + } + } + + // Apply indentation + indentLevel = GetIndentLevel(); + if (indentLevel != 0) + { + #define INDENT_FACTOR 2 /* Spaces per indent level */ + #define MAX_INDENT 64 /* Maximum number of indentation _spaces_ */ + + unsigned indent; + // String of 64 spaces (null-terminated) + const char spaces[MAX_INDENT + 1] = + " "; + const char *indentString; + + indent = INDENT_FACTOR * indentLevel; + if (MAX_INDENT < indent) indent = MAX_INDENT; + indentString = &spaces[MAX_INDENT - indent]; + + formattedMessage = [NSString stringWithFormat:@"%s%@", indentString, formattedMessage]; + } + + OOLogOutputHandlerPrint(formattedMessage); + NS_HANDLER + OOLogInternal(OOLOG_EXCEPTION_IN_LOG, @"***** Exception thrown during logging: %@ : %@", [localException name], [localException reason]); + NS_ENDHANDLER + + [pool release]; +} + + +void OOLogGenericParameterErrorForFunction(const char *inFunction) +{ + OOLog(kOOLogParameterError, @"***** %s: bad parameters. (This is an internal programming error, please report it.)", inFunction); +} + + +void OOLogGenericSubclassResponsibilityForFunction(const char *inFunction) +{ + OOLog(kOOLogSubclassResponsibility, @"***** %s is a subclass responsibility. (This is an internal programming error, please report it.)", inFunction); +} + + +BOOL OOLogShowFunction(void) +{ + return sShowFunction; +} + + +void OOLogSetShowFunction(BOOL flag) +{ + flag = !!flag; // YES or NO, not 42. + + if (flag != sShowFunction) + { + sShowFunction = flag; + [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"logging-show-function"]; + } +} + + +BOOL OOLogShowFileAndLine(void) +{ + return sShowFileAndLine; +} + + +void OOLogSetShowFileAndLine(BOOL flag) +{ + flag = !!flag; // YES or NO, not 42. + + if (flag != sShowFileAndLine) + { + sShowFileAndLine = flag; + [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"logging-show-file-and-line"]; + } +} + + +BOOL OOLogShowMessageClass(void) +{ + return sShowClass; +} + + +void OOLogSetShowMessageClass(BOOL flag) +{ + flag = !!flag; // YES or NO, not 42. + + if (flag != sShowClass) + { + sShowClass = flag; + [[NSUserDefaults standardUserDefaults] setBool:flag forKey:@"logging-show-class"]; + } +} + + +void OOLogSetShowMessageClassTemporary(BOOL flag) +{ + sShowClass = !!flag; // YES or NO, not 42. +} + + +void OOLoggingInit(void) +{ + NSAutoreleasePool *pool = nil; + + if (sInited) return; + + pool = [[NSAutoreleasePool alloc] init]; + + OOLogOutputHandlerInit(); + + sLock = [[NSLock alloc] init]; + [sLock ooSetName:@"OOLogging lock"]; + if (sLock == nil) exit(EXIT_FAILURE); + + LoadExplicitSettings(); + sInited = YES; + + OOPrintLogHeader(); + + [pool release]; +} + + +void OOLoggingTerminate(void) +{ + if (!sInited) return; + + OOLogOutputHandlerClose(); + + /* We do not set sInited to NO. Instead, the output handler is required + to be able to handle working even after being closed. Under OS X, this + is done by writing to stderr in this case; on other platforms, NSLog() + is used and OOLogOutputHandlerClose() is a no-op. + */ +} + + +void OOLogInsertMarker(void) +{ + static unsigned lastMarkerID = 0; + unsigned thisMarkerID; + NSString *marker = nil; + + [sLock lock]; + thisMarkerID = ++lastMarkerID; + [sLock unlock]; + + marker = [NSString stringWithFormat:@"\n\n========== [Marker %u] ==========", thisMarkerID]; + OOLogOutputHandlerPrint(marker); +} + +NSString * const kOOLogSubclassResponsibility = @"general.error.subclassResponsibility"; +NSString * const kOOLogParameterError = @"general.error.parameterError"; +NSString * const kOOLogDeprecatedMethod = @"general.error.deprecatedMethod"; +NSString * const kOOLogAllocationFailure = @"general.error.allocationFailure"; +NSString * const kOOLogInconsistentState = @"general.error.inconsistentState"; +NSString * const kOOLogException = @"exception"; +NSString * const kOOLogFileNotFound = @"files.notFound"; +NSString * const kOOLogFileNotLoaded = @"files.notLoaded"; +NSString * const kOOLogOpenGLError = @"rendering.opengl.error"; +NSString * const kOOLogUnconvertedNSLog = @"unclassified"; + + +/* OOLogInternal_() + Implementation of OOLogInternal(), private logging function used by + OOLogging so it doesn’t depend on itself (and risk recursiveness). +*/ +static void OOLogInternal_(const char *inFunction, NSString *inFormat, ...) +{ + va_list args; + NSString *formattedMessage = nil; + NSAutoreleasePool *pool = nil; + + pool = [[NSAutoreleasePool alloc] init]; + + NS_DURING + va_start(args, inFormat); + formattedMessage = [[[NSString alloc] initWithFormat:inFormat arguments:args] autorelease]; + va_end(args); + + formattedMessage = [NSString stringWithFormat:@"OOLogging internal - %s: %@", inFunction, formattedMessage]; + + OOLogOutputHandlerPrint(formattedMessage); + NS_HANDLER + fprintf(stderr, "***** Exception in OOLogInternal_(): %s : %s", [[localException name] UTF8String], [[localException reason] UTF8String]); + NS_ENDHANDLER + + [pool release]; +} + + +/* LoadExplicitSettings() + Read settings from logcontrol.plist, merge in settings from preferences. +*/ +static void LoadExplicitSettings(void) +{ + NSEnumerator *rootEnum = nil; + NSString *basePath = nil; + NSString *configPath = nil; + NSDictionary *dict = nil; + NSUserDefaults *prefs = nil; + id value = nil; + + if (sExplicitSettings != nil) return; + + sExplicitSettings = [[NSMutableDictionary alloc] init]; + + rootEnum = [[ResourceManager rootPaths] objectEnumerator]; + while ((basePath = [rootEnum nextObject])) + { + configPath = [[basePath stringByAppendingPathComponent:@"Config"] + stringByAppendingPathComponent:@"logcontrol.plist"]; + dict = OODictionaryFromFile(configPath); + if (dict == nil) + { + configPath = [basePath stringByAppendingPathComponent:@"logcontrol.plist"]; + dict = OODictionaryFromFile(configPath); + } + if (dict != nil) + { + LoadExplicitSettingsFromDictionary(dict); + } + } + + // Get overrides from preferences + prefs = [NSUserDefaults standardUserDefaults]; + dict = [prefs objectForKey:@"logging-enable"]; + if ([dict isKindOfClass:[NSDictionary class]]) + { + LoadExplicitSettingsFromDictionary(dict); + } + + // Get _default and _override value + value = [sExplicitSettings objectForKey:@"_default"]; + if (value != nil && [value respondsToSelector:@selector(boolValue)]) + { + if (value == kTrueToken) sDefaultDisplay = YES; + else if (value == kFalseToken) sDefaultDisplay = NO; + else OOLogInternal(OOLOG_BAD_DEFAULT_SETTING, @"_default may not be set to a metaclass, ignoring."); + + [sExplicitSettings removeObjectForKey:@"_default"]; + } + value = [sExplicitSettings objectForKey:@"_override"]; + if (value != nil && [value respondsToSelector:@selector(boolValue)]) + { + if (value == kTrueToken) + { + sOverrideInEffect = YES; + sOverrideValue = YES; + } + else if (value == kFalseToken) + { + sOverrideInEffect = YES; + sOverrideValue = NO; + } + else OOLogInternal(OOLOG_BAD_DEFAULT_SETTING, @"_override may not be set to a metaclass, ignoring."); + + [sExplicitSettings removeObjectForKey:@"_override"]; + } + + // Load display settings + sShowFunction = [prefs boolForKey:@"logging-show-function" defaultValue:sShowFunction]; + sShowFileAndLine = [prefs boolForKey:@"logging-show-file-and-line" defaultValue:sShowFileAndLine]; + sShowClass = [prefs boolForKey:@"logging-show-class" defaultValue:sShowClass]; + + OOLogInternal(OOLOG_SETTING_SET, @"Settings: %@", sExplicitSettings); +} + + +/* LoadExplicitSettingsFromDictionary() + Helper for LoadExplicitSettings(). +*/ +static void LoadExplicitSettingsFromDictionary(NSDictionary *inDict) +{ + NSEnumerator *keyEnum = nil; + id key = nil; + id value = nil; + + for (keyEnum = [inDict keyEnumerator]; (key = [keyEnum nextObject]); ) + { + value = [inDict objectForKey:key]; + + /* Supported values: + "yes", "true" or "on" -> kTrueToken + "no", "false" or "off" -> kFalseToken + "inherit" or "inherited" -> nil + NSNumber -> kTrueToken or kFalseToken + "$metaclass" -> "$metaclass" + */ + if ([value isKindOfClass:[NSString class]]) + { + if (NSOrderedSame == [value caseInsensitiveCompare:@"yes"] || + NSOrderedSame == [value caseInsensitiveCompare:@"true"] || + NSOrderedSame == [value caseInsensitiveCompare:@"on"]) + { + value = kTrueToken; + } + else if (NSOrderedSame == [value caseInsensitiveCompare:@"no"] || + NSOrderedSame == [value caseInsensitiveCompare:@"false"] || + NSOrderedSame == [value caseInsensitiveCompare:@"off"]) + { + value = kFalseToken; + } + else if (NSOrderedSame == [value caseInsensitiveCompare:@"inherit"] || + NSOrderedSame == [value caseInsensitiveCompare:@"inherited"]) + { + value = nil; + [sExplicitSettings removeObjectForKey:key]; + } + else if (![value hasPrefix:@"$"]) + { + OOLogInternal(OOLOG_BAD_SETTING, @"Bad setting value \"%@\" (expected yes, no, inherit or $metaclass).", value); + value = nil; + } + } + else if ([value respondsToSelector:@selector(boolValue)]) + { + value = CacheValue([value boolValue]); + } + else + { + OOLogInternal(OOLOG_BAD_SETTING, @"Bad setting value \"%@\" (expected yes, no, inherit or $metaclass).", value); + value = nil; + } + + if (value != nil) + { + [sExplicitSettings setObject:value forKey:key]; + } + } +} + + +#ifndef OOLOG_NO_FILE_NAME +/* AbbreviatedFileName() + Map full file paths provided by __FILE__ to more mananagable file names, + with caching. +*/ +static NSString *AbbreviatedFileName(const char *inName) +{ + NSValue *key = nil; + NSString *name = nil; + + if (inName == NULL) return @"unspecified file"; + + [sLock lock]; + key = [NSValue valueWithPointer:inName]; + name = [sFileNamesCache objectForKey:key]; + if (name == nil) + { + name = [[NSString stringWithUTF8String:inName] lastPathComponent]; + if (sFileNamesCache == nil) sFileNamesCache = [[NSMutableDictionary alloc] init]; + [sFileNamesCache setObject:name forKey:key]; + } + [sLock unlock]; + + return name; +} +#endif + + +/* Look up setting for a message class in explicit settings, resolving + inheritance and metaclasses. +*/ +static id ResolveDisplaySetting(NSString *inMessageClass) +{ + id value = nil; + NSMutableSet *seenMetaClasses = nil; + + if (inMessageClass == nil) return CacheValue(sDefaultDisplay); + + value = [sExplicitSettings objectForKey:inMessageClass]; + + // Simple case: explicit setting for this value + if (value == kTrueToken || value == kFalseToken) return value; + + // Simplish case: use inherited value + if (value == nil || value == kInheritToken) return ResolveDisplaySetting(OOLogGetParentMessageClass(inMessageClass)); + + // Less simple case: should be a metaclass. + seenMetaClasses = [NSMutableSet set]; + return ResolveMetaClassReference(value, seenMetaClasses); +} + + +/* Resolve a metaclass reference, recursively if necessary. The + ioSeenMetaClasses dictionary is used to avoid loops. +*/ +static id ResolveMetaClassReference(NSString *inMetaClass, NSMutableSet *ioSeenMetaClasses) +{ + id value = nil; + + // All values should have been checked at load time, but what the hey. + if (![inMetaClass isKindOfClass:[NSString class]] || ![inMetaClass hasPrefix:@"$"]) + { + OOLogInternal(OOLOG_BAD_SETTING, @"Bad setting value \"%@\" (expected yes, no, inherit or $metaclass). Falling back to _default.", inMetaClass); + return CacheValue(sDefaultDisplay); + } + + [ioSeenMetaClasses addObject:inMetaClass]; + + value = [sExplicitSettings objectForKey:inMetaClass]; + + if (value == kTrueToken || value == kFalseToken) return value; + if (value == nil) + { + OOLogInternal(OOLOG_UNDEFINED_METACLASS, @"Reference to undefined metaclass %@, falling back to _default.", inMetaClass); + return CacheValue(sDefaultDisplay); + } + + // If we get here, it should be a metaclass reference. + return ResolveMetaClassReference(value, ioSeenMetaClasses); +} diff --git a/src/Core/OOLoggingExtended.h b/src/Core/OOLoggingExtended.h new file mode 100644 index 00000000..89f86947 --- /dev/null +++ b/src/Core/OOLoggingExtended.h @@ -0,0 +1,75 @@ +/* + +OOLoggingExtended.h +By Jens Ayton + +Configuration functions for OOLogging.h. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOLogging.h" + + +void OOLogSetDisplayMessagesInClass(NSString *inClass, BOOL inFlag); +NSString *OOLogGetParentMessageClass(NSString *inClass); + + +void OOLoggingInit(void); +void OOLoggingTerminate(void); + + +void OOLogInsertMarker(void); + + +// Get/set display settings. These are stored in user defaults. +BOOL OOLogShowFunction(void); +void OOLogSetShowFunction(BOOL flag); +BOOL OOLogShowFileAndLine(void); +void OOLogSetShowFileAndLine(BOOL flag); +BOOL OOLogShowMessageClass(void); +void OOLogSetShowMessageClass(BOOL flag); + +// Change message class visibility without saving to user defaults. +void OOLogSetShowMessageClassTemporary(BOOL flag); diff --git a/src/Core/OOMacroOpenGL.h b/src/Core/OOMacroOpenGL.h new file mode 100644 index 00000000..3c406a76 --- /dev/null +++ b/src/Core/OOMacroOpenGL.h @@ -0,0 +1,51 @@ +/* + +OOMacroOpenGL.h + +Under Mac OS X, OpenGL performance can be improved somewhat by using macros +that call through to a function table directly, avoiding calls to functions +that just look up the current context and pass their parameters through to +a context-specific implementation function. + +This header abstracts that behaviour for cross-platformity. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#if OOLITE_MAC_OS_X && !defined(OOLITE_NO_CGL_MACRO) + +#if MAC_OS_X_VERSION_10_4 <= MAC_OS_X_VERSION_MAX_ALLOWED + +#define CGL_MACRO_CACHE_RENDERER +#import + +#define OO_ENTER_OPENGL CGL_MACRO_DECLARE_VARIABLES + +#else + +#import + +#define OO_ENTER_OPENGL() CGLContextObj CGL_MACRO_CONTEXT = CGLGetCurrentContext(); \ + +#endif +#else +// Not OS X +#define OO_ENTER_OPENGL() do {} while (0) +#endif diff --git a/src/Core/OOMaths.h b/src/Core/OOMaths.h new file mode 100644 index 00000000..8973b2a2 --- /dev/null +++ b/src/Core/OOMaths.h @@ -0,0 +1,114 @@ +/* + +OOMaths.h + +Mathematical framework for Oolite. + +Provides utility routines for Vectors, Quaternions, rotation matrices, and +conversion to OpenGL transformation matrices. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h +#define INCLUDED_OOMATHS_h + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef __OBJC__ +#import +#endif + +#include "OOFunctionAttributes.h" +#include +#include +#include +#include "OOOpenGL.h" + + +#ifndef M_PI + #define M_PI 3.14159265358979323846 /* pi */ +#endif +#ifndef M_PI_2 + #define M_PI_2 1.57079632679489661923 /* pi/2 */ +#endif +#ifndef M_PI_4 + #define M_PI_4 0.78539816339744830962 /* pi/4 */ +#endif +#ifndef M_1_PI + #define M_1_PI 0.31830988618379067154 /* 1/pi */ +#endif +#ifndef M_2_PI + #define M_2_PI 0.63661977236758134308 /* 2/pi */ +#endif +#ifndef M_2_SQRTPI + #define M_2_SQRTPI 1.12837916709551257390 /* 2/sqrt(pi) */ +#endif +#ifndef M_SQRT2 + #define M_SQRT2 1.41421356237309504880 /* sqrt(2) */ +#endif +#ifndef M_SQRT1_2 + #define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */ +#endif + + +#if defined(__GNUC__) && !defined(__STRICT_ANSI__) + #ifndef MIN + #define MIN(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __a : __b; }) + #endif + #if !defined(MAX) + #define MAX(A,B) ({ __typeof__(A) __a = (A); __typeof__(B) __b = (B); __a < __b ? __b : __a; }) + #endif + #if !defined(ABS) + #define ABS(A) ({ __typeof__(A) __a = (A); __a < 0 ? -__a : __a; }) + #endif +#else + /* These definitions are unsafe in that the "winning" expression is evaluated twice. */ + #if !defined(MIN) + #define MIN(A,B) ((A) < (B) ? (A) : (B)) + #endif + #if !defined(MAX) + #define MAX(A,B) ((A) > (B) ? (A) : (B)) + #endif + #if !defined(ABS) + #define ABS(A) ((A) < 0 ? (-(A)) : (A)) + #endif +#endif + + +#include "OOFastArithmetic.h" +#include "OOVector.h" +#include "OOQuaternion.h" +#include "OOMatrix.h" +#include "OOVoxel.h" +#include "OOTriangle.h" +#include "OOBoundingBox.h" + +#include "legacy_random.h" + + +#ifdef __cplusplus +} +#endif + +#endif /* INCLUDED_OOMATHS_h */ diff --git a/src/Core/OOMatrix.h b/src/Core/OOMatrix.h new file mode 100644 index 00000000..436a8ba8 --- /dev/null +++ b/src/Core/OOMatrix.h @@ -0,0 +1,278 @@ +/* + +OOMatrix.h + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOMatrix.h directly; include OOMaths.h. +#else + +typedef struct OOMatrix +{ + GLfloat m[4][4]; +} OOMatrix; + + +extern const OOMatrix kIdentityMatrix; /* {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1} */ +extern const OOMatrix kZeroMatrix; /* {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0} */ + + +/* Matrix construction and standard primitive matrices */ +OOINLINE OOMatrix OOMatrixConstruct(GLfloat aa, GLfloat ab, GLfloat ac, GLfloat ad, + GLfloat ba, GLfloat bb, GLfloat bc, GLfloat bd, + GLfloat ca, GLfloat cb, GLfloat cc, GLfloat cd, + GLfloat da, GLfloat db, GLfloat dc, GLfloat dd) INLINE_CONST_FUNC; + +OOINLINE OOMatrix OOMatrixFromOrientationAndPosition(Quaternion orientation, Vector position) INLINE_CONST_FUNC; + +OOINLINE OOMatrix OOMatrixFromBasisVectorsAndPosition(Vector i, Vector j, Vector k, Vector position) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixFromBasisVectors(Vector i, Vector j, Vector k) INLINE_CONST_FUNC; + +OOINLINE OOMatrix OOMatrixForRotationX(GLfloat angle) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixForRotationY(GLfloat angle) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixForRotationZ(GLfloat angle) INLINE_CONST_FUNC; +OOMatrix OOMatrixForRotation(Vector axis, GLfloat angle) CONST_FUNC; +OOMatrix OOMatrixForQuaternionRotation(Quaternion orientation); + +OOINLINE OOMatrix OOMatrixForTranslation(Vector v) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixForTranslationComponents(GLfloat dx, GLfloat dy, GLfloat dz) INLINE_CONST_FUNC; + +OOMatrix OOMatrixForBillboard(Vector bbPos, Vector eyePos) CONST_FUNC; + + +/* Matrix transformations */ +OOINLINE OOMatrix OOMatrixTranslate(OOMatrix m, Vector offset) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixTranslateComponents(OOMatrix m, GLfloat dx, GLfloat dy, GLfloat dz) INLINE_CONST_FUNC; + +OOINLINE OOMatrix OOMatrixRotateX(OOMatrix m, GLfloat angle) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixRotateY(OOMatrix m, GLfloat angle) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixRotateZ(OOMatrix m, GLfloat angle) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixRotate(OOMatrix m, Vector axis, GLfloat angle) INLINE_CONST_FUNC; +OOINLINE OOMatrix OOMatrixRotateQuaternion(OOMatrix m, Quaternion quat) INLINE_CONST_FUNC; + + +/* Matrix multiplication */ +OOMatrix OOMatrixMultiply(OOMatrix a, OOMatrix b) CONST_FUNC; +Vector OOVectorMultiplyMatrix(Vector v, OOMatrix m) CONST_FUNC; + + +/* Extraction */ +OOINLINE void OOMatrixGetBasisVectors(OOMatrix m, Vector *outRight, Vector *outUp, Vector *outForward) NONNULL_FUNC ALWAYS_INLINE_FUNC; + + +/* Orthogonalizion - avoidance of distortions due to numerical inaccuracy. */ +OOMatrix OOMatrixOrthogonalize(OOMatrix m) CONST_FUNC; + + +/* OpenGL conveniences. Need to be macros to work with OOMacroOpenGL. */ +#define OOMatrixValuesForOpenGL(M) (&(M).m[0][0]) +#define GLMultOOMatrix(M) do { OOMatrix m_ = M; glMultMatrixf(OOMatrixValuesForOpenGL(m_)); } while (0) +#define GLLoadOOMatrix(M) do { OOMatrix m_ = M; glLoadMatrixf(OOMatrixValuesForOpenGL(m_)); } while (0) +#define GLMultTransposeOOMatrix(M) do { OOMatrix m_ = M; glMultTransposeMatrixf(OOMatrixValuesForOpenGL(m_)); } while (0) +#define GLLoadTransposeOOMatrix(M) do { OOMatrix m_ = M; glLoadTransposeMatrixf(OOMatrixValuesForOpenGL(m_)); } while (0) +#define GLUniformMatrix(location, M) do { glUniformMatrix4fvARB(location, 1, NO, OOMatrixValuesForOpenGL(M)); } while (0) + +OOINLINE OOMatrix OOMatrixLoadGLMatrix(unsigned long /* GLenum */ matrixID) ALWAYS_INLINE_FUNC; + + +#ifdef __OBJC__ +NSString *OOMatrixDescription(OOMatrix matrix); // @"{{#, #, #, #}, {#, #, #, #}, {#, #, #, #}, {#, #, #, #}}" +#endif + + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE OOMatrix OOMatrixConstruct(GLfloat aa, GLfloat ab, GLfloat ac, GLfloat ad, + GLfloat ba, GLfloat bb, GLfloat bc, GLfloat bd, + GLfloat ca, GLfloat cb, GLfloat cc, GLfloat cd, + GLfloat da, GLfloat db, GLfloat dc, GLfloat dd) +{ + OOMatrix r = + {{ + { aa, ab, ac, ad }, + { ba, bb, bc, bd }, + { ca, cb, cc, cd }, + { da, db, dc, dd } + }}; + return r; +} + +OOINLINE OOMatrix OOMatrixFromOrientationAndPosition(Quaternion orientation, Vector position) +{ + OOMatrix m = OOMatrixForQuaternionRotation(orientation); + return OOMatrixTranslate(m, position); +} + + +OOINLINE OOMatrix OOMatrixFromBasisVectorsAndPosition(Vector i, Vector j, Vector k, Vector p) +{ + return OOMatrixConstruct + ( + i.x, i.y, i.z, 0.0f, + j.x, j.y, j.z, 0.0f, + k.x, k.y, k.z, 0.0f, + p.x, p.y, p.z, 1.0f + ); +} + + +OOINLINE OOMatrix OOMatrixFromBasisVectors(Vector i, Vector j, Vector k) +{ + return OOMatrixFromBasisVectorsAndPosition(i, j, k, kZeroVector); +} + + +/* Standard primitive transformation matrices: */ +OOMatrix OOMatrixForRotationX(GLfloat angle) +{ + GLfloat s, c; + + s = sinf(angle); + c = cosf(angle); + + return OOMatrixConstruct + ( + 1, 0, 0, 0, + 0, c, s, 0, + 0, -s, c, 0, + 0, 0, 0, 1 + ); +} + + +OOMatrix OOMatrixForRotationY(GLfloat angle) +{ + GLfloat s, c; + + s = sinf(angle); + c = cosf(angle); + + return OOMatrixConstruct + ( + c, 0, -s, 0, + 0, 1, 0, 0, + s, 0, c, 0, + 0, 0, 0, 1 + ); +} + + +OOMatrix OOMatrixForRotationZ(GLfloat angle) +{ + GLfloat s, c; + + s = sinf(angle); + c = cosf(angle); + + return OOMatrixConstruct + ( + c, s, 0, 0, + -s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ); +} +OOINLINE OOMatrix OOMatrixForTranslationComponents(GLfloat dx, GLfloat dy, GLfloat dz) +{ + return OOMatrixConstruct + ( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + dx, dy, dz, 1 + ); +} + + +OOINLINE OOMatrix OOMatrixForTranslation(Vector v) +{ + return OOMatrixForTranslationComponents(v.x, v.y, v.z); +} + + +OOINLINE OOMatrix OOMatrixTranslateComponents(OOMatrix m, GLfloat dx, GLfloat dy, GLfloat dz) +{ + m.m[3][0] += dx; + m.m[3][1] += dy; + m.m[3][2] += dz; + return m; +} + + +OOINLINE OOMatrix OOMatrixTranslate(OOMatrix m, Vector offset) +{ + return OOMatrixTranslateComponents(m, offset.x, offset.y, offset.z); +} + + +OOINLINE OOMatrix OOMatrixRotateX(OOMatrix m, GLfloat angle) +{ + return OOMatrixMultiply(m, OOMatrixForRotationX(angle)); +} + + +OOINLINE OOMatrix OOMatrixRotateY(OOMatrix m, GLfloat angle) +{ + return OOMatrixMultiply(m, OOMatrixForRotationY(angle)); +} + + +OOINLINE OOMatrix OOMatrixRotateZ(OOMatrix m, GLfloat angle) +{ + return OOMatrixMultiply(m, OOMatrixForRotationZ(angle)); +} + + +OOINLINE OOMatrix OOMatrixRotate(OOMatrix m, Vector axis, GLfloat angle) +{ + return OOMatrixMultiply(m, OOMatrixForRotation(axis, angle)); +} + + +OOINLINE OOMatrix OOMatrixRotateQuaternion(OOMatrix m, Quaternion quat) +{ + return OOMatrixMultiply(m, OOMatrixForQuaternionRotation(quat)); +} + + +OOINLINE void OOMatrixGetBasisVectors(OOMatrix m, Vector *outRight, Vector *outUp, Vector *outForward) +{ + assert(outRight != NULL && outUp != NULL && outForward != NULL); + + *outRight = make_vector(m.m[0][0], m.m[1][0], m.m[2][0]); + *outUp = make_vector(m.m[0][1], m.m[1][1], m.m[2][1]); + *outForward = make_vector(m.m[0][2], m.m[1][2], m.m[2][2]); +} + + +OOINLINE OOMatrix OOMatrixLoadGLMatrix(unsigned long /* GLenum */ matrixID) +{ + OOMatrix m; + glGetFloatv(matrixID, OOMatrixValuesForOpenGL(m)); + return m; +} + +#endif /* INCLUDED_OOMATHS_h */ diff --git a/src/Core/OOMatrix.m b/src/Core/OOMatrix.m new file mode 100644 index 00000000..e9129a81 --- /dev/null +++ b/src/Core/OOMatrix.m @@ -0,0 +1,171 @@ +/* + +OOMatrix.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOMaths.h" + + +const OOMatrix kIdentityMatrix = + { m: { + {1.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 1.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 1.0f} + }}; +const OOMatrix kZeroMatrix = { m: { + {0.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f}, + {0.0f, 0.0f, 0.0f, 0.0f}, + }}; + + +OOMatrix OOMatrixForRotation(Vector axis, GLfloat angle) +{ + axis = vector_normal(axis); + + GLfloat x = axis.x, y = axis.y, z = axis.z; + GLfloat s = sinf(angle), c = cosf(angle); + GLfloat t = 1.0f - c; + + // Lots of opportunity for common subexpression elimintation here, but I'll leave it to the compiler for now. + return OOMatrixConstruct + ( + t * x * x + c, t * x * y + s * z, t * x * z - s * y, 0.0f, + t * x * y - s * z, t * y * y + c, t * y * z + s * x, 0.0f, + t * x * y + s * y, t * y * z - s * x, t * z * z + c, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); +} + + +OOMatrix OOMatrixForQuaternionRotation(Quaternion orientation) +{ + GLfloat w, wz, wy, wx; + GLfloat x, xz, xy, xx; + GLfloat y, yz, yy; + GLfloat z, zz; + + Quaternion q = orientation; + quaternion_normalize(&q); + + w = q.w; + z = q.z; + y = q.y; + x = q.x; + + xx = 2.0f * x; yy = 2.0f * y; zz = 2.0f * z; + wx = w * xx; wy = w * yy; wz = w * zz; + xx = x * xx; xy = x * yy; xz = x * zz; + yy = y * yy; yz = y * zz; + zz = z * zz; + + return OOMatrixConstruct + ( + 1.0f - yy - zz, xy - wz, xz + wy, 0.0f, + xy + wz, 1.0f - xx - zz, yz - wx, 0.0f, + xz - wy, yz + wx, 1.0f - xx - yy, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ); +} + + +OOMatrix OOMatrixMultiply(OOMatrix a, OOMatrix b) +{ + unsigned i = 0; + OOMatrix r; + + // This is amenable to significant optimization with Altivec, and presumably also SSE. + for (i = 0; i != 4; ++i) + { + r.m[i][0] = a.m[i][0] * b.m[0][0] + a.m[i][1] * b.m[1][0] + a.m[i][2] * b.m[2][0] + a.m[i][3] * b.m[3][0]; + r.m[i][1] = a.m[i][0] * b.m[0][1] + a.m[i][1] * b.m[1][1] + a.m[i][2] * b.m[2][1] + a.m[i][3] * b.m[3][1]; + r.m[i][2] = a.m[i][0] * b.m[0][2] + a.m[i][1] * b.m[1][2] + a.m[i][2] * b.m[2][2] + a.m[i][3] * b.m[3][2]; + r.m[i][3] = a.m[i][0] * b.m[0][3] + a.m[i][1] * b.m[1][3] + a.m[i][2] * b.m[2][3] + a.m[i][3] * b.m[3][3]; + } + + return r; +} + + +Vector OOVectorMultiplyMatrix(Vector v, OOMatrix m) +{ + GLfloat x, y, z, w; + + x = m.m[0][0] * v.x + m.m[1][0] * v.y + m.m[2][0] * v.z + m.m[3][0]; + y = m.m[0][1] * v.x + m.m[1][1] * v.y + m.m[2][1] * v.z + m.m[3][1]; + z = m.m[0][2] * v.x + m.m[1][2] * v.y + m.m[2][2] * v.z + m.m[3][2]; + w = m.m[0][3] * v.x + m.m[1][3] * v.y + m.m[2][3] * v.z + m.m[3][3]; + + w = 1.0f/w; + return make_vector(x * w, y * w, z * w); +} + + +OOMatrix OOMatrixOrthogonalize(OOMatrix m) +{ + // Simple orthogonalization: make everything orthogonal to everything else. + + Vector i = { m.m[0][0], m.m[1][0], m.m[2][0] }; + Vector j = { m.m[0][1], m.m[1][1], m.m[2][1] }; + Vector k = { m.m[0][2], m.m[1][2], m.m[2][2] }; + + k = vector_normal(k); + i = vector_normal(cross_product(j, k)); + j = cross_product(k, i); + + m.m[0][0] = i.x; m.m[1][0] = i.y; m.m[2][0] = i.z; + m.m[0][1] = j.x; m.m[1][1] = j.y; m.m[2][1] = j.z; + m.m[0][2] = k.x; m.m[1][2] = k.y; m.m[2][2] = k.z; + + return m; +} + + +NSString *OOMatrixDescription(OOMatrix matrix) +{ + return [NSString stringWithFormat:@"{{%g, %g, %g, %g}, {%g, %g, %g, %g}, {%g, %g, %g, %g}, {%g, %g, %g, %g}}", + matrix.m[0][0], matrix.m[0][1], matrix.m[0][2], matrix.m[0][3], + matrix.m[1][0], matrix.m[1][1], matrix.m[1][2], matrix.m[1][3], + matrix.m[2][0], matrix.m[2][1], matrix.m[2][2], matrix.m[2][3], + matrix.m[3][0], matrix.m[3][1], matrix.m[3][2], matrix.m[3][3]]; +} + + +OOMatrix OOMatrixForBillboard(Vector bbPos, Vector eyePos) +{ + Vector v0, v1, v2, arbv; + + v0 = vector_subtract(bbPos, eyePos); + v0 = vector_normal_or_fallback(v0, kBasisZVector); + + // arbitrary axis - not aligned with v0 + if (EXPECT_NOT(v0.x == 0.0 && v0.y == 0.0)) arbv = kBasisXVector; + else arbv = kBasisZVector; + + v1 = cross_product(v0, arbv); // 90 degrees to (v0 x arb1) + v2 = cross_product(v0, v1); // 90 degrees to (v0 x v1) + + return OOMatrixFromBasisVectors(v1, v2, v0); +} diff --git a/src/Core/OOMesh.h b/src/Core/OOMesh.h new file mode 100644 index 00000000..e124a834 --- /dev/null +++ b/src/Core/OOMesh.h @@ -0,0 +1,156 @@ +/* + +OOMesh.h + +Standard OODrawable for static meshes from DAT files. OOMeshes are immutable +(and can therefore be shared). Avoid the temptation to add externally-visible +mutator methods as it will break such sharing. (Sharing will be implemented +when ship types are turned into objects instead of dictionaries; this is +currently slated for post-1.70. -- Ahruman) + +Hmm. On further consideration, sharing will be problematic because of material +bindings. Two possible solutions: separate mesh data into shared object with +each mesh instance having its own set of materials but shared data, or +retarget bindings each frame. -- Ahruman + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OODrawable.h" +#import "OOOpenGL.h" +#import "OOWeakReference.h" + +@class OOMaterial, Octree; + + +enum +{ + kOOMeshMaxMaterials = 8, + kOOMeshMaxVertsPerFace = 16, + + kOOMeshVARCount = 16 +}; + + +typedef uint8_t OOMeshSmoothGroup; +typedef uint8_t OOMeshMaterialIndex, OOMeshMaterialCount; +typedef uint16_t OOMeshVertexCount; +typedef uint16_t OOMeshFaceCount; +typedef uint8_t OOMeshFaceVertexCount; + + +typedef struct +{ + OOMeshSmoothGroup smoothGroup; + OOMeshMaterialIndex materialIndex; + OOMeshFaceVertexCount n_verts; + GLint vertex[kOOMeshMaxVertsPerFace]; + + Vector normal; + Vector tangent; + GLfloat s[kOOMeshMaxVertsPerFace]; + GLfloat t[kOOMeshMaxVertsPerFace]; +} OOMeshFace; + + +typedef struct +{ + GLint *indexArray; + GLfloat *textureUVArray; + Vector *vertexArray; + Vector *normalArray; + Vector *tangentArray; + + GLuint count; +} OOMeshDisplayLists; + + +@interface OOMesh: OODrawable +{ +@private + uint8_t isSmoothShaded: 1, + brokenInRender: 1, + listsReady: 1; + + OOMeshMaterialCount materialCount; + OOMeshVertexCount vertexCount; + OOMeshFaceCount faceCount; + + NSString *baseFile; + + Vector *_vertices; + Vector *_normals; + Vector *_tangents; + OOMeshFace *_faces; + + // Redundancy! Needs fixing. + OOMeshDisplayLists _displayLists; + + NSRange triangle_range[kOOMeshMaxMaterials]; + NSString *materialKeys[kOOMeshMaxMaterials]; + OOMaterial *materials[kOOMeshMaxMaterials]; + GLuint displayList0; + + GLfloat collisionRadius; + GLfloat maxDrawDistance; + BoundingBox boundingBox; + + Octree *octree; + + NSMutableDictionary *_retainedObjects; +} + ++ (id)meshWithName:(NSString *)name +materialDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + smooth:(BOOL)smooth + shaderMacros:(NSDictionary *)macros +shaderBindingTarget:(id)object; + ++ (OOMaterial *)placeholderMaterial; + +- (NSString *) modelName; + +- (size_t)vertexCount; +- (size_t)faceCount; + +- (Octree *)octree; + +// This needs a better name. +- (BoundingBox) findBoundingBoxRelativeToPosition:(Vector)opv + basis:(Vector)ri :(Vector)rj :(Vector)rk + selfPosition:(Vector)position + selfBasis:(Vector)si :(Vector)sj :(Vector)sk; +- (BoundingBox)findSubentityBoundingBoxWithPosition:(Vector)position rotMatrix:(OOMatrix)rotMatrix; + +- (OOMesh *)meshRescaledBy:(GLfloat)scaleFactor; +- (OOMesh *)meshRescaledByX:(GLfloat)scaleX y:(GLfloat)scaleY z:(GLfloat)scaleZ; + +@end + + +#import "OOCacheManager.h" +@interface OOCacheManager (Octree) + ++ (Octree *)octreeForModel:(NSString *)inKey; ++ (void)setOctree:(Octree *)inOctree forModel:(NSString *)inKey; + +@end diff --git a/src/Core/OOMesh.m b/src/Core/OOMesh.m new file mode 100644 index 00000000..1e09d91d --- /dev/null +++ b/src/Core/OOMesh.m @@ -0,0 +1,1573 @@ +/* + +OOMesh.m + +A note on memory management: +The dynamically-sized buffers used by OOMesh (_vertex etc) are the byte arrays +of NSDatas, which are tracked using the _retainedObjects dictionary. This +simplifies the implementation of -dealloc, but more importantly, it means +bytes are refcounted. This means bytes read from the cache don't need to be +copied, we just need to retain the relevant NSData object (by sticking it in +_retainedObjects). + +Since _retainedObjects is a dictionary its members can be replaced, +potentially allowing mutable meshes, although we have no use for this at +present. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOMesh.h" +#import "Universe.h" +#import "Geometry.h" +#import "ResourceManager.h" +#import "Entity.h" // for NO_DRAW_DISTANCE_FACTOR. +#import "Octree.h" +#import "OOMaterial.h" +#import "OOBasicMaterial.h" +#import "OOCollectionExtractors.h" +#import "OOOpenGLExtensionManager.h" +#import "OOGraphicsResetManager.h" +#import "OODebugGLDrawing.h" +#import "OOShaderMaterial.h" +#import "OOMacroOpenGL.h" + + +// If set, collision octree depth varies depending on the size of the mesh. This seems to cause collision handling glitches at present. +#define ADAPTIVE_OCTREE_DEPTH 0 + + +enum +{ + kBaseOctreeDepth = 5, // 32x32x32 + kMaxOctreeDepth = 7, // 128x128x128 + kSmallOctreeDepth = 4, // 16x16x16 + kVerySmallOctreeDepth = 3, // 8x8x8 + kOctreeSizeThreshold = 900, // Size at which we start increasing octree depth + kOctreeSmallSizeThreshold = 60, + kOctreeVerySmallSizeThreshold = 15 +}; + + +static NSString * const kOOLogMeshDataNotFound = @"mesh.load.failed.fileNotFound"; +static NSString * const kOOLogMeshTooManyVertices = @"mesh.load.failed.tooManyVertices"; +static NSString * const kOOLogMeshTooManyFaces = @"mesh.load.failed.tooManyFaces"; +static NSString * const kOOLogMeshTooManyMaterials = @"mesh.load.failed.tooManyMaterials"; + + +@interface OOMesh (Private) + +- (id)initWithName:(NSString *)name +materialDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + smooth:(BOOL)smooth + shaderMacros:(NSDictionary *)macros +shaderBindingTarget:(id)object; + +- (void)setUpMaterialsWithMaterialsDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + shaderMacros:(NSDictionary *)macros + shaderBindingTarget:(id)target; + +- (BOOL) loadData:(NSString *)filename; +- (void) checkNormalsAndAdjustWinding; +- (void) generateFaceTangents; +- (void) calculateVertexNormals; + +- (NSDictionary*) modelData; +- (BOOL) setModelFromModelData:(NSDictionary*) dict; + +- (void) getNormal:(Vector *)outNormal andTangent:(Vector *)outTangent forVertex:(int) v_index inSmoothGroup:(OOMeshSmoothGroup)smoothGroup; + +- (BOOL) setUpVertexArrays; + +- (void) calculateBoundingVolumes; + +- (void)rescaleByX:(GLfloat)scaleX y:(GLfloat)scaleY z:(GLfloat)scaleZ; + +#ifndef NDEBUG +- (void)debugDrawNormals; +#endif + +// Manage set of objects we need to hang on to, particularly NSDatas owning buffers. +- (void) setRetainedObject:(id)object forKey:(NSString *)key; +- (void *) allocateBytesWithSize:(size_t)size count:(OOUInteger)count key:(NSString *)key; + +// Allocate all per-vertex/per-face buffers. +- (BOOL) allocateVertexBuffersWithCount:(OOUInteger)count; +- (BOOL) allocateFaceBuffersWithCount:(OOUInteger)count; +- (BOOL) allocateVertexArrayBuffersWithCount:(OOUInteger)count; + +@end + + +@interface OOCacheManager (OOMesh) + ++ (NSDictionary *)meshDataForName:(NSString *)inShipName; ++ (void)setMeshData:(NSDictionary *)inData forName:(NSString *)inShipName; + +@end + + +@implementation OOMesh + ++ (id)meshWithName:(NSString *)name +materialDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + smooth:(BOOL)smooth + shaderMacros:(NSDictionary *)macros +shaderBindingTarget:(id)object +{ + return [[[self alloc] initWithName:name + materialDictionary:materialDict + shadersDictionary:shadersDict + smooth:smooth + shaderMacros:macros + shaderBindingTarget:object] autorelease]; +} + + ++ (OOMaterial *)placeholderMaterial +{ + static OOBasicMaterial *placeholderMaterial = nil; + + if (placeholderMaterial == nil) + { + NSDictionary *materialDefaults = nil; + + materialDefaults = [ResourceManager dictionaryFromFilesNamed:@"material-defaults.plist" inFolder:@"Config" andMerge:YES]; + placeholderMaterial = [[OOBasicMaterial alloc] initWithName:@"/placeholder/" configuration:[materialDefaults dictionaryForKey:@"no-textures-material"]]; + } + + return placeholderMaterial; +} + + +- (id)init +{ + self = [super init]; + if (self == nil) return nil; + + baseFile = @"No Model"; + + return self; +} + + +- (void) dealloc +{ + unsigned i; + + [baseFile release]; + [octree autorelease]; + + [self resetGraphicsState]; + + for (i = 0; i != kOOMeshMaxMaterials; ++i) + { + [materials[i] release]; + [materialKeys[i] release]; + } + + [[OOGraphicsResetManager sharedManager] unregisterClient:self]; + + [_retainedObjects release]; + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{\"%@\", %u vertices, %u faces, radius: %g m smooth: %s}", [self class], self, [self modelName], [self vertexCount], [self faceCount], [self collisionRadius], isSmoothShaded ? "YES" : "NO"]; +} + + +- (id)copyWithZone:(NSZone *)zone +{ + if (zone == [self zone]) return [self retain]; // OK because we're immutable seen from the outside + else return [self mutableCopyWithZone:zone]; +} + + +- (NSString *) modelName +{ + return baseFile; +} + + +- (size_t)vertexCount +{ + return vertexCount; +} + + +- (size_t)faceCount +{ + return faceCount; +} + + +- (void)renderOpaqueParts +{ + if (EXPECT_NOT(baseFile == nil)) + { + OOLog(kOOLogFileNotLoaded, @"***** ERROR: no baseFile for entity %@", self); + return; + } + + OO_ENTER_OPENGL(); + + int ti; + + glPushAttrib(GL_ENABLE_BIT); + + if (isSmoothShaded) glShadeModel(GL_SMOOTH); + else glShadeModel(GL_FLAT); + + glDisableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_INDEX_ARRAY); + glDisableClientState(GL_EDGE_FLAG_ARRAY); + + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_NORMAL_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + + glVertexPointer(3, GL_FLOAT, 0, _displayLists.vertexArray); + glNormalPointer(GL_FLOAT, 0, _displayLists.normalArray); + glTexCoordPointer(2, GL_FLOAT, 0, _displayLists.textureUVArray); + if ([[OOOpenGLExtensionManager sharedManager] shadersSupported]) + { + glEnableVertexAttribArrayARB(kTangentAttributeIndex); + glVertexAttribPointerARB(kTangentAttributeIndex, 3, GL_FLOAT, GL_FALSE, 0, _displayLists.tangentArray); + } + + glDisable(GL_BLEND); + glEnable(GL_TEXTURE_2D); + + NS_DURING + if (!listsReady) + { + displayList0 = glGenLists(materialCount); + + // Ensure all textures are loaded + for (ti = 0; ti < materialCount; ti++) + { + [materials[ti] ensureFinishedLoading]; + } + } + + for (ti = 0; ti < materialCount; ti++) + { + [materials[ti] apply]; +#if 0 + if (listsReady) + { + glCallList(displayList0 + ti); + } + else + { + glNewList(displayList0 + ti, GL_COMPILE_AND_EXECUTE); + glDrawArrays(GL_TRIANGLES, triangle_range[ti].location, triangle_range[ti].length); + glEndList(); + } +#else + glDrawArrays(GL_TRIANGLES, triangle_range[ti].location, triangle_range[ti].length); +#endif + } + + listsReady = YES; + brokenInRender = NO; + NS_HANDLER + if (!brokenInRender) + { + OOLog(kOOLogException, @"***** %s for %@ encountered exception: %@ : %@ *****", __FUNCTION__, self, [localException name], [localException reason]); + brokenInRender = YES; + } + if ([[localException name] hasPrefix:@"Oolite"]) [UNIVERSE handleOoliteException:localException]; // handle these ourself + else [localException raise]; // pass these on + NS_ENDHANDLER + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_DRAW_NORMALS) [self debugDrawNormals]; +#endif + + if ([[OOOpenGLExtensionManager sharedManager] shadersSupported]) + { + glDisableVertexAttribArrayARB(kTangentAttributeIndex); + } + + [OOMaterial applyNone]; + CheckOpenGLErrors(@"OOMesh after drawing %@", self); + +#ifndef NDEBUG + if (gDebugFlags & DEBUG_OCTREE_DRAW) [[self octree] drawOctree]; +#endif + + glPopAttrib(); +} + + +- (BOOL)hasOpaqueParts +{ + return YES; +} + +- (GLfloat)collisionRadius +{ + return collisionRadius; +} + + +- (GLfloat)maxDrawDistance +{ + return maxDrawDistance; +} + + +- (Geometry *)geometry +{ + Geometry *result = [[Geometry alloc] initWithCapacity:faceCount]; + int i; + for (i = 0; i < faceCount; i++) + { + Triangle tri; + tri.v[0] = _vertices[_faces[i].vertex[0]]; + tri.v[1] = _vertices[_faces[i].vertex[1]]; + tri.v[2] = _vertices[_faces[i].vertex[2]]; + [result addTriangle:tri]; + } + return [result autorelease]; +} + + +#if ADAPTIVE_OCTREE_DEPTH +- (unsigned) octreeDepth +{ + float threshold = kOctreeSizeThreshold; + unsigned result = kBaseOctreeDepth; + GLfloat xs, ys, zs, t, size; + + bounding_box_get_dimensions(boundingBox, &xs, &ys, &zs); + // Shuffle dimensions around so zs is smallest + if (xs < zs) { t = zs; zs = xs; xs = t; } + if (ys < zs) { t = zs; zs = ys; ys = t; } + size = (xs + ys) / 2.0f; // Use average of two largest + + if (size < kOctreeVerySmallSizeThreshold) result = kVerySmallOctreeDepth; + else if (size < kOctreeSmallSizeThreshold) result = kSmallOctreeDepth; + else while (result < kMaxOctreeDepth) + { + if (size < threshold) break; + threshold *= 2.0f; + result++; + } + + OOLog(@"mesh.load.octree.size", @"Selected octree depth %u for size %g for %@", result, size, baseFile); + return result; +} +#else +- (unsigned) octreeDepth +{ + return kBaseOctreeDepth; +} +#endif + + +- (Octree *)octree +{ + if (octree == nil) + { + octree = [OOCacheManager octreeForModel:baseFile]; + if (octree == nil) + { + octree = [[self geometry] findOctreeToDepth:[self octreeDepth]]; + [OOCacheManager setOctree:octree forModel:baseFile]; + } + [octree retain]; + } + + return octree; +} + + +- (BoundingBox) findBoundingBoxRelativeToPosition:(Vector)opv + basis:(Vector)ri :(Vector)rj :(Vector)rk + selfPosition:(Vector)position + selfBasis:(Vector)si :(Vector)sj :(Vector)sk +{ + BoundingBox result; + Vector pv, rv; + Vector rpos = position; + int i; + + // FIXME: rewrite with matrices + rpos = vector_subtract(position, opv); // model origin relative to opv + + rv.x = dot_product(ri,rpos); + rv.y = dot_product(rj,rpos); + rv.z = dot_product(rk,rpos); // model origin rel to opv in ijk + + if (EXPECT_NOT(vertexCount < 1)) + { + bounding_box_reset_to_vector(&result, rv); + } + else + { + pv.x = rpos.x + si.x * _vertices[0].x + sj.x * _vertices[0].y + sk.x * _vertices[0].z; + pv.y = rpos.y + si.y * _vertices[0].x + sj.y * _vertices[0].y + sk.y * _vertices[0].z; + pv.z = rpos.z + si.z * _vertices[0].x + sj.z * _vertices[0].y + sk.z * _vertices[0].z; // _vertices[0] position rel to opv + rv.x = dot_product(ri, pv); + rv.y = dot_product(rj, pv); + rv.z = dot_product(rk, pv); // _vertices[0] position rel to opv in ijk + bounding_box_reset_to_vector(&result, rv); + } + for (i = 1; i < vertexCount; i++) + { + pv.x = rpos.x + si.x * _vertices[i].x + sj.x * _vertices[i].y + sk.x * _vertices[i].z; + pv.y = rpos.y + si.y * _vertices[i].x + sj.y * _vertices[i].y + sk.y * _vertices[i].z; + pv.z = rpos.z + si.z * _vertices[i].x + sj.z * _vertices[i].y + sk.z * _vertices[i].z; + rv.x = dot_product(ri, pv); + rv.y = dot_product(rj, pv); + rv.z = dot_product(rk, pv); + bounding_box_add_vector(&result, rv); + } + + return result; +} + + +- (BoundingBox)findSubentityBoundingBoxWithPosition:(Vector)position rotMatrix:(OOMatrix)rotMatrix +{ + // HACK! Should work out what the various bounding box things do and make it neat and consistent. + BoundingBox result; + Vector v; + int i; + + v = vector_add(position, OOVectorMultiplyMatrix(_vertices[0], rotMatrix)); + bounding_box_reset_to_vector(&result,v); + + for (i = 1; i < vertexCount; i++) + { + v = vector_add(position, OOVectorMultiplyMatrix(_vertices[i], rotMatrix)); + bounding_box_add_vector(&result,v); + } + + return result; +} + + +- (OOMesh *)meshRescaledBy:(GLfloat)scaleFactor +{ + return [self meshRescaledByX:scaleFactor y:scaleFactor z:scaleFactor]; +} + + +- (OOMesh *)meshRescaledByX:(GLfloat)scaleX y:(GLfloat)scaleY z:(GLfloat)scaleZ +{ + id result = nil; + + result = [self mutableCopy]; + [result rescaleByX:scaleX y:scaleY z:scaleZ]; + return [result autorelease]; +} + + +- (void)setBindingTarget:(id)target +{ + unsigned i; + + for (i = 0; i != kOOMeshMaxMaterials; ++i) + { + [materials[i] setBindingTarget:target]; + } +} + + +#ifndef NDEBUG +- (void)dumpSelfState +{ + NSMutableArray *flags = nil; + NSString *flagsString = nil; + + [super dumpSelfState]; + + if (baseFile != nil) OOLog(@"dumpState.mesh", @"Model file: %@", baseFile); + OOLog(@"dumpState.mesh", @"Vertex count: %u, face count: %u", vertexCount, faceCount); + + flags = [NSMutableArray array]; + #define ADD_FLAG_IF_SET(x) if (x) { [flags addObject:@#x]; } + ADD_FLAG_IF_SET(isSmoothShaded); + flagsString = [flags count] ? [flags componentsJoinedByString:@", "] : (NSString *)@"none"; + OOLog(@"dumpState.mesh", @"Flags: %@", flagsString); +} +#endif + +@end + + +@implementation OOMesh (Private) + +- (id)initWithName:(NSString *)name +materialDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + smooth:(BOOL)smooth + shaderMacros:(NSDictionary *)macros +shaderBindingTarget:(id)target +{ + self = [super init]; + if (self == nil) return nil; + + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + isSmoothShaded = smooth != NO; + + if ([self loadData:name]) + { + [self checkNormalsAndAdjustWinding]; + [self calculateBoundingVolumes]; + baseFile = [name copy]; + [self setUpMaterialsWithMaterialsDictionary:materialDict shadersDictionary:shadersDict shaderMacros:macros shaderBindingTarget:target]; + [[OOGraphicsResetManager sharedManager] registerClient:self]; + } + else + { + [self release]; + self = nil; + } + + [pool release]; + return self; +} + + +- (void)setUpMaterialsWithMaterialsDictionary:(NSDictionary *)materialDict + shadersDictionary:(NSDictionary *)shadersDict + shaderMacros:(NSDictionary *)macros + shaderBindingTarget:(id)target +{ + OOMeshMaterialCount i; + OOMaterial *material = nil; + + if (materialCount != 0) + { + for (i = 0; i != materialCount; ++i) + { + if (![materialKeys[i] isEqualToString:@"_oo_placeholder_material"]) + { + material = [OOMaterial materialWithName:materialKeys[i] + forModelNamed:baseFile + materialDictionary:materialDict + shadersDictionary:shadersDict + macros:macros + bindingTarget:target + forSmoothedMesh:isSmoothShaded]; + } + else + { + material = nil; + } + + if (material != nil) + { + materials[i] = [material retain]; + } + else + { + materials[i] = [[OOMesh placeholderMaterial] retain]; + } + } + } +} + + +- (id)mutableCopyWithZone:(NSZone *)zone +{ + OOMesh *result = nil; + OOMeshMaterialCount i; + + result = (OOMesh *)NSCopyObject(self, 0, zone); + + if (result != nil) + { + [result->baseFile retain]; + [result->octree retain]; + [result->_retainedObjects retain]; + + for (i = 0; i != kOOMeshMaxMaterials; ++i) + { + [result->materialKeys[i] retain]; + [result->materials[i] retain]; + } + + // Reset unsharable GL state + result->listsReady = NO; + + [[OOGraphicsResetManager sharedManager] registerClient:result]; + } + + return result; +} + + +- (void) resetGraphicsState +{ + if (listsReady) + { + OO_ENTER_OPENGL(); + + glDeleteLists(displayList0, materialCount); + listsReady = NO; + } +} + + +- (NSDictionary *)modelData +{ + NSNumber *vertCnt = nil, + *faceCnt = nil; + NSData *vertData = nil, + *normData = nil, + *tanData = nil, + *faceData = nil; + NSArray *mtlKeys = nil; + NSNumber *smooth = nil; + + // Prepare cache data elements. + vertCnt = [NSNumber numberWithUnsignedInt:vertexCount]; + faceCnt = [NSNumber numberWithUnsignedInt:faceCount]; + +#if 0 + vertData = [NSData dataWithBytes:_vertices length:sizeof *_vertices * vertexCount]; + normData = [NSData dataWithBytes:_normals length:sizeof *_normals * vertexCount]; + tanData = [NSData dataWithBytes:_tangents length:sizeof *_tangents * vertexCount]; + faceData = [NSData dataWithBytes:_faces length:sizeof *_faces * faceCount]; +#else + vertData = [_retainedObjects objectForKey:@"vertices"]; + normData = [_retainedObjects objectForKey:@"normals"]; + tanData = [_retainedObjects objectForKey:@"tangents"]; + faceData = [_retainedObjects objectForKey:@"faces"]; +#endif + + if (materialCount != 0) + { + mtlKeys = [NSArray arrayWithObjects:materialKeys count:materialCount]; + } + else + { + mtlKeys = [NSArray array]; + } + smooth = [NSNumber numberWithBool:isSmoothShaded]; + + // Ensure we have all the required data elements. + if (vertCnt == nil || + faceCnt == nil || + vertData == nil || + normData == nil || + tanData == nil || + faceData == nil || + mtlKeys == nil || + smooth == nil) + { + return nil; + } + + // All OK; stick 'em in a dictionary. + return [NSDictionary dictionaryWithObjectsAndKeys: + vertCnt, @"vertex count", + vertData, @"vertex data", + normData, @"normal data", + tanData, @"tangent data", + faceCnt, @"face count", + faceData, @"face data", + mtlKeys, @"material keys", + smooth, @"smooth", + nil]; +} + + +- (BOOL)setModelFromModelData:(NSDictionary *)dict +{ + NSData *vertData = nil, + *normData = nil, + *tanData = nil, + *faceData = nil; + NSArray *mtlKeys = nil; + NSString *key = nil; + unsigned i; + + if (dict == nil || ![dict isKindOfClass:[NSDictionary class]]) return NO; + + vertexCount = [dict unsignedIntForKey:@"vertex count"]; + faceCount = [dict unsignedIntForKey:@"face count"]; + + if (vertexCount == 0 || faceCount == 0) return NO; + + // Read data elements from dictionary. + vertData = [dict dataForKey:@"vertex data"]; + normData = [dict dataForKey:@"normal data"]; + tanData = [dict dataForKey:@"tangent data"]; + faceData = [dict dataForKey:@"face data"]; + + mtlKeys = [dict arrayForKey:@"material keys"]; + isSmoothShaded = [dict boolForKey:@"smooth"]; + + // Ensure we have all the required data elements. + if (vertData == nil || + normData == nil || + tanData == nil || + faceData == nil || + mtlKeys == nil) + { + return NO; + } + + // Ensure data objects are of correct size. + if ([vertData length] != sizeof *_vertices * vertexCount) return NO; + if ([normData length] != sizeof *_normals * vertexCount) return NO; + if ([tanData length] != sizeof *_tangents * vertexCount) return NO; + if ([faceData length] != sizeof *_faces * faceCount) return NO; + + // Retain data. + _vertices = (Vector *)[vertData bytes]; + [self setRetainedObject:vertData forKey:@"vertices"]; + _normals = (Vector *)[normData bytes]; + [self setRetainedObject:normData forKey:@"normals"]; + _tangents = (Vector *)[tanData bytes]; + [self setRetainedObject:tanData forKey:@"tangents"]; + _faces = (OOMeshFace *)[faceData bytes]; + [self setRetainedObject:faceData forKey:@"faces"]; + + // Copy material keys. + materialCount = [mtlKeys count]; + for (i = 0; i != materialCount; ++i) + { + key = [mtlKeys stringAtIndex:i]; + if (key != nil) materialKeys[i] = [key retain]; + else return NO; + } + + return YES; +} + + +- (BOOL)loadData:(NSString *)filename +{ + NSScanner *scanner; + NSDictionary *cacheData = nil; + NSString *data = nil; + NSMutableArray *lines; + BOOL failFlag = NO; + NSString *failString = @"***** "; + unsigned i, j; + NSMutableDictionary *texFileName2Idx = nil; + + BOOL using_preloaded = NO; + + cacheData = [OOCacheManager meshDataForName:filename]; + if (cacheData != nil) + { + if ([self setModelFromModelData:cacheData]) using_preloaded = YES; + } + + if (!using_preloaded) + { + texFileName2Idx = [NSMutableDictionary dictionary]; + + data = [ResourceManager stringFromFilesNamed:filename inFolder:@"Models"]; + if (data == nil) + { + // Model not found + OOLog(kOOLogMeshDataNotFound, @"***** ERROR: could not find %@", filename); + return NO; + } + + // strip out comments and commas between values + // + lines = [NSMutableArray arrayWithArray:[data componentsSeparatedByString:@"\n"]]; + for (i = 0; i < [lines count]; i++) + { + NSString *line = [lines objectAtIndex:i]; + NSArray *parts; + // + // comments + // + parts = [line componentsSeparatedByString:@"#"]; + line = [parts objectAtIndex:0]; + parts = [line componentsSeparatedByString:@"//"]; + line = [parts objectAtIndex:0]; + // + // commas + // + line = [[line componentsSeparatedByString:@","] componentsJoinedByString:@" "]; + // + [lines replaceObjectAtIndex:i withObject:line]; + } + data = [lines componentsJoinedByString:@"\n"]; + + scanner = [NSScanner scannerWithString:data]; + + // get number of vertices + // + [scanner setScanLocation:0]; //reset + if ([scanner scanString:@"NVERTS" intoString:NULL]) + { + int n_v; + if ([scanner scanInt:&n_v]) + vertexCount = n_v; + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read value of NVERTS\n",failString]; + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read NVERTS\n",failString]; + } + + if (![self allocateVertexBuffersWithCount:vertexCount]) + { + OOLog(kOOLogAllocationFailure, @"***** ERROR: failed to allocate memory for model %@ (%u vertices).", filename, vertexCount); + return NO; + } + + // get number of faces + if ([scanner scanString:@"NFACES" intoString:NULL]) + { + int n_f; + if ([scanner scanInt:&n_f]) + { + faceCount = n_f; + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read value of NFACES\n",failString]; + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read NFACES\n",failString]; + } + + if (![self allocateFaceBuffersWithCount:faceCount]) + { + OOLog(kOOLogAllocationFailure, @"***** ERROR: failed to allocate memory for model %@ (%u vertices, %u faces).", filename, vertexCount, faceCount); + return NO; + } + + // get vertex data + // + //[scanner setScanLocation:0]; //reset + if ([scanner scanString:@"VERTEX" intoString:NULL]) + { + for (j = 0; j < vertexCount; j++) + { + float x, y, z; + if (!failFlag) + { + if (![scanner scanFloat:&x]) failFlag = YES; + if (![scanner scanFloat:&y]) failFlag = YES; + if (![scanner scanFloat:&z]) failFlag = YES; + if (!failFlag) + { + _vertices[j].x = x; + _vertices[j].y = y; + _vertices[j].z = z; + } + else + { + failString = [NSString stringWithFormat:@"%@Failed to read a value for vertex[%d] in VERTEX\n", failString, j]; + } + } + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to find VERTEX data\n",failString]; + } + + // get face data + // + if ([scanner scanString:@"FACES" intoString:NULL]) + { + for (j = 0; j < faceCount; j++) + { + int r, g, b; + float nx, ny, nz; + int n_v; + if (!failFlag) + { + // colors + if (![scanner scanInt:&r]) failFlag = YES; + if (![scanner scanInt:&g]) failFlag = YES; + if (![scanner scanInt:&b]) failFlag = YES; + if (!failFlag) + { + _faces[j].smoothGroup = r; + } + else + { + failString = [NSString stringWithFormat:@"%@Failed to read a color for face[%d] in FACES\n", failString, j]; + } + + // normal + if (![scanner scanFloat:&nx]) failFlag = YES; + if (![scanner scanFloat:&ny]) failFlag = YES; + if (![scanner scanFloat:&nz]) failFlag = YES; + if (!failFlag) + { + _faces[j].normal = make_vector(nx, ny, nz); + } + else + { + failString = [NSString stringWithFormat:@"%@Failed to read a normal for face[%d] in FACES\n", failString, j]; + } + + // vertices + if ([scanner scanInt:&n_v]) + { + _faces[j].n_verts = n_v; + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read number of vertices for face[%d] in FACES\n", failString, j]; + } + // + if (!failFlag) + { + int vi; + for (i = 0; (int)i < n_v; i++) + { + if ([scanner scanInt:&vi]) + { + _faces[j].vertex[i] = vi; + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read vertex[%d] for face[%d] in FACES\n", failString, i, j]; + } + } + } + } + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to find FACES data\n",failString]; + } + + // get textures data + // + if ([scanner scanString:@"TEXTURES" intoString:NULL]) + { + for (j = 0; j < faceCount; j++) + { + NSString *materialKey; + float max_x, max_y; + float s, t; + if (!failFlag) + { + // materialKey + // + [scanner scanCharactersFromSet:[NSCharacterSet whitespaceAndNewlineCharacterSet] intoString:NULL]; + if (![scanner scanUpToCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&materialKey]) + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to read texture filename for face[%d] in TEXTURES\n", failString, j]; + } + else + { + NSNumber *index = [texFileName2Idx objectForKey:materialKey]; + if (index != nil) + { + _faces[j].materialIndex = [index unsignedIntValue]; + } + else + { + if (materialCount == kOOMeshMaxMaterials) + { + OOLog(kOOLogMeshTooManyMaterials, @"***** ERROR: model %@ has too many materials (maximum is %d)", filename, kOOMeshMaxMaterials); + return NO; + } + _faces[j].materialIndex = materialCount; + materialKeys[materialCount] = [materialKey retain]; + index = [NSNumber numberWithUnsignedInt:materialCount]; + [texFileName2Idx setObject:index forKey:materialKey]; + ++materialCount; + } + } + + // texture size + // + if (!failFlag) + { + if (![scanner scanFloat:&max_x]) failFlag = YES; + if (![scanner scanFloat:&max_y]) failFlag = YES; + if (failFlag) + failString = [NSString stringWithFormat:@"%@Failed to read texture size for max_x and max_y in face[%d] in TEXTURES\n", failString, j]; + } + + // vertices + // + if (!failFlag) + { + for (i = 0; i < _faces[j].n_verts; i++) + { + if (![scanner scanFloat:&s]) failFlag = YES; + if (![scanner scanFloat:&t]) failFlag = YES; + if (!failFlag) + { + _faces[j].s[i] = s / max_x; + _faces[j].t[i] = t / max_y; + } + else + failString = [NSString stringWithFormat:@"%@Failed to read s t coordinates for vertex[%d] in face[%d] in TEXTURES\n", failString, i, j]; + } + } + } + } + } + else + { + failFlag = YES; + failString = [NSString stringWithFormat:@"%@Failed to find TEXTURES data (will use placeholder material)\n",failString]; + materialKeys[0] = @"_oo_placeholder_material"; + materialCount = 1; + + for (j = 0; j < faceCount; j++) + { + _faces[j].materialIndex = 0; + } + } + + [self checkNormalsAndAdjustWinding]; + [self generateFaceTangents]; + + if (failFlag) + { + OOLog(@"mesh.error", @"%@ ..... from %@ %@", failString, filename, (using_preloaded)? @"(from preloaded data)" :@"(from file)"); + } + + // check for smooth shading and recalculate normals + if (isSmoothShaded) [self calculateVertexNormals]; + + // save the resulting data for possible reuse + [OOCacheManager setMeshData:[self modelData] forName:filename]; + } + + [self calculateBoundingVolumes]; + + // set up vertex arrays for drawing + if (![self setUpVertexArrays]) return NO; + + return YES; +} + + +// FIXME: this isn't working, we're getting smoothed models with inside-out winding. --Ahruman +- (void) checkNormalsAndAdjustWinding +{ + Vector calculatedNormal; + int i, j; + for (i = 0; i < faceCount; i++) + { + Vector v0, v1, v2, norm; + v0 = _vertices[_faces[i].vertex[0]]; + v1 = _vertices[_faces[i].vertex[1]]; + v2 = _vertices[_faces[i].vertex[2]]; + norm = _faces[i].normal; + calculatedNormal = normal_to_surface(v2, v1, v0); + if (vector_equal(norm, kZeroVector)) + { + _faces[i].normal = normal_to_surface(v0, v1, v2); + norm = normal_to_surface (v0, v1, v2); + } + if (norm.x*calculatedNormal.x < 0 || norm.y*calculatedNormal.y < 0 || norm.z*calculatedNormal.z < 0) + { + // normal lies in the WRONG direction! + // reverse the winding + int v[_faces[i].n_verts]; + GLfloat s[_faces[i].n_verts]; + GLfloat t[_faces[i].n_verts]; + + for (j = 0; j < _faces[i].n_verts; j++) + { + v[j] = _faces[i].vertex[_faces[i].n_verts - 1 - j]; + s[j] = _faces[i].s[_faces[i].n_verts - 1 - j]; + t[j] = _faces[i].t[_faces[i].n_verts - 1 - j]; + } + for (j = 0; j < _faces[i].n_verts; j++) + { + _faces[i].vertex[j] = v[j]; + _faces[i].s[j] = s[j]; + _faces[i].t[j] = t[j]; + } + } + } +} + + +- (void) generateFaceTangents +{ + int i; + for (i = 0; i < faceCount; i++) + { + OOMeshFace *face = _faces + i; + + /* Generate tangents, i.e. vectors that run in the direction of the s + texture coordinate. Based on code I found in a forum somewhere and + then lost track of. Sorry to whomever I should be crediting. + -- Ahruman 2008-11-23 + */ + Vector vAB = vector_subtract(_vertices[face->vertex[1]], _vertices[face->vertex[0]]); + Vector vAC = vector_subtract(_vertices[face->vertex[2]], _vertices[face->vertex[0]]); + Vector nA = face->normal; + + // projAB = aB - (nA . vAB) * nA + Vector vProjAB = vector_subtract(vAB, vector_multiply_scalar(nA, dot_product(nA, vAB))); + Vector vProjAC = vector_subtract(vAC, vector_multiply_scalar(nA, dot_product(nA, vAC))); + + // delta s/t + GLfloat dsAB = face->s[1] - face->s[0]; + GLfloat dsAC = face->s[2] - face->s[0]; + GLfloat dtAB = face->t[1] - face->t[0]; + GLfloat dtAC = face->t[2] - face->t[0]; + + if (dsAC * dtAB > dsAB * dtAC) + { + dsAB = -dsAB; + dsAC = -dsAC; + } + + Vector tangent = vector_subtract(vector_multiply_scalar(vProjAB, dsAC), vector_multiply_scalar(vProjAC, dsAB)); + face->tangent = cross_product(face->normal, tangent); // Rotate 90 degrees. Done this way because I'm too lazy to grok the code above. + } +} + + +static float FaceArea(GLint *vertIndices, Vector *vertices) +{ + // calculate areas using Herons formula + // in the form Area = sqrt(2*(a2*b2+b2*c2+c2*a2)-(a4+b4+c4))/4 + float a2 = distance2(vertices[vertIndices[0]], vertices[vertIndices[1]]); + float b2 = distance2(vertices[vertIndices[1]], vertices[vertIndices[2]]); + float c2 = distance2(vertices[vertIndices[2]], vertices[vertIndices[0]]); + return sqrtf(2.0 * (a2 * b2 + b2 * c2 + c2 * a2) - 0.25 * (a2 * a2 + b2 * b2 +c2 * c2)); +} + + +- (void) calculateVertexNormals +{ + int i,j; + float triangle_area[faceCount]; + for (i = 0 ; i < faceCount; i++) + { + triangle_area[i] = FaceArea(_faces[i].vertex, _vertices); + } + for (i = 0; i < vertexCount; i++) + { + Vector normal_sum = kZeroVector; + Vector tangent_sum = kZeroVector; + + for (j = 0; j < faceCount; j++) + { + BOOL is_shared = ((_faces[j].vertex[0] == i)||(_faces[j].vertex[1] == i)||(_faces[j].vertex[2] == i)); + if (is_shared) + { + float t = triangle_area[j]; // weight sum by area + normal_sum = vector_add(normal_sum, vector_multiply_scalar(_faces[j].normal, t)); + tangent_sum = vector_add(tangent_sum, vector_multiply_scalar(_faces[j].tangent, t)); + } + } + + normal_sum = vector_normal_or_fallback(normal_sum, kBasisZVector); + tangent_sum = vector_normal_or_fallback(tangent_sum, kBasisXVector); + + _normals[i] = normal_sum; + _tangents[i] = tangent_sum; + } +} + + +- (void) getNormal:(Vector *)outNormal andTangent:(Vector *)outTangent forVertex:(int) v_index inSmoothGroup:(OOMeshSmoothGroup)smoothGroup +{ + assert(outNormal != NULL && outTangent != NULL); + + int j; + Vector normal_sum = kZeroVector; + Vector tangent_sum = kZeroVector; + for (j = 0; j < faceCount; j++) + { + if (_faces[j].smoothGroup == smoothGroup) + { + if ((_faces[j].vertex[0] == v_index)||(_faces[j].vertex[1] == v_index)||(_faces[j].vertex[2] == v_index)) + { + float area = FaceArea(_faces[j].vertex, _vertices); + normal_sum = vector_add(normal_sum, vector_multiply_scalar(_faces[j].normal, area)); + tangent_sum = vector_add(tangent_sum, vector_multiply_scalar(_faces[j].tangent, area)); + } + } + } + + *outNormal = vector_normal_or_fallback(normal_sum, kBasisZVector); + *outTangent = vector_normal_or_fallback(tangent_sum, kBasisXVector); +} + + +- (BOOL) setUpVertexArrays +{ + int fi, vi, mi; + + if (![self allocateVertexArrayBuffersWithCount:faceCount]) return NO; + + // if isSmoothShaded find any vertices that are between faces of different + // smoothing groups and mark them as being on an edge and therefore NOT + // smooth shaded + BOOL is_edge_vertex[vertexCount]; + GLfloat smoothGroup[vertexCount]; + for (vi = 0; vi < vertexCount; vi++) + { + is_edge_vertex[vi] = NO; + smoothGroup[vi] = -1; + } + if (isSmoothShaded) + { + for (fi = 0; fi < faceCount; fi++) + { + GLfloat rv = _faces[fi].smoothGroup; + int i; + for (i = 0; i < 3; i++) + { + vi = _faces[fi].vertex[i]; + if (smoothGroup[vi] < 0.0) // unassigned + smoothGroup[vi] = rv; + else if (smoothGroup[vi] != rv) // a different colour + is_edge_vertex[vi] = YES; + } + } + } + + + // base model, flat or smooth shaded, all triangles + int tri_index = 0; + int uv_index = 0; + int vertex_index = 0; + + // Iterate over material names + for (mi = 0; mi != materialCount; ++mi) + { + triangle_range[mi].location = tri_index; + + for (fi = 0; fi < faceCount; fi++) + { + Vector normal, tangent; + + if (_faces[fi].materialIndex == mi) + { + for (vi = 0; vi < 3; vi++) + { + int v = _faces[fi].vertex[vi]; + if (isSmoothShaded) + { + if (is_edge_vertex[v]) + { + [self getNormal:&normal andTangent:&tangent forVertex:v inSmoothGroup:_faces[fi].smoothGroup]; + } + else + { + normal = _normals[v]; + tangent = _tangents[v]; + } + } + else + { + normal = _faces[fi].normal; + tangent = _faces[fi].tangent; + } + + // FIXME: avoid redundant vertices so index array is actually useful. + _displayLists.indexArray[tri_index++] = vertex_index; + _displayLists.normalArray[vertex_index] = normal; + _displayLists.tangentArray[vertex_index] = tangent; + _displayLists.vertexArray[vertex_index++] = _vertices[v]; + _displayLists.textureUVArray[uv_index++] = _faces[fi].s[vi]; + _displayLists.textureUVArray[uv_index++] = _faces[fi].t[vi]; + } + } + } + triangle_range[mi].length = tri_index - triangle_range[mi].location; + } + + _displayLists.count = tri_index; // total number of triangle vertices + return YES; +} + + +- (void) calculateBoundingVolumes +{ + int i; + double d_squared, length_longest_axis, length_shortest_axis; + GLfloat result; + + result = 0.0f; + if (vertexCount) bounding_box_reset_to_vector(&boundingBox, _vertices[0]); + else bounding_box_reset(&boundingBox); + + for (i = 0; i < vertexCount; i++) + { + d_squared = magnitude2(_vertices[i]); + if (d_squared > result) result = d_squared; + bounding_box_add_vector(&boundingBox, _vertices[i]); + } + + length_longest_axis = boundingBox.max.x - boundingBox.min.x; + if (boundingBox.max.y - boundingBox.min.y > length_longest_axis) + length_longest_axis = boundingBox.max.y - boundingBox.min.y; + if (boundingBox.max.z - boundingBox.min.z > length_longest_axis) + length_longest_axis = boundingBox.max.z - boundingBox.min.z; + + length_shortest_axis = boundingBox.max.x - boundingBox.min.x; + if (boundingBox.max.y - boundingBox.min.y < length_shortest_axis) + length_shortest_axis = boundingBox.max.y - boundingBox.min.y; + if (boundingBox.max.z - boundingBox.min.z < length_shortest_axis) + length_shortest_axis = boundingBox.max.z - boundingBox.min.z; + + d_squared = (length_longest_axis + length_shortest_axis) * (length_longest_axis + length_shortest_axis) * 0.25; // square of average length + maxDrawDistance = d_squared * NO_DRAW_DISTANCE_FACTOR * NO_DRAW_DISTANCE_FACTOR; // no longer based on the collision radius + + collisionRadius = sqrt(result); +} + + +- (void)rescaleByX:(GLfloat)scaleX y:(GLfloat)scaleY z:(GLfloat)scaleZ +{ + + OOMeshVertexCount i; + BOOL isotropic; + Vector *vertex = NULL, *normal = NULL; + + isotropic = (scaleX == scaleY && scaleY == scaleZ); + + for (i = 0; i != vertexCount; ++i) + { + vertex = &_vertices[i]; + + vertex->x *= scaleX; + vertex->y *= scaleY; + vertex->z *= scaleZ; + + if (!isotropic) + { + normal = &_normals[i]; + // For efficiency freaks: let's assume some sort of adaptive branch prediction. + normal->x *= scaleX; + normal->y *= scaleY; + normal->z *= scaleZ; + *normal = vector_normal(*normal); + } + } + + [self calculateBoundingVolumes]; + [octree release]; + octree = nil; +} + + +- (BoundingBox)boundingBox +{ + return boundingBox; +} + + +#ifndef NDEBUG +- (void)debugDrawNormals +{ + GLuint i; + Vector v, n, t, b; + float length, blend; + GLfloat color[3]; + OODebugWFState state; + + OO_ENTER_OPENGL(); + + state = OODebugBeginWireframe(NO); + + // Draw + glBegin(GL_LINES); + for (i = 0; i < _displayLists.count; ++i) + { + v = _displayLists.vertexArray[i]; + n = _displayLists.normalArray[i]; + t = _displayLists.tangentArray[i]; + b = true_cross_product(n, t); + + // Draw normal + length = magnitude2(n); + blend = fabsf(length - 1) * 5.0; + color[0] = MIN(blend, 1.0f); + color[1] = 1.0f - color[0]; + color[2] = color[1]; + glColor3fv(color); + + glVertex3f(v.x, v.y, v.z); + scale_vector(&n, 5.0f); + n = vector_add(n, v); + glVertex3f(n.x, n.y, n.z); + + // Draw tangent + glColor3f(1.0f, 1.0f, 0.0f); + t = vector_add(v, vector_multiply_scalar(t, 3.0f)); + glVertex3f(v.x, v.y, v.z); + glVertex3f(t.x, t.y, t.z); + + // Draw binormal + glColor3f(0.0f, 1.0f, 0.0f); + b = vector_add(v, vector_multiply_scalar(b, 3.0f)); + glVertex3f(v.x, v.y, v.z); + glVertex3f(b.x, b.y, b.z); + } + glEnd(); + + OODebugEndWireframe(state); +} +#endif + + +- (void) setRetainedObject:(id)object forKey:(NSString *)key +{ + assert(key != nil); + + if (object != nil) + { + if (_retainedObjects == nil) _retainedObjects = [[NSMutableDictionary alloc] init]; + [_retainedObjects setObject:object forKey:key]; + } +} + + +- (void *) allocateBytesWithSize:(size_t)size count:(OOUInteger)count key:(NSString *)key +{ + size *= count; + void *bytes = malloc(size); + if (bytes != NULL) + { + NSData *holder = [NSData dataWithBytesNoCopy:bytes length:size freeWhenDone:YES]; + [self setRetainedObject:holder forKey:key]; + } + return bytes; +} + + +- (BOOL) allocateVertexBuffersWithCount:(OOUInteger)count +{ + _vertices = [self allocateBytesWithSize:sizeof *_vertices count:vertexCount key:@"vertices"]; + _normals = [self allocateBytesWithSize:sizeof *_normals count:vertexCount key:@"normals"]; + _tangents = [self allocateBytesWithSize:sizeof *_tangents count:vertexCount key:@"tangents"]; + + return _vertices != NULL && + _normals != NULL && + _tangents != NULL; +} + + +- (BOOL) allocateFaceBuffersWithCount:(OOUInteger)count +{ + _faces = [self allocateBytesWithSize:sizeof *_faces count:faceCount key:@"faces"]; + return _faces != NULL; +} + + +- (BOOL) allocateVertexArrayBuffersWithCount:(OOUInteger)count +{ + _displayLists.indexArray = [self allocateBytesWithSize:sizeof *_displayLists.indexArray count:count * 3 key:@"indexArray"]; + _displayLists.textureUVArray = [self allocateBytesWithSize:sizeof *_displayLists.textureUVArray count:count * 6 key:@"textureUVArray"]; + _displayLists.vertexArray = [self allocateBytesWithSize:sizeof *_displayLists.vertexArray count:count * 3 key:@"vertexArray"]; + _displayLists.normalArray = [self allocateBytesWithSize:sizeof *_displayLists.normalArray count:count * 3 key:@"normalArray"]; + _displayLists.tangentArray = [self allocateBytesWithSize:sizeof *_displayLists.tangentArray count:count * 3 key:@"tangentArray"]; + + return _faces != NULL && + _displayLists.indexArray != NULL && + _displayLists.textureUVArray != NULL && + _displayLists.vertexArray != NULL && + _displayLists.normalArray != NULL && + _displayLists.tangentArray != NULL; +} + +@end + + +static NSString * const kOOCacheMeshes = @"OOMesh"; + +@implementation OOCacheManager (OOMesh) + ++ (NSDictionary *)meshDataForName:(NSString *)inShipName +{ + return [[self sharedCache] objectForKey:inShipName inCache:kOOCacheMeshes]; +} + + ++ (void)setMeshData:(NSDictionary *)inData forName:(NSString *)inShipName +{ + if (inData != nil && inShipName != nil) + { + [[self sharedCache] setObject:inData forKey:inShipName inCache:kOOCacheMeshes]; + } +} + +@end + + +static NSString * const kOOCacheOctrees = @"octrees"; + +@implementation OOCacheManager (Octree) + ++ (Octree *)octreeForModel:(NSString *)inKey +{ + NSDictionary *dict = nil; + Octree *result = nil; + + dict = [[self sharedCache] objectForKey:inKey inCache:kOOCacheOctrees]; + if (dict != nil) + { + result = [[Octree alloc] initWithDictionary:dict]; + [result autorelease]; + } + + return result; +} + + ++ (void)setOctree:(Octree *)inOctree forModel:(NSString *)inKey +{ + if (inOctree != nil && inKey != nil) + { + [[self sharedCache] setObject:[inOctree dict] forKey:inKey inCache:kOOCacheOctrees]; + } +} + +@end diff --git a/src/Core/OOMusic.m b/src/Core/OOMusic.m new file mode 100644 index 00000000..55d2571b --- /dev/null +++ b/src/Core/OOMusic.m @@ -0,0 +1,11 @@ +// OOMusic.m: Selects the appropriate music class source file +// depending on the operating system defined. +// +// Add new OS imports here. The -DOS_NAME flag in the GNUmakefile +// will select which one gets compiled. +// +// David Taylor, 2005-05-04 + +#ifdef LINUX +#import "SDLMusic.m" +#endif diff --git a/src/Core/OOMusicController.h b/src/Core/OOMusicController.h new file mode 100644 index 00000000..24d3a309 --- /dev/null +++ b/src/Core/OOMusicController.h @@ -0,0 +1,83 @@ +/* + +OOMusicController.h + +Singleton controller for music playback. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + +@class OOMusic; + + +#define OOLITE_ITUNES_SUPPORT OOLITE_MAC_OS_X + + +typedef enum +{ + kOOMusicOff, + kOOMusicOn, + kOOMusicITunes, + +#if OOLITE_ITUNES_SUPPORT + kOOMusicModeMax = kOOMusicITunes +#else + kOOMusicModeMax = kOOMusicOn +#endif +} OOMusicMode; + + +@interface OOMusicController: NSObject +{ + OOMusicMode _mode; + NSString *_missionMusic; + OOMusic *_current; + uint8_t _special; +} + ++ (OOMusicController *) sharedController; + +- (void) playMusicNamed:(NSString *)name loop:(BOOL)loop; + +- (void) playThemeMusic; +- (void) playDockingMusic; +- (void) playDockedMusic; + +- (void) setMissionMusic:(NSString *)missionMusicName; +- (void) playMissionMusic; + +- (void) stop; +- (void) stopMusicNamed:(NSString *)name; // Stop only if name == playingMusic +- (void) stopThemeMusic; +- (void) stopDockingMusic; +- (void) stopMissionMusic; + +- (void) toggleDockingMusic; // Start docking music if none playing, stop docking music if currently playing docking music. + +- (NSString *) playingMusic; +- (BOOL) isPlaying; + +- (OOMusicMode) mode; +- (void) setMode:(OOMusicMode)mode; + +@end diff --git a/src/Core/OOMusicController.m b/src/Core/OOMusicController.m new file mode 100644 index 00000000..4a323e94 --- /dev/null +++ b/src/Core/OOMusicController.m @@ -0,0 +1,370 @@ +/* + +OOMusicController.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOMusicController.h" +#import "OOSound.h" +#import "OOCollectionExtractors.h" +#import "ResourceManager.h" + + +static id sSingleton = nil; + + +@interface OOMusicController (Private) + +- (void) playiTunesPlaylist:(NSString *)playlistName; +- (void) pauseiTunes; + +@end + + + +// Values for _special +enum +{ + kSpecialNone, + kSpecialTheme, + kSpecialDocking, + kSpecialDocked, + kSpecialMission +}; + + +@implementation OOMusicController + ++ (OOMusicController *) sharedController +{ + if (sSingleton == nil) + { + [[self alloc] init]; + } + + return sSingleton; +} + + +- (id) init +{ + self = [super init]; + if (self != nil) + { + NSString *modeString = [[NSUserDefaults standardUserDefaults] stringForKey:@"music mode"]; + if ([modeString isEqualToString:@"off"]) _mode = kOOMusicOff; + else if ([modeString isEqualToString:@"iTunes"]) _mode = kOOMusicITunes; + else _mode = kOOMusicOn; + + // Handle unlikely case of taking prefs from iTunes-enabled system to other. + if (_mode > kOOMusicModeMax) _mode = kOOMusicModeMax; + + [self setMissionMusic:@"OoliteTheme.ogg"]; + } + + return self; +} + + +- (void) dealloc +{ + /* This class can never be dealloced, but this is here to placate the + Clang Static Analyzer. + */ + [super dealloc]; +} + + +- (void) playMusicNamed:(NSString *)name loop:(BOOL)loop +{ + if ([self isPlaying] && [name isEqual:[self playingMusic]]) return; + + if (_mode == kOOMusicOn) + { + OOMusic *music = [ResourceManager ooMusicNamed:name inFolder:@"Music"]; + if (music != nil) + { + [_current stop]; + + [music playLooped:loop]; + + [_current release]; + _current = [music retain]; + } + } +} + + +- (void) playThemeMusic +{ + _special = kSpecialTheme; + [self playMusicNamed:@"OoliteTheme.ogg" loop:YES]; +} + + +- (void) playDockingMusic +{ + _special = kSpecialDocking; + + if (_mode == kOOMusicITunes) + { + [self playiTunesPlaylist:@"Oolite-Docking"]; + } + else + { + [self playMusicNamed:@"BlueDanube.ogg" loop:NO]; + } +} + + +- (void) playDockedMusic +{ + _special = kSpecialDocked; + + if (_mode == kOOMusicITunes) + { + [self playiTunesPlaylist:@"Oolite-Docked"]; + } + else + { + [self playMusicNamed:@"OoliteDocked.ogg" loop:NO]; + } +} + + +- (void) setMissionMusic:(NSString *)missionMusicName +{ + [_missionMusic autorelease]; + _missionMusic = [missionMusicName copy]; +} + + +- (void) playMissionMusic +{ + if (_missionMusic != nil) + { + _special = kSpecialMission; + [self playMusicNamed:_missionMusic loop:NO]; + } +} + + +- (void) stop +{ + [_current stop]; + [_current release]; + _current = nil; + _special = kSpecialNone; + + if (_mode == kOOMusicITunes) + { + [self playiTunesPlaylist:@"Oolite-Inflight"]; + } +} + + +- (void) stopMusicNamed:(NSString *)name +{ + if ([name isEqual:[self playingMusic]]) [self stop]; +} + + +- (void) stopThemeMusic +{ + if (_special == kSpecialTheme) [self stop]; +} + + +- (void) stopDockingMusic +{ + if (_special == kSpecialDocking) [self stop]; +} + + +- (void) stopMissionMusic +{ + if (_special == kSpecialMission) [self stop]; +} + + +- (void) toggleDockingMusic +{ + if (_mode != kOOMusicOn) return; + + if (![self isPlaying]) [self playDockingMusic]; + else if (_special == kSpecialDocking) [self stop]; +} + + +- (NSString *) playingMusic +{ + return [_current name]; +} + + +- (BOOL) isPlaying +{ + return [_current isPlaying]; +} + + +- (OOMusicMode) mode +{ + return _mode; +} + + +- (void) setMode:(OOMusicMode)mode +{ + if (mode <= kOOMusicModeMax && _mode != mode) + { + _mode = mode; + + if (_mode == kOOMusicOff) [self stop]; + else switch (_special) + { + case kSpecialNone: + [self stop]; + break; + + case kSpecialTheme: + [self playThemeMusic]; + break; + + case kSpecialDocked: + [self playDockedMusic]; + break; + + case kSpecialDocking: + [self playDockingMusic]; + break; + + case kSpecialMission: + [self playMissionMusic]; + break; + } + + NSString *modeString = nil; + switch (_mode) + { + case kOOMusicOff: modeString = @"off"; break; + case kOOMusicOn: modeString = @"on"; break; + case kOOMusicITunes: modeString = @"iTunes"; break; + } + [[NSUserDefaults standardUserDefaults] setObject:modeString forKey:@"music mode"]; + } +} + +@end + + + +@implementation OOMusicController (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedController above. + + NOTE: assumes single-threaded access. +*/ + ++ (id) allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id) copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id) retain +{ + return self; +} + + +- (OOUInteger) retainCount +{ + return UINT_MAX; +} + + +- (void) release +{} + + +- (id) autorelease +{ + return self; +} + +@end + + +@implementation OOMusicController (Private) + +#if OOLITE_MAC_OS_X +- (void) playiTunesPlaylist:(NSString *)playlistName +{ + NSString *ootunesScriptString = + [NSString stringWithFormat: + @"tell application \"iTunes\"\n" + " if playlist \"%@\" exists then\n" + " set song repeat of playlist \"%@\" to all\nset shuffle of playlist \"%@\" to true\n" + " play some track of playlist \"%@\"\n" + " end if\n" + "end tell", + playlistName, playlistName, playlistName, playlistName]; + + NSAppleScript *ootunesScript = [[[NSAppleScript alloc] initWithSource:ootunesScriptString] autorelease]; + NSDictionary *errDict = nil; + + [ootunesScript executeAndReturnError:&errDict]; + if (errDict) + OOLog(@"iTunesIntegration.failed", @"ootunes returned :%@", [errDict description]); +} + + +- (void) pauseiTunes +{ + NSString *ootunesScriptString = [NSString stringWithFormat:@"tell application \"iTunes\" to pause"]; + NSAppleScript *ootunesScript = [[NSAppleScript alloc] initWithSource:ootunesScriptString]; + NSDictionary *errDict = nil; + [ootunesScript executeAndReturnError:&errDict]; + if (errDict) + OOLog(@"iTunesIntegration.failed", @"ootunes returned :%@", [errDict description]); + [ootunesScript release]; +} +#else +- (void) playiTunesPlaylist:(NSString *)playlistName {} +- (void) pauseiTunes {} +#endif + +@end diff --git a/src/Core/OOOpenGL.h b/src/Core/OOOpenGL.h new file mode 100644 index 00000000..60d0ce45 --- /dev/null +++ b/src/Core/OOOpenGL.h @@ -0,0 +1,121 @@ +/* + +OOOpenGL.h + +Do whatever is appropriate to get gl.h, glu.h and glext.h included. + +Also declares OpenGL-related utility functions. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + + +#if OOLITE_MAC_OS_X + +// Apple OpenGL includes... +#include +#include +#include +#include + +#elif OOLITE_SDL + +// SDL OpenGL includes... + +// prevent the including of SDL_opengl.h loading a previous version of glext.h +#define NO_SDL_GLEXT + +// GL_GLEXT_PROTOTYPES must be defined for the Linux build to use shaders. +#if OOLITE_LINUX +#ifndef GL_GLEXT_PROTOTYPES +#define GL_GLEXT_PROTOTYPES +#define __DEFINED_GL_GLEXT_PROTOTYPES +#endif // GL_GLEXT_PROTOTYPES +#endif // OOLITE_LINUX && !OOLITE_WINDOWS + +// the standard SDL_opengl.h +#include + +// include an up-to-date version of glext.h +#include + +#ifdef __DEFINED_GL_GLEXT_PROTOTYPES +#undef GL_GLEXT_PROTOTYPES +#undef __DEFINED_GL_GLEXT_PROTOTYPES +#endif + +#else // Not OS X or SDL + +#error OOOpenGL.h: unknown target! + +#endif + + +#define NULL_SHADER ((GLhandleARB)0) + + +/* CheckOpenGLErrors() + Check for and log OpenGL errors, and returns YES if an error occurred. + NOTE: this is controlled by the log message class rendering.opengl.error. + If logging is disabled, no error checking will occur. This is done + because glGetError() is quite expensive, requiring a full OpenGL + state sync. +*/ +BOOL CheckOpenGLErrors(NSString *format, ...); + +/* LogOpenGLState() + Write a bunch of OpenGL state information to the log. +*/ +void LogOpenGLState(void); + + +/* GLDebugWireframeModeOn() + GLDebugWireframeModeOff() + Enable/disable debug wireframe mode. In debug wireframe mode, the polygon + mode is set to GL_LINE, textures are disabled and the line size is set to + 1 pixel. +*/ +void GLDebugWireframeModeOn(void); +void GLDebugWireframeModeOff(void); + +/* GLDrawBallBillboard() + Draws a circle corresponding to a sphere of given radius at given distance. + Assumes Z buffering will be disabled. +*/ +void GLDrawBallBillboard(GLfloat radius, GLfloat step, GLfloat z_distance); + +/* GLDrawOval(), GLDrawFilledOval() + Draw axis-alligned ellipses, as outline and fill respectively. +*/ +void GLDrawOval(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step); +void GLDrawFilledOval(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step); + + +/* Texture name cache. + + glGenTextures() and glDeleteTextures() are expensive operations -- each + requres a complete flush of the rendering pipeline. We work around this by + caching texture objects. +*/ +GLuint GLAllocateTextureName(void); +void GLRecycleTextureName(GLuint name, GLuint mipLevels); diff --git a/src/Core/OOOpenGL.m b/src/Core/OOOpenGL.m new file mode 100644 index 00000000..0784ca27 --- /dev/null +++ b/src/Core/OOOpenGL.m @@ -0,0 +1,448 @@ +/* + +OOOpenGL.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOOpenGL.h" +#import "OOLogging.h" +#import "OOMaths.h" +#import "OOMacroOpenGL.h" +#import "OOFunctionAttributes.h" + + +static NSString * const kOOLogOpenGLStateDump = @"rendering.opengl.stateDump"; + + +BOOL CheckOpenGLErrors(NSString *format, ...) +{ + GLenum errCode; + const GLubyte *errString = NULL; + BOOL errorOccurred = NO; + va_list args; + + OO_ENTER_OPENGL(); + + // Short-circut here, because glGetError() is quite expensive. + if (OOLogWillDisplayMessagesInClass(kOOLogOpenGLError)) + { + for (;;) + { + errCode = glGetError(); + + if (errCode == GL_NO_ERROR) break; + + errorOccurred = YES; + errString = gluErrorString(errCode); + if (format == nil) format = @""; + + va_start(args, format); + format = [[[NSString alloc] initWithFormat:format arguments:args] autorelease]; + va_end(args); + OOLog(kOOLogOpenGLError, @"OpenGL error: \"%s\" (%#x), context: %@", errString, errCode, format); + } + } + return errorOccurred; +} + + +void GLDebugWireframeModeOn(void) +{ + OO_ENTER_OPENGL(); + + glPushAttrib(GL_POLYGON_BIT | GL_LINE_BIT | GL_TEXTURE_BIT); + glLineWidth(1.0f); + glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); + glDisable(GL_TEXTURE_2D); +} + + +void GLDebugWireframeModeOff(void) +{ + OO_ENTER_OPENGL(); + + glPopAttrib(); +} + + +void GLDrawBallBillboard(GLfloat radius, GLfloat step, GLfloat z_distance) +{ + if (EXPECT_NOT((radius <= 0)||(step < 1))) return; + if (EXPECT_NOT(radius >= z_distance)) return; // inside the sphere + + GLfloat i, delta; + GLfloat s, c; + GLfloat r; + + OO_ENTER_OPENGL(); + + r = radius * z_distance / sqrt(z_distance * z_distance - radius * radius); + delta = step * M_PI / 180.0f; // Convert step from degrees to radians + + glVertex3i(0, 0, 0); + for (i = 0; i < (M_PI * 2.0); i += delta) + { + s = r * sinf(i); + c = r * cosf(i); + glVertex3f(s, c, 0.0); + } + glVertex3f(0.0, r, 0.0); //repeat the zero value to close +} + + +static void GLDrawOvalPoints(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step); + +static void GLDrawOvalPoints(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step) +{ + GLfloat ww = 0.5 * siz.width; + GLfloat hh = 0.5 * siz.height; + GLfloat theta; + GLfloat delta; + + OO_ENTER_OPENGL(); + + delta = step * M_PI / 180.0f; + + for (theta = 0.0f; theta < (2.0f * M_PI); theta += delta) + { + glVertex3f(x + ww * sinf(theta), y + hh * cosf(theta), z); + } + glVertex3f(x, y + hh, z); +} + + +void GLDrawOval(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step) +{ + OO_ENTER_OPENGL(); + + glBegin(GL_LINE_STRIP); + GLDrawOvalPoints(x, y, z, siz, step); + glEnd(); +} + + +void GLDrawFilledOval(GLfloat x, GLfloat y, GLfloat z, NSSize siz, GLfloat step) +{ + OO_ENTER_OPENGL(); + + glBegin(GL_TRIANGLE_FAN); + GLDrawOvalPoints(x, y, z, siz, step); + glEnd(); +} + + +enum +{ + // Number of cached texture names. Unused texture names are cheap, so we use lots. + kTextureNameCacheMaxSize = 128, + + // Number of texture names to discard at a time when cache overflows. + kTextureNameCacheFlushCount = kTextureNameCacheMaxSize / 4 +}; + +static GLuint sTextureNameCache[kTextureNameCacheMaxSize]; +static unsigned sTextureNameCacheSize = 0; + + +GLuint GLAllocateTextureName(void) +{ + OOLog(@"textureCache.allocate", @"Request for texture name while cache size is %u.", sTextureNameCacheSize); + + if (sTextureNameCacheSize == 0) + { + OO_ENTER_OPENGL(); + + OOLog(@"textureCache.fill", @"Adding %u elements to texture names cache.", kTextureNameCacheMaxSize); + // Allocate a block of names. + glGenTextures(kTextureNameCacheMaxSize, sTextureNameCache); + sTextureNameCacheSize = kTextureNameCacheMaxSize; + } + + assert(sTextureNameCacheSize != 0); + + return sTextureNameCache[--sTextureNameCacheSize]; +} + + +void GLRecycleTextureName(GLuint name, GLuint mipLevels) +{ + if (name == 0) return; + + OOLog(@"textureCache.recycle", @"Recycling texture name while cache size is %u.", sTextureNameCacheSize); + + OO_ENTER_OPENGL(); + + if (sTextureNameCacheSize == kTextureNameCacheMaxSize) + { + OOLog(@"textureCache.flush", @"Deleting %u elements from texture names cache.", kTextureNameCacheFlushCount); + // No more space; delete several elements (to avoid a series of individual deletes) + sTextureNameCacheSize -= kTextureNameCacheFlushCount; + glDeleteTextures(kTextureNameCacheFlushCount, &sTextureNameCache[sTextureNameCacheSize]); + } + + assert(sTextureNameCacheSize < kTextureNameCacheMaxSize); + + GLuint i; + uint8_t junk[4]; + + for (i = 0; i != mipLevels; ++i) + { + glBindTexture(GL_TEXTURE_2D, name); + glTexImage2D(GL_TEXTURE_2D, i, GL_RGBA, 0, 0, 0, GL_RGBA, GL_UNSIGNED_BYTE, junk); + } + + sTextureNameCache[sTextureNameCacheSize++] = name; +} + + +// ======== LogOpenGLState() and helpers ======== + +static NSString *GLColorToString(GLfloat color[4]); +static NSString *GLEnumToString(GLenum value); + +static void GLDumpLightState(unsigned lightIdx); +static void GLDumpMaterialState(void); +static void GLDumpCullingState(void); +static void GLDumpFogState(void); + + +void LogOpenGLState() +{ + unsigned i; + + if (!OOLogWillDisplayMessagesInClass(kOOLogOpenGLStateDump)) return; + + OOLog(kOOLogOpenGLStateDump, @"OpenGL state dump:"); + OOLogIndent(); + + GLDumpMaterialState(); + GLDumpCullingState(); + for (i = 0; i != 8; ++i) + { + GLDumpLightState(i); + } + GLDumpFogState(); + + CheckOpenGLErrors(@"After state dump"); + + OOLogOutdent(); +} + + +static NSString *GLColorToString(GLfloat color[4]) +{ + #define COLOR_EQUAL(color, r, g, b, a) (color[0] == (r) && color[1] == (g) && color[2] == (b) && color[3] == (a)) + #define COLOR_CASE(r, g, b, a, str) do { if (COLOR_EQUAL(color, (r), (g), (b), (a))) return (str); } while (0) + + COLOR_CASE(1, 1, 1, 1, @"white"); + COLOR_CASE(0, 0, 0, 1, @"black"); + COLOR_CASE(0, 0, 0, 0, @"clear"); + COLOR_CASE(1, 0, 0, 1, @"red"); + COLOR_CASE(0, 1, 0, 1, @"green"); + COLOR_CASE(0, 0, 1, 1, @"blue"); + COLOR_CASE(0, 1, 1, 1, @"cyan"); + COLOR_CASE(1, 0, 1, 1, @"magenta"); + COLOR_CASE(1, 1, 0, 1, @"yellow"); + + return [NSString stringWithFormat:@"(%.2ff, %.2ff, %.2ff, %.2ff)", color[0], color[1], color[2], color[3]]; +} + + +static void GLDumpLightState(unsigned lightIdx) +{ + BOOL enabled; + GLenum lightID = GL_LIGHT0 + lightIdx; + GLfloat color[4]; + + OO_ENTER_OPENGL(); + + enabled = glIsEnabled(lightID); + OOLog(kOOLogOpenGLStateDump, @"Light %u: %s", lightIdx, enabled ? "enabled" : "disabled"); + + if (enabled) + { + OOLogIndent(); + + glGetLightfv(GL_LIGHT1, GL_AMBIENT, color); + OOLog(kOOLogOpenGLStateDump, @"Ambient: %@", GLColorToString(color)); + glGetLightfv(GL_LIGHT1, GL_DIFFUSE, color); + OOLog(kOOLogOpenGLStateDump, @"Diffuse: %@", GLColorToString(color)); + glGetLightfv(GL_LIGHT1, GL_SPECULAR, color); + OOLog(kOOLogOpenGLStateDump, @"Specular: %@", GLColorToString(color)); + + OOLogOutdent(); + } +} + + +static void GLDumpMaterialState(void) +{ + GLfloat color[4]; + GLfloat shininess; + GLint shadeModel, + blendSrc, + blendDst, + texMode; + BOOL blending; + + OO_ENTER_OPENGL(); + + OOLog(kOOLogOpenGLStateDump, @"Material state:"); + OOLogIndent(); + + glGetMaterialfv(GL_FRONT, GL_AMBIENT, color); + OOLog(kOOLogOpenGLStateDump, @"Ambient: %@", GLColorToString(color)); + + glGetMaterialfv(GL_FRONT, GL_DIFFUSE, color); + OOLog(kOOLogOpenGLStateDump, @"Diffuse: %@", GLColorToString(color)); + + glGetMaterialfv(GL_FRONT, GL_EMISSION, color); + OOLog(kOOLogOpenGLStateDump, @"Emission: %@", GLColorToString(color)); + + glGetMaterialfv(GL_FRONT, GL_SPECULAR, color); + OOLog(kOOLogOpenGLStateDump, @"Specular: %@", GLColorToString(color)); + + glGetMaterialfv(GL_FRONT, GL_SHININESS, &shininess); + OOLog(kOOLogOpenGLStateDump, @"Shininess: %g", shininess); + + OOLog(kOOLogOpenGLStateDump, @"Colour material: %s", glIsEnabled(GL_COLOR_MATERIAL) ? "enabled" : "disabled"); + + glGetFloatv(GL_CURRENT_COLOR, color); + OOLog(kOOLogOpenGLStateDump, @"Current color: %@", GLColorToString(color)); + + glGetIntegerv(GL_SHADE_MODEL, &shadeModel); + OOLog(kOOLogOpenGLStateDump, @"Shade model: %@", GLEnumToString(shadeModel)); + + blending = glIsEnabled(GL_BLEND); + OOLog(kOOLogOpenGLStateDump, @"Blending: %s", blending ? "enabled" : "disabled"); + if (blending) + { + glGetIntegerv(GL_BLEND_SRC, &blendSrc); + glGetIntegerv(GL_BLEND_DST, &blendDst); + OOLog(kOOLogOpenGLStateDump, @"Blend function: %@, %@", GLEnumToString(blendSrc), GLEnumToString(blendDst)); + } + + glGetTexEnviv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, &texMode); + OOLog(kOOLogOpenGLStateDump, @"Texture env mode: %@", GLEnumToString(texMode)); + + OOLogOutdent(); +} + + +static void GLDumpCullingState(void) +{ + GLint value; + + OO_ENTER_OPENGL(); + + glGetIntegerv(GL_CULL_FACE_MODE, &value); + OOLog(kOOLogOpenGLStateDump, @"Cull face mode: %@", GLEnumToString(value)); + + glGetIntegerv(GL_FRONT_FACE, &value); + OOLog(kOOLogOpenGLStateDump, @"Front face direction: %@", GLEnumToString(value)); +} + + +static void GLDumpFogState(void) +{ + BOOL enabled; + GLint value; + GLfloat start, + end, + density, + index; + GLfloat color[4]; + + OO_ENTER_OPENGL(); + + enabled = glIsEnabled(GL_FOG); + OOLog(kOOLogOpenGLStateDump, @"Fog: %s", enabled ? "enabled" : "disabled"); + if (enabled) + { + OOLogIndent(); + + glGetIntegerv(GL_FOG_MODE, &value); + OOLog(kOOLogOpenGLStateDump, @"Fog mode: *@", GLEnumToString(value)); + + glGetFloatv(GL_FOG_COLOR, color); + OOLog(kOOLogOpenGLStateDump, @"Fog colour: %@", GLColorToString(color)); + + glGetFloatv(GL_FOG_START, &start); + glGetFloatv(GL_FOG_START, &end); + OOLog(kOOLogOpenGLStateDump, @"Fog start, end: %g, %g", start, end); + + glGetFloatv(GL_FOG_DENSITY, &density); + OOLog(kOOLogOpenGLStateDump, @"Fog density: %g", density); + + glGetFloatv(GL_FOG_DENSITY, &index); + OOLog(kOOLogOpenGLStateDump, @"Fog index: %g", index); + + OOLogOutdent(); + } +} + + +#define CASE(x) case x: return @#x + +static NSString *GLEnumToString(GLenum value) +{ + switch (value) + { + // ShadingModel + CASE(GL_FLAT); + CASE(GL_SMOOTH); + + // BlendingFactorSrc/BlendingFactorDest + CASE(GL_ZERO); + CASE(GL_ONE); + CASE(GL_DST_COLOR); + CASE(GL_SRC_COLOR); + CASE(GL_ONE_MINUS_DST_COLOR); + CASE(GL_ONE_MINUS_SRC_COLOR); + CASE(GL_SRC_ALPHA); + CASE(GL_DST_ALPHA); + CASE(GL_ONE_MINUS_SRC_ALPHA); + CASE(GL_ONE_MINUS_DST_ALPHA); + CASE(GL_SRC_ALPHA_SATURATE); + + // TextureEnvMode + CASE(GL_MODULATE); + CASE(GL_DECAL); + CASE(GL_BLEND); + CASE(GL_REPLACE); + + // FrontFaceDirection + CASE(GL_CW); + CASE(GL_CCW); + + // CullFaceMode + CASE(GL_FRONT); + CASE(GL_BACK); + CASE(GL_FRONT_AND_BACK); + + // FogMode + CASE(GL_LINEAR); + CASE(GL_EXP); + CASE(GL_EXP2); + + default: return [NSString stringWithFormat:@"unknown: %u", value]; + } +} diff --git a/src/Core/OOOpenGLExtensionManager.h b/src/Core/OOOpenGLExtensionManager.h new file mode 100644 index 00000000..19c13128 --- /dev/null +++ b/src/Core/OOOpenGLExtensionManager.h @@ -0,0 +1,156 @@ +/* + +OOOpenGLExtensionManager.h + +Handles checking for and using OpenGL extensions and related information. + +This is thread safe, except for initialization; that is, +sharedManager should +be called from the main thread at an early point. The OpenGL context must be +set up by then. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" +#import "OOFunctionAttributes.h" + + +#ifndef NO_SHADERS + +// Certain extensions are required for shader support. +#ifndef GL_ARB_multitexture +#warning NO_SHADERS not defined and GL_ARB_multitexture not defined. +#endif + +#ifndef GL_ARB_shader_objects +#warning NO_SHADERS not defined and GL_ARB_shader_objects not defined. +#endif + +#ifndef GL_ARB_shading_language_100 +#warning NO_SHADERS not defined and GL_ARB_shading_language_100 not defined. +#endif + +#ifndef GL_ARB_fragment_shader +#warning NO_SHADERS not defined and GL_ARB_fragment_shader not defined. +#endif + +#ifndef GL_ARB_vertex_shader +#warning NO_SHADERS not defined and GL_ARB_vertex_shader not defined. +#endif + +#endif //NO_SHADERS + + +#define OOOPENGLEXTMGR_LOCK_SET_ACCESS (!OOLITE_MAC_OS_X) + + +@interface OOOpenGLExtensionManager: NSObject +{ +#if OOOPENGLEXTMGR_LOCK_SET_ACCESS + NSLock *lock; +#endif + NSSet *extensions; + + NSString *vendor; + NSString *renderer; + + unsigned major, minor, release; + +#ifndef NO_SHADERS + BOOL shadersAvailable; +#endif +} + ++ (id)sharedManager; + +- (BOOL)haveExtension:(NSString *)extension; + +- (BOOL)shadersSupported; + +- (unsigned)majorVersionNumber; +- (unsigned)minorVersionNumber; +- (unsigned)releaseVersionNumber; +- (void)getVersionMajor:(unsigned *)outMajor minor:(unsigned *)outMinor release:(unsigned *)outRelease; + +@end + + +OOINLINE BOOL OOShadersSupported(void) INLINE_PURE_FUNC; +OOINLINE BOOL OOShadersSupported(void) +{ + return [[OOOpenGLExtensionManager sharedManager] shadersSupported]; +} + + +#if OOLITE_WINDOWS +/* Declare the function pointers for the OpenGL extensions used in the game + (required for Windows only). +*/ + +#ifndef NO_SHADERS +// Shader functions +PFNGLUSEPROGRAMOBJECTARBPROC glUseProgramObjectARB; +PFNGLACTIVETEXTUREARBPROC glActiveTextureARB; +PFNGLGETUNIFORMLOCATIONARBPROC glGetUniformLocationARB; +PFNGLUNIFORM1IARBPROC glUniform1iARB; +PFNGLUNIFORM1FARBPROC glUniform1fARB; +PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fvARB; +PFNGLUNIFORM4FVARBPROC glUniform4fvARB; +PFNGLGETOBJECTPARAMETERIVARBPROC glGetObjectParameterivARB; +PFNGLCREATESHADEROBJECTARBPROC glCreateShaderObjectARB; +PFNGLGETINFOLOGARBPROC glGetInfoLogARB; +PFNGLCREATEPROGRAMOBJECTARBPROC glCreateProgramObjectARB; +PFNGLATTACHOBJECTARBPROC glAttachObjectARB; +PFNGLDELETEOBJECTARBPROC glDeleteObjectARB; +PFNGLLINKPROGRAMARBPROC glLinkProgramARB; +PFNGLCOMPILESHADERARBPROC glCompileShaderARB; +PFNGLSHADERSOURCEARBPROC glShaderSourceARB; +PFNGLUNIFORM2FVARBPROC glUniform2fvARB; +PFNGLBINDATTRIBLOCATIONARBPROC glBindAttribLocationARB; +PFNGLENABLEVERTEXATTRIBARRAYARBPROC glEnableVertexAttribArrayARB; +PFNGLVERTEXATTRIBPOINTERARBPROC glVertexAttribPointerARB; +PFNGLDISABLEVERTEXATTRIBARRAYARBPROC glDisableVertexAttribArrayARB; +#endif // !defined(NO_SHADERS) + +#endif // OOLITE_WINDOWS diff --git a/src/Core/OOOpenGLExtensionManager.m b/src/Core/OOOpenGLExtensionManager.m new file mode 100644 index 00000000..8fdbd94a --- /dev/null +++ b/src/Core/OOOpenGLExtensionManager.m @@ -0,0 +1,405 @@ +/* + +OOOpenGLExtensionManager.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOOpenGLExtensionManager.h" +#import "OOLogging.h" +#import "OOFunctionAttributes.h" +#import +#import "NSThreadOOExtensions.h" + + +/* If nonzero, disable shaders for OpenGL versions less than 1.5. It is my + contention that this isn't needed since we test for extensions. Enabling + this check disables shaders on Intel GMA 950 hardware under Mac OS + 10.4.9; they seem to work fine otherwise. -- Ahruman +*/ +#define SHADER_CHECK_FOR_VERSION 0 + + +#if OOLITE_WINDOWS && !defined(NO_SHADERS) +/* Define the function pointers for the OpenGL extensions used in the game + (required for Windows only). +*/ + +#ifndef NO_SHADERS +void OOBadOpenGLExtensionUsed(void) GCC_ATTR((noreturn)); + +PFNGLUSEPROGRAMOBJECTARBPROC glUseProgramObjectARB = (PFNGLUSEPROGRAMOBJECTARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLGETUNIFORMLOCATIONARBPROC glGetUniformLocationARB = (PFNGLGETUNIFORMLOCATIONARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLUNIFORM1IARBPROC glUniform1iARB = (PFNGLUNIFORM1IARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLUNIFORM1FARBPROC glUniform1fARB = (PFNGLUNIFORM1FARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLUNIFORMMATRIX4FVARBPROC glUniformMatrix4fvARB = (PFNGLUNIFORMMATRIX4FVARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLUNIFORM4FVARBPROC glUniform4fvARB = (PFNGLUNIFORM4FVARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLGETOBJECTPARAMETERIVARBPROC glGetObjectParameterivARB = (PFNGLGETOBJECTPARAMETERIVARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLCREATESHADEROBJECTARBPROC glCreateShaderObjectARB = (PFNGLCREATESHADEROBJECTARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLGETINFOLOGARBPROC glGetInfoLogARB = (PFNGLGETINFOLOGARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLCREATEPROGRAMOBJECTARBPROC glCreateProgramObjectARB = (PFNGLCREATEPROGRAMOBJECTARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLATTACHOBJECTARBPROC glAttachObjectARB = (PFNGLATTACHOBJECTARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLDELETEOBJECTARBPROC glDeleteObjectARB = (PFNGLDELETEOBJECTARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLLINKPROGRAMARBPROC glLinkProgramARB = (PFNGLLINKPROGRAMARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLCOMPILESHADERARBPROC glCompileShaderARB = (PFNGLCOMPILESHADERARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLSHADERSOURCEARBPROC glShaderSourceARB = (PFNGLSHADERSOURCEARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLUNIFORM2FVARBPROC glUniform2fvARB = (PFNGLUNIFORM2FVARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLBINDATTRIBLOCATIONARBPROC glBindAttribLocationARB = (PFNGLBINDATTRIBLOCATIONARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLENABLEVERTEXATTRIBARRAYARBPROC glEnableVertexAttribArrayARB = (PFNGLENABLEVERTEXATTRIBARRAYARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLVERTEXATTRIBPOINTERARBPROC glVertexAttribPointerARB = (PFNGLVERTEXATTRIBPOINTERARBPROC)&OOBadOpenGLExtensionUsed; +PFNGLDISABLEVERTEXATTRIBARRAYARBPROC glDisableVertexAttribArrayARB = (PFNGLDISABLEVERTEXATTRIBARRAYARBPROC)&OOBadOpenGLExtensionUsed; +#endif +#endif + + +static NSString * const kOOLogOpenGLShaderSupport = @"rendering.opengl.shader.support"; + + +static OOOpenGLExtensionManager *sSingleton = nil; + + +// Read integer from string, advancing string to end of read data. +static unsigned IntegerFromString(const GLubyte **ioString); + + +@interface OOOpenGLExtensionManager (OOPrivate) + +#ifndef NO_SHADERS +- (void)checkShadersSupported; +#endif // NO_SHADERS + +@end + + + +@implementation OOOpenGLExtensionManager + +- (id)init +{ + NSString *extensionString = nil; + NSArray *components = nil; + const GLubyte *versionString = NULL, *curr = NULL; + + self = [super init]; + if (self != nil) + { +#if OOOPENGLEXTMGR_LOCK_SET_ACCESS + lock = [[NSLock alloc] init]; + [lock ooSetName:@"OOOpenGLExtensionManager extension set lock"]; +#endif + + extensionString = [NSString stringWithUTF8String:(char *)glGetString(GL_EXTENSIONS)]; + components = [extensionString componentsSeparatedByString:@" "]; + extensions = [[NSSet alloc] initWithArray:components]; + + vendor = [[NSString alloc] initWithUTF8String:(const char *)glGetString(GL_VENDOR)]; + renderer = [[NSString alloc] initWithUTF8String:(const char *)glGetString(GL_RENDERER)]; + + versionString = glGetString(GL_VERSION); + if (versionString != NULL) + { + /* String is supposed to be "major.minorFOO" or + "major.minor.releaseFOO" where FOO is an empty string or + a string beginning with space. + */ + curr = versionString; + major = IntegerFromString(&curr); + if (*curr == '.') + { + curr++; + minor = IntegerFromString(&curr); + } + if (*curr == '.') + { + curr++; + release = IntegerFromString(&curr); + } + } + + OOLog(@"rendering.opengl.version", @"OpenGL renderer version: %u.%u.%u (\"%s\")\nVendor: %@\nRenderer: %@", major, minor, release, versionString, vendor, renderer); + OOLog(@"rendering.opengl.extensions", @"OpenGL extensions (%u):\n%@", [extensions count], extensionString); + + if (major <= 1 && minor < 1) + { + /* Ensure we have OpenGL 1.1 or later (basic stuff like + glBindTexture(), glDrawArrays()). + We probably have implicit requirements for later versions, but + I don't feel like auditing. + -- Ahruman + */ + OOLog(@"rendering.opengl.version.insufficient", @"***** Oolite requires OpenGL version 1.1 or later."); + [NSException raise:@"OoliteOpenGLTooOldException" + format:@"Oolite requires at least OpenGL 1.1. You have %u.%u (\"%s\").", major, minor, versionString]; + } + +#ifndef NO_SHADERS + [self checkShadersSupported]; +#endif + } + return self; +} + + +- (void)dealloc +{ + if (sSingleton == self) sSingleton = nil; + +#if OOOPENGLEXTMGR_LOCK_SET_ACCESS + [lock release]; +#endif + [extensions release]; + [vendor release]; + [renderer release]; + + [super dealloc]; +} + + ++ (id)sharedManager +{ + // NOTE: assumes single-threaded first access. See header. + if (sSingleton == nil) [[self alloc] init]; + return sSingleton; +} + + +- (BOOL)haveExtension:(NSString *)extension +{ +// NSSet is documented as thread-safe under OS X, but I'm not sure about GNUstep. -- Ahruman +#if OOOPENGLEXTMGR_LOCK_SET_ACCESS + [lock lock]; +#endif + + BOOL result = [extensions containsObject:extension]; + +#if OOOPENGLEXTMGR_LOCK_SET_ACCESS + [lock unlock]; +#endif + + return result; +} + + +- (BOOL)shadersSupported +{ +#ifndef NO_SHADERS + return shadersAvailable; +#else + return NO; +#endif +} + + +- (unsigned)majorVersionNumber +{ + return major; +} + + +- (unsigned)minorVersionNumber +{ + return minor; +} + + +- (unsigned)releaseVersionNumber +{ + return release; +} + + +- (void)getVersionMajor:(unsigned *)outMajor minor:(unsigned *)outMinor release:(unsigned *)outRelease +{ + if (outMajor != NULL) *outMajor = major; + if (outMinor != NULL) *outMinor = minor; + if (outRelease != NULL) *outRelease = release; +} + +@end + + +static unsigned IntegerFromString(const GLubyte **ioString) +{ + if (EXPECT_NOT(ioString == NULL)) return 0; + + unsigned result = 0; + const GLubyte *curr = *ioString; + + while ('0' <= *curr && *curr <= '9') + { + result = result * 10 + *curr++ - '0'; + } + + *ioString = curr; + return result; +} + + +@implementation OOOpenGLExtensionManager (OOPrivate) + + +#ifndef NO_SHADERS + +- (void)checkShadersSupported +{ + shadersAvailable = NO; + +#if SHADER_CHECK_FOR_VERSION + if (major <= 1 && minor < 5) + { + OOLog(kOOLogOpenGLShaderSupport, @"Shaders will not be used (OpenGL version < 1.5)."); + return; + } +#endif + + const NSString *requiredExtension[] = + { + @"GL_ARB_shading_language_100", + @"GL_ARB_fragment_shader", + @"GL_ARB_vertex_shader", + @"GL_ARB_multitexture", + @"GL_ARB_shader_objects", + nil // sentinel - don't remove! + }; + NSString **required = NULL; + + for (required = requiredExtension; *required != nil; ++required) + { + if (![self haveExtension:*required]) + { + OOLog(kOOLogOpenGLShaderSupport, @"Shaders will not be used (OpenGL extension %@ is not available).", *required); + return; + } + } + +#if OOLITE_WINDOWS + glGetObjectParameterivARB = (PFNGLGETOBJECTPARAMETERIVARBPROC)wglGetProcAddress("glGetObjectParameterivARB"); + glCreateShaderObjectARB = (PFNGLCREATESHADEROBJECTARBPROC)wglGetProcAddress("glCreateShaderObjectARB"); + glGetInfoLogARB = (PFNGLGETINFOLOGARBPROC)wglGetProcAddress("glGetInfoLogARB"); + glCreateProgramObjectARB = (PFNGLCREATEPROGRAMOBJECTARBPROC)wglGetProcAddress("glCreateProgramObjectARB"); + glAttachObjectARB = (PFNGLATTACHOBJECTARBPROC)wglGetProcAddress("glAttachObjectARB"); + glDeleteObjectARB = (PFNGLDELETEOBJECTARBPROC)wglGetProcAddress("glDeleteObjectARB"); + glLinkProgramARB = (PFNGLLINKPROGRAMARBPROC)wglGetProcAddress("glLinkProgramARB"); + glCompileShaderARB = (PFNGLCOMPILESHADERARBPROC)wglGetProcAddress("glCompileShaderARB"); + glShaderSourceARB = (PFNGLSHADERSOURCEARBPROC)wglGetProcAddress("glShaderSourceARB"); + glUseProgramObjectARB = (PFNGLUSEPROGRAMOBJECTARBPROC)wglGetProcAddress("glUseProgramObjectARB"); + glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC)wglGetProcAddress("glActiveTextureARB"); + glGetUniformLocationARB = (PFNGLGETUNIFORMLOCATIONARBPROC)wglGetProcAddress("glGetUniformLocationARB"); + glUniform1iARB = (PFNGLUNIFORM1IARBPROC)wglGetProcAddress("glUniform1iARB"); + glUniform1fARB = (PFNGLUNIFORM1FARBPROC)wglGetProcAddress("glUniform1fARB"); + glUniformMatrix4fvARB = (PFNGLUNIFORMMATRIX4FVARBPROC)wglGetProcAddress("glUniformMatrix4fvARB"); + glUniform4fvARB = (PFNGLUNIFORM4FVARBPROC)wglGetProcAddress("glUniform4fvARB"); + glUniform2fvARB = (PFNGLUNIFORM2FVARBPROC)wglGetProcAddress("glUniform2fvARB"); + glBindAttribLocationARB = (PFNGLBINDATTRIBLOCATIONARBPROC)wglGetProcAddress("glBindAttribLocationARB"); + glEnableVertexAttribArrayARB = (PFNGLENABLEVERTEXATTRIBARRAYARBPROC)wglGetProcAddress("glEnableVertexAttribArrayARB"); + glVertexAttribPointerARB = (PFNGLVERTEXATTRIBPOINTERARBPROC)wglGetProcAddress("glVertexAttribPointerARB"); + glDisableVertexAttribArrayARB = (PFNGLDISABLEVERTEXATTRIBARRAYARBPROC)wglGetProcAddress("glDisableVertexAttribArrayARB"); +#endif + + shadersAvailable = YES; +} +#endif + +@end + + +@implementation OOOpenGLExtensionManager (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedManager above. + + // NOTE: assumes single-threaded first access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end + + +#if OOLITE_WINDOWS && !defined(NO_SHADERS) + +void OOBadOpenGLExtensionUsed(void) +{ + OOLog(@"rendering.opengl.badExtension", @"***** An uninitialized OpenGL extension function has been called, terminating. This is a serious error, please report it. *****"); + exit(EXIT_FAILURE); +} + +#endif diff --git a/src/Core/OOPListParsing.h b/src/Core/OOPListParsing.h new file mode 100644 index 00000000..8278fe70 --- /dev/null +++ b/src/Core/OOPListParsing.h @@ -0,0 +1,41 @@ +/* + +OOPListParsing.h + +Property list parser. Tries to use native Foundation property list parsing, +then falls back on Oolite ad-hoc parser for backwards-compatibility (Oolite's +XML plist parser is more lenient than Foundation on OS X). + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + + +// whereFrom is an optional description of the data source, for error reporting. +id OOPropertyListFromData(NSData *data, NSString *whereFrom); +id OOPropertyListFromFile(NSString *path); + +// Wrappers which ensure that the plist contains the right type of object. +NSDictionary *OODictionaryFromData(NSData *data, NSString *whereFrom); +NSDictionary *OODictionaryFromFile(NSString *path); + +NSArray *OOArrayFromData(NSData *data, NSString *whereFrom); +NSArray *OOArrayFromFile(NSString *path); diff --git a/src/Core/OOPListParsing.m b/src/Core/OOPListParsing.m new file mode 100644 index 00000000..67e2a12a --- /dev/null +++ b/src/Core/OOPListParsing.m @@ -0,0 +1,731 @@ +/* + +OOPListParsing.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOPListParsing.h" +#import "OOLogging.h" +#import "OOStringParsing.h" +#import +#import + + +#if !OOLITE_GNUSTEP +#define NO_DYNAMIC_PLIST_DTD_CHANGE +#endif + + +static NSString * const kOOLogPListFoundationParseError = @"plist.parse.foundation.failed"; +static NSString * const kOOLogPListWrongType = @"plist.wrongType"; +static NSString * const kOOLogPListHomebrewBadEncoding = @"plist.homebrew.badEncoding"; +static NSString * const kOOLogPListHomebrewException = @"plist.homebrew.exception"; +static NSString * const kOOLogPListHomebrewParseError = @"plist.homebrew.parseError"; +static NSString * const kOOLogPListHomebrewParseWarning = @"plist.homebrew.parseWarning"; +static NSString * const kOOLogPListHomebrewTokenizeTrace = @"plist.homebrew.tokenize.trace"; +static NSString * const kOOLogPListHomebrewInterpretTrace = @"plist.homebrew.interpret.trace"; +static NSString * const kOOLogPListHomebrewSuccess = @"plist.homebrew.success"; + + +#define OOLITE_EXCEPTION_XML_PARSING_FAILURE @"OOXMLException" + + +typedef struct +{ + NSString *tag; // name of the tag + id content; // content of tag +} OOXMLElement; + + +#ifndef NO_DYNAMIC_PLIST_DTD_CHANGE +static NSData *ChangeDTDIfApplicable(NSData *data); +#endif + +static NSData *CopyDataFromFile(NSString *path); +static id ValueIfClass(id value, Class class); +static id ParseXMLPropertyList(NSData *data, NSString *whereFrom); + +static NSArray *TokensFromXML(NSData *data, NSString *whereFrom); +static id InterpretXMLTokens(NSArray *tokens, NSString *whereFrom); +static OOXMLElement ParseXMLElement(NSScanner *scanner, NSString *closingTag, NSString *whereFrom); +static NSString *ResolveXMLEntities(NSString *string); +static id ObjectFromXMLElement(NSArray *tokens, BOOL expectKey, NSString *whereFrom); +static NSData *DataFromXMLString(NSString *string, NSString *whereFrom); +static NSArray *ArrayFromXMLString(NSArray *tokens, NSString *whereFrom); +static NSDictionary *DictionaryFromXMLString(NSArray *tokens, NSString *whereFrom); + +static NSString *ShortDescription(id object); + + +id OOPropertyListFromData(NSData *data, NSString *whereFrom) +{ + id result = nil; + NSString *error = nil; + + if (data != nil) + { +#ifndef NO_DYNAMIC_PLIST_DTD_CHANGE + data = ChangeDTDIfApplicable(data); +#endif + + result = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:&error]; + if (result == nil) // Foundation parser failed + { +#if OOLITE_RELEASE_PLIST_ERROR_STRINGS + [error autorelease]; +#endif + // Ensure we can say something sensible... + if (error == nil) error = @""; + if (whereFrom == nil) whereFrom = @""; + +#ifndef NDEBUG + // Complain + OOLog(kOOLogPListFoundationParseError, @"Failed to parse %@ as a property list using Foundation. Retrying using homebrew parser. WARNING: the homebrew parser is deprecated and will be removed in a future version of Oolite.\n%@", whereFrom, error); + OOLogIndentIf(kOOLogPListFoundationParseError); +#endif + + result = ParseXMLPropertyList(data, whereFrom); + +#ifndef NDEBUG + OOLogOutdentIf(kOOLogPListFoundationParseError); +#endif + } + } + + return result; +} + + +id OOPropertyListFromFile(NSString *path) +{ + id result = nil; + NSData *data = nil; + + if (path != nil) + { + // Load file, if it exists... + data = CopyDataFromFile(path); + if (data != nil) + { + // ...and parse it + result = OOPropertyListFromData(data, path); + [data release]; + } + // Non-existent file is not an error. + } + + return result; +} + + +#ifndef NO_DYNAMIC_PLIST_DTD_CHANGE +static NSData *ChangeDTDIfApplicable(NSData *data) +{ + const uint8_t *bytes = NULL; + uint8_t *newBytes = NULL; + size_t length, + newLength, + offset = 0, + newOffset = 0; + const char xmlDeclLine[] = "<\?xml version=\"1.0\" encoding=\"UTF-8\"\?>"; + const char *appleDTDLines[] = + { + "", + "", + NULL + }; + const char gstepDTDLine[] = ""; + const char *srcDTDLine = NULL; + size_t srcDTDLineSize = 0; + unsigned i; + + length = [data length]; + if (length < sizeof xmlDeclLine) return data; + + bytes = [data bytes]; + + // Check if it starts with an XML declaration. Bogus: there are valid XML declarations which don't match xmlDeclLine. + if (memcmp(bytes, xmlDeclLine, sizeof xmlDeclLine - 1) != 0) return data; + + offset += sizeof xmlDeclLine - 1; + while (offset < length && isspace(bytes[offset])) ++offset; + + // Check if first non-blank stuff after XML declaration is any known Apple plist DTD. Also somewhat bogus. + for (i = 0; ; i++) + { + srcDTDLine = appleDTDLines[i]; + if (srcDTDLine == NULL) return data; // No matches + + srcDTDLineSize = strlen(appleDTDLines[i]); + + if (srcDTDLineSize <= length - offset && + memcmp(bytes + offset, srcDTDLine, srcDTDLineSize) == 0) + { + // Match + break; + } + } + + offset += srcDTDLineSize; + + newLength = length - offset + sizeof xmlDeclLine + sizeof gstepDTDLine - 1; + newBytes = malloc(newLength); + if (newBytes == NULL) return data; + + // Construct modified version with altered DTD line + memcpy(newBytes, xmlDeclLine, sizeof xmlDeclLine - 1); + newOffset = sizeof xmlDeclLine - 1; + newBytes[newOffset++] = '\n'; + memcpy(newBytes + newOffset, gstepDTDLine, sizeof gstepDTDLine - 1); + newOffset += sizeof gstepDTDLine - 1; + memcpy(newBytes + newOffset, bytes + offset, length - offset); + + return [NSData dataWithBytes:newBytes length:newLength]; +} +#endif + + +/* Load data from file. Returns a retained pointer. + -initWithContentsOfMappedFile fails quietly under OS X if there's no file, + but GNUstep complains. +*/ +static NSData *CopyDataFromFile(NSString *path) +{ +#if OOLITE_MAC_OS_X + return [[NSData alloc] initWithContentsOfMappedFile:path]; +#else + NSFileManager *fmgr = [NSFileManager defaultManager]; + BOOL dir; + + if ([fmgr fileExistsAtPath:path isDirectory:&dir]) + { + if (!dir) + { + return [[NSData alloc] initWithContentsOfMappedFile:path]; + } + else + { + OOLog(kOOLogFileNotFound, @"Expected property list but found directory at %@", path); + } + } + + return nil; +#endif +} + + +// Wrappers which ensure that the plist contains the right type of object. +NSDictionary *OODictionaryFromData(NSData *data, NSString *whereFrom) +{ + id result = OOPropertyListFromData(data, whereFrom); + return ValueIfClass(result, [NSDictionary class]); +} + + +NSDictionary *OODictionaryFromFile(NSString *path) +{ + id result = OOPropertyListFromFile(path); + return ValueIfClass(result, [NSDictionary class]); +} + + +NSArray *OOArrayFromData(NSData *data, NSString *whereFrom) +{ + id result = OOPropertyListFromData(data, whereFrom); + return ValueIfClass(result, [NSArray class]); +} + + +NSArray *OOArrayFromFile(NSString *path) +{ + id result = OOPropertyListFromFile(path); + return ValueIfClass(result, [NSArray class]); +} + + +// Ensure that object is of desired class. +static id ValueIfClass(id value, Class class) +{ + if (value != nil && ![value isKindOfClass:class]) + { + OOLog(kOOLogPListWrongType, @"Property list is wrong type - expected %@, got %@.", class, [value class]); + value = nil; + } + return value; +} + + +static id ParseXMLPropertyList(NSData *data, NSString *whereFrom) +{ + id result = nil; + NSArray *tokens = nil; + NSAutoreleasePool *pool = nil; + + pool = [[NSAutoreleasePool alloc] init]; + OOLogPushIndent(); + NS_DURING + OOLog(kOOLogPListHomebrewTokenizeTrace, @">>>>> Tokenizing property list."); + OOLogIndentIf(kOOLogPListHomebrewTokenizeTrace); + tokens = TokensFromXML(data, whereFrom); + OOLogOutdentIf(kOOLogPListHomebrewTokenizeTrace); + + if (tokens != nil) + { + OOLog(kOOLogPListHomebrewInterpretTrace, @"Property list tokenization successful, interpreting."); + OOLogIndentIf(kOOLogPListHomebrewInterpretTrace); + result = InterpretXMLTokens(tokens, whereFrom); + OOLogOutdentIf(kOOLogPListHomebrewInterpretTrace); + if (result != nil) OOLog(kOOLogPListHomebrewSuccess, @"Successfully interpreted property list... for now."); + } + NS_HANDLER + // OOLITE_EXCEPTION_XML_PARSING_FAILURE indicates an error we've already logged. + if (![[localException name] isEqual:OOLITE_EXCEPTION_XML_PARSING_FAILURE]) + { + OOLog(kOOLogException, @"Encountered exception while parsing property list %@ (parsing failed). Exception: %@: %@", whereFrom, [localException name], [localException reason]); + } + NS_ENDHANDLER + OOLogPopIndent(); + [result retain]; + [pool release]; + return [result autorelease]; + +} + + +static NSArray *TokensFromXML(NSData *data, NSString *whereFrom) +{ + NSString *xmlString = nil; + NSScanner *scanner = nil; + OOXMLElement xml = { nil, nil }; + + // Assume UTF-8, UTF-16 or system encoding... not robust. + xmlString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + if (xmlString == nil) xmlString = [[NSString alloc] initWithData:data encoding:NSUnicodeStringEncoding]; + if (xmlString == nil) xmlString = [[NSString alloc] initWithData:data]; + if (xmlString == nil) + { + OOLog(kOOLogPListHomebrewBadEncoding, @"Could not interpret property list %@ as UTF-8 text.", whereFrom); + } + else + { + scanner = [NSScanner scannerWithString:xmlString]; + [xmlString release]; + + OOLogPushIndent(); + NS_DURING + xml = ParseXMLElement(scanner, @"ROOT", whereFrom); + NS_HANDLER + // OOLITE_EXCEPTION_XML_PARSING_FAILURE indicates an error we've already logged. + if (![[localException name] isEqual:OOLITE_EXCEPTION_XML_PARSING_FAILURE]) + { + OOLog(kOOLogException, @"Encountered exception while parsing property list %@ (parsing failed). Exception: %@: %@", whereFrom, [localException name], [localException reason]); + } + NS_ENDHANDLER + OOLogPopIndent(); + } + + if ([xml.content isKindOfClass:[NSArray class]]) return xml.content; + else if ([xml.content isKindOfClass:[NSString class]]) + { + OOLog(kOOLogPListHomebrewParseError, @"Property list isn't in XML format, homebrew parser can't help you."); + } + else + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list parser error: expected root element tokenization to be NSArray, but got %@.", [xml.content class]); + } + return nil; +} + + +static id InterpretXMLTokens(NSArray *tokens, NSString *whereFrom) +{ + NSEnumerator *elementEnum = nil; + id element = nil; + NSString *tag = nil; + id content = nil; + NSArray *plist = nil; + + for (elementEnum = [tokens objectEnumerator]; (element = [elementEnum nextObject]); ) + { + // Elements are OOXMLElements converted to two-member arrays. + tag = [element objectAtIndex:0]; + content = [element objectAtIndex:1]; + + OOLog(kOOLogPListHomebrewInterpretTrace, @"Got element: <%@>", tag); + + if ([tag isEqual:@"plist"]) + { + if ([content isKindOfClass:[NSArray class]]) + { + plist = [content objectAtIndex:0]; + if ([plist isKindOfClass:[NSArray class]]) + { + return ObjectFromXMLElement(plist, NO, whereFrom); + } + } + + OOLog(kOOLogPListHomebrewParseError, @"***** Property list parser error: invalid structure for tokenization of element."); + } + + if (![tag hasPrefix:@"!"] && ![tag hasPrefix:@"?"]) + { + // Bad root-level element - not or directive + OOLog(kOOLogPListHomebrewParseWarning, @"----- Bad property list: root level element <%@> is not or directive.", tag); + } + } + + // If we got here, there was no + OOLog(kOOLogPListHomebrewParseError, @"***** Property list parser error: could not find a element."); + return nil; +} + + +static OOXMLElement ParseXMLElement(NSScanner *scanner, NSString *closingTag, NSString *whereFrom) +{ + OOXMLElement result = {0}, element = {0}; + NSMutableArray *elements = nil; + NSString *preamble = nil; + NSString *tag = nil; + int openBracketLocation; + NSArray *tagbits = nil; + BOOL done = NO; + BOOL foundPreamble, foundBracket, foundTag; + + elements = [NSMutableArray array]; + + while (!done && ![scanner isAtEnd]) + { + foundPreamble = [scanner scanUpToString:@"<" intoString:&preamble]; + foundBracket = [scanner scanString:@"<" intoString:NULL]; + if (!foundBracket) + { + // No < found + // These cases appear to be ignored, since tag is nil. + if (foundPreamble) + { + // Is this useful? -- ahruman + element.tag = nil; + element.content = ResolveXMLEntities(preamble); + OOLog(kOOLogPListHomebrewTokenizeTrace, @"Found preamble but no <, using \"%@\"", ShortDescription(element.content)); + } + else + { + // Nothing found. + element.tag = nil; + element.content = @""; + OOLog(kOOLogPListHomebrewTokenizeTrace, @"Found nothing, using empty string"); + } + } + else + { + // < found + // Look for closing > + openBracketLocation = [scanner scanLocation]; + foundTag = [scanner scanUpToString:@">" intoString:&tag]; + foundBracket = [scanner scanString:@">" intoString:NULL]; + if (!foundBracket) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: found tag with no closing bracket (\"<%@\").", tag); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Unclosed tag <%@", tag]; + } + if (!foundTag || [tag length] == 0) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: found empty tag (\"<>\")."); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Empty tag"]; + } + + // If we get here, we’ve got a tag. + OOLog(kOOLogPListHomebrewTokenizeTrace, @"Found tag <%@>", tag); + if ([tag hasPrefix:@"!"] || [tag hasPrefix:@"?"] || [tag hasSuffix:@"/"]) + { + // Directive, self-closing tag or comment + if ([tag hasPrefix:@"!--"]) + { + // Comment. This comment handling is techincally invalid because it doesn't fail if the comment contains "--". XML Is such fun. + [scanner setScanLocation:openBracketLocation + 3]; + [scanner scanUpToString:@"-->" intoString:NULL]; + foundBracket = [scanner scanString:@"-->" intoString:NULL]; + if (!foundBracket) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: found unterminated comment (no -->)."); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Unterminated comment"]; + } + else + { + element.tag = nil; + element.content = nil; + } + } + else + { + // Directive or self-closing tag + tagbits = ScanTokensFromString(tag); + tag = ResolveXMLEntities([[tagbits objectAtIndex:0] lowercaseString]); // Whut? How can there be entities in a tag name? Also, tags are case-sensetive. -- ahruman + element.tag = tag; + element.content = tagbits; + } + } + else + { + // Opening or closing tag + if ([tag hasPrefix:@"/"]) + { + // Closing tag + if ([tag hasSuffix:closingTag]) // Not general - will match when looking for - but good enough for plists. -- ahruman + { + // End of bit we're looking for + element.tag = nil; + if (foundPreamble) element.content = ResolveXMLEntities(preamble); + else element.content = nil; + done = YES; + } + else + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: closing tag <%@> with no opening tag (expected ).", tag, closingTag); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Wrong closing tag <%@>, should be <%@>", tag, closingTag]; + } + } + else + { + // It's an opening tag; recurse + tagbits = ScanTokensFromString(tag); + if ([tagbits count] == 0) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: empty opening tag (<>)."); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Empty opening tag"]; + } + tag = ResolveXMLEntities([[tagbits objectAtIndex:0] lowercaseString]); // Se before re "whut?". -- ahruman + + OOLog(kOOLogPListHomebrewTokenizeTrace, @"Recursively parsing children of tag %@", tag); + OOLogIndentIf(kOOLogPListHomebrewTokenizeTrace); + element = ParseXMLElement(scanner, tag, whereFrom); + OOLogOutdentIf(kOOLogPListHomebrewTokenizeTrace); + } + } + } + + if (element.tag != nil && element.content != nil) + { + [elements addObject:[NSArray arrayWithObjects:element.tag, element.content, nil]]; + } + } + + result.tag = closingTag; + if ([elements count] != 0) result.content = elements; + else result.content = element.content; + + return result; +} + + +static NSString *ResolveXMLEntities(NSString *string) +{ + if ([string rangeOfString:@"&"].location == NSNotFound) return string; + + NSMutableString* result = [[string mutableCopy] autorelease]; + + // These shouldn't really be case-insensetive, but we're going for bugwards-compatibility here. + [result replaceOccurrencesOfString:@"&" withString:@"&" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [result length])]; + [result replaceOccurrencesOfString:@"<" withString:@"<" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [result length])]; + [result replaceOccurrencesOfString:@">" withString:@">" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [result length])]; + [result replaceOccurrencesOfString:@"'" withString:@"\'" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [result length])]; + [result replaceOccurrencesOfString:@""" withString:@"\"" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [result length])]; + + return result; +} + + +static id ObjectFromXMLElement(NSArray *tokens, BOOL expectKey, NSString *whereFrom) +{ + NSString *tag = nil; + id content = nil; + id result = nil; + + if ([tokens count] != 2) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list parser error: invalid token structure."); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Invalid token structure"]; + } + + tag = [tokens objectAtIndex:0]; + content = [tokens objectAtIndex:1]; + + if ([content isKindOfClass:[NSString class]]) OOLog(kOOLogPListHomebrewInterpretTrace, @"Interpreting <%@>: %@", tag, ShortDescription(content)); + else OOLog(kOOLogPListHomebrewInterpretTrace, @"Interpreting <%@>", tag); + OOLogIndentIf(kOOLogPListHomebrewInterpretTrace); + + + if ([tag isEqual:@"key"]) + { + result = [NSString stringWithString:content]; + if (!expectKey) OOLog(kOOLogPListHomebrewParseWarning, @"----- Bad property list: element (%@) found when expecting a value, treating as .", result); + } + else + { + if ([tag isEqual:@"true/"]) result = [NSNumber numberWithBool:YES]; + else if ([tag isEqual:@"false/"]) result = [NSNumber numberWithBool:NO]; + else if ([tag isEqual:@"real"]) result = [NSNumber numberWithDouble:[content doubleValue]]; + else if ([tag isEqual:@"integer"]) result = [NSNumber numberWithDouble:[content intValue]]; + else if ([tag isEqual:@"string"]) result = [NSString stringWithString:content]; + else if ([tag isEqual:@"string/"]) result = @""; + else if ([tag isEqual:@"date"]) result = [NSDate dateWithString:content]; + else if ([tag isEqual:@"data"]) result = DataFromXMLString(content, whereFrom); + else if ([tag isEqual:@"array"]) result = ArrayFromXMLString(content, whereFrom); + else if ([tag isEqual:@"array/"]) result = [NSArray array]; + else if ([tag isEqual:@"dict"]) result = DictionaryFromXMLString(content, whereFrom); + else if ([tag isEqual:@"dict/"]) result = [NSDictionary dictionary]; + + if (result != nil) + { + if (expectKey) OOLog(kOOLogPListHomebrewParseWarning, @"----- Bad property list: expected , got <%@>. Allowing for backwards compatibility, but the property list will not function as intended.", tag); + } + else + { + OOLog(kOOLogPListHomebrewParseWarning, @"----- Bad property list: unknown value class element <%@>, ignoring.", tag); + } + } + + OOLogOutdentIf(kOOLogPListHomebrewInterpretTrace); + return result; +} + + +static NSData *DataFromXMLString(NSString *string, NSString *whereFrom) +{ + if (![string isKindOfClass:[NSString class]]) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: expected string inside , found %@.", string); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Bad type"]; + } + + // String should be base64 data. + // we're going to decode the string from base64 + NSString *base64String = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; + NSMutableData *resultingData = [NSMutableData data]; + char bytes3[3]; + OOUInteger n_64Chars; + int tripletValue; + OOUInteger n_chars = [string length]; + OOUInteger i = 0; + + while (i < n_chars) + { + n_64Chars = 0; + tripletValue = 0; + while ((n_64Chars < 4)&(i < n_chars)) + { + OOUInteger b64 = [base64String rangeOfString:[string substringWithRange:NSMakeRange(i,1)]].location; + if (b64 != NSNotFound) + { + tripletValue *= 64; + tripletValue += (b64 & 63); + n_64Chars++; + } + i++; + } + while (n_64Chars < 4) //shouldn't need to pad, but we do just in case + { + tripletValue *= 64; + n_64Chars++; + } + bytes3[0] = (tripletValue & 0xff0000) >> 16; + bytes3[1] = (tripletValue & 0xff00) >> 8; + bytes3[2] = (tripletValue & 0xff); + [resultingData appendBytes:(const void *)bytes3 length:3]; + } + return [NSData dataWithData:resultingData]; +} + + +static NSArray *ArrayFromXMLString(NSArray *tokens, NSString *whereFrom) +{ + NSMutableArray *result = nil; + NSEnumerator *elementEnum = nil; + id element = nil; + + if (![tokens isKindOfClass:[NSArray class]]) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: expected elements inside , found %@.", tokens); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Bad type"]; + } + + result = [NSMutableArray arrayWithCapacity:[tokens count]]; + for (elementEnum = [tokens objectEnumerator]; (element = [elementEnum nextObject]); ) + { + element = ObjectFromXMLElement(element, NO, whereFrom); + if (element != nil) [result addObject:element]; + } + + return result; +} + + +static NSDictionary *DictionaryFromXMLString(NSArray *tokens, NSString *whereFrom) +{ + NSMutableDictionary *result = nil; + NSEnumerator *elementEnum = nil; + id keyElement = nil; + id valueElement = nil; + id key = nil; + id value = nil; + + if (![tokens isKindOfClass:[NSArray class]]) + { + OOLog(kOOLogPListHomebrewParseError, @"***** Property list error: expected elements inside , found %@.", tokens); + [NSException raise:OOLITE_EXCEPTION_XML_PARSING_FAILURE format:@"Bad type"]; + } + + result = [NSMutableDictionary dictionaryWithCapacity:[tokens count]]; + for (elementEnum = [tokens objectEnumerator]; (keyElement = [elementEnum nextObject]); ) + { + valueElement = [elementEnum nextObject]; + if (valueElement == nil) + { + OOLog(kOOLogPListHomebrewParseWarning, @"----- Bad property list: odd number of elements in , ignoring trailing <%@>.", [keyElement objectAtIndex:0]); + } + + key = ObjectFromXMLElement(keyElement, YES, whereFrom); + value = ObjectFromXMLElement(valueElement, NO, whereFrom); + + if (key != nil && value != nil) + { + [result setObject:value forKey:key]; + } + } + + return result; +} + + +static NSString *ShortDescription(id object) +{ + NSString *desc = nil; + + if (object == nil) return @"(null)"; + + desc = [object description]; + if (100 < [desc length]) + { + desc = [[desc substringToIndex:80] stringByAppendingString:@"..."]; + } + return desc; +} diff --git a/src/Core/OOPriorityQueue.h b/src/Core/OOPriorityQueue.h new file mode 100644 index 00000000..491804ad --- /dev/null +++ b/src/Core/OOPriorityQueue.h @@ -0,0 +1,71 @@ +/* + +OOPriorityQueue.h + +A prority queue is a collection into which objects may be inserted in any +order, but (primarily) extracted in sorted order. The order is defined by the +comparison selector specified at creation time, which is assumed to have the +same signature as a compare method used for array sorting: +- (NSComparisonResult)compare:(id)other +and must define a partial order on the objects in the priority queue. The +behaviour when provided with an inconsistent comparison method is undefined. + +The implementation is the standard one, a binary heap. It is described in +detail in most algorithm textbooks. + +This collection is *not* thread-safe. + + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@interface OOPriorityQueue: NSObject +{ + SEL _comparator; + id *_heap; + OOUInteger _count, + _capacity; +} + +// Note: -init is equivalent to -initWithComparator:@selector(compare:) ++ (id) queueWithComparator:(SEL)comparator; +- (id) initWithComparator:(SEL)comparator; + +- (void) addObject:(id)object; // May throw NSInvalidArgumentException or NSMallocException. +- (void) removeObject:(id)object; // Uses comparator (looking for NSOrderedEqual) to find object. Note: relatively expensive. +- (void) removeExactObject:(id)object; // Uses pointer comparison to find object. Note: still relatively expensive. + +- (OOUInteger) count; + +- (id) nextObject; +- (id) peekAtNextObject; // Returns next object without removing it. +- (void) removeNextObject; + +- (void) addObjects:(id)collection; // collection must respond to -nextObject, or implement -objectEnumerator to return something that implements -nextObject -- such as an NSEnumerator. + +- (NSArray *) sortedObjects; // Returns the objects in -nextObject order and empties the heap. To get the objects without emptying the heap, copy the priority queue first. +- (NSEnumerator *) objectEnumerator; // Enumerator which pulls objects off the heap until it's empty. Note however that the queue itself behaves like an enumerator, as -nextObject has similar semantics (except that the enumerator's -nextObject can never start returning objects after it returns nil). + +@end diff --git a/src/Core/OOPriorityQueue.m b/src/Core/OOPriorityQueue.m new file mode 100644 index 00000000..bbc17b9c --- /dev/null +++ b/src/Core/OOPriorityQueue.m @@ -0,0 +1,712 @@ +/* + +OOPriorityQueue.m + + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#import "OOPriorityQueue.h" +#import "OOFunctionAttributes.h" + + +/* Capacity grows by 50% each time. kMinCapacity must be at least 2 or Bad + Things will happen. Some examples of growth patterns based on kMinCapacity: + 2: 2 3 4 6 9 13 19 28 42 63 94 141 211 316 474 711 1066... + 4: 4 6 9 13 19 28 42 63 94 141 211 316 474 711 1066 1599... + 8: 8 12 18 27 40 60 90 135 202 303 454 681 1021 1531 2296... + 16: 16 24 36 54 81 121 181 271 406 609 913 1369 2053 3079... + 32: 32 48 72 108 162 243 364 546 819 1228 1842 2763 4144... + + Special cases: when an OOPriorityQueue is copied, the copy's capacity is + set to the same as its count, which may be less than kMinCapacity. However, + when it is grown, it will be set to at least kMinCapacity (except in the + case of a reallocation failure, in which case an attempt will be made to + grow capacity by one element). +*/ +enum +{ + kMinCapacity = 16 +}; + + +/* Functions to navigate the binary heap. + Using one-based indices, the following hold: + * The left child of a node, L(n), is 2n. + * The right child of a node, R(n), is 2n + 1. + * The parent of a node, P(n), is n/2 for n > 1. + + Using zero-based indices, we must convert to and from one-based indices to + perform these calculations, giving: + * Lz(n) = L(n+1)-1 = (2(n+1))-1 = 2n+1 + * Rz(n) = R(n+1)-1 = (2(n+1)+1)-1 = 2n+2 + * Pz(n) = P(n+1)-1 = ((n+1)/2)-1 +*/ + + +OOINLINE OOUInteger PQLeftChild(OOUInteger n) INLINE_CONST_FUNC; +OOINLINE OOUInteger PQRightChild(OOUInteger n) INLINE_CONST_FUNC; +OOINLINE OOUInteger PQParent(OOUInteger n) INLINE_CONST_FUNC; + +OOINLINE OOUInteger PQLeftChild(OOUInteger n) +{ + return (n << 1) + 1; +} + +OOINLINE OOUInteger PQRightChild(OOUInteger n) +{ + return (n << 1) + 2; +} + + +OOINLINE OOUInteger PQParent(OOUInteger n) +{ + return ((n + 1) >> 1) - 1; +} + + +typedef NSComparisonResult (*CompareIMP)(id self, SEL _cmd, id other); + + +OOINLINE NSComparisonResult PQCompare(id a, id b, SEL comparator) +{ + CompareIMP compare = NULL; + NSComparisonResult result; + + // This is equivalent to [a performSelector:comparator withObject:b], except the resulting value isn't an object. + compare = (CompareIMP)[a methodForSelector:comparator]; + result = compare(a, comparator, b); + return result; +} + + +// Private priority queue methods. +@interface OOPriorityQueue (Private) + +- (void) makeObjectsPerformSelector:(SEL)selector; + +- (void) bubbleUpFrom:(OOUInteger)i; +- (void) bubbleDownFrom:(OOUInteger)i; + +- (void) growBuffer; +- (void) shrinkBuffer; + +- (void)removeObjectAtIndex:(OOUInteger)i; + +#if OO_DEBUG +- (void) appendDebugDataToString:(NSMutableString *)string index:(OOUInteger)i depth:(OOUInteger)depth; +#endif + +@end + + +// NSEnumerator subclass to pull objects from queue. +@interface OOPriorityQueueEnumerator: NSEnumerator +{ + OOPriorityQueue *_queue; +} + +- (id) initWithPriorityQueue:(OOPriorityQueue *)queue; + +@end + + +@implementation OOPriorityQueue + ++ (id) queueWithComparator:(SEL)comparator +{ + return [[[self alloc] initWithComparator:comparator] autorelease]; +} + + +- (id) initWithComparator:(SEL)comparator +{ + if (comparator == NULL) + { + [self release]; + return nil; + } + + self = [super init]; + if (self != nil) + { + _comparator = comparator; + } + + return self; +} + + +- (id) init +{ + return [self initWithComparator:@selector(compare:)]; +} + + +- (void) dealloc +{ + [self makeObjectsPerformSelector:@selector(release)]; + free(_heap); + + [super dealloc]; +} + + +- (NSString *) description +{ + return [NSString stringWithFormat:@"<%@ %p>{count=%u, capacity=%u}", [self class], self, _count, _capacity]; +} + + +#if OO_DEBUG +- (NSString *) debugDescription +{ + NSMutableString *result = nil; + + result = [NSMutableString string]; + [result appendFormat:@"<%@ %p> (count=%u, capacity=%u, comparator=%@)", [self class], self, _count, _capacity, NSStringFromSelector(_comparator)]; + + if (_count != 0) + { + [result appendString:@"\n{\n"]; + [self appendDebugDataToString:result index:0 depth:0]; + [result appendString:@"}"]; + } + else + { + [result appendString:@" {}"]; + } + return result; +} +#endif + + +- (BOOL) isEqual:(id)object +{ + OOUInteger i; + OOPriorityQueue *selfCopy = nil, *otherCopy = nil; + BOOL identical = YES; + + if (object == self) return YES; + if (![object isKindOfClass:[self class]]) return NO; + + if (_count != [object count]) return NO; + if (_count == 0) return YES; + + selfCopy = [self copy]; + otherCopy = [object copy]; + i = _count; + while (i--) + { + if (![[selfCopy nextObject] isEqual:[otherCopy nextObject]]) + { + identical = NO; + break; + } + } + [selfCopy release]; + [otherCopy release]; + + return identical; +} + + +- (OOUInteger) hash +{ + if (_count == 0) return NSNotFound; + return _count ^ [_heap[0] hash]; +} + + +- (id) copyWithZone:(NSZone *)zone +{ + OOPriorityQueue *copy = nil; + + copy = [[self class] allocWithZone:zone]; + if (copy != nil) + { + copy->_comparator = _comparator; + copy->_count = _count; + copy->_capacity = _count; + + copy->_heap = malloc(_count * sizeof(id)); + if (copy->_heap != NULL) + { + memcpy(copy->_heap, _heap, _count * sizeof(id)); + [copy makeObjectsPerformSelector:@selector(retain)]; + } + else if (_count != 0) + { + [copy release]; + copy = nil; + } + } + + return copy; +} + + +- (void) addObject:(id)object +{ + OOUInteger i; + + // Validate object + if (object == nil) + { + [NSException raise:NSInvalidArgumentException + format:@"Attempt to insert nil into OOPriorityQueue."]; + } + + if (![object respondsToSelector:_comparator]) + { + [NSException raise:NSInvalidArgumentException + format:@"Attempt to insert object (%@) which does not support comparator %@ into OOPriorityQueue.", + object, NSStringFromSelector(_comparator)]; + } + + // Ensure there is sufficent space. + if (_count == _capacity) [self growBuffer]; + + // insert object at end of buffer. + i = _count++; + _heap[i] = object; + + [self bubbleUpFrom:i]; + [object retain]; +} + + +- (void) removeObject:(id)object +{ + OOUInteger i; + + /* Perform linear search for object (using comparator). A depth-first + search could skip leaves of lower priority, but I don't expect this to + be called very often. + */ + + if (object == nil) return; + + for (i = 0; i < _count; ++i) + { + if (PQCompare(object, _heap[i], _comparator) == 0) + { + [self removeObjectAtIndex:i]; + } + } +} + + +- (void) removeExactObject:(id)object +{ + OOUInteger i; + + if (object == nil) return; + + for (i = 0; i < _count; ++i) + { + if (object == _heap[i]) + { + [self removeObjectAtIndex:i]; + } + } +} + + +- (OOUInteger) count +{ + return _count; +} + + +- (id) nextObject +{ + id result = [self peekAtNextObject]; + [self removeNextObject]; + return result; +} + + +- (id) peekAtNextObject +{ + if (_count == 0) return nil; + return [[_heap[0] retain] autorelease]; +} + + +- (void) removeNextObject +{ + [self removeObjectAtIndex:0]; +} + + +- (void) addObjects:(id)collection +{ + id value = nil; + + if ([collection respondsToSelector:@selector(objectEnumerator)]) collection = [collection objectEnumerator]; + if (![collection respondsToSelector:@selector(nextObject)]) return; + + while ((value = [collection nextObject])) [self addObject:value]; +} + + +- (NSArray *) sortedObjects +{ + return [[self objectEnumerator] allObjects]; +} + + +- (NSEnumerator *) objectEnumerator +{ + return [[[OOPriorityQueueEnumerator alloc] initWithPriorityQueue:self] autorelease]; +} + +@end + + +@implementation OOPriorityQueue (Private) + +- (void) makeObjectsPerformSelector:(SEL)selector +{ + OOUInteger i; + + if (selector == NULL) return; + + for (i = 0; i != _count; ++i) + { + [_heap[i] performSelector:selector]; + } +} + + +- (void) bubbleUpFrom:(OOUInteger)i +{ + OOUInteger pi; + id obj = nil, par = nil; + + while (0 < i) + { + pi = PQParent(i); + obj = _heap[i]; + par = _heap[pi]; + + if (PQCompare(obj, par, _comparator) < 0) + { + _heap[i] = par; + _heap[pi] = obj; + i = pi; + } + else break; + } +} + + +- (void) bubbleDownFrom:(OOUInteger)i +{ + OOUInteger end = _count - 1; + OOUInteger li, ri, next; + id obj = nil; + + obj = _heap[i]; + while (PQLeftChild(i) <= end) + { + li = PQLeftChild(i); + ri = PQRightChild(i); + + // If left child has lower priority than right child, or there is only one child... + if (li == end || PQCompare(_heap[li], _heap[ri], _comparator) < 0) + { + next = li; + } + else + { + next = ri; + } + + if (PQCompare(_heap[next], obj, _comparator) < 0) + { + // Exchange parent with lowest-priority child + _heap[i] = _heap[next]; + _heap[next] = obj; + i = next; + } + else break; + } +} + + +- (void) growBuffer +{ + id *newBuffer = NULL; + OOUInteger newCapacity; + + newCapacity = _capacity * 3 / 2; + if (newCapacity < kMinCapacity) newCapacity = kMinCapacity; + + // Note: realloc(NULL, size) with non-zero size is equivalent to malloc(size), so this is OK starting from a NULL buffer. + newBuffer = realloc(_heap, newCapacity * sizeof(id)); + if (newBuffer == NULL) + { + // Attempt to grow by just one waffer-thin slot. + newCapacity = _capacity + 1; + newBuffer = realloc(_heap, newCapacity * sizeof(id)); + + if (newBuffer == NULL) + { + // Failed to grow. + [NSException raise:NSMallocException + format:@"Could not expand capacity of OOPriorityQueue."]; + } + } + + _heap = newBuffer; + _capacity = newCapacity; + + assert(_count < _capacity); +} + + +- (void)shrinkBuffer +{ + OOUInteger amountToRemove; + id *newBuffer = NULL; + OOUInteger newCapacity; + + if (kMinCapacity < _capacity) + { + // Remove two thirds of free space, if at least three slots are free. + amountToRemove = _capacity - _count * 2 / 3; + if (2 < amountToRemove) + { + newCapacity = _capacity = amountToRemove; + newBuffer = realloc(_heap, newCapacity * sizeof(id)); + if (newBuffer != NULL) + { + _heap = newBuffer; + _capacity = newCapacity; + } + } + } +} + + +- (void)removeObjectAtIndex:(OOUInteger)i +{ + id object = nil; + + if (_count <= i) return; + + object = _heap[i]; + if (i < --_count) + { + // Overwrite object with last object in array + _heap[i] = _heap[_count]; + + // Push previously-last object down until tree is partially ordered. + [self bubbleDownFrom:i]; + } + else + { + // Special case: removing last (or only) object. No bubbling needed. + } + + [object release]; + if (_count * 2 <= _capacity) [self shrinkBuffer]; +} + + +#if OO_DEBUG +- (void) appendDebugDataToString:(NSMutableString *)string index:(OOUInteger)i depth:(OOUInteger)depth +{ + OOUInteger spaces; + + if (_count <= i) return; + + spaces = 2 + depth; + while (spaces--) [string appendString:@" "]; + [string appendString:[_heap[i] description]]; + [string appendString:@"\n"]; + + [self appendDebugDataToString:string index:PQLeftChild(i) depth:depth + 1]; + [self appendDebugDataToString:string index:PQRightChild(i) depth:depth + 1]; +} +#endif + +@end + + +@implementation OOPriorityQueueEnumerator + +- (id) initWithPriorityQueue:(OOPriorityQueue *)queue +{ + if (queue == nil) + { + [self release]; + return nil; + } + + self = [super init]; + if (self != nil) + { + _queue = [queue retain]; + } + return self; +} + + +- (void) dealloc +{ + [_queue release]; + + [super dealloc]; +} + + +- (id) nextObject +{ + id value = [_queue nextObject]; + if (value == nil) + { + // Maintain enumerator semantics by ensuring we don't start returning new values after returning nil. + [_queue release]; + _queue = nil; + } + return value; +} + +@end + + +#if DEBUG_GRAPHVIZ +@implementation OOPriorityQueue (DebugGraphViz) + +// Workaround for Xcode auto-indent bug +static NSString * const kQuotationMark = @"\""; +static NSString * const kEscapedQuotationMark = @"\\\""; + + +static NSString *EscapedString(NSString *string) +{ + const NSString *srcStrings[] = + { + //Note: backslash must be first. + @"\\", @"\"", @"\'", @"\r", @"\n", @"\t", nil + }; + const NSString *subStrings[] = + { + //Note: must be same order. + @"\\\\", @"\\\"", @"\\\'", @"\\r", @"\\n", @"\\t", nil + }; + + NSString **src = srcStrings, **sub = subStrings; + NSMutableString *mutable = nil; + NSString *result = nil; + + mutable = [string mutableCopy]; + while (*src != nil) + { + [mutable replaceOccurrencesOfString:*src++ + withString:*sub++ + options:0 + range:NSMakeRange(0, [mutable length])]; + } + + if ([mutable length] == [string length]) + { + result = string; + } + else + { + result = [[mutable copy] autorelease]; + } + [mutable release]; + return result; +} + + +- (NSString *) generateGraphViz +{ + NSMutableString *result = nil; + OOUInteger i; + id node = nil; + NSString *desc = nil; + + result = [NSMutableString string]; + + // Header + [result appendString: + @"// OOPriorityQueue partially ordered tree dump\n\n" + "digraph heap\n" + "{\n" + "\tgraph [charset=\"UTF-8\", label=\"OOPriorityQueue heap dump\", labelloc=t, labeljust=l];\n" + "\tnode [shape=record];\n\t\n\t"]; + + // Nodes + for (i = 0; i < _count; ++i) + { + node = _heap[i]; + desc = [node description]; + if ([desc length] > 70) + { + desc = [[desc substringToIndex:64] stringByAppendingString:@"..."]; + } + + [result appendFormat:@"\tnode_%u [label=\" | %@ | \"];\n", i, EscapedString(desc)]; + } + + // Arcs + for (i = 0; PQLeftChild(i) < _count; ++i) + { + [result appendFormat:@"\tnode_%u:f0 -> node_%u:f1;\n", i, PQLeftChild(i)]; + if (PQRightChild(i) < _count) [result appendFormat:@"\tnode_%u:f2 -> node_%u:f1;\n", i, PQRightChild(i)]; + } + + [result appendString:@"}\n"]; + + return result; +} + + +- (void) writeGraphVizToURL:(NSURL *)url +{ + NSString *graphViz = nil; + NSData *data = nil; + + graphViz = [self generateGraphViz]; + data = [graphViz dataUsingEncoding:NSUTF8StringEncoding]; + + if (data != nil) + { + [data writeToURL:url atomically:YES]; + } +} + + +- (void) writeGraphVizToPath:(NSString *)path +{ + [self writeGraphVizToURL:[NSURL fileURLWithPath:path]]; +} + +@end +#endif diff --git a/src/Core/OOProbabilisticTextureManager.h b/src/Core/OOProbabilisticTextureManager.h new file mode 100644 index 00000000..e777fcbc --- /dev/null +++ b/src/Core/OOProbabilisticTextureManager.h @@ -0,0 +1,98 @@ +/* + +OOProbabilisticTextureManager.h + +Manages a set of textures, specified in a property list, with associated +probabilities. To avoid interfering with other PRNG-based code, it uses its +own ranrot state. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOTexture.h" +#import "OOMaths.h" + + +@interface OOProbabilisticTextureManager: NSObject +{ + unsigned _count; + OOTexture **_textures; + float *_prob; + float _probMax; + RANROTSeed _seed; +} + +/* plistName is the name of the property list specifying the actual textures + to use. The plist will be loaded from Config directories and merged. It + should contain an array of dictionaries; each dictionary must have a + "texture" entry specifying the texture file name (in Textures directories) + and an optional "probability" entry (default: 1.0). As a convenience, an + entry may also be a string, in which case probability will be 1.0. + + If no seed is specified, the current seed will be copied. +*/ +- (id)initWithPListName:(NSString *)plistName + options:(uint32_t)options + anisotropy:(GLfloat)anisotropy + lodBias:(GLfloat)lodBias; + +- (id)initWithPListName:(NSString *)plistName + options:(uint32_t)options + anisotropy:(GLfloat)anisotropy + lodBias:(GLfloat)lodBias + seed:(RANROTSeed)seed; + +/* Select a texture, weighted-randomly. +*/ +- (OOTexture *)selectTexture; + +- (unsigned)textureCount; + +- (void)ensureTexturesLoaded; + +- (RANROTSeed)seed; +- (void)setSeed:(RANROTSeed)seed; + +@end diff --git a/src/Core/OOProbabilisticTextureManager.m b/src/Core/OOProbabilisticTextureManager.m new file mode 100644 index 00000000..90788ce1 --- /dev/null +++ b/src/Core/OOProbabilisticTextureManager.m @@ -0,0 +1,232 @@ +/* + +OOProbabilisticTextureManager.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOProbabilisticTextureManager.h" +#import "ResourceManager.h" +#import "OOTexture.h" +#import "OOCollectionExtractors.h" + + +@implementation OOProbabilisticTextureManager + +- (id)initWithPListName:(NSString *)plistName + options:(uint32_t)options + anisotropy:(GLfloat)anisotropy + lodBias:(GLfloat)lodBias +{ + return [self initWithPListName:plistName + options:options + anisotropy:anisotropy + lodBias:lodBias + seed:RANROTGetFullSeed()]; +} + + +- (id)initWithPListName:(NSString *)plistName + options:(uint32_t)options + anisotropy:(GLfloat)anisotropy + lodBias:(GLfloat)lodBias + seed:(RANROTSeed)seed +{ + BOOL OK = YES; + NSArray *config = nil; + unsigned i, count; + id entry = nil; + NSString *name = nil; + float probability; + OOTexture *texture = nil; + + self = [super init]; + if (self == nil) OK = NO; + + if (OK) + { + config = [ResourceManager arrayFromFilesNamed:plistName inFolder:@"Config" andMerge:YES]; + if (config == nil) OK = NO; + } + + if (OK) + { + count = [config count]; + + _textures = malloc(sizeof *_textures * count); + _prob = malloc(sizeof *_prob * count); + + if (_textures == NULL || _prob == NULL) OK = NO; + } + + if (OK) + { + // Go through list and load textures. + for (i = 0; i != count; ++i) + { + entry = [config objectAtIndex:i]; + if ([entry isKindOfClass:[NSDictionary class]]) + { + name = [(NSDictionary *)entry stringForKey:@"texture"]; + probability = [entry floatForKey:@"probability" defaultValue:1.0f]; + } + else if ([entry isKindOfClass:[NSString class]]) + { + name = entry; + probability = 1.0f; + } + else + { + name = nil; + } + + if (name != nil && 0.0f < probability) + { + texture = [OOTexture textureWithName:name + inFolder:@"Textures" + options:options + anisotropy:anisotropy + lodBias:lodBias]; + if (texture != nil) + { + _textures[_count] = [texture retain]; + _prob[_count] = probability + _probMax; + _probMax += probability; + ++_count; + } + } + } + + if (_count == 0) OK = NO; + } + + if (OK) _seed = seed; + + if (!OK) + { + [self release]; + self = nil; + } + + return self; +} + + +- (void)dealloc +{ + unsigned i; + + if (_textures != NULL) + { + for (i = 0; i != _count; ++i) + { + [_textures[i] release]; + } + free(_textures); + } + + if (_prob != NULL) free(_prob); + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{%u textures, cumulative probability=%g}", [self class], self, _count, _probMax]; +} + + +- (OOTexture *)selectTexture +{ + float selection; + RANROTSeed savedSeed; + unsigned i; + + savedSeed = RANROTGetFullSeed(); + RANROTSetFullSeed(_seed); + selection = randf(); + _seed = RANROTGetFullSeed(); + RANROTSetFullSeed(savedSeed); + + selection *= _probMax; + + for (i = 0; i != _count; ++i) + { + if (selection <= _prob[i]) return _textures[i]; + } + + OOLog(@"probabilisticTextureManager.internalFailure", @"%s: overrun! Choosing last texture.", __FUNCTION__); + return _textures[_count - 1]; +} + + +- (unsigned)textureCount +{ + return _count; +} + + +- (void)ensureTexturesLoaded +{ + unsigned i; + + for (i = 0; i != _count; ++i) + { + [_textures[i] ensureFinishedLoading]; + } +} + + +- (RANROTSeed)seed +{ + return _seed; +} + + +- (void)setSeed:(RANROTSeed)seed +{ + _seed = seed; +} + +@end diff --git a/src/Core/OOProbabilitySet.h b/src/Core/OOProbabilitySet.h new file mode 100644 index 00000000..e995e532 --- /dev/null +++ b/src/Core/OOProbabilitySet.h @@ -0,0 +1,99 @@ +/* + +OOProbabilitySet.h + +A collection for selecting objects randomly, with probability weighting. +Probability weights can be 0 - an object may be in the set but not selectable. +Comes in mutable and immutable variants. + +Performance characteristics: + * -randomObject, the primary method, is O(log n) for immutable + OOProbabilitySets and O(n) for mutable ones. + * -containsObject: and -probabilityForObject: are O(n). This could be + optimized, but there's currently no need. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@interface OOProbabilitySet: NSObject + ++ (id) probabilitySet; ++ (id) probabilitySetWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count; ++ (id) probabilitySetWithPropertyListRepresentation:(NSDictionary *)plist; + +- (id) init; +- (id) initWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count; +- (id) initWithPropertyListRepresentation:(NSDictionary *)plist; + +// propertyListRepresentation is only valid if objects are property list objects. +- (NSDictionary *) propertyListRepresentation; + +- (OOUInteger) count; +- (id) randomObject; + +- (float) weightForObject:(id)object; // Returns -1 for unknown objects. +- (float) sumOfWeights; +- (NSArray *) allObjects; + +@end + + +@interface OOProbabilitySet (OOExtendedProbabilitySet) + +- (BOOL) containsObject:(id)object; +- (NSEnumerator *) objectEnumerator; +- (float) probabilityForObject:(id)object; // Returns -1 for unknown objects, or a value from 0 to 1 inclusive for known objects. + +@end + + +@interface OOMutableProbabilitySet: OOProbabilitySet + +- (void) setWeight:(float)weight forObject:(id)object; // Adds object if needed. +- (void) removeObject:(id)object; + +@end diff --git a/src/Core/OOProbabilitySet.m b/src/Core/OOProbabilitySet.m new file mode 100644 index 00000000..3600eed4 --- /dev/null +++ b/src/Core/OOProbabilitySet.m @@ -0,0 +1,1084 @@ +/* + +OOProbabilitySet.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +IMPLEMENTATION NOTES +OOProbabilitySet is implemented as a class cluster with two abstract classes, +two special-case implementations for immutable sets with zero or one object, +and two general implementations (one mutable and one immutable). The general +implementations are the only non-trivial ones. + +The general immutable implementation, OOConcreteProbabilitySet, consists of +two parallel arrays, one of objects and one of cumulative weights. The +"cumulative weight" for an entry is the sum of its weight and the cumulative +weight of the entry to the left (i.e., with a lower index), with the implicit +entry -1 having a cumulative weight of 0. Since weight cannot be negative, +this means that cumulative weights increase to the right (not strictly +increasing, though, since weights may be zero). We can thus find an object +with a given cumulative weight through a binary search. + +OOConcreteMutableProbabilitySet is a naïve implementation using arrays. It +could be optimized, but isn't expected to be used much except for building +sets that will then be immutablized. + +*/ + +#import "OOProbabilitySet.h" +#import "OOFunctionAttributes.h" +#import "OOCollectionExtractors.h" +#import "legacy_random.h" + + +static NSString * const kObjectsKey = @"objects"; +static NSString * const kWeightsKey = @"weights"; + + +@protocol OOProbabilitySetEnumerable + +- (id) privObjectAtIndex:(OOUInteger)index; + +@end + + +@interface OOProbabilitySet (OOPrivate) + +// Designated initializer. This must be used by subclasses, since init is overriden for public use. +- (id) initPriv; + +@end + + +@interface OOEmptyProbabilitySet: OOProbabilitySet + ++ (OOEmptyProbabilitySet *) singleton; + +@end + + +@interface OOSingleObjectProbabilitySet: OOProbabilitySet +{ + id _object; + float _weight; +} + +- (id) initWithObject:(id)object weight:(float)weight; + +@end + + +@interface OOConcreteProbabilitySet: OOProbabilitySet +{ + OOUInteger _count; + id *_objects; + float *_cumulativeWeights; // Each cumulative weight is weight of object at this index + weight of all objects to left. + float _sumOfWeights; +} +@end + + +@interface OOConcreteMutableProbabilitySet: OOMutableProbabilitySet +{ + NSMutableArray *_objects; + NSMutableArray *_weights; + float _sumOfWeights; +} + +- (id) initPrivWithObjectArray:(NSMutableArray *)objects weightsArray:(NSMutableArray *)weights sum:(float)sumOfWeights; + +@end + + +@interface OOProbabilitySetEnumerator: NSEnumerator +{ + id _enumerable; + OOUInteger _index; +} + +- (id) initWithEnumerable:(id)enumerable; + +@end + + +static void ThrowAbstractionViolationException(id obj) GCC_ATTR((noreturn)); + + +@implementation OOProbabilitySet + +// Abstract class just tosses allocations over to concrete class, and throws exception if you try to use it directly. + ++ (id) probabilitySet +{ + return [OOEmptyProbabilitySet singleton]; +} + + ++ (id) probabilitySetWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count +{ + return [[[self alloc] initWithObjects:objects weights:weights count:count] autorelease]; +} + + ++ (id) probabilitySetWithPropertyListRepresentation:(NSDictionary *)plist +{ + return [[[self alloc] initWithPropertyListRepresentation:plist] autorelease]; +} + + +- (id) init +{ + [self release]; + return [OOEmptyProbabilitySet singleton]; +} + + +- (id) initWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count +{ + NSZone *zone = [self zone]; + [self release]; + self = nil; + + // Zero objects: return empty-set singleton. + if (count == 0) return [OOEmptyProbabilitySet singleton]; + + // If count is not zero and one of the paramters is nil, we've got us a programming error. + if (objects == NULL || weights == NULL) + { + [NSException raise:NSInvalidArgumentException format:@"Attempt to create %@ with non-zero count but nil objects or weights.", @"OOProbabilitySet"]; + } + + // Single object: simple one-object set. Expected to be quite common. + if (count == 1) return [[OOSingleObjectProbabilitySet allocWithZone:zone] initWithObject:objects[0] weight:weights[0]]; + + // Otherwise, use general implementation. + return [[OOConcreteProbabilitySet allocWithZone:zone] initWithObjects:objects weights:weights count:count]; +} + + +- (id) initWithPropertyListRepresentation:(NSDictionary *)plist +{ + NSArray *objects = nil; + NSArray *weights = nil; + OOUInteger i = 0, count = 0; + id *rawObjects = NULL; + float *rawWeights = NULL; + + objects = [plist arrayForKey:kObjectsKey]; + weights = [plist arrayForKey:kWeightsKey]; + + // Validate + if (objects == nil || weights == nil) return nil; + count = [objects count]; + if (count != [weights count]) return nil; + + // Extract contents. + rawObjects = malloc(sizeof *rawObjects * count); + rawWeights = malloc(sizeof *rawWeights * count); + + if (rawObjects != NULL || rawWeights != NULL) + { + // Extract objects. + [objects getObjects:rawObjects]; + + // Extract and convert weights. + for (i = 0; i < count; ++i) + { + rawWeights[i] = fmaxf([weights floatAtIndex:i], 0.0f); + } + + self = [self initWithObjects:rawObjects weights:rawWeights count:count]; + } + else + { + self = nil; + } + + // Clean up. + free(rawObjects); + free(rawWeights); + + return self; +} + + +- (id) initPriv +{ + return [super init]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"count=%lu", [self count]]; +} + + +- (NSDictionary *) propertyListRepresentation +{ + ThrowAbstractionViolationException(self); +} + +- (id) randomObject +{ + ThrowAbstractionViolationException(self); +} + + +- (float) weightForObject:(id)object +{ + ThrowAbstractionViolationException(self); +} + + +- (float) sumOfWeights +{ + ThrowAbstractionViolationException(self); +} + + +- (OOUInteger) count +{ + ThrowAbstractionViolationException(self); +} + + +- (NSArray *) allObjects +{ + ThrowAbstractionViolationException(self); +} + + +- (id) copyWithZone:(NSZone *)zone +{ + if (zone == [self zone]) + { + return [self retain]; + } + else + { + return [[OOProbabilitySet allocWithZone:zone] initWithPropertyListRepresentation:[self propertyListRepresentation]]; + } +} + + +- (id) mutableCopyWithZone:(NSZone *)zone +{ + return [[OOMutableProbabilitySet allocWithZone:zone] initWithPropertyListRepresentation:[self propertyListRepresentation]]; +} + +@end + + +@implementation OOProbabilitySet (OOExtendedProbabilitySet) + +- (BOOL) containsObject:(id)object +{ + return [self weightForObject:object] >= 0.0f; +} + + +- (NSEnumerator *) objectEnumerator +{ + return [[self allObjects] objectEnumerator]; +} + + +- (float) probabilityForObject:(id)object +{ + float weight = [self weightForObject:object]; + if (weight > 0) weight /= [self sumOfWeights]; + + return weight; +} + +@end + + +static OOEmptyProbabilitySet *sOOEmptyProbabilitySetSingleton = nil; + +@implementation OOEmptyProbabilitySet: OOProbabilitySet + ++ (OOEmptyProbabilitySet *) singleton +{ + if (sOOEmptyProbabilitySetSingleton == nil) + { + [[self alloc] init]; + } + + return sOOEmptyProbabilitySetSingleton; +} + + +- (NSDictionary *) propertyListRepresentation +{ + NSArray *empty = [NSArray array]; + return [NSDictionary dictionaryWithObjectsAndKeys:empty, kObjectsKey, empty, kWeightsKey, nil]; +} + + +- (id) randomObject +{ + return nil; +} + + +- (float) weightForObject:(id)object +{ + return -1.0f; +} + + +- (float) sumOfWeights +{ + return 0.0f; +} + + +- (OOUInteger) count +{ + return 0; +} + + +- (NSArray *) allObjects +{ + return [NSArray array]; +} + + +- (id) mutableCopyWithZone:(NSZone *)zone +{ + // A mutable copy of an empty probability set is equivalent to a new empty mutable probability set. + return [[OOConcreteMutableProbabilitySet allocWithZone:zone] initPriv]; +} + +@end + + +@implementation OOEmptyProbabilitySet (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +singleton above. + + NOTE: assumes single-threaded access. +*/ + ++ (id) allocWithZone:(NSZone *)inZone +{ + if (sOOEmptyProbabilitySetSingleton == nil) + { + sOOEmptyProbabilitySetSingleton = [super allocWithZone:inZone]; + return sOOEmptyProbabilitySetSingleton; + } + return nil; +} + + +- (id) copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id) retain +{ + return self; +} + + +- (OOUInteger) retainCount +{ + return UINT_MAX; +} + + +- (void) release +{} + + +- (id) autorelease +{ + return self; +} + +@end + + +@implementation OOSingleObjectProbabilitySet: OOProbabilitySet + +- (id) initWithObject:(id)object weight:(float)weight +{ + if (object == nil) + { + [self release]; + return nil; + } + + if ((self = [super initPriv])) + { + _object = [object retain]; + _weight = fmaxf(weight, 0.0f); + } + + return self; +} + + +- (void) dealloc +{ + [_object release]; + + [super dealloc]; +} + + +- (NSDictionary *) propertyListRepresentation +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSArray arrayWithObject:_object], kObjectsKey, + [NSArray arrayWithObject:[NSNumber numberWithFloat:_weight]], kWeightsKey, + nil]; +} + + +- (id) randomObject +{ + return _object; +} + + +- (float) weightForObject:(id)object +{ + if ([_object isEqual:object]) return _weight; + else return -1.0f; +} + + +- (float) sumOfWeights +{ + return _weight; +} + + +- (OOUInteger) count +{ + return 1; +} + + +- (NSArray *) allObjects +{ + return [NSArray arrayWithObject:_object]; +} + + +- (id) mutableCopyWithZone:(NSZone *)zone +{ + return [[OOConcreteMutableProbabilitySet allocWithZone:zone] initWithObjects:&_object weights:&_weight count:1]; +} + +@end + + +@implementation OOConcreteProbabilitySet + +- (id) initWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count +{ + OOUInteger i = 0; + float cuWeight = 0.0f; + + assert(count > 1 && objects != NULL && weights != NULL); + + if ((self = [super initPriv])) + { + // Allocate arrays + _objects = malloc(sizeof *objects * count); + _cumulativeWeights = malloc(sizeof *_cumulativeWeights * count); + if (_objects == NULL || _cumulativeWeights == NULL) + { + [self release]; + return nil; + } + + // Fill in arrays, retain objects, add up weights. + for (i = 0; i != count; ++i) + { + _objects[i] = [objects[i] retain]; + cuWeight += weights[i]; + _cumulativeWeights[i] = cuWeight; + } + _count = count; + _sumOfWeights = cuWeight; + } + + return self; +} + + +- (void) dealloc +{ + OOUInteger i = 0; + + if (_objects != NULL) + { + for (i = 0; i < _count; ++i) + { + [_objects[i] release]; + } + free(_objects); + _objects = NULL; + } + + if (_cumulativeWeights != NULL) + { + free(_cumulativeWeights); + _cumulativeWeights = NULL; + } + + [super dealloc]; +} + + +- (NSDictionary *) propertyListRepresentation +{ + NSArray *objects = nil; + NSMutableArray *weights = nil; + float cuWeight = 0.0f, sum = 0.0f; + OOUInteger i = 0; + + objects = [NSArray arrayWithObjects:_objects count:_count]; + weights = [NSMutableArray arrayWithCapacity:_count]; + for (i = 0; i < _count; ++i) + { + cuWeight = _cumulativeWeights[i]; + [weights addFloat:cuWeight - sum]; + sum = cuWeight; + } + + return [NSDictionary dictionaryWithObjectsAndKeys: + objects, kObjectsKey, + [[weights copy] autorelease], kWeightsKey, + nil]; +} + +- (OOUInteger) count +{ + return _count; +} + + +- (id) privObjectForWeight:(float)target +{ + /* Select an object at random. This is a binary search in the cumulative + weights array. Since weights of zero are allowed, there may be several + objects with the same cumulative weight, in which case we select the + leftmost, i.e. the one where the delta is non-zero. + */ + + OOUInteger low = 0, high = _count - 1, idx = 0; + float weight = 0.0f; + + while (low < high) + { + idx = (low + high) / 2; + weight = _cumulativeWeights[idx]; + if (weight > target) + { + if (EXPECT_NOT(idx == 0)) break; + high = idx - 1; + } + else if (weight < target) low = idx + 1; + else break; + } + + if (weight > target) + { + while (idx > 0 && _cumulativeWeights[idx - 1] >= target) --idx; + } + else + { + while (idx < (_count - 1) && _cumulativeWeights[idx] < target) ++idx; + } + + assert(idx < _count); + id result = _objects[idx]; + return result; +} + + +- (id) randomObject +{ + if (_sumOfWeights <= 0.0f) return nil; + return [self privObjectForWeight:randf() * _sumOfWeights]; +} + + +- (float) weightForObject:(id)object +{ + OOUInteger i; + + // Can't have nil in collection. + if (object == nil) return -1.0f; + + // Perform linear search, then get weight by subtracting cumulative weight from cumulative weight to left. + for (i = 0; i < _count; ++i) + { + if ([_objects[i] isEqual:object]) + { + float leftWeight = (i != 0) ? _cumulativeWeights[i - 1] : 0.0f; + return _cumulativeWeights[i] - leftWeight; + } + } + + // If we got here, object not found. + return -1.0f; +} + + +- (float) sumOfWeights +{ + return _sumOfWeights; +} + + +- (NSArray *) allObjects +{ + return [NSArray arrayWithObjects:_objects count:_count]; +} + + +- (NSEnumerator *) objectEnumerator +{ + return [[[OOProbabilitySetEnumerator alloc] initWithEnumerable:self] autorelease]; +} + + +- (id) privObjectAtIndex:(OOUInteger)index +{ + return (index < _count) ? _objects[index] : nil; +} + + +- (id) mutableCopyWithZone:(NSZone *)zone +{ + id result = nil; + float *weights = NULL; + OOUInteger i = 0; + float weight = 0.0f, sum = 0.0f; + + // Convert cumulative weights to "plain" weights. + weights = malloc(sizeof *weights * _count); + if (weights == NULL) return nil; + + for (i = 0; i < _count; ++i) + { + weight = _cumulativeWeights[i]; + weights[i] = weight - sum; + sum += weights[i]; + } + + result = [[OOConcreteMutableProbabilitySet allocWithZone:zone] initWithObjects:_objects weights:weights count:_count]; + free(weights); + + return result; +} + +@end + + +@implementation OOMutableProbabilitySet + ++ (id) probabilitySet +{ + return [[[OOConcreteMutableProbabilitySet alloc] initPriv] autorelease]; +} + + +- (id) init +{ + NSZone *zone = [self zone]; + [self release]; + return [[OOConcreteMutableProbabilitySet allocWithZone:zone] initPriv]; +} + + +- (id) initWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count +{ + NSZone *zone = [self zone]; + [self release]; + return [[OOConcreteMutableProbabilitySet allocWithZone:zone] initWithObjects:objects weights:weights count:count]; +} + + +- (id) initWithPropertyListRepresentation:(NSDictionary *)plist +{ + NSZone *zone = [self zone]; + [self release]; + return [[OOConcreteMutableProbabilitySet allocWithZone:zone] initWithPropertyListRepresentation:plist]; +} + + +- (id) copyWithZone:(NSZone *)zone +{ + return [[OOProbabilitySet allocWithZone:zone] initWithPropertyListRepresentation:[self propertyListRepresentation]]; +} + + +- (void) setWeight:(float)weight forObject:(id)object +{ + ThrowAbstractionViolationException(self); +} + + +- (void) removeObject:(id)object +{ + ThrowAbstractionViolationException(self); +} + +@end + + +@implementation OOConcreteMutableProbabilitySet + +- (id) initPriv +{ + if ((self = [super initPriv])) + { + _objects = [[NSMutableArray alloc] init]; + _weights = [[NSMutableArray alloc] init]; + } + + return self; +} + + +// For internal use by mutableCopy +- (id) initPrivWithObjectArray:(NSMutableArray *)objects weightsArray:(NSMutableArray *)weights sum:(float)sumOfWeights +{ + assert(objects != nil && weights != nil && [objects count] == [weights count] && sumOfWeights >= 0.0f); + + if ((self = [super initPriv])) + { + _objects = objects; + _weights = weights; + _sumOfWeights = sumOfWeights; + } + + return self; +} + + +- (id) initWithObjects:(id *)objects weights:(float *)weights count:(OOUInteger)count +{ + OOUInteger i = 0; + + // Validate parameters. + if (count != 0 && (objects == NULL || weights == NULL)) + { + [self release]; + [NSException raise:NSInvalidArgumentException format:@"Attempt to create %@ with non-zero count but nil objects or weights.", @"OOMutableProbabilitySet"]; + } + + // Set up & go. + if ((self == [self initPriv])) + { + for (i = 0; i != count; ++i) + { + [self setWeight:fmaxf(weights[i], 0.0f) forObject:objects[i]]; + } + } + + return self; +} + + +- (id) initWithPropertyListRepresentation:(NSDictionary *)plist +{ + BOOL OK = YES; + NSArray *objects = nil; + NSArray *weights = nil; + OOUInteger i = 0, count = 0; + + if (!(self = [super initPriv])) OK = NO; + + if (OK) + { + objects = [plist arrayForKey:kObjectsKey]; + weights = [plist arrayForKey:kWeightsKey]; + + // Validate + if (objects == nil || weights == nil) OK = NO; + count = [objects count]; + if (count != [weights count]) OK = NO; + } + + if (OK) + { + for (i = 0; i < count; ++i) + { + [self setWeight:[weights floatAtIndex:i] forObject:[objects objectAtIndex:i]]; + } + } + + if (!OK) + { + [self release]; + self = nil; + } + + return self; +} + + +- (void) dealloc +{ + [_objects release]; + [_weights release]; + + [super dealloc]; +} + + +- (NSDictionary *) propertyListRepresentation +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + _objects, kObjectsKey, + _weights, kWeightsKey, + nil]; +} + + +- (OOUInteger) count +{ + return [_objects count]; +} + + +- (id) randomObject +{ + float target = 0.0f, sum = 0.0f, sumOfWeights; + OOUInteger i = 0, count = 0; + + sumOfWeights = [self sumOfWeights]; + target = randf() * sumOfWeights; + count = [_objects count]; + if (count == 0 || sumOfWeights <= 0.0f) return nil; + + for (i = 0; i < count; ++i) + { + sum += [_weights floatAtIndex:i]; + if (sum >= target) return [_objects objectAtIndex:i]; + } + + OOLog(@"probabilitySet.broken", @"%s fell off end, returning first object. Nominal sum = %f, target = %f, actual sum = %f, count = %u. This is an internal error, please report it.", __PRETTY_FUNCTION__, sumOfWeights, target, sum, count); + return [_objects objectAtIndex:0]; +} + + +- (float) weightForObject:(id)object +{ + float result = -1.0f; + + if (object != nil) + { + OOUInteger index = [_objects indexOfObject:object]; + if (index != NSNotFound) + { + result = [_weights floatAtIndex:index]; + if (index != 0) result -= [_weights floatAtIndex:index - 1]; + } + } + return result; +} + + +- (float) sumOfWeights +{ + if (_sumOfWeights < 0.0f) + { + OOUInteger i, count; + count = [self count]; + + _sumOfWeights = 0.0f; + for (i = 0; i < count; ++i) + { + _sumOfWeights += [_weights floatAtIndex:i]; + } + } + return _sumOfWeights; +} + + +- (NSArray *) allObjects +{ + return [[_objects copy] autorelease]; +} + + +- (NSEnumerator *) objectEnumerator +{ + return [_objects objectEnumerator]; +} + + +- (void) setWeight:(float)weight forObject:(id)object +{ + if (object == nil) return; + + weight = fmaxf(weight, 0.0f); + OOUInteger index = [_objects indexOfObject:object]; + if (index == NSNotFound) + { + [_objects addObject:object]; + [_weights addFloat:weight]; + _sumOfWeights += weight; + } + else + { + _sumOfWeights = -1.0f; // Simply subtracting the relevant weight doesn't work if the weight is large, due to floating-point precision issues. + [_weights replaceObjectAtIndex:index withObject:[NSNumber numberWithFloat:weight]]; + } +} + + +- (void) removeObject:(id)object +{ + if (object == nil) return; + + OOUInteger index = [_objects indexOfObject:object]; + if (index != NSNotFound) + { + [_objects removeObjectAtIndex:index]; + _sumOfWeights = -1.0f; // Simply subtracting the relevant weight doesn't work if the weight is large, due to floating-point precision issues. + [_weights removeObjectAtIndex:index]; + } +} + + +- (id) copyWithZone:(NSZone *)zone +{ + id result = nil; + id *objects = NULL; + float *weights = NULL; + OOUInteger i = 0, count = 0; + + count = [_objects count]; + if (EXPECT_NOT(count == 0)) return [OOEmptyProbabilitySet singleton]; + + objects = malloc(sizeof *objects * count); + weights = malloc(sizeof *weights * count); + if (objects != NULL && weights != NULL) + { + [_objects getObjects:objects]; + + for (i = 0; i < count; ++i) + { + weights[i] = [_weights floatAtIndex:i]; + } + + result = [[OOProbabilitySet probabilitySetWithObjects:objects weights:weights count:count] retain]; + } + + if (objects != NULL) free(objects); + if (objects != NULL) free(weights); + + return result; +} + + +- (id) mutableCopyWithZone:(NSZone *)zone +{ + return [[OOConcreteMutableProbabilitySet alloc] initPrivWithObjectArray:[_objects mutableCopyWithZone:zone] + weightsArray:[_weights mutableCopyWithZone:zone] + sum:_sumOfWeights]; +} + +@end + + +@implementation OOProbabilitySetEnumerator + +- (id) initWithEnumerable:(id)enumerable +{ + if ((self = [super init])) + { + _enumerable = [enumerable retain]; + } + + return self; +} + + +- (void) dealloc +{ + [_enumerable release]; + + [super dealloc]; +} + + +- (id) nextObject +{ + if (_index < [_enumerable count]) + { + return [_enumerable privObjectAtIndex:_index++]; + } + else + { + [_enumerable release]; + _enumerable = nil; + return nil; + } +} + +@end + + +static void ThrowAbstractionViolationException(id obj) +{ + [NSException raise:NSGenericException format:@"Attempt to use abstract class %@ - this indicates an incorrect initialization.", [obj class]]; + abort(); // unreachable +} diff --git a/src/Core/OOQuaternion.h b/src/Core/OOQuaternion.h new file mode 100644 index 00000000..ab065829 --- /dev/null +++ b/src/Core/OOQuaternion.h @@ -0,0 +1,191 @@ +/* + +OOQuaternion.h + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOQuaternion.h directly; include OOMaths.h. +#else + + +typedef struct Quaternion +{ + GLfloat w; + GLfloat x; + GLfloat y; + GLfloat z; +} Quaternion; + + +extern const Quaternion kIdentityQuaternion; // 1, 0, 0, 0 +extern const Quaternion kZeroQuaternion; // 0, 0, 0, 0 + + +/* Construct quaternion */ +OOINLINE Quaternion make_quaternion(GLfloat qw, GLfloat qx, GLfloat qy, GLfloat qz) INLINE_CONST_FUNC; + +/* Comparison of quaternions */ +OOINLINE GLboolean quaternion_equal(Quaternion a, Quaternion b) INLINE_CONST_FUNC; + +/* Multiply quaternions */ +Quaternion quaternion_multiply(Quaternion q1, Quaternion q2) CONST_FUNC; + +/* Negation, or additive inverse -- negate all components */ +OOINLINE Quaternion quaternion_negate(Quaternion q) INLINE_CONST_FUNC; + +/* Conjugate, or spacial inverse -- negate x, y, z components */ +OOINLINE Quaternion quaternion_conjugate(Quaternion q) INLINE_CONST_FUNC; + +/* Set quaternion to random unit quaternion */ +void quaternion_set_random(Quaternion *quat) NONNULL_FUNC; +OOINLINE Quaternion OORandomQuaternion(void) ALWAYS_INLINE_FUNC; + +/* Build quaternion representing a rotation around a given axis */ +OOINLINE void quaternion_set_rotate_about_axis(Quaternion *quat, Vector axis, GLfloat angle) NONNULL_FUNC; + +/* Inner product of two quaternions */ +OOINLINE GLfloat quaternion_dot_product(Quaternion q1, Quaternion q2) CONST_FUNC; + +/* Create basis vectors from a quaternion. */ +Vector vector_forward_from_quaternion(Quaternion quat) CONST_FUNC; +Vector vector_up_from_quaternion(Quaternion quat) CONST_FUNC; +Vector vector_right_from_quaternion(Quaternion quat) CONST_FUNC; + +void basis_vectors_from_quaternion(Quaternion quat, Vector *outRight, Vector *outUp, Vector *outForward); + +/* produce a quaternion representing an angle between two vectors. Assumes the vectors are normalized. */ +Quaternion quaternion_rotation_between(Vector v0, Vector v1) CONST_FUNC; + +/* produce a quaternion representing an angle between two vectors with a maximum arc */ +Quaternion quaternion_limited_rotation_between(Vector v0, Vector v1, float maxArc) CONST_FUNC; + +/* Rotate a quaternion about a fixed axis. */ +void quaternion_rotate_about_x(Quaternion *quat, GLfloat angle) NONNULL_FUNC; +void quaternion_rotate_about_y(Quaternion *quat, GLfloat angle) NONNULL_FUNC; +void quaternion_rotate_about_z(Quaternion *quat, GLfloat angle) NONNULL_FUNC; +void quaternion_rotate_about_axis(Quaternion *quat, Vector axis, GLfloat angle) NONNULL_FUNC; + +/* Normalize quaternion */ +OOINLINE void quaternion_normalize(Quaternion *quat) NONNULL_FUNC ALWAYS_INLINE_FUNC; +OOINLINE void fast_quaternion_normalize(Quaternion *quat) NONNULL_FUNC ALWAYS_INLINE_FUNC; + +#ifdef __OBJC__ +NSString *QuaternionDescription(Quaternion quaternion); // @"(w + xi + yj + zk)" +#endif + + +Vector quaternion_rotate_vector(Quaternion q, Vector vector) CONST_FUNC; + + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE Quaternion make_quaternion(GLfloat qw, GLfloat qx, GLfloat qy, GLfloat qz) +{ + Quaternion result; + result.w = qw; + result.x = qx; + result.y = qy; + result.z = qz; + return result; +} + + +OOINLINE GLboolean quaternion_equal(Quaternion a, Quaternion b) +{ + return a.w == b.w && a.x == b.x && a.y == b.y && a.z == b.z; +} + + +OOINLINE Quaternion quaternion_negate(Quaternion q) +{ + return make_quaternion(-q.w, -q.x, -q.y, -q.z); +} + + +OOINLINE Quaternion quaternion_conjugate(Quaternion q) +{ + return make_quaternion(q.w, -q.x, -q.y, -q.z); +} + + +OOINLINE void quaternion_set_rotate_about_axis(Quaternion *quat, Vector axis, GLfloat angle) +{ + GLfloat a = angle * 0.5f; + GLfloat scale = sinf(a); + + quat->w = cosf(a); + quat->x = axis.x * scale; + quat->y = axis.y * scale; + quat->z = axis.z * scale; +} + + +OOINLINE GLfloat quaternion_dot_product(Quaternion q1, Quaternion q2) +{ + return q1.w*q2.w + q1.x*q2.x + q1.y*q2.y + q1.z*q2.z; +} + + +OOINLINE void quaternion_normalize(Quaternion *quat) +{ + GLfloat w = quat->w; + GLfloat x = quat->x; + GLfloat y = quat->y; + GLfloat z = quat->z; + + GLfloat lv = OOInvSqrtf(w*w + x*x + y*y + z*z); + + quat->w = lv * w; + quat->x = lv * x; + quat->y = lv * y; + quat->z = lv * z; +} + + +OOINLINE void fast_quaternion_normalize(Quaternion *quat) +{ + GLfloat w = quat->w; + GLfloat x = quat->x; + GLfloat y = quat->y; + GLfloat z = quat->z; + + GLfloat lv = OOFastInvSqrtf(w*w + x*x + y*y + z*z); + + quat->w = lv * w; + quat->x = lv * x; + quat->y = lv * y; + quat->z = lv * z; +} + + +OOINLINE Quaternion OORandomQuaternion(void) +{ + Quaternion q; + quaternion_set_random(&q); + return q; +} + +#endif /* INCLUDED_OOMATHS_h */ diff --git a/src/Core/OOQuaternion.m b/src/Core/OOQuaternion.m new file mode 100644 index 00000000..c24ab3aa --- /dev/null +++ b/src/Core/OOQuaternion.m @@ -0,0 +1,376 @@ +/* + +OOQuaternion.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOMaths.h" +#import "OOLogging.h" + + +const Quaternion kIdentityQuaternion = { 1.0f, 0.0f, 0.0f, 0.0f }; +const Quaternion kZeroQuaternion = { 0.0f, 0.0f, 0.0f, 0.0f }; + + +static NSString * const kOOLogMathsZeroRotation = @"maths.quaternion.zeroRotation"; +static NSString * const kOOLogMathsQuatLimitedRotationDebug = @"maths.quaternion.limitedRotation.debug"; + + +Quaternion quaternion_multiply(Quaternion q1, Quaternion q2) +{ + Quaternion result; + result.w = q1.w * q2.w - q1.x * q2.x - q1.y * q2.y - q1.z * q2.z; + result.x = q1.w * q2.x + q1.x * q2.w + q1.y * q2.z - q1.z * q2.y; + result.y = q1.w * q2.y + q1.y * q2.w + q1.z * q2.x - q1.x * q2.z; + result.z = q1.w * q2.z + q1.z * q2.w + q1.x * q2.y - q1.y * q2.x; + return result; +} + + +// NOTE: this is broken - its distribution is weighted towards corners of the hypercube. Probably doesn't matter, though. +void quaternion_set_random(Quaternion *quat) +{ + quat->w = (GLfloat)(Ranrot() % 1024) - 511.5f; // -511.5 to +511.5 + quat->x = (GLfloat)(Ranrot() % 1024) - 511.5f; // -511.5 to +511.5 + quat->y = (GLfloat)(Ranrot() % 1024) - 511.5f; // -511.5 to +511.5 + quat->z = (GLfloat)(Ranrot() % 1024) - 511.5f; // -511.5 to +511.5 + quaternion_normalize(quat); +} + + +Vector vector_forward_from_quaternion(Quaternion quat) +{ + GLfloat w, wy, wx; + GLfloat x, xz, xx; + GLfloat y, yz, yy; + GLfloat z, zz; + Vector res; + + w = quat.w; + z = quat.z; + y = quat.y; + x = quat.x; + + xx = 2.0f * x; yy = 2.0f * y; zz = 2.0f * z; + wx = w * xx; wy = w * yy; + xx = x * xx; xz = x * zz; + yy = y * yy; yz = y * zz; + + res.x = xz - wy; + res.y = yz + wx; + res.z = 1.0f - xx - yy; + + if (res.x||res.y||res.z) return vector_normal(res); + else return make_vector(0.0f, 0.0f, 1.0f); +} + + +Vector vector_up_from_quaternion(Quaternion quat) +{ + GLfloat w, wz, wx; + GLfloat x, xy, xx; + GLfloat y, yz, yy; + GLfloat z, zz; + Vector res; + + w = quat.w; + z = quat.z; + y = quat.y; + x = quat.x; + + xx = 2.0f * x; yy = 2.0f * y; zz = 2.0f * z; + wx = w * xx; wz = w * zz; + xx = x * xx; xy = x * yy; + yz = y * zz; + zz = z * zz; + + res.x = xy + wz; + res.y = 1.0f - xx - zz; + res.z = yz - wx; + + if (res.x||res.y||res.z) return vector_normal(res); + else return make_vector(0.0f, 1.0f, 0.0f); +} + + +Vector vector_right_from_quaternion(Quaternion quat) +{ + GLfloat w, wz, wy; + GLfloat x, xz, xy; + GLfloat y, yy; + GLfloat z, zz; + Vector res; + + w = quat.w; + z = quat.z; + y = quat.y; + x = quat.x; + + yy = 2.0f * y; zz = 2.0f * z; + wy = w * yy; wz = w * zz; + xy = x * yy; xz = x * zz; + yy = y * yy; + zz = z * zz; + + res.x = 1.0f - yy - zz; + res.y = xy - wz; + res.z = xz + wy; + + if (res.x||res.y||res.z) return vector_normal(res); + else return make_vector(1.0f, 0.0f, 0.0f); +} + + +void basis_vectors_from_quaternion(Quaternion quat, Vector *outRight, Vector *outUp, Vector *outForward) +{ + GLfloat w, wz, wy, wx; + GLfloat x, xz, xy, xx; + GLfloat y, yz, yy; + GLfloat z, zz; + + w = quat.w; + z = quat.z; + y = quat.y; + x = quat.x; + + xx = 2.0f * x; yy = 2.0f * y; zz = 2.0f * z; + wx = w * xx; wy = w * yy; wz = w * zz; + xx = x * xx; xy = x * yy; xz = x * zz; + yy = y * yy; yz = y * zz; + zz = z * zz; + + if (outRight != NULL) + { + outRight->x = 1.0f - yy - zz; + outRight->y = xy - wz; + outRight->z = xz + wy; + + if (outRight->x || outRight->y || outRight->z) *outRight = vector_normal(*outRight); + else *outRight = make_vector(1.0f, 0.0f, 0.0f); + } + + if (outUp != NULL) + { + outUp->x = xy + wz; + outUp->y = 1.0f - xx - zz; + outUp->z = yz - wx; + + if (outUp->x || outUp->y || outUp->z) *outUp = vector_normal(*outUp); + else *outUp = make_vector(0.0f, 1.0f, 0.0f); + } + + if (outForward != NULL) + { + outForward->x = xz - wy; + outForward->y = yz + wx; + outForward->z = 1.0f - xx - yy; + + if (outForward->x || outForward->y || outForward->z) *outForward = vector_normal(*outForward); + else *outForward = make_vector(0.0f, 0.0f, 1.0f); + } +} + + +Quaternion quaternion_rotation_between(Vector v0, Vector v1) +{ + Quaternion q; + GLfloat s = sqrtf((1.0f + v0.x * v1.x + v0.y * v1.y + v0.z * v1.z) * 2.0f); + if (EXPECT(s)) + { + GLfloat is = 1.0f / s; + q.x = (v0.y * v1.z - v0.z * v1.y) * is; + q.y = (v0.z * v1.x - v0.x * v1.z) * is; + q.z = (v0.x * v1.y - v0.y * v1.x) * is; + q.w = s * 0.5f; + } + else + { + // Is this actually a problem? + q = kIdentityQuaternion; + // OOLog(kOOLogMathsZeroRotation, @"***** minarc s == zero!"); + } + return q; +} + + +Quaternion quaternion_limited_rotation_between(Vector v0, Vector v1, float maxArc) // vectors both normalised +{ + Quaternion q; + GLfloat min_s = 2.0f * cosf(0.5f * maxArc); + GLfloat s = sqrtf((1.0f + v0.x * v1.x + v0.y * v1.y + v0.z * v1.z) * 2.0f); + if (EXPECT(s != 0)) + { + if (s < min_s) // larger angle => smaller cos + { + GLfloat a = maxArc * 0.5f; + GLfloat w = cosf(a); + GLfloat scale = sinf(a); + OOLog(kOOLogMathsQuatLimitedRotationDebug, @"DEBUG using maxArc %.5f \tw %.5f \tscale %.5f", maxArc, w, scale); + q.x = (v0.y * v1.z - v0.z * v1.y) * scale; + q.y = (v0.z * v1.x - v0.x * v1.z) * scale; + q.z = (v0.x * v1.y - v0.y * v1.x) * scale; + q.w = w; + } + else + { + GLfloat is = 1.0f / s; + q.x = (v0.y * v1.z - v0.z * v1.y) * is; + q.y = (v0.z * v1.x - v0.x * v1.z) * is; + q.z = (v0.x * v1.y - v0.y * v1.x) * is; + q.w = s * 0.5f; + } + } + else + { + // Is this actually a problem? + q = kIdentityQuaternion; + // OOLog(kOOLogMathsZeroRotation, @"***** minarc s == zero!"); + } + return q; +} + + +void quaternion_rotate_about_x(Quaternion *quat, GLfloat angle) +{ + Quaternion result; + GLfloat a = angle * 0.5; + GLfloat w = cos(a); + GLfloat scale = sin(a); + + result.w = quat->w * w - quat->x * scale; + result.x = quat->w * scale + quat->x * w; + result.y = quat->y * w + quat->z * scale; + result.z = quat->z * w - quat->y * scale; + + quat->w = result.w; + quat->x = result.x; + quat->y = result.y; + quat->z = result.z; +} + + +void quaternion_rotate_about_y(Quaternion *quat, GLfloat angle) +{ + Quaternion result; + GLfloat a = angle * 0.5f; + GLfloat w = cosf(a); + GLfloat scale = sinf(a); + + result.w = quat->w * w - quat->y * scale; + result.x = quat->x * w - quat->z * scale; + result.y = quat->w * scale + quat->y * w; + result.z = quat->z * w + quat->x * scale; + + quat->w = result.w; + quat->x = result.x; + quat->y = result.y; + quat->z = result.z; +} + + +void quaternion_rotate_about_z(Quaternion *quat, GLfloat angle) +{ + Quaternion result; + GLfloat a = angle * 0.5f; + GLfloat w = cosf(a); + GLfloat scale = sinf(a); + + result.w = quat->w * w - quat->z * scale; + result.x = quat->x * w + quat->y * scale; + result.y = quat->y * w - quat->x * scale; + result.z = quat->w * scale + quat->z * w; + + quat->w = result.w; + quat->x = result.x; + quat->y = result.y; + quat->z = result.z; +} + + +void quaternion_rotate_about_axis(Quaternion *quat, Vector axis, GLfloat angle) +{ + Quaternion q2 /*, result */; + GLfloat a = angle * 0.5f; + GLfloat w = cosf(a); + GLfloat scale = sinf(a); + + q2.w = w; + q2.x = axis.x * scale; + q2.y = axis.y * scale; + q2.z = axis.z * scale; + + *quat = quaternion_multiply(*quat, q2); +} + + +NSString *QuaternionDescription(Quaternion quaternion) +{ + float x, y, z; + char xs, ys, zs; + + x = fabsf(quaternion.x); + y = fabsf(quaternion.y); + z = fabsf(quaternion.z); + + xs = (quaternion.x > 0) ? '+' : '-'; + ys = (quaternion.y > 0) ? '+' : '-'; + zs = (quaternion.z > 0) ? '+' : '-'; + + return [NSString stringWithFormat:@"(%g %c %gi %c %gj %c %gk)", quaternion.w, xs, x, ys, y, zs, z]; +} + + +#if 0 +Vector quaternion_rotate_vector(Quaternion q, Vector vector) +{ + Quaternion v, cq, qv; + + /* + Quaternion rotation formula: + r(q, v) = q * v * q^-1, where q^-1 is the spacial inverse of q. + */ + + v = make_quaternion(0, vector.x, vector.y, vector.z); + + cq = quaternion_conjugate(q); + qv = quaternion_multiply(q, v); + v = quaternion_multiply(qv, cq); + + return make_vector(v.x, v.y, v.z); // w will be zero +} +#else +// Same as above with some terms eliminated. Yay algebra. +Vector quaternion_rotate_vector(Quaternion q, Vector v) +{ + Quaternion qv; + + qv.w = 0 - q.x * v.x - q.y * v.y - q.z * v.z; + qv.x = q.w * v.x + q.y * v.z - q.z * v.y; + qv.y = q.w * v.y + q.z * v.x - q.x * v.z; + qv.z = q.w * v.z + q.x * v.y - q.y * v.x; + // w is ignored. + v.x = qv.w * -q.x + qv.x * q.w + qv.y * -q.z - qv.z * -q.y; + v.y = qv.w * -q.y + qv.y * q.w + qv.z * -q.x - qv.x * -q.z; + v.z = qv.w * -q.z + qv.z * q.w + qv.x * -q.y - qv.y * -q.x; + return v; +} +#endif diff --git a/src/Core/OORoleSet.h b/src/Core/OORoleSet.h new file mode 100644 index 00000000..f39c8f0d --- /dev/null +++ b/src/Core/OORoleSet.h @@ -0,0 +1,92 @@ +/* + +OORoleSet.h + +Manage a set of roles for a ship (or ship type), including probabilities. + +A role set is an immutable object. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + + +@interface OORoleSet: NSObject +{ + NSString *_roleString; + NSDictionary *_rolesAndProbabilities; + NSSet *_roles; + float _totalProb; +} + ++ (id)roleSetWithString:(NSString *)roleString; ++ (id)roleSetWithRole:(NSString *)role probability:(float)probability; + +- (id)initWithRoleString:(NSString *)roleString; +- (id)initWithRole:(NSString *)role probability:(float)probability; + +- (NSString *)roleString; + +- (BOOL)hasRole:(NSString *)role; +- (float)probabilityForRole:(NSString *)role; +- (BOOL)intersectsSet:(id)set; // set may be an OORoleSet or an NSSet. + +- (NSSet *)roles; +- (NSArray *)sortedRoles; +- (NSDictionary *)rolesAndProbabilities; + +// Returns a random role, taking probabilities into account. +- (NSString *)anyRole; + + // Creating modified copies of role sets: +- (id)roleSetWithAddedRole:(NSString *)role probability:(float)probability; +- (id)roleSetWithAddedRoleIfNotSet:(NSString *)role probability:(float)probability; // Unlike the above, does not change probability if role exists. +- (id)roleSetWithRemovedRole:(NSString *)role; + +@end + + +// Returns a dictionary whose keys are roles and whose values are weights. +NSDictionary *OOParseRolesFromString(NSString *string); diff --git a/src/Core/OORoleSet.m b/src/Core/OORoleSet.m new file mode 100644 index 00000000..22338768 --- /dev/null +++ b/src/Core/OORoleSet.m @@ -0,0 +1,376 @@ +/* + +OORoleSet.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OORoleSet.h" + +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" +#import "OOLogging.h" + + +@interface OORoleSet (OOPrivate) + +- (id)initWithRolesAndProbabilities:(NSDictionary *)dict; + +@end + + +@implementation OORoleSet + ++ (id)roleSetWithString:(NSString *)roleString +{ + return [[[self alloc] initWithRoleString:roleString] autorelease]; +} + + ++ (id)roleSetWithRole:(NSString *)role probability:(float)probability +{ + return [[[self alloc] initWithRole:role probability:probability] autorelease]; +} + +- (id)initWithRoleString:(NSString *)roleString +{ + NSDictionary *dict = nil; + + dict = OOParseRolesFromString(roleString); + return [self initWithRolesAndProbabilities:dict]; +} + + +- (id)initWithRole:(NSString *)role probability:(float)probability +{ + NSDictionary *dict = nil; + + if (role != nil && 0 <= probability) + { + dict = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:probability] forKey:role]; + } + return [self initWithRolesAndProbabilities:dict]; +} + + +- (void)dealloc +{ + [_roleString autorelease]; + [_rolesAndProbabilities autorelease]; + [_roles autorelease]; + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{%@}", [self class], self, [self roleString]]; +} + + +- (BOOL)isEqual:(id)other +{ + if ([other isKindOfClass:[OORoleSet class]]) + { + return [_rolesAndProbabilities isEqual:[other rolesAndProbabilities]]; + } + else return NO; +} + + +- (OOUInteger)hash +{ + return [_rolesAndProbabilities hash]; +} + + +- (id)copyWithZone:(NSZone *)zone +{ + // Note: since object is immutable, a copy is no different from the original. + return [self retain]; +} + + +- (NSString *)roleString +{ + NSArray *roles = nil; + NSEnumerator *roleEnum = nil; + NSString *role = nil; + float probability; + NSMutableString *result = nil; + BOOL first = YES; + + if (_roleString == nil) + { + // Construct role string. We always do this so that it's in a normalized form. + result = [NSMutableString string]; + roles = [self sortedRoles]; + for (roleEnum = [roles objectEnumerator]; (role = [roleEnum nextObject]); ) + { + if (!first) [result appendString:@" "]; + else first = NO; + + [result appendString:role]; + + probability = [self probabilityForRole:role]; + if (probability != 1.0f) + { + [result appendFormat:@"(%g)", probability]; + } + } + + _roleString = [result copy]; + } + + return _roleString; +} + + +- (BOOL)hasRole:(NSString *)role +{ + return role != nil && [_rolesAndProbabilities objectForKey:role] != nil; +} + + +- (float)probabilityForRole:(NSString *)role +{ + return [_rolesAndProbabilities floatForKey:role defaultValue:0.0f]; +} + + +- (BOOL)intersectsSet:(id)set +{ + if ([set isKindOfClass:[OORoleSet class]]) set = [set roles]; + else if (![set isKindOfClass:[NSSet class]]) return NO; + + return [[self roles] intersectsSet:set]; +} + + +- (NSSet *)roles +{ + if (_roles == nil) + { + _roles = [[NSSet alloc] initWithArray:[_rolesAndProbabilities allKeys]]; + } + return _roles; +} + + +- (NSArray *)sortedRoles +{ + return [[_rolesAndProbabilities allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; +} + + +- (NSDictionary *)rolesAndProbabilities +{ + return _rolesAndProbabilities; +} + + +- (NSString *)anyRole +{ + NSEnumerator *roleEnum = nil; + NSString *role; + float prob, selected; + + selected = randf() * _totalProb; + prob = 0.0f; + + if ([_rolesAndProbabilities count] == 0) return nil; + + for (roleEnum = [_rolesAndProbabilities keyEnumerator]; (role = [roleEnum nextObject]); ) + { + prob += [_rolesAndProbabilities floatForKey:role]; + if (selected <= prob) break; + } + if (role == nil) + { + role = [[self roles] anyObject]; + OOLog(@"roleSet.anyRole.failed", @"Could not get a weighted-random role from role set %@, returning unweighted selection %@. TotalProb: %g, selected: %g, prob at end: %f", self, role, _totalProb, selected, prob); + } + return role; +} + + +- (id)roleSetWithAddedRoleIfNotSet:(NSString *)role probability:(float)probability +{ + NSMutableDictionary *dict = nil; + + if (role == nil || probability < 0 || ([self hasRole:role] && [self probabilityForRole:role] == probability)) + { + return [[self copy] autorelease]; + } + + dict = [[_rolesAndProbabilities mutableCopy] autorelease]; + [dict setObject:[NSNumber numberWithFloat:probability] forKey:role]; + return [[[[self class] alloc] initWithRolesAndProbabilities:dict] autorelease]; +} + + +- (id)roleSetWithAddedRole:(NSString *)role probability:(float)probability +{ + NSMutableDictionary *dict = nil; + + if (role == nil || probability < 0 || [self hasRole:role]) + { + return [[self copy] autorelease]; + } + + dict = [[_rolesAndProbabilities mutableCopy] autorelease]; + [dict setObject:[NSNumber numberWithFloat:probability] forKey:role]; + return [[[[self class] alloc] initWithRolesAndProbabilities:dict] autorelease]; +} + + +- (id)roleSetWithRemovedRole:(NSString *)role +{ + NSMutableDictionary *dict = nil; + + if (![self hasRole:role]) return [[self copy] autorelease]; + + dict = [[_rolesAndProbabilities mutableCopy] autorelease]; + [dict removeObjectForKey:role]; + return [[[[self class] alloc] initWithRolesAndProbabilities:dict] autorelease]; +} + +@end + + +@implementation OORoleSet (OOPrivate) + +- (id)initWithRolesAndProbabilities:(NSDictionary *)dict +{ + NSEnumerator *roleEnum = nil; + NSString *role; + float prob; + + if (dict == nil) + { + [self release]; + return nil; + } + + if ([super init] == nil) return nil; + + // Note: _roles and _roleString are derived on the fly as needed. + // MKW 20090815 - if we are re-initialising this OORoleSet object, we need + // to ensure that _roles and _roleString are cleared. + if( _roles ) + { + [_roles release]; + _roles = nil; + } + if( _roleString ) + { + [_roleString release]; + _roleString = nil; + } + _rolesAndProbabilities = [dict copy]; + + for (roleEnum = [dict keyEnumerator]; (role = [roleEnum nextObject]); ) + { + prob = [dict floatForKey:role defaultValue:-1]; + if (prob < 0) + { + OOLog(@"roleSet.badValue", @"Attempt to create a role set with negative or non-numerical probability for role %@.", role); + [self release]; + return nil; + } + + _totalProb += prob; + } + + return self; +} + +@end + + +NSDictionary *OOParseRolesFromString(NSString *string) +{ + NSMutableDictionary *result = nil; + NSArray *tokens = nil; + unsigned i, count; + NSString *role = nil; + float probability; + NSScanner *scanner = nil; + + // Split string at spaces, sanity checks, set-up. + if (string == nil) return nil; + + tokens = ScanTokensFromString(string); + count = [tokens count]; + if (count == 0) return nil; + + result = [NSMutableDictionary dictionaryWithCapacity:count]; + + // Scan tokens, looking for probabilities. + for (i = 0; i != count; ++i) + { + role = [tokens objectAtIndex:i]; + + probability = 1.0f; + if ([role rangeOfString:@"("].location != NSNotFound) + { + scanner = [[NSScanner alloc] initWithString:role]; + [scanner scanUpToString:@"(" intoString:&role]; + [scanner scanString:@"(" intoString:NULL]; + if (![scanner scanFloat:&probability]) probability = 1.0f; + // Ignore rest of string + + [scanner release]; + } + + if (0 <= probability) + { + [result setObject:[NSNumber numberWithFloat:probability] forKey:role]; + } + } + + if ([result count] == 0) result = nil; + return result; +} diff --git a/src/Core/OOShipGroup.h b/src/Core/OOShipGroup.h new file mode 100644 index 00000000..1daa1da5 --- /dev/null +++ b/src/Core/OOShipGroup.h @@ -0,0 +1,72 @@ +/* +OOShipGroup.h + +A weak-referencing, mutable set of ships. Not thread safe. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOWeakReference.h" + +@class ShipEntity; + + +@interface OOShipGroup: OOWeakRefObject +#if OOLITE_FAST_ENUMERATION + +#endif +{ +@private + OOUInteger _count, _capacity; + unsigned long _updateCount; + OOWeakReference **_members; + OOWeakReference *_leader; + NSString *_name; + + struct JSObject *_jsSelf; +} + +- (id) init; +- (id) initWithName:(NSString *)name; ++ (id) groupWithName:(NSString *)name; ++ (id) groupWithName:(NSString *)name leader:(ShipEntity *)leader; + +- (NSString *) name; +- (void) setName:(NSString *)name; + +- (ShipEntity *) leader; +- (void) setLeader:(ShipEntity *)leader; + +- (NSEnumerator *) objectEnumerator; +- (NSSet *) members; +- (NSArray *) memberArray; // arbitrary order +- (NSSet *) membersExcludingLeader; +- (NSArray *) memberArrayExcludingLeader; // arbitrary order + +- (BOOL) containsShip:(ShipEntity *)ship; +- (void) addShip:(ShipEntity *)ship; +- (void) removeShip:(ShipEntity *)ship; + +- (OOUInteger) count; // NOTE: this is O(n). +- (BOOL) isEmpty; + +@end diff --git a/src/Core/OOShipGroup.m b/src/Core/OOShipGroup.m new file mode 100644 index 00000000..dd1ecef5 --- /dev/null +++ b/src/Core/OOShipGroup.m @@ -0,0 +1,559 @@ +/* +OOShipGroup.m + +IMPLEMENTATION NOTE: +This is implemented as a dynamic array rather than a hash table for the +following reasons: + * Ship groups are generally quite small, not motivating a more complex + implementation. + * The code ship groups replace was all array-based and not a significant + bottleneck. + * Ship groups are compacted (i.e., dead weak references removed) as a side + effect of iteration. + * Many uses of ship groups involve iterating over the whole group anyway. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ShipEntity.h" +#import "OOShipGroup.h" +#import "OOMaths.h" + + +enum +{ + kMinSize = 4, + kMaxFreeSpace = 128 +}; + + +@interface OOShipGroupEnumerator: NSEnumerator +{ +@public + OOShipGroup *_group; + OOUInteger _index, _updateCount; + BOOL _performCleanup; +} + +- (id) initWithShipGroup:(OOShipGroup *)group; + +- (OOUInteger) index; +- (void) setPerformCleanup:(BOOL)flag; + +@end + + +@interface OOShipGroup (Private) + +- (BOOL) resizeTo:(OOUInteger)newCapacity; +- (void) cleanUp; + +- (OOUInteger) updateCount; + +@end + + +static id ShipGroupIterate(OOShipGroupEnumerator *enumerator); + + +@implementation OOShipGroup + +- (id) init +{ + return [self initWithName:nil]; +} + + +- (id) initWithName:(NSString *)name +{ + if ((self = [super init])) + { + _capacity = kMinSize; + _members = malloc(sizeof *_members * _capacity); + if (_members == NULL) + { + [self release]; + return nil; + } + + [self setName:name]; + } + + return self; +} + + ++ (id) groupWithName:(NSString *)name +{ + return [[[self alloc] initWithName:name] autorelease]; +} + + ++ (id) groupWithName:(NSString *)name leader:(ShipEntity *)leader +{ + OOShipGroup *result = [self groupWithName:name]; + [result setLeader:leader]; + return result; +} + + +- (void) dealloc +{ + OOUInteger i; + + for (i = 0; i < _count; i++) + { + [_members[i] release]; + } + free(_members); + [_name release]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + NSString *desc = [NSString stringWithFormat:@"%llu ships", (unsigned long long)_count]; + if ([self name] != nil) + { + desc = [NSString stringWithFormat:@"\"%@\", %@", [self name], desc]; + } + if ([self leader] != nil) + { + desc = [NSString stringWithFormat:@"%@, leader: %@", desc, [[self leader] shortDescription]]; + } + return desc; +} + + +- (NSString *) name +{ + return _name; +} + + +- (void) setName:(NSString *)name +{ + _updateCount++; + + if (_name != name) + { + [_name release]; + _name = [name retain]; + } +} + + +- (ShipEntity *) leader +{ + ShipEntity *result = [_leader weakRefUnderlyingObject]; + + // If reference is stale, delete weakref object. + if (result == nil && _leader != nil) + { + [_leader release]; + _leader = nil; + } + + return result; +} + + +- (void) setLeader:(ShipEntity *)leader +{ + _updateCount++; + + if (leader != [self leader]) + { + [_leader release]; + [self addShip:leader]; + _leader = [leader weakRetain]; + } +} + + +- (NSEnumerator *) objectEnumerator +{ + return [[[OOShipGroupEnumerator alloc] initWithShipGroup:self] autorelease]; +} + + +- (NSSet *) members +{ + return [NSSet setWithArray:[self memberArray]]; +} + + +- (NSSet *) membersExcludingLeader +{ + return [NSSet setWithArray:[self memberArrayExcludingLeader]]; +} + + +#if OOLITE_FAST_ENUMERATION +- (NSArray *) memberArray +{ + id *objects = NULL; + OOUInteger count = 0; + NSArray *result = nil; + + if (_count == 0) return [NSArray array]; + + objects = malloc(sizeof *objects * _count); + for (id ship in self) + { + objects[count++] = ship; + } + + result = [NSArray arrayWithObjects:objects count:count]; + free(objects); + + return result; +} + + +- (NSArray *) memberArrayExcludingLeader +{ + id *objects = NULL; + OOUInteger count = 0; + NSArray *result = nil; + ShipEntity *leader = nil; + + if (_count == 0) return [NSArray array]; + leader = self.leader; + + objects = malloc(sizeof *objects * _count); + for (id ship in self) + { + if (ship != leader) + { + objects[count++] = ship; + } + } + + result = [NSArray arrayWithObjects:objects count:count]; + free(objects); + + return result; +} + + +- (BOOL) containsShip:(ShipEntity *)ship +{ + ShipEntity *containedShip = nil; + + for (containedShip in self) + { + if ([ship isEqual:containedShip]) + { + return YES; + } + } + + return NO; +} +#else +- (NSArray *) memberArray +{ + return [[self objectEnumerator] allObjects]; +} + + +- (NSArray *) memberArrayExcludingLeader +{ + id *objects = NULL; + OOUInteger count = 0; + NSArray *result = nil; + NSEnumerator *shipEnum = nil; + ShipEntity *ship = nil; + ShipEntity *leader = nil; + + if (_count == 0) return [NSArray array]; + leader = [self leader]; + if (leader == nil) return [self memberArray]; + + objects = malloc(sizeof *objects * _count); + for (shipEnum = [self objectEnumerator]; (ship = [shipEnum nextObject]); ) + { + if (ship != leader) + { + objects[count++] = ship; + } + } + + result = [NSArray arrayWithObjects:objects count:count]; + free(objects); + + return result; +} + + +- (BOOL) containsShip:(ShipEntity *)ship +{ + OOShipGroupEnumerator *shipEnum = nil; + ShipEntity *containedShip = nil; + BOOL result = NO; + + shipEnum = (OOShipGroupEnumerator *)[self objectEnumerator]; + [shipEnum setPerformCleanup:NO]; + while ((containedShip = [shipEnum nextObject])) + { + if ([ship isEqual:containedShip]) + { + result = YES; + break; + } + } + + // Clean up + [self cleanUp]; + + return result; +} +#endif + + +- (void) addShip:(ShipEntity *)ship +{ + _updateCount++; + + if ([self containsShip:ship]) return; + + // Ensure there's space. + if (_count == _capacity) + { + if (![self resizeTo:(_capacity > kMaxFreeSpace) ? (_capacity + kMaxFreeSpace) : (_capacity * 2)]) + { + if (![self resizeTo:_capacity + 1]) + { + // Out of memory? Fail silently, groups are non-critical. + return; + } + } + } + + _members[_count++] = [ship weakRetain]; +} + + +- (void) removeShip:(ShipEntity *)ship +{ + OOShipGroupEnumerator *shipEnum = nil; + ShipEntity *containedShip = nil; + OOUInteger index; + + _updateCount++; + + if (ship == [self leader]) [self setLeader:nil]; + + shipEnum = (OOShipGroupEnumerator *)[self objectEnumerator]; + [shipEnum setPerformCleanup:NO]; + while ((containedShip = [shipEnum nextObject])) + { + if ([ship isEqual:containedShip]) + { + index = [shipEnum index] - 1; + _members[index] = _members[--_count]; + + // Clean up + [self cleanUp]; + break; + } + } +} + + +- (OOUInteger) count +{ + NSEnumerator *memberEnum = nil; + OOUInteger result = 0; + + if (_count != 0) + { + memberEnum = [self objectEnumerator]; + while ([memberEnum nextObject] != nil) result++; + } + + assert(result == _count); + + return result; +} + + +- (BOOL) isEmpty +{ + if (_count == 0) return YES; + + return [[self objectEnumerator] nextObject] == nil; +} + + +- (BOOL) resizeTo:(OOUInteger)newCapacity +{ + OOWeakReference **temp = NULL; + + if (newCapacity < _count) return NO; + + temp = realloc(_members, newCapacity * sizeof *_members); + if (temp == NULL) return NO; + + _members = temp; + _capacity = newCapacity; + return YES; +} + + +- (void) cleanUp +{ + OOUInteger newCapacity = _capacity; + + if (_count >= kMaxFreeSpace) + { + if (_capacity > _count + kMaxFreeSpace) + { + newCapacity = _count + 1; // +1 keeps us at powers of two + multiples of kMaxFreespace. + } + } + else + { + if (_capacity > _count * 2) + { + newCapacity = OORoundUpToPowerOf2(_count); + if (newCapacity < kMinSize) newCapacity = kMinSize; + } + } + + if (newCapacity != _capacity) [self resizeTo:newCapacity]; +} + + +- (OOUInteger) updateCount +{ + return _updateCount; +} + + +static id ShipGroupIterate(OOShipGroupEnumerator *enumerator) +{ + // The work is done here so that we can have access to both OOShipGroup's and OOShipGroupEnumerator's ivars. + + OOShipGroup *group = enumerator->_group; + ShipEntity *result = nil; + BOOL considerCleanup = NO; + + if (enumerator->_updateCount != group->_updateCount) + { + [NSException raise:NSGenericException format:@"Collection was mutated while being enumerated.", group]; + } + + while (enumerator->_index < group->_count) + { + result = [group->_members[enumerator->_index] weakRefUnderlyingObject]; + if (result != nil) + { + enumerator->_index++; + break; + } + + // If we got here, the group contains a stale reference to a dead ship. + group->_members[enumerator->_index] = group->_members[--group->_count]; + considerCleanup = YES; + } + + // Clean up. + if (considerCleanup && enumerator->_performCleanup) [group cleanUp]; + + return result; +} + + +#if OOLITE_FAST_ENUMERATION +- (OOUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id *)stackbuf count:(OOUInteger)len +{ + OOUInteger srcIndex, dstIndex = 0; + ShipEntity *item = nil; + BOOL cleanupNeeded = NO; + + srcIndex = state->state; + while (srcIndex < _count && dstIndex < len) + { + item = [_members[srcIndex] weakRefUnderlyingObject]; + if (item != nil) + { + stackbuf[dstIndex++] = item; + srcIndex++; + } + else + { + _members[srcIndex] = _members[--_count]; + cleanupNeeded = YES; + } + } + + if (cleanupNeeded) [self cleanUp]; + + state->state = srcIndex; + state->itemsPtr = stackbuf; + state->mutationsPtr = &_updateCount; + + return dstIndex; +} +#endif + +@end + + +@implementation OOShipGroupEnumerator + +- (id) initWithShipGroup:(OOShipGroup *)group +{ + assert(group != nil); + + if ((self = [super init])) + { + _group = [group retain]; + _performCleanup = YES; + _updateCount = [_group updateCount]; + } + + return self; +} + + +- (id) nextObject +{ + return ShipGroupIterate(self); +} + + +- (OOUInteger) index +{ + return _index; +} + + +- (void) setPerformCleanup:(BOOL)flag +{ + _performCleanup = flag; +} + +@end diff --git a/src/Core/OOShipRegistry.h b/src/Core/OOShipRegistry.h new file mode 100644 index 00000000..d51d8c1a --- /dev/null +++ b/src/Core/OOShipRegistry.h @@ -0,0 +1,81 @@ +/* + +OOShipRegistry.h + +Manage the set of installed ships. + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008-2009 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" + +@class OOProbabilitySet; + + +@interface OOShipRegistry: NSObject +{ + NSDictionary *_shipData; + NSArray *_demoShips; + NSArray *_playerShips; + NSDictionary *_probabilitySets; +} + ++ (OOShipRegistry *) sharedRegistry; + +- (NSDictionary *) shipInfoForKey:(NSString *)key; +- (NSDictionary *) shipyardInfoForKey:(NSString *)key; +- (OOProbabilitySet *) probabilitySetForRole:(NSString *)role; + +- (NSArray *) demoShipKeys; +- (NSArray *) playerShipKeys; + +@end + + +@interface OOShipRegistry (OOConveniences) + +- (NSArray *) shipKeysWithRole:(NSString *)role; +- (NSString *) randomShipKeyForRole:(NSString *)role; + +@end diff --git a/src/Core/OOShipRegistry.m b/src/Core/OOShipRegistry.m new file mode 100644 index 00000000..1aa6fcfd --- /dev/null +++ b/src/Core/OOShipRegistry.m @@ -0,0 +1,1336 @@ +/* + +OOShipRegistry.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008-2009 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOShipRegistry.h" +#import "OOCacheManager.h" +#import "ResourceManager.h" +#import "OOCollectionExtractors.h" +#import "NSDictionaryOOExtensions.h" +#import "OOProbabilitySet.h" +#import "OORoleSet.h" +#import "OOStringParsing.h" +#import "OOMesh.h" +#import "GameController.h" +#import "OOLegacyScriptWhitelist.h" +#import "OODeepCopy.h" + + +#define PRELOAD 0 + + +static OOShipRegistry *sSingleton = nil; + +static NSString * const kShipRegistryCacheName = @"ship registry"; +static NSString * const kShipDataCacheKey = @"ship data"; +static NSString * const kPlayerShipsCacheKey = @"player ships"; +static NSString * const kDemoShipsCacheKey = @"demo ships"; +static NSString * const kRoleWeightsCacheKey = @"role weights"; +static NSString * const kDefaultDemoShip = @"coriolis-station"; + + +@interface OOShipRegistry (OODataLoader) + +- (void) loadShipData; +- (void) loadDemoShips; +- (void) loadCachedRoleProbabilitySets; +- (void) buildRoleProbabilitySets; + +- (BOOL) applyLikeShips:(NSMutableDictionary *)ioData; +- (BOOL) loadAndMergeShipyard:(NSMutableDictionary *)ioData; +- (BOOL) stripPrivateKeys:(NSMutableDictionary *)ioData; +- (BOOL) makeShipEntriesMutable:(NSMutableDictionary *)ioData; +- (BOOL) loadAndApplyShipDataOverrides:(NSMutableDictionary *)ioData; +- (BOOL) canonicalizeAndTagSubentities:(NSMutableDictionary *)ioData; +- (BOOL) removeUnusableEntries:(NSMutableDictionary *)ioData; +- (BOOL) sanitizeConditions:(NSMutableDictionary *)ioData; + +#if PRELOAD +- (BOOL) preloadShipMeshes:(NSMutableDictionary *)ioData; +#endif + +- (NSMutableDictionary *) mergeShip:(NSDictionary *)child withParent:(NSDictionary *)parent; +- (void) mergeShipRoles:(NSString *)roles forShipKey:(NSString *)shipKey intoProbabilityMap:(NSMutableDictionary *)probabilitySets; + +- (NSDictionary *) canonicalizeSubentityDeclaration:(id)declaration + forShip:(NSString *)shipKey + shipData:(NSDictionary *)shipData + fatalError:(BOOL *)outFatalError; +- (NSDictionary *) translateOldStyleSubentityDeclaration:(NSString *)declaration + forShip:(NSString *)shipKey + shipData:(NSDictionary *)shipData + fatalError:(BOOL *)outFatalError; +- (NSDictionary *) translateOldStyleFlasherDeclaration:(NSArray *)tokens + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError; +- (NSDictionary *) translateOldStandardBasicSubentityDeclaration:(NSArray *)tokens + forShip:(NSString *)shipKey + shipData:(NSDictionary *)shipData + fatalError:(BOOL *)outFatalError; +- (NSDictionary *) validateNewStyleSubentityDeclaration:(NSDictionary *)declaration + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError; +- (NSDictionary *) validateNewStyleFlasherDeclaration:(NSDictionary *)declaration + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError; +- (NSDictionary *) validateNewStyleStandardSubentityDeclaration:(NSDictionary *)declaration + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError; + +- (BOOL) shipIsBallTurretForKey:(NSString *)shipKey inShipData:(NSDictionary *)shipData; + +@end + + +@implementation OOShipRegistry + ++ (OOShipRegistry *) sharedRegistry +{ + if (sSingleton == nil) + { + [[self alloc] init]; + } + + return sSingleton; +} + + +- (id) init +{ + if ((self = [super init])) + { + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + OOCacheManager *cache = [OOCacheManager sharedCache]; + + _shipData = [[cache objectForKey:kShipDataCacheKey inCache:kShipRegistryCacheName] retain]; + _playerShips = [[cache objectForKey:kPlayerShipsCacheKey inCache:kShipRegistryCacheName] retain]; + if ([_shipData count] == 0) // Don't accept nil or empty + { + [self loadShipData]; + if ([_shipData count] == 0) + { + [NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load any ship data."]; + } + if ([_playerShips count] == 0) + { + [NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load any player ships."]; + } + } + + _demoShips = [[cache objectForKey:kDemoShipsCacheKey inCache:kShipRegistryCacheName] retain]; + if ([_demoShips count] == 0) + { + [self loadDemoShips]; + if ([_demoShips count] == 0) + { + [NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load or synthesize any demo ships."]; + } + } + + [self loadCachedRoleProbabilitySets]; + if (_probabilitySets == nil) + { + [self buildRoleProbabilitySets]; + if ([_probabilitySets count] == 0) + { + [NSException raise:@"OOShipRegistryLoadFailure" format:@"Could not load or synthesize role probability sets."]; + } + } + + [pool release]; + } + return self; +} + + +- (void) dealloc +{ + [_shipData release]; + [_demoShips release]; + [_playerShips release]; + [_probabilitySets release]; + + [super dealloc]; +} + + +- (NSDictionary *) shipInfoForKey:(NSString *)key +{ + return [_shipData objectForKey:key]; +} + + +- (NSDictionary *) shipyardInfoForKey:(NSString *)key +{ + return [[self shipInfoForKey:key] objectForKey:@"_oo_shipyard"]; +} + + +- (OOProbabilitySet *) probabilitySetForRole:(NSString *)role +{ + if (role == nil) return nil; + return [_probabilitySets objectForKey:role]; +} + + +- (NSArray *) demoShipKeys +{ + return _demoShips; +} + + +- (NSArray *) playerShipKeys +{ + return _playerShips; +} + +@end + + +@implementation OOShipRegistry (OOConveniences) + +- (NSArray *) shipKeysWithRole:(NSString *)role +{ + return [[self probabilitySetForRole:role] allObjects]; +} + + +- (NSString *) randomShipKeyForRole:(NSString *)role +{ + return [[self probabilitySetForRole:role] randomObject]; +} + +@end + + +@implementation OOShipRegistry (OODataLoader) + +/* -loadShipData + + Load the data for all ships. This consists of five stages: + * Load merges shipdata.plist dictionary. + * Apply all like_ship entries. + * Load shipdata-overrides.plist and apply patches. + * Load shipyard.plist, add shipyard data into ship dictionaries, and + create _playerShips array. + * Build role->ship type probability sets. +*/ +- (void) loadShipData +{ + NSMutableDictionary *result = nil; + + OOLog(@"shipData.load.begin", @"Loading ship data..."); + OOLogIndentIf(@"shipData.load.begin"); + + [_shipData release]; + _shipData = nil; + [_playerShips release]; + _playerShips = nil; + + // Load shipdata.plist. + result = [[[ResourceManager dictionaryFromFilesNamed:@"shipdata.plist" + inFolder:@"Config" + mergeMode:MERGE_BASIC + cache:NO] mutableCopy] autorelease]; + if (result == nil) return; + + // Make each entry mutable to simplify later stages. Also removes any entries that aren't dictionaries. + if (![self makeShipEntriesMutable:result]) return; + + // Apply patches. + if (![self loadAndApplyShipDataOverrides:result]) return; + + // Strip private keys (anything starting with _oo_). + if (![self stripPrivateKeys:result]) return; + + // Resolve like_ship entries. + if (![self applyLikeShips:result]) return; + + // Clean up subentity declarations and tag subentities so they won't be pruned. + if (![self canonicalizeAndTagSubentities:result]) return; + + // Clean out templates and invalid entries. + if (![self removeUnusableEntries:result]) return; + + // Add shipyard entries into shipdata entries. + if (![self loadAndMergeShipyard:result]) return; + + // Sanitize conditions. + if (![self sanitizeConditions:result]) return; + +#if PRELOAD + // Preload and cache meshes. + if (![self preloadShipMeshes:result]) return; +#endif + + _shipData = OODeepCopy(result); + [[OOCacheManager sharedCache] setObject:_shipData forKey:kShipDataCacheKey inCache:kShipRegistryCacheName]; + + OOLogOutdentIf(@"shipData.load.begin"); + OOLog(@"shipData.load.done", @"Ship data loaded."); +} + + +/* -loadDemoShips + + Load demoships.plist, and filter out non-existent ships. If no existing + ships remain, try adding coriolis; if this fails, add any ship in + shipdata. +*/ +- (void) loadDemoShips +{ + NSEnumerator *enumerator = nil; + NSString *key = nil; + NSArray *initialDemoShips = nil; + NSMutableArray *demoShips = nil; + + [_demoShips release]; + _demoShips = nil; + + initialDemoShips = [ResourceManager arrayFromFilesNamed:@"demoships.plist" + inFolder:@"Config" + andMerge:YES]; + demoShips = [NSMutableArray arrayWithArray:initialDemoShips]; + + // Note: iterate over initialDemoShips to avoid mutating the collection being enu,erated. + for (enumerator = [initialDemoShips objectEnumerator]; (key = [enumerator nextObject]); ) + { + if (![key isKindOfClass:[NSString class]] || [self shipInfoForKey:key] == nil) + { + [demoShips removeObject:key]; + } + } + + if ([demoShips count] == 0) + { + if ([self shipInfoForKey:kDefaultDemoShip] != nil) [demoShips addObject:kDefaultDemoShip]; + else [demoShips addObject:[[_shipData allKeys] objectAtIndex:0]]; + } + + _demoShips = [demoShips copy]; + [[OOCacheManager sharedCache] setObject:_demoShips forKey:kDemoShipsCacheKey inCache:kShipRegistryCacheName]; +} + + +- (void) loadCachedRoleProbabilitySets +{ + NSDictionary *cachedSets = nil; + NSMutableDictionary *restoredSets = nil; + NSEnumerator *roleEnum = nil; + NSString *role = nil; + + cachedSets = [[OOCacheManager sharedCache] objectForKey:kRoleWeightsCacheKey inCache:kShipRegistryCacheName]; + if (cachedSets == nil) return; + + restoredSets = [NSMutableDictionary dictionaryWithCapacity:[cachedSets count]]; + for (roleEnum = [cachedSets keyEnumerator]; (role = [roleEnum nextObject]); ) + { + [restoredSets setObject:[OOProbabilitySet probabilitySetWithPropertyListRepresentation:[cachedSets objectForKey:role]] forKey:role]; + } + + _probabilitySets = [restoredSets copy]; +} + + +- (void) buildRoleProbabilitySets +{ + NSMutableDictionary *probabilitySets = nil; + NSEnumerator *shipEnum = nil; + NSString *shipKey = nil; + NSDictionary *shipEntry = nil; + NSString *roles = nil; + NSEnumerator *roleEnum = nil; + NSString *role = nil; + OOProbabilitySet *pset = nil; + NSMutableDictionary *cacheEntry = nil; + + probabilitySets = [NSMutableDictionary dictionary]; + + // Build role sets + for (shipEnum = [_shipData keyEnumerator]; (shipKey = [shipEnum nextObject]); ) + { + shipEntry = [_shipData objectForKey:shipKey]; + roles = [shipEntry stringForKey:@"roles"]; + [self mergeShipRoles:roles forShipKey:shipKey intoProbabilityMap:probabilitySets]; + } + + // Convert role sets to immutable form, and build cache entry. + // Note: we iterate over a copy of the keys to avoid mutating while iterating. + cacheEntry = [NSMutableDictionary dictionaryWithCapacity:[probabilitySets count]]; + for (roleEnum = [[probabilitySets allKeys] objectEnumerator]; (role = [roleEnum nextObject]); ) + { + pset = [probabilitySets objectForKey:role]; + pset = [[pset copy] autorelease]; + [probabilitySets setObject:pset forKey:role]; + [cacheEntry setObject:[pset propertyListRepresentation] forKey:role]; + } + + _probabilitySets = [probabilitySets copy]; + [[OOCacheManager sharedCache] setObject:cacheEntry forKey:kRoleWeightsCacheKey inCache:kShipRegistryCacheName]; +} + + +/* -applyLikeShips: + + Implement like_ship by copying inherited ship and overwriting with child + ship values. Done iteratively to report recursive references of arbitrary + depth. Also removes and reports ships whose like_ship entry does not + resolve, and handles reference loops by removing all ships involved. + + We start with a set of keys all ships that have a like_ships entry. In + each iteration, every ship whose like_ship entry does not refer to a ship + which itself has a like_ship entry is finalized. If the set of pending + ships does not shrink in an iteration, the remaining ships cannot be + resolved (either their like_ships do not exist, or they form reference + cycles) so we stop looping and report it. +*/ +- (BOOL) applyLikeShips:(NSMutableDictionary *)ioData +{ + NSMutableSet *remainingLikeShips = nil; + NSEnumerator *enumerator = nil; + NSString *key = nil; + NSString *parentKey = nil; + NSDictionary *shipEntry = nil; + NSDictionary *parentEntry = nil; + unsigned count, lastCount; + NSMutableArray *reportedBadShips = nil; + + // Build set of ships with like_ship references + remainingLikeShips = [NSMutableSet set]; + for (enumerator = [ioData keyEnumerator]; (key = [enumerator nextObject]); ) + { + shipEntry = [ioData objectForKey:key]; + if ([shipEntry stringForKey:@"like_ship"] != nil) + { + [remainingLikeShips addObject:key]; + } + } + + count = lastCount = [remainingLikeShips count]; + while (count != 0) + { + for (enumerator = [[[remainingLikeShips copy] autorelease] objectEnumerator]; (key = [enumerator nextObject]); ) + { + // Look up like_ship entry + shipEntry = [ioData objectForKey:key]; + parentKey = [shipEntry objectForKey:@"like_ship"]; + if (![remainingLikeShips containsObject:parentKey]) + { + // If parent is fully resolved, we can resolve this child. + parentEntry = [ioData objectForKey:parentKey]; + shipEntry = [self mergeShip:shipEntry withParent:parentEntry]; + if (shipEntry != nil) + { + [remainingLikeShips removeObject:key]; + [ioData setObject:shipEntry forKey:key]; + } + } + } + + count = [remainingLikeShips count]; + if (count == lastCount) + { + /* Fail: we couldn't resolve all like_ship entries. + Remove unresolved entries, building a list of the ones that + don't have is_external_dependency set. + */ + reportedBadShips = [NSMutableArray array]; + for (enumerator = [remainingLikeShips objectEnumerator]; (key = [enumerator nextObject]); ) + { + if (![[ioData dictionaryForKey:key] boolForKey:@"is_external_dependency"]) + { + [reportedBadShips addObject:key]; + } + [ioData removeObjectForKey:key]; + } + + if ([reportedBadShips count] != 0) + { + [reportedBadShips sortUsingSelector:@selector(caseInsensitiveCompare:)]; + OOLog(@"shipData.merge.failed", @"***** ERROR: one or more shipdata.plist entries have like_ship references that cannot be resolved: %@", [reportedBadShips componentsJoinedByString:@", "]); + } + break; + } + lastCount = count; + } + + return YES; +} + + +- (NSMutableDictionary *) mergeShip:(NSDictionary *)child withParent:(NSDictionary *)parent +{ + NSMutableDictionary *result = [[parent mutableCopy] autorelease]; + if (result == nil) return nil; + + [result addEntriesFromDictionary:child]; + [result removeObjectForKey:@"like_ship"]; + + // Certain properties cannot be inherited. + if ([child stringForKey:@"display_name"] == nil) [result removeObjectForKey:@"display_name"]; + if ([child stringForKey:@"is_template"] == nil) [result removeObjectForKey:@"is_template"]; + + return result; +} + + +- (BOOL) makeShipEntriesMutable:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSDictionary *shipEntry = nil; + + for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + if (![shipEntry isKindOfClass:[NSDictionary class]]) + { + OOLog(@"shipData.load.badEntry", @"***** ERROR: the shipdata.plist entry \"%@\" is not a dictionary.", shipKey); + [ioData removeObjectForKey:shipKey]; + } + else + { + shipEntry = [shipEntry mutableCopy]; + + [ioData setObject:shipEntry forKey:shipKey]; + [shipEntry release]; + } + } + + return YES; +} + + +- (BOOL) loadAndApplyShipDataOverrides:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + NSDictionary *overrides = nil; + NSDictionary *overridesEntry = nil; + + overrides = [ResourceManager dictionaryFromFilesNamed:@"shipdata-overrides.plist" + inFolder:@"Config" + mergeMode:MERGE_SMART + cache:NO]; + + for (shipKeyEnum = [overrides keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + if (shipEntry != nil) + { + overridesEntry = [overrides objectForKey:shipKey]; + if (![overridesEntry isKindOfClass:[NSDictionary class]]) + { + OOLog(@"shipData.load.error", @"***** ERROR: the shipdata-overrides.plist entry \"%@\" is not a dictionary.", shipKey); + } + else + { + [shipEntry addEntriesFromDictionary:overridesEntry]; + } + } + } + + return YES; +} + + +- (BOOL) stripPrivateKeys:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + NSEnumerator *attrKeyEnum = nil; + NSString *attrKey = nil; + + for (shipKeyEnum = [ioData keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + + for (attrKeyEnum = [shipEntry keyEnumerator]; (attrKey = [attrKeyEnum nextObject]); ) + { + if ([attrKey hasPrefix:@"_oo_"]) + { + [shipEntry removeObjectForKey:attrKey]; + } + } + } + + return YES; +} + + +/* -loadAndMergeShipyard: + + Load shipyard.plist, add its entries to appropriate shipyard entries as + a dictionary under the key "shipyard", and build list of player ships. + Before that, we strip out any "shipyard" entries already in shipdata, and + apply any shipyard-overrides.plist stuff to shipyard. +*/ +- (BOOL) loadAndMergeShipyard:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + NSDictionary *shipyard = nil; + NSDictionary *shipyardOverrides = nil; + NSDictionary *shipyardEntry = nil; + NSDictionary *shipyardOverridesEntry = nil; + NSMutableSet *playerShips = nil; + + // Strip out any shipyard stuff in shipdata (there shouldn't be any). + for (shipKeyEnum = [ioData keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + if ([shipEntry objectForKey:@"_oo_shipyard"] != nil) + { + [shipEntry removeObjectForKey:@"_oo_shipyard"]; + } + } + + shipyard = [ResourceManager dictionaryFromFilesNamed:@"shipyard.plist" + inFolder:@"Config" + mergeMode:MERGE_BASIC + cache:NO]; + shipyardOverrides = [ResourceManager dictionaryFromFilesNamed:@"shipyard-overrides.plist" + inFolder:@"Config" + mergeMode:MERGE_SMART + cache:NO]; + + playerShips = [NSMutableArray arrayWithCapacity:[shipyard count]]; + + // Insert merged shipyard and shipyardOverrides entries. + for (shipKeyEnum = [shipyard keyEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + if (shipEntry != nil) + { + shipyardEntry = [shipyard objectForKey:shipKey]; + shipyardOverridesEntry = [shipyardOverrides objectForKey:shipKey]; + shipyardEntry = [shipyardEntry dictionaryByAddingEntriesFromDictionary:shipyardOverridesEntry]; + + [shipEntry setObject:shipyardEntry forKey:@"_oo_shipyard"]; + + [playerShips addObject:shipKey]; + } + else + { + OOLog(@"shipData.load.shipyard.unknown", @"----- WARNING: the shipyard.plist entry \"%@\" does not have a corresponding shipdata.plist entry, ignoring.", shipKey); + } + } + + _playerShips = [playerShips copy]; + [[OOCacheManager sharedCache] setObject:_playerShips forKey:kPlayerShipsCacheKey inCache:kShipRegistryCacheName]; + + return YES; +} + + +- (BOOL) canonicalizeAndTagSubentities:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + NSArray *subentityDeclarations = nil; + NSEnumerator *subentityEnum = nil; + id subentityDecl = nil; + NSDictionary *subentityDict = nil; + NSString *subentityKey = nil; + NSMutableDictionary *subentityShipEntry = nil; + NSMutableSet *badSubentities = nil; + NSString *badSubentitiesList = nil; + NSMutableArray *okSubentities = nil; + BOOL remove, fatal; + + // Convert all subentity declarations to dictionaries and add + // _oo_is_subentity=YES to all entries used as subentities. + + // Iterate over all ships. (Iterates over a copy of keys since it mutates the dictionary.) + for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + remove = NO; + badSubentities = nil; + + // Iterate over each subentity declaration of each ship + subentityDeclarations = [shipEntry arrayForKey:@"subentities"]; + if (subentityDeclarations != nil) + { + okSubentities = [NSMutableArray arrayWithCapacity:[subentityDeclarations count]]; + for (subentityEnum = [subentityDeclarations objectEnumerator]; (subentityDecl = [subentityEnum nextObject]); ) + { + subentityDict = [self canonicalizeSubentityDeclaration:subentityDecl forShip:shipKey shipData:ioData fatalError:&fatal]; + + // If entry is broken, we need to kill this ship. + if (fatal) + { + remove = YES; + } + else + { + [okSubentities addObject:subentityDict]; + + // Tag subentities. + if (![[subentityDict stringForKey:@"type"] isEqualToString:@"flasher"]) + { + subentityKey = [subentityDict stringForKey:@"subentity_key"]; + subentityShipEntry = [ioData objectForKey:subentityKey]; + if (subentityKey == nil) + { + // Oops, reference to non-existent subent. + if (badSubentities == nil) badSubentities = [NSMutableSet set]; + [badSubentities addObject:subentityKey]; + } + else + { + // Subent exists, add _oo_is_subentity so roles aren't required. + [subentityShipEntry setBool:YES forKey:@"_oo_is_subentity"]; + } + } + } + } + + // Set updated subentity list. + if ([okSubentities count] != 0) + { + [shipEntry setObject:okSubentities forKey:@"subentities"]; + } + else + { + [shipEntry removeObjectForKey:@"subentities"]; + } + + if (badSubentities != nil) + { + if (![shipEntry boolForKey:@"is_external_dependency"]) + { + badSubentitiesList = [[[badSubentities allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] componentsJoinedByString:@", "]; + OOLog(@"shipData.load.error", @"***** ERROR: the shipdata.plist entry \"%@\" has unresolved subentit%@ %@.", shipKey, ([badSubentities count] == 1) ? @"y" : @"ies", badSubentitiesList); + } + remove = YES; + } + + if (remove) + { + // Removal is deferred to avoid bogus "entry doesn't exist" errors. + [shipEntry setBool:YES forKey:@"_oo_deferred_remove"]; + } + } + } + + return YES; +} + + +- (BOOL) removeUnusableEntries:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + BOOL remove; + NSString *modelName = nil; + + // Clean out invalid entries and templates. (Iterates over a copy of keys since it mutates the dictionary.) + for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + remove = NO; + + if ([shipEntry boolForKey:@"is_template"] || [shipEntry boolForKey:@"_oo_deferred_remove"]) remove = YES; + else if ([[shipEntry stringForKey:@"roles"] length] == 0 && ![shipEntry boolForKey:@"_oo_is_subentity"]) + { + OOLog(@"shipData.load.error", @"***** ERROR: the shipdata.plist entry \"%@\" specifies no %@.", shipKey, @"roles"); + remove = YES; + } + else + { + modelName = [shipEntry stringForKey:@"model"]; + if ([modelName length] == 0) + { + OOLog(@"shipData.load.error", @"***** ERROR: the shipdata.plist entry \"%@\" specifies no %@.", shipKey, @"model"); + remove = YES; + } + else if ([ResourceManager pathForFileNamed:modelName inFolder:@"Models"] == nil) + { + OOLog(@"shipData.load.error", @"***** ERROR: the shipdata.plist entry \"%@\" specifies non-existent model \"%@\".", shipKey, modelName, @"model"); + remove = YES; + } + } + if (remove) [ioData removeObjectForKey:shipKey]; + } + + return YES; +} + + +/* Transform conditions, determinant (if conditions array) and + shipyard.conditions from hasShipyard to sanitized form. +*/ +- (BOOL) sanitizeConditions:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + NSMutableDictionary *mutableShipyard = nil; + NSArray *conditions = nil; + NSArray *hasShipyard = nil; + NSArray *shipyardConditions = nil; + + for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + shipEntry = [ioData objectForKey:shipKey]; + conditions = [shipEntry objectForKey:@"conditions"]; + hasShipyard = [shipEntry objectForKey:@"hasShipyard"]; + if (![hasShipyard isKindOfClass:[NSArray class]]) hasShipyard = nil; // May also be fuzzy boolean + shipyardConditions = [[shipEntry dictionaryForKey:@"_oo_shipyard"] objectForKey:@"conditions"]; + + if (conditions == nil && hasShipyard && shipyardConditions == nil) continue; + + if (conditions != nil) + { + if ([conditions isKindOfClass:[NSArray class]]) + { + conditions = OOSanitizeLegacyScriptConditions(conditions, [NSString stringWithFormat:@"shipdata.plist entry \"%@\"", shipKey]); + } + else + { + OOLog(@"shipdata.load.warning", @"----- WARNING: conditions for shipdata.plist entry \"%@\" are not an array, ignoring.", shipKey); + conditions = nil; + } + + if (conditions != nil) + { + [shipEntry setObject:conditions forKey:@"conditions"]; + } + else + { + [shipEntry removeObjectForKey:@"conditions"]; + } + } + + if (hasShipyard != nil) + { + hasShipyard = OOSanitizeLegacyScriptConditions(hasShipyard, [NSString stringWithFormat:@"shipdata.plist entry \"%@\" hasShipyard conditions", shipKey]); + + if (hasShipyard != nil) + { + [shipEntry setObject:hasShipyard forKey:@"hasShipyard"]; + } + else + { + [shipEntry removeObjectForKey:@"hasShipyard"]; + } + } + + if (shipyardConditions != nil) + { + mutableShipyard = [[[shipEntry dictionaryForKey:@"_oo_shipyard"] mutableCopy] autorelease]; + + if ([shipyardConditions isKindOfClass:[NSArray class]]) + { + shipyardConditions = OOSanitizeLegacyScriptConditions(shipyardConditions, [NSString stringWithFormat:@"shipyard.plist entry \"%@\"", shipKey]); + } + else + { + OOLog(@"shipdata.load.warning", @"----- WARNING: conditions for shipyard.plist entry \"%@\" are not an array, ignoring.", shipKey); + shipyardConditions = nil; + } + + if (shipyardConditions != nil) + { + [mutableShipyard setObject:shipyardConditions forKey:@"conditions"]; + } + else + { + [mutableShipyard removeObjectForKey:@"conditions"]; + } + + [shipEntry setObject:mutableShipyard forKey:@"_oo_shipyard"]; + } + } + + return YES; +} + + +#if PRELOAD +- (BOOL) preloadShipMeshes:(NSMutableDictionary *)ioData +{ + NSEnumerator *shipKeyEnum = nil; + NSString *shipKey = nil; + NSMutableDictionary *shipEntry = nil; + BOOL remove; + NSString *modelName = nil; + OOMesh *mesh = nil; + NSAutoreleasePool *pool = nil; + OOUInteger i = 0, count; + + count = [ioData count]; + + // Preload ship meshes. (Iterates over a copy of keys since it mutates the dictionary.) + for (shipKeyEnum = [[ioData allKeys] objectEnumerator]; (shipKey = [shipKeyEnum nextObject]); ) + { + pool = [[NSAutoreleasePool alloc] init]; + + [[GameController sharedController] setProgressBarValue:(float)i++ / (float)count]; + + shipEntry = [ioData objectForKey:shipKey]; + remove = NO; + + modelName = [shipEntry stringForKey:@"model"]; + mesh = [OOMesh meshWithName:modelName + materialDictionary:[shipEntry dictionaryForKey:@"materials"] + shadersDictionary:[shipEntry dictionaryForKey:@"shaders"] + smooth:[shipEntry boolForKey:@"smooth"] + shaderMacros:nil + shaderBindingTarget:nil]; + + [pool release]; // NOTE: mesh is now invalid, but pointer nil check is OK. + + if (mesh == nil) + { + // FIXME: what if it's a subentity? Need to rearrange things. + OOLog(@"shipData.load.error", @"***** ERROR: model \"%@\" could not be loaded for ship \"%@\", removing.", modelName, shipKey); + [ioData removeObjectForKey:shipKey]; + } + } + + [[GameController sharedController] setProgressBarValue:-1.0f]; + + return YES; +} +#endif + + +- (void) mergeShipRoles:(NSString *)roles + forShipKey:(NSString *)shipKey + intoProbabilityMap:(NSMutableDictionary *)probabilitySets +{ + NSDictionary *rolesAndWeights = nil; + NSEnumerator *roleEnum = nil; + NSString *role = nil; + OOMutableProbabilitySet *probSet = nil; + + /* probabilitySets is a dictionary whose keys are roles and whose values + are mutable probability sets, whose values are ship keys. + */ + + rolesAndWeights = OOParseRolesFromString(roles); + for (roleEnum = [rolesAndWeights keyEnumerator]; (role = [roleEnum nextObject]); ) + { + probSet = [probabilitySets objectForKey:role]; + if (probSet == nil) + { + probSet = [OOMutableProbabilitySet probabilitySet]; + [probabilitySets setObject:probSet forKey:role]; + } + + [probSet setWeight:[rolesAndWeights floatForKey:role] forObject:shipKey]; + } +} + + +- (NSDictionary *) canonicalizeSubentityDeclaration:(id)declaration + forShip:(NSString *)shipKey + shipData:(NSDictionary *)shipData + fatalError:(BOOL *)outFatalError +{ + NSDictionary *result = nil; + + assert(outFatalError != NULL); + *outFatalError = NO; + + if ([declaration isKindOfClass:[NSString class]]) + { + // Update old-style string-based declaration. + result = [self translateOldStyleSubentityDeclaration:declaration + forShip:shipKey + shipData:shipData + fatalError:outFatalError]; + + // TODO: remove + result = [self validateNewStyleSubentityDeclaration:result + forShip:shipKey + fatalError:outFatalError]; + } + else if ([declaration isKindOfClass:[NSDictionary class]]) + { + // Validate dictionary-based declaration. + result = [self validateNewStyleSubentityDeclaration:declaration + forShip:shipKey + fatalError:outFatalError]; + } + else + { + OOLog(@"shipData.load.error.badSubentity", @"***** ERROR: subentity declaration for ship %@ should be string or dictionary, found %@.", shipKey, [declaration class]); + *outFatalError = YES; + } + + // For frangible ships, bad subentities are non-fatal. + if (*outFatalError && [[shipData dictionaryForKey:shipKey] boolForKey:@"frangible"]) *outFatalError = NO; + + return result; +} + + +- (NSDictionary *) translateOldStyleSubentityDeclaration:(NSString *)declaration + forShip:(NSString *)shipKey + shipData:(NSDictionary *)shipData + fatalError:(BOOL *)outFatalError +{ + NSArray *tokens = nil; + NSString *subentityKey = nil; + BOOL isFlasher; + + tokens = ScanTokensFromString(declaration); + + subentityKey = [tokens objectAtIndex:0]; + isFlasher = [subentityKey isEqualToString:@"*FLASHER*"]; + + // Sanity check: require eight tokens. + if ([tokens count] != 8) + { + if (!isFlasher) + { + OOLog(@"shipData.load.error.badSubentity", @"***** ERROR: the shipdata.plist entry \"%@\" has a broken subentity definition \"%@\" (should have 8 tokens, has %u).", shipKey, subentityKey, [tokens count]); + *outFatalError = YES; + } + else + { + OOLog(@"shipData.load.warning.badFlasher", @"----- WARNING: the shipdata.plist entry \"%@\" has a broken flasher definition \"%@\" (should have 8 tokens, has %u). This flasher will be ignored.", shipKey, subentityKey, [tokens count]); + } + return nil; + } + + if (isFlasher) + { + return [self translateOldStyleFlasherDeclaration:tokens + forShip:shipKey + fatalError:outFatalError]; + } + else + { + return [self translateOldStandardBasicSubentityDeclaration:tokens + forShip:shipKey + shipData:shipData + fatalError:outFatalError]; + } +} + + +- (NSDictionary *) translateOldStyleFlasherDeclaration:(NSArray *)tokens + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError +{ + Vector position; + float size, frequency, phase, hue; + NSDictionary *colorDict = nil; + NSDictionary *result = nil; + + position.x = [tokens floatAtIndex:1]; + position.y = [tokens floatAtIndex:2]; + position.z = [tokens floatAtIndex:3]; + + hue = [tokens floatAtIndex:4]; + frequency = [tokens floatAtIndex:5]; + phase = [tokens floatAtIndex:6]; + size = [tokens floatAtIndex:7]; + + colorDict = [NSDictionary dictionaryWithObject:[NSNumber numberWithFloat:hue] forKey:@"hue"]; + + result = [NSDictionary dictionaryWithObjectsAndKeys: + @"flasher", @"type", + OOPropertyListFromVector(position), @"position", + [NSArray arrayWithObject:colorDict], @"colors", + [NSNumber numberWithFloat:frequency], @"frequency", + [NSNumber numberWithFloat:phase], @"phase", + [NSNumber numberWithFloat:size], @"size", + nil]; + + OOLog(@"shipData.translateSubentity.flasher", @"Translated flasher declaration \"%@\" to %@", [tokens componentsJoinedByString:@" "], result); + + return result; +} + + +- (NSDictionary *) translateOldStandardBasicSubentityDeclaration:(NSArray *)tokens + forShip:(NSString *)shipKey + shipData:(NSDictionary *)shipData + fatalError:(BOOL *)outFatalError +{ + NSString *subentityKey = nil; + Vector position; + Quaternion orientation; + NSMutableDictionary *result = nil; + BOOL isTurret, isDock = NO; + + subentityKey = [tokens stringAtIndex:0]; + + isTurret = [self shipIsBallTurretForKey:subentityKey inShipData:shipData]; + + position.x = [tokens floatAtIndex:1]; + position.y = [tokens floatAtIndex:2]; + position.z = [tokens floatAtIndex:3]; + + orientation.w = [tokens floatAtIndex:4]; + orientation.x = [tokens floatAtIndex:5]; + orientation.y = [tokens floatAtIndex:6]; + orientation.z = [tokens floatAtIndex:7]; + + quaternion_normalize(&orientation); + + if (!isTurret) + { + isDock = [subentityKey rangeOfString:@"dock"].location != NSNotFound; + } + + result = [NSMutableDictionary dictionaryWithCapacity:5]; + [result setObject:isTurret ? @"ball_turret" : @"standard" forKey:@"type"]; + [result setObject:subentityKey forKey:@"subentity_key"]; + [result setVector:position forKey:@"position"]; + [result setQuaternion:orientation forKey:@"orientation"]; + if (isDock) [result setBool:YES forKey:@"is_dock"]; + + OOLog(@"shipData.translateSubentity.standard", @"Translated subentity declaration \"%@\" to %@", [tokens componentsJoinedByString:@" "], result); + + return [[result copy] autorelease]; +} + + +- (NSDictionary *) validateNewStyleSubentityDeclaration:(NSDictionary *)declaration + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError +{ + NSString *type = nil; + + type = [declaration stringForKey:@"type"]; + if (type == nil) type = @"standard"; + + if ([type isEqualToString:@"flasher"]) + { + return [self validateNewStyleFlasherDeclaration:declaration forShip:shipKey fatalError:outFatalError]; + } + else if ([type isEqualToString:@"standard"] || [type isEqualToString:@"ball_turret"]) + { + return [self validateNewStyleStandardSubentityDeclaration:declaration forShip:shipKey fatalError:outFatalError]; + } + else + { + OOLog(@"shipData.load.error.badSubentity", @"***** ERROR: subentity declaration for ship %@ does not declare a valid type (must be standard, flasher or ball_turret).", shipKey); + *outFatalError = YES; + return nil; + } +} + + +- (NSDictionary *) validateNewStyleFlasherDeclaration:(NSDictionary *)declaration + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError +{ + NSMutableDictionary *result = nil; + Vector position = kZeroVector; + NSArray *colors = nil; + id colorDesc = nil; + float size, frequency, phase; + BOOL initiallyOn; + + // "Validate" is really "clean up", since all values have defaults. + colors = [declaration arrayForKey:@"colors"]; + if ([colors count] == 0) + { + colorDesc = [declaration objectForKey:@"color"]; + if (colorDesc == nil) colorDesc = @"redColor"; + colors = [NSArray arrayWithObject:colorDesc]; + } + + position = [declaration vectorForKey:@"position"]; + + size = [declaration floatForKey:@"size" defaultValue:8.0]; + + if (size <= 0) + { + OOLog(@"shipData.load.warning.flasher.badSize", @"----- WARNING: skipping flasher of invalid size %g for ship %@.", size, shipKey); + return nil; + } + + frequency = [declaration floatForKey:@"frequency" defaultValue:2.0]; + phase = [declaration floatForKey:@"phase" defaultValue:0.0]; + + initiallyOn = [declaration boolForKey:@"initially_on" defaultValue:NO]; + + result = [NSMutableDictionary dictionaryWithCapacity:7]; + [result setObject:@"flasher" forKey:@"type"]; + [result setObject:colors forKey:@"colors"]; + [result setVector:position forKey:@"position"]; + [result setObject:[NSNumber numberWithFloat:size] forKey:@"size"]; + [result setObject:[NSNumber numberWithFloat:frequency] forKey:@"frequency"]; + if (phase != 0) [result setObject:[NSNumber numberWithFloat:phase] forKey:@"phase"]; + if (initiallyOn) [result setObject:[NSNumber numberWithBool:YES] forKey:@"initially_on"]; + + return [[result copy] autorelease]; +} + + +- (NSDictionary *) validateNewStyleStandardSubentityDeclaration:(NSDictionary *)declaration + forShip:(NSString *)shipKey + fatalError:(BOOL *)outFatalError +{ + NSMutableDictionary *result = nil; + NSString *subentityKey = nil; + Vector position = kZeroVector; + Quaternion orientation = kIdentityQuaternion; + BOOL isTurret; + BOOL isDock = NO; + float fireRate = 0.5f; + + subentityKey = [declaration objectForKey:@"subentity_key"]; + if (subentityKey == nil) + { + OOLog(@"shipData.load.error.badSubentity", @"***** ERROR: subentity declaration for ship %@ specifies no subentity_key.", shipKey); + *outFatalError = YES; + return nil; + } + + isTurret = [[declaration stringForKey:@"type"] isEqualToString:@"ball_turret"]; + if (isTurret) + { + fireRate = [declaration floatForKey:@"fire_rate" defaultValue:0.5f]; + if (fireRate < 0.25f) + { + OOLog(@"shipData.load.warning.turret.badFireRate", @"----- WARNING: ball turret fire rate of %g for subenitity of ship %@ is invalid, using 0.25.", fireRate, shipKey); + fireRate = 0.25f; + } + } + else + { + isDock = [declaration boolForKey:@"is_dock"]; + } + + position = [declaration vectorForKey:@"position"]; + orientation = [declaration quaternionForKey:@"orientation"]; + quaternion_normalize(&orientation); + + result = [NSMutableDictionary dictionaryWithCapacity:5]; + [result setObject:isTurret ? @"ball_turret" : @"standard" forKey:@"type"]; + [result setObject:subentityKey forKey:@"subentity_key"]; + [result setVector:position forKey:@"position"]; + [result setQuaternion:orientation forKey:@"orientation"]; + if (isDock) [result setBool:YES forKey:@"is_dock"]; + if (isTurret) [result setFloat:fireRate forKey:@"fire_rate"]; + + return [[result copy] autorelease]; +} + + +- (BOOL) shipIsBallTurretForKey:(NSString *)shipKey inShipData:(NSDictionary *)shipData +{ + // Test for presence of setup_actions containing initialiseTurret. + NSArray *setupActions = nil; + NSEnumerator *actionEnum = nil; + NSString *action = nil; + + setupActions = [[shipData dictionaryForKey:shipKey] arrayForKey:@"setup_actions"]; + + for (actionEnum = [setupActions objectEnumerator]; (action = [actionEnum nextObject]); ) + { + if ([[ScanTokensFromString(action) objectAtIndex:0] isEqualToString:@"initialiseTurret"]) return YES; + } + + return NO; +} + +@end + + +@implementation OOShipRegistry (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedRegistry above. + + NOTE: assumes single-threaded access. +*/ + ++ (id) allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id) copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id) retain +{ + return self; +} + + +- (OOUInteger) retainCount +{ + return UINT_MAX; +} + + +- (void) release +{} + + +- (id) autorelease +{ + return self; +} + +@end diff --git a/src/Core/OOSkyDrawable.h b/src/Core/OOSkyDrawable.h new file mode 100644 index 00000000..6dabfd96 --- /dev/null +++ b/src/Core/OOSkyDrawable.h @@ -0,0 +1,75 @@ +/* + +OOSkyDrawable.h + +Drawable for the sky (i.e., the surrounding space, not planetary atmosphere). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OODrawable.h" +#import "OOOpenGL.h" + +@class OOColor, OOTexture; + + +@interface OOSkyDrawable: OODrawable +{ + unsigned _starCount; + unsigned _nebulaCount; + + NSMutableArray *_quadSets; + + GLint _displayListName; +} + +- (id)initWithColor1:(OOColor *)color1 + Color2:(OOColor *)color2 + starCount:(unsigned)starCount + nebulaCount:(unsigned)nebulaCount + clusterFactor:(float)nebulaClusterFactor + alpha:(float)nebulaAlpha + scale:(float)nebulaScale; + +@end diff --git a/src/Core/OOSkyDrawable.m b/src/Core/OOSkyDrawable.m new file mode 100644 index 00000000..1d48c27b --- /dev/null +++ b/src/Core/OOSkyDrawable.m @@ -0,0 +1,665 @@ +/* + +OOSkyDrawable.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSkyDrawable.h" +#import "ResourceManager.h" +#import "OOTexture.h" +#import "GameController.h" +#import "OOColor.h" +#import "OOProbabilisticTextureManager.h" +#import "OOGraphicsResetManager.h" +#import "Universe.h" +#import "OOMacroOpenGL.h" + + +#define SKY_ELEMENT_SCALE_FACTOR (BILLBOARD_DEPTH / 500.0f) +#define NEBULA_SHUFFLE_FACTOR 0.005f + +BOOL gSkyWireframe = NO; + + +/* Min and max coords are 0 and 1 normally, but the default + sky-render-inset-coords can be used to modify them slightly as an attempted + work-around for artefacts on buggy S3/Via renderers. +*/ +static float sMinTexCoord = 0.0f, sMaxTexCoord = 1.0f; +static BOOL sInited = NO; + + +/* Struct used to describe quads initially. This form is optimized for + reasoning about. +*/ +typedef struct OOSkyQuadDesc +{ + Vector corners[4]; + OOColor *color; + OOTexture *texture; +} OOSkyQuadDesc; + + +enum +{ + kSkyQuadSetPositionEntriesPerVertex = 3, + kSkyQuadSetTexCoordEntriesPerVertex = 2, + kSkyQuadSetColorEntriesPerVertex = 4 +}; + + +/* Class containing a set of quads with the same texture. This form is + optimized for rendering. +*/ +@interface OOSkyQuadSet: NSObject +{ + OOTexture *_texture; + unsigned _count; + GLfloat *_positions; // 3 entries per vertex, 12 per quad + GLfloat *_texCoords; // 2 entries per vertex, 8 per quad + GLfloat *_colors; // 4 entries per vertex, 16 per quad +} + ++ (void)addQuads:(OOSkyQuadDesc *)quads count:(unsigned)count toArray:(NSMutableArray *)ioArray; + +- (id)initWithQuadsWithTexture:(OOTexture *)texture inArray:(OOSkyQuadDesc *)array count:(unsigned)totalCount; + +- (void)render; + +@end + + +/* Textures are global because there is always a sky, but the sky isn't + replaced very often, so the textures are likely to fall out of the cache. +*/ +static OOProbabilisticTextureManager *sStarTextures; +static OOProbabilisticTextureManager *sNebulaTextures; + + +static OOColor *SaturatedColorInRange(OOColor *color1, OOColor *color2); + + +@interface OOSkyDrawable (OOPrivate) + +- (void)setUpStarsWithColor1:(OOColor *)color1 color2:(OOColor *)color2; +- (void)setUpNebulaeWithColor1:(OOColor *)color1 + color2:(OOColor *)color2 + clusterFactor:(float)nebulaClusterFactor + alpha:(float)nebulaAlpha + scale:(float)nebulaScale; + + +- (void)loadStarTextures; +- (void)loadNebulaTextures; + +- (void)addQuads:(OOSkyQuadDesc *)quads count:(unsigned)count; + +- (void)ensureTexturesLoaded; + +@end + + +@implementation OOSkyDrawable + +- (id)initWithColor1:(OOColor *)color1 + Color2:(OOColor *)color2 + starCount:(unsigned)starCount + nebulaCount:(unsigned)nebulaCount + clusterFactor:(float)nebulaClusterFactor + alpha:(float)nebulaAlpha + scale:(float)nebulaScale +{ + NSAutoreleasePool *pool = nil; + + if (!sInited) + { + sInited = YES; + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"sky-render-inset-coords"]) + { + sMinTexCoord += 1.0f/128.0f; + sMaxTexCoord -= 1.0f/128.0f; + } + } + + self = [super init]; + if (self == nil) return nil; + + _starCount = starCount; + _nebulaCount = nebulaCount; + + pool = [[NSAutoreleasePool alloc] init]; + [self setUpStarsWithColor1:color1 color2:color2]; + + if (![UNIVERSE reducedDetail]) + { + [self setUpNebulaeWithColor1:color1 + color2:color2 + clusterFactor:nebulaClusterFactor + alpha:nebulaAlpha + scale:nebulaScale]; + } + [pool release]; + + [[OOGraphicsResetManager sharedManager] registerClient:self]; + + return self; +} + + +- (void)dealloc +{ + OO_ENTER_OPENGL(); + + [_quadSets release]; + [[OOGraphicsResetManager sharedManager] unregisterClient:self]; + if (_displayListName != 0) glDeleteLists(_displayListName, 1); + + [super dealloc]; +} + + +- (void)renderOpaqueParts +{ + // While technically translucent, the sky doesn't need to be depth-sorted + // since it'll be behind everything else anyway. + + OO_ENTER_OPENGL(); + + glDisable(GL_LIGHTING); + glDisable(GL_DEPTH_TEST); // don't read the depth buffer + glDepthMask(GL_FALSE); // don't write to depth buffer + glDisable(GL_CULL_FACE); + glDisable(GL_FOG); + + if (_displayListName != 0) + { + glCallList(_displayListName); + } + else + { + // Set up display list + [self ensureTexturesLoaded]; + _displayListName = glGenLists(1); + glNewList(_displayListName, GL_COMPILE); + + glEnable(GL_TEXTURE_2D); + glBlendFunc(GL_ONE, GL_ONE); // Pure additive blending, ignoring alpha + + glDisableClientState(GL_INDEX_ARRAY); + glDisableClientState(GL_NORMAL_ARRAY); + glDisableClientState(GL_EDGE_FLAG_ARRAY); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + + [_quadSets makeObjectsPerformSelector:@selector(render)]; + + glDisable(GL_TEXTURE_2D); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Basic alpha blending + + glEndList(); + } + + // Restore state + glEnable(GL_CULL_FACE); + glEnable(GL_DEPTH_TEST); // read the depth buffer + glDepthMask(GL_TRUE); // restore write to depth buffer + glEnable(GL_LIGHTING); +} + + +- (BOOL)hasOpaqueParts +{ + return YES; +} + + +- (GLfloat)maxDrawDistance +{ + return INFINITY; +} + +@end + + +@implementation OOSkyDrawable (OOPrivate) + +- (void)setUpStarsWithColor1:(OOColor *)color1 color2:(OOColor *)color2 +{ + OOSkyQuadDesc *quads = NULL, *currQuad = NULL; + unsigned i; + Quaternion q; + Vector vi, vj, vk; + float size; + Vector middle, offset; + + [self loadStarTextures]; + + quads = malloc(sizeof *quads * _starCount); + if (quads == NULL) return; + + currQuad = quads; + for (i = 0; i != _starCount; ++i) + { + // Select colour and texture. + currQuad->color = [color1 blendedColorWithFraction:randf() ofColor:color2]; + currQuad->texture = [sStarTextures selectTexture]; // Not retained, since sStarTextures is never released. + + // Select a direction and rotation. + q = OORandomQuaternion(); + basis_vectors_from_quaternion(q, &vi, &vj, &vk); + + // Select scale; calculate centre position and offset to first corner. + size = (1 + (ranrot_rand() % 6)) * SKY_ELEMENT_SCALE_FACTOR / 2; // Halve star size, looks a + // bit better - Nikos 20081031 + middle = vector_multiply_scalar(vk, BILLBOARD_DEPTH); + offset = vector_multiply_scalar(vector_add(vi, vj), 0.5f * size); + + // Scale the "side" vectors. + Vector vj2 = vector_multiply_scalar(vj, size); + Vector vi2 = vector_multiply_scalar(vi, size); + + // Set up corners. + currQuad->corners[0] = vector_subtract(middle, offset); + currQuad->corners[1] = vector_add(currQuad->corners[0], vj2); + currQuad->corners[2] = vector_add(currQuad->corners[1], vi2); + currQuad->corners[3] = vector_add(currQuad->corners[0], vi2); + + ++currQuad; + } + + [self addQuads:quads count:_starCount]; + free(quads); +} + + +- (void)setUpNebulaeWithColor1:(OOColor *)color1 + color2:(OOColor *)color2 + clusterFactor:(float)nebulaClusterFactor + alpha:(float)nebulaAlpha + scale:(float)nebulaScale +{ + OOSkyQuadDesc *quads = NULL, *currQuad = NULL; + unsigned i, actualCount = 0, clusters = 0; + OOColor *color; + Quaternion q; + Vector vi, vj, vk; + double size, r2; + Vector middle, offset; + int r1; + + [self loadNebulaTextures]; + + quads = malloc(sizeof *quads * _nebulaCount); + if (quads == NULL) return; + + currQuad = quads; + for (i = 0; i < _nebulaCount; ++i) + { + color = SaturatedColorInRange(color1, color2); + + // Select a direction and rotation. + q = OORandomQuaternion(); + + // Create a cluster of nebula quads. + while ((i < _nebulaCount) && (randf() < nebulaClusterFactor)) + { + // Select size. + r1 = 1 + (ranrot_rand() & 15); + size = nebulaScale * r1 * SKY_ELEMENT_SCALE_FACTOR; + + // Select colour and texture. Smaller nebula quads are dimmer. + currQuad->color = [color colorWithBrightnessFactor:nebulaAlpha * (0.5f + (float)r1 / 32.0f)]; + currQuad->texture = [sNebulaTextures selectTexture]; // Not retained, since sStarTextures is never released. + + // Calculate centre position and offset to first corner. + basis_vectors_from_quaternion(q, &vi, &vj, &vk); + middle = vector_multiply_scalar(vk, BILLBOARD_DEPTH); + offset = vector_multiply_scalar(vector_add(vi, vj), 0.5f * size); + + // Rotate vi and vj by a random angle + r2 = randf() * M_PI * 2.0; + quaternion_rotate_about_axis(&q, vk, r2); + vi = vector_right_from_quaternion(q); + vj = vector_up_from_quaternion(q); + + #if 1 + // Scale the "side" vectors. + vj = vector_multiply_scalar(vj, size); + vi = vector_multiply_scalar(vi, size); + + // Set up corners. + currQuad->corners[0] = vector_subtract(middle, offset); + currQuad->corners[1] = vector_add(currQuad->corners[0], vj); + currQuad->corners[2] = vector_add(currQuad->corners[1], vi); + currQuad->corners[3] = vector_add(currQuad->corners[0], vi); + #else + // Set up corners. + vj = vector_multiply_scalar(vj, size); + Vector vi2 = vector_multiply_scalar(vi, size); + currQuad->corners[0] = vector_subtract(middle, offset); + currQuad->corners[1] = vector_add(currQuad->corners[0], vj); + currQuad->corners[2] = vector_add(currQuad->corners[1], vi2); + currQuad->corners[3] = vector_add(currQuad->corners[0], vi2); + #endif + + // Shuffle direction quat around a bit to spread the cluster out. + size = NEBULA_SHUFFLE_FACTOR / (nebulaScale * SKY_ELEMENT_SCALE_FACTOR); + q.x += size * (randf() - 0.5); + q.y += size * (randf() - 0.5); + q.z += size * (randf() - 0.5); + q.w += size * (randf() - 0.5); + quaternion_normalize(&q); + + ++i; + ++currQuad; + ++actualCount; + } + ++clusters; + } + + /* The above code generates less than _nebulaCount quads, because i is + incremented once in the outer loop as well as in the inner loop. To + keep skies looking the same, we leave the bug in and fill in the + actual generated count here. + */ + _nebulaCount = actualCount; + + [self addQuads:quads count:_nebulaCount]; + free(quads); +} + + +- (void)addQuads:(OOSkyQuadDesc *)quads count:(unsigned)count +{ + if (_quadSets == nil) _quadSets = [[NSMutableArray alloc] init]; + + [OOSkyQuadSet addQuads:quads count:count toArray:_quadSets]; +} + + +- (void)loadStarTextures +{ + if (sStarTextures == nil) + { + sStarTextures = [[OOProbabilisticTextureManager alloc] + initWithPListName:@"startextures.plist" + options:kOOTextureDefaultOptions + anisotropy:0.0f + lodBias:-0.6f]; + if (sStarTextures == nil) + { + [NSException raise:OOLITE_EXCEPTION_DATA_NOT_FOUND format:@"No star textures could be loaded."]; + } + } + + [sStarTextures setSeed:RANROTGetFullSeed()]; + +} + + +- (void)loadNebulaTextures +{ + if (sNebulaTextures == nil) + { + sNebulaTextures = [[OOProbabilisticTextureManager alloc] + initWithPListName:@"nebulatextures.plist" + options:kOOTextureDefaultOptions + anisotropy:0.0f + lodBias:0.0f]; + if (sNebulaTextures == nil) + { + [NSException raise:OOLITE_EXCEPTION_DATA_NOT_FOUND format:@"No nebula textures could be loaded."]; + } + } + + [sNebulaTextures setSeed:RANROTGetFullSeed()]; + +} + + +- (void)ensureTexturesLoaded +{ + [sStarTextures ensureTexturesLoaded]; + [sNebulaTextures ensureTexturesLoaded]; +} + + +- (void)resetGraphicsState +{ + OO_ENTER_OPENGL(); + + if (_displayListName != 0) + { + glDeleteLists(_displayListName, 1); + _displayListName = 0; + } +} + +@end + + +@implementation OOSkyQuadSet + ++ (void)addQuads:(OOSkyQuadDesc *)quads count:(unsigned)count toArray:(NSMutableArray *)ioArray +{ + NSMutableSet *seenTextures = nil; + OOTexture *texture = nil; + OOSkyQuadSet *quadSet = nil; + unsigned i; + + // Iterate over all quads. + seenTextures = [NSMutableSet set]; + for (i = 0; i != count; ++i) + { + texture = quads[i].texture; + + // If we haven't seen this quad's texture before... + if (![seenTextures containsObject:texture]) + { + [seenTextures addObject:texture]; + + // ...create a quad set for this texture. + quadSet = [[self alloc] initWithQuadsWithTexture:texture + inArray:quads + count:count]; + if (quadSet != nil) + { + [ioArray addObject:quadSet]; + [quadSet release]; + } + } + } +} + + +- (id)initWithQuadsWithTexture:(OOTexture *)texture inArray:(OOSkyQuadDesc *)array count:(unsigned)totalCount +{ + BOOL OK = YES; + unsigned i, j, vertexCount; + GLfloat *pos; + GLfloat *tc; + GLfloat *col; + GLfloat r, g, b; + size_t posSize, tcSize, colSize; + unsigned count = 0; + + self = [super init]; + if (self == nil) OK = NO; + + if (OK) + { + // Count the quads in the array using this texture. + for (i = 0; i != totalCount; ++i) + { + if (array[i].texture == texture) ++_count; + } + if (_count == 0) OK = NO; + } + + if (OK) + { + // Allocate arrays. + vertexCount = _count * 4; + posSize = sizeof *_positions * vertexCount * kSkyQuadSetPositionEntriesPerVertex; + tcSize = sizeof *_texCoords * vertexCount * kSkyQuadSetTexCoordEntriesPerVertex; + colSize = sizeof *_colors * vertexCount * kSkyQuadSetColorEntriesPerVertex; + + _positions = malloc(posSize); + _texCoords = malloc(tcSize); + _colors = malloc(colSize); + + if (_positions == NULL || _texCoords == NULL || _colors == NULL) OK = NO; + + pos = _positions; + tc = _texCoords; + col = _colors; + } + + if (OK) + { + // Find the matching quads again, and convert them to renderable representation. + for (i = 0; i != totalCount; ++i) + { + if (array[i].texture == texture) + { + r = [array[i].color redComponent]; + g = [array[i].color greenComponent]; + b = [array[i].color blueComponent]; + + // Loop over vertices + for (j = 0; j != 4; ++j) + { + *pos++ = array[i].corners[j].x; + *pos++ = array[i].corners[j].y; + *pos++ = array[i].corners[j].z; + + // Colour is the same for each vertex + *col++ = r; + *col++ = g; + *col++ = b; + *col++ = 1.0f; // Alpha is unused but needs to be there + } + + // Texture co-ordinates are the same for each quad. + *tc++ = sMinTexCoord; + *tc++ = sMinTexCoord; + + *tc++ = sMaxTexCoord; + *tc++ = sMinTexCoord; + + *tc++ = sMaxTexCoord; + *tc++ = sMaxTexCoord; + + *tc++ = sMinTexCoord; + *tc++ = sMaxTexCoord; + + count++; + } + } + + _texture = [texture retain]; + OOLog(@"sky.setup", @"Generated quadset with %u quads for texture %@", count, _texture); + } + + if (!OK) + { + [self release]; + self = nil; + } + + return self; +} + + +- (void)dealloc +{ + [_texture release]; + + if (_positions != NULL) free(_positions); + if (_texCoords != NULL) free(_texCoords); + if (_colors != NULL) free(_colors); + + [super dealloc]; +} + + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@ %p>{%u quads, texture: %@}", [self class], self, _count, _texture]; +} + + +- (void)render +{ + OO_ENTER_OPENGL(); + + [_texture apply]; + + glVertexPointer(kSkyQuadSetPositionEntriesPerVertex, GL_FLOAT, 0, _positions); + glTexCoordPointer(kSkyQuadSetTexCoordEntriesPerVertex, GL_FLOAT, 0, _texCoords); + glColorPointer(kSkyQuadSetColorEntriesPerVertex, GL_FLOAT, 0, _colors); + + glDrawArrays(GL_QUADS, 0, 4 * _count); +} + +@end + + +static OOColor *SaturatedColorInRange(OOColor *color1, OOColor *color2) +{ + OOColor *color = nil; + OOCGFloat hue, saturation, brightness, alpha; + + color = [color1 blendedColorWithFraction:randf() ofColor:color2]; + [color getHue:&hue saturation:&saturation brightness:&brightness alpha:&alpha]; + + saturation = 0.5 * saturation + 0.5; // move saturation up a notch! + + /* NOTE: this changes the hue, because getHue:... produces hue values + in [0, 360], but colorWithCalibratedHue:... takes hue values in + [0, 1]. + */ + return [OOColor colorWithCalibratedHue:hue saturation:saturation brightness:brightness alpha:alpha]; +} diff --git a/src/Core/OOSound.h b/src/Core/OOSound.h new file mode 100644 index 00000000..689727b5 --- /dev/null +++ b/src/Core/OOSound.h @@ -0,0 +1,57 @@ +/* + +OOSound.h + +Dispatch header to select the appropriate implementation of OOSound. + +Add new OS imports here. The -DOS_NAME flag in the GNUmakefile +will select which one gets compiled. + + +== Overview of Oolite sound architecture == +There are four public sound classes: +* OOSound: represents a sound, i.e. some data that can be played. +* OOMusic: subclass of OOSound with support for looping, and the special + constraint that only one OOMusic may play at a time. +* OOSoundSource: a thing that can play a sound. Each sound played is + conceptually played through a sound source, although this can be + implicit using OOSound's -play method. +* OOSoundReferencePoint: a point in space relative to which a sound source is + positioned. Since positional sound is not implemented, this serves + no practical purpose. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + + +#if OOLITE_SDL + #import "OOSDLSound.h" + #import "SDLMusic.h" + #import "OOBasicSoundReferencePoint.h" +#else + #import "OOCASound.h" + #import "OOCAMusic.h" + #import "OOCASoundReferencePoint.h" +#endif + +#import "OOSoundSource.h" diff --git a/src/Core/OOSoundInternal.h b/src/Core/OOSoundInternal.h new file mode 100644 index 00000000..96a46eba --- /dev/null +++ b/src/Core/OOSoundInternal.h @@ -0,0 +1,21 @@ +#import "OOSound.h" + +#if OOLITE_SDL + +#import "OOSDLSoundMixer.h" +#import "OOSDLSoundChannel.h" + +#define OOSoundAcquireLock() do {} while(0) +#define OOSoundReleaseLock() do {} while(0) + +#elif OOLITE_MAC_OS_X + +#import "OOCASoundMixer.h" +#import "OOCASoundChannel.h" + +extern NSRecursiveLock *gOOCASoundSyncLock; + +#define OOSoundAcquireLock() [gOOCASoundSyncLock lock] +#define OOSoundReleaseLock() [gOOCASoundSyncLock unlock] + +#endif diff --git a/src/Core/OOSoundSource.h b/src/Core/OOSoundSource.h new file mode 100644 index 00000000..bc708efe --- /dev/null +++ b/src/Core/OOSoundSource.h @@ -0,0 +1,98 @@ +/* + +OOSoundSource.h + +A sound source. +Each playing sound is associated with a sound source, either explicitly or by +creating one on the fly. Each sound source can play one sound at a time, and +has a number of attributes related to positional audio (which is currently +unimplemented). + +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR + +*/ + +#import "OOSoundSource.h" +#import "OOMaths.h" + +@class OOSound, OOSoundChannel, OOSoundReferencePoint; + + +@interface OOSoundSource: NSObject +{ + OOSound *_sound; + OOSoundChannel *_channel; + BOOL _loop; + uint8_t _repeatCount, + _remainingCount; +} + ++ (id) sourceWithSound:(OOSound *)inSound; +- (id) initWithSound:(OOSound *)inSound; + +// These options should be set before playing. Effect of setting them while playing is undefined. +- (OOSound *) sound; +- (void )setSound:(OOSound *)inSound; +- (BOOL) loop; +- (void) setLoop:(BOOL)inLoop; +- (uint8_t) repeatCount; +- (void) setRepeatCount:(uint8_t)inCount; + +- (BOOL) isPlaying; +- (void) play; +- (void) playOrRepeat; +- (void) stop; + +// Conveniences: +- (void) playSound:(OOSound *)inSound; +- (void) playSound:(OOSound *)inSound repeatCount:(uint8_t)inCount; +- (void) playOrRepeatSound:(OOSound *)inSound; + +// Positional audio attributes are ignored in this implementation +- (void) setPositional:(BOOL)inPositional; +- (void) setPosition:(Vector)inPosition; +- (void) setVelocity:(Vector)inVelocity; +- (void) setOrientation:(Vector)inOrientation; +- (void) setConeAngle:(float)inAngle; +- (void) setGainInsideCone:(float)inInside outsideCone:(float)inOutside; +- (void) positionRelativeTo:(OOSoundReferencePoint *)inPoint; + +@end diff --git a/src/Core/OOSoundSource.m b/src/Core/OOSoundSource.m new file mode 100644 index 00000000..45c15243 --- /dev/null +++ b/src/Core/OOSoundSource.m @@ -0,0 +1,275 @@ +/* + +OOSoundSource.m + +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSoundInternal.h" +#import "OOLogging.h" + + +@implementation OOSoundSource + ++ (id) sourceWithSound:(OOSound *)inSound +{ + return [[[self alloc] initWithSound:inSound] autorelease]; +} + + +- (id) initWithSound:(OOSound *)inSound +{ + self = [self init]; + if (!self) return nil; + + [self setSound:inSound]; + + return self; +} + + +- (void) dealloc +{ + [self stop]; + [_sound autorelease]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + if ([self isPlaying]) + { + return [NSString stringWithFormat:@"sound=%@, loop=%s, repeatCount=%u, playing on channel %@", _sound, [self loop] ? "YES" : "NO", [self repeatCount], _channel]; + } + else + { + return [NSString stringWithFormat:@"sound=%@, loop=%s, repeatCount=%u, not playing", _sound, [self loop] ? "YES" : "NO", [self repeatCount]]; + } +} + + +- (OOSound *) sound +{ + return _sound; +} + + +- (void) setSound:(OOSound *)sound +{ + if (_sound != sound) + { + [_sound autorelease]; + _sound = [sound retain]; + } +} + + +- (BOOL) loop +{ + return _loop; +} + + +- (void) setLoop:(BOOL)loop +{ + _loop = !!loop; +} + + +- (uint8_t) repeatCount +{ + return _repeatCount ? _repeatCount : 1; +} + + +- (void) setRepeatCount:(uint8_t)count +{ + _repeatCount = count; +} + + +- (BOOL) isPlaying +{ + return _channel != nil; +} + + +- (void)play +{ + if ([self sound] == nil) return; + + OOSoundAcquireLock(); + + if (_channel) [self stop]; + + _channel = [[OOSoundMixer sharedMixer] popChannel]; + if (nil != _channel) + { + _remainingCount = [self repeatCount]; + [_channel setDelegate:self]; + [_channel playSound:[self sound] looped:[self loop]]; + [self retain]; + } + + OOSoundReleaseLock(); +} + + +- (void) playOrRepeat +{ + if (![self isPlaying]) [self play]; + else ++_remainingCount; +} + + +- (void)stop +{ + OOSoundAcquireLock(); + + if (nil != _channel) + { + [_channel setDelegate:[self class]]; + [_channel stop]; + _channel = nil; + [self release]; + } + + OOSoundReleaseLock(); +} + + +- (void) playSound:(OOSound *)sound +{ + [self playSound:sound repeatCount:_repeatCount]; +} + + +- (void) playSound:(OOSound *)sound repeatCount:(uint8_t)count +{ + [self stop]; + [self setSound:sound]; + [self setRepeatCount:count]; + [self play]; +} + + +- (void) playOrRepeatSound:(OOSound *)sound +{ + if (_sound != sound) [self playSound:sound]; + else [self playOrRepeat]; +} + + +- (void) setPositional:(BOOL)inPositional +{ + +} + + +- (void) setPosition:(Vector)inPosition +{ + +} + + +- (void) setVelocity:(Vector)inVelocity +{ + +} + + +- (void) setOrientation:(Vector)inOrientation +{ + +} + + +- (void) setConeAngle:(float)inAngle +{ + +} + + +- (void) setGainInsideCone:(float)inInside outsideCone:(float)inOutside +{ + +} + + +- (void) positionRelativeTo:(OOSoundReferencePoint *)inPoint +{ + +} + + +// OOSoundChannelDelegate +- (void)channel:(OOSoundChannel *)channel didFinishPlayingSound:(OOSound *)sound +{ + assert(_channel == channel); + + OOSoundAcquireLock(); + + if (--_remainingCount) + { + [_channel playSound:[self sound] looped:NO]; + } + else + { + [_channel setDelegate:nil]; + [[OOSoundMixer sharedMixer] pushChannel:_channel]; + _channel = nil; + [self release]; + } + OOSoundReleaseLock(); +} + + ++ (void)channel:(OOSoundChannel *)inChannel didFinishPlayingSound:(OOSound *)inSound +{ + // This delegate is used for a stopped source + [[OOSoundMixer sharedMixer] pushChannel:inChannel]; +} + +@end diff --git a/src/Core/OOSoundSourcePool.h b/src/Core/OOSoundSourcePool.h new file mode 100644 index 00000000..4fdc6adb --- /dev/null +++ b/src/Core/OOSoundSourcePool.h @@ -0,0 +1,83 @@ +/* + +OOSoundSourcePool.h + +Manages a fixed number of sound sources and distributes sounds between them. +Each sound has a priority and an expiry time. When a new sound is played, it +replaces (if possible) a sound of lower priority that has expired, a sound of +the same priority that has expired, or a sound of lower priority that has not +expired. + +All sounds are specified by customsounds.plist key. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import +#import "OOTypes.h" + + +@interface OOSoundSourcePool: NSObject +{ + struct OOSoundSourcePoolElement *_sources; + uint8_t _count; + uint8_t _latest; + OOTimeDelta _minRepeat; + OOTimeAbsolute _nextRepeat; + NSString *_lastKey; +} + ++ (id) poolWithCount:(uint8_t)count minRepeatTime:(OOTimeDelta)minRepeat; +- (id) initWithCount:(uint8_t)count minRepeatTime:(OOTimeDelta)minRepeat; + +- (void) playSoundWithKey:(NSString *)key + priority:(float)priority + expiryTime:(OOTimeDelta)expiryTime; + +- (void) playSoundWithKey:(NSString *)key + priority:(float)priority; // expiryTime:0.1 +/- 0.5 + +- (void) playSoundWithKey:(NSString *)key; // priority: 1.0, expiryTime:0.1 +/- 0.5 + +@end diff --git a/src/Core/OOSoundSourcePool.m b/src/Core/OOSoundSourcePool.m new file mode 100644 index 00000000..e864d0a6 --- /dev/null +++ b/src/Core/OOSoundSourcePool.m @@ -0,0 +1,221 @@ +/* + +OOSoundSourcePool.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSoundSourcePool.h" +#import "OOSound.h" +#import "Universe.h" + + +enum +{ + kNoSlot = UINT8_MAX +}; + + +typedef struct OOSoundSourcePoolElement +{ + OOSoundSource *source; + OOTimeAbsolute expiryTime; + float priority; +} PoolElement; + + +@interface OOSoundSourcePool (Private) + +- (uint8_t) selectSlotForPriority:(float)priority; + +@end + + +@implementation OOSoundSourcePool + ++ (id) poolWithCount:(uint8_t)count minRepeatTime:(OOTimeDelta)minRepeat +{ + return [[[self alloc] initWithCount:count minRepeatTime:minRepeat] autorelease]; +} + + +- (id) initWithCount:(uint8_t)count minRepeatTime:(OOTimeDelta)minRepeat +{ + if ((self = [super init])) + { + // Sanity-check count + if (count == 0) count = 1; + if (count == kNoSlot) --count; + _count = count; + + if (minRepeat < 0.0) minRepeat = 0.0; + _minRepeat = minRepeat; + + // Create source pool + _sources = calloc(sizeof(PoolElement), count); + if (_sources == NULL) + { + [self release]; + self = nil; + } + } + return self; +} + + +- (void) dealloc +{ + uint8_t i; + + for (i = 0; i != _count; i++) + { + [_sources[i].source release]; + } + + [_lastKey release]; + + [super dealloc]; +} + + +- (void) playSoundWithKey:(NSString *)key + priority:(float)priority + expiryTime:(OOTimeDelta)expiryTime +{ + uint8_t slot; + OOTimeAbsolute now, absExpiryTime; + PoolElement *element = NULL; + OOSound *sound = NULL; + + // Convert expiry time to absolute + now = [UNIVERSE getTime]; + absExpiryTime = expiryTime + now; + + // Avoid repeats if required + if (now < _nextRepeat && [key isEqualToString:_lastKey]) return; + + // Look for a slot in the source list to use + slot = [self selectSlotForPriority:priority]; + if (slot == kNoSlot) return; + element = &_sources[slot]; + + // Load sound + sound = [OOSound soundWithCustomSoundKey:key]; + if (sound == nil) return; + + // Stop playing sound or set up sound source as appropriate + if (element->source != nil) [element->source stop]; + else + { + element->source = [[OOSoundSource alloc] init]; + if (element->source == nil) return; + } + + // Play and store metadata + [element->source playSound:sound]; + element->expiryTime = absExpiryTime; + element->priority = priority; + if (_minRepeat > 0.0) + { + _nextRepeat = now + _minRepeat; + [_lastKey release]; + _lastKey = [key copy]; + } + + // Set staring search location for next slot lookup + _latest = slot; +} + + +- (void) playSoundWithKey:(NSString *)key + priority:(float)priority +{ + [self playSoundWithKey:key + priority:priority + expiryTime:0.5 + randf() * 0.1]; +} + +- (void) playSoundWithKey:(NSString *)key +{ + [self playSoundWithKey:key priority:1.0]; +} + +@end + + +@implementation OOSoundSourcePool (Private) + +- (uint8_t) selectSlotForPriority:(float)priority +{ + uint8_t curr, count, expiredLower = kNoSlot, unexpiredLower = kNoSlot, expiredEqual = kNoSlot; + PoolElement *element = NULL; + OOTimeAbsolute now = [UNIVERSE getTime]; + +#define NEXT(x) (((x) + 1) % _count) + + curr = _latest; + count = _count; + do + { + curr = NEXT(curr); + element = &_sources[curr]; + + if (element->source == nil || ![element->source isPlaying]) return curr; // Best type of slot: empty + else if (element->priority < priority) + { + if (element->expiryTime <= now) expiredLower = curr; // Second-best type: expired lower-priority + else unexpiredLower = curr; // Third-best type: unexpired lower-priority + } + else if (element->priority == priority && element->expiryTime <= now) + { + expiredEqual = curr; // Fourth-best type: expired equal-priority. + } + } while (--count); + + if (expiredLower != kNoSlot) return expiredLower; + if (unexpiredLower != kNoSlot) return unexpiredLower; + return expiredEqual; // Will be kNoSlot if none found +} + +@end diff --git a/src/Core/OOSpatialReference.h b/src/Core/OOSpatialReference.h new file mode 100644 index 00000000..439ddddf --- /dev/null +++ b/src/Core/OOSpatialReference.h @@ -0,0 +1,43 @@ +/* + +OOSpatialReference.h + +Formal protocol for objects whose transformation matrix (and thus, position +and orientation) can be observed. Currently used for cameras, potentially +useful for stuff like positional audio (instead of OOSoundReferencePoint). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOMaths.h" +#import "Entity.h" + + +@protocol OOSpatialReference + +- (OOMatrix) transformationMatrix; + +@end + + +@interface Entity (OOSpatialReference) +@end diff --git a/src/Core/OOSpatialReference.m b/src/Core/OOSpatialReference.m new file mode 100644 index 00000000..9d1188c5 --- /dev/null +++ b/src/Core/OOSpatialReference.m @@ -0,0 +1,39 @@ +/* + +OOSpatialReference.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOSpatialReference.h" + + +@implementation Entity (OOSpatialReference) + +- (OOMatrix) transformationMatrix +{ + OOMatrix rotate = [self rotationMatrix]; + OOMatrix translate = OOMatrixForTranslation([self position]); + + return OOMatrixMultiply(translate, rotate); +} + +@end diff --git a/src/Core/OOStringParsing.h b/src/Core/OOStringParsing.h new file mode 100644 index 00000000..3a296d10 --- /dev/null +++ b/src/Core/OOStringParsing.h @@ -0,0 +1,110 @@ +/* + +OOStringParsing.h + +Various functions for interpreting values from strings. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOMaths.h" +#import "OOTypes.h" +#import "legacy_random.h" + +@class Entity; + + +NSMutableArray *ScanTokensFromString(NSString *values); + +// Note: these functions will leave their out values untouched if they fail (and return NO). They will not log an error if passed a NULL string (but will return NO). This means they can be used to, say, read dictionary entries which might not exist. They also ignore any extra components in the string. +BOOL ScanVectorFromString(NSString *xyzString, Vector *outVector); +BOOL ScanQuaternionFromString(NSString *wxyzString, Quaternion *outQuaternion); +BOOL ScanVectorAndQuaternionFromString(NSString *xyzwxyzString, Vector *outVector, Quaternion *outQuaternion); + +Vector VectorFromString(NSString *xyzString, Vector defaultValue); +Quaternion QuaternionFromString(NSString *wxyzString, Quaternion defaultValue); + +NSString *StringFromPoint(NSPoint point); +NSPoint PointFromString(NSString *xyString); + +Random_Seed RandomSeedFromString(NSString *abcdefString); +NSString *StringFromRandomSeed(Random_Seed seed); + + +NSString *ExpandDescriptionForSeed(NSString *text, Random_Seed seed); +NSString *ExpandDescriptionForSeedName(NSString *text, Random_Seed seed, NSString *name); +NSString *ExpandDescriptionForCurrentSystem(NSString *text); + +NSString *ExpandDescriptionsWithLocalsForSystemSeed(NSString *text, Random_Seed seed, NSDictionary *locals); +NSString *ExpandDescriptionsWithLocalsForSystemSeedName(NSString *text, Random_Seed seed, NSDictionary *locals, NSString *name); +NSString *ExpandDescriptionsWithLocalsForCurrentSystem(NSString *text, NSDictionary *locals); + +NSString *DescriptionForSystem(Random_Seed seed,NSString *name); +NSString *DescriptionForCurrentSystem(void); + +// target and localVariables are optional; target will default to the player. +NSString *ReplaceVariables(NSString *string, Entity *target, NSDictionary *localVariables); + +NSString *RandomDigrams(void); + + +NSString *OOStringFromDeciCredits(OOCreditsQuantity tenthsOfCredits, BOOL includeDecimal, BOOL includeSymbol); +OOINLINE NSString *OOStringFromIntCredits(OOCreditsQuantity integerCredits, BOOL includeSymbol) +{ + return OOStringFromDeciCredits(integerCredits * 10, NO, includeSymbol); +} + +OOINLINE NSString *OOCredits(OOCreditsQuantity tenthsOfCredits) +{ + return OOStringFromDeciCredits(tenthsOfCredits, YES, YES); +} +OOINLINE NSString *OOIntCredits(OOCreditsQuantity integerCredits) +{ + return OOStringFromIntCredits(integerCredits, YES); +} + +NSString *OOPadStringTo(NSString * string, float numSpaces); + +@interface NSString (OOUtilities) + +// Case-insensitive match of [self pathExtension] +- (BOOL)pathHasExtension:(NSString *)extension; +- (BOOL)pathHasExtensionInArray:(NSArray *)extensions; + +@end + + +// Given a string of the form 1.2.3.4 (with arbitrarily many components), return an array of unsigned ints. +NSArray *ComponentsFromVersionString(NSString *string); + +/* Compare two arrays of unsigned int NSNumbers, as returned by + ComponentsFromVersionString(). + + Components are ordered from most to least significant, and a missing + component is treated as 0. Thus "1.7" < "1.60", and "1.2.3.0" == "1.2.3". +*/ +NSComparisonResult CompareVersions(NSArray *version1, NSArray *version2); + + +NSString *ClockToString(double clock, BOOL adjusting); + + +NSString *EscapedGraphVizString(NSString *string); diff --git a/src/Core/OOStringParsing.m b/src/Core/OOStringParsing.m new file mode 100644 index 00000000..5e1c4077 --- /dev/null +++ b/src/Core/OOStringParsing.m @@ -0,0 +1,791 @@ +/* + +OOStringParsing.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOStringParsing.h" +#import "OOLogging.h" +#import "NSScannerOOExtensions.h" +#import "legacy_random.h" +#import "Universe.h" +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "OOFunctionAttributes.h" +#import "OOCollectionExtractors.h" + + +static NSString * const kOOLogStringVectorConversion = @"strings.conversion.vector"; +static NSString * const kOOLogStringQuaternionConversion = @"strings.conversion.quaternion"; +static NSString * const kOOLogStringVecAndQuatConversion = @"strings.conversion.vectorAndQuaternion"; +static NSString * const kOOLogStringRandomSeedConversion = @"strings.conversion.randomSeed"; +static NSString * const kOOLogExpandDescriptionsRecursionLimitExceeded = @"strings.expand.recursionLimit"; +static NSString * const kOOLogDebugReplaceVariablesInString = @"script.debug.replaceVariablesInString"; + +static NSString *OldRandomDigrams(void); +static NSString *NewRandomDigrams(void); + + +NSMutableArray *ScanTokensFromString(NSString *values) +{ + NSMutableArray *result = nil; + NSScanner *scanner = nil; + NSString *token = nil; + static NSCharacterSet *space_set = nil; + + // Note: Shark suggests we're getting a lot of early exits, but testing showed a pretty steady 2% early exit rate. + if (EXPECT_NOT(values == nil)) return [NSArray array]; + if (EXPECT_NOT(space_set == nil)) space_set = [[NSCharacterSet whitespaceAndNewlineCharacterSet] retain]; + + result = [NSMutableArray array]; + scanner = [NSScanner scannerWithString:values]; + + while (![scanner isAtEnd]) + { + [scanner ooliteScanCharactersFromSet:space_set intoString:NULL]; + if ([scanner ooliteScanUpToCharactersFromSet:space_set intoString:&token]) + { + [result addObject:token]; + } + } + + return result; +} + + +BOOL ScanVectorFromString(NSString *xyzString, Vector *outVector) +{ + GLfloat xyz[] = {0.0, 0.0, 0.0}; + int i = 0; + NSString *error = nil; + NSScanner *scanner = nil; + + assert(outVector != NULL); + if (xyzString == nil) return NO; + + if (!error) scanner = [NSScanner scannerWithString:xyzString]; + while (![scanner isAtEnd] && i < 3 && !error) + { + if (![scanner scanFloat:&xyz[i++]]) error = @"could not scan a float value."; + } + + if (!error && i < 3) error = @"found less than three float values."; + + if (!error) + { + *outVector = make_vector(xyz[0], xyz[1], xyz[2]); + return YES; + } + else + { + OOLogERR(kOOLogStringVectorConversion, @"cannot make vector from '%@': %@", xyzString, error); + return NO; + } +} + + +BOOL ScanQuaternionFromString(NSString *wxyzString, Quaternion *outQuaternion) +{ + GLfloat wxyz[] = {1.0, 0.0, 0.0, 0.0}; + int i = 0; + NSString *error = nil; + NSScanner *scanner = nil; + + assert(outQuaternion != NULL); + if (wxyzString == nil) return NO; + + if (!error) scanner = [NSScanner scannerWithString:wxyzString]; + while (![scanner isAtEnd] && i < 4 && !error) + { + if (![scanner scanFloat:&wxyz[i++]]) error = @"could not scan a float value."; + } + + if (!error && i < 4) error = @"found less than four float values."; + + if (!error) + { + outQuaternion->w = wxyz[0]; + outQuaternion->x = wxyz[1]; + outQuaternion->y = wxyz[2]; + outQuaternion->z = wxyz[3]; + quaternion_normalize(outQuaternion); + return YES; + } + else + { + OOLogERR(kOOLogStringQuaternionConversion, @"cannot make quaternion from '%@': %@", wxyzString, error); + return NO; + } +} + +BOOL ScanVectorAndQuaternionFromString(NSString *xyzwxyzString, Vector *outVector, Quaternion *outQuaternion) +{ + GLfloat xyzwxyz[] = { 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0}; + int i = 0; + NSString *error = nil; + NSScanner *scanner = nil; + + assert(outVector != NULL && outQuaternion != NULL); + if (xyzwxyzString == nil) return NO; + + if (!error) scanner = [NSScanner scannerWithString:xyzwxyzString]; + while (![scanner isAtEnd] && i < 7 && !error) + { + if (![scanner scanFloat:&xyzwxyz[i++]]) error = @"Could not scan a float value."; + } + + if (!error && i < 7) error = @"Found less than seven float values."; + + if (error) + { + OOLogERR(kOOLogStringQuaternionConversion, @"cannot make vector and quaternion from '%@': %@", xyzwxyzString, error); + return NO; + } + + outVector->x = xyzwxyz[0]; + outVector->y = xyzwxyz[1]; + outVector->z = xyzwxyz[2]; + outQuaternion->w = xyzwxyz[3]; + outQuaternion->x = xyzwxyz[4]; + outQuaternion->y = xyzwxyz[5]; + outQuaternion->z = xyzwxyz[6]; + + return YES; +} + + +Vector VectorFromString(NSString *xyzString, Vector defaultValue) +{ + Vector result; + if (!ScanVectorFromString(xyzString, &result)) result = defaultValue; + return result; +} + + +Quaternion QuaternionFromString(NSString *wxyzString, Quaternion defaultValue) +{ + Quaternion result; + if (!ScanQuaternionFromString(wxyzString, &result)) result = defaultValue; + return result; +} + + +NSString *StringFromPoint(NSPoint point) +{ + return [NSString stringWithFormat:@"%f %f", point.x, point.y]; +} + + +NSPoint PointFromString(NSString *xyString) +{ + NSArray *tokens = ScanTokensFromString(xyString); + NSPoint result = NSZeroPoint; + + int n_tokens = [tokens count]; + if (n_tokens == 2) + { + result.x = [[tokens objectAtIndex:0] floatValue]; + result.y = [[tokens objectAtIndex:1] floatValue]; + } + return result; +} + + +Random_Seed RandomSeedFromString(NSString *abcdefString) +{ + Random_Seed result; + int abcdef[] = { 0, 0, 0, 0, 0, 0}; + int i = 0; + NSString *error = nil; + NSScanner *scanner = [NSScanner scannerWithString:abcdefString]; + + while (![scanner isAtEnd] && i < 6 && !error) + { + if (![scanner scanInt:&abcdef[i++]]) error = @"could not scan a int value."; + } + + if (!error && i < 6) error = @"found less than six int values."; + + if (!error) + { + result.a = abcdef[0]; + result.b = abcdef[1]; + result.c = abcdef[2]; + result.d = abcdef[3]; + result.e = abcdef[4]; + result.f = abcdef[5]; + } + else + { + OOLogERR(kOOLogStringRandomSeedConversion, @"cannot make Random_Seed from '%@': %@", abcdefString, error); + result = kNilRandomSeed; + } + + return result; +} + + +NSString *StringFromRandomSeed(Random_Seed seed) +{ + return [NSString stringWithFormat: @"%d %d %d %d %d %d", seed.a, seed.b, seed.c, seed.d, seed.e, seed.f]; +} + + +NSString *ExpandDescriptionForSeed(NSString *text, Random_Seed seed) +{ + return ExpandDescriptionForSeedName(text, seed, nil); + +} + + +NSString *ExpandDescriptionForSeedName(NSString *text, Random_Seed seed, NSString *name) +{ + // to enable variables to return strings that can be expanded (eg. @"[commanderName_string]") + // we're going to loop until every expansion has been done! + // but to check this does not infinitely recurse + // we'll stop after 32 loops. + + if (text == nil) return nil; + + int stack_check = 32; + NSString *old_desc = [NSString stringWithString:text]; + NSString *result = text; + + do + { + old_desc = result; + result = ExpandDescriptionsWithLocalsForSystemSeedName(result, seed, nil, name); + } while (--stack_check && ![result isEqual:old_desc]); + + if (!stack_check) + { + // If we get here, we broke the loop due to recursion; the resulting string will have [expansionKey]s in it. + OOLogERR(kOOLogExpandDescriptionsRecursionLimitExceeded, @"exceeded recusion limit trying to expand description \"%@\"", text); + } + + return result; +} + + +NSString *ExpandDescriptionForCurrentSystem(NSString *text) +{ + return ExpandDescriptionForSeed(text, [[PlayerEntity sharedPlayer] system_seed]); +} + + +NSString *ExpandDescriptionsWithLocalsForSystemSeed(NSString *text, Random_Seed seed, NSDictionary *locals) +{ + return ExpandDescriptionsWithLocalsForSystemSeedName(text, seed, locals, nil); +} + + +NSString *ExpandDescriptionsWithLocalsForSystemSeedName(NSString *text, Random_Seed seed, NSDictionary *locals, NSString *pName) +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + NSMutableString *partial = [[text mutableCopy] autorelease]; + NSMutableDictionary *all_descriptions = [[[UNIVERSE descriptions] mutableCopy] autorelease]; + id value = nil; + NSString *part = nil, *before = nil, *after = nil, *middle = nil; + unsigned sub, rnd, opt; + unsigned p1, p2; + NSArray *sysDesc = nil; + NSArray *sysDescItem = nil; + unsigned sysDescCount = 0, descItemCount; + + // add in player info if required + // -- this is now duplicated with new commanderXXX_string and commanderYYY_number methods in PlayerEntity Additions -- GILES + + if ([text rangeOfString:@"[commander_"].location != NSNotFound) + { + [all_descriptions setObject:[player commanderName_string] forKey:@"commander_name"]; + [all_descriptions setObject:[player commanderShip_string] forKey:@"commander_shipname"]; + [all_descriptions setObject:[player commanderShipDisplayName_string] forKey:@"commander_shipdisplayname"]; + [all_descriptions setObject:[player commanderRank_string] forKey:@"commander_rank"]; + [all_descriptions setObject:[player commanderLegalStatus_string] forKey:@"commander_legal_status"]; + } + if (pName == nil) pName=[UNIVERSE getSystemName:seed]; + + while ([partial rangeOfString:@"["].location != NSNotFound) + { + p1 = [partial rangeOfString:@"["].location; + p2 = [partial rangeOfString:@"]"].location + 1; + + before = [partial substringWithRange:NSMakeRange(0, p1)]; + after = [partial substringWithRange:NSMakeRange(p2,[partial length] - p2)]; + middle = [partial substringWithRange:NSMakeRange(p1 + 1 , p2 - p1 - 2)]; + + // check all_descriptions for an array that's keyed to middle + value = [all_descriptions objectForKey:middle]; + if ([value isKindOfClass:[NSArray class]] && [value count] > 0) + { + rnd = gen_rnd_number() % [value count]; + part = [value stringAtIndex:rnd]; + if (part == nil) part = @""; + } + else if ([value isKindOfClass:[NSString class]]) + { + part = [all_descriptions objectForKey:middle]; + } + else if ([[middle stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"0123456789"]] isEqual:@""]) + { + part = @""; + + // if all characters are all from the set "0123456789" interpret it as a number in system_description array + if (![middle isEqual:@""]) + { + if (sysDesc == nil) + { + sysDesc = [all_descriptions arrayForKey:@"system_description"]; + sysDescCount = [sysDesc count]; + } + + sub = [middle intValue]; + rnd = gen_rnd_number(); + + if (sub < sysDescCount) + { + sysDescItem = [sysDesc arrayAtIndex:sub]; + if (sysDescItem != nil) + { + descItemCount = [sysDescItem count]; + if (descItemCount == 5) + { + // Time-honoured Elite-compatible way for five items + opt = 0; + if (rnd >= 0x33) opt++; + if (rnd >= 0x66) opt++; + if (rnd >= 0x99) opt++; + if (rnd >= 0xCC) opt++; + } + else + { + // General way + opt = (rnd * descItemCount) / 256; + } + + part = [sysDescItem objectAtIndex:opt]; + } + } + } + } + else + { + // do replacement of mission and local variables here instead + part = ReplaceVariables(middle, NULL, locals); + } + + partial = [NSMutableString stringWithFormat:@"%@%@%@",before,part,after]; + } + + [partial replaceOccurrencesOfString:@"%H" + withString:pName + options:NSLiteralSearch range:NSMakeRange(0, [partial length])]; + + [partial replaceOccurrencesOfString:@"%I" + withString:[NSString stringWithFormat:@"%@%@",pName, DESC(@"planetname-derivative-suffix")] + options:NSLiteralSearch range:NSMakeRange(0, [partial length])]; + + [partial replaceOccurrencesOfString:@"%R" + withString:OldRandomDigrams() + options:NSLiteralSearch range:NSMakeRange(0, [partial length])]; + + [partial replaceOccurrencesOfString:@"%N" + withString:NewRandomDigrams() + options:NSLiteralSearch range:NSMakeRange(0, [partial length])]; + + + // Now replace all occurrences of %J000 to %J255 with the corresponding system name. + + NSRange foundToken, foundID; + NSString *stringID=@""; + char s; + BOOL err=NO; + int intVal; + + foundToken = [partial rangeOfString:@"%J"]; + + while (foundToken.location != NSNotFound) + { + foundID=NSMakeRange(foundToken.location+2,3); + if(foundID.location+3 > [partial length]) + { + err = YES; + stringID=[partial substringFromIndex:foundID.location]; + } + else + { + stringID = [partial substringWithRange:foundID]; + // these 3 characters must be numerical: 000 to 255 + s=[stringID characterAtIndex:0]; + if (s < '0' || s > '2') err = YES; + s=[stringID characterAtIndex:1]; + if (s < '0' || s > '9') err = YES; + s=[stringID characterAtIndex:2]; + if (s < '0' || s > '9') err = YES; + if (!err) + { + intVal = [stringID intValue]; + if ( intVal < 256 ) + { + [partial replaceOccurrencesOfString:[NSString stringWithFormat:@"%%J%@",stringID] + withString:[UNIVERSE getSystemName:[UNIVERSE systemSeedForSystemNumber:(OOSystemID)intVal]] + options:NSLiteralSearch range:NSMakeRange(0, [partial length])]; + } + else + err = YES; + } + } + if (err) + { + static NSMutableSet *warned = nil; + if (![warned containsObject:stringID]) + { + OOLogWARN(@"strings.expand", @"'%%J%@' not a planetary system number - use %Jxxx, where xxx is a number from 000 to 255",stringID); + if (warned == nil) warned = [[NSMutableSet alloc] init]; + [warned addObject:stringID]; + } + err = NO; // keep parsing the string for other %J tokens! + } + + if (foundID.location + 5 > [partial length]) + { + foundToken.location=NSNotFound; + } + else + { + foundToken = [[partial substringFromIndex:foundID.location] rangeOfString:@"%J"]; + if (foundToken.location!=NSNotFound) foundToken.location += foundID.location; + } + } + + return partial; +} + + +NSString *ExpandDescriptionsWithLocalsForCurrentSystem(NSString *text, NSDictionary *locals) +{ + return ExpandDescriptionsWithLocalsForSystemSeed(text, [[PlayerEntity sharedPlayer] system_seed], locals); +} + + +NSString *DescriptionForSystem(Random_Seed seed,NSString *name) +{ + seed_RNG_only_for_planet_description(seed); + return ExpandDescriptionForSeedName(@"[system-description-string]", seed, name); +} + + +NSString *DescriptionForCurrentSystem(void) +{ + return DescriptionForSystem([[PlayerEntity sharedPlayer] system_seed], [UNIVERSE getSystemName:[[PlayerEntity sharedPlayer] system_seed]]); +} + + +NSString *ReplaceVariables(NSString *string, Entity *target, NSDictionary *localVariables) +{ + NSMutableString *resultString = nil; + NSMutableArray *tokens = nil; + NSEnumerator *tokenEnum = nil; + NSString *token = nil; + NSString *replacement = nil; + Entity *effeciveTarget = nil; + PlayerEntity *player = nil; + + tokens = ScanTokensFromString(string); + resultString = [NSMutableString stringWithString:string]; + player = [PlayerEntity sharedPlayer]; + if (target == nil) target = player; + + for (tokenEnum = [tokens objectEnumerator]; (token = [tokenEnum nextObject]); ) + { + replacement = [player missionVariableForKey:token]; + if (replacement == nil) replacement = [localVariables objectForKey:token]; + if (replacement == nil) + { + if ([token hasSuffix:@"_number"] || [token hasSuffix:@"_bool"] || [token hasSuffix:@"_string"]) + { + SEL value_selector = NSSelectorFromString(token); + if ([target respondsToSelector:value_selector]) effeciveTarget = target; + else if (target != player && [player respondsToSelector:value_selector]) effeciveTarget = player; + else effeciveTarget = nil; + + if (effeciveTarget != nil) replacement = [[effeciveTarget performSelector:value_selector] description]; + } + else if ([token hasPrefix:@"["] && [token hasSuffix:@"]"]) + { + replacement = ExpandDescriptionForCurrentSystem(token); + } + } + + if (replacement != nil) [resultString replaceOccurrencesOfString:token withString:replacement options:NSLiteralSearch range:NSMakeRange(0, [resultString length])]; + } + + OOLog(kOOLogDebugReplaceVariablesInString, @"EXPANSION: \"%@\" becomes \"%@\"", string, resultString); + + return resultString; +} + + +/* Generates pseudo-random digram string using gen_rnd_number() + (world-generation consistent PRNG), but misses some possibilities. Used + for "%R" description string for backwards compatibility. +*/ +static NSString *OldRandomDigrams(void) +{ + int i; + int len = gen_rnd_number() & 3; + NSString* digrams = [[UNIVERSE descriptions] objectForKey:@"digrams"]; + NSMutableString* name = [NSMutableString stringWithCapacity:256]; + for (i = 0; i <=len; i++) + { + int x = gen_rnd_number() & 0x3e; + [name appendString:[digrams substringWithRange:NSMakeRange(x,2)]]; + } + return [name capitalizedString]; +} + + +/* Generates pseudo-random digram string using gen_rnd_number() + (world-generation consistent PRNG). Used for "%N" description string. +*/ +static NSString *NewRandomDigrams(void) +{ + unsigned i, length, count; + NSString *digrams = nil; + NSMutableString *name = nil; + + length = (gen_rnd_number() % 4) + 1; + if ((Ranrot() % 5) < ((length == 1) ? 3 : 1)) ++length; // Make two-letter names rarer and 10-letter names happen sometimes + digrams = [[UNIVERSE descriptions] objectForKey:@"digrams"]; + count = [digrams length] / 2; + name = [NSMutableString stringWithCapacity:length * 2]; + + for (i = 0; i != length; ++i) + { + [name appendString:[digrams substringWithRange:NSMakeRange((gen_rnd_number() % count) * 2, 2)]]; + } + return [name capitalizedString]; +} + + +// Similar to NewRandomDigrams(), but uses Ranrot() (the "really random" PRNG). +NSString *RandomDigrams(void) +{ + unsigned i, length, count; + NSString *digrams = nil; + NSMutableString *name = nil; + + length = (Ranrot() % 4) + 1; + if ((Ranrot() % 5) < ((length == 1) ? 3 : 1)) ++length; // Make two-letter names rarer and 10-letter names happen sometimes + digrams = [[UNIVERSE descriptions] objectForKey:@"digrams"]; + count = [digrams length] / 2; + name = [NSMutableString stringWithCapacity:length * 2]; + + for (i = 0; i != length; ++i) + { + [name appendString:[digrams substringWithRange:NSMakeRange((Ranrot() % count) * 2, 2)]]; + } + return [name capitalizedString]; +} + + +NSString *OOPadStringTo(NSString * string, float numSpaces) +{ + NSString *result = string; + numSpaces -= [result length]; + if (numSpaces>0) + { + result=[[@"" stringByPaddingToLength: numSpaces*2 withString: @" " startingAtIndex:0] stringByAppendingString: result]; + } + return result; +} + + +NSString *OOStringFromDeciCredits(OOCreditsQuantity tenthsOfCredits, BOOL includeDecimal, BOOL includeSymbol) +{ + NSString *result = nil; + unsigned long long integerCredits = tenthsOfCredits / 10; + unsigned long long tenths = tenthsOfCredits % 10; + + if (includeDecimal) + { + result = [NSString stringWithFormat:@"%llu.%llu", integerCredits, tenths]; + } + else + { + if (tenths >= 5) integerCredits++; + result = [NSString stringWithFormat:@"%llu", integerCredits]; + } + + // Append credits sybol if desired. + if (includeSymbol) + { + result = [NSString stringWithFormat:DESC(@"@-credits"), result]; + } + + return result; +} + + +@implementation NSString (OOUtilities) + +- (BOOL)pathHasExtension:(NSString *)extension +{ + return [[self pathExtension] caseInsensitiveCompare:extension] == NSOrderedSame; +} + + +- (BOOL)pathHasExtensionInArray:(NSArray *)extensions +{ + NSEnumerator *extEnum = nil; + NSString *extension = nil; + + for (extEnum = [extensions objectEnumerator]; (extension = [extEnum nextObject]); ) + { + if ([[self pathExtension] caseInsensitiveCompare:extension] == NSOrderedSame) return YES; + } + + return NO; +} + +@end + + +NSArray *ComponentsFromVersionString(NSString *string) +{ + NSArray *stringComponents = nil; + NSMutableArray *result = nil; + unsigned i, count; + int value; + id component; + + stringComponents = [string componentsSeparatedByString:@" "]; + stringComponents = [[stringComponents objectAtIndex:0] componentsSeparatedByString:@"-"]; + stringComponents = [[stringComponents objectAtIndex:0] componentsSeparatedByString:@"."]; + count = [stringComponents count]; + result = [NSMutableArray arrayWithCapacity:count]; + + for (i = 0; i != count; ++i) + { + component = [stringComponents objectAtIndex:i]; + if ([component respondsToSelector:@selector(intValue)]) value = MAX([component intValue], 0); + else value = 0; + + [result addObject:[NSNumber numberWithUnsignedInt:value]]; + } + + return result; +} + + +NSComparisonResult CompareVersions(NSArray *version1, NSArray *version2) +{ + NSEnumerator *leftEnum = nil, + *rightEnum = nil; + NSNumber *leftComponent = nil, + *rightComponent = nil; + unsigned leftValue, + rightValue; + + leftEnum = [version1 objectEnumerator]; + rightEnum = [version2 objectEnumerator]; + + for (;;) + { + leftComponent = [leftEnum nextObject]; + rightComponent = [rightEnum nextObject]; + + if (leftComponent == nil && rightComponent == nil) break; // End of both versions + + // We'll get 0 if the component is nil, which is what we want. + leftValue = [leftComponent unsignedIntValue]; + rightValue = [rightComponent unsignedIntValue]; + + if (leftValue < rightValue) return NSOrderedAscending; + if (leftValue > rightValue) return NSOrderedDescending; + } + + // If there was a difference, we'd have returned already. + return NSOrderedSame; +} + + +NSString *ClockToString(double clock, BOOL adjusting) +{ + int days, hrs, mins, secs; + NSString *format = nil; + + days = floor(clock / 86400.0); + secs = floor(clock - days * 86400.0); + hrs = floor(secs / 3600.0); + secs %= 3600; + mins = floor(secs / 60.0); + secs %= 60; + + if (adjusting) format = DESC(@"clock-format-adjusting"); + else format = DESC(@"clock-format"); + + return [NSString stringWithFormat:format, days, hrs, mins, secs]; +} + + +// Workaround for Xcode auto-indent bug +static NSString * const kQuotationMark = @"\""; +static NSString * const kEscapedQuotationMark = @"\\\""; + + +NSString *EscapedGraphVizString(NSString *string) +{ + const NSString *srcStrings[] = + { + //Note: backslash must be first. + @"\\", @"\"", @"\'", @"\r", @"\n", @"\t", nil + }; + const NSString *subStrings[] = + { + //Note: must be same order. + @"\\\\", @"\\\"", @"\\\'", @"\\r", @"\\n", @"\\t", nil + }; + + NSString **src = srcStrings, **sub = subStrings; + NSMutableString *mutable = nil; + NSString *result = nil; + + mutable = [string mutableCopy]; + while (*src != nil) + { + [mutable replaceOccurrencesOfString:*src++ + withString:*sub++ + options:0 + range:NSMakeRange(0, [mutable length])]; + } + + if ([mutable length] == [string length]) + { + result = string; + } + else + { + result = [[mutable copy] autorelease]; + } + [mutable release]; + return result; +} diff --git a/src/Core/OOTextureScaling.h b/src/Core/OOTextureScaling.h new file mode 100644 index 00000000..813eb8c8 --- /dev/null +++ b/src/Core/OOTextureScaling.h @@ -0,0 +1,71 @@ +/* + +OOTextureScaling.h + +Functions used to rescale texture maps. +These are bottlenecks! They should be optimized or, better, replaced with use +of an optimized library. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOMaths.h" + + +typedef uint_fast32_t OOTextureDimension; // Note: dimensions are assumed to be less than 1048576 (2^20) pixels. +typedef uint_fast8_t OOTexturePlaneCount; // Currently supported values are 1 and 4. + + +/* Assumes 8 bits per sample, interleaved. + dstPixels must have space for dstWidth * dstHeight pixels (no row padding + is generated). + + IMPORTANT: this will free() srcPixels. +*/ +void *OOScalePixMap(void *srcPixels, OOTextureDimension srcWidth, OOTextureDimension srcHeight, OOTexturePlaneCount planes, size_t srcRowBytes, OOTextureDimension dstWidth, OOTextureDimension dstHeight, BOOL leaveSpaceForMipMaps); + + +/* Assumes 8 bits per sample, interleaved. + Buffer must have space for (4 * width * height) / 3 pixels. +*/ +BOOL OOGenerateMipMaps(void *textureBytes, OOTextureDimension width, OOTextureDimension height, OOTexturePlaneCount planes); diff --git a/src/Core/OOTextureScaling.m b/src/Core/OOTextureScaling.m new file mode 100644 index 00000000..72917d1e --- /dev/null +++ b/src/Core/OOTextureScaling.m @@ -0,0 +1,1379 @@ +/* + +OOTextureScaling.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#import "OOTextureScaling.h" +#import "OOFunctionAttributes.h" +#import +#import "OOLogging.h" +#import "OOMaths.h" +#import "OOCPUInfo.h" + + +// #define DUMP_MIP_MAPS + + +// Structure used to track buffers in OOScalePixMap() and its helpers. +typedef struct +{ + void *pixels; + OOTextureDimension width, height; + size_t rowBytes; + size_t dataSize; +} OOScalerPixMap; + + +/* Internal function declarations. + + NOTE: the function definitions are grouped together for best code cache + coherence rather than the order listed here. +*/ +static BOOL GenerateMipMaps1(void *textureBytes, OOTextureDimension width, OOTextureDimension height) NONNULL_FUNC; +static BOOL GenerateMipMaps4(void *textureBytes, OOTextureDimension width, OOTextureDimension height) NONNULL_FUNC; + + +/* ScaleToHalf_P_xN functions + These scale a texture with P planes (components) to half its size in each + dimension, handling N pixels at a time. srcWidth must be a multiple of N. + Parameters are not validated -- bad parameters will lead to bad data or a + crash. + + Scaling is an unweighted average. 8 bits per channel assumed. + It is safe and meaningful for srcBytes == dstBytes. +*/ +static void ScaleToHalf_1_x1(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) NONNULL_FUNC; +static void ScaleToHalf_4_x1(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) NONNULL_FUNC; + +#if OOLITE_NATIVE_64_BIT + static void ScaleToHalf_1_x8(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) NONNULL_FUNC; + static void ScaleToHalf_4_x2(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) NONNULL_FUNC; +#else + static void ScaleToHalf_1_x4(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) NONNULL_FUNC; +#endif + + +OOINLINE void StretchVertically(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes) ALWAYS_INLINE_FUNC; + +static void StretchVerticallyN_x1(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes); + +static void SqueezeVertically4(OOScalerPixMap srcPx, OOTextureDimension dstHeight); +static void SqueezeVertically1(OOScalerPixMap srcPx, OOTextureDimension dstHeight); +static void StretchHorizontally1(OOScalerPixMap srcPx, OOScalerPixMap dstPx); +static void StretchHorizontally4(OOScalerPixMap srcPx, OOScalerPixMap dstPx); +static void SqueezeHorizontally4(OOScalerPixMap srcPx, OOTextureDimension dstWidth); +static void SqueezeHorizontally1(OOScalerPixMap srcPx, OOTextureDimension dstWidth); + + +static BOOL EnsureCorrectDataSize(OOScalerPixMap *pixMap, BOOL leaveSpaceForMipMaps) NONNULL_FUNC; + + +#if !OOLITE_NATIVE_64_BIT + +static void StretchVerticallyN_x4(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes); + +OOINLINE void StretchVertically(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes) +{ + if (!((srcPx.rowBytes) & 3)) + { + StretchVerticallyN_x4(srcPx, dstPx, planes); + } + else + { + StretchVerticallyN_x1(srcPx, dstPx, planes); + } +} + +#else // OOLITE_NATIVE_64_BIT + +static void StretchVerticallyN_x8(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes); + +OOINLINE void StretchVertically(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes) +{ + if (!((srcPx.rowBytes) & 7)) + { + StretchVerticallyN_x8(srcPx, dstPx, planes); + } + else + { + StretchVerticallyN_x1(srcPx, dstPx, planes); + } +} + +#endif + + +#ifdef DUMP_MIP_MAPS +// NOTE: currently only works on OS X because of OSAtomicAdd32() (used to increment ID counter in thread-safe way). A simple increment would be sufficient if limited to a single thread (in OOTextureLoader). +volatile int32_t sPreviousDumpID = 0; +int32_t OSAtomicAdd32(int32_t __theAmount, volatile int32_t *__theValue); + +#define DUMP_CHANNELS 4 // Bitmap of channel counts - 5 for both 4-chan and 1-chan dumps + +#define DUMP_MIP_MAP_PREPARE(pl) uint32_t dumpPlanes = pl; \ + uint32_t dumpLevel = 0; \ + BOOL dumpThis = (dumpPlanes & DUMP_CHANNELS) != 0; \ + SInt32 dumpID = dumpThis ? OSAtomicAdd32(1, &sPreviousDumpID) : 0; \ + if (dumpThis) OOLog(@"texture.mipMap.dump", @"Dumping mip-maps as dump ID%u lv# %uch XxY.tiff.", dumpID, dumpPlanes); +#define DUMP_MIP_MAP_DUMP(px, w, h) if (dumpThis) DumpMipMap(px, w, h, dumpPlanes, dumpID, dumpLevel++); +static void DumpMipMap(void *data, OOTextureDimension width, OOTextureDimension height, OOTexturePlaneCount planes, SInt32 ID, uint32_t level); +#else +#define DUMP_MIP_MAP_PREPARE(pl) do {} while (0) +#define DUMP_MIP_MAP_DUMP(px, w, h) do {} while (0) +#endif + + +void *OOScalePixMap(void *srcPixels, OOTextureDimension srcWidth, OOTextureDimension srcHeight, OOTexturePlaneCount planes, size_t srcRowBytes, OOTextureDimension dstWidth, OOTextureDimension dstHeight, BOOL leaveSpaceForMipMaps) +{ + OOScalerPixMap srcPx, dstPx = {0}, sparePx = {0}; + BOOL OK = YES; + + // Sanity check. + if (EXPECT_NOT(srcPixels == NULL || (planes != 4 && planes != 1) || (srcRowBytes < srcWidth * planes))) + { + OOLogGenericParameterError(); + free(srcPixels); + return NULL; + } + + srcPx.pixels = srcPixels; + srcPx.width = srcWidth; + srcPx.height = srcHeight; + srcPx.rowBytes = srcRowBytes; + srcPx.dataSize = srcRowBytes * srcHeight; + + if (srcHeight < dstHeight) + { + // Stretch vertically. This requires a separate buffer. + dstPx.width = srcPx.width; // Not dstWidth! + dstPx.height = dstHeight; + dstPx.rowBytes = srcPx.width * planes; + dstPx.dataSize = dstPx.rowBytes * dstPx.height; + if (leaveSpaceForMipMaps && dstWidth <= srcWidth) dstPx.dataSize = dstPx.dataSize * 4 / 3; + dstPx.pixels = malloc(dstPx.dataSize); + if (EXPECT_NOT(dstPx.pixels == NULL)) goto FAIL; + + StretchVertically(srcPx, dstPx, planes); + + sparePx = srcPx; + srcPx = dstPx; + } + else if (dstHeight < srcHeight) + { + // Squeeze vertically. This can be done in-place. + if (planes == 4) SqueezeVertically4(srcPx, dstHeight); + else /* planes == 1 */ SqueezeVertically1(srcPx, dstHeight); + srcPx.height = dstHeight; + } + + if (srcWidth < dstWidth) + { + // Stretch horizontally. This requires a separate buffer. + dstPx.height = srcPx.height; + dstPx.width = dstWidth; + dstPx.rowBytes = dstPx.width * planes; + dstPx.dataSize = dstPx.rowBytes * srcPx.height; + if (leaveSpaceForMipMaps) dstPx.dataSize = dstPx.dataSize * 4 / 3; + if (dstPx.dataSize <= sparePx.dataSize) + { + dstPx.pixels = sparePx.pixels; + dstPx.dataSize = sparePx.dataSize; + } + else + { + free(sparePx.pixels); + sparePx.pixels = NULL; + dstPx.pixels = malloc(dstPx.dataSize); + if (EXPECT_NOT(dstPx.pixels == NULL)) goto FAIL; + } + + if (planes == 4) StretchHorizontally4(srcPx, dstPx); + else /* planes == 1 */ StretchHorizontally1(srcPx, dstPx); + } + else if (dstWidth < srcWidth) + { + // Squeeze horizontally. This can be done in-place. + if (planes == 4) SqueezeHorizontally4(srcPx, dstWidth); + else /* planes == 1 */ SqueezeHorizontally1(srcPx, dstWidth); + + dstPx = srcPx; + dstPx.width = dstWidth; + dstPx.rowBytes = dstPx.width * planes; + } + else + { + // No horizontal scaling. + dstPx = srcPx; + } + + // Avoid a potential double free (if the realloc in EnsureCorrectDataSize() relocates the block). + if (srcPx.pixels == dstPx.pixels) srcPx.pixels = NULL; + + // dstPx is now the result. + OK = EnsureCorrectDataSize(&dstPx, leaveSpaceForMipMaps); + +FAIL: + if (srcPx.pixels != NULL) + { + free(srcPx.pixels); + } + if (sparePx.pixels != NULL && sparePx.pixels != dstPx.pixels && sparePx.pixels != srcPx.pixels) + { + free(sparePx.pixels); + } + if (!OK && dstPx.pixels != NULL) + { + free(dstPx.pixels); + dstPx.pixels = NULL; + } + + return dstPx.pixels; +} + + +BOOL OOGenerateMipMaps(void *textureBytes, OOTextureDimension width, OOTextureDimension height, OOTexturePlaneCount planes) +{ + if (EXPECT_NOT(width != OORoundUpToPowerOf2(width) || height != OORoundUpToPowerOf2(height))) + { + OOLog(kOOLogParameterError, @"Non-power-of-two dimensions (%ux%u) passed to %s() - ignoring, data will be junk.", width, height, __FUNCTION__); + return NO; + } + if (EXPECT_NOT(textureBytes == NULL)) + { + OOLog(kOOLogParameterError, @"NULL texutre pointer passed to GenerateMipMaps()."); + return NO; + } + + if (planes == 4) return GenerateMipMaps4(textureBytes, width, height); + if (planes == 1) return GenerateMipMaps1(textureBytes, width, height); + + OOLog(kOOLogParameterError, @"%s(): bad plane count (%u, should be 1 or 4) - ignoring, data will be junk.", __FUNCTION__, planes); + return NO; +} + + +static BOOL GenerateMipMaps1(void *textureBytes, OOTextureDimension width, OOTextureDimension height) +{ + OOTextureDimension w = width, h = height; + uint8_t *curr, *next; + + DUMP_MIP_MAP_PREPARE(1); + curr = textureBytes; + +#if OOLITE_NATIVE_64_BIT + while (8 < w && 1 < h) + { + DUMP_MIP_MAP_DUMP(curr, w, h); + + next = curr + w * h; + ScaleToHalf_1_x8(curr, next, w, h); + + w >>= 1; + h >>= 1; + curr = next; + } +#else + while (4 < w && 1 < h) + { + DUMP_MIP_MAP_DUMP(curr, w, h); + + next = curr + w * h; + ScaleToHalf_1_x4(curr, next, w, h); + + w >>= 1; + h >>= 1; + curr = next; + } +#endif + + while (1 < w && 1 < h) + { + DUMP_MIP_MAP_DUMP(curr, w, h); + + next = curr + w * h; + ScaleToHalf_1_x1(curr, next, w, h); + + w >>= 1; + h >>= 1; + curr = next; + } + + DUMP_MIP_MAP_DUMP(curr, w, h); + + // TODO: handle residual 1xN/Nx1 mips. For now, we just limit maximum mip level for non-square textures. + return YES; +} + + +static void ScaleToHalf_1_x1(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) +{ + OOTextureDimension x, y; + uint8_t *src0, *src1, *dst; + uint_fast8_t px00, px01, px10, px11; + uint_fast16_t sum; + + src0 = srcBytes; + src1 = src0 + srcWidth; + dst = dstBytes; + + y = srcHeight >> 1; + do + { + x = srcWidth >> 1; + do + { + // Read four pixels in a square... + px00 = *src0++; + px01 = *src0++; + px10 = *src1++; + px11 = *src1++; + + // ...add them together... + sum = px00 + px01 + px10 + px11; + + // ...shift the sum into place... + sum >>= 2; + + // ...and write output pixel. + *dst++ = sum; + } while (--x); + + // Skip a row for each source row + src0 = src1; + src1 += srcWidth; + } while (--y); +} + + +#if !OOLITE_NATIVE_64_BIT + +static void ScaleToHalf_1_x4(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) +{ + OOTextureDimension x, y; + uint32_t *src0, *src1, *dst; + uint_fast32_t px00, px01, px10, px11; + uint_fast32_t sum0, sum1; + + srcWidth >>= 2; // Four (output) pixels at a time + src0 = srcBytes; + src1 = src0 + srcWidth; + dst = dstBytes; + + y = srcHeight >> 1; + do + { + x = srcWidth >> 1; + do + { + // Read 8 pixels in a 4x2 rectangle... + px00 = *src0++; + px01 = *src0++; + px10 = *src1++; + px11 = *src1++; + + // ...add them together. + sum0 = (px00 & 0x00FF00FF) + + (px10 & 0x00FF00FF) + + ((px00 & 0xFF00FF00) >> 8) + + ((px10 & 0xFF00FF00) >> 8); + sum1 = (px01 & 0x00FF00FF) + + (px11 & 0x00FF00FF) + + ((px01 & 0xFF00FF00) >> 8) + + ((px11 & 0xFF00FF00) >> 8); + + // ...swizzle the sums around... +#if OOLITE_BIG_ENDIAN + sum0 = ((sum0 << 6) & 0xFF000000) | ((sum0 << 14) & 0x00FF0000); + sum1 = ((sum1 >> 10) & 0x0000FF00) | ((sum1 >>2) & 0x000000FF); +#elif OOLITE_LITTLE_ENDIAN + sum0 = ((sum0 >> 10) & 0x0000FF00) | ((sum0 >>2) & 0x000000FF); + sum1 = ((sum1 << 6) & 0xFF000000) | ((sum1 << 14) & 0x00FF0000); +#else + #error Neither OOLITE_BIG_ENDIAN nor OOLITE_LITTLE_ENDIAN is defined as nonzero! +#endif + + // ...and write output pixel. + *dst++ = sum0 | sum1; + } while (--x); + + // Skip a row for each source row + src0 = src1; + src1 += srcWidth; + } while (--y); +} + +#else // OOLITE_NATIVE_64_BIT + +static void ScaleToHalf_1_x8(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) +{ + OOTextureDimension x, y; + uint64_t *src0, *src1; + uint64_t *dst; + uint_fast64_t px00, px01, px10, px11; + uint_fast64_t sum0, sum1; + + srcWidth >>= 3; // Eight (output) pixels at a time + src0 = srcBytes; + src1 = src0 + srcWidth; + dst = dstBytes; + + y = srcHeight >> 1; + do + { + x = srcWidth >> 1; + do + { + // Read 16 pixels in an 8x2 rectangle... + px00 = *src0++; + px01 = *src0++; + px10 = *src1++; + px11 = *src1++; + + // ...add them together... + sum0 = ((px00 & 0x00FF00FF00FF00FFULL)) + + ((px10 & 0x00FF00FF00FF00FFULL)) + + ((px00 & 0xFF00FF00FF00FF00ULL) >> 8) + + ((px10 & 0xFF00FF00FF00FF00ULL) >> 8); + sum1 = ((px01 & 0x00FF00FF00FF00FFULL)) + + ((px11 & 0x00FF00FF00FF00FFULL)) + + ((px01 & 0xFF00FF00FF00FF00ULL) >> 8) + + ((px11 & 0xFF00FF00FF00FF00ULL) >> 8); + + // ...swizzle the sums around... +#if OOLITE_BIG_ENDIAN + sum0 = ((sum0 << 06) & 0xFF00000000000000ULL) | + ((sum0 << 14) & 0x00FF000000000000ULL) | + ((sum0 << 22) & 0x0000FF0000000000ULL) | + ((sum0 << 30) & 0x000000FF00000000ULL); + sum1 = ((sum1 >> 26) & 0x00000000FF000000ULL) | + ((sum1 >> 18) & 0x0000000000FF0000ULL) | + ((sum1 >> 10) & 0x000000000000FF00ULL) | + ((sum1 >> 02) & 0x00000000000000FFULL); +#elif OOLITE_LITTLE_ENDIAN + sum0 = ((sum0 >> 26) & 0x00000000FF000000ULL) | + ((sum0 >> 18) & 0x0000000000FF0000ULL) | + ((sum0 >> 10) & 0x000000000000FF00ULL) | + ((sum0 >> 02) & 0x00000000000000FFULL); + sum1 = ((sum1 << 06) & 0xFF00000000000000ULL) | + ((sum1 << 14) & 0x00FF000000000000ULL) | + ((sum1 << 22) & 0x0000FF0000000000ULL) | + ((sum1 << 30) & 0x000000FF00000000ULL); +#else + #error Neither OOLITE_BIG_ENDIAN nor OOLITE_LITTLE_ENDIAN is defined as nonzero! +#endif + // ...and write output pixel. + *dst++ = sum0 | sum1; + } while (--x); + + // Skip a row for each source row + src0 = src1; + src1 += srcWidth; + } while (--y); +} + +#endif + + +static BOOL GenerateMipMaps4(void *textureBytes, OOTextureDimension width, OOTextureDimension height) +{ + OOTextureDimension w = width, h = height; + uint32_t *curr, *next; + + DUMP_MIP_MAP_PREPARE(4); + curr = textureBytes; + +#if OOLITE_NATIVE_64_BIT + while (2 < w && 1 < h) + { + DUMP_MIP_MAP_DUMP(curr, w, h); + + next = curr + w * h; + ScaleToHalf_4_x2(curr, next, w, h); + + w >>= 1; + h >>= 1; + curr = next; + } + if (EXPECT(1 < w && 1 < h)) + { + DUMP_MIP_MAP_DUMP(curr, w, h); + + next = curr + w * h; + ScaleToHalf_4_x1(curr, next, w, h); + + w >>= 1; + h >>= 1; + } +#else + while (1 < w && 1 < h) + { + DUMP_MIP_MAP_DUMP(curr, w, h); + + next = curr + w * h; + ScaleToHalf_4_x1(curr, next, w, h); + + w >>= 1; + h >>= 1; + curr = next; + } +#endif + + DUMP_MIP_MAP_DUMP(curr, w, h); + + // TODO: handle residual 1xN/Nx1 mips. For now, we just limit maximum mip level for non-square textures. + return YES; +} + + +static void ScaleToHalf_4_x1(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) +{ + OOTextureDimension x, y; + uint32_t *src0, *src1, *dst; + uint_fast32_t px00, px01, px10, px11; + + /* We treat channel layout as ABGR -- actual layout doesn't matter since + each channel is handled the same. We use two accumulators, with + alternating channels, so overflow doesn't cross channel boundaries, + while having less overhead than one accumulator per channel. + */ + uint_fast32_t ag, br; + + src0 = srcBytes; + src1 = src0 + srcWidth; + dst = dstBytes; + + y = srcHeight >> 1; + do + { + x = srcWidth >> 1; + do + { + // Read four pixels in a square... + px00 = *src0++; + px01 = *src0++; + px10 = *src1++; + px11 = *src1++; + + // ...and add them together, channel by channel. + ag = (px00 & 0xFF00FF00) >> 8; + br = (px00 & 0x00FF00FF); + ag += (px01 & 0xFF00FF00) >> 8; + br += (px01 & 0x00FF00FF); + ag += (px10 & 0xFF00FF00) >> 8; + br += (px10 & 0x00FF00FF); + ag += (px11 & 0xFF00FF00) >> 8; + br += (px11 & 0x00FF00FF); + + // Shift the sums into place... + ag <<= 6; + br >>= 2; + + // ...and write output pixel. + *dst++ = (ag & 0xFF00FF00) | (br & 0x00FF00FF); + } while (--x); + + // Skip a row for each source row + src0 = src1; + src1 += srcWidth; + } while (--y); +} + + +#if OOLITE_NATIVE_64_BIT + +static void ScaleToHalf_4_x2(void *srcBytes, void *dstBytes, OOTextureDimension srcWidth, OOTextureDimension srcHeight) +{ + OOTextureDimension x, y; + uint_fast64_t *src0, *src1, *dst; + uint_fast64_t px00, px01, px10, px11; + + /* We treat channel layout as ABGR -- actual layout doesn't matter since + each channel is handled the same. We use two accumulators, with + alternating channels, so overflow doesn't cross channel boundaries, + while having less overhead than one accumulator per channel. + */ + uint_fast64_t ag0, ag1, br0, br1; + + srcWidth >>= 1; // Two bytes at a time + src0 = srcBytes; + src1 = src0 + srcWidth; + dst = dstBytes; + + y = srcHeight >> 1; + do + { + x = srcWidth >> 1; + do + { + // Read eight pixels (4x2)... + px00 = *src0++; + px01 = *src0++; + px10 = *src1++; + px11 = *src1++; + + // ...and add them together, channel by channel. + ag0 = (px00 & 0xFF00FF00FF00FF00ULL) >> 8; + br0 = (px00 & 0x00FF00FF00FF00FFULL); + ag0 += (px10 & 0xFF00FF00FF00FF00ULL) >> 8; + br0 += (px10 & 0x00FF00FF00FF00FFULL); + ag1 = (px01 & 0xFF00FF00FF00FF00ULL) >> 8; + br1 = (px01 & 0x00FF00FF00FF00FFULL); + ag1 += (px11 & 0xFF00FF00FF00FF00ULL) >> 8; + br1 += (px11 & 0x00FF00FF00FF00FFULL); + +#if OOLITE_BIG_ENDIAN + // Shift and add some more... + ag0 = ag0 + (ag0 << 32); + br0 = br0 + (br0 << 32); + ag1 = ag1 + (ag1 >> 32); + br1 = br1 + (br1 >> 32); + + // ...merge and shift some more... + ag0 = ((ag0 & 0x03FC03FC00000000ULL) | (ag1 & 0x0000000003FC03FCULL)) << 6; + br0 = ((br0 & 0x03FC03FC00000000ULL) | (br1 & 0x0000000003FC03FCULL)) >> 2; +#elif OOLITE_LITTLE_ENDIAN + // Shift and add some more... + ag0 = ag0 + (ag0 >> 32); + br0 = br0 + (br0 >> 32); + ag1 = ag1 + (ag1 << 32); + br1 = br1 + (br1 << 32); + + // ...merge and shift some more... + ag0 = ((ag0 & 0x0000000003FC03FCULL) | (ag1 & 0x03FC03FC00000000ULL)) << 6; + br0 = ((br0 & 0x0000000003FC03FCULL) | (br1 & 0x03FC03FC00000000ULL)) >> 2; +#else + #error Unknown architecture. +#endif + + // ...and write output pixel. + *dst++ = ag0 | br0; + } while (--x); + + // Skip a row for each source row + src0 = src1; + src1 += srcWidth; + } while (--y); +} + +#endif + + +#ifdef DUMP_MIP_MAPS + +static NSData *TIFFFromTexture(void *data, OOTexturePlaneCount planes, OOTextureDimension width, OOTextureDimension height); +static void ByteSwapData(void *data, unsigned count); + + +static void DumpMipMap(void *data, OOTextureDimension width, OOTextureDimension height, OOTexturePlaneCount planes, SInt32 ID, uint32_t level) +{ + NSString *dumpName = nil; + NSString *dumpPath = nil; + NSData *dumpData = nil; + static NSString *outDirectory = nil; + + if (outDirectory == nil) + { + outDirectory = [NSSearchPathForDirectoriesInDomains(NSDesktopDirectory, NSUserDomainMask, YES) objectAtIndex:0]; + outDirectory = [outDirectory stringByAppendingPathComponent:@"Oolite mip-map dumps"]; + [[NSFileManager defaultManager] createDirectoryAtPath:outDirectory attributes:NULL]; + [outDirectory retain]; + } + + dumpName = [NSString stringWithFormat:@"dump ID %u lv%u %uch %ux%u.tiff", ID, level, planes, width, height]; + dumpPath = [outDirectory stringByAppendingPathComponent:dumpName]; + dumpData = TIFFFromTexture(data, planes, width, height); + if (dumpData != nil) + { + [dumpData writeToFile:dumpPath atomically:NO]; + } +} + + +static NSData *TIFFFromTexture(void *data, OOTexturePlaneCount planes, OOTextureDimension width, OOTextureDimension height) +{ + NSBitmapImageRep *rep = nil; + BOOL hasAlpha; + NSString *colorSpaceName = nil; + NSBitmapFormat bmFormat; + NSData *result = nil; + + switch (planes) + { + case 4: + // assume RGBA + hasAlpha = YES; + colorSpaceName = NSCalibratedRGBColorSpace; + bmFormat = NSAlphaNonpremultipliedBitmapFormat; + ByteSwapData(data, width * height); + break; + + case 1: + // assume grayscale + hasAlpha = NO; + colorSpaceName = NSCalibratedWhiteColorSpace; + bmFormat = 0; + break; + + default: + OOLog(@"texture.mipMap.dump.unknownFormat", @"**** Unknown texture format %u.", planes); + return nil; + } + + rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:(unsigned char **)&data + pixelsWide:width + pixelsHigh:height + bitsPerSample:8 + samplesPerPixel:planes + hasAlpha:hasAlpha + isPlanar:NO + colorSpaceName:colorSpaceName + bitmapFormat:bmFormat + bytesPerRow:planes * width + bitsPerPixel:planes * 8]; + + if (rep == nil) + { + OOLog(@"texture.mipMap.dump.conversionFailed", @"**** Failed to convert texture to NSBitmapImageRep."); + } + + result = [rep TIFFRepresentation]; + [rep release]; + + // Restore data order + if (planes == 4) ByteSwapData(data, width * height); + + return result; +} + + +static void ByteSwapData(void *data, unsigned count) +{ + uint32_t *words = (uint32_t *)data; + while (count--) + { + *words = OSSwapInt32(*words); + ++words; + } +} + +#endif + + +static void StretchVerticallyN_x1(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes) +{ + uint8_t *src, *src0, *src1, *prev, *dst; + uint8_t px0, px1; + uint_fast32_t x, y, xCount, srcRowBytes; + uint_fast16_t weight0, weight1; + uint_fast32_t fractY; // Y coordinate, fixed-point (24.8) + + src = srcPx.pixels; + srcRowBytes = srcPx.rowBytes; + dst = dstPx.pixels; // Assumes dstPx.width == dstPx.rowBytes. + + src0 = prev = src; + + xCount = srcPx.width * planes; + + for (y = 1; y != dstPx.height; ++y) + { + fractY = ((srcPx.height * y) << 8) / dstPx.height; + + src0 = prev; + prev = src1 = src + srcRowBytes * (fractY >> 8); + + weight1 = fractY & 0xFF; + weight0 = 0x100 - weight1; + + x = xCount; + while (x--) + { + px0 = *src0++; + px1 = *src1++; + + *dst++ = (px0 * weight0 + px1 * weight1) >> 8; + } + } + + // Copy last row (without referring to the last-plus-oneth row) + x = xCount; + while (x--) + { + *dst++ = *src0++; + } +} + + +#if !OOLITE_NATIVE_64_BIT + +static void StretchVerticallyN_x4(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes) +{ + uint8_t *src; + uint32_t *src0, *src1, *prev, *dst; + uint32_t px0, px1, ag, br; + uint_fast32_t x, y, xCount, srcRowBytes; + uint_fast16_t weight0, weight1; + uint_fast32_t fractY; // Y coordinate, fixed-point (24.8) + + src = srcPx.pixels; + srcRowBytes = srcPx.rowBytes; + dst = dstPx.pixels; // Assumes no row padding. + + src0 = prev = (uint32_t *)src; + + xCount = (srcPx.width * planes) >> 2; + + for (y = 1; y != dstPx.height; ++y) + { + fractY = ((srcPx.height * y) << 8) / dstPx.height; + + src0 = prev; + prev = src1 = (uint32_t *)(src + srcRowBytes * (fractY >> 8)); + + weight1 = fractY & 0xFF; + weight0 = 0x100 - weight1; + + x = xCount; + while (x--) + { + px0 = *src0++; + px1 = *src1++; + + ag = ((px0 & 0xFF00FF00) >> 8) * weight0 + ((px1 & 0xFF00FF00) >> 8) * weight1; + br = (px0 & 0x00FF00FF) * weight0 + (px1 & 0x00FF00FF) * weight1; + + *dst++ = (ag & 0xFF00FF00) | ((br >> 8) & 0x00FF00FF); + } + } + + // Copy last row (without referring to the last-plus-oneth row) + x = xCount; + while (x--) + { + *dst++ = *src0++; + } +} + +#else // OOLITE_NATIVE_64_BIT + +static void StretchVerticallyN_x8(OOScalerPixMap srcPx, OOScalerPixMap dstPx, OOTexturePlaneCount planes) +{ + uint8_t *src; + uint64_t *src0, *src1, *prev, *dst; + uint64_t px0, px1, agag, brbr; + uint_fast32_t x, y, xCount, srcRowBytes; + uint_fast16_t weight0, weight1; + uint_fast32_t fractY; // Y coordinate, fixed-point (24.8) + + src = srcPx.pixels; + srcRowBytes = srcPx.rowBytes; + dst = dstPx.pixels; // Assumes dstPx.width == dstPx.rowBytes. + + src0 = prev = (uint64_t *)src; + + xCount = (srcPx.width * planes) >> 3; + + for (y = 1; y != dstPx.height; ++y) + { + fractY = ((srcPx.height * y) << 8) / dstPx.height; + + src0 = prev; + prev = src1 = (uint64_t *)(src + srcRowBytes * (fractY >> 8)); + + weight1 = fractY & 0xFF; + weight0 = 0x100 - weight1; + + x = xCount; + while (x--) + { + px0 = *src0++; + px1 = *src1++; + + agag = ((px0 & 0xFF00FF00FF00FF00ULL) >> 8) * weight0 + ((px1 & 0xFF00FF00FF00FF00ULL) >> 8) * weight1; + brbr = (px0 & 0x00FF00FF00FF00FFULL) * weight0 + (px1 & 0x00FF00FF00FF00FFULL) * weight1; + + *dst++ = (agag & 0xFF00FF00FF00FF00ULL) | ((brbr >> 8) & 0x00FF00FF00FF00FFULL); + } + } + + // Copy last row (without referring to the last-plus-oneth row) + x = xCount; + while (x--) + { + *dst++ = *src0++; + } +} +#endif + + +static void StretchHorizontally1(OOScalerPixMap srcPx, OOScalerPixMap dstPx) +{ + uint8_t *src, *srcStart, *dst; + uint8_t px0, px1; + uint_fast32_t x, y, xCount, srcRowBytes; + uint_fast16_t weight0, weight1; + uint_fast32_t fractX, deltaX; // X coordinate, fixed-point (20.12), allowing widths up to 1 mebipixel + + srcStart = srcPx.pixels; + srcRowBytes = srcPx.rowBytes; + xCount = dstPx.width; + dst = dstPx.pixels; // Assumes no row padding + + deltaX = (srcPx.width << 12) / dstPx.width; + px1 = *srcStart; + + for (y = 0; y < dstPx.height - 1; ++y) + { + fractX = 0; + + for (x = 0; x!= xCount; ++x) + { + fractX += deltaX; + + weight1 = (fractX >> 4) & 0xFF; + weight0 = 0x100 - weight1; + + px0 = px1; + src = srcStart + (fractX >> 12); + px1 = *src; + + *dst++ = (px0 * weight0 + px1 * weight1) >> 8; + } + + srcStart = (uint8_t *)((char *)srcStart + srcRowBytes); + px1 = *srcStart; + } + + // Copy last row without reading off end of buffer + fractX = 0; + for (x = 0; x!= xCount; ++x) + { + fractX += deltaX; + + weight1 = (fractX >> 4) & 0xFF; + weight0 = 0x100 - weight1; + + px0 = px1; + src = srcStart + (fractX >> 12); + px1 = *src; + + *dst++ = (px0 * weight0 + px1 * weight1) >> 8; + } +} + + +static void StretchHorizontally4(OOScalerPixMap srcPx, OOScalerPixMap dstPx) +{ + uint32_t *src, *srcStart, *dst; + uint32_t px0, px1; + uint32_t ag, br; + uint_fast32_t x, y, xCount, srcRowBytes; + uint_fast16_t weight0, weight1; + uint_fast32_t fractX, deltaX; // X coordinate, fixed-point (20.12), allowing widths up to 1 mebipixel + + srcStart = srcPx.pixels; + srcRowBytes = srcPx.rowBytes; + xCount = dstPx.width; + dst = dstPx.pixels; // Assumes no row padding + + deltaX = (srcPx.width << 12) / dstPx.width; + px1 = *srcStart; + + for (y = 0; y < dstPx.height - 1; ++y) + { + fractX = 0; + + for (x = 0; x!= xCount; ++x) + { + fractX += deltaX; + + weight1 = (fractX >> 4) & 0xFF; + weight0 = 0x100 - weight1; + + px0 = px1; + src = srcStart + (fractX >> 12); + px1 = *src; + + ag = ((px0 & 0xFF00FF00) >> 8) * weight0 + ((px1 & 0xFF00FF00) >> 8) * weight1; + br = (px0 & 0x00FF00FF) * weight0 + (px1 & 0x00FF00FF) * weight1; + + *dst++ = (ag & 0xFF00FF00) | ((br & 0xFF00FF00) >> 8); + } + + srcStart = (uint32_t *)((char *)srcStart + srcRowBytes); + px1 = *srcStart; + } + + // Copy last row without reading off end of buffer + fractX = 0; + for (x = 0; x!= xCount; ++x) + { + fractX += deltaX; + + weight1 = (fractX >> 4) & 0xFF; + weight0 = 0x100 - weight1; + + px0 = px1; + src = srcStart + (fractX >> 12); + if (EXPECT(x < xCount - 1)) px1 = *src; + + ag = ((px0 & 0xFF00FF00) >> 8) * weight0 + ((px1 & 0xFF00FF00) >> 8) * weight1; + br = (px0 & 0x00FF00FF) * weight0 + (px1 & 0x00FF00FF) * weight1; + + *dst++ = (ag & 0xFF00FF00) | ((br & 0xFF00FF00) >> 8); + } +} + + +static void SqueezeHorizontally1(OOScalerPixMap srcPx, OOTextureDimension dstWidth) +{ + uint8_t *src, *srcStart, *dst; + uint8_t borderPx; + uint_fast32_t x, y, xCount, endX, srcRowBytes; + uint_fast32_t fractX, endFractX, deltaX; + uint_fast32_t accum, weight; + uint_fast8_t borderWeight; + + srcStart = srcPx.pixels; + dst = srcStart; // Output is placed in same buffer, without line padding. + srcRowBytes = srcPx.rowBytes; + + deltaX = (srcPx.width << 12) / dstWidth; + + for (y = 0; y != srcPx.height; ++y) + { + borderPx = *srcStart; + endFractX = 0; + borderWeight = 0; + + src = srcStart; + + x = 0; + xCount = dstWidth; + while (xCount--) + { + fractX = endFractX; + endFractX += deltaX; + endX = endFractX >> 12; + + borderWeight = 0xFF - borderWeight; + accum = borderPx * borderWeight; + weight = borderWeight; + + borderWeight = (endFractX >> 4) & 0xFF; + weight += borderWeight; + + for (;;) + { + ++x; + if (EXPECT(x == endX)) + { + if (EXPECT(xCount)) borderPx = *++src; + accum += borderPx * borderWeight; + break; + } + else + { + accum += *++src * 0xFF; + weight += 0xFF; + } + } + + *dst++ = accum / weight; + } + + srcStart = (uint8_t *)((char *)srcStart + srcRowBytes); + } +} + + +static void SqueezeVertically1(OOScalerPixMap srcPx, OOTextureDimension dstHeight) +{ + uint8_t *src, *srcStart, *dst; + uint_fast32_t x, y, xCount, startY, endY, srcRowBytes, lastRow; + uint_fast32_t fractY, endFractY, deltaY; + uint_fast32_t accum, weight; + uint_fast8_t startWeight, endWeight; + + dst = srcPx.pixels; // Output is placed in same buffer, without line padding. + srcRowBytes = srcPx.rowBytes; + xCount = srcPx.width; + + deltaY = (srcPx.height << 12) / dstHeight; + endFractY = 0; + + endWeight = 0; + endY = 0; + + lastRow = srcPx.height - 1; + + while (endY < lastRow) + { + fractY = endFractY; + endFractY += deltaY; + startY = endY; + endY = endFractY >> 12; + + startWeight = 0xFF - endWeight; + endWeight = (endFractY >> 4) & 0xFF; + + srcStart = (uint8_t *)((char *)srcPx.pixels + srcRowBytes * startY); + + for (x = 0; x != xCount; ++x) + { + src = srcStart++; + accum = startWeight * *src; + weight = startWeight + endWeight; + + y = startY; + for (;;) + { + ++y; + src = (uint8_t *)((char *)src + srcRowBytes); + if (EXPECT_NOT(y == endY)) + { + if (EXPECT(endY < lastRow)) accum += *src * endWeight; + break; + } + else + { + accum += *src * 0xFF; + weight += 0xFF; + } + } + + *dst++ = accum / weight; + } + } +} + + +// Macros to manage 4-channel accumulators in 4-channel squeeze scalers. +#define ACCUM(PX, WT) do { \ + uint32_t px = PX; \ + uint_fast32_t wt = WT; \ + ag = ((px & 0xFF00FF00) >> 8) * wt; \ + br = (px & 0x00FF00FF) * wt; \ + accum1 += ag >> 16; \ + accum2 += br >> 16; \ + accum3 += ag & 0xFFFF; \ + accum4 += br & 0xFFFF; \ + weight += wt; \ + } while (0) + +#define CLEAR_ACCUM() do { \ + accum1 = 0; \ + accum2 = 0; \ + accum3 = 0; \ + accum4 = 0; \ + weight = 0; \ + } while (0) + +/* These integer divisions cause a stall -- this is the biggest + bottleneck in this file. Unrolling the loop might help on PPC. + Linear interpolation instead of box filtering would help, with + a quality hit. Given that scaling doesn't happen very often, + I think I'll leave it this way. -- Ahruman +*/ +#define ACCUM2PX() ( \ + (((accum1 / weight) & 0xFF) << 24) | \ + (((accum3 / weight) & 0xFF) << 8) | \ + (((accum2 / weight) & 0xFF) << 16) | \ + ((accum4 / weight) & 0xFF) \ + ) + + +static void SqueezeHorizontally4(OOScalerPixMap srcPx, OOTextureDimension dstWidth) +{ + uint32_t *src, *srcStart, *dst; + uint32_t borderPx, ag, br; + uint_fast32_t x, y, xCount, endX, srcRowBytes; + uint_fast32_t fractX, endFractX, deltaX; + uint_fast32_t accum1, accum2, accum3, accum4, weight; + uint_fast8_t borderWeight; + + srcStart = srcPx.pixels; + dst = srcStart; // Output is placed in same buffer, without line padding. + srcRowBytes = srcPx.rowBytes; + + deltaX = (srcPx.width << 12) / dstWidth; + + for (y = 0; y != srcPx.height; ++y) + { + borderPx = *srcStart; + endFractX = 0; + borderWeight = 0; + + src = srcStart; + + x = 0; + xCount = dstWidth; + while (xCount--) + { + fractX = endFractX; + endFractX += deltaX; + endX = endFractX >> 12; + + CLEAR_ACCUM(); + + borderWeight = 0xFF - borderWeight; + ACCUM(borderPx, borderWeight); + + borderWeight = (endFractX >> 4) & 0xFF; + + for (;;) + { + ++x; + if (x == endX) + { + if (xCount) borderPx = *++src; + ACCUM(borderPx, borderWeight); + break; + } + else + { + ACCUM(*++src, 0xFF); + } + } + + *dst++ = ACCUM2PX(); + } + + srcStart = (uint32_t *)((char *)srcStart + srcRowBytes); + } +} + + +static void SqueezeVertically4(OOScalerPixMap srcPx, OOTextureDimension dstHeight) +{ + uint32_t *src, *srcStart, *dst; + uint_fast32_t x, y, xCount, startY, endY, srcRowBytes, lastRow; + uint32_t ag, br; + uint_fast32_t fractY, endFractY, deltaY; + uint_fast32_t accum1, accum2, accum3, accum4, weight; + uint_fast8_t startWeight, endWeight; + + dst = srcPx.pixels; // Output is placed in same buffer, without line padding. + srcRowBytes = srcPx.rowBytes; + xCount = srcPx.width; + + deltaY = (srcPx.height << 12) / dstHeight; + endFractY = 0; + + endWeight = 0; + endY = 0; + + lastRow = srcPx.height - 1; + + while (endY < lastRow) + { + fractY = endFractY; + endFractY += deltaY; + startY = endY; + endY = endFractY >> 12; + + startWeight = 0xFF - endWeight; + endWeight = (endFractY >> 4) & 0xFF; + + srcStart = (uint32_t *)((char *)srcPx.pixels + srcRowBytes * startY); + + for (x = 0; x != xCount; ++x) + { + src = srcStart++; + + CLEAR_ACCUM(); + ACCUM(*src, startWeight); + + y = startY; + for (;;) + { + ++y; + src = (uint32_t *)((char *)src + srcRowBytes); + if (EXPECT_NOT(y == endY)) + { + if (EXPECT(endY <= lastRow)) ACCUM(*src, endWeight); + break; + } + else + { + ACCUM(*src, 0xFF); + } + } + + *dst++ = ACCUM2PX(); + } + } +} + + +static BOOL EnsureCorrectDataSize(OOScalerPixMap *pixMap, BOOL leaveSpaceForMipMaps) +{ + size_t correctSize; + void *bytes = NULL; + + correctSize = pixMap->rowBytes * pixMap->height; + + /* Ensure that the block is not too small. This needs to be done before + adding the mip-map space, as the texture may have been shrunk in place + without being grown for mip-maps. + */ + if (EXPECT_NOT(pixMap->dataSize < correctSize)) + { + OOLogGenericParameterError(); + return NO; + } + + if (leaveSpaceForMipMaps) correctSize = correctSize * 4 / 3; + if (correctSize != pixMap->dataSize) + { + bytes = realloc(pixMap->pixels, correctSize); + if (EXPECT_NOT(bytes == NULL)) free(pixMap->pixels); + pixMap->pixels = bytes; + pixMap->dataSize = correctSize; + } + + return YES; +} diff --git a/src/Core/OOTriangle.h b/src/Core/OOTriangle.h new file mode 100644 index 00000000..621f8f31 --- /dev/null +++ b/src/Core/OOTriangle.h @@ -0,0 +1,62 @@ +/* + +OOTriangle.h + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOTriangle.h directly; include OOMaths.h. +#else + + +typedef struct +{ + Vector v[4]; // three vertices + normal +} Triangle; + + +/* Calculate normal for triangle, storing it in v[3] */ +Vector calculateNormalForTriangle(Triangle *ioTriangle) NONNULL_FUNC; + +/* Generate a triangle from three vertices. Also calculates normal. */ +OOINLINE Triangle make_triangle(Vector v0, Vector v1, Vector v2) CONST_FUNC; + +/* resolve vector in arbitrary ijk vectors */ +Vector resolveVectorInIJK(Vector v0, Triangle ijk); + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE Triangle make_triangle(Vector v0, Vector v1, Vector v2) +{ + Triangle result; + result.v[0] = v0; + result.v[1] = v1; + result.v[2] = v2; + calculateNormalForTriangle(&result); + return result; +} + + +#endif /* INCLUDED_OOMATHS_h */ diff --git a/src/Core/OOTriangle.m b/src/Core/OOTriangle.m new file mode 100644 index 00000000..ae7c600a --- /dev/null +++ b/src/Core/OOTriangle.m @@ -0,0 +1,47 @@ +/* + +OOTriangle.m + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOMaths.h" + + +Vector calculateNormalForTriangle(Triangle *tri) +{ + Vector v01 = vector_subtract(tri->v[1], tri->v[0]); + Vector v12 = vector_subtract(tri->v[2], tri->v[1]); + tri->v[3] = cross_product( v01, v12); + return tri->v[3]; +} + + +Vector resolveVectorInIJK(Vector v0, Triangle ijk) +{ + Vector result; + result.x = dot_product(v0, ijk.v[0]); + result.y = dot_product(v0, ijk.v[1]); + result.z = dot_product(v0, ijk.v[2]); + return result; +} diff --git a/src/Core/OOTrumble.h b/src/Core/OOTrumble.h new file mode 100644 index 00000000..b2302afd --- /dev/null +++ b/src/Core/OOTrumble.h @@ -0,0 +1,164 @@ +/* + +OOTrumble.h + +Implements cute, fuzzy trumbles. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOOpenGL.h" + + +@class PlayerEntity, AI, OOSound, OOTexture; + +#define TRUMBLE_MAX_ROTATION 15.0 +#define TRUMBLE_MAX_ROTATIONAL_VELOCITY 5.0 + +#define TRUMBLE_GROWTH_RATE 0.01 + +enum trumble_animation +{ + TRUMBLE_ANIM_NONE = 0, + TRUMBLE_ANIM_IDLE, + TRUMBLE_ANIM_BLINK, + TRUMBLE_ANIM_SNARL, + TRUMBLE_ANIM_PROOT, + TRUMBLE_ANIM_SHUDDER, + TRUMBLE_ANIM_STONED, + TRUMBLE_ANIM_SPAWN, + TRUMBLE_ANIM_SLEEP, + TRUMBLE_ANIM_DIE +}; + +enum trumble_eyes +{ + TRUMBLE_EYES_NONE = 0, + TRUMBLE_EYES_OPEN, + TRUMBLE_EYES_SHUT, + TRUMBLE_EYES_WIDE +}; + +enum trumble_mouth +{ + TRUMBLE_MOUTH_NONE = 0, + TRUMBLE_MOUTH_POUT, + TRUMBLE_MOUTH_GROWL, + TRUMBLE_MOUTH_SNARL, + TRUMBLE_MOUTH_NORMAL +}; + +float trumbleAppetiteAccumulator; + +@interface OOTrumble : NSObject +{ + PlayerEntity *player; // owning entity (not retained) + // + unichar digram[2]; // seed for pseudo-randomly setting up Trumble (pair of characters) + // + GLfloat colorBase[4]; // color of Trumble + GLfloat colorPoint1[4]; // color of Trumble (variation 1) + GLfloat colorPoint2[4]; // color of Trumble (variation 2) + GLfloat colorEyes[4]; // color of Trumble (eye color) + GLfloat *pointColor[6]; // pointscheme + // + GLfloat hunger; // behaviour modifier 0.0 (satiated) to 1.0 (starving) + GLfloat discomfort; // behaviour modifier 0.0 (very happy) to 1.0 (extremely uncomfortable) + // + GLfloat size; // 0.0 -> max_size + GLfloat max_size; // 0.90 -> 1.25 + GLfloat growth_rate; // diff to size per sec. + // + GLfloat rotation; // CW rotation in radians (starts at 0.0) + GLfloat rotational_velocity; // +r (radians/sec) + // + NSPoint position; // x, y onscreen relative to center of screen + NSPoint movement; // +x, +y (screen units / sec) + // + NSPoint eye_position; // current position of eyes relative to their starting position + NSPoint mouth_position; // current position of eyes relative to their starting position + // + double animationTime; // set to 0.0 at start of current animation + double animationDuration; // set to 0.0 at start of current animation + // + enum trumble_animation animation; // current animation sequence + enum trumble_animation nextAnimation; // next animation sequence + // + int animationStage; // sub-sequence within animation + // + enum trumble_mouth mouthFrame; // which mouth position - determines what part of the texture to display + enum trumble_eyes eyeFrame; // which eye position - determines what part of the texture to display + // + OOTexture *texture; + // + GLfloat saved_float1, saved_float2; + // + BOOL readyToSpawn; +} + +- (id) initForPlayer:(PlayerEntity*) p1; +- (id) initForPlayer:(PlayerEntity*) p1 digram:(NSString*) digramString; + +- (void) setupForPlayer:(PlayerEntity*) p1 digram:(NSString*) digramString; + +- (void) spawnFrom:(OOTrumble*) parentTrumble; + +- (void) calcGrowthRate; + +- (unichar *) digram; +- (NSPoint) position; +- (NSPoint) movement; +- (GLfloat) rotation; +- (GLfloat) size; +- (GLfloat) hunger; +- (GLfloat) discomfort; + +// AI methods here +- (void) actionIdle; +- (void) actionBlink; +- (void) actionSnarl; +- (void) actionProot; +- (void) actionShudder; +- (void) actionStoned; +- (void) actionPop; +- (void) actionSleep; +- (void) actionSpawn; + +- (void) randomizeMotionX; +- (void) randomizeMotionY; + +- (void) drawTrumble:(double) z; +- (void) updateTrumble:(double) delta_t; + +- (void) updateIdle:(double) delta_t; +- (void) updateBlink:(double) delta_t; +- (void) updateSnarl:(double) delta_t; +- (void) updateProot:(double) delta_t; +- (void) updateShudder:(double) delta_t; +- (void) updateStoned:(double) delta_t; +- (void) updatePop:(double) delta_t; +- (void) updateSleep:(double) delta_t; +- (void) updateSpawn:(double) delta_t; + +- (NSDictionary*) dictionary; +- (void) setFromDictionary:(NSDictionary*) dict; + +@end diff --git a/src/Core/OOTrumble.m b/src/Core/OOTrumble.m new file mode 100644 index 00000000..f05e3a56 --- /dev/null +++ b/src/Core/OOTrumble.m @@ -0,0 +1,1006 @@ +/* + +OOTrumble.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOTrumble.h" +#import "Universe.h" +#import "PlayerEntity.h" +#import "OOTexture.h" +#import "ResourceManager.h" +#import "OOSound.h" +#import "OOStringParsing.h" + + +static void InitTrumbleSounds(void); +static void PlayTrumbleIdle(void); +static void PlayTrumbleSqueal(void); + + +@implementation OOTrumble + +- (id) init +{ + self = [super init]; + + int i; + for (i = 0; i < 4; i++) + { + colorPoint1[i] = 1.0; + colorPoint2[i] = 1.0; + } + + return self; +} + +- (id) initForPlayer:(PlayerEntity*) p1 +{ + self = [super init]; + + [self setupForPlayer: p1 digram: @"a1"]; + + return self; +} + +- (id) initForPlayer:(PlayerEntity*) p1 digram:(NSString*) digramString +{ + self = [super init]; + + [self setupForPlayer: p1 digram: digramString]; + + return self; +} + +- (void) setupForPlayer:(PlayerEntity*) p1 digram:(NSString*) digramString +{ + // set digram + // + digram[0] = [digramString characterAtIndex:0]; + digram[1] = [digramString characterAtIndex:1]; + + // set player + // + player = p1; + + // set color points + int r0 = (int)digram[0]; + int r1 = (int)digram[1]; + int pointscheme[6] = { 1, 1, 0, 0, 1, 1}; + int ps = r0 >> 2; // first digram determines pattern of points + pointscheme[0] = (ps >> 3) & 1; + pointscheme[1] = (ps >> 2) & 1; + pointscheme[2] = (ps >> 1) & 1; + pointscheme[3] = ps & 1; + pointscheme[4] = (ps >> 2) & 1; + pointscheme[5] = (ps >> 3) & 1; + + GLfloat c1[4] = { 1.0, 1.0, 1.0, 1.0}; + GLfloat c2[4] = { 1.0, 0.0, 0.0, 1.0}; + + // I am missing something. Please clarify the intent of the following statement. + // The next statement shift the result of adding two masked values + // max_size = 0.90 + 0.50 * (((r0 & 0x38) + (r1 & 0x38)) >> 3) / 63.0; // inheritable + // The next statement adds masked(r0) to shifted(masked(r1) + // max_size = 0.90 + 0.50 * ((r0 & 0x38) + ((r1 & 0x38) >> 3)) / 63.0; // inheritable + // Sorry, but I cannot determine what you intended to do here. + // + // GILES: It's the second one, we're just determining a pseudo random max_size from the first digram + max_size = 0.90 + 0.50 * ((r0 & 0x38) + ((r1 & 0x38) >> 3)) / 63.0; // inheritable + + // seed the random number generator + // + ranrot_srand(r0 + r1 * 256); + + // set random colors + int col1 = r0 & 7; + int col2 = r1 & 7; + while (((col1 == 7)||(col1 == 0)) && ((col2 == 7)||(col2 == 0))) // monochrome not allowed + { + if (col1 == col2) + col1 = ranrot_rand() & 7; + else + col2 = ranrot_rand() & 7; + } + c1[0] = (GLfloat)(col1 & 1); + c1[1] = (GLfloat)((col1 >> 1) & 1); + c1[2] = (GLfloat)((col1 >> 2) & 1); + c2[0] = (GLfloat)(col2 & 1); + c2[1] = (GLfloat)((col2 >> 1) & 1); + c2[2] = (GLfloat)((col2 >> 2) & 1); + if (col1 == 0) + { + c1[0] = 0.5 + 0.1 * c2[1]; c1[1] = 0.5 + 0.1 * c2[2]; c1[2] = 0.5; + } + if (col1 == 7) + { + c1[0] = 1.0 - 0.1 * c2[1]; c1[1] = 1.0 - 0.1 * c2[2]; c1[2] = 0.9; + } + if (col2 == 0) + { + c2[0] = 0.5 + 0.1 * c1[2]; c2[1] = 0.5 + 0.1 * c1[0]; c2[2] = 0.5; + } + if (col2 == 7) + { + c2[0] = 1.0 - 0.1 * c1[2]; c2[1] = 1.0 - 0.1 * c1[0]; c2[2] = 0.9; + } + + // position and motion + // + position.x = (ranrot_rand() & 15)* 28 - 210; + position.y = (ranrot_rand() & 15)* 28 - 210; + // + [self randomizeMotionX]; + [self randomizeMotionY]; + + // rotation + // + rotation = TRUMBLE_MAX_ROTATION * (randf() - randf()); + rotational_velocity = TRUMBLE_MAX_ROTATIONAL_VELOCITY * (randf() - randf()); + + // + int i; + for (i = 0; i < 4; i++) + { + colorPoint1[i] = c1[i]; + colorPoint2[i] = c2[i]; + } + // + for (i = 0; i < 6; i++) + { + pointColor[i] = colorPoint1; + if (pointscheme[i] == 0) + pointColor[i] = colorPoint1; + if (pointscheme[i] == 1) + pointColor[i] = colorPoint2; + } + // + for (i = 0; i < 4; i++) + { + colorEyes[i] = 0.2 * (2.0 * pointColor[3][i] + 2.0 * pointColor[1][i] + 1.0); // eyes - paler than average + colorBase[i] = 0.5 * (pointColor[2][i] + pointColor[3][i]); // mouth + } + // + size = 0.5 * (1.0 + randf()); + [self calcGrowthRate]; + hunger = 0.0; + discomfort = 0.0; + // + eye_position = NSMakePoint( 0.0, 0.075 * (randf() - randf())); + eyeFrame = TRUMBLE_EYES_OPEN; + // + mouth_position = NSMakePoint( 0.0, 0.035 * (randf() - randf())); + mouthFrame = TRUMBLE_MOUTH_NORMAL; + // + animation = TRUMBLE_ANIM_IDLE; + nextAnimation = TRUMBLE_ANIM_IDLE; + animationTime = 0.0; + animationDuration = 1.5 + randf() * 3.0; // time until next animation + // + texture = [OOTexture textureWithName:@"trumblekit.png" + inFolder:@"Textures" + options:kOOTextureDefaultOptions | kOOTextureNoShrink + anisotropy:0.0f + lodBias:kOOTextureDefaultLODBias]; + [texture retain]; + + InitTrumbleSounds(); + + readyToSpawn = NO; +} + +- (void) dealloc +{ + [texture release]; + + [super dealloc]; +} + +- (void) spawnFrom:(OOTrumble*) parentTrumble +{ + if (parentTrumble) + { + // mutate.. + unichar mutation1 = ranrot_rand() & ranrot_rand() & ranrot_rand() & 0xff; // each bit has a 1/8 chance of being set + unichar mutation2 = ranrot_rand() & ranrot_rand() & ranrot_rand() & 0xff; // each bit has a 1/8 chance of being set + unichar* parentdigram = [parentTrumble digram]; + unichar newdigram[2]; + newdigram[0] = parentdigram[0] ^ mutation1; + newdigram[1] = parentdigram[1] ^ mutation2; + // + [self setupForPlayer: player digram: [NSString stringWithCharacters:newdigram length:2]]; + // + size = [parentTrumble size] * 0.4; + if (size < 0.5) + size = 0.5; // minimum size + position = [parentTrumble position]; + rotation = [parentTrumble rotation]; + movement = [parentTrumble movement]; + movement.y += 8.0; // emerge! + } + else + { + size = 0.5; // minimum size + position.x = (ranrot_rand() & 15)* 28 - 210; + position.y = (ranrot_rand() & 15)* 28 - 210; + [self randomizeMotionX]; + [self randomizeMotionY]; + rotation = TRUMBLE_MAX_ROTATION * (randf() - randf()); + rotational_velocity = TRUMBLE_MAX_ROTATIONAL_VELOCITY * (randf() - randf()); + } + hunger = 0.25; + [self calcGrowthRate]; + discomfort = 0.0; + [self actionSleep]; +} + +- (void) calcGrowthRate +{ + float rsize = size / max_size; + growth_rate = TRUMBLE_GROWTH_RATE * (1.0 - rsize); +} + + +- (unichar *) digram +{ + return digram; +} + +- (NSPoint) position +{ + return position; +} + +- (NSPoint) movement +{ + return movement; +} + +- (GLfloat) rotation +{ + return rotation; +} + +- (GLfloat) size +{ + return size; +} + +- (GLfloat) hunger +{ + return hunger; +} + +- (GLfloat) discomfort +{ + return discomfort; +} + + + +// AI methods here +- (void) actionIdle +{ + nextAnimation = TRUMBLE_ANIM_IDLE; + animationDuration = 1.5 + 3.0 * randf(); // time until next animation +} + +- (void) actionBlink +{ + nextAnimation = TRUMBLE_ANIM_BLINK; + animationDuration = 0.5 + 0.5 * randf(); // time until next animation +} + +- (void) actionSnarl +{ + nextAnimation = TRUMBLE_ANIM_SNARL; + animationDuration = 4.0 + 1.0 * randf(); // time until next animation +} + +- (void) actionProot +{ + nextAnimation = TRUMBLE_ANIM_PROOT; + animationDuration = 1.5 + 0.5 * randf(); // time until next animation +} + +- (void) actionShudder +{ + nextAnimation = TRUMBLE_ANIM_SHUDDER; + animationDuration = 2.25 + randf() * 1.5; // time until next animation +} + +- (void) actionStoned +{ + nextAnimation = TRUMBLE_ANIM_STONED; + animationDuration = 1.5 + randf() * 3.0; // time until next animation +} + +- (void) actionPop +{ + nextAnimation = TRUMBLE_ANIM_DIE; + animationDuration = 1.5 + randf() * 3.0; // time until next animation +} + +- (void) actionSleep +{ + nextAnimation = TRUMBLE_ANIM_SLEEP; + animationDuration = 12.0 + 12.0 * randf(); // time until next animation +} + +- (void) actionSpawn +{ + nextAnimation = TRUMBLE_ANIM_SPAWN; + animationDuration = 9.0 + 3.0 * randf(); // time until next animation +} + + +- (void) randomizeMotionX +{ + movement.x = 36 * (randf() - 0.5); + movement.x += (movement.x > 0)? 2.0: -2.0; + rotational_velocity = TRUMBLE_MAX_ROTATIONAL_VELOCITY * (randf() - randf()); +} + +- (void) randomizeMotionY +{ + movement.y = 36 * (randf() - 0.5); + movement.y += (movement.y > 0)? 2.0: -2.0; + rotational_velocity = TRUMBLE_MAX_ROTATIONAL_VELOCITY * (randf() - randf()); +} + +- (void) drawTrumble:(double) z +{ + /* + draws a trumble body as a fan of triangles... + 2-------3-------4 + | \ | / | + | \ | / | + | \ | / | + 1-------0-------5 + + */ + GLfloat wd = 96 * size; + GLfloat ht = 96 * size; + glShadeModel(GL_SMOOTH); + glEnable(GL_TEXTURE_2D); + [texture apply]; + // + glPushMatrix(); + // + glTranslatef( position.x, position.y, z); + glRotatef( rotation, 0.0, 0.0, 1.0); + + // + // Body.. + // + glBegin(GL_TRIANGLE_FAN); + // + glColor4fv(pointColor[3]); + glTexCoord2f( 0.25, 0.5); + glVertex2f( 0.0, -0.5 * ht); + // + glColor4fv(pointColor[0]); + glTexCoord2f( 0.0, 0.5); + glVertex2f( -0.5 * wd, -0.5 * ht); + // + glColor4fv(pointColor[1]); + glTexCoord2f( 0.0, 0.0); + glVertex2f( -0.5 * wd, 0.5 * ht); + // + glColor4fv(pointColor[2]); + glTexCoord2f( 0.25, 0.0); + glVertex2f( 0.0, 0.5 * ht); + // + glColor4fv(pointColor[4]); + glTexCoord2f( 0.5, 0.0); + glVertex2f( 0.5 * wd, 0.5 * ht); + // + glColor4fv(pointColor[5]); + glTexCoord2f( 0.5, 0.5); + glVertex2f( 0.5 * wd, -0.5 * ht); + // + glEnd(); + + // + // Eyes + // + GLfloat eyeTextureOffset = 0.0; + switch(eyeFrame) + { + case TRUMBLE_EYES_NONE : + case TRUMBLE_EYES_OPEN : + eyeTextureOffset = 0.0; break; + case TRUMBLE_EYES_SHUT : + eyeTextureOffset = 0.25; break; + case TRUMBLE_EYES_WIDE : + eyeTextureOffset = 0.5; break; + } + // + glTranslatef( eye_position.x * wd, eye_position.y * ht, 0.0); + // + glBegin(GL_QUADS); + // + glColor4fv(colorEyes); + // + glTexCoord2f( 0.5, eyeTextureOffset); + glVertex2f( -0.5 * wd, 0.20 * ht); + // + glTexCoord2f( 1.0, eyeTextureOffset); + glVertex2f( 0.5 * wd, 0.20 * ht); + // + glTexCoord2f( 1.0, eyeTextureOffset + 0.25); + glVertex2f( 0.5 * wd, -0.30 * ht); + // + glTexCoord2f( 0.5, eyeTextureOffset + 0.25); + glVertex2f( -0.5 * wd, -0.30 * ht); + // + glEnd(); + + // + // Mouth + // + GLfloat mouthTextureOffset = 0.0; + switch(mouthFrame) + { + case TRUMBLE_MOUTH_POUT : + mouthTextureOffset = 0.500; break; + case TRUMBLE_MOUTH_NONE : + case TRUMBLE_MOUTH_NORMAL : + mouthTextureOffset = 0.625; break; + case TRUMBLE_MOUTH_GROWL: + mouthTextureOffset = 0.750; break; + case TRUMBLE_MOUTH_SNARL: + mouthTextureOffset = 0.875; break; + } + // + glTranslatef( mouth_position.x * wd, mouth_position.y * ht, 0.0); + // + glBegin(GL_QUADS); + // + glColor4fv(colorBase); + // + glTexCoord2f( 0.0, mouthTextureOffset); + glVertex2f( -0.25 * wd, -0.10 * ht); + // + glTexCoord2f( 0.25, mouthTextureOffset); + glVertex2f( 0.25 * wd, -0.10 * ht); + // + glTexCoord2f( 0.25, mouthTextureOffset + 0.125); + glVertex2f( 0.25 * wd, -0.35 * ht); + // + glTexCoord2f( 0.0, mouthTextureOffset + 0.125); + glVertex2f( -0.25 * wd, -0.35 * ht); + // + glEnd(); + + // finally.. + glPopMatrix(); + glDisable(GL_TEXTURE_2D); +} + +- (void) updateTrumble:(double) delta_t +{ + // player movement + NSPoint p_mov = NSMakePoint(TRUMBLE_MAX_ROTATIONAL_VELOCITY * [player dialPitch], TRUMBLE_MAX_ROTATIONAL_VELOCITY * [player dialRoll]); + switch ([UNIVERSE viewDirection]) + { + GLfloat t; + case VIEW_AFT: + p_mov.x = -p_mov.x; + p_mov.y = -p_mov.y; + break; + case VIEW_STARBOARD: + t = p_mov.x; + p_mov.x = -p_mov.y; + p_mov.y = t; + break; + case VIEW_PORT: + t = p_mov.x; + p_mov.x = p_mov.y; + p_mov.y = -t; + break; + + default: + break; + } + p_mov.x *= -4.0; + + // movement + // + GLfloat wd = 0.5 * 96 * size; + GLfloat ht = 0.5 * 96 * size; + // + GLfloat bumpx = 320 * 1.5 - wd; + GLfloat bumpy = 240 * 1.5 - ht; + // + position.x += delta_t * movement.x; + if ((position.x < -bumpx)||(position.x > bumpx)) + { + position.x = (position.x < -bumpx)? -bumpx : bumpx; + [self randomizeMotionX]; + } + position.y += delta_t * (movement.y + p_mov.x); + if ((position.y < -bumpy)||(position.y > bumpy)) + { + position.y = (position.y < -bumpy)? -bumpy : bumpy; + [self randomizeMotionY]; + } + + // rotation + // + rotation += delta_t * (rotational_velocity + p_mov.y); + if (animation != TRUMBLE_ANIM_DIE) + { + if (rotation < -TRUMBLE_MAX_ROTATION) + { + rotation = -TRUMBLE_MAX_ROTATION; + rotational_velocity = TRUMBLE_MAX_ROTATIONAL_VELOCITY * 0.5 * (randf() + randf()); + } + if (rotation > TRUMBLE_MAX_ROTATION) + { + rotation = TRUMBLE_MAX_ROTATION; + rotational_velocity = -TRUMBLE_MAX_ROTATIONAL_VELOCITY * 0.5 * (randf() + randf()); + } + } + // growth + // + size += delta_t * growth_rate; + hunger += delta_t * (growth_rate + TRUMBLE_GROWTH_RATE); + if (size > max_size) // fully_grown.. stop growing + { + size = max_size; + growth_rate = 0.0; + } + [self calcGrowthRate]; + if (hunger > 0.75) + growth_rate = 0.0; + if (hunger > 1.0) + hunger = 1.0; // clamp + + // feelings + // + GLfloat temp = [player hullHeatLevel]; + discomfort += delta_t * hunger * 0.02 * (1.0 - hunger); + if (temp > 0.33) + discomfort += delta_t * (temp - 0.33) * (temp - 0.33) * 0.05; + if (discomfort > 1.0) + discomfort = 1.0; // clamp + + // feeding & reproducing + // + // am I really hungry? + if (hunger > 0.50) + { + // consult menu... + ShipEntity* selectedCargopod = nil; + float mostYummy = 0.0; + int i; + NSMutableArray* cargopods = [player cargo]; // the cargo pods + int n_pods = [cargopods count]; + float foodfactor[17] = { 1.00, 0.25, 0.75, 0.01, 0.95, 1.25, 1.05, 0.00, 0.00, 0.00, 0.00, 0.15, 0.00, 0.00, 0.00, 0.00, 0.00}; + for (i = 0 ; i < n_pods; i++) + { + ShipEntity* cargopod = [cargopods objectAtIndex:i]; + int cargo_type = [cargopod commodityType]; + float yumminess = (1.0 + randf()) * foodfactor[cargo_type]; + if (yumminess > mostYummy) + { + selectedCargopod = cargopod; + mostYummy = yumminess; + } + } + if (selectedCargopod) + { + // feed + trumbleAppetiteAccumulator += hunger; + hunger = 0.0; + discomfort -= mostYummy * 0.5; + if (discomfort < 0.0) + discomfort = 0.0; + if (trumbleAppetiteAccumulator > 10.0) + { + // eaten all of this cargo! + NSString* ms = [NSString stringWithFormat:DESC(@"trumbles-eat-@"), + [UNIVERSE displayNameForCommodity:[selectedCargopod commodityType]]]; + + [UNIVERSE addMessage: ms forCount: 4.5]; + [cargopods removeObject:selectedCargopod]; + trumbleAppetiteAccumulator -= 10.0; + + // consider breeding - must be full grown and happy + if ((size > 0.95)&&(discomfort < 0.25)) + { + readyToSpawn = YES; + } + } + } + } + + // animations + // + switch (animation) + { + case TRUMBLE_ANIM_SNARL : + [self updateSnarl: delta_t]; break; + case TRUMBLE_ANIM_SHUDDER : + [self updateShudder: delta_t]; break; + case TRUMBLE_ANIM_STONED : + [self updateStoned: delta_t]; break; + case TRUMBLE_ANIM_DIE : + [self updatePop: delta_t]; break; + case TRUMBLE_ANIM_BLINK : + [self updateBlink: delta_t]; break; + case TRUMBLE_ANIM_PROOT : + [self updateProot: delta_t]; break; + case TRUMBLE_ANIM_SLEEP : + [self updateSleep: delta_t]; break; + case TRUMBLE_ANIM_SPAWN : + [self updateSpawn: delta_t]; break; + case TRUMBLE_ANIM_IDLE : + default: + [self updateIdle: delta_t]; break; + } + + +} + +- (void) updateIdle:(double) delta_t +{ + animationTime += delta_t; + if (animationTime > animationDuration) + { + // blink or proot or idle and/or change direction + [self actionIdle]; + if (randf() < 0.25) + [self actionBlink]; + if (randf() < 0.10) + [self randomizeMotionX]; + if (randf() < 0.10) + [self randomizeMotionY]; + if (randf() < 0.05) + [self actionProot]; + if (randf() < 0.01) + [self actionSleep]; + if (randf() < 0.01) + [self actionSnarl]; + // + if (readyToSpawn) + { + [self actionSpawn]; + readyToSpawn = NO; + } + // + if (discomfort > 0.5 + randf()) + { + [self actionShudder]; + } + // + if (discomfort > 0.96) + { + [self actionPop]; + } + // + animation = nextAnimation; + animationTime = 0.0; + } +} + +- (void) updateBlink:(double) delta_t +{ + eyeFrame = TRUMBLE_EYES_SHUT; + animationTime += delta_t; + if (animationTime > animationDuration) + { + // blink or proot or idle + [self actionIdle]; + if (randf() < 0.05) + [self actionBlink]; + if (randf() < 0.1) + [self actionProot]; + animation = nextAnimation; + animationTime = 0.0; + eyeFrame = TRUMBLE_EYES_OPEN; + } +} + +- (void) updateSnarl:(double) delta_t +{ + int pc = 100 * animationTime / animationDuration; + if (pc < 25) + { + eyeFrame = TRUMBLE_EYES_SHUT; + mouthFrame = TRUMBLE_MOUTH_GROWL; + } + if ((pc >=25)&&(pc < 90)) + { + double vibr = (pc & 1)? -1.0 : 1.0; + if (digram[1] & 4) + position.x += size * vibr * 0.5; + else + position.y += size * vibr * 0.5; + eyeFrame = TRUMBLE_EYES_WIDE; + if (pc & 2) + mouthFrame = TRUMBLE_MOUTH_SNARL; + else + mouthFrame = TRUMBLE_MOUTH_GROWL; + } + if ((pc >=90)&&(pc < 100)) + { + eyeFrame = TRUMBLE_EYES_WIDE; + mouthFrame = TRUMBLE_MOUTH_GROWL; + } + animationTime += delta_t; + if (animationTime > animationDuration) + { + // blink or idle + [self actionIdle]; + if (randf() < 0.1) + [self actionBlink]; + animation = nextAnimation; + animationTime = 0.0; + eyeFrame = TRUMBLE_EYES_OPEN; + mouthFrame = TRUMBLE_MOUTH_NORMAL; + } +} + +- (void) updateProot:(double) delta_t +{ + if (!animationTime) + { + animationStage = 0; + } + int pc = 100 * animationTime / animationDuration; + if (pc < 10) + { + eyeFrame = TRUMBLE_EYES_SHUT; + mouthFrame = TRUMBLE_MOUTH_POUT; + } + if (pc >=10) + { + double vibr = (pc & 2)? -1.0 : 1.0; + position.x += size * vibr * 0.25; + eyeFrame = TRUMBLE_EYES_SHUT; + mouthFrame = TRUMBLE_MOUTH_GROWL; + if (!animationStage) + { + animationStage = 1; + PlayTrumbleIdle(); + } + } + animationTime += delta_t; + if (animationTime > animationDuration) + { + // blink or idle + [self actionIdle]; + if (randf() < 0.1) + [self actionBlink]; + animation = nextAnimation; + animationTime = 0.0; + eyeFrame = TRUMBLE_EYES_OPEN; + mouthFrame = TRUMBLE_MOUTH_NORMAL; + } +} + +- (void) updateShudder:(double) delta_t +{ + if (!animationTime) + { + eyeFrame = TRUMBLE_EYES_WIDE; + mouthFrame = TRUMBLE_MOUTH_GROWL; + PlayTrumbleSqueal(); + } + int pc = 100 * animationTime / animationDuration; + if (pc < 10) + { + eyeFrame = TRUMBLE_EYES_WIDE; + mouthFrame = TRUMBLE_MOUTH_GROWL; + } + if (pc >= 10) + { + double vibr = (pc & 2)? -0.25 : 0.25; + position.x += size * vibr; + eyeFrame = TRUMBLE_EYES_OPEN; + mouthFrame = TRUMBLE_MOUTH_GROWL; + } + animationTime += delta_t; + if (animationTime > animationDuration) + { + // feel better + discomfort *= 0.9; + // blink or idle + [self actionIdle]; + if (randf() < 0.1) + [self actionBlink]; + animation = nextAnimation; + animationTime = 0.0; + eyeFrame = TRUMBLE_EYES_OPEN; + mouthFrame = TRUMBLE_MOUTH_NORMAL; + } +} + +- (void) updateStoned:(double) delta_t +{ +} + +- (void) updatePop:(double) delta_t +{ + if (!animationTime) + { + eyeFrame = TRUMBLE_EYES_SHUT; + mouthFrame = TRUMBLE_MOUTH_GROWL; + movement.y = (ranrot_rand() & 7); + if (randf() < 0.5) + rotational_velocity = 63 + (ranrot_rand() & 127); + else + rotational_velocity = -63 - (ranrot_rand() & 127); + // squeal here! + PlayTrumbleSqueal(); + } + float pc = animationTime / animationDuration; + + // fading alpha + colorPoint1[1] *= (1.0 - delta_t); + colorPoint2[1] *= (1.0 - delta_t); + colorPoint1[2] *= (1.0 - delta_t); + colorPoint2[2] *= (1.0 - delta_t); + colorPoint1[3] = (1.0 - pc); + colorPoint2[3] = (1.0 - pc); + colorBase[3] = (1.0 - pc); + + // falling + movement.y -= delta_t * 98.0; + rotational_velocity *= (1.0 + delta_t); + + // shrinking + size -= delta_t * (1.0 - pc) * size; + + animationTime += delta_t; + if (animationTime > animationDuration) + { + // kaputnik! + [player removeTrumble:self]; + } +} + +- (void) updateSleep:(double) delta_t +{ + if (!animationTime) + { + saved_float1 = eye_position.y; + saved_float2 = mouth_position.y; + } + eyeFrame = TRUMBLE_EYES_SHUT; + int pc = 512 * animationTime / animationDuration; + if (pc & 16) + { + double vibr = (pc & 2)? -0.0025 : 0.0025; + eye_position.y += size * vibr; + mouth_position.y += size * vibr * -0.5; + } + else + { + eye_position.y = saved_float1; + mouth_position.y = saved_float2; + } + animationTime += delta_t; + if (animationTime > animationDuration) + { + // idle or proot + eye_position.y = saved_float1; + mouth_position.y = saved_float2; + [self actionIdle]; + if (randf() < 0.25) + [self actionProot]; + animation = nextAnimation; + animationTime = 0.0; + eyeFrame = TRUMBLE_EYES_OPEN; + } +} + +- (void) updateSpawn:(double) delta_t +{ + movement.x *= (1.0 - delta_t); + movement.y *= (1.0 - delta_t); + rotation *= (1.0 - delta_t); + rotational_velocity *= (1.0 - delta_t); + eyeFrame = TRUMBLE_EYES_WIDE; + mouthFrame = TRUMBLE_MOUTH_POUT; + int pc = 256 * animationTime / animationDuration; + double vibr = (pc & 2)? -0.002 * pc : 0.002 * pc; + position.x += size * vibr; + animationTime += delta_t; + if (animationTime > animationDuration) + { + // proot + eye_position.y = saved_float1; + mouth_position.y = saved_float2; + [self actionProot]; + animation = nextAnimation; + animationTime = 0.0; + eyeFrame = TRUMBLE_EYES_OPEN; + mouthFrame = TRUMBLE_MOUTH_NORMAL; + [self randomizeMotionX]; + [player addTrumble:self]; + } +} + +- (NSDictionary*) dictionary +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithCharacters:digram length:2], @"digram", + [NSNumber numberWithFloat:hunger], @"hunger", + [NSNumber numberWithFloat:discomfort], @"discomfort", + [NSNumber numberWithFloat:size], @"size", + [NSNumber numberWithFloat:growth_rate], @"growth_rate", + [NSNumber numberWithFloat:rotation], @"rotation", + [NSNumber numberWithFloat:rotational_velocity], @"rotational_velocity", + StringFromPoint(position), @"position", + StringFromPoint(movement), @"movement", + nil]; +} + +- (void) setFromDictionary:(NSDictionary*) dict +{ + NSString* digramString = (NSString*)[dict objectForKey:@"digram"]; + [self setupForPlayer: player digram: digramString]; + hunger = [[dict objectForKey: @"hunger"] floatValue]; + discomfort = [[dict objectForKey: @"discomfort"] floatValue]; + size = [[dict objectForKey: @"size"] floatValue]; + growth_rate = [[dict objectForKey: @"growth_rate"] floatValue]; + rotation = [[dict objectForKey: @"rotation"] floatValue]; + rotational_velocity = [[dict objectForKey: @"rotational_velocity"] floatValue]; + position = PointFromString([dict objectForKey: @"position"]); + movement = PointFromString([dict objectForKey: @"movement"]); +} + +@end + + +static OOSoundSource *sTrumbleSoundSource; +static OOSound *sTrumbleIdleSound; +static OOSound *sTrumbleSqealSound; + +static void InitTrumbleSounds(void) +{ + if (sTrumbleSoundSource == nil) + { + sTrumbleSoundSource = [[OOSoundSource alloc] init]; + sTrumbleIdleSound = [[OOSound alloc] initWithCustomSoundKey:@"[trumble-idle]"]; + sTrumbleSqealSound = [[OOSound alloc] initWithCustomSoundKey:@"[trumble-squeal]"]; + } +} + + +static void PlayTrumbleIdle(void) +{ + // Only play idle sound if no trumble is making noise. + if (![sTrumbleSoundSource isPlaying]) [sTrumbleSoundSource playSound:sTrumbleIdleSound]; +} + + +static void PlayTrumbleSqueal(void) +{ + // Play squeal sound if no trumble is currently squealing, but trumping idle sound. + if (![sTrumbleSoundSource isPlaying] || [sTrumbleSoundSource sound] == sTrumbleIdleSound) + { + [sTrumbleSoundSource playSound:sTrumbleSqealSound]; + } +} diff --git a/src/Core/OOTypes.h b/src/Core/OOTypes.h new file mode 100644 index 00000000..6169d6ea --- /dev/null +++ b/src/Core/OOTypes.h @@ -0,0 +1,361 @@ +/* + +OOTypes.h + +Various simple types that don't require us to pull in the associated class +headers. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#include "OOFunctionAttributes.h" +#include "OOCocoa.h" + +#define ARRAY_LENGTH(array) (sizeof (array) / sizeof (array)[0]) + + +typedef enum +{ + STATUS_EFFECT = 10, + STATUS_ACTIVE = 5, + STATUS_COCKPIT_DISPLAY = 2, + STATUS_TEST = 1, + STATUS_INACTIVE = 0, + STATUS_DEAD = -1, + STATUS_START_GAME = -10, + STATUS_IN_FLIGHT = 100, + STATUS_DOCKED = 200, + STATUS_AUTOPILOT_ENGAGED = 300, + STATUS_DOCKING = 401, + STATUS_LAUNCHING = 402, + STATUS_WITCHSPACE_COUNTDOWN = 410, + STATUS_ENTERING_WITCHSPACE = 411, + STATUS_EXITING_WITCHSPACE = 412, + STATUS_ESCAPE_SEQUENCE = 500, + STATUS_IN_HOLD = 600, + STATUS_BEING_SCOOPED = 700, + STATUS_HANDLING_ERROR = 999 +} OOEntityStatus; + + +typedef enum +{ + CLASS_NOT_SET = -1, + CLASS_NO_DRAW = 0, + CLASS_NEUTRAL = 1, + CLASS_STATION = 3, + CLASS_TARGET = 4, + CLASS_CARGO = 5, + CLASS_MISSILE = 6, + CLASS_ROCK = 7, + CLASS_MINE = 8, + CLASS_THARGOID = 9, + CLASS_BUOY = 10, + CLASS_WORMHOLE = 444, + CLASS_PLAYER = 100, + CLASS_POLICE = 999, + CLASS_MILITARY = 333 +} OOScanClass; + + +typedef enum +{ + GALACTIC_HYPERSPACE_BEHAVIOUR_STANDARD, + GALACTIC_HYPERSPACE_BEHAVIOUR_ALL_SYSTEMS_REACHABLE, + GALACTIC_HYPERSPACE_BEHAVIOUR_FIXED_COORDINATES, + + GALACTIC_HYPERSPACE_BEHAVIOUR_UNKNOWN = -1, + GALACTIC_HYPERSPACE_MAX = GALACTIC_HYPERSPACE_BEHAVIOUR_FIXED_COORDINATES +} OOGalacticHyperspaceBehaviour; + + +typedef enum +{ +// NOTE: numerical values are available to scripts and shaders. + ALERT_CONDITION_DOCKED, + ALERT_CONDITION_GREEN, + ALERT_CONDITION_YELLOW, + ALERT_CONDITION_RED +} OOAlertCondition; + + +typedef enum +{ + GUI_SCREEN_MAIN, + GUI_SCREEN_INTRO1, + GUI_SCREEN_INTRO2, + GUI_SCREEN_STATUS, + GUI_SCREEN_MANIFEST, + GUI_SCREEN_EQUIP_SHIP, + GUI_SCREEN_SHIPYARD, + GUI_SCREEN_LONG_RANGE_CHART, + GUI_SCREEN_SHORT_RANGE_CHART, + GUI_SCREEN_SYSTEM_DATA, + GUI_SCREEN_MARKET, + GUI_SCREEN_CONTRACTS, + GUI_SCREEN_OPTIONS, + GUI_SCREEN_GAMEOPTIONS, + GUI_SCREEN_LOAD, + GUI_SCREEN_SAVE, + GUI_SCREEN_SAVE_OVERWRITE, + GUI_SCREEN_STICKMAPPER, + GUI_SCREEN_MISSION, + GUI_SCREEN_REPORT +} OOGUIScreenID; + + +typedef enum +{ + SHADERS_NOT_SUPPORTED, + SHADERS_OFF, + SHADERS_SIMPLE, + SHADERS_FULL +} OOShaderSetting; + +#define SHADERS_MIN SHADERS_OFF +#define SHADERS_MAX SHADERS_FULL + + +typedef enum +{ + INSTINCT_NULL = 0U, + + // basic behavioural instincts + INSTINCT_AVOID_HAZARDS = 101, + INSTINCT_FLOCK_ALIKE = 102, + + // threats should be defined + INSTINCT_FIGHT_OR_FLIGHT = 103, + +// 'prey' should be defined + INSTINCT_ATTACK_PREY = 105, + INSTINCT_AVOID_PREDATORS = 106, + + // advanced AI instincts + INSTINCT_FOLLOW_AI = 201 +} OOInstinctID; + + +typedef enum +{ + BEHAVIOUR_IDLE = 0U, + BEHAVIOUR_TRACK_TARGET = 1, +// BEHAVIOUR_FLY_TO_TARGET = 2, // Unused +// BEHAVIOUR_HANDS_OFF = 3, // Unused + BEHAVIOUR_TUMBLE = 4, + BEHAVIOUR_STOP_STILL = 5, + + BEHAVIOUR_STATION_KEEPING = 10, + + BEHAVIOUR_ATTACK_TARGET = 101, + BEHAVIOUR_ATTACK_FLY_TO_TARGET = 102, + BEHAVIOUR_ATTACK_FLY_FROM_TARGET = 103, + BEHAVIOUR_RUNNING_DEFENSE = 104, + +// fleeing + BEHAVIOUR_FLEE_TARGET = 105, + +// advanced combat... + BEHAVIOUR_ATTACK_FLY_TO_TARGET_SIX = 106, + BEHAVIOUR_ATTACK_MINING_TARGET = 107, + +// further advanced combat... + BEHAVIOUR_ATTACK_FLY_TO_TARGET_TWELVE = 112, + + BEHAVIOUR_AVOID_COLLISION = 130, + + BEHAVIOUR_TRACK_AS_TURRET = 150, + + BEHAVIOUR_FLY_RANGE_FROM_DESTINATION = 200, + BEHAVIOUR_FLY_TO_DESTINATION = 201, + BEHAVIOUR_FLY_FROM_DESTINATION = 202, + BEHAVIOUR_FACE_DESTINATION = 203, + + BEHAVIOUR_FLY_THRU_NAVPOINTS = 210, + + BEHAVIOUR_COLLECT_TARGET = 300, + BEHAVIOUR_INTERCEPT_TARGET = 350, + +// BEHAVIOUR_MISSILE_FLY_TO_TARGET = 901, // Unused + + BEHAVIOUR_FORMATION_FORM_UP = 501, + BEHAVIOUR_FORMATION_BREAK = 502, + + BEHAVIOUR_ENERGY_BOMB_COUNTDOWN = 601, + + BEHAVIOUR_TRACTORED = 701 +} OOBehaviour; + + +OOINLINE BOOL IsBehaviourHostile(OOBehaviour behaviour) INLINE_CONST_FUNC; +OOINLINE BOOL IsBehaviourHostile(OOBehaviour behaviour) +{ + return 100 < behaviour && behaviour < 120; +} + + +typedef enum +{ + VIEW_FORWARD = 0, + VIEW_AFT = 1, + VIEW_PORT = 2, + VIEW_STARBOARD = 3, + VIEW_CUSTOM = 7, + VIEW_NONE = -1, + VIEW_GUI_DISPLAY = 10, + VIEW_BREAK_PATTERN = 20 +} OOViewID; + + +typedef enum +{ + DEMO_NO_DEMO = 0, + DEMO_FLY_IN = 101, + DEMO_SHOW_THING, + DEMO_FLY_OUT +} OODemoMode; + + +typedef enum +{ + WEAPON_NONE = 0U, + WEAPON_PLASMA_CANNON = 1, + WEAPON_PULSE_LASER = 2, + WEAPON_BEAM_LASER = 3, + WEAPON_MINING_LASER = 4, + WEAPON_MILITARY_LASER = 5, + WEAPON_THARGOID_LASER = 10 +} OOWeaponType; + + +typedef enum +{ + AEGIS_NONE, + AEGIS_CLOSE_TO_ANY_PLANET, + AEGIS_CLOSE_TO_MAIN_PLANET, + AEGIS_IN_DOCKING_RANGE +} OOAegisStatus; + + +typedef enum +{ + CARGO_UNDEFINED = -2, // FIXME: it's unclear whether there's a useful distinction between CARGO_UNDEFINED (previously NSNotFound) and CARGO_NOT_CARGO. + CARGO_NOT_CARGO = -1, + CARGO_SLAVES = 3, + CARGO_ALLOY = 9, + CARGO_MINERALS = 12, + CARGO_THARGOID = 16, + CARGO_RANDOM = 100, + CARGO_SCRIPTED_ITEM = 200, + CARGO_CHARACTER = 300 +} OOCargoType; + + +typedef enum +{ + CARGO_FLAG_NONE = 400, + CARGO_FLAG_FULL_PLENTIFUL = 501, + CARGO_FLAG_FULL_SCARCE = 502, + CARGO_FLAG_PIRATE = 505, + CARGO_FLAG_FULL_UNIFORM = 510, + CARGO_FLAG_CANISTERS = 600, + CARGO_FLAG_FULL_PASSENGERS = 700 +} OOCargoFlag; + + +typedef enum +{ + UNITS_TONS, + UNITS_KILOGRAMS, + UNITS_GRAMS +} OOMassUnit; + + +typedef enum +{ + ENERGY_UNIT_NONE, + ENERGY_UNIT_NORMAL_DAMAGED, + ENERGY_UNIT_NAVAL_DAMAGED, + OLD_ENERGY_UNIT_NORMAL =15, + OLD_ENERGY_UNIT_NAVAL = 20, + ENERGY_UNIT_NORMAL = 8, + ENERGY_UNIT_NAVAL = 16 +} OOEnergyUnitType; + + +#if DOCKING_CLEARANCE_ENABLED +typedef enum +{ + DOCKING_CLEARANCE_STATUS_NONE, + DOCKING_CLEARANCE_STATUS_REQUESTED, + DOCKING_CLEARANCE_STATUS_NOT_REQUIRED, + DOCKING_CLEARANCE_STATUS_GRANTED, + DOCKING_CLEARANCE_STATUS_TIMING_OUT, +} OODockingClearanceStatus; +#endif + + +typedef uint32_t OOCargoQuantity; +typedef int32_t OOCargoQuantityDelta; + +typedef uint16_t OOFuelQuantity; + + +typedef uint64_t OOCreditsQuantity; + + +typedef uint16_t OOKeyCode; + + +typedef uint16_t OOUniversalID; // Valid IDs range from 100 to 1000. + +enum +{ + UNIVERSE_MAX_ENTITIES = 2048, + NO_TARGET = 0, + MIN_ENTITY_UID = 100, + MAX_ENTITY_UID = MIN_ENTITY_UID + UNIVERSE_MAX_ENTITIES + 1 +}; + + +typedef OOUInteger OOTechLevelID; // 0..14, 99 is special. NSNotFound is used, so OOUInteger required. +typedef uint8_t OOGovernmentID; // 0..7 +typedef uint8_t OOEconomyID; // 0..7 + +enum +{ + kOOVariableTechLevel = 99 +}; + + +typedef uint8_t OOGalaxyID; // 0..7 +typedef int16_t OOSystemID; // 0..255, -1 for interstellar space (?) + + +enum +{ + kOOMaximumGalaxyID = 7, + kOOMaximumSystemID = 255, + kOOMinimumSystemID = -1 +}; + + +typedef double OOTimeAbsolute; +typedef double OOTimeDelta; diff --git a/src/Core/OOVector.h b/src/Core/OOVector.h new file mode 100644 index 00000000..537c2cc9 --- /dev/null +++ b/src/Core/OOVector.h @@ -0,0 +1,329 @@ +/* + +OOVector.h + +Mathematical framework for Oolite. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOVector.h directly; include OOMaths.h. +#else + + +typedef struct Vector +{ + GLfloat x; + GLfloat y; + GLfloat z; +} Vector; + + +extern const Vector kZeroVector, /* 0, 0, 0 */ + kBasisXVector, /* 1, 0, 0 */ + kBasisYVector, /* 0, 1, 0 */ + kBasisZVector; /* 0, 0, 1 */ + + +/* Construct vector */ +OOINLINE Vector make_vector(GLfloat vx, GLfloat vy, GLfloat vz) INLINE_CONST_FUNC; + +/* Generate random vectors. */ +Vector OORandomUnitVector(void); +Vector OOVectorRandomSpatial(GLfloat maxLength); // Random vector uniformly distributed in radius-maxLength sphere. (Longer vectors are more common.) +Vector OOVectorRandomRadial(GLfloat maxLength); // Random vector with uniform distribution of direction and radius in radius-maxLength sphere. (Causes clustering at centre.) + +/* Multiply vector by scalar (in place) */ +OOINLINE void scale_vector(Vector *outVector, GLfloat factor) ALWAYS_INLINE_FUNC NONNULL_FUNC; + +/* Multiply vector by scalar */ +OOINLINE Vector vector_multiply_scalar(Vector v, GLfloat s) INLINE_CONST_FUNC; + +/* Addition and subtraction of vectors */ +OOINLINE Vector vector_add(Vector a, Vector b) INLINE_CONST_FUNC; +OOINLINE Vector vector_subtract(Vector a, Vector b) INLINE_CONST_FUNC; +#define vector_between(a, b) vector_subtract(b, a) +OOINLINE Vector vector_flip(Vector v) INLINE_CONST_FUNC; + +/* Vector linear interpolation */ +OOINLINE Vector OOVectorInterpolate(Vector a, Vector b, GLfloat where) INLINE_CONST_FUNC; +OOINLINE Vector OOVectorTowards(Vector a, Vector b, GLfloat where) INLINE_CONST_FUNC; + +/* Comparison of vectors */ +OOINLINE GLboolean vector_equal(Vector a, Vector b) INLINE_CONST_FUNC; + +/* Square of magnitude of vector */ +OOINLINE GLfloat magnitude2(Vector vec) INLINE_CONST_FUNC; + +/* Magnitude of vector */ +OOINLINE GLfloat magnitude(Vector vec) INLINE_CONST_FUNC; +OOINLINE GLfloat fast_magnitude(Vector vec) INLINE_CONST_FUNC; + +/* Normalize vector */ +OOINLINE Vector vector_normal(Vector vec) INLINE_CONST_FUNC; +OOINLINE Vector fast_vector_normal(Vector vec) INLINE_CONST_FUNC; + +/* Normalize vector, returning fallback if zero vector. */ +OOINLINE Vector vector_normal_or_fallback(Vector vec, Vector fallback) INLINE_CONST_FUNC; +OOINLINE Vector vector_normal_or_zbasis(Vector vec) INLINE_CONST_FUNC; +OOINLINE Vector fast_vector_normal_or_fallback(Vector vec, Vector fallback) INLINE_CONST_FUNC; + +/* Square of distance between vectors */ +OOINLINE GLfloat distance2(Vector v1, Vector v2) INLINE_CONST_FUNC; + +/* Distance between vectors */ +OOINLINE GLfloat distance(Vector v1, Vector v2) INLINE_CONST_FUNC; +OOINLINE GLfloat fast_distance(Vector v1, Vector v2) INLINE_CONST_FUNC; + +/* Dot product */ +OOINLINE GLfloat dot_product (Vector first, Vector second) INLINE_CONST_FUNC; + +/* NORMALIZED cross product */ +OOINLINE Vector cross_product(Vector first, Vector second) INLINE_CONST_FUNC; +OOINLINE Vector fast_cross_product(Vector first, Vector second) INLINE_CONST_FUNC; + +/* General cross product */ +OOINLINE Vector true_cross_product(Vector first, Vector second) CONST_FUNC; + +/* Triple product */ +OOINLINE GLfloat triple_product(Vector first, Vector second, Vector third) INLINE_CONST_FUNC; + +/* Given three points on a surface, returns the normal to the surface. */ +OOINLINE Vector normal_to_surface(Vector v1, Vector v2, Vector v3) CONST_FUNC; +OOINLINE Vector fast_normal_to_surface(Vector v1, Vector v2, Vector v3) CONST_FUNC; + +#ifdef __OBJC__ +NSString *VectorDescription(Vector vector); // @"(x, y, z)" +#endif + +/* OpenGL conveniences. Need to be macros to work with OOMacroOpenGL. */ +#define GLVertexOOVector(v) do { Vector v_ = v; glVertex3f(v_.x, v_.y, v_.z); } while (0) +#define GLTranslateOOVector(v) do { Vector v_ = v; glTranslatef(v_.x, v_.y, v_.z); } while (0) + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE Vector make_vector (GLfloat vx, GLfloat vy, GLfloat vz) +{ + Vector result; + result.x = vx; + result.y = vy; + result.z = vz; + return result; +} + + +OOINLINE void scale_vector(Vector *vec, GLfloat factor) +{ + vec->x *= factor; + vec->y *= factor; + vec->z *= factor; +} + + +OOINLINE Vector vector_multiply_scalar(Vector v, GLfloat s) +{ + Vector r; + r.x = v.x * s; + r.y = v.y * s; + r.z = v.z * s; + return r; +} + + +OOINLINE Vector vector_add(Vector a, Vector b) +{ + Vector r; + r.x = a.x + b.x; + r.y = a.y + b.y; + r.z = a.z + b.z; + return r; +} + + +OOINLINE Vector OOVectorInterpolate(Vector a, Vector b, GLfloat where) +{ + GLfloat invWhere = 1.0f - where; + return make_vector(a.x * invWhere + b.x * where, + a.y * invWhere + b.y * where, + a.z * invWhere + b.z * where); +} + + +OOINLINE Vector OOVectorTowards(Vector a, Vector b, GLfloat where) +{ + return make_vector(a.x + b.x * where, + a.y + b.y * where, + a.z + b.z * where); +} + + +OOINLINE Vector vector_subtract(Vector a, Vector b) +{ + Vector r; + r.x = a.x - b.x; + r.y = a.y - b.y; + r.z = a.z - b.z; + return r; +} + + +OOINLINE Vector vector_flip(Vector v) +{ + return vector_subtract(kZeroVector, v); +} + + +OOINLINE GLboolean vector_equal(Vector a, Vector b) +{ + return a.x == b.x && a.y == b.y && a.z == b.z; +} + + +OOINLINE GLfloat magnitude2(Vector vec) +{ + return vec.x * vec.x + vec.y * vec.y + vec.z * vec.z; +} + + +OOINLINE GLfloat magnitude(Vector vec) +{ + return sqrtf(magnitude2(vec)); +} + + +OOINLINE GLfloat fast_magnitude(Vector vec) +{ + #if FASTINVSQRT_ENABLED || OO_PPC + GLfloat mag2 = magnitude2(vec); + return mag2 * OOFastInvSqrtf(mag2); /* x = sqrt(x) * sqrt(x); x * 1/sqrt(x) = (sqrt(x) * sqrt(x))/sqrt(x) = sqrt(x). */ + #else + return magnitude(vec); + #endif +} + + +OOINLINE Vector vector_normal_or_fallback(Vector vec, Vector fallback) +{ + GLfloat mag2 = magnitude2(vec); + if (EXPECT_NOT(mag2 == 0)) return fallback; + return vector_multiply_scalar(vec, OOInvSqrtf(mag2)); +} + + +OOINLINE Vector vector_normal_or_zbasis(Vector vec) +{ + return vector_normal_or_fallback(vec, kBasisZVector); +} + + +OOINLINE Vector vector_normal(Vector vec) +{ + return vector_normal_or_fallback(vec, kZeroVector); +} + + +OOINLINE Vector fast_vector_normal_or_fallback(Vector vec, Vector fallback) +{ + GLfloat mag2 = magnitude2(vec); + if (EXPECT_NOT(mag2 == 0)) return fallback; + return vector_multiply_scalar(vec, OOFastInvSqrtf(mag2)); +} + + +OOINLINE Vector fast_vector_normal(Vector vec) +{ + return fast_vector_normal_or_fallback(vec, kZeroVector); +} + + +OOINLINE GLfloat distance2(Vector v1, Vector v2) +{ + return magnitude2(vector_subtract(v1, v2)); +} + + +OOINLINE GLfloat distance(Vector v1, Vector v2) +{ + return magnitude(vector_subtract(v1, v2)); +} + + +OOINLINE GLfloat fast_distance(Vector v1, Vector v2) +{ + return fast_magnitude(vector_subtract(v1, v2)); +} + + +OOINLINE Vector true_cross_product(Vector first, Vector second) +{ + Vector result; + result.x = (first.y * second.z) - (first.z * second.y); + result.y = (first.z * second.x) - (first.x * second.z); + result.z = (first.x * second.y) - (first.y * second.x); + return result; +} + + +OOINLINE Vector cross_product(Vector first, Vector second) +{ + return vector_normal(true_cross_product(first, second)); +} + + +OOINLINE Vector fast_cross_product(Vector first, Vector second) +{ + return fast_vector_normal(true_cross_product(first, second)); +} + + +OOINLINE GLfloat dot_product (Vector a, Vector b) +{ + return (a.x * b.x) + (a.y * b.y) + (a.z * b.z); +} + + +OOINLINE GLfloat triple_product(Vector first, Vector second, Vector third) +{ + return dot_product(first, true_cross_product(second, third)); +} + + +OOINLINE Vector normal_to_surface(Vector v1, Vector v2, Vector v3) +{ + Vector d0, d1; + d0 = vector_subtract(v2, v1); + d1 = vector_subtract(v3, v2); + return cross_product(d0, d1); +} + + +OOINLINE Vector fast_normal_to_surface(Vector v1, Vector v2, Vector v3) +{ + Vector d0, d1; + d0 = vector_subtract(v2, v1); + d1 = vector_subtract(v3, v2); + return fast_cross_product(d0, d1); +} + + +#endif /* INCLUDED_OOMATHS_h */ diff --git a/src/Core/OOVector.m b/src/Core/OOVector.m new file mode 100644 index 00000000..4d429323 --- /dev/null +++ b/src/Core/OOVector.m @@ -0,0 +1,87 @@ +/* + +OOVector.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOMaths.h" +#import "OOLogging.h" + + +const Vector kZeroVector = { 0.0f, 0.0f, 0.0f }; +const Vector kBasisXVector = { 1.0f, 0.0f, 0.0f }; +const Vector kBasisYVector = { 0.0f, 1.0f, 0.0f }; +const Vector kBasisZVector = { 0.0f, 0.0f, 1.0f }; +const BoundingBox kZeroBoundingBox = {{ 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f }}; + + +NSString *VectorDescription(Vector vector) +{ + return [NSString stringWithFormat:@"(%g, %g, %g)", vector.x, vector.y, vector.z]; +} + + +/* This generates random vectors distrubuted evenly over the surface of the + unit sphere. It does this the simple way, by generating vectors in the + half-unit cube and rejecting those outside the half-unit sphere (and the + zero vector), then normalizing the result. (Half-unit measures are used + to avoid unnecessary multiplications of randf() values.) + + In principle, using three normally-distributed co-ordinates (and again + normalizing the result) would provide the right result without looping, but + I don't trust bellf() so I'll go with the simple approach for now. +*/ +Vector OORandomUnitVector(void) +{ + Vector v; + float m; + + do + { + v = make_vector(randf() - 0.5f, randf() - 0.5f, randf() - 0.5f); + m = magnitude2(v); + } + while (m > 0.25f || m == 0.0f); // We're confining to a sphere of radius 0.5 using the sqared magnitude; 0.5 squared is 0.25. + + return vector_normal(v); +} + + +Vector OOVectorRandomSpatial(GLfloat maxLength) +{ + Vector v; + float m; + + do + { + v = make_vector(randf() - 0.5f, randf() - 0.5f, randf() - 0.5f); + m = magnitude2(v); + } + while (m > 0.25f); // We're confining to a sphere of radius 0.5 using the sqared magnitude; 0.5 squared is 0.25. + + return vector_multiply_scalar(v, maxLength * 2.0f); // 2.0 is to compensate for the 0.5-radius sphere. +} + + +Vector OOVectorRandomRadial(GLfloat maxLength) +{ + return vector_multiply_scalar(OORandomUnitVector(), randf() * maxLength); +} diff --git a/src/Core/OOVoxel.h b/src/Core/OOVoxel.h new file mode 100644 index 00000000..d701787b --- /dev/null +++ b/src/Core/OOVoxel.h @@ -0,0 +1,47 @@ +/* + +OOVoxel.h + +Mathematical framework for Oolite. + +Primitive functions used for octree intersection tests. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifndef INCLUDED_OOMATHS_h + #error Do not include OOVoxel.h directly; include OOMaths.h. +#else + + +#define CUBE_FACE_RIGHT 0x01 +#define CUBE_FACE_LEFT 0x02 +#define CUBE_FACE_TOP 0x04 +#define CUBE_FACE_BOTTOM 0x08 +#define CUBE_FACE_FRONT 0x10 +#define CUBE_FACE_BACK 0x20 + + +Vector lineIntersectionWithFace(Vector p1, Vector p2, long mask, GLfloat rd) CONST_FUNC; +int lineCubeIntersection(Vector v0, Vector v1, GLfloat rd) CONST_FUNC; + + +#endif diff --git a/src/Core/OOVoxel.m b/src/Core/OOVoxel.m new file mode 100644 index 00000000..57c563bb --- /dev/null +++ b/src/Core/OOVoxel.m @@ -0,0 +1,173 @@ +/* + +OOVoxel.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOMaths.h" + + +// routines concerning octree voxels + +int checkFace(Vector p, GLfloat rd) +{ + int faces = 0; + if (p.x > rd) faces |= CUBE_FACE_RIGHT; // right + if (p.x < -rd) faces |= CUBE_FACE_LEFT; // left + if (p.y > rd) faces |= CUBE_FACE_TOP; // above + if (p.y < -rd) faces |= CUBE_FACE_BOTTOM; // below + if (p.z > rd) faces |= CUBE_FACE_FRONT; // ahead + if (p.z < -rd) faces |= CUBE_FACE_BACK; // behind + return faces ; +} + +int checkBevel(Vector p, GLfloat rd) +{ + GLfloat r2 = rd * 2; + int bevels = 0; + if ( p.x + p.y > r2) bevels |= 0x001; + if ( p.x - p.y > r2) bevels |= 0x002; + if (-p.x + p.y > r2) bevels |= 0x004; + if (-p.x - p.y > r2) bevels |= 0x008; + if ( p.x + p.z > r2) bevels |= 0x010; + if ( p.x - p.z > r2) bevels |= 0x020; + if (-p.x + p.z > r2) bevels |= 0x040; + if (-p.x - p.z > r2) bevels |= 0x080; + if ( p.y + p.z > r2) bevels |= 0x100; + if ( p.y - p.z > r2) bevels |= 0x200; + if (-p.y + p.z > r2) bevels |= 0x400; + if (-p.y - p.z > r2) bevels |= 0x800; + return bevels; +} + +int checkCorner(Vector p, GLfloat rd) +{ + GLfloat r3 = rd * 3; + int corners = 0; + if (( p.x + p.y + p.z) > r3) corners |= 0x01; + if (( p.x + p.y - p.z) > r3) corners |= 0x02; + if (( p.x - p.y + p.z) > r3) corners |= 0x04; + if (( p.x - p.y - p.z) > r3) corners |= 0x08; + if ((-p.x + p.y + p.z) > r3) corners |= 0x10; + if ((-p.x + p.y - p.z) > r3) corners |= 0x20; + if ((-p.x - p.y + p.z) > r3) corners |= 0x40; + if ((-p.x - p.y - p.z) > r3) corners |= 0x80; + return corners; +} + +Vector lineIntersectionWithFace(Vector p1, Vector p2, long mask, GLfloat rd) +{ + if (CUBE_FACE_RIGHT & mask) + return make_vector( rd, + p1.y + (p2.y - p1.y) * (rd - p1.x) / (p2.x - p1.x), + p1.z + (p2.z - p1.z) * (rd - p1.x) / (p2.x - p1.x)); + + if (CUBE_FACE_LEFT & mask) + return make_vector( -rd, + p1.y + (p2.y - p1.y) * (-rd - p1.x) / (p2.x - p1.x), + p1.z + (p2.z - p1.z) * (-rd - p1.x) / (p2.x - p1.x)); + + if (CUBE_FACE_TOP & mask) + return make_vector( p1.x + (p2.x - p1.x) * (rd - p1.y) / (p2.y - p1.y), + rd, + p1.z + (p2.z - p1.z) * (rd - p1.y) / (p2.y - p1.y)); + + if (CUBE_FACE_BOTTOM & mask) + return make_vector( p1.x + (p2.x - p1.x) * (-rd - p1.y) / (p2.y - p1.y), + -rd, + p1.z + (p2.z - p1.z) * (-rd - p1.y) / (p2.y - p1.y)); + + if (CUBE_FACE_FRONT & mask) + return make_vector( p1.x + (p2.x - p1.x) * (rd - p1.z) / (p2.z - p1.z), + p1.y + (p2.y - p1.y) * (rd - p1.z) / (p2.z - p1.z), + rd); + + if (CUBE_FACE_BACK & mask) + return make_vector( p1.x + (p2.x - p1.x) * (-rd - p1.z) / (p2.z - p1.z), + p1.y + (p2.y - p1.y) * (-rd - p1.z) / (p2.z - p1.z), + -rd); + return p1; +} + +int checkPoint(Vector p1, Vector p2, GLfloat alpha, long mask, GLfloat rd) +{ + Vector pp; + pp.x = p1.x + alpha * (p2.x - p1.x); + pp.y = p1.y + alpha * (p2.y - p1.y); + pp.z = p1.z + alpha * (p2.z - p1.z); + return (checkFace( pp, rd) & mask); +} + +int checkLine(Vector p1, Vector p2, int mask, GLfloat rd) +{ + int result = 0; + if ((CUBE_FACE_RIGHT & mask) && (p1.x > p2.x) && (checkPoint( p1, p2, (rd-p1.x)/(p2.x-p1.x), 0x3f - CUBE_FACE_RIGHT, rd) == 0)) // right + result |= CUBE_FACE_RIGHT; + if ((CUBE_FACE_LEFT & mask) && (p1.x < p2.x) && (checkPoint( p1, p2, (-rd-p1.x)/(p2.x-p1.x), 0x3f - CUBE_FACE_LEFT, rd) == 0)) // left + result |= CUBE_FACE_LEFT; + if ((CUBE_FACE_TOP & mask) && (p1.y > p2.y) && (checkPoint( p1, p2, (rd-p1.y)/(p2.y-p1.y), 0x3f - CUBE_FACE_TOP, rd) == 0)) // above + result |= CUBE_FACE_TOP; + if ((CUBE_FACE_BOTTOM & mask) && (p1.y < p2.y) && (checkPoint( p1, p2, (-rd-p1.y)/(p2.y-p1.y), 0x3f - CUBE_FACE_BOTTOM, rd) == 0)) // below + result |= CUBE_FACE_BOTTOM; + if ((CUBE_FACE_FRONT & mask) && (p1.z > p2.z) && (checkPoint( p1, p2, (rd-p1.z)/(p2.z-p1.z), 0x3f - CUBE_FACE_FRONT, rd) == 0)) // ahead + result |= CUBE_FACE_FRONT; + if ((CUBE_FACE_BACK & mask) && (p1.z < p2.z) && (checkPoint( p1, p2, (-rd-p1.z)/(p2.z-p1.z), 0x3f - CUBE_FACE_BACK, rd) == 0)) // behind + result |= CUBE_FACE_BACK; + return result; +} + +// line v0 to v1 is compared with a cube centered on the origin (corners at -rd,-rd,-rd to rd,rd,rd). +// returns -1 if the line intersects the cube. +int lineCubeIntersection(Vector v0, Vector v1, GLfloat rd) +{ + int v0_test, v1_test; + + // compare both vertexes with all six face-planes + // + if ((v0_test = checkFace( v0, rd)) == 0) + return -1; // v0 is inside the cube + if ((v1_test = checkFace( v1, rd)) == 0) + return -1; // v1 is inside the cube + + // check they're not both outside one face-plane + // + if ((v0_test & v1_test) != 0) + return 0; // both v0 and v1 are outside the same face of the cube + + // Now do the same test for the 12 edge planes + // + v0_test |= checkBevel( v0, rd) << 8; + v1_test |= checkBevel( v1, rd) << 8; + if ((v0_test & v1_test) != 0) + return 0; // v0 and v1 outside of the same bevel + + // Now do the same test for the 8 corner planes + // + v0_test |= checkCorner( v0, rd) << 24; + v1_test |= checkCorner( v1, rd) << 24; + if ((v0_test & v1_test) != 0) + return 0; // v0 and v1 outside of same corner + + // see if the v0-->v1 line intersects the cube. + // + return checkLine( v0, v1, v0_test | v1_test, rd); +} diff --git a/src/Core/OOWeakReference.h b/src/Core/OOWeakReference.h new file mode 100644 index 00000000..717e1e6a --- /dev/null +++ b/src/Core/OOWeakReference.h @@ -0,0 +1,140 @@ +/* + +OOWeakReference.h + +Weak reference class for Cocoa/GNUstep/OpenStep. As it stands, this will not +work as a weak reference in a garbage-collected environment. + +A weak reference allows code to maintain a reference to an object while +allowing the object to reach a retain count of zero and deallocate itself. +To function, the referenced object must implement the OOWeakReferenceSupport +protocol. + +Client use is extremely simple: to get a weak reference to the object, call +-weakRetain and use the returned proxy instead of the actual object. When +finished, release the proxy. Messages sent to the proxy will be forwarded as +long as the underlying object exists; beyond that, they will act exactly like +messages to nil. (IMPORTANT: this means messages returning floating-point or +struct values have undefined return values, so use -weakRefObjectStillExists +or -weakRefUnderlyingObject in such cases.) Example: + +@interface ThingWatcher: NSObject +{ + Thing *thing; +} +@end + +@implementation ThingWatcher +- (void)setThing:(Thing *)aThing +{ + [thing release]; + thing = [aThing weakRetain]; +} + +- (void)frobThing +{ + [thing frob]; +} + +- (void)dealloc +{ + [thing release]; + [super dealloc]; +} +@end + + +Note that the only reference to OOWeakReference being involved is the call to +weakRetain instead of retain. However, the following would not work: + thing = aThing; + [thing weakRetain]; + +Additionally, it is not possible to access instance variables directly -- but +then, that's a filthy habit. + +OOWeakReferenceSupport implementation is also simple: + +@interface Thing: NSObject +{ + OOWeakReference *weakSelf; +} +@end + +@implementation Thing +- (id)weakRetain +{ + if (weakSelf == nil) weakSelf = [OOWeakReference weakRefWithObject:self]; + return [weakSelf retain]; +} + +- (void)weakRefDied:(OOWeakReference *)weakRef +{ + if (weakRef == weakSelf) weakSelf = nil; +} + +- (void)dealloc +{ + [weakSelf weakRefDrop]; // Very important! + [super dealloc]; +} + +- (void)frob +{ + NSBeep(); +} +@end + + +Written by Jens Ayton in 2007 for Oolite. +This code is hereby placed in the public domain. + +*/ + +#import + +@class OOWeakReference; + + +@protocol OOWeakReferenceSupport + +- (id)weakRetain; // Returns a retained OOWeakReference, which should be released when finished with. +- (void)weakRefDied:(OOWeakReference *)weakRef; + +@end + + +@interface OOWeakReference: NSProxy +{ + id _object; +} + +- (BOOL)weakRefObjectStillExists; +- (id)weakRefUnderlyingObject; + +- (id)weakRetain; // Returns self for weakrefs. + +// For referred object only: ++ (id)weakRefWithObject:(id)object; +- (void)weakRefDrop; + +@end + + +@interface NSObject (OOWeakReference) + +- (BOOL)weakRefObjectStillExists; // Always YES for non-weakrefs. ObjC semantics causes it to return NO for nil, so it acts as an existence check. +- (id)weakRefUnderlyingObject; // Always self for non-weakrefs (and of course nil for nil). + +@end + + +/* OOWeakRefObject + Simple object implementing OOWeakReferenceSupport, to subclass. This + provides a full implementation for simplicity, but keep in mind that the + protocol can be implemented by any class. +*/ +@interface OOWeakRefObject: NSObject +{ + OOWeakReference *weakSelf; +} +@end diff --git a/src/Core/OOWeakReference.m b/src/Core/OOWeakReference.m new file mode 100644 index 00000000..dfbc4a82 --- /dev/null +++ b/src/Core/OOWeakReference.m @@ -0,0 +1,191 @@ +/* + +OOWeakReference.m + +Written by Jens Ayton in 2007 for Oolite. +This code is hereby placed in the public domain. + +*/ + +#import "OOWeakReference.h" + + +@interface OOWeakReferenceTemplates: NSObject + ++ (void)weakRefDrop; ++ (BOOL)weakRefObjectStillExists; ++ (id)weakRefUnderlyingObject; ++ (id)nilMethod; + +@end + + +@implementation OOWeakReference + +// *** Core functionality. + ++ (id)weakRefWithObject:(id)object +{ + if (object == nil) return nil; + + OOWeakReference *result = [OOWeakReference alloc]; + // No init for proxies. + result->_object = object; + return [result autorelease]; +} + + +- (void)dealloc +{ + [_object weakRefDied:self]; + + [super dealloc]; +} + + +- (NSString *)description +{ + if (_object != nil) return [_object description]; + else return [NSString stringWithFormat:@"", [self class], self]; +} + + +- (BOOL)weakRefObjectStillExists +{ + return _object != nil; +} + + +- (id)weakRefUnderlyingObject +{ + return _object; +} + + +- (id)weakRetain +{ + return [self retain]; +} + + +- (void)weakRefDrop +{ + _object = nil; +} + + +- (void) release +{ + [super release]; +} + + +// *** Proxy evilness beyond this point. + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + // Does the right thing even with nil _object. + [invocation invokeWithTarget:_object]; +} + + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + NSMethodSignature *result = nil; + + if (__builtin_expect( + selector != @selector(weakRefDrop) && + selector != @selector(weakRefObjectStillExists) && + selector != @selector(weakRefUnderlyingObject), 1)) + { + // Not a proxy method; get signature from _object if it exists, otherwise generic signature for nil calls. + if (__builtin_expect(_object != nil, 1)) result = [(id)_object methodSignatureForSelector:selector]; + else result = [OOWeakReferenceTemplates methodSignatureForSelector:@selector(nilMethod)]; + } + else + { + // One of OOWeakReference's own methods. + result = [OOWeakReferenceTemplates methodSignatureForSelector:selector]; + } + + return result; +} + + +- (BOOL)respondsToSelector:(SEL)selector +{ + if (__builtin_expect(_object != nil && + selector != @selector(weakRefDrop) + && selector != @selector(weakRefObjectStillExists) && + selector != @selector(weakRefUnderlyingObject), 1)) + { + // _object exists and it's not one of our methods, ask _object. + return [_object respondsToSelector:selector]; + } + else + { + // Selector we responds to, or _object is nil and therefore responds to everything. + return YES; + } +} + + +// New fast forwarding mechanism introduced in Mac OS X 10.5. +// Note that -forwardInvocation: is still called if _object is nil. +- (id)forwardingTargetForSelector:(SEL)sel +{ + return _object; +} + +@end + + +@implementation NSObject (OOWeakReference) + +- (BOOL)weakRefObjectStillExists +{ + return YES; +} + + +- (id)weakRefUnderlyingObject +{ + return self; +} + +@end + + +@implementation OOWeakRefObject + +- (id)weakRetain +{ + if (weakSelf == nil) weakSelf = [OOWeakReference weakRefWithObject:self]; + return [weakSelf retain]; // Each caller releases this, as -weakRetain must be balanced with -release. +} + + +- (void)weakRefDied:(OOWeakReference *)weakRef +{ + if (weakRef == weakSelf) weakSelf = nil; +} + + +- (void)dealloc +{ + [weakSelf weakRefDrop]; // Very important! + [super dealloc]; +} + +@end + + +@implementation OOWeakReferenceTemplates + +// These are never called, but an implementation must exist so that -methodSignatureForSelector: works. ++ (void)weakRefDrop {} ++ (BOOL)weakRefObjectStillExists { return NO; } ++ (id)weakRefUnderlyingObject { return nil; } ++ (id)nilMethod { return nil; } + +@end diff --git a/src/Core/OOXMLExtensions.h b/src/Core/OOXMLExtensions.h new file mode 100644 index 00000000..dff0fbd0 --- /dev/null +++ b/src/Core/OOXMLExtensions.h @@ -0,0 +1,41 @@ +/* + +OOXMLExtensions.h + +Extensions to Foundation property list classes to export property lists in +XML format, which both Cocoa and GNUstep can read. This is done because +GNUstep defaults to writing a version of OpenStep text-based property lists +that Cocoa can't understand. The XML format is understood by both +implementations, although GNUstep complains about not being able to find the +DTD. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import + +/* interfaces */ + +@interface NSDictionary (OOXMLExtensions) + +- (BOOL) writeOOXMLToFile:(NSString *)path atomically:(BOOL)flag errorDescription:(NSString **)outErrorDesc; + +@end diff --git a/src/Core/OOXMLExtensions.m b/src/Core/OOXMLExtensions.m new file mode 100644 index 00000000..ef49fc1a --- /dev/null +++ b/src/Core/OOXMLExtensions.m @@ -0,0 +1,60 @@ +/* + +OOXMLExtensions.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOXMLExtensions.h" + + +@implementation NSDictionary (OOXMLExtensions) + +- (BOOL) writeOOXMLToFile:(NSString *)path atomically:(BOOL)flag errorDescription:(NSString **)outErrorDesc +{ + NSData *data = nil; + NSString *errorDesc = nil; + + data = [NSPropertyListSerialization dataFromPropertyList:self format:NSPropertyListXMLFormat_v1_0 errorDescription:outErrorDesc]; + if (data == nil) + { + if (outErrorDesc != NULL) + { + *outErrorDesc = [NSString stringWithFormat:@"could not convert property list to XML: %@", errorDesc]; + } +#if OOLITE_RELEASE_PLIST_ERROR_STRINGS + [errorDesc release]; +#endif + return NO; + } + + if (![data writeToFile:path atomically:YES]) + { + if (outErrorDesc != NULL) + { + *outErrorDesc = [NSString stringWithFormat:@"could not write data to %@.", path]; + } + return NO; + } + + return YES; +} + +@end diff --git a/src/Core/OXPVerifier/OOAIStateMachineVerifierStage.h b/src/Core/OXPVerifier/OOAIStateMachineVerifierStage.h new file mode 100644 index 00000000..c94d3bd4 --- /dev/null +++ b/src/Core/OXPVerifier/OOAIStateMachineVerifierStage.h @@ -0,0 +1,46 @@ +/* + +OOAIStateMachineVerifierStage.h + +OOOXPVerifierStage which validates AI plists. + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOFileScannerVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOAIStateMachineVerifierStage: OOFileHandlingVerifierStage +{ +@private + NSSet *_whitelist; + NSMutableSet *_usedAIs; +} + +// Returns name to be used in -dependents by other stages. ++ (NSString *) nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier; + +- (void) stateMachineNamed:(NSString *)name usedByShip:(NSString *)shipName; + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOAIStateMachineVerifierStage.m b/src/Core/OXPVerifier/OOAIStateMachineVerifierStage.m new file mode 100644 index 00000000..8a9bd706 --- /dev/null +++ b/src/Core/OXPVerifier/OOAIStateMachineVerifierStage.m @@ -0,0 +1,216 @@ +/* + +OOAIStateMachineVerifierStage.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOAIStateMachineVerifierStage.h" +#import "OOCollectionExtractors.h" +#import "OOPListParsing.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "ResourceManager.h" + +static NSString * const kStageName = @"Validating AIs"; + + +@interface OOAIStateMachineVerifierStage (Private) + +- (void) validateAI:(NSString *)aiName; + +@end + + +@implementation OOAIStateMachineVerifierStage + +- (void) dealloc +{ + [_whitelist release]; + [_usedAIs release]; + + [super dealloc]; +} + + +- (NSString *) name +{ + return kStageName; +} + + +- (BOOL) shouldRun +{ + return [_usedAIs count] != 0; +} + + +- (void) run +{ + NSArray *aiNames = nil; + NSEnumerator *aiEnum = nil; + NSString *aiName = nil; + NSMutableSet *whitelist = nil; + + // Build whitelist. Note that we merge in aliases since the distinction doesn't matter when just validating. + whitelist = [[NSMutableSet alloc] init]; + [whitelist addObjectsFromArray:[[ResourceManager whitelistDictionary] arrayForKey:@"ai_methods"]]; + [whitelist addObjectsFromArray:[[ResourceManager whitelistDictionary] arrayForKey:@"ai_and_action_methods"]]; + [whitelist addObjectsFromArray:[[[ResourceManager whitelistDictionary] dictionaryForKey:@"ai_method_aliases"] allKeys]]; + _whitelist = [whitelist copy]; + [whitelist release]; + + aiNames = [[_usedAIs allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + for (aiEnum = [aiNames objectEnumerator]; (aiName = [aiEnum nextObject]); ) + { + [self validateAI:aiName]; + } + + [_whitelist release]; + _whitelist = nil; +} + + ++ (NSString *) nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier +{ + return kStageName; +} + + +- (void) stateMachineNamed:(NSString *)name usedByShip:(NSString *)shipName +{ + OOFileScannerVerifierStage *fileScanner = nil; + + if (name == nil) return; + if ([_usedAIs containsObject:name]) return; + if (_usedAIs == nil) _usedAIs = [[NSMutableSet alloc] init]; + [_usedAIs addObject:name]; + + fileScanner = [[self verifier] fileScannerStage]; + if (![fileScanner fileExists:name + inFolder:@"AIs" + referencedFrom:[NSString stringWithFormat:@"shipdata.plist entry \"%@\"", shipName] + checkBuiltIn:YES]) + { + OOLog(@"verifyOXP.validateAI.notFound", @"----- WARNING: AI state machine \"%@\" referenced in shipdata.plist entry \"%@\" could not be found in %@ or in Oolite.", name, shipName, [[self verifier] oxpDisplayName]); + } +} + +@end + + +@implementation OOAIStateMachineVerifierStage (Private) + +- (void) validateAI:(NSString *)aiName +{ + NSString *path = nil; + NSDictionary *aiStateMachine = nil; + NSEnumerator *stateEnum = nil; + NSString *stateKey = nil; + NSDictionary *stateHandlers = nil; + NSEnumerator *handlerEnum = nil; + NSString *handlerKey = nil; + NSArray *handlerActions = nil; + NSEnumerator *actionEnum = nil; + NSString *action = nil; + NSRange spaceRange; + NSString *selector = nil; + NSMutableSet *badSelectors = nil; + NSString *badSelectorDesc = nil; + OOUInteger index = 0; + + OOLog(@"verifyOXP.verbose.validateAI", @"- Validating AI \"%@\".", aiName); + OOLogIndentIf(@"verifyOXP.verbose.validateAI"); + + // Attempt to load AI. + path = [[[self verifier] fileScannerStage] pathForFile:aiName inFolder:@"AIs" referencedFrom:@"AI list" checkBuiltIn:NO]; + if (path == nil) return; + + badSelectors = [NSMutableSet set]; + + aiStateMachine = OODictionaryFromFile(path); + if (aiStateMachine == nil) + { + OOLog(@"verifyOXP.validateAI.failed.notDictPlist", @"***** ERROR: could not interpret \"%@\" as a dictionary."); + return; + } + + // Validate each state. + for (stateEnum = [aiStateMachine keyEnumerator]; (stateKey = [stateEnum nextObject]); ) + { + stateHandlers = [aiStateMachine objectForKey:stateKey]; + if (![stateHandlers isKindOfClass:[NSDictionary class]]) + { + OOLog(@"verifyOXP.validateAI.failed.invalidFormat.state", @"***** ERROR: state \"%@\" in AI \"%@\" is not a dictionary.", stateKey, aiName); + continue; + } + + // Verify handlers for this state. + for (handlerEnum = [stateHandlers keyEnumerator]; (handlerKey = [handlerEnum nextObject]); ) + { + handlerActions = [stateHandlers objectForKey:handlerKey]; + if (![handlerActions isKindOfClass:[NSArray class]]) + { + OOLog(@"verifyOXP.validateAI.failed.invalidFormat.handler", @"***** ERROR: handler \"%@\" for state \"%@\" in AI \"%@\" is not an array, ignoring.", handlerKey, stateKey, aiName); + continue; + } + + // Verify commands for this handler. + index = 0; + for (actionEnum = [handlerActions objectEnumerator]; (action = [actionEnum nextObject]); ) + { + index++; + if (![action isKindOfClass:[NSString class]]) + { + OOLog(@"verifyOXP.validateAI.failed.invalidFormat.action", @"***** ERROR: action %u in handler \"%@\" for state \"%@\" in AI \"%@\" is not a string, ignoring.", index - 1, handlerKey, stateKey, aiName); + continue; + } + + // Trim spaces from beginning and end. + action = [action stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + + // Cut off parameters. + spaceRange = [action rangeOfString:@" "]; + if (spaceRange.location == NSNotFound) selector = action; + else selector = [action substringToIndex:spaceRange.location]; + + // Check against whitelist. + if (![_whitelist containsObject:selector]) + { + [badSelectors addObject:selector]; + } + } + } + } + + if ([badSelectors count] != 0) + { + badSelectorDesc = [[[badSelectors allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] componentsJoinedByString:@", "]; + OOLog(@"verifyOXP.validateAI.failed.badSelector", @"***** ERROR: the AI \"%@\" uses %u unpermitted method%s: %@", aiName, [badSelectors count], ([badSelectors count] == 1) ? "" : "s", badSelectorDesc); + } + + OOLogOutdentIf(@"verifyOXP.verbose.validateAI"); +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.h b/src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.h new file mode 100644 index 00000000..fba04867 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.h @@ -0,0 +1,36 @@ +/* + +OOCheckDemoShipsPListVerifierStage.h + +OOOXPVerifierStage which checks that demoships.plist only contains ships +defined in shipdata.plist. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOFileScannerVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOCheckDemoShipsPListVerifierStage: OOFileHandlingVerifierStage +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.m b/src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.m new file mode 100644 index 00000000..9f745df4 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckDemoShipsPListVerifierStage.m @@ -0,0 +1,123 @@ +/* + +OOCheckDemoShipsPListVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCheckDemoShipsPListVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOFileScannerVerifierStage.h" + +static NSString * const kStageName = @"Checking demoships.plist"; + + +@interface OOCheckDemoShipsPListVerifierStage (OOPrivate) + +- (void)runCheckWithDemoShips:(NSArray *)demoshipsPList shipData:(NSDictionary *)shipdataPList; + +@end + + +@implementation OOCheckDemoShipsPListVerifierStage + +- (NSString *)name +{ + return kStageName; +} + + +- (BOOL)shouldRun +{ + OOFileScannerVerifierStage *fileScanner = nil; + + fileScanner = [[self verifier] fileScannerStage]; + return [fileScanner fileExists:@"demoships.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; +} + + +- (void)run +{ + OOFileScannerVerifierStage *fileScanner = nil; + NSArray *demoshipsPList = nil; + NSDictionary *shipdataPList = nil; + + fileScanner = [[self verifier] fileScannerStage]; + + demoshipsPList = [fileScanner plistNamed:@"demoships.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; + + if (demoshipsPList == nil) return; + + // Check that it's an array + if (![demoshipsPList isKindOfClass:[NSArray class]]) + { + OOLog(@"verifyOXP.demoshipsPList.notArray", @"***** ERROR: demoships.plist is not an array."); + return; + } + + + shipdataPList = [fileScanner plistNamed:@"shipdata.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; + + if (shipdataPList == nil) return; + + // Check that it's a dictionary + if (![shipdataPList isKindOfClass:[NSDictionary class]]) + { + OOLog(@"verifyOXP.demoshipsPList.notDict", @"***** ERROR: shipdata.plist is not a dictionary."); + return; + } + + [self runCheckWithDemoShips:demoshipsPList shipData:shipdataPList]; +} + +@end + + +@implementation OOCheckDemoShipsPListVerifierStage (OOPrivate) + +- (void)runCheckWithDemoShips:(NSArray *)demoshipsPList shipData:(NSDictionary *)shipdataPList +{ + NSEnumerator *nameEnum = nil; + NSString *name = nil; + + for (nameEnum = [demoshipsPList objectEnumerator]; (name = [nameEnum nextObject]); ) + { + if ([shipdataPList objectForKey:name] == nil) + { + OOLog(@"verifyOXP.demoshipsPList.unknownShip", @"----- WARNING: demoships.plist entry \"%@\" not found in shipdata.plist.", name); + } + } +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.h b/src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.h new file mode 100644 index 00000000..46fd0065 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.h @@ -0,0 +1,35 @@ +/* + +OOCheckEquipmentPListVerifierStage.h + +OOOXPVerifierStage which verifies equipment.plist. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOFileScannerVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOCheckEquipmentPListVerifierStage: OOFileHandlingVerifierStage +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.m b/src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.m new file mode 100644 index 00000000..d06a02d7 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckEquipmentPListVerifierStage.m @@ -0,0 +1,172 @@ +/* + +OOCheckEquipmentPListVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCheckEquipmentPListVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOFileScannerVerifierStage.h" +#import "Universe.h" +#import "OOCollectionExtractors.h" + +static NSString * const kStageName = @"Checking equipment.plist"; + + +@interface OOCheckEquipmentPListVerifierStage (OOPrivate) + +- (void)runCheckWithEquipment:(NSArray *)equipmentPList; + +@end + + +@implementation OOCheckEquipmentPListVerifierStage + +- (NSString *)name +{ + return kStageName; +} + + +- (BOOL)shouldRun +{ + OOFileScannerVerifierStage *fileScanner = nil; + + fileScanner = [[self verifier] fileScannerStage]; + return [fileScanner fileExists:@"equipment.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; +} + + +- (void)run +{ + OOFileScannerVerifierStage *fileScanner = nil; + NSArray *equipmentPList = nil; + + fileScanner = [[self verifier] fileScannerStage]; + + equipmentPList = [fileScanner plistNamed:@"equipment.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; + + if (equipmentPList == nil) return; + + // Check that it's an array + if (![equipmentPList isKindOfClass:[NSArray class]]) + { + OOLog(@"verifyOXP.equipmentPList.notArray", @"***** ERROR: equipment.plist is not an array."); + return; + } + + + [self runCheckWithEquipment:equipmentPList]; +} + +@end + + +@implementation OOCheckEquipmentPListVerifierStage (OOPrivate) + +- (void)runCheckWithEquipment:(NSArray *)equipmentPList +{ + NSEnumerator *entryEnum = nil; + NSArray *entry = nil; + unsigned entryIndex = 0; + unsigned elemCount; + NSString *name = nil; + NSString *entryDesc = nil; + + for (entryEnum = [equipmentPList objectEnumerator]; (entry = [entryEnum nextObject]); ) + { + ++entryIndex; + + // Entries should be arrays. + if (![entry isKindOfClass:[NSArray class]]) + { + OOLog(@"verifyOXP.equipmentPList.entryNotArray", @"***** ERROR: equipment.plist entry %u of equipment.plist is not an array.", entryIndex); + continue; + } + + elemCount = [entry count]; + + // Make a name for entry for display purposes. + if (EQUIPMENT_KEY_INDEX < elemCount) name = [entry stringAtIndex:EQUIPMENT_KEY_INDEX]; + else name = nil; + + if (name != nil) entryDesc = [NSString stringWithFormat:@"%u (\"%@\")", entryIndex, name]; + else entryDesc = [NSString stringWithFormat:@"%u", entryIndex]; + + // Check that the entry has an acceptable number of elements. + if (elemCount < 5) + { + OOLog(@"verifyOXP.equipmentPList.badEntrySize", @"***** ERROR: equipment.plist entry %@ has too few elements (%u, should be 5 or 6).", entryDesc, elemCount); + continue; + } + if (6 < elemCount) + { + OOLog(@"verifyOXP.equipmentPList.badEntrySize", @"----- WARNING: equipment.plist entry %@ has too many elements (%u, should be 5 or 6).", entryDesc, elemCount); + } + + /* Check element types. The numbers are required to be unsigned + integers; the use of a negative default will catch both negative + values and unconvertable values. + */ + if ([entry longAtIndex:EQUIPMENT_TECH_LEVEL_INDEX defaultValue:-1] < 0) + { + OOLog(@"verifyOXP.equipmentPList.badElementType", @"***** ERROR: tech level for entry %@ of equipment.plist is not a positive integer.", entryDesc); + } + if ([entry longAtIndex:EQUIPMENT_PRICE_INDEX defaultValue:-1] < 0) + { + OOLog(@"verifyOXP.equipmentPList.badElementType", @"***** ERROR: price for entry %@ of equipment.plist is not a positive integer.", entryDesc); + } + if ([entry stringAtIndex:EQUIPMENT_SHORT_DESC_INDEX] == nil) + { + OOLog(@"verifyOXP.equipmentPList.badElementType", @"***** ERROR: short description for entry %@ of equipment.plist is not a string.", entryDesc); + } + if ([entry stringAtIndex:EQUIPMENT_KEY_INDEX] == nil) + { + OOLog(@"verifyOXP.equipmentPList.badElementType", @"***** ERROR: key for entry %@ of equipment.plist is not a string.", entryDesc); + } + if ([entry stringAtIndex:EQUIPMENT_LONG_DESC_INDEX] == nil) + { + OOLog(@"verifyOXP.equipmentPList.badElementType", @"***** ERROR: long description for entry %@ of equipment.plist is not a string.", entryDesc); + } + + if (5 < elemCount) + { + if ([entry dictionaryAtIndex:EQUIPMENT_EXTRA_INFO_INDEX] == nil) + { + OOLog(@"verifyOXP.equipmentPList.badElementType", @"***** ERROR: equipment.plist entry %@'s extra information dictionary is not a dictionary.", entryDesc); + } + // TODO: verify contents of extra info dictionary. + } + } +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.h b/src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.h new file mode 100644 index 00000000..afcaae82 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.h @@ -0,0 +1,36 @@ +/* + +OOCheckRequiresPListVerifierStage.h + +OOOXPVerifierStage which checks that requires.plist only contains recognized +keys and that the version numbers make sense. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOFileScannerVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOCheckRequiresPListVerifierStage: OOFileHandlingVerifierStage +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.m b/src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.m new file mode 100644 index 00000000..10f69b5e --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckRequiresPListVerifierStage.m @@ -0,0 +1,165 @@ +/* + +OOCheckRequiresPListVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCheckRequiresPListVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOFileScannerVerifierStage.h" +#import "OOStringParsing.h" + +static NSString * const kStageName = @"Checking requires.plist"; + + +@implementation OOCheckRequiresPListVerifierStage + +- (NSString *)name +{ + return kStageName; +} + + +- (BOOL)shouldRun +{ + OOFileScannerVerifierStage *fileScanner = nil; + + fileScanner = [[self verifier] fileScannerStage]; + return [fileScanner fileExists:@"requires.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; +} + + +- (void)run +{ + OOFileScannerVerifierStage *fileScanner = nil; + NSDictionary *requiresPList = nil; + NSSet *knownKeys = nil; + NSMutableSet *actualKeys = nil; + NSString *version = nil, + *maxVersion = nil; + NSArray *ooVersionComponents = nil, + *versionComponents = nil, + *maxVersionComponents = nil; + + fileScanner = [[self verifier] fileScannerStage]; + requiresPList = [fileScanner plistNamed:@"requires.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; + + if (requiresPList == nil) return; + + // Check that it's a dictionary + if (![requiresPList isKindOfClass:[NSDictionary class]]) + { + OOLog(@"verifyOXP.requiresPList.notDict", @"***** ERROR: requires.plist is not a dictionary."); + return; + } + + // Check that all the keys are known. + knownKeys = [[self verifier] configurationSetForKey:@"requiresPListSupportedKeys"]; + actualKeys = [NSMutableSet setWithArray:[requiresPList allKeys]]; + [actualKeys minusSet:knownKeys]; + + if ([actualKeys count] != 0) + { + + OOLog(@"verifyOXP.requiresPList.unknownKeys", @"----- WARNING: requires.plist contains unknown keys. This OXP will not be loaded by this version of Oolite. Unknown keys are: %@.", [[actualKeys allObjects] componentsJoinedByString:@", "]); + } + + // Sanity check the known keys. + version = [requiresPList objectForKey:@"version"]; + if (version != nil) + { + if (![version isKindOfClass:[NSString class]]) + { + OOLog(@"verifyOXP.requiresPList.badValue", @"***** ERROR: Value for 'version' is not a string."); + version = nil; + } + } + + maxVersion = [requiresPList objectForKey:@"max_version"]; + if (maxVersion != nil) + { + if (![maxVersion isKindOfClass:[NSString class]]) + { + OOLog(@"verifyOXP.requiresPList.badValue", @"***** ERROR: Value for 'max_version' is not a string."); + maxVersion = nil; + } + } + + if (version != nil || maxVersion != nil) + { + ooVersionComponents = ComponentsFromVersionString([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]); + if (ooVersionComponents == nil) + { + OOLog(@"verifyOXP.requiresPList.cantFindOoliteVersion", @"----- WARNING: could not find Oolite's version for requires.plist sanity check."); + } + if (version != nil) + { + versionComponents = ComponentsFromVersionString(version); + if (versionComponents == nil) + { + OOLog(@"verifyOXP.requiresPList.badValue", @"***** ERROR: could not interpret version string \"%@\" as version number.", version); + } + else if (ooVersionComponents != nil) + { + if (CompareVersions(ooVersionComponents, versionComponents) == NSOrderedAscending) + { + OOLog(@"verifyOXP.requiresPList.oxpRequiresNewerOolite", @"----- WARNING: this OXP requires a newer version of Oolite (%@) to work.", version); + } + } + } + if (maxVersion != nil) + { + maxVersionComponents = ComponentsFromVersionString(maxVersion); + if (maxVersionComponents == nil) + { + OOLog(@"verifyOXP.requiresPList.badValue", @"***** ERROR: could not interpret max_version string \"%@\" as version number.", maxVersion); + } + else if (ooVersionComponents != nil) + { + if (CompareVersions(ooVersionComponents, maxVersionComponents) == NSOrderedDescending) + { + OOLog(@"verifyOXP.requiresPList.oxpRequiresOlderOolite", @"----- WARNING: this OXP requires an older version of Oolite (%@) to work.", maxVersion); + } + } + } + + if (versionComponents != nil && maxVersionComponents != nil) + { + if (CompareVersions(versionComponents, maxVersionComponents) == NSOrderedDescending) + { + OOLog(@"verifyOXP.requiresPList.noVersionsInRange", @"***** ERROR: this OXP's maximum version (%@) is less than its minimum version (%@).", maxVersion, version); + } + } + } +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.h b/src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.h new file mode 100644 index 00000000..9cc73a79 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.h @@ -0,0 +1,55 @@ +/* + +OOCheckShipDataPListVerifierStage.h + +OOOXPVerifierStage which checks shipdata.plist. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOTextureVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@class OOPListSchemaVerifier, OOAIStateMachineVerifierStage; + +@interface OOCheckShipDataPListVerifierStage: OOTextureHandlingStage +{ + NSDictionary *_shipdataPList; + NSSet *_ooliteShipNames; + NSSet *_basicKeys, + *_stationKeys, + *_playerKeys, + *_allKeys; + OOPListSchemaVerifier *_schemaVerifier; + OOAIStateMachineVerifierStage *_aiVerifierStage; + + // Info about ship currently being checked. + NSString *_name; + NSDictionary *_info; + NSSet *_roles; + uint32_t _isStation: 1, + _isPlayer: 1, + _havePrintedMessage: 1; +} +@end + +#endif diff --git a/src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.m b/src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.m new file mode 100644 index 00000000..92cd9054 --- /dev/null +++ b/src/Core/OXPVerifier/OOCheckShipDataPListVerifierStage.m @@ -0,0 +1,390 @@ +/* + +OOCheckShipDataPListVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCheckShipDataPListVerifierStage.h" +#import "OOModelVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOFileScannerVerifierStage.h" +#import "OOStringParsing.h" +#import "ResourceManager.h" +#import "OOCollectionExtractors.h" +#import "OOStringParsing.h" +#import "OOPListSchemaVerifier.h" +#import "OOAIStateMachineVerifierStage.h" + +static NSString * const kStageName = @"Checking shipdata.plist"; + + +@interface OOCheckShipDataPListVerifierStage (OOPrivate) + +- (void)verifyShipInfo:(NSDictionary *)info withName:(NSString *)name; + +- (void)message:(NSString *)format, ...; +- (void)verboseMessage:(NSString *)format, ...; + +- (void)getRoles; +- (void)checkKeys; +- (void)checkSchema; +- (void)checkModel; + +- (NSSet *)rolesFromString:(NSString *)string; + +@end + + +@implementation OOCheckShipDataPListVerifierStage + +- (void) dealloc +{ + /* This class never retains any instance variables (we can get away with + that because they're never used after returning to the caller of -run), + but this is here to placate the Clang Static Analyzer. + */ + [super dealloc]; +} + + +- (NSString *)name +{ + return kStageName; +} + + +- (NSSet *)dependents +{ + NSMutableSet *result = [[super dependents] mutableCopy]; + [result addObject:[OOModelVerifierStage nameForReverseDependencyForVerifier:[self verifier]]]; + [result addObject:[OOAIStateMachineVerifierStage nameForReverseDependencyForVerifier:[self verifier]]]; + return [result autorelease]; +} + + +- (BOOL)shouldRun +{ + OOFileScannerVerifierStage *fileScanner = nil; + + fileScanner = [[self verifier] fileScannerStage]; + return [fileScanner fileExists:@"shipdata.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; +} + + +- (void)run +{ + OOFileScannerVerifierStage *fileScanner = nil; + NSAutoreleasePool *pool = nil; + NSEnumerator *shipEnum = nil; + NSString *shipKey = nil; + NSDictionary *shipInfo = nil; + NSDictionary *ooliteShipData = nil; + NSDictionary *settings = nil; + NSMutableSet *mergeSet = nil; + NSArray *shipList = nil; + + fileScanner = [[self verifier] fileScannerStage]; + _shipdataPList = [fileScanner plistNamed:@"shipdata.plist" + inFolder:@"Config" + referencedFrom:nil + checkBuiltIn:NO]; + + if (_shipdataPList == nil) return; + + // Get AI verifier stage (may be nil). + _aiVerifierStage = [[self verifier] stageWithName:[OOAIStateMachineVerifierStage nameForReverseDependencyForVerifier:[self verifier]]]; + + ooliteShipData = [ResourceManager dictionaryFromFilesNamed:@"shipdata.plist" + inFolder:@"Config" + andMerge:YES]; + + // Check that it's a dictionary + if (![_shipdataPList isKindOfClass:[NSDictionary class]]) + { + OOLog(@"verifyOXP.shipdataPList.notDict", @"***** ERROR: shipdata.plist is not a dictionary."); + return; + } + + // Keys that apply to all ships + _ooliteShipNames = [NSSet setWithArray:[ooliteShipData allKeys]]; + settings = [[self verifier] configurationDictionaryForKey:@"shipdataPListSettings"]; + _basicKeys = [settings setForKey:@"knownShipKeys"]; + + // Keys that apply to stations/carriers + mergeSet = [_basicKeys mutableCopy]; + [mergeSet addObjectsFromArray:[settings arrayForKey:@"knownStationKeys"]]; + _stationKeys = mergeSet; + + // Keys that apply to player ships + mergeSet = [_basicKeys mutableCopy]; + [mergeSet addObjectsFromArray:[settings arrayForKey:@"knownPlayerKeys"]]; + _playerKeys = [[mergeSet copy] autorelease]; + + // Keys that apply to _any_ ship -- union of the above + [mergeSet unionSet:_stationKeys]; + _allKeys = mergeSet; + + _schemaVerifier = [OOPListSchemaVerifier verifierWithSchema:[ResourceManager dictionaryFromFilesNamed:@"shipdataEntrySchema.plist" inFolder:@"Schemata" andMerge:NO]]; + [_schemaVerifier setDelegate:self]; + + shipList = [[_shipdataPList allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; + for (shipEnum = [shipList objectEnumerator]; (shipKey = [shipEnum nextObject]); ) + { + pool = [[NSAutoreleasePool alloc] init]; + + shipInfo = [_shipdataPList dictionaryForKey:shipKey]; + if (shipInfo == nil) + { + OOLog(@"verifyOXP.shipdata.badType", @"***** ERROR: shipdata.plist entry for \"%@\" is not a dictionary.", shipKey); + } + else + { + [self verifyShipInfo:shipInfo withName:shipKey]; + } + + [pool release]; + } + + _shipdataPList = nil; + _ooliteShipNames = nil; + _basicKeys = nil; + _stationKeys = nil; + _playerKeys = nil; +} + +@end + + +@implementation OOCheckShipDataPListVerifierStage (OOPrivate) + +- (void)verifyShipInfo:(NSDictionary *)info withName:(NSString *)name +{ + _name = name; + _info = info; + _havePrintedMessage = NO; + OOLogPushIndent(); + + [self getRoles]; + [self checkKeys]; + [self checkSchema]; + [self checkModel]; + + NSString *aiName = [info stringForKey:@"ai_type"]; + if (aiName != nil) [_aiVerifierStage stateMachineNamed:aiName usedByShip:name]; + + // Todo: check for pirates with 0 bounty + + OOLogPopIndent(); + if (!_havePrintedMessage) + { + OOLog(@"verifyOXP.verbose.shipData.OK", @"- ship \"%@\" OK.", _name); + } + _name = nil; + _info = nil; + _roles = nil; +} + + +// Custom log method to group messages by ship. +- (void)message:(NSString *)format, ... +{ + va_list args; + + if (!_havePrintedMessage) + { + OOLog(@"verifyOXP.shipData.firstMessage", @"Ship \"%@\":", _name); + OOLogIndent(); + _havePrintedMessage = YES; + } + + va_start(args, format); + OOLogWithFunctionFileAndLineAndArguments(@"verifyOXP.shipData", NULL, NULL, 0, format, args); + va_end(args); +} + + +- (void)verboseMessage:(NSString *)format, ... +{ + va_list args; + + if (!OOLogWillDisplayMessagesInClass(@"verifyOXP.verbose.shipData")) return; + + if (!_havePrintedMessage) + { + OOLog(@"verifyOXP.shipData.firstMessage", @"Ship \"%@\":", _name); + OOLogIndent(); + _havePrintedMessage = YES; + } + + va_start(args, format); + OOLogWithFunctionFileAndLineAndArguments(@"verifyOXP.verbose.shipData", NULL, NULL, 0, format, args); + va_end(args); +} + + +- (void)getRoles +{ + NSString *rolesString = nil; + + rolesString = [_info objectForKey:@"roles"]; + _roles = [self rolesFromString:rolesString]; + _isPlayer = [_roles containsObject:@"player"]; + _isStation = [_info boolForKey:@"isCarrier" defaultValue:NO] || + [rolesString rangeOfString:@"station"].location != NSNotFound || + [rolesString rangeOfString:@"carrier"].location != NSNotFound; + + if (_isPlayer && _isStation) + { + [self message:@"***** ERROR: ship is both a player ship and a station. Treating as non-station."]; + _isStation = NO; + } +} + + +- (void)checkKeys +{ + NSSet *referenceSet = nil; + NSEnumerator *keyEnum = nil; + NSString *key = nil; + + if (_isPlayer) referenceSet = _playerKeys; + else if (_isStation) referenceSet = _stationKeys; + else referenceSet = _basicKeys; + + for (keyEnum = [_info keyEnumerator]; (key = [keyEnum nextObject]); ) + { + if (![referenceSet containsObject:key]) + { + if ([_allKeys containsObject:key]) + { + [self message:@"----- WARNING: key \"%@\" does not apply to this category of ship.", key]; + } + else + { + [self message:@"----- WARNING: unknown key \"%@\".", key]; + } + } + } +} + + +- (void)checkSchema +{ + [_schemaVerifier verifyPropertyList:_info named:_name]; +} + + +- (void)checkModel +{ + id model = nil, + materials = nil, + shaders = nil; + + model = [_info stringForKey:@"model"]; + materials = [_info dictionaryForKey:@"materials"]; + shaders = [_info dictionaryForKey:@"shaders"]; + + if (model != nil) + { + if (![[[self verifier] modelVerifierStage] modelNamed:model + usedForEntry:_name + inFile:@"shipdata.plist" + withMaterials:materials + andShaders:shaders]) + { + [self message:@"----- WARNING: model \"%@\" could not be found in %@ or in Oolite.", model, [[self verifier] oxpDisplayName]]; + } + } + else + { + if ([_info stringForKey:@"like_ship"] == nil) + { + [self message:@"***** ERROR: ship does not specify model or like_ship."]; + } + } +} + + +// Convert a roles string to a set of role names, discarding probabilities. +- (NSSet *)rolesFromString:(NSString *)string +{ + NSArray *parts = nil; + NSMutableSet *result = nil; + unsigned i, count; + NSString *role = nil; + NSRange parenRange; + + if (string == nil) return [NSSet set]; + + parts = ScanTokensFromString(string); + count = [parts count]; + if (count == 0) return [NSSet set]; + + result = [NSMutableSet setWithCapacity:count]; + for (i = 0; i != count; ++i) + { + role = [parts objectAtIndex:i]; + parenRange = [role rangeOfString:@"("]; + if (parenRange.location != NSNotFound) + { + role = [role substringToIndex:parenRange.location]; + } + [result addObject:role]; + } + + return result; +} + + +- (BOOL)verifier:(OOPListSchemaVerifier *)verifier +withPropertyList:(id)rootPList + named:(NSString *)name + testProperty:(id)subPList + atPath:(NSArray *)keyPath + againstType:(NSString *)typeKey + error:(NSError **)outError +{ + [self verboseMessage:@"- Skipping verification for type %@ at %@.%@.", typeKey, _name, [OOPListSchemaVerifier descriptionForKeyPath:keyPath]]; + return YES; +} + + +- (BOOL)verifier:(OOPListSchemaVerifier *)verifier +withPropertyList:(id)rootPList + named:(NSString *)name + failedForProperty:(id)subPList + withError:(NSError *)error + expectedType:(NSDictionary *)localSchema +{ + // FIXME: use fancy new error codes to provide useful error descriptions. + [self message:@"***** ERROR: verification of ship \"%@\" failed at \"%@\": %@", name, [error plistKeyPathDescription], [error localizedDescription]]; + return YES; +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOFileScannerVerifierStage.h b/src/Core/OXPVerifier/OOFileScannerVerifierStage.h new file mode 100644 index 00000000..ac504bef --- /dev/null +++ b/src/Core/OXPVerifier/OOFileScannerVerifierStage.h @@ -0,0 +1,137 @@ +/* + +OOFileScannerVerifierStage.h + +OOOXPVerifierStage which keeps track of which files are used and ensures file +name capitalization is consistent. It also provides the file lookup service +for other stages. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOOXPVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOFileScannerVerifierStage: OOOXPVerifierStage +{ + NSString *_basePath; + NSMutableSet *_usedFiles; + NSMutableSet *_caseWarnings; + NSDictionary *_directoryListings; + NSDictionary *_directoryCases; + NSMutableSet *_badPLists; + NSSet *_junkFileNames; + NSSet *_skipDirectoryNames; +} + +// Returns name to be used in -dependencies by other stages; also registers stage. ++ (NSString *)nameForDependencyForVerifier:(OOOXPVerifier *)verifier; + +/* This method does the following: + A. Checks whether a file exists. + B. Checks whether case matches, and logs a warning otherwise. + C. Maintains list of files which are referred to. + D. Optionally falls back on Oolite's built-in files. + + For example, to test whether a texture referenced in a shipdata.plist entry + exists, one would use: + [fileScanner fileExists:textureName inFolder:@"Textures" referencedFrom:@"shipdata.plist" checkBuiltIn:YES]; +*/ +- (BOOL)fileExists:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn; + +// This method performs all the checks the previous one does, but also returns a file path. +- (NSString *)pathForFile:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn; + +// Data getters based on above method. +- (NSData *)dataForFile:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn; + +- (id)plistNamed:(NSString *)file // Only uses "real" plist parser, not homebrew. + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn; + + +/* Utility to handle display names of files. + If a file and folder are provided, returns folder/file, otherwise just file. +*/ +- (id)displayNameForFile:(NSString *)file andFolder:(NSString *)folder; + +/* Get a list of files in a subfolder of the OXP. Order is undefined. +*/ +- (NSArray *)filesInFolder:(NSString *)folder; + +@end + + +@interface OOListUnusedFilesStage: OOOXPVerifierStage + +// Returns name to be used in -dependents by other stages; also registers stage. ++ (NSString *)nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier; + +@end + + +@interface OOOXPVerifier(OOFileScannerVerifierStage) + +- (OOFileScannerVerifierStage *)fileScannerStage; + +@end + + +// Convenience base class for stages that require OOFileScannerVerifierStage and OOListUnusedFilesStage. +@interface OOFileHandlingVerifierStage: OOOXPVerifierStage + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOFileScannerVerifierStage.m b/src/Core/OXPVerifier/OOFileScannerVerifierStage.m new file mode 100644 index 00000000..0533e68b --- /dev/null +++ b/src/Core/OXPVerifier/OOFileScannerVerifierStage.m @@ -0,0 +1,805 @@ +/* + +OOFileScannerVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +/* Design notes: + In order to be able to look files up case-insenstively, but warn about + case mismatches, the OOFileScannerVerifierStage builds its own + representation of the file hierarchy. Dictionaries are used heavily: the + _directoryListings is keyed by folder names mapped to lower case, and its + entries map lowercase file names to actual case, that is, the case found + in the file system. The companion dictionary _directoryCases maps + lowercase directory names to actual case. + + The class design is based on the knowledge that Oolite uses a two-level + namespace for files. Each file type has an appropriate folder, and files + may either be in the appropriate folder or "bare". For instance, a texture + file in an OXP may be either in the Textures subdirectory or in the root + directory of the OXP. The root directory's contents are listed in + _directoryListings with the empty string as key. This architecture means + the OOFileScannerVerifierStage doesn't need to take full file system + hierarchy into account. +*/ + +#import "OOFileScannerVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOCollectionExtractors.h" +#import "ResourceManager.h" + +static NSString * const kFileScannerStageName = @"Scanning files"; +static NSString * const kUnusedListerStageName = @"Checking for unused files"; + + +static BOOL CheckNameConflict(NSString *lcName, NSDictionary *directoryCases, NSDictionary *rootFiles, NSString **outExisting, NSString **outExistingType); + + +@interface OOFileScannerVerifierStage (OOPrivate) + +- (void)scanForFiles; + +- (void)checkRootFolders; +- (void)checkKnownFiles; + +/* Given an array of strings, return a dictionary mapping lowercase strings + to the canonicial case given in the array. For instance, given + (Foo, BAR) + + it will return + { foo = Foo; bar = BAR } +*/ +- (NSDictionary *)lowercaseMap:(NSArray *)array; + +- (NSDictionary *)scanDirectory:(NSString *)path; +- (void)checkPListFormat:(NSPropertyListFormat)format file:(NSString *)file folder:(NSString *)folder; +- (NSSet *)constructReadMeNames; + +@end + + +@implementation OOFileScannerVerifierStage + +- (void)dealloc +{ + [_basePath release]; + [_usedFiles release]; + [_caseWarnings release]; + [_directoryListings release]; + [_directoryCases release]; + [_badPLists release]; + + [super dealloc]; +} + + +- (NSString *)name +{ + return kFileScannerStageName; +} + + +- (void)run +{ + NSAutoreleasePool *pool = nil; + + _usedFiles = [[NSMutableSet alloc] init]; + _caseWarnings = [[NSMutableSet alloc] init]; + _badPLists = [[NSMutableSet alloc] init]; + + pool = [[NSAutoreleasePool alloc] init]; + [self scanForFiles]; + [pool release]; + + pool = [[NSAutoreleasePool alloc] init]; + [self checkRootFolders]; + [self checkKnownFiles]; + [pool release]; +} + + ++ (NSString *)nameForDependencyForVerifier:(OOOXPVerifier *)verifier +{ + OOFileScannerVerifierStage *stage = [verifier stageWithName:kFileScannerStageName]; + if (stage == nil) + { + stage = [[OOFileScannerVerifierStage alloc] init]; + [verifier registerStage:stage]; + [stage release]; + } + + return kFileScannerStageName; +} + + +- (BOOL)fileExists:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn +{ + return [self pathForFile:file inFolder:folder referencedFrom:context checkBuiltIn:checkBuiltIn] != nil; +} + + +- (NSString *)pathForFile:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn +{ + NSString *lcName = nil, + *lcDirName = nil, + *realDirName = nil, + *realFileName = nil, + *path = nil, + *expectedPath = nil; + + if (file == nil) return nil; + lcName = [file lowercaseString]; + + if (folder != nil) + { + lcDirName = [folder lowercaseString]; + realFileName = [[_directoryListings dictionaryForKey:lcDirName] objectForKey:lcName]; + + if (realFileName != nil) + { + realDirName = [_directoryCases objectForKey:lcDirName]; + path = [realDirName stringByAppendingPathComponent:realFileName]; + } + } + + if (path == nil) + { + realFileName = [[_directoryListings dictionaryForKey:@""] objectForKey:lcName]; + + if (realFileName != nil) + { + path = realFileName; + } + } + + if (path != nil) + { + [_usedFiles addObject:path]; + if (realDirName != nil && ![realDirName isEqual:folder]) + { + // Case mismatch for folder name + if (![_caseWarnings containsObject:lcDirName]) + { + [_caseWarnings addObject:lcDirName]; + OOLog(@"verifyOXP.files.caseMismatch", @"***** ERROR: case mismatch: directory '%@' should be called '%@'.", realDirName, folder); + } + } + + if (![realFileName isEqual:file]) + { + // Case mismatch for file name + if (![_caseWarnings containsObject:lcName]) + { + [_caseWarnings addObject:lcName]; + + expectedPath = [self displayNameForFile:file andFolder:folder]; + + if (context != nil) context = [@" referenced in " stringByAppendingString:context]; + else context = @""; + + OOLog(@"verifyOXP.files.caseMismatch", @"***** ERROR: case mismatch: request for file '%@'%@ resolved to '%@'.", expectedPath, context, path); + } + } + + return [_basePath stringByAppendingPathComponent:path]; + } + + // If we get here, the file wasn't found in the OXP. + // FIXME: should check case for built-in files. + if (checkBuiltIn) return [ResourceManager pathForFileNamed:file inFolder:folder]; + + return nil; +} + + +- (NSData *)dataForFile:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn +{ + NSString *path = nil; + + path = [self pathForFile:file inFolder:folder referencedFrom:context checkBuiltIn:checkBuiltIn]; + if (path == nil) return nil; + + return [NSData dataWithContentsOfMappedFile:path]; +} + + +- (id)plistNamed:(NSString *)file + inFolder:(NSString *)folder + referencedFrom:(NSString *)context + checkBuiltIn:(BOOL)checkBuiltIn +{ + NSData *data = nil; + NSString *errorString = nil; + NSPropertyListFormat format; + id plist = nil; + NSArray *errorLines = nil; + NSEnumerator *errLineEnum = nil; + NSString *displayName = nil, + *errorKey = nil; + NSAutoreleasePool *pool = nil; + + data = [self dataForFile:file inFolder:folder referencedFrom:context checkBuiltIn:checkBuiltIn]; + if (data == nil) return nil; + + pool = [[NSAutoreleasePool alloc] init]; + + plist = [NSPropertyListSerialization propertyListFromData:data + mutabilityOption:NSPropertyListImmutable + format:&format + errorDescription:&errorString]; + +#if OOLITE_RELEASE_PLIST_ERROR_STRINGS + [errorString autorelease]; +#endif + + if (plist != nil) + { + // PList is readable; check that it's in an official Oolite format. + [self checkPListFormat:format file:file folder:folder]; + } + else + { + /* Couldn't parse plist; report problem. + This is complicated somewhat by the need to present a possibly + multi-line error description while maintaining our indentation. + */ + displayName = [self displayNameForFile:file andFolder:folder]; + errorKey = [displayName lowercaseString]; + if (![_badPLists containsObject:errorKey]) + { + [_badPLists addObject:errorKey]; + OOLog(@"verifyOXP.plist.parseError", @"Could not interpret property list %@.", displayName); + OOLogIndent(); + errorLines = [errorString componentsSeparatedByString:@"\n"]; + for (errLineEnum = [errorLines objectEnumerator]; (errorString = [errLineEnum nextObject]); ) + { + while ([errorString hasPrefix:@"\t"]) + { + errorString = [@" " stringByAppendingString:[errorString substringFromIndex:1]]; + } + OOLog(@"verifyOXP.plist.parseError", errorString); + } + OOLogOutdent(); + } + } + + [plist retain]; + [pool release]; + + return [plist autorelease]; +} + + +- (id)displayNameForFile:(NSString *)file andFolder:(NSString *)folder +{ + if (file != nil && folder != nil) return [folder stringByAppendingPathComponent:file]; + return file; +} + + +- (NSArray *)filesInFolder:(NSString *)folder +{ + if (folder == nil) return nil; + return [[_directoryListings objectForKey:[folder lowercaseString]] allValues]; +} + +@end + + +@implementation OOFileScannerVerifierStage (OOPrivate) + +- (void)scanForFiles +{ + NSDirectoryEnumerator *dirEnum = nil; + NSString *name = nil, + *path = nil, + *type = nil, + *lcName = nil, + *existing = nil, + *existingType = nil; + NSMutableDictionary *directoryListings = nil, + *directoryCases = nil, + *rootFiles = nil; + NSDictionary *dirFiles = nil; + NSSet *readMeNames = nil; + + _basePath = [[[self verifier] oxpPath] copy]; + + _junkFileNames = [[self verifier] configurationSetForKey:@"junkFiles"]; + _skipDirectoryNames = [[self verifier] configurationSetForKey:@"skipDirectories"]; + + directoryCases = [NSMutableDictionary dictionary]; + directoryListings = [NSMutableDictionary dictionary]; + rootFiles = [NSMutableDictionary dictionary]; + readMeNames = [self constructReadMeNames]; + + dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:_basePath]; + while ((name = [dirEnum nextObject])) + { + path = [_basePath stringByAppendingPathComponent:name]; + type = [[dirEnum fileAttributes] fileType]; + lcName = [name lowercaseString]; + + if ([type isEqualToString:NSFileTypeDirectory]) + { + [dirEnum skipDescendents]; + + if ([_skipDirectoryNames containsObject:name]) + { + // Silently skip .svn and CVS + OOLog(@"verifyOXP.verbose.listFiles", @"- Skipping %@/", name); + } + else if (!CheckNameConflict(lcName, directoryCases, rootFiles, &existing, &existingType)) + { + OOLog(@"verifyOXP.verbose.listFiles", @"- %@/", name); + OOLogIndentIf(@"verifyOXP.verbose.listFiles"); + dirFiles = [self scanDirectory:path]; + [directoryListings setObject:dirFiles forKey:lcName]; + [directoryCases setObject:name forKey:lcName]; + OOLogOutdentIf(@"verifyOXP.verbose.listFiles"); + } + else + { + OOLog(@"verifyOXP.scanFiles.overloadedName", @"***** ERROR: %@ '%@' conflicts with %@ named '%@', ignoring. (OXPs must work on case-insensitive file systems!)", @"directory", name, existingType, existing); + } + } + else if ([type isEqualToString:NSFileTypeRegular]) + { + if ([_junkFileNames containsObject:name]) + { + OOLog(@"verifyOXP.scanFiles.skipJunk", @"NOTE: skipping junk file %@.", name); + } + else if ([readMeNames containsObject:lcName]) + { + OOLog(@"verifyOXP.scanFiles.readMe", @"----- WARNING: apparent Read Me file (\"%@\") inside OXP. This is the wrong place for a Read Me file, because it will not be read.", name); + } + else if (!CheckNameConflict(lcName, directoryCases, rootFiles, &existing, &existingType)) + { + OOLog(@"verifyOXP.verbose.listFiles", @"- %@", name); + [rootFiles setObject:name forKey:lcName]; + } + else + { + OOLog(@"verifyOXP.scanFiles.overloadedName", @"***** ERROR: %@ '%@' conflicts with %@ named '%@', ignoring. (OXPs must work on case-insensitive file systems!)", @"file", name, existingType, existing); + } + } + else if ([type isEqualToString:NSFileTypeSymbolicLink]) + { + OOLog(@"verifyOXP.scanFiles.symLink", @"----- WARNING: \"%@\" is a symbolic link, ignoring.", name); + } + else + { + OOLog(@"verifyOXP.scanFiles.nonStandardFile", @"----- WARNING: \"%@\" is a non-standard file (%@), ignoring.", name, type); + } + } + + _junkFileNames = nil; + _skipDirectoryNames = nil; + + [directoryListings setObject:rootFiles forKey:@""]; + _directoryListings = [directoryListings copy]; + _directoryCases = [directoryCases copy]; +} + + +- (void)checkRootFolders +{ + NSArray *knownNames = nil; + NSEnumerator *nameEnum = nil; + NSString *name = nil; + NSString *lcName = nil; + NSString *actual = nil; + + knownNames = [[self verifier] configurationArrayForKey:@"knownRootDirectories"]; + for (nameEnum = [knownNames objectEnumerator]; (name = [nameEnum nextObject]); ) + { + if (![name isKindOfClass:[NSString class]]) continue; + + lcName = [name lowercaseString]; + actual = [_directoryCases objectForKey:lcName]; + if (actual == nil) continue; + + if (![actual isEqualToString:name]) + { + OOLog(@"verifyOXP.files.caseMismatch", @"***** ERROR: case mismatch: directory '%@' should be called '%@'.", actual, name); + } + [_caseWarnings addObject:lcName]; + } +} + + +- (void)checkConfigFiles +{ + NSArray *knownNames = nil; + NSEnumerator *nameEnum = nil; + NSString *name = nil, + *lcName = nil, + *realFileName = nil; + BOOL inConfigDir; + + knownNames = [[self verifier] configurationArrayForKey:@"knownConfigFiles"]; + for (nameEnum = [knownNames objectEnumerator]; (name = [nameEnum nextObject]); ) + { + if (![name isKindOfClass:[NSString class]]) continue; + + /* In theory, we could use -fileExists:inFolder:referencedFrom:checkBuiltIn: + here, but we want a different error message. + */ + + lcName = [name lowercaseString]; + realFileName = [[_directoryListings dictionaryForKey:@"config"] objectForKey:lcName]; + inConfigDir = realFileName != nil; + if (!inConfigDir) realFileName = [[_directoryListings dictionaryForKey:@""] objectForKey:lcName]; + if (realFileName == nil) continue; + + if (![realFileName isEqualToString:name]) + { + if (inConfigDir) realFileName = [@"Config" stringByAppendingPathComponent:realFileName]; + OOLog(@"verifyOXP.files.caseMismatch", @"***** ERROR: case mismatch: configuration file '%@' should be called '%@'.", realFileName, name); + } + } +} + + +- (void)checkKnownFiles +{ + NSDictionary *directories = nil; + NSEnumerator *directoryEnum = nil; + NSString *directory = nil, + *lcDirectory = nil; + NSArray *fileList = nil; + NSEnumerator *nameEnum = nil; + NSString *name = nil, + *lcName = nil, + *realFileName = nil; + BOOL inDirectory; + + directories = [[self verifier] configurationDictionaryForKey:@"knownFiles"]; + for (directoryEnum = [directories keyEnumerator]; (directory = [directoryEnum nextObject]); ) + { + fileList = [directories objectForKey:directory]; + lcDirectory = [directory lowercaseString]; + for (nameEnum = [fileList objectEnumerator]; (name = [nameEnum nextObject]); ) + { + if (![name isKindOfClass:[NSString class]]) continue; + + /* In theory, we could use -fileExists:inFolder:referencedFrom:checkBuiltIn: + here, but we want a different error message. + */ + + lcName = [name lowercaseString]; + realFileName = [[_directoryListings dictionaryForKey:lcDirectory] objectForKey:lcName]; + inDirectory = (realFileName != nil); + if (!inDirectory) + { + // Allow for files in root directory of OXP + realFileName = [[_directoryListings dictionaryForKey:@""] objectForKey:lcName]; + } + if (realFileName == nil) continue; + + if (![realFileName isEqualToString:name]) + { + if (inDirectory) realFileName = [directory stringByAppendingPathComponent:realFileName]; + OOLog(@"verifyOXP.files.caseMismatch", @"***** ERROR: case mismatch: file '%@' should be called '%@'.", realFileName, name); + } + } + } +} + + +- (NSDictionary *)lowercaseMap:(NSArray *)array +{ + unsigned i, count; + NSString *canonical = nil, + *lowercase = nil; + NSMutableDictionary *result = nil; + + count = [array count]; + if (count == 0) return [NSDictionary dictionary]; + result = [NSMutableDictionary dictionaryWithCapacity:count]; + + for (i = 0; i != count; ++i) + { + canonical = [array stringAtIndex:i]; + if (canonical != nil) + { + lowercase = [canonical lowercaseString]; + [result setObject:canonical forKey:lowercase]; + } + } + + return result; +} + + +- (NSDictionary *)scanDirectory:(NSString *)path +{ + NSDirectoryEnumerator *dirEnum = nil; + NSMutableDictionary *result = nil; + NSString *name = nil, + *lcName = nil, + *type = nil, + *dirName = nil, + *relativeName = nil, + *existing = nil; + + result = [NSMutableDictionary dictionary]; + dirName = [path lastPathComponent]; + + dirEnum = [[NSFileManager defaultManager] enumeratorAtPath:path]; + while ((name = [dirEnum nextObject])) + { + type = [[dirEnum fileAttributes] fileType]; + relativeName = [dirName stringByAppendingPathComponent:name]; + + if ([_junkFileNames containsObject:name]) + { + OOLog(@"verifyOXP.scanFiles.skipJunk", @"NOTE: skipping junk file %@/%@.", dirName, name); + } + else if ([type isEqualToString:NSFileTypeRegular]) + { + lcName = [name lowercaseString]; + existing = [result objectForKey:lcName]; + + if (existing == nil) + { + OOLog(@"verifyOXP.verbose.listFiles", @"- %@", name); + [result setObject:name forKey:lcName]; + } + else + { + OOLog(@"verifyOXP.scanFiles.overloadedName", @"***** ERROR: %@ '%@' conflicts with %@ named '%@', ignoring. (OXPs must work on case-insensitive file systems!)", @"file", relativeName, @"file", [dirName stringByAppendingPathComponent:existing]); + } + } + else + { + if ([type isEqualToString:NSFileTypeDirectory]) + { + [dirEnum skipDescendents]; + if (![_skipDirectoryNames containsObject:name]) + { + OOLog(@"verifyOXP.scanFiles.directory", @"----- WARNING: \"%@\" is a nested directory, ignoring.", relativeName); + } + else + { + OOLog(@"verifyOXP.verbose.listFiles", @"- Skipping %@/%@/", dirName, name); + } + } + else if ([type isEqualToString:NSFileTypeSymbolicLink]) + { + OOLog(@"verifyOXP.scanFiles.symLink", @"----- WARNING: \"%@\" is a symbolic link, ignoring.", relativeName); + } + else + { + OOLog(@"verifyOXP.scanFiles.nonStandardFile", @"----- WARNING: \"%@\" is a non-standard file (%@), ignoring.", relativeName, type); + } + } + } + + return result; +} + + +- (void)checkPListFormat:(NSPropertyListFormat)format file:(NSString *)file folder:(NSString *)folder +{ + NSString *weirdnessKey = nil; + NSString *formatDesc = nil; + NSString *displayPath = nil; + + if (format != NSPropertyListOpenStepFormat && format != NSPropertyListXMLFormat_v1_0) + { + displayPath = [self displayNameForFile:file andFolder:folder]; + weirdnessKey = [displayPath lowercaseString]; + + if (![_badPLists containsObject:weirdnessKey]) + { + // Warn about "non-standard" format + [_badPLists addObject:weirdnessKey]; + + switch (format) + { + case NSPropertyListBinaryFormat_v1_0: + formatDesc = @"Apple binary format"; + break; + +#if OOLITE_GNUSTEP + case NSPropertyListGNUstepFormat: + formatDesc = @"GNUstep text format"; + break; + + case NSPropertyListGNUstepBinaryFormat: + formatDesc = @"GNUstep binary format"; + break; +#endif + + default: + formatDesc = [NSString stringWithFormat:@"unknown format (%i)", (int)format]; + } + + OOLog(@"verifyOXP.plist.weirdFormat", @"----- WARNING: Property list %@ is in %@; OpenStep text format and XML format are the recommended formats for Oolite.", displayPath, formatDesc); + } + } +} + + +- (NSSet *)constructReadMeNames +{ + NSDictionary *dict = nil; + NSArray *stems = nil, + *extensions = nil; + NSMutableSet *result = nil; + unsigned i, j, stemCount, extCount; + NSString *stem = nil, + *extension = nil; + + dict = [[self verifier] configurationDictionaryForKey:@"readMeNames"]; + stems = [dict arrayForKey:@"stems"]; + extensions = [dict arrayForKey:@"extensions"]; + stemCount = [stems count]; + extCount = [extensions count]; + if (stemCount * extCount == 0) return nil; + + // Construct all stem+extension permutations + result = [NSMutableSet setWithCapacity:stemCount * extCount]; + for (i = 0; i != stemCount; ++i) + { + stem = [[stems stringAtIndex:i] lowercaseString]; + if (stem != nil) + { + for (j = 0; j != extCount; ++j) + { + extension = [[extensions stringAtIndex:j] lowercaseString]; + if (extension != nil) + { + [result addObject:[stem stringByAppendingString:extension]]; + } + } + } + } + + return result; +} + +@end + + +@implementation OOListUnusedFilesStage: OOOXPVerifierStage + +- (NSString *)name +{ + return kUnusedListerStageName; +} + + +- (NSSet *)dependencies +{ + return [NSSet setWithObject:kFileScannerStageName]; +} + + +- (void)run +{ + OOLog(@"verifyOXP.unusedFiles.unimplemented", @"TODO: implement unused files check."); +} + + ++ (NSString *)nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier +{ + OOListUnusedFilesStage *stage = [verifier stageWithName:kUnusedListerStageName]; + if (stage == nil) + { + stage = [[OOListUnusedFilesStage alloc] init]; + [verifier registerStage:stage]; + [stage release]; + } + + return kUnusedListerStageName; +} + +@end + + +@implementation OOOXPVerifier(OOFileScannerVerifierStage) + +- (OOFileScannerVerifierStage *)fileScannerStage +{ + return [self stageWithName:kFileScannerStageName]; +} + +@end + + +@implementation OOFileHandlingVerifierStage + +- (NSSet *)dependencies +{ + return [NSSet setWithObject:[OOFileScannerVerifierStage nameForDependencyForVerifier:[self verifier]]]; +} + + +- (NSSet *)dependents +{ + return [NSSet setWithObject:[OOListUnusedFilesStage nameForReverseDependencyForVerifier:[self verifier]]]; +} + +@end + + +static BOOL CheckNameConflict(NSString *lcName, NSDictionary *directoryCases, NSDictionary *rootFiles, NSString **outExisting, NSString **outExistingType) +{ + NSString *existing = nil; + + existing = [directoryCases objectForKey:lcName]; + if (existing != nil) + { + if (outExisting != NULL) *outExisting = existing; + if (outExistingType != NULL) *outExistingType = @"directory"; + return YES; + } + + existing = [rootFiles objectForKey:lcName]; + if (existing != nil) + { + if (outExisting != NULL) *outExisting = existing; + if (outExistingType != NULL) *outExistingType = @"file"; + return YES; + } + + return NO; +} + +#endif diff --git a/src/Core/OXPVerifier/OOModelVerifierStage.h b/src/Core/OXPVerifier/OOModelVerifierStage.h new file mode 100644 index 00000000..d600185b --- /dev/null +++ b/src/Core/OXPVerifier/OOModelVerifierStage.h @@ -0,0 +1,60 @@ +/* + +OOModelVerifierStage.h + +OOOXPVerifierStage which keeps track of models that are used and ensures they +are loadable. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOTextureVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOModelVerifierStage: OOTextureHandlingStage +{ + NSMutableSet *_modelsToCheck; +} + +// Returns name to be used in -dependents by other stages; also registers stage. ++ (NSString *)nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier; + +/* This can be called by other stages *before* the model stage runs. + returns YES if the model is found, NO if it is not. Caller is responsible + for complaining if it is not. +*/ +- (BOOL)modelNamed:(NSString *)name + usedForEntry:(NSString *)entryName + inFile:(NSString *)fileName + withMaterials:(NSDictionary *)materials + andShaders:(NSDictionary *)shaders; + +@end + + +@interface OOOXPVerifier(OOModelVerifierStage) + +- (OOModelVerifierStage *)modelVerifierStage; + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOModelVerifierStage.m b/src/Core/OXPVerifier/OOModelVerifierStage.m new file mode 100644 index 00000000..a67667e6 --- /dev/null +++ b/src/Core/OXPVerifier/OOModelVerifierStage.m @@ -0,0 +1,198 @@ +/* + +OOModelVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOModelVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOFileScannerVerifierStage.h" + +static NSString * const kStageName = @"Testing models"; + +static id NSNULL = nil; + + +@interface OOModelVerifierStage (OOPrivate) + +- (void)checkModel:(NSString *)name + context:(NSString *)context + materials:(NSDictionary *)materials + shaders:(NSDictionary *)shaders; + +@end + + +@implementation OOModelVerifierStage + +- (id)init +{ + self = [super init]; + if (self != nil) + { + NSNULL = [[NSNull null] retain]; + _modelsToCheck = [[NSMutableSet alloc] init]; + } + return self; +} + + +- (void)dealloc +{ + [_modelsToCheck release]; + + [super dealloc]; +} + + ++ (NSString *)nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier +{ + OOModelVerifierStage *stage = [verifier stageWithName:kStageName]; + if (stage == nil) + { + stage = [[OOModelVerifierStage alloc] init]; + [verifier registerStage:stage]; + [stage release]; + } + + return kStageName; +} + + +- (NSString *)name +{ + return kStageName; +} + + +- (BOOL)shouldRun +{ + return [_modelsToCheck count] != 0; +} + + +- (void)run +{ + NSEnumerator *nameEnum = nil; + NSDictionary *info = nil; + NSAutoreleasePool *pool = nil; + NSString *name = nil, + *context = nil; + NSDictionary *materials = nil, + *shaders = nil; + + OOLog(@"verifyOXP.models.unimplemented", @"TODO: implement model verifier."); + + for (nameEnum = [_modelsToCheck objectEnumerator]; (info = [nameEnum nextObject]); ) + { + pool = [[NSAutoreleasePool alloc] init]; + + name = [info objectForKey:@"name"]; + context = [info objectForKey:@"context"]; + if (context == NSNULL) context = nil; + materials = [info objectForKey:@"materials"]; + if (materials == NSNULL) materials = nil; + shaders = [info objectForKey:@"shaders"]; + if (shaders == NSNULL) shaders = nil; + + [self checkModel:name + context:context + materials:materials + shaders:shaders]; + + [pool release]; + } + [_modelsToCheck release]; + _modelsToCheck = nil; +} + + +- (BOOL) modelNamed:(NSString *)name + usedForEntry:(NSString *)entryName + inFile:(NSString *)fileName + withMaterials:(NSDictionary *)materials + andShaders:(NSDictionary *)shaders +{ + OOFileScannerVerifierStage *fileScanner = nil; + NSDictionary *info = nil; + NSString *context = nil; + + if (name == nil) return NO; + + if (entryName != nil) context = [NSString stringWithFormat:@"entry \"%@\" of %@", entryName, fileName]; + else context = fileName; + + fileScanner = [[self verifier] fileScannerStage]; + if (![fileScanner fileExists:name + inFolder:@"Models" + referencedFrom:context + checkBuiltIn:YES]) + { + return NO; + } + + if (context == nil) context = NSNULL; + if (materials == nil) materials = NSNULL; + if (shaders == nil) shaders = NSNULL; + + info = [NSDictionary dictionaryWithObjectsAndKeys: + name, @"name", + context, @"context", + materials, @"materials", + shaders, @"shaders", + nil]; + + [_modelsToCheck addObject:info]; + + return YES; +} + +@end + + +@implementation OOModelVerifierStage (OOPrivate) + + +- (void)checkModel:(NSString *)name + context:(NSString *)context + materials:(NSDictionary *)materials + shaders:(NSDictionary *)shaders +{ + OOLog(@"verifyOXP.verbose.model.unimp", @"- Pretending to verify model %@ referenced in %@.", name, context); + // FIXME: this should check DAT files. +} + +@end + + +@implementation OOOXPVerifier(OOModelVerifierStage) + +- (OOModelVerifierStage *)modelVerifierStage +{ + return [self stageWithName:kStageName]; +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOOXPVerifier.h b/src/Core/OXPVerifier/OOOXPVerifier.h new file mode 100644 index 00000000..b61e5a1a --- /dev/null +++ b/src/Core/OXPVerifier/OOOXPVerifier.h @@ -0,0 +1,113 @@ +/* + +OOOXPVerifier.h + +Oolite expansion pack verification manager. + +NOTE: the overall design is discussed in OXP verifier design.txt. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#ifndef OO_OXP_VERIFIER_ENABLED + #ifdef NDEBUG + #define OO_OXP_VERIFIER_ENABLED 0 + #else + #define OO_OXP_VERIFIER_ENABLED 1 + #endif +#endif + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOCocoa.h" + +@class OOOXPVerifierStage; + + +@interface OOOXPVerifier: NSObject +{ + NSDictionary *_verifierPList; + + NSString *_basePath; + NSString *_displayName; + + NSMutableDictionary *_stagesByName; + NSMutableSet *_waitingStages; + + BOOL _openForRegistration; +} + +/* Look for command-line arguments requesting OXP verification. If any are + found, run the verification and return YES. Otherwise, return NO. + + At the moment, only one OXP may be verified per run; additional requests + are ignored. +*/ ++ (BOOL)runVerificationIfRequested; + + +/* Stage registration. Currently, stages are registered by OOOXPVerifier + itself. Stages may also register other stages - substages, as it were - + in their -initWithVerifier: methods, or when -dependencies or + -dependents are called. Registration at later points is not permitted. +*/ +- (void)registerStage:(OOOXPVerifierStage *)stage; + + +// All other methods are for use by verifier stages. +- (NSString *)oxpPath; +- (NSString *)oxpDisplayName; + +- (id)stageWithName:(NSString *)name; + +// Read from verifyOXP.plist +- (id)configurationValueForKey:(NSString *)key; +- (NSArray *)configurationArrayForKey:(NSString *)key; +- (NSDictionary *)configurationDictionaryForKey:(NSString *)key; +- (NSString *)configurationStringForKey:(NSString *)key; +- (NSSet *)configurationSetForKey:(NSString *)key; + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOOXPVerifier.m b/src/Core/OXPVerifier/OOOXPVerifier.m new file mode 100644 index 00000000..489e0401 --- /dev/null +++ b/src/Core/OXPVerifier/OOOXPVerifier.m @@ -0,0 +1,745 @@ +/* + +OOOXPVerifier.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +/* Design notes: + see "verifier design.txt". +*/ + +#import "OOOXPVerifier.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOOXPVerifierStageInternal.h" +#import "OOLoggingExtended.h" +#import "ResourceManager.h" +#import "OOCollectionExtractors.h" +#import "GameController.h" +#import "OOCacheManager.h" + + +static void SwitchLogFile(NSString *name); +static void NoteVerificationStage(NSString *displayName, NSString *stage); + +#if OOLITE_MAC_OS_X +static void OpenLogFile(NSString *name); +#else +#define OpenLogFile(name) do {} while (0) +#endif + + +@interface OOOXPVerifier (OOPrivate) + +- (id)initWithPath:(NSString *)path; +- (void)run; + +- (void)setUpLogOverrides; + +- (void)registerBaseStages; +- (void)buildDependencyGraph; +- (void)runStages; + +- (BOOL)setUpDependencies:(NSSet *)dependencies + forStage:(OOOXPVerifierStage *)stage; + +- (void)setUpDependents:(NSSet *)dependents + forStage:(OOOXPVerifierStage *)stage; + +- (void)dumpDebugGraphviz; + +@end + + +@implementation OOOXPVerifier + ++ (BOOL)runVerificationIfRequested +{ + NSArray *arguments = nil; + NSEnumerator *argEnum = nil; + NSString *arg = nil; + NSString *foundPath = nil; + BOOL exists, isDirectory; + OOOXPVerifier *verifier = nil; + NSAutoreleasePool *pool = nil; + + pool = [[NSAutoreleasePool alloc] init]; + + arguments = [[NSProcessInfo processInfo] arguments]; + + // Scan for -verify-oxp or --verify-oxp followed by relative path + for (argEnum = [arguments objectEnumerator]; (arg = [argEnum nextObject]); ) + { + if ([arg isEqual:@"-verify-oxp"] || [arg isEqual:@"--verify-oxp"]) + { + foundPath = [argEnum nextObject]; + if (foundPath == nil) + { + OOLog(@"verifyOXP.noPath", @"***** ERROR: %@ passed without path argument; nothing to verify.", arg); + [pool release]; + return YES; + } + foundPath = [foundPath stringByExpandingTildeInPath]; + break; + } + } + + if (foundPath == nil) + { + [pool release]; + return NO; + } + + // We got a path; does it point to a directory? + exists = [[NSFileManager defaultManager] fileExistsAtPath:foundPath isDirectory:&isDirectory]; + if (!exists) + { + OOLog(@"verifyOXP.badPath", @"***** ERROR: no OXP exists at path \"%@\"; nothing to verify.", foundPath); + } + else if (!isDirectory) + { + OOLog(@"verifyOXP.badPath", @"***** ERROR: path \"%@\" refers to a file, not an OXP directory; nothing to verify.", foundPath); + } + else + { + verifier = [[OOOXPVerifier alloc] initWithPath:foundPath]; + [pool release]; + pool = [[NSAutoreleasePool alloc] init]; + [verifier run]; + [verifier release]; + } + [pool release]; + + // Whether or not we got a valid path, -verify-oxp was passed. + return YES; +} + + +- (void)dealloc +{ + [_verifierPList release]; + [_basePath release]; + [_displayName release]; + [_stagesByName release]; + [_waitingStages release]; + + [super dealloc]; +} + + +- (void)registerStage:(OOOXPVerifierStage *)stage +{ + NSString *name = nil; + OOOXPVerifierStage *existing = nil; + + // Sanity checking + if (stage == nil) return; + + if (![stage isKindOfClass:[OOOXPVerifierStage class]]) + { + OOLog(@"verifyOXP.registration.failed", @"Attempt to register class %@ as a verifier stage, but it is not a subclass of OOOXPVerifierStage; ignoring.", [stage class]); + return; + } + + if (!_openForRegistration) + { + OOLog(@"verifyOXP.registration.failed", @"Attempt to register verifier stage %@ after registration closed, ignoring.", stage); + return; + } + + name = [stage name]; + if (name == nil) + { + OOLog(@"verifyOXP.registration.failed", @"Attempt to register verifier stage %@ with nil name, ignoring.", stage); + return; + } + + // We can only have one stage with a given name. Registering the same stage twice is OK, though. + existing = [_stagesByName objectForKey:name]; + if (existing == stage) return; + if (existing != nil) + { + OOLog(@"verifyOXP.registration.failed", @"Attempt to register verifier stage %@ with same name as stage %@, ignoring.", stage, existing); + return; + } + + // Checks passed, store state. + [stage setVerifier:self]; + [_stagesByName setObject:stage forKey:name]; + [_waitingStages addObject:stage]; +} + + +- (NSString *)oxpPath +{ + return [[_basePath retain] autorelease]; +} + + +- (NSString *)oxpDisplayName +{ + return [[_displayName retain] autorelease]; +} + + +- (id)stageWithName:(NSString *)name +{ + if (name == nil) return nil; + + return [_stagesByName objectForKey:name]; +} + + +- (id)configurationValueForKey:(NSString *)key +{ + return [_verifierPList objectForKey:key]; +} + + +- (NSArray *)configurationArrayForKey:(NSString *)key +{ + return [_verifierPList arrayForKey:key]; +} + + +- (NSDictionary *)configurationDictionaryForKey:(NSString *)key +{ + return [_verifierPList dictionaryForKey:key]; +} + + +- (NSString *)configurationStringForKey:(NSString *)key +{ + return [_verifierPList stringForKey:key]; +} + + +- (NSSet *)configurationSetForKey:(NSString *)key +{ + NSArray *array = [_verifierPList arrayForKey:key]; + return array != nil ? [NSSet setWithArray:array] : nil; +} + +@end + + +@implementation OOOXPVerifier (OOPrivate) + +- (id)initWithPath:(NSString *)path +{ + self = [super init]; + + NSString *verifierPListPath = [[[ResourceManager builtInPath] stringByAppendingPathComponent:@"Config"] stringByAppendingPathComponent:@"verifyOXP.plist"]; + _verifierPList = [[NSDictionary dictionaryWithContentsOfFile:verifierPListPath] retain]; + + _basePath = [path copy]; + _displayName = [[NSFileManager defaultManager] displayNameAtPath:_basePath]; + if (_displayName == nil) _displayName = [_basePath lastPathComponent]; + [_displayName retain]; + + _stagesByName = [[NSMutableDictionary alloc] init]; + _waitingStages = [[NSMutableSet alloc] init]; + + if (_verifierPList == nil || + _basePath == nil) + { + OOLog(@"verifyOXP.setup.failed", @"***** ERROR: failed to set up OXP verifier."); + [self release]; + return nil; + } + + _openForRegistration = YES; + + return self; +} + + +- (void)run +{ + NoteVerificationStage(_displayName, @""); + + [self setUpLogOverrides]; + + /* We need to be able to look up internal files, but not other OXP files. + To do this without clobbering the disk cache, we disable cache writes. + */ + [[OOCacheManager sharedCache] flush]; + [[OOCacheManager sharedCache] setAllowCacheWrites:NO]; + [ResourceManager setUseAddOns:NO]; + + SwitchLogFile(_displayName); + OOLog(@"verifyOXP.start", @"Running OXP verifier for %@", _basePath);//_displayName); + + [self registerBaseStages]; + [self buildDependencyGraph]; + [self runStages]; + + NoteVerificationStage(_displayName, @""); + OOLog(@"verifyOXP.done", @"OXP verification complete."); + + OpenLogFile(_displayName); +} + + +- (void)setUpLogOverrides +{ + NSDictionary *overrides = nil; + NSEnumerator *messageClassEnum = nil; + NSString *messageClass = nil; + id verbose = nil; + + OOLogSetShowMessageClassTemporary([_verifierPList boolForKey:@"logShowMessageClassOverride" defaultValue:NO]); + + overrides = [_verifierPList dictionaryForKey:@"logControlOverride"]; + for (messageClassEnum = [overrides keyEnumerator]; (messageClass = [messageClassEnum nextObject]); ) + { + OOLogSetDisplayMessagesInClass(messageClass, [overrides boolForKey:messageClass defaultValue:NO]); + } + + /* Since actually editing logControlOverride is a pain, we also allow + overriding verifyOXP.verbose through user defaults. This is at least + as much a pain under GNUstep, but very convenient under OS X. + */ + verbose = [[NSUserDefaults standardUserDefaults] objectForKey:@"oxp-verifier-verbose-logging"]; + if (verbose != nil) OOLogSetDisplayMessagesInClass(@"verifyOXP.verbose", OOBooleanFromObject(verbose, NO)); +} + + +- (void)registerBaseStages +{ + NSAutoreleasePool *pool = nil; + NSSet *stages = nil; + NSSet *excludeStages = nil; + NSEnumerator *stageEnum = nil; + NSString *stageName = nil; + Class stageClass = Nil; + OOOXPVerifierStage *stage = nil; + + pool = [[NSAutoreleasePool alloc] init]; + + // Load stages specified as array of class names in verifyOXP.plist + stages = [self configurationSetForKey:@"stages"]; + excludeStages = [self configurationSetForKey:@"excludeStages"]; + if ([excludeStages count] != 0) + { + stages = [[stages mutableCopy] autorelease]; + [(NSMutableSet *)stages minusSet:excludeStages]; + } + for (stageEnum = [stages objectEnumerator]; (stageName = [stageEnum nextObject]); ) + { + if ([stageName isKindOfClass:[NSString class]]) + { + stageClass = NSClassFromString(stageName); + if (stageClass == Nil) + { + OOLog(@"verifyOXP.registration.failed", @"Attempt to register unknown class %@ as a verifier stage, ignoring.", stageName); + continue; + } + stage = [[stageClass alloc] init]; + [self registerStage:stage]; + [stage release]; + } + } + + [pool release]; +} + + +- (void)buildDependencyGraph +{ + NSAutoreleasePool *pool = nil; + NSArray *stageKeys = nil; + NSEnumerator *stageEnum = nil; + NSString *stageKey = nil; + OOOXPVerifierStage *stage = nil; + NSString *name = nil; + NSMutableDictionary *dependenciesByStage = nil, + *dependentsByStage = nil; + NSSet *dependencies = nil, + *dependents = nil; + NSValue *key = nil; + + pool = [[NSAutoreleasePool alloc] init]; + + /* Iterate over all stages, getting dependency and dependent sets. + This is done in advance so that -dependencies and -dependents may + register stages. + */ + dependenciesByStage = [NSMutableDictionary dictionary]; + dependentsByStage = [NSMutableDictionary dictionary]; + + for (;;) + { + /* Loop while there are stages whose dependency lists haven't been + checked. This is an indeterminate loop since new ones can be + added. + */ + stage = [_waitingStages anyObject]; + if (stage == nil) break; + [_waitingStages removeObject:stage]; + + key = [NSValue valueWithNonretainedObject:stage]; + + dependencies = [stage dependencies]; + if (dependencies != nil) + { + [dependenciesByStage setObject:dependencies + forKey:key]; + } + + dependents = [stage dependents]; + if (dependents != nil) + { + [dependentsByStage setObject:dependents + forKey:key]; + } + } + [_waitingStages release]; + _waitingStages = nil; + _openForRegistration = NO; + + // Iterate over all stages, resolving dependencies. + stageKeys = [_stagesByName allKeys]; // Get the keys up front because we may need to remove entries from dictionary. + + for (stageEnum = [stageKeys objectEnumerator]; (stageKey = [stageEnum nextObject]); ) + { + stage = [_stagesByName objectForKey:stageKey]; + if (stage == nil) continue; + + // Sanity check + name = [stage name]; + if (![stageKey isEqualToString:name]) + { + OOLog(@"verifyOXP.buildDependencyGraph.badName", @"***** Stage name appears to have changed from \"%@\" to \"%@\" for verifier stage %@, removing.", stageKey, name, stage); + [_stagesByName removeObjectForKey:stageKey]; + continue; + } + + // Get dependency set + key = [NSValue valueWithNonretainedObject:stage]; + dependencies = [dependenciesByStage objectForKey:key]; + + if (dependencies != nil && ![self setUpDependencies:dependencies forStage:stage]) + { + [_stagesByName removeObjectForKey:stageKey]; + } + } + + /* Iterate over all stages again, resolving reverse dependencies. + This is done in a separate pass because reverse dependencies are "weak" + while forward dependencies are "strong". + */ + stageKeys = [_stagesByName allKeys]; + + for (stageEnum = [stageKeys objectEnumerator]; (stageKey = [stageEnum nextObject]); ) + { + stage = [_stagesByName objectForKey:stageKey]; + if (stage == nil) continue; + + // Get dependent set + key = [NSValue valueWithNonretainedObject:stage]; + dependents = [dependentsByStage objectForKey:key]; + + if (dependents != nil) + { + [self setUpDependents:dependents forStage:stage]; + } + } + + _waitingStages = [[NSMutableSet alloc] initWithArray:[_stagesByName allValues]]; + [_waitingStages makeObjectsPerformSelector:@selector(dependencyRegistrationComplete)]; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"oxp-verifier-dump-debug-graphviz"]) + { + [self dumpDebugGraphviz]; + } + + [pool release]; +} + + +- (void)runStages +{ + NSAutoreleasePool *pool = nil; + NSEnumerator *stageEnum = nil; + OOOXPVerifierStage *candidateStage = nil, + *stageToRun = nil; + NSString *stageName = nil; + + // Loop while there are still stages to run. + for (;;) + { + pool = [[NSAutoreleasePool alloc] init]; + + // Look through queue for a stage that's ready + stageToRun = nil; + for (stageEnum = [_waitingStages objectEnumerator]; (candidateStage = [stageEnum nextObject]); ) + { + if ([candidateStage canRun]) + { + stageToRun = candidateStage; + break; + } + } + if (stageToRun == nil) + { + // No more runnable stages + [pool release]; + break; + } + + stageName = nil; + OOLogPushIndent(); + NS_DURING + stageName = [stageToRun name]; + if ([stageToRun shouldRun]) + { + NoteVerificationStage(_displayName, stageName); + OOLog(@"verifyOXP.runStage", @"%@", stageName); + OOLogIndent(); + [stageToRun performRun]; + } + else + { + OOLog(@"verifyOXP.verbose.skipStage", @"- Skipping stage: %@ (nothing to do).", stageName); + [stageToRun noteSkipped]; + } + NS_HANDLER + if (stageName == nil) stageName = [[stageToRun class] description]; + OOLog(@"verifyOXP.exception", @"***** Exception occurred when running OXP verifier stage \"%@\": %@: %@", stageName, [localException name], [localException reason]); + NS_ENDHANDLER + OOLogPopIndent(); + + [_waitingStages removeObject:stageToRun]; + [pool release]; + } + + pool = [[NSAutoreleasePool alloc] init]; + + if ([_waitingStages count] != 0) + { + OOLog(@"verifyOXP.incomplete", @"Some verifier stages could not be run:"); + OOLogIndent(); + for (stageEnum = [_waitingStages objectEnumerator]; (candidateStage = [stageEnum nextObject]); ) + { + OOLog(@"verifyOXP.incomplete.item", @"%@", candidateStage); + } + OOLogOutdent(); + } + [_waitingStages release]; + _waitingStages = nil; + + [pool release]; +} + + +- (BOOL)setUpDependencies:(NSSet *)dependencies + forStage:(OOOXPVerifierStage *)stage +{ + NSString *depName = nil; + NSEnumerator *depEnum = nil; + OOOXPVerifierStage *depStage = nil; + + // Iterate over dependencies, connecting them up. + for (depEnum = [dependencies objectEnumerator]; (depName = [depEnum nextObject]); ) + { + depStage = [_stagesByName objectForKey:depName]; + if (depStage == nil) + { + OOLog(@"verifyOXP.buildDependencyGraph.unresolved", @"Verifier stage %@ has unresolved dependency \"%@\", skipping.", stage, depName); + return NO; + } + + if ([depStage isDependentOf:stage]) + { + OOLog(@"verifyOXP.buildDependencyGraph.circularReference", @"Verifier stages %@ and %@ have a dependency loop, skipping.", stage, depStage); + [_stagesByName removeObjectForKey:depName]; + return NO; + } + + [stage registerDependency:depStage]; + } + + return YES; +} + + +- (void)setUpDependents:(NSSet *)dependents + forStage:(OOOXPVerifierStage *)stage +{ + NSString *depName = nil; + NSEnumerator *depEnum = nil; + OOOXPVerifierStage *depStage = nil; + + // Iterate over dependents, connecting them up. + for (depEnum = [dependents objectEnumerator]; (depName = [depEnum nextObject]); ) + { + depStage = [_stagesByName objectForKey:depName]; + if (depStage == nil) + { + OOLog(@"verifyOXP.buildDependencyGraph.unresolved", @"Verifier stage %@ has unresolved dependent \"%@\".", stage, depName); + continue; // Unresolved/conflicting dependents are non-fatal + } + + if ([stage isDependentOf:depStage]) + { + OOLog(@"verifyOXP.buildDependencyGraph.circularReference", @"Verifier stage %@ lists %@ as both dependent and dependency (possibly indirectly); will execute %@ after %@.", stage, depStage, stage, depStage); + continue; + } + + [depStage registerDependency:stage]; + } +} + + +- (void)dumpDebugGraphviz +{ + NSMutableString *graphViz = nil; + NSDictionary *graphVizTemplate = nil; + NSString *template = nil, + *startTemplate = nil, + *endTemplate = nil; + NSEnumerator *stageEnum = nil; + OOOXPVerifierStage *stage = nil; + NSSet *deps = nil; + NSEnumerator *depEnum = nil; + OOOXPVerifierStage *dep = nil; + + graphVizTemplate = [self configurationDictionaryForKey:@"debugGraphvizTempate"]; + graphViz = [NSMutableString stringWithFormat:[graphVizTemplate stringForKey:@"preamble"], [NSDate date]]; + + /* Pass 1: enumerate over graph setting node attributes for each stage. + We use pointers as node names for simplicity of generation. + */ + template = [graphVizTemplate stringForKey:@"node"]; + for (stageEnum = [_stagesByName objectEnumerator]; (stage = [stageEnum nextObject]); ) + { + [graphViz appendFormat:template, stage, [stage class], [stage name]]; + } + + [graphViz appendString:[graphVizTemplate stringForKey:@"forwardPreamble"]]; + + /* Pass 2: enumerate over graph setting forward arcs for each dependency. + */ + template = [graphVizTemplate stringForKey:@"forwardArc"]; + startTemplate = [graphVizTemplate stringForKey:@"startArc"]; + for (stageEnum = [_stagesByName objectEnumerator]; (stage = [stageEnum nextObject]); ) + { + deps = [stage resolvedDependencies]; + if ([deps count] != 0) + { + for (depEnum = [deps objectEnumerator]; (dep = [depEnum nextObject]); ) + { + [graphViz appendFormat:template, dep, stage]; + } + } + else + { + [graphViz appendFormat:startTemplate, stage]; + } + } + + [graphViz appendString:[graphVizTemplate stringForKey:@"backwardPreamble"]]; + + /* Pass 3: enumerate over graph setting backward arcs for each dependent. + */ + template = [graphVizTemplate stringForKey:@"backwardArc"]; + endTemplate = [graphVizTemplate stringForKey:@"endArc"]; + for (stageEnum = [_stagesByName objectEnumerator]; (stage = [stageEnum nextObject]); ) + { + deps = [stage resolvedDependents]; + if ([deps count] != 0) + { + for (depEnum = [deps objectEnumerator]; (dep = [depEnum nextObject]); ) + { + [graphViz appendFormat:template, dep, stage]; + } + } + else + { + [graphViz appendFormat:endTemplate, stage]; + } + } + + [graphViz appendString:[graphVizTemplate stringForKey:@"postamble"]]; + + // Write file + [ResourceManager writeDiagnosticData:[graphViz dataUsingEncoding:NSUTF8StringEncoding] toFileNamed:@"OXPVerifierStageDependencies.dot"]; +} + +@end + + +#import "OOLogOutputHandler.h" + +static void SwitchLogFile(NSString *name) +{ + name = [name stringByAppendingPathExtension:@"log"]; + OOLog(@"verifyOXP.switchingLog", @"Switching log files -- logging to \"%@\".", name); + OOLogOutputHandlerChangeLogFile(name); +} + + +static void NoteVerificationStage(NSString *displayName, NSString *stage) +{ + [[GameController sharedController] logProgress:[NSString stringWithFormat:@"Verifying %@\n%@", displayName, stage]]; +} + + +#if OOLITE_MAC_OS_X + +static void OpenLogFile(NSString *name) +{ + // Open log file in appropriate application. + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"oxp-verifier-open-log" defaultValue:YES]) + { + [[NSWorkspace sharedWorkspace] openFile:OOLogHandlerGetLogPath()]; + } +} + +#endif +#endif // OO_OXP_VERIFIER_ENABLED diff --git a/src/Core/OXPVerifier/OOOXPVerifierStage.h b/src/Core/OXPVerifier/OOOXPVerifierStage.h new file mode 100644 index 00000000..b2781793 --- /dev/null +++ b/src/Core/OXPVerifier/OOOXPVerifierStage.h @@ -0,0 +1,103 @@ +/* + +OOOXPVerifierStage.h + +Pipeline stage for OXP verification pipeline managed by OOOXPVerifier. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOOXPVerifier.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOOXPVerifierStage: NSObject +{ +@private + OOOXPVerifier *_verifier; + NSMutableSet *_dependencies; + NSMutableSet *_incompleteDependencies; + NSMutableSet *_dependents; + BOOL _canRun, _hasRun; +} + +- (OOOXPVerifier *)verifier; +- (BOOL)completed; + +// Subclass responsibilities: + +/* Name of stage. Used for display and for dependency resolution; must be + unique. The name should be a phrase describing what will be done, like + "Scanning files" or "Verifying plist scripts". +*/ +- (NSString *)name; + +/* Dependencies and dependents: + -dependencies returns a set of names of stages that must be run before this + one. If it contains the name of a stage that's not registered, this stage + cannot run. + -dependents returns a set of names of stages that should not be run before + this one. Unlike -dependencies, these are considered non-critical. +*/ +- (NSSet *)dependencies; +- (NSSet *)dependents; + +/* This is called once by the verifier. + When it is called, all the verifier stages listed in -requiredStages will + have run. At this point, it is possible to access them using the + verifier's -stageWithName: method in order to query them about results. + Stages whose dependencies have all run will be released, so the result of + calling -stageWithName: with a name not in -requiredStages is undefined. + + shouldRun can be overridden to avoid running at all (without anything + being logged). For dependency resolution purposes, returning NO from + shouldRun counts as running; that is, it will stop this verifier stage + from running but will not stop dependencies from running. +*/ +- (BOOL)shouldRun; +- (void)run; + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOOXPVerifierStage.m b/src/Core/OXPVerifier/OOOXPVerifierStage.m new file mode 100644 index 00000000..b88c4931 --- /dev/null +++ b/src/Core/OXPVerifier/OOOXPVerifierStage.m @@ -0,0 +1,249 @@ +/* + +OOOXPVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOOXPVerifierStageInternal.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOOXPVerifierStage (OOPrivate) + +- (void)registerDepedent:(OOOXPVerifierStage *)dependent; +- (void)dependencyCompleted:(OOOXPVerifierStage *)dependency; + +@end + + +@implementation OOOXPVerifierStage + +- (id)init +{ + self = [super init]; + + if (self != nil) + { + _dependencies = [[NSMutableSet alloc] init]; + _incompleteDependencies = [[NSMutableSet alloc] init]; + _dependents = [[NSMutableSet alloc] init]; + _canRun = NO; + } + + return self; +} + + +- (void)dealloc +{ + [_dependencies release]; + [_incompleteDependencies release]; + [_dependents release]; + + [super dealloc]; +} + + +- (id)description +{ + return [NSString stringWithFormat:@"<%@ %p>{\"%@\"}", [self class], self, [self name]]; +} + + +- (OOOXPVerifier *)verifier +{ + return [[_verifier retain] autorelease]; +} + + +- (BOOL)completed +{ + return _hasRun; +} + + +- (NSString *)name +{ + OOLogGenericSubclassResponsibility(); + return nil; +} + + +- (NSSet *)dependencies +{ + return nil; +} + + +- (NSSet *)dependents +{ + return nil; +} + + +- (BOOL)shouldRun +{ + return YES; +} + + +- (void)run +{ + OOLogGenericSubclassResponsibility(); +} + +@end + + +@implementation OOOXPVerifierStage (OOInternal) + +- (void)setVerifier:(OOOXPVerifier *)verifier +{ + _verifier = verifier; // Not retained. +} + + +- (BOOL)isDependentOf:(OOOXPVerifierStage *)stage +{ + NSEnumerator *directDepEnum = nil; + OOOXPVerifierStage *directDep = nil; + + if (stage == nil) return NO; + + // Direct dependency check. + if ([_dependencies containsObject:stage]) return YES; + + // Recursive dependency check. + for (directDepEnum = [_dependencies objectEnumerator]; (directDep = [directDepEnum nextObject]); ) + { + if ([directDep isDependentOf:stage]) return YES; + } + + return NO; +} + + +- (void)registerDependency:(OOOXPVerifierStage *)dependency +{ + [_dependencies addObject:dependency]; + [_incompleteDependencies addObject:dependency]; + + [dependency registerDepedent:self]; +} + + +- (BOOL)canRun +{ + return _canRun; +} + + +- (void)performRun +{ + assert(_canRun && !_hasRun); + + OOLogPushIndent(); + NS_DURING + [self run]; + NS_HANDLER + OOLog(@"verifyOXP.exception", @"***** Exception while running verification stage \"%@\": %@", [self name], localException); + NS_ENDHANDLER + OOLogPopIndent(); + + _hasRun = YES; + _canRun = NO; + [_dependents makeObjectsPerformSelector:@selector(dependencyCompleted:) withObject:self]; +} + + +- (void)noteSkipped +{ + assert(_canRun && !_hasRun); + + _hasRun = YES; + _canRun = NO; + [_dependents makeObjectsPerformSelector:@selector(dependencyCompleted:) withObject:self]; +} + + +- (void)dependencyRegistrationComplete +{ + _canRun = [_incompleteDependencies count] == 0; +} + + +- (NSSet *)resolvedDependencies +{ + return _dependencies; +} + + +- (NSSet *)resolvedDependents +{ + return _dependents; +} + +@end + + +@implementation OOOXPVerifierStage (OOPrivate) + +- (void)registerDepedent:(OOOXPVerifierStage *)dependent +{ + assert(![self isDependentOf:dependent]); + + [_dependents addObject:dependent]; +} + + +- (void)dependencyCompleted:(OOOXPVerifierStage *)dependency +{ + [_incompleteDependencies removeObject:dependency]; + if ([_incompleteDependencies count] == 0) _canRun = YES; +} + +@end + +#endif //OO_OXP_VERIFIER_ENABLED diff --git a/src/Core/OXPVerifier/OOOXPVerifierStageInternal.h b/src/Core/OXPVerifier/OOOXPVerifierStageInternal.h new file mode 100644 index 00000000..8280265e --- /dev/null +++ b/src/Core/OXPVerifier/OOOXPVerifierStageInternal.h @@ -0,0 +1,73 @@ +/* + +OOOXPVerifierStageInternal.h + +Private interface between OOOXPVerifierStage and OOOXPVerifier. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2007 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOOXPVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOOXPVerifierStage (OOInternal) + +- (void)setVerifier:(OOOXPVerifier *)verifier; +- (BOOL)isDependentOf:(OOOXPVerifierStage *)stage; +- (void)registerDependency:(OOOXPVerifierStage *)dependency; +- (void)dependencyRegistrationComplete; + +- (BOOL)canRun; + +- (void)performRun; +- (void)noteSkipped; + +// These return sets of stages set up by -registerDependency, wheras -dependencies/dependents return sets of names. +- (NSSet *)resolvedDependencies; +- (NSSet *)resolvedDependents; + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOPListSchemaVerifier.h b/src/Core/OXPVerifier/OOPListSchemaVerifier.h new file mode 100644 index 00000000..3adece69 --- /dev/null +++ b/src/Core/OXPVerifier/OOPListSchemaVerifier.h @@ -0,0 +1,190 @@ +/* + + OOPListSchemaVerifier.h + + Utility class to verify the structure of a property list based on a schema + (which is itself a property list). + + + Oolite + Copyright (C) 2004-2008 Giles C Williams and contributors + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + MA 02110-1301, USA. + + + This file may also be distributed under the MIT/X11 license: + + Copyright (C) 2007 Jens Ayton + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ + +#import "OOOXPVerifier.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import +#import "OOFunctionAttributes.h" + + +@interface OOPListSchemaVerifier: NSObject +{ + NSDictionary *_schema; + NSDictionary *_definitions; + + id _delegate; + uint32_t _badDelegateWarning: 1; +} + ++ (id)verifierWithSchema:(NSDictionary *)schema; +- (id)initWithSchema:(NSDictionary *)schema; + +- (void)setDelegate:(id)delegate; +- (id)delegate; + +- (BOOL)verifyPropertyList:(id)plist named:(NSString *)name; + +/* Convert a key path (such as provided to the delegate method + -verifier:withPropertyList:failedForProperty:atPath:expectedType:) to a + human-readable string. Strings are separated by dots and numbers are give + brackets. For instance, the key path ( "adder-player", "custom_views", 0, + "view_description" ) is transfomed to + "adder-player.custom_views[0].view_description". +*/ ++ (NSString *)descriptionForKeyPath:(NSArray *)keyPath; + +@end + + +@interface NSObject (OOPListSchemaVerifierDelegate) + +// Handle "delegated types". Return YES for valid, NO for invalid. +- (BOOL)verifier:(OOPListSchemaVerifier *)verifier +withPropertyList:(id)rootPList + named:(NSString *)name + testProperty:(id)subPList + atPath:(NSArray *)keyPath + againstType:(NSString *)typeKey + error:(NSError **)outError; + +/* Method notifying of verification failure. + Return YES to continue verifying, NO to stop. +*/ +- (BOOL)verifier:(OOPListSchemaVerifier *)verifier +withPropertyList:(id)rootPList + named:(NSString *)name + failedForProperty:(id)subPList + withError:(NSError *)error + expectedType:(NSDictionary *)localSchema; + +@end + + +// NSError domain and codes used to report schema verifier errors. +extern NSString * const kOOPListSchemaVerifierErrorDomain; + +extern NSString * const kPListKeyPathErrorKey; // Array specifying key path. + +extern NSString * const kExpectedClassErrorKey; // Expected class. Nil for vector and quaternion. +extern NSString * const kExpectedClassNameErrorKey; // String describing expected class. May be more specific (for instance, "boolean" or "positive integer" for NSNumber). +extern NSString * const kUnknownKeyErrorKey; // Unallowed key found in dictionary. +extern NSString * const kMissingRequiredKeysErrorKey; // Set of required keys not present in dictionary +extern NSString * const kMissingSubStringErrorKey; // String or array of strings not found for kPListErrorStringPrefixMissing/kPListErrorStringSuffixMissing/kPListErrorStringSubstringMissing. +extern NSString * const kUnnownFilterErrorKey; // Unrecognized filter specifier for kPListErrorSchemaUnknownFilter. Not specified if filter is not a string. +extern NSString * const kErrorsByOptionErrorKey; // Dictionary of errors for oneOf types. + +extern NSString * const kUnknownTypeErrorKey; // Set for kPListErrorSchemaUnknownType. +extern NSString * const kUndefinedMacroErrorKey; // Set for kPListErrorSchemaUndefiniedMacroReference. + + +// All plist verifier errors have a short error description in their -localizedDescription. Generally this is something that would be more suitable to -localizedFailureReason, but we need Mac OS X 10.3 compatibility. + +typedef enum +{ + kPListErrorNone, + kPListErrorInternal, // PList verifier did something dumb. + + // Verification errors -- property list doesn't match schema. + kPListErrorTypeMismatch, // Basic type mismatch -- array instead of number, for instance. + + kPListErrorMinimumConstraintNotMet, // minimum/minCount/minLength constraint violated + kPListErrorMaximumConstraintNotMet, // maximum/maxCount/maxLength constraint violated + kPListErrorNumberIsNegative, // Negative number in positiveFloat. + + kPListErrorStringPrefixMissing, // String does not match requiredPrefix rule. kMissingSubStringErrorKey is set. + kPListErrorStringSuffixMissing, // String does not match requiredSuffix rule. kMissingSubStringErrorKey is set. + kPListErrorStringSubstringMissing, // String does not match requiredSuffix rule. kMissingSubStringErrorKey is set. + + kPListErrorDictionaryUnknownKey, // Unknown key for dictionary with allowOthers = NO. + kPListErrorDictionaryMissingRequiredKeys, // requiredKeys rule is not fulfilled. The missing keys are listed in kMissingRequiredKeysErrorKey. + + kPListErrorEnumerationBadValue, // Enumeration type contains string that isn't in permitted set. + + kPListErrorOneOfNoMatch, // No match for oneOf type. kErrorsByOptionErrorKey is set to a dictionary of type specifiers to errors. Note that the keys in this dictionary can be either strings or dictionaries. + + kPListDelegatedTypeError, // Delegate's verification method failed. If it returned an error, this will be in NSUnderlyingErrorKey. + + // Schema errors -- schema is broken. + kPListErrorStartOfSchemaErrors = 100, + + kPListErrorSchemaBadTypeSpecifier, // Bad type specifier - specifier is not a string or a dictionary, or is a dictionary with no type key. kUndefinedMacroErrorKey is set. + kPListErrorSchemaUndefiniedMacroReference, // Reference to $macro not found in $definitions. + kPListErrorSchemaUnknownType, // Unknown type specified in type specifier. kUnknownTypeErrorKey is set. + kPListErrorSchemaNoOneOfOptions, // OneOf clause has no options array. + kPListErrorSchemaNoEnumerationValues, // Enumeration clause has no values array. + kPListErrorSchemaUnknownFilter, // Bad value for string/enumeration filter specifier. + kPListErrorSchemaBadComparator, // String comparision requirement value (requiredPrefix etc.) is not a string. + + kPListErrorLastErrorCode +} OOPListSchemaVerifierErrorCode; + + +OOINLINE BOOL OOPlistErrorIsSchemaError(OOPListSchemaVerifierErrorCode error) +{ + return kPListErrorStartOfSchemaErrors < error && error < kPListErrorLastErrorCode; +} + + +@interface NSError (OOPListSchemaVerifierConveniences) + +- (NSArray *)plistKeyPath; +- (NSString *)plistKeyPathDescription; // Result of calling +[OOPListSchemaVerifier descriptionForKeyPath:] on kPListKeyPathErrorKey. + +- (NSSet *)missingRequiredKeys; + +- (Class)expectedClass; +- (NSString *)expectedClassName; + +@end + +#endif // OO_OXP_VERIFIER_ENABLED diff --git a/src/Core/OXPVerifier/OOPListSchemaVerifier.m b/src/Core/OXPVerifier/OOPListSchemaVerifier.m new file mode 100644 index 00000000..b8e02604 --- /dev/null +++ b/src/Core/OXPVerifier/OOPListSchemaVerifier.m @@ -0,0 +1,1522 @@ +/* + + OOPListSchemaVerifier.m + + + Oolite + Copyright (C) 2004-2008 Giles C Williams and contributors + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + MA 02110-1301, USA. + + + This file may also be distributed under the MIT/X11 license: + + Copyright (C) 2007 Jens Ayton + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED ìAS ISî, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + */ + +#import "OOPListSchemaVerifier.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOLoggingExtended.h" +#import "OOCollectionExtractors.h" +#import "OOMaths.h" +#import + + +#define PLIST_VERIFIER_DEBUG_DUMP_ENABLED 1 + + +enum +{ + // Largest allowable number of characters for string included in error message. + kMaximumLengthForStringInErrorMessage = 100 +}; + + +// Internal error codes. +enum +{ + kStartOfPrivateErrorCodes = kPListErrorLastErrorCode, + + kPListErrorFailedAndErrorHasBeenReported +}; + + +#if PLIST_VERIFIER_DEBUG_DUMP_ENABLED +static BOOL sDebugDump = NO; + +#define DebugDumpIndent() do { if (sDebugDump) OOLogIndent(); } while (0) +#define DebugDumpOutdent() do { if (sDebugDump) OOLogOutdent(); } while (0) +#define DebugDumpPushIndent() do { if (sDebugDump) OOLogPushIndent(); } while (0) +#define DebugDumpPopIndent() do { if (sDebugDump) OOLogPopIndent(); } while (0) +#define DebugDump(...) do { if (sDebugDump) OOLog(@"verifyOXP.verbose.plistDebugDump", __VA_ARGS__); } while (0) +#else +#define DebugDumpIndent() do { } while (0) +#define DebugDumpOutdent() do { } while (0) +#define DebugDumpPushIndent() do { } while (0) +#define DebugDumpPopIndent() do { } while (0) +#define DebugDump(...) do { } while (0) +#endif + + +NSString * const kOOPListSchemaVerifierErrorDomain = @"org.aegidian.oolite.OOPListSchemaVerifier.ErrorDomain"; + +NSString * const kPListKeyPathErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier plist key path"; +NSString * const kSchemaKeyPathErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier schema key path"; + +NSString * const kExpectedClassErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier expected class"; +NSString * const kExpectedClassNameErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier expected class name"; +NSString * const kUnknownKeyErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier unknown key"; +NSString * const kMissingRequiredKeysErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier missing required keys"; +NSString * const kMissingSubStringErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier missing substring"; +NSString * const kUnnownFilterErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier unknown filter"; +NSString * const kErrorsByOptionErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier errors by option"; + +NSString * const kUnknownTypeErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier unknown type"; +NSString * const kUndefinedMacroErrorKey = @"org.aegidian.oolite.OOPListSchemaVerifier undefined macro"; + + +typedef enum +{ + kTypeUnknown, + kTypeString, + kTypeArray, + kTypeDictionary, + kTypeInteger, + kTypePositiveInteger, + kTypeFloat, + kTypePositiveFloat, + kTypeOneOf, + kTypeEnumeration, + kTypeBoolean, + kTypeFuzzyBoolean, + kTypeVector, + kTypeQuaternion, + kTypeDelegatedType +} SchemaType; + + +typedef struct BackLinkChain BackLinkChain; +struct BackLinkChain +{ + BackLinkChain *link; + id element; +}; + +OOINLINE BackLinkChain BackLink(BackLinkChain *link, id element) +{ + BackLinkChain result = { link, element }; + return result; +} + +OOINLINE BackLinkChain BackLinkIndex(BackLinkChain *link, unsigned index) +{ + BackLinkChain result = { link, [NSNumber numberWithInt:index] }; + return result; +} + +OOINLINE BackLinkChain BackLinkRoot(void) +{ + BackLinkChain result = { NULL, NULL }; + return result; +} + + +static SchemaType StringToSchemaType(NSString *string, NSError **outError); +static NSString *ApplyStringFilter(NSString *string, id filterSpec, BackLinkChain keyPath, NSError **outError); +static BOOL ApplyStringTest(NSString *string, id test, SEL testSelector, NSString *testDescription, BackLinkChain keyPath, NSError **outError); +static NSArray *KeyPathToArray(BackLinkChain keyPath); +static NSString *KeyPathToString(BackLinkChain keyPath); +static NSString *StringForErrorReport(NSString *string); +static NSString *ArrayForErrorReport(NSArray *array); +static NSString *SetForErrorReport(NSSet *set); +static NSString *StringOrArrayForErrorReport(id value, NSString *arrayPrefix); + +static NSError *Error(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSString *format, ...); +static NSError *ErrorWithProperty(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSString *propKey, id propValue, NSString *format, ...); +static NSError *ErrorWithDictionary(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSDictionary *dict, NSString *format, ...); +static NSError *ErrorWithDictionaryAndArguments(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSDictionary *dict, NSString *format, va_list arguments); + +static NSError *ErrorTypeMismatch(Class expectedClass, NSString *expectedClassName, id actualObject, BackLinkChain keyPath); +static NSError *ErrorFailureAlreadyReported(void); +static BOOL IsFailureAlreadyReportedError(NSError *error); + + +@interface OOPListSchemaVerifier (OOPrivate) + +// Call delegate methods. +- (BOOL)delegateVerifierWithPropertyList:(id)rootPList + named:(NSString *)name + testProperty:(id)subPList + atPath:(BackLinkChain)keyPath + againstType:(NSString *)typeKey + error:(NSError **)outError; + +- (BOOL)delegateVerifierWithPropertyList:(id)rootPList + named:(NSString *)name + failedForProperty:(id)subPList + withError:(NSError *)error + expectedType:(NSDictionary *)localSchema; + +- (BOOL)verifyPList:(id)rootPList + named:(NSString *)name + subProperty:(id)subProperty + againstSchemaType:(id)subSchema + atPath:(BackLinkChain)keyPath + tentative:(BOOL)tentative + error:(NSError **)outError + stop:(BOOL *)outStop; + +- (NSDictionary *)resolveSchemaType:(id)specifier + atPath:(BackLinkChain)keyPath + error:(NSError **)outError; + +@end + + +@interface NSString (OOPListSchemaVerifierHelpers) + +- (BOOL)ooPListVerifierHasSubString:(NSString *)string; + +@end + + +#define VERIFY_PROTO(T) static NSError *Verify_##T(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +VERIFY_PROTO(String); +VERIFY_PROTO(Array) GCC_ATTR((noinline)); // Inlining suppressed to avoid "variable may be clobbered" warning when building for Mac OS X/x86. +VERIFY_PROTO(Dictionary) GCC_ATTR((noinline)); // Ditto. +VERIFY_PROTO(Integer); +VERIFY_PROTO(PositiveInteger); +VERIFY_PROTO(Float); +VERIFY_PROTO(PositiveFloat); +VERIFY_PROTO(OneOf); +VERIFY_PROTO(Enumeration); +VERIFY_PROTO(Boolean); +VERIFY_PROTO(FuzzyBoolean); +VERIFY_PROTO(Vector); +VERIFY_PROTO(Quaternion); +VERIFY_PROTO(DelegatedType); + + +@implementation OOPListSchemaVerifier + ++ (id)verifierWithSchema:(NSDictionary *)schema +{ + return [[[self alloc] initWithSchema:schema] autorelease]; +} + + +- (id)initWithSchema:(NSDictionary *)schema +{ + self = [super init]; + if (self != nil) + { + _schema = [schema retain]; + _definitions = [[_schema dictionaryForKey:@"$definitions"] retain]; + sDebugDump = [[NSUserDefaults standardUserDefaults] boolForKey:@"plist-schema-verifier-dump-structure"]; + if (sDebugDump) OOLogSetDisplayMessagesInClass(@"verifyOXP.verbose.plistDebugDump", YES); + + if (_schema == nil) + { + [self release]; + self = nil; + } + } + + return self; +} + + +- (void)dealloc +{ + [_schema release]; + [_definitions release]; + + [super dealloc]; +} + + +- (void)setDelegate:(id)delegate +{ + if (_delegate != delegate) + { + _delegate = delegate; + _badDelegateWarning = NO; + } +} + + +- (id)delegate +{ + return _delegate; +} + + +- (BOOL)verifyPropertyList:(id)plist named:(NSString *)name +{ + BOOL OK; + BOOL stop = NO; + + OK = [self verifyPList:plist + named:name + subProperty:plist + againstSchemaType:_schema + atPath:BackLinkRoot() + tentative:NO + error:NULL + stop:&stop]; + + return OK; +} + + ++ (NSString *)descriptionForKeyPath:(NSArray *)keyPath +{ + NSMutableString *result = nil; + NSEnumerator *componentEnum = nil; + id component = nil; + BOOL first = YES; + + result = [NSMutableString string]; + + for (componentEnum = [keyPath objectEnumerator]; (component = [componentEnum nextObject]); ) + { + if ([component isKindOfClass:[NSNumber class]]) + { + [result appendFormat:@"[%@]", component]; + } + else if ([component isKindOfClass:[NSString class]]) + { + if (!first) [result appendString:@"."]; + [result appendString:component]; + } + else return nil; + first = NO; + } + + if (first) + { + // Empty path + return @"root"; + } + + return result; +} + +@end + + +@implementation OOPListSchemaVerifier (OOPrivate) + +- (BOOL)delegateVerifierWithPropertyList:(id)rootPList + named:(NSString *)name + testProperty:(id)subPList + atPath:(BackLinkChain)keyPath + againstType:(NSString *)typeKey + error:(NSError **)outError +{ + BOOL result; + NSError *error = nil; + + if ([_delegate respondsToSelector:@selector(verifier:withPropertyList:named:testProperty:atPath:againstType:error:)]) + { + NS_DURING + result = [_delegate verifier:self + withPropertyList:rootPList + named:name + testProperty:subPList + atPath:KeyPathToArray(keyPath) + againstType:typeKey + error:&error]; + NS_HANDLER + OOLog(@"plistVerifier.delegateException", @"Property list schema verifier: delegate threw exception (%@) in -verifier:withPropertyList:named:testProperty:atPath:againstType: for type \"%@\" at %@ in %@ -- treating as failure.", [localException name], typeKey,KeyPathToString(keyPath), name); + result = NO; + error = nil; + NS_ENDHANDLER + + if (outError != NULL) + { + if (!result || error != nil) + { + // Note: Generates an error if delegate returned NO (meaning stop) or if delegate produced an error but did not request a stop. + *outError = ErrorWithProperty(kPListDelegatedTypeError, &keyPath, NSUnderlyingErrorKey, error, @"Value at %@ does not match delegated type \"%@\".", KeyPathToString(keyPath), typeKey); + } + else *outError = nil; + } + } + else + { + if (!_badDelegateWarning) + { + OOLog(@"plistVerifier.badDelegate", @"Property list schema verifier: delegate does not handle delegated types."); + _badDelegateWarning = YES; + } + result = YES; + } + + return result; +} + + +- (BOOL)delegateVerifierWithPropertyList:(id)rootPList + named:(NSString *)name + failedForProperty:(id)subPList + withError:(NSError *)error + expectedType:(NSDictionary *)localSchema +{ + BOOL result; + + if ([_delegate respondsToSelector:@selector(verifier:withPropertyList:named:failedForProperty:withError:expectedType:)]) + { + NS_DURING + result = [_delegate verifier:self + withPropertyList:rootPList + named:name + failedForProperty:subPList + withError:error + expectedType:localSchema]; + NS_HANDLER + OOLog(@"plistVerifier.delegateException", @"Property list schema verifier: delegate threw exception (%@) in -verifier:withPropertyList:named:failedForProperty:atPath:expectedType: at %@ in %@ -- stopping.", [localException name], [error plistKeyPathDescription], name); + result = NO; + NS_ENDHANDLER + } + else + { + OOLog(@"plistVerifier.failed", @"Verification of property list \"%@\" failed at %@: %@", name, [error plistKeyPathDescription], [error localizedDescription]); + result = NO; + } + return result; +} + + +- (BOOL)verifyPList:(id)rootPList + named:(NSString *)name + subProperty:(id)subProperty + againstSchemaType:(id)subSchema + atPath:(BackLinkChain)keyPath + tentative:(BOOL)tentative + error:(NSError **)outError + stop:(BOOL *)outStop +{ + SchemaType type = kTypeUnknown; + NSError *error = nil; + NSDictionary *resolvedSpecifier = nil; + NSAutoreleasePool *pool = nil; + + assert(outStop != NULL); + + pool = [[NSAutoreleasePool alloc] init]; + + DebugDumpPushIndent(); + + NS_DURING + DebugDumpIndent(); + + resolvedSpecifier = [self resolveSchemaType:subSchema atPath:keyPath error:&error]; + if (resolvedSpecifier != nil) type = StringToSchemaType([resolvedSpecifier objectForKey:@"type"], &error); + + #define VERIFY_CASE(T) case kType##T: error = Verify_##T(self, subProperty, resolvedSpecifier, rootPList, name, keyPath, tentative, outStop); break; + + switch (type) + { + VERIFY_CASE(String); + VERIFY_CASE(Array); + VERIFY_CASE(Dictionary); + VERIFY_CASE(Integer); + VERIFY_CASE(PositiveInteger); + VERIFY_CASE(Float); + VERIFY_CASE(PositiveFloat); + VERIFY_CASE(OneOf); + VERIFY_CASE(Enumeration); + VERIFY_CASE(Boolean); + VERIFY_CASE(FuzzyBoolean); + VERIFY_CASE(Vector); + VERIFY_CASE(Quaternion); + VERIFY_CASE(DelegatedType); + + case kTypeUnknown: + // resolveSchemaType:... or StringToSchemaType() should have provided an error. + *outStop = YES; + } + NS_HANDLER + error = Error(kPListErrorInternal, (BackLinkChain *)&keyPath, @"Uncaught exception %@: %@ in plist verifier for \"%@\" at %@.", [localException name], [localException reason], name, KeyPathToString(keyPath)); + NS_ENDHANDLER + + DebugDumpPopIndent(); + + if (error != nil) + { + if (!tentative && !IsFailureAlreadyReportedError(error)) + { + *outStop = ![self delegateVerifierWithPropertyList:rootPList + named:name + failedForProperty:subProperty + withError:error + expectedType:subSchema]; + } + else if (tentative) *outStop = YES; + } + + if (outError != NULL && error != nil) + { + *outError = [error retain]; + [pool release]; + [error autorelease]; + } + else + { + [pool release]; + } + + return error == nil; +} + + +- (NSDictionary *)resolveSchemaType:(id)specifier + atPath:(BackLinkChain)keyPath + error:(NSError **)outError +{ + id typeVal = nil; + NSString *complaint = nil; + + assert(outError != NULL); + + if (![specifier isKindOfClass:[NSString class]] && ![specifier isKindOfClass:[NSDictionary class]]) goto BAD_TYPE; + + for (;;) + { + if ([specifier isKindOfClass:[NSString class]]) specifier = [NSDictionary dictionaryWithObject:specifier forKey:@"type"]; + typeVal = [(NSDictionary *)specifier objectForKey:@"type"]; + + if ([typeVal isKindOfClass:[NSString class]]) + { + if ([typeVal hasPrefix:@"$"]) + { + // Macro reference; look it up in $definitions + specifier = [_definitions objectForKey:typeVal]; + if (specifier == nil) + { + *outError = ErrorWithProperty(kPListErrorSchemaUndefiniedMacroReference, &keyPath, kUndefinedMacroErrorKey, typeVal, @"Bad schema: reference to undefined macro \"%@\".", StringForErrorReport(typeVal)); + return nil; + } + } + else + { + // Non-macro string + return specifier; + } + } + else if ([typeVal isKindOfClass:[NSDictionary class]]) + { + specifier = typeVal; + } + else + { + goto BAD_TYPE; + } + } + +BAD_TYPE: + // Error: bad type + if (typeVal == nil) complaint = @"no type specified"; + else complaint = @"not string or dictionary"; + + *outError = Error(kPListErrorSchemaBadTypeSpecifier, &keyPath, @"Bad schema: invalid type specifier for path %@ (%@).", KeyPathToString(keyPath), complaint); + return nil; +} + +@end + + +static SchemaType StringToSchemaType(NSString *string, NSError **outError) +{ + static NSDictionary *typeMap = nil; + SchemaType result; + + if (typeMap == nil) + { + typeMap = + [[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:kTypeString], @"string", + [NSNumber numberWithUnsignedInt:kTypeArray], @"array", + [NSNumber numberWithUnsignedInt:kTypeDictionary], @"dictionary", + [NSNumber numberWithUnsignedInt:kTypeInteger], @"integer", + [NSNumber numberWithUnsignedInt:kTypePositiveInteger], @"positiveInteger", + [NSNumber numberWithUnsignedInt:kTypeFloat], @"float", + [NSNumber numberWithUnsignedInt:kTypePositiveFloat], @"positiveFloat", + [NSNumber numberWithUnsignedInt:kTypeOneOf], @"oneOf", + [NSNumber numberWithUnsignedInt:kTypeEnumeration], @"enumeration", + [NSNumber numberWithUnsignedInt:kTypeBoolean], @"boolean", + [NSNumber numberWithUnsignedInt:kTypeFuzzyBoolean], @"fuzzyBoolean", + [NSNumber numberWithUnsignedInt:kTypeVector], @"vector", + [NSNumber numberWithUnsignedInt:kTypeQuaternion], @"quaternion", + [NSNumber numberWithUnsignedInt:kTypeDelegatedType], @"delegatedType", + nil + ] retain]; + } + + result = [[typeMap objectForKey:string] unsignedIntValue]; + if (result == kTypeUnknown && outError != NULL) + { + if ([string hasPrefix:@"$"]) + { + *outError = ErrorWithProperty(kPListErrorSchemaUnknownType, NULL, kUnknownTypeErrorKey, string, @"Bad schema: unresolved macro reference \"%@\".", string); + } + else + { + *outError = ErrorWithProperty(kPListErrorSchemaUnknownType, NULL, kUnknownTypeErrorKey, string, @"Bad schema: unknown type \"%@\".", string); + } + } + + return result; +} + + +static NSString *ApplyStringFilter(NSString *string, id filterSpec, BackLinkChain keyPath, NSError **outError) +{ + NSEnumerator *filterEnum = nil; + id filter = nil; + NSRange range; + + assert(outError != NULL); + + if (filterSpec == nil) return string; + + if ([filterSpec isKindOfClass:[NSString class]]) + { + filterSpec = [NSArray arrayWithObject:filterSpec]; + } + if ([filterSpec isKindOfClass:[NSArray class]]) + { + for (filterEnum = [filterSpec objectEnumerator]; (filter = [filterEnum nextObject]); ) + { + if ([filter isKindOfClass:[NSString class]]) + { + if ([filter isEqual:@"lowerCase"]) string = [string lowercaseString]; + else if ([filter isEqual:@"upperCase"]) string = [string uppercaseString]; + else if ([filter isEqual:@"capitalized"]) string = [string capitalizedString]; + else if ([filter hasPrefix:@"truncFront:"]) + { + string = [string substringToIndex:[[filter substringFromIndex:11] intValue]]; + } + else if ([filter hasPrefix:@"truncBack:"]) + { + string = [string substringToIndex:[[filter substringFromIndex:10] intValue]]; + } + else if ([filter hasPrefix:@"subStringTo:"]) + { + range = [string rangeOfString:[filter substringFromIndex:12]]; + if (range.location != NSNotFound) + { + string = [string substringToIndex:range.location]; + } + } + else if ([filter hasPrefix:@"subStringFrom:"]) + { + range = [string rangeOfString:[filter substringFromIndex:14]]; + if (range.location != NSNotFound) + { + string = [string substringFromIndex:range.location + range.length]; + } + } + else if ([filter hasPrefix:@"subStringToInclusive:"]) + { + range = [string rangeOfString:[filter substringFromIndex:21]]; + if (range.location != NSNotFound) + { + string = [string substringToIndex:range.location + range.length]; + } + } + else if ([filter hasPrefix:@"subStringFromInclusive:"]) + { + range = [string rangeOfString:[filter substringFromIndex:23]]; + if (range.location != NSNotFound) + { + string = [string substringFromIndex:range.location]; + } + } + else + { + *outError = ErrorWithProperty(kPListErrorSchemaUnknownFilter, &keyPath, kUnnownFilterErrorKey, filter, @"Bad schema: unknown string filter specifier \"%@\".", filter); + } + } + else + { + *outError = Error(kPListErrorSchemaUnknownFilter, &keyPath, @"Bad schema: filter specifier is not a string."); + } + } + } + else + { + *outError = Error(kPListErrorSchemaUnknownFilter, &keyPath, @"Bad schema: \"filter\" must be a string or an array."); + } + + return string; +} + + +static BOOL ApplyStringTest(NSString *string, id test, SEL testSelector, NSString *testDescription, BackLinkChain keyPath, NSError **outError) +{ + BOOL (*testIMP)(id, SEL, NSString *); + NSEnumerator *testEnum = nil; + id subTest = nil; + + assert(outError != NULL); + + if (test == nil) return YES; + + testIMP = (BOOL(*)(id, SEL, NSString *))[string methodForSelector:testSelector]; + if (testIMP == NULL) + { + *outError = Error(kPListErrorInternal, &keyPath, @"OOPListSchemaVerifier internal error: NSString does not respond to test selector %@.", NSStringFromSelector(testSelector)); + return NO; + } + + if ([test isKindOfClass:[NSString class]]) + { + test = [NSArray arrayWithObject:test]; + } + + if ([test isKindOfClass:[NSArray class]]) + { + for (testEnum = [test objectEnumerator]; (subTest = [testEnum nextObject]); ) + { + if ([subTest isKindOfClass:[NSString class]]) + { + if (testIMP(string, testSelector, subTest)) return YES; + } + else + { + *outError = Error(kPListErrorSchemaBadComparator, &keyPath, @"Bad schema: required %@ is not a string.", testDescription); + return NO; + } + } + } + else + { + *outError = Error(kPListErrorSchemaBadComparator, &keyPath, @"Bad schema: %@ requirement specification is not a string or array.", testDescription); + } + return NO; +} + + +static NSArray *KeyPathToArray(BackLinkChain keyPath) +{ + NSMutableArray *result = nil; + BackLinkChain *curr = NULL; + + result = [NSMutableArray array]; + for (curr = &keyPath; curr != NULL; curr = curr->link) + { + if (curr->element != nil) [result insertObject:curr->element atIndex:0]; + } + + return result; +} + + +static NSString *KeyPathToString(BackLinkChain keyPath) +{ + return [OOPListSchemaVerifier descriptionForKeyPath:KeyPathToArray(keyPath)]; +} + + +static NSString *StringForErrorReport(NSString *string) +{ + id result = nil; + + if (kMaximumLengthForStringInErrorMessage < [string length]) + { + string = [string substringToIndex:kMaximumLengthForStringInErrorMessage]; + } + result = [NSMutableString stringWithString:string]; + [result replaceOccurrencesOfString:@"\t" withString:@" " options:0 range:NSMakeRange(0, [string length])]; + [result replaceOccurrencesOfString:@"\r\n" withString:@" \\ " options:0 range:NSMakeRange(0, [string length])]; + [result replaceOccurrencesOfString:@"\n" withString:@" \\ " options:0 range:NSMakeRange(0, [string length])]; + [result replaceOccurrencesOfString:@"\r" withString:@" \\ " options:0 range:NSMakeRange(0, [string length])]; + + if (kMaximumLengthForStringInErrorMessage < [result length]) + { + result = [result substringToIndex:kMaximumLengthForStringInErrorMessage - 3]; + result = [result stringByAppendingString:@"..."]; + } + + return result; +} + + +static NSString *ArrayForErrorReport(NSArray *array) +{ + NSString *result = nil; + NSString *string = nil; + unsigned i, count; + NSAutoreleasePool *pool = nil; + + count = [array count]; + if (count == 0) return @"( )"; + + pool = [[NSAutoreleasePool alloc] init]; + + result = [NSString stringWithFormat:@"(%@", [array objectAtIndex:0]]; + + for (i = 1; i != count; ++i) + { + string = [result stringByAppendingFormat:@", %@", [array objectAtIndex:i]]; + if (kMaximumLengthForStringInErrorMessage < [string length]) + { + result = [result stringByAppendingString:@", ..."]; + break; + } + result = string; + } + + result = [result stringByAppendingString:@")"]; + + [result retain]; + [pool release]; + return [result autorelease]; +} + + +static NSString *SetForErrorReport(NSSet *set) +{ + return ArrayForErrorReport([[set allObjects] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]); +} + + +static NSString *StringOrArrayForErrorReport(id value, NSString *arrayPrefix) +{ + if ([value isKindOfClass:[NSString class]]) + { + return [NSString stringWithFormat:@"\"%@\"", StringForErrorReport(value)]; + } + + if (arrayPrefix == nil) arrayPrefix = @""; + if ([value isKindOfClass:[NSArray class]]) + { + return [arrayPrefix stringByAppendingString:ArrayForErrorReport(value)]; + } + if ([value isKindOfClass:[NSSet class]]) + { + return [arrayPrefix stringByAppendingString:SetForErrorReport(value)]; + } + if (value == nil) return @"(null)"; + return @""; +} + + +// Specific type verifiers + +#define REQUIRE_TYPE(CLASSNAME, NAMESTRING) do { \ + if (![value isKindOfClass:[CLASSNAME class]]) \ + { \ + return ErrorTypeMismatch([CLASSNAME class], NAMESTRING, value, keyPath); \ + } \ + } while (0) + +static NSError *Verify_String(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + NSString *filteredString = nil; + id testValue = nil; + unsigned length; + unsigned lengthConstraint; + NSError *error = nil; + + REQUIRE_TYPE(NSString, @"string"); + + DebugDump(@"* string: \"%@\"", StringForErrorReport(value)); + + // Apply filters + filteredString = ApplyStringFilter(value, [params objectForKey:@"filter"], keyPath, &error); + if (filteredString == nil) return error; + + // Apply substring requirements + testValue = [params objectForKey:@"requiredPrefix"]; + if (testValue != nil) + { + if (!ApplyStringTest(filteredString, testValue, @selector(hasPrefix:), @"prefix", keyPath, &error)) + { + if (error == nil) error = ErrorWithProperty(kPListErrorStringPrefixMissing, &keyPath, kMissingSubStringErrorKey, testValue, @"String \"%@\" does not have required %@ %@.", StringForErrorReport(value), @"prefix", StringOrArrayForErrorReport(testValue, @"in ")); + return error; + } + } + + testValue = [params objectForKey:@"requiredSuffix"]; + if (testValue != nil) + { + if (!ApplyStringTest(filteredString, testValue, @selector(hasSuffix:), @"suffix", keyPath, &error)) + { + if (error == nil) error = ErrorWithProperty(kPListErrorStringSuffixMissing, &keyPath, kMissingSubStringErrorKey, testValue, @"String \"%@\" does not have required %@ %@.", StringForErrorReport(value), @"suffix", StringOrArrayForErrorReport(testValue, @"in ")); + return error; + } + } + + testValue = [params objectForKey:@"requiredSubString"]; + if (testValue != nil) + { + if (!ApplyStringTest(filteredString, testValue, @selector(ooPListVerifierHasSubString:), @"substring", keyPath, &error)) + { + if (error == nil) error = ErrorWithProperty(kPListErrorStringSubstringMissing, &keyPath, kMissingSubStringErrorKey, testValue, @"String \"%@\" does not have required %@ %@.", StringForErrorReport(value), @"substring", StringOrArrayForErrorReport(testValue, @"in ")); + return error; + } + } + + // Apply length bounds. + length = [filteredString length]; + lengthConstraint = [params unsignedIntForKey:@"minLength"]; + if (length < lengthConstraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"String \"%@\" is too short (%u bytes, minimum is %u).", StringForErrorReport(filteredString), length, lengthConstraint); + } + + lengthConstraint = [params unsignedIntForKey:@"maxLength" defaultValue:UINT_MAX]; + if (lengthConstraint < length) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"String \"%@\" is too long (%u bytes, maximum is %u).", StringForErrorReport(filteredString), length, lengthConstraint); + } + + // All tests passed. + return nil; +} + + +static NSError *Verify_Array(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + id valueType = nil; + BOOL OK = YES, stop = NO; + unsigned i, count; + id subProperty = nil; + unsigned constraint; + + REQUIRE_TYPE(NSArray, @"array"); + + DebugDump(@"* array"); + + // Apply count bounds. + count = [value count]; + constraint = [params unsignedIntForKey:@"minCount" defaultValue:0]; + if (count < constraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"Array has too few members (%u, minimum is %u).", count, constraint); + } + + constraint = [params unsignedIntForKey:@"maxCount" defaultValue:UINT_MAX]; + if (constraint < count) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"Array has too many members (%u, maximum is %u).", count, constraint); + } + + // Test member objects. + valueType = [params objectForKey:@"valueType"]; + if (valueType != nil) + { + for (i = 0; i != count; ++i) + { + subProperty = [value objectAtIndex:i]; + + if (![verifier verifyPList:rootPList + named:name + subProperty:subProperty + againstSchemaType:valueType + atPath:BackLinkIndex(&keyPath, i) + tentative:tentative + error:NULL + stop:&stop]) + { + OK = NO; + } + + if ((stop && !tentative) || (tentative && !OK)) break; + } + } + + *outStop = stop && !tentative; + + if (!OK) return ErrorFailureAlreadyReported(); + else return nil; +} + + +static NSError *Verify_Dictionary(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + NSDictionary *schema = nil; + id valueType = nil, + typeSpec = nil; + NSEnumerator *keyEnum = nil; + NSString *key = nil; + id subProperty = nil; + BOOL OK = YES, stop = NO, prematureExit = NO; + BOOL allowOthers; + NSMutableSet *requiredKeys = nil; + NSArray *requiredKeyList = nil; + unsigned count, constraint; + NSError *error = nil; + + REQUIRE_TYPE(NSDictionary, @"dictionary"); + + DebugDump(@"* dictionary"); + + // Apply count bounds. + count = [value count]; + constraint = [params unsignedIntForKey:@"minCount" defaultValue:0]; + if (count < constraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"Dictionary has too few pairs (%u, minimum is %u).", count, constraint); + } + constraint = [params unsignedIntForKey:@"maxCount" defaultValue:UINT_MAX]; + if (constraint < count) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"Dictionary has too manu pairs (%u, maximum is %u).", count, constraint); + } + + // Get schema. + schema = [params dictionaryForKey:@"schema"]; + valueType = [params objectForKey:@"valueType"]; + allowOthers = [params boolForKey:@"allowOthers" defaultValue:YES]; + requiredKeyList = [params arrayForKey:@"requiredKeys"]; + + // If these conditions are met, all members must pass: + if (schema == nil && valueType == nil && requiredKeyList == nil && allowOthers) return nil; + + if (requiredKeyList != nil) + { + requiredKeys = [NSMutableSet setWithArray:requiredKeyList]; + } + + DebugDumpIndent(); + + // Test member objects. + for (keyEnum = [value keyEnumerator]; (key = [keyEnum nextObject]) && !stop; ) + { + subProperty = [(NSDictionary *)value objectForKey:key]; + typeSpec = [schema objectForKey:key]; + if (typeSpec == nil) typeSpec = valueType; + + DebugDump(@"- \"%@\"", key); + DebugDumpIndent(); + + if (typeSpec != nil) + { + if (![verifier verifyPList:rootPList + named:name + subProperty:subProperty + againstSchemaType:typeSpec + atPath:BackLink(&keyPath, key) + tentative:tentative + error:NULL + stop:&stop]) + { + OK = NO; + } + } + else if (!allowOthers && ![requiredKeys containsObject:key] && [schema objectForKey:key] == nil) + { + // Report error now rather than returning it, since there may be several unknown keys. + if (!tentative) + { + error = ErrorWithProperty(kPListErrorDictionaryUnknownKey, &keyPath, kUnknownKeyErrorKey, key, @"Unpermitted key \"%@\" in dictionary.", StringForErrorReport(key)); + stop = ![verifier delegateVerifierWithPropertyList:rootPList + named:name + failedForProperty:value + withError:error + expectedType:params]; + error = nil; + } + OK = NO; + } + + DebugDumpOutdent(); + + [requiredKeys removeObject:key]; + + if ((stop && !tentative) || (tentative && !OK)) + { + prematureExit = YES; + break; + } + } + + DebugDumpOutdent(); + + // Check that all required keys were present. + if (!prematureExit && [requiredKeys count] != 0) + { + error = ErrorWithProperty(kPListErrorDictionaryMissingRequiredKeys, &keyPath, kMissingRequiredKeysErrorKey, requiredKeys, @"Required keys %@ missing from dictionary.", SetForErrorReport(requiredKeys)); + } + + *outStop = stop && !tentative; + + if (!OK) return ErrorFailureAlreadyReported(); + else return nil; +} + + +static NSError *Verify_Integer(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + long long numericValue; + long long constraint; + + numericValue = OOLongLongFromObject(value, 0); + + DebugDump(@"* integer: %lli", numericValue); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (numericValue != OOLongLongFromObject(value, 1)) + { + return ErrorTypeMismatch([NSNumber class], @"integer", value, keyPath); + } + + // Check constraints. + constraint = [params longLongForKey:@"minimum" defaultValue:LLONG_MIN]; + if (numericValue < constraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"Number is too small (%lli, minimum is %lli).", numericValue, constraint); + } + + constraint = [params longLongForKey:@"maximum" defaultValue:LLONG_MAX]; + if (constraint < numericValue) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"Number is too large (%lli, maximum is %lli).", numericValue, constraint); + } + + return nil; +} + + +static NSError *Verify_PositiveInteger(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + unsigned long long numericValue; + unsigned long long constraint; + + numericValue = OOUnsignedLongLongFromObject(value, 0); + + DebugDump(@"* positive integer: %llu", numericValue); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (numericValue != OOUnsignedLongLongFromObject(value, 1)) + { + return ErrorTypeMismatch([NSNumber class], @"positive integer", value, keyPath); + } + + // Check constraints. + constraint = [params unsignedLongLongForKey:@"minimum" defaultValue:0]; + if (numericValue < constraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"Number is too small (%llu, minimum is %llu).", numericValue, constraint); + } + + constraint = [params unsignedLongLongForKey:@"maximum" defaultValue:ULLONG_MAX]; + if (constraint < numericValue) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"Number is too large (%llu, maximum is %llu).", numericValue, constraint); + } + + return nil; +} + + +static NSError *Verify_Float(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + double numericValue; + double constraint; + + numericValue = OODoubleFromObject(value, 0); + + DebugDump(@"* float: %g", numericValue); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (numericValue != OODoubleFromObject(value, 1)) + { + return ErrorTypeMismatch([NSNumber class], @"number", value, keyPath); + } + + // Check constraints. + constraint = [params doubleForKey:@"minimum" defaultValue:-INFINITY]; + if (numericValue < constraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"Number is too small (%g, minimum is %g).", numericValue, constraint); + } + + constraint = [params doubleForKey:@"maximum" defaultValue:INFINITY]; + if (constraint < numericValue) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"Number is too large (%g, maximum is %g).", numericValue, constraint); + } + + return nil; +} + + +static NSError *Verify_PositiveFloat(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + double numericValue; + double constraint; + + numericValue = OODoubleFromObject(value, 0); + + DebugDump(@"* positive float: %g", numericValue); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (numericValue != OODoubleFromObject(value, 1)) + { + return ErrorTypeMismatch([NSNumber class], @"positive number", value, keyPath); + } + + if (numericValue < 0) + { + return Error(kPListErrorNumberIsNegative, &keyPath, @"Expected non-negative number, found %g.", numericValue); + } + + // Check constraints. + constraint = [params doubleForKey:@"minimum" defaultValue:0]; + if (numericValue < constraint) + { + return Error(kPListErrorMinimumConstraintNotMet, &keyPath, @"Number is too small (%g, minimum is %g).", numericValue, constraint); + } + + constraint = [params doubleForKey:@"maximum" defaultValue:INFINITY]; + if (constraint < numericValue) + { + return Error(kPListErrorMaximumConstraintNotMet, &keyPath, @"Number is too large (%g, maximum is %g).", numericValue, constraint); + } + + return nil; +} + + +static NSError *Verify_OneOf(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + NSArray *options = nil; + BOOL OK = NO, stop = NO; + NSEnumerator *optionEnum = nil; + id option = nil; + NSError *error; + NSMutableDictionary *errors = nil; + + DebugDump(@"* oneOf"); + + options = [params arrayForKey:@"options"]; + if (options == nil) + { + *outStop = YES; + return Error(kPListErrorSchemaNoOneOfOptions, &keyPath, @"Bad schema: no options specified for oneOf type."); + } + + errors = [[NSMutableDictionary alloc] initWithCapacity:[options count]]; + + for (optionEnum = [options objectEnumerator]; (option = [optionEnum nextObject]) ;) + { + if ([verifier verifyPList:rootPList + named:name + subProperty:value + againstSchemaType:option + atPath:keyPath + tentative:YES + error:&error + stop:&stop]) + { + DebugDump(@"> Match."); + OK = YES; + break; + } + [errors setObject:error forKey:option]; + } + + if (!OK) + { + DebugDump(@"! No match."); + return ErrorWithProperty(kPListErrorOneOfNoMatch, &keyPath, kErrorsByOptionErrorKey, [errors autorelease], @"No matching type rule could be found."); + } + + // Ignore stop in tentatives. + [errors release]; + return nil; +} + + +static NSError *Verify_Enumeration(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + NSArray *values = nil; + NSString *filteredString = nil; + NSError *error = nil; + + DebugDump(@"* enumeration"); + + REQUIRE_TYPE(NSString, @"string"); + + values = [params arrayForKey:@"values"]; + DebugDump(@" - \"%@\" in %@", StringForErrorReport(value), ArrayForErrorReport(values)); + + if (values == nil) + { + *outStop = YES; + return Error(kPListErrorSchemaNoEnumerationValues, &keyPath, @"Bad schema: no options specified for oneOf type."); + } + + filteredString = ApplyStringFilter(value, [params objectForKey:@"filter"], keyPath, &error); + if (filteredString == nil) return error; + + if ([values containsObject:filteredString]) return nil; + + return Error(kPListErrorEnumerationBadValue, &keyPath, @"Value \"%@\" not recognized, should be one of %@.", StringForErrorReport(value), ArrayForErrorReport(values)); +} + + +static NSError *Verify_Boolean(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + DebugDump(@"* boolean: %@", value); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (OOBooleanFromObject(value, 0) == OOBooleanFromObject(value, 1)) return nil; + else return ErrorTypeMismatch([NSNumber class], @"boolean", value, keyPath); +} + + +static NSError *Verify_FuzzyBoolean(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + DebugDump(@"* fuzzy boolean: %@", value); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (OODoubleFromObject(value, 0) == OODoubleFromObject(value, 1)) return nil; + else if (OOBooleanFromObject(value, 0) == OOBooleanFromObject(value, 1)) return nil; + else return ErrorTypeMismatch([NSNumber class], @"fuzzy boolean", value, keyPath); +} + + +static NSError *Verify_Vector(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + DebugDump(@"* vector: %@", value); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (vector_equal(OOVectorFromObject(value, kZeroVector), OOVectorFromObject(value, kBasisXVector))) return nil; + else return ErrorTypeMismatch(Nil, @"vector", value, keyPath); +} + + +static NSError *Verify_Quaternion(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + DebugDump(@"* quaternion: %@", value); + + // Check basic parseability. If there's inequality here, the default value is being returned. + if (quaternion_equal(OOQuaternionFromObject(value, kZeroQuaternion), OOQuaternionFromObject(value, kIdentityQuaternion))) return nil; + else return ErrorTypeMismatch(Nil, @"quaternion", value, keyPath); +} + + +static NSError *Verify_DelegatedType(OOPListSchemaVerifier *verifier, id value, NSDictionary *params, id rootPList, NSString *name, BackLinkChain keyPath, BOOL tentative, BOOL *outStop) +{ + id baseType = nil; + NSString *key = nil; + BOOL stop = NO; + NSError *error = nil; + + DebugDump(@"* delegated type: %@", [params objectForKey:@"key"]); + + baseType = [params objectForKey:@"baseType"]; + if (baseType != nil) + { + if (![verifier verifyPList:rootPList + named:name + subProperty:value + againstSchemaType:baseType + atPath:keyPath + tentative:tentative + error:NULL + stop:&stop]) + { + *outStop = stop; + return nil; + } + } + + key = [params objectForKey:@"key"]; + *outStop = ![verifier delegateVerifierWithPropertyList:rootPList + named:name + testProperty:value + atPath:keyPath + againstType:key + error:&error]; + return error; +} + + +@implementation NSString (OOPListSchemaVerifierHelpers) + +- (BOOL)ooPListVerifierHasSubString:(NSString *)string +{ + return [self rangeOfString:string].location != NSNotFound; +} + +@end + + +@implementation NSError (OOPListSchemaVerifierConveniences) + +- (NSArray *)plistKeyPath +{ + return [[self userInfo] arrayForKey:kPListKeyPathErrorKey]; +} + + +- (NSString *)plistKeyPathDescription +{ + return [OOPListSchemaVerifier descriptionForKeyPath:[self plistKeyPath]]; +} + + +- (NSSet *)missingRequiredKeys +{ + return [[self userInfo] setForKey:kMissingRequiredKeysErrorKey]; +} + + +- (Class)expectedClass +{ + return [[self userInfo] objectForKey:kExpectedClassErrorKey]; +} + + +- (NSString *)expectedClassName +{ + NSString *result = [[self userInfo] objectForKey:kExpectedClassNameErrorKey]; + if (result == nil) result = [[self expectedClass] description]; + return result; +} + +@end + + +static NSError *Error(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSString *format, ...) +{ + NSError *result = nil; + va_list args; + + va_start(args, format); + result = ErrorWithDictionaryAndArguments(errorCode, keyPath, nil, format, args); + va_end(args); + + return result; +} + + +static NSError *ErrorWithProperty(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSString *propKey, id propValue, NSString *format, ...) +{ + NSError *result = nil; + va_list args; + NSDictionary *dict = nil; + + if (propKey != nil && propValue != nil) + { + dict = [NSDictionary dictionaryWithObject:propValue forKey:propKey]; + } + va_start(args, format); + result = ErrorWithDictionaryAndArguments(errorCode, keyPath, dict, format, args); + va_end(args); + + return result; +} + + +static NSError *ErrorWithDictionary(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSDictionary *dict, NSString *format, ...) +{ + NSError *result = nil; + va_list args; + + va_start(args, format); + result = ErrorWithDictionaryAndArguments(errorCode, keyPath, dict, format, args); + va_end(args); + + return result; +} + + +static NSError *ErrorWithDictionaryAndArguments(OOPListSchemaVerifierErrorCode errorCode, BackLinkChain *keyPath, NSDictionary *dict, NSString *format, va_list arguments) +{ + NSString *message = nil; + NSMutableDictionary *userInfo = nil; + + message = [[NSString alloc] initWithFormat:format arguments:arguments]; + + userInfo = [NSMutableDictionary dictionaryWithDictionary:dict]; + [userInfo setObject:message forKey:NSLocalizedDescriptionKey]; + if (keyPath != NULL) + { + [userInfo setObject:KeyPathToArray(*keyPath) forKey:kPListKeyPathErrorKey]; + } + + [message release]; + + return [NSError errorWithDomain:kOOPListSchemaVerifierErrorDomain code:errorCode userInfo:userInfo]; +} + + +static NSError *ErrorTypeMismatch(Class expectedClass, NSString *expectedClassName, id actualObject, BackLinkChain keyPath) +{ + NSDictionary *dict = nil; + NSString *className = nil; + + if (expectedClassName == nil) expectedClassName = [expectedClass description]; + + dict = [NSDictionary dictionaryWithObjectsAndKeys: + expectedClassName, kExpectedClassNameErrorKey, + expectedClass, kExpectedClassErrorKey, + nil]; + + if (actualObject == nil) className = @"nothing"; + else if ([actualObject isKindOfClass:[NSString class]]) className = @"string"; + else if ([actualObject isKindOfClass:[NSNumber class]]) className = @"number"; + else if ([actualObject isKindOfClass:[NSArray class]]) className = @"array"; + else if ([actualObject isKindOfClass:[NSDictionary class]]) className = @"dictionary"; + else if ([actualObject isKindOfClass:[NSData class]]) className = @"data"; + else if ([actualObject isKindOfClass:[NSDate class]]) className = @"date"; + else className = [[actualObject class] description]; + + return ErrorWithDictionary(kPListErrorTypeMismatch, &keyPath, dict, @"Expected %@, found %@.", expectedClassName, className); +} + + +static NSError *ErrorFailureAlreadyReported(void) +{ + return [NSError errorWithDomain:kOOPListSchemaVerifierErrorDomain code:kPListErrorFailedAndErrorHasBeenReported userInfo:nil]; +} + + +static BOOL IsFailureAlreadyReportedError(NSError *error) +{ + return [[error domain] isEqualToString:kOOPListSchemaVerifierErrorDomain] && [error code] == kPListErrorFailedAndErrorHasBeenReported; +} + +#endif // OO_OXP_VERIFIER_ENABLED diff --git a/src/Core/OXPVerifier/OOTextureVerifierStage.h b/src/Core/OXPVerifier/OOTextureVerifierStage.h new file mode 100644 index 00000000..7c052cbf --- /dev/null +++ b/src/Core/OXPVerifier/OOTextureVerifierStage.h @@ -0,0 +1,63 @@ +/* + +OOTextureVerifierStage.h + +OOOXPVerifierStage which keeps track of textures (and images) that are used +and ensures they are loadable. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOFileScannerVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +@interface OOTextureVerifierStage: OOFileHandlingVerifierStage +{ + NSMutableSet *_usedTextures; +} + +// Returns name to be used in -dependents by other stages. ++ (NSString *)nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier; + +/* This can be called by other stages *before* the texture stage runs. + The context specifies where the texture is used; something like + "fooShip.dat" or "shipdata.plist materials dictionary for ship \"foo\"". + It should make sense with "Texture \"foo\" referenced in " in front of it. +*/ +- (void) textureNamed:(NSString *)name usedInContext:(NSString *)context; + +@end + + +// Convenience base class for stages that need to run before OOTextureHandlingStage. +@interface OOTextureHandlingStage: OOFileHandlingVerifierStage + +@end + + +@interface OOOXPVerifier(OOTextureVerifierStage) + +- (OOTextureVerifierStage *)textureVerifierStage; + +@end + +#endif diff --git a/src/Core/OXPVerifier/OOTextureVerifierStage.m b/src/Core/OXPVerifier/OOTextureVerifierStage.m new file mode 100644 index 00000000..766aa8df --- /dev/null +++ b/src/Core/OXPVerifier/OOTextureVerifierStage.m @@ -0,0 +1,209 @@ +/* + +OOTextureVerifierStage.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOTextureVerifierStage.h" + +#if OO_OXP_VERIFIER_ENABLED + +#import "OOTextureLoader.h" +#import "OOFileScannerVerifierStage.h" +#import "OOMaths.h" + +static NSString * const kStageName = @"Testing textures and images"; + + +@interface OOTextureVerifierStage (OOPrivate) + +- (void)checkTextureNamed:(NSString *)name inFolder:(NSString *)folder; + +@end + + +@implementation OOTextureVerifierStage + +- (id)init +{ + self = [super init]; + if (self != nil) + { + _usedTextures = [[NSMutableSet alloc] init]; + } + return self; +} + + +- (void)dealloc +{ + [_usedTextures release]; + + [super dealloc]; +} + + ++ (NSString *)nameForReverseDependencyForVerifier:(OOOXPVerifier *)verifier +{ + return kStageName; +} + + +- (NSString *)name +{ + return kStageName; +} + + +- (BOOL)shouldRun +{ + return [_usedTextures count] != 0 || [[[self verifier] fileScannerStage] filesInFolder:@"Images"] != nil; +} + + +- (void)run +{ + NSEnumerator *nameEnum = nil; + NSString *name = nil; + NSAutoreleasePool *pool = nil; + + for (nameEnum = [_usedTextures objectEnumerator]; (name = [nameEnum nextObject]); ) + { + pool = [[NSAutoreleasePool alloc] init]; + [self checkTextureNamed:name inFolder:@"Textures"]; + [pool release]; + } + [_usedTextures release]; + _usedTextures = nil; + + // All "images" are considered used, since we don't have a reasonable way to look for images referenced in JavaScript scripts. + nameEnum = [[[[self verifier] fileScannerStage] filesInFolder:@"Images"] objectEnumerator]; + while ((name = [nameEnum nextObject])) + { + [self checkTextureNamed:name inFolder:@"Images"]; + } +} + + +- (void) textureNamed:(NSString *)name usedInContext:(NSString *)context +{ + OOFileScannerVerifierStage *fileScanner = nil; + + if (name == nil) return; + if ([_usedTextures containsObject:name]) return; + [_usedTextures addObject:name]; + + fileScanner = [[self verifier] fileScannerStage]; + if (![fileScanner fileExists:name + inFolder:@"Textures" + referencedFrom:context + checkBuiltIn:YES]) + { + OOLog(@"verifyOXP.texture.notFound", @"----- WARNING: texture \"%@\" referenced in %@ could not be found in %@ or in Oolite.", name, context, [[self verifier] oxpDisplayName]); + } +} + +@end + + +@implementation OOTextureVerifierStage (OOPrivate) + +- (void)checkTextureNamed:(NSString *)name inFolder:(NSString *)folder +{ + OOTextureLoader *loader = nil; + NSString *path = nil; + OOFileScannerVerifierStage *fileScanner = nil; + NSString *displayName = nil; + void *data = nil; + uint32_t width, height, rWidth, rHeight; + BOOL success; + + fileScanner = [[self verifier] fileScannerStage]; + path = [fileScanner pathForFile:name + inFolder:folder + referencedFrom:nil + checkBuiltIn:NO]; + + if (path == nil) return; + + loader = [OOTextureLoader loaderWithPath:path + options:kOOTextureMinFilterNearest | + kOOTextureMinFilterNearest | + kOOTextureNoShrink | + kOOTextureNoFNFMessage | + kOOTextureNeverScale]; + + displayName = [fileScanner displayNameForFile:name andFolder:folder]; + if (loader == nil) + { + OOLog(@"verifyOXP.texture.failed", @"***** ERROR: image %@ could not be read.", displayName); + } + else + { + success = [loader getResult:&data format:NULL width:&width height:&height]; + if (data != NULL) free(data); + + if (success) + { + rWidth = OORoundUpToPowerOf2((2 * width) / 3); + rHeight = OORoundUpToPowerOf2((2 * height) / 3); + if (width != rWidth || height != rHeight) + { + OOLog(@"verifyOXP.texture.notPOT", @"----- WARNING: image %@ has non-power-of-two dimensions; it will have to be rescaled (from %ux%u pixels to %ux%u pixels) at runtime.", displayName, width, height, rWidth, rHeight); + } + else + { + OOLog(@"verifyOXP.verbose.texture.OK", @"- %@ (%ux%u px) OK.", displayName, width, height); + } + } + else + { + OOLog(@"verifyOXP.texture.failed", @"***** ERROR: texture loader failed to load %@.", displayName); + } + } +} + +@end + + +@implementation OOTextureHandlingStage + +- (NSSet *)dependents +{ + NSMutableSet *result = [[super dependents] mutableCopy]; + [result addObject:[OOTextureVerifierStage nameForReverseDependencyForVerifier:[self verifier]]]; + return [result autorelease]; +} + +@end + + +@implementation OOOXPVerifier(OOTextureVerifierStage) + +- (OOTextureVerifierStage *)textureVerifierStage +{ + return [self stageWithName:kStageName]; +} + +@end + +#endif diff --git a/src/Core/OXPVerifier/OXP verifier design.txt b/src/Core/OXPVerifier/OXP verifier design.txt new file mode 100644 index 00000000..55edb1c5 --- /dev/null +++ b/src/Core/OXPVerifier/OXP verifier design.txt @@ -0,0 +1,120 @@ +Oolite: OXP Verifier design notes +================================= + +The OXP Verifier needs to perform a variety of different checks on an OXP, +most of which are independent of each other. However, some checks do need to +be performed in specific orders; notably, most checks need to be able to look +up files while ensuring case-insensitivity checks. Additionally, some checks +need to be performed after an open set of other checks which provide +information for them, notably checking for unused files and testing that +referenced textures are readable. + +The general independence and potentially large number of verifier passes +suggests a modular design. The partial ordering suggests some form of +dependency management. The chosen solution involves objects representing the +individual passes, or stages, and a manager object to ensure the stages are +executed in an appropriate order. The manager also handles some aspects of +resource management, such as autorelease pools and a dictionary of settings +read from verifyOXP.plist. + +Each verifier stage is implemented as a subclass of OXPVerifierStage. Each +stage has a unique name, a set of stages that must be executed before it (its +dependencies), and a set of stages that should be executed after it (its +dependents, or reverse dependencies). The difference between “must” and +“should” here is significant: a stage whose dependencies are not fulfilled +cannot be run, but the verifier will ignore reverse dependencies if necessary +to produce a cycle-free dependency graph. + +The actual verification process works as follows: first, an OXPVerifier object +is created for an OXP. The design allows for multiple OXPs to be verified by +separate OXPVerifiers in a serial fashion, although this is not currently done. +The OXPVerifier loads verifyOXP.plist, which contains, among other things, a +root set of verifier stages (by class name). The OXPVerifier looks up these +classes, instantiates them, and registers each one with itself. It then +proceeds to build the dependency tree. + +In order to do this, it asks each registered verifier stage for its +dependencies and reverse dependencies. The verifier stages may register +additional stages at this time; for instance, the OOFileScannerVerifierStage, +which is used by almost all other stages, is initialized at this time. + +Once there are no registered stages whose dependencies and dependents have not +been queried, the OXP verifier iterates over each stage’s dependencies, +building the basic dependency graph. If a stage has a dependency that can’t be +resolved, or a dependency that would introduce a cycle, that stage is removed. +Then, the reverse dependencies are checked; in this case, unresolved or +cyclical dependencies are simply ignored. + +Having built the dependency graph (and optionally written a graphviz file for +debugging, see below), the verifier repeatedly looks for verifier stages whose +dependencies are resolved, i.e., have run. For each such ready-to-run stage, +it queries whether the stage wishes to run (using -shouldRun), runs it if so, +then marks it as completed and updates all stages depending on it. (Note that +this means dependent stages can run even if -shouldRun returns NO.) The use of +-shouldRun is essentially to avoid listing irrelevant stages in the run log, +such as the OOCheckRequiresPListVerifierStage for an OXP with no +requires.plist. + +When no stages are ready to run, the verifier exits its stage-running loop. +If any stages have not been run for any reason, the verifier lists these. +After this, the verification is complete. + + +Debugging +========= +For analysis and debugging of OXPVerifier’s dependency management, it can +generate a graphviz file showing the dependency graph. This is done by setting +the "oxp-verifier-dump-debug-graphviz" default to true. (Under Mac OS X, use +“defaults write org.aegidian.oolite oxp-verifier-dump-debug-graphviz -bool yes”; +for GNUstep, edit oolite.app/gnustep/defaults/.gnustepdefaults and add +“oxp-verifier-dump-debug-graphviz = <*BY>”.) The graphviz file, named +OXPVerifierStageDependencies.dot, will be written to the current working +directory. There should be no problem viewing the file with the cross-platform +graphviz tools (see http://www.graphviz.org/ ); the Mac GUI version (see +http://www.pixelglow.com/graphviz/ ) will helpfully update whenever the file +is regenerated. + +The graph consists of two special nodes, Start and End, and a node for each +runnable verifier stage. Blue arrows indicate possible orders of execution, +starting with Start. Green lines with circles indicate reverse dependencies, +terminating in End. Whenever a blue arrow connects two stage nodes, there +should be a matching green line and vice versa; if this is not the case, the +dependency graph generation is broken. + +Additionally, the log message class verifyOXP.verbose may be set to cause +additional data to be logged, including messages about verifier stages that +are skipped (because -shouldRun returned NO) with the more specific message +class verifyOXP.verbose.skipStage. + + +The file scanner +================ +The file scanner stage, OOFileScannerVerifierStage, is somewhat special in +that it is used by almost every other stage. It is responsible for finding the +files in an OXP, tracking which are used, ensuring OXPs will work on both +case-sensitive and case-insensistive systems, loading files for other stages +and warning about unused files. For this last task, a helper stage, the +OOListUnusedFilesStage, is used. Every other stage, at the time of writing, +has OOFileScannerVerifierStage as a dependency and OOListUnusedFilesStage as a +reverse dependency. To assist in implementing this pattern, an abstract class +OOFileHandlingVerifierStage is provided. + +The file scanner is built around dictionaries mapping lowercase file and +directory names to actual case names, i.e., names in the case present in the +file system. When another stage requests a file, the file scanner converts the +name to lowercase, looks up the actual name, and logs a warning if the actual +case string is not the same as the requested name (in “canonical case”). It +also keeps track of every file that is requested, in order to be able to list +the ones that aren’t referenced. Additionally, it checks that the root +directory only contains directories with known names and that there are no +Read Me files inside the root directory, and warns about junk files like +.DS_Store and Thumbs.db. + +The file scanner is based on Oolite’s specific needs. In particular, it +assumes there will be no more than one level of directories inside an OXP. + + +Acknowledgement +=============== +This design was probably influenced by the video session on the LLVM +optimization pass manager I saw recently. diff --git a/src/Core/OXPVerifier/plist verifier design.txt b/src/Core/OXPVerifier/plist verifier design.txt new file mode 100644 index 00000000..c8e4b53c --- /dev/null +++ b/src/Core/OXPVerifier/plist verifier design.txt @@ -0,0 +1,47 @@ +String filters: +lowerCase +upperCase +capitalized +truncFront:# +truncBack:# +subStringTo:$ (No effect if specified string is not a substring) +subStringFrom:$ (No effect if specified string is not a substring) +subStringToInclusive:$ (No effect if specified string is not a substring) +subStringFromInclusive:$ (No effect if specified string is not a substring) +Can be chained using array. + +Strings: +filters applied first. +requiredPrefix (may be array) +requiredSuffix (may be array) +requiredSubString (may be array) +minLength +maxLength + +Numerical types: +minimum +maximum + +Enumerations: +filters applied before test. +values + +Dictionary: +minCount +maxCount +valueType +schema +allowOthers (default:YES) +requiredKeys + +Array: +minCount +maxCount +valueType + +oneOf: +options + +delegatedType: +baseType +key \ No newline at end of file diff --git a/src/Core/Octree.h b/src/Core/Octree.h new file mode 100644 index 00000000..d804c798 --- /dev/null +++ b/src/Core/Octree.h @@ -0,0 +1,95 @@ +/* + +Octtree.h + +Octtree class for collision detection. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" +#import "OOMaths.h" + +#define OCTREE_MIN_RADIUS 1.0 + + +#if !defined(OODEBUGLDRAWING_DISABLE) && defined(NDEBUG) +#define OODEBUGLDRAWING_DISABLE 1 +#endif + + +typedef struct +{ + GLfloat radius; + int* octree; + unsigned char* octree_collision; +} Octree_details; + + +@interface Octree : NSObject +{ + GLfloat radius; + int leafs; + int* octree; + BOOL hasCollision; + + unsigned char* octree_collision; +} + +- (GLfloat) radius; +- (int) leafs; +- (int*) octree; +- (BOOL) hasCollision; +- (void) setHasCollision:(BOOL) value; +- (unsigned char*) octree_collision; + +- (Octree_details) octreeDetails; + +- (id) initWithRepresentationOfOctree:(GLfloat) octRadius :(NSObject*) octreeArray :(int) leafsize; +- (id) initWithDictionary:(NSDictionary*) dict; + +- (Octree*) octreeScaledBy:(GLfloat) factor; + +int copyRepresentationIntoOctree(NSObject* theRep, int* theBuffer, int atLocation, int nextFreeLocation); + +Vector offsetForOctant(int oct, GLfloat r); + +#ifndef OODEBUGLDRAWING_DISABLE +- (void) drawOctree; +- (void) drawOctreeCollisions; +#endif + +BOOL isHitByLine(int* octbuffer, unsigned char* collbuffer, int level, GLfloat rad, Vector v0, Vector v1, Vector off, int face_hit); +- (GLfloat) isHitByLine: (Vector) v0: (Vector) v1; + +BOOL isHitByOctree( Octree_details axialDetails, + Octree_details otherDetails, Vector delta, Triangle other_ijk); +- (BOOL) isHitByOctree:(Octree*) other withOrigin: (Vector) v0 andIJK: (Triangle) ijk; +- (BOOL) isHitByOctree:(Octree*) other withOrigin: (Vector) v0 andIJK: (Triangle) ijk andScales: (GLfloat) s1: (GLfloat) s2; + +- (NSDictionary*) dict; + +- (GLfloat) volume; +GLfloat volumeOfOctree(Octree_details octree_details); + +Vector randomFullNodeFrom( Octree_details details, Vector offset); + +@end diff --git a/src/Core/Octree.m b/src/Core/Octree.m new file mode 100644 index 00000000..5ca5b2c0 --- /dev/null +++ b/src/Core/Octree.m @@ -0,0 +1,801 @@ +/* + +Octtree.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "Octree.h" +#import "OOMaths.h" +#import "Entity.h" +#import "OOOpenGL.h" +#import "OODebugGLDrawing.h" +#import "OOMacroOpenGL.h" + + +#ifndef NDEBUG +#define OctreeDebugLogVerbose(format, ...) do { if (EXPECT_NOT(gDebugFlags & DEBUG_OCTREE_TEXT)) OOLog(@"octree.debug", format, ## __VA_ARGS__); } while (0) +#define OctreeDebugLog(format, ...) do { if (EXPECT_NOT(gDebugFlags & DEBUG_OCTREE)) OOLog(@"octree.debug", format, ## __VA_ARGS__); } while (0) +#else +#define OctreeDebugLogVerbose(...) do {} while (0) +#define OctreeDebugLog(...) do {} while (0) +#endif + + +@interface Octree (Private) + +#ifndef OODEBUGLDRAWING_DISABLE + +- (void) drawOctreeFromLocation:(int) loc :(GLfloat) scale :(Vector) offset; +- (void) drawOctreeCollisionFromLocation:(int) loc :(GLfloat) scale :(Vector) offset; + +#endif + +@end + + +@implementation Octree + +- (id) init +{ + self = [super init]; + radius = 0; + leafs = 0; + octree = malloc(sizeof(int)); + octree_collision = malloc(sizeof(char)); + octree[0] = 0; + octree_collision[0] = (char)0; + hasCollision = NO; + return self; +} + +- (void) dealloc +{ + free(octree); + free(octree_collision); + [super dealloc]; +} + +- (GLfloat) radius +{ + return radius; +} + +- (int) leafs +{ + return leafs; +} + +- (int*) octree +{ + return octree; +} + +- (BOOL) hasCollision +{ + return hasCollision; +} + +- (void) setHasCollision:(BOOL) value +{ + hasCollision = value; +} + +- (unsigned char*) octree_collision +{ + return octree_collision; +} + +- (Octree_details) octreeDetails +{ + Octree_details details; + details.octree = octree; + details.radius = radius; + details.octree_collision = octree_collision; + return details; +} + +- (id) initWithRepresentationOfOctree:(GLfloat) octRadius :(NSObject*) octreeArray :(int) leafsize +{ + self = [super init]; + + radius = octRadius; + leafs = leafsize; + octree = malloc(leafsize *sizeof(int)); + octree_collision = malloc(leafsize *sizeof(char)); + hasCollision = NO; + + int i; + for (i = 0; i< leafsize; i++) + { + octree[i] = 0; + octree_collision[i] = (char)0; + } + + copyRepresentationIntoOctree( octreeArray, octree, 0, 1); + + return self; +} + +- (id) initWithDictionary:(NSDictionary*) dict +{ + self = [super init]; + + radius = [[dict objectForKey:@"radius"] floatValue]; + leafs = [[dict objectForKey:@"leafs"] intValue]; + octree = malloc(leafs *sizeof(int)); + octree_collision = malloc(leafs *sizeof(char)); + int* data = (int*)[(NSData*)[dict objectForKey:@"octree"] bytes]; + hasCollision = NO; + + int i; + for (i = 0; i< leafs; i++) + { + octree[i] = data[i]; + octree_collision[i] = (char)0; + } + + return self; +} + +- (Octree*) octreeScaledBy:(GLfloat) factor +{ + GLfloat temp = radius; + radius *= factor; + Octree* result = [[Octree alloc] initWithDictionary:[self dict]]; + radius = temp; + return [result autorelease]; +} + +int copyRepresentationIntoOctree(NSObject* theRep, int* theBuffer, int atLocation, int nextFreeLocation) +{ + if ([theRep isKindOfClass:[NSNumber class]]) // ie. a terminating leaf + { + if ([(NSNumber*)theRep intValue] != 0) + { + theBuffer[atLocation] = -1; + return nextFreeLocation; + } + else + { + theBuffer[atLocation] = 0; + return nextFreeLocation; + } + } + if ([theRep isKindOfClass:[NSArray class]]) // ie. a subtree + { + NSArray* theArray = (NSArray*)theRep; + int i; + int theNextSpace = nextFreeLocation + 8; + for (i = 0; i < 8; i++) + { + NSObject* rep = [theArray objectAtIndex:i]; + theNextSpace = copyRepresentationIntoOctree( rep, theBuffer, nextFreeLocation + i, theNextSpace); + } + theBuffer[atLocation] = nextFreeLocation - atLocation; // now a relative reference + return theNextSpace; + } + + OOLog(@"octree.unarchive.failed", @"**** some error creating octree *****"); + return nextFreeLocation; +} + +Vector offsetForOctant(int oct, GLfloat r) +{ + return make_vector(((GLfloat)0.5 - (GLfloat)((oct >> 2) & 1)) * r, ((GLfloat)0.5 - (GLfloat)((oct >> 1) & 1)) * r, ((GLfloat)0.5 - (GLfloat)(oct & 1)) * r); +} + + +#ifndef OODEBUGLDRAWING_DISABLE + +- (void) drawOctree +{ + OODebugWFState state = OODebugBeginWireframe(NO); + + OO_ENTER_OPENGL(); + glEnable(GL_BLEND); + glBegin(GL_LINES); + glColor4f(0.4, 0.4, 0.4, 0.5); + + // it's a series of cubes + [self drawOctreeFromLocation:0 :radius : kZeroVector]; + + glEnd(); + + OODebugEndWireframe(state); + CheckOpenGLErrors(@"Octree after drawing %@", self); +} + + +#if 0 +#define OCTREE_COLOR(r, g, b, a) glColor4f(r, g, b, a) +#else +#define OCTREE_COLOR(r, g, b, a) do {} while (0) +#endif + + +- (void) drawOctreeFromLocation:(int) loc :(GLfloat) scale :(Vector) offset +{ + if (octree[loc] == 0) + return; + + OO_ENTER_OPENGL(); + + if (octree[loc] == -1) // full + { + // draw a cube + glVertex3f(-scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, -scale + offset.y, -scale + offset.z); + + + glVertex3f(-scale + offset.x, -scale + offset.y, scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, scale + offset.z); + glVertex3f(-scale + offset.x, -scale + offset.y, scale + offset.z); + + glVertex3f(-scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, -scale + offset.y, scale + offset.z); + + glVertex3f(-scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, scale + offset.z); + + glVertex3f(scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, scale + offset.z); + + glVertex3f(scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, scale + offset.z); + } + else if (octree[loc] > 0) + { + GLfloat sc = 0.5 * scale; + OCTREE_COLOR( 0.4, 0.4, 0.4, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 0 :sc :make_vector( offset.x - sc, offset.y - sc, offset.z - sc)]; + OCTREE_COLOR( 0.0, 0.0, 1.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 1 :sc :make_vector( offset.x - sc, offset.y - sc, offset.z + sc)]; + OCTREE_COLOR( 0.0, 1.0, 0.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 2 :sc :make_vector( offset.x - sc, offset.y + sc, offset.z - sc)]; + OCTREE_COLOR( 0.0, 1.0, 1.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 3 :sc :make_vector( offset.x - sc, offset.y + sc, offset.z + sc)]; + OCTREE_COLOR( 1.0, 0.0, 0.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 4 :sc :make_vector( offset.x + sc, offset.y - sc, offset.z - sc)]; + OCTREE_COLOR( 1.0, 0.0, 1.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 5 :sc :make_vector( offset.x + sc, offset.y - sc, offset.z + sc)]; + OCTREE_COLOR( 1.0, 1.0, 0.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 6 :sc :make_vector( offset.x + sc, offset.y + sc, offset.z - sc)]; + OCTREE_COLOR( 1.0, 1.0, 1.0, 0.5); + [self drawOctreeFromLocation: loc + octree[loc] + 7 :sc :make_vector( offset.x + sc, offset.y + sc, offset.z + sc)]; + } +} + +BOOL drawTestForCollisions; + +- (void) drawOctreeCollisions +{ + OODebugWFState state = OODebugBeginWireframe(NO); + + // it's a series of cubes + drawTestForCollisions = NO; + if (hasCollision) + [self drawOctreeCollisionFromLocation:0 :radius : kZeroVector]; + hasCollision = drawTestForCollisions; + + OODebugEndWireframe(state); + CheckOpenGLErrors(@"Octree after drawing collisions for %@", self); +} + +- (void) drawOctreeCollisionFromLocation:(int) loc :(GLfloat) scale :(Vector) offset +{ + if (octree[loc] == 0) + return; + if ((octree[loc] != 0)&&(octree_collision[loc] != (unsigned char)0)) // full - draw + { + OO_ENTER_OPENGL(); + + GLfloat red = (GLfloat)(octree_collision[loc])/255.0; + glColor4f( 1.0, 0.0, 0.0, red); // 50% translucent + + drawTestForCollisions = YES; + octree_collision[loc]--; + + // draw a cube + glDisable(GL_CULL_FACE); // face culling + + glDisable(GL_TEXTURE_2D); + + glBegin(GL_LINE_STRIP); + + glVertex3f(-scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, -scale + offset.y, -scale + offset.z); + + glEnd(); + + glBegin(GL_LINE_STRIP); + + glVertex3f(-scale + offset.x, -scale + offset.y, scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, scale + offset.z); + glVertex3f(-scale + offset.x, -scale + offset.y, scale + offset.z); + + glEnd(); + + glBegin(GL_LINES); + + glVertex3f(-scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, -scale + offset.y, scale + offset.z); + + glVertex3f(-scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(-scale + offset.x, scale + offset.y, scale + offset.z); + + glVertex3f(scale + offset.x, scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, scale + offset.y, scale + offset.z); + + glVertex3f(scale + offset.x, -scale + offset.y, -scale + offset.z); + glVertex3f(scale + offset.x, -scale + offset.y, scale + offset.z); + + glEnd(); + + glEnable(GL_CULL_FACE); // face culling + } + if (octree[loc] > 0) + { + GLfloat sc = 0.5 * scale; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 0 :sc :make_vector( offset.x - sc, offset.y - sc, offset.z - sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 1 :sc :make_vector( offset.x - sc, offset.y - sc, offset.z + sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 2 :sc :make_vector( offset.x - sc, offset.y + sc, offset.z - sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 3 :sc :make_vector( offset.x - sc, offset.y + sc, offset.z + sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 4 :sc :make_vector( offset.x + sc, offset.y - sc, offset.z - sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 5 :sc :make_vector( offset.x + sc, offset.y - sc, offset.z + sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 6 :sc :make_vector( offset.x + sc, offset.y + sc, offset.z - sc)]; + [self drawOctreeCollisionFromLocation: loc + octree[loc] + 7 :sc :make_vector( offset.x + sc, offset.y + sc, offset.z + sc)]; + } +} +#endif // OODEBUGLDRAWING_DISABLE + + +BOOL hasCollided = NO; +GLfloat hit_dist = 0.0; +BOOL isHitByLine(int* octbuffer, unsigned char* collbuffer, int level, GLfloat rad, Vector v0, Vector v1, Vector off, int face_hit) +{ + // displace the line by the offset + Vector u0 = make_vector( v0.x + off.x, v0.y + off.y, v0.z + off.z); + Vector u1 = make_vector( v1.x + off.x, v1.y + off.y, v1.z + off.z); + + OctreeDebugLogVerbose(@"DEBUG octant: [%d] radius: %.2f vs. line: ( %.2f, %.2f, %.2f) - ( %.2f, %.2f, %.2f)", + level, rad, u0.x, u0.y, u0.z, u1.x, u1.y, u1.z); + + if (octbuffer[level] == 0) + { + OctreeDebugLogVerbose(@"DEBUG Hit an empty octant: [%d]", level); + return NO; + } + + if (octbuffer[level] == -1) + { + OctreeDebugLogVerbose(@"DEBUG Hit a solid octant: [%d]", level); + collbuffer[level] = 2; // green + hit_dist = sqrt( u0.x * u0.x + u0.y * u0.y + u0.z * u0.z); + return YES; + } + + int faces = face_hit; + if (faces == 0) + faces = lineCubeIntersection( u0, u1, rad); + + if (faces == 0) + { + OctreeDebugLogVerbose(@"----> Line misses octant: [%d].", level); + return NO; + } + + int octantIntersected = 0; + + if (faces > 0) + { + Vector vi = lineIntersectionWithFace( u0, u1, faces, rad); + + if (CUBE_FACE_FRONT & faces) + octantIntersected = ((vi.x < 0.0)? 1: 5) + ((vi.y < 0.0)? 0: 2); + if (CUBE_FACE_BACK & faces) + octantIntersected = ((vi.x < 0.0)? 0: 4) + ((vi.y < 0.0)? 0: 2); + + if (CUBE_FACE_RIGHT & faces) + octantIntersected = ((vi.y < 0.0)? 4: 6) + ((vi.z < 0.0)? 0: 1); + if (CUBE_FACE_LEFT & faces) + octantIntersected = ((vi.y < 0.0)? 0: 2) + ((vi.z < 0.0)? 0: 1); + + if (CUBE_FACE_TOP & faces) + octantIntersected = ((vi.x < 0.0)? 2: 6) + ((vi.z < 0.0)? 0: 1); + if (CUBE_FACE_BOTTOM & faces) + octantIntersected = ((vi.x < 0.0)? 0: 4) + ((vi.z < 0.0)? 0: 1); + + OctreeDebugLogVerbose(@"----> found intersection with face 0x%2x of cube of radius %.2f at ( %.2f, %.2f, %.2f) octant:%d", + faces, rad, vi.x, vi.y, vi.z, octantIntersected); + } + else + { + OctreeDebugLogVerbose(@"----> inside cube of radius %.2f octant:%d", rad, octantIntersected); + faces = 0; // inside the cube! + } + + hasCollided = YES; + + collbuffer[level] = 1; // red + + OctreeDebugLogVerbose(@"----> testing octants..."); + + int nextlevel = level + octbuffer[level]; + + GLfloat rd2 = 0.5 * rad; + + // first test the intersected octant, then the three adjacent, then the next three adjacent, the finally the diagonal octant + int oct0, oct1, oct2, oct3; // test oct0 then oct1, oct2, oct3 then (7 - oct1), (7 - oct2), (7 - oct3) then (7 - oct0) + oct0 = octantIntersected; + oct1 = oct0 ^ 0x01; // adjacent x + oct2 = oct0 ^ 0x02; // adjacent y + oct3 = oct0 ^ 0x04; // adjacent z + + Vector moveLine = off; + + OctreeDebugLogVerbose(@"----> testing first octant hit [+%d]", oct0); + if (octbuffer[nextlevel + oct0]) + { + moveLine = offsetForOctant( oct0, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct0, rd2, u0, u1, moveLine, faces)) return YES; // first octant + } + + // test the three adjacent octants + + OctreeDebugLogVerbose(@"----> testing next three octants [+%d] [+%d] [+%d]", oct1, oct2, oct3); + + if (octbuffer[nextlevel + oct1]) + { + moveLine = offsetForOctant( oct1, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct1, rd2, u0, u1, moveLine, 0)) return YES; // second octant + } + if (octbuffer[nextlevel + oct2]) + { + moveLine = offsetForOctant( oct2, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct2, rd2, u0, u1, moveLine, 0)) return YES; // third octant + } + if (octbuffer[nextlevel + oct3]) + { + moveLine = offsetForOctant( oct3, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct3, rd2, u0, u1, moveLine, 0)) return YES; // fourth octant + } + + // go to the next four octants + + oct0 ^= 0x07; oct1 ^= 0x07; oct2 ^= 0x07; oct3 ^= 0x07; + + OctreeDebugLogVerbose(@"----> testing back three octants [+%d] [+%d] [+%d]", oct1, oct2, oct3); + + if (octbuffer[nextlevel + oct1]) + { + moveLine = offsetForOctant( oct1, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct1, rd2, u0, u1, moveLine, 0)) return YES; // fifth octant + } + if (octbuffer[nextlevel + oct2]) + { + moveLine = offsetForOctant( oct2, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct2, rd2, u0, u1, moveLine, 0)) return YES; // sixth octant + } + if (octbuffer[nextlevel + oct3]) + { + moveLine = offsetForOctant( oct3, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct3, rd2, u0, u1, moveLine, 0)) return YES; // seventh octant + } + + // and check the last octant + OctreeDebugLogVerbose(@"----> testing final octant [+%d]", oct0); + + if (octbuffer[nextlevel + oct0]) + { + moveLine = offsetForOctant( oct0, rad); + if (isHitByLine(octbuffer, collbuffer, nextlevel + oct0, rd2, u0, u1, moveLine, 0)) return YES; // last octant + } + + return NO; +} + +- (GLfloat) isHitByLine: (Vector) v0: (Vector) v1 +{ + int i; + for (i = 0; i< leafs; i++) octree_collision[i] = (char)0; + hasCollided = NO; + + if (isHitByLine(octree, octree_collision, 0, radius, v0, v1, kZeroVector, 0)) + { + OctreeDebugLogVerbose(@"DEBUG Hit at distance %.2f", hit_dist); + hasCollision = hasCollided; + return hit_dist; + } + else + { + OctreeDebugLogVerbose(@"DEBUG Missed!", hit_dist); + hasCollision = hasCollided; + return 0.0; + } +} + +int change_oct[] = { 0, 1, 2, 4, 3, 5, 6, 7}; // used to move from nearest to furthest octant + +BOOL isHitByOctree( Octree_details axialDetails, + Octree_details otherDetails, Vector otherPosition, Triangle other_ijk) +{ + int* axialBuffer = axialDetails.octree; + int* otherBuffer = otherDetails.octree; + + if (axialBuffer[0] == 0) + { + OctreeDebugLogVerbose(@"DEBUG Axial octree is empty."); + return NO; + } + + if (!otherBuffer) + { + OctreeDebugLogVerbose(@"DEBUG Other octree is undefined."); + return NO; + } + + if (otherBuffer[0] == 0) + { + OctreeDebugLogVerbose(@"DEBUG Other octree is empty."); + return NO; + } + + GLfloat axialRadius = axialDetails.radius; + GLfloat otherRadius = otherDetails.radius; + + if (otherRadius < axialRadius) // test axial cube against other sphere + { + // 'crude and simple' - test sphere against cube... + if ((otherPosition.x + otherRadius < -axialRadius)||(otherPosition.x - otherRadius > axialRadius)|| + (otherPosition.y + otherRadius < -axialRadius)||(otherPosition.y - otherRadius > axialRadius)|| + (otherPosition.z + otherRadius < -axialRadius)||(otherPosition.z - otherRadius > axialRadius)) + { + OctreeDebugLogVerbose(@"----> Other sphere does not intersect axial cube"); + return NO; + } + } + else // test axial sphere against other cube + { + Vector d2 = make_vector( - otherPosition.x, - otherPosition.y, -otherPosition.z); + Vector axialPosition = resolveVectorInIJK( d2, other_ijk); + if ((axialPosition.x + axialRadius < -otherRadius)||(axialPosition.x - axialRadius > otherRadius)|| + (axialPosition.y + axialRadius < -otherRadius)||(axialPosition.y - axialRadius > otherRadius)|| + (axialPosition.z + axialRadius < -otherRadius)||(axialPosition.z - axialRadius > otherRadius)) + { + OctreeDebugLogVerbose(@"----> Axial sphere does not intersect other cube"); + return NO; + } + } + + // from here on, this Octree and the other Octree are considered to be intersecting + Octree_details nextDetails; // for subdivision (may not be required) + unsigned char* axialCollisionBuffer = axialDetails.octree_collision; + unsigned char* otherCollisionBuffer = otherDetails.octree_collision; + if (axialBuffer[0] == -1) + { + // we are SOLID - is the other octree? + if (otherBuffer[0] == -1) + { + // YES so octrees collide + axialCollisionBuffer[0] = (unsigned char)255; // mark + otherCollisionBuffer[0] = (unsigned char)255; // mark + + OctreeDebugLog(@"DEBUG Octrees collide!"); + return YES; + } + // the other octree must be decomposed + // and each of its octants tested against the axial octree + // if any of them collides with this octant + // then we have a solid collision + + OctreeDebugLogVerbose(@"----> testing other octants..."); + + // work out the nearest octant to the axial octree + int nearest_oct = ((otherPosition.x > 0.0)? 0:4)|((otherPosition.y > 0.0)? 0:2)|((otherPosition.z > 0.0)? 0:1); + + int nextLevel = otherBuffer[0]; + int* nextBuffer = &otherBuffer[nextLevel]; + unsigned char* nextCollisionBuffer = &otherCollisionBuffer[nextLevel]; + Vector voff, nextPosition; + nextDetails.radius = 0.5 * otherRadius; + int i, oct; + for (i = 0; i < 8; i++) + { + oct = nearest_oct ^ change_oct[i]; // work from nearest to furthest + if (nextBuffer[oct]) // don't test empty octants + { + nextDetails.octree = &nextBuffer[oct]; + nextDetails.octree_collision = &nextCollisionBuffer[oct]; + + voff = resolveVectorInIJK( offsetForOctant( oct, otherRadius), other_ijk); + nextPosition.x = otherPosition.x - voff.x; + nextPosition.y = otherPosition.y - voff.y; + nextPosition.z = otherPosition.z - voff.z; + if (isHitByOctree( axialDetails, nextDetails, nextPosition, other_ijk)) // test octant + return YES; + } + } + + // otherwise + return NO; + } + // we are not solid + // we must test each of our octants against + // the other octree, if any of them collide + // we have a solid collision + + OctreeDebugLogVerbose(@"----> testing axial octants..."); + + // work out the nearest octant to the other octree + int nearest_oct = ((otherPosition.x > 0.0)? 4:0)|((otherPosition.y > 0.0)? 2:0)|((otherPosition.z > 0.0)? 1:0); + + int nextLevel = axialBuffer[0]; + int* nextBuffer = &axialBuffer[nextLevel]; + unsigned char* nextCollisionBuffer = &axialCollisionBuffer[nextLevel]; + nextDetails.radius = 0.5 * axialRadius; + Vector voff, nextPosition; + int i, oct; + for (i = 0; i < 8; i++) + { + oct = nearest_oct ^ change_oct[i]; // work from nearest to furthest + if (nextBuffer[oct]) // don't test empty octants + { + nextDetails.octree = &nextBuffer[oct]; + nextDetails.octree_collision = &nextCollisionBuffer[oct]; + + voff = offsetForOctant(oct, axialRadius); + nextPosition.x = otherPosition.x + voff.x; + nextPosition.y = otherPosition.y + voff.y; + nextPosition.z = otherPosition.z + voff.z; + if (isHitByOctree( nextDetails, otherDetails, nextPosition, other_ijk)) + return YES; // test octant + } + } + // otherwise we're done! + return NO; +} + +- (BOOL) isHitByOctree:(Octree*) other withOrigin: (Vector) v0 andIJK: (Triangle) ijk +{ + if (other == nil) return NO; + + BOOL hit = isHitByOctree( [self octreeDetails], [other octreeDetails], v0, ijk); + + hasCollision = hasCollision | hit; + [other setHasCollision: [other hasCollision] | hit]; + + return hit; +} + +- (BOOL) isHitByOctree:(Octree*) other withOrigin: (Vector) v0 andIJK: (Triangle) ijk andScales: (GLfloat) s1: (GLfloat) s2 +{ + Octree_details details1 = [self octreeDetails]; + Octree_details details2 = [other octreeDetails]; + + details1.radius *= s1; + details2.radius *= s2; + + BOOL hit = isHitByOctree( details1, details2, v0, ijk); + + hasCollision = hasCollision | hit; + [other setHasCollision: [other hasCollision] | hit]; + + return hit; +} + + + +- (NSDictionary*) dict; +{ + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithFloat:radius], @"radius", + [NSNumber numberWithInt:leafs], @"leafs", + [NSData dataWithBytes:(const void *)octree length: leafs * sizeof(int)], @"octree", + nil]; +} + +- (GLfloat) volume +{ + return volumeOfOctree([self octreeDetails]); +} + +GLfloat volumeOfOctree(Octree_details octree_details) +{ + int* octBuffer = octree_details.octree; + GLfloat octRadius = octree_details.radius; + + if (octBuffer[0] == 0) + return 0.0f; + + if (octBuffer[0] == -1) + return octRadius * octRadius * octRadius; + + // we are not empty or solid + // we sum the volume of each of our octants + GLfloat sumVolume = 0.0f; + Octree_details nextDetails; // for subdivision (may not be required) + int nextLevel = octBuffer[0]; + int* nextBuffer = &octBuffer[nextLevel]; + nextDetails.radius = 0.5 * octRadius; + int i; + for (i = 0; i < 8; i++) + { + if (nextBuffer[i]) // don't test empty octants + { + nextDetails.octree = &nextBuffer[i]; + sumVolume += volumeOfOctree(nextDetails); + } + } + return sumVolume; +} + +Vector randomFullNodeFrom( Octree_details details, Vector offset) +{ + int* octBuffer = details.octree; + GLfloat octRadius = details.radius; + + if (octBuffer[0] == 0) + return offset; + + if (octBuffer[0] == -1) + return offset; + + // we are not empty or solid + // we sum the volume of each of our octants + Octree_details nextDetails; // for subdivision (may not be required) + int nextLevel = octBuffer[0]; + int* nextBuffer = &octBuffer[nextLevel]; + nextDetails.radius = 0.5 * octRadius; + int i, oct; + oct = Ranrot() & 7; + for (i = 0; i < 8; i++) + { + int octant = oct ^ i; + if (nextBuffer[octant]) // don't test empty octants + { + nextDetails.octree = &nextBuffer[octant]; + Vector voff = offsetForOctant( octant, octRadius); + voff.x += offset.x; + voff.y += offset.y; + voff.z += offset.z; + return randomFullNodeFrom( nextDetails, voff); + } + } + return offset; +} + +@end diff --git a/src/Core/OldSchoolPropertyListWriting.h b/src/Core/OldSchoolPropertyListWriting.h new file mode 100644 index 00000000..2bc0dcb4 --- /dev/null +++ b/src/Core/OldSchoolPropertyListWriting.h @@ -0,0 +1,56 @@ +/* + OldSchoolPropertyListWriting.h + Copyright 2006 Jens Ayton + + A protocol for writing property lists in the OpenStep/simple text format. Why? Because as of + Tiger, the system functions to do write plists reject the format. I, however, like it, because + it’s clear and legible. Fight the power! + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#import + + +@protocol OldSchoolPropertyListWriting + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription; + +@end + + +@interface NSObject (OldSchoolPropertyListWriting) + +- (NSData *)oldSchoolPListFormatWithErrorDescription:(NSString **)outErrorDescription; + +@end + + + +@interface NSString (OldSchoolPropertyListWriting) +@end + +@interface NSNumber (OldSchoolPropertyListWriting) +@end + +@interface NSData (OldSchoolPropertyListWriting) +@end + +@interface NSArray (OldSchoolPropertyListWriting) +@end + +@interface NSDictionary (OldSchoolPropertyListWriting) +@end diff --git a/src/Core/OldSchoolPropertyListWriting.m b/src/Core/OldSchoolPropertyListWriting.m new file mode 100644 index 00000000..602a58d9 --- /dev/null +++ b/src/Core/OldSchoolPropertyListWriting.m @@ -0,0 +1,339 @@ +/* + OldSchoolPropertyListWriting.m + Copyright 2006 Jens Ayton + + Permission is hereby granted, free of charge, to any person obtaining a copy of this software + and associated documentation files (the "Software"), to deal in the Software without + restriction, including without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all copies or + substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, + DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +#import "OldSchoolPropertyListWriting.h" + + +static BOOL IsFloatingPoint(NSNumber *number); +static void AppendNewLineAndIndent(NSMutableString *ioString, unsigned indentDepth); + + +@implementation NSString (OldSchoolPropertyListWriting) + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription +{ + NSCharacterSet *charSet; + NSRange foundRange, searchRange; + NSString *foundString; + NSMutableString *newString; + unsigned length; + + length = [self length]; + if (0 != length + && [self rangeOfCharacterFromSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]].location == NSNotFound + && ![[NSCharacterSet decimalDigitCharacterSet] longCharacterIsMember:[self characterAtIndex:0]]) + { + // This is an alphanumeric string whose first character is not a digit + return [[self copy] autorelease]; + } + else + { + charSet = [NSCharacterSet characterSetWithCharactersInString:@"\"\r\n\\"]; + foundRange = [self rangeOfCharacterFromSet:charSet options:NSLiteralSearch]; + if (NSNotFound == foundRange.location) + { + newString = (NSMutableString *)self; + } + else + { + // Escape quotes, backslashes and newlines + newString = [[[self substringToIndex:foundRange.location] mutableCopy] autorelease]; + + for (;;) + { + // Append escaped character + foundString = [self substringWithRange:foundRange]; + if ([foundString isEqual:@"\""]) [newString appendString:@"\\\""]; + else if ([foundString isEqual:@"\n"]) [newString appendString:@"\\\n"]; + else if ([foundString isEqual:@"\r"]) [newString appendString:@"\\\r"]; + else if ([foundString isEqual:@"\\"]) [newString appendString:@"\\\\"]; + else + { + [NSException raise:NSInternalInconsistencyException format:@"%s: expected \" or newline, found %@", __FUNCTION__, foundString]; + } + + // Use rest of string… + searchRange.location = foundRange.location + foundRange.length; + searchRange.length = length - searchRange.location; + + // …to search for next char needing escaping + foundRange = [self rangeOfCharacterFromSet:charSet options:NSLiteralSearch range:searchRange]; + if (NSNotFound == foundRange.location) + { + [newString appendString:[self substringWithRange:searchRange]]; + break; + } + } + } + + return [NSString stringWithFormat:@"\"%@\"", newString]; + } +} + +@end + + +@implementation NSNumber (OldSchoolPropertyListWriting) + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription +{ + NSString *result; + double dVal; + + + if (self == [NSNumber numberWithBool:YES]) result = @"true"; + else if ([NSNumber numberWithBool:NO]) result = @"false"; + else if (IsFloatingPoint(self)) + { + dVal = [self doubleValue]; + result = [NSString stringWithFormat:@"%.8g", dVal]; + } + else result = [NSString stringWithFormat:@"%@", self]; + + // Allow infinities, but remember that they’ll be read in as strings + /* + if ([result isEqual:@"inf"] || [result isEqual:@"-inf"]) + { + *outErrorDescription = @"infinities cannot be represented in old-school property lists"; + return nil; + } + */ + + return result; +} + +@end + + +@implementation NSData (OldSchoolPropertyListWriting) + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription +{ + const uint8_t *srcBytes; + uint8_t *dstBytes, *curr; + unsigned i, j, srcLength, dstLength; + const char hexTable[] = "0123456789ABCDEF"; + NSString *result; + + srcBytes = [self bytes]; + srcLength = [self length]; + + dstLength = 2 * srcLength + srcLength/8 + 2 + (srcLength/64 * (1 + inIndentation)); + + dstBytes = malloc(dstLength); + if (dstBytes == NULL) + { + if (NULL != outErrorDescription) + { + *outErrorDescription = [NSString stringWithFormat:@"failed to allocate space (%u bytes) for conversion of NSData to old-school property list representation", dstLength]; + } + return nil; + } + + curr = dstBytes; + *curr++ = '<'; + for (i = 0; i != srcLength; ++i) + { + if (0 != i && 0 == (i & 3)) + { + if (0 == (i & 31)) + { + *curr++ = '\n'; + j = inIndentation; + while (--j) *curr++ = '\t'; + } + *curr++ = ' '; + } + *curr++ = hexTable[srcBytes[i] >> 4]; + *curr++ = hexTable[srcBytes[i] & 0xF]; + } + *curr = '>'; + + assert((size_t)(curr - dstBytes) <= dstLength); + + result = [[NSString alloc] initWithBytesNoCopy:dstBytes length:dstLength encoding:NSASCIIStringEncoding freeWhenDone:YES]; + return [result autorelease]; +} + +@end + + +@implementation NSArray (OldSchoolPropertyListWriting) + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription +{ + NSMutableString *result; + unsigned i, count; + id object; + + result = [NSMutableString string]; + + [result appendString:@"("]; + + count = [self count]; + if (3 < count) AppendNewLineAndIndent(result, inIndentation + 1); + + for (i = 0; i != count; ++i) + { + if (0 != i) + { + if (count <= 3) [result appendString:@", "]; + else + { + [result appendString:@","]; + AppendNewLineAndIndent(result, inIndentation + 1); + } + } + + object = [self objectAtIndex:i]; + if (![object conformsToProtocol:@protocol (OldSchoolPropertyListWriting)]) + { + if (nil != object && NULL != outErrorDescription) + { + *outErrorDescription = [NSString stringWithFormat:@"non-plist object in dictionary"]; + } + return nil; + } + + object = [object oldSchoolPListFormatWithIndentation:inIndentation + 1 errorDescription:outErrorDescription]; + if (nil == object) return nil; + [result appendString:object]; + } + if (3 < count) AppendNewLineAndIndent(result, inIndentation); + [result appendString:@")"]; + return result; +} + +@end + + +@implementation NSDictionary (OldSchoolPropertyListWriting) + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription +{ + NSMutableString *result; + unsigned i, count; + NSArray *allKeys; + id key, value; + NSString *valueDesc; + + result = [NSMutableString string]; + allKeys = [self allKeys]; + count = [allKeys count]; + + [result appendString:@"{"]; + + if (2 < count || inIndentation == 0) AppendNewLineAndIndent(result, inIndentation + 1); + + for (i = 0; i != count; ++i) + { + if (0 != i) + { + if (count <= 2 && 0 != inIndentation) [result appendString:@" "]; + else AppendNewLineAndIndent(result, inIndentation + 1); + } + + key = [allKeys objectAtIndex:i]; + if (![key isKindOfClass:[NSString class]]) + { + if (NULL != outErrorDescription) *outErrorDescription = [NSString stringWithFormat:@"non-string key in dictionary"]; + return nil; + } + value = [self objectForKey:key]; + if (![value conformsToProtocol:@protocol(OldSchoolPropertyListWriting)]) + { + if (nil != value && NULL != outErrorDescription) + { + *outErrorDescription = [NSString stringWithFormat:@"non-plist object in dictionary"]; + } + return nil; + } + + key = [key oldSchoolPListFormatWithIndentation:inIndentation + 1 errorDescription:outErrorDescription]; + if (nil == key) return nil; + valueDesc = [value oldSchoolPListFormatWithIndentation:inIndentation + 1 errorDescription:outErrorDescription]; + if (nil == valueDesc) return nil; + + [result appendFormat:@"%@ =", key]; + if (([value isKindOfClass:[NSArray class]] && [value count] >= 3) || ([value isKindOfClass:[NSDictionary class]] && [value count] >= 2)) + { + AppendNewLineAndIndent(result, inIndentation + 1); + } + else + { + [result appendString:@" "]; + } + [result appendFormat:@"%@;", valueDesc]; + } + + if (2 < count || inIndentation == 0) AppendNewLineAndIndent(result, inIndentation); + [result appendString:@"}"]; + + return result; +} + +@end + + +@interface NSObject (OldSchoolPropertyListWriting_Private) + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription; + +@end + + +@implementation NSObject (OldSchoolPropertyListWriting) + +- (NSData *)oldSchoolPListFormatWithErrorDescription:(NSString **)outErrorDescription +{ + NSString *string; + + string = [self oldSchoolPListFormatWithIndentation:0 errorDescription:outErrorDescription]; + return [[string stringByAppendingString:@"\n"] dataUsingEncoding:NSUTF8StringEncoding]; +} + + +- (NSString *)oldSchoolPListFormatWithIndentation:(unsigned)inIndentation errorDescription:(NSString **)outErrorDescription +{ + if (NULL != outErrorDescription) + { + *outErrorDescription = [NSString stringWithFormat:@"Class %@ does not support OldSchoolPropertyListWriting", [self className]]; + } + return nil; +} + +@end + + +static BOOL IsFloatingPoint(NSNumber *number) +{ + const char *type = [number objCType]; + if (strcmp(type, @encode(float)) == 0) return YES; + if (strcmp(type, @encode(double)) == 0) return YES; + + return NO; +} + + +static void AppendNewLineAndIndent(NSMutableString *ioString, unsigned indentDepth) +{ + [ioString appendString:@"\n"]; + while (indentDepth--) [ioString appendString:@"\t"]; +} diff --git a/src/Core/OpenGLSprite.h b/src/Core/OpenGLSprite.h new file mode 100644 index 00000000..b0bacd89 --- /dev/null +++ b/src/Core/OpenGLSprite.h @@ -0,0 +1,50 @@ +/* + +OpenGLSprite.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" + +@class OOTexture; + + +#define OPEN_GL_SPRITE_MIN_WIDTH 64.0 +#define OPEN_GL_SPRITE_MIN_HEIGHT 64.0 + + +@interface OpenGLSprite: NSObject +{ + OOTexture *texture; + NSSize size; +} + + +- (id) initWithTexture:(OOTexture *)texture; +- (id) initWithTexture:(OOTexture *)texture size:(NSSize)spriteSize; + +- (NSSize)size; + +- (void)blitToX:(float)x Y:(float)y Z:(float)z alpha:(float)a; +- (void)blitCentredToX:(float)x Y:(float)y Z:(float)z alpha:(float)a; + +@end diff --git a/src/Core/OpenGLSprite.m b/src/Core/OpenGLSprite.m new file mode 100644 index 00000000..c528ad52 --- /dev/null +++ b/src/Core/OpenGLSprite.m @@ -0,0 +1,103 @@ +/* + +OpenGLSprite.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OpenGLSprite.h" +#import "OOTexture.h" +#import "OOMaths.h" + + +@implementation OpenGLSprite + +- (id)initWithTexture:(OOTexture *)inTexture +{ + return [self initWithTexture:inTexture size:[inTexture dimensions]]; +} + + +- (id)initWithTexture:(OOTexture *)inTexture size:(NSSize)spriteSize +{ + if (inTexture == nil) + { + [self release]; + return nil; + } + + self = [super init]; + if (self != nil) + { + texture = [inTexture retain]; + size = spriteSize; + } + return self; +} + + +- (void)dealloc +{ + [texture release]; + + [super dealloc]; +} + +- (NSSize)size +{ + return size; +} + + +- (void)blitToX:(float)x Y:(float)y Z:(float)z alpha:(float)a +{ + a = OOClamp_0_1_f(a); + glEnable(GL_TEXTURE_2D); + glColor4f(1.0, 1.0, 1.0, a); + + // Note that the textured Quad is drawn ACW from the Top Left + + [texture apply]; + glBegin(GL_QUADS); + + glTexCoord2f(0.0, 0.0); + glVertex3f(x, y+size.height, z); + + glTexCoord2f(0.0, 1.0); + glVertex3f(x, y, z); + + glTexCoord2f(1.0, 1.0); + glVertex3f(x+size.width, y, z); + + glTexCoord2f(1.0, 0.0); + glVertex3f(x+size.width, y+size.height, z); + + glEnd(); + glDisable(GL_TEXTURE_2D); +} + +- (void)blitCentredToX:(float)x Y:(float)y Z:(float)z alpha:(float)a +{ + float xs = x - size.width / 2.0; + float ys = y - size.height / 2.0; + [self blitToX:xs Y:ys Z:z alpha:a]; +} + +@end diff --git a/src/Core/ReleaseLockProxy.h b/src/Core/ReleaseLockProxy.h new file mode 100644 index 00000000..b77da957 --- /dev/null +++ b/src/Core/ReleaseLockProxy.h @@ -0,0 +1,31 @@ +/* ReleaseLockProxy.h + By Jens Ayton + This code is hereby placed in the public domain. + + Hacky debug utility. + A ReleaseLockProxy proxies an object, and stops the proxied object from + being released until -rlpAllowRelease is called. All releases are logged, + and one that would cause the object to die is stopped. Breakpoints in + release may be handy. +*/ + +#import + + +@interface ReleaseLockProxy: NSProxy +{ + id _object; + NSString *_name; + BOOL _locked; +} + ++ (id)proxyWithObject:(id)object name:(NSString *)name; ++ (id)proxyWithRetainedObject:(id)object name:(NSString *)name; // Doesn't retain the object + +- (id)initWithObject:(id)object name:(NSString *)name; +- (id)initWithRetainedObject:(id)object name:(NSString *)name; // Doesn't retain the object + +- (void)rlpAllowRelease; +- (NSString *)rlpObjectDescription; // name if not nil, otherwise object description. + +@end diff --git a/src/Core/ReleaseLockProxy.m b/src/Core/ReleaseLockProxy.m new file mode 100644 index 00000000..51d0ae7a --- /dev/null +++ b/src/Core/ReleaseLockProxy.m @@ -0,0 +1,192 @@ +/* ReleaseLockProxy.h + By Jens Ayton + This code is hereby placed in the public domain. +*/ + +#import "ReleaseLockProxy.h" + + +#define VERBOSE 0 + +#if VERBOSE +#define VerboseLog NSLog +#else +#define VerboseLog(...) do {} while (0) +#endif + + +@interface ReleaseLockProxy_SignatureTemplateClass: NSObject + +// These exist purely to provide NSMethodSignatures for -[ReleaseLockProxy methodSignatureForSelector:]. ++ (id)initWithObject:(id)object name:(NSString *)name; ++ (id)initWithRetainedObject:(id)object name:(NSString *)name; ++ (void)rlpAllowRelease; ++ (NSString *)rlpObjectDescription; + +@end + + +@implementation ReleaseLockProxy + +// *** Boilerplate + ++ (id)proxyWithObject:(id)object name:(NSString *)name +{ + return [[[self alloc] initWithObject:object name:name] autorelease]; +} + + ++ (id)proxyWithRetainedObject:(id)object name:(NSString *)name +{ + return [[[self alloc] initWithRetainedObject:object name:name] autorelease]; +} + + +- (id)initWithObject:(id)object name:(NSString *)name +{ + return [self initWithRetainedObject:[object retain] name:name]; +} + + +- (id)initWithRetainedObject:(id)object name:(NSString *)name +{ + if (object == nil) + { + NSLog(@"** ReleaseLockProxy: passed nil object, returning nil proxy."); + [self release]; + return nil; + } + + // No super init for proxies. + + _object = object; + _name = [name copy]; + _locked = YES; + + return self; +} + + +- (void)dealloc +{ + if (_locked) + { + NSLog(@"** ReleaseLockProxy (%@): deallocated while locked. This shouldn't happen, unless -dealloc is being called directly.", [self rlpObjectDescription]); + } + else + { + VerboseLog(@"-- ReleaseLockProxy (%@): deallocated while not locked.", [self rlpObjectDescription]); + } + + [_object release]; + [_name release]; + + [super dealloc]; +} + + +- (void)rlpAllowRelease +{ + _locked = NO; +} + + +- (NSString *)rlpObjectDescription +{ + return _name ? _name : [_object description]; +} + + +// *** Core functionality + +- (void)release +{ + unsigned retainCount = [self retainCount]; + + if (_locked && retainCount == 1) + { + // Breakpoint here to catch what would otherwise be the last release before crashing. + NSLog(@"** ReleaseLockProxy (%@): released while locked and retain count is one - intercepting retain. Something is broken.", [self rlpObjectDescription]); + return; + } + + if (_locked) + { + VerboseLog(@"-- ReleaseLockProxy (%@): released while locked, but retain count > 1; retain count going from %u to %u.", [self rlpObjectDescription], retainCount, retainCount - 1); + } + else + { + VerboseLog(@"-- ReleaseLockProxy (%@): released while not locked; retain count going from %u to %u.", [self rlpObjectDescription], retainCount, retainCount - 1); + } + + [super release]; +} + + +- (id)autorelease +{ + VerboseLog(@"-- ReleaseLockProxy (%@): autoreleased while %slocked and retain count at %u.", [self rlpObjectDescription], _locked ? "" : "not ", [self retainCount]); + return [super autorelease]; +} + + +// *** Proxy stuff. + +- (void)forwardInvocation:(NSInvocation *)invocation +{ + [invocation invokeWithTarget:_object]; +} + + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector +{ + NSMethodSignature *result = nil; + + if (selector == @selector(initWithObject:name:) || + selector == @selector(initWithRetainedObject:name:) || + selector == @selector(rlpAllowRelease) || + selector == @selector(rlpObjectDescription)) + { + result = [ReleaseLockProxy_SignatureTemplateClass methodSignatureForSelector:selector]; + } + else + { + result = [(id)_object methodSignatureForSelector:selector]; + } + + return result; +} + + ++ (BOOL)instancesRespondToSelector:(SEL)selector +{ + if (selector == @selector(initWithObject:name:) || + selector == @selector(initWithRetainedObject:name:) || + selector == @selector(rlpAllowRelease) || + selector == @selector(rlpObjectDescription)) + { + return YES; + } + else + { + return NO; + } +} + + +- (BOOL)respondsToSelector:(SEL)selector +{ + return [ReleaseLockProxy instancesRespondToSelector:selector]; +} + +@end + + +@implementation ReleaseLockProxy_SignatureTemplateClass + ++ (id)initWithObject:(id)object name:(NSString *)name { return nil; } ++ (id)initWithRetainedObject:(id)object name:(NSString *)name { return nil; } ++ (void)rlpAllowRelease { } ++ (NSString *)rlpObjectDescription { return nil; } + +@end diff --git a/src/Core/ResourceManager.h b/src/Core/ResourceManager.h new file mode 100644 index 00000000..555a544e --- /dev/null +++ b/src/Core/ResourceManager.h @@ -0,0 +1,85 @@ +/* + +ResourceManager.h + +Singleton class responsible for loading various data files. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" + + +#define OOLITE_EXCEPTION_FATAL @"OoliteFatalException" + +@class OOSound, OOMusic; + + +typedef enum +{ + MERGE_NONE, // Just use the last file in search order. + MERGE_BASIC, // Merge files by adding the top-level items of each file. + MERGE_SMART // Merge files by merging the top-level elements of each file (second-order merge, but not recursive) +} OOResourceMergeMode; + + +@interface ResourceManager : NSObject + ++ (NSArray *)rootPaths; // Places add-ons are searched for, not including add-on paths. ++ (NSString *)builtInPath; // Path for built-in data only. ++ (NSArray *)pathsWithAddOns; // Root paths + add-on paths. ++ (NSArray *)paths; // builtInPath or pathsWithAddOns, depending on useAddOns state. ++ (BOOL)useAddOns; ++ (void)setUseAddOns:(BOOL)useAddOns; ++ (void)addExternalPath:(NSString *)fileName; ++ (NSEnumerator *)pathEnumerator; ++ (NSEnumerator *)reversePathEnumerator; + ++ (void)handleEquipmentListMerging: (NSMutableArray *)arrayToProcess forLookupIndex:(unsigned)lookupIndex; + ++ (NSString *)errors; // Errors which occured during path scanning - essentially a list of OXPs whose requires.plist is bad. + ++ (NSString *) pathForFileNamed:(NSString *)fileName inFolder:(NSString *)folderName; + ++ (NSDictionary *)dictionaryFromFilesNamed:(NSString *)fileName + inFolder:(NSString *)folderName + andMerge:(BOOL) mergeFiles; ++ (NSDictionary *)dictionaryFromFilesNamed:(NSString *)fileName + inFolder:(NSString *)folderName + mergeMode:(OOResourceMergeMode)mergeMode + cache:(BOOL)cache; + ++ (NSArray *)arrayFromFilesNamed:(NSString *)fileName + inFolder:(NSString *)folderName + andMerge:(BOOL) mergeFiles; + ++ (NSDictionary *) whitelistDictionary; // method-whitelist.plist, explicitly not merged like normal plists. + ++ (OOSound *)ooSoundNamed:(NSString *)fileName inFolder:(NSString *)folderName; ++ (OOMusic *)ooMusicNamed:(NSString *)fileName inFolder:(NSString *)folderName; + ++ (NSString *)stringFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName; + ++ (NSDictionary *)loadScripts; + ++ (BOOL) writeDiagnosticData:(NSData *)data toFileNamed:(NSString *)name; + +@end diff --git a/src/Core/ResourceManager.m b/src/Core/ResourceManager.m new file mode 100644 index 00000000..e20e13e1 --- /dev/null +++ b/src/Core/ResourceManager.m @@ -0,0 +1,894 @@ +/* + +ResourceManager.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "ResourceManager.h" +#import "NSScannerOOExtensions.h" +#import "NSMutableDictionaryOOExtensions.h" +#import "OOSound.h" +#import "OOCacheManager.h" +#import "Universe.h" +#import "OOStringParsing.h" +#import "OOPListParsing.h" +#import "MyOpenGLView.h" +#import "OOCollectionExtractors.h" +#import "OOLogOutputHandler.h" + +#import "OOJSScript.h" +#import "OOPListScript.h" + +#define kOOLogUnconvertedNSLog @"unclassified.ResourceManager" + + +static NSString * const kOOLogCacheUpToDate = @"dataCache.upToDate"; +static NSString * const kOOLogCacheExplicitFlush = @"dataCache.rebuild.explicitFlush"; +static NSString * const kOOLogCacheStalePaths = @"dataCache.rebuild.pathsChanged"; +static NSString * const kOOLogCacheStaleDates = @"dataCache.rebuild.datesChanged"; +static NSString * const kOOCacheSearchPathModDates = @"search path modification dates"; +static NSString * const kOOCacheKeySearchPaths = @"search paths"; +static NSString * const kOOCacheKeyModificationDates = @"modification dates"; + + +extern NSDictionary* ParseOOSScripts(NSString* script); + + +@interface ResourceManager (OOPrivate) + ++ (void)checkPotentialPath:(NSString *)path :(NSMutableArray *)searchPaths; ++ (BOOL)areRequirementsFulfilled:(NSDictionary*)requirements forOXP:(NSString *)path; ++ (void) addErrorWithKey:(NSString *)descriptionKey param1:(id)param1 param2:(id)param2; ++ (void)checkCacheUpToDateForPaths:(NSArray *)searchPaths; ++ (NSString *) diagnosticFileLocation; + +@end + + +static NSMutableArray *sSearchPaths; +static BOOL sUseAddOns = YES; +static NSMutableArray *sExternalPaths; +static NSMutableArray *sErrors; + +// caches allow us to load any given file once only +// +static NSMutableDictionary *sound_cache; +static NSMutableDictionary *string_cache; + + +@implementation ResourceManager + ++ (NSString *) errors +{ + NSArray *error = nil; + unsigned i, count; + NSMutableArray *result = nil; + NSString *errStr = nil; + + count = [sErrors count]; + if (count == 0) return nil; + + // Expand error messages. This is deferred for localizability. + result = [NSMutableArray arrayWithCapacity:count]; + for (i = 0; i != count; ++i) + { + error = [sErrors objectAtIndex:i]; + errStr = [UNIVERSE descriptionForKey:[error stringAtIndex:0]]; + if (errStr != nil) + { + errStr = [NSString stringWithFormat:errStr, [error objectAtIndex:1], [error objectAtIndex:2]]; + [result addObject:errStr]; + } + } + + [sErrors release]; + sErrors = nil; + + return [result componentsJoinedByString:@"\n"]; +} + + ++ (NSArray *)rootPaths +{ + static NSArray *sRootPaths = nil; + + if (sRootPaths == nil) + { +#if OOLITE_MAC_OS_X + NSString *app_addon_path = [[[[NSBundle mainBundle] bundlePath] + stringByDeletingLastPathComponent] + stringByAppendingPathComponent:@"AddOns"]; + NSString *appsupport_path = [[[[NSHomeDirectory() + stringByAppendingPathComponent:@"Library"] + stringByAppendingPathComponent:@"Application Support"] + stringByAppendingPathComponent:@"Oolite"] + stringByAppendingPathComponent:@"AddOns"]; +#elif OOLITE_WINDOWS + NSString *app_addon_path = @"../AddOns"; + NSString *appsupport_path = nil; +#else + NSString *app_addon_path = @"AddOns"; + NSString *appsupport_path = nil; +#endif + NSString *nix_path = nil; +#if !OOLITE_WINDOWS + /* Enabling this path in Windows causes a log message, and it's not + actually useful. + */ + nix_path = [[NSHomeDirectory() + stringByAppendingPathComponent:@".Oolite"] + stringByAppendingPathComponent:@"AddOns"]; +#endif + + sRootPaths = [[NSArray alloc] initWithObjects:[self builtInPath], app_addon_path, nix_path, appsupport_path, nil]; + } + + return sRootPaths; +} + + ++ (NSString *)builtInPath +{ +#if OOLITE_WINDOWS + /* [[NSBundle mainBundle] resourcePath] causes complaints under Windows, + because we don't have a properly-built bundle. + */ + return @"Resources"; +#else + return [[NSBundle mainBundle] resourcePath]; +#endif +} + + ++ (NSArray *)pathsWithAddOns +{ + if (sSearchPaths != nil) return sSearchPaths; + + [sErrors release]; + sErrors = nil; + + NSFileManager *fmgr = [NSFileManager defaultManager]; + NSArray *rootPaths = nil; + NSEnumerator *pathEnum = nil; + NSString *root = nil; + NSDirectoryEnumerator *dirEnum = nil; + NSString *subPath = nil; + NSString *path = nil; + BOOL isDirectory; + + rootPaths = [self rootPaths]; + sSearchPaths = [rootPaths mutableCopy]; + + // Iterate over root paths + for (pathEnum = [rootPaths objectEnumerator]; (root = [pathEnum nextObject]); ) + { + // Iterate over each root path's contents + if ([fmgr fileExistsAtPath:root isDirectory:&isDirectory] && isDirectory) + { + for (dirEnum = [fmgr enumeratorAtPath:root]; (subPath = [dirEnum nextObject]); ) + { + // Check if it's a directory + path = [root stringByAppendingPathComponent:subPath]; + if ([fmgr fileExistsAtPath:path isDirectory:&isDirectory] && isDirectory) + { + // If it is, is it an OXP? + if ([[[path pathExtension] lowercaseString] isEqualToString:@"oxp"]) + { + [self checkPotentialPath:path :sSearchPaths]; + } + else + { + // If not, don't search subdirectories + [dirEnum skipDescendents]; + } + } + } + } + } + + for (pathEnum = [sExternalPaths objectEnumerator]; (path = [pathEnum nextObject]); ) + { + [self checkPotentialPath:path :sSearchPaths]; + } + + OOLog(@"searchPaths.dumpAll", @"---> OXP search paths:\n%@", sSearchPaths); + [self checkCacheUpToDateForPaths:sSearchPaths]; + + return sSearchPaths; +} + + ++ (NSArray *)paths +{ + return sUseAddOns ? [self pathsWithAddOns] : (NSArray *)[NSArray arrayWithObject:[self builtInPath]]; +} + + ++ (BOOL)useAddOns +{ + return sUseAddOns; +} + + ++ (void)setUseAddOns:(BOOL)useAddOns +{ + useAddOns = (useAddOns != 0); + if (sUseAddOns != useAddOns) + { + sUseAddOns = useAddOns; + [self checkCacheUpToDateForPaths:[self paths]]; + } +} + + ++ (void) addExternalPath:(NSString *)path +{ + if (sSearchPaths == nil) sSearchPaths = [[NSMutableArray alloc] init]; + if (![sSearchPaths containsObject:path]) + { + [sSearchPaths addObject:path]; + + if (sExternalPaths == nil) sExternalPaths = [[NSMutableArray alloc] init]; + [sExternalPaths addObject:path]; + } +} + + ++ (NSEnumerator *)pathEnumerator +{ + return [[self paths] objectEnumerator]; +} + + ++ (NSEnumerator *)reversePathEnumerator +{ + return [[self paths] reverseObjectEnumerator]; +} + + +// Given a path to an assumed OXP (or other location where files are permissible), check for a requires.plist and add to search paths if acceptable. ++ (void)checkPotentialPath:(NSString *)path :(NSMutableArray *)searchPaths +{ + NSDictionary *requirements = nil; + BOOL requirementsMet; + + requirements = OODictionaryFromFile([path stringByAppendingPathComponent:@"requires.plist"]); + requirementsMet = [self areRequirementsFulfilled:requirements forOXP:path]; + + if (requirementsMet) [searchPaths addObject:path]; + else + { + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; + OOLog(@"oxp.versionMismatch", @"OXP %@ is incompatible with version %@ of Oolite.", path, version); + [self addErrorWithKey:@"oxp-is-incompatible" param1:[path lastPathComponent] param2:version]; + } +} + + ++ (BOOL) areRequirementsFulfilled:(NSDictionary*)requirements forOXP:(NSString *)path +{ + BOOL OK = YES; + NSString *requiredVersion = nil; + NSString *maxVersion = nil; + unsigned conditionsHandled = 0; + static NSArray *ooVersionComponents = nil; + NSArray *oxpVersionComponents = nil; + + if (requirements == nil) return YES; + + if (ooVersionComponents == nil) + { + ooVersionComponents = ComponentsFromVersionString([[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]); + [ooVersionComponents retain]; + } + + // Check "version" (minimum version) + if (OK) + { + // Not stringForKey:, because we need to be able to complain about non-strings. + requiredVersion = [requirements objectForKey:@"version"]; + if (requiredVersion != nil) + { + ++conditionsHandled; + if ([requiredVersion isKindOfClass:[NSString class]]) + { + oxpVersionComponents = ComponentsFromVersionString(requiredVersion); + if (NSOrderedAscending == CompareVersions(ooVersionComponents, oxpVersionComponents)) OK = NO; + } + else + { + OOLog(@"requirements.wrongType", @"Expected requires.plist entry \"%@\" to be string, but got %@ in OXP %@.", @"version", [requirements class], [path lastPathComponent]); + OK = NO; + } + } + } + + // Check "max_version" (minimum max_version) + if (OK) + { + // Not stringForKey:, because we need to be able to complain about non-strings. + maxVersion = [requirements objectForKey:@"max_version"]; + if (maxVersion != nil) + { + ++conditionsHandled; + if ([maxVersion isKindOfClass:[NSString class]]) + { + oxpVersionComponents = ComponentsFromVersionString(maxVersion); + if (NSOrderedDescending == CompareVersions(ooVersionComponents, oxpVersionComponents)) OK = NO; + } + else + { + OOLog(@"requirements.wrongType", @"Expected requires.plist entry \"%@\" to be string, but got %@ in OXP %@.", @"max_version", [requirements class], [path lastPathComponent]); + OK = NO; + } + } + } + + if (OK && conditionsHandled < [requirements count]) + { + // There are unknown requirement keys - don't support. NOTE: this check was not made pre 1.69! + OOLog(@"requirements.unknown", @"requires.plist for OXP %@ contains unknown keys, rejecting.", [path lastPathComponent]); + OK = NO; + } + + return OK; +} + + ++ (void) addErrorWithKey:(NSString *)descriptionKey param1:(id)param1 param2:(id)param2 +{ + if (descriptionKey != nil) + { + if (sErrors == nil) sErrors = [[NSMutableArray alloc] init]; + [sErrors addObject:[NSArray arrayWithObjects:descriptionKey, param1 ?: (id)@"", param2 ?: (id)@"", nil]]; + } +} + + ++ (void)checkCacheUpToDateForPaths:(NSArray *)searchPaths +{ + /* Check if caches are up to date. + The strategy is to use a two-entry cache. One entry is an array + containing the search paths, the other an array of modification dates + (in the same order). If either fails to match the correct settings, + we delete both. + */ + OOCacheManager *cacheMgr = [OOCacheManager sharedCache]; + NSFileManager *fmgr = [NSFileManager defaultManager]; + BOOL upToDate = YES; + id oldPaths = nil; + NSMutableArray *modDates = nil; + NSEnumerator *pathEnum = nil; + NSString *path = nil; + id modDate = nil; + + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"always-flush-cache"]) + { + OOLog(kOOLogCacheExplicitFlush, @"Cache explicitly flushed with always-flush-cache preference. Rebuilding from scratch."); + upToDate = NO; + } + + if (upToDate && [[UNIVERSE gameView] pollShiftKey]) + { + OOLog(kOOLogCacheExplicitFlush, @"Cache explicitly flushed with shift key. Rebuilding from scratch."); + upToDate = NO; + } + + oldPaths = [cacheMgr objectForKey:kOOCacheKeySearchPaths inCache:kOOCacheSearchPathModDates]; + if (upToDate && ![oldPaths isEqual:searchPaths]) + { + // OXPs added/removed + if (oldPaths != nil) OOLog(kOOLogCacheStalePaths, @"Cache is stale (search paths have changed). Rebuilding from scratch."); + upToDate = NO; + } + + // Build modification date list. (We need this regardless of whether the search paths matched.) + + modDates = [NSMutableArray arrayWithCapacity:[searchPaths count]]; + for (pathEnum = [searchPaths objectEnumerator]; (path = [pathEnum nextObject]); ) + { + modDate = [[fmgr fileAttributesAtPath:path traverseLink:YES] objectForKey:NSFileModificationDate]; + if (modDate != nil) + { + // Converts to double because I'm not sure the cache can deal with dates under GNUstep. + modDate = [NSNumber numberWithDouble:[modDate timeIntervalSince1970]]; + [modDates addObject:modDate]; + } + } + + if (upToDate && ![[cacheMgr objectForKey:kOOCacheKeyModificationDates inCache:kOOCacheSearchPathModDates] isEqual:modDates]) + { + OOLog(kOOLogCacheStaleDates, @"Cache is stale (modification dates have changed). Rebuilding from scratch."); + upToDate = NO; + } + + if (!upToDate) + { + [cacheMgr clearAllCaches]; + [cacheMgr setObject:searchPaths forKey:kOOCacheKeySearchPaths inCache:kOOCacheSearchPathModDates]; + [cacheMgr setObject:modDates forKey:kOOCacheKeyModificationDates inCache:kOOCacheSearchPathModDates]; + } + else OOLog(kOOLogCacheUpToDate, @"Data cache is up to date."); +} + + ++ (NSDictionary *)dictionaryFromFilesNamed:(NSString *)fileName + inFolder:(NSString *)folderName + andMerge:(BOOL) mergeFiles +{ + return [ResourceManager dictionaryFromFilesNamed:fileName inFolder:folderName mergeMode:mergeFiles ? MERGE_BASIC : MERGE_NONE cache:YES]; +} + + ++ (NSDictionary *)dictionaryFromFilesNamed:(NSString *)fileName + inFolder:(NSString *)folderName + mergeMode:(OOResourceMergeMode)mergeMode + cache:(BOOL)cache +{ + id result = nil; + NSMutableArray *results = nil; + NSString *cacheKey = nil; + NSString *mergeType = nil; + OOCacheManager *cacheMgr = [OOCacheManager sharedCache]; + NSEnumerator *enumerator = nil; + NSString *path = nil; + NSString *dictPath = nil; + NSDictionary *dict = nil; + + if (fileName == nil) return nil; + + switch (mergeMode) + { + case MERGE_NONE: + mergeType = @"none"; + break; + + case MERGE_BASIC: + mergeType = @"basic"; + break; + + case MERGE_SMART: + mergeType = @"smart"; + break; + + default: + OOLog(kOOLogParameterError, @"Unknown dictionary merge mode %u for %@. (This is an internal programming error, please report it.)", fileName, mergeMode); + return nil; + } + + if (cache) + { + + if (folderName != nil) + { + cacheKey = [NSString stringWithFormat:@"%@/%@ merge:%@", folderName, fileName, mergeType]; + } + else + { + cacheKey = [NSString stringWithFormat:@"%@ merge:%@", fileName, mergeType]; + } + result = [cacheMgr objectForKey:cacheKey inCache:@"dictionaries"]; + if (result != nil) return result; + } + + if (mergeMode == MERGE_NONE) + { + // Find "last" matching dictionary + for (enumerator = [ResourceManager reversePathEnumerator]; (path = [enumerator nextObject]); ) + { + if (folderName != nil) + { + dictPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName]; + dict = OODictionaryFromFile(dictPath); + if (dict != nil) break; + } + dictPath = [path stringByAppendingPathComponent:fileName]; + dict = OODictionaryFromFile(dictPath); + if (dict != nil) break; + } + result = dict; + } + else + { + // Find all matching dictionaries + results = [NSMutableArray array]; + for (enumerator = [ResourceManager pathEnumerator]; (path = [enumerator nextObject]); ) + { + dictPath = [path stringByAppendingPathComponent:fileName]; + dict = OODictionaryFromFile(dictPath); + if (dict != nil) [results addObject:dict]; + if (folderName != nil) + { + dictPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName]; + dict = OODictionaryFromFile(dictPath); + if (dict != nil) [results addObject:dict]; + } + } + + if ([results count] == 0) return nil; + + // Merge result + result = [NSMutableDictionary dictionary]; + + for (enumerator = [results objectEnumerator]; (dict = [enumerator nextObject]); ) + { + if (mergeMode == MERGE_SMART) [result mergeEntriesFromDictionary:dict]; + else [result addEntriesFromDictionary:dict]; + } + result = [[result copy] autorelease]; // Make immutable + } + + if (cache && result != nil) [cacheMgr setObject:result forKey:cacheKey inCache:@"dictionaries"]; + + return result; +} + + ++ (NSArray *) arrayFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName andMerge:(BOOL) mergeFiles +{ + id result = nil; + NSMutableArray *results = nil; + NSString *cacheKey = nil; + OOCacheManager *cache = [OOCacheManager sharedCache]; + NSEnumerator *enumerator = nil; + NSString *path = nil; + NSString *arrayPath = nil; + NSMutableArray *array = nil; + NSArray *arrayNonEditable = nil; + + if (fileName == nil) return nil; + + cacheKey = [NSString stringWithFormat:@"%@%@ merge:%@", (folderName != nil) ? [folderName stringByAppendingString:@"/"] : (NSString *)@"", fileName, mergeFiles ? @"yes" : @"no"]; + result = [cache objectForKey:cacheKey inCache:@"arrays"]; + if (result != nil) return result; + + if (!mergeFiles) + { + // Find "last" matching array + for (enumerator = [ResourceManager reversePathEnumerator]; (path = [enumerator nextObject]); ) + { + if (folderName != nil) + { + arrayPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName]; + arrayNonEditable = OOArrayFromFile(arrayPath); + if (arrayNonEditable != nil) break; + } + arrayPath = [path stringByAppendingPathComponent:fileName]; + arrayNonEditable = OOArrayFromFile(arrayPath); + if (arrayNonEditable != nil) break; + } + result = arrayNonEditable; + } + else + { + // Find all matching arrays + results = [NSMutableArray array]; + for (enumerator = [ResourceManager pathEnumerator]; (path = [enumerator nextObject]); ) + { + arrayPath = [path stringByAppendingPathComponent:fileName]; + array = [[OOArrayFromFile(arrayPath) mutableCopy] autorelease]; + if (array != nil) [results addObject:array]; + + // Special handling for arrays merging. Currently, equipment.plist only gets its objects merged. + // A lookup index is required. For the equipment.plist items, this is the index corresponging to the + // EQ_* string, which describes the role of an equipment item and is unique. + if (array != nil && [[array objectAtIndex:0] isKindOfClass:[NSArray class]]) + { + if ([[fileName lowercaseString] isEqualToString:@"equipment.plist"]) + [self handleEquipmentListMerging:results forLookupIndex:3]; // Index 3 is the role string (EQ_*). + } + if (folderName != nil) + { + arrayPath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName]; + array = [[OOArrayFromFile(arrayPath) mutableCopy] autorelease]; + if (array != nil) [results addObject:array]; + + if (array != nil && [[array objectAtIndex:0] isKindOfClass:[NSArray class]]) + { + if ([[fileName lowercaseString] isEqualToString:@"equipment.plist"]) + [self handleEquipmentListMerging:results forLookupIndex:3]; // Index 3 is the role string (EQ_*). + } + } + } + + if ([results count] == 0) return nil; + + // Merge result + result = [NSMutableArray array]; + + for (enumerator = [results objectEnumerator]; (array = [enumerator nextObject]); ) + { + [result addObjectsFromArray:array]; + } + result = [[result copy] autorelease]; // Make immutable + } + + if (result != nil) [cache setObject:result forKey:cacheKey inCache:@"arrays"]; + + return [NSArray arrayWithArray:result]; +} + + +// A method for handling merging of arrays. Currently used with the equipment.plist entries. +// The arrayToProcess array is scanned for repetitions of the item at lookup index location and, if found, +// the latest entry replaces the earliest. ++ (void) handleEquipmentListMerging: (NSMutableArray *)arrayToProcess forLookupIndex:(unsigned)lookupIndex +{ + unsigned i,j,k; + NSMutableArray *refArray = [arrayToProcess objectAtIndex:[arrayToProcess count] - 1]; + + // Any change to arrayRef will directly modify arrayToProcess. + + for (i = 0; i < [refArray count]; i++) + { + for (j = 0; j < [arrayToProcess count] - 1; j++) + { + for (k=0; k < [[arrayToProcess objectAtIndex:j] count] - 1; k++) + { + if ([[[[arrayToProcess objectAtIndex:j] objectAtIndex:k] objectAtIndex:lookupIndex] isEqual: + [[refArray objectAtIndex:i] objectAtIndex:lookupIndex]]) + { + [[arrayToProcess objectAtIndex:j] replaceObjectAtIndex:k withObject:[refArray objectAtIndex:i]]; + [refArray removeObjectAtIndex:i]; + } + } + } + } + // arrayToProcess has been processed at this point. Any necessary merging has been done. +} + + ++ (NSDictionary *) whitelistDictionary +{ + static id whitelistDictionary = nil; + NSString *path = nil; + + if (whitelistDictionary == nil) + { + path = [[[ResourceManager builtInPath] stringByAppendingPathComponent:@"Config"] stringByAppendingPathComponent:@"whitelist.plist"]; + whitelistDictionary = [NSDictionary dictionaryWithContentsOfFile:path]; + if (whitelistDictionary == nil) whitelistDictionary = [NSNull null]; + + [whitelistDictionary retain]; + } + + if (whitelistDictionary == [NSNull null]) return nil; + return whitelistDictionary; +} + + ++ (NSString *) pathForFileNamed:(NSString *)fileName inFolder:(NSString *)folderName +{ + NSString *result = nil; + NSString *cacheKey = nil; + OOCacheManager *cache = [OOCacheManager sharedCache]; + NSEnumerator *pathEnum = nil; + NSString *path = nil; + NSString *filePath = nil; + NSFileManager *fmgr = nil; + + if (fileName == nil) return nil; + + if (folderName != nil) cacheKey = [NSString stringWithFormat:@"%@/%@", folderName, fileName]; + else cacheKey = fileName; + result = [cache objectForKey:cacheKey inCache:@"resolved paths"]; + if (result != nil) return result; + + // Search for file + fmgr = [NSFileManager defaultManager]; + for (pathEnum = [[ResourceManager paths] reverseObjectEnumerator]; (path = [pathEnum nextObject]); ) + { + filePath = [[path stringByAppendingPathComponent:folderName] stringByAppendingPathComponent:fileName]; + if ([fmgr fileExistsAtPath:filePath]) + { + result = filePath; + break; + } + + filePath = [path stringByAppendingPathComponent:fileName]; + if ([fmgr fileExistsAtPath:filePath]) + { + result = filePath; + break; + } + } + + if (result != nil) + { + OOLog(@"resourceManager.foundFile", @"Found %@/%@ at %@", folderName, fileName, filePath); + [cache setPruneThreshold:500 forCache:@"resolved paths"]; + [cache setObject:result forKey:cacheKey inCache:@"resolved paths"]; + } + return result; +} + + ++ (id) retrieveFileNamed:(NSString *)fileName inFolder:(NSString *)folderName cache:(NSMutableDictionary **)ioCache key:(NSString *)key class:(Class)class +{ + id result = nil; + NSString *path = nil; + + if (ioCache) + { + if (key == nil) key = [NSString stringWithFormat:@"%@:%@", folderName, fileName]; + if (*ioCache != nil) + { + // return the cached object, if any + result = [*ioCache objectForKey:key]; + if (result) return result; + } + } + + path = [self pathForFileNamed:fileName inFolder:folderName]; + if (path != nil) result = [[[class alloc] initWithContentsOfFile:path] autorelease]; + + if (result != nil && ioCache != NULL) + { + if (*ioCache == nil) *ioCache = [[NSMutableDictionary alloc] init]; + [*ioCache setObject:result forKey:key]; + } + + return result; +} + + ++ (OOMusic *) ooMusicNamed:(NSString *)fileName inFolder:(NSString *)folderName +{ + return [self retrieveFileNamed:fileName + inFolder:folderName + cache:NULL // Don't cache music objects; minimizing latency isn't really important. + key:[NSString stringWithFormat:@"OOMusic:%@:%@", folderName, fileName] + class:[OOMusic class]]; +} + + ++ (OOSound *) ooSoundNamed:(NSString *)fileName inFolder:(NSString *)folderName +{ + return [self retrieveFileNamed:fileName + inFolder:folderName + cache:&sound_cache + key:[NSString stringWithFormat:@"OOSound:%@:%@", folderName, fileName] + class:[OOSound class]]; +} + ++ (NSString *) stringFromFilesNamed:(NSString *)fileName inFolder:(NSString *)folderName +{ + return [self retrieveFileNamed:fileName + inFolder:folderName + cache:&string_cache + key:nil + class:[NSString class]]; +} + + ++ (NSDictionary *)loadScripts +{ + NSMutableDictionary *loadedScripts = nil; + NSArray *results = nil; + NSArray *paths = nil; + NSEnumerator *pathEnum = nil; + NSString *path = nil; + NSEnumerator *scriptEnum = nil; + OOScript *script = nil; + NSString *name = nil; + NSAutoreleasePool *pool = nil; + + loadedScripts = [NSMutableDictionary dictionary]; + paths = [ResourceManager paths]; + for (pathEnum = [paths objectEnumerator]; (path = [pathEnum nextObject]); ) + { + pool = [[NSAutoreleasePool alloc] init]; + + NS_DURING + results = [OOScript worldScriptsAtPath:[path stringByAppendingPathComponent:@"Config"]]; + if (results == nil) results = [OOScript worldScriptsAtPath:path]; + if (results != nil) + { + for (scriptEnum = [results objectEnumerator]; (script = [scriptEnum nextObject]); ) + { + name = [script name]; + if (name != nil) [loadedScripts setObject:script forKey:name]; + else OOLog(@"script.load.unnamed", @"Discarding anonymous script %@", script); + } + } + NS_HANDLER + OOLog(@"script.load.exception", @"***** %s encountered exception %@ (%@) while trying to load script from %@ -- ignoring this location.", __FUNCTION__, [localException name], [localException reason], path); + // Ignore exception and keep loading other scripts. + NS_ENDHANDLER + + [pool release]; + } + + if (OOLogWillDisplayMessagesInClass(@"script.load.world.listAll")) + { + unsigned count = [loadedScripts count]; + if (count != 0) + { + NSMutableArray *displayNames = nil; + NSEnumerator *scriptEnum = nil; + OOScript *script = nil; + NSString *displayString = nil; + + displayNames = [NSMutableArray arrayWithCapacity:count]; + + for (scriptEnum = [loadedScripts objectEnumerator]; (script = [scriptEnum nextObject]); ) + { + [displayNames addObject:[script displayName]]; + } + + displayString = [[displayNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)] componentsJoinedByString:@", "]; + OOLog(@"script.load.world.listAll", @"Loaded %u world scripts: %@", count, displayString); + } + else + { + OOLog(@"script.load.world.listAll", @"*** No world scripts loaded."); + } + } + + return loadedScripts; +} + + ++ (BOOL) writeDiagnosticData:(NSData *)data toFileNamed:(NSString *)name +{ + NSString *basePath = nil; + + if (data == nil || name == nil) return NO; + + basePath = [self diagnosticFileLocation]; + if (basePath == nil) return NO; + + return [data writeToFile:[basePath stringByAppendingPathComponent:name] atomically:YES]; +} + + ++ (BOOL)directoryExists:(NSString *)inPath create:(BOOL)inCreate +{ + BOOL exists, directory; + NSFileManager *fmgr = [NSFileManager defaultManager]; + + exists = [fmgr fileExistsAtPath:inPath isDirectory:&directory]; + + if (exists && !directory) + { + OOLog(@"resourceManager.write.buildPath.failed", @"Expected %@ to be a folder, but it is a file.", inPath); + return NO; + } + if (!exists) + { + if (!inCreate) return NO; + if (![fmgr createDirectoryAtPath:inPath attributes:nil]) + { + OOLog(@"resourceManager.write.buildPath.failed", @"Could not create folder %@.", inPath); + return NO; + } + } + + return YES; +} + + ++ (NSString *) diagnosticFileLocation +{ + return OOLogHandlerGetLogBasePath(); +} + +@end diff --git a/src/Core/Scripting/EntityOOJavaScriptExtensions.h b/src/Core/Scripting/EntityOOJavaScriptExtensions.h new file mode 100644 index 00000000..83a27e21 --- /dev/null +++ b/src/Core/Scripting/EntityOOJavaScriptExtensions.h @@ -0,0 +1,59 @@ +/* + +EntityOOJavaScriptExtensions.h + +JavaScript support methods for entities. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "Entity.h" +#import "OOJavaScriptEngine.h" + + +@interface Entity (OOJavaScriptExtensions) + +- (BOOL)isVisibleToScripts; + +- (NSString *)jsClassName; + +// Internal: +- (void)getJSClass:(JSClass **)outClass andPrototype:(JSObject **)outPrototype; +- (void)deleteJSSelf; + +@end + + +@interface ShipEntity (OOJavaScriptExtensions) + +// "Normal" subentities, excluding flashers and exhaust plumes. +- (NSArray *)subEntitiesForScript; + +- (void)setTargetForScript:(ShipEntity *)target; + +@end + + +@interface PlayerEntity (OOJavaScriptExtensions) + +- (void)setJSSelf:(JSObject *)val context:(JSContext *)context; + +@end diff --git a/src/Core/Scripting/EntityOOJavaScriptExtensions.m b/src/Core/Scripting/EntityOOJavaScriptExtensions.m new file mode 100644 index 00000000..70d7d0e5 --- /dev/null +++ b/src/Core/Scripting/EntityOOJavaScriptExtensions.m @@ -0,0 +1,138 @@ +/* + +EntityOOJavaScriptExtensions.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "EntityOOJavaScriptExtensions.h" +#import "OOJSEntity.h" +#import "OOJSShip.h" +#import "OOJSStation.h" +#import "StationEntity.h" +#import "PlanetEntity.h" + + +@implementation Entity (OOJavaScriptExtensions) + +- (BOOL) isVisibleToScripts +{ + return NO; +} + + +- (NSString *) jsClassName +{ + return @"Entity"; +} + + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + JSClass *class = NULL; + JSObject *prototype = NULL; + jsval result = JSVAL_NULL; + + if (jsSelf == NULL && [self isVisibleToScripts]) + { + // Create JS object + [self getJSClass:&class andPrototype:&prototype]; + + jsSelf = JS_NewObject(context, class, prototype, NULL); + if (jsSelf != NULL) + { + if (!JS_SetPrivate(context, jsSelf, [self weakRetain])) jsSelf = NULL; + } + + if (jsSelf != NULL) JS_AddNamedRoot(context, &jsSelf, "Entity jsSelf"); + } + + if (jsSelf != NULL) result = OBJECT_TO_JSVAL(jsSelf); + + return result; +} + + +- (void) getJSClass:(JSClass **)outClass andPrototype:(JSObject **)outPrototype +{ + *outClass = JSEntityClass(); + *outPrototype = JSEntityPrototype(); +} + + +- (void) deleteJSSelf +{ + if (jsSelf != NULL) + { + [[OOJavaScriptEngine sharedEngine] removeGCRoot:&jsSelf]; + jsSelf = NULL; + } +} + +@end + + +@implementation ShipEntity (OOJavaScriptExtensions) + +- (BOOL) isVisibleToScripts +{ + return YES; +} + + +- (void) getJSClass:(JSClass **)outClass andPrototype:(JSObject **)outPrototype +{ + *outClass = JSShipClass(); + *outPrototype = JSShipPrototype(); +} + + +- (NSString *) jsClassName +{ + return @"Ship"; +} + + +- (NSArray *) subEntitiesForScript +{ + return [[self shipSubEntityEnumerator] allObjects]; +} + + +- (void) setTargetForScript:(ShipEntity *)target +{ + ShipEntity *me = self; + + // Ensure coherence by not fiddling with subentities + while ([me isSubEntity]) { + if (me == [me owner] || [me owner] == nil) break; + me = (ShipEntity *)[me owner]; + } + while ([target isSubEntity]) { + if (target == [target owner] || [target owner] == nil) break; + target = (ShipEntity *)[target owner]; + } + if (![me isKindOfClass:[ShipEntity class]]) return; + if (target != nil) [me addTarget:target]; + else [me removeTarget:[me primaryTarget]]; +} + +@end diff --git a/src/Core/Scripting/OOJSCall.h b/src/Core/Scripting/OOJSCall.h new file mode 100644 index 00000000..11216824 --- /dev/null +++ b/src/Core/Scripting/OOJSCall.h @@ -0,0 +1,46 @@ +/* + +OOJSCall.h + +Basic JavaScript-to-ObjC bridge implementation. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJavaScriptEngine.h" + + +/* OOJSCallObjCObjectMethod() + + Function for implementing JavaScript call() methods. + + The argument list is expected to be either a single string (selector), or + a string ending with a : followed by arbitrary arguments which will be + concatenated as a string. (This behaviour reflects Oolite's traditional + scripting system and the expectations of its script methods. It also has + the advantage that it can be done with GNUstep's buggy implementation of + NSMethodSignature.) + + If the method returns an object, *outResult will be set to that object's + -javaScriptValueInContext:. Otherwise, it will be left unchanged. + + argv is assumed to contain at least one value. +*/ +BOOL OOJSCallObjCObjectMethod(JSContext *context, id object, NSString *jsClassName, uintN argc, jsval *argv, jsval *outResult); diff --git a/src/Core/Scripting/OOJSCall.m b/src/Core/Scripting/OOJSCall.m new file mode 100644 index 00000000..ff924616 --- /dev/null +++ b/src/Core/Scripting/OOJSCall.m @@ -0,0 +1,182 @@ +/* + +OOJSCall.h + +Basic JavaScript-to-ObjC bridge implementation. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSCall.h" + +#import "OOFunctionAttributes.h" +#import "ShipEntity.h" +#import "OOCollectionExtractors.h" + + +typedef enum +{ + kMethodTypeVoidVoid, + kMethodTypeObjectVoid, + kMethodTypeVoidObject, + kMethodTypeObjectObject, + kMethodTypeInvalid +} MethodType; + + +static MethodType GetMethodType(id object, SEL selector); +OOINLINE BOOL MethodExpectsParameter(MethodType type) { return type == kMethodTypeVoidObject || type == kMethodTypeObjectObject; } +OOINLINE BOOL MethodReturnsObject(MethodType type) { return type == kMethodTypeObjectVoid || type == kMethodTypeObjectObject; } + + +BOOL OOJSCallObjCObjectMethod(JSContext *context, id object, NSString *jsClassName, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *selectorString = nil; + SEL selector = NULL; + NSString *paramString = nil; + MethodType type; + BOOL haveParameter = NO, + success = NO; + id result = nil; + + if ([object isKindOfClass:[ShipEntity class]]) + { + [[PlayerEntity sharedPlayer] setScriptTarget:object]; + } + + selectorString = JSValToNSString(context,argv[0]); + + // Join all parameters together with spaces. + if (1 < argc && [selectorString hasSuffix:@":"]) + { + haveParameter = YES; + paramString = [NSString concatenationOfStringsFromJavaScriptValues:argv + 1 count:argc - 1 separator:@" " inContext:context]; + } + + selector = NSSelectorFromString(selectorString); + if ([object respondsToSelector:selector]) + { + // Validate signature. + type = GetMethodType(object, selector); + + if (type == kMethodTypeInvalid) + { + OOReportJSError(context, @"%@.call(): method %@ cannot be called from JavaScript.", jsClassName, selectorString); + } + else if (MethodExpectsParameter(type) && !haveParameter) + { + OOReportJSError(context, @"%@.call(): method %@ requires a parameter.", jsClassName, selectorString); + } + else + { + // Method is acceptable. + if (haveParameter) + { + OOLog(@"script.trace.javaScript.call", @"%@.call(%@, \"%@\")", jsClassName, selectorString, paramString); + OOLogIndentIf(@"script.trace.javaScript.call"); + + result = [object performSelector:selector withObject:paramString]; + + OOLogOutdentIf(@"script.trace.javaScript.call"); + } + else + { + OOLog(@"script.trace.javaScript.call", @"%@.call(%@)", jsClassName, selectorString); + OOLogIndentIf(@"script.trace.javaScript.call"); + + result = [object performSelector:selector]; + + OOLogOutdentIf(@"script.trace.javaScript.call"); + } + success = YES; + + if (MethodReturnsObject(type) && outResult != NULL) + { + if ([selectorString hasSuffix:@"_bool"]) result = [NSNumber numberWithBool:OOBooleanFromObject(result, NO)]; + *outResult = [result javaScriptValueInContext:context]; + } + } + } + else + { + OOReportJSError(context, @"%@.call(): %@ does not respond to method %@.", jsClassName, object, selectorString); + } + + return success; +} + + +// Template class providing method signature strings for the four signatures we support. +@interface OOJSCallMethodSignatureTemplateClass: NSObject + +- (void)voidVoidMethod; +- (id)objectVoidMethod; +- (void)voidObjectMethod:(id)object; +- (id)objectObjectMethod:(id)object; + +@end + + +static BOOL SignatureMatch(NSMethodSignature *sig, SEL selector) +{ + NSMethodSignature *template = nil; + + template = [OOJSCallMethodSignatureTemplateClass instanceMethodSignatureForSelector:selector]; + return [sig isEqual:template]; +} + + +static MethodType GetMethodType(id object, SEL selector) +{ + NSMethodSignature *sig = [object methodSignatureForSelector:selector]; + + if (SignatureMatch(sig, @selector(voidVoidMethod))) return kMethodTypeVoidVoid; + if (SignatureMatch(sig, @selector(objectVoidMethod))) return kMethodTypeObjectVoid; + if (SignatureMatch(sig, @selector(voidObjectMethod:))) return kMethodTypeVoidObject; + if (SignatureMatch(sig, @selector(objectObjectMethod:))) return kMethodTypeObjectObject; + + return kMethodTypeInvalid; +} + + +@implementation OOJSCallMethodSignatureTemplateClass: NSObject + +- (void)voidVoidMethod +{ +} + + +- (id)objectVoidMethod +{ + return nil; +} + + +- (void)voidObjectMethod:(id)object +{ +} + + +- (id)objectObjectMethod:(id)object +{ + return nil; +} + +@end diff --git a/src/Core/Scripting/OOJSClock.h b/src/Core/Scripting/OOJSClock.h new file mode 100644 index 00000000..4dd691d6 --- /dev/null +++ b/src/Core/Scripting/OOJSClock.h @@ -0,0 +1,31 @@ +/* + +OOJSClock.h + +JavaScript clock global object. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + + +void InitOOJSClock(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSClock.m b/src/Core/Scripting/OOJSClock.m new file mode 100644 index 00000000..7434d056 --- /dev/null +++ b/src/Core/Scripting/OOJSClock.m @@ -0,0 +1,206 @@ +/* + +OOJSClock.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSClock.h" +#import "OOJavaScriptEngine.h" +#import "Universe.h" +#import "OOJSPlayer.h" +#import "PlayerEntity.h" +#import "PlayerEntityScriptMethods.h" +#import "OOStringParsing.h" + + +// Minimum allowable interval for repeating timers. +#define kMinInterval 0.25 + + +static JSBool ClockGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); + +// Methods +static JSBool JSClockToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ClockClockStringForTime(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSClass sClockClass = +{ + "Clock", + JSCLASS_HAS_PRIVATE, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + ClockGetProperty, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JS_FinalizeStub, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS +}; + + +enum +{ + // Property IDs + kClock_absoluteSeconds, // game real time clock, double, read-only + kClock_seconds, // game clock time, double, read-only + kClock_minutes, // game clock time minutes (rounded down), integer double, read-only + kClock_hours, // game clock time hours (rounded down), integer double, read-only + kClock_days, // game clock time days (rounded down), int, read-only + kClock_secondsComponent, // second component of game clock time, double, read-only + kClock_minutesComponent, // minute component of game clock time (rounded down), int, read-only + kClock_hoursComponent, // hour component of game clock time (rounded down), int, read-only + kClock_daysComponent, // day component of game clock time (rounded down), int, read-only + kClock_clockString, // game clock time as display string, string, read-only + kClock_isAdjusting, // clock is adjusting, boolean, read-only + kClock_legacy_scriptTimer // legacy scriptTimer_number, double, read-only +}; + + +static JSPropertySpec sClockProperties[] = +{ + // JS name ID flags + { "absoluteSeconds", kClock_absoluteSeconds, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "seconds", kClock_seconds, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "minutes", kClock_minutes, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "hours", kClock_hours, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "days", kClock_days, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "secondsComponent", kClock_secondsComponent, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "minutesComponent", kClock_minutesComponent, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "hoursComponent", kClock_hoursComponent, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "daysComponent", kClock_daysComponent, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "clockString", kClock_clockString, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isAdjusting", kClock_isAdjusting, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "legacy_scriptTimer", kClock_legacy_scriptTimer, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sClockMethods[] = +{ + // JS name Function min args + { "toString", JSClockToString, 0 }, + { "clockStringForTime", ClockClockStringForTime, 1 }, + { 0 } +}; + + +void InitOOJSClock(JSContext *context, JSObject *global) +{ + JSObject *clockPrototype = JS_InitClass(context, global, NULL, &sClockClass, NULL, 0, sClockProperties, sClockMethods, NULL, NULL); + JS_DefineObject(context, global, "clock", &sClockClass, clockPrototype, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +static JSBool ClockGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + PlayerEntity *player = nil; + double clockTime; + + player = OOPlayerForScripting(); + clockTime = [player clockTime]; + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kClock_absoluteSeconds: + OK = JS_NewDoubleValue(context, [UNIVERSE getTime], outValue); + break; + + case kClock_seconds: + OK = JS_NewDoubleValue(context, clockTime, outValue); + break; + + case kClock_minutes: + OK = JS_NewDoubleValue(context, floor(clockTime / 60.0), outValue); + break; + + case kClock_hours: + OK = JS_NewDoubleValue(context, floor(clockTime / 3600.0), outValue); + break; + + case kClock_secondsComponent: + *outValue = INT_TO_JSVAL(fmod(clockTime, 60.0)); + OK = YES; + break; + + case kClock_minutesComponent: + *outValue = INT_TO_JSVAL(fmod(floor(clockTime / 60.0), 60.0)); + OK = YES; + break; + + case kClock_hoursComponent: + *outValue = INT_TO_JSVAL(fmod(floor(clockTime / 3600.0), 24.0)); + OK = YES; + break; + + case kClock_days: + case kClock_daysComponent: + *outValue = INT_TO_JSVAL(floor(clockTime / 86400.0)); + OK = YES; + break; + + case kClock_clockString: + *outValue = [[player dial_clock] javaScriptValueInContext:context]; + OK = YES; + break; + + case kClock_isAdjusting: + *outValue = BOOLToJSVal([player clockAdjusting]); + OK = YES; + break; + + case kClock_legacy_scriptTimer: + OK = JS_NewDoubleValue(context, [OOPlayerForScripting() scriptTimer], outValue); + break; + + default: + OOReportJSBadPropertySelector(context, @"Clock", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Methods *** + +// toString() : String +static JSBool JSClockToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + *outResult = [[OOPlayerForScripting() dial_clock] javaScriptValueInContext:context]; + return YES; +} + + +// clockStringForTime(time : Number) : String +static JSBool ClockClockStringForTime(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + double time; + + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[0], &time))) return NO; + + *outResult = [ClockToString(time, NO) javaScriptValueInContext:context]; + return YES; +} diff --git a/src/Core/Scripting/OOJSEntity.h b/src/Core/Scripting/OOJSEntity.h new file mode 100644 index 00000000..eaa25f9f --- /dev/null +++ b/src/Core/Scripting/OOJSEntity.h @@ -0,0 +1,54 @@ +/* + +OOJSEntity.h + +JavaScript proxy for entities. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOJavaScriptEngine.h" + +@class Entity; + + +void InitOOJSEntity(JSContext *context, JSObject *global); + +BOOL JSValueToEntity(JSContext *context, jsval value, Entity **outEntity); // Value may be Entity or integer (OOUniversalID). + +DEFINE_JS_OBJECT_GETTER(JSEntityGetEntity, Entity) + +JSClass *JSEntityClass(void); +JSObject *JSEntityPrototype(void); + + +/* EntityFromArgumentList() + + Construct a entity from an argument list which is either a (JS) entity or + an integer (a OOUniversalID). The optional outConsumed argument can be + used to find out how many parameters were used (currently, this will be 0 + on failure, otherwise 1). + + On failure, it will return NO, annd the entity will be unaltered. If + scriptClass and function are non-nil, a warning will be reported to the + log. +*/ +BOOL EntityFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, Entity **outEntity, uintN *outConsumed); diff --git a/src/Core/Scripting/OOJSEntity.m b/src/Core/Scripting/OOJSEntity.m new file mode 100644 index 00000000..f67b689d --- /dev/null +++ b/src/Core/Scripting/OOJSEntity.m @@ -0,0 +1,392 @@ +/* + +OOJSEntity.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSEntity.h" +#import "OOJSVector.h" +#import "OOJSQuaternion.h" +#import "OOJavaScriptEngine.h" +#import "OOConstToString.h" +#import "EntityOOJavaScriptExtensions.h" +#import "OOJSCall.h" + +#import "OOJSPlayer.h" +#import "PlayerEntity.h" + + +static JSObject *sEntityPrototype; + + +static JSBool EntityGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool EntitySetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +// Static methods +static JSBool EntityStaticEntityWithID(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sEntityClass = +{ + { + "Entity", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + EntityGetProperty, // getProperty + EntitySetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kEntity_ID, // universalID, int, read-only + kEntity_position, // position in system space, Vector, read/write + kEntity_orientation, // orientation, quaternion, read/write + kEntity_heading, // heading, vector, read-only (like orientation but ignoring twist angle) + kEntity_status, // entity status, string, read-only + kEntity_scanClass, // scan class, string, read-only + kEntity_mass, // mass, double, read-only + kEntity_owner, // owner, Entity, read-only. (Parent ship for subentities, station for defense ships, launching ship for missiles etc) + kEntity_energy, // energy, double, read-write. + kEntity_maxEnergy, // maxEnergy, double, read-only. + kEntity_isValid, // is not stale, boolean, read-only. + kEntity_isShip, // is ship, boolean, read-only. + kEntity_isStation, // is station, boolean, read-only. + kEntity_isSubEntity, // is subentity, boolean, read-only. + kEntity_isPlayer, // is player, boolean, read-only. + kEntity_isPlanet, // is planet, boolean, read-only. + kEntity_isSun, // is sun, boolean, read-only. + kEntity_distanceTravelled, // distance travelled, double, read-only. + kEntity_spawnTime, // spawn time, double, read-only. +}; + + +static JSPropertySpec sEntityProperties[] = +{ + // JS name ID flags + { "ID", kEntity_ID, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "position", kEntity_position, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "orientation", kEntity_orientation, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "heading", kEntity_heading, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "status", kEntity_status, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "scanClass", kEntity_scanClass, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "mass", kEntity_mass, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "owner", kEntity_owner, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "energy", kEntity_energy, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "maxEnergy", kEntity_maxEnergy, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isValid", kEntity_isValid, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isShip", kEntity_isShip, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isStation", kEntity_isStation, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isSubEntity", kEntity_isSubEntity, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPlayer", kEntity_isPlayer, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPlanet", kEntity_isPlanet, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isSun", kEntity_isSun, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "distanceTravelled", kEntity_distanceTravelled, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "spawnTime", kEntity_spawnTime, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sEntityMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0 }, + { 0 } +}; + + +static JSFunctionSpec sEntityStaticMethods[] = +{ + // JS name Function min args + { "entityWithID", EntityStaticEntityWithID, 1 }, + { 0 } +}; + + +void InitOOJSEntity(JSContext *context, JSObject *global) +{ + sEntityPrototype = JS_InitClass(context, global, NULL, &sEntityClass.base, NULL, 0, sEntityProperties, sEntityMethods, NULL, sEntityStaticMethods); + JSRegisterObjectConverter(&sEntityClass.base, JSBasicPrivateObjectConverter); +} + + +BOOL JSValueToEntity(JSContext *context, jsval value, Entity **outEntity) +{ + Entity *entity = nil; + + if (outEntity == NULL) return NO; + + if (JSVAL_IS_OBJECT(value)) + { + return JSEntityGetEntity(context, JSVAL_TO_OBJECT(value), outEntity); + } + else if (JSVAL_IS_INT(value)) // Should we accept general numbers? (Currently, UniversalIDs are clamped to [100, 1000].) + { + OOReportJSWarning(context, @"The ability to pass an entity ID instead of an entity is deprecated and will be removed in a future version of Oolite."); + entity = [UNIVERSE entityForUniversalID:JSVAL_TO_INT(value)]; + if (entity && [entity isVisibleToScripts]) + { + *outEntity = [[entity retain] autorelease]; + return YES; + } + } + + return NO; +} + + +JSClass *JSEntityClass(void) +{ + return &sEntityClass.base; +} + + +JSObject *JSEntityPrototype(void) +{ + return sEntityPrototype; +} + + +BOOL EntityFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, Entity **outEntity, uintN *outConsumed) +{ + // Sanity checks. + if (outConsumed != NULL) *outConsumed = 0; + if (EXPECT_NOT(argc == 0 || argv == NULL || outEntity == NULL)) + { + OOLogGenericParameterError(); + return NO; + } + + // Get value, if possible. + if (EXPECT_NOT(!JSValueToEntity(context, argv[0], outEntity))) + { + // Failed; report bad parameters, if given a class and function. + if (scriptClass != nil && function != nil) + { + OOReportJSWarning(context, @"%@.%@(): expected entity or universal ID, got %@.", scriptClass, function, [NSString stringWithJavaScriptParameters:argv count:1 inContext:context]); + return NO; + } + } + + // Success. + if (outConsumed != NULL) *outConsumed = 1; + return YES; +} + + +static JSBool EntityGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + Entity *entity = nil; + id result = nil; + + if (!JSVAL_IS_INT(name)) return YES; + JSEntityGetEntity(context, this, &entity); + if (entity == nil) + { + if (JSVAL_TO_INT(name) == kEntity_isValid) *outValue = JSVAL_FALSE; + else *outValue = JSVAL_VOID; + + return YES; + } + + switch (JSVAL_TO_INT(name)) + { + case kEntity_ID: + OOReportJSWarning(context, @"The property Entity.ID is deprecated and will be removed in a future version of Oolite."); + *outValue = INT_TO_JSVAL([entity universalID]); + OK = YES; + break; + + case kEntity_position: + OK = VectorToJSValue(context, [entity position], outValue); + break; + + case kEntity_orientation: + OK = QuaternionToJSValue(context, [entity normalOrientation], outValue); + break; + + case kEntity_heading: + OK = VectorToJSValue(context, vector_forward_from_quaternion([entity normalOrientation]), outValue); + break; + + case kEntity_status: + result = EntityStatusToString([entity status]); + break; + + case kEntity_scanClass: + result = ScanClassToString([entity scanClass]); + break; + + case kEntity_mass: + OK = JS_NewDoubleValue(context, [entity mass], outValue); + break; + + case kEntity_owner: + result = [entity owner]; + if (result == entity) result = nil; + if (result == nil) result = [NSNull null]; + break; + + case kEntity_energy: + OK = JS_NewDoubleValue(context, [entity energy], outValue); + break; + + case kEntity_maxEnergy: + OK = JS_NewDoubleValue(context, [entity maxEnergy], outValue); + break; + + case kEntity_isValid: + *outValue = JSVAL_TRUE; + OK = YES; + break; + + case kEntity_isShip: + *outValue = BOOLToJSVal([entity isShip]); + OK = YES; + break; + + case kEntity_isStation: + *outValue = BOOLToJSVal([entity isStation]); + OK = YES; + break; + + case kEntity_isSubEntity: + *outValue = BOOLToJSVal([entity isSubEntity]); + OK = YES; + break; + + case kEntity_isPlayer: + *outValue = BOOLToJSVal([entity isPlayer]); + OK = YES; + break; + + case kEntity_isPlanet: + *outValue = BOOLToJSVal([entity isPlanet] && ![entity isSun]); + OK = YES; + break; + + case kEntity_isSun: + *outValue = BOOLToJSVal([entity isSun]); + OK = YES; + break; + + case kEntity_distanceTravelled: + OK = JS_NewDoubleValue(context, [entity distanceTravelled], outValue); + break; + + case kEntity_spawnTime: + OK = JS_NewDoubleValue(context, [entity spawnTime], outValue); + break; + + default: + OOReportJSBadPropertySelector(context, @"Entity", JSVAL_TO_INT(name)); + } + + if (result != nil) + { + *outValue = [result javaScriptValueInContext:context]; + OK = YES; + } + return OK; +} + + +static JSBool EntitySetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + Entity *entity = nil; + double fValue; + Vector vValue; + Quaternion qValue; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSEntityGetEntity(context, this, &entity))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kEntity_position: + if (JSValueToVector(context, *value, &vValue)) + { + [entity setPosition:vValue]; + OK = YES; + } + break; + + case kEntity_orientation: + if (JSValueToQuaternion(context, *value, &qValue)) + { + [entity setNormalOrientation:qValue]; + OK = YES; + } + break; + + case kEntity_energy: + if (JS_ValueToNumber(context, *value, &fValue)) + { + fValue = OOClamp_0_max_d(fValue, [entity maxEnergy]); + [entity setEnergy:fValue]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"Entity", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Static methods *** + +// entityWithID(ID : Number) : Entity +static JSBool EntityStaticEntityWithID(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Entity *result = nil; + int32 ID; + + OOReportJSWarning(context, @"The function Entity.entityWithID() is deprecated and will be removed in a future version of Oolite."); + + if (EXPECT_NOT(!JS_ValueToInt32(context, *argv, &ID))) + { + OOReportJSBadArguments(context, @"Entity", @"entityWithID", argc, argv, @"Invalid entity ID", @"integer"); + return NO; + } + + result = [UNIVERSE entityForUniversalID:ID]; + if (result != nil) *outResult = [result javaScriptValueInContext:context]; + else *outResult = JSVAL_NULL; + return YES; +} diff --git a/src/Core/Scripting/OOJSEquipmentInfo.h b/src/Core/Scripting/OOJSEquipmentInfo.h new file mode 100644 index 00000000..21e10eac --- /dev/null +++ b/src/Core/Scripting/OOJSEquipmentInfo.h @@ -0,0 +1,31 @@ +/* + +OOJSEquipmentInfo.h + +JavaScript equipment introspection class, wrapper for OOEquipmentType. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + + +void InitOOJSEquipmentInfo(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSEquipmentInfo.m b/src/Core/Scripting/OOJSEquipmentInfo.m new file mode 100644 index 00000000..f444c025 --- /dev/null +++ b/src/Core/Scripting/OOJSEquipmentInfo.m @@ -0,0 +1,311 @@ +/* + +OOJSEquipmentInfo.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSEquipmentInfo.h" +#import "OOJavaScriptEngine.h" +#import "OOEquipmentType.h" +#import "OOJSPlayer.h" + + +static JSObject *sEquipmentInfoPrototype; + + +static JSBool EquipmentInfoGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool EquipmentInfoSetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); + +// Methods +static JSBool EquipmentInfoStaticInfoForKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +enum +{ + // Property IDs + kEquipmentInfo_equipmentKey, + kEquipmentInfo_name, + kEquipmentInfo_description, + kEquipmentInfo_techLevel, + kEquipmentInfo_effectiveTechLevel, + kEquipmentInfo_price, + kEquipmentInfo_isAvailableToAll, + kEquipmentInfo_requiresEmptyPylon, + kEquipmentInfo_requiresMountedPylon, + kEquipmentInfo_requiresCleanLegalRecord, + kEquipmentInfo_requiresNonCleanLegalRecord, + kEquipmentInfo_requiresFreePassengerBerth, + kEquipmentInfo_requiresFullFuel, + kEquipmentInfo_requiresNonFullFuel, + kEquipmentInfo_isExternalStore, // is missile or mine + kEquipmentInfo_isPortableBetweenShips, + kEquipmentInfo_requiredCargoSpace, + kEquipmentInfo_requiresEquipment, + kEquipmentInfo_requiresAnyEquipment, + kEquipmentInfo_incompatibleEquipment +}; + + +static JSPropertySpec sEquipmentInfoProperties[] = +{ + // JS name ID flags + { "equipmentKey", kEquipmentInfo_equipmentKey, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "name", kEquipmentInfo_name, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "description", kEquipmentInfo_description, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "techLevel", kEquipmentInfo_techLevel, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "effectiveTechLevel", kEquipmentInfo_effectiveTechLevel, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "price", kEquipmentInfo_price, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isAvailableToAll", kEquipmentInfo_isAvailableToAll, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresEmptyPylon", kEquipmentInfo_requiresEmptyPylon, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresMountedPylon", kEquipmentInfo_requiresMountedPylon, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresCleanLegalRecord", kEquipmentInfo_requiresCleanLegalRecord, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresNonCleanLegalRecord", kEquipmentInfo_requiresNonCleanLegalRecord, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresFreePassengerBerth", kEquipmentInfo_requiresFreePassengerBerth, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresFullFuel", kEquipmentInfo_requiresFullFuel, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresNonFullFuel", kEquipmentInfo_requiresNonFullFuel, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isExternalStore", kEquipmentInfo_isExternalStore, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPortableBetweenShips", kEquipmentInfo_isPortableBetweenShips, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiredCargoSpace", kEquipmentInfo_requiredCargoSpace, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresEquipment", kEquipmentInfo_requiresEquipment, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "requiresAnyEquipment", kEquipmentInfo_requiresAnyEquipment, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "incompatibleEquipment", kEquipmentInfo_incompatibleEquipment, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sEquipmentInfoStaticMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0, }, + { "infoForKey", EquipmentInfoStaticInfoForKey, 0, }, + { 0 } +}; + + +static JSExtendedClass sEquipmentInfoClass = +{ + { + "EquipmentInfo", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + EquipmentInfoGetProperty, // getProperty + EquipmentInfoSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +// *** Public *** + +void InitOOJSEquipmentInfo(JSContext *context, JSObject *global) +{ + sEquipmentInfoPrototype = JS_InitClass(context, global, NULL, &sEquipmentInfoClass.base, NULL, 0, sEquipmentInfoProperties, NULL, NULL, sEquipmentInfoStaticMethods); + JSRegisterObjectConverter(&sEquipmentInfoClass.base, JSBasicPrivateObjectConverter); +} + + +// *** Implementation stuff *** + +static JSBool EquipmentInfoGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + OOEquipmentType *eqType = nil; + id result = nil; + + if (!JSVAL_IS_INT(name)) return YES; + eqType = JSObjectToObjectOfClass(context, this, [OOEquipmentType class]); + if (EXPECT_NOT(eqType == nil)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kEquipmentInfo_equipmentKey: + result = [eqType identifier]; + break; + + case kEquipmentInfo_name: + result = [eqType name]; + break; + + case kEquipmentInfo_description: + result = [eqType descriptiveText]; + break; + + case kEquipmentInfo_techLevel: + *outValue = INT_TO_JSVAL([eqType techLevel]); + break; + + case kEquipmentInfo_effectiveTechLevel: + *outValue = INT_TO_JSVAL([eqType effectiveTechLevel]); + break; + + case kEquipmentInfo_price: + *outValue = INT_TO_JSVAL([eqType price]); + break; + + case kEquipmentInfo_isAvailableToAll: + *outValue = BOOLToJSVal([eqType isAvailableToAll]); + break; + + case kEquipmentInfo_requiresEmptyPylon: + *outValue = BOOLToJSVal([eqType requiresEmptyPylon]); + break; + + case kEquipmentInfo_requiresMountedPylon: + *outValue = BOOLToJSVal([eqType requiresMountedPylon]); + break; + + case kEquipmentInfo_requiresCleanLegalRecord: + *outValue = BOOLToJSVal([eqType requiresCleanLegalRecord]); + break; + + case kEquipmentInfo_requiresNonCleanLegalRecord: + *outValue = BOOLToJSVal([eqType requiresNonCleanLegalRecord]); + break; + + case kEquipmentInfo_requiresFreePassengerBerth: + *outValue = BOOLToJSVal([eqType requiresFreePassengerBerth]); + break; + + case kEquipmentInfo_requiresFullFuel: + *outValue = BOOLToJSVal([eqType requiresFullFuel]); + break; + + case kEquipmentInfo_requiresNonFullFuel: + *outValue = BOOLToJSVal([eqType requiresNonFullFuel]); + break; + + case kEquipmentInfo_isExternalStore: + *outValue = BOOLToJSVal([eqType isMissileOrMine]); + break; + + case kEquipmentInfo_isPortableBetweenShips: + *outValue = BOOLToJSVal([eqType isPortableBetweenShips]); + break; + + case kEquipmentInfo_requiredCargoSpace: + *outValue = BOOLToJSVal([eqType requiredCargoSpace]); + break; + + case kEquipmentInfo_requiresEquipment: + result = [[eqType requiresEquipment] allObjects]; + break; + + case kEquipmentInfo_requiresAnyEquipment: + result = [[eqType requiresAnyEquipment] allObjects]; + break; + + case kEquipmentInfo_incompatibleEquipment: + result = [[eqType incompatibleEquipment] allObjects]; + break; + + default: + OOReportJSBadPropertySelector(context, @"EquipmentInfo", JSVAL_TO_INT(name)); + return NO; + } + + if (result != nil) + { + *outValue = [result javaScriptValueInContext:context]; + } + return YES; +} + + +static JSBool EquipmentInfoSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + OOEquipmentType *eqType = nil; + int32 iValue; + + if (!JSVAL_IS_INT(name)) return YES; + eqType = JSObjectToObjectOfClass(context, this, [OOEquipmentType class]); + if (EXPECT_NOT(eqType == nil)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kEquipmentInfo_effectiveTechLevel: + if ([eqType techLevel] != kOOVariableTechLevel) return YES; // Only TL-99 items can be modified in this way + if (JS_ValueToInt32(context, *value, &iValue)) + { + if (iValue < 0) iValue = 0; + if (14 < iValue && iValue != kOOVariableTechLevel) iValue = 14; + [OOPlayerForScripting() setMissionVariable:[NSString stringWithFormat:@"%u", iValue] + forKey:[@"mission_TL_FOR_" stringByAppendingString:[eqType identifier]]]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"EquipmentInfo", JSVAL_TO_INT(name)); + } + + return OK; +} + + + +@implementation OOEquipmentType (OOJavaScriptExtensions) + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + if (_jsSelf == NULL) + { + _jsSelf = JS_NewObject(context, &sEquipmentInfoClass.base, sEquipmentInfoPrototype, NULL); + if (_jsSelf != NULL) + { + if (!JS_SetPrivate(context, _jsSelf, [self retain])) _jsSelf = NULL; + } + } + + return OBJECT_TO_JSVAL(_jsSelf); +} + +@end + + +// *** Static methods *** + +// infoForKey(key : String): EquipmentInfo +static JSBool EquipmentInfoStaticInfoForKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *key = nil; + + key = JSValToNSString(context, argv[0]); + if (key == nil) + { + OOReportJSBadArguments(context, @"EquipmentInfo", @"infoForKey", argc, argv, nil, @"string"); + return NO; + } + + *outResult = [[OOEquipmentType equipmentTypeWithIdentifier:key] javaScriptValueInContext:context]; + + return YES; +} diff --git a/src/Core/Scripting/OOJSFunction.h b/src/Core/Scripting/OOJSFunction.h new file mode 100644 index 00000000..b5819704 --- /dev/null +++ b/src/Core/Scripting/OOJSFunction.h @@ -0,0 +1,56 @@ +/* + +OOJSFunction.h + +Object encapsulating a runnable JavaScript function. This is mostly a holder +for a JSFunction *; NSValue can't be used for this purpose because a GC root +is needed. + +Could easily be extended with obvious things like -evaluateWithArguments:, but +we don't need that at the moment. + + +JavaScript support for Oolite +Copyright (C) 2007-2009 David Taylor and Jens Ayton. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOJavaScriptEngine.h" + + +@interface OOJSFunction: NSObject +{ +@private + JSFunction *_function; +} + +- (id) initWithFunction:(JSFunction *)function; +- (id) initWithName:(NSString *)name + scope:(JSObject *)scope // may be NULL, in which case global object is used. + code:(NSString *)code // full JS code for function, including function declaration. + argumentCount:(OOUInteger)argCount + argumentNames:(const char **)argNames + fileName:(NSString *)fileName + lineNumber:(OOUInteger)lineNumber + context:(JSContext *)context; // may be NULL. + +- (NSString *) name; +- (JSFunction *) function; + +@end diff --git a/src/Core/Scripting/OOJSFunction.m b/src/Core/Scripting/OOJSFunction.m new file mode 100644 index 00000000..88b47001 --- /dev/null +++ b/src/Core/Scripting/OOJSFunction.m @@ -0,0 +1,136 @@ +/* + +OOJSFunction.m + + +JavaScript support for Oolite +Copyright (C) 2007-2009 David Taylor and Jens Ayton. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSFunction.h" + + +@implementation OOJSFunction + +- (id) initWithFunction:(JSFunction *)function +{ + if (function == NULL) + { + [self release]; + return nil; + } + + if ((self = [super init])) + { + _function = function; + [[OOJavaScriptEngine sharedEngine] addGCRoot:&function named:"OOJSFunction._function"]; + } + + return self; +} + + +- (id) initWithName:(NSString *)name + scope:(JSObject *)scope + code:(NSString *)code + argumentCount:(OOUInteger)argCount + argumentNames:(const char **)argNames + fileName:(NSString *)fileName + lineNumber:(OOUInteger)lineNumber + context:(JSContext *)context +{ + BOOL OK = YES; + BOOL releaseContext = NO; + jschar *buffer = NULL; + size_t length; + JSFunction *function; + + if (context == NULL) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + releaseContext = YES; + } + if (scope == NULL) scope = [[OOJavaScriptEngine sharedEngine] globalObject]; + + if (code == nil || (argCount > 0 && argNames == NULL)) OK = NO; + + if (OK) + { + // jschar and unichar are both defined to be 16-bit elements. + assert(sizeof(jschar) == sizeof(unichar)); + + length = [code length]; + buffer = malloc(sizeof(jschar) * length); + if (buffer == NULL) OK = NO; + } + + if (OK) + { + [code getCharacters:buffer]; + function = JS_CompileUCFunction(context, scope, [name UTF8String], argCount, argNames, buffer, length, [fileName UTF8String], lineNumber); + if (function == NULL) OK = NO; + + free(buffer); + } + + if (OK) + { + self = [self initWithFunction:function]; + } + else + { + [self release]; + self = nil; + } + + if (releaseContext) [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + + return self; +} + + +- (void) dealloc +{ + [[OOJavaScriptEngine sharedEngine] removeGCRoot:&_function]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + NSString *name = [self name]; + if (name == nil) name = @""; + return [NSString stringWithFormat:@"%@()", [self name]]; +} + + +- (NSString *) name +{ + JSString *name = JS_GetFunctionId(_function); + return [NSString stringWithJavaScriptString:name]; +} + + +- (JSFunction *) function +{ + return _function; +} + +@end diff --git a/src/Core/Scripting/OOJSGlobal.h b/src/Core/Scripting/OOJSGlobal.h new file mode 100644 index 00000000..3dcc16ee --- /dev/null +++ b/src/Core/Scripting/OOJSGlobal.h @@ -0,0 +1,33 @@ +/* + +OOJSGlobal.h + +JavaScript global object. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void CreateOOJSGlobal(JSContext *context, JSObject **outGlobal); +void SetUpOOJSGlobal(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSGlobal.m b/src/Core/Scripting/OOJSGlobal.m new file mode 100644 index 00000000..8cd08ebc --- /dev/null +++ b/src/Core/Scripting/OOJSGlobal.m @@ -0,0 +1,271 @@ +/* + +OOJSGlobal.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSGlobal.h" +#import "OOJavaScriptEngine.h" + +#import "OOJSPlayer.h" +#import "PlayerEntityScriptMethods.h" +#import "OOStringParsing.h" +#import "OOConstToString.h" + + +#if OOJSENGINE_MONITOR_SUPPORT + +@interface OOJavaScriptEngine (OOMonitorSupportInternal) + +- (void)sendMonitorLogMessage:(NSString *)message + withMessageClass:(NSString *)messageClass + inContext:(JSContext *)context; + +@end + +#endif + + +extern NSString * const kOOLogDebugMessage; + + +static JSBool GlobalGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool GlobalSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool GlobalLog(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool GlobalExpandDescription(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool GlobalDisplayNameForCommodity(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool GlobalRandomName(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool GlobalRandomInhabitantsDescription(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSClass sGlobalClass = +{ + "Global", + JSCLASS_GLOBAL_FLAGS, + + JS_PropertyStub, + JS_PropertyStub, + GlobalGetProperty, + GlobalSetProperty, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + JS_FinalizeStub +}; + + +enum +{ + // Property IDs + kGlobal_galaxyNumber, // galaxy number, integer, read-only + kGlobal_guiScreen, // current GUI screen, string, read-only + kGlobal_timeAccelerationFactor // time acceleration, float, read/write +}; + + +static JSPropertySpec sGlobalProperties[] = +{ + // JS name ID flags + { "galaxyNumber", kGlobal_galaxyNumber, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "guiScreen", kGlobal_guiScreen, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "timeAccelerationFactor", kGlobal_timeAccelerationFactor, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sGlobalMethods[] = +{ + // JS name Function min args + { "log", GlobalLog, 1 }, + { "expandDescription", GlobalExpandDescription, 1 }, + { "displayNameForCommodity", GlobalDisplayNameForCommodity, 1 }, + { "randomName", GlobalRandomName, 0 }, + { "randomInhabitantsDescription", GlobalRandomInhabitantsDescription, 1 }, + { 0 } +}; + + +void CreateOOJSGlobal(JSContext *context, JSObject **outGlobal) +{ + assert(outGlobal != NULL); + + *outGlobal = JS_NewObject(context, &sGlobalClass, NULL, NULL); +} + + +void SetUpOOJSGlobal(JSContext *context, JSObject *global) +{ + JS_DefineProperties(context, global, sGlobalProperties); + JS_DefineFunctions(context, global, sGlobalMethods); +} + + +static JSBool GlobalGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + PlayerEntity *player = OOPlayerForScripting(); + id result = nil; + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kGlobal_galaxyNumber: + *outValue = INT_TO_JSVAL([player currentGalaxyID]); + break; + + case kGlobal_guiScreen: + result = [player gui_screen_string]; + break; + + case kGlobal_timeAccelerationFactor: + JS_NewDoubleValue(context, [UNIVERSE timeAccelerationFactor], outValue); + break; + + default: + OOReportJSBadPropertySelector(context, @"Global", JSVAL_TO_INT(name)); + return NO; + } + + if (result != nil) *outValue = [result javaScriptValueInContext:context]; + return YES; +} + + +static JSBool GlobalSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + jsdouble fValue; + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kGlobal_timeAccelerationFactor: + if (JS_ValueToNumber(context, *value, &fValue)) + { + [UNIVERSE setTimeAccelerationFactor:fValue]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"Global", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Methods *** + +// log([messageClass : String,] message : string, ...) +static JSBool GlobalLog(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *message = nil; + NSString *messageClass = nil; + + if (argc < 2) + { + messageClass = kOOLogDebugMessage; + message = JSValToNSString(context,argv[0]); + } + else + { + messageClass = [NSString stringWithJavaScriptValue:argv[0] inContext:context]; + message = [NSString concatenationOfStringsFromJavaScriptValues:argv + 1 count:argc - 1 separator:@", " inContext:context]; + } + OOLog(messageClass, @"%@", message); + +#if OOJSENGINE_MONITOR_SUPPORT + [[OOJavaScriptEngine sharedEngine] sendMonitorLogMessage:message + withMessageClass:nil + inContext:context]; +#endif + + return YES; +} + + +// expandDescription(description : String) : String +static JSBool GlobalExpandDescription(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *string = nil; + + string = JSValToNSString(context,argv[0]); + if (string == nil) + { + OOReportJSBadArguments(context, @"System", @"expandDescription", argc, argv, nil, @"string"); + return NO; + } + string = ExpandDescriptionForCurrentSystem(string); + *outResult = [string javaScriptValueInContext:context]; + + return YES; +} + + +// displayNameForCommodity(commodityName : String) : String +static JSBool GlobalDisplayNameForCommodity(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *string = nil; + + string = JSValToNSString(context,argv[0]); + if (string == nil) + { + OOReportJSBadArguments(context, @"System", @"displayNameForCommodity", argc, argv, nil, @"string"); + return NO; + } + string = CommodityDisplayNameForSymbolicName(string); + *outResult = [string javaScriptValueInContext:context]; + + return YES; +} + + +// randomName() : String +static JSBool GlobalRandomName(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *string = nil; + + string = RandomDigrams(); + *outResult = [string javaScriptValueInContext:context]; + + return YES; +} + + +// randomInhabitantsDescription() : String +static JSBool GlobalRandomInhabitantsDescription(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *string = nil; + Random_Seed aSeed; + JSBool isPlural = YES; + + if (!JS_ValueToBoolean(context, argv[0], &isPlural)) isPlural = NO; + + make_pseudo_random_seed(&aSeed); + string = [UNIVERSE generateSystemInhabitants:aSeed plural:isPlural]; + *outResult = [string javaScriptValueInContext:context]; + + return YES; +} diff --git a/src/Core/Scripting/OOJSMission.h b/src/Core/Scripting/OOJSMission.h new file mode 100644 index 00000000..6cbf005d --- /dev/null +++ b/src/Core/Scripting/OOJSMission.h @@ -0,0 +1,32 @@ +/* + +OOJSMission.h + +JavaScript mission screen interface object. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void InitOOJSMission(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSMission.m b/src/Core/Scripting/OOJSMission.m new file mode 100644 index 00000000..023b8a9b --- /dev/null +++ b/src/Core/Scripting/OOJSMission.m @@ -0,0 +1,310 @@ +/* + +OOJSMission.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSMission.h" +#import "OOJavaScriptEngine.h" +#import "OOJSScript.h" + +#import "OOJSPlayer.h" +#import "PlayerEntityScriptMethods.h" + + +static JSBool MissionGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool MissionSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool MissionShowMissionScreen(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionShowShipModel(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionMarkSystem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionUnmarkSystem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionAddMessageTextKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionAddMessageText(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionSetBackgroundImage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionSetMusic(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionSetChoicesKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionSetInstructionsKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool MissionClearMissionScreen(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +static JSClass sMissionClass = +{ + "Mission", + 0, + + JS_PropertyStub, + JS_PropertyStub, + MissionGetProperty, + MissionSetProperty, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + JS_FinalizeStub +}; + + +enum +{ + // Property IDs + kMission_choice, // selected option, string, read-only. +}; + + +static JSPropertySpec sMissionProperties[] = +{ + // JS name ID flags + { "choice", kMission_choice, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sMissionMethods[] = +{ + // JS name Function min args + { "showMissionScreen", MissionShowMissionScreen, 0 }, + { "showShipModel", MissionShowShipModel, 1 }, + { "markSystem", MissionMarkSystem, 1 }, + { "unmarkSystem", MissionUnmarkSystem, 1 }, + { "addMessageTextKey", MissionAddMessageTextKey, 1 }, + { "addMessageText", MissionAddMessageText, 1 }, + { "setBackgroundImage", MissionSetBackgroundImage, 1 }, + { "setMusic", MissionSetMusic, 1 }, + { "setChoicesKey", MissionSetChoicesKey, 1 }, + { "setInstructionsKey", MissionSetInstructionsKey, 1 }, + { "clearMissionScreen", MissionClearMissionScreen, 0 }, + { 0 } +}; + + +void InitOOJSMission(JSContext *context, JSObject *global) +{ + JSObject *missionPrototype = JS_InitClass(context, global, NULL, &sMissionClass, NULL, 0, sMissionProperties, sMissionMethods, NULL, NULL); + JS_DefineObject(context, global, "mission", &sMissionClass, missionPrototype, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +static JSBool MissionGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + id result = nil; + PlayerEntity *player = nil; + + if (!JSVAL_IS_INT(name)) return YES; + + player = OOPlayerForScripting(); + + switch (JSVAL_TO_INT(name)) + { + case kMission_choice: + result = [player missionChoice_string]; + if (result == nil) result = [NSNull null]; + break; + + default: + OOReportJSBadPropertySelector(context, @"Mission", JSVAL_TO_INT(name)); + return NO; + } + + if (result != nil) *outValue = [result javaScriptValueInContext:context]; + return YES; +} + + +static JSBool MissionSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + PlayerEntity *player = nil; + + if (!JSVAL_IS_INT(name)) return YES; + + player = OOPlayerForScripting(); + + switch (JSVAL_TO_INT(name)) + { + case kMission_choice: + if (*value == JSVAL_VOID || *value == JSVAL_NULL) [player resetMissionChoice]; + else [player setMissionChoice:[NSString stringWithJavaScriptValue:*value inContext:context]]; + break; + + default: + OOReportJSBadPropertySelector(context, @"Mission", JSVAL_TO_INT(name)); + return NO; + } + + return YES; +} + + +// *** Methods *** + +// showMissionScreen() +static JSBool MissionShowMissionScreen(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + PlayerEntity *player = OOPlayerForScripting(); + + [player setGuiToMissionScreen]; + + return YES; +} + + +// showShipModel(modelName : String) +static JSBool MissionShowShipModel(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + PlayerEntity *player = OOPlayerForScripting(); + + // If argv[0] can't be converted to a string -- e.g., null or undefined -- this will clear the ship model. + [player showShipModel:JSValToNSString(context,argv[0])]; + + return YES; +} + + +// markSystem(systemCoords : String) +static JSBool MissionMarkSystem(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *params = nil; + + params = [NSString concatenationOfStringsFromJavaScriptValues:argv count:argc separator:@" " inContext:context]; + [player addMissionDestination:params]; + + return YES; +} + + +// unmarkSystem(systemCoords : String) +static JSBool MissionUnmarkSystem(JSContext *context, JSObject *obj, uintN argc, jsval *argv, jsval *rval) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *params = nil; + + params = [NSString concatenationOfStringsFromJavaScriptValues:argv count:argc separator:@" " inContext:context]; + [player removeMissionDestination:params]; + + return YES; +} + + +// addMessageTextKey(textKey : String) +static JSBool MissionAddMessageTextKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context,argv[0]); + [player addMissionText:key]; + + return YES; +} + + +// addMessageText(text : String) +static JSBool MissionAddMessageText(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *text = nil; + + text = JSValToNSString(context,argv[0]); + [player addLiteralMissionText:text]; + + return YES; +} + + +// setBackgroundImage(imageName : String) +static JSBool MissionSetBackgroundImage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + if (argc >= 1) key = JSValToNSString(context,argv[0]); + [player setMissionImage:key]; + + return YES; +} + + +// setMusic(musicName : String) +static JSBool MissionSetMusic(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context,argv[0]); + [player setMissionMusic:key]; + + return YES; +} + + +// setChoicesKey(choicesKey : String) +static JSBool MissionSetChoicesKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context,argv[0]); + [player setMissionChoices:key]; + + return YES; +} + + +// setInstructionsKey(instructionsKey : String [, missionKey : String]) +static JSBool MissionSetInstructionsKey(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + NSString *missionKey = nil; + + key = JSValToNSString(context,argv[0]); + if ([key isKindOfClass:[NSNull class]]) key = nil; + + if (argc > 1) + { + missionKey = [NSString stringWithJavaScriptValue:argv[1] inContext:context]; + } + else + { + missionKey = [[OOJSScript currentlyRunningScript] name]; + } + + if (key != nil) + { + [player setMissionDescription:key forMission:missionKey]; + } + else + { + [player clearMissionDescriptionForMission:missionKey]; + } + + return YES; +} + + +// clearMissionScreen() +static JSBool MissionClearMissionScreen(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + + [player clearMissionScreen]; + return YES; +} diff --git a/src/Core/Scripting/OOJSMissionVariables.h b/src/Core/Scripting/OOJSMissionVariables.h new file mode 100644 index 00000000..151ba342 --- /dev/null +++ b/src/Core/Scripting/OOJSMissionVariables.h @@ -0,0 +1,32 @@ +/* + +OOJSMissionVariables.h + +JavaScript mission variables object. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void InitOOJSMissionVariables(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSMissionVariables.m b/src/Core/Scripting/OOJSMissionVariables.m new file mode 100644 index 00000000..6fb6a55e --- /dev/null +++ b/src/Core/Scripting/OOJSMissionVariables.m @@ -0,0 +1,132 @@ +/* + +OOJSMissionVariables.h + +JavaScript mission variables object. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSMissionVariables.h" +#import "OOJavaScriptEngine.h" +#import "OOIsNumberLiteral.h" + +#import "OOJSPlayer.h" + + +static NSString *KeyForName(JSContext *context, jsval name) +{ + return [@"mission_" stringByAppendingString:[NSString stringWithJavaScriptValue:name inContext:context]]; +} + + +static JSBool MissionVariablesDeleteProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool MissionVariablesGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool MissionVariablesSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + + +static JSClass sMissionVariablesClass = +{ + "MissionVariables", + JSCLASS_IS_ANONYMOUS, + + JS_PropertyStub, + MissionVariablesDeleteProperty, + MissionVariablesGetProperty, + MissionVariablesSetProperty, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + JS_FinalizeStub +}; + + +void InitOOJSMissionVariables(JSContext *context, JSObject *global) +{ + JS_DefineObject(context, global, "missionVariables", &sMissionVariablesClass, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +static JSBool MissionVariablesDeleteProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + PlayerEntity *player = OOPlayerForScripting(); + + if (JSVAL_IS_STRING(name)) + { + NSString *key = KeyForName(context, name); + [player setMissionVariable:nil forKey:key]; + } + return YES; +} + + +static JSBool MissionVariablesGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + PlayerEntity *player = OOPlayerForScripting(); + + if (JSVAL_IS_STRING(name)) + { + NSString *key = KeyForName(context, name); + id value = [player missionVariableForKey:key]; + + *outValue = JSVAL_VOID; + if ([value isKindOfClass:[NSString class]]) // Currently there should only be strings, but we may want to change this. + { + if (OOIsNumberLiteral(value, YES)) + { + BOOL OK = JS_NewDoubleValue(context, [value doubleValue], outValue); + if (!OK) *outValue = JSVAL_VOID; + } + } + + if (value != nil && *outValue == JSVAL_VOID) + { + *outValue = [value javaScriptValueInContext:context]; + } + + if (*outValue == JSVAL_VOID) + { + /* "undefined" is the normal JS expectation, but "null" is easier + to deal with. For instance, foo = missionVaraibles.undefinedThing + is an error if JSVAL_VOID is used, but fine if JSVAL_NULL is + used. + */ + *outValue = JSVAL_NULL; + } + } + return YES; +} + + +static JSBool MissionVariablesSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + PlayerEntity *player = OOPlayerForScripting(); + + if (JSVAL_IS_STRING(name)) + { + NSString *key = KeyForName(context, name); + NSString *objValue = JSValToNSString(context,*value); + + if ([objValue isKindOfClass:[NSNull class]]) objValue = nil; + [player setMissionVariable:objValue forKey:key]; + } + return YES; +} diff --git a/src/Core/Scripting/OOJSOolite.h b/src/Core/Scripting/OOJSOolite.h new file mode 100644 index 00000000..783f47ea --- /dev/null +++ b/src/Core/Scripting/OOJSOolite.h @@ -0,0 +1,32 @@ +/* + +OOJSOolite.h + +JavaScript proxy for Oolite (for version checking and similar). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void InitOOJSOolite(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSOolite.m b/src/Core/Scripting/OOJSOolite.m new file mode 100644 index 00000000..47044811 --- /dev/null +++ b/src/Core/Scripting/OOJSOolite.m @@ -0,0 +1,182 @@ +/* + +OOJSOolite.h + +JavaScript proxy for Oolite (for version checking and similar). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSOolite.h" +#import "OOJavaScriptEngine.h" + +#import "OOStringParsing.h" + + +static JSBool OoliteGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); + +static NSString *VersionString(void); +static NSArray *VersionComponents(void); + +static JSBool OoliteCompareVersion(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + + +static JSClass sOoliteClass = +{ + "Oolite", + 0, + + JS_PropertyStub, + JS_PropertyStub, + OoliteGetProperty, + JS_PropertyStub, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + JS_FinalizeStub +}; + + +enum +{ + // Property IDs + kOolite_version, // version number components, array, read-only + kOolite_versionString, // version number as string, string, read-only + kOolite_jsVersion, // JavaScript version, integer, read-only + kOolite_jsVersionString // JavaScript version as string, string, read-only +}; + + +static JSPropertySpec sOoliteProperties[] = +{ + // JS name ID flags + { "version", kOolite_version, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "versionString", kOolite_versionString, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "jsVersion", kOolite_jsVersion, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "jsVersionString", kOolite_jsVersionString, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sOoliteMethods[] = +{ + // JS name Function min args + { "compareVersion", OoliteCompareVersion, 1 }, + { 0 } +}; + + +void InitOOJSOolite(JSContext *context, JSObject *global) +{ + JSObject *oolitePrototype = JS_InitClass(context, global, NULL, &sOoliteClass, NULL, 0, sOoliteProperties, sOoliteMethods, NULL, NULL); + JS_DefineObject(context, global, "oolite", &sOoliteClass, oolitePrototype, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +static JSBool OoliteGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + id result = nil; + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kOolite_version: + result = VersionComponents(); + if (result == nil) result = [NSNull null]; + break; + + case kOolite_versionString: + result = VersionString(); + if (result == nil) result = [NSNull null]; + break; + + case kOolite_jsVersion: + *outValue = INT_TO_JSVAL(JS_GetVersion(context)); + break; + + case kOolite_jsVersionString: + *outValue = STRING_TO_JSVAL(JS_NewStringCopyZ(context, JS_VersionToString(JS_GetVersion(context)))); + break; + + default: + OOReportJSBadPropertySelector(context, @"Oolite", JSVAL_TO_INT(name)); + return NO; + } + + if (result != nil) *outValue = [result javaScriptValueInContext:context]; + return YES; +} + + +static NSString *VersionString(void) +{ + return [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]; +} + + +static NSArray *VersionComponents(void) +{ + return ComponentsFromVersionString(VersionString()); +} + + +/* oolite.compareVersion(versionSpec) : Number + returns -1 if the current version of Oolite is less than versionSpec, 0 if + they are equal, and 1 if the current version is newer. versionSpec may be + a string or an array. Example: + if (0 < oolite.compareVersion("1.70")) log("Old version of Oolite!") + else this.doStuffThatRequires170() +*/ +static JSBool OoliteCompareVersion(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + id components = nil; + NSEnumerator *componentEnum = nil; + id component = nil; + + components = JSValueToObject(context, argv[0]); + if ([components isKindOfClass:[NSArray class]]) + { + // Require that each element is a number + for (componentEnum = [components objectEnumerator]; (component = [componentEnum nextObject]); ) + { + if (![component isKindOfClass:[NSNumber class]]) + { + components = nil; + break; + } + } + } + else if ([components isKindOfClass:[NSString class]]) + { + components = ComponentsFromVersionString(components); + } + else components = nil; + + if (components != nil) + { + *outResult = INT_TO_JSVAL(CompareVersions(components, VersionComponents())); + } + // Else leave as JSVAL_VOID + + return YES; +} diff --git a/src/Core/Scripting/OOJSPlanet.h b/src/Core/Scripting/OOJSPlanet.h new file mode 100644 index 00000000..d8868dfc --- /dev/null +++ b/src/Core/Scripting/OOJSPlanet.h @@ -0,0 +1,34 @@ +/* + +OOJSPlanet.h + +JavaScript proxy for PlanetEntities. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +@class PlanetEntity; + + +void InitOOJSPlanet(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSPlanet.m b/src/Core/Scripting/OOJSPlanet.m new file mode 100644 index 00000000..43fe4fae --- /dev/null +++ b/src/Core/Scripting/OOJSPlanet.m @@ -0,0 +1,202 @@ +/* + +OOJSPlanet.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSPlanet.h" +#import "OOJSEntity.h" +#import "OOJavaScriptEngine.h" +#import "OOJSSun.h" + +#import "PlanetEntity.h" + + +DEFINE_JS_OBJECT_GETTER(JSPlanetGetPlanetEntity, PlanetEntity) + + +static JSObject *sPlanetPrototype; + + +static JSBool PlanetGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); + +static JSBool PlanetSetTexture(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sPlanetClass = +{ + { + "Planet", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + PlanetGetProperty, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kPlanet_isMainPlanet, // Is [UNIVERSE planet], boolean, read-only + kPlanet_hasAtmosphere, + kPlanet_radius, // Radius of planet in metres. +}; + + +static JSPropertySpec sPlanetProperties[] = +{ + // JS name ID flags + { "isMainPlanet", kPlanet_isMainPlanet, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "hasAtmosphere", kPlanet_hasAtmosphere, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "radius", kPlanet_radius, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sPlanetMethods[] = +{ + // JS name Function min args + { "setTexture", PlanetSetTexture, 1 }, + { 0 } +}; + + +void InitOOJSPlanet(JSContext *context, JSObject *global) +{ + sPlanetPrototype = JS_InitClass(context, global, JSEntityPrototype(), &sPlanetClass.base, NULL, 0, sPlanetProperties, sPlanetMethods, NULL, NULL); + JSRegisterObjectConverter(&sPlanetClass.base, JSBasicPrivateObjectConverter); +} + + +@implementation PlanetEntity (OOJavaScriptExtensions) + +- (BOOL) isVisibleToScripts +{ + return YES; +} + + +- (void)getJSClass:(JSClass **)outClass andPrototype:(JSObject **)outPrototype +{ + if ([self planetType] == PLANET_TYPE_SUN) + { + OOSunGetClassAndPrototype(outClass, outPrototype); + } + else + { + *outClass = &sPlanetClass.base; + *outPrototype = sPlanetPrototype; + } +} + + +- (NSString *)jsClassName +{ + switch ([self planetType]) { + case PLANET_TYPE_SUN: + return @"Sun"; + case PLANET_TYPE_GREEN: + return @"Planet"; + case PLANET_TYPE_MOON: + return @"Moon"; + default: + return @"Unknown"; + } + +} + +@end + + +static JSBool PlanetGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + PlanetEntity *planet = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (!JSPlanetGetPlanetEntity(context, this, &planet)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kPlanet_isMainPlanet: + *outValue = BOOLToJSVal(planet == [UNIVERSE planet]); + OK = YES; + break; + + case kPlanet_radius: + OK = JS_NewDoubleValue(context, [planet radius], outValue); + break; + + case kPlanet_hasAtmosphere: + *outValue = BOOLToJSVal([planet hasAtmosphere]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"Planet", JSVAL_TO_INT(name)); + } + return OK; +} + + +static JSBool PlanetSetTexture(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlanetEntity *thisEnt = nil; + NSString *name = nil; + BOOL procGen = NO; + NSString *pre = @""; + OOEntityStatus playerStatus = [[PlayerEntity sharedPlayer] status]; + + if (!JSPlanetGetPlanetEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + name = JSValToNSString(context,argv[0]); +#if ALLOW_PROCEDURAL_PLANETS + procGen = [UNIVERSE doProcedurallyTexturedPlanets]; + if (!procGen) pre=@"Detailed planets option not set. "; +#endif + // if procGen == on we can retexture at any time, eg during huge surface explosions + if(!procGen && playerStatus != STATUS_LAUNCHING && playerStatus != STATUS_EXITING_WITCHSPACE) + { + OOReportJSError(context, @"%@Use of %@ restricted to shipWillLaunchFromStation and shipWillExitWitchspace.", pre, @"setTexture"); + } + else if (name != nil) + { + if ([thisEnt setUpPlanetFromTexture:name]) return YES; + else OOReportJSError(context, @"Planet.%@(\"%@\"): cannot set texture for planet.", @"setTexture", name); + } + else + { + OOReportJSError(context, @"Planet.%@(): no texture name specified.", @"setTexture"); + } + return NO; +} diff --git a/src/Core/Scripting/OOJSPlayer.h b/src/Core/Scripting/OOJSPlayer.h new file mode 100644 index 00000000..6e61327b --- /dev/null +++ b/src/Core/Scripting/OOJSPlayer.h @@ -0,0 +1,46 @@ +/* + +OOJSPlayer.h + +JavaScript proxy for the player. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +@class PlayerEntity; + + +void InitOOJSPlayer(JSContext *context, JSObject *global); + +JSClass *JSPlayerClass(void); +JSObject *JSPlayerPrototype(void); +JSObject *JSPlayerObject(void); + + +/* All JS functions which talk to the player entity should call + OOOPlayerForScripting() to ensure that the script target (for the legacy + system) is set correctly. Additionally, all such functions should _always_ + call OOPlayerForScripting(), even if they end up not using it, to ensure + consistent state. +*/ +PlayerEntity *OOPlayerForScripting(void); diff --git a/src/Core/Scripting/OOJSPlayer.m b/src/Core/Scripting/OOJSPlayer.m new file mode 100644 index 00000000..8259611a --- /dev/null +++ b/src/Core/Scripting/OOJSPlayer.m @@ -0,0 +1,412 @@ +/* + +OOJSPlayer.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSPlayer.h" +#import "OOJSEntity.h" +#import "OOJSShip.h" +#import "OOJSVector.h" +#import "OOJavaScriptEngine.h" +#import "EntityOOJavaScriptExtensions.h" + +#import "PlayerEntity.h" +#import "PlayerEntityContracts.h" +#import "PlayerEntityScriptMethods.h" +#import "PlayerEntityLegacyScriptEngine.h" + +#import "OOConstToString.h" +#import "OOFunctionAttributes.h" + + +static JSObject *sPlayerPrototype; +static JSObject *sPlayerObject; + + +static JSBool PlayerGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool PlayerSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool PlayerCommsMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerConsoleMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerIncreaseContractReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerDecreaseContractReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerIncreasePassengerReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerDecreasePassengerReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerAddMessageToArrivalReport(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + + +static JSExtendedClass sPlayerClass = +{ + { + "Player", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + PlayerGetProperty, // getProperty + PlayerSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kPlayer_name, // Player name, string, read-only + kPlayer_score, // kill count, integer, read/write + kPlayer_credits, // credit balance, float, read/write + kPlayer_rank, // rank, string, read-only + kPlayer_legalStatus, // legalStatus, string, read-only + kPlayer_alertCondition, // alert level, integer, read-only + kPlayer_alertTemperature, // cabin temperature alert flag, boolean, read-only + kPlayer_alertMassLocked, // mass lock alert flag, boolean, read-only + kPlayer_alertAltitude, // low altitude alert flag, boolean, read-only + kPlayer_alertEnergy, // low energy alert flag, boolean, read-only + kPlayer_alertHostiles, // hostiles present alert flag, boolean, read-only + kPlayer_trumbleCount, // number of trumbles, integer, read-only + kPlayer_contractReputation, // reputation for cargo contracts, integer, read only + kPlayer_passengerReputation, // reputation for passenger contracts, integer, read only +#if DOCKING_CLEARANCE_ENABLED + kPlayer_dockingClearanceStatus, // docking clearance status, string, read only +#endif + kPlayer_bounty // bounty, unsigned int, read/write +}; + + +static JSPropertySpec sPlayerProperties[] = +{ + // JS name ID flags + { "name", kPlayer_name, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "score", kPlayer_score, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "credits", kPlayer_credits, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "rank", kPlayer_rank, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "legalStatus", kPlayer_legalStatus, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "alertCondition", kPlayer_alertCondition, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "alertTemperature", kPlayer_alertTemperature, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "alertMassLocked", kPlayer_alertMassLocked, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "alertAltitude", kPlayer_alertAltitude, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "alertEnergy", kPlayer_alertEnergy, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "alertHostiles", kPlayer_alertHostiles, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "trumbleCount", kPlayer_trumbleCount, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "contractReputation", kPlayer_contractReputation, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "passengerReputation", kPlayer_passengerReputation, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, +#if DOCKING_CLEARANCE_ENABLED + { "dockingClearanceStatus", kPlayer_dockingClearanceStatus, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, +#endif + { "bounty", kPlayer_bounty, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sPlayerMethods[] = +{ + // JS name Function min args + { "commsMessage", PlayerCommsMessage, 1 }, + { "consoleMessage", PlayerConsoleMessage, 1 }, + { "increaseContractReputation", PlayerIncreaseContractReputation, 0 }, + { "decreaseContractReputation", PlayerDecreaseContractReputation, 0 }, + { "increasePassengerReputation", PlayerIncreasePassengerReputation, 0 }, + { "decreasePassengerReputation", PlayerDecreasePassengerReputation, 0 }, + { "addMessageToArrivalReport", PlayerAddMessageToArrivalReport, 1 }, + { 0 } +}; + + +void InitOOJSPlayer(JSContext *context, JSObject *global) +{ + sPlayerPrototype = JS_InitClass(context, global, NULL, &sPlayerClass.base, NULL, 0, sPlayerProperties, sPlayerMethods, NULL, NULL); + JSRegisterObjectConverter(&sPlayerClass.base, JSBasicPrivateObjectConverter); + + // Create player object as a property of the global object. + sPlayerObject = JS_DefineObject(context, global, "player", &sPlayerClass.base, sPlayerPrototype, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +JSClass *JSPlayerClass(void) +{ + return &sPlayerClass.base; +} + + +JSObject *JSPlayerPrototype(void) +{ + return sPlayerPrototype; +} + + +JSObject *JSPlayerObject(void) +{ + return sPlayerObject; +} + + +PlayerEntity *OOPlayerForScripting(void) +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + [player setScriptTarget:player]; + + return player; +} + + +static JSBool PlayerGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + id result = nil; + PlayerEntity *player = OOPlayerForScripting(); + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kPlayer_name: + result = [player playerName]; + OK = YES; + break; + + case kPlayer_score: + *outValue = INT_TO_JSVAL([player score]); + OK = YES; + break; + + case kPlayer_credits: + OK = JS_NewDoubleValue(context, [player creditBalance], outValue); + break; + + case kPlayer_rank: + *outValue = [KillCountToRatingString([player score]) javaScriptValueInContext:context]; + OK = YES; + break; + + case kPlayer_legalStatus: + *outValue = [LegalStatusToString([player bounty]) javaScriptValueInContext:context]; + OK = YES; + break; + + case kPlayer_alertCondition: + *outValue = INT_TO_JSVAL([player alertCondition]); + OK = YES; + break; + + case kPlayer_alertTemperature: + *outValue = BOOLToJSVal([player alertFlags] & ALERT_FLAG_TEMP); + OK = YES; + break; + + case kPlayer_alertMassLocked: + *outValue = BOOLToJSVal([player alertFlags] & ALERT_FLAG_MASS_LOCK); + OK = YES; + break; + + case kPlayer_alertAltitude: + *outValue = BOOLToJSVal([player alertFlags] & ALERT_FLAG_ALT); + OK = YES; + break; + + case kPlayer_alertEnergy: + *outValue = BOOLToJSVal([player alertFlags] & ALERT_FLAG_ENERGY); + OK = YES; + break; + + case kPlayer_alertHostiles: + *outValue = BOOLToJSVal([player alertFlags] & ALERT_FLAG_HOSTILES); + OK = YES; + break; + + case kPlayer_trumbleCount: + OK = JS_NewNumberValue(context, [player trumbleCount], outValue); + break; + + case kPlayer_contractReputation: + *outValue = INT_TO_JSVAL([player contractReputation]); + OK = YES; + break; + + case kPlayer_passengerReputation: + *outValue = INT_TO_JSVAL([player passengerReputation]); + OK = YES; + break; + +#if DOCKING_CLEARANCE_ENABLED + case kPlayer_dockingClearanceStatus: + *outValue = [DockingClearanceStatusToString([player getDockingClearanceStatus]) javaScriptValueInContext:context]; + OK = YES; + break; +#endif + + case kPlayer_bounty: + *outValue = INT_TO_JSVAL([player legalStatus]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"Player", JSVAL_TO_INT(name)); + } + + if (OK && result != nil) *outValue = [result javaScriptValueInContext:context]; + return OK; +} + + +static JSBool PlayerSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + PlayerEntity *player = OOPlayerForScripting(); + jsdouble fValue; + int32 iValue; + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kPlayer_score: + if (JS_ValueToInt32(context, *value, &iValue)) + { + iValue = MAX(iValue, 0); + [player setScore:iValue]; + OK = YES; + } + break; + + case kPlayer_credits: + if (JS_ValueToNumber(context, *value, &fValue)) + { + [player setCreditBalance:fValue]; + OK = YES; + } + break; + + case kPlayer_bounty: + if (JS_ValueToInt32(context, *value, &iValue)) + { + if (iValue < 0) iValue = 0; + [player setBounty:iValue]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"Player", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Methods *** + +// commsMessage(message : String [, duration : Number]) +static JSBool PlayerCommsMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *message = nil; + double time = 4.5; + BOOL gotTime = YES; + + message = JSValToNSString(context, argv[0]); + if (argc > 1) gotTime = JS_ValueToNumber(context, argv[1], &time); + if (message == nil || !gotTime) + { + OOReportJSBadArguments(context, @"Player", @"commsMessage", argc, argv, nil, @"message and optional duration"); + return NO; + } + + [UNIVERSE addCommsMessage:message forCount:time]; + return YES; +} + + +// consoleMessage(message : String [, duration : Number]) +static JSBool PlayerConsoleMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *message = nil; + double time = 3.0; + BOOL gotTime = YES; + + message = JSValToNSString(context, argv[0]); + if (argc > 1) gotTime = JS_ValueToNumber(context, argv[1], &time); + if (message == nil || !gotTime) + { + OOReportJSBadArguments(context, @"Player", @"consoleMessage", argc, argv, nil, @"message and optional duration"); + return NO; + } + + [UNIVERSE addMessage:message forCount:time]; + return YES; +} + + +// increaseContractReputation() +static JSBool PlayerIncreaseContractReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + [OOPlayerForScripting() increaseContractReputation]; + return YES; +} + + +// decreaseContractReputation() +static JSBool PlayerDecreaseContractReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + [OOPlayerForScripting() decreaseContractReputation]; + return YES; +} + + +// increasePassengerReputation() +static JSBool PlayerIncreasePassengerReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + [OOPlayerForScripting() increasePassengerReputation]; + return YES; +} + + +// decreasePassengerReputation() +static JSBool PlayerDecreasePassengerReputation(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + [OOPlayerForScripting() decreasePassengerReputation]; + return YES; +} + +// addMessageToReport(message : String) +static JSBool PlayerAddMessageToArrivalReport(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *report = nil; + + report = JSValToNSString(context, argv[0]); + if (report == nil) + { + OOReportJSBadArguments(context, @"Player", @"addMessageToArrivalReport", argc, argv, nil, @"arrival message"); + return NO; + } + + [OOPlayerForScripting() addMessageToReport:report]; + return YES; +} diff --git a/src/Core/Scripting/OOJSPlayerShip.h b/src/Core/Scripting/OOJSPlayerShip.h new file mode 100644 index 00000000..a97fb021 --- /dev/null +++ b/src/Core/Scripting/OOJSPlayerShip.h @@ -0,0 +1,43 @@ +/* + +OOJSPlayerShip.h + +JavaScript proxy for the player's ship. +While the player and player's ship are not differentiated in Oolite, such a +separation makes more sense conceptually and design-wise, and we might want to +make it that way in the future. The scripting interface anticipates this by +using two separate objects for the player and player's ship. + +The -javaScriptValue of the PlayerEntity is the player's ship. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +@class PlayerEntity; + + +void InitOOJSPlayerShip(JSContext *context, JSObject *global); + +JSClass *JSPlayerShipClass(void); +JSObject *JSPlayerShipPrototype(void); diff --git a/src/Core/Scripting/OOJSPlayerShip.m b/src/Core/Scripting/OOJSPlayerShip.m new file mode 100644 index 00000000..462fc797 --- /dev/null +++ b/src/Core/Scripting/OOJSPlayerShip.m @@ -0,0 +1,590 @@ +/* + +OOJSPlayerShip.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSPlayer.h" +#import "OOJSEntity.h" +#import "OOJSShip.h" +#import "OOJSVector.h" +#import "OOJavaScriptEngine.h" +#import "EntityOOJavaScriptExtensions.h" + +#import "PlayerEntity.h" +#import "PlayerEntityContracts.h" +#import "PlayerEntityScriptMethods.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "HeadUpDisplay.h" + +#import "OOConstToString.h" +#import "OOFunctionAttributes.h" + + +static JSObject *sPlayerShipPrototype; +static JSObject *sPlayerShipObject; + + +static JSBool PlayerShipGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool PlayerShipSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool PlayerShipAwardEquipment(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipRemoveEquipment(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipEquipmentStatus(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipSetEquipmentStatus(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipLaunch(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipAwardCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipCanAwardCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipRemoveAllCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool PlayerShipUseSpecialCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + + +static JSExtendedClass sPlayerShipClass = +{ + { + "PlayerShip", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + PlayerShipGetProperty, // getProperty + PlayerShipSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kPlayerShip_fuelLeakRate, // fuel leak rate, float, read/write + kPlayerShip_docked, // docked, boolean, read-only + kPlayerShip_dockedStation, // docked station, entity, read-only + kPlayerShip_specialCargo, // special cargo, string, read-only + kPlayerShip_reticleTargetSensitive, // target box changes color when primary target in crosshairs, boolean, read/write + kPlayerShip_galacticHyperspaceBehaviour, // can be standard, all systems reachable or fixed coordinates, integer, read-only + kPlayerShip_galacticHyperspaceFixedCoords, // used when fixed coords behaviour is selected, vector, read-only + kPlayerShip_forwardShield, // forward shield charge level, nonnegative float, read/write + kPlayerShip_aftShield, // aft shield charge level, nonnegative float, read/write + kPlayerShip_maxForwardShield, // maximum forward shield charge level, positive float, read-only + kPlayerShip_maxAftShield, // maximum aft shield charge level, positive float, read-only + kPlayerShip_forwardShieldRechargeRate, // forward shield recharge rate, positive float, read-only + kPlayerShip_aftShieldRechargeRate, // aft shield recharge rate, positive float, read-only + kPlayerShip_galaxyCoordinates, // galaxy coordinates, vector, read only + kPlayerShip_cursorCoordinates, // cursor coordinates, vector, read only + kPlayerShip_scriptedMisjump, // next jump will miss if set to true, boolean, read/write +}; + + +static JSPropertySpec sPlayerShipProperties[] = +{ + // JS name ID flags + { "fuelLeakRate", kPlayerShip_fuelLeakRate, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "docked", kPlayerShip_docked, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "dockedStation", kPlayerShip_dockedStation, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "specialCargo", kPlayerShip_specialCargo, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "reticleTargetSensitive", kPlayerShip_reticleTargetSensitive, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "galacticHyperspaceBehaviour", kPlayerShip_galacticHyperspaceBehaviour, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "galacticHyperspaceFixedCoords", kPlayerShip_galacticHyperspaceFixedCoords, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "forwardShield", kPlayerShip_forwardShield, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "aftShield", kPlayerShip_aftShield, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "maxForwardShield", kPlayerShip_maxForwardShield, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "maxAftShield", kPlayerShip_maxAftShield, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "forwardShieldRechargeRate", kPlayerShip_forwardShieldRechargeRate, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "aftShieldRechargeRate", kPlayerShip_aftShieldRechargeRate, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "galaxyCoordinates", kPlayerShip_galaxyCoordinates, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "cursorCoordinates", kPlayerShip_cursorCoordinates, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "scriptedMisjump", kPlayerShip_scriptedMisjump, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sPlayerShipMethods[] = +{ + // JS name Function min args + { "awardEquipment", PlayerShipAwardEquipment, 1 }, // Should be deprecated in favour of equipment object model + { "removeEquipment", PlayerShipRemoveEquipment, 1 }, // Should be deprecated in favour of equipment object model + { "equipmentStatus", PlayerShipEquipmentStatus, 1 }, + { "setEquipmentStatus", PlayerShipSetEquipmentStatus, 2 }, + { "launch", PlayerShipLaunch, 0 }, + { "awardCargo", PlayerShipAwardCargo, 1 }, + { "canAwardCargo", PlayerShipCanAwardCargo, 1 }, + { "removeAllCargo", PlayerShipRemoveAllCargo, 0 }, + { "useSpecialCargo", PlayerShipUseSpecialCargo, 1 }, + { 0 } +}; + + +void InitOOJSPlayerShip(JSContext *context, JSObject *global) +{ + sPlayerShipPrototype = JS_InitClass(context, global, JSShipPrototype(), &sPlayerShipClass.base, NULL, 0, sPlayerShipProperties, sPlayerShipMethods, NULL, NULL); + JSRegisterObjectConverter(&sPlayerShipClass.base, JSBasicPrivateObjectConverter); + + // Create ship object as a property of the player object. + sPlayerShipObject = JS_DefineObject(context, JSPlayerObject(), "ship", &sPlayerShipClass.base, sPlayerShipPrototype, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); + JS_SetPrivate(context, sPlayerShipObject, [[PlayerEntity sharedPlayer] weakRetain]); + [[PlayerEntity sharedPlayer] setJSSelf:sPlayerShipObject context:context]; +} + + +JSClass *JSPlayerShipClass(void) +{ + return &sPlayerShipClass.base; +} + + +JSObject *JSPlayerShipPrototype(void) +{ + return sPlayerShipPrototype; +} + + +@implementation PlayerEntity (OOJavaScriptExtensions) + +- (NSString *)jsClassName +{ + return @"PlayerShip"; +} + + +- (void)setJSSelf:(JSObject *)val context:(JSContext *)context +{ + jsSelf = val; + JS_AddNamedRoot(context, &jsSelf, "Player jsSelf"); +} + +@end + + +static JSBool PlayerShipGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + id result = nil; + PlayerEntity *player = OOPlayerForScripting(); + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kPlayerShip_fuelLeakRate: + OK = JS_NewDoubleValue(context, [player fuelLeakRate], outValue); + break; + + case kPlayerShip_docked: + *outValue = BOOLToJSVal([player isDocked]); + OK = YES; + break; + + case kPlayerShip_dockedStation: + result = [player dockedStation]; + if (result == nil) result = [NSNull null]; + OK = YES; + break; + + case kPlayerShip_specialCargo: + result = [player specialCargo]; + OK = YES; + break; + + case kPlayerShip_reticleTargetSensitive: + *outValue = BOOLToJSVal([[player hud] reticleTargetSensitive]); + OK = YES; + break; + + case kPlayerShip_galacticHyperspaceBehaviour: + result = GalacticHyperspaceBehaviourToString([player galacticHyperspaceBehaviour]); + OK = YES; + break; + + case kPlayerShip_galacticHyperspaceFixedCoords: + OK = NSPointToVectorJSValue(context, [player galacticHyperspaceFixedCoords], outValue); + break; + + case kPlayerShip_forwardShield: + OK = JS_NewDoubleValue(context, [player forwardShieldLevel], outValue); + break; + + case kPlayerShip_aftShield: + OK = JS_NewDoubleValue(context, [player aftShieldLevel], outValue); + break; + + case kPlayerShip_maxForwardShield: + OK = JS_NewDoubleValue(context, [player maxForwardShieldLevel], outValue); + break; + + case kPlayerShip_maxAftShield: + OK = JS_NewDoubleValue(context, [player maxAftShieldLevel], outValue); + break; + + case kPlayerShip_forwardShieldRechargeRate: + case kPlayerShip_aftShieldRechargeRate: + // No distinction made internally + OK = JS_NewDoubleValue(context, [player shieldRechargeRate], outValue); + break; + + case kPlayerShip_galaxyCoordinates: + OK = NSPointToVectorJSValue(context, [player galaxy_coordinates], outValue); + break; + + case kPlayerShip_cursorCoordinates: + OK = NSPointToVectorJSValue(context, [player cursor_coordinates], outValue); + break; + + case kPlayerShip_scriptedMisjump: + *outValue = BOOLToJSVal([player scriptedMisjump]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"PlayerShip", JSVAL_TO_INT(name)); + } + + if (OK && result != nil) *outValue = [result javaScriptValueInContext:context]; + return OK; +} + + +static JSBool PlayerShipSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + PlayerEntity *player = OOPlayerForScripting(); + jsdouble fValue; + JSBool bValue; + NSString *sValue = nil; + OOGalacticHyperspaceBehaviour ghBehaviour; + Vector vValue; + + if (!JSVAL_IS_INT(name)) return YES; + + switch (JSVAL_TO_INT(name)) + { + case kPlayerShip_fuelLeakRate: + if (JS_ValueToNumber(context, *value, &fValue)) + { + [player setFuelLeakRate:fValue]; + OK = YES; + } + break; + + case kPlayerShip_reticleTargetSensitive: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [[player hud] setReticleTargetSensitive:bValue]; + OK = YES; + } + break; + + case kPlayerShip_galacticHyperspaceBehaviour: + sValue = JSValToNSString(context,*value); + if (sValue != nil) + { + ghBehaviour = StringToGalacticHyperspaceBehaviour(sValue); + if (ghBehaviour != GALACTIC_HYPERSPACE_BEHAVIOUR_UNKNOWN) + { + [player setGalacticHyperspaceBehaviour:ghBehaviour]; + } + OK = YES; + } + break; + + case kPlayerShip_galacticHyperspaceFixedCoords: + if (JSValueToVector(context, *value, &vValue)) + { + [player setGalacticHyperspaceFixedCoordsX:OOClamp_0_max_f(vValue.x, 255.0f) y:OOClamp_0_max_f(vValue.y, 255.0f)]; + OK = YES; + } + break; + + case kPlayerShip_forwardShield: + if (JS_ValueToNumber(context, *value, &fValue)) + { + [player setForwardShieldLevel:fValue]; + OK = YES; + } + break; + + case kPlayerShip_aftShield: + if (JS_ValueToNumber(context, *value, &fValue)) + { + [player setAftShieldLevel:fValue]; + OK = YES; + } + break; + + case kPlayerShip_scriptedMisjump: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [player setScriptedMisjump:bValue]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"PlayerShip", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Methods *** + +// awardEquipment(key : String) +static JSBool PlayerShipAwardEquipment(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(key == nil)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"awardEquipment", argc, argv, nil, @"equipment key"); + return NO; + } + + [player awardEquipment:key]; + return YES; +} + + +// removeEquipment(key : String) +static JSBool PlayerShipRemoveEquipment(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(key == nil)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"removeEquipment", argc, argv, nil, @"equipment key"); + return NO; + } + + [player removeEquipmentItem:key]; + return YES; +} + + +// setEquipmentStatus(key : String, status : String) +static JSBool PlayerShipSetEquipmentStatus(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + // equipment status accepted: @"EQUIPMENT_OK", @"EQUIPMENT_DAMAGED" + + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = JSValToNSString(context, argv[0]); + NSString *damagedKey = [key stringByAppendingString:@"_DAMAGED"]; + NSString *status = JSValToNSString(context, argv[1]); + BOOL hasOK = NO, hasDamaged = NO; + + if (EXPECT_NOT([UNIVERSE strict])) + { + // It's OK to have a hard error here since only built-in scripts run in strict mode. + OOReportJSError(context, @"Cannot set equipment status while in strict mode."); + return NO; + } + + if (EXPECT_NOT(key == nil || status == nil)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"setEquipmentStatus", argc, argv, nil, @"equipment key and status"); + return NO; + } + + hasOK = [player hasEquipmentItem:key]; + hasDamaged = [player hasEquipmentItem:damagedKey]; + + if ([status isEqualToString:@"EQUIPMENT_OK"]) + { + if (hasDamaged) + { + [player removeEquipmentItem:damagedKey]; + [player addEquipmentItem:key]; + } + } + else if ([status isEqualToString:@"EQUIPMENT_DAMAGED"]) + { + if (hasOK) + { + [player removeEquipmentItem:key]; + [player addEquipmentItem:damagedKey]; + [player doScriptEvent:@"equipmentDamaged" withArgument:key]; + } + } + else + { + OOReportJSErrorForCaller(context, @"PlayerShip", @"setEquipmentStatus", @"Second parameter for setEquipmentStatus must be either \"EQUIPMENT_OK\" or \"EQUIPMENT_DAMAGED\"."); + return NO; + } + + *outResult = BOOLToJSVal(hasOK || hasDamaged); + return YES; +} + + +// equipmentStatus(key : String) : String +static JSBool PlayerShipEquipmentStatus(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + // values returned: @"EQUIPMENT_OK", @"EQUIPMENT_DAMAGED", @"EQUIPMENT_UNAVAILABLE" + + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = JSValToNSString(context, argv[0]); + NSString *result = @"EQUIPMENT_UNAVAILABLE"; + + if (EXPECT_NOT(key == nil)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"setEquipmentStatus", argc, argv, nil, @"equipment key"); + return NO; + } + + if([player hasEquipmentItem:key]) result = @"EQUIPMENT_OK"; + else if([player hasEquipmentItem:[key stringByAppendingString:@"_DAMAGED"]]) result = @"EQUIPMENT_DAMAGED"; + + *outResult = [result javaScriptValueInContext:context]; + return YES; +} + + +// launch() +static JSBool PlayerShipLaunch(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + [OOPlayerForScripting() launchFromStation]; + return YES; +} + + +// awardCargo(type : String [, quantity : Number]) +static JSBool PlayerShipAwardCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *typeString = nil; + OOCargoType type; + int32 amount = 1; + BOOL gotAmount = YES; + + typeString = JSValToNSString(context, argv[0]); + if (argc > 1) gotAmount = JS_ValueToInt32(context, argv[1], &amount); + if (EXPECT_NOT(typeString == nil || !gotAmount)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"awardCargo", argc, argv, nil, @"type and optional quantity"); + return NO; + } + + type = [UNIVERSE commodityForName:typeString]; + if (EXPECT_NOT(type == CARGO_UNDEFINED)) + { + OOReportJSErrorForCaller(context, @"PlayerShip", @"awardCargo", @"Unknown cargo type \"%@\".", typeString); + return NO; + } + + if (EXPECT_NOT(amount < 0)) + { + OOReportJSErrorForCaller(context, @"PlayerShip", @"awardCargo", @"Cargo quantity (%i) is negative.", amount); + return NO; + } + + if (EXPECT_NOT(![player canAwardCargoType:type amount:amount])) + { + OOReportJSErrorForCaller(context, @"PlayerShip", @"awardCargo", @"Cannot award %u units of cargo \"%@\" at this time (use canAwardCargo() to avoid this error).", amount, typeString); + return NO; + } + + [player awardCargoType:type amount:amount]; + return YES; +} + + +// canAwardCargo(type : String [, quantity : Number]) : Boolean +static JSBool PlayerShipCanAwardCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *typeString = nil; + OOCargoType type; + int32 amount = 1; + BOOL gotAmount = YES; + + typeString = JSValToNSString(context, argv[0]); + if (argc > 1) gotAmount = JS_ValueToInt32(context, argv[1], &amount); + if (EXPECT_NOT(typeString == nil || !gotAmount)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"canAwardCargo", argc, argv, nil, @"type and optional quantity"); + return NO; + } + + type = [UNIVERSE commodityForName:typeString]; + if (EXPECT_NOT(type == CARGO_UNDEFINED)) + { + OOReportJSErrorForCaller(context, @"PlayerShip", @"canAwardCargo", @"Unknown cargo type \"%@\".", typeString); + return NO; + } + + if (EXPECT_NOT(amount < 0)) + { + OOReportJSErrorForCaller(context, @"PlayerShip", @"canAwardCargo", @"Cargo quantity (%i) is negative.", amount); + return NO; + } + + *outResult = BOOLToJSVal([player canAwardCargoType:type amount:amount]); + return YES; +} + + +// removeAllCargo() +static JSBool PlayerShipRemoveAllCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + + if ([player isDocked]) + { + [player removeAllCargo]; + return YES; + } + else + { + OOReportJSError(context, @"PlayerShip.removeAllCargo() may only be called when the player is docked."); + return NO; + } +} + + +// useSpecialCargo(name : String) +static JSBool PlayerShipUseSpecialCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *name = nil; + + name = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(name == nil)) + { + OOReportJSBadArguments(context, @"PlayerShip", @"useSpecialCargo", argc, argv, nil, @"special cargo description"); + return NO; + } + + [player useSpecialCargo:JSValToNSString(context, argv[0])]; + return YES; +} diff --git a/src/Core/Scripting/OOJSQuaternion.h b/src/Core/Scripting/OOJSQuaternion.h new file mode 100644 index 00000000..c49b5689 --- /dev/null +++ b/src/Core/Scripting/OOJSQuaternion.h @@ -0,0 +1,68 @@ +/* + +OOJSQuaternion.h + +JavaScript proxy for quaternions. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import +#import "OOMaths.h" + + +void InitOOJSQuaternion(JSContext *context, JSObject *global); + + +JSObject *JSQuaternionWithQuaternion(JSContext *context, Quaternion quaternion); + +BOOL QuaternionToJSValue(JSContext *context, Quaternion quaternion, jsval *outValue); +BOOL JSValueToQuaternion(JSContext *context, jsval value, Quaternion *outQuaternion); + +/* Given a JS Quaternion object, get the corresponding Vector struct. Given a + JS Entity, get its orientation. Given a JS Array with exactly four + elements, all of them numbers, treat them as [w, x, y, z] components. For + anything else, return NO. (Other implicit conversions may be added in + future.) +*/ +BOOL JSObjectGetQuaternion(JSContext *context, JSObject *quaternionObj, Quaternion *outQuaternion); + +// Set the value of a JS quaternion object. +BOOL JSQuaternionSetQuaternion(JSContext *context, JSObject *quaternionObj, Quaternion quaternion); + + +/* QuaternionFromArgumentList() + + Construct a quaternion from an argument list which is either a (JS) + quaternion, a (JS) entity, four numbers or a JS array of four numbers. The + optional outConsumed argument can be used to find out how many parameters + were used (currently, this will be 0 on failure, otherwise 1 or 4). + + On failure, it will return NO and raise an error. If the caller is a JS + callback, it must return NO to signal an error. +*/ +BOOL QuaternionFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, Quaternion *outQuaternion, uintN *outConsumed); + +/* QuaternionFromArgumentList() + + Like VectorFromArgumentList(), but does not report an error on failure. +*/ +BOOL QuaternionFromArgumentListNoError(JSContext *context, uintN argc, jsval *argv, Quaternion *outVector, uintN *outConsumed); diff --git a/src/Core/Scripting/OOJSQuaternion.m b/src/Core/Scripting/OOJSQuaternion.m new file mode 100644 index 00000000..1b7a207c --- /dev/null +++ b/src/Core/Scripting/OOJSQuaternion.m @@ -0,0 +1,678 @@ +/* + +OOJSQuaternion.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSQuaternion.h" +#import "OOJavaScriptEngine.h" + +#if OOLITE_GNUSTEP +#import +#else +#import +#endif + +#import "OOConstToString.h" +#import "OOJSEntity.h" +#import "OOJSVector.h" + + +static JSObject *sQuaternionPrototype; + + +static JSBool QuaternionGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool QuaternionSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static void QuaternionFinalize(JSContext *context, JSObject *this); +static JSBool QuaternionConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionEquality(JSContext *context, JSObject *this, jsval value, JSBool *outEqual); + +// Methods +static JSBool QuaternionToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionToSource(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionMultiply(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionDot(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionRotate(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionRotateX(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionRotateY(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionRotateZ(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionNormalize(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionVectorForward(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionVectorUp(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionVectorRight(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool QuaternionToArray(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +// Static methods +static JSBool QuaternionStaticRandom(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sQuaternionClass = +{ + { + "Quaternion", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + QuaternionGetProperty, // getProperty + QuaternionSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + QuaternionFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + QuaternionEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kQuaternion_w, + kQuaternion_x, + kQuaternion_y, + kQuaternion_z +}; + + +static JSPropertySpec sQuaternionProperties[] = +{ + // JS name ID flags + { "w", kQuaternion_w, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "x", kQuaternion_x, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "y", kQuaternion_y, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "z", kQuaternion_z, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sQuaternionMethods[] = +{ + // JS name Function min args + { "toString", QuaternionToString, 0, }, + { "toSource", QuaternionToSource, 0, }, + { "multiply", QuaternionMultiply, 1, }, + { "dot", QuaternionDot, 1, }, + { "rotate", QuaternionRotate, 2, }, + { "rotateX", QuaternionRotateX, 1, }, + { "rotateY", QuaternionRotateY, 1, }, + { "rotateZ", QuaternionRotateZ, 1, }, + { "normalize", QuaternionNormalize, 0, }, + { "vectorForward", QuaternionVectorForward, 0, }, + { "vectorUp", QuaternionVectorUp, 0, }, + { "vectorRight", QuaternionVectorRight, 0, }, + { "toArray", QuaternionToArray, 0, }, + { 0 } +}; + + +static JSFunctionSpec sQuaternionStaticMethods[] = +{ + // JS name Function min args + { "random", QuaternionStaticRandom, 0, }, + { 0 } +}; + + +// *** Public *** + +void InitOOJSQuaternion(JSContext *context, JSObject *global) +{ + sQuaternionPrototype = JS_InitClass(context, global, NULL, &sQuaternionClass.base, QuaternionConstruct, 4, sQuaternionProperties, sQuaternionMethods, NULL, sQuaternionStaticMethods); +} + + +JSObject *JSQuaternionWithQuaternion(JSContext *context, Quaternion quaternion) +{ + JSObject *result = NULL; + Quaternion *private = NULL; + + private = malloc(sizeof *private); + if (EXPECT_NOT(private == NULL)) return NULL; + + *private = quaternion; + + result = JS_NewObject(context, &sQuaternionClass.base, sQuaternionPrototype, NULL); + if (result != NULL) + { + if (!JS_SetPrivate(context, result, private)) result = NULL; + } + + if (EXPECT_NOT(result == NULL)) free(private); + + return result; +} + + +BOOL QuaternionToJSValue(JSContext *context, Quaternion quaternion, jsval *outValue) +{ + JSObject *object = NULL; + + if (EXPECT_NOT(outValue == NULL)) return NO; + + object = JSQuaternionWithQuaternion(context, quaternion); + if (EXPECT_NOT(object == NULL)) return NO; + + *outValue = OBJECT_TO_JSVAL(object); + return YES; +} + + +BOOL JSValueToQuaternion(JSContext *context, jsval value, Quaternion *outQuaternion) +{ + if (EXPECT_NOT(!JSVAL_IS_OBJECT(value))) return NO; + + return JSObjectGetQuaternion(context, JSVAL_TO_OBJECT(value), outQuaternion); +} + + +BOOL JSObjectGetQuaternion(JSContext *context, JSObject *quaternionObj, Quaternion *outQuaternion) +{ + Quaternion *private = NULL; + Entity *entity = nil; + jsuint arrayLength; + jsval arrayW, arrayX, arrayY, arrayZ; + jsdouble dVal; + + if (EXPECT_NOT(outQuaternion == NULL || quaternionObj == NULL)) return NO; + + private = JS_GetInstancePrivate(context, quaternionObj, &sQuaternionClass.base, NULL); + if (private != NULL) // If this is a (JS) Quaternion... + { + *outQuaternion = *private; + return YES; + } + + // If it's an entity, use its orientation. + if (JSEntityGetEntity(context, quaternionObj, &entity)) + { + *outQuaternion = [entity orientation]; + return YES; + } + + // If it's an array... + if (JS_IsArrayObject(context, quaternionObj)) + { + // ...and it has exactly four elements... + if (JS_GetArrayLength(context, quaternionObj, &arrayLength) && arrayLength == 4) + { + if (JS_LookupElement(context, quaternionObj, 0, &arrayW) && + JS_LookupElement(context, quaternionObj, 1, &arrayX) && + JS_LookupElement(context, quaternionObj, 2, &arrayY) && + JS_LookupElement(context, quaternionObj, 3, &arrayZ)) + { + // ...se the four numbers as [w, x, y, z] + if (!JS_ValueToNumber(context, arrayW, &dVal)) return NO; + outQuaternion->w = dVal; + if (!JS_ValueToNumber(context, arrayX, &dVal)) return NO; + outQuaternion->x = dVal; + if (!JS_ValueToNumber(context, arrayY, &dVal)) return NO; + outQuaternion->y = dVal; + if (!JS_ValueToNumber(context, arrayZ, &dVal)) return NO; + outQuaternion->z = dVal; + return YES; + } + } + } + + return NO; +} + + +BOOL JSQuaternionSetQuaternion(JSContext *context, JSObject *quaternionObj, Quaternion quaternion) +{ + Quaternion *private = NULL; + + if (EXPECT_NOT(quaternionObj == NULL)) return NO; + + private = JS_GetInstancePrivate(context, quaternionObj, &sQuaternionClass.base, NULL); + if (private != NULL) // If this is a (JS) Quaternion... + { + *private = quaternion; + return YES; + } + + return NO; +} + + +BOOL QuaternionFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, Quaternion *outQuaternion, uintN *outConsumed) +{ + if (QuaternionFromArgumentListNoError(context, argc, argv, outQuaternion, outConsumed)) return YES; + else + { + OOReportJSBadArguments(context, scriptClass, function, argc, argv, + @"Could not construct quaternion from parameters", + @"Quaternion, Entity or four numbers"); + return NO; + } +} + + +static BOOL QuaternionFromArgumentListNoErrorInternal(JSContext *context, uintN argc, jsval *argv, Quaternion *outQuaternion, uintN *outConsumed, BOOL warnAboutNumberList) +{ + double w, x, y, z; + + if (outConsumed != NULL) *outConsumed = 0; + // Sanity checks. + if (EXPECT_NOT(argc == 0 || argv == NULL || outQuaternion == NULL)) + { + OOLogGenericParameterError(); + return NO; + } + + // Is first object a quaternion or entity? + if (JSVAL_IS_OBJECT(argv[0])) + { + if (JSObjectGetQuaternion(context, JSVAL_TO_OBJECT(argv[0]), outQuaternion)) + { + if (outConsumed != NULL) *outConsumed = 1; + return YES; + } + } + + // Otherwise, look for four numbers. + if (EXPECT_NOT(argc < 4)) return NO; + + // Given a string, JS_ValueToNumber() returns YES but provides a NaN number. + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[0], &w) || isnan(w))) return NO; + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[1], &x) || isnan(x))) return NO; + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[2], &y) || isnan(y))) return NO; + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[3], &z) || isnan(z))) return NO; + + // We got our four numbers. + *outQuaternion = make_quaternion(w, x, y, z); + if (outConsumed != NULL) *outConsumed = 4; + + if (warnAboutNumberList) + { + OOReportJSWarning(context, @"The ability to pass four numbers instead of a quaternion is deprecated and will be removed in a future version of Oolite. Use an array literal instead (for instance, replace q.multiply(w, 1, 2, 3) with q.multiply([w, 1, 2, 3])."); + } + + return YES; +} + + +BOOL QuaternionFromArgumentListNoError(JSContext *context, uintN argc, jsval *argv, Quaternion *outQuaternion, uintN *outConsumed) +{ + return QuaternionFromArgumentListNoErrorInternal(context, argc, argv, outQuaternion, outConsumed, YES); +} + + +// *** Implementation stuff *** + +static JSBool QuaternionGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + Quaternion quaternion; + GLfloat value; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &quaternion))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kQuaternion_w: + value = quaternion.w; + break; + + case kQuaternion_x: + value = quaternion.x; + break; + + case kQuaternion_y: + value = quaternion.y; + break; + + case kQuaternion_z: + value = quaternion.z; + break; + + default: + OOReportJSBadPropertySelector(context, @"Quaternion", JSVAL_TO_INT(name)); + return NO; + } + + return JS_NewDoubleValue(context, value, outValue); +} + + +static JSBool QuaternionSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + Quaternion quaternion; + jsdouble dval; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &quaternion))) return NO; + JS_ValueToNumber(context, *value, &dval); + + switch (JSVAL_TO_INT(name)) + { + case kQuaternion_w: + quaternion.w = dval; + break; + + case kQuaternion_x: + quaternion.x = dval; + break; + + case kQuaternion_y: + quaternion.y = dval; + break; + + case kQuaternion_z: + quaternion.z = dval; + break; + + default: + OOReportJSBadPropertySelector(context, @"Quaternion", JSVAL_TO_INT(name)); + return NO; + } + + return JSQuaternionSetQuaternion(context, this, quaternion); +} + + +static void QuaternionFinalize(JSContext *context, JSObject *this) +{ + Quaternion *private = NULL; + + private = JS_GetInstancePrivate(context, this, &sQuaternionClass.base, NULL); + if (private != NULL) + { + free(private); + } +} + + +static JSBool QuaternionConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion quaternion = kZeroQuaternion; + Quaternion *private = NULL; + + private = malloc(sizeof *private); + if (EXPECT_NOT(private == NULL)) return NO; + + // If called without new, replace this with a new Vector object. + if (!JS_IsConstructing(context)) + { + this = JS_NewObject(context, &sQuaternionClass.base, NULL, NULL); + if (this == NULL) return NO; + *outResult = OBJECT_TO_JSVAL(this); + } + + if (argc != 0) + { + if (EXPECT_NOT(!QuaternionFromArgumentListNoErrorInternal(context, argc, argv, &quaternion, NULL, NO))) + { + free(private); + OOReportJSBadArguments(context, NULL, NULL, argc, argv, + @"Could not construct quaternion from parameters", + @"Vector, Entity or array of four numbers"); + return NO; + } + } + + *private = quaternion; + + if (!JS_SetPrivate(context, this, private)) + { + free(private); + return NO; + } + + return YES; +} + + +static JSBool QuaternionEquality(JSContext *context, JSObject *this, jsval value, JSBool *outEqual) +{ + Quaternion thisq, thatq; + + // Note: "return YES" means no error, not equality. + *outEqual = NO; + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; // This is not a quaternion? + if (EXPECT_NOT(!JSVAL_IS_OBJECT(value))) return YES; // Non-object value - not equal + if (EXPECT_NOT(!JSObjectGetQuaternion(context, JSVAL_TO_OBJECT(value), &thatq))) return YES; // Non-quaternion value - not equal + + *outEqual = quaternion_equal(thisq, thatq); + return YES; +} + + +// *** Methods *** + +// toString() : String +static JSBool QuaternionToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + + *outResult = [QuaternionDescription(thisq) javaScriptValueInContext:context]; + return YES; +} + + +// toSource() : String +static JSBool QuaternionToSource(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + + *outResult = [[NSString stringWithFormat:@"Quaternion(%g, %g, %g, %g)", thisq.w, thisq.x, thisq.y, thisq.z] + javaScriptValueInContext:context]; + return YES; +} + + +// multiply(q : quaternionExpression) : Quaternion +static JSBool QuaternionMultiply(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq, thatq, result; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + if (EXPECT_NOT(!QuaternionFromArgumentList(context, @"Quaternion", @"multiply", argc, argv, &thatq, NULL))) return NO; + + result = quaternion_multiply(thisq, thatq); + + return QuaternionToJSValue(context, result, outResult); +} + + +// dot(q : quaternionExpression) : Number +static JSBool QuaternionDot(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq, thatq; + double result; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + if (EXPECT_NOT(!QuaternionFromArgumentList(context, @"Quaternion", @"dot", argc, argv, &thatq, NULL))) return NO; + + result = quaternion_dot_product(thisq, thatq); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// rotate(axis : vectorExpression, angle : Number) : Quaternion +static JSBool QuaternionRotate(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + Vector axis; + double angle; + uintN consumed; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Quaternion", @"rotate", argc, argv, &axis, &consumed))) return NO; + argv += consumed; + argc -= consumed; + if (argc > 0) + { + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Quaternion", @"rotate", argc, argv, &angle, NULL))) return NO; + quaternion_rotate_about_axis(&thisq, axis, angle); + } + // Else no angle specified, so don't rotate and pass value through unchanged. + + return QuaternionToJSValue(context, thisq, outResult); +} + + +// rotateX(angle : Number) : Quaternion +static JSBool QuaternionRotateX(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion quat; + double angle; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &quat))) return NO; + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Quaternion", @"rotateX", argc, argv, &angle, NULL))) return NO; + + quaternion_rotate_about_x(&quat, angle); + + return QuaternionToJSValue(context, quat, outResult); +} + + +// rotateY(angle : Number) : Quaternion +static JSBool QuaternionRotateY(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion quat; + double angle; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &quat))) return NO; + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Quaternion", @"rotateY", argc, argv, &angle, NULL))) return NO; + + quaternion_rotate_about_y(&quat, angle); + + return QuaternionToJSValue(context, quat, outResult); +} + + +// rotateZ(angle : Number) : Quaternion +static JSBool QuaternionRotateZ(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion quat; + double angle; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &quat))) return NO; + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Quaternion", @"rotateZ", argc, argv, &angle, NULL))) return NO; + + quaternion_rotate_about_z(&quat, angle); + + return QuaternionToJSValue(context, quat, outResult); +} + + +// normalize() : Quaternion +static JSBool QuaternionNormalize(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion quat; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &quat))) return NO; + + quaternion_normalize(&quat); + + return QuaternionToJSValue(context, quat, outResult); +} + + +// vectorForward() : Vector +static JSBool QuaternionVectorForward(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + Vector result; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + + result = vector_forward_from_quaternion(thisq); + + return VectorToJSValue(context, result, outResult); +} + + +// vectorUp() : Vector +static JSBool QuaternionVectorUp(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + Vector result; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + + result = vector_up_from_quaternion(thisq); + + return VectorToJSValue(context, result, outResult); +} + + +// vectorRight() : Vector +static JSBool QuaternionVectorRight(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + Vector result; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + + result = vector_right_from_quaternion(thisq); + + return VectorToJSValue(context, result, outResult); +} + + +// toArray() : Array +static JSBool QuaternionToArray(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Quaternion thisq; + JSObject *result = NULL; + BOOL OK = YES; + jsval nVal; + + if (EXPECT_NOT(!JSObjectGetQuaternion(context, this, &thisq))) return NO; + + result = JS_NewArrayObject(context, 0, NULL); + if (result != NULL) + { + // We do this at the top because *outResult is a GC root. + *outResult = OBJECT_TO_JSVAL(result); + + if (JS_NewNumberValue(context, thisq.w, &nVal)) JS_SetElement(context, result, 0, &nVal); + else OK = NO; + if (JS_NewNumberValue(context, thisq.x, &nVal)) JS_SetElement(context, result, 1, &nVal); + else OK = NO; + if (JS_NewNumberValue(context, thisq.y, &nVal)) JS_SetElement(context, result, 2, &nVal); + else OK = NO; + if (JS_NewNumberValue(context, thisq.z, &nVal)) JS_SetElement(context, result, 3, &nVal); + else OK = NO; + } + + if (!OK) *outResult = JSVAL_VOID; + return YES; +} + + +// random() : Quaternion +static JSBool QuaternionStaticRandom(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + return QuaternionToJSValue(context, OORandomQuaternion(), outResult); +} diff --git a/src/Core/Scripting/OOJSScript.h b/src/Core/Scripting/OOJSScript.h new file mode 100644 index 00000000..81a7a26c --- /dev/null +++ b/src/Core/Scripting/OOJSScript.h @@ -0,0 +1,55 @@ +/* + +OOJSScript.h + +JavaScript support for Oolite +Copyright (C) 2007 David Taylor and Jens Ayton. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import "OOScript.h" +#import "OOJavaScriptEngine.h" + + +@interface OOJSScript: OOScript +{ + JSObject *_jsSelf; + + NSString *name; + NSString *description; + NSString *version; + + OOWeakReference *weakSelf; +} + ++ (id)scriptWithPath:(NSString *)path properties:(NSDictionary *)properties; + +- (id)initWithPath:(NSString *)path properties:(NSDictionary *)properties; + ++ (OOJSScript *)currentlyRunningScript; ++ (NSArray *)scriptStack; + ++ (void)pushScript:(OOJSScript *)script; // Used, for instance, by timers. Failing to balance these will crash! ++ (void)popScript:(OOJSScript *)script; + +@end + + +void InitOOJSScript(JSContext *context, JSObject *global); + diff --git a/src/Core/Scripting/OOJSScript.m b/src/Core/Scripting/OOJSScript.m new file mode 100644 index 00000000..5cc8faa0 --- /dev/null +++ b/src/Core/Scripting/OOJSScript.m @@ -0,0 +1,681 @@ +/* + +OOJSScript.m + +JavaScript support for Oolite +Copyright (C) 2007 David Taylor and Jens Ayton. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#ifndef OO_CACHE_JS_SCRIPTS +#define OO_CACHE_JS_SCRIPTS 1 +#endif + +// Enable support for old event handler names through changedScriptHandlers.plist. +#define SUPPORT_CHANGED_HANDLERS 1 + + +#import "OOJSScript.h" +#import "OOLogging.h" +#import "OOConstToString.h" +#import "Entity.h" +#import "OOJavaScriptEngine.h" +#import "NSStringOOExtensions.h" +#import "EntityOOJavaScriptExtensions.h" + +#if SUPPORT_CHANGED_HANDLERS +#import "ResourceManager.h" +#endif + +#if OO_CACHE_JS_SCRIPTS +#import +#import "OOCacheManager.h" +#endif + + +typedef struct RunningStack RunningStack; +struct RunningStack +{ + RunningStack *back; + OOJSScript *current; +}; + + +static JSObject *sScriptPrototype; +static RunningStack *sRunningStack = NULL; + + +static void AddStackToArrayReversed(NSMutableArray *array, RunningStack *stack); + +static JSScript *LoadScriptWithName(JSContext *context, NSString *name, JSObject *object, NSString **outErrorMessage); + +#if OO_CACHE_JS_SCRIPTS +static NSData *CompiledScriptData(JSContext *context, JSScript *script); +static JSScript *ScriptWithCompiledData(JSContext *context, NSData *data); +#endif + + +static JSClass sScriptClass = +{ + "Script", + JSCLASS_HAS_PRIVATE, + + JS_PropertyStub, + JS_PropertyStub, + JS_PropertyStub, + JS_PropertyStub, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + JSObjectWrapperFinalize +}; + + +static JSFunctionSpec sScriptMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0, }, + { 0 } +}; + + +@interface OOJSScript (OOPrivate) + +- (NSString *)scriptNameFromPath:(NSString *)path; + +@end + + +@implementation OOJSScript + ++ (id)scriptWithPath:(NSString *)path properties:(NSDictionary *)properties +{ + return [[[self alloc] initWithPath:path properties:properties] autorelease]; +} + + +- (id)initWithPath:(NSString *)path properties:(NSDictionary *)properties +{ + OOJavaScriptEngine *engine = nil; + JSContext *context = NULL; + NSString *problem = nil; // Acts as error flag. + JSScript *script = NULL; + jsval returnValue; + NSEnumerator *keyEnum = nil; + NSString *key = nil; + id property = nil; + + self = [super init]; + if (self == nil) problem = @"allocation failure"; + + engine = [OOJavaScriptEngine sharedEngine]; + context = [engine acquireContext]; + + // Set up JS object + if (!problem) + { + // Do we actually want parent to be the global object here? + _jsSelf = JS_NewObject(context, &sScriptClass, sScriptPrototype, NULL /*JS_GetGlobalObject(context)*/); + if (_jsSelf == NULL) problem = @"allocation failure"; + } + + if (!problem) + { + if (!JS_SetPrivate(context, _jsSelf, [self weakRetain])) problem = @"could not set private backreference"; + } + + if (!problem) + { + if (![engine addGCRoot:&_jsSelf named:"Script object"]) + { + problem = @"could not add JavaScript root object"; + } + } + + if (!problem) + { + script = LoadScriptWithName(context, path, _jsSelf, &problem); + } + + // Set properties. + if (!problem && properties != nil) + { + for (keyEnum = [properties keyEnumerator]; (key = [keyEnum nextObject]); ) + { + if ([key isKindOfClass:[NSString class]]) + { + property = [properties objectForKey:key]; + [self defineProperty:property named:key]; + } + } + } + + // Run the script (allowing it to set up the properties we need, as well as setting up those event handlers) + if (!problem) + { + if (!JS_ExecuteScript(context, _jsSelf, script, &returnValue)) + { + problem = @"could not run script"; + } + + // We don't need the script any more - the event handlers hang around as long as the JS object exists. + JS_DestroyScript(context, script); + } + + if (!problem) + { + // Get display attributes from script + name = [[[self propertyNamed:@"name"] description] copy]; + if (name == nil) + { + name = [[self scriptNameFromPath:path] retain]; + [self setProperty:name named:@"name"]; + } + + version = [[[self propertyNamed:@"version"] description] copy]; + description = [[[self propertyNamed:@"description"] description] copy]; + + OOLog(@"script.javaScript.load.success", @"Loaded JavaScript OXP: %@ -- %@", [self displayName], description ? description : (NSString *)@"(no description)"); + } + + if (problem) + { + OOLog(@"script.javaScript.load.failed", @"***** Error loading JavaScript script %@ -- %@", path, problem); + [self release]; + self = nil; + } + + [engine releaseContext:context]; + + return self; +} + + +- (void) dealloc +{ + [name release]; + [description release]; + [version release]; + + JSContext *context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + JSObjectWrapperFinalize(context, _jsSelf); // Release weakref to self + JS_RemoveRoot(context, &_jsSelf); // Unroot jsSelf + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + + [weakSelf weakRefDrop]; + + [super dealloc]; +} + + +- (NSString *)jsClassName +{ + return @"Script"; +} + + ++ (OOJSScript *)currentlyRunningScript +{ + if (sRunningStack == NULL) return NULL; + return sRunningStack->current; +} + + ++ (NSArray *)scriptStack +{ + NSMutableArray *result = nil; + + result = [NSMutableArray array]; + AddStackToArrayReversed(result, sRunningStack); + return result; +} + + +- (id) weakRetain +{ + if (weakSelf == nil) weakSelf = [OOWeakReference weakRefWithObject:self]; + return [weakSelf retain]; +} + + +- (void) weakRefDied:(OOWeakReference *)weakRef +{ + if (weakRef == weakSelf) weakSelf = nil; +} + + +- (NSString *) name +{ + return name; +} + + +- (NSString *) scriptDescription +{ + return description; +} + + +- (NSString *) version +{ + return version; +} + + +- (void)runWithTarget:(Entity *)target +{ + OOLog(@"script.trace.js.run", @"Runing script \"%@\"", [self name]); + OOLogIndentIf(@"script.trace.js.run"); + + [self doEvent:@"tickle" withArguments:[NSArray arrayWithObject:[[PlayerEntity sharedPlayer] status_string]]]; + + OOLogOutdentIf(@"script.trace.js.run"); +} + + +- (JSFunction *) functionNamed:(NSString *)eventName context:(JSContext *)context +{ + BOOL OK; + jsval value; + JSFunction *function = NULL; + + OK = JS_GetProperty(context, _jsSelf, [eventName UTF8String], &value); + +#if SUPPORT_CHANGED_HANDLERS + if (!OK || value == JSVAL_VOID) + { + // Look up event name in renaming table. + static NSDictionary *changedHandlers = nil; + static NSMutableSet *notedChanges = nil; + id oldNames = nil; + NSEnumerator *oldNameEnum = nil; + NSString *oldName = nil; + NSString *key = nil; + + if (notedChanges == nil) + { + notedChanges = [[NSMutableSet alloc] init]; + changedHandlers = [ResourceManager dictionaryFromFilesNamed:@"changedScriptHandlers.plist" + inFolder:@"Config" + andMerge:NO]; + [changedHandlers retain]; + } + oldNames = [changedHandlers objectForKey:eventName]; + if ([oldNames isKindOfClass:[NSString class]]) oldNames = [NSArray arrayWithObject:oldNames]; + if ([oldNames isKindOfClass:[NSArray class]]) + { + for (oldNameEnum = [oldNames objectEnumerator]; (oldName = [oldNameEnum nextObject]) && value == JSVAL_VOID && OK; ) + { + OK = JS_GetProperty(context, _jsSelf, [oldName UTF8String], &value); + + if (OK && value != JSVAL_VOID) + { + key = [NSString stringWithFormat:@"%@\n%@", self->name, oldName]; + if (![notedChanges containsObject:key]) + { + [notedChanges addObject:key]; + OOReportJSWarning(context, @"The event handler %@ has been renamed to %@. The script %@ must be updated. The old form will not be supported in future versions of Oolite!", oldName, eventName, self->name); + } + } + } + } + } +#endif + + if (OK && value != JSVAL_VOID) + { + function = JS_ValueToFunction(context, value); + } + return function; +} + + +- (BOOL)doEvent:(NSString *)eventName withArguments:(NSArray *)arguments +{ + BOOL OK = YES; + jsval value; + JSFunction *function; + uintN i, argc; + jsval *argv = NULL; + RunningStack stackElement; + OOJavaScriptEngine *engine = nil; + JSContext *context = NULL; + + engine = [OOJavaScriptEngine sharedEngine]; + context = [engine acquireContext]; + + function = [self functionNamed:eventName context:context]; + if (function != NULL) + { + // Push self on stack of running scripts + stackElement.back = sRunningStack; + stackElement.current = self; + sRunningStack = &stackElement; + + // Convert arguments to JS values and make them temporarily un-garbage-collectable + argc = [arguments count]; + if (argc != 0) + { + argv = malloc(sizeof *argv * argc); + if (argv != NULL) + { + for (i = 0; i != argc; ++i) + { + argv[i] = [[arguments objectAtIndex:i] javaScriptValueInContext:context]; + if (JSVAL_IS_GCTHING(argv[i])) JS_AddNamedRoot(context, &argv[i], "JSScript event parameter"); + } + } + else argc = 0; + } + + // Actually call the function + OK = JS_CallFunction(context, _jsSelf, function, argc, argv, &value); + + // Re-garbage-collectibalize the arguments and free the array + if (argv != NULL) + { + for (i = 0; i != argc; ++i) + { + if (JSVAL_IS_GCTHING(argv[i])) JS_RemoveRoot(context, &argv[i]); + } + free(argv); + } + + // Pop running scripts stack + sRunningStack = stackElement.back; + + JS_ClearNewbornRoots(context); + } + else + { + // No function + OK = YES; + } + + [engine releaseContext:context]; + + return OK; +} + + +- (id)propertyNamed:(NSString *)propName +{ + BOOL OK; + jsval value = JSVAL_VOID; + JSContext *context = NULL; + id result = nil; + + if (propName == nil) return nil; + + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + OK = JSGetNSProperty(NULL, _jsSelf, propName, &value); + if (OK && !JSVAL_IS_VOID(value)) result = JSValueToObject(context, value); + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + + return result; +} + + +- (BOOL)setProperty:(id)value named:(NSString *)propName +{ + jsval jsValue; + JSContext *context = NULL; + BOOL result = NO; + + if (value == nil || propName == nil) return NO; + + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + jsValue = [value javaScriptValueInContext:context]; + if (!JSVAL_IS_VOID(jsValue)) + { + result = JSDefineNSProperty(context, _jsSelf, propName, jsValue, NULL, NULL, JSPROP_ENUMERATE); + } + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + return result; +} + + +- (BOOL)defineProperty:(id)value named:(NSString *)propName +{ + jsval jsValue; + JSContext *context = NULL; + + if (value == nil || propName == nil) return NO; + BOOL result = NO; + + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + jsValue = [value javaScriptValueInContext:context]; + if (!JSVAL_IS_VOID(jsValue)) + { + result = JSDefineNSProperty(context, _jsSelf, propName, jsValue, NULL, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); + } + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + return result; +} + + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + return OBJECT_TO_JSVAL(_jsSelf); +} + + ++ (void)pushScript:(OOJSScript *)script +{ + RunningStack *element = NULL; + + if (script == nil) return; + + element = malloc(sizeof *element); + if (element == NULL) exit(EXIT_FAILURE); + + element->back = sRunningStack; + element->current = script; + sRunningStack = element; +} + + ++ (void)popScript:(OOJSScript *)script +{ + RunningStack *element = NULL; + + if (script == nil) return; + assert(sRunningStack->current == script); + + element = sRunningStack; + sRunningStack = sRunningStack->back; + free(element); +} + +@end + + +@implementation OOJSScript (OOPrivate) + + + +/* Generate default name for script which doesn't set its name property when + first run. + + The generated name is .anon-script, where is selected as + follows: + * If path is nil (futureproofing), use the address of the script object. + * If the file's name is something other than script.*, use the file name. + * If the containing directory is something other than Config, use the + containing directory's name. + * Otherwise, use the containing directory's parent (which will generally + be an OXP root directory). + * If either of the two previous steps results in an empty string, fall + back on the full path. +*/ +- (NSString *)scriptNameFromPath:(NSString *)path +{ + NSString *lastComponent = nil; + NSString *truncatedPath = nil; + NSString *theName = nil; + + if (path == nil) theName = [NSString stringWithFormat:@"%p", self]; + else + { + lastComponent = [path lastPathComponent]; + if (![lastComponent hasPrefix:@"script."]) theName = lastComponent; + else + { + truncatedPath = [path stringByDeletingLastPathComponent]; + if (NSOrderedSame == [[truncatedPath lastPathComponent] caseInsensitiveCompare:@"Config"]) + { + truncatedPath = [truncatedPath stringByDeletingLastPathComponent]; + } + if (NSOrderedSame == [[truncatedPath pathExtension] caseInsensitiveCompare:@"oxp"]) + { + truncatedPath = [truncatedPath stringByDeletingPathExtension]; + } + + lastComponent = [truncatedPath lastPathComponent]; + theName = lastComponent; + } + } + + if (0 == [theName length]) theName = path; + + return [theName stringByAppendingString:@".anon-script"]; +} + +@end + + +@implementation OOScript (OOJavaScriptConversion) + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + return JSVAL_NULL; +} + +@end + + +void InitOOJSScript(JSContext *context, JSObject *global) +{ + sScriptPrototype = JS_InitClass(context, global, NULL, &sScriptClass, NULL, 0, NULL, sScriptMethods, NULL, NULL); + JSRegisterObjectConverter(&sScriptClass, JSBasicPrivateObjectConverter); +} + + +static void AddStackToArrayReversed(NSMutableArray *array, RunningStack *stack) +{ + if (stack != NULL) + { + AddStackToArrayReversed(array, stack->back); + [array addObject:stack->current]; + } +} + + +static JSScript *LoadScriptWithName(JSContext *context, NSString *path, JSObject *object, NSString **outErrorMessage) +{ +#if OO_CACHE_JS_SCRIPTS + OOCacheManager *cache = nil; +#endif + NSString *fileContents = nil; + NSData *data = nil; + JSScript *script = NULL; + + assert(outErrorMessage != NULL); + *outErrorMessage = nil; + +#if OO_CACHE_JS_SCRIPTS + // Look for cached compiled script + cache = [OOCacheManager sharedCache]; + data = [cache objectForKey:path inCache:@"compiled JavaScript scripts"]; + if (data != nil) + { + script = ScriptWithCompiledData(context, data); + } +#endif + + if (script == NULL) + { + fileContents = [NSString stringWithContentsOfUnicodeFile:path]; + if (fileContents != nil) data = [fileContents utf16DataWithBOM:NO]; + if (data == nil) *outErrorMessage = @"could not load file"; + else + { + script = JS_CompileUCScript(context, object, [data bytes], [data length] / sizeof(unichar), [path UTF8String], 1); + if (script == NULL) *outErrorMessage = @"compilation failed"; + } + +#if OO_CACHE_JS_SCRIPTS + if (script != NULL) + { + // Write compiled script to cache + data = CompiledScriptData(context, script); + [cache setObject:data forKey:path inCache:@"compiled JavaScript scripts"]; + } +#endif + } + + return script; +} + + +#if OO_CACHE_JS_SCRIPTS +static NSData *CompiledScriptData(JSContext *context, JSScript *script) +{ + JSXDRState *xdr = NULL; + NSData *result = nil; + uint32 length; + void *bytes = NULL; + + xdr = JS_XDRNewMem(context, JSXDR_ENCODE); + if (xdr != NULL) + { + if (JS_XDRScript(xdr, &script)) + { + bytes = JS_XDRMemGetData(xdr, &length); + if (bytes != NULL) + { + result = [NSData dataWithBytes:bytes length:length]; + } + } + JS_XDRDestroy(xdr); + } + + return result; +} + + +static JSScript *ScriptWithCompiledData(JSContext *context, NSData *data) +{ + JSXDRState *xdr = NULL; + JSScript *result = NULL; + + if (data == nil) return NULL; + + xdr = JS_XDRNewMem(context, JSXDR_DECODE); + if (xdr != NULL) + { + JS_XDRMemSetData(xdr, (void *)[data bytes], [data length]); + if (!JS_XDRScript(xdr, &result)) result = NULL; + + JS_XDRMemSetData(xdr, NULL, 0); // Don't let it be freed by XDRDestroy + JS_XDRDestroy(xdr); + } + + return result; +} +#endif diff --git a/src/Core/Scripting/OOJSShip.h b/src/Core/Scripting/OOJSShip.h new file mode 100644 index 00000000..c6be303a --- /dev/null +++ b/src/Core/Scripting/OOJSShip.h @@ -0,0 +1,36 @@ +/* + +OOJSShip.h + +JavaScript proxy for ShipEntities. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +@class ShipEntity; + + +void InitOOJSShip(JSContext *context, JSObject *global); + +JSClass *JSShipClass(void); +JSObject *JSShipPrototype(void); diff --git a/src/Core/Scripting/OOJSShip.m b/src/Core/Scripting/OOJSShip.m new file mode 100644 index 00000000..2b67ca8d --- /dev/null +++ b/src/Core/Scripting/OOJSShip.m @@ -0,0 +1,1082 @@ +/* + +OOJSShip.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSShip.h" +#import "OOJSEntity.h" +#import "OOJavaScriptEngine.h" +#import "ShipEntity.h" +#import "ShipEntityAI.h" +#import "ShipEntityScriptMethods.h" +#import "PlayerEntityScriptMethods.h" +#import "AI.h" +#import "OOStringParsing.h" +#import "EntityOOJavaScriptExtensions.h" +#import "OORoleSet.h" +#import "OOJSPlayer.h" +#import "OOShipGroup.h" + + +DEFINE_JS_OBJECT_GETTER(JSShipGetShipEntity, ShipEntity) + + +static JSObject *sShipPrototype; + + +static JSBool ShipGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool ShipSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool ShipSetScript(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipSetAI(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipSwitchAI(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipExitAI(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipReactToAIMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipDeployEscorts(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipDockEscorts(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipHasRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipEjectItem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipEjectSpecificItem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipDumpCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipSpawn(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipExplode(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipRemove(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipRunLegacyScriptActions(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipCommsMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipFireECM(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipHasEquipment(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +static BOOL RemoveOrExplodeShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult, BOOL explode); + + +static JSExtendedClass sShipClass = +{ + { + "Ship", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + ShipGetProperty, // getProperty + ShipSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kShip_name, // name, string, read-only + kShip_displayName, // name displayed on screen, string, read-only + kShip_roles, // roles, array, read-only + kShip_roleProbabilities, // roles and probabilities, dictionary, read-only + kShip_primaryRole, // Primary role, string, read-only + kShip_AI, // AI state machine name, string, read/write + kShip_AIState, // AI state machine state, string, read/write + kShip_fuel, // fuel, float, read/write + kShip_bounty, // bounty, unsigned int, read/write + kShip_subEntities, // subentities, array of Ship, read-only + kShip_hasSuspendedAI, // AI has suspended staes, boolean, read-only + kShip_target, // target, Ship, read/write + kShip_escorts, // deployed escorts, array of Ship, read-only + kShip_group, // group, ShipGroup, read/write + kShip_escortGroup, // group, ShipGroup, read-only + kShip_temperature, // hull temperature, double, read/write + kShip_heatInsulation, // hull heat insulation, double, read/write + kShip_entityPersonality, // per-ship random number, int, read-only + kShip_isBeacon, // is beacon, boolean, read-only + kShip_beaconCode, // beacon code, string, read-only (should probably be read/write, but the beacon list needs to be maintained.) + kShip_isFrangible, // frangible, boolean, read-only + kShip_isCloaked, // cloaked, boolean, read/write (if cloaking device installed) + kShip_isJamming, // jamming scanners, boolean, read/write (if jammer installed) + kShip_potentialCollider, // "proximity alert" ship, Entity, read-only + kShip_hasHostileTarget, // has hostile target, boolean, read-only + kShip_weaponRange, // weapon range, double, read-only + kShip_scannerRange, // scanner range, double, read-only + kShip_reportAIMessages, // report AI messages, boolean, read/write + kShip_withinStationAegis, // within main station aegis, boolean, read/write + kShip_cargoCapacity, // free cargo space, integer, read-only + kShip_cargoSpaceUsed, // cargo on board, integer, read-only + kShip_availableCargoSpace, // maximum cargo, integer, read-only + kShip_speed, // current flight speed, double, read-only + kShip_desiredSpeed, // AI desired flight speed, double, read/write + kShip_maxSpeed, // maximum flight speed, double, read-only + kShip_script, // script, Script, read-only + kShip_isPirate, // is pirate, boolean, read-only + kShip_isPlayer, // is player, boolean, read-only + kShip_isPolice, // is police, boolean, read-only + kShip_isThargoid, // is thargoid, boolean, read-only + kShip_isTrader, // is trader, boolean, read-only + kShip_isPirateVictim, // is pirate victim, boolean, read-only + kShip_isMissile, // is missile, boolean, read-only + kShip_isMine, // is mine, boolean, read-only + kShip_isWeapon, // is missile or mine, boolean, read-only + kShip_scriptInfo, // arbitrary data for scripts, dictionary, read-only + kShip_trackCloseContacts, // generate close contact events, boolean, read/write + kShip_passengerCount, // number of passengers on ship, integer, read-only + kShip_passengerCapacity // amount of passenger space on ship, integer, read-only + +}; + + +static JSPropertySpec sShipProperties[] = +{ + // JS name ID flags + { "AI", kShip_AI, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "AIState", kShip_AIState, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "beaconCode", kShip_beaconCode, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "bounty", kShip_bounty, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "entityPersonality", kShip_entityPersonality, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "escorts", kShip_escorts, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "group", kShip_group, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "escortGroup", kShip_escortGroup, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "fuel", kShip_fuel, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "hasHostileTarget", kShip_hasHostileTarget, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "hasSuspendedAI", kShip_hasSuspendedAI, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "heatInsulation", kShip_heatInsulation, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "isBeacon", kShip_isBeacon, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isCloaked", kShip_isCloaked, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "isFrangible", kShip_isFrangible, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isJamming", kShip_isJamming, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPirate", kShip_isPirate, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPirateVictim", kShip_isPirateVictim, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPlayer", kShip_isPlayer, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isPolice", kShip_isPolice, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isThargoid", kShip_isThargoid, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isTrader", kShip_isTrader, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isMissile", kShip_isMissile, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isMine", kShip_isMine, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isWeapon", kShip_isWeapon, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, +// "cargo" reserved for array of cargo pods or similar. + { "cargoSpaceUsed", kShip_cargoSpaceUsed, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "cargoCapacity", kShip_cargoCapacity, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "availableCargoSpace", kShip_availableCargoSpace, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "maxSpeed", kShip_maxSpeed, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "potentialCollider", kShip_potentialCollider, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "primaryRole", kShip_primaryRole, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "reportAIMessages", kShip_reportAIMessages, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "roleProbabilities", kShip_roleProbabilities, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "roles", kShip_roles, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "scannerRange", kShip_scannerRange, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "script", kShip_script, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "scriptInfo", kShip_scriptInfo, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "name", kShip_name, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "displayName", kShip_displayName, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "speed", kShip_speed, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "desiredSpeed", kShip_desiredSpeed, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "subEntities", kShip_subEntities, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "target", kShip_target, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "temperature", kShip_temperature, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "weaponRange", kShip_weaponRange, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "withinStationAegis", kShip_withinStationAegis, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "trackCloseContacts", kShip_trackCloseContacts, JSPROP_PERMANENT | JSPROP_ENUMERATE }, +// "passengers" reserved for array of characters or similar. + { "passengerCount", kShip_passengerCount, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "passengerCapacity", kShip_passengerCapacity, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sShipMethods[] = +{ + // JS name Function min args + { "setScript", ShipSetScript, 1 }, + { "setAI", ShipSetAI, 1 }, + { "switchAI", ShipSwitchAI, 1 }, + { "exitAI", ShipExitAI, 0 }, + { "reactToAIMessage", ShipReactToAIMessage, 1 }, + { "deployEscorts", ShipDeployEscorts, 0 }, + { "dockEscorts", ShipDockEscorts, 0 }, + { "hasRole", ShipHasRole, 1 }, + { "ejectItem", ShipEjectItem, 1 }, + { "ejectSpecificItem", ShipEjectSpecificItem, 1 }, + { "dumpCargo", ShipDumpCargo, 0 }, + { "runLegacyScriptActions", ShipRunLegacyScriptActions, 2 }, + { "spawn", ShipSpawn, 1 }, + { "explode", ShipExplode, 0 }, + { "remove", ShipRemove, 0 }, + { "commsMessage", ShipCommsMessage, 1 }, + { "fireECM", ShipFireECM, 0 }, + { "hasEquipment", ShipHasEquipment, 1 }, + { 0 } +}; + + +void InitOOJSShip(JSContext *context, JSObject *global) +{ + sShipPrototype = JS_InitClass(context, global, JSEntityPrototype(), &sShipClass.base, NULL, 0, sShipProperties, sShipMethods, NULL, NULL); + JSRegisterObjectConverter(&sShipClass.base, JSBasicPrivateObjectConverter); +} + + +JSClass *JSShipClass(void) +{ + return &sShipClass.base; +} + + +JSObject *JSShipPrototype(void) +{ + return sShipPrototype; +} + + +static JSBool ShipGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + ShipEntity *entity = nil; + id result = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSShipGetShipEntity(context, this, &entity))) return NO; // NOTE: entity may be nil. + if (EXPECT_NOT(!JS_EnterLocalRootScope(context))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kShip_name: + result = [entity name]; + break; + + case kShip_displayName: + result = [entity displayName]; + break; + + case kShip_roles: + result = [[entity roleSet] sortedRoles]; + break; + + case kShip_roleProbabilities: + result = [[entity roleSet] rolesAndProbabilities]; + break; + + case kShip_primaryRole: + result = [entity primaryRole]; + break; + + case kShip_AI: + result = [[entity getAI] name]; + break; + + case kShip_AIState: + result = [[entity getAI] state]; + break; + + case kShip_fuel: + OK = JS_NewDoubleValue(context, [entity fuel] * 0.1, outValue); + break; + + case kShip_bounty: + *outValue = INT_TO_JSVAL([entity legalStatus]); + OK = YES; + break; + + case kShip_subEntities: + result = [entity subEntitiesForScript]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_hasSuspendedAI: + *outValue = BOOLToJSVal([[entity getAI] hasSuspendedStateMachines]); + OK = YES; + break; + + case kShip_target: + result = [entity primaryTarget]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_escorts: + // FIXME: use implemention in oolite-global-prefix.js once ShipGroup works. + result = [[entity escortGroup] memberArrayExcludingLeader]; + if ([result count] == 0) result = [NSNull null]; + break; + + case kShip_group: + result = [entity group]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_escortGroup: + result = [entity escortGroup]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_temperature: + OK = JS_NewDoubleValue(context, [entity temperature] / SHIP_MAX_CABIN_TEMP, outValue); + break; + + case kShip_heatInsulation: + OK = JS_NewDoubleValue(context, [entity heatInsulation], outValue); + break; + + case kShip_entityPersonality: + *outValue = INT_TO_JSVAL([entity entityPersonalityInt]); + OK = YES; + break; + + case kShip_isBeacon: + *outValue = BOOLToJSVal([entity isBeacon]); + OK = YES; + break; + + case kShip_beaconCode: + result = [entity beaconCode]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_isFrangible: + *outValue = BOOLToJSVal([entity isFrangible]); + OK = YES; + break; + + case kShip_isCloaked: + *outValue = BOOLToJSVal([entity isCloaked]); + OK = YES; + break; + + case kShip_isJamming: + *outValue = BOOLToJSVal([entity isJammingScanning]); + OK = YES; + break; + + case kShip_potentialCollider: + result = [entity proximity_alert]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_hasHostileTarget: + *outValue = BOOLToJSVal([entity hasHostileTarget]); + OK = YES; + break; + + case kShip_weaponRange: + OK = JS_NewDoubleValue(context, [entity weaponRange], outValue); + break; + + case kShip_scannerRange: + OK = JS_NewDoubleValue(context, [entity scannerRange], outValue); + break; + + case kShip_reportAIMessages: + *outValue = BOOLToJSVal([entity reportAIMessages]); + OK = YES; + break; + + case kShip_withinStationAegis: + *outValue = BOOLToJSVal([entity withinStationAegis]); + OK = YES; + break; + + case kShip_cargoCapacity: + *outValue = INT_TO_JSVAL([entity maxCargo]); + OK = YES; + break; + + case kShip_cargoSpaceUsed: + *outValue = INT_TO_JSVAL([entity maxCargo] - [entity availableCargoSpace]); + OK = YES; + break; + + case kShip_availableCargoSpace: + *outValue = INT_TO_JSVAL([entity availableCargoSpace]); + OK = YES; + break; + + case kShip_speed: + OK = JS_NewDoubleValue(context, [entity flightSpeed], outValue); + break; + + case kShip_desiredSpeed: + OK = JS_NewDoubleValue(context, [entity desiredSpeed], outValue); + break; + + case kShip_maxSpeed: + OK = JS_NewDoubleValue(context, [entity maxFlightSpeed], outValue); + break; + + case kShip_script: + result = [entity shipScript]; + if (result == nil) result = [NSNull null]; + break; + + case kShip_isPirate: + *outValue = BOOLToJSVal([entity isPirate]); + OK = YES; + break; + + case kShip_isPlayer: + *outValue = BOOLToJSVal([entity isPlayer]); + OK = YES; + break; + + case kShip_isPolice: + *outValue = BOOLToJSVal([entity isPolice]); + OK = YES; + break; + + case kShip_isThargoid: + *outValue = BOOLToJSVal([entity isThargoid]); + OK = YES; + break; + + case kShip_isTrader: + *outValue = BOOLToJSVal([entity isTrader]); + OK = YES; + break; + + case kShip_isPirateVictim: + *outValue = BOOLToJSVal([entity isPirateVictim]); + OK = YES; + break; + + case kShip_isMissile: + *outValue = BOOLToJSVal([entity isMissile]); + OK = YES; + break; + + case kShip_isMine: + *outValue = BOOLToJSVal([entity isMine]); + OK = YES; + break; + + case kShip_isWeapon: + *outValue = BOOLToJSVal([entity isWeapon]); + OK = YES; + break; + + case kShip_scriptInfo: + result = [entity scriptInfo]; + if (result == nil) result = [NSDictionary dictionary]; // empty rather than NULL + break; + + case kShip_trackCloseContacts: + *outValue = BOOLToJSVal([entity trackCloseContacts]); + OK = YES; + break; + + case kShip_passengerCount: + *outValue = INT_TO_JSVAL([entity passengerCount]); + OK = YES; + break; + + case kShip_passengerCapacity: + *outValue = INT_TO_JSVAL([entity passengerCapacity]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"Ship", JSVAL_TO_INT(name)); + } + + if (result != nil) + { + *outValue = [result javaScriptValueInContext:context]; + OK = YES; + } + JS_LeaveLocalRootScope(context); + return OK; +} + + +static JSBool ShipSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + ShipEntity *entity = nil; + ShipEntity *target = nil; + NSString *sValue = nil; + jsdouble fValue; + int32 iValue; + JSBool bValue; + OOShipGroup *group = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSShipGetShipEntity(context, this, &entity))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kShip_name: + if ([entity isPlayer]) + { + OOReportJSError(context, @"Ship.%@ [setter]: cannot set %@ for player.", @"name", @"name"); + } + else + { + sValue = JSValToNSString(context,*value); + if (sValue != nil) + { + [entity setName:sValue]; + OK = YES; + } + } + break; + + case kShip_displayName: + if ([entity isPlayer]) + { + OOReportJSError(context, @"Ship.%@ [setter]: cannot set %@ for player.", @"displayName", @"displayName"); + } + else + { + sValue = JSValToNSString(context,*value); + if (sValue != nil) + { + [entity setDisplayName:sValue]; + OK = YES; + } + } + break; + + case kShip_primaryRole: + if ([entity isPlayer]) + { + OOReportJSError(context, @"Ship.%@ [setter]: cannot set %@ for player.", @"primaryRole", @"primary role"); + } + else + { + sValue = JSValToNSString(context,*value); + if (sValue != nil) + { + [entity setPrimaryRole:sValue]; + OK = YES; + } + } + break; + + case kShip_AIState: + if ([entity isPlayer]) + { + OOReportJSError(context, @"Ship.%@ [setter]: cannot set %@ for player.", @"AIState", @"AI state"); + } + else + { + sValue = JSValToNSString(context,*value); + if (sValue != nil) + { + [[entity getAI] setState:sValue]; + OK = YES; + } + } + break; + + case kShip_fuel: + if (JS_ValueToNumber(context, *value, &fValue)) + { + fValue = OOClamp_0_max_d(fValue, 7.0); + [entity setFuel:lround(fValue * 10.0)]; + OK = YES; + } + break; + + case kShip_bounty: + if (JS_ValueToInt32(context, *value, &iValue)) + { + if (iValue < 0) iValue = 0; + [entity setBounty:iValue]; + OK = YES; + } + break; + + case kShip_target: + if (JSValueToEntity(context, *value, &target) && [target isKindOfClass:[ShipEntity class]]) + { + [entity setTargetForScript:target]; + OK = YES; + } + break; + + case kShip_group: + group = JSValueToObjectOfClass(context, *value, [OOShipGroup class]); + if (group != nil || JSVAL_IS_NULL(*value)) + { + [entity setGroup:group]; + OK = YES; + } + break; + + case kShip_temperature: + if (JS_ValueToNumber(context, *value, &fValue)) + { + fValue = OOMax_d(fValue, 0.0); + [entity setTemperature:fValue * SHIP_MAX_CABIN_TEMP]; + OK = YES; + } + break; + + case kShip_heatInsulation: + if (JS_ValueToNumber(context, *value, &fValue)) + { + fValue = OOMax_d(fValue, 0.125); + [entity setHeatInsulation:fValue]; + OK = YES; + } + break; + + case kShip_isCloaked: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [entity setCloaked:bValue]; + OK = YES; + } + break; + + case kShip_reportAIMessages: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [entity setReportAIMessages:bValue]; + OK = YES; + } + break; + + case kShip_trackCloseContacts: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [entity setTrackCloseContacts:bValue]; + OK = YES; + } + break; + + case kShip_desiredSpeed: + if ([entity isPlayer]) + { + OOReportJSError(context, @"Ship.%@ [setter]: cannot set %@ for player.", @"desiredSpeed", @"desired speed"); + } + else + { + if (JS_ValueToNumber(context, *value, &fValue)) + { + [entity setDesiredSpeed:fmax(fValue, 0.0)]; + OK = YES; + } + } + break; + + default: + OOReportJSBadPropertySelector(context, @"Ship", JSVAL_TO_INT(name)); + } + if (OK == NO) + { + OOReportJSWarning(context, @"Invalid value type for this property. Value not set."); + } + return OK; +} + + +// *** Methods *** + +// setScript(scriptName : String) +static JSBool ShipSetScript(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *name = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + name = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(name == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"setScript", argc, argv, nil, @"script name"); + return NO; + } + if (EXPECT_NOT([thisEnt isPlayer])) + { + OOReportJSErrorForCaller(context, @"Ship", @"setScript", @"Cannot change script for player."); + return NO; + } + + [thisEnt setShipScript:name]; + return YES; +} + + +// setAI(aiName : String) +static JSBool ShipSetAI(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *name = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + name = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(name == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"setAI", argc, argv, nil, @"AI name"); + return NO; + } + if (EXPECT_NOT([thisEnt isPlayer])) + { + OOReportJSErrorForCaller(context, @"Ship", @"setAI", @"Cannot modify AI for player."); + return NO; + } + + [thisEnt setAITo:name]; + return YES; +} + + +// switchAI(aiName : String) +static JSBool ShipSwitchAI(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *name = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + name = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(name == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"switchAI", argc, argv, nil, @"AI name"); + return NO; + } + if (EXPECT_NOT([thisEnt isPlayer])) + { + OOReportJSErrorForCaller(context, @"Ship", @"switchAI", @"Cannot modify AI for player."); + return NO; + } + + [thisEnt switchAITo:name]; + return YES; +} + + +// exitAI() +static JSBool ShipExitAI(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + AI *thisAI = nil; + NSString *message = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + if (EXPECT_NOT([thisEnt isPlayer])) + { + OOReportJSErrorForCaller(context, @"Ship", @"exitAI", @"Cannot modify AI for player."); + return NO; + } + thisAI = [thisEnt getAI]; + + if (argc > 0) + { + message = JSValToNSString(context, argv[0]); + } + + if (![thisAI hasSuspendedStateMachines]) + { + OOReportJSWarningForCaller(context, @"Ship", @"exitAI()", @"Cannot exit current AI state machine because there are no suspended state machines."); + } + else + { + [thisAI exitStateMachineWithMessage:message]; + } + return YES; +} + + +// reactToAIMessage(message : String) +static JSBool ShipReactToAIMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *message = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + message = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(message == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"reactToAIMessage", argc, argv, nil, @"message"); + return NO; + } + if (EXPECT_NOT([thisEnt isPlayer])) + { + OOReportJSErrorForCaller(context, @"Ship", @"reactToAIMessage", @"Cannot modify AI for player."); + return NO; + } + + [thisEnt reactToAIMessage:message]; + return YES; +} + + +// deployEscorts() +static JSBool ShipDeployEscorts(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + [thisEnt deployEscorts]; + return YES; +} + + +// dockEscorts() +static JSBool ShipDockEscorts(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + [thisEnt dockEscorts]; + return YES; +} + + +// hasRole(role : String) : Boolean +static JSBool ShipHasRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *role = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + role = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(role == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"hasRole", argc, argv, nil, @"role"); + return NO; + } + + *outResult = BOOLToJSVal([thisEnt hasRole:role]); + return YES; +} + + +// ejectItem(role : String) : Ship +static JSBool ShipEjectItem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *role = nil; + ShipEntity *result = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + role = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(role == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"ejectItem", argc, argv, nil, @"role"); + return NO; + } + + result = [thisEnt ejectShipOfRole:role]; + *outResult = [result javaScriptValueInContext:context]; + return YES; +} + + +// ejectSpecificItem(itemKey : String) : Ship +static JSBool ShipEjectSpecificItem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *itemKey = nil; + ShipEntity *result = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + itemKey = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(itemKey == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"ejectSpecificItem", argc, argv, nil, @"ship key"); + return NO; + } + + result = [thisEnt ejectShipOfType:itemKey]; + *outResult = [result javaScriptValueInContext:context]; + return YES; +} + + +// dumpCargo() : Ship +static JSBool ShipDumpCargo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + ShipEntity *result = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + if ([thisEnt isPlayer] && [(PlayerEntity *)thisEnt isDocked]) + { + OOReportJSWarningForCaller(context, @"Player", @"dumpCargo", @"Can't dump cargo while docked, ignoring."); + return YES; + } + + result = [thisEnt dumpCargoItem]; + *outResult = [result javaScriptValueInContext:context]; + return YES; +} + + +// spawn(role : String [, number : count]) : Array +static JSBool ShipSpawn(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *role = nil; + int32 count = 1; + BOOL gotCount = YES; + NSArray *result = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + role = JSValToNSString(context, argv[0]); + if (argc > 1) gotCount = JS_ValueToInt32(context, argv[1], &count); + if (EXPECT_NOT(role == nil || !gotCount || count < 1 || count > 64)) + { + OOReportJSBadArguments(context, @"Ship", @"spawn", argc, argv, nil, @"role and optional positive count no greater than 64"); + return NO; + } + + result = [thisEnt spawnShipsWithRole:role count:count]; + + *outResult = [result javaScriptValueInContext:context]; + return YES; +} + + +// explode() +static JSBool ShipExplode(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + return RemoveOrExplodeShip(context, this, argc, argv, outResult, YES); +} + + +// remove() +static JSBool ShipRemove(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + return RemoveOrExplodeShip(context, this, argc, argv, outResult, NO); +} + + +// runLegacyShipActions(target : Ship, actions : Array) +static JSBool ShipRunLegacyScriptActions(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + PlayerEntity *player = nil; + ShipEntity *target = nil; + NSArray *actions = nil; + + player = OOPlayerForScripting(); + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + actions = JSValueToObject(context, argv[1]); + if (EXPECT_NOT(!JSVAL_IS_OBJECT(argv[0]) || + !JSShipGetShipEntity(context, JSVAL_TO_OBJECT(argv[0]), &target) || + ![actions isKindOfClass:[NSArray class]])) + { + OOReportJSBadArguments(context, @"Ship", @"runLegacyScriptActions", argc, argv, nil, @"target and array of actions"); + return NO; + } + + [player setScriptTarget:thisEnt]; + [player runUnsanitizedScriptActions:actions + allowingAIMethods:YES + withContextName:[NSString stringWithFormat:@"ship \"%@\" legacy actions", [thisEnt name]] + forTarget:target]; + + return YES; +} + + +// commsMessage(message : String) +static JSBool ShipCommsMessage(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *message = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + message = JSValToNSString(context, *argv); + if (EXPECT_NOT(message == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"commsMessage", argc, argv, nil, @"message"); + return NO; + } + + if (![thisEnt isPlayer]) + { + [thisEnt commsMessage:message withUnpilotedOverride:YES]; + } + return YES; +} + + +// fireECM() +static JSBool ShipFireECM(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + if (![thisEnt fireECM]) + { + OOReportJSWarning(context, @"Ship %@ was requested to fire ECM burst but does not carry ECM equipment.", thisEnt); + } + return YES; +} + + +// hasEquipment(key : String [, includeWeapons : Boolean]) : Boolean +static JSBool ShipHasEquipment(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + ShipEntity *thisEnt = nil; + NSString *key = nil; + JSBool includeWeapons = YES; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + key = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(key == nil)) + { + OOReportJSBadArguments(context, @"Ship", @"hasEquipment", argc, argv, nil, @"equipment key"); + return NO; + } + + if (argc > 1) + { + if (EXPECT_NOT(!JS_ValueToBoolean(context, argv[1], &includeWeapons))) + { + OOReportJSBadArguments(context, @"Ship", @"hasEquipment", argc - 1, argv + 1, nil, @"boolean"); + return NO; + } + } + + *outResult = BOOLToJSVal([thisEnt hasEquipmentItem:key includeWeapons:includeWeapons]); + return YES; +} + + +static BOOL RemoveOrExplodeShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult, BOOL explode) +{ + ShipEntity *thisEnt = nil; + + if (!JSShipGetShipEntity(context, this, &thisEnt)) return YES; // stale reference, no-op. + + if (!explode && [thisEnt isPlayer]) return YES; // Silently fail on player.ship.remove() + + if (thisEnt == (ShipEntity *)[UNIVERSE station]) + { + // Allow exploding of main station (e.g. nova mission) + [UNIVERSE unMagicMainStation]; + } + + [thisEnt setSuppressExplosion:!explode]; + [thisEnt setEnergy:1]; + [thisEnt takeEnergyDamage:500000000.0 from:nil becauseOf:nil]; + + return YES; +} diff --git a/src/Core/Scripting/OOJSShipGroup.h b/src/Core/Scripting/OOJSShipGroup.h new file mode 100644 index 00000000..b37eb720 --- /dev/null +++ b/src/Core/Scripting/OOJSShipGroup.h @@ -0,0 +1,31 @@ +/* + +OOJSShipGroup.h + +JavaScript wrapper for ship group objects. + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + + +void InitOOJSShipGroup(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSShipGroup.m b/src/Core/Scripting/OOJSShipGroup.m new file mode 100644 index 00000000..8b77b271 --- /dev/null +++ b/src/Core/Scripting/OOJSShipGroup.m @@ -0,0 +1,329 @@ +/* + +OOShipGroup.m + + +Oolite +Copyright (C) 2004-200/ Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOShipGroup.h" +#import "OOJavaScriptEngine.h" +#import "OOShipGroup.h" +#import "Universe.h" + + +static JSObject *sShipGroupPrototype; + + +static JSBool ShipGroupGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool ShipGroupSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static JSBool ShipGroupConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +// Methods +static JSBool ShipGroupAddShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipGroupRemoveShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool ShipGroupContainsShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sShipGroupClass = +{ + { + "ShipGroup", + JSCLASS_HAS_PRIVATE, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + ShipGroupGetProperty, // getProperty + ShipGroupSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kShipGroup_ships, // array of ships, double, read-only + kShipGroup_leader, // leader, Ship, read/write + kShipGroup_name, // name, string, read/write + kShipGroup_count, // number of ships, integer, read-only +}; + + +static JSPropertySpec sShipGroupProperties[] = +{ + // JS name ID flags + { "ships", kShipGroup_ships, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "leader", kShipGroup_leader, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "name", kShipGroup_name, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "count", kShipGroup_count, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sShipGroupMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0 }, + { "addShip", ShipGroupAddShip, 1 }, + { "removeShip", ShipGroupRemoveShip, 1 }, + { "containsShip", ShipGroupContainsShip, 1 }, + { 0 } +}; + + +void InitOOJSShipGroup(JSContext *context, JSObject *global) +{ + sShipGroupPrototype = JS_InitClass(context, global, NULL, &sShipGroupClass.base, ShipGroupConstruct, 0, sShipGroupProperties, sShipGroupMethods, NULL, NULL); + JSRegisterObjectConverter(&sShipGroupClass.base, JSBasicPrivateObjectConverter); +} + + +static BOOL JSShipGroupGetShipGroup(JSContext *context, JSObject *entityObj, OOShipGroup **outShipGroup) +{ + id value = nil; + + value = JSObjectToObjectOfClass(context, entityObj, [OOShipGroup class]); + if (value != nil && outShipGroup != NULL) + { + *outShipGroup = value; + return YES; + } + return NO; +} + + +static JSBool ShipGroupGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + OOShipGroup *group = nil; + BOOL OK = NO; + id result = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSShipGroupGetShipGroup(context, this, &group))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kShipGroup_ships: + result = [group memberArray]; + if (result == nil) result = [NSArray array]; + break; + + case kShipGroup_leader: + result = [group leader]; + break; + + case kShipGroup_name: + result = [group name]; + if (result == nil) result = [NSNull null]; + break; + + case kShipGroup_count: + *outValue = INT_TO_JSVAL([group count]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"ShipGroup", JSVAL_TO_INT(name)); + } + + if (!OK && result != nil) + { + *outValue = [result javaScriptValueInContext:context]; + OK = YES; + } + + return OK; +} + + +static JSBool ShipGroupSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + OOShipGroup *group = nil; + ShipEntity *shipValue = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSShipGroupGetShipGroup(context, this, &group))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kShipGroup_leader: + shipValue = JSValueToObjectOfClass(context, *value, [ShipEntity class]); + if (shipValue != nil || JSVAL_IS_NULL(*value)) + { + [group setLeader:shipValue]; + OK = YES; + } + break; + + case kShipGroup_name: + [group setName:[NSString stringWithJavaScriptValue:*value inContext:context]]; + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"ShipGroup", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// new ShipGroup([name : String [, leader : Ship]]) : ShipGroup +static JSBool ShipGroupConstruct(JSContext *context, JSObject *inThis, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *name = nil; + ShipEntity *leader = nil; + OOShipGroup *group = nil; + + if (argc >= 1) + { + if (!JSVAL_IS_STRING(argv[0])) + { + OOReportJSBadArguments(context, nil, @"ShipGroup()", 1, argv, @"Could not create ShipGroup", @"group name"); + return NO; + } + name = [NSString stringWithJavaScriptValue:argv[0] inContext:context]; + } + + if (argc >= 2) + { + leader = JSValueToObjectOfClass(context, argv[1], [ShipEntity class]); + if (leader == nil && !JSVAL_IS_NULL(argv[1])) + { + OOReportJSBadArguments(context, nil, @"ShipGroup()", 1, argv + 1, @"Could not create ShipGroup", @"ship"); + return NO; + } + } + + + group = [OOShipGroup groupWithName:name leader:leader]; + *outResult = [group javaScriptValueInContext:context]; + + return YES; +} + + +@implementation OOShipGroup (OOJavaScriptExtensions) + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + jsval result = JSVAL_NULL; + + if (_jsSelf == NULL) + { + _jsSelf = JS_NewObject(context, &sShipGroupClass.base, sShipGroupPrototype, NULL); + if (_jsSelf != NULL) + { + if (!JS_SetPrivate(context, _jsSelf, [self retain])) _jsSelf = NULL; + } + + if (_jsSelf != NULL) JS_AddNamedRoot(context, &_jsSelf, "OOShipGroup"); + } + + if (_jsSelf != NULL) result = OBJECT_TO_JSVAL(_jsSelf); + + return result; +} + +@end + + + +// *** Methods *** + +// addShip(ship : Ship) +static JSBool ShipGroupAddShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOShipGroup *thisGroup = nil; + ShipEntity *ship = nil; + + if (EXPECT_NOT(!JSShipGroupGetShipGroup(context, this, &thisGroup))) return NO; + + ship = JSValueToObjectOfClass(context, argv[0], [ShipEntity class]); + if (ship == nil) + { + if (JSVAL_IS_NULL(argv[0])) return YES; // OK, do nothing for null ship. + + OOReportJSBadArguments(context, @"ShipGroup", @"addShip", 1, argv, nil, @"ship"); + return NO; + } + + [thisGroup addShip:ship]; + return YES; +} + + +// removeShip(ship : Ship) +static JSBool ShipGroupRemoveShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOShipGroup *thisGroup = nil; + ShipEntity *ship = nil; + + if (EXPECT_NOT(!JSShipGroupGetShipGroup(context, this, &thisGroup))) return NO; + + ship = JSValueToObjectOfClass(context, argv[0], [ShipEntity class]); + if (ship == nil) + { + if (JSVAL_IS_NULL(argv[0])) return YES; // OK, do nothing for null ship. + + OOReportJSBadArguments(context, @"ShipGroup", @"removeShip", 1, argv, nil, @"ship"); + return NO; + } + + [thisGroup removeShip:ship]; + return YES; +} + + +// containsShip(ship : Ship) : Boolean +static JSBool ShipGroupContainsShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOShipGroup *thisGroup = nil; + ShipEntity *ship = nil; + + if (EXPECT_NOT(!JSShipGroupGetShipGroup(context, this, &thisGroup))) return NO; + + ship = JSValueToObjectOfClass(context, argv[0], [ShipEntity class]); + if (ship == nil) + { + if (JSVAL_IS_NULL(argv[0])) + { + // OK, return false for null ship. + *outResult = JSVAL_FALSE; + return YES; + } + + OOReportJSBadArguments(context, @"ShipGroup", @"containsShip", 1, argv, nil, @"ship"); + return NO; + } + + *outResult = BOOLEAN_TO_JSVAL([thisGroup containsShip:ship]); + return YES; +} diff --git a/src/Core/Scripting/OOJSSound.h b/src/Core/Scripting/OOJSSound.h new file mode 100644 index 00000000..c5beafb0 --- /dev/null +++ b/src/Core/Scripting/OOJSSound.h @@ -0,0 +1,40 @@ +/* + +OOJSSound.h + +JavaScript sound object. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import +@class OOSound; + + +void InitOOJSSound(JSContext *context, JSObject *global); + + +/* SoundFromJSValue() + + Convert a JS value to a sound. The value may be either a Sound object or a + string specifying a sound name. + */ +OOSound *SoundFromJSValue(JSContext *context, jsval value); diff --git a/src/Core/Scripting/OOJSSound.m b/src/Core/Scripting/OOJSSound.m new file mode 100644 index 00000000..59ed46e4 --- /dev/null +++ b/src/Core/Scripting/OOJSSound.m @@ -0,0 +1,267 @@ +/* + +OOJSSound.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSSound.h" +#import "OOJavaScriptEngine.h" +#import "OOSound.h" +#import "OOMusicController.h" +#import "ResourceManager.h" +#import "Universe.h" + + +static JSObject *sSoundPrototype; + + +DEFINE_JS_OBJECT_GETTER(JSSoundGetSound, OOSound) + + +static OOSound *GetNamedSound(NSString *name); + + +static JSBool SoundGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); + +// Static methods +static JSBool SoundStaticLoad(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SoundStaticPlayMusic(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SoundStaticStopMusic(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sSoundClass = +{ + { + "Sound", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + SoundGetProperty, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality. Relies on the fact that the resource manager will always return the same object for a given sound name. + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kSound_name +}; + + +static JSPropertySpec sSoundProperties[] = +{ + // JS name ID flags + { "name", kSound_name, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sSoundMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0, }, + { 0 } +}; + + +static JSFunctionSpec sSoundStaticMethods[] = +{ + // JS name Function min args + { "load", SoundStaticLoad, 1, }, + { "playMusic", SoundStaticPlayMusic, 1, }, + { "stopMusic", SoundStaticStopMusic, 0, }, + { 0 } +}; + + +// *** Public *** + +void InitOOJSSound(JSContext *context, JSObject *global) +{ + sSoundPrototype = JS_InitClass(context, global, NULL, &sSoundClass.base, NULL, 0, sSoundProperties, sSoundMethods, NULL, sSoundStaticMethods); + JSRegisterObjectConverter(&sSoundClass.base, JSBasicPrivateObjectConverter); +} + + +OOSound *SoundFromJSValue(JSContext *context, jsval value) +{ + if (JSVAL_IS_STRING(value)) + { + return GetNamedSound(JSValToNSString(context, value)); + } + else + { + return JSValueToObjectOfClass(context, value, [OOSound class]); + } +} + + +// *** Implementation stuff *** + +static JSBool SoundGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + OOSound *sound = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSSoundGetSound(context, this, &sound))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kSound_name: + *outValue = [[sound name] javaScriptValueInContext:context]; + break; + + default: + OOReportJSBadPropertySelector(context, @"Sound", JSVAL_TO_INT(name)); + return NO; + } + + return YES; +} + + +static OOSound *GetNamedSound(NSString *name) +{ + OOSound *sound = nil; + + if ([name hasPrefix:@"["] && [name hasSuffix:@"["]) + { + sound = [OOSound soundWithCustomSoundKey:name]; + } + else + { + sound = [ResourceManager ooSoundNamed:name inFolder:@"Sounds"]; + } + + return sound; +} + + +// *** Static methods *** + +// load(name : String) : Sound +static JSBool SoundStaticLoad(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *name = nil; + OOSound *sound = nil; + + name = JSValToNSString(context, argv[0]); + if (name == nil) + { + OOReportJSBadArguments(context, @"Sound", @"load", argc, argv, nil, @"string"); + return NO; + } + + sound = GetNamedSound(name); + *outResult = [sound javaScriptValueInContext:context]; + if (*outResult == JSVAL_VOID) *outResult = JSVAL_NULL; // No sound by that name + return YES; +} + + +static JSBool SoundStaticPlayMusic(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *name = nil; + JSBool loop = NO; + + name = JSValToNSString(context, argv[0]); + if (name == nil) + { + OOReportJSBadArguments(context, @"Sound", @"playMusic", 1, &argv[0], nil, @"string"); + return NO; + } + if (argc >= 2) + { + if (!JS_ValueToBoolean(context, argv[1], &loop)) + { + OOReportJSBadArguments(context, @"Sound", @"playMusic", 1, &argv[1], nil, @"boolean"); + return NO; + } + } + + [[OOMusicController sharedController] playMusicNamed:name loop:loop]; + return YES; +} + + +static JSBool SoundStaticStopMusic(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *name = nil; + + if (argc > 0) + { + name = JSValToNSString(context, argv[0]); + if (name == nil) + { + OOReportJSBadArguments(context, @"Sound", @"stopMusic", argc, argv, nil, @"string or no argument"); + return NO; + } + [[OOMusicController sharedController] stopMusicNamed:name]; + } + else + { + [[OOMusicController sharedController] stop]; + } + return YES; +} + + +@implementation OOSound (OOJavaScriptExtentions) + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + JSObject *jsSelf = NULL; + jsval result = JSVAL_NULL; + + jsSelf = JS_NewObject(context, &sSoundClass.base, sSoundPrototype, NULL); + if (jsSelf != NULL) + { + if (!JS_SetPrivate(context, jsSelf, [self retain])) jsSelf = NULL; + } + if (jsSelf != NULL) result = OBJECT_TO_JSVAL(jsSelf); + + return result; +} + + +- (NSString *) javaScriptDescription +{ + return [NSString stringWithFormat:@"[Sound \"%@\"]", [self name]]; +} + + +- (NSString *) jsClassName +{ + return @"Sound"; +} + +@end diff --git a/src/Core/Scripting/OOJSSoundSource.h b/src/Core/Scripting/OOJSSoundSource.h new file mode 100644 index 00000000..8455b97e --- /dev/null +++ b/src/Core/Scripting/OOJSSoundSource.h @@ -0,0 +1,32 @@ +/* + +OOJSSoundSource.h + +JavaScript sound source object. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import +@class OOSoundSource; + + +void InitOOJSSoundSource(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSSoundSource.m b/src/Core/Scripting/OOJSSoundSource.m new file mode 100644 index 00000000..5372b8c2 --- /dev/null +++ b/src/Core/Scripting/OOJSSoundSource.m @@ -0,0 +1,307 @@ +/* + +OOJSSound.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSSound.h" +#import "OOJavaScriptEngine.h" +#import "OOSound.h" +#import "ResourceManager.h" + + +static JSObject *sSoundSourcePrototype; + + +DEFINE_JS_OBJECT_GETTER(JSSoundSourceGetSoundSource, OOSoundSource) + + +static JSBool SoundSourceGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool SoundSourceSetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool SoundSourceConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +// Methods +static JSBool SoundSourcePlay(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SoundSourceStop(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SoundSourcePlayOrRepeat(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SoundSourcePlaySound(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sSoundSourceClass = +{ + { + "SoundSource", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + SoundSourceGetProperty, // getProperty + SoundSourceSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kSoundSource_sound, + kSoundSource_isPlaying, + kSoundSource_loop, + kSoundSource_repeatCount +}; + + +static JSPropertySpec sSoundSourceProperties[] = +{ + // JS name ID flags + { "sound", kSoundSource_sound, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "isPlaying", kSoundSource_isPlaying, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "loop", kSoundSource_loop, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "repeatCount", kSoundSource_repeatCount, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sSoundSourceMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0, }, + { "play", SoundSourcePlay, 0, }, + { "stop", SoundSourceStop, 0, }, + { "playOrRepeat", SoundSourcePlayOrRepeat, 0, }, + { "playSound", SoundSourcePlaySound, 1, }, + { 0 } +}; + + +// *** Public *** + +void InitOOJSSoundSource(JSContext *context, JSObject *global) +{ + sSoundSourcePrototype = JS_InitClass(context, global, NULL, &sSoundSourceClass.base, SoundSourceConstruct, 0, sSoundSourceProperties, sSoundSourceMethods, NULL, NULL); + JSRegisterObjectConverter(&sSoundSourceClass.base, JSBasicPrivateObjectConverter); +} + + +static JSBool SoundSourceConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOSoundSource *soundSource = nil; + + soundSource = [[[OOSoundSource alloc] init] autorelease]; + if (soundSource == nil) return NO; + *outResult = [soundSource javaScriptValueInContext:context]; + return YES; +} + + +// *** Implementation stuff *** + +static JSBool SoundSourceGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + OOSoundSource *soundSource = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (!JSSoundSourceGetSoundSource(context, this, &soundSource)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kSoundSource_sound: + *outValue = [[soundSource sound] javaScriptValueInContext:context]; + break; + + case kSoundSource_isPlaying: + *outValue = BOOLToJSVal([soundSource isPlaying]); + break; + + case kSoundSource_loop: + *outValue = BOOLToJSVal([soundSource loop]); + break; + + case kSoundSource_repeatCount: + *outValue = INT_TO_JSVAL([soundSource repeatCount]); + break; + + default: + OOReportJSBadPropertySelector(context, @"SoundSource", JSVAL_TO_INT(name)); + return NO; + } + + return YES; +} + + +static JSBool SoundSourceSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + OOSoundSource *soundSource = nil; + int32 iValue; + JSBool bValue; + + if (!JSVAL_IS_INT(name)) return YES; + if (!JSSoundSourceGetSoundSource(context, this, &soundSource)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kSoundSource_sound: + [soundSource setSound:SoundFromJSValue(context, *value)]; + OK = YES; + break; + + case kSoundSource_loop: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [soundSource setLoop:bValue]; + OK = YES; + } + break; + + case kSoundSource_repeatCount: + if (JS_ValueToInt32(context, *value, &iValue) && 0 < iValue) + { + if (iValue > 100) iValue = 100; + if (100 < 1) iValue = 1; + [soundSource setRepeatCount:iValue]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"SoundSource", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Methods *** + +// play([count : Number]) +static JSBool SoundSourcePlay(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOSoundSource *thisv = nil; + int32 count = 0; + + if (EXPECT_NOT(!JSSoundSourceGetSoundSource(context, this, &thisv))) return NO; + if (argc > 0 && !JS_ValueToInt32(context, argv[0], &count)) + { + OOReportJSBadArguments(context, @"SoundSource", @"play", argc, argv, nil, @"integer count or no argument"); + return NO; + } + + if (count > 0) + { + if (count > 100) count = 100; + [thisv setRepeatCount:count]; + } + [thisv play]; + return YES; +} + + +// stop() +static JSBool SoundSourceStop(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOSoundSource *thisv = nil; + + if (EXPECT_NOT(!JSSoundSourceGetSoundSource(context, this, &thisv))) return NO; + + [thisv stop]; + return YES; +} + + +// playOrRepeat() +static JSBool SoundSourcePlayOrRepeat(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOSoundSource *thisv = nil; + + if (EXPECT_NOT(!JSSoundSourceGetSoundSource(context, this, &thisv))) return NO; + + [thisv playOrRepeat]; + return YES; +} + + +// playSound(sound : SoundExpression [, count : Number]) +static JSBool SoundSourcePlaySound(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOSoundSource *thisv; + OOSound *sound = nil; + int32 count = 0; + + if (EXPECT_NOT(!JSSoundSourceGetSoundSource(context, this, &thisv))) return NO; + sound = SoundFromJSValue(context, argv[0]); + if (sound == nil) + { + OOReportJSBadArguments(context, @"SoundSource", @"playSound", argc, argv, nil, @"sound or sound name"); + return NO; + } + + if (argc > 1 && !JS_ValueToInt32(context, argv[1], &count)) + { + OOReportJSBadArguments(context, @"SoundSource", @"playSound", argc, argv, nil, @"sound or sound name and optional integer count"); + return NO; + } + + [thisv setSound:sound]; + if (count > 0) + { + if (count > 100) count = 100; + [thisv setRepeatCount:count]; + } + [thisv play]; + return YES; +} + + +@implementation OOSoundSource (OOJavaScriptExtentions) + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + JSObject *jsSelf = NULL; + jsval result = JSVAL_NULL; + + jsSelf = JS_NewObject(context, &sSoundSourceClass.base, sSoundSourcePrototype, NULL); + if (jsSelf != NULL) + { + if (!JS_SetPrivate(context, jsSelf, [self retain])) jsSelf = NULL; + } + if (jsSelf != NULL) result = OBJECT_TO_JSVAL(jsSelf); + + return result; +} + + +- (NSString *) jsClassName +{ + return @"SoundSource"; +} + +@end diff --git a/src/Core/Scripting/OOJSSpecialFunctions.h b/src/Core/Scripting/OOJSSpecialFunctions.h new file mode 100644 index 00000000..47dd6a33 --- /dev/null +++ b/src/Core/Scripting/OOJSSpecialFunctions.h @@ -0,0 +1,38 @@ +/* + +OOJSSpecialFunctions.h + +Special functions for certain scripts, currently the global prefix script and +the debug console script. Note that it's possible for other scripts to get at +the "special" object through the debug console object +(debugConsole.script.special). If putting actually dangerous functions in here, +it'd be a good idea to learn to use SpiderMonkey's security architecture +(JSPrincipals and such). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJavaScriptEngine.h" + + +void InitOOJSSpecialFunctions(JSContext *context, JSObject *global); +JSObject *JSSpecialFunctionsObject(void); +OOJSValue *JSSpecialFunctionsObjectWrapper(JSContext *context); diff --git a/src/Core/Scripting/OOJSSpecialFunctions.m b/src/Core/Scripting/OOJSSpecialFunctions.m new file mode 100644 index 00000000..4778c62e --- /dev/null +++ b/src/Core/Scripting/OOJSSpecialFunctions.m @@ -0,0 +1,79 @@ +/* + +OOJSSpecialFunctions.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSSpecialFunctions.h" + + +static JSObject *sSpecialFunctionsObject; + + +static JSBool SpecialToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SpecialJsWarning(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSFunctionSpec sSpecialFunctionsMethods[] = +{ + // JS name Function min args + { "toString", SpecialToString, 0 }, + { "jsWarning", SpecialJsWarning, 1 }, + { 0 } +}; + + +void InitOOJSSpecialFunctions(JSContext *context, JSObject *global) +{ + sSpecialFunctionsObject = JS_NewObject(context, NULL, NULL, NULL); + JS_AddNamedRoot(context, &sSpecialFunctionsObject, "OOJSSpecialFunctions"); + JS_DefineFunctions(context, sSpecialFunctionsObject, sSpecialFunctionsMethods); + JS_SealObject(context, sSpecialFunctionsObject, NO); +} + + +JSObject *JSSpecialFunctionsObject(void) +{ + return sSpecialFunctionsObject; +} + + +OOJSValue *JSSpecialFunctionsObjectWrapper(JSContext *context) +{ + return [OOJSValue valueWithJSObject:JSSpecialFunctionsObject() inContext:context]; +} + + +static JSBool SpecialToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + *outResult = STRING_TO_JSVAL(JS_NewStringCopyZ(context, "[object OoliteSpecialFunctions]")); + return YES; +} + + +static JSBool SpecialJsWarning(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOSetJSWarningOrErrorStackSkip(1); + OOReportJSWarning(context, @"%@", [NSString stringWithJavaScriptValue:argv[0] inContext:context]); + OOSetJSWarningOrErrorStackSkip(0); + return YES; +} diff --git a/src/Core/Scripting/OOJSStation.h b/src/Core/Scripting/OOJSStation.h new file mode 100644 index 00000000..c61d5933 --- /dev/null +++ b/src/Core/Scripting/OOJSStation.h @@ -0,0 +1,33 @@ +/* + +OOJSStation.h + +JavaScript proxy for StationEntities. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + +@class StationEntity; + + +void InitOOJSStation(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSStation.m b/src/Core/Scripting/OOJSStation.m new file mode 100644 index 00000000..fdbdeb20 --- /dev/null +++ b/src/Core/Scripting/OOJSStation.m @@ -0,0 +1,248 @@ +/* +OOJSStation.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + */ + +#import "OOJSStation.h" +#import "OOJSEntity.h" +#import "OOJSShip.h" +#import "OOJSPlayer.h" +#import "OOJavaScriptEngine.h" + +#import "StationEntity.h" + + +static JSObject *sStationPrototype; + +static BOOL JSStationGetStationEntity(JSContext *context, JSObject *stationObj, StationEntity **outEntity); + + +static JSBool StationGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool StationSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool StationDockPlayer(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sStationClass = +{ + { + "Station", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + StationGetProperty, // getProperty + StationSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kStation_isMainStation, // Is [UNIVERSE station], boolean, read-only + kStation_hasNPCTraffic, + kStation_alertCondition, +#if DOCKING_CLEARANCE_ENABLED + kStation_requiresDockingClearance, +#endif +}; + + +static JSPropertySpec sStationProperties[] = +{ + // JS name ID flags + { "isMainStation", kStation_isMainStation, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "hasNPCTraffic", kStation_hasNPCTraffic, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "alertCondition", kStation_alertCondition, JSPROP_PERMANENT | JSPROP_ENUMERATE }, +#if DOCKING_CLEARANCE_ENABLED + { "requiresDockingClearance", kStation_requiresDockingClearance, JSPROP_PERMANENT | JSPROP_ENUMERATE }, +#endif + { 0 } +}; + + +static JSFunctionSpec sStationMethods[] = +{ + // JS name Function min args + { "dockPlayer", StationDockPlayer, 0 }, + { 0 } +}; + + +void InitOOJSStation(JSContext *context, JSObject *global) +{ + sStationPrototype = JS_InitClass(context, global, JSShipPrototype(), &sStationClass.base, NULL, 0, sStationProperties, sStationMethods, NULL, NULL); + JSRegisterObjectConverter(&sStationClass.base, JSBasicPrivateObjectConverter); +} + + +static BOOL JSStationGetStationEntity(JSContext *context, JSObject *stationObj, StationEntity **outEntity) +{ + BOOL result; + Entity *entity = nil; + + if (outEntity == NULL) return NO; + *outEntity = nil; + + result = JSEntityGetEntity(context, stationObj, &entity); + if (!result) return NO; + + if (![entity isKindOfClass:[StationEntity class]]) return NO; + + *outEntity = (StationEntity *)entity; + return YES; +} + + +@implementation StationEntity (OOJavaScriptExtensions) + +- (void)getJSClass:(JSClass **)outClass andPrototype:(JSObject **)outPrototype +{ + *outClass = &sStationClass.base; + *outPrototype = sStationPrototype; +} + + +- (NSString *)jsClassName +{ + return @"Station"; +} + +@end + + +static JSBool StationGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + StationEntity *entity = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (!JSStationGetStationEntity(context, this, &entity)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kStation_isMainStation: + *outValue = BOOLToJSVal(entity == [UNIVERSE station]); + break; + + case kStation_hasNPCTraffic: + *outValue = BOOLToJSVal([entity hasNPCTraffic]); + break; + + case kStation_alertCondition: + *outValue = INT_TO_JSVAL([entity alertLevel]); + break; + +#if DOCKING_CLEARANCE_ENABLED + case kStation_requiresDockingClearance: + *outValue = BOOLToJSVal([entity requiresDockingClearance]); + break; +#endif + + default: + OOReportJSBadPropertySelector(context, @"Station", JSVAL_TO_INT(name)); + return NO; + } + return YES; +} + + +static JSBool StationSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + StationEntity *entity = nil; + JSBool bValue; + int32 iValue; + + + if (!JSVAL_IS_INT(name)) return YES; + if (!JSStationGetStationEntity(context, this, &entity)) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kStation_hasNPCTraffic: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [entity setHasNPCTraffic:bValue]; + OK = YES; + } + break; + + case kStation_alertCondition: + if (JS_ValueToInt32(context, *value, &iValue)) + { + [entity setAlertLevel:iValue signallingScript:NO]; // Performs range checking + OK = YES; + } + break; + +#if DOCKING_CLEARANCE_ENABLED + case kStation_requiresDockingClearance: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + [entity setRequiresDockingClearance:bValue]; + OK = YES; + } + break; +#endif + + default: + OOReportJSBadPropertySelector(context, @"Station", JSVAL_TO_INT(name)); + } + + return OK; +} + + +// *** Methods *** + +// dockPlayer() +// Proposed and written by Frame 20090729 +static JSBool StationDockPlayer(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + + if ([player isDocked]) + { + return YES; //fail silently + } + + StationEntity *stationForDockingPlayer = nil; + JSStationGetStationEntity(context, this, &stationForDockingPlayer); + +#if DOCKING_CLEARANCE_ENABLED + [player setDockingClearanceStatus:DOCKING_CLEARANCE_STATUS_GRANTED]; +#endif + + [player safeAllMissiles]; + [UNIVERSE setViewDirection:VIEW_FORWARD]; + [player enterDock:stationForDockingPlayer]; + return YES; +} diff --git a/src/Core/Scripting/OOJSSun.h b/src/Core/Scripting/OOJSSun.h new file mode 100644 index 00000000..4606887d --- /dev/null +++ b/src/Core/Scripting/OOJSSun.h @@ -0,0 +1,36 @@ +/* + +OOJSSun.h + +JavaScript proxy for suns (currently PlanetEntities of type PLANET_TYPE_SUN, +expected to become a separate entity class in future). + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void InitOOJSSun(JSContext *context, JSObject *global); + + +void OOSunGetClassAndPrototype(JSClass **outClass, JSObject **outPrototype); diff --git a/src/Core/Scripting/OOJSSun.m b/src/Core/Scripting/OOJSSun.m new file mode 100644 index 00000000..5f526349 --- /dev/null +++ b/src/Core/Scripting/OOJSSun.m @@ -0,0 +1,169 @@ +/* + +OOJSSun.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSSun.h" +#import "OOJSEntity.h" +#import "OOJavaScriptEngine.h" + +#import "PlanetEntity.h" + + +DEFINE_JS_OBJECT_GETTER(JSSunGetPlanetEntity, PlanetEntity) + + +static JSObject *sSunPrototype; + + +static JSBool SunGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool SunGoNova(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SunCancelNova(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sSunClass = +{ + { + "Sun", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + SunGetProperty, // getProperty + JS_PropertyStub, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + JSObjectWrapperFinalize,// finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kSun_radius, // Radius of sun in metres, number, read-only + kSun_hasGoneNova, // Has sun gone nova, boolean, read-only + kSun_isGoingNova // Will sun go nova, boolean, read-only +}; + + +static JSPropertySpec sSunProperties[] = +{ + // JS name ID flags + { "radius", kSun_radius, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "hasGoneNova", kSun_hasGoneNova, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "isGoingNova", kSun_isGoingNova, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sSunMethods[] = +{ + // JS name Function min args + { "goNova", SunGoNova, 1 }, + { "cancelNova", SunCancelNova, 0 }, + { 0 } +}; + + +void InitOOJSSun(JSContext *context, JSObject *global) +{ + sSunPrototype = JS_InitClass(context, global, JSEntityPrototype(), &sSunClass.base, NULL, 0, sSunProperties, sSunMethods, NULL, NULL); + JSRegisterObjectConverter(&sSunClass.base, JSBasicPrivateObjectConverter); +} + + +void OOSunGetClassAndPrototype(JSClass **outClass, JSObject **outPrototype) +{ + *outClass = &sSunClass.base; + *outPrototype = sSunPrototype; +} + + +static JSBool SunGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + BOOL OK = NO; + PlanetEntity *sun = nil; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSSunGetPlanetEntity(context, this, &sun))) return NO; + + switch (JSVAL_TO_INT(name)) + { + + case kSun_radius: + OK = JS_NewDoubleValue(context, [sun radius], outValue); + break; + + case kSun_hasGoneNova: + *outValue = BOOLToJSVal([sun goneNova]); + OK = YES; + break; + + case kSun_isGoingNova: + *outValue = BOOLToJSVal([sun willGoNova] && ![sun goneNova]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"Sun", JSVAL_TO_INT(name)); + } + return OK; +} + + +// *** Methods *** + +// goNova([delay : Number]) +static JSBool SunGoNova(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlanetEntity *sun = nil; + jsdouble delay = 0; + + if (EXPECT_NOT(!JSSunGetPlanetEntity(context, this, &sun))) return NO; + if (argc > 0 && EXPECT_NOT(!JS_ValueToNumber(context, argv[0], &delay))) return NO; + + [sun setGoingNova:YES inTime:delay]; + return YES; +} + + +// cancelNova() +static JSBool SunCancelNova(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlanetEntity *sun = nil; + + if (EXPECT_NOT(!JSSunGetPlanetEntity(context, this, &sun))) return NO; + + if ([sun willGoNova] && ![sun goneNova]) + { + [sun setGoingNova:NO inTime:0]; + } + return YES; +} diff --git a/src/Core/Scripting/OOJSSystem.h b/src/Core/Scripting/OOJSSystem.h new file mode 100644 index 00000000..8401f35f --- /dev/null +++ b/src/Core/Scripting/OOJSSystem.h @@ -0,0 +1,32 @@ +/* + +OOJSSystem.h + +JavaScript proxy for the current system. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void InitOOJSSystem(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSSystem.m b/src/Core/Scripting/OOJSSystem.m new file mode 100644 index 00000000..88ad0306 --- /dev/null +++ b/src/Core/Scripting/OOJSSystem.m @@ -0,0 +1,1022 @@ +/* + + OOJSSystem.m + + + Oolite + Copyright (C) 2004-2008 Giles C Williams and contributors + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + MA 02110-1301, USA. + + */ + +#import "OOJSSystem.h" +#import "OOJavaScriptEngine.h" + +#import "OOJSVector.h" +#import "OOJSEntity.h" +#import "OOJSPlayer.h" +#import "Universe.h" +#import "PlanetEntity.h" +#import "PlayerEntityScriptMethods.h" +#import "OOJSSystemInfo.h" + +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" +#import "OOEntityFilterPredicate.h" + + +// system.addShips not yet implemented +#define ADD_SHIPS 0 + + +static JSObject *sSystemPrototype; + + +// Support functions for entity search methods. +static BOOL GetRelativeToAndRange(JSContext *context, uintN *ioArgc, jsval **ioArgv, Entity **outRelativeTo, double *outRange) NONNULL_FUNC; +static NSArray *FindJSVisibleEntities(EntityFilterPredicate predicate, void *parameter, Entity *relativeTo, double range); +static NSArray *FindShips(EntityFilterPredicate predicate, void *parameter, Entity *relativeTo, double range); +static NSComparisonResult CompareEntitiesByDistance(id a, id b, void *relativeTo); + + +static JSBool SystemGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool SystemSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); + +static JSBool SystemToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemAddPlanet(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemAddMoon(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemSendAllShipsAway(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemCountShipsWithPrimaryRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemCountShipsWithRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemShipsWithPrimaryRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemShipsWithRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemEntitiesWithScanClass(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemFilteredEntities(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +#if ADD_SHIPS +static JSBool SystemAddShips(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +#endif + +static JSBool SystemLegacyAddShips(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemLegacyAddSystemShips(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemLegacyAddShipsAt(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemLegacyAddShipsAtPrecisely(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemLegacyAddShipsWithinRadius(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemLegacySpawnShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +static JSBool SystemStaticSystemNameForID(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemStaticSystemIDForName(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool SystemStaticInfoForSystem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSClass sSystemClass = +{ + "System", + 0, + + JS_PropertyStub, + JS_PropertyStub, + SystemGetProperty, + SystemSetProperty, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + JS_FinalizeStub +}; + + +enum +{ + // Property IDs + kSystem_ID, // planet number, integer, read-only + kSystem_name, // name, string, read/write + kSystem_description, // description, string, read/write + kSystem_inhabitantsDescription, // description of inhabitant species, string, read/write + kSystem_government, // government ID, integer, read/write + kSystem_governmentDescription, // government ID description, string, read-only + kSystem_economy, // economy ID, integer, read/write + kSystem_economyDescription, // economy ID description, string, read-only + kSystem_techLevel, // tech level ID, integer, read/write + kSystem_population, // population, integer, read/write + kSystem_productivity, // productivity, integer, read/write + kSystem_isInterstellarSpace, // is interstellar space, boolean, read-only + kSystem_mainStation, // system's main station, Station, read-only + kSystem_mainPlanet, // system's main planet, Planet, read-only + kSystem_sun, // system's sun, Planet, read-only + kSystem_planets, // planets in system, array of Planet, read-only + kSystem_allShips, // ships in system, array of Ship, read-only + kSystem_info, // system info dictionary, SystemInfo, read-only + kSystem_pseudoRandomNumber, // constant-per-system pseudorandom number in [0..1), double, read-only + kSystem_pseudoRandom100, // constant-per-system pseudorandom number in [0..100), integer, read-only + kSystem_pseudoRandom256 // constant-per-system pseudorandom number in [0..256), integer, read-only +}; + + +static JSPropertySpec sSystemProperties[] = +{ + // JS name ID flags + { "ID", kSystem_ID, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "name", kSystem_name, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "description", kSystem_description, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "inhabitantsDescription", kSystem_inhabitantsDescription, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "government", kSystem_government, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "governmentDescription", kSystem_governmentDescription, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "economy", kSystem_economy, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "economyDescription", kSystem_economyDescription, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "techLevel", kSystem_techLevel, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "population", kSystem_population, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "productivity", kSystem_productivity, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "isInterstellarSpace", kSystem_isInterstellarSpace, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY}, + { "mainStation", kSystem_mainStation, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "mainPlanet", kSystem_mainPlanet, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "sun", kSystem_sun, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "planets", kSystem_planets, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "allShips", kSystem_allShips, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "info", kSystem_info, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "pseudoRandomNumber", kSystem_pseudoRandomNumber, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "pseudoRandom100", kSystem_pseudoRandom100, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { "pseudoRandom256", kSystem_pseudoRandom256, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sSystemMethods[] = +{ + // JS name Function min args + { "toString", SystemToString, 0 }, + { "addPlanet", SystemAddPlanet, 1 }, + { "addMoon", SystemAddMoon, 1 }, + { "sendAllShipsAway", SystemSendAllShipsAway, 1 }, + { "countShipsWithPrimaryRole", SystemCountShipsWithPrimaryRole, 1 }, + { "countShipsWithRole", SystemCountShipsWithRole, 1 }, + { "shipsWithPrimaryRole", SystemShipsWithPrimaryRole, 1 }, + { "shipsWithRole", SystemShipsWithRole, 1 }, + { "entitiesWithScanClass", SystemEntitiesWithScanClass, 1 }, + { "filteredEntities", SystemFilteredEntities, 2 }, + +#if ADD_SHIPS + { "addShips", SystemAddShips, 3 }, +#endif + + { "legacy_addShips", SystemLegacyAddShips, 2 }, + { "legacy_addSystemShips", SystemLegacyAddSystemShips, 3 }, + { "legacy_addShipsAt", SystemLegacyAddShipsAt, 6 }, + { "legacy_addShipsAtPrecisely", SystemLegacyAddShipsAtPrecisely, 6 }, + { "legacy_addShipsWithinRadius", SystemLegacyAddShipsWithinRadius, 7 }, + { "legacy_spawnShip", SystemLegacySpawnShip, 1 }, + { 0 } +}; + + +static JSFunctionSpec sSystemStaticMethods[] = +{ + { "systemNameForID", SystemStaticSystemNameForID, 1 }, + { "systemIDForName", SystemStaticSystemIDForName, 1 }, + { "infoForSystem", SystemStaticInfoForSystem, 2 }, + { 0 } +}; + + +void InitOOJSSystem(JSContext *context, JSObject *global) +{ + sSystemPrototype = JS_InitClass(context, global, NULL, &sSystemClass, NULL, 0, sSystemProperties, sSystemMethods, NULL, sSystemStaticMethods); + + // Create system object as a property of the global object. + JS_DefineObject(context, global, "system", &sSystemClass, sSystemPrototype, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +static JSBool SystemGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + id result = nil; + PlayerEntity *player = nil; + NSDictionary *systemData = nil; + static Random_Seed sCurrentSystem = {0}; + + if (!JSVAL_IS_INT(name)) return YES; + + player = OOPlayerForScripting(); + + if (!equal_seeds(sCurrentSystem, player->system_seed)) + { + sCurrentSystem = player->system_seed; + } + + systemData = [UNIVERSE generateSystemData:sCurrentSystem]; + + switch (JSVAL_TO_INT(name)) + { + case kSystem_ID: + *outValue = INT_TO_JSVAL([player currentSystemID]); + break; + + case kSystem_name: + result = [systemData objectForKey:KEY_NAME]; + if (result == nil) result = [NSNull null]; + break; + + case kSystem_description: + result = [systemData objectForKey:KEY_DESCRIPTION]; + if (result == nil) result = [NSNull null]; + break; + + case kSystem_inhabitantsDescription: + result = [systemData objectForKey:KEY_INHABITANTS]; + if (result == nil) result = [NSNull null]; + break; + + case kSystem_government: + *outValue = INT_TO_JSVAL([systemData intForKey:KEY_GOVERNMENT]); + break; + + case kSystem_governmentDescription: + result = GovernmentToString([systemData intForKey:KEY_GOVERNMENT]); + if (result == nil && [UNIVERSE inInterstellarSpace]) result = DESC(@"not-applicable"); + if (result == nil) result = [NSNull null]; + break; + + case kSystem_economy: + *outValue = INT_TO_JSVAL([systemData intForKey:KEY_ECONOMY]); + break; + + case kSystem_economyDescription: + result = EconomyToString([systemData intForKey:KEY_ECONOMY]); + if (result == nil && [UNIVERSE inInterstellarSpace]) result = DESC(@"not-applicable"); + if (result == nil) result = [NSNull null]; + break; + + case kSystem_techLevel: + *outValue = INT_TO_JSVAL([systemData intForKey:KEY_TECHLEVEL]); + break; + + case kSystem_population: + *outValue = INT_TO_JSVAL([systemData intForKey:KEY_POPULATION]); + break; + + case kSystem_productivity: + *outValue = INT_TO_JSVAL([systemData intForKey:KEY_PRODUCTIVITY]); + break; + + case kSystem_isInterstellarSpace: + *outValue = BOOLToJSVal([UNIVERSE inInterstellarSpace]); + break; + + case kSystem_mainStation: + result = [UNIVERSE station]; + if (result == nil) result = [NSNull null]; + break; + + case kSystem_mainPlanet: + result = [UNIVERSE planet]; + if (result == nil) result = [NSNull null]; + break; + + case kSystem_sun: + result = [UNIVERSE sun]; + if (result == nil) result = [NSNull null]; + break; + + case kSystem_planets: + result = [UNIVERSE planets]; + if (result == nil) result = [NSArray array]; + break; + + case kSystem_allShips: + result = [UNIVERSE findShipsMatchingPredicate:NULL parameter:NULL inRange:-1 ofEntity:nil]; + break; + + case kSystem_info: + if (!GetJSSystemInfoForCurrentSystem(context, outValue)) return NO; + break; + + case kSystem_pseudoRandomNumber: + JS_NewDoubleValue(context, [player systemPseudoRandomFloat], outValue); + break; + + case kSystem_pseudoRandom100: + *outValue = INT_TO_JSVAL([player systemPseudoRandom100]); + break; + + case kSystem_pseudoRandom256: + *outValue = INT_TO_JSVAL([player systemPseudoRandom256]); + break; + + default: + OOReportJSBadPropertySelector(context, @"System", JSVAL_TO_INT(name)); + return NO; + } + + if (result != nil) *outValue = [result javaScriptValueInContext:context]; + return YES; +} + + +static JSBool SystemSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = NO; + PlayerEntity *player = nil; + OOGalaxyID galaxy; + OOSystemID system; + NSString *stringValue = nil; + int32 iValue; + + if (!JSVAL_IS_INT(name)) return YES; + + player = OOPlayerForScripting(); + + galaxy = [player currentGalaxyID]; + system = [player currentSystemID]; + + if (system == -1) return YES; // Can't change anything in interstellar space. + + switch (JSVAL_TO_INT(name)) + { + case kSystem_name: + stringValue = JSValToNSString(context, *value); + if (stringValue != nil) + { + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_NAME value:stringValue]; + OK = YES; + } + break; + + case kSystem_description: + stringValue = JSValToNSString(context, *value); + if (stringValue != nil) + { + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_DESCRIPTION value:stringValue]; + OK = YES; + } + break; + + case kSystem_inhabitantsDescription: + stringValue = JSValToNSString(context, *value); + if (stringValue != nil) + { + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_INHABITANTS value:stringValue]; + OK = YES; + } + break; + + case kSystem_government: + if (JS_ValueToInt32(context, *value, &iValue)) + { + if (iValue < 0) iValue = 0; + if (7 < iValue) iValue = 7; + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_GOVERNMENT value:[NSNumber numberWithInt:iValue]]; + OK = YES; + } + break; + + case kSystem_economy: + if (JS_ValueToInt32(context, *value, &iValue)) + { + if (iValue < 0) iValue = 0; + if (7 < iValue) iValue = 7; + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_ECONOMY value:[NSNumber numberWithInt:iValue]]; + OK = YES; + } + break; + + case kSystem_techLevel: + if (JS_ValueToInt32(context, *value, &iValue)) + { + if (iValue < 0) iValue = 0; + if (15 < iValue) iValue = 15; + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_TECHLEVEL value:[NSNumber numberWithInt:iValue]]; + OK = YES; + } + break; + + case kSystem_population: + if (JS_ValueToInt32(context, *value, &iValue)) + { + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_POPULATION value:[NSNumber numberWithInt:iValue]]; + OK = YES; + } + break; + + case kSystem_productivity: + if (JS_ValueToInt32(context, *value, &iValue)) + { + [UNIVERSE setSystemDataForGalaxy:galaxy planet:system key:KEY_PRODUCTIVITY value:[NSNumber numberWithInt:iValue]]; + OK = YES; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"System", JSVAL_TO_INT(name)); + } + // Must reset the systemdata cache now, otherwise getproperty will fetch the unchanged values. + // A cleaner implementation than the rev1545 fix (somehow removed in rev1661). + //[UNIVERSE generateSystemData:player->system_seed useCache:NO]; + // more comprehensive implementation now inside setSystemDataForGalaxy: now + // cache resets when system info changes are made via legacy script & jssysteminfo too. + return OK; +} + + +// *** Methods *** + +// toString() : String +static JSBool SystemToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *systemDesc = nil; + + systemDesc = [NSString stringWithFormat:@"[System %u:%u \"%@\"]", [player currentGalaxyID], [player currentSystemID], [[UNIVERSE currentSystemData] objectForKey:KEY_NAME]]; + *outResult = [systemDesc javaScriptValueInContext:context]; + return YES; +} + + +// addPlanet(key : String) +static JSBool SystemAddPlanet(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(key == nil)) + { + OOReportJSBadArguments(context, @"System", @"addPlanet", argc, argv, @"Expected planet key, got", nil); + return NO; + } + + [player addPlanet:key]; + return YES; +} + + +// addMoon(key : String) +static JSBool SystemAddMoon(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *key = nil; + + key = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(key == nil)) + { + OOReportJSBadArguments(context, @"System", @"addMoon", argc, argv, @"Expected planet key, got", nil); + return NO; + } + + [player addMoon:key]; + return YES; +} + + +// sendAllShipsAway() +static JSBool SystemSendAllShipsAway(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + + [player sendAllShipsAway]; + return YES; +} + + +// countShipsWithPrimaryRole(role : String [, relativeTo : Entity [, range : Number]]) : Number +static JSBool SystemCountShipsWithPrimaryRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *role = nil; + Entity *relativeTo = nil; + double range = -1; + + role = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(role == nil)) + { + OOReportJSBadArguments(context, @"System", @"countShipsWithPrimaryRole", argc, argv, nil, @"role"); + return NO; + } + + // Get optional arguments + argc--; + argv++; + if (!GetRelativeToAndRange(context, &argc, &argv, &relativeTo, &range)) return NO; + + *outResult = INT_TO_JSVAL([UNIVERSE countShipsWithPrimaryRole:role inRange:range ofEntity:relativeTo]); + return YES; +} + + +// countShipsWithRole(role : String [, relativeTo : Entity [, range : Number]]) : Number +static JSBool SystemCountShipsWithRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *role = nil; + Entity *relativeTo = nil; + double range = -1; + + role = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(role == nil)) + { + OOReportJSBadArguments(context, @"System", @"countShipsWithRole", argc, argv, nil, @"role"); + return NO; + } + + // Get optional arguments + argc--; + argv++; + if (!GetRelativeToAndRange(context, &argc, &argv, &relativeTo, &range)) return NO; + + *outResult = INT_TO_JSVAL([UNIVERSE countShipsWithRole:role inRange:range ofEntity:relativeTo]); + return YES; +} + + +// shipsWithPrimaryRole(role : String [, relativeTo : Entity [, range : Number]]) : Array (Entity) +static JSBool SystemShipsWithPrimaryRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *role = nil; + Entity *relativeTo = nil; + double range = -1; + NSArray *result = nil; + + role = JSValToNSString(context, *argv); + if (EXPECT_NOT(role == nil)) + { + OOReportJSBadArguments(context, @"System", @"shipsWithPrimaryRole", argc, argv, nil, @"role and optional reference entity and range"); + return NO; + } + + // Get optional arguments + argc--; + argv++; + if (!GetRelativeToAndRange(context, &argc, &argv, &relativeTo, &range)) return NO; + + // Search for entities + result = FindShips(HasPrimaryRolePredicate, role, relativeTo, range); + if (result != nil) + { + *outResult = [result javaScriptValueInContext:context]; + return YES; + } + else + { + return NO; + } +} + + +// shipsWithRole(role : String [, relativeTo : Entity [, range : Number]]) : Array (Entity) +static JSBool SystemShipsWithRole(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *role = nil; + Entity *relativeTo = nil; + double range = -1; + NSArray *result = nil; + + role = JSValToNSString(context, *argv); + if (EXPECT_NOT(role == nil)) + { + OOReportJSBadArguments(context, @"System", @"shipsWithRole", argc, argv, nil, @"role and optional reference entity and range"); + return NO; + } + + // Get optional arguments + argc--; + argv++; + if (EXPECT_NOT(!GetRelativeToAndRange(context, &argc, &argv, &relativeTo, &range))) return NO; + + // Search for entities + result = FindShips(HasRolePredicate, role, relativeTo, range); + if (result != nil) + { + *outResult = [result javaScriptValueInContext:context]; + return YES; + } + else + { + return NO; + } +} + + +// entitiesWithScanClass(scanClass : String [, relativeTo : Entity [, range : Number]]) : Array (Entity) +static JSBool SystemEntitiesWithScanClass(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *scString = nil; + OOScanClass scanClass = CLASS_NOT_SET; + Entity *relativeTo = nil; + double range = -1; + NSArray *result = nil; + + scString = JSValToNSString(context, *argv); + if (scString == nil) + { + OOReportJSBadArguments(context, @"System", @"entitiesWithScanClass", argc, argv, nil, @"scan class and optional reference entity and range"); + return NO; + } + + scanClass = StringToScanClass(scString); + if (EXPECT_NOT(scanClass == CLASS_NOT_SET)) + { + OOReportJSErrorForCaller(context, @"System", @"entitiesWithScanClass", @"Invalid scan class specifier \"%@\"", scString); + return NO; + } + + // Get optional arguments + argc--; + argv++; + if (EXPECT_NOT(!GetRelativeToAndRange(context, &argc, &argv, &relativeTo, &range))) return NO; + + // Search for entities + result = FindJSVisibleEntities(HasScanClassPredicate, [NSNumber numberWithInt:scanClass], relativeTo, range); + if (result != nil) + { + *outResult = [result javaScriptValueInContext:context]; + return YES; + } + else + { + return NO; + } +} + + +// filteredEntities(this : Object, predicate : Function [, relativeTo : Entity [, range : Number]]) : Array (Entity) +static JSBool SystemFilteredEntities(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + JSObject *jsThis = NULL; + JSFunction *function = NULL; + Entity *relativeTo = nil; + double range = -1; + NSArray *result = nil; + + // Get this and predicate arguments + function = JS_ValueToFunction(context, argv[1]); + if (EXPECT_NOT(function == NULL || !JS_ValueToObject(context, argv[0], &jsThis))) + { + OOReportJSBadArguments(context, @"System", @"filteredEntities", argc, argv, nil, @"this, predicate function, and optional reference entity and range"); + return NO; + } + + // Get optional arguments + argc -= 2; + argv += 2; + if (EXPECT_NOT(!GetRelativeToAndRange(context, &argc, &argv, &relativeTo, &range))) return NO; + + // Search for entities + JSFunctionPredicateParameter param = { context, function, jsThis, NO }; + result = FindJSVisibleEntities(JSFunctionPredicate, ¶m, relativeTo, range); + if (EXPECT_NOT(param.errorFlag)) return NO; + + if (result != nil) + { + *outResult = [result javaScriptValueInContext:context]; + return YES; + } + else + { + return NO; + } +} + + +#define DEFAULT_RADIUS 500.0 + +#if ADD_SHIPS +// Forum thread: http://www.aegidian.org/bb/viewtopic.php?t=3371 +static JSBool SystemAddShips(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *role = nil; + int32 count; + Vector where; + double radius = DEFAULT_RADIUS; + uintN consumed; + + role = JSValToNSString(context, argv[0]); + if (scString == nil) + { + OOReportJSError(context, @"System.%@(): role not defined.", @"addShips"); + return NO; + } + if (!JS_ValueToInt32(context, argv[1], &count) || count < 1 || 64 < count) + { + OOReportJSError(context, @"System.%@(): expected positive count, got %@.", @"addShips", [NSString stringWithJavaScriptValue:argv[1] inContext:context]); + return NO; + } + + if (!VectorFromArgumentList(context, @"System", @"addShips", argc - 2, argv + 2, &where, &consumed)) return YES; + argc += 2 + consumed; + argv += 2 + consumed; + + if (argc != 0 && JS_ValueToNumber(context, argv[0], &radius)) + { + + } + + // Note: need a way to specify the use of witchspace-in effects (as in legacy_addShips). + OOReportJSError(context, @"System.addShips(): not implemented."); + + return YES; +} +#endif + +// legacy_addShips(role : String, count : Number) +static JSBool SystemLegacyAddShips(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *role = nil; + int32 count; + + role = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(role == nil || + !JS_ValueToInt32(context, argv[1], &count) || + argc < 2 || + count < 1 || 64 < count)) + { + OOReportJSBadArguments(context, @"System", @"legacy_addShips", argc, argv, nil, @"role and positive count no greater than 64"); + return NO; + } + + while (count--) [UNIVERSE witchspaceShipWithPrimaryRole:role]; + + return YES; +} + + +// legacy_addSystemShips(role : String, count : Number, location : Number) +static JSBool SystemLegacyAddSystemShips(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + jsdouble position; + NSString *role = nil; + int32 count; + + role = JSValToNSString(context, argv[0]); + if (EXPECT_NOT(role == nil || + !JS_ValueToInt32(context, argv[1], &count) || + count < 1 || 64 < count || + argc < 3 || + !JS_ValueToNumber(context, argv[2], &position))) + { + OOReportJSBadArguments(context, @"System", @"legacy_addSystemShips", argc, argv, nil, @"role, positive count no greater than 64, and position along route"); + return NO; + } + + while (count--) [UNIVERSE addShipWithRole:role nearRouteOneAt:position]; + + return YES; +} + + +// legacy_addShipsAt(role : String, count : Number, coordScheme : String, coords : vectorExpression) +static JSBool SystemLegacyAddShipsAt(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + Vector where; + NSString *role = nil; + int32 count; + NSString *coordScheme = nil; + NSString *arg = nil; + + role = JSValToNSString(context, argv[0]); + coordScheme = JSValToNSString(context, argv[2]); + if (EXPECT_NOT(role == nil || + !JS_ValueToInt32(context, argv[1], &count) || + count < 1 || 64 < count || + coordScheme == nil || + argc < 4 || + !VectorFromArgumentListNoError(context, argc - 3, argv + 3, &where, NULL))) + { + OOReportJSBadArguments(context, @"System", @"legacy_addShipsAt", argc, argv, nil, @"role, positive count no greater than 64, coordinate scheme and coordinates"); + return NO; + } + + arg = [NSString stringWithFormat:@"%@ %d %@ %f %f %f", role, count, coordScheme, where.x, where.y, where.z]; + [player addShipsAt:arg]; + + return YES; +} + + +// legacy_addShipsAtPrecisely(role : String, count : Number, coordScheme : Number, coords : vectorExpression) +static JSBool SystemLegacyAddShipsAtPrecisely(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + Vector where; + NSString *role = nil; + int32 count; + NSString *coordScheme = nil; + NSString *arg = nil; + + role = JSValToNSString(context, argv[0]); + coordScheme = JSValToNSString(context, argv[2]); + if (EXPECT_NOT(role == nil || + !JS_ValueToInt32(context, argv[1], &count) || + count < 1 || 64 < count || + coordScheme == nil || + argc < 4 || + !VectorFromArgumentListNoError(context, argc - 3, argv + 3, &where, NULL))) + { + OOReportJSBadArguments(context, @"System", @"legacy_addShipsAtPrecisely", argc, argv, nil, @"role, positive count no greater than 64, coordinate scheme and coordinates"); + return NO; + } + + arg = [NSString stringWithFormat:@"%@ %d %@ %f %f %f", role, count, coordScheme, where.x, where.y, where.z]; + [player addShipsAtPrecisely:arg]; + + return YES; +} + + +// legacy_addShipsWithinRadius(role : String, count : Number, coordScheme : Number, coords : vectorExpression, radius : Number) +static JSBool SystemLegacyAddShipsWithinRadius(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + PlayerEntity *player = OOPlayerForScripting(); + Vector where; + jsdouble radius; + NSString *role = nil; + int32 count; + NSString *coordScheme = nil; + NSString *arg = nil; + uintN consumed = 0; + + role = JSValToNSString(context, argv[0]); + coordScheme = JSValToNSString(context, argv[2]); + if (EXPECT_NOT(role == nil || + !JS_ValueToInt32(context, argv[1], &count) || + count < 1 || 64 < count || + coordScheme == nil || + argc < 5 || + !VectorFromArgumentListNoError(context, argc - 3, argv + 3, &where, &consumed) || + !JS_ValueToNumber(context, argv[3 + consumed], &radius))) + { + OOReportJSBadArguments(context, @"System", @"legacy_addShipWithinRadius", argc, argv, nil, @"role, positive count no greater than 64, coordinate scheme, coordinates and radius"); + return NO; + } + + arg = [NSString stringWithFormat:@"%@ %d %@ %f %f %f %f", role, count, coordScheme, where.x, where.y, where.z, radius]; + [player addShipsWithinRadius:arg]; + + return YES; +} + + +// legacy_spawnShip(key : string) +static JSBool SystemLegacySpawnShip(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *key = nil; + OOPlayerForScripting(); // For backwards-compatibility + + key = JSValToNSString(context, argv[0]); + if (key == nil) + { + OOReportJSBadArguments(context, @"System", @"legacy_addShipWithinRadius", argc, argv, nil, @"ship key"); + return NO; + } + + [UNIVERSE spawnShip:key]; + return YES; +} + + +// *** Static methods *** + +// systemNameForID(ID : Number) : String +static JSBool SystemStaticSystemNameForID(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + int32 systemID; + + if (!JS_ValueToInt32(context, argv[0], &systemID) || systemID < 0 || 255 < systemID) + { + OOReportJSBadArguments(context, @"System", @"systemNameForID", argc, argv, nil, @"system ID"); + return NO; + } + + *outResult = [[UNIVERSE getSystemName:[UNIVERSE systemSeedForSystemNumber:systemID]] javaScriptValueInContext:context]; + return YES; +} + + +// systemIDForName(name : String) : Number +static JSBool SystemStaticSystemIDForName(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + NSString *name = nil; + + name = JSValToNSString(context, argv[0]); + if (name == nil) + { + OOReportJSBadArguments(context, @"System", @"systemIDForName", argc, argv, nil, @"string"); + return NO; + } + + *outResult = INT_TO_JSVAL([UNIVERSE systemIDForSystemSeed:[UNIVERSE systemSeedForSystemName:name]]); + return YES; +} + + +// infoForSystem(galaxyID : Number, systemID : Number) : SystemInfo +static JSBool SystemStaticInfoForSystem(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + int32 galaxyID; + int32 systemID; + + if (argc < 2 || !JS_ValueToInt32(context, argv[0], &galaxyID) || !JS_ValueToInt32(context, argv[1], &systemID)) + { + OOReportJSBadArguments(context, @"System", @"infoForSystem", argc, argv, nil, @"galaxy ID and system ID"); + return NO; + } + + if (galaxyID < 0 || galaxyID > kOOMaximumGalaxyID) + { + OOReportJSBadArguments(context, @"System", @"infoForSystem", 1, argv, @"Invalid galaxy ID", [NSString stringWithFormat:@"number in the range 0 to %u", kOOMaximumGalaxyID]); + return NO; + } + + if (systemID < kOOMinimumSystemID || systemID > kOOMaximumSystemID) + { + OOReportJSBadArguments(context, @"System", @"infoForSystem", 1, argv + 1, @"Invalid system ID", [NSString stringWithFormat:@"number in the range %i to %i", kOOMinimumSystemID, kOOMaximumSystemID]); + return NO; + } + + return GetJSSystemInfoForSystem(context, galaxyID, systemID, outResult); +} + + +// *** Helper functions *** + +static BOOL GetRelativeToAndRange(JSContext *context, uintN *ioArgc, jsval **ioArgv, Entity **outRelativeTo, double *outRange) +{ + // No NULL arguments accepted. + assert(ioArgc && ioArgv && outRelativeTo && outRange); + + // Get optional argument relativeTo : Entity + if (*ioArgc != 0) + { + if (!JSValueToEntity(context, **ioArgv, outRelativeTo)) return NO; + (*ioArgv)++; (*ioArgc)--; + } + + // Get optional argument range : Number + if (*ioArgc != 0) + { + if (!JS_ValueToNumber(context, **ioArgv, outRange)) return NO; + (*ioArgv)++; (*ioArgc)--; + } + + return YES; +} + + +static NSArray *FindJSVisibleEntities(EntityFilterPredicate predicate, void *parameter, Entity *relativeTo, double range) +{ + NSMutableArray *result = nil; + BinaryOperationPredicateParameter param = + { + JSEntityIsJavaScriptSearchablePredicate, NULL, + predicate, parameter + }; + + result = [UNIVERSE findEntitiesMatchingPredicate:ANDPredicate + parameter:¶m + inRange:range + ofEntity:relativeTo]; + + if (result != nil && relativeTo != nil && ![relativeTo isPlayer]) + { + [result sortUsingFunction:CompareEntitiesByDistance context:relativeTo]; + } + if (result == nil) result = [NSArray array]; + return result; +} + + +static NSArray *FindShips(EntityFilterPredicate predicate, void *parameter, Entity *relativeTo, double range) +{ + BinaryOperationPredicateParameter param = + { + IsShipPredicate, NULL, + predicate, parameter + }; + return FindJSVisibleEntities(ANDPredicate, ¶m, relativeTo, range); +} + + +static NSComparisonResult CompareEntitiesByDistance(id a, id b, void *relativeTo) +{ + Entity *ea = a, + *eb = b, + *r = (id)relativeTo; + float d1, d2; + + d1 = distance2(ea->position, r->position); + d2 = distance2(eb->position, r->position); + + if (d1 < d2) return NSOrderedAscending; + else if (d1 > d2) return NSOrderedDescending; + else return NSOrderedSame; +} diff --git a/src/Core/Scripting/OOJSSystemInfo.h b/src/Core/Scripting/OOJSSystemInfo.h new file mode 100644 index 00000000..6cc17cb9 --- /dev/null +++ b/src/Core/Scripting/OOJSSystemInfo.h @@ -0,0 +1,36 @@ +/* + +OOJSSystemInfo.h + +JavaScript object representing system info overrides. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import +#import "OOTypes.h" + + +void InitOOJSSystemInfo(JSContext *context, JSObject *global); + +BOOL GetJSSystemInfoForCurrentSystem(JSContext *context, jsval *outInfo); +BOOL GetJSSystemInfoForSystem(JSContext *context, OOGalaxyID galaxy, OOSystemID system, jsval *outInfo); diff --git a/src/Core/Scripting/OOJSSystemInfo.m b/src/Core/Scripting/OOJSSystemInfo.m new file mode 100644 index 00000000..92bf0f04 --- /dev/null +++ b/src/Core/Scripting/OOJSSystemInfo.m @@ -0,0 +1,294 @@ +/* + +OOJSSystemInfo.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSSystemInfo.h" +#import "OOJavaScriptEngine.h" +#import "PlayerEntityScriptMethods.h" +#import "Universe.h" + + +static JSObject *sSystemInfoPrototype; +static JSObject *sCachedSystemInfo; +static OOGalaxyID sCachedGalaxy; +static OOSystemID sCachedSystem; + + +static JSBool SystemInfoDeleteProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static JSBool SystemInfoGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool SystemInfoSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static void SystemInfoFinalize(JSContext *context, JSObject *this); + + +static JSExtendedClass sSystemInfoClass = +{ + { + "SystemInfo", + JSCLASS_IS_ANONYMOUS | JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, + SystemInfoDeleteProperty, + SystemInfoGetProperty, + SystemInfoSetProperty, + JS_EnumerateStub, + JS_ResolveStub, + JS_ConvertStub, + SystemInfoFinalize, + JSCLASS_NO_OPTIONAL_MEMBERS + }, + JSObjectWrapperEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + + +// Helper class wrapped by JS SystemInfo objects +@interface OOSystemInfo: NSObject +{ + OOGalaxyID _galaxy; + OOSystemID _system; + NSString *_planetKey; +} + +- (id) initWithGalaxy:(OOGalaxyID)galaxy system:(OOSystemID)system; + +- (id) valueForKey:(NSString *)key; +- (void) setValue:(id)value forKey:(NSString *)key; + +- (OOGalaxyID) galaxy; +- (OOSystemID) system; + +@end + + +@implementation OOSystemInfo + +- (id) init +{ + [self release]; + return nil; +} + + +- (id) initWithGalaxy:(OOGalaxyID)galaxy system:(OOSystemID)system +{ + if (galaxy > kOOMaximumGalaxyID || system > kOOMaximumSystemID || system < kOOMinimumSystemID) + { + [self release]; + return nil; + } + + if ((self = [super init])) + { + _galaxy = galaxy; + _system = system; + _planetKey = [[NSString stringWithFormat:@"%u %i", galaxy, system] retain]; + } + return self; +} + + +- (void) dealloc +{ + [_planetKey release]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"galaxy %u, system %i", _galaxy, _system]; +} + + +- (NSString *) shortDescriptionComponents +{ + return _planetKey; +} + + +- (NSString *) jsClassName +{ + return @"SystemInfo"; +} + + +- (BOOL) isEqual:(id)other +{ + return other == self || + ([other isKindOfClass:[OOSystemInfo class]] && + [other galaxy] == _galaxy && + [other system] == _system); + +} + + +- (OOUInteger) hash +{ + OOUInteger hash = _galaxy; + hash <<= 16; + hash |= (uint16_t)_system; + return hash; +} + + +- (id) valueForKey:(NSString *)key +{ + return [UNIVERSE getSystemDataForGalaxy:_galaxy planet:_system key:key]; +} + + +- (void) setValue:(id)value forKey:(NSString *)key +{ + [UNIVERSE setSystemDataForGalaxy:_galaxy planet:_system key:key value:value]; +} + + +- (OOGalaxyID) galaxy +{ + return _galaxy; +} + + +- (OOSystemID) system +{ + return _system; +} + + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + JSObject *jsSelf = NULL; + jsval result = JSVAL_NULL; + + jsSelf = JS_NewObject(context, &sSystemInfoClass.base, sSystemInfoPrototype, NULL); + if (jsSelf != NULL) + { + if (!JS_SetPrivate(context, jsSelf, [self retain])) jsSelf = NULL; + } + if (jsSelf != NULL) result = OBJECT_TO_JSVAL(jsSelf); + + return result; +} + +@end + + + +void InitOOJSSystemInfo(JSContext *context, JSObject *global) +{ + sSystemInfoPrototype = JS_InitClass(context, global, NULL, &sSystemInfoClass.base, NULL, 0, NULL, NULL, NULL, NULL); + JSRegisterObjectConverter(&sSystemInfoClass.base, JSBasicPrivateObjectConverter); +} + + +BOOL GetJSSystemInfoForCurrentSystem(JSContext *context, jsval *outInfo) +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + return GetJSSystemInfoForSystem(context, [player currentGalaxyID], [player currentSystemID], outInfo); +} + + +BOOL GetJSSystemInfoForSystem(JSContext *context, OOGalaxyID galaxy, OOSystemID system, jsval *outInfo) +{ + // Use cached object if possible. + if (sCachedSystemInfo != NULL && + sCachedGalaxy == galaxy && + sCachedSystem == system) + { + *outInfo = OBJECT_TO_JSVAL(sCachedSystemInfo); + return YES; + } + + // If not, create a new one. + OOSystemInfo *info = [[[OOSystemInfo alloc] initWithGalaxy:galaxy system:system] autorelease]; + if (info == nil) + { + OOReportJSError(context, @"Could not create system info object for galaxy %u, system %i.", galaxy, system); + return NO; + } + + *outInfo = [info javaScriptValueInContext:context]; + if (JSVAL_IS_OBJECT(*outInfo) && !JSVAL_IS_NULL(*outInfo)) + { + // Cache is not a root; we clear it in finalize if necessary. + sCachedSystemInfo = JSVAL_TO_OBJECT(*outInfo); + sCachedGalaxy = galaxy; + sCachedSystem = system; + return YES; + } + else return NO; +} + + +static void SystemInfoFinalize(JSContext *context, JSObject *this) +{ + [(id)JS_GetPrivate(context, this) release]; + JS_SetPrivate(context, this, nil); + + // Clear now-stale cache entry if appropriate. + if (sCachedSystemInfo == this) sCachedSystemInfo = NULL; +} + + +static JSBool SystemInfoDeleteProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + jsval v = JSVAL_VOID; + return SystemInfoSetProperty(context, this, name, &v); +} + + +static JSBool SystemInfoGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + if (JSVAL_IS_STRING(name)) + { + NSString *key = [NSString stringWithJavaScriptValue:name inContext:context]; + OOSystemInfo *info = JSObjectToObjectOfClass(context, this, [OOSystemInfo class]); + id value = nil; + + value = [info valueForKey:key]; + if ([@"_OTHER_GALAXY_" isEqualToString:(NSString *)value]) + { + OOReportJSWarning(context, @"Cannot read systemInfo values for other galaxies."); + value = nil; + } + *outValue = [value javaScriptValueInContext:context]; + } + return YES; +} + + +static JSBool SystemInfoSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + if (JSVAL_IS_STRING(name)) + { + NSString *key = [NSString stringWithJavaScriptValue:name inContext:context]; + OOSystemInfo *info = JSObjectToObjectOfClass(context, this, [OOSystemInfo class]); + + [info setValue:JSValToNSString(context, *value) forKey:key]; + } + return YES; +} diff --git a/src/Core/Scripting/OOJSTimer.h b/src/Core/Scripting/OOJSTimer.h new file mode 100644 index 00000000..ff730317 --- /dev/null +++ b/src/Core/Scripting/OOJSTimer.h @@ -0,0 +1,50 @@ +/* + +OOJSTimer.h + +JavaScript timer class. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOScriptTimer.h" +#import "OOJSScript.h" +#import + + +@interface OOJSTimer: OOScriptTimer +{ + BOOL _persistent; + JSFunction *_function; + JSObject *_functionObject; + JSObject *_jsThis; // The object that is 'this' in the function call. + + OOJSScript *_owningScript; + + JSObject *_jsSelf; // The JS Timer object proxy for this OOJSTimer. +} + +- (void) setPersistent:(BOOL)value; + +@end + + +void InitOOJSTimer(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSTimer.m b/src/Core/Scripting/OOJSTimer.m new file mode 100644 index 00000000..33232bfa --- /dev/null +++ b/src/Core/Scripting/OOJSTimer.m @@ -0,0 +1,420 @@ +/* + +OOJSTimer.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSTimer.h" +#import "OOJavaScriptEngine.h" +#import "Universe.h" + + +// Minimum allowable interval for repeating timers. +#define kMinInterval 0.25 + + +static JSObject *sTimerPrototype; +static JSClass sTimerClass; + + +@interface OOJSTimer (Private) + +- (id) initWithDelay:(OOTimeAbsolute)delay + interval:(OOTimeDelta)interval + function:(JSFunction *)function + this:(JSObject *)jsThis; + +@end + + +@implementation OOJSTimer + +- (id) initWithDelay:(OOTimeAbsolute)delay + interval:(OOTimeDelta)interval + function:(JSFunction *)function + this:(JSObject *)jsThis +{ + JSContext *context = NULL; + + if (function == NULL) + { + [self release]; + return nil; + } + + self = [super initWithNextTime:[UNIVERSE getTime] + delay interval:interval]; + if (self != nil) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + + _jsThis = jsThis; + JS_AddNamedRoot(context, &_jsThis, "OOJSTimer this"); + + _function = function; + _functionObject = JS_GetFunctionObject(_function); + + JS_AddNamedRoot(context, &_functionObject, "OOJSTimer function"); + + _jsSelf = JS_NewObject(context, &sTimerClass, sTimerPrototype, NULL); + if (_jsSelf != NULL) + { + if (!JS_SetPrivate(context, _jsSelf, [self retain])) _jsSelf = NULL; + } + if (_jsSelf == NULL) + { + [self release]; + self = nil; + } + + _owningScript = [[OOJSScript currentlyRunningScript] weakRetain]; + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + } + + return self; +} + + +- (void) dealloc +{ + [_owningScript release]; + + // Allow garbage collection. + [[OOJavaScriptEngine sharedEngine] removeGCRoot:&_jsThis]; + [[OOJavaScriptEngine sharedEngine] removeGCRoot:&_functionObject]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + JSString *funcJSName = NULL; + NSString *funcName = nil; + NSString *thisDesc = nil; + + funcJSName = JS_GetFunctionId(_function); + if (funcJSName == NULL) funcName = @"anonymous"; + else + { + funcName = [NSString stringWithFormat:@"\"%@\"", [NSString stringWithJavaScriptString:funcJSName]]; + } + + thisDesc = [NSString stringWithJavaScriptValue:OBJECT_TO_JSVAL(_jsThis) inContext:NULL]; + + return [NSString stringWithFormat:@"%@, %spersistent, function: %@", [super descriptionComponents], [self isPersistent] ? "" : "not ", funcName]; +} + + +- (NSString *)jsClassName +{ + return @"Timer"; +} + + +- (void) timerFired +{ + jsval rval = JSVAL_VOID; + + [OOJSScript pushScript:_owningScript]; + [[OOJavaScriptEngine sharedEngine] callJSFunction:_function + forObject:_jsThis + argc:0 + argv:NULL + result:&rval]; + [OOJSScript popScript:_owningScript]; +} + + +- (BOOL) isPersistent +{ + return _persistent; +} + + +- (void) setPersistent:(BOOL)value +{ + _persistent = (value != NO); +} + + +- (jsval) javaScriptValueInContext:(JSContext *)context +{ + return OBJECT_TO_JSVAL(_jsSelf); +} + +@end + + +static JSBool TimerGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool TimerSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static void TimerFinalize(JSContext *context, JSObject *this); +static JSBool TimerConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +// Methods +static JSBool TimerStart(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool TimerStop(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSClass sTimerClass = +{ + "Timer", + JSCLASS_HAS_PRIVATE, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + TimerGetProperty, // getProperty + TimerSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + TimerFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS +}; + + +enum +{ + // Property IDs + kTimer_nextTime, // next fire time, double, read/write + kTimer_interval, // interval, double, read/write + kTimer_isPersistent, // is persistent, boolean, read/write + kTimer_isRunning // is scheduled, boolean, read-only +}; + + +static JSPropertySpec sTimerProperties[] = +{ + // JS name ID flags + { "nextTime", kTimer_nextTime, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "interval", kTimer_interval, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "isPersistent", kTimer_isPersistent, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "isRunning", kTimer_isRunning, JSPROP_PERMANENT | JSPROP_ENUMERATE | JSPROP_READONLY }, + { 0 } +}; + + +static JSFunctionSpec sTimerMethods[] = +{ + // JS name Function min args + { "toString", JSObjectWrapperToString, 0 }, + { "start", TimerStart, 0 }, + { "stop", TimerStop, 0 }, + { 0 } +}; + + +void InitOOJSTimer(JSContext *context, JSObject *global) +{ + sTimerPrototype = JS_InitClass(context, global, NULL, &sTimerClass, TimerConstruct, 0, sTimerProperties, sTimerMethods, NULL, NULL); + JSRegisterObjectConverter(&sTimerClass, JSBasicPrivateObjectConverter); +} + + +static BOOL JSTimerGetTimer(JSContext *context, JSObject *entityObj, OOJSTimer **outTimer) +{ + id value = nil; + + value = JSObjectToObject(context, entityObj); + if ([value isKindOfClass:[OOJSTimer class]] && outTimer != NULL) + { + *outTimer = value; + return YES; + } + return NO; +} + + +static JSBool TimerGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + OOJSTimer *timer = nil; + BOOL OK = NO; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSTimerGetTimer(context, this, &timer))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kTimer_nextTime: + OK = JS_NewDoubleValue(context, [timer nextTime], outValue); + break; + + case kTimer_interval: + OK = JS_NewDoubleValue(context, [timer interval], outValue); + break; + + case kTimer_isPersistent: + *outValue = BOOLToJSVal([timer isPersistent]); + OK = YES; + break; + + case kTimer_isRunning: + *outValue = BOOLToJSVal([timer isScheduled]); + OK = YES; + break; + + default: + OOReportJSBadPropertySelector(context, @"Timer", JSVAL_TO_INT(name)); + } + + return OK; +} + + +static JSBool TimerSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + BOOL OK = YES; + OOJSTimer *timer = nil; + double fValue; + JSBool bValue; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSTimerGetTimer(context, this, &timer))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kTimer_nextTime: + if (JS_ValueToNumber(context, *value, &fValue)) + { + OK = YES; + if (![timer setNextTime:fValue]) + { + OOReportJSWarning(context, @"Ignoring attempt to change next fire time for running timer %@.", timer); + } + } + break; + + case kTimer_interval: + if (JS_ValueToNumber(context, *value, &fValue)) + { + OK = YES; + [timer setInterval:fValue]; + } + break; + + case kTimer_isPersistent: + if (JS_ValueToBoolean(context, *value, &bValue)) + { + OK = YES; + [timer setPersistent:bValue]; + } + break; + + default: + OOReportJSBadPropertySelector(context, @"Timer", JSVAL_TO_INT(name)); + } + + return OK; +} + + +static void TimerFinalize(JSContext *context, JSObject *this) +{ + OOJSTimer *timer = nil; + + if (JSTimerGetTimer(context, this, &timer)) + { + if ([timer isScheduled]) + { + OOReportJSWarning(context, @"Timer %@ is being garbage-collected while still running. You must keep a reference to all running timers, or they will stop unpredictably!", timer); + } + [timer release]; + JS_SetPrivate(context, this, NULL); + } +} + + +// new Timer(this : Object, function : Function, delay : Number [, interval : Number]) : Timer +static JSBool TimerConstruct(JSContext *context, JSObject *inThis, uintN argc, jsval *argv, jsval *outResult) +{ + JSObject *this = NULL; + JSFunction *function = NULL; + double delay; + double interval = -1.0; + OOJSTimer *timer = nil; + + if (!JSVAL_IS_NULL(argv[0]) && !JSVAL_IS_VOID(argv[0])) + { + if (!JS_ValueToObject(context, argv[0], &this)) + { + OOReportJSError(context, @"Could not construct Timer because %@ argument ('%@') is not %@.", @"first", @"this", @"an object"); + return NO; + } + } + + function = JS_ValueToFunction(context, argv[1]); + if (function == NULL) + { + OOReportJSError(context, @"Could not construct Timer because %@ argument ('%@') is not %@.", @"second", @"function", @"a function"); + return NO; + } + + if (!JS_ValueToNumber(context, argv[2], &delay)) + { + OOReportJSError(context, @"Could not construct Timer because %@ argument ('%@') is not %@.", @"third", @"delay", @"a number"); + return NO; + } + + // Fourth argument is optional. + if (3 < argc && !JS_ValueToNumber(context, argv[3], &interval)) interval = -1; + + // Ensure interval is not too small. + if (0.0 < interval && interval < kMinInterval) interval = kMinInterval; + + timer = [[OOJSTimer alloc] initWithDelay:delay + interval:interval + function:function + this:this]; + *outResult = [timer javaScriptValueInContext:context]; + if (delay >= 0) // Leave in stopped state if delay is negative + { + [timer scheduleTimer]; + } + [timer release]; // The JS object retains the ObjC object. + + return YES; +} + + +// *** Methods *** + +// start() : Boolean +static JSBool TimerStart(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOJSTimer *thisTimer = nil; + + if (EXPECT_NOT(!JSTimerGetTimer(context, this, &thisTimer))) return NO; + + *outResult = BOOLToJSVal([thisTimer scheduleTimer]); + return YES; +} + + +// stop() +static JSBool TimerStop(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + OOJSTimer *thisTimer = nil; + + if (EXPECT_NOT(!JSTimerGetTimer(context, this, &thisTimer))) return NO; + + [thisTimer unscheduleTimer]; + return YES; +} diff --git a/src/Core/Scripting/OOJSVector.h b/src/Core/Scripting/OOJSVector.h new file mode 100644 index 00000000..aa1061ac --- /dev/null +++ b/src/Core/Scripting/OOJSVector.h @@ -0,0 +1,68 @@ +/* + +OOJSVector.h + +JavaScript proxy for vectors. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import +#import "OOMaths.h" + + +void InitOOJSVector(JSContext *context, JSObject *global); + + +JSObject *JSVectorWithVector(JSContext *context, Vector vector); + +BOOL VectorToJSValue(JSContext *context, Vector vector, jsval *outValue); +BOOL NSPointToVectorJSValue(JSContext *context, NSPoint point, jsval *outValue); +BOOL JSValueToVector(JSContext *context, jsval value, Vector *outVector); + +/* Given a JS Vector object, get the corresponding Vector struct. Given a JS + Entity, get its position. Given a JS Array with exactly three elements, + all of them numbers, treat them as [x, y, z] components. For anything + else, return NO. (Other implicit conversions may be added in future.) +*/ +BOOL JSObjectGetVector(JSContext *context, JSObject *vectorObj, Vector *outVector); + +// Set the value of a JS vector object. +BOOL JSVectorSetVector(JSContext *context, JSObject *vectorObj, Vector vector); + + +/* VectorFromArgumentList() + + Construct a vector from an argument list which is either a (JS) vector, a + (JS) entity, three numbers or an array of three numbers. The optional + outConsumed argument can be used to find out how many parameters were used + (currently, this will be 0 on failure, otherwise 1 or 3). + + On failure, it will return NO and raise an error. If the caller is a JS + callback, it must return NO to signal an error. +*/ +BOOL VectorFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, Vector *outVector, uintN *outConsumed); + +/* VectorFromArgumentListNoError() + + Like VectorFromArgumentList(), but does not report an error on failure. +*/ +BOOL VectorFromArgumentListNoError(JSContext *context, uintN argc, jsval *argv, Vector *outVector, uintN *outConsumed); diff --git a/src/Core/Scripting/OOJSVector.m b/src/Core/Scripting/OOJSVector.m new file mode 100644 index 00000000..650f23ad --- /dev/null +++ b/src/Core/Scripting/OOJSVector.m @@ -0,0 +1,807 @@ +/* + +OOJSVector.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSVector.h" +#import "OOJavaScriptEngine.h" + +#if OOLITE_GNUSTEP +#import +#else +#import +#endif + +#import "OOConstToString.h" +#import "OOJSEntity.h" +#import "OOJSQuaternion.h" + + +static JSObject *sVectorPrototype; + + +static JSBool VectorGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool VectorSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value); +static void VectorFinalize(JSContext *context, JSObject *this); +static JSBool VectorConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorEquality(JSContext *context, JSObject *this, jsval value, JSBool *outEqual); + +// Methods +static JSBool VectorToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorToSource(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorAdd(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorSubtract(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorDistanceTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorSquaredDistanceTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorMultiply(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorDot(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorAngleTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorCross(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorTripleProduct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorDirection(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorMagnitude(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorSquaredMagnitude(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorRotationTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorRotateBy(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorToArray(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + +// Static methods +static JSBool VectorStaticInterpolate(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorStaticRandom(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorStaticRandomDirection(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +static JSBool VectorStaticRandomDirectionAndLength(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); +//static JSBool VectorStaticConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +static JSExtendedClass sVectorClass = +{ + { + "Vector3D", + JSCLASS_HAS_PRIVATE | JSCLASS_IS_EXTENDED, + + JS_PropertyStub, // addProperty + JS_PropertyStub, // delProperty + VectorGetProperty, // getProperty + VectorSetProperty, // setProperty + JS_EnumerateStub, // enumerate + JS_ResolveStub, // resolve + JS_ConvertStub, // convert + VectorFinalize, // finalize + JSCLASS_NO_OPTIONAL_MEMBERS + }, + VectorEquality, // equality + NULL, // outerObject + NULL, // innerObject + JSCLASS_NO_RESERVED_MEMBERS +}; + + +enum +{ + // Property IDs + kVector_x, + kVector_y, + kVector_z +}; + + +static JSPropertySpec sVectorProperties[] = +{ + // JS name ID flags + { "x", kVector_x, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "y", kVector_y, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { "z", kVector_z, JSPROP_PERMANENT | JSPROP_ENUMERATE }, + { 0 } +}; + + +static JSFunctionSpec sVectorMethods[] = +{ + // JS name Function min args + { "toString", VectorToString, 0, }, + { "toSource", VectorToSource, 0, }, + { "add", VectorAdd, 1, }, + { "subtract", VectorSubtract, 1, }, + { "distanceTo", VectorDistanceTo, 1, }, + { "squaredDistanceTo", VectorSquaredDistanceTo, 1, }, + { "multiply", VectorMultiply, 1, }, + { "dot", VectorDot, 1, }, + { "angleTo", VectorAngleTo, 1, }, + { "cross", VectorCross, 1, }, + { "tripleProduct", VectorTripleProduct, 2, }, + { "direction", VectorDirection, 0, }, + { "magnitude", VectorMagnitude, 0, }, + { "squaredMagnitude", VectorSquaredMagnitude, 0, }, + { "rotateBy", VectorRotateBy, 1, }, + { "rotationTo", VectorRotationTo, 1, }, + { "toArray", VectorToArray, 0, }, + { 0 } +}; + + +static JSFunctionSpec sVectorStaticMethods[] = +{ + // JS name Function min args + { "interpolate", VectorStaticInterpolate, 3, }, + { "random", VectorStaticRandom, 0, }, + { "randomDirection", VectorStaticRandomDirection, 0, }, + { "randomDirectionAndLength", VectorStaticRandomDirectionAndLength, 0, }, + { 0 } +}; + + +// *** Public *** + +void InitOOJSVector(JSContext *context, JSObject *global) +{ + sVectorPrototype = JS_InitClass(context, global, NULL, &sVectorClass.base, VectorConstruct, 0, sVectorProperties, sVectorMethods, NULL, sVectorStaticMethods); +} + + +JSObject *JSVectorWithVector(JSContext *context, Vector vector) +{ + JSObject *result = NULL; + Vector *private = NULL; + + private = malloc(sizeof *private); + if (EXPECT_NOT(private == NULL)) return NULL; + + *private = vector; + + result = JS_NewObject(context, &sVectorClass.base, sVectorPrototype, NULL); + if (result != NULL) + { + if (EXPECT_NOT(!JS_SetPrivate(context, result, private))) result = NULL; + } + + if (EXPECT_NOT(result == NULL)) free(private); + + return result; +} + + +BOOL VectorToJSValue(JSContext *context, Vector vector, jsval *outValue) +{ + JSObject *object = NULL; + + if (EXPECT_NOT(outValue == NULL)) return NO; + + object = JSVectorWithVector(context, vector); + if (EXPECT_NOT(object == NULL)) return NO; + + *outValue = OBJECT_TO_JSVAL(object); + return YES; +} + + +BOOL NSPointToVectorJSValue(JSContext *context, NSPoint point, jsval *outValue) +{ + return VectorToJSValue(context, make_vector(point.x, point.y, 0), outValue); +} + + +BOOL JSValueToVector(JSContext *context, jsval value, Vector *outVector) +{ + if (EXPECT_NOT(!JSVAL_IS_OBJECT(value))) return NO; + + return JSObjectGetVector(context, JSVAL_TO_OBJECT(value), outVector); +} + + +BOOL JSObjectGetVector(JSContext *context, JSObject *vectorObj, Vector *outVector) +{ + Vector *private = NULL; + Entity *entity = nil; + jsuint arrayLength; + jsval arrayX, arrayY, arrayZ; + jsdouble x, y, z; + + if (EXPECT_NOT(outVector == NULL || vectorObj == NULL)) return NO; + + private = JS_GetInstancePrivate(context, vectorObj, &sVectorClass.base, NULL); + if (private != NULL) // If this is a (JS) Vector... + { + *outVector = *private; + return YES; + } + + // If it's an entity, use its position. + if (JSEntityGetEntity(context, vectorObj, &entity)) + { + *outVector = [entity position]; + return YES; + } + + // If it's an array... + if (JS_IsArrayObject(context, vectorObj)) + { + // ...and it has exactly three elements... + if (JS_GetArrayLength(context, vectorObj, &arrayLength) && arrayLength == 3) + { + if (JS_LookupElement(context, vectorObj, 0, &arrayX) && + JS_LookupElement(context, vectorObj, 1, &arrayY) && + JS_LookupElement(context, vectorObj, 2, &arrayZ)) + { + // ...use the three numbers as [x, y, z] + if (JS_ValueToNumber(context, arrayX, &x) && + JS_ValueToNumber(context, arrayY, &y) && + JS_ValueToNumber(context, arrayZ, &z)) + { + *outVector = make_vector(x, y, z); + return YES; + } + } + } + } + + return NO; +} + + +BOOL JSVectorSetVector(JSContext *context, JSObject *vectorObj, Vector vector) +{ + Vector *private = NULL; + + if (EXPECT_NOT(vectorObj == NULL)) return NO; + + private = JS_GetInstancePrivate(context, vectorObj, &sVectorClass.base, NULL); + if (private != NULL) // If this is a (JS) Vector... + { + *private = vector; + return YES; + } + + return NO; +} + + +BOOL VectorFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, Vector *outVector, uintN *outConsumed) +{ + if (VectorFromArgumentListNoError(context, argc, argv, outVector, outConsumed)) return YES; + else + { + OOReportJSBadArguments(context, scriptClass, function, argc, argv, + @"Could not construct vector from parameters", + @"Vector, Entity or array of three numbers"); + return NO; + } +} + + +static BOOL VectorFromArgumentListNoErrorInternal(JSContext *context, uintN argc, jsval *argv, Vector *outVector, uintN *outConsumed, BOOL warnAboutNumberList) +{ + double x, y, z; + + if (outConsumed != NULL) *outConsumed = 0; + // Sanity checks. + if (EXPECT_NOT(argc == 0 || argv == NULL || outVector == NULL)) + { + OOLogGenericParameterError(); + return NO; + } + + // Is first object a vector, array or entity? + if (JSVAL_IS_OBJECT(argv[0])) + { + if (JSObjectGetVector(context, JSVAL_TO_OBJECT(argv[0]), outVector)) + { + if (outConsumed != NULL) *outConsumed = 1; + return YES; + } + } + + // Otherwise, look for three numbers. + if (argc < 3) return NO; + + // Given a string, JS_ValueToNumber() returns YES but provides a NaN number. + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[0], &x) || isnan(x))) return NO; + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[1], &y) || isnan(y))) return NO; + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[2], &z) || isnan(z))) return NO; + + // We got our three numbers. + *outVector = make_vector(x, y, z); + if (outConsumed != NULL) *outConsumed = 3; + + if (warnAboutNumberList) + { + OOReportJSWarning(context, @"The ability to pass three numbers instead of a vector is deprecated and will be removed in a future version of Oolite. Use an array literal instead (for instance, replace v.add(1, 2, z) with v.add([1, 2, z])."); + } + + return YES; +} +BOOL VectorFromArgumentListNoError(JSContext *context, uintN argc, jsval *argv, Vector *outVector, uintN *outConsumed) +{ + return VectorFromArgumentListNoErrorInternal(context, argc, argv, outVector, outConsumed, YES); +} + + +// *** Implementation stuff *** + +static JSBool VectorGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + Vector vector; + GLfloat value; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSObjectGetVector(context, this, &vector))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kVector_x: + value = vector.x; + break; + + case kVector_y: + value = vector.y; + break; + + case kVector_z: + value = vector.z; + break; + + default: + OOReportJSBadPropertySelector(context, @"Vector3D", JSVAL_TO_INT(name)); + return NO; + } + + return JS_NewDoubleValue(context, value, outValue); +} + + +static JSBool VectorSetProperty(JSContext *context, JSObject *this, jsval name, jsval *value) +{ + Vector vector; + jsdouble dval; + + if (!JSVAL_IS_INT(name)) return YES; + if (EXPECT_NOT(!JSObjectGetVector(context, this, &vector))) return NO; + if (EXPECT_NOT(!JS_ValueToNumber(context, *value, &dval))) return NO; + + switch (JSVAL_TO_INT(name)) + { + case kVector_x: + vector.x = dval; + break; + + case kVector_y: + vector.y = dval; + break; + + case kVector_z: + vector.z = dval; + break; + + default: + OOReportJSBadPropertySelector(context, @"Vector3D", JSVAL_TO_INT(name)); + return NO; + } + + return JSVectorSetVector(context, this, vector); +} + + +static void VectorFinalize(JSContext *context, JSObject *this) +{ + Vector *private = NULL; + + private = JS_GetInstancePrivate(context, this, &sVectorClass.base, NULL); + if (private != NULL) + { + free(private); + } +} + + +static JSBool VectorConstruct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector vector = kZeroVector; + Vector *private = NULL; + + private = malloc(sizeof *private); + if (EXPECT_NOT(private == NULL)) return NO; + + // If called without new, replace this with a new Vector object. + if (!JS_IsConstructing(context)) + { + this = JS_NewObject(context, &sVectorClass.base, NULL, NULL); + if (this == NULL) return NO; + *outResult = OBJECT_TO_JSVAL(this); + } + + if (argc != 0) + { + if (EXPECT_NOT(!VectorFromArgumentListNoErrorInternal(context, argc, argv, &vector, NULL, NO))) + { + free(private); + OOReportJSBadArguments(context, NULL, NULL, argc, argv, + @"Could not construct vector from parameters", + @"Vector, Entity or array of three numbers"); + return NO; + } + } + + *private = vector; + + if (EXPECT_NOT(!JS_SetPrivate(context, this, private))) + { + free(private); + return NO; + } + + return YES; +} + + +static JSBool VectorEquality(JSContext *context, JSObject *this, jsval value, JSBool *outEqual) +{ + Vector thisv, thatv; + + *outEqual = NO; + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!JSVAL_IS_OBJECT(value))) return YES; + if (EXPECT_NOT(!JSObjectGetVector(context, JSVAL_TO_OBJECT(value), &thatv))) return YES; + + *outEqual = vector_equal(thisv, thatv); + return YES; +} + + +// *** Methods *** + +// toString() : String +static JSBool VectorToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + + *outResult = [VectorDescription(thisv) javaScriptValueInContext:context]; + return YES; +} + + +// toSource() : String +static JSBool VectorToSource(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + + *outResult = [[NSString stringWithFormat:@"Vector(%g, %g, %g)", thisv.x, thisv.y, thisv.z] + javaScriptValueInContext:context]; + return YES; +} + + +// add(v : vectorExpression) : Vector3D +static JSBool VectorAdd(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv, result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"add", argc, argv, &thatv, NULL))) return NO; + + result = vector_add(thisv, thatv); + + return VectorToJSValue(context, result, outResult); +} + + +// subtract(v : vectorExpression) : Vector3D +static JSBool VectorSubtract(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv, result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"subtract", argc, argv, &thatv, NULL))) return NO; + + result = vector_subtract(thisv, thatv); + + return VectorToJSValue(context, result, outResult); +} + + +// distanceTo(v : vectorExpression) : Vector3D +static JSBool VectorDistanceTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv; + GLfloat result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"distanceTo", argc, argv, &thatv, NULL))) return NO; + + result = distance(thisv, thatv); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// squaredDistanceTo(v : vectorExpression) : Vector3D +static JSBool VectorSquaredDistanceTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv; + GLfloat result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"squaredDistanceTo", argc, argv, &thatv, NULL))) return NO; + + result = distance2(thisv, thatv); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// multiply(n : Number) : Vector3D +static JSBool VectorMultiply(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, result; + double scalar; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Vector3D", @"multiply", argc, argv, &scalar, NULL))) return NO; + + result = vector_multiply_scalar(thisv, scalar); + + return VectorToJSValue(context, result, outResult); +} + + +// dot(v : vectorExpression) : Number +static JSBool VectorDot(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv; + GLfloat result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"dot", argc, argv, &thatv, NULL))) return NO; + + result = dot_product(thisv, thatv); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// angleTo(v : vectorExpression) : Number +static JSBool VectorAngleTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv; + GLfloat result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"angleTo", argc, argv, &thatv, NULL))) return NO; + + result = acosf(dot_product(vector_normal(thisv), vector_normal(thatv))); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// cross(v : vectorExpression) : Vector3D +static JSBool VectorCross(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv, result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"cross", argc, argv, &thatv, NULL))) return NO; + + result = true_cross_product(thisv, thatv); + + return VectorToJSValue(context, result, outResult); +} + + +// tripleProduct(v : vectorExpression, u : vectorExpression) : Number +static JSBool VectorTripleProduct(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv, theotherv; + GLfloat result; + uintN consumed; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"tripleProduct", argc, argv, &thatv, &consumed))) return NO; + argc += consumed; + argv += consumed; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"tripleProduct", argc, argv, &theotherv, NULL))) return NO; + + result = triple_product(thisv, thatv, theotherv); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// direction() : Vector3D +static JSBool VectorDirection(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + + result = vector_normal(thisv); + + return VectorToJSValue(context, result, outResult); +} + + +// magnitude() : Number +static JSBool VectorMagnitude(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv; + GLfloat result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + + result = magnitude(thisv); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// squaredMagnitude() : Number +static JSBool VectorSquaredMagnitude(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv; + GLfloat result; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + + result = magnitude2(thisv); + + return JS_NewDoubleValue(context, result, outResult); +} + + +// rotationTo(v : vectorExpression [, limit : Number]) : Quaternion +static JSBool VectorRotationTo(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, thatv; + double limit; + BOOL gotLimit; + Quaternion result; + uintN consumed; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"rotationTo", argc, argv, &thatv, &consumed))) return NO; + + argc -= consumed; + argv += consumed; + if (argc != 0) // limit parameter is optional. + { + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Vector3D", @"rotationTo", argc, argv, &limit, NULL))) return NO; + gotLimit = YES; + } + else gotLimit = NO; + + if (gotLimit) result = quaternion_limited_rotation_between(thisv, thatv, limit); + else result = quaternion_rotation_between(thisv, thatv); + + return QuaternionToJSValue(context, result, outResult); +} + + +// rotateBy(q : quaternionExpression) : Vector3D +static JSBool VectorRotateBy(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv, result; + Quaternion q; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + if (EXPECT_NOT(!QuaternionFromArgumentList(context, @"Vector3D", @"rotateBy", argc, argv, &q, NULL))) return NO; + + result = quaternion_rotate_vector(q, thisv); + + return VectorToJSValue(context, result, outResult); +} + + +// toArray() : Array +static JSBool VectorToArray(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector thisv; + JSObject *result = NULL; + jsval nVal; + + if (EXPECT_NOT(!JSObjectGetVector(context, this, &thisv))) return NO; + + result = JS_NewArrayObject(context, 0, NULL); + if (result != NULL) + { + // We do this at the top because *outResult is a GC root. + *outResult = OBJECT_TO_JSVAL(result); + + if (JS_NewNumberValue(context, thisv.x, &nVal) && JS_SetElement(context, result, 0, &nVal) && + JS_NewNumberValue(context, thisv.y, &nVal) && JS_SetElement(context, result, 1, &nVal) && + JS_NewNumberValue(context, thisv.z, &nVal) && JS_SetElement(context, result, 2, &nVal)) + { + return YES; + } + // If we get here, the conversion and stuffing in the previous condition failed. + *outResult = JSVAL_VOID; + } + + return NO; +} + + +// interpolate(v : Vector3D, u : Vector3D, alpha : Number) : Vector3D +static JSBool VectorStaticInterpolate(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + Vector av, bv; + double interp; + Vector result; + uintN consumed; + uintN inArgc = argc; + jsval *inArgv = argv; + + if (EXPECT_NOT(argc < 3)) goto INSUFFICIENT_ARGUMENTS; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"interpolate", argc, argv, &av, &consumed))) return NO; + argc -= consumed; + argv += consumed; + if (EXPECT_NOT(argc < 2)) goto INSUFFICIENT_ARGUMENTS; + if (EXPECT_NOT(!VectorFromArgumentList(context, @"Vector3D", @"interpolate", argc, argv, &bv, &consumed))) return NO; + argc -= consumed; + argv += consumed; + if (EXPECT_NOT(argc < 1)) goto INSUFFICIENT_ARGUMENTS; + if (EXPECT_NOT(!NumberFromArgumentList(context, @"Vector3D", @"interpolate", argc, argv, &interp, NULL))) return NO; + + result = OOVectorInterpolate(av, bv, interp); + + return VectorToJSValue(context, result, outResult); + +INSUFFICIENT_ARGUMENTS: + OOReportJSBadArguments(context, @"Vector3D", @"interpolate", inArgc, inArgv, + @"Insufficient parameters", + @"vector expression, vector expression and number"); + return NO; +} + + +// random([maxLength : Number]) : Vector3D +static JSBool VectorStaticRandom(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + double maxLength; + + if (argc == 0 || !NumberFromArgumentListNoError(context, argc, argv, &maxLength, NULL)) maxLength = 1.0; + + return VectorToJSValue(context, OOVectorRandomSpatial(maxLength), outResult); +} + + +// randomDirection([scale : Number]) : Vector3D +static JSBool VectorStaticRandomDirection(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + double scale; + + if (argc == 0 || !NumberFromArgumentListNoError(context, argc, argv, &scale, NULL)) scale = 1.0; + + return VectorToJSValue(context, vector_multiply_scalar(OORandomUnitVector(), scale), outResult); +} + + +// randomDirectionAndLength([maxLength : Number]) : Vector3D +static JSBool VectorStaticRandomDirectionAndLength(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + double maxLength; + + if (argc == 0 || !NumberFromArgumentListNoError(context, argc, argv, &maxLength, NULL)) maxLength = 1.0; + + return VectorToJSValue(context, OOVectorRandomSpatial(maxLength), outResult); +} diff --git a/src/Core/Scripting/OOJSWorldScripts.h b/src/Core/Scripting/OOJSWorldScripts.h new file mode 100644 index 00000000..3d12970e --- /dev/null +++ b/src/Core/Scripting/OOJSWorldScripts.h @@ -0,0 +1,33 @@ +/* + +OOJSWorldScripts.h + +JavaScript world scripts object. This is a global object that allows scripts +to look up and iterate over world scripts. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import + + +void InitOOJSWorldScripts(JSContext *context, JSObject *global); diff --git a/src/Core/Scripting/OOJSWorldScripts.m b/src/Core/Scripting/OOJSWorldScripts.m new file mode 100644 index 00000000..84f32962 --- /dev/null +++ b/src/Core/Scripting/OOJSWorldScripts.m @@ -0,0 +1,120 @@ +/* + +OOJSWorldScripts.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJSWorldScripts.h" +#import "OOJavaScriptEngine.h" +#import "PlayerEntity.h" +#import "OOJSPlayer.h" + + +static JSBool WorldScriptsGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue); +static JSBool WorldScriptsEnumerate(JSContext *cx, JSObject *obj); + +static JSBool GetWorldScriptNames(JSContext *context, JSObject *this, jsval name, jsval *outValue); + + +static JSClass sWorldScriptsClass = +{ + "WorldScripts", + JSCLASS_IS_ANONYMOUS, + + JS_PropertyStub, + JS_PropertyStub, + WorldScriptsGetProperty, + JS_PropertyStub, + WorldScriptsEnumerate, + JS_ResolveStub, + JS_ConvertStub, + JS_FinalizeStub +}; + + +void InitOOJSWorldScripts(JSContext *context, JSObject *global) +{ + JS_DefineObject(context, global, "worldScripts", &sWorldScriptsClass, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); + JS_DefineProperty(context, global, "worldScriptNames", JSVAL_NULL, GetWorldScriptNames, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT); +} + + +static JSBool WorldScriptsGetProperty(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + PlayerEntity *player = OOPlayerForScripting(); + NSString *scriptName = nil; + id script = nil; + + scriptName = JSValToNSString(context, name); + if (scriptName != nil) + { + script = [[player worldScriptsByName] objectForKey:scriptName]; + if (script != nil) + { + /* If script is an OOJSScript, this should return a JS Script + object. For other OOScript subclasses, it will return + JSVAL_NULL. If no script exists, the value will be + JSVAL_VOID. + */ + *outValue = [script javaScriptValueInContext:context]; + } + } + + return YES; +} + + +static JSBool WorldScriptsEnumerate(JSContext *context, JSObject *object) +{ + /* In order to support enumeration of world scripts (e.g., + for (name in worldScripts) { ... }), define each property on demand. + Since world scripts cannot be deleted, we don't need to worry about + that case (as in OOJSMissionVariables). + + Since WorldScriptsGetProperty() will be called for each access anyway, + we define the value as null here. + */ + + NSArray *names = nil; + NSEnumerator *nameEnum = nil; + NSString *name = nil; + + names = [OOPlayerForScripting() worldScriptNames]; + + for (nameEnum = [names objectEnumerator]; (name = [nameEnum nextObject]); ) + { + if (!JS_DefineProperty(context, object, [name UTF8String], JSVAL_NULL, WorldScriptsGetProperty, NULL, JSPROP_ENUMERATE | JSPROP_READONLY | JSPROP_PERMANENT)) return NO; + } + + return YES; +} + + +static JSBool GetWorldScriptNames(JSContext *context, JSObject *this, jsval name, jsval *outValue) +{ + NSArray *names = nil; + + names = [OOPlayerForScripting() worldScriptNames]; + *outValue = [names javaScriptValueInContext:context]; + + return YES; +} diff --git a/src/Core/Scripting/OOJavaScriptEngine.h b/src/Core/Scripting/OOJavaScriptEngine.h new file mode 100644 index 00000000..02c45b97 --- /dev/null +++ b/src/Core/Scripting/OOJavaScriptEngine.h @@ -0,0 +1,327 @@ +/* + +OOJavaScriptEngine.h + +JavaScript support for Oolite +Copyright (C) 2007 David Taylor and Jens Ayton. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#import +#import "Universe.h" +#import "PlayerEntity.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import + +#define OOJSENGINE_MONITOR_SUPPORT (!defined(NDEBUG)) + + +// Macro to selectively compile with JS 1.8 support. +// To be removed when switching to JS 1.8 as default, once FF 3 is final. +#define OOJSENGINE_JS_18 (defined JSFUN_FAST_NATIVE) + + +@protocol OOJavaScriptEngineMonitor; + + +enum +{ + kOOJavaScriptEngineContextPoolCount = 5 +}; + + +@interface OOJavaScriptEngine : NSObject +{ + JSRuntime *runtime; + JSContext *mainContext; + JSContext *contextPool[kOOJavaScriptEngineContextPoolCount]; + uint8_t contextPoolCount; + uint8_t mainContextInUse; + JSObject *globalObject; +#if OOJSENGINE_MONITOR_SUPPORT + id monitor; +#endif +} + ++ (OOJavaScriptEngine *)sharedEngine; + +- (JSObject *)globalObject; + +// The current context. NULL if nothing executing. +// - (JSContext *)context; + +// Call a JS function, setting up new contexts as necessary. +- (BOOL) callJSFunction:(JSFunction *)function + forObject:(JSObject *)jsThis + argc:(uintN)argc + argv:(jsval *)argv + result:(jsval *)outResult; + +// Get a context for doing something other than calling a function. +- (JSContext *)acquireContext; +- (void)releaseContext:(JSContext *)context; + +- (BOOL) addGCRoot:(void *)rootPtr + named:(const char *)name; +- (void) removeGCRoot:(void *)rootPtr; + +@end + + +/* Error and warning reporters. + + Note that after reporting an error in a JavaScript callback, the caller + must return NO to signal an error. +*/ +void OOReportJSError(JSContext *context, NSString *format, ...); +void OOReportJSErrorWithArguments(JSContext *context, NSString *format, va_list args); +void OOReportJSErrorForCaller(JSContext *context, NSString *scriptClass, NSString *function, NSString *format, ...); + +void OOReportJSWarning(JSContext *context, NSString *format, ...); +void OOReportJSWarningWithArguments(JSContext *context, NSString *format, va_list args); +void OOReportJSWarningForCaller(JSContext *context, NSString *scriptClass, NSString *function, NSString *format, ...); + +void OOReportJSBadPropertySelector(JSContext *context, NSString *className, jsint selector); +void OOReportJSBadArguments(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, NSString *message, NSString *expectedArgsDescription); + +/* OOSetJSWarningOrErrorStackSkip() + + Indicate that the direct call site is not relevant for error handler. + Currently, if non-zero, no call site information is provided. + Ideally, we'd stack crawl instead. +*/ +void OOSetJSWarningOrErrorStackSkip(unsigned skip); + + +/* NumberFromArgumentList() + + Get a single number from an argument list. The optional outConsumed + argument can be used to find out how many parameters were used (currently, + this will be 0 on failure, otherwise 1). + + On failure, it will return NO and raise an error. If the caller is a JS + callback, it must return NO to signal an error. +*/ +BOOL NumberFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, double *outNumber, uintN *outConsumed); + +/* NumberFromArgumentListNoError() + + Like NumberFromArgumentList(), but does not report an error on failure. +*/ +BOOL NumberFromArgumentListNoError(JSContext *context, uintN argc, jsval *argv, double *outNumber, uintN *outConsumed); + + +// Typed as int rather than BOOL to work with more general expressions such as bitfield tests. +OOINLINE jsval BOOLToJSVal(int b) INLINE_CONST_FUNC; +OOINLINE jsval BOOLToJSVal(int b) +{ + return BOOLEAN_TO_JSVAL(b != NO); +} + + +/* JSFooNSBar() + + Wrappers to corresponding JS_FooBar()/JS_FooUCBar() functions, but taking + an NSString. Additionally, a NULL context parameter may be used. +*/ +BOOL JSGetNSProperty(JSContext *context, JSObject *object, NSString *name, jsval *value); +BOOL JSSetNSProperty(JSContext *context, JSObject *object, NSString *name, jsval *value); +BOOL JSDefineNSProperty(JSContext *context, JSObject *object, NSString *name, jsval value, JSPropertyOp getter, JSPropertyOp setter, uintN attrs); + + +@interface NSObject (OOJavaScriptConversion) + +/* -javaScriptValueInContext: + + Return the JavaScript object representation of an object. The default + implementation returns JSVAL_VOID. At this time, NSString, NSNumber, + NSArray, NSDictionary, NSNull, Entity, OOScript and OOJSTimer override this. +*/ +- (jsval)javaScriptValueInContext:(JSContext *)context; + +/* -javaScriptDescription + -javaScriptDescriptionWithClassName: + -jsClassName + + See comments for -descriptionComponents in OOCocoa.h. +*/ +- (NSString *)javaScriptDescription; +- (NSString *)javaScriptDescriptionWithClassName:(NSString *)className; +- (NSString *)jsClassName; + +@end + + +/* OOJSValue: an object whose purpose in life is to hold a JavaScript value. + This is somewhat useful for putting JavaScript objects in ObjC collections, + for instance to pass as properties to script loaders. The value is + GC rooted for the lifetime of the OOJSValue. +*/ +@interface OOJSValue: NSObject +{ + jsval _val; +} + ++ (id) valueWithJSValue:(jsval)value inContext:(JSContext *)context; ++ (id) valueWithJSObject:(JSObject *)object inContext:(JSContext *)context; + +- (id) initWithJSValue:(jsval)value inContext:(JSContext *)context; +- (id) initWithJSObject:(JSObject *)object inContext:(JSContext *)context; + +@end + + +/* JSObjectWrapperToString + + Implementation of toString() for JS classes whose private storage is an + Objective-C object reference (generally an OOWeakReference). + + Calls -javaScriptDescription and, if that fails, -description. +*/ +JSBool JSObjectWrapperToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult); + + +/* JSObjectWrapperFinalize + + Finalizer for JS classes whose private storage is a retained object + reference (generally an OOWeakReference, but doesn't have to be). +*/ +void JSObjectWrapperFinalize(JSContext *context, JSObject *this); + + +/* JSObjectWrapperEquality + + Comparator for JS classes whose private storage is a retained object; + wraps isEqual. +*/ +JSBool JSObjectWrapperEquality(JSContext *context, JSObject *this, jsval value, JSBool *outEqual); + + +@interface NSString (OOJavaScriptExtensions) + +// Convert a JSString to an NSString. ++ (id)stringWithJavaScriptString:(JSString *)string; + +// Convert an arbitrary JS object to an NSString, using JS_ValueToString. ++ (id) stringWithJavaScriptValue:(jsval)value inContext:(JSContext *)context; ++ (id)stringOrNilWithJavaScriptValue:(jsval)value inContext:(JSContext *)context; + + +// For diagnostic messages; produces things like @"(42, true, "a string", an object description)". ++ (id)stringWithJavaScriptParameters:(jsval *)params count:(uintN)count inContext:(JSContext *)context; + +// Concatenate sequence of arbitrary JS objects into string. ++ (id)concatenationOfStringsFromJavaScriptValues:(jsval *)values count:(size_t)count separator:(NSString *)separator inContext:(JSContext *)context; + +// Add escape codes for string so that it's a valid JavaScript literal (if you put "" or '' around it). +- (NSString *)escapedForJavaScriptLiteral; + +@end + + +OOINLINE NSString *JSValToNSString(JSContext *context, jsval value) +{ + return [NSString stringOrNilWithJavaScriptValue:value inContext:context]; +} + + +// OOEntityFilterPredicate wrapping a JavaScript function. +typedef struct +{ + JSContext *context; + JSFunction *function; + JSObject *jsThis; + BOOL errorFlag; // Set if a JS exception occurs. The + // exception will have been reported. + // This also supresses further filtering. +} JSFunctionPredicateParameter; +BOOL JSFunctionPredicate(Entity *entity, void *parameter); + +// YES for ships and planets. Parameter: ignored. +BOOL JSEntityIsJavaScriptVisiblePredicate(Entity *entity, void *parameter); + +// YES for ships other than sub-entities and menu-display ships, and planets other than atmospheres and menu miniatures. Parameter: ignored. +BOOL JSEntityIsJavaScriptSearchablePredicate(Entity *entity, void *parameter); + + +id JSValueToObject(JSContext *context, jsval value); +id JSObjectToObject(JSContext *context, JSObject *object); +id JSValueToObjectOfClass(JSContext *context, jsval value, Class requiredClass); +id JSObjectToObjectOfClass(JSContext *context, JSObject *object, Class requiredClass); + +#define DEFINE_JS_OBJECT_GETTER(NAME, CLASS) \ +OOINLINE BOOL NAME(JSContext *context, JSObject *inObject, CLASS **outObject) \ +{ \ + if (outObject == NULL) return NO; \ + *outObject = JSObjectToObjectOfClass(context, inObject, [CLASS class]); \ + return *outObject != nil; \ +} + + +/* Support for JSValueToObject() + + JSClassConverterCallback specifies the prototype for a callback function + which converts a JavaScript object to an Objective-C object. + + JSBasicPrivateObjectConverter() is a JSClassConverterCallback which + returns the JS object's private storage value. It automatically unpacks + OOWeakReferences if relevant. + + JSRegisterObjectConverter() registers a callback for a specific JS class. + It is not automatically propagated to subclasses. +*/ +typedef id (*JSClassConverterCallback)(JSContext *context, JSObject *object); +id JSBasicPrivateObjectConverter(JSContext *context, JSObject *object); + +void JSRegisterObjectConverter(JSClass *theClass, JSClassConverterCallback converter); + + +#if OOJSENGINE_MONITOR_SUPPORT + +/* Protocol for debugging "monitor" object. + The monitor is an object -- in Oolite, or via Distributed Objects -- which + is provided with debugging information by the OOJavaScriptEngine. + Currently, this is implemented in the Debug OXP for Mac OS X only. +*/ + +@protocol OOJavaScriptEngineMonitor + +// Sent for JS errors or warnings. +- (oneway void)jsEngine:(in byref OOJavaScriptEngine *)engine + context:(in JSContext *)context + error:(in JSErrorReport *)errorReport + stackSkip:(in unsigned)stackSkip + withMessage:(in NSString *)message; + +// Sent for JS log messages. Note: messageClass will be nil of Log() is used rather than LogWithClass(). +- (oneway void)jsEngine:(in byref OOJavaScriptEngine *)engine + context:(in JSContext *)context + logMessage:(in NSString *)message + ofClass:(in NSString *)messageClass; + +@end + + +@interface OOJavaScriptEngine (OOMonitorSupport) + +- (void)setMonitor:(id)monitor; + +@end + +#endif diff --git a/src/Core/Scripting/OOJavaScriptEngine.m b/src/Core/Scripting/OOJavaScriptEngine.m new file mode 100644 index 00000000..a857a94b --- /dev/null +++ b/src/Core/Scripting/OOJavaScriptEngine.m @@ -0,0 +1,1547 @@ +/* + +OOJavaScriptEngine.m + +JavaScript support for Oolite +Copyright (C) 2007 David Taylor and Jens Ayton. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOJavaScriptEngine.h" +#import "OOJSScript.h" + +#import "OOCollectionExtractors.h" +#import "Universe.h" +#import "PlanetEntity.h" +#import "NSStringOOExtensions.h" +#import "OOWeakReference.h" +#import "EntityOOJavaScriptExtensions.h" +#import "ResourceManager.h" + +#import "OOJSGlobal.h" +#import "OOJSMissionVariables.h" +#import "OOJSMission.h" +#import "OOJSVector.h" +#import "OOJSQuaternion.h" +#import "OOJSEntity.h" +#import "OOJSShip.h" +#import "OOJSStation.h" +#import "OOJSPlayer.h" +#import "OOJSPlayerShip.h" +#import "OOJSPlanet.h" +#import "OOJSSystem.h" +#import "OOJSOolite.h" +#import "OOJSTimer.h" +#import "OOJSClock.h" +#import "OOJSSun.h" +#import "OOJSWorldScripts.h" +#import "OOJSSound.h" +#import "OOJSSoundSource.h" +#import "OOJSSpecialFunctions.h" +#import "OOJSSystemInfo.h" +#import "OOJSEquipmentInfo.h" +#import "OOJSShipGroup.h" + +#import + + +#if OOJSENGINE_JS_18 +#define OOJSENGINE_JSVERSION JSVERSION_1_8 +#define OOJSENGINE_CONTEXT_OPTIONS JSOPTION_VAROBJFIX | JSOPTION_STRICT | JSOPTION_NATIVE_BRANCH_CALLBACK | JSOPTION_RELIMIT | JSOPTION_ANONFUNFIX +#else +#define OOJSENGINE_JSVERSION JSVERSION_1_7 +#define OOJSENGINE_CONTEXT_OPTIONS JSOPTION_VAROBJFIX | JSOPTION_STRICT | JSOPTION_NATIVE_BRANCH_CALLBACK +#endif + + +#ifdef MOZILLA_1_8_BRANCH +#error Oolite and libjs must be built with MOZILLA_1_8_BRANCH undefined. +#endif +#ifdef JS_THREADSAFE +#error Oolite and libjs must be built with JS_THREADSAFE undefined. +#endif + + +static OOJavaScriptEngine *sSharedEngine = nil; +static unsigned sErrorHandlerStackSkip = 0; + + +#if OOJSENGINE_MONITOR_SUPPORT + +@interface OOJavaScriptEngine (OOMonitorSupportInternal) + +- (void)sendMonitorError:(JSErrorReport *)errorReport + withMessage:(NSString *)message + inContext:(JSContext *)context; + +- (void)sendMonitorLogMessage:(NSString *)message + withMessageClass:(NSString *)messageClass + inContext:(JSContext *)context; + +@end + +#endif + + +static void ReportJSError(JSContext *context, const char *message, JSErrorReport *report); + + +static void RegisterStandardObjectConverters(JSContext *context); + +static id JSArrayConverter(JSContext *context, JSObject *object); +static id JSGenericObjectConverter(JSContext *context, JSObject *object); + + +static void ReportJSError(JSContext *context, const char *message, JSErrorReport *report) +{ + NSString *severity = @"error"; + NSString *messageText = nil; + NSString *lineBuf = nil; + NSString *messageClass = nil; + NSString *highlight = @"*****"; + NSString *activeScript = nil; + + jschar empty[1] = { 0 }; + JSErrorReport blankReport = + { + .filename = "", + .linebuf = "", + .uclinebuf = empty, + .uctokenptr = empty, + .ucmessage = empty + }; + if (EXPECT_NOT(report == NULL)) report = &blankReport; + if (EXPECT_NOT(message == NULL || *message == '\0')) message = ""; + + // Type of problem: error, warning or exception? (Strict flag wilfully ignored.) + if (report->flags & JSREPORT_EXCEPTION) severity = @"exception"; + else if (report->flags & JSREPORT_WARNING) + { + severity = @"warning"; + highlight = @"-----"; + } + + // The error message itself + messageText = [NSString stringWithUTF8String:message]; + + // Get offending line, if present, and trim trailing line breaks + lineBuf = [NSString stringWithUTF16String:report->uclinebuf]; + while ([lineBuf hasSuffix:@"\n"] || [lineBuf hasSuffix:@"\r"]) lineBuf = [lineBuf substringToIndex:[lineBuf length] - 1]; + + // Get string for error number, for useful log message classes + NSDictionary *errorNames = [ResourceManager dictionaryFromFilesNamed:@"oolite-javascript-errors.plist" inFolder:@"Config" andMerge:YES]; + NSString *errorNumberStr = [NSString stringWithFormat:@"%u", report->errorNumber]; + NSString *errorName = [errorNames stringForKey:errorNumberStr]; + if (errorName == nil) errorName = errorNumberStr; + + // Log message class + messageClass = [NSString stringWithFormat:@"script.javaScript.%@.%@", severity, errorName]; + + // First line: problem description + activeScript = [[OOJSScript currentlyRunningScript] displayName]; + if (activeScript == nil) activeScript = @""; + OOLog(messageClass, @"%@ JavaScript %@ (%@): %@", highlight, severity, activeScript, messageText); + + if (sErrorHandlerStackSkip == 0) + { + // Second line: where error occured, and line if provided. (The line is only provided for compile-time errors, not run-time errors.) + if ([lineBuf length] != 0) + { + OOLog(messageClass, @" %s, line %d: %@", report->filename, report->lineno, lineBuf); + } + else + { + OOLog(messageClass, @" %s, line %d.", report->filename, report->lineno); + } + } + +#if OOJSENGINE_MONITOR_SUPPORT + JSExceptionState *exState = JS_SaveExceptionState(context); + [[OOJavaScriptEngine sharedEngine] sendMonitorError:report + withMessage:messageText + inContext:context]; + JS_RestoreExceptionState(context, exState); +#endif +} + + +//=========================================================================== +// JavaScript engine initialisation and shutdown +//=========================================================================== + +@implementation OOJavaScriptEngine + ++ (OOJavaScriptEngine *)sharedEngine +{ + if (sSharedEngine == nil) [[self alloc] init]; + + return sSharedEngine; +} + + +- (id) init +{ + assert(sSharedEngine == nil); + +#ifndef NDEBUG + // This one is causing trouble for the Linux crowd. :-/ + if (!JS_CStringsAreUTF8()) + { + OOLog(@"script.javaScript.init.badSpiderMonkey", @"SpiderMonkey (libjs/libmozjs) must be built with the JS_C_STRINGS_ARE_UTF8 macro defined. Additionally, JS_THREADSAFE must be undefined and MOZILLA_1_8_BRANCH must be undefined."); + exit(EXIT_FAILURE); + } +#endif + + self = [super init]; + + assert(sizeof(jschar) == sizeof(unichar)); + + // set up global JS variables, including global and custom objects + + // initialize the JS run time, and return result in runtime + runtime = JS_NewRuntime(8L * 1024L * 1024L); + + // if runtime creation failed, end the program here + if (runtime == NULL) + { + OOLog(@"script.javaScript.init.error", @"***** FATAL ERROR: failed to create JavaScript %@.", @"runtime"); + exit(1); + } + + // create a context and associate it with the JS run time + mainContext = JS_NewContext(runtime, 8192); + + // if context creation failed, end the program here + if (mainContext == NULL) + { + OOLog(@"script.javaScript.init.error", @"***** FATAL ERROR: failed to create JavaScript %@.", @"context"); + exit(1); + } + + JS_SetOptions(mainContext, OOJSENGINE_CONTEXT_OPTIONS); + JS_SetVersion(mainContext, OOJSENGINE_JSVERSION); + +#if JS_GC_ZEAL + uint8_t gcZeal = [[NSUserDefaults standardUserDefaults] unsignedCharForKey:@"js-gc-zeal"]; + if (gcZeal > 0) + { + // Useful js-gc-zeal values are 0 (off), 1 and 2. + OOLog(@"script.javaScript.debug.gcZeal", @"Setting JavaScript garbage collector zeal to %u.", gcZeal); + JS_SetGCZeal(mainContext, gcZeal); + } +#endif + + JS_SetErrorReporter(mainContext, ReportJSError); + + // Create the global object. + CreateOOJSGlobal(mainContext, &globalObject); + + // Initialize the built-in JS objects and the global object. + JS_InitStandardClasses(mainContext, globalObject); + RegisterStandardObjectConverters(mainContext); + + SetUpOOJSGlobal(mainContext, globalObject); + + // Initialize Oolite classes. + InitOOJSMissionVariables(mainContext, globalObject); + InitOOJSMission(mainContext, globalObject); + InitOOJSOolite(mainContext, globalObject); + InitOOJSVector(mainContext, globalObject); + InitOOJSQuaternion(mainContext, globalObject); + InitOOJSSystem(mainContext, globalObject); + InitOOJSEntity(mainContext, globalObject); + InitOOJSShip(mainContext, globalObject); + InitOOJSStation(mainContext, globalObject); + InitOOJSPlayer(mainContext, globalObject); + InitOOJSPlayerShip(mainContext, globalObject); + InitOOJSSun(mainContext, globalObject); + InitOOJSPlanet(mainContext, globalObject); + InitOOJSScript(mainContext, globalObject); + InitOOJSTimer(mainContext, globalObject); + InitOOJSClock(mainContext, globalObject); + InitOOJSWorldScripts(mainContext, globalObject); + InitOOJSSound(mainContext, globalObject); + InitOOJSSoundSource(mainContext, globalObject); + InitOOJSSpecialFunctions(mainContext, globalObject); + InitOOJSSystemInfo(mainContext, globalObject); + InitOOJSEquipmentInfo(mainContext, globalObject); + InitOOJSShipGroup(mainContext, globalObject); + + sSharedEngine = self; + + // Run prefix script. + [OOJSScript nonLegacyScriptFromFileNamed:@"oolite-global-prefix.js" + properties:[NSDictionary dictionaryWithObject:JSSpecialFunctionsObjectWrapper(mainContext) + forKey:@"special"]]; + + OOLog(@"script.javaScript.init.success", @"Set up JavaScript context."); + + return self; +} + + +- (void) dealloc +{ + unsigned i; + + sSharedEngine = nil; + + for (i = 0; i != contextPoolCount; ++i) + { + JS_DestroyContext(contextPool[i]); + } + JS_DestroyContext(mainContext); + JS_DestroyRuntime(runtime); + + [super dealloc]; +} + + +- (JSObject *) globalObject +{ + return globalObject; +} + + +- (BOOL) callJSFunction:(JSFunction *)function + forObject:(JSObject *)jsThis + argc:(uintN)argc + argv:(jsval *)argv + result:(jsval *)outResult +{ + JSContext *context = NULL; + BOOL result; + + context = [self acquireContext]; + result = JS_CallFunction(context, jsThis, function, argc, argv, outResult); + JS_ReportPendingException(context); + [self releaseContext:context]; + + return result; +} + + +- (JSContext *)acquireContext +{ + JSContext *context = NULL; + + if (!mainContextInUse) + { + /* Favour the main context. + There's overhead to using objects from a different context. By + having one preferred context, most objects will belong to that + context and that context will be the one used in the common case + of only one script running. + */ + mainContextInUse = YES; + context = mainContext; + } + else if (contextPoolCount != 0) + { + context = contextPool[--contextPoolCount]; + } + else + { + OOLog(@"script.javaScript.context.create", @"Creating JS context."); + + context = JS_NewContext(runtime, 8192); + // if context creation failed, end the program here + if (context == NULL) + { + OOLog(@"script.javaScript.error", @"***** FATAL ERROR: failed to create JavaScript %@.", @"context"); + exit(1); + } + + JS_SetOptions(context, OOJSENGINE_CONTEXT_OPTIONS); + JS_SetVersion(context, OOJSENGINE_JSVERSION); + JS_SetErrorReporter(context, ReportJSError); + JS_SetGlobalObject(context, globalObject); + } + + return context; +} + + +- (void)releaseContext:(JSContext *)context +{ + if (context == NULL) return; + + if (context == mainContext) + { + mainContextInUse = NO; + } + else if (contextPoolCount < kOOJavaScriptEngineContextPoolCount) + { + contextPool[contextPoolCount++] = context; + } + else + { + OOLog(@"script.javaScript.context.destroy", @"Destroying JS context."); + + JS_DestroyContextMaybeGC(context); + } +} + + +- (BOOL) addGCRoot:(void *)rootPtr + named:(const char *)name +{ + BOOL result; + JSContext *context = NULL; + + context = [self acquireContext]; + result = JS_AddNamedRoot(context, rootPtr, name); + [self releaseContext:context]; + return result; +} + + +- (void) removeGCRoot:(void *)rootPtr +{ + JSContext *context = NULL; + + context = [self acquireContext]; + JS_RemoveRoot(context, rootPtr); + [self releaseContext:context]; +} + +@end + + +#if OOJSENGINE_MONITOR_SUPPORT + +@implementation OOJavaScriptEngine (OOMonitorSupport) + +- (void)setMonitor:(id)inMonitor +{ + [monitor autorelease]; + monitor = [inMonitor retain]; +} + +@end + + +@implementation OOJavaScriptEngine (OOMonitorSupportInternal) + +- (void)sendMonitorError:(JSErrorReport *)errorReport + withMessage:(NSString *)message + inContext:(JSContext *)theContext +{ + if ([monitor respondsToSelector:@selector(jsEngine:context:error:stackSkip:withMessage:)]) + { + [monitor jsEngine:self context:theContext error:errorReport stackSkip:sErrorHandlerStackSkip withMessage:message]; + } +} + + +- (void)sendMonitorLogMessage:(NSString *)message + withMessageClass:(NSString *)messageClass + inContext:(JSContext *)theContext +{ + if ([monitor respondsToSelector:@selector(jsEngine:context:logMessage:ofClass:)]) + { + [monitor jsEngine:self context:theContext logMessage:message ofClass:messageClass]; + } +} + +@end + +#endif + + +static NSString *CallerPrefix(NSString *scriptClass, NSString *function) +{ + if (function == nil) return @""; + if (scriptClass == nil) return [function stringByAppendingString:@": "]; + return [NSString stringWithFormat:@"%@.%@: ", scriptClass, function]; +} + + +void OOReportJSError(JSContext *context, NSString *format, ...) +{ + va_list args; + + va_start(args, format); + OOReportJSErrorWithArguments(context, format, args); + va_end(args); +} + + +void OOReportJSErrorForCaller(JSContext *context, NSString *scriptClass, NSString *function, NSString *format, ...) +{ + va_list args; + NSString *msg = nil; + + va_start(args, format); + msg = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + OOReportJSError(context, @"%@%@", CallerPrefix(scriptClass, function), msg); + [msg release]; +} + + +void OOReportJSErrorWithArguments(JSContext *context, NSString *format, va_list args) +{ + NSString *msg = nil; + + msg = [[NSString alloc] initWithFormat:format arguments:args]; + JS_ReportError(context, "%s", [msg UTF8String]); + [msg release]; +} + + +void OOReportJSWarning(JSContext *context, NSString *format, ...) +{ + va_list args; + + va_start(args, format); + OOReportJSWarningWithArguments(context, format, args); + va_end(args); +} + + +void OOReportJSWarningForCaller(JSContext *context, NSString *scriptClass, NSString *function, NSString *format, ...) +{ + va_list args; + NSString *msg = nil; + + va_start(args, format); + msg = [[NSString alloc] initWithFormat:format arguments:args]; + va_end(args); + + OOReportJSWarning(context, @"%@%@", CallerPrefix(scriptClass, function), msg); + [msg release]; +} + + +void OOReportJSWarningWithArguments(JSContext *context, NSString *format, va_list args) +{ + NSString *msg = nil; + + msg = [[NSString alloc] initWithFormat:format arguments:args]; + JS_ReportWarning(context, "%s", [msg UTF8String]); + [msg release]; +} + + +void OOReportJSBadPropertySelector(JSContext *context, NSString *className, jsint selector) +{ + OOReportJSError(context, @"Internal error: bad property identifier %i in property accessor for class %@.", selector, className); +} + + +void OOReportJSBadArguments(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, NSString *message, NSString *expectedArgsDescription) +{ + if (message == nil) message = @"Invalid arguments"; + message = [NSString stringWithFormat:@"%@ %@", message, [NSString stringWithJavaScriptParameters:argv count:argc inContext:context]]; + if (expectedArgsDescription != nil) message = [NSString stringWithFormat:@"%@ -- expected %@", message, expectedArgsDescription]; + + OOReportJSErrorForCaller(context, scriptClass, function, @"%@.", message); +} + + +void OOSetJSWarningOrErrorStackSkip(unsigned skip) +{ + sErrorHandlerStackSkip = skip; +} + + +BOOL NumberFromArgumentList(JSContext *context, NSString *scriptClass, NSString *function, uintN argc, jsval *argv, double *outNumber, uintN *outConsumed) +{ + if (NumberFromArgumentListNoError(context, argc, argv, outNumber, outConsumed)) return YES; + else + { + OOReportJSBadArguments(context, scriptClass, function, argc, argv, + @"Expected number, got", NULL); + return NO; + } +} + + +BOOL NumberFromArgumentListNoError(JSContext *context, uintN argc, jsval *argv, double *outNumber, uintN *outConsumed) +{ + double value; + + // Sanity checks. + if (outConsumed != NULL) *outConsumed = 0; + if (EXPECT_NOT(argc == 0 || argv == NULL || outNumber == NULL)) + { + OOLogGenericParameterError(); + return NO; + } + + // Get value, if possible. + if (EXPECT_NOT(!JS_ValueToNumber(context, argv[0], &value) || isnan(value))) + { + return NO; + } + + // Success. + *outNumber = value; + if (outConsumed != NULL) *outConsumed = 1; + return YES; +} + + +static BOOL ExtractString(NSString *string, jschar **outString, size_t *outLength) +{ + assert(outString != NULL && outLength != NULL); + assert(sizeof (unichar) == sizeof (jschar)); // Should both be 16 bits + + *outLength = [string length]; + if (*outLength == 0) return NO; // nil/empty strings not accepted. + + *outString = malloc(sizeof (unichar) * *outLength); + if (*outString == NULL) return NO; + + [string getCharacters:(unichar *)*outString]; + return YES; +} + + +BOOL JSGetNSProperty(JSContext *context, JSObject *object, NSString *name, jsval *value) +{ + jschar *buffer = NULL; + size_t length; + BOOL OK = NO; + BOOL tempCtxt = NO; + + if (context == NULL) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + tempCtxt = YES; + } + if (ExtractString(name, &buffer, &length)) + { + OK = JS_GetUCProperty(context, object, buffer, length, value); + free(buffer); + } + + if (tempCtxt) [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + return OK; +} + + +BOOL JSSetNSProperty(JSContext *context, JSObject *object, NSString *name, jsval *value) +{ + jschar *buffer = NULL; + size_t length; + BOOL OK = NO; + BOOL tempCtxt = NO; + + if (context == NULL) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + tempCtxt = YES; + } + if (ExtractString(name, &buffer, &length)) + { + OK = JS_SetUCProperty(context, object, buffer, length, value); + free(buffer); + } + + if (tempCtxt) [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + return OK; +} + + +BOOL JSDefineNSProperty(JSContext *context, JSObject *object, NSString *name, jsval value, JSPropertyOp getter, JSPropertyOp setter, uintN attrs) +{ + jschar *buffer = NULL; + size_t length; + BOOL OK = NO; + BOOL tempCtxt = NO; + + if (context == NULL) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + tempCtxt = YES; + } + if (ExtractString(name, &buffer, &length)) + { + OK = JS_DefineUCProperty(context, object, buffer, length, value, getter, setter, attrs); + free(buffer); + } + + if (tempCtxt) [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + return OK; +} + + +static JSObject *JSArrayFromNSArray(JSContext *context, NSArray *array) +{ + JSObject *result = NULL; + unsigned i; + unsigned count; + jsval value; + BOOL OK = YES; + + if (array == nil) return NULL; + + NS_DURING + result = JS_NewArrayObject(context, 0, NULL); + if (result == NULL) NS_VALUERETURN(NULL, JSObject *); + + count = [array count]; + for (i = 0; i != count; ++i) + { + value = [[array objectAtIndex:i] javaScriptValueInContext:context]; + OK = JS_SetElement(context, result, i, &value); + if (!OK) NS_VALUERETURN(NULL, JSObject *); + } + NS_HANDLER + result = NULL; + NS_ENDHANDLER + + return (JSObject *)result; +} + + +static BOOL JSNewNSArrayValue(JSContext *context, NSArray *array, jsval *value) +{ + JSObject *object = NULL; + BOOL OK = YES; + + if (value == NULL) return NO; + + // NOTE: should be called within a local root scope or have *value be a set root for GC reasons. + if (!JS_EnterLocalRootScope(context)) return NO; + + object = JSArrayFromNSArray(context, array); + if (object == NULL) + { + *value = JSVAL_VOID; + OK = NO; + } + else + { + *value = OBJECT_TO_JSVAL(object); + } + + JS_LeaveLocalRootScope(context); + return OK; +} + + +/* Convert an NSDictionary to a JavaScript Object. + Only properties whose keys are either strings or non-negative NSNumbers, + and whose values have a non-void JS representation, are converted. +*/ +static JSObject *JSObjectFromNSDictionary(JSContext *context, NSDictionary *dictionary) +{ + JSObject *result = NULL; + BOOL OK = YES; + NSEnumerator *keyEnum = nil; + id key = nil; + jsval value; + jsint index; + + if (dictionary == nil) return NULL; + + NS_DURING + result = JS_NewObject(context, NULL, NULL, NULL); // create object of class Object + if (result == NULL) NS_VALUERETURN(NULL, JSObject *); + + for (keyEnum = [dictionary keyEnumerator]; (key = [keyEnum nextObject]); ) + { + if ([key isKindOfClass:[NSString class]] && [key length] != 0) + { + value = [[dictionary objectForKey:key] javaScriptValueInContext:context]; + if (value != JSVAL_VOID) + { + OK = JSSetNSProperty(context, result, key, &value); + if (!OK) NS_VALUERETURN(NULL, JSObject *); + } + } + else if ([key isKindOfClass:[NSNumber class]]) + { + index = [key intValue]; + if (0 < index) + { + value = [[dictionary objectForKey:key] javaScriptValueInContext:context]; + if (value != JSVAL_VOID) + { + OK = JS_SetElement(context, (JSObject *)result, index, &value); + if (!OK) NS_VALUERETURN(NULL, JSObject *); + } + } + } + } + NS_HANDLER + result = NULL; + NS_ENDHANDLER + + return (JSObject *)result; +} + + +static BOOL JSNewNSDictionaryValue(JSContext *context, NSDictionary *dictionary, jsval *value) +{ + JSObject *object = NULL; + BOOL OK = YES; + + if (value == NULL) return NO; + + // NOTE: should be called within a local root scope or have *value be a set root for GC reasons. + if (!JS_EnterLocalRootScope(context)) return NO; + + object = JSObjectFromNSDictionary(context, dictionary); + if (object == NULL) + { + *value = JSVAL_VOID; + OK = NO; + } + else + { + *value = OBJECT_TO_JSVAL(object); + } + + JS_LeaveLocalRootScope(context); + return OK; +} + + +@implementation NSObject (OOJavaScriptConversion) + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + return JSVAL_VOID; +} + + +- (NSString *)jsClassName +{ + return nil; +} + + +- (NSString *)javaScriptDescription +{ + return [self javaScriptDescriptionWithClassName:[self jsClassName]]; +} + + +- (NSString *)javaScriptDescriptionWithClassName:(NSString *)className +{ + NSString *components = nil; + NSString *description = nil; + + components = [self descriptionComponents]; + if (className == nil) className = [[self class] description]; + + if (components != nil) + { + description = [NSString stringWithFormat:@"[%@ %@]", className, components]; + } + else + { + description = [NSString stringWithFormat:@"[object %@]", className]; + } + + return description; +} + +@end + + +@implementation OOJSValue + ++ (id) valueWithJSValue:(jsval)value inContext:(JSContext *)context +{ + return [[[self alloc] initWithJSValue:value inContext:context] autorelease]; +} + + ++ (id) valueWithJSObject:(JSObject *)object inContext:(JSContext *)context +{ + return [[[self alloc] initWithJSObject:object inContext:context] autorelease]; +} + + +- (id) initWithJSValue:(jsval)value inContext:(JSContext *)context +{ + self = [super init]; + if (self != nil) + { + BOOL tempCtxt = NO; + if (context == NULL) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + tempCtxt = YES; + } + + _val = value; + JS_AddNamedRoot(context, &_val, "OOJSValue"); + + if (tempCtxt) [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + } + return self; +} + + +- (id) initWithJSObject:(JSObject *)object inContext:(JSContext *)context +{ + return [self initWithJSValue:OBJECT_TO_JSVAL(object) inContext:context]; +} + + +- (void) dealloc +{ + JSContext *context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + JS_RemoveRoot(context, &_val); + [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + + [super dealloc]; +} + + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + return _val; +} + +@end + + +@implementation NSString (OOJavaScriptExtensions) + +// Convert a JSString to an NSString. ++ (id)stringWithJavaScriptString:(JSString *)string +{ + jschar *chars = NULL; + size_t length; + + chars = JS_GetStringChars(string); + length = JS_GetStringLength(string); + + return [NSString stringWithCharacters:chars length:length]; +} + + ++ (id)stringWithJavaScriptValue:(jsval)value inContext:(JSContext *)context +{ + // In some cases we didn't test the original stringWith... function for nil, causing difficult + // to track crashes. We now have two similar functions: stringWith... which never returns nil and + // stringOrNilWith... (alias JSValToNSString) which can return nil and is used in most cases. + + if (JSVAL_IS_VOID(value)) return @"undefined"; + if (JSVAL_IS_NULL(value)) return @"null"; + + return [self stringOrNilWithJavaScriptValue:value inContext:context]; +} + + ++ (id)stringOrNilWithJavaScriptValue:(jsval)value inContext:(JSContext *)context +{ + JSString *string = NULL; + BOOL tempCtxt = NO; + + if (JSVAL_IS_NULL(value) || JSVAL_IS_VOID(value)) return nil; + + if (context == NULL) + { + context = [[OOJavaScriptEngine sharedEngine] acquireContext]; + tempCtxt = YES; + } + string = JS_ValueToString(context, value); // Calls the value's toString method if needed. + if (tempCtxt) [[OOJavaScriptEngine sharedEngine] releaseContext:context]; + + return [NSString stringWithJavaScriptString:string]; +} + + ++ (id)stringWithJavaScriptParameters:(jsval *)params count:(uintN)count inContext:(JSContext *)context +{ + if (params == NULL && count != 0) return nil; + + uintN i; + jsval val; + NSMutableString *result = [NSMutableString stringWithString:@"("]; + NSString *valString = nil; + + for (i = 0; i < count; ++i) + { + if (i != 0) [result appendString:@", "]; + + val = params[i]; + valString = [self stringWithJavaScriptValue:val inContext:context]; + if (JSVAL_IS_STRING(val)) + { + [result appendFormat:@"\"%@\"", valString]; + } + else if (val && JSVAL_IS_OBJECT(val) && JS_IsArrayObject(context, JSVAL_TO_OBJECT(val))) + { + [result appendFormat:@"[%@]", valString ]; + } + else + { + [result appendString:valString]; //crash if valString is nil + } + } + + [result appendString:@")"]; + return result; +} + + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + size_t length; + unichar *buffer = NULL; + JSString *string = NULL; + + length = [self length]; + if (length == 0) return JS_GetEmptyStringValue(context); + + buffer = malloc(length * sizeof *buffer); + if (buffer == NULL) return JSVAL_VOID; + + [self getCharacters:buffer]; + + string = JS_NewUCStringCopyN(context, buffer, length); + free(buffer); + + return STRING_TO_JSVAL(string); +} + + ++ (id)concatenationOfStringsFromJavaScriptValues:(jsval *)values count:(size_t)count separator:(NSString *)separator inContext:(JSContext *)context +{ + size_t i; + NSMutableString *result = nil; + NSString *element = nil; + + if (count < 1) return nil; + if (values == NULL) return NULL; + + for (i = 0; i != count; ++i) + { + element = [NSString stringWithJavaScriptValue:values[i] inContext:context]; + if (result == nil) result = [[element mutableCopy] autorelease]; + else + { + if (separator != nil) [result appendString:separator]; + [result appendString:element]; + } + } + + return result; +} + + +- (NSString *)escapedForJavaScriptLiteral +{ + NSMutableString *result = nil; + unsigned i, length; + unichar c; + NSAutoreleasePool *pool = nil; + + length = [self length]; + result = [NSMutableString stringWithCapacity:[self length]]; + + // Not hugely efficient. + pool = [[NSAutoreleasePool alloc] init]; + for (i = 0; i != length; ++i) + { + c = [self characterAtIndex:i]; + switch (c) + { + case '\\': + [result appendString:@"\\\\"]; + break; + + case '\b': + [result appendString:@"\\b"]; + break; + + case '\f': + [result appendString:@"\\f"]; + break; + + case '\n': + [result appendString:@"\\n"]; + break; + + case '\r': + [result appendString:@"\\r"]; + break; + + case '\t': + [result appendString:@"\\t"]; + break; + + case '\v': + [result appendString:@"\\v"]; + break; + + case '\'': + [result appendString:@"\\\'"]; + break; + + case '\"': + [result appendString:@"\\\""]; + break; + + default: + [result appendString:[NSString stringWithCharacters:&c length:1]]; + } + } + [pool release]; + return result; +} + +@end + + +#ifndef NDEBUG + +// For use in debugger +const char *JSValueToStrDbg(jsval val) +{ + return [JSValToNSString(NULL, val) UTF8String]; +} + + +const char *JSObjectToStrDbg(JSObject *obj) +{ + return [JSValToNSString(NULL, OBJECT_TO_JSVAL(obj)) UTF8String]; +} + + +const char *JSValueTypeDbg(jsval val) +{ + if (JSVAL_IS_INT(val)) return "integer"; + if (JSVAL_IS_DOUBLE(val)) return "double"; + if (JSVAL_IS_STRING(val)) return "string"; + if (JSVAL_IS_BOOLEAN(val)) return "boolean"; + if (JSVAL_IS_NULL(val)) return "null"; + if (JSVAL_IS_VOID(val)) return "void"; + if (JSVAL_IS_OBJECT(val)) return JS_GetClass(JSVAL_TO_OBJECT(val))->name; + return "unknown"; +} + +#endif + + +@implementation NSArray (OOJavaScriptConversion) + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + jsval value = JSVAL_VOID; + JSNewNSArrayValue(context, self, &value); + return value; +} + +@end + + +@implementation NSDictionary (OOJavaScriptConversion) + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + jsval value = JSVAL_VOID; + JSNewNSDictionaryValue(context, self, &value); + return value; +} + +@end + + +@implementation NSNumber (OOJavaScriptConversion) + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + jsval result; + BOOL isFloat = NO; + const char *type; + long long longLongValue; + + if (self == [NSNumber numberWithBool:YES]) + { + /* Under OS X, at least, numberWithBool: returns one of two singletons. + There is no other way to reliably identify a boolean NSNumber. + Fun, eh? */ + result = JSVAL_TRUE; + } + else if (self == [NSNumber numberWithBool:NO]) + { + result = JSVAL_FALSE; + } + else + { + longLongValue = [self longLongValue]; + if (longLongValue < (long long)JSVAL_INT_MIN || (long long)JSVAL_INT_MAX < longLongValue) + { + // values outside JSVAL_INT range are returned as doubles. + isFloat = YES; + } + else + { + // Check value type. + type = [self objCType]; + if (type[0] == 'f' || type[0] == 'd') isFloat = YES; + } + + if (isFloat) + { + if (!JS_NewDoubleValue(context, [self doubleValue], &result)) result = JSVAL_VOID; + } + else + { + result = INT_TO_JSVAL(longLongValue); + } + } + + return result; +} + +@end + + +@implementation NSNull (OOJavaScriptConversion) + +- (jsval)javaScriptValueInContext:(JSContext *)context +{ + return JSVAL_NULL; +} + +@end + + +JSBool JSObjectWrapperToString(JSContext *context, JSObject *this, uintN argc, jsval *argv, jsval *outResult) +{ + id object = nil; + NSString *description = nil; + JSClass *jsClass = NULL; + + object = JSObjectToObject(context, this); + if (object != nil) + { + description = [object javaScriptDescription]; + if (description == nil) description = [object description]; + } + if (description == nil) + { + jsClass = JS_GetClass(this); + if (jsClass != NULL) + { + description = [NSString stringWithFormat:@"[object %@]", [NSString stringWithUTF8String:jsClass->name]]; + } + } + if (description == nil) description = @"[object]"; + + *outResult = [description javaScriptValueInContext:context]; + return YES; +} + + +void JSObjectWrapperFinalize(JSContext *context, JSObject *this) +{ + [(id)JS_GetPrivate(context, this) release]; + JS_SetPrivate(context, this, nil); +} + + +JSBool JSObjectWrapperEquality(JSContext *context, JSObject *this, jsval value, JSBool *outEqual) +{ + id thisObj, thatObj; + + thisObj = JSObjectToObject(context, this); + thatObj = JSValueToObject(context, value); + + *outEqual = [thisObj isEqual:thatObj]; + return YES; +} + + +BOOL JSFunctionPredicate(Entity *entity, void *parameter) +{ + JSFunctionPredicateParameter *param = parameter; + jsval args[1]; + jsval rval = JSVAL_VOID; + JSBool result = NO; + + if (param->errorFlag) return NO; + + args[0] = [entity javaScriptValueInContext:param->context]; + if (JS_CallFunction(param->context, param->jsThis, param->function, 1, args, &rval)) + { + if (!JS_ValueToBoolean(param->context, rval, &result)) result = NO; + if (JS_IsExceptionPending(param->context)) + { + JS_ReportPendingException(param->context); + param->errorFlag = YES; + } + } + + return result; +} + + +BOOL JSEntityIsJavaScriptVisiblePredicate(Entity *entity, void *parameter) +{ + return [entity isVisibleToScripts]; +} + + +BOOL JSEntityIsJavaScriptSearchablePredicate(Entity *entity, void *parameter) +{ + if (![entity isVisibleToScripts]) return NO; + if ([entity isShip]) + { + if ([entity isSubEntity]) return NO; + if ([entity status] == STATUS_COCKPIT_DISPLAY) return NO; // Demo ship + return YES; + } + else if ([entity isPlanet]) + { + switch ([(PlanetEntity *)entity planetType]) + { + case PLANET_TYPE_MOON: + case PLANET_TYPE_GREEN: + case PLANET_TYPE_SUN: + return YES; + + case PLANET_TYPE_ATMOSPHERE: + case PLANET_TYPE_MINIATURE: + return NO; + } + } + + return YES; // would happen if we added a new script-visible class +} + + +static NSMutableDictionary *sObjectConverters; + +id JSValueToObject(JSContext *context, jsval value) +{ + if (JSVAL_IS_NULL(value) || JSVAL_IS_VOID(value)) return nil; + + if (JSVAL_IS_INT(value)) + { + return [NSNumber numberWithInt:JSVAL_TO_INT(value)]; + } + if (JSVAL_IS_DOUBLE(value)) + { + return [NSNumber numberWithDouble:*JSVAL_TO_DOUBLE(value)]; + } + if (JSVAL_IS_BOOLEAN(value)) + { + return [NSNumber numberWithBool:JSVAL_TO_BOOLEAN(value)]; + } + if (JSVAL_IS_STRING(value)) + { + return JSValToNSString(context, value); + } + if (JSVAL_IS_OBJECT(value)) + { + return JSObjectToObject(context, JSVAL_TO_OBJECT(value)); + } + return nil; +} + + +id JSObjectToObject(JSContext *context, JSObject *object) +{ + NSValue *wrappedClass = nil; + NSValue *wrappedConverter = nil; + JSClassConverterCallback converter = NULL; + JSClass *class = NULL; + + if (object == NULL) return nil; + + class = JS_GetClass(object); + wrappedClass = [NSValue valueWithPointer:class]; + if (wrappedClass != nil) wrappedConverter = [sObjectConverters objectForKey:wrappedClass]; + if (wrappedConverter != nil) + { + converter = [wrappedConverter pointerValue]; + return converter(context, object); + } + return nil; +} + + +id JSValueToObjectOfClass(JSContext *context, jsval value, Class requiredClass) +{ + id result = JSValueToObject(context, value); + if (![result isKindOfClass:requiredClass]) result = nil; + return result; +} + + +id JSObjectToObjectOfClass(JSContext *context, JSObject *object, Class requiredClass) +{ + id result = JSObjectToObject(context, object); + if (![result isKindOfClass:requiredClass]) result = nil; + return result; +} + + +id JSBasicPrivateObjectConverter(JSContext *context, JSObject *object) +{ + id result; + + /* This will do the right thing - for non-OOWeakReferences, + weakRefUnderlyingObject returns the object itself. For nil, of course, + it returns nil. + */ + result = JS_GetPrivate(context, object); + return [result weakRefUnderlyingObject]; +} + + +void JSRegisterObjectConverter(JSClass *theClass, JSClassConverterCallback converter) +{ + NSValue *wrappedClass = nil; + NSValue *wrappedConverter = nil; + + if (theClass == NULL) return; + if (sObjectConverters == nil) sObjectConverters = [[NSMutableDictionary alloc] init]; + + wrappedClass = [NSValue valueWithPointer:theClass]; + if (converter != NULL) + { + wrappedConverter = [NSValue valueWithPointer:converter]; + [sObjectConverters setObject:wrappedConverter forKey:wrappedClass]; + } + else + { + [sObjectConverters removeObjectForKey:wrappedClass]; + } +} + + +static void RegisterStandardObjectConverters(JSContext *context) +{ + JSObject *templateObject = NULL; + JSClass *class = NULL; + + // Create an array in order to get array class. + templateObject = JS_NewArrayObject(context, 0, NULL); + class = JS_GetClass(templateObject); + JSRegisterObjectConverter(class, JSArrayConverter); + + // Likewise, create a blank object to get its class. + // This is not documented (not much is) but JS_NewObject falls back to Object if passed a NULL class. + templateObject = JS_NewObject(context, NULL, NULL, NULL); + class = JS_GetClass(templateObject); + JSRegisterObjectConverter(class, JSGenericObjectConverter); +} + +static id JSArrayConverter(JSContext *context, JSObject *array) +{ + jsuint i, count; + id *values = NULL; + jsval value = JSVAL_VOID; + id object = nil; + NSArray *result = nil; + + // Convert a JS array to an NSArray by calling JSValueToObject() on all its elements. + if (!JS_IsArrayObject(context, array)) return nil; + if (!JS_GetArrayLength(context, array, &count)) return nil; + + if (count == 0) return [NSArray array]; + + values = calloc(count, sizeof *values); + if (values == NULL) return nil; + + for (i = 0; i != count; ++i) + { + value = JSVAL_VOID; + if (!JS_GetElement(context, array, i, &value)) value = JSVAL_VOID; + + object = JSValueToObject(context, value); + if (object == nil) object = [NSNull null]; + values[i] = object; + } + + result = [NSArray arrayWithObjects:values count:count]; + free(values); + return result; +} + + +static id JSGenericObjectConverter(JSContext *context, JSObject *object) +{ + JSIdArray *ids; + jsint i; + NSMutableDictionary *result = nil; + jsval propKey = JSVAL_VOID, + value = JSVAL_VOID; + id objKey = nil; + id objValue = nil; + jsint intKey; + JSString *stringKey = NULL; + + /* Convert a JS Object to an NSDictionary by calling + JSValueToObject() on all its enumerable properties. This is desireable + because it allows objects declared with JavaScript property list + syntax to be converted to native property lists. + + This won't convert all objects, since JS has no concept of a class + heirarchy. Also, note that prototype properties are not included. + */ + + ids = JS_Enumerate(context, object); + if (ids == NULL) return nil; + + result = [NSMutableDictionary dictionaryWithCapacity:ids->length]; + for (i = 0; i != ids->length; ++i) + { + propKey = value = JSVAL_VOID; + objKey = nil; + + if (JS_IdToValue(context, ids->vector[i], &propKey)) + { + // Properties with string keys + if (JSVAL_IS_STRING(propKey)) + { + stringKey = JSVAL_TO_STRING(propKey); + if (JS_LookupProperty(context, object, JS_GetStringBytes(stringKey), &value)) + { + objKey = [NSString stringWithJavaScriptString:stringKey]; + } + } + + // Properties with int keys + else if (JSVAL_IS_INT(propKey)) + { + intKey = JSVAL_TO_INT(propKey); + if (JS_GetElement(context, object, intKey, &value)) + { + objKey = [NSNumber numberWithInt:intKey]; + } + } + } + + if (objKey != nil && value != JSVAL_VOID) + { + objValue = JSValueToObject(context, value); + if (objValue != nil) + { + [result setObject:objValue forKey:objKey]; + } + } + } + + JS_DestroyIdArray(context, ids); + return result; +} diff --git a/src/Core/Scripting/OOLegacyScriptWhitelist.h b/src/Core/Scripting/OOLegacyScriptWhitelist.h new file mode 100644 index 00000000..4a0a358a --- /dev/null +++ b/src/Core/Scripting/OOLegacyScriptWhitelist.h @@ -0,0 +1,137 @@ +/* + +OOLegacyScriptWhitelist.h + +Functions to apply method whitelist and basic tokenization to legacy scripts. + + +A sanitized script is an array of zero or more sanitized statements. + +A sanitized statement is an array whose first element is a boolean indicating +whether it's a conditional statement (true) or an action statement (false). + +A conditinal statement has three additional elements. The first is a sanitized +condition (see below). The second is a sanitized script to execute if the +condition evaluates to true. The third is a sanitized script to execute if the +condition evaluates to false. + +An action statement has one or two additional elements, both strings. The +first is a selector. If the selector ends with a colon (i.e., takes an +argument), the second is the argument. + + +A sanitized condition is an array of the form: + (opType, rawString, selector, comparisonType, operandArray). + +opType and comparisonType are NSNumbers containing OOOperationType and +OOComparisonType enumerators, respectively. + +rawString is the original textual representation of the condition for +display purposes. + +selector is a string, either a method selector or a mission/local +variable name. + +operandArray is an array of operands. Each operand is itself an array +of two items: a boolean indicating whether it's a method selector +(true) or a string (false), and a string. + +The special opType OP_FALSE doesn't require any other elements in the +array. All other valid opTypes require the array to have five elements. + + +A complete example: given the following script (the Cloaking Device mission +script from Oolite 1.65): + ( + { + conditions = ( + "galaxy_number equal 4", + "status_string equal STATUS_EXITING_WITCHSPACE", + "mission_cloak undefined" + ); + do = ( + { + conditions = ("mission_cloakcounter undefined"); + do = ("set: mission_cloakcounter 0"); + }, + "increment: mission_cloakcounter", + "checkForShips: asp-cloaked", + { + conditions = ("shipsFound_number equal 0", "mission_cloakcounter greaterthan 6"); + do = ("addShips: asp-cloaked 1", "addShips: asp-pirate 2"); + } + ); + } + ) +the sanitized form (with rawString values replaced with "..." for simplicity) is: + ( + ( + true, // This is a conditonal statement + ( // conditions + (OP_NUMBER, "...", "galaxy_number", COMPARISON_EQUAL, (false, "4")), + (OP_STRING, "...", "status_string", COMPARISON_EQUAL, (false, "STATUS_EXITING_WITCHSPACE")), + (OP_MISSION_VAR, "...", "mission_cloak", COMPARISON_UNDEFINED, ()) + ), + ( // do + ( + true, + ( (OP_MISSION_VAR, "...", "mission_cloakcounter", COMPARISON_UNDEFINED, ()) ), + ( (false, "set:", "mission_cloakcounter 0") ), + () + ), + (false, "increment:", "mission_cloakcounter"), + (false, "checkForShips:", "asp-cloaked"), + (true, + ( + (OP_NUMBER, "...", "shipsFound_number", COMPARISON_EQUAL, (false, "0")), + (OP_MISSION_VAR, "...", "mission_cloakcounter, COMPARISON_GREATERTHAN, (false, "6")), + ), + ( + (false, "addShips:", "asp-cloaked 1"), + (false, "addShips:", "asp-pirate 2"), + ), + () + ) + ), + () // else + ) + ) + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" + + +// context is used for error messages. +NSArray *OOSanitizeLegacyScript(NSArray *script, NSString *context, BOOL allowAIMethods); +NSArray *OOSanitizeLegacyScriptConditions(NSArray *conditions, NSString *context); + + +/* Quick test of whether a conditions array is sanitized. It is assumed that + this will only be passed fully-sanitized or fully-unsanitized conditions + arrays, so the test doesn't need to be exhaustive. + + Note that OOLegacyConditionsAreSanitized() is *not* called by + OOSanitizeLegacyScript(), so that it is not possible to sneak an + unwhitelisted "pre-compiled" condition past it. +*/ +BOOL OOLegacyConditionsAreSanitized(NSArray *conditions); diff --git a/src/Core/Scripting/OOLegacyScriptWhitelist.m b/src/Core/Scripting/OOLegacyScriptWhitelist.m new file mode 100644 index 00000000..3d29ce2c --- /dev/null +++ b/src/Core/Scripting/OOLegacyScriptWhitelist.m @@ -0,0 +1,471 @@ +/* + +OOLegacyScriptWhitelist.m + + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOLegacyScriptWhitelist.h" +#import "OOStringParsing.h" +#import "ResourceManager.h" +#import "OOCollectionExtractors.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "NSDictionaryOOExtensions.h" + + +static NSArray *SanitizeCondition(NSString *condition, NSString *context); +static NSArray *SanitizeConditionalStatement(NSDictionary *statement, NSString *context, BOOL allowAIMethods); +static NSArray *SanitizeActionStatement(NSString *statement, NSString *context, BOOL allowAIMethods); +static OOOperationType ClassifyLHSConditionSelector(NSString *selectorString, NSString **outSanitizedMethod, NSString *context); +static NSString *SanitizeQueryMethod(NSString *selectorString); // Checks aliases and whitelist, returns nil if whitelist fails. +static NSString *SanitizeActionMethod(NSString *selectorString, BOOL allowAIMethods); // Checks aliases and whitelist, returns nil if whitelist fails. +static NSArray *AlwaysFalseConditions(void); +static BOOL IsAlwaysFalseConditions(NSArray *conditions); + + +NSArray *OOSanitizeLegacyScript(NSArray *script, NSString *context, BOOL allowAIMethods) +{ + NSAutoreleasePool *pool = nil; + NSMutableArray *result = nil; + NSEnumerator *statementEnum = nil; + id statement = nil; + + pool = [[NSAutoreleasePool alloc] init]; + + result = [NSMutableArray arrayWithCapacity:[script count]]; + + for (statementEnum = [script objectEnumerator]; (statement = [statementEnum nextObject]); ) + { + if ([statement isKindOfClass:[NSDictionary class]]) + { + statement = SanitizeConditionalStatement(statement, context, allowAIMethods); + } + else if ([statement isKindOfClass:[NSString class]]) + { + statement = SanitizeActionStatement(statement, context, allowAIMethods); + } + else + { + OOLog(@"script.syntax.statement.invalidType", @"***** SCRIPT ERROR: in %@, statement is of invalid type - expected string or dictionary, got %@.", context, [statement class]); + statement = nil; + } + + if (statement != nil) + { + [result addObject:statement]; + } + } + + result = [result copy]; + [pool release]; + + return [result autorelease]; +} + + +NSArray *OOSanitizeLegacyScriptConditions(NSArray *conditions, NSString *context) +{ + NSEnumerator *conditionEnum = nil; + NSString *condition = nil; + NSMutableArray *result = nil; + NSArray *tokens = nil; + BOOL OK = YES; + + if (OOLegacyConditionsAreSanitized(conditions) || conditions == nil) return conditions; + if (context == nil) context = @""; + + result = [NSMutableArray arrayWithCapacity:[conditions count]]; + + for (conditionEnum = [conditions objectEnumerator]; (condition = [conditionEnum nextObject]); ) + { + if (![condition isKindOfClass:[NSString class]]) + { + OOLog(@"script.syntax.condition.notString", @"***** SCRIPT ERROR: in %@, bad condition - expected string, got %@; ignoring.", context, [condition class]); + OK = NO; + break; + } + + tokens = SanitizeCondition(condition, context); + if (tokens != nil) + { + [result addObject:tokens]; + } + else + { + OK = NO; + break; + } + } + + if (OK) return [[result copy] autorelease]; + else return AlwaysFalseConditions(); +} + + +BOOL OOLegacyConditionsAreSanitized(NSArray *conditions) +{ + if ([conditions count] == 0) return YES; // Empty array is safe. + return [[conditions objectAtIndex:0] isKindOfClass:[NSArray class]]; +} + + +static NSArray *SanitizeCondition(NSString *condition, NSString *context) +{ + NSArray *tokens = nil; + OOUInteger i, tokenCount; + OOOperationType opType; + NSString *selectorString = nil; + NSString *sanitizedSelectorString = nil; + NSString *comparatorString = nil; + OOComparisonType comparatorValue; + NSMutableArray *rhs = nil; + NSString *rhsItem = nil; + NSString *rhsSelector = nil; + NSArray *sanitizedRHSItem = nil; + NSString *stringSegment = nil; + + tokens = ScanTokensFromString(condition); + tokenCount = [tokens count]; + + if (tokenCount < 1) + { + OOLog(@"script.debug.syntax.scriptCondition.noneSpecified", @"***** SCRIPT ERROR: in %@, empty script condition.", context); + return NO; + } + + // Parse left-hand side. + selectorString = [tokens stringAtIndex:0]; + opType = ClassifyLHSConditionSelector(selectorString, &sanitizedSelectorString, context); + if (opType >= OP_INVALID) + { + OOLog(@"script.unpermittedMethod", @"***** SCRIPT ERROR: in %@, method '%@' not allowed.", context, selectorString); + return NO; + } + + // Parse operator. + if (tokenCount > 1) + { + comparatorString = [tokens stringAtIndex:1]; + if ([comparatorString isEqualToString:@"equal"]) comparatorValue = COMPARISON_EQUAL; + else if ([comparatorString isEqualToString:@"notequal"]) comparatorValue = COMPARISON_NOTEQUAL; + else if ([comparatorString isEqualToString:@"lessthan"]) comparatorValue = COMPARISON_LESSTHAN; + else if ([comparatorString isEqualToString:@"greaterthan"]) comparatorValue = COMPARISON_GREATERTHAN; + else if ([comparatorString isEqualToString:@"morethan"]) comparatorValue = COMPARISON_GREATERTHAN; + else if ([comparatorString isEqualToString:@"oneof"]) comparatorValue = COMPARISON_ONEOF; + else if ([comparatorString isEqualToString:@"undefined"]) comparatorValue = COMPARISON_UNDEFINED; + else + { + OOLog(@"script.debug.syntax.badComparison", @"***** SCRIPT ERROR: in %@, unknown comparison operator '%@', will return NO.", context, comparatorString); + return NO; + } + } + else + { + /* In the direct interpreter, having no operator resulted in an + implicit COMPARISON_NO operator, which always evaluated to false. + Returning NO here causes AlwaysFalseConditions() to be used, which + has the same effect. + */ + OOLog(@"script.debug.syntax.noOperator", @"----- WARNING: SCRIPT in %@ -- No operator in expression '%@', will always evaluate as false.", context, condition); + return NO; + } + + // Check for invalid opType/comparator combinations. + if (opType == OP_NUMBER && comparatorValue == COMPARISON_UNDEFINED) + { + OOLog(@"script.debug.syntax.invalidOperator", @"***** SCRIPT ERROR: in %@, comparison operator '%@' is not valid for %@.", context, @"undefined", @"numbers"); + return NO; + } + else if (opType == OP_BOOL) + { + switch (comparatorValue) + { + // Valid comparators + case COMPARISON_EQUAL: + case COMPARISON_NOTEQUAL: + break; + + default: + OOLog(@"script.debug.syntax.invalidOperator", @"***** SCRIPT ERROR: in %@, comparison operator '%@' is not valid for %@.", context, OOComparisonTypeToString(comparatorValue), @"booleans"); + return NO; + + } + } + + /* Parse right-hand side. Each token is converted to an array of the + token and a boolean indicating whether it's a selector. + + This also coalesces non-selector tokens, i.e. whitespace-separated + string segments. + */ + if (tokenCount > 2) + { + rhs = [NSMutableArray arrayWithCapacity:tokenCount - 2]; + for (i = 2; i < tokenCount; i++) + { + rhsItem = [tokens stringAtIndex:i]; + rhsSelector = SanitizeQueryMethod(rhsItem); + if (rhsSelector != nil) + { + // Method + if (stringSegment != nil) + { + // Add stringSegment as a literal token. + sanitizedRHSItem = [NSArray arrayWithObjects:[NSNumber numberWithBool:NO], stringSegment, nil]; + [rhs addObject:sanitizedRHSItem]; + stringSegment = nil; + } + + sanitizedRHSItem = [NSArray arrayWithObjects:[NSNumber numberWithBool:YES], rhsSelector, nil]; + [rhs addObject:sanitizedRHSItem]; + } + else + { + // String; append to stringSegment + if (stringSegment == nil) stringSegment = rhsItem; + else stringSegment = [NSString stringWithFormat:@"%@ %@", stringSegment, rhsItem]; + } + } + + if (stringSegment != nil) + { + sanitizedRHSItem = [NSArray arrayWithObjects:[NSNumber numberWithBool:NO], stringSegment, nil]; + [rhs addObject:sanitizedRHSItem]; + } + } + else + { + rhs = [NSArray array]; + } + + return [NSArray arrayWithObjects: + [NSNumber numberWithUnsignedInt:opType], + condition, + sanitizedSelectorString, + [NSNumber numberWithUnsignedInt:comparatorValue], + rhs, + nil]; +} + + +static NSArray *SanitizeConditionalStatement(NSDictionary *statement, NSString *context, BOOL allowAIMethods) +{ + NSArray *conditions = nil; + NSArray *doActions = nil; + NSArray *elseActions = nil; + + conditions = [statement arrayForKey:@"conditions"]; + if (conditions == nil) + { + OOLog(@"script.syntax.noConditions", @"***** SCRIPT ERROR: in %@, conditions array contains no \"conditions\" entry, ignoring.", context); + return nil; + } + + // Sanitize conditions. + conditions = OOSanitizeLegacyScriptConditions(conditions, context); + if (conditions == nil) + { + return nil; + } + + // Sanitize do and else. + if (!IsAlwaysFalseConditions(conditions)) doActions = [statement arrayForKey:@"do"]; + if (doActions != nil) doActions = OOSanitizeLegacyScript(doActions, context, allowAIMethods); + + elseActions = [statement arrayForKey:@"else"]; + if (elseActions != nil) elseActions = OOSanitizeLegacyScript(elseActions, context, allowAIMethods); + + // If neither does anything, the statment has no effect. + if ([doActions count] == 0 && [elseActions count] == 0) + { + return nil; + } + + if (doActions == nil) doActions = [NSArray array]; + if (elseActions == nil) elseActions = [NSArray array]; + + return [NSArray arrayWithObjects:[NSNumber numberWithBool:YES], conditions, doActions, elseActions, nil]; +} + + +static NSArray *SanitizeActionStatement(NSString *statement, NSString *context, BOOL allowAIMethods) +{ + NSMutableArray *tokens = nil; + OOUInteger tokenCount; + NSString *rawSelectorString = nil; + NSString *selectorString = nil; + NSString *argument = nil; + + tokens = ScanTokensFromString(statement); + tokenCount = [tokens count]; + if (tokenCount == 0) return nil; + + rawSelectorString = [tokens objectAtIndex:0]; + selectorString = SanitizeActionMethod(rawSelectorString, allowAIMethods); + if (selectorString == nil) + { + OOLog(@"script.unpermittedMethod", @"***** SCRIPT ERROR: in %@, method '%@' not allowed. In a future version of Oolite, this method will be removed from the handler. If you believe the handler should allow this method, please report it to bugs@oolite.org.", context, rawSelectorString); + + // return nil; + selectorString = rawSelectorString; + } + + if ([selectorString isEqualToString:@"doNothing"]) + { + return nil; + } + + if ([selectorString hasSuffix:@":"]) + { + // Expects an argument + if (tokenCount == 2) + { + argument = [tokens objectAtIndex:1]; + } + else + { + [tokens removeObjectAtIndex:0]; + argument = [tokens componentsJoinedByString:@" "]; + } + } + + return [NSArray arrayWithObjects:[NSNumber numberWithBool:NO], selectorString, argument, nil]; +} + + +static OOOperationType ClassifyLHSConditionSelector(NSString *selectorString, NSString **outSanitizedSelector, NSString *context) +{ + assert(outSanitizedSelector != NULL); + + *outSanitizedSelector = selectorString; + + // Allow arbitrary mission_foo or local_foo pseudo-selectors. + if ([selectorString hasPrefix:@"mission_"]) return OP_MISSION_VAR; + if ([selectorString hasPrefix:@"local_"]) return OP_LOCAL_VAR; + + // If it's a real method, check against whitelist. + *outSanitizedSelector = SanitizeQueryMethod(selectorString); + if (*outSanitizedSelector == nil) + { + OOLog(@"script.unpermittedMethod", @"***** SCRIPT ERROR: in %@, method '%@' not allowed. In a future version of Oolite, this method will be removed from the handler. If you believe the handler should allow this method, please report it to bugs@oolite.org.", context, selectorString); + + // return OP_INVALID; + *outSanitizedSelector = selectorString; + } + + // If it's a real method, and in the whitelist, classify by suffix. + if ([selectorString hasSuffix:@"_string"]) return OP_STRING; + if ([selectorString hasSuffix:@"_number"]) return OP_NUMBER; + if ([selectorString hasSuffix:@"_bool"]) return OP_BOOL; + + // If we got here, something's wrong. + OOLog(@"script.sanitize.unclassifiedSelector", @"***** ERROR: Whitelisted query method '%@' has no type suffix, treating as invalid.", selectorString); + return OP_INVALID; +} + + +static NSString *SanitizeQueryMethod(NSString *selectorString) +{ + static NSSet *whitelist = nil; + static NSDictionary *aliases = nil; + NSString *aliasedSelector = nil; + + if (whitelist == nil) + { + whitelist = [[NSSet alloc] initWithArray:[[ResourceManager whitelistDictionary] arrayForKey:@"query_methods"]]; + aliases = [[[ResourceManager whitelistDictionary] dictionaryForKey:@"query_method_aliases"] retain]; + } + + aliasedSelector = [aliases stringForKey:selectorString]; + if (aliasedSelector != nil) selectorString = aliasedSelector; + + if (![whitelist containsObject:selectorString]) selectorString = nil; + + return selectorString; +} + + +static NSString *SanitizeActionMethod(NSString *selectorString, BOOL allowAIMethods) +{ + static NSSet *whitelist = nil; + static NSSet *whitelistWithAI = nil; + static NSDictionary *aliases = nil; + static NSDictionary *aliasesWithAI = nil; + NSString *aliasedSelector = nil; + + if (whitelist == nil) + { + NSArray *actionMethods = nil; + NSArray *aiMethods = nil; + NSArray *aiAndActionMethods = nil; + + actionMethods = [[ResourceManager whitelistDictionary] arrayForKey:@"action_methods"]; + aiMethods = [[ResourceManager whitelistDictionary] arrayForKey:@"ai_methods"]; + aiAndActionMethods = [[ResourceManager whitelistDictionary] arrayForKey:@"ai_and_action_methods"]; + + if (actionMethods == nil) actionMethods = [NSArray array]; + if (aiMethods == nil) aiMethods = [NSArray array]; + + if (aiAndActionMethods != nil) actionMethods = [actionMethods arrayByAddingObjectsFromArray:aiAndActionMethods]; + + whitelist = [[NSSet alloc] initWithArray:actionMethods]; + whitelistWithAI = [[NSSet alloc] initWithArray:[aiMethods arrayByAddingObjectsFromArray:actionMethods]]; + + aliases = [[[ResourceManager whitelistDictionary] dictionaryForKey:@"action_method_aliases"] retain]; + + aliasesWithAI = [[ResourceManager whitelistDictionary] dictionaryForKey:@"ai_method_aliases"]; + if (aliasesWithAI != nil) + { + aliasesWithAI = [[aliasesWithAI dictionaryByAddingEntriesFromDictionary:aliases] copy]; + } + else + { + aliasesWithAI = [aliases copy]; + } + } + + aliasedSelector = [(allowAIMethods ? aliasesWithAI : aliases) stringForKey:selectorString]; + if (aliasedSelector != nil) selectorString = aliasedSelector; + + if (![(allowAIMethods ? whitelistWithAI : whitelist) containsObject:selectorString]) selectorString = nil; + + return selectorString; +} + + +// Return a conditions array that always evaluates as false. +static NSArray *AlwaysFalseConditions(void) +{ + static NSArray *alwaysFalse = nil; + if (alwaysFalse != nil) + { + alwaysFalse = [NSArray arrayWithObject:[NSArray arrayWithObject:[NSNumber numberWithUnsignedInt:OP_FALSE]]]; + [alwaysFalse retain]; + } + + return alwaysFalse; +} + + +static BOOL IsAlwaysFalseConditions(NSArray *conditions) +{ + return [[conditions arrayAtIndex:0] unsignedIntAtIndex:0] == OP_FALSE; +} diff --git a/src/Core/Scripting/OOPListScript.h b/src/Core/Scripting/OOPListScript.h new file mode 100644 index 00000000..e8c983b0 --- /dev/null +++ b/src/Core/Scripting/OOPListScript.h @@ -0,0 +1,44 @@ +/* + +OOPListScript.h + +Property list-based script. + +I started off reimplementing plist scripting here, in order to remove one of +PlayerEntity's many overloaded functions. The scale of the task was such that +I've stepped back, and this simply wraps the old plist scripting in +PlayerEntity. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOScript.h" + + +@interface OOPListScript: OOScript +{ + NSArray *_script; + NSDictionary *_metadata; +} + ++ (NSArray *)scriptsInPListFile:(NSString *)filePath; + +@end diff --git a/src/Core/Scripting/OOPListScript.m b/src/Core/Scripting/OOPListScript.m new file mode 100644 index 00000000..d16c403d --- /dev/null +++ b/src/Core/Scripting/OOPListScript.m @@ -0,0 +1,172 @@ +/* + +OOPListScript.h + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOPListScript.h" +#import "OOPListParsing.h" +#import "PlayerEntityLegacyScriptEngine.h" +#import "OOLegacyScriptWhitelist.h" + + +static NSString * const kMDKeyName = @"name"; +static NSString * const kMDKeyDescription = @"description"; +static NSString * const kMDKeyVersion = @"version"; +static NSString * const kKeyMetadata = @"!metadata!"; + + +@interface OOPListScript (SetUp) + ++ (NSArray *)scriptsFromDictionaryOfScripts:(NSDictionary *)dictionary; +- (id)initWithName:(NSString *)name scriptArray:(NSArray *)script metadata:(NSDictionary *)metadata; + +@end + + +@implementation OOPListScript + ++ (NSArray *)scriptsInPListFile:(NSString *)filePath +{ + NSDictionary *dict = nil; + + dict = OODictionaryFromFile(filePath); + return [self scriptsFromDictionaryOfScripts:dict]; +} + + +- (void)dealloc +{ + [_script release]; + [_metadata release]; + + [super dealloc]; +} + + +- (NSString *)name +{ + return [_metadata objectForKey:kMDKeyName]; +} + + +- (NSString *)scriptDescription +{ + return [_metadata objectForKey:kMDKeyDescription]; +} + + +- (NSString *)version +{ + return [_metadata objectForKey:kMDKeyVersion]; +} + + +- (void)runWithTarget:(Entity *)target +{ + if (target != nil && ![target isKindOfClass:[ShipEntity class]]) + { + OOLog(@"script.plist.run.badTarget", @"Expected ShipEntity or nil for target, got %@.", [target class]); + return; + } + + OOLog(@"script.trace.plist.run", @"Running script %@", [self displayName]); + OOLogIndentIf(@"script.trace.plist.run"); + + [[PlayerEntity sharedPlayer] runScriptActions:_script + withContextName:[self name] + forTarget:(ShipEntity *)target]; + + OOLogOutdentIf(@"script.trace.plist.run"); +} + + +- (BOOL)doEvent:(NSString *)eventName withArguments:(NSArray *)argument +{ + // PList scripts don't have event handlers. + return NO; +} + +@end + + +@implementation OOPListScript (SetUp) + ++ (NSArray *)scriptsFromDictionaryOfScripts:(NSDictionary *)dictionary +{ + NSMutableArray *result = nil; + NSEnumerator *keyEnum = nil; + NSString *key = nil; + NSArray *scriptArray = nil; + NSDictionary *metadata = nil; + OOPListScript *script = nil; + + result = [NSMutableArray arrayWithCapacity:[dictionary count]]; + metadata = [dictionary objectForKey:kKeyMetadata]; + if (![metadata isKindOfClass:[NSDictionary class]]) metadata = nil; + + for (keyEnum = [dictionary keyEnumerator]; (key = [keyEnum nextObject]); ) + { + scriptArray = [dictionary objectForKey:key]; + if ([key isKindOfClass:[NSString class]] && + [scriptArray isKindOfClass:[NSArray class]] && + ![key isEqual:kKeyMetadata]) + { + scriptArray = OOSanitizeLegacyScript(scriptArray, key, NO); + if (scriptArray != nil) + { + script = [[self alloc] initWithName:key scriptArray:scriptArray metadata:metadata]; + if (script != nil) + { + [result addObject:script]; + [script release]; + } + } + } + } + + return result; +} + + +- (id)initWithName:(NSString *)name scriptArray:(NSArray *)script metadata:(NSDictionary *)metadata +{ + self = [super init]; + if (self != nil) + { + _script = [script retain]; + if (name != nil) + { + if (metadata == nil) metadata = [NSDictionary dictionaryWithObject:name forKey:kMDKeyName]; + else + { + NSMutableDictionary *mutableMetadata = [[metadata mutableCopy] autorelease]; + [mutableMetadata setObject:name forKey:kMDKeyName]; + metadata = mutableMetadata; + } + } + _metadata = [metadata copy]; + } + + return self; +} + +@end diff --git a/src/Core/Scripting/OOScript.h b/src/Core/Scripting/OOScript.h new file mode 100644 index 00000000..d57ba23f --- /dev/null +++ b/src/Core/Scripting/OOScript.h @@ -0,0 +1,76 @@ +/* + +OOScript.h + +Abstract base class for scripts. +Currently, Oolite supports two types of script: the original property list +scripts and JavaScript scripts. OOS, a format that translated into plist +scripts, was supported until 1.69.1, but never used. OOScript unifies the +interfaces to the script types and abstracts loading. Additionally, it falls +back to a more "primitive" script if loading of one type fails; specifically, +the order of precedence is: + script.js (JavaScript) +// script.oos (OOS) + script.plist (property list) + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import + +@class Entity; + + +@interface OOScript: NSObject + +/* Looks for path/world-scripts.plist, path/script.js, then path/script.plist. + May return zero or more scripts. +*/ ++ (NSArray *)worldScriptsAtPath:(NSString *)path; + +// Load named scripts from Scripts folders. ++ (NSArray *)scriptsFromFileNamed:(NSString *)fileName; ++ (NSArray *)scriptsFromList:(NSArray *)fileNames; + ++ (NSArray *)scriptsFromFileAtPath:(NSString *)filePath; + +// Load a single JavaScript script. Or, y'know, a future-scripting-language script. ++ (id)nonLegacyScriptFromFileNamed:(NSString *)fileName properties:(NSDictionary *)properties; + +- (void)resetState; // Clear local variables, for instance. + +- (NSString *)name; +- (NSString *)scriptDescription; +- (NSString *)version; +- (NSString *)displayName; // '"name" version' if version is defined, otherwise just '"name"'. + +- (void)runWithTarget:(Entity *)target; + +- (BOOL)doEvent:(NSString *)eventName; +- (BOOL)doEvent:(NSString *)eventName withArguments:(NSArray *)arguments; +- (BOOL)doEvent:(NSString *)eventName withArgument:(id)argument; + +- (id)propertyNamed:(NSString *)name; +// Set a property which can be modified or deleted by the script. +- (BOOL)setProperty:(id)value named:(NSString *)name; +// Set a special property which cannot be modified or deleted by the script. +- (BOOL)defineProperty:(id)value named:(NSString *)name; + +@end diff --git a/src/Core/Scripting/OOScript.m b/src/Core/Scripting/OOScript.m new file mode 100644 index 00000000..d917bda6 --- /dev/null +++ b/src/Core/Scripting/OOScript.m @@ -0,0 +1,300 @@ +/* + +OOScript.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOScript.h" +#import "OOJSScript.h" +#import "OOPListScript.h" +#import "OOLogging.h" +#import "Universe.h" +#import "OOJavaScriptEngine.h" +#import "OOPListParsing.h" +#import "ResourceManager.h" + + +static NSString * const kOOLogScriptSubclassResponsibility = @"general.error.subclassResponsibility.OOScript"; +static NSString * const kOOLogLoadScriptJavaScript = @"script.load.javaScript"; +static NSString * const kOOLogLoadScriptPList = @"script.load.pList"; +static NSString * const kOOLogLoadScriptOK = @"script.load.parseOK"; +static NSString * const kOOLogLoadScriptParseError = @"script.load.parseError"; +static NSString * const kOOLogLoadScriptNone = @"script.load.none"; + + +@implementation OOScript + ++ (NSArray *)worldScriptsAtPath:(NSString *)path +{ + NSFileManager *fmgr = nil; + NSString *filePath = nil; + NSArray *names = nil; + NSArray *result = nil; + id script = nil; + BOOL foundScript = NO; + + fmgr = [NSFileManager defaultManager]; + + // First, look for world-scripts.plist. + filePath = [path stringByAppendingPathComponent:@"world-scripts.plist"]; + if (filePath != nil) + { + names = OOArrayFromFile(filePath); + if (names != nil) + { + foundScript = YES; + result = [self scriptsFromList:names]; + } + } + + // Second, try to load a JavaScript. + if (result == nil) + { + filePath = [path stringByAppendingPathComponent:@"script.js"]; + if ([fmgr fileExistsAtPath:filePath]) foundScript = YES; + else + { + filePath = [path stringByAppendingPathComponent:@"script.es"]; + if ([fmgr fileExistsAtPath:filePath]) foundScript = YES; + } + if (foundScript) + { + OOLog(kOOLogLoadScriptJavaScript, @"Trying to load JavaScript script %@", filePath); + OOLogIndentIf(kOOLogLoadScriptJavaScript); + + script = [OOJSScript scriptWithPath:filePath properties:nil]; + if (script != nil) + { + result = [NSArray arrayWithObject:script]; + OOLog(kOOLogLoadScriptOK, @"Successfully loaded JavaScript script %@", filePath); + } + else OOLog(kOOLogLoadScriptParseError, @"*** Failed to load JavaScript script %@", filePath); + + OOLogOutdentIf(kOOLogLoadScriptJavaScript); + } + } + + // Third, try to load an plist script. + if (result == nil) + { + filePath = [path stringByAppendingPathComponent:@"script.plist"]; + if ([fmgr fileExistsAtPath:filePath]) + { + foundScript = YES; + OOLog(kOOLogLoadScriptPList, @"Trying to load property list script %@", filePath); + OOLogIndentIf(kOOLogLoadScriptPList); + + result = [OOPListScript scriptsInPListFile:filePath]; + if (result != nil) OOLog(kOOLogLoadScriptOK, @"Successfully loaded property list script %@", filePath); + else OOLog(kOOLogLoadScriptParseError, @"*** Failed to load property list script %@", filePath); + + OOLogOutdentIf(kOOLogLoadScriptPList); + } + } + + if (result == nil && foundScript) + { + OOLog(kOOLogLoadScriptNone, @"No script could be loaded from %@", path); + } + + return result; +} + + ++ (NSArray *)scriptsFromFileNamed:(NSString *)fileName +{ + NSEnumerator *pathEnum = nil; + NSString *path = nil; + NSString *filePath = nil; + NSArray *result = nil; + + if (fileName == nil) return nil; + + for (pathEnum = [[ResourceManager paths] objectEnumerator]; (path = [pathEnum nextObject]); ) + { + filePath = [[path stringByAppendingPathComponent:@"Scripts"] stringByAppendingPathComponent:fileName]; + result = [self scriptsFromFileAtPath:filePath]; + if (result != nil) return result; + } + + OOLog(@"script.load.notFound", @"***** Could not find a valid script file named %@.", fileName); + return nil; +} + + ++ (NSArray *)scriptsFromList:(NSArray *)fileNames +{ + NSEnumerator *nameEnum = nil; + NSString *name = nil; + NSMutableArray *result = nil; + NSArray *scripts = nil; + + result = [NSMutableArray arrayWithCapacity:[fileNames count]]; + + for (nameEnum = [fileNames objectEnumerator]; (name = [nameEnum nextObject]); ) + { + scripts = [self scriptsFromFileNamed:name]; + if (scripts != nil) [result addObjectsFromArray:scripts]; + } + + return result; +} + + ++ (NSArray *)scriptsFromFileAtPath:(NSString *)filePath +{ + BOOL directory; + if (![[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&directory] || directory) return nil; + + NSString *extension = [[filePath pathExtension] lowercaseString]; + + if ([extension isEqualToString:@"js"] || [extension isEqualToString:@"es"]) + { + NSArray *result = nil; + OOScript *script = [OOJSScript scriptWithPath:filePath properties:nil]; + if (script != nil) result = [NSArray arrayWithObject:script]; + return result; + } + else if ([extension isEqualToString:@"plist"]) + { + return [OOPListScript scriptsInPListFile:filePath]; + } + + OOLog(@"script.load.badName", @"***** Don't know how to load a script from %@.", filePath); + return nil; +} + + ++ (id)nonLegacyScriptFromFileNamed:(NSString *)fileName properties:(NSDictionary *)properties +{ + NSString *extension = nil; + NSString *path = nil; + + if (fileName == nil) return nil; + + extension = [[fileName pathExtension] lowercaseString]; + if ([extension isEqualToString:@"js"] || [extension isEqualToString:@"es"]) + { + path = [ResourceManager pathForFileNamed:fileName inFolder:@"Scripts"]; + if (path == nil) + { + OOLog(@"script.load.notFound", @"***** Could not find a script file named %@.", fileName); + return nil; + } + return [OOJSScript scriptWithPath:path properties:properties]; + } + else if ([extension isEqualToString:@"plist"]) + { + OOLog(@"script.load.badName", @"***** Can't load script named %@ - legacy scripts are not supported in this context.", fileName); + return nil; + } + + OOLog(@"script.load.badName", @"***** Don't know how to load a script from %@.", fileName); + return nil; +} + + +- (NSString *)descriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\" version %@", [self name], [self version]]; +} + + +- (void)resetState +{ + +} + + +- (NSString *)name +{ + OOLog(kOOLogScriptSubclassResponsibility, @"OOScript should not be used directly!"); + return nil; +} + + +- (NSString *)scriptDescription +{ + OOLog(kOOLogScriptSubclassResponsibility, @"OOScript should not be used directly!"); + return nil; +} + + +- (NSString *)version +{ + OOLog(kOOLogScriptSubclassResponsibility, @"OOScript should not be used directly!"); + return nil; +} + + +- (NSString *)displayName +{ + NSString *name = [self name]; + NSString *version = [self version]; + + if (version != NULL) return [NSString stringWithFormat:@"\"%@\" %@", name, version]; + else return [NSString stringWithFormat:@"\"%@\"", name]; +} + + +- (void)runWithTarget:(Entity *)target +{ + OOLog(kOOLogScriptSubclassResponsibility, @"OOScript should not be used directly!"); +} + + +- (BOOL)doEvent:(NSString *)eventName +{ + return [self doEvent:eventName withArguments:nil]; +} + + +- (BOOL)doEvent:(NSString *)eventName withArguments:(NSArray *)arguments +{ + OOLog(kOOLogScriptSubclassResponsibility, @"OOScript should not be used directly!"); + return NO; +} + + +- (BOOL)doEvent:(NSString *)eventName withArgument:(id)argument +{ + return [self doEvent:eventName withArguments:[NSArray arrayWithObject:argument]]; +} + + +- (id)propertyNamed:(NSString *)name +{ + return nil; +} + + +- (BOOL)setProperty:(id)value named:(NSString *)name +{ + return NO; +} + + +- (BOOL)defineProperty:(id)value named:(NSString *)name +{ + return NO; +} + +@end diff --git a/src/Core/Scripting/OOScriptTimer.h b/src/Core/Scripting/OOScriptTimer.h new file mode 100644 index 00000000..b8d55ff7 --- /dev/null +++ b/src/Core/Scripting/OOScriptTimer.h @@ -0,0 +1,75 @@ +/* + +OOScriptTimer.h + +Abstract base class for script timers. An OOScriptTimer does nothing when it +fires; subclasses should override the -timerFired method. + +Timers are immutable. They are retained by the timer subsystem while scheduled. +A timer with a negative interval will only fire once. A negative nexttime when +inited will cause the timer to fire after the specified interval. A persistent +timer will remain if the player dies and respawns; non-persistent timers will +be removed. + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOTypes.h" + + +@interface OOScriptTimer: NSObject +{ +@private + OOTimeAbsolute _nextTime; + OOTimeDelta _interval; + BOOL _isScheduled; +} + +- (id) initWithNextTime:(OOTimeAbsolute)nextTime + interval:(OOTimeDelta)interval; + +// Sets nextTime to current time + delay. +- (id) initOneShotTimerWithDelay:(OOTimeDelta)delay; + +- (OOTimeAbsolute)nextTime; +- (BOOL)setNextTime:(OOTimeAbsolute)nextTime; // Only works when timer is not scheduled. +- (OOTimeDelta)interval; +- (void)setInterval:(OOTimeDelta)interval; + +// Subclass responsibility: +- (void) timerFired; +- (BOOL) isPersistent; // Default: NO. + +- (BOOL) scheduleTimer; +- (void) unscheduleTimer; +- (BOOL) isScheduled; + + ++ (void) updateTimers; ++ (void) noteGameReset; + + +- (BOOL) isValidForScheduling; + +- (NSComparisonResult) compareByNextFireTime:(OOScriptTimer *)other; + +@end diff --git a/src/Core/Scripting/OOScriptTimer.m b/src/Core/Scripting/OOScriptTimer.m new file mode 100644 index 00000000..ae142c96 --- /dev/null +++ b/src/Core/Scripting/OOScriptTimer.m @@ -0,0 +1,222 @@ +/* + +OOScriptTimer.m + + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOScriptTimer.h" +#import "Universe.h" +#import "OOLogging.h" +#import "OOPriorityQueue.h" + + +static OOPriorityQueue *sTimers = nil; + + +@implementation OOScriptTimer + +- (id) initWithNextTime:(OOTimeAbsolute)nextTime + interval:(OOTimeDelta)interval +{ + OOTimeAbsolute now; + + self = [super init]; + if (self != nil) + { + if (interval <= 0.0) interval = -1.0; + + now = [UNIVERSE getTime]; + if (nextTime < 0.0) nextTime = now + interval; + if (nextTime < now) + { + // Negative or old nextTime and negative interval = meaningless. + [self release]; + self = nil; + } + + _nextTime = nextTime; + _interval = interval; + } + + return self; +} + + // Sets nextTime to current time + delay. +- (id) initOneShotTimerWithDelay:(OOTimeDelta)delay +{ + return [self initWithNextTime:[UNIVERSE getTime] + delay interval:-1.0]; +} + + +- (void) dealloc +{ + if (_isScheduled) [self unscheduleTimer]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + NSString *intervalDesc = nil; + + if (_interval <= 0.0) intervalDesc = @"one-shot"; + else intervalDesc = [NSString stringWithFormat:@"interval: %g", _interval]; + + return [NSString stringWithFormat:@"nextTime: %g, %@, %srunning", _nextTime, intervalDesc, _isScheduled ? "" : "not "]; +} + + +- (OOTimeAbsolute)nextTime +{ + return _nextTime; +} + + +- (BOOL)setNextTime:(OOTimeAbsolute)nextTime +{ + if (_isScheduled) return NO; + + _nextTime = nextTime; + return YES; +} + + +- (OOTimeDelta)interval +{ + return _interval; +} + + +- (void)setInterval:(OOTimeDelta)interval +{ + if (interval <= 0.0) interval = -1.0; + _interval = interval; +} + + +- (void) timerFired +{ + OOLogGenericSubclassResponsibility(); +} + + +- (BOOL) isPersistent +{ + return NO; +} + + +- (BOOL) scheduleTimer +{ + if (_isScheduled) return YES; + if (![self isValidForScheduling]) return NO; + + if (sTimers == nil) sTimers = [[OOPriorityQueue alloc] initWithComparator:@selector(compareByNextFireTime:)]; + [sTimers addObject:self]; + _isScheduled = YES; + return YES; +} + + +- (void) unscheduleTimer +{ + [sTimers removeExactObject:self]; + _isScheduled = NO; +} + + +- (BOOL) isScheduled +{ + return _isScheduled; +} + + ++ (void) updateTimers +{ + OOScriptTimer *timer = nil; + OOTimeAbsolute now; + + now = [UNIVERSE getTime]; + for (;;) + { + timer = [sTimers peekAtNextObject]; + if (timer == nil || now < [timer nextTime]) break; + + [sTimers removeNextObject]; + timer->_isScheduled = NO; + [timer scheduleTimer]; // Must reschedule before firing so that unscheduling works. + + [timer timerFired]; + } +} + + ++ (void) noteGameReset +{ + NSArray *timers = nil; + NSEnumerator *timerEnum = nil; + OOScriptTimer *timer = nil; + + // Intermediate array is required so we don't get stuck in an endless loop over reinserted timers + timers = [sTimers sortedObjects]; + for (timerEnum = [timers objectEnumerator]; (timer = [timerEnum nextObject]); ) + { + timer->_isScheduled = NO; + if ([timer isPersistent]) [timer scheduleTimer]; + } +} + + +- (BOOL) isValidForScheduling +{ + OOTimeAbsolute now; + double scaled; + + now = [UNIVERSE getTime]; + if (_nextTime <= now) + { + if (_interval <= 0.0) return NO; // One-shot timer which has expired + + // Move _nextTime to the closest future time that's a multiple of _interval + scaled = (now - _nextTime) / _interval; + scaled = ceil(scaled); + _nextTime += scaled * _interval; + if (_nextTime <= now) _nextTime += _interval; // Should only happen if _nextTime is exactly equal to now after previous stuff + } + + return YES; +} + +- (NSComparisonResult) compareByNextFireTime:(OOScriptTimer *)other +{ + OOTimeAbsolute otherTime; + + if (other != nil) otherTime = [other nextTime]; + else otherTime = -INFINITY; + + if (_nextTime < otherTime) return NSOrderedAscending; + else if (_nextTime < otherTime) return NSOrderedDescending; + else return NSOrderedSame; +} + +@end diff --git a/src/Core/TextureStore.h b/src/Core/TextureStore.h new file mode 100644 index 00000000..fce56b15 --- /dev/null +++ b/src/Core/TextureStore.h @@ -0,0 +1,70 @@ +/* + +TextureStore.m + +Singleton responsible for loading, binding and caching textures. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#import "OOOpenGL.h" + +#define OOLITE_EXCEPTION_TEXTURE_NOT_FOUND @"OoliteTextureNotFoundException" +#define OOLITE_EXCEPTION_TEXTURE_NOT_UNDERSTOOD @"OoliteTextureNotUnderstoodException" +#define OOLITE_EXCEPTION_FATAL @"OoliteFatalException" + + +@class OOColor; + +@interface TextureStore : NSObject +{ +} + ++ (GLuint) maxTextureDimension; + ++ (GLuint) getTextureNameFor:(NSString *)filename; ++ (GLuint) getImageNameFor:(NSString *)filename; ++ (GLuint) getTextureNameFor:(NSString *)filename inFolder:(NSString*) foldername; ++ (NSString*) getNameOfTextureWithGLuint:(GLuint) value; ++ (NSSize) getSizeOfTexture:(NSString *)filename; + + +// routines to create textures... + ++ (GLuint) getPlanetTextureNameFor:(NSDictionary*)planetInfo intoData:(unsigned char **)textureData; ++ (GLuint) getPlanetNormalMapNameFor:(NSDictionary*)planetInfo intoData:(unsigned char **)textureData; ++ (GLuint) getCloudTextureNameFor:(OOColor*) color: (GLfloat) impress: (GLfloat) bias intoData:(unsigned char **)textureData; + +void fillRanNoiseBuffer(); + +void fillSquareImageDataWithBlur(unsigned char * imageBuffer, int width, int nplanes); + +void addNoise(float * buffer, int p, int n, float scale); +void fillSquareImageDataWithSmoothNoise(unsigned char * imageBuffer, int width, int nplanes); +void fillSquareImageDataWithCloudTexture(unsigned char * imageBuffer, int width, int nplanes, OOColor* cloudcolor, float impress, float bias); +void fillSquareImageWithPlanetTex(unsigned char * imageBuffer, int width, int nplanes, float impress, float bias, + OOColor* seaColor, + OOColor* paleSeaColor, + OOColor* landColor, + OOColor* paleLandColor); +void fillSquareImageWithPlanetNMap(unsigned char * imageBuffer, int width, int nplanes, float impress, float bias, float factor); + +@end diff --git a/src/Core/TextureStore.m b/src/Core/TextureStore.m new file mode 100644 index 00000000..1e140f06 --- /dev/null +++ b/src/Core/TextureStore.m @@ -0,0 +1,549 @@ +/* + +TextureStore.m + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" + +#import "ResourceManager.h" +#import "legacy_random.h" + +#import "TextureStore.h" +#import "OOColor.h" +#import "OOMaths.h" +#import "OOTextureScaling.h" +#import "OOStringParsing.h" +#import "OOTexture.h" + +#define kOOLogUnconvertedNSLog @"unclassified.TextureStore" + + +static NSString * const kOOLogPlanetTextureGen = @"texture.planet.generate"; + + +@implementation TextureStore + +NSMutableDictionary *textureUniversalDictionary = nil; +NSMutableDictionary *shaderUniversalDictionary = nil; + +BOOL done_maxsize_test = NO; +GLuint max_texture_dimension = 512; // conservative start ++ (GLuint) maxTextureDimension +{ + if (done_maxsize_test) + return max_texture_dimension; + GLint result; + glGetIntegerv( GL_MAX_TEXTURE_SIZE, &result); + max_texture_dimension = result; + done_maxsize_test = YES; + return max_texture_dimension; +} + + ++ (GLuint) getTextureNameFor:(NSString *)filename +{ + if ([textureUniversalDictionary objectForKey:filename]) + return [[(NSDictionary *)[textureUniversalDictionary objectForKey:filename] objectForKey:@"texName"] intValue]; + return [TextureStore getTextureNameFor: filename inFolder: @"Textures"]; +} + ++ (GLuint) getImageNameFor:(NSString *)filename +{ + if ([textureUniversalDictionary objectForKey:filename]) + return [[(NSDictionary *)[textureUniversalDictionary objectForKey:filename] objectForKey:@"texName"] intValue]; + return [TextureStore getTextureNameFor: filename inFolder: @"Images"]; +} + + ++ (GLuint) getTextureNameFor:(NSString *)fileName inFolder:(NSString*)folderName +{ + OOTexture *texture = nil; + NSDictionary *texProps = nil; + GLint texName; + NSSize dimensions; + NSNumber *texNameObj = nil; + + texture = [OOTexture textureWithName:fileName inFolder:folderName]; + texName = [texture glTextureName]; + if (texName != 0) + { + dimensions = [texture dimensions]; + texNameObj = [NSNumber numberWithInt:texName]; + + texProps = [NSDictionary dictionaryWithObjectsAndKeys: + texNameObj, @"texName", + [NSNumber numberWithInt:dimensions.width], @"width", + [NSNumber numberWithInt:dimensions.height], @"height", + texture, @"OOTexture", + nil]; + + if (textureUniversalDictionary == nil) textureUniversalDictionary = [[NSMutableDictionary alloc] init]; + + [textureUniversalDictionary setObject:texProps forKey:fileName]; + [textureUniversalDictionary setObject:fileName forKey:texNameObj]; + } + return texName; +} + + ++ (NSString*) getNameOfTextureWithGLuint:(GLuint) value +{ + return (NSString*)[textureUniversalDictionary objectForKey:[NSNumber numberWithInt:value]]; +} + ++ (NSSize) getSizeOfTexture:(NSString *)filename +{ + NSSize size = NSMakeSize(0.0, 0.0); // zero size + if ([textureUniversalDictionary objectForKey:filename]) + { + size.width = [[(NSDictionary *)[textureUniversalDictionary objectForKey:filename] objectForKey:@"width"] intValue]; + size.height = [[(NSDictionary *)[textureUniversalDictionary objectForKey:filename] objectForKey:@"height"] intValue]; + } + return size; +} + + ++ (GLuint) getPlanetTextureNameFor:(NSDictionary*)planetInfo intoData:(unsigned char **)textureData +{ + GLuint texName; + + int texsize = 512; + + unsigned char *texBytes; + + int texture_h = texsize; + int texture_w = texsize; + + int tex_bytes = texture_w * texture_h * 4; + + unsigned char* imageBuffer = malloc( tex_bytes); + if (imageBuffer) + (*textureData) = imageBuffer; + + float land_fraction = [[planetInfo objectForKey:@"land_fraction"] floatValue]; + float sea_bias = land_fraction - 1.0; + + OOLog(kOOLogPlanetTextureGen, @"genning texture for land_fraction %.5f", land_fraction); + + OOColor* land_color = (OOColor*)[planetInfo objectForKey:@"land_color"]; + OOColor* sea_color = (OOColor*)[planetInfo objectForKey:@"sea_color"]; + OOColor* polar_land_color = (OOColor*)[planetInfo objectForKey:@"polar_land_color"]; + OOColor* polar_sea_color = (OOColor*)[planetInfo objectForKey:@"polar_sea_color"]; + + fillSquareImageWithPlanetTex( imageBuffer, texture_w, 4, 1.0, sea_bias, + sea_color, + polar_sea_color, + land_color, + polar_land_color); + + texBytes = imageBuffer; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + texName = GLAllocateTextureName(); + glBindTexture(GL_TEXTURE_2D, texName); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // adjust this + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // adjust this + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture_w, texture_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, texBytes); + + return texName; +} + ++ (GLuint) getPlanetNormalMapNameFor:(NSDictionary*)planetInfo intoData:(unsigned char **)textureData +{ + GLuint texName; + + int texsize = 512; + + unsigned char *texBytes; + + int texture_h = texsize; + int texture_w = texsize; + + int tex_bytes = texture_w * texture_h * 4; + + unsigned char* imageBuffer = malloc( tex_bytes); + if (imageBuffer) + (*textureData) = imageBuffer; + + float land_fraction = [[planetInfo objectForKey:@"land_fraction"] floatValue]; + float sea_bias = land_fraction - 1.0; + + OOLog(@"textureStore.genNormalMap", @"genning normal map for land_fraction %.5f", land_fraction); + +// fillRanNoiseBuffer(); + fillSquareImageWithPlanetNMap( imageBuffer, texture_w, 4, 1.0, sea_bias, 24.0); + + texBytes = imageBuffer; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + texName = GLAllocateTextureName(); + glBindTexture(GL_TEXTURE_2D, texName); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // adjust this + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // adjust this + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture_w, texture_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, texBytes); + + return texName; +} + ++ (GLuint) getCloudTextureNameFor:(OOColor*) color: (GLfloat) impress: (GLfloat) bias intoData:(unsigned char **)textureData +{ + GLuint texName; + + unsigned char *texBytes; + + int texture_h = 512; + int texture_w = 512; + int tex_bytes; + + tex_bytes = texture_w * texture_h * 4; + + unsigned char* imageBuffer = malloc( tex_bytes); + if (imageBuffer) + (*textureData) = imageBuffer; + +// fillRanNoiseBuffer(); + fillSquareImageDataWithCloudTexture( imageBuffer, texture_w, 4, color, impress, bias); + + texBytes = imageBuffer; + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + texName = GLAllocateTextureName(); + glBindTexture(GL_TEXTURE_2D, texName); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // adjust this + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // adjust this + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, texture_w, texture_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, texBytes); + + return texName; +} + +void fillSquareImageDataWithBlur(unsigned char * imageBuffer, int width, int nplanes) +{ + OOLog(@"texture.generatingBlur", @"Genrating blur - %u pixels wide, %u planes.", width, nplanes); + + int x, y; + int r = width / 2; + float r1 = 1.0 / r; + float i_error = 0; + for (y = 0; y < r; y++) for (x = 0; x < r; x++) + { + int x1 = r - x - 1; + int x2 = r + x; + int y1 = r - y - 1; + int y2 = r + y; + float d = sqrt(x*x + y*y); + if (d > r) + d = r; + float fi = 255.0 - 255.0 * d * r1; + unsigned char i = (unsigned char)fi; + + i_error += fi - i; // accumulate the error between i and fi + + if ((i_error > 1.0)&&(i < 255)) + { + i_error -= 1.0; + i++; + } + + int p; + for (p = 0; p < nplanes - 1; p++) + { + imageBuffer[ p + nplanes * (y1 * width + x1) ] = 128 | (ranrot_rand() & 127); + imageBuffer[ p + nplanes * (y1 * width + x2) ] = 128 | (ranrot_rand() & 127); + imageBuffer[ p + nplanes * (y2 * width + x1) ] = 128 | (ranrot_rand() & 127); + imageBuffer[ p + nplanes * (y2 * width + x2) ] = 128 | (ranrot_rand() & 127); + } + imageBuffer[ p + nplanes * (y1 * width + x1) ] = i; // hoping RGBA last plane is alpha + imageBuffer[ p + nplanes * (y1 * width + x2) ] = i; + imageBuffer[ p + nplanes * (y2 * width + x1) ] = i; + imageBuffer[ p + nplanes * (y2 * width + x2) ] = i; + } +} + +float ranNoiseBuffer[ 128 * 128]; +void fillRanNoiseBuffer() +{ + int i; + for (i = 0; i < 16384; i++) + ranNoiseBuffer[i] = randf(); +} + +float my_lerp( float v0, float v1, float q) +{ + //float q1 = 0.5 * (1.0 + cosf((q + 1.0) * M_PI)); + //return v0 * (1.0 - q1) + v1 * q1; + return (v0 + q * (v1 - v0)); +} + +void addNoise(float * buffer, int p, int n, float scale) +{ + int x, y; + + float r = (float)p / (float)n; + for (y = 0; y < p; y++) for (x = 0; x < p; x++) + { + int ix = floor( (float)x / r); + int jx = (ix + 1) % n; + int iy = floor( (float)y / r); + int jy = (iy + 1) % n; + float qx = x / r - ix; + float qy = y / r - iy; + ix &= 127; + iy &= 127; + jx &= 127; + jy &= 127; + float rix = my_lerp( ranNoiseBuffer[iy * 128 + ix], ranNoiseBuffer[iy * 128 + jx], qx); + float rjx = my_lerp( ranNoiseBuffer[jy * 128 + ix], ranNoiseBuffer[jy * 128 + jx], qx); + float rfinal = scale * my_lerp( rix, rjx, qy); + + buffer[ y * p + x ] += rfinal; + } +} + +void fillSquareImageDataWithSmoothNoise(unsigned char * imageBuffer, int width, int nplanes) +{ + float accbuffer[width * width]; + int x, y; + for (y = 0; y < width; y++) for (x = 0; x < width; x++) accbuffer[ y * width + x] = 0.0f; + + int octave = 4; + float scale = 0.5; + while (octave < width) + { + addNoise( accbuffer, width, octave, scale); + octave *= 2; + scale *= 0.5; + } + + for (y = 0; y < width; y++) for (x = 0; x < width; x++) + { + int p; + float q = accbuffer[ y * width + x]; + q = 2.0f * ( q - 0.5f); + if (q < 0.0f) + q = 0.0f; + for (p = 0; p < nplanes - 1; p++) + imageBuffer[ p + nplanes * (y * width + x) ] = 255 * q; + imageBuffer[ p + nplanes * (y * width + x) ] = 255; + } +} + +float q_factor(float* accbuffer, int x, int y, int width, BOOL polar_y_smooth, float polar_y_value, BOOL polar_x_smooth, float polar_x_value, float impress, float bias) +{ + while ( x < 0 ) x+= width; + while ( y < 0 ) y+= width; + while ( x >= width ) x-= width; + while ( y >= width ) y-= width; + + float q = accbuffer[ y * width + x]; // 0.0 -> 1.0 + + q *= impress; // impress + q += bias; // + bias + + float polar_y = (2.0f * y - width) / (float) width; + float polar_x = (2.0f * x - width) / (float) width; + + polar_x *= polar_x; + polar_y *= polar_y; + + if (polar_x_smooth) + q = q * (1.0 - polar_x) + polar_x * polar_x_value; + if (polar_y_smooth) + q = q * (1.0 - polar_y) + polar_y * polar_y_value; + + if (q > 1.0) q = 1.0; + if (q < 0.0) q = 0.0; + + return q; +} + + +void fillSquareImageDataWithCloudTexture(unsigned char * imageBuffer, int width, int nplanes, OOColor* cloudcolor, float impress, float bias) +{ + float accbuffer[width * width]; + int x, y; + y = width * width; + for (x = 0; x < y; x++) accbuffer[x] = 0.0f; + + GLfloat rgba[4]; + rgba[0] = [cloudcolor redComponent]; + rgba[1] = [cloudcolor greenComponent]; + rgba[2] = [cloudcolor blueComponent]; + rgba[3] = [cloudcolor alphaComponent]; + + int octave = 8; + float scale = 0.5; + while (octave < width) + { + addNoise( accbuffer, width, octave, scale); + octave *= 2; + scale *= 0.5; + } + + float pole_value = (impress * accbuffer[0] - bias < 0.0)? 0.0: 1.0; + + for (y = 0; y < width; y++) for (x = 0; x < width; x++) + { + float q = q_factor( accbuffer, x, y, width, YES, pole_value, NO, 0.0, impress, bias); + + if (nplanes == 1) + imageBuffer[ y * width + x ] = 255 * q; + if (nplanes == 3) + { + imageBuffer[ 0 + 3 * (y * width + x) ] = 255 * rgba[0] * q; + imageBuffer[ 1 + 3 * (y * width + x) ] = 255 * rgba[1] * q; + imageBuffer[ 2 + 3 * (y * width + x) ] = 255 * rgba[2] * q; + } + if (nplanes == 4) + { + imageBuffer[ 0 + 4 * (y * width + x) ] = 255 * rgba[0]; + imageBuffer[ 1 + 4 * (y * width + x) ] = 255 * rgba[1]; + imageBuffer[ 2 + 4 * (y * width + x) ] = 255 * rgba[2]; + imageBuffer[ 3 + 4 * (y * width + x) ] = 255 * rgba[3] * q; + } + } +} + +void fillSquareImageWithPlanetTex(unsigned char * imageBuffer, int width, int nplanes, float impress, float bias, + OOColor* seaColor, + OOColor* paleSeaColor, + OOColor* landColor, + OOColor* paleLandColor) +{ + float accbuffer[width * width]; + int x, y; + y = width * width; + for (x = 0; x < y; x++) accbuffer[x] = 0.0f; + + int octave = 8; + float scale = 0.5; + while (octave < width) + { + addNoise( accbuffer, width, octave, scale); + octave *= 2; + scale *= 0.5; + } + + float pole_value = (impress + bias > 0.5)? 0.5 * (impress + bias) : 0.0; + + for (y = 0; y < width; y++) for (x = 0; x < width; x++) + { + float q = q_factor( accbuffer, x, y, width, YES, pole_value, NO, 0.0, impress, bias); + + float yN = q_factor( accbuffer, x, y - 1, width, YES, pole_value, NO, 0.0, impress, bias); + float yS = q_factor( accbuffer, x, y + 1, width, YES, pole_value, NO, 0.0, impress, bias); + float yW = q_factor( accbuffer, x - 1, y, width, YES, pole_value, NO, 0.0, impress, bias); + float yE = q_factor( accbuffer, x + 1, y, width, YES, pole_value, NO, 0.0, impress, bias); + + Vector norm = make_vector( 24.0 * (yW - yE), 24.0 * (yS - yN), 2.0); + + norm = vector_normal(norm); + + GLfloat shade = powf( norm.z, 3.2); + + OOColor* color = [OOColor planetTextureColor:q:impress:bias :seaColor :paleSeaColor :landColor :paleLandColor]; + + float red = [color redComponent]; + float green = [color greenComponent]; + float blue = [color blueComponent]; + + red *= shade; + green *= shade; + blue *= shade; + + if (nplanes == 1) + imageBuffer[ y * width + x ] = 255 * q; + if (nplanes == 3) + { + imageBuffer[ 0 + 3 * (y * width + x) ] = 255 * red; + imageBuffer[ 1 + 3 * (y * width + x) ] = 255 * green; + imageBuffer[ 2 + 3 * (y * width + x) ] = 255 * blue; + } + if (nplanes == 4) + { + imageBuffer[ 0 + 4 * (y * width + x) ] = 255 * red; + imageBuffer[ 1 + 4 * (y * width + x) ] = 255 * green; + imageBuffer[ 2 + 4 * (y * width + x) ] = 255 * blue; + imageBuffer[ 3 + 4 * (y * width + x) ] = 255; + } + } +} + +void fillSquareImageWithPlanetNMap(unsigned char * imageBuffer, int width, int nplanes, float impress, float bias, float factor) +{ + if (nplanes != 4) + { + OOLog(@"textureStore.planetMap.failed", @"***** ERROR: fillSquareImageWithPlanetNMap() can only create textures with 4 planes."); + return; + } + + float accbuffer[width * width]; + int x, y; + y = width * width; + for (x = 0; x < y; x++) accbuffer[x] = 0.0f; + + int octave = 8; + float scale = 0.5; + while (octave < width) + { + addNoise( accbuffer, width, octave, scale); + octave *= 2; + scale *= 0.5; + } + + float pole_value = (impress + bias > 0.5)? 0.5 * (impress + bias) : 0.0; + + for (y = 0; y < width; y++) for (x = 0; x < width; x++) + { + float yN = q_factor( accbuffer, x, y - 1, width, YES, pole_value, NO, 0.0, impress, bias); + float yS = q_factor( accbuffer, x, y + 1, width, YES, pole_value, NO, 0.0, impress, bias); + float yW = q_factor( accbuffer, x - 1, y, width, YES, pole_value, NO, 0.0, impress, bias); + float yE = q_factor( accbuffer, x + 1, y, width, YES, pole_value, NO, 0.0, impress, bias); + + Vector norm = make_vector( factor * (yW - yE), factor * (yS - yN), 2.0); + + norm = vector_normal(norm); + + norm.x = 0.5 * (norm.x + 1.0); + norm.y = 0.5 * (norm.y + 1.0); + norm.z = 0.5 * (norm.z + 1.0); + + imageBuffer[ 0 + 4 * (y * width + x) ] = 255 * norm.x; + imageBuffer[ 1 + 4 * (y * width + x) ] = 255 * norm.y; + imageBuffer[ 2 + 4 * (y * width + x) ] = 255 * norm.z; + imageBuffer[ 3 + 4 * (y * width + x) ] = 255;// * q; // alpha is heightmap + } +} + +@end diff --git a/src/Core/Universe.h b/src/Core/Universe.h new file mode 100644 index 00000000..60fd60d3 --- /dev/null +++ b/src/Core/Universe.h @@ -0,0 +1,650 @@ +/* + +Universe.h + +Manages a lot of stuff that isn't managed somewhere else. + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" +#import "legacy_random.h" +#import "OOMaths.h" +#import "OOColor.h" +#import "OOWeakReference.h" +#import "OOTypes.h" +#import "OOSound.h" + +#if OOLITE_ESPEAK +#include +#endif + +@class GameController, CollisionRegion, MyOpenGLView, GuiDisplayGen, + Entity, ShipEntity, StationEntity, PlanetEntity, PlayerEntity, + OORoleSet; + + +typedef BOOL (*EntityFilterPredicate)(Entity *entity, void *parameter); + + +#define CROSSHAIR_SIZE 32.0 + +enum +{ + MARKET_NAME = 0, + MARKET_QUANTITY = 1, + MARKET_PRICE = 2, + MARKET_BASE_PRICE = 3, + MARKET_ECO_ADJUST_PRICE = 4, + MARKET_ECO_ADJUST_QUANTITY = 5, + MARKET_BASE_QUANTITY = 6, + MARKET_MASK_PRICE = 7, + MARKET_MASK_QUANTITY = 8, + MARKET_UNITS = 9 +}; + + +enum +{ + EQUIPMENT_TECH_LEVEL_INDEX = 0, + EQUIPMENT_PRICE_INDEX = 1, + EQUIPMENT_SHORT_DESC_INDEX = 2, + EQUIPMENT_KEY_INDEX = 3, + EQUIPMENT_LONG_DESC_INDEX = 4, + EQUIPMENT_EXTRA_INFO_INDEX = 5 +}; + + +#define MAX_MESSAGES 5 + +#define PROXIMITY_WARN_DISTANCE 10.0 +#define PROXIMITY_WARN_DISTANCE2 100.0 +#define PROXIMITY_AVOID_DISTANCE 10.0 + +#define SUN_SKIM_RADIUS_FACTOR 1.15470053838 +#define SUN_SPARKS_RADIUS_FACTOR 2.0 + +#define KEY_TECHLEVEL @"techlevel" +#define KEY_ECONOMY @"economy" +#define KEY_GOVERNMENT @"government" +#define KEY_POPULATION @"population" +#define KEY_PRODUCTIVITY @"productivity" +#define KEY_RADIUS @"radius" +#define KEY_NAME @"name" +#define KEY_INHABITANT @"inhabitant" +#define KEY_INHABITANTS @"inhabitants" +#define KEY_DESCRIPTION @"description" +#define KEY_SHORT_DESCRIPTION @"short_description" + +#define KEY_CHANCE @"chance" +#define KEY_PRICE @"price" +#define KEY_OPTIONAL_EQUIPMENT @"optional_equipment" +#define KEY_STANDARD_EQUIPMENT @"standard_equipment" +#define KEY_EQUIPMENT_MISSILES @"missiles" +#define KEY_EQUIPMENT_FORWARD_WEAPON @"forward_weapon_type" +#define KEY_EQUIPMENT_AFT_WEAPON @"aft_weapon_type" +#define KEY_EQUIPMENT_PORT_WEAPON @"port_weapon_type" +#define KEY_EQUIPMENT_STARBOARD_WEAPON @"starboard_weapon_type" +#define KEY_EQUIPMENT_EXTRAS @"extras" +#define KEY_WEAPON_FACINGS @"weapon_facings" + +#define SHIPYARD_KEY_ID @"id" +#define SHIPYARD_KEY_SHIPDATA_KEY @"shipdata_key" +#define SHIPYARD_KEY_SHIP @"ship" +#define SHIPYARD_KEY_PRICE @"price" +#define SHIPYARD_KEY_DESCRIPTION @"description" + +#define PLANETINFO_UNIVERSAL_KEY @"universal" + +#define MAX_ENTITY_UID 1000 + +#define NUMBER_OF_STRICT_EQUIPMENT_ITEMS 16 + +#define UNIVERSE_MAX_ENTITIES 2048 + +#define OOLITE_EXCEPTION_LOOPING @"OoliteLoopingException" +#define OOLITE_EXCEPTION_DATA_NOT_FOUND @"OoliteDataNotFoundException" +#define OOLITE_EXCEPTION_FATAL @"OoliteFatalException" + +#define BILLBOARD_DEPTH 50000.0 + +#define TIME_ACCELERATION_FACTOR_MIN 0.0625f +#define TIME_ACCELERATION_FACTOR_DEFAULT 1.0f +#define TIME_ACCELERATION_FACTOR_MAX 16.0f + + +@interface Universe: OOWeakRefObject +{ +@public + // use a sorted list for drawing and other activities + Entity *sortedEntities[UNIVERSE_MAX_ENTITIES]; + unsigned n_entities; + + int cursor_row; + + // collision optimisation sorted lists + Entity *x_list_start, *y_list_start, *z_list_start; + + GLfloat stars_ambient[4]; + +@private + // colors + GLfloat sun_diffuse[4]; + GLfloat sun_specular[4]; + + OOViewID viewDirection; // read only + + OOMatrix viewMatrix; + + GLfloat airResistanceFactor; + + MyOpenGLView *gameView; + + int next_universal_id; + Entity *entity_for_uid[MAX_ENTITY_UID]; + + NSMutableArray *entities; + + OOUniversalID firstBeacon, lastBeacon; + + GLfloat skyClearColor[4]; + + NSString *currentMessage; + OOTimeAbsolute messageRepeatTime; + + GuiDisplayGen *gui; + GuiDisplayGen *message_gui; + GuiDisplayGen *comm_log_gui; + + BOOL displayGUI; + BOOL displayCursor; + + BOOL autoSaveNow; + BOOL autoSave; + BOOL wireframeGraphics; + BOOL reducedDetail; + OOShaderSetting shaderEffectsLevel; + + BOOL displayFPS; + + OOTimeAbsolute universal_time; + OOTimeDelta time_delta; + + OOTimeAbsolute demo_stage_time; + OOTimeAbsolute demo_start_time; + GLfloat demo_start_z; + int demo_stage; + int demo_ship_index; + NSArray *demo_ships; + + GLfloat sun_center_position[4]; + + BOOL dumpCollisionInfo; + + NSDictionary *commodityLists; // holds data on commodities for various types of station, loaded at initialisation + NSArray *commodityData; // holds data on commodities extracted from commodityLists + + NSDictionary *illegal_goods; // holds the legal penalty for illicit commodities, loaded at initialisation + NSDictionary *descriptions; // holds descriptive text for lots of stuff, loaded at initialisation + NSDictionary *customsounds; // holds descriptive audio for lots of stuff, loaded at initialisation + NSDictionary *characters; // holds descriptons of characters + NSDictionary *planetInfo; // holds overrides for individual planets, keyed by "g# p#" where g# is the galaxy number 0..7 and p# the planet number 0..255 + NSDictionary *missiontext; // holds descriptive text for missions, loaded at initialisation + NSArray *equipmentData; // holds data on available equipment, loaded at initialisation + NSSet *pirateVictimRoles; // Roles listed in pirateVictimRoles.plist. + NSDictionary *autoAIMap; // Default AIs for roles from autoAImap.plist. + + Random_Seed galaxy_seed; + Random_Seed system_seed; + Random_Seed target_system_seed; + + Random_Seed systems[256]; // hold pregenerated universe info + NSString *system_names[256]; // hold pregenerated universe info + BOOL system_found[256]; // holds matches for input strings + + int breakPatternCounter; + + ShipEntity *demo_ship; + + StationEntity *cachedStation; + PlanetEntity *cachedPlanet; + PlanetEntity *cachedSun; + NSMutableArray *allPlanets; + + BOOL strict; + + BOOL no_update; + + float time_acceleration_factor; + + NSMutableDictionary *localPlanetInfoOverrides; + + NSException *exception; + + NSMutableArray *activeWormholes; + + NSMutableArray *characterPool; + + CollisionRegion *universeRegion; + + // check and maintain linked lists occasionally + BOOL doLinkedListMaintenanceThisUpdate; + + // experimental proc-genned textures +#if ALLOW_PROCEDURAL_PLANETS + BOOL doProcedurallyTexturedPlanets; +#endif + + NSMutableArray *entitiesDeadThisUpdate; + int framesDoneThisUpdate; + +#if OOLITE_SPEECH_SYNTH +#if OOLITE_MAC_OS_X + NSSpeechSynthesizer *speechSynthesizer; // use this from OS X 10.3 onwards +#elif OOLITE_ESPEAK + const espeak_VOICE **espeak_voices; + unsigned int espeak_voice_count; +#endif + NSArray *speechArray; +#endif +} + +- (id)initWithGameView:(MyOpenGLView *)gameView; + +#if ALLOW_PROCEDURAL_PLANETS +- (BOOL) doProcedurallyTexturedPlanets; +- (void) setDoProcedurallyTexturedPlanets:(BOOL) value; +#endif + +- (BOOL) strict; +- (void) setStrict:(BOOL) value; + +- (void) reinit; +- (void) reinitAndShowDemo:(BOOL)showDemo; + + +- (int) obj_count; +#ifndef NDEBUG +- (void) obj_dump; +#endif + +- (void) sleepytime: (id) thing; + +- (void) setUpUniverseFromStation; +- (void) set_up_universe_from_witchspace; +- (void) set_up_universe_from_misjump; +- (void) set_up_witchspace; +- (void) setUpSpace; +- (void) setLighting; +- (PlanetEntity *) setUpPlanet; + +- (void) populateSpaceFromActiveWormholes; +- (void) populateSpaceFromHyperPoint:(Vector) h1_pos toPlanetPosition:(Vector) p1_pos andSunPosition:(Vector) s1_pos; +- (int) scatterAsteroidsAt:(Vector) spawnPos withVelocity:(Vector) spawnVel includingRockHermit:(BOOL) spawnHermit; +- (void) addShipWithRole:(NSString *) desc nearRouteOneAt:(double) route_fraction; +- (Vector) coordinatesForPosition:(Vector) pos withCoordinateSystem:(NSString *) system returningScalar:(GLfloat*) my_scalar; +- (NSString *) expressPosition:(Vector) pos inCoordinateSystem:(NSString *) system; +- (Vector) coordinatesFromCoordinateSystemString:(NSString *) system_x_y_z; +- (BOOL) addShipWithRole:(NSString *) desc nearPosition:(Vector) pos withCoordinateSystem:(NSString *) system; +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc atPosition:(Vector) pos withCoordinateSystem:(NSString *) system; +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc nearPosition:(Vector) pos withCoordinateSystem:(NSString *) system; +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc nearPosition:(Vector) pos withCoordinateSystem:(NSString *) system withinRadius:(GLfloat) radius; +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc intoBoundingBox:(BoundingBox) bbox; +- (BOOL) spawnShip:(NSString *) shipdesc; +- (void) witchspaceShipWithPrimaryRole:(NSString *)role; +- (ShipEntity *) spawnShipWithRole:(NSString *) desc near:(Entity *) entity; + +- (BOOL) roleIsPirateVictim:(NSString *)role; + +- (void) set_up_break_pattern:(Vector) pos quaternion:(Quaternion) q; +- (void) game_over; + +- (void) setupIntroFirstGo: (BOOL) justCobra; +- (void) selectIntro2Previous; +- (void) selectIntro2Next; + +- (StationEntity *) station; +- (PlanetEntity *) planet; +- (PlanetEntity *) sun; +- (NSArray *) planets; // Note: does not include sun. + +// Turn main station into just another station, for blowUpStation. +- (void) unMagicMainStation; + +- (void) resetBeacons; +- (ShipEntity *) firstBeacon; +- (ShipEntity *) lastBeacon; +- (void) setNextBeacon:(ShipEntity *) beaconShip; + +- (GLfloat *) skyClearColor; +// Note: the alpha value is also air resistance! +- (void) setSkyColorRed:(GLfloat)red green:(GLfloat)green blue:(GLfloat)blue alpha:(GLfloat)alpha; + +- (BOOL) breakPatternOver; +- (BOOL) breakPatternHide; + +- (ShipEntity *) newShipWithRole:(NSString *)role; // Selects ship using role weights, applies auto_ai, respects conditions +- (ShipEntity *) newShipWithName:(NSString *)shipKey; // Does not apply auto_ai or respect conditions + +- (NSString *)defaultAIForRole:(NSString *)role; // autoAImap.plist lookup + +- (OOCargoQuantity) maxCargoForShip:(NSString *) desc; + +- (OOCreditsQuantity) getPriceForWeaponSystemWithKey:(NSString *)weapon_key; + +- (int) legal_status_of_manifest:(NSArray *)manifest; + +- (NSArray *) getContainersOfGoods:(OOCargoQuantity)how_many scarce:(BOOL)scarce; +- (NSArray *) getContainersOfDrugs:(OOCargoQuantity) how_many; +- (NSArray *) getContainersOfCommodity:(NSString*) commodity_name :(OOCargoQuantity) how_many; +- (void) fillCargopodWithRandomCargo:(ShipEntity *)cargopod; + +- (OOCargoType) getRandomCommodity; +- (OOCargoQuantity) getRandomAmountOfCommodity:(OOCargoType) co_type; + +- (NSArray *) commodityDataForType:(OOCargoType)type; +- (OOCargoType) commodityForName:(NSString *) co_name; +- (NSString *) symbolicNameForCommodity:(OOCargoType) co_type; +- (NSString *) displayNameForCommodity:(OOCargoType) co_type; +- (OOMassUnit) unitsForCommodity:(OOCargoType) co_type; +- (NSString *) describeCommodity:(OOCargoType) co_type amount:(OOCargoQuantity) co_amount; + +- (void) setGameView:(MyOpenGLView *)view; +- (MyOpenGLView *) gameView; +- (GameController *) gameController; + +- (void) drawUniverse; +- (void) drawMessage; + +// Used to draw subentities. Should be getting this from camera. +- (OOMatrix) viewMatrix; + +- (id) entityForUniversalID:(OOUniversalID)u_id; + +- (BOOL) addEntity:(Entity *) entity; +- (BOOL) removeEntity:(Entity *) entity; +- (void) ensureEntityReallyRemoved:(Entity *)entity; +- (void) removeAllEntitiesExceptPlayer:(BOOL) restore; +- (void) removeDemoShips; + +- (BOOL) isVectorClearFromEntity:(Entity *) e1 toDistance:(double)dist fromPoint:(Vector) p2; +- (Entity*) hazardOnRouteFromEntity:(Entity *) e1 toDistance:(double)dist fromPoint:(Vector) p2; +- (Vector) getSafeVectorFromEntity:(Entity *) e1 toDistance:(double)dist fromPoint:(Vector) p2; + +- (OOUniversalID) getFirstEntityHitByLaserFromEntity:(ShipEntity *)srcEntity inView:(OOViewID)viewdir offset:(Vector)offset rangeFound:(GLfloat*)range_ptr; +- (Entity *) getFirstEntityTargetedByPlayer; + +- (NSArray *) getEntitiesWithinRange:(double)range ofEntity:(Entity *)entity; +- (unsigned) countShipsWithRole:(NSString *)role inRange:(double)range ofEntity:(Entity *)entity; +- (unsigned) countShipsWithRole:(NSString *)role; +- (unsigned) countShipsWithPrimaryRole:(NSString *)role inRange:(double)range ofEntity:(Entity *)entity; +- (unsigned) countShipsWithPrimaryRole:(NSString *)role; +- (void) sendShipsWithPrimaryRole:(NSString *)role messageToAI:(NSString *)message; + + +// General count/search methods. Pass range of -1 and entity of nil to search all of system. +- (unsigned) countEntitiesMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)entity; +- (unsigned) countShipsMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)entity; +- (NSMutableArray *) findEntitiesMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)entity; +- (id) findOneEntityMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter; +- (NSMutableArray *) findShipsMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)entity; +- (id) nearestEntityMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + relativeToEntity:(Entity *)entity; +- (id) nearestShipMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + relativeToEntity:(Entity *)entity; + + +- (OOTimeAbsolute) getTime; +- (OOTimeDelta) getTimeDelta; + +- (void) findCollisionsAndShadows; +- (NSString*) collisionDescription; +- (void) dumpCollisions; + +- (void) setViewDirection:(OOViewID) vd; +- (OOViewID) viewDirection; + +- (NSString *) soundNameForCustomSoundKey:(NSString *)key; + +- (void) clearPreviousMessage; +- (void) setMessageGuiBackgroundColor:(OOColor *) some_color; +- (void) displayMessage:(NSString *) text forCount:(OOTimeDelta) count; +- (void) displayCountdownMessage:(NSString *) text forCount:(OOTimeDelta) count; +- (void) addDelayedMessage:(NSString *) text forCount:(OOTimeDelta) count afterDelay:(OOTimeDelta) delay; +- (void) addDelayedMessage:(NSDictionary *) textdict; +- (void) addMessage:(NSString *) text forCount:(OOTimeDelta) count; +- (void) addCommsMessage:(NSString *) text forCount:(OOTimeDelta) count; +- (void) addCommsMessage:(NSString *) text forCount:(OOTimeDelta) count andShowComms:(BOOL) showComms; +- (void) showCommsLog:(OOTimeDelta) how_long; + +- (void) update:(OOTimeDelta)delta_t; + +- (float) timeAccelerationFactor; +- (void) setTimeAccelerationFactor:(float)newTimeAccelerationFactor; + +- (void) filterSortedLists; + +/////////////////////////////////////// + +- (void) setGalaxy_seed:(Random_Seed) gal_seed; +- (void) setGalaxy_seed:(Random_Seed) gal_seed andReinit:(BOOL) forced; + +- (void) setSystemTo:(Random_Seed) s_seed; + +- (Random_Seed) systemSeed; +- (Random_Seed) systemSeedForSystemNumber:(OOSystemID) n; +- (Random_Seed) systemSeedForSystemName:(NSString *)sysname; +- (OOSystemID) systemIDForSystemSeed:(Random_Seed)seed; +- (OOSystemID) currentSystemID; + +- (NSDictionary *) descriptions; +- (NSDictionary *) characters; +- (NSDictionary *) missiontext; + +- (NSString *)descriptionForKey:(NSString *)key; // String, or random item from array +- (NSString *)descriptionForArrayKey:(NSString *)key index:(unsigned)index; // Indexed item from array +- (BOOL) descriptionBooleanForKey:(NSString *)key; // Boolean from descriptions.plist, for configuration. + +- (NSString *) keyForPlanetOverridesForSystemSeed:(Random_Seed) s_seed inGalaxySeed:(Random_Seed) g_seed; +- (NSString *) keyForInterstellarOverridesForSystemSeeds:(Random_Seed) s_seed1 :(Random_Seed) s_seed2 inGalaxySeed:(Random_Seed) g_seed; +- (NSDictionary *) generateSystemData:(Random_Seed) system_seed; +- (NSDictionary *) generateSystemData:(Random_Seed) s_seed useCache:(BOOL) useCache; +- (NSDictionary *) currentSystemData; // Same as generateSystemData:systemSeed unless in interstellar space. +- (BOOL) inInterstellarSpace; + +- (void)setObject:(id)object forKey:(NSString *)key forPlanetKey:(NSString *)planetKey; + +- (void) setSystemDataKey:(NSString*) key value:(NSObject*) object; +- (void) setSystemDataForGalaxy:(OOGalaxyID) gnum planet:(OOSystemID) pnum key:(NSString *)key value:(id)object; +- (id) getSystemDataForGalaxy:(OOGalaxyID) gnum planet:(OOSystemID) pnum key:(NSString *)key; +- (NSString *) getSystemName:(Random_Seed) s_seed; +- (NSString *) getSystemInhabitants:(Random_Seed) s_seed; +- (NSString *) getSystemInhabitants:(Random_Seed) s_seed plural:(BOOL)plural; +- (NSString *) generateSystemName:(Random_Seed) system_seed; +- (NSString *) generatePhoneticSystemName:(Random_Seed) s_seed; +- (NSString *) generateSystemInhabitants:(Random_Seed) s_seed plural:(BOOL)plural; +- (Random_Seed) findSystemAtCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed; + +- (NSArray*) nearbyDestinationsWithinRange:(double) range; +- (Random_Seed) findNeighbouringSystemToCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed; +- (Random_Seed) findConnectedSystemAtCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed; +- (int) findSystemNumberAtCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed; +- (NSPoint) findSystemCoordinatesWithPrefix:(NSString *) p_fix withGalaxySeed:(Random_Seed) gal_seed; +- (BOOL*) systems_found; +- (NSString*) systemNameIndex:(OOSystemID) index; +- (NSDictionary *) routeFromSystem:(OOSystemID) start toSystem:(OOSystemID) goal; +- (NSArray *) neighboursToSystem:(OOSystemID) system_number; + +- (NSMutableDictionary *) localPlanetInfoOverrides; +- (void) setLocalPlanetInfoOverrides:(NSDictionary*) dict; + +- (NSDictionary *) planetInfo; + +- (NSArray *) equipmentData; +- (NSDictionary *) commodityLists; +- (NSArray *) commodityData; + +- (BOOL) generateEconomicDataWithEconomy:(OOEconomyID) economy andRandomFactor:(int) random_factor; +- (NSArray *) commodityDataForEconomy:(OOEconomyID) economy andStation:(StationEntity *)some_station andRandomFactor:(int) random_factor; + +double estimatedTimeForJourney(double distance, int hops); + +- (NSArray *) passengersForSystem:(Random_Seed) s_seed atTime:(OOTimeAbsolute) current_time; +- (NSString *) timeDescription:(OOTimeDelta) interval; +- (NSString *) shortTimeDescription:(OOTimeDelta) interval; +- (NSArray *) contractsForSystem:(Random_Seed) s_seed atTime:(OOTimeAbsolute) current_time; + +- (NSArray *) shipsForSaleForSystem:(Random_Seed) s_seed withTL:(OOTechLevelID) specialTL atTime:(OOTimeAbsolute) current_time; + +/* Calculate base cost, before depreciation */ +- (OOCreditsQuantity) tradeInValueForCommanderDictionary:(NSDictionary*) cmdr_dict; + +- (NSString*) brochureDescriptionWithDictionary:(NSDictionary*) dict standardEquipment:(NSArray*) extras optionalEquipment:(NSArray*) options; + +- (Vector) getWitchspaceExitPosition; +- (Quaternion) getWitchspaceExitRotation; + +- (Vector) getSunSkimStartPositionForShip:(ShipEntity*) ship; +- (Vector) getSunSkimEndPositionForShip:(ShipEntity*) ship; + +- (NSArray*) listBeaconsWithCode:(NSString*) code; + +- (void) allShipsDoScriptEvent:(NSString*) event andReactToAIMessage:(NSString*) message; + +/////////////////////////////////////// + +- (void) clearGUIs; + +- (GuiDisplayGen *) gui; +- (GuiDisplayGen *) comm_log_gui; +- (GuiDisplayGen *) message_gui; + +- (void) resetCommsLogColor; + +- (void) setDisplayCursor:(BOOL) value; +- (BOOL) displayCursor; + +- (void) setDisplayText:(BOOL) value; +- (BOOL) displayGUI; + +- (void) setDisplayFPS:(BOOL) value; +- (BOOL) displayFPS; + +- (void) setAutoSave:(BOOL) value; +- (BOOL) autoSave; + +- (void) setWireframeGraphics:(BOOL) value; +- (BOOL) wireframeGraphics; + +- (void) setReducedDetail:(BOOL) value; +- (BOOL) reducedDetail; + +- (void) setShaderEffectsLevel:(OOShaderSetting)value; +- (OOShaderSetting) shaderEffectsLevel; +- (BOOL) useShaders; + +- (void) handleOoliteException:(NSException*) ooliteException; + +- (GLfloat)airResistanceFactor; + +// speech routines +// +- (void) startSpeakingString:(NSString *) text; +// +- (void) stopSpeaking; +// +- (BOOL) isSpeaking; +// +#if OOLITE_ESPEAK +- (NSString *) voiceName:(unsigned int) index; +- (unsigned int) voiceNumber:(NSString *) name; +- (unsigned int) nextVoice:(unsigned int) index; +- (unsigned int) prevVoice:(unsigned int) index; +- (unsigned int) setVoice:(unsigned int) index withGenderM:(BOOL) isMale; +#endif +// +//// + +//autosave +- (void) setAutoSaveNow:(BOOL) value; +- (BOOL) autoSaveNow; + +- (int) framesDoneThisUpdate; +- (void) resetFramesDoneThisUpdate; + +@end + + +/* Use UNIVERSE to refer to the global universe object. + The purpose of this is that it makes UNIVERSE essentially a read-only + global with zero overhead. +*/ +extern Universe *gSharedUniverse; +#ifndef NDEBUG +OOINLINE Universe *GetUniverse(void) INLINE_CONST_FUNC; +OOINLINE Universe *GetUniverse(void) +{ + return gSharedUniverse; +} +#define UNIVERSE GetUniverse() +#else +#define UNIVERSE gSharedUniverse // Just in case the overhead isn't zero. :-p +#endif + + +// Only for use with string literals, and only for looking up strings. +NSString *DESC_(NSString *key); +NSString *DESC_PLURAL_(NSString *key, int count); +#define DESC(key) (DESC_(key "")) +#define DESC_PLURAL(key,count) (DESC_PLURAL_(key, count)) + + +@interface OOSound (OOCustomSounds) + ++ (id) soundWithCustomSoundKey:(NSString *)key; +- (id) initWithCustomSoundKey:(NSString *)key; + +@end + + +@interface OOSoundSource (OOCustomSounds) + ++ (id) sourceWithCustomSoundKey:(NSString *)key; +- (id) initWithCustomSoundKey:(NSString *)key; + +- (void) playCustomSoundWithKey:(NSString *)key; + +@end diff --git a/src/Core/Universe.m b/src/Core/Universe.m new file mode 100644 index 00000000..bb24ee2f --- /dev/null +++ b/src/Core/Universe.m @@ -0,0 +1,8764 @@ +/* + +Universe.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOOpenGL.h" +#import "Universe.h" +#import "MyOpenGLView.h" +#import "GameController.h" +#import "ResourceManager.h" +#import "AI.h" +#import "GuiDisplayGen.h" +#import "HeadUpDisplay.h" +#import "OOSound.h" +#import "OOColor.h" +#import "OOCacheManager.h" +#import "OOStringParsing.h" +#import "OOCollectionExtractors.h" +#import "OOConstToString.h" +#import "OOOpenGLExtensionManager.h" +#import "OOCPUInfo.h" +#import "OOMaterial.h" +#import "OOTexture.h" +#import "OORoleSet.h" +#import "OOShipGroup.h" + +#import "Octree.h" +#import "CollisionRegion.h" +#import "OOGraphicsResetManager.h" +#import "OODebugSupport.h" +#import "OOEntityFilterPredicate.h" + +#import "OOCharacter.h" +#import "OOShipRegistry.h" +#import "OOProbabilitySet.h" +#import "OOEquipmentType.h" + +#import "PlayerEntity.h" +#import "PlayerEntityContracts.h" +#import "StationEntity.h" +#import "SkyEntity.h" +#import "DustEntity.h" +#import "PlanetEntity.h" +#import "WormholeEntity.h" +#import "RingEntity.h" +#import "ParticleEntity.h" +#import "ShipEntityAI.h" + +#ifndef NDEBUG +#import "OOConvertSystemDescriptions.h" +#endif + +#if OOLITE_ESPEAK +#include +#endif + + +#if OOLITE_MAC_OS_X && !OOLITE_LEOPARD + +enum +{ + NSSpeechImmediateBoundary = 0, + NSSpeechWordBoundary, + NSSpeechSentenceBoundary +}; +typedef OOUInteger NSSpeechBoundary; + + +@interface NSSpeechSynthesizer (Leopard) + +- (void) stopSpeakingAtBoundary:(NSSpeechBoundary)boundary; + +@end + +#endif + + +#define kOOLogUnconvertedNSLog @"unclassified.Universe" + +#define MAX_NUMBER_OF_ENTITIES 200 +#define MAX_NUMBER_OF_SOLAR_SYSTEM_ENTITIES 20 + + +#define DEMO_LIGHT_POSITION 5000.0f, 25000.0f, -10000.0f + +#define SUPPORT_GRAPHVIZ_OUT (!defined NDEBUG) + + +static NSString * const kOOLogUniversePopulate = @"universe.populate"; +static NSString * const kOOLogUniversePopulateWitchspace = @"universe.populate.witchspace"; +extern NSString * const kOOLogEntityVerificationError; +static NSString * const kOOLogEntityVerificationRebuild = @"entity.linkedList.verify.rebuild"; +static NSString * const kOOLogFoundBeacon = @"beacon.list"; + + +Universe *gSharedUniverse = nil; + + +static BOOL MaintainLinkedLists(Universe* uni); + + +static OOComparisonResult compareName(id dict1, id dict2, void * context); +static OOComparisonResult comparePrice(id dict1, id dict2, void * context); + + +@interface Universe (OOPrivate) + +- (BOOL)doRemoveEntity:(Entity *)entity; +- (void) preloadSounds; + +#if SUPPORT_GRAPHVIZ_OUT +- (void) dumpDebugGraphViz; +- (void) dumpSystemDescriptionGraphViz; +#endif + +#ifndef NDEBUG +- (void) runLocalizationTools; +#endif + +@end + + +@implementation Universe + +- (id) initWithGameView:(MyOpenGLView *)inGameView +{ + PlayerEntity *player = nil; + + if (gSharedUniverse != nil) + { + [self release]; + [NSException raise:NSInternalInconsistencyException format:@"%s: expected only one Universe to exist at a time.", __FUNCTION__]; + } + + self = [super init]; + [self setGameView:inGameView]; + gSharedUniverse = self; + + allPlanets = [[NSMutableArray alloc] init]; + + OOCPUInfoInit(); + + // init OpenGL extension manager (must be done before any other threads might use it) + [OOOpenGLExtensionManager sharedManager]; + + [OOMaterial setUp]; + + // Preload cache + [OOCacheManager sharedCache]; + + // init the Resource Manager + [ResourceManager paths]; + + NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults]; + reducedDetail = [prefs boolForKey:@"reduced-detail-graphics" defaultValue:NO]; + autoSave = [prefs boolForKey:@"autosave" defaultValue:NO]; + wireframeGraphics = [prefs boolForKey:@"wireframe-graphics" defaultValue:NO]; +#if ALLOW_PROCEDURAL_PLANETS + doProcedurallyTexturedPlanets = [prefs boolForKey:@"procedurally-textured-planets" defaultValue:YES]; +#endif + shaderEffectsLevel = SHADERS_SIMPLE; + [self setShaderEffectsLevel:[prefs intForKey:@"shader-effects-level" defaultValue:shaderEffectsLevel]]; + + // Set up the internal game strings + descriptions = [[ResourceManager dictionaryFromFilesNamed:@"descriptions.plist" inFolder:@"Config" andMerge:YES] retain]; + // DESC expansion is now possible! + [[GameController sharedController] logProgress:DESC(@"Initialising universe")]; + +#if OOLITE_SPEECH_SYNTH +#if OOLITE_MAC_OS_X + //// speech stuff + speechSynthesizer = [[NSSpeechSynthesizer alloc] init]; +#elif OOLITE_ESPEAK + espeak_Initialize(AUDIO_OUTPUT_PLAYBACK, 100, NULL, 0); + espeak_SetParameter(espeakPUNCTUATION, espeakPUNCT_NONE, 0); + espeak_voices = espeak_ListVoices(NULL); + for (espeak_voice_count = 0; + espeak_voices[espeak_voice_count]; + ++espeak_voice_count) + /**/; +#endif + speechArray = [[ResourceManager arrayFromFilesNamed:@"speech_pronunciation_guide.plist" inFolder:@"Config" andMerge:YES] retain]; +#endif + + [[GameController sharedController] logProgress:DESC(@"loading-ships")]; + // Load ship data + [OOShipRegistry sharedRegistry]; + + next_universal_id = 100; // start arbitrarily above zero + + entities = [[NSMutableArray arrayWithCapacity:MAX_NUMBER_OF_ENTITIES] retain]; + + sun_center_position[0] = 4000000.0; + sun_center_position[1] = 0.0; + sun_center_position[2] = 0.0; + sun_center_position[3] = 1.0; + + // this MUST have the default no. of rows else the GUI_ROW macros in PlayerEntity.h need modification + gui = [[GuiDisplayGen alloc] init]; // alloc retains + + message_gui = [[GuiDisplayGen alloc] + initWithPixelSize:NSMakeSize(480, 160) + columns:1 + rows:9 + rowHeight:19 + rowStart:20 + title:nil]; + [message_gui setCurrentRow:8]; + [message_gui setCharacterSize:NSMakeSize(16,20)]; // slightly narrower characters + [message_gui setDrawPosition: make_vector(0.0, -40.0, 640.0)]; + [message_gui setAlpha:1.0]; + + comm_log_gui = [[GuiDisplayGen alloc] + initWithPixelSize:NSMakeSize(360, 120) + columns:1 + rows:10 + rowHeight:12 + rowStart:12 + title:nil]; + [comm_log_gui setCurrentRow:9]; + [comm_log_gui setBackgroundColor:[OOColor colorWithCalibratedRed:0.0 green:0.05 blue:0.45 alpha:0.5]]; + [comm_log_gui setTextColor:[OOColor whiteColor]]; + [comm_log_gui setAlpha:0.0]; + [comm_log_gui printLongText:DESC(@"communications-log-string") align:GUI_ALIGN_CENTER color:[OOColor yellowColor] fadeTime:0 key:nil addToArray:nil]; + [comm_log_gui setDrawPosition: make_vector(0.0, 180.0, 640.0)]; + + commodityLists = [(NSDictionary *)[ResourceManager dictionaryFromFilesNamed:@"commodities.plist" inFolder:@"Config" andMerge:YES] retain]; + commodityData = [[NSArray arrayWithArray:[commodityLists arrayForKey:@"default"]] retain]; + + illegal_goods = [[ResourceManager dictionaryFromFilesNamed:@"illegal_goods.plist" inFolder:@"Config" andMerge:YES] retain]; + + characters = [[ResourceManager dictionaryFromFilesNamed:@"characters.plist" inFolder:@"Config" andMerge:YES] retain]; + + customsounds = [[ResourceManager dictionaryFromFilesNamed:@"customsounds.plist" inFolder:@"Config" andMerge:YES] retain]; + [self preloadSounds]; + + planetInfo = [[ResourceManager dictionaryFromFilesNamed:@"planetinfo.plist" inFolder:@"Config" mergeMode:MERGE_SMART cache:YES] retain]; + + pirateVictimRoles = [[NSSet alloc] initWithArray:[ResourceManager arrayFromFilesNamed:@"pirate-victim-roles.plist" inFolder:@"Config" andMerge:YES]]; + + autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES]; + + equipmentData = [[ResourceManager arrayFromFilesNamed:@"equipment.plist" inFolder:@"Config" andMerge:YES] retain]; + [OOEquipmentType loadEquipment]; + + localPlanetInfoOverrides = [[NSMutableDictionary alloc] initWithCapacity:8]; + + missiontext = [[ResourceManager dictionaryFromFilesNamed:@"missiontext.plist" inFolder:@"Config" andMerge:YES] retain]; + + demo_ships = [[OOShipRegistry sharedRegistry] demoShipKeys]; + + time_acceleration_factor = TIME_ACCELERATION_FACTOR_DEFAULT; + + player = [[PlayerEntity alloc] init]; // alloc retains! + [self addEntity:player]; + [player release]; + + player->x_next = nil; player->x_previous = nil; x_list_start = player; + player->y_next = nil; player->y_previous = nil; y_list_start = player; + player->z_next = nil; player->z_previous = nil; z_list_start = player; + + [player setUpShipFromDictionary:[[OOShipRegistry sharedRegistry] shipInfoForKey:[player ship_desc]]]; // ship desc is the standard cobra at this point + + [player setStatus:STATUS_START_GAME]; + [player setShowDemoShips: YES]; + + [self setGalaxy_seed: [player galaxy_seed] andReinit:YES]; + + system_seed = [self findSystemAtCoords:[player galaxy_coordinates] withGalaxySeed:galaxy_seed]; + + + activeWormholes = [[NSMutableArray arrayWithCapacity:16] retain]; + + characterPool = [[NSMutableArray arrayWithCapacity:256] retain]; + + [[GameController sharedController] logProgress:DESC(@"populating-space")]; + + [self setUpSpace]; + + if (cachedStation) [player setPosition:cachedStation->position]; + + [self setViewDirection:VIEW_GUI_DISPLAY]; + + universeRegion = [[CollisionRegion alloc] initAsUniverse]; + + entitiesDeadThisUpdate = [[NSMutableArray alloc] init]; + + framesDoneThisUpdate = 0; + + OOInitDebugSupport(); + + [[GameController sharedController] logProgress:DESC(@"running-scripts")]; + + [player completeInitialSetUp]; + +#ifndef NDEBUG + [self runLocalizationTools]; +#endif + +#if SUPPORT_GRAPHVIZ_OUT + [self dumpDebugGraphViz]; +#endif + + return self; +} + + +- (void) dealloc +{ + gSharedUniverse = nil; + + [currentMessage release]; + + [gui release]; + [message_gui release]; + [comm_log_gui release]; + + [entities release]; + + [commodityLists release]; + [commodityData release]; + + [illegal_goods release]; + [descriptions release]; + [characters release]; + [customsounds release]; + [planetInfo release]; + [missiontext release]; + [equipmentData release]; + [demo_ships release]; + [gameView release]; + + [localPlanetInfoOverrides release]; + [activeWormholes release]; + [characterPool release]; + [universeRegion release]; + + unsigned i; + for (i = 0; i < 256; i++) [system_names[i] release]; + + [entitiesDeadThisUpdate release]; + + [[OOCacheManager sharedCache] flush]; + +#if OOLITE_SPEECH_SYNTH + [speechArray release]; +#if OOLITE_MAC_OS_X + [speechSynthesizer release]; +#elif OOLITE_ESPEAK + espeak_Cancel(); +#endif +#endif + + [super dealloc]; +} + + +#if ALLOW_PROCEDURAL_PLANETS +- (BOOL) doProcedurallyTexturedPlanets +{ + return doProcedurallyTexturedPlanets; +} + + +- (void) setDoProcedurallyTexturedPlanets:(BOOL) value +{ + doProcedurallyTexturedPlanets = !!value; // ensure yes or no + [[NSUserDefaults standardUserDefaults] setBool:doProcedurallyTexturedPlanets forKey:@"procedurally-textured-planets"]; +} +#endif + + +- (BOOL) strict +{ + return strict; +} + + +- (void) setStrict:(BOOL)value +{ + if (strict == value) return; + + strict = !!value; + [OOTexture clearCache]; // Force reload of textures, since search paths effectively change + + [self reinit]; +} + + +- (void) reinit +{ + [self reinitAndShowDemo:YES]; +} + + +- (void) reinitAndShowDemo:(BOOL)showDemo +{ + PlayerEntity* player = [[PlayerEntity sharedPlayer] retain]; + Quaternion q0 = kIdentityQuaternion; + int i; + BOOL delayedReset=NO; + + no_update = YES; + + [self removeAllEntitiesExceptPlayer:NO]; + + [ResourceManager setUseAddOns:!strict]; + [ResourceManager loadScripts]; + + // NOTE: Anything in the sharedCache is now trashed and must be + // reloaded. Ideally anything using the sharedCache should + // be aware of cache flushes so it can automatically + // reinitialize itself - mwerle 20081107. + [[OOShipRegistry sharedRegistry] init]; + + [[gameView gameController] unpause_game]; + +#if OOLITE_SPEECH_SYNTH + [speechArray autorelease]; + speechArray = [[ResourceManager arrayFromFilesNamed:@"speech_pronunciation_guide.plist" inFolder:@"Config" andMerge:YES] retain]; +#endif + + + firstBeacon = NO_TARGET; + lastBeacon = NO_TARGET; + + next_universal_id = 100; // start arbitrarily above zero + for (i = 0; i < MAX_ENTITY_UID; i++) + entity_for_uid[i] = nil; + + sun_center_position[0] = 4000000.0; + sun_center_position[1] = 0.0; + sun_center_position[2] = 0.0; + sun_center_position[3] = 1.0; + + [gui autorelease]; + gui = [[GuiDisplayGen alloc] init]; + + [message_gui autorelease]; + message_gui = [[GuiDisplayGen alloc] + initWithPixelSize:NSMakeSize(480, 160) + columns:1 + rows:9 + rowHeight:19 + rowStart:20 + title:nil]; + [message_gui setCurrentRow:8]; + [message_gui setCharacterSize:NSMakeSize(16,20)]; // slightly narrower characters + [message_gui setDrawPosition: make_vector(0.0, -40.0, 640.0)]; + [message_gui setAlpha:1.0]; + + [comm_log_gui autorelease]; + comm_log_gui = [[GuiDisplayGen alloc] + initWithPixelSize:NSMakeSize(360, 120) + columns:1 + rows:10 + rowHeight:12 + rowStart:12 + title:nil]; + [comm_log_gui setCurrentRow:9]; + [comm_log_gui setBackgroundColor:[OOColor colorWithCalibratedRed:0.0 green:0.05 blue:0.45 alpha:0.5]]; + [comm_log_gui setTextColor:[OOColor whiteColor]]; + [comm_log_gui setAlpha:0.0]; + [comm_log_gui printLongText:DESC(@"communications-log-string") align:GUI_ALIGN_CENTER color:[OOColor yellowColor] fadeTime:0 key:nil addToArray:nil]; + [comm_log_gui setDrawPosition: make_vector(0.0, 180.0, 640.0)]; + + time_delta = 0.0; + time_acceleration_factor = TIME_ACCELERATION_FACTOR_DEFAULT; + universal_time = 0.0; + messageRepeatTime = 0.0; + + [commodityLists autorelease]; + commodityLists = [[ResourceManager dictionaryFromFilesNamed:@"commodities.plist" inFolder:@"Config" andMerge:YES] retain]; + + [commodityData autorelease]; + commodityData = [[NSArray arrayWithArray:[commodityLists arrayForKey:@"default"]] retain]; + + [illegal_goods autorelease]; + illegal_goods = [[ResourceManager dictionaryFromFilesNamed:@"illegal_goods.plist" inFolder:@"Config" andMerge:YES] retain]; + + [descriptions autorelease]; + descriptions = [[ResourceManager dictionaryFromFilesNamed:@"descriptions.plist" inFolder:@"Config" andMerge:YES] retain]; + + [characters autorelease]; + characters = [[ResourceManager dictionaryFromFilesNamed:@"characters.plist" inFolder:@"Config" andMerge:YES] retain]; + + [customsounds autorelease]; + customsounds = [[ResourceManager dictionaryFromFilesNamed:@"customsounds.plist" inFolder:@"Config" andMerge:YES] retain]; + + [planetInfo autorelease]; + planetInfo = [[ResourceManager dictionaryFromFilesNamed:@"planetinfo.plist" inFolder:@"Config" mergeMode:MERGE_SMART cache:YES] retain]; + + [pirateVictimRoles autorelease]; + pirateVictimRoles = [[NSSet alloc] initWithArray:[ResourceManager arrayFromFilesNamed:@"pirate-victim-roles.plist" inFolder:@"Config" andMerge:YES]]; + + //[autoAIMap autorelease]; // Having this line in causes a crash when switching from normal to strict and then back to normal. + autoAIMap = [ResourceManager dictionaryFromFilesNamed:@"autoAImap.plist" inFolder:@"Config" andMerge:YES]; + + [equipmentData autorelease]; + equipmentData = [[ResourceManager arrayFromFilesNamed:@"equipment.plist" inFolder:@"Config" andMerge:YES] retain]; + if (strict && ([equipmentData count] > NUMBER_OF_STRICT_EQUIPMENT_ITEMS)) + { + NSArray* strict_equipment = [equipmentData subarrayWithRange:NSMakeRange(0, NUMBER_OF_STRICT_EQUIPMENT_ITEMS)]; // alloc retains + [equipmentData autorelease]; + equipmentData = [strict_equipment retain]; + } + [OOEquipmentType loadEquipment]; + if(showDemo) + { + [demo_ships release]; + demo_ships = [[OOShipRegistry sharedRegistry] demoShipKeys]; + demo_ship_index = 0; + } + + breakPatternCounter = 0; + + cachedSun = nil; + cachedPlanet = nil; + cachedStation = nil; + + if (player == nil) + player = [[PlayerEntity alloc] init]; + else + { + [player set_up:NO]; + delayedReset=YES; + } + [self addEntity:player]; + + [[gameView gameController] setPlayerFileToLoad:nil]; // reset Quicksave + [player setUpShipFromDictionary:[[OOShipRegistry sharedRegistry] shipInfoForKey:[player ship_desc]]]; // ship desc is the standard cobra at this point + + if (activeWormholes) + [activeWormholes autorelease]; + activeWormholes = [[NSMutableArray arrayWithCapacity:16] retain]; + + [characterPool removeAllObjects]; + + // these lines are needed here to reset systeminfo and long range chart properly + [localPlanetInfoOverrides removeAllObjects]; + + [self setGalaxy_seed: [player galaxy_seed] andReinit:YES]; + system_seed = [self findSystemAtCoords:[player galaxy_coordinates] withGalaxySeed:galaxy_seed]; + + [self setUpSpace]; + + [[self station] initialiseLocalMarketWithSeed:system_seed andRandomFactor:[player random_factor]]; + [player setDockedAtMainStation]; + + demo_ship = nil; + + [self setViewDirection:VIEW_GUI_DISPLAY]; + [player setPosition:kZeroVector]; + [player setOrientation:q0]; + if(showDemo) + { + [player setGuiToIntroFirstGo:NO]; + [gui setText:(strict)? DESC(@"strict-play-enabled"):DESC(@"unrestricted-play-enabled") forRow:1 align:GUI_ALIGN_CENTER]; + } + else + { + [player setGuiToStatusScreen]; + } + if (delayedReset) [player doScriptEvent:@"reset"]; + [player release]; + no_update = NO; +} + + +- (int) obj_count +{ + return [entities count]; +} + + +#ifndef NDEBUG +- (void) obj_dump +{ + int i; + int show_count = n_entities; + + if (!OOLogWillDisplayMessagesInClass(@"universe.objectDump")) return; + + OOLog(@"universe.objectDump", @"DEBUG: Entity Dump - [entities count] = %d,\tn_entities = %d", [entities count], n_entities); + + OOLogIndent(); + for (i = 0; i < show_count; i++) + { + ShipEntity* se = (sortedEntities[i]->isShip)? (id)sortedEntities[i]: nil; + OOLog(@"universe.objectDump", @"-> Ent:%d\t\t%@ mass %.2f %@", i, sortedEntities[i], [sortedEntities[i] mass], [se getAI]); + } + OOLogOutdent(); + + if ([entities count] != n_entities) + { + OOLog(@"universe.objectDump", @"entities = %@", [entities description]); + } +} +#endif + + +- (void) sleepytime: (id) thing +{ + // deal with the machine going to sleep + PlayerEntity *player = [PlayerEntity sharedPlayer]; + if ((player)&&([player status] == STATUS_IN_FLIGHT)) + { + [self displayMessage:@" Paused (press 'p') " forCount:1.0]; + [[gameView gameController] pause_game]; + } +} + + +- (void) setUpUniverseFromStation +{ + if (![self sun]) + { + // we're in witchspace or this is the first launch... + // save the player + PlayerEntity* player = [PlayerEntity sharedPlayer]; + // save the docked craft + Entity* dockedStation = [player dockedStation]; + // jump to the nearest system + Random_Seed s_seed = [self findSystemAtCoords:[player galaxy_coordinates] withGalaxySeed:[player galaxy_seed]]; + [player setSystem_seed:s_seed]; + + // I think we need to do this too! + [self setSystemTo: s_seed]; + + // remove everything except the player and the docked station + if (dockedStation) + { + int index = 0; + while ([entities count] > 2) + { + Entity* ent = [entities objectAtIndex:index]; + if ((ent != player)&&(ent != dockedStation)) + { + if (ent->isStation) // clear out queues + [(StationEntity *)ent clear]; + [self removeEntity:ent]; + } + else + { + index++; // leave that one alone + } + } + } + else + { + [self removeAllEntitiesExceptPlayer:NO]; // get rid of witchspace sky etc. if still extant + } + + [self setUpSpace]; // first launch + } + + [self setViewDirection:VIEW_FORWARD]; + displayGUI = NO; + + //reset atmospherics in case we ejected while we were in the atmophere + [UNIVERSE setSkyColorRed:0.0f // back to black + green:0.0f + blue:0.0f + alpha:0.0f]; +} + + +- (void) set_up_universe_from_witchspace +{ + PlayerEntity *player; + + // + // check the player is still around! + // + if ([entities count] == 0) + { + /*- the player ship -*/ + player = [[PlayerEntity alloc] init]; // alloc retains! + + [self addEntity:player]; + + /*--*/ + } + else + { + player = [[PlayerEntity sharedPlayer] retain]; // retained here + } + + + [self setUpSpace]; + + [player leaveWitchspace]; + [player release]; // released here + + [self setViewDirection:VIEW_FORWARD]; + + [comm_log_gui printLongText:[NSString stringWithFormat:@"%@ %@", [self getSystemName:system_seed], [player dial_clock_adjusted]] + align:GUI_ALIGN_CENTER color:[OOColor whiteColor] fadeTime:0 key:nil addToArray:[player commLog]]; + + displayGUI = NO; +} + + +- (void) set_up_universe_from_misjump +{ + PlayerEntity *player; + + // + // check the player is still around! + // + if ([entities count] == 0) + { + /*- the player ship -*/ + player = [[PlayerEntity alloc] init]; // alloc retains! + + [self addEntity:player]; + + /*--*/ + } + else + { + player = [[PlayerEntity sharedPlayer] retain]; // retained here + } + + + [self set_up_witchspace]; + + [player leaveWitchspace]; + [player release]; // released here + + [self setViewDirection:VIEW_FORWARD]; + + displayGUI = NO; +} + + +- (void) set_up_witchspace +{ + // new system is hyper-centric : witchspace exit point is origin + + Entity *thing; + PlayerEntity* player = [PlayerEntity sharedPlayer]; + Quaternion randomQ; + + NSMutableDictionary *systeminfo = [NSMutableDictionary dictionaryWithCapacity:4]; + + Random_Seed s1 = player->system_seed; + Random_Seed s2 = player->target_system_seed; + NSString* override_key = [self keyForInterstellarOverridesForSystemSeeds:s1 :s2 inGalaxySeed:galaxy_seed]; + + // check at this point + // for scripted overrides for this insterstellar area + [systeminfo addEntriesFromDictionary:[planetInfo dictionaryForKey:PLANETINFO_UNIVERSAL_KEY]]; + [systeminfo addEntriesFromDictionary:[planetInfo dictionaryForKey:@"interstellar space"]]; + [systeminfo addEntriesFromDictionary:[planetInfo dictionaryForKey:override_key]]; + [systeminfo addEntriesFromDictionary:[localPlanetInfoOverrides dictionaryForKey:override_key]]; + + [universeRegion clearSubregions]; + + // fixed entities (part of the graphics system really) come first... + + /*- the sky backdrop -*/ + OOColor *col1 = [OOColor colorWithCalibratedRed:0.0 green:1.0 blue:0.5 alpha:1.0]; + OOColor *col2 = [OOColor colorWithCalibratedRed:0.0 green:1.0 blue:0.0 alpha:1.0]; + thing = [[SkyEntity alloc] initWithColors:col1:col2 andSystemInfo: systeminfo]; // alloc retains! + [thing setScanClass: CLASS_NO_DRAW]; + quaternion_set_random(&randomQ); + [thing setOrientation:randomQ]; + [self addEntity:thing]; + [thing release]; + + /*- the dust particle system -*/ + thing = [[DustEntity alloc] init]; + [thing setScanClass: CLASS_NO_DRAW]; + [self addEntity:thing]; + [thing release]; + + sun_center_position[0] = 0.0; + sun_center_position[1] = 0.0; + sun_center_position[2] = 0.0; + sun_center_position[3] = 1.0; + + ranrot_srand([[NSDate date] timeIntervalSince1970]); // reset randomiser with current time + + [self setLighting]; + + OOLog(kOOLogUniversePopulateWitchspace, @"Populating witchspace ..."); + OOLogIndentIf(kOOLogUniversePopulateWitchspace); + + // actual thargoids and tharglets next... + int n_thargs = 2 + (Ranrot() & 3); + if (n_thargs < 1) + n_thargs = 2; // just to be sure + int i; + + Vector tharg_start_pos = [self getWitchspaceExitPosition]; + ranrot_srand([[NSDate date] timeIntervalSince1970]); // reset randomiser with current time + + OOLog(kOOLogUniversePopulateWitchspace, @"... adding %d Thargoid warships", n_thargs); + + OOShipGroup *thargoidGroup = [OOShipGroup groupWithName:@"thargoid group"]; + + for (i = 0; i < n_thargs; i++) + { + Quaternion tharg_quaternion; + ShipEntity *thargoid = [self newShipWithRole:@"thargoid"]; // is retained + if (thargoid) + { + Vector tharg_pos = tharg_start_pos; + + tharg_pos.x += 1.5 * SCANNER_MAX_RANGE * (randf() - 0.5); + tharg_pos.y += 1.5 * SCANNER_MAX_RANGE * (randf() - 0.5); + tharg_pos.z += 1.5 * SCANNER_MAX_RANGE * (randf() - 0.5); + [thargoid setPosition:tharg_pos]; + quaternion_set_random(&tharg_quaternion); + [thargoid setOrientation:tharg_quaternion]; + [thargoid setScanClass: CLASS_THARGOID]; + [thargoid setBounty:100]; + [thargoid setStatus:STATUS_IN_FLIGHT]; + [self addEntity:thargoid]; + + [thargoid setGroup:thargoidGroup]; + + [thargoid release]; + } + } + + // systeminfo might have a 'script_actions' resource we want to activate now... + NSArray *script_actions = [systeminfo arrayForKey:@"script_actions"]; + if (script_actions != nil) + { + [player runUnsanitizedScriptActions:script_actions + allowingAIMethods:NO + withContextName:@"witchspace script_actions" + forTarget:nil]; + } + + OOLogOutdentIf(kOOLogUniversePopulateWitchspace); +} + + +- (PlanetEntity *) setUpPlanet +{ + PlanetEntity *a_planet; + + // set the system seed for random number generation + seed_for_planet_description(system_seed); + + /*- space planet -*/ + a_planet = [[PlanetEntity alloc] initWithSeed: system_seed]; // alloc retains! + double planet_radius = [a_planet radius]; + double planet_zpos = (12.0 + (Ranrot() & 3) - (Ranrot() & 3) ) * planet_radius; // 9..15 pr (planet radii) ahead + + [a_planet setStatus:STATUS_ACTIVE]; + [a_planet setPositionX:0 y:0 z:planet_zpos]; + [a_planet setEnergy: 1000000.0]; + + if ([self planet]) + { + PlanetEntity *tmp=[allPlanets objectAtIndex:0]; + [self addEntity:a_planet]; + [allPlanets removeObject:a_planet]; + cachedPlanet=a_planet; + [allPlanets replaceObjectAtIndex:0 withObject:a_planet]; + [self removeEntity:(Entity *)tmp]; + } + else + { + [self addEntity:a_planet]; + } + return a_planet; +} + + +- (void) setUpSpace +{ + Entity *thing; + ShipEntity *nav_buoy; + StationEntity *a_station; + PlanetEntity *a_sun; + PlanetEntity *a_planet; + + Vector stationPos; + + Vector vf; + + NSDictionary *systeminfo = [self generateSystemData:system_seed useCache:NO]; + unsigned techlevel = [systeminfo unsignedIntForKey:KEY_TECHLEVEL]; + NSString *stationDesc = nil, *defaultStationDesc = nil; + OOColor *bgcolor; + OOColor *pale_bgcolor; + BOOL sunGoneNova; + + sunGoneNova = [systeminfo boolForKey:@"sun_gone_nova"]; + + [universeRegion clearSubregions]; + + // fixed entities (part of the graphics system really) come first... + [self setSkyColorRed:0.0f + green:0.0f + blue:0.0f + alpha:0.0f]; + + // set the system seed for random number generation + seed_for_planet_description(system_seed); + + /*- the sky backdrop -*/ + // colors... + float h1 = randf(); + float h2 = h1 + 1.0 / (1.0 + (Ranrot() % 5)); + while (h2 > 1.0) + h2 -= 1.0; + OOColor *col1 = [OOColor colorWithCalibratedHue:h1 saturation:randf() brightness:0.5 + randf()/2.0 alpha:1.0]; + OOColor *col2 = [OOColor colorWithCalibratedHue:h2 saturation:0.5 + randf()/2.0 brightness:0.5 + randf()/2.0 alpha:1.0]; + + thing = [[SkyEntity alloc] initWithColors:col1:col2 andSystemInfo: systeminfo]; // alloc retains! + [thing setScanClass: CLASS_NO_DRAW]; + [self addEntity:thing]; + bgcolor = [(SkyEntity *)thing skyColor]; + pale_bgcolor = [bgcolor blendedColorWithFraction:0.5 ofColor:[OOColor whiteColor]]; + [thing release]; + /*--*/ + + [self setLighting]; + + /*- the dust particle system -*/ + thing = [[DustEntity alloc] init]; // alloc retains! + [thing setScanClass: CLASS_NO_DRAW]; + [self addEntity:thing]; // [entities addObject:thing]; + [(DustEntity *)thing setDustColor:pale_bgcolor]; + [thing release]; + /*--*/ + + + // actual entities next... + + a_planet=[self setUpPlanet]; + double planet_radius = [a_planet radius]; + + // set the system seed for random number generation + seed_for_planet_description(system_seed); + + /*- space sun -*/ + double sun_radius; + double sun_distance; + double sunDistanceModifier; + double safeDistance; + int posIterator=0; + id dict_object; + Quaternion q_sun; + Vector sunPos; + + sunDistanceModifier = [systeminfo nonNegativeDoubleForKey:@"sun_distance_modifier" defaultValue:20.0]; + // Any smaller than 6, the main planet can end up inside the sun + if (sunDistanceModifier < 6.0) sunDistanceModifier = 6.0; + sun_distance = (sunDistanceModifier + (Ranrot() % 5) - (Ranrot() % 5) ) * planet_radius; + + sun_radius = [systeminfo nonNegativeDoubleForKey:@"sun_radius" defaultValue:(2.5 + randf() - randf() ) * planet_radius]; + // clamp the sun radius + if (sun_radius < 1000.0 || sun_radius > 1000000.0 ) + { + sun_radius = sun_radius < 1000.0 ? 1000.0 : 1000000.0; + } + safeDistance=16 * sun_radius * sun_radius; // 4 times the sun radius + + // generated sun_distance/sun_radius ratios vary from 4.29 ( 15/3.5 ) to 16.67 ( 25/1.5 ) + // if ratio is less than 4 there's an OXP asking for an unusual system. + if (sun_distance <= 4.2 * sun_radius) + { + // recalculate base distance: lowest 2.60 sun radii, highest 4.28 sun radii + sun_distance= (2.6 + sun_distance /(2.5 * sun_radius)) * sun_radius; + // decrease the safe distance, so we have a better chance to exit the loop normally + safeDistance *= 0.6; // ~ 3 times the sun radius + } + + // here we need to check if the sun collides with (or is too close to) the witchpoint + // otherwise at (for example) Maregais in Galaxy 1 we go BANG! + do + { + sunPos = [a_planet position]; + + quaternion_set_random(&q_sun); + // set up planet's direction in space so it gets a proper day + [a_planet setOrientation:q_sun]; + + vf = vector_right_from_quaternion(q_sun); + sunPos = vector_subtract(sunPos, vector_multiply_scalar(vf, sun_distance)); // back off from the planet by 15..25 planet radii + posIterator++; + + } while (magnitude2(sunPos) < safeDistance && posIterator <= 10); // try 10 times before giving up + + if (posIterator>10) + { + OOLogWARN(@"universe.setup.badSun",@"Sun positioning: max iterations exceeded for '%@'. Adjust radius, sun_radius or sun_distance_modifier.",[systeminfo objectForKey: @"name"]); + } + + NSMutableDictionary* sun_dict = [[NSMutableDictionary alloc] initWithCapacity:4]; + [sun_dict setObject:[NSNumber numberWithDouble:sun_radius] forKey:@"sun_radius"]; + dict_object=[systeminfo objectForKey: @"corona_shimmer"]; + if (dict_object!=nil) [sun_dict setObject:dict_object forKey:@"corona_shimmer"]; + dict_object=[systeminfo objectForKey: @"corona_hues"]; + if (dict_object!=nil) [sun_dict setObject:dict_object forKey:@"corona_hues"]; + dict_object=[systeminfo objectForKey: @"corona_flare"]; + if (dict_object!=nil) [sun_dict setObject:dict_object forKey:@"corona_flare"]; + //dict_object=[systeminfo objectForKey: @"sun_texture"]; + //if (dict_object!=nil) [sun_dict setObject:dict_object forKey:@"sun_texture"]; + + a_sun = [[PlanetEntity alloc] initSunWithColor:pale_bgcolor andDictionary:sun_dict]; // alloc retains! + [a_sun setStatus:STATUS_ACTIVE]; + [a_sun setPosition:sunPos]; + sun_center_position[0] = sunPos.x; + sun_center_position[1] = sunPos.y; + sun_center_position[2] = sunPos.z; + sun_center_position[3] = 1.0; + [a_sun setEnergy: 1000000.0]; + [self addEntity:a_sun]; + + if (sunGoneNova) + { + [a_sun setRadius: sun_radius + MAX_CORONAFLARE]; + [a_sun setThrowSparks:YES]; + [a_sun setVelocity: kZeroVector]; + } + + /*- space station -*/ + stationPos = [a_planet position]; + double station_orbit = 2.0 * planet_radius; + Quaternion q_station; + vf.z = -1; + while (vf.z <= 0.0) // keep station on the correct side of the planet + { + quaternion_set_random(&q_station); + vf = vector_forward_from_quaternion(q_station); + } + stationPos = vector_subtract(stationPos, vector_multiply_scalar(vf, station_orbit)); + + defaultStationDesc = @"coriolis"; + if (techlevel > 10) + { + if (system_seed.f & 0x03) // 3 out of 4 get this type + defaultStationDesc = @"dodecahedron"; + else + defaultStationDesc = @"icosahedron"; + } + + //// possibly systeminfo has an override for the station + stationDesc = [systeminfo stringForKey:@"station" defaultValue:defaultStationDesc]; + + a_station = (StationEntity *)[self newShipWithRole:stationDesc]; // retain count = 1 + + /* Sanity check: ensure that only stations are generated here. This is an + attempt to fix exceptions of the form: + NSInvalidArgumentException : *** -[ShipEntity setPlanet:]: selector + not recognized [self = 0x19b7e000] ***** + which I presume to be originating here since all other uses of + setPlanet: are guarded by isStation checks. This error could happen if + a ship that is not a station has a station role, or equivalently if an + OXP sets a system's station role to a role used by non-stations. + -- Ahruman 20080303 + */ + if (![a_station isStation] || ![a_station validForAddToUniverse]) + { + if (a_station == nil) + { + // Should have had a more specific error already, just specify context + OOLog(@"universe.setup.badStation", @"Failed to set up a ship for role \"%@\" as system station, trying again with \"%@\".", stationDesc, defaultStationDesc); + } + else + { + OOLog(@"universe.setup.badStation", @"***** ERROR: Attempt to use non-station ship of type \"%@\" for role \"%@\" as system station, trying again with \"%@\".", [a_station name], stationDesc, defaultStationDesc); + } + [a_station release]; + stationDesc = defaultStationDesc; + a_station = (StationEntity *)[self newShipWithRole:stationDesc]; // retain count = 1 + + if (![a_station isStation] || ![a_station validForAddToUniverse]) + { + if (a_station == nil) + { + OOLog(@"universe.setup.badStation", @"On retry, failed to set up a ship for role \"%@\" as system station. Trying to fall back to built-in Coriolis station.", stationDesc); + } + else + { + OOLog(@"universe.setup.badStation", @"***** ERROR: On retry, rolled non-station ship of type \"%@\" for role \"%@\". Non-station ships should not have this role! Trying to fall back to built-in Coriolis station.", [a_station name], stationDesc); + } + [a_station release]; + + a_station = (StationEntity *)[self newShipWithName:@"coriolis-station"]; + if (![a_station isStation] || ![a_station validForAddToUniverse]) + { + OOLog(@"universe.setup.badStation", @"Could not create built-in Coriolis station! Generating a stationless system."); + [a_station release]; + a_station = nil; + } + } + } + + if (a_station) + { + [a_station setStatus:STATUS_ACTIVE]; + [a_station setOrientation: q_station]; + [a_station setPosition: stationPos]; + [a_station setPitch: 0.0]; + [a_station setScanClass: CLASS_STATION]; + [a_station setPlanet:[self planet]]; + [a_station setEquivalentTechLevel:techlevel]; + [self addEntity:a_station]; + } + + cachedSun = a_sun; + cachedPlanet = a_planet; + cachedStation = a_station; + + ranrot_srand([[NSDate date] timeIntervalSince1970]); // reset randomiser with current time + + [self populateSpaceFromActiveWormholes]; + + [self populateSpaceFromHyperPoint:[self getWitchspaceExitPosition] toPlanetPosition: a_planet->position andSunPosition: a_sun->position]; + + /*- nav beacon -*/ + nav_buoy = [self newShipWithRole:@"buoy"]; // retain count = 1 + if (nav_buoy) + { + [nav_buoy setRoll: 0.10]; + [nav_buoy setPitch: 0.15]; + [nav_buoy setPosition: [cachedStation getBeaconPosition]]; + [nav_buoy setScanClass: CLASS_BUOY]; + [self addEntity:nav_buoy]; + [nav_buoy setStatus:STATUS_IN_FLIGHT]; + [nav_buoy release]; + } + /*--*/ + + /*- nav beacon witchpoint -*/ + Vector witchpoint = [self getWitchspaceExitPosition]; // witchpoint + nav_buoy = [self newShipWithRole:@"buoy-witchpoint"]; // retain count = 1 + if (nav_buoy) + { + [nav_buoy setRoll: 0.10]; + [nav_buoy setPitch: 0.15]; + [nav_buoy setPosition:witchpoint]; + [nav_buoy setScanClass: CLASS_BUOY]; + [self addEntity:nav_buoy]; + [nav_buoy setStatus:STATUS_IN_FLIGHT]; + [nav_buoy release]; + } + /*--*/ + + if (sunGoneNova) + { + Vector v0 = make_vector(0,0,34567.89); + Vector planetPos = a_planet->position; + double min_safe_dist2 = 5000000.0 * 5000000.0; + while (magnitude2(a_sun->position) < min_safe_dist2) // back off the planetary bodies + { + v0.z *= 2.0; + planetPos = a_planet->position; + [a_planet setPosition:vector_add(planetPos, v0)]; + [a_sun setPosition:vector_add(sunPos, v0)]; + sunPos = a_sun->position; + [a_station setPosition:vector_add(stationPos, v0)]; + stationPos = a_station->position; + } + sun_center_position[0] = sunPos.x; + sun_center_position[1] = sunPos.y; + sun_center_position[2] = sunPos.z; + sun_center_position[3] = 1.0; + + [self removeEntity:a_planet]; // and Poof! it's gone + cachedPlanet = nil; + int i; + for (i = 0; i < 3; i++) + { + [self scatterAsteroidsAt:planetPos withVelocity:kZeroVector includingRockHermit:NO]; + [self scatterAsteroidsAt:kZeroVector withVelocity:kZeroVector includingRockHermit:NO]; + } + + } + + [a_sun release]; + [a_station release]; + [a_planet release]; + + // systeminfo might have a 'script_actions' resource we want to activate now... + NSArray *script_actions = [systeminfo arrayForKey:@"script_actions"]; + if (script_actions != nil) + { + [[PlayerEntity sharedPlayer] runUnsanitizedScriptActions:script_actions + allowingAIMethods:NO + withContextName:@"system script_actions" + forTarget:nil]; + } +} + + +// track the position and status of the lights +BOOL sun_light_on = NO; +BOOL demo_light_on = NO; +GLfloat demo_light_position[4] = { DEMO_LIGHT_POSITION, 1.0 }; +// +GLfloat docked_light_ambient[4] = { (GLfloat) 0.4, (GLfloat) 0.4, (GLfloat) 0.4, (GLfloat) 1.0}; // bright-ish, but shaders still visible! +GLfloat docked_light_diffuse[4] = { (GLfloat) 0.7, (GLfloat) 0.7, (GLfloat) 0.7, (GLfloat) 1.0}; // whitish +GLfloat docked_light_specular[4] = { (GLfloat) 0.7, (GLfloat) 0.7, (GLfloat) 0.4, (GLfloat) 1.0}; // yellow-white + +// Weight of sun in ambient light calculation. 1.0 means only sun's diffuse is used for ambient, 0.0 means only sky colour is used. +// TODO: considering the size of the sun and the number of background stars might be worthwhile. -- Ahruman 20080322 +#define SUN_AMBIENT_INFLUENCE 0.75 + +- (void) setLighting +{ + /* + + GL_LIGHT1 is the sun and is active while a sun exists in space + where there is no sun (witch/interstellar space) this is placed at the origin + + GL_LIGHT0 is the light for inside the station and needs to have its position reset + relative to the player whenever demo ships or background scenes are to be shown + + */ + + NSDictionary* systeminfo = [self generateSystemData:system_seed]; + PlanetEntity* the_sun = [self sun]; + SkyEntity* the_sky = nil; + GLfloat sun_pos[] = {4000000.0, 0.0, 0.0, 1.0}; + int i; + for (i = n_entities - 1; i > 0; i--) + if ((sortedEntities[i]) && ([sortedEntities[i] isKindOfClass:[SkyEntity class]])) + the_sky = (SkyEntity*)sortedEntities[i]; + if (the_sun) + { + GLfloat sun_ambient[] = { 0.0, 0.0, 0.0, 1.0}; // ambient light about 5% + sun_diffuse[0] = the_sun->sun_diffuse[0]; + sun_diffuse[1] = the_sun->sun_diffuse[1]; + sun_diffuse[2] = the_sun->sun_diffuse[2]; + sun_diffuse[3] = the_sun->sun_diffuse[3]; + sun_specular[0] = the_sun->sun_specular[0]; + sun_specular[1] = the_sun->sun_specular[1]; + sun_specular[2] = the_sun->sun_specular[2]; + sun_specular[3] = the_sun->sun_specular[3]; + glLightfv(GL_LIGHT1, GL_AMBIENT, sun_ambient); + glLightfv(GL_LIGHT1, GL_DIFFUSE, sun_diffuse); + glLightfv(GL_LIGHT1, GL_SPECULAR, sun_specular); + sun_pos[0] = the_sun->position.x; + sun_pos[1] = the_sun->position.y; + sun_pos[2] = the_sun->position.z; + } + else + { + // witchspace + GLfloat sun_ambient[] = { 0.0, 0.0, 0.0, 1.0}; // ambient light nil + stars_ambient[0] = 0.05; stars_ambient[1] = 0.20; stars_ambient[2] = 0.05; stars_ambient[3] = 1.0; + sun_diffuse[0] = 0.85; sun_diffuse[1] = 1.0; sun_diffuse[2] = 0.85; sun_diffuse[3] = 1.0; + sun_specular[0] = 0.95; sun_specular[1] = 1.0; sun_specular[2] = 0.95; sun_specular[3] = 1.0; + glLightfv(GL_LIGHT1, GL_AMBIENT, sun_ambient); + glLightfv(GL_LIGHT1, GL_DIFFUSE, sun_diffuse); + glLightfv(GL_LIGHT1, GL_SPECULAR, sun_specular); + } + + glLightfv(GL_LIGHT1, GL_POSITION, sun_pos); + + if (the_sky) + { + // ambient lighting! + GLfloat r,g,b,a; + [[the_sky skyColor] getGLRed:&r green:&g blue:&b alpha:&a]; + r = r * (1.0 - SUN_AMBIENT_INFLUENCE) + sun_diffuse[0] * SUN_AMBIENT_INFLUENCE; + g = g * (1.0 - SUN_AMBIENT_INFLUENCE) + sun_diffuse[1] * SUN_AMBIENT_INFLUENCE; + b = b * (1.0 - SUN_AMBIENT_INFLUENCE) + sun_diffuse[2] * SUN_AMBIENT_INFLUENCE; + GLfloat ambient_level = [systeminfo floatForKey:@"ambient_level" defaultValue:1.0]; + stars_ambient[0] = ambient_level * 0.0625 * (1.0 + r) * (1.0 + r); + stars_ambient[1] = ambient_level * 0.0625 * (1.0 + g) * (1.0 + g); + stars_ambient[2] = ambient_level * 0.0625 * (1.0 + b) * (1.0 + b); + stars_ambient[3] = 1.0; + } + + // light for demo ships display.. + glLightfv(GL_LIGHT0, GL_AMBIENT, docked_light_ambient); + glLightfv(GL_LIGHT0, GL_DIFFUSE, docked_light_diffuse); + glLightfv(GL_LIGHT0, GL_SPECULAR, docked_light_specular); + + // glLightModel details... + + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, stars_ambient); + + glDisable(GL_LIGHT0); + demo_light_on = NO; + glDisable(GL_LIGHT1); + sun_light_on = NO; +} + + +- (void) populateSpaceFromActiveWormholes +{ + NSAutoreleasePool *pool = nil; + + while ([activeWormholes count]) + { + pool = [[NSAutoreleasePool alloc] init]; + NS_DURING + WormholeEntity* whole = [activeWormholes objectAtIndex:0]; +#if WORMHOLE_SCANNER + // If the wormhole has been scanned by the player then the + // PlayerEntity will take care of it + if (![whole isScanned] && + equal_seeds([whole destination], system_seed)) +#else + if (equal_seeds([whole destination], system_seed)) +#endif + { + // this is a wormhole to this system + [whole disgorgeShips]; + } + [activeWormholes removeObjectAtIndex:0]; // empty it out + NS_HANDLER + OOLog(kOOLogException, @"Squashing exception during wormhole unpickling (%@: %@).", [localException name], [localException reason]); + NS_ENDHANDLER + [pool release]; + } +} + + +- (void) populateSpaceFromHyperPoint:(Vector) h1_pos toPlanetPosition:(Vector) p1_pos andSunPosition:(Vector) s1_pos +{ + unsigned i, r, escortsAdded; + unsigned totalRocks = 0; + BOOL includeHermit; + unsigned clusterSize; + double asteroidLocation; + Vector launchPos; + double start, end, maxLength; + NSAutoreleasePool *pool = nil; + NSDictionary *systeminfo = nil; + BOOL sunGoneNova; + OOTechLevelID techlevel; + OOGovernmentID government; + OOEconomyID economy; + unsigned thargoidChance; + Vector lastPiratePosition = kZeroVector; + unsigned wolfPackCounter = 0; + + systeminfo = [self generateSystemData:system_seed]; + sunGoneNova = [systeminfo boolForKey:@"sun_gone_nova"]; + + techlevel = [systeminfo unsignedCharForKey:KEY_TECHLEVEL]; // 0 .. 13 + government = [systeminfo unsignedCharForKey:KEY_GOVERNMENT]; // 0 .. 7 (0 anarchic .. 7 most stable) + economy = [systeminfo unsignedCharForKey:KEY_ECONOMY]; // 0 .. 7 (0 richest .. 7 poorest) + + thargoidChance = (system_seed.e < 127) ? 10 : 3; // if Human Colonials live here, there's a greater % chance the Thargoids will attack! + + ranrot_srand([[NSDate date] timeIntervalSince1970]); // reset randomiser with current time + + OOLog(kOOLogUniversePopulate, @"Populating a system with economy \"%@\" (%u), and government \"%@\" (%u).", EconomyToString(economy), economy, GovernmentToString(government), government); + OOLogIndentIf(kOOLogUniversePopulate); + + // traders + // TODO: consider using floating point for this stuff, giving a greater variety of possible results while maintaining the same range. + unsigned trading_parties = (9 - economy); // 2 .. 9 + if (government == 0) trading_parties *= 1.25; // 25% more trade where there are no laws! + if (trading_parties > 0) + trading_parties = 1 + trading_parties * (randf()+randf()); // randomize 0..2 + while (trading_parties > 15) + trading_parties = 1 + (Ranrot() % trading_parties); // reduce + + OOLog(kOOLogUniversePopulate, @"... adding %d trading vessels", trading_parties); + + unsigned skim_trading_parties = (Ranrot() & 3) + trading_parties * (Ranrot() & 31) / 120; // about 12% + + OOLog(kOOLogUniversePopulate, @"... adding %d sun skimming vessels", skim_trading_parties); + + // pirates + int anarchy = (8 - government); + unsigned raiding_parties = (Ranrot() % anarchy) + (Ranrot() % anarchy) + anarchy * trading_parties / 3; // boosted + raiding_parties *= randf() + randf(); // randomize + while (raiding_parties > 25) + raiding_parties = 12 + (Ranrot() % raiding_parties); // reduce + + OOLog(kOOLogUniversePopulate, @"... adding %d pirate vessels", raiding_parties); + + unsigned skim_raiding_parties = ((randf() < 0.14 * economy)? 1:0) + raiding_parties * (Ranrot() & 31) / 120; // about 12% + + OOLog(kOOLogUniversePopulate, @"... adding %d sun skim pirates", skim_raiding_parties); + + // bounty-hunters and the law + unsigned hunting_parties = (1 + government) * trading_parties / 8; + if (government == 0) hunting_parties *= 1.25; // 25% more bounty hunters in an anarchy + hunting_parties *= (randf()+randf()); // randomize + while (hunting_parties > 15) + hunting_parties = 5 + (Ranrot() % hunting_parties); // reduce + + //debug + if (hunting_parties < 1) + hunting_parties = 1; + + OOLog(kOOLogUniversePopulate, @"... adding %d law/bounty-hunter vessels", hunting_parties); + + unsigned skim_hunting_parties = ((randf() < 0.14 * government)? 1:0) + hunting_parties * (Ranrot() & 31) / 160; // about 10% + + OOLog(kOOLogUniversePopulate, @"... adding %d sun skim law/bounty hunter vessels", skim_hunting_parties); + + unsigned thargoid_parties = 0; + while ((Ranrot() % 100) < thargoidChance && thargoid_parties < 16) + thargoid_parties++; + + OOLog(kOOLogUniversePopulate, @"... adding %d Thargoid warships", thargoid_parties); + + unsigned rockClusters = Ranrot() % 3; + if (trading_parties + raiding_parties + hunting_parties < 10) + rockClusters += 1 + (Ranrot() % 3); + + rockClusters *= 2; + + OOLog(kOOLogUniversePopulate, @"... adding %d asteroid clusters", rockClusters); + + unsigned total_clicks = trading_parties + raiding_parties + hunting_parties + thargoid_parties + rockClusters + skim_hunting_parties + skim_raiding_parties + skim_trading_parties; + + OOLog(kOOLogUniversePopulate, @"... for a total of %d parties", total_clicks); + OOLogOutdentIf(kOOLogUniversePopulate); + + Vector v_route1 = vector_subtract(p1_pos, h1_pos); + v_route1.x -= h1_pos.x; v_route1.y -= h1_pos.y; v_route1.z -= h1_pos.z; + double d_route1 = fast_magnitude(v_route1) - 60000.0; // -60km to avoid planet + + v_route1 = vector_normal_or_fallback(v_route1, kBasisZVector); + + // add the traders to route1 (witchspace exit to space-station / planet) + if (total_clicks < 3) total_clicks = 3; + for (i = 0; (i < trading_parties)&&(!sunGoneNova); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity *trader_ship; + launchPos = h1_pos; + r = 2 + (Ranrot() % (total_clicks - 2)); // find an empty slot + double ship_location = d_route1 * r / total_clicks; + launchPos.x += ship_location * v_route1.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y += ship_location * v_route1.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z += ship_location * v_route1.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + trader_ship = [self newShipWithRole:@"trader"]; // retain count = 1 + if (trader_ship) + { + if (![trader_ship crew]) + [trader_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"trader" + andOriginalSystem: systems[Ranrot() & 255]]]]; + + if (trader_ship->scanClass == CLASS_NOT_SET) + [trader_ship setScanClass:CLASS_NEUTRAL]; + [trader_ship setPosition:launchPos]; + [trader_ship setBounty:0]; + [trader_ship setCargoFlag:CARGO_FLAG_FULL_SCARCE]; + [trader_ship setStatus:STATUS_IN_FLIGHT]; + + if (([trader_ship pendingEscortCount] > 0)&&((Ranrot() % 7) < government)) // remove escorts if we feel safe + { + int nx = [trader_ship pendingEscortCount] - 2 * (1 + (Ranrot() & 3)); // remove 2,4,6, or 8 escorts + [trader_ship setPendingEscortCount:(nx > 0) ? nx : 0]; + } + + [self addEntity:trader_ship]; + // [[trader_ship getAI] setStateMachine:@"route1traderAI.plist"]; // must happen after adding to the universe! + [[trader_ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe to start the AI! + [trader_ship release]; + } + + [pool release]; + } + + // add the raiders to route1 (witchspace exit to space-station / planet) + // Eric 2009-07-13: reordered the wolfpack addition code for the new group code so each wolfpack group stays a seperate group. + i = 0; + while ((i < raiding_parties)&&(!sunGoneNova)) + { + OOShipGroup *wolfpackGroup = [OOShipGroup groupWithName:@"raider wolfpack"]; + wolfPackCounter = 0; + + launchPos = h1_pos; + // random group position along route1 + r = 2 + (Ranrot() % (total_clicks - 2)); // find an empty slot + double ship_location = d_route1 * r / total_clicks; + launchPos.x += ship_location * v_route1.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y += ship_location * v_route1.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z += ship_location * v_route1.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + lastPiratePosition = launchPos; + + while (((Ranrot() & 7) > wolfPackCounter || wolfPackCounter == 0) && i < raiding_parties) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity *pirate_ship; + // use last group position + launchPos = lastPiratePosition; + launchPos.x += SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5)*0.1; // pack them closer together + launchPos.y += SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5)*0.1; + launchPos.z += SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5)*0.1; + wolfPackCounter++; + + pirate_ship = [self newShipWithRole:@"pirate"]; // retain count = 1 + if (pirate_ship) + { + if (![pirate_ship crew]) + { + [pirate_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"pirate" + andOriginalSystem: (randf() > 0.25)? systems[Ranrot() & 255]:system_seed]]]; + } + + if (pirate_ship->scanClass == CLASS_NOT_SET) + { + [pirate_ship setScanClass: CLASS_NEUTRAL]; + } + [pirate_ship setPosition:launchPos]; + [pirate_ship setStatus:STATUS_IN_FLIGHT]; + [pirate_ship setBounty: 20 + government + wolfPackCounter + (Ranrot() & 7)]; + [pirate_ship setCargoFlag: CARGO_FLAG_PIRATE]; + + [self addEntity:pirate_ship]; + + [pirate_ship setGroup:wolfpackGroup]; + + // [[pirate_ship getAI] setStateMachine:@"pirateAI.plist"]; // must happen after adding to the universe! + [[pirate_ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe to start the AI! + [pirate_ship release]; + } + + [pool release]; + i++; + } + } + + // add the hunters and police ships to route1 (witchspace exit to space-station / planet) + for (i = 0; (i < hunting_parties)&&(!sunGoneNova); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity *hunter_ship; + launchPos = h1_pos; + // random position along route1 + r = 2 + (Ranrot() % (total_clicks - 2)); // find an empty slot + double ship_location = d_route1 * r / total_clicks; + launchPos.x += ship_location * v_route1.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y += ship_location * v_route1.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z += ship_location * v_route1.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + + escortsAdded = 0; + + if ((Ranrot() & 7) < government) + { + if ((Ranrot() & 7) + 6 <= techlevel) + hunter_ship = [self newShipWithRole:@"interceptor"]; // retain count = 1 + else + hunter_ship = [self newShipWithRole:@"police"]; // retain count = 1 + if (hunter_ship) + { + if (![hunter_ship crew]) + [hunter_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"police" + andOriginalSystem: (randf() > 0.05)? systems[Ranrot() & 255]:system_seed]]]; + + [hunter_ship setPrimaryRole:@"police"]; + if (hunter_ship->scanClass == CLASS_NOT_SET) + [hunter_ship setScanClass: CLASS_POLICE]; + while (((Ranrot() & 7) + 2 < government)&&([hunter_ship pendingEscortCount] < 6)) + { + [hunter_ship setPendingEscortCount:[hunter_ship pendingEscortCount] + 2]; + } + escortsAdded = [hunter_ship pendingEscortCount]; + } + } + else + { + hunter_ship = [self newShipWithRole:@"hunter"]; // retain count = 1 + if ((hunter_ship)&&(hunter_ship->scanClass == CLASS_NOT_SET)) + [hunter_ship setScanClass: CLASS_NEUTRAL]; + if (![hunter_ship crew]) + [hunter_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"hunter" + andOriginalSystem: (randf() > 0.75)? systems[Ranrot() & 255]:system_seed]]]; + + } + if (hunter_ship) + { + unsigned halfOfEscortsAdded = escortsAdded / 2; + if (hunting_parties > halfOfEscortsAdded) // ensure we are not trying to assign a negative + { // value to unsigned hunting_parties + hunting_parties -= halfOfEscortsAdded; // reduce the number needed so we don't get huge swarms! + } + + [hunter_ship setPosition:launchPos]; + [hunter_ship setStatus:STATUS_IN_FLIGHT]; + [hunter_ship setBounty:0]; + + [self addEntity:hunter_ship]; + // [[hunter_ship getAI] setStateMachine:@"route1patrolAI.plist"]; // must happen after adding to the universe! + [[hunter_ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe to start the AI! + + [hunter_ship release]; + } + + [pool release]; + } + + // add the thargoids to route1 (witchspace exit to space-station / planet) clustered together + r = 2 + (Ranrot() % (total_clicks - 2)); // find an empty slot + double thargoid_location = d_route1 * r / total_clicks; + for (i = 0; (i < thargoid_parties)&&(!sunGoneNova); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity *thargoid_ship; + launchPos.x = h1_pos.x + thargoid_location * v_route1.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y = h1_pos.y + thargoid_location * v_route1.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z = h1_pos.z + thargoid_location * v_route1.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + thargoid_ship = [self newShipWithRole:@"thargoid"]; // retain count = 1 + if (thargoid_ship) + { + if (thargoid_ship->scanClass == CLASS_NOT_SET) + [thargoid_ship setScanClass: CLASS_THARGOID]; + [thargoid_ship setPosition:launchPos]; + [thargoid_ship setBounty:100]; + [thargoid_ship setStatus:STATUS_IN_FLIGHT]; + [self addEntity:thargoid_ship]; + [[thargoid_ship getAI] setState:@"GLOBAL"]; + [thargoid_ship release]; + } + + [pool release]; + } + + // add the asteroids to route1 (witchspace exit to space-station / planet) clustered together in a preset location. + // set the system seed for random number generation + seed_RNG_only_for_planet_description(system_seed); + + for (i = 0; (i + 1) < (rockClusters / 2); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + clusterSize = 1 + (Ranrot() % 6) + (Ranrot() % 6); + r = 2 + (gen_rnd_number() % (total_clicks - 2)); // find an empty slot + asteroidLocation = d_route1 * r / total_clicks; + + launchPos = OOVectorTowards(h1_pos, v_route1, asteroidLocation); + includeHermit = (Ranrot() & 31) <= clusterSize && r < total_clicks * 2 / 3 && !sunGoneNova; + + totalRocks += [self scatterAsteroidsAt:launchPos + withVelocity:kZeroVector + includingRockHermit:includeHermit]; + + [pool release]; + } + + + // Now do route2 planet -> sun + + + Vector v_route2 = s1_pos; + v_route2.x -= p1_pos.x; v_route2.y -= p1_pos.y; v_route2.z -= p1_pos.z; + double d_route2 = sqrt(magnitude2(v_route2)); + + if (v_route2.x||v_route2.y||v_route2.z) + v_route2 = vector_normal(v_route2); + else + v_route2.x = 1.0; + + // add the traders to route2 + for (i = 0; (i < skim_trading_parties)&&(!sunGoneNova); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity* trader_ship; + Vector launchPos = p1_pos; + double start = 4.0 * [[self planet] radius]; + double end = 3.0 * [[self sun] radius]; + double maxLength = d_route2 - (start + end); + double ship_location = randf() * maxLength + start; +// + launchPos.x += ship_location * v_route2.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y += ship_location * v_route2.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z += ship_location * v_route2.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + trader_ship = [self newShipWithRole:@"sunskim-trader"]; // retain count = 1 + if (trader_ship) + { + if (![trader_ship crew]) + [trader_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"trader" + andOriginalSystem: (randf() > 0.85)? systems[Ranrot() & 255]:system_seed]]]; + + [trader_ship setPrimaryRole:@"trader"]; // set this to allow escorts to pair with the ship + if ((trader_ship)&&(trader_ship->scanClass == CLASS_NOT_SET)) + [trader_ship setScanClass: CLASS_NEUTRAL]; + [trader_ship setPosition:launchPos]; + [trader_ship setBounty:0]; + [trader_ship setCargoFlag:CARGO_FLAG_FULL_PLENTIFUL]; + if([trader_ship heatInsulation] < 7) [trader_ship setHeatInsulation:7]; // With this value most ships will survive the sun trip. + [trader_ship setStatus:STATUS_IN_FLIGHT]; + + if (([trader_ship pendingEscortCount] > 0)&&((Ranrot() % 7) < government)) // remove escorts if we feel safe + { + int nx = [trader_ship pendingEscortCount] - 2 * (1 + (Ranrot() & 3)); // remove 2,4,6, or 8 escorts + [trader_ship setPendingEscortCount:(nx > 0) ? nx : 0]; + } + + [self addEntity:trader_ship]; + // [[trader_ship getAI] setStateMachine:@"route2sunskimAI.plist"]; // must happen after adding to the universe! + [[trader_ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe to start the AI! + + [trader_ship release]; + } + + [pool release]; + } + + // add the raiders to route2 + i = 0; + while ((i < skim_raiding_parties)&&(!sunGoneNova)) + { + OOShipGroup *wolfpackGroup = [OOShipGroup groupWithName:@"raider wolfpack"]; + wolfPackCounter = 0; + + Vector launchPos = p1_pos; + // random position along route2 + double start = 4.0 * [[self planet] radius]; + double end = 3.0 * [[self sun] radius]; + double maxLength = d_route2 - (start + end); + double ship_location = randf() * maxLength + start; + launchPos.x += ship_location * v_route2.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y += ship_location * v_route2.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z += ship_location * v_route2.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + lastPiratePosition = launchPos; + + while (((Ranrot() & 7) > wolfPackCounter || wolfPackCounter == 0) && i < skim_raiding_parties) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity* pirate_ship; + // use last position + launchPos = lastPiratePosition; + launchPos.x += SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5)*0.1; // pack them closer together + launchPos.y += SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5)*0.1; + launchPos.z += SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5)*0.1; + wolfPackCounter++; + + pirate_ship = [self newShipWithRole:@"pirate"]; // retain count = 1 + if (pirate_ship) + { + if (![pirate_ship crew]) + [pirate_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"pirate" + andOriginalSystem: (randf() > 0.25)? systems[Ranrot() & 255]:system_seed]]]; + + if (pirate_ship->scanClass == CLASS_NOT_SET) + [pirate_ship setScanClass: CLASS_NEUTRAL]; + [pirate_ship setPosition: launchPos]; + [pirate_ship setStatus: STATUS_IN_FLIGHT]; + [pirate_ship setBounty: 20 + government + wolfPackCounter + (Ranrot() % 7)]; + [pirate_ship setCargoFlag: CARGO_FLAG_PIRATE]; + + [self addEntity:pirate_ship]; + + [pirate_ship setGroup:wolfpackGroup]; + + // [[pirate_ship getAI] setStateMachine:@"pirateAI.plist"]; // must happen after adding to the universe! + [[pirate_ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe to start the AI! + [pirate_ship release]; + } + + [pool release]; + i++; + } + } + + // add the hunters and police ships to route2 + for (i = 0; (i < skim_hunting_parties)&&(!sunGoneNova); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + ShipEntity* hunter_ship; + Vector launchPos = p1_pos; + double start = 4.0 * [[self planet] radius]; + double end = 3.0 * [[self sun] radius]; + double maxLength = d_route2 - (start + end); + double ship_location = randf() * maxLength + start; + + launchPos.x += ship_location * v_route2.x + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.y += ship_location * v_route2.y + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + launchPos.z += ship_location * v_route2.z + SCANNER_MAX_RANGE*((Ranrot() & 255)/256.0 - 0.5); + + if ((Ranrot() & 7) < government) + { + if ((Ranrot() & 7) + 6 <= techlevel) + hunter_ship = [self newShipWithRole:@"interceptor"]; // retain count = 1 + else + hunter_ship = [self newShipWithRole:@"police"]; // retain count = 1 + if (hunter_ship) + { + if (![hunter_ship crew]) + [hunter_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"police" + andOriginalSystem: (randf() > 0.05)? systems[Ranrot() & 255]:system_seed]]]; + + [hunter_ship setPrimaryRole:@"police"]; + if (hunter_ship->scanClass == CLASS_NOT_SET) + [hunter_ship setScanClass: CLASS_POLICE]; + while (((Ranrot() & 7) + 2 < government)&&([hunter_ship pendingEscortCount] < 6)) + { + [hunter_ship setPendingEscortCount:[hunter_ship pendingEscortCount] + 2]; + } + } + } + else + { + hunter_ship = [self newShipWithRole:@"hunter"]; // retain count = 1 + if ((hunter_ship)&&(hunter_ship->scanClass == CLASS_NOT_SET)) + [hunter_ship setScanClass: CLASS_NEUTRAL]; + if (![hunter_ship crew]) + [hunter_ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:@"hunter" + andOriginalSystem: (randf() > 0.75)? systems[Ranrot() & 255]:system_seed]]]; + + } + + if (hunter_ship) + { + [hunter_ship setPosition:launchPos]; + [hunter_ship setStatus:STATUS_IN_FLIGHT]; + [hunter_ship setBounty:0]; + + [self addEntity:hunter_ship]; + [[hunter_ship getAI] setStateMachine:@"route2patrolAI.plist"]; // this is not set by auto_ai! + + if (randf() > 0.50) // 50% chance + [[hunter_ship getAI] setState:@"HEAD_FOR_PLANET"]; + else + [[hunter_ship getAI] setState:@"HEAD_FOR_SUN"]; + + [hunter_ship release]; + } + + [pool release]; + } + + // add the asteroids to route2 clustered together in a preset location. + seed_RNG_only_for_planet_description(system_seed); // set the system seed for random number generation + + for (i = 0; i < (rockClusters / 2 + 1U); i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + start = 6.0 * [[self planet] radius]; + end = 4.5 * [[self sun] radius]; + maxLength = d_route2 - (start + end); + + asteroidLocation = randf() * maxLength + start; + clusterSize = 1 + (Ranrot() % 6) + (Ranrot() % 6); + + launchPos = OOVectorTowards(p1_pos, v_route2, asteroidLocation); + includeHermit = (Ranrot() & 31) <= clusterSize && asteroidLocation > 0.33 * maxLength && !sunGoneNova; + + totalRocks += [self scatterAsteroidsAt:launchPos + withVelocity:kZeroVector + includingRockHermit:includeHermit]; + [pool release]; + } + +} + + +- (int) scatterAsteroidsAt:(Vector) spawnPos withVelocity:(Vector) spawnVel includingRockHermit:(BOOL) spawnHermit +{ + int rocks = 0; + Vector launchPos; + int i; + int clusterSize = 1 + (Ranrot() % 6) + (Ranrot() % 6); + for (i = 0; i < clusterSize; i++) + { + ShipEntity* asteroid; + launchPos.x = spawnPos.x + SCANNER_MAX_RANGE * (randf() - randf()); + launchPos.y = spawnPos.y + SCANNER_MAX_RANGE * (randf() - randf()); + launchPos.z = spawnPos.z + SCANNER_MAX_RANGE * (randf() - randf()); + asteroid = [self newShipWithRole:@"asteroid"]; // retain count = 1 + if (asteroid) + { + if (asteroid->scanClass == CLASS_NOT_SET) + [asteroid setScanClass: CLASS_ROCK]; + [asteroid setPosition:launchPos]; + [asteroid setVelocity:spawnVel]; + [asteroid setStatus:STATUS_IN_FLIGHT]; + [self addEntity:asteroid]; + [[asteroid getAI] setState:@"GLOBAL"]; + [asteroid release]; + rocks++; + } + } + // rock-hermit : chance is related to the number of asteroids + // hermits are placed near to other asteroids for obvious reasons + + // hermits should not be placed too near the planet-end of route2, + // or ships will dock there rather than at the main station ! + + if (spawnHermit) + { + StationEntity* hermit; + launchPos.x = spawnPos.x + 0.5 * SCANNER_MAX_RANGE * (randf() - randf()); + launchPos.y = spawnPos.y + 0.5 * SCANNER_MAX_RANGE * (randf() - randf()); + launchPos.z = spawnPos.z + 0.5 * SCANNER_MAX_RANGE * (randf() - randf()); + hermit = (StationEntity *)[self newShipWithRole:@"rockhermit"]; // retain count = 1 + if (hermit) + { + if (hermit->scanClass == CLASS_NOT_SET) + [hermit setScanClass: CLASS_ROCK]; + [hermit setPosition:launchPos]; + [hermit setVelocity:spawnVel]; + [hermit setStatus:STATUS_IN_FLIGHT]; + [self addEntity:hermit]; + [[hermit getAI] setState:@"GLOBAL"]; + [hermit release]; +#if DEAD_STORE + clusterSize++; +#endif + } + } + + return rocks; +} + + +- (void) addShipWithRole:(NSString *) desc nearRouteOneAt:(double) route_fraction +{ + // adds a ship within scanner range of a point on route 1 + + Entity* the_station = [self station]; + if (!the_station) + return; + Vector h1_pos = [self getWitchspaceExitPosition]; + Vector launchPos = the_station->position; + launchPos.x -= h1_pos.x; launchPos.y -= h1_pos.y; launchPos.z -= h1_pos.z; + launchPos.x *= route_fraction; launchPos.y *= route_fraction; launchPos.z *= route_fraction; + launchPos.x += h1_pos.x; launchPos.y += h1_pos.y; launchPos.z += h1_pos.z; + + launchPos.x += SCANNER_MAX_RANGE*(randf() - randf()); + launchPos.y += SCANNER_MAX_RANGE*(randf() - randf()); + launchPos.z += SCANNER_MAX_RANGE*(randf() - randf()); + + ShipEntity *ship; + ship = [self newShipWithRole:desc]; // retain count = 1 + + // Deal with scripted cargopods and ensure they are filled with something. + if (ship && [ship hasRole:@"cargopod"]) + { + [self fillCargopodWithRandomCargo:ship]; + } + + if (ship) + { + if (![ship crew] && ![ship isUnpiloted] && !(ship->scanClass == CLASS_CARGO || ship->scanClass == CLASS_ROCK)) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: desc + andOriginalSystem: systems[Ranrot() & 255]]]]; + + if (ship->scanClass == CLASS_NOT_SET) + [ship setScanClass: CLASS_NEUTRAL]; + [ship setPosition:launchPos]; + [self addEntity:ship]; + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + + [ship setStatus:STATUS_IN_FLIGHT]; // or ships that were 'demo' ships become invisible! + + [ship release]; + } + +} + + +- (Vector) coordinatesForPosition:(Vector) pos withCoordinateSystem:(NSString *) system returningScalar:(GLfloat*) my_scalar +{ + /* the point is described using a system selected by a string + consisting of a three letter code. + + The first letter indicates the feature that is the origin of the coordinate system. + w => witchpoint + s => sun + p => planet + + The next letter indicates the feature on the 'z' axis of the coordinate system. + w => witchpoint + s => sun + p => planet + + Then the 'y' axis of the system is normal to the plane formed by the planet, sun and witchpoint. + And the 'x' axis of the system is normal to the y and z axes. + So: + ps: z axis = (planet -> sun) y axis = normal to (planet - sun - witchpoint) x axis = normal to y and z axes + pw: z axis = (planet -> witchpoint) y axis = normal to (planet - witchpoint - sun) x axis = normal to y and z axes + sp: z axis = (sun -> planet) y axis = normal to (sun - planet - witchpoint) x axis = normal to y and z axes + sw: z axis = (sun -> witchpoint) y axis = normal to (sun - witchpoint - planet) x axis = normal to y and z axes + wp: z axis = (witchpoint -> planet) y axis = normal to (witchpoint - planet - sun) x axis = normal to y and z axes + ws: z axis = (witchpoint -> sun) y axis = normal to (witchpoint - sun - planet) x axis = normal to y and z axes + + The third letter denotes the units used: + m: meters + p: planetary radii + s: solar radii + u: distance between first two features indicated (eg. spu means that u = distance from sun to the planet) + + in interstellar space (== no sun) coordinates are absolute irrespective of the system used. + + [1.71] The position code "abs" can also be used for absolute coordinates. + + */ + + NSString* l_sys = [system lowercaseString]; + if ([l_sys length] != 3) + return kZeroVector; + PlanetEntity* the_planet = [self planet]; + PlanetEntity* the_sun = [self sun]; + if (the_planet == nil || the_sun == nil || [l_sys isEqualToString:@"abs"]) + { + if (my_scalar) *my_scalar = 1.0; + return pos; + } + Vector w_pos = [self getWitchspaceExitPosition]; + Vector p_pos = the_planet->position; + Vector s_pos = the_sun->position; + + const char* c_sys = [l_sys UTF8String]; + Vector p0, p1, p2; + + switch (c_sys[0]) + { + case 'w': + p0 = w_pos; + switch (c_sys[1]) + { + case 'p': + p1 = p_pos; p2 = s_pos; break; + case 's': + p1 = s_pos; p2 = p_pos; break; + default: + return kZeroVector; + } + break; + case 'p': + p0 = p_pos; + switch (c_sys[1]) + { + case 'w': + p1 = w_pos; p2 = s_pos; break; + case 's': + p1 = s_pos; p2 = w_pos; break; + default: + return kZeroVector; + } + break; + case 's': + p0 = s_pos; + switch (c_sys[1]) + { + case 'w': + p1 = w_pos; p2 = p_pos; break; + case 'p': + p1 = p_pos; p2 = w_pos; break; + default: + return kZeroVector; + } + break; + default: + return kZeroVector; + } + Vector k = make_vector(p1.x - p0.x, p1.y - p0.y, p1.z - p0.z); + if (k.x||k.y||k.z) + k = vector_normal(k); // 'forward' + else + k.z = 1.0; + Vector v = make_vector(p2.x - p0.x, p2.y - p0.y, p2.z - p0.z); + if (v.x||v.y||v.z) + v = vector_normal(v); // temporary vector in plane of 'forward' and 'right' + else + v.x = 1.0; + Vector j = cross_product(k, v); // 'up' + Vector i = cross_product(j, k); // 'right' + GLfloat scalar = 1.0; + switch (c_sys[2]) + { + case 'p': + scalar = ([self planet])? [self planet]->collision_radius: 5000; break; + case 's': + scalar = ([self sun])? [self sun]->collision_radius: 100000; break; + case 'u': + scalar = sqrt(magnitude2(make_vector(p1.x - p0.x, p1.y - p0.y, p1.z - p0.z))); break; + case 'm': + scalar = 1.0; break; + default: + return kZeroVector; + } + if (my_scalar) + *my_scalar = scalar; + + // result = p0 + ijk + Vector result = p0; // origin + result.x += scalar * (pos.x * i.x + pos.y * j.x + pos.z * k.x); + result.y += scalar * (pos.x * i.y + pos.y * j.y + pos.z * k.y); + result.z += scalar * (pos.x * i.z + pos.y * j.z + pos.z * k.z); + + return result; +} + + +- (NSString *) expressPosition:(Vector) pos inCoordinateSystem:(NSString *) system +{ + + NSString* l_sys = [system lowercaseString]; + if ([l_sys length] != 3) + return nil; + PlanetEntity* the_planet = [self planet]; + PlanetEntity* the_sun = [self sun]; + if ((!the_planet)||(!the_sun)) + { + return [NSString stringWithFormat:@"%@ %.2f %.2f %.2f", system, pos.x, pos.y, pos.z]; + } + Vector w_pos = [self getWitchspaceExitPosition]; + Vector p_pos = the_planet->position; + Vector s_pos = the_sun->position; + + const char* c_sys = [l_sys UTF8String]; + Vector p0, p1, p2; + + switch (c_sys[0]) + { + case 'w': + p0 = w_pos; + switch (c_sys[1]) + { + case 'p': + p1 = p_pos; p2 = s_pos; break; + case 's': + p1 = s_pos; p2 = p_pos; break; + default: + return nil; + } + break; + case 'p': + p0 = p_pos; + switch (c_sys[1]) + { + case 'w': + p1 = w_pos; p2 = s_pos; break; + case 's': + p1 = s_pos; p2 = w_pos; break; + default: + return nil; + } + break; + case 's': + p0 = s_pos; + switch (c_sys[1]) + { + case 'w': + p1 = w_pos; p2 = p_pos; break; + case 'p': + p1 = p_pos; p2 = w_pos; break; + default: + return nil; + } + break; + default: + return nil; + } + Vector k = make_vector(p1.x - p0.x, p1.y - p0.y, p1.z - p0.z); + if (k.x||k.y||k.z) + k = vector_normal(k); // 'z' axis in m + else + k.z = 1.0; + Vector v = make_vector(p2.x - p0.x, p2.y - p0.y, p2.z - p0.z); + if (v.x||v.y||v.z) + v = vector_normal(v); // temporary vector in plane of 'forward' and 'right' + else + v.x = 1.0; + Vector j = cross_product(k, v); // 'y' axis in m + Vector i = cross_product(j, k); // 'x' axis in m + GLfloat scalar = 1.0; + switch (c_sys[2]) + { + case 'p': + scalar = 1.0 / (([self planet])? [self planet]->collision_radius: 5000); break; + case 's': + scalar = 1.0 / (([self sun])? [self sun]->collision_radius: 100000); break; + case 'u': + scalar = 1.0 / sqrt(magnitude2(make_vector(p1.x - p0.x, p1.y - p0.y, p1.z - p0.z))); break; + case 'm': + scalar = 1.0; break; + default: + return nil; + } + + // result = p0 + ijk + Vector r_pos = make_vector(pos.x - p0.x, pos.y - p0.y, pos.z - p0.z); + Vector result = make_vector( scalar * (r_pos.x * i.x + r_pos.y * i.y + r_pos.z * i.z), + scalar * (r_pos.x * j.x + r_pos.y * j.y + r_pos.z * j.z), + scalar * (r_pos.x * k.x + r_pos.y * k.y + r_pos.z * k.z) ); // scalar * dot_products + + return [NSString stringWithFormat:@"%@ %.2f %.2f %.2f", system, result.x, result.y, result.z]; +} + + +- (Vector) coordinatesFromCoordinateSystemString:(NSString *) system_x_y_z +{ + NSArray* tokens = ScanTokensFromString(system_x_y_z); + if ([tokens count] != 4) + { + // Not necessarily an error. + return make_vector(0,0,0); + } + GLfloat dummy; + return [self coordinatesForPosition:make_vector([tokens floatAtIndex:1], [tokens floatAtIndex:2], [tokens floatAtIndex:3]) withCoordinateSystem:[tokens stringAtIndex:0] returningScalar:&dummy]; +} + + +- (BOOL) addShipWithRole:(NSString *) desc nearPosition:(Vector) pos withCoordinateSystem:(NSString *) system +{ + // initial position + GLfloat scalar = 1.0; + Vector launchPos = [self coordinatesForPosition:pos withCoordinateSystem:system returningScalar:&scalar]; + // randomise + GLfloat rfactor = scalar; + if (rfactor > SCANNER_MAX_RANGE) + rfactor = SCANNER_MAX_RANGE; + if (rfactor < 1000) + rfactor = 1000; + launchPos.x += rfactor*(randf() - randf()); + launchPos.y += rfactor*(randf() - randf()); + launchPos.z += rfactor*(randf() - randf()); + + ShipEntity *ship; + ship = [self newShipWithRole:desc]; // retain count = 1 + + // Deal with scripted cargopods and ensure they are filled with something. + if (ship && [ship hasRole:@"cargopod"]) + { + [self fillCargopodWithRandomCargo:ship]; + } + + if (ship) + { + if (![ship crew] && ![ship isUnpiloted] && !(ship->scanClass == CLASS_CARGO || ship->scanClass == CLASS_ROCK)) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: desc + andOriginalSystem: systems[Ranrot() & 255]]]]; + + if (ship->scanClass == CLASS_NOT_SET) + [ship setScanClass: CLASS_NEUTRAL]; + [ship setPosition:launchPos]; + [self addEntity:ship]; + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + [ship setStatus:STATUS_IN_FLIGHT]; // or ships that were 'demo' ships become invisible! + + [ship release]; + return YES; + } + + return NO; +} + + +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc atPosition:(Vector) pos withCoordinateSystem:(NSString *) system +{ + // initial bounding box + GLfloat scalar = 1.0; + Vector launchPos = [self coordinatesForPosition:pos withCoordinateSystem:system returningScalar:&scalar]; + GLfloat distance_from_center = 0.0; + Vector v_from_center, ship_pos; + Vector ship_positions[howMany]; + int i = 0; + int scale_up_after = 0; + int current_shell = 0; + GLfloat walk_factor = 2.0; + while (i < howMany) + { + ShipEntity *ship; + ship = [self newShipWithRole:desc]; // retain count = 1 + if (!ship) + return NO; + + // Deal with scripted cargopods and ensure they are filled with something. + if (ship && [ship hasRole:@"cargopod"]) + { + [self fillCargopodWithRandomCargo:ship]; + } + + if (![ship crew] && ![ship isUnpiloted] && !(ship->scanClass == CLASS_CARGO || ship->scanClass == CLASS_ROCK)) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: desc + andOriginalSystem: systems[Ranrot() & 255]]]]; + + GLfloat safe_distance2 = 2.0 * ship->collision_radius * ship->collision_radius * PROXIMITY_WARN_DISTANCE2; + + BOOL safe; + + int limit_count = 8; + + v_from_center = kZeroVector; + do + { + do + { + v_from_center.x += walk_factor * (randf() - 0.5); + v_from_center.y += walk_factor * (randf() - 0.5); + v_from_center.z += walk_factor * (randf() - 0.5); // drunkards walk + } while ((v_from_center.x == 0.0)&&(v_from_center.y == 0.0)&&(v_from_center.z == 0.0)); + v_from_center = vector_normal(v_from_center); // guaranteed non-zero + + ship_pos = make_vector( launchPos.x + distance_from_center * v_from_center.x, + launchPos.y + distance_from_center * v_from_center.y, + launchPos.z + distance_from_center * v_from_center.z); + + // check this position against previous ship positions in this shell + safe = YES; + int j = i - 1; + while (safe && (j >= current_shell)) + { + safe = (safe && (distance2(ship_pos, ship_positions[j]) > safe_distance2)); + j--; + } + if (!safe) + { + limit_count--; + if (!limit_count) // give up and expand the shell + { + limit_count = 8; + distance_from_center += sqrt(safe_distance2); // expand to the next distance + } + } + + + } while (!safe); + + if (ship->scanClass == CLASS_NOT_SET) + [ship setScanClass: CLASS_NEUTRAL]; + [ship setPosition:ship_pos]; + + Quaternion qr; + quaternion_set_random(&qr); + [ship setOrientation:qr]; + + [self addEntity:ship]; + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + [ship setStatus:STATUS_IN_FLIGHT]; // or ships that were 'demo' ships become invisible! + [ship release]; + + ship_positions[i] = ship_pos; + i++; + if (i > scale_up_after) + { + current_shell = i; + scale_up_after += 1 + 2 * i; + distance_from_center += sqrt(safe_distance2); // fill the next shell + } + } + return YES; +} + + +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc nearPosition:(Vector) pos withCoordinateSystem:(NSString *) system +{ + // initial bounding box + GLfloat scalar = 1.0; + Vector launchPos = [self coordinatesForPosition:pos withCoordinateSystem:system returningScalar:&scalar]; + GLfloat rfactor = scalar; + if (rfactor > SCANNER_MAX_RANGE) + rfactor = SCANNER_MAX_RANGE; + if (rfactor < 1000) + rfactor = 1000; + BoundingBox launch_bbox; + bounding_box_reset_to_vector(&launch_bbox, make_vector(launchPos.x - rfactor, launchPos.y - rfactor, launchPos.z - rfactor)); + bounding_box_add_xyz(&launch_bbox, launchPos.x + rfactor, launchPos.y + rfactor, launchPos.z + rfactor); + + return [self addShips: howMany withRole: desc intoBoundingBox: launch_bbox]; +} + + +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc nearPosition:(Vector) pos withCoordinateSystem:(NSString *) system withinRadius:(GLfloat) radius +{ + // initial bounding box + GLfloat scalar = 1.0; + Vector launchPos = [self coordinatesForPosition:pos withCoordinateSystem:system returningScalar:&scalar]; + GLfloat rfactor = radius; + if (rfactor < 1000) + rfactor = 1000; + BoundingBox launch_bbox; + bounding_box_reset_to_vector(&launch_bbox, make_vector(launchPos.x - rfactor, launchPos.y - rfactor, launchPos.z - rfactor)); + bounding_box_add_xyz(&launch_bbox, launchPos.x + rfactor, launchPos.y + rfactor, launchPos.z + rfactor); + + return [self addShips: howMany withRole: desc intoBoundingBox: launch_bbox]; +} + + +- (BOOL) addShips:(int) howMany withRole:(NSString *) desc intoBoundingBox:(BoundingBox) bbox +{ + if (howMany < 1) + return YES; + if (howMany > 1) + { + // divide the number of ships in two + int h0 = howMany / 2; + int h1 = howMany - h0; + // split the bounding box into two along its longest dimension + GLfloat lx = bbox.max.x - bbox.min.x; + GLfloat ly = bbox.max.y - bbox.min.y; + GLfloat lz = bbox.max.z - bbox.min.z; + BoundingBox bbox0 = bbox; + BoundingBox bbox1 = bbox; + if ((lx > lz)&&(lx > ly)) // longest dimension is x + { + bbox0.min.x += 0.5 * lx; + bbox1.max.x -= 0.5 * lx; + } + else + { + if (ly > lz) // longest dimension is y + { + bbox0.min.y += 0.5 * ly; + bbox1.max.y -= 0.5 * ly; + } + else // longest dimension is z + { + bbox0.min.z += 0.5 * lz; + bbox1.max.z -= 0.5 * lz; + } + } + // place half the ships into each bounding box + return ([self addShips: h0 withRole: desc intoBoundingBox: bbox0] && [self addShips: h1 withRole: desc intoBoundingBox: bbox1]); + } + + // randomise within the bounding box (biased towards the center of the box) + Vector pos = make_vector(bbox.min.x, bbox.min.y, bbox.min.z); + pos.x += 0.5 * (randf() + randf()) * (bbox.max.x - bbox.min.x); + pos.y += 0.5 * (randf() + randf()) * (bbox.max.y - bbox.min.y); + pos.z += 0.5 * (randf() + randf()) * (bbox.max.z - bbox.min.z); + + + ShipEntity *ship; + ship = [self newShipWithRole:desc]; // retain count = 1 + + // Deal with scripted cargopods and ensure they are filled with something. + if (ship && [ship hasRole:@"cargopod"]) + { + [self fillCargopodWithRandomCargo:ship]; + } + + if (ship) + { + if (![ship crew] && ![ship isUnpiloted] && !(ship->scanClass == CLASS_CARGO || ship->scanClass == CLASS_ROCK)) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: desc + andOriginalSystem: systems[Ranrot() & 255]]]]; + + if (ship->scanClass == CLASS_NOT_SET) + [ship setScanClass: CLASS_NEUTRAL]; + [ship setPosition: pos]; + [self addEntity:ship]; + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + [ship setStatus:STATUS_IN_FLIGHT]; // or ships that were 'demo' ships become invisible! + + [ship release]; + + return YES; // success at last! + } + return NO; +} + + +- (BOOL) spawnShip:(NSString *) shipdesc +{ + ShipEntity* ship; + NSDictionary* shipdict = nil; + + shipdict = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipdesc]; + if (shipdict == nil) return NO; + + ship = [self newShipWithName:shipdesc]; // retain count is 1 + + if (ship == nil) return NO; + + // set any spawning characteristics + NSDictionary *spawndict = [shipdict dictionaryForKey:@"spawn"]; + Vector pos, rpos, spos; + NSString *positionString = nil; + + // position + positionString = [spawndict stringForKey:@"position"]; + if (positionString != nil) + { + if([positionString hasPrefix:@"abs "] && ([self planet] != nil || [self sun] !=nil)) + { + OOLogWARN(@"script.deprecated", @"setting %@ for %@ '%@' in 'abs' inside .plists can cause compatibility issues across Oolite versions. Use coordinates relative to main system objects instead.",@"position",@"entity",shipdesc); + } + + pos = [self coordinatesFromCoordinateSystemString:positionString]; + [ship setPosition:pos]; + } + + // facing_position + positionString = [spawndict stringForKey:@"facing_position"]; + if (positionString != nil) + { + if([positionString hasPrefix:@"abs "] && ([self planet] != nil || [self sun] !=nil)) + { + OOLogWARN(@"script.deprecated", @"setting %@ for %@ '%@' in 'abs' inside .plists can cause compatibility issues across Oolite versions. Use coordinates relative to main system objects instead.",@"facing_position",@"entity",shipdesc); + } + + spos = [ship position]; + Quaternion q1; + rpos = [self coordinatesFromCoordinateSystemString:positionString]; + rpos.x -= spos.x; rpos.y -= spos.y; rpos.z -= spos.z; // position relative to ship + if (rpos.x || rpos.y || rpos.z) + { + rpos = vector_normal(rpos); + q1 = quaternion_rotation_between(make_vector(0,0,1), rpos); + + GLfloat check = dot_product(vector_forward_from_quaternion(q1), rpos); + if (check < 0) + quaternion_rotate_about_axis(&q1, vector_right_from_quaternion(q1), M_PI); // 180 degree flip + + [ship setOrientation:q1]; + } + } + + [self addEntity:ship]; + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + [ship setStatus:STATUS_IN_FLIGHT]; // or ships that were 'demo' ships become invisible! + [ship release]; + + return YES; +} + + +- (void) witchspaceShipWithPrimaryRole:(NSString *)role +{ + // adds a ship exiting witchspace (corollary of when ships leave the system) + ShipEntity *ship = nil; + NSDictionary *systeminfo = nil; + OOGovernmentID government; + + government = [systeminfo unsignedCharForKey:KEY_GOVERNMENT]; + systeminfo = [self generateSystemData:system_seed]; + + ship = [self newShipWithRole:role]; // retain count = 1 + + // Deal with scripted cargopods and ensure they are filled with something. + if (ship && [ship hasRole:@"cargopod"]) + { + [self fillCargopodWithRandomCargo:ship]; + } + + if (ship) + { + if ((ship->scanClass == CLASS_NO_DRAW)||(ship->scanClass == CLASS_NOT_SET)) + [ship setScanClass: CLASS_NEUTRAL]; + if ([role isEqual:@"trader"]) + { + [ship setCargoFlag: CARGO_FLAG_FULL_SCARCE]; + if (randf() > 0.10) + [[ship getAI] setStateMachine:@"route1traderAI.plist"]; + else + [[ship getAI] setStateMachine:@"route2sunskimAI.plist"]; // route3 really, but the AI's the same + + if (([ship pendingEscortCount] > 0)&&((Ranrot() % 7) < government)) // remove escorts if we feel safe + { + int nx = [ship pendingEscortCount] - 2 * (1 + (Ranrot() & 3)); // remove 2,4,6, or 8 escorts + [ship setPendingEscortCount:(nx > 0) ? nx : 0]; + } + } + if ([role isEqual:@"pirate"]) + { + [ship setCargoFlag: CARGO_FLAG_PIRATE]; + [ship setBounty: (Ranrot() & 7) + (Ranrot() & 7) + ((randf() < 0.05)? 63 : 23)]; // they already have a price on their heads + } + if (![ship crew] && ![ship isUnpiloted] && !(ship->scanClass == CLASS_CARGO || ship->scanClass == CLASS_ROCK)) + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole:role + andOriginalSystem: systems[Ranrot() & 255]]]]; + + [ship leaveWitchspace]; // gets added to the universe here! + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + [ship setStatus:STATUS_IN_FLIGHT]; // or ships may not werk rite d'uh! + + [ship release]; + } +} + + +// adds a ship within the collision radius of the other entity +- (ShipEntity *) spawnShipWithRole:(NSString *) desc near:(Entity *) entity +{ + if (entity == nil) return nil; + + ShipEntity *ship = nil; + Vector spawn_pos; + Quaternion spawn_q; + GLfloat offset = (randf() + randf()) * entity->collision_radius; + + quaternion_set_random(&spawn_q); + spawn_pos = vector_add([entity position], vector_multiply_scalar(vector_forward_from_quaternion(spawn_q), offset)); + + ship = [self newShipWithRole:desc]; // retain count = 1 + + // Deal with scripted cargopods and ensure they are filled with something. + if ([ship hasRole:@"cargopod"]) + { + [self fillCargopodWithRandomCargo:ship]; + } + + if (ship != nil) + { + if (![ship crew] && ![ship isUnpiloted] && !(ship->scanClass == CLASS_CARGO || ship->scanClass == CLASS_ROCK)) + { + [ship setCrew:[NSArray arrayWithObject: + [OOCharacter randomCharacterWithRole: desc + andOriginalSystem: systems[Ranrot() & 255]]]]; + } + + if (ship->scanClass == CLASS_NOT_SET) + { + [ship setScanClass: CLASS_NEUTRAL]; + } + [ship setPosition:spawn_pos]; + [ship setOrientation:spawn_q]; + [self addEntity:ship]; + [[ship getAI] setState:@"GLOBAL"]; // must happen after adding to the universe! + [ship setStatus:STATUS_IN_FLIGHT]; + [ship autorelease]; + } + + return ship; +} + + +- (BOOL) roleIsPirateVictim:(NSString *)role +{ + return [pirateVictimRoles containsObject:role]; +} + + +- (void) set_up_break_pattern:(Vector) pos quaternion:(Quaternion) q +{ + int i; + RingEntity* ring; + + [self setViewDirection:VIEW_FORWARD]; + + q.w = -q.w; // reverse the quaternion because this is from the player's viewpoint + + Vector v = vector_forward_from_quaternion(q); + + for (i = 1; i < 11; i++) + { + ring = [[RingEntity alloc] init]; + [ring setPositionX:pos.x+v.x*i*50.0 y:pos.y+v.y*i*50.0 z:pos.z+v.z*i*50.0]; // ahead of the player + [ring setOrientation:q]; + [ring setVelocity:v]; + [ring setLifetime:i*50.0]; + [ring setScanClass: CLASS_NO_DRAW]; + [self addEntity:ring]; + breakPatternCounter++; + [ring release]; + } +} + + +- (void) game_over +{ + [self reinitAndShowDemo:NO]; +} + +- (void) setupIntroFirstGo: (BOOL) justCobra +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + ShipEntity *ship; + Quaternion q2; + q2.x = 0.0; q2.y = 0.0; q2.z = 0.0; q2.w = 1.0; + quaternion_rotate_about_y(&q2,M_PI); + + // in status demo draw ships and display text + if (justCobra==NO) + { + [self removeDemoShips]; + } + [player setStatus: STATUS_START_GAME]; + [player setShowDemoShips: YES]; + displayGUI = YES; + + if (justCobra==YES) + { + /*- cobra -*/ + ship = [self newShipWithName:PLAYER_SHIP_DESC]; // retain count = 1 // shows the cobra-player ship + } + else + { + /*- demo ships -*/ + demo_ship_index = 0; + ship = [self newShipWithName:[demo_ships stringAtIndex:0]]; // retain count = 1 + } + + if (ship) + { + [ship setOrientation:q2]; + [ship setPositionX:0.0f y:0.0f z:3.6f * ship->collision_radius]; + if (justCobra==NO) + { + [ship setDestination: ship->position]; // ideal position + } + [ship setScanClass: CLASS_NO_DRAW]; + [ship setRoll:M_PI/5.0]; + [ship setPitch:M_PI/10.0]; + [[ship getAI] setStateMachine:@"nullAI.plist"]; + if([ship pendingEscortCount] > 0) [ship setPendingEscortCount:0]; + [self addEntity:ship]; + // set status here because addEntity may affect status + [ship setStatus:STATUS_COCKPIT_DISPLAY]; + demo_ship = ship; + if (justCobra==NO) + { + [gui setText:[ship displayName] forRow:19 align:GUI_ALIGN_CENTER]; + [gui setColor:[OOColor whiteColor] forRow:19]; + } + [ship release]; + } + + [self setViewDirection:VIEW_GUI_DISPLAY]; + displayGUI = YES; + if (justCobra==NO) + { + demo_stage = DEMO_SHOW_THING; + demo_stage_time = universal_time + 3.0; + } + +} + + +- (void) selectIntro2Previous +{ + demo_stage = DEMO_SHOW_THING; + demo_ship_index = (demo_ship_index + [demo_ships count] - 2) % [demo_ships count]; + demo_stage_time = universal_time - 1.0; // force change +} + + +- (void) selectIntro2Next +{ + demo_stage = DEMO_SHOW_THING; + demo_stage_time = universal_time - 1.0; // force change +} + + +static BOOL IsCandidateMainStationPredicate(Entity *entity, void *parameter) +{ + return [entity isStation] && !entity->isExplicitlyNotMainStation; +} + + +- (StationEntity *) station +{ + if (cachedSun != nil && cachedStation == nil) + { + cachedStation = [self findOneEntityMatchingPredicate:IsCandidateMainStationPredicate + parameter:nil]; + } + return cachedStation; +} + + +- (PlanetEntity *) planet +{ + if (cachedPlanet == nil && [allPlanets count] != 0) + { + cachedPlanet = [allPlanets objectAtIndex:0]; + } + return cachedPlanet; +} + + +- (PlanetEntity *) sun +{ + if (cachedSun == nil) + { + cachedSun = [self findOneEntityMatchingPredicate:IsSunPredicate parameter:nil]; + } + return cachedSun; +} + + +- (NSArray *) planets +{ + return [[allPlanets copy] autorelease]; +} + + +- (void) unMagicMainStation +{ + StationEntity *theStation = [self station]; + if (theStation != nil) theStation->isExplicitlyNotMainStation = YES; + cachedStation = nil; +} + + +- (void) resetBeacons +{ + ShipEntity *beaconShip = [self firstBeacon]; + while (beaconShip) + { + firstBeacon = [beaconShip nextBeaconID]; + [beaconShip setNextBeacon:nil]; + beaconShip = (ShipEntity *)[self entityForUniversalID:firstBeacon]; + } + firstBeacon = NO_TARGET; + lastBeacon = NO_TARGET; +} + + +- (ShipEntity *) firstBeacon +{ + return (ShipEntity *)[self entityForUniversalID:firstBeacon]; +} + + +- (ShipEntity *) lastBeacon +{ + return (ShipEntity *)[self entityForUniversalID:lastBeacon]; +} + + +- (void) setNextBeacon:(ShipEntity *) beaconShip +{ + if ([beaconShip isBeacon]) + { + [beaconShip setNextBeacon:nil]; + if ([self lastBeacon]) + [[self lastBeacon] setNextBeacon:beaconShip]; + lastBeacon = [beaconShip universalID]; + if (![self firstBeacon]) + firstBeacon = lastBeacon; + } + else + { + OOLog(@"universe.beacon.error", @"***** ERROR: Universe setNextBeacon '%@'. The ship has no beaconChar set.", beaconShip); + } +} + + +- (GLfloat *) skyClearColor +{ + return skyClearColor; +} + + +- (void) setSkyColorRed:(GLfloat)red green:(GLfloat)green blue:(GLfloat)blue alpha:(GLfloat)alpha +{ + skyClearColor[0] = red; + skyClearColor[1] = green; + skyClearColor[2] = blue; + skyClearColor[3] = alpha; + airResistanceFactor = alpha; +} + + +- (BOOL) breakPatternOver +{ + return (breakPatternCounter == 0); +} + + +- (BOOL) breakPatternHide +{ + Entity* player = [PlayerEntity sharedPlayer]; + return ((breakPatternCounter > 5)||(!player)||([player status] == STATUS_DOCKING)); +} + + +#define PROFILE_SHIP_SELECTION 1 + + +- (BOOL) canInstantiateShip:(NSString *)shipKey +{ + NSDictionary *shipInfo = nil; + NSArray *conditions = nil; + + shipInfo = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipKey]; + conditions = [shipInfo arrayForKey:@"conditions"]; + if (conditions == nil) return YES; + + // Check conditions + return [[PlayerEntity sharedPlayer] scriptTestConditions:conditions]; +} + + +- (NSString *) randomShipKeyForRoleRespectingConditions:(NSString *)role +{ + OOShipRegistry *registry = [OOShipRegistry sharedRegistry]; + NSString *shipKey = nil; + OOMutableProbabilitySet *pset = nil; + +#if PROFILE_SHIP_SELECTION + static unsigned long profTotal = 0, profSlowPath = 0; + ++profTotal; +#endif + + // Select a ship, check conditions and return it if possible. + shipKey = [registry randomShipKeyForRole:role]; + if ([self canInstantiateShip:shipKey]) return shipKey; + + /* If we got here, condition check failed. + We now need to keep trying until we either find an acceptable ship or + run out of candidates. + This is special-cased because it has more overhead than the more + common conditionless lookup. + */ + +#if PROFILE_SHIP_SELECTION + ++profSlowPath; + if ((profSlowPath % 10) == 0) // Only print every tenth slow path, to reduce spamminess. + { + OOLog(@"shipRegistry.selection.profile", @"Hit slow path in ship selection for role \"%@\", having selected ship \"%@\". Now %lu of %lu on slow path (%f%%).", role, shipKey, profSlowPath, profTotal, ((double)profSlowPath)/((double)profTotal) * 100.0f); + } +#endif + + pset = [[[registry probabilitySetForRole:role] mutableCopy] autorelease]; + + while ([pset count] > 0) + { + // Select a ship, check conditions and return it if possible. + shipKey = [pset randomObject]; + if ([self canInstantiateShip:shipKey]) return shipKey; + + // Condition failed -> remove ship from consideration. + [pset removeObject:shipKey]; + } + + // If we got here, some ships existed but all failed conditions test. + return nil; +} + + +- (ShipEntity *) newShipWithRole:(NSString *)role +{ + ShipEntity *ship = nil; + NSString *shipKey = nil; + NSDictionary *shipInfo = nil; + NSString *autoAI = nil; + + shipKey = [self randomShipKeyForRoleRespectingConditions:role]; + if (shipKey != nil) + { + ship = [self newShipWithName:shipKey]; + if (ship != nil) + { + [ship setPrimaryRole:role]; + + shipInfo = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipKey]; + if ([shipInfo fuzzyBooleanForKey:@"auto_ai" defaultValue:YES]) + { + // Set AI based on role + autoAI = [self defaultAIForRole:role]; + if (autoAI != nil) + { + [ship setAITo:autoAI]; + // Nikos 20090604 + // Pirate or trader with auto_ai? Follow populator rules for them. + if ([role isEqualToString:@"pirate"]) [ship setBounty:20 + randf() * 50]; + if ([role isEqualToString:@"trader"]) [ship setBounty:0]; + } + } + } + } + + return ship; +} + + +- (ShipEntity *) newShipWithName:(NSString *)shipKey +{ + NSDictionary *shipDict = nil; + ShipEntity *ship = nil; + + shipDict = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipKey]; + + if (shipDict == nil) return nil; + + BOOL isStation = NO; + NSString *shipRoles = [shipDict stringForKey:@"roles"]; + if (shipRoles != nil) isStation = ([shipRoles rangeOfString:@"station"].location != NSNotFound)||([shipRoles rangeOfString:@"carrier"].location != NSNotFound); + isStation = [shipDict boolForKey:@"isCarrier" defaultValue:isStation]; + + volatile Class shipClass; + if (!isStation) shipClass = [ShipEntity class]; + else shipClass = [StationEntity class]; + + NS_DURING + ship =[[shipClass alloc] initWithDictionary:shipDict]; + NS_HANDLER + [ship release]; + ship = nil; + + if ([[localException name] isEqual:OOLITE_EXCEPTION_DATA_NOT_FOUND]) + { + OOLog(kOOLogException, @"***** Oolite Exception : '%@' in [Universe newShipWithName: %@ ] *****", [localException reason], shipKey); + } + else [localException raise]; + NS_ENDHANDLER + + // Set primary role to same as ship name, if ship name is also a role. + // Otherwise, if caller doesn't set a role, one will be selected randomly. + if ([ship hasRole:shipKey]) [ship setPrimaryRole:shipKey]; + + // MKW 20090327 - retain count is actually 2! + return ship; // retain count = 1 +} + + +- (NSString *)defaultAIForRole:(NSString *)role +{ + return [autoAIMap stringForKey:role]; +} + + +- (OOCargoQuantity) maxCargoForShip:(NSString *) desc +{ + NSDictionary *dict = nil; + + dict = [[OOShipRegistry sharedRegistry] shipInfoForKey:desc]; + + if (dict) + { + return [dict unsignedIntForKey:@"max_cargo" defaultValue:0]; + } + else return 0; +} + +/* + * Price for an item expressed in 10ths of credits (divide by 10 to get credits) + */ +- (OOCreditsQuantity) getPriceForWeaponSystemWithKey:(NSString *)weapon_key +{ + unsigned i, count; + NSArray *itemData = nil; + NSString *itemType = nil; + + count = [equipmentData count]; + for (i = 0; i < count; i++) + { + itemData = [equipmentData arrayAtIndex:i]; + itemType = [itemData stringAtIndex:EQUIPMENT_KEY_INDEX]; + + if ([itemType isEqual:weapon_key]) + { + return [itemData unsignedLongLongAtIndex:EQUIPMENT_PRICE_INDEX]; + } + } + return 0; +} + + +- (int) legal_status_of_manifest:(NSArray *)manifest +{ + unsigned i, count; + unsigned penalty = 0; + NSString *commodity = nil; + OOCargoQuantity amount; + NSArray *entry = nil; + unsigned penaltyPerUnit; + + count = [manifest count]; + for (i = 0; i < count; i++) + { + entry = [manifest arrayAtIndex:i]; + commodity = [entry stringAtIndex:MARKET_NAME]; + amount = [entry unsignedIntAtIndex:MARKET_QUANTITY]; + + penaltyPerUnit = [illegal_goods unsignedIntForKey:commodity defaultValue:0]; + penalty += amount * penaltyPerUnit; + } + return penalty; +} + + +- (NSArray *) getContainersOfGoods:(OOCargoQuantity)how_many scarce:(BOOL)scarce +{ + /* build list of goods allocating 0..100 for each based on how much of + each quantity there is. Use a ratio of n x 100/64 for plentiful goods; + reverse the probabilities for scarce goods. + */ + NSMutableArray *accumulator = [NSMutableArray arrayWithCapacity:how_many]; + OOCargoQuantity quantities[[commodityData count]]; + OOCargoQuantity total_quantity = 0; + unsigned i; + for (i = 0; i < [commodityData count]; i++) + { + OOCargoQuantity q = [[commodityData arrayAtIndex:i] unsignedIntAtIndex:MARKET_QUANTITY]; + if (scarce) + { + if (q < 64) q = 64 - q; + else q = 0; + } + if (q > 64) q = 64; + q *= 100; q/= 64; + quantities[i] = q; + total_quantity += q; + } + // quantities is now used to determine which good get into the containers + for (i = 0; i < how_many; i++) + { + ShipEntity* container = [self newShipWithRole:@"cargopod"]; // retained + + // look for a pre-set filling + OOCargoType co_type = [container commodityType]; + OOCargoQuantity co_amount = [container commodityAmount]; + + if ((co_type == CARGO_UNDEFINED)||(co_amount == 0)) + { + // choose a random filling + // select a random point in the histogram + int qr=0; + if(total_quantity) + qr = Ranrot() % total_quantity; + + co_type = 0; + while (qr > 0) + { + qr -= quantities[co_type++]; + } + co_type--; + + co_amount = [self getRandomAmountOfCommodity:co_type]; + + ShipEntity* special_container = [self newShipWithRole: [self symbolicNameForCommodity:co_type]]; + if (special_container) + { + [container release]; + container = special_container; + } + } + + // into the barrel it goes... + if (container != nil) + { + [container setScanClass: CLASS_CARGO]; + [container setCommodity:co_type andAmount:co_amount]; + [accumulator addObject:container]; + [container release]; // released + } + } + return [NSArray arrayWithArray:accumulator]; +} + + +- (NSArray *) getContainersOfDrugs:(OOCargoQuantity) how_many +{ + return [self getContainersOfCommodity:@"Narcotics" :how_many]; +} + + +- (NSArray *) getContainersOfCommodity:(NSString*) commodity_name :(OOCargoQuantity) how_many +{ + NSMutableArray *accumulator = [NSMutableArray arrayWithCapacity:how_many]; + OOCargoType commodity_type = [self commodityForName: commodity_name]; + if (commodity_type == CARGO_UNDEFINED) return [NSArray array]; // empty array + OOCargoQuantity commodity_units = [self unitsForCommodity:commodity_type]; + OOCargoQuantity how_much = how_many; + + while (how_much > 0) + { + ShipEntity *container = [self newShipWithRole: commodity_name]; // try the commodity name first + if (!container) container = [self newShipWithRole:@"cargopod"]; + OOCargoQuantity amount = 1; + if (commodity_units != 0) + amount += Ranrot() & (15 * commodity_units); + if (amount > how_much) + amount = how_much; + // into the barrel it goes... + if (container) + { + [container setScanClass: CLASS_CARGO]; + [container setCommodity:commodity_type andAmount:amount]; + [accumulator addObject:container]; + [container release]; + } + else + { + OOLog(@"universe.createContainer.failed", @"***** ERROR: failed to find a container to fill with %@.", commodity_name); + } + how_much -= amount; + } + return [NSArray arrayWithArray:accumulator]; +} + + +- (void) fillCargopodWithRandomCargo:(ShipEntity *)cargopod +{ + if (cargopod == nil || ![cargopod hasRole:@"cargopod"]) return; + + if ([cargopod commodityType] == CARGO_UNDEFINED || ![cargopod commodityAmount]) + { + OOCargoType aCommodity = [self getRandomCommodity]; + OOCargoQuantity aQuantity = [self getRandomAmountOfCommodity:aCommodity]; + [cargopod setCommodity:aCommodity andAmount:aQuantity]; + } +} + + +- (OOCargoType) getRandomCommodity +{ + return Ranrot() % [commodityData count]; +} + + +- (OOCargoQuantity) getRandomAmountOfCommodity:(OOCargoType) co_type +{ + OOMassUnit units; + unsigned commidityIndex = (unsigned)co_type; + + if (co_type < 0 || [commodityData count] <= commidityIndex) { + return 0; + } + units = [[commodityData arrayAtIndex:commidityIndex] intAtIndex:MARKET_UNITS]; + switch (units) + { + case 0 : // TONNES + return 1; + break; + case 1 : // KILOGRAMS + return 1 + (Ranrot() % 6) + (Ranrot() % 6) + (Ranrot() % 6); + break; + case 2 : // GRAMS + return 4 + 3 * (Ranrot() % 6) + 2 * (Ranrot() % 6) + (Ranrot() % 6); + break; + } + return 1; +} + + +- (NSArray *)commodityDataForType:(OOCargoType)type +{ + if (type < 0 || [commodityData count] <= (unsigned)type) return nil; + + return [commodityData arrayAtIndex:type]; +} + + +- (OOCargoType) commodityForName:(NSString *) co_name +{ + unsigned i, count; + NSString *name; + + count = [commodityData count]; + for (i = 0; i < count; i++) + { + /* Bug: NSNotFound being returned for valid names. + Analysis: Looking for name in commodityData rather than ith element. + Fix: look in [commodityData objectAtIndex:i]. + -- Ahruman 20070714 + */ + name = [[commodityData arrayAtIndex:i] stringAtIndex:MARKET_NAME]; + if ([co_name caseInsensitiveCompare:name] == NSOrderedSame) + { + return i; + } + } + return CARGO_UNDEFINED; +} + + +- (NSString *) symbolicNameForCommodity:(OOCargoType) co_type +{ + NSArray *commodity = [self commodityDataForType:co_type]; + + if (commodity == nil) return @""; + + return [commodity stringAtIndex:MARKET_NAME]; +} + + +- (NSString *) displayNameForCommodity:(OOCargoType) co_type +{ + return CommodityDisplayNameForSymbolicName([self symbolicNameForCommodity:co_type]); +} + + +- (OOMassUnit) unitsForCommodity:(OOCargoType)co_type +{ + NSArray *commodity = [self commodityDataForType:co_type]; + + if (commodity == nil) return NSNotFound; + + return [commodity intAtIndex:MARKET_UNITS]; +} + + + +- (NSString *) describeCommodity:(OOCargoType) co_type amount:(OOCargoQuantity) co_amount +{ + int units; + NSString *unitDesc = nil, *typeDesc = nil; + NSArray *commodity = [self commodityDataForType:co_type]; + + if (commodity == nil) return @""; + + units = [commodity intAtIndex:MARKET_UNITS]; + if (co_amount == 1) + { + switch (units) + { + case UNITS_KILOGRAMS : // KILOGRAM + unitDesc = DESC(@"cargo-kilogram"); + break; + case UNITS_GRAMS : // GRAM + unitDesc = DESC(@"cargo-gram"); + break; + case UNITS_TONS : // TONNE + default : + unitDesc = DESC(@"cargo-ton"); + break; + } + } + else + { + switch (units) + { + case UNITS_KILOGRAMS : // KILOGRAMS + unitDesc = DESC(@"cargo-kilograms"); + break; + case UNITS_GRAMS : // GRAMS + unitDesc = DESC(@"cargo-grams"); + break; + case UNITS_TONS : // TONNES + default : + unitDesc = DESC(@"cargo-tons"); + break; + } + } + + typeDesc = CommodityDisplayNameForCommodityArray(commodity); + + return [NSString stringWithFormat:@"%d %@ %@",co_amount, unitDesc, typeDesc]; +} + +//////////////////////////////////////////////////// + +- (void) setGameView:(MyOpenGLView *)view +{ + [gameView release]; + gameView = view; + [gameView retain]; +} + + +- (MyOpenGLView *) gameView +{ + return gameView; +} + + +- (GameController *) gameController +{ + return [gameView gameController]; +} + + +void setSunLight(BOOL yesno) +{ + if (yesno != sun_light_on) + { + if (yesno) + glEnable(GL_LIGHT1); + else + glDisable(GL_LIGHT1); + sun_light_on = yesno; + } +} +void setDemoLight(BOOL yesno, Vector position) +{ + if (yesno != demo_light_on) + { + if ((demo_light_position[0] != position.x)||(demo_light_position[1] != position.y)||(demo_light_position[2] != position.z)) + { + demo_light_position[0] = position.x; + demo_light_position[1] = position.y; + demo_light_position[2] = position.z; + glLightfv(GL_LIGHT0, GL_POSITION, demo_light_position); + } + if (yesno) + glEnable(GL_LIGHT0); + else + glDisable(GL_LIGHT0); + demo_light_on = yesno; + } +} + + +// global rotation matrix definitions +static const OOMatrix fwd_matrix = + {{ + { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }}; +static const OOMatrix aft_matrix = + {{ + {-1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, -1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }}; +static const OOMatrix port_matrix = + {{ + { 0.0f, 0.0f, -1.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }}; +static const OOMatrix starboard_matrix = + {{ + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + {-1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } + }}; + + +- (void) getActiveViewMatrix:(OOMatrix *)outMatrix forwardVector:(Vector *)outForward upVector:(Vector *)outUp +{ + assert(outMatrix != NULL && outForward != NULL && outUp != NULL); + + PlayerEntity *player = nil; + + switch (viewDirection) + { + case VIEW_AFT: + *outMatrix = aft_matrix; + *outForward = vector_flip(kBasisZVector); + *outUp = kBasisYVector; + return; + + case VIEW_PORT: + *outMatrix = port_matrix; + *outForward = vector_flip(kBasisXVector); + *outUp = kBasisYVector; + return; + + case VIEW_STARBOARD: + *outMatrix = starboard_matrix; + *outForward = kBasisXVector; + *outUp = kBasisYVector; + return; + + case VIEW_CUSTOM: + player = [PlayerEntity sharedPlayer]; + *outMatrix = [player customViewMatrix]; + *outForward = [player customViewForwardVector]; + *outUp = [player customViewUpVector]; + return; + + case VIEW_FORWARD: + case VIEW_NONE: + case VIEW_GUI_DISPLAY: + case VIEW_BREAK_PATTERN: + ; + } + + *outMatrix = fwd_matrix; + *outForward = kBasisZVector; + *outUp = kBasisYVector; +} + + +- (OOMatrix) activeViewMatrix +{ + OOMatrix m; + Vector f, u; + + [self getActiveViewMatrix:&m forwardVector:&f upVector:&u]; + return m; +} + + +- (void) drawUniverse +{ + if (!no_update) + { + NS_DURING + + no_update = YES; // block other attempts to draw + + int i, v_status; + Vector position, obj_position, view_dir, view_up; + OOMatrix view_matrix; + BOOL inGUIMode = NO; + int ent_count = n_entities; + Entity *my_entities[ent_count]; + int draw_count = 0; + + // use a non-mutable copy so this can't be changed under us. + for (i = 0; i < ent_count; i++) + { + // we check to see that we draw only the things that need to be drawn! + Entity *e = sortedEntities[i]; // ordered NEAREST -> FURTHEST AWAY + double zd2 = e->zero_distance; + if ((e->isSky)||(e->isPlanet)) + { + my_entities[draw_count++] = [e retain]; // planets and sky are always drawn! + continue; + } + if ((zd2 > ABSOLUTE_NO_DRAW_DISTANCE2)||((e->isShip)&&(zd2 > e->no_draw_distance))) + continue; + // it passed all drawing tests - and it's not a planet or the sky - we can add it to the list + my_entities[draw_count++] = [e retain]; // retained + } + + PlayerEntity *player = [PlayerEntity sharedPlayer]; + Entity *drawthing = nil; + OOCamera *camera = [player currentCamera]; + camera = camera; + + position = kZeroVector; + + inGUIMode = [player showDemoShips]; + position = [player viewpointPosition]; + v_status = [player status]; + + [self getActiveViewMatrix:&view_matrix forwardVector:&view_dir upVector:&view_up]; + + CheckOpenGLErrors(@"Universe before doing anything"); + + glEnable(GL_LIGHTING); + glEnable(GL_DEPTH_TEST); + glEnable(GL_CULL_FACE); // face culling + glDepthMask(GL_TRUE); // restore write to depth buffer + + if (!displayGUI) + glClearColor(skyClearColor[0], skyClearColor[1], skyClearColor[2], skyClearColor[3]); + else + glClearColor(0.0, 0.0, 0.0, 0.0); + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + glLoadIdentity(); // reset matrix + + gluLookAt(0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 1.0, 0.0); + + // HACK BUSTED + glScalef(-1.0, 1.0, 1.0); // flip left and right + glPushMatrix(); // save this flat viewpoint + + // Set up view transformation matrix + OOMatrix flipMatrix = kIdentityMatrix; + flipMatrix.m[2][2] = -1; + view_matrix = OOMatrixMultiply(view_matrix, flipMatrix); + Vector viewOffset = [player viewpointOffset]; + + gluLookAt(view_dir.x, view_dir.y, view_dir.z, 0.0, 0.0, 0.0, view_up.x, view_up.y, view_up.z); + + if (!displayGUI || inGUIMode) + { + // set up the light for demo ships + Vector demo_light_origin = { DEMO_LIGHT_POSITION }; + + if (!inGUIMode) + { + // rotate the view + GLMultOOMatrix([player rotationMatrix]); + // translate the view + GLTranslateOOVector(vector_flip(position)); + } + + // position the sun and docked lights correctly + glLightfv(GL_LIGHT1, GL_POSITION, sun_center_position); // this is necessary or the sun will move with the player + + if (inGUIMode) + { + // light for demo ships display.. + glLightfv(GL_LIGHT0, GL_AMBIENT, docked_light_ambient); + glLightfv(GL_LIGHT0, GL_DIFFUSE, docked_light_diffuse); + glLightfv(GL_LIGHT0, GL_SPECULAR, docked_light_specular); + + demo_light_on = NO; // be contrary - force enabling of the light + setDemoLight(YES, demo_light_origin); + sun_light_on = YES; // be contrary - force disabling of the light + setSunLight(NO); + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, docked_light_ambient); + } + else + { + demo_light_on = YES; // be contrary - force disabling of the light + setDemoLight(NO, demo_light_origin); + sun_light_on = NO; // be contrary - force enabling of the light + setSunLight(YES); + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, stars_ambient); + } + + // HACK: store view matrix for absolute drawing of active subentities (i.e., turrets). + viewMatrix = OOMatrixLoadGLMatrix(GL_MODELVIEW); + + // turn on lighting + glEnable(GL_LIGHTING); + + int furthest = draw_count - 1; + int nearest = 0; + BOOL bpHide = [self breakPatternHide]; + + // DRAW ALL THE OPAQUE ENTITIES + for (i = furthest; i >= nearest; i--) + { + drawthing = my_entities[i]; + OOEntityStatus d_status = [drawthing status]; + + if (bpHide && !drawthing->isImmuneToBreakPatternHide) continue; + + GLfloat flat_ambdiff[4] = {1.0, 1.0, 1.0, 1.0}; // for alpha + GLfloat mat_no[4] = {0.0, 0.0, 0.0, 1.0}; // nothing + + if ((d_status == STATUS_COCKPIT_DISPLAY && inGUIMode) || (d_status != STATUS_COCKPIT_DISPLAY && !inGUIMode)) + { + // reset material properties + glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, flat_ambdiff); + glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, mat_no); + + // atmospheric fog + BOOL fogging = ((airResistanceFactor > 0.01)&&(!drawthing->isPlanet)); + + glPushMatrix(); + obj_position = [drawthing position]; + if (drawthing != player) + { + //translate the object + GLTranslateOOVector(obj_position); + //rotate the object + GLMultOOMatrix([drawthing drawRotationMatrix]); + } + else + { + // Load transformation matrix + GLLoadOOMatrix(view_matrix); + //translate the object from the viewpoint + GLTranslateOOVector(vector_flip(viewOffset)); + } + + // atmospheric fog + if (fogging) + { + double fog_scale = 0.50 * BILLBOARD_DEPTH / airResistanceFactor; + double half_scale = fog_scale * 0.50; + glEnable(GL_FOG); + glFogi(GL_FOG_MODE, GL_LINEAR); + glFogfv(GL_FOG_COLOR, skyClearColor); + glHint(GL_FOG_HINT, GL_NICEST); + glFogf(GL_FOG_START, half_scale); + glFogf(GL_FOG_END, fog_scale); + } + + // lighting + if (inGUIMode) + { + setDemoLight(YES, demo_light_origin); + setSunLight(NO); + } + else + { + setSunLight(drawthing->isSunlit); + setDemoLight(NO, demo_light_origin); + } + + // draw the thing + + [drawthing drawEntity:NO:NO]; + + // atmospheric fog + if (fogging) + glDisable(GL_FOG); + + glPopMatrix(); + + } + } + + + // DRAW ALL THE TRANSLUCENT entsInDrawOrder + + glDepthMask(GL_FALSE); // don't write to depth buffer + glDisable(GL_LIGHTING); + + for (i = furthest; i >= nearest; i--) + { + drawthing = my_entities[i]; + OOEntityStatus d_status = [drawthing status]; + + if (bpHide && !drawthing->isImmuneToBreakPatternHide) continue; + + if ((d_status == STATUS_COCKPIT_DISPLAY && inGUIMode) || (d_status != STATUS_COCKPIT_DISPLAY && !inGUIMode)) + { + // experimental - atmospheric fog + BOOL fogging = (airResistanceFactor > 0.01); + + glPushMatrix(); + obj_position = [drawthing position]; + if (drawthing != player) + { + //translate the object + GLTranslateOOVector(obj_position); + //rotate the object + GLMultOOMatrix([drawthing drawRotationMatrix]); + } + else + { + // Load transformation matrix + GLLoadOOMatrix(view_matrix); + //translate the object from the viewpoint + GLTranslateOOVector(vector_flip(viewOffset)); + } + + // atmospheric fog + if (fogging) + { + double fog_scale = 0.50 * BILLBOARD_DEPTH / airResistanceFactor; + double half_scale = fog_scale * 0.50; + glEnable(GL_FOG); + glFogi(GL_FOG_MODE, GL_LINEAR); + glFogfv(GL_FOG_COLOR, skyClearColor); + glHint(GL_FOG_HINT, GL_NICEST); + glFogf(GL_FOG_START, half_scale); + glFogf(GL_FOG_END, fog_scale); + } + + // draw the thing + [drawthing drawEntity:NO:YES]; + + // atmospheric fog + if (fogging) + glDisable(GL_FOG); + + glPopMatrix(); + } + } + + glDepthMask(GL_TRUE); // restore write to depth buffer + } + + glPopMatrix(); //restore saved flat viewpoint + + glDisable(GL_LIGHTING); // disable lighting + glDisable(GL_DEPTH_TEST); // disable depth test + glDisable(GL_CULL_FACE); // face culling + glDepthMask(GL_FALSE); // don't write to depth buffer + + GLfloat line_width = [gameView viewSize].width / 1024.0; // restore line size + if (line_width < 1.0) line_width = 1.0; + glLineWidth(line_width); + + [self drawMessage]; + + HeadUpDisplay *theHUD = [player hud]; +#ifndef NDEBUG + static float sPrevHudAlpha = -1.0f; + if (gDebugFlags & DEBUG_HIDE_HUD) + { + if (sPrevHudAlpha < 0.0f) + { + sPrevHudAlpha = [theHUD overallAlpha]; + } + [theHUD setOverallAlpha:0.0f]; + } + else if (sPrevHudAlpha >= 0.0f) + { + [theHUD setOverallAlpha:sPrevHudAlpha]; + sPrevHudAlpha = -1.0f; + } +#endif + + if (v_status != STATUS_DEAD && v_status != STATUS_ESCAPE_SEQUENCE) + { + [theHUD setLine_width:line_width]; + [theHUD renderHUD]; + } + + glFlush(); // don't wait around for drawing to complete + + // clear errors - and announce them + CheckOpenGLErrors(@"Universe after all entity drawing is done."); + + for (i = 0; i < draw_count; i++) + [my_entities[i] release]; // released + + no_update = NO; // allow other attempts to draw + +#if (defined (SNAPSHOT_BUILD) && defined (OOLITE_SNAPSHOT_VERSION)) + + [theHUD drawWatermarkString:@"Development version " @OOLITE_SNAPSHOT_VERSION]; +#endif + + // frame complete, when it is time to update the fps_counter, updateClocks:delta_t + // in PlayerEntity.m will take care of resetting the processed frames number to 0. + if (![[self gameController] gameIsPaused]) + { + framesDoneThisUpdate++; + } + + NS_HANDLER + + if ([[localException name] hasPrefix:@"Oolite"]) + [self handleOoliteException:localException]; + else + { + OOLog(kOOLogException, @"***** Exception: %@ : %@ *****",[localException name], [localException reason]); + [localException raise]; + } + + NS_ENDHANDLER + } +} + + +- (int) framesDoneThisUpdate +{ + return framesDoneThisUpdate; +} + + +- (void) resetFramesDoneThisUpdate +{ + framesDoneThisUpdate = 0; +} + + +- (OOMatrix) viewMatrix +{ + return viewMatrix; +} + + +- (void) drawMessage +{ + glDisable(GL_TEXTURE_2D); // for background sheets + + float overallAlpha = [[[PlayerEntity sharedPlayer] hud] overallAlpha]; + [message_gui drawGUI:[message_gui alpha] * overallAlpha drawCursor:NO]; + [comm_log_gui drawGUI:[comm_log_gui alpha] * overallAlpha drawCursor:NO]; + + if (displayGUI) + { + if (displayCursor) cursor_row = [gui drawGUI:1.0 drawCursor:YES]; + else [gui drawGUI:1.0 drawCursor:NO]; + } +} + + +- (id)entityForUniversalID:(OOUniversalID)u_id +{ + if (u_id == 100) + return [PlayerEntity sharedPlayer]; // the player + + if (MAX_ENTITY_UID < u_id) + { + OOLog(@"universe.badUID", @"Attempt to retrieve entity for out-of-range UID %u. (This is an internal programming error, please report it.)", u_id); + return nil; + } + + if ((u_id == NO_TARGET)||(!entity_for_uid[u_id])) + return nil; + + Entity *ent = entity_for_uid[u_id]; + if ([ent isParticle]) // particles SHOULD NOT HAVE U_IDs! + { + return nil; + } + + if ([ent status] == STATUS_DEAD || [ent status] == STATUS_DOCKED) + { + return nil; + } + + return ent; +} + + +static BOOL MaintainLinkedLists(Universe* uni) +{ + BOOL result; + + if (!uni) + return NO; + + result = YES; + + // DEBUG check for loops and short lists + if (uni->n_entities > 0) + { + int n; + Entity *check, *last; + + last = nil; + + n = uni->n_entities; + check = uni->x_list_start; + while ((n--)&&(check)) + { + last = check; + check = check->x_next; + } + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken x_next %@ list (%d) ***", uni->x_list_start, n); + result = NO; + } + + n = uni->n_entities; + check = last; + while ((n--)&&(check)) check = check->x_previous; + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken x_previous %@ list (%d) ***", uni->x_list_start, n); + if (result) + { + OOLog(kOOLogEntityVerificationRebuild, @"REBUILDING x_previous list from x_next list"); + check = uni->x_list_start; + check->x_previous = nil; + while (check->x_next) + { + last = check; + check = check->x_next; + check->x_previous = last; + } + } + } + + n = uni->n_entities; + check = uni->y_list_start; + while ((n--)&&(check)) + { + last = check; + check = check->y_next; + } + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken *** broken y_next %@ list (%d) ***", uni->y_list_start, n); + result = NO; + } + + n = uni->n_entities; + check = last; + while ((n--)&&(check)) check = check->y_previous; + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken y_previous %@ list (%d) ***", uni->y_list_start, n); + if (result) + { + OOLog(kOOLogEntityVerificationRebuild, @"REBUILDING y_previous list from y_next list"); + check = uni->y_list_start; + check->y_previous = nil; + while (check->y_next) + { + last = check; + check = check->y_next; + check->y_previous = last; + } + } + } + + n = uni->n_entities; + check = uni->z_list_start; + while ((n--)&&(check)) + { + last = check; + check = check->z_next; + } + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken z_next %@ list (%d) ***", uni->z_list_start, n); + result = NO; + } + + n = uni->n_entities; + check = last; + while ((n--)&&(check)) check = check->z_previous; + if ((check)||(n > 0)) + { + OOLog(kOOLogEntityVerificationError, @"Broken z_previous %@ list (%d) ***", uni->z_list_start, n); + if (result) + { + OOLog(kOOLogEntityVerificationRebuild, @"REBUILDING z_previous list from z_next list"); + check = uni->z_list_start; + check->z_previous = nil; + while (check->z_next) + { + last = check; + check = check->z_next; + check->z_previous = last; + } + } + } + } + + if (!result) + { + OOLog(kOOLogEntityVerificationRebuild, @"Rebuilding all linked lists from scratch"); + NSArray* allEntities = uni->entities; + uni->x_list_start = nil; + uni->y_list_start = nil; + uni->z_list_start = nil; + int n_ents = [allEntities count]; + int i; + for (i = 0; i < n_ents; i++) + { + Entity* ent = (Entity*)[allEntities objectAtIndex:i]; + ent->x_next = nil; + ent->x_previous = nil; + ent->y_next = nil; + ent->y_previous = nil; + ent->z_next = nil; + ent->z_previous = nil; + [ent addToLinkedLists]; + } + } + + return result; +} + + +- (BOOL) addEntity:(Entity *) entity +{ + if (entity) + { +#ifndef NDEBUG + if (gDebugFlags & DEBUG_ENTITIES) + OOLog(@"universe.addEntity", @"Adding entity: %@", entity); +#endif + + if (![entity validForAddToUniverse]) return NO; + + // don't add things twice! + if ([entities containsObject:entity]) + return YES; + + if (n_entities >= UNIVERSE_MAX_ENTITIES - 1) + { + // throw an exception here... + OOLog(@"universe.addEntity.failed", @"***** Universe cannot addEntity:%@ -- Universe is full (%d entities out of %d)", entity, n_entities, UNIVERSE_MAX_ENTITIES); +#ifndef NDEBUG + [self obj_dump]; +#endif + // [NSException raise:@"OoliteException" + // format:@"Maximum number of entities (%d) in Universe reached. Cannot add %@", UNIVERSE_MAX_ENTITIES, entity]; + return NO; + } + + if (![entity isParticle]) + { + unsigned limiter = UNIVERSE_MAX_ENTITIES; + while (entity_for_uid[next_universal_id] != nil) // skip allocated numbers + { + next_universal_id++; // increment keeps idkeys unique + if (next_universal_id >= MAX_ENTITY_UID) + { + next_universal_id = MIN_ENTITY_UID; + } + if (limiter-- == 0) + { + // Every slot has been tried! This should not happen due to previous test, but there was a problem here in 1.70. + OOLog(@"universe.addEntity.failed", @"***** Universe cannot addEntity:%@ -- Could not find free slot for entity.", entity); + return NO; + } + } + [entity setUniversalID:next_universal_id]; + entity_for_uid[next_universal_id] = entity; + if ([entity isShip]) + { + ShipEntity* se = (ShipEntity *)entity; + [[se getAI] setOwner:se]; + [[se getAI] setState:@"GLOBAL"]; + if ([se isBeacon]) + { + [self setNextBeacon:se]; + } + if ([se isStation]) + { + // check if it is a proper rotating station (ie. roles contains the word "station") + if ([(StationEntity*)se isRotatingStation]) + { + // check for station_roll override + NSDictionary* systeminfo = [self generateSystemData:system_seed]; + double stationRoll = [systeminfo doubleForKey:@"station_roll" defaultValue:0.4]; + + [se setRoll: stationRoll]; + [(StationEntity*)se setPlanet:[self planet]]; + [se setStatus:STATUS_ACTIVE]; + } + else + { + [se setRoll: 0.0]; + [(StationEntity*)se setPlanet:[self planet]]; + [se setStatus:STATUS_ACTIVE]; + } + } + } + } + else + { + [entity setUniversalID:NO_TARGET]; + } + + // lighting considerations + entity->isSunlit = YES; + entity->shadingEntityID = NO_TARGET; + + // add it to the universe + [entities addObject:entity]; + [entity wasAddedToUniverse]; + + // maintain sorted list (and for the scanner relative position) + Vector entity_pos = entity->position; + Vector delta = vector_between(entity_pos, ((PlayerEntity *)[PlayerEntity sharedPlayer])->position); + double z_distance = magnitude2(delta); + entity->zero_distance = z_distance; + entity->relativePosition = delta; + unsigned index = n_entities; + sortedEntities[index] = entity; + entity->zero_index = index; + while ((index > 0)&&(z_distance < sortedEntities[index - 1]->zero_distance)) // bubble into place + { + sortedEntities[index] = sortedEntities[index - 1]; + sortedEntities[index]->zero_index = index; + index--; + sortedEntities[index] = entity; + entity->zero_index = index; + } + + + // increase n_entities... + n_entities++; + + // add entity to linked lists + [entity addToLinkedLists]; // position and universe have been set - so we can do this + if ([entity canCollide]) // filter only collidables disappearing + { + doLinkedListMaintenanceThisUpdate = YES; + } + + if ([entity isWormhole]) + { + [activeWormholes addObject:entity]; + } + else if ([entity isPlanet] && ![entity isSun]) + { + [allPlanets addObject:entity]; + } + + return YES; + } + return NO; +} + + +- (BOOL) removeEntity:(Entity *) entity +{ + if (entity) + { + /* Ensure entity won't actually be dealloced until the end of this + update (or the next update if none is in progress), because + there may be things pointing to it but not retaining it. + */ + [entitiesDeadThisUpdate addObject:entity]; + + return [self doRemoveEntity:entity]; + } + return NO; +} + + +- (void) ensureEntityReallyRemoved:(Entity *)entity +{ + if ([entity universalID] != NO_TARGET) + { + OOLog(@"universe.unremovedEntity", @"Entity %@ dealloced without being removed from universe! (This is an internal programming error, please report it.)", entity); + [self doRemoveEntity:entity]; + } +} + + +- (void) removeAllEntitiesExceptPlayer:(BOOL) restore +{ + BOOL updating = no_update; + no_update = YES; // no drawing while we do this! + +#ifndef NDEBUG + Entity* p0 = [entities objectAtIndex:0]; + if (!(p0->isPlayer)) + { + OOLog(kOOLogInconsistentState, @"***** First entity is not the player in Universe.removeAllEntitiesExceptPlayer - exiting."); + exit(1); + } +#endif + + // preserve wormholes + NSArray* savedWormholes = [NSArray arrayWithArray:activeWormholes]; + + while ([entities count] > 1) + { + Entity* ent = [entities objectAtIndex:1]; + if (ent->isStation) // clear out queues + [(StationEntity *)ent clear]; + [self removeEntity:ent]; + } + + [activeWormholes addObjectsFromArray:savedWormholes]; // will be cleared out by populateFromActiveWormholes + + // maintain sorted list + n_entities = 1; + + cachedSun = nil; + cachedPlanet = nil; + cachedStation = nil; + firstBeacon = NO_TARGET; + lastBeacon = NO_TARGET; + + no_update = updating; // restore drawing +} + + +- (void) removeDemoShips +{ + int i; + int ent_count = n_entities; + if (ent_count > 0) + { + Entity* ent; + for (i = 0; i < ent_count; i++) + { + ent = sortedEntities[i]; + if ([ent status] == STATUS_COCKPIT_DISPLAY && ![ent isPlayer]) + { + [self removeEntity:ent]; + } + } + } + demo_ship = nil; +} + + +- (BOOL) isVectorClearFromEntity:(Entity *) e1 toDistance:(double)dist fromPoint:(Vector) p2 +{ + if (!e1) + return NO; + + Vector f1; + Vector p1 = e1->position; + Vector v1 = p2; + v1.x -= p1.x; v1.y -= p1.y; v1.z -= p1.z; // vector from entity to p2 + + double nearest = sqrt(v1.x*v1.x + v1.y*v1.y + v1.z*v1.z) - dist; // length of vector + + if (nearest < 0.0) + return YES; // within range already! + + int i; + int ent_count = n_entities; + Entity* my_entities[ent_count]; + for (i = 0; i < ent_count; i++) + my_entities[i] = [sortedEntities[i] retain]; // retained + + if (v1.x || v1.y || v1.z) + f1 = vector_normal(v1); // unit vector in direction of p2 from p1 + else + f1 = make_vector(0, 0, 1); + + for (i = 0; i < ent_count ; i++) + { + Entity *e2 = my_entities[i]; + if ((e2 != e1)&&([e2 canCollide])) + { + Vector epos = e2->position; + epos.x -= p1.x; epos.y -= p1.y; epos.z -= p1.z; // epos now holds vector from p1 to this entities position + + double d_forward = dot_product(epos,f1); // distance along f1 which is nearest to e2's position + + if ((d_forward > 0)&&(d_forward < nearest)) + { + double cr = 1.10 * (e2->collision_radius + e1->collision_radius); // 10% safety margin + Vector p0 = e1->position; + p0.x += d_forward * f1.x; p0.y += d_forward * f1.y; p0.z += d_forward * f1.z; + // p0 holds nearest point on current course to center of incident object + Vector epos = e2->position; + p0.x -= epos.x; p0.y -= epos.y; p0.z -= epos.z; + // compare with center of incident object + double dist2 = p0.x * p0.x + p0.y * p0.y + p0.z * p0.z; + if (dist2 < cr*cr) + { + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // released + return NO; + } + } + } + } + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // released + return YES; +} + + +- (Entity*) hazardOnRouteFromEntity:(Entity *) e1 toDistance:(double)dist fromPoint:(Vector) p2 +{ + if (!e1) + return nil; + + Vector f1; + Vector p1 = e1->position; + Vector v1 = p2; + v1.x -= p1.x; v1.y -= p1.y; v1.z -= p1.z; // vector from entity to p2 + + double nearest = sqrt(v1.x*v1.x + v1.y*v1.y + v1.z*v1.z) - dist; // length of vector + + if (nearest < 0.0) + return nil; // within range already! + + Entity* result = nil; + int i; + int ent_count = n_entities; + Entity* my_entities[ent_count]; + for (i = 0; i < ent_count; i++) + my_entities[i] = [sortedEntities[i] retain]; // retained + + if (v1.x || v1.y || v1.z) + f1 = vector_normal(v1); // unit vector in direction of p2 from p1 + else + f1 = make_vector(0, 0, 1); + + for (i = 0; (i < ent_count) && (!result) ; i++) + { + Entity *e2 = my_entities[i]; + if ((e2 != e1)&&([e2 canCollide])) + { + Vector epos = e2->position; + epos.x -= p1.x; epos.y -= p1.y; epos.z -= p1.z; // epos now holds vector from p1 to this entities position + + double d_forward = dot_product(epos,f1); // distance along f1 which is nearest to e2's position + + if ((d_forward > 0)&&(d_forward < nearest)) + { + double cr = 1.10 * (e2->collision_radius + e1->collision_radius); // 10% safety margin + Vector p0 = e1->position; + p0.x += d_forward * f1.x; p0.y += d_forward * f1.y; p0.z += d_forward * f1.z; + // p0 holds nearest point on current course to center of incident object + Vector epos = e2->position; + p0.x -= epos.x; p0.y -= epos.y; p0.z -= epos.z; + // compare with center of incident object + double dist2 = p0.x * p0.x + p0.y * p0.y + p0.z * p0.z; + if (dist2 < cr*cr) + result = e2; + } + } + } + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // released + return result; +} + + +- (Vector) getSafeVectorFromEntity:(Entity *) e1 toDistance:(double)dist fromPoint:(Vector) p2 +{ + // heuristic three + + if (!e1) + { + OOLog(kOOLogParameterError, @"***** No entity set in Universe getSafeVectorFromEntity:toDistance:fromPoint:"); + return kZeroVector; + } + + Vector f1; + Vector result = p2; + int i; + int ent_count = n_entities; + Entity* my_entities[ent_count]; + for (i = 0; i < ent_count; i++) + my_entities[i] = [sortedEntities[i] retain]; // retained + Vector p1 = e1->position; + Vector v1 = p2; + v1.x -= p1.x; v1.y -= p1.y; v1.z -= p1.z; // vector from entity to p2 + + double nearest = sqrt(v1.x*v1.x + v1.y*v1.y + v1.z*v1.z) - dist; // length of vector + + if (v1.x || v1.y || v1.z) + f1 = vector_normal(v1); // unit vector in direction of p2 from p1 + else + f1 = make_vector(0, 0, 1); + + for (i = 0; i < ent_count; i++) + { + Entity *e2 = my_entities[i]; + if ((e2 != e1)&&([e2 canCollide])) + { + Vector epos = e2->position; + epos.x -= p1.x; epos.y -= p1.y; epos.z -= p1.z; + double d_forward = dot_product(epos,f1); + if ((d_forward > 0)&&(d_forward < nearest)) + { + double cr = 1.20 * (e2->collision_radius + e1->collision_radius); // 20% safety margin + + Vector p0 = e1->position; + p0.x += d_forward * f1.x; p0.y += d_forward * f1.y; p0.z += d_forward * f1.z; + // p0 holds nearest point on current course to center of incident object + + Vector epos = e2->position; + p0.x -= epos.x; p0.y -= epos.y; p0.z -= epos.z; + // compare with center of incident object + + double dist2 = p0.x * p0.x + p0.y * p0.y + p0.z * p0.z; + + if (dist2 < cr*cr) + { + result = e2->position; // center of incident object + nearest = d_forward; + + if (dist2 == 0.0) + { + // ie. we're on a line through the object's center ! + // jitter the position somewhat! + result.x += ((int)(Ranrot() % 1024) - 512)/512.0; // -1.0 .. +1.0 + result.y += ((int)(Ranrot() % 1024) - 512)/512.0; // -1.0 .. +1.0 + result.z += ((int)(Ranrot() % 1024) - 512)/512.0; // -1.0 .. +1.0 + } + + Vector nearest_point = p1; + nearest_point.x += d_forward * f1.x; nearest_point.y += d_forward * f1.y; nearest_point.z += d_forward * f1.z; + // nearest point now holds nearest point on line to center of incident object + + Vector outward = nearest_point; + outward.x -= result.x; outward.y -= result.y; outward.z -= result.z; + if (outward.x||outward.y||outward.z) + outward = vector_normal(outward); + else + outward.y = 1.0; + // outward holds unit vector through the nearest point on the line from the center of incident object + + Vector backward = p1; + backward.x -= result.x; backward.y -= result.y; backward.z -= result.z; + if (backward.x||backward.y||backward.z) + backward = vector_normal(backward); + else + backward.z = -1.0; + // backward holds unit vector from center of the incident object to the center of the ship + + Vector dd = result; + dd.x -= p1.x; dd.y -= p1.y; dd.z -= p1.z; + double current_distance = sqrt (dd.x*dd.x + dd.y*dd.y + dd.z*dd.z); + + // sanity check current_distance + if (current_distance < cr * 1.25) // 25% safety margin + current_distance = cr * 1.25; + if (current_distance > cr * 5.0) // up to 2 diameters away + current_distance = cr * 5.0; + + // choose a point that's three parts backward and one part outward + + result.x += 0.25 * (outward.x * current_distance) + 0.75 * (backward.x * current_distance); // push 'out' by this amount + result.y += 0.25 * (outward.y * current_distance) + 0.75 * (backward.y * current_distance); + result.z += 0.25 * (outward.z * current_distance) + 0.75 * (backward.z * current_distance); + + } + } + } + } + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // released + return result; +} + + +- (OOUniversalID) getFirstEntityHitByLaserFromEntity:(ShipEntity *)srcEntity inView:(OOViewID)viewdir offset:(Vector)offset rangeFound:(GLfloat*)range_ptr +{ + if (srcEntity == nil) return NO_TARGET; + + ShipEntity *hit_entity = nil; + ShipEntity *hit_subentity = nil; + Vector p0 = [srcEntity position]; + Quaternion q1 = [srcEntity orientation]; + + if ([srcEntity isPlayer]) q1.w = -q1.w; // reverse for player viewpoint + + ShipEntity *parent = [srcEntity owner]; + + if ([parent isShipWithSubEntityShip:srcEntity]) + { + // we're a subentity! + BoundingBox bbox = [srcEntity boundingBox]; + Vector midfrontplane = make_vector(0.5 * (bbox.max.x + bbox.min.x), 0.5 * (bbox.max.y + bbox.min.y), bbox.max.z); + p0 = [srcEntity absolutePositionForSubentityOffset:midfrontplane]; + q1 = [parent orientation]; + if ([parent isPlayer]) q1.w = -q1.w; + } + + int result = NO_TARGET; + double nearest; + + nearest = [srcEntity weaponRange]; + + int i; + int ent_count = n_entities; + int ship_count = 0; + ShipEntity *my_entities[ent_count]; + + for (i = 0; i < ent_count; i++) + { + Entity* ent = sortedEntities[i]; + if (ent != srcEntity && ent != parent && [ent isShip] && [ent canCollide]) + { + my_entities[ship_count++] = [ent retain]; + } + } + + Vector u1 = vector_up_from_quaternion(q1); + Vector f1 = vector_forward_from_quaternion(q1); + Vector r1 = vector_right_from_quaternion(q1); + + p0.x += offset.x * r1.x + offset.y * u1.x + offset.z * f1.x; + p0.y += offset.x * r1.y + offset.y * u1.y + offset.z * f1.y; + p0.z += offset.x * r1.z + offset.y * u1.z + offset.z * f1.z; + + switch (viewdir) + { + case VIEW_AFT : + quaternion_rotate_about_axis(&q1, u1, M_PI); + break; + case VIEW_PORT : + quaternion_rotate_about_axis(&q1, u1, M_PI/2.0); + break; + case VIEW_STARBOARD : + quaternion_rotate_about_axis(&q1, u1, -M_PI/2.0); + break; + default: + break; + } + + f1 = vector_forward_from_quaternion(q1); + r1 = vector_right_from_quaternion(q1); + + Vector p1 = make_vector(p0.x + nearest *f1.x, p0.y + nearest *f1.y, p0.z + nearest *f1.z); //endpoint + + for (i = 0; i < ship_count; i++) + { + ShipEntity *e2 = my_entities[i]; + + // check outermost bounding sphere + GLfloat cr = e2->collision_radius; + Vector rpos = vector_between(p0, e2->position); + Vector v_off = make_vector(dot_product(rpos, r1), dot_product(rpos, u1), dot_product(rpos, f1)); + if ((v_off.z > 0.0)&&(v_off.z < nearest + cr) // ahead AND within range + &&(v_off.x < cr)&&(v_off.x > -cr)&&(v_off.y < cr)&&(v_off.y > -cr) // AND not off to one side or another + &&(v_off.x*v_off.x + v_off.y*v_off.y < cr*cr)) // AND not off to both sides + { + // within the bounding sphere - do further tests + GLfloat ar = e2->collision_radius; + if ((v_off.z > 0.0)&&(v_off.z < nearest + ar) // ahead AND within range + &&(v_off.x < ar)&&(v_off.x > -ar)&&(v_off.y < ar)&&(v_off.y > -ar) // AND not off to one side or another + &&(v_off.x*v_off.x + v_off.y*v_off.y < ar*ar)) // AND not off to both sides + { + ShipEntity* entHit = (ShipEntity*)nil; + GLfloat hit = [(ShipEntity*)e2 doesHitLine:p0:p1:&entHit]; // octree detection + + if ((hit > 0.0)&&(hit < nearest)) + { + if ([entHit isSubEntity]) + { + hit_subentity = entHit; + } + hit_entity = e2; + nearest = hit; + p1 = make_vector(p0.x + nearest *f1.x, p0.y + nearest *f1.y, p0.z + nearest *f1.z); + } + } + + } + } + + if (hit_entity) + { + result = [hit_entity universalID]; + if (hit_subentity) [hit_entity setSubEntityTakingDamage:hit_subentity]; + + if (range_ptr != NULL) + { + range_ptr[0] = (GLfloat)nearest; + } + } + + for (i = 0; i < ship_count; i++) [my_entities[i] release]; // released + + return result; +} + + +- (Entity *)getFirstEntityTargetedByPlayer +{ + PlayerEntity *player = [PlayerEntity sharedPlayer]; + Entity *hit_entity = nil; + double nearest = SCANNER_MAX_RANGE - 100; // 100m shorter than range at which target is lost + int i; + int ent_count = n_entities; + int ship_count = 0; + Entity *my_entities[ent_count]; + + for (i = 0; i < ent_count; i++) +#if WORMHOLE_SCANNER + if ( ([sortedEntities[i] isShip] && ![sortedEntities[i] isPlayer]) || [sortedEntities[i] isWormhole]) +#else + if ((sortedEntities[i]->isShip)&&(sortedEntities[i] != player)) +#endif + my_entities[ship_count++] = [sortedEntities[i] retain]; // retained + + Vector p1 = player->position; + Quaternion q1 = player->orientation; + q1.w = -q1.w; // reverse for player viewpoint + Vector u1 = vector_up_from_quaternion(q1); + Vector f1 = vector_forward_from_quaternion(q1); + Vector r1 = vector_right_from_quaternion(q1); + Vector offset = [player weaponViewOffset]; + p1.x += offset.x * r1.x + offset.y * u1.x + offset.z * f1.x; + p1.y += offset.x * r1.y + offset.y * u1.y + offset.z * f1.y; + p1.z += offset.x * r1.z + offset.y * u1.z + offset.z * f1.z; + switch (viewDirection) + { + case VIEW_AFT : + quaternion_rotate_about_axis(&q1, u1, M_PI); + break; + case VIEW_PORT : + quaternion_rotate_about_axis(&q1, u1, 0.5 * M_PI); + break; + case VIEW_STARBOARD : + quaternion_rotate_about_axis(&q1, u1, -0.5 * M_PI); + break; + default: + break; + } + f1 = vector_forward_from_quaternion(q1); + r1 = vector_right_from_quaternion(q1); + for (i = 0; i < ship_count; i++) + { + Entity *e2 = my_entities[i]; + if ([e2 canCollide]&&([e2 scanClass] != CLASS_NO_DRAW)) + { + Vector rp = [e2 position]; + rp.x -= p1.x; rp.y -= p1.y; rp.z -= p1.z; + double dist2 = magnitude2(rp); + if (dist2 < nearest * nearest) + { + double df = dot_product(f1,rp); + if ((df > 0.0)&&(df < nearest)) + { + double du = dot_product(u1,rp); + double dr = dot_product(r1,rp); + double cr = [e2 collisionRadius]; + if (du*du + dr*dr < cr*cr) + { + hit_entity = e2; + nearest = sqrt(dist2); + } + } + } + } + } + // check for MASC'M + if ((hit_entity) && [hit_entity isShip]) + { + ShipEntity * ship = (ShipEntity*)hit_entity; + if ([ship isJammingScanning] && ![player hasMilitaryScannerFilter]) + { + hit_entity = nil; + } + } + + for (i = 0; i < ship_count; i++) + [my_entities[i] release]; // released + + return hit_entity; +} + + +- (NSArray *) getEntitiesWithinRange:(double)range ofEntity:(Entity *)entity +{ + if (entity == nil) return nil; + + return [self findShipsMatchingPredicate:YESPredicate + parameter:NULL + inRange:range + ofEntity:entity]; +} + + +- (unsigned) countShipsWithRole:(NSString *)role inRange:(double)range ofEntity:(Entity *)entity +{ + return [self countShipsMatchingPredicate:HasRolePredicate + parameter:role + inRange:range + ofEntity:entity]; +} + + +- (unsigned) countShipsWithRole:(NSString *)role +{ + return [self countShipsWithRole:role inRange:-1 ofEntity:nil]; +} + + +- (unsigned) countShipsWithPrimaryRole:(NSString *)role inRange:(double)range ofEntity:(Entity *)entity +{ + return [self countShipsMatchingPredicate:HasPrimaryRolePredicate + parameter:role + inRange:range + ofEntity:entity]; +} + + +- (unsigned) countShipsWithPrimaryRole:(NSString *)role +{ + return [self countShipsWithPrimaryRole:role inRange:-1 ofEntity:nil]; +} + + +- (void) sendShipsWithPrimaryRole:(NSString *)role messageToAI:(NSString *)ms +{ + NSArray *targets = nil; + + targets = [self findShipsMatchingPredicate:HasPrimaryRolePredicate + parameter:role + inRange:-1 + ofEntity:nil]; + + [targets makeObjectsPerformSelector:@selector(reactToMessage:) withObject:ms]; +} + + +- (unsigned) countEntitiesMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)e1 +{ + unsigned i, found = 0; + Vector p1, p2; + double distance, cr; + + if (predicate == NULL) predicate = YESPredicate; + + if (e1 != nil) p1 = e1->position; + else p1 = kZeroVector; + + for (i = 0; i < n_entities; i++) + { + Entity *e2 = sortedEntities[i]; + if (e2 != e1 && predicate(e2, parameter)) + { + if (range < 0) distance = -1; // Negative range means infinity + else + { + p2 = vector_subtract(e2->position, p1); + cr = range + e2->collision_radius; + distance = magnitude2(p2) - cr * cr; + } + if (distance < 0) + { + found++; + } + } + } + + return found; +} + + +- (unsigned) countShipsMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)entity +{ + if (predicate != NULL) + { + BinaryOperationPredicateParameter param = + { + IsShipPredicate, NULL, + predicate, parameter + }; + + return [self countEntitiesMatchingPredicate:ANDPredicate + parameter:¶m + inRange:range + ofEntity:entity]; + } + else + { + return [self countEntitiesMatchingPredicate:IsShipPredicate + parameter:NULL + inRange:range + ofEntity:entity]; + } +} + + +OOINLINE BOOL EntityInRange(Vector p1, Entity *e2, float range) +{ + if (range < 0) return YES; + Vector p2 = vector_subtract(e2->position, p1); + float cr = range + e2->collision_radius; + return magnitude2(p2) < cr * cr; +} + + +// NOTE: OOJSSystem relies on this returning entities in distance-from-player order. +// This can be easily changed by removing the [reference isPlayer] conditions in FindJSVisibleEntities(). +- (NSMutableArray *) findEntitiesMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)e1 +{ + unsigned i; + Vector p1; + NSMutableArray *result = nil; + + if (predicate == NULL) predicate = YESPredicate; + + result = [NSMutableArray arrayWithCapacity:n_entities]; + + if (e1 != nil) p1 = [e1 position]; + else p1 = kZeroVector; + + for (i = 0; i < n_entities; i++) + { + Entity *e2 = sortedEntities[i]; + + if (e1 != e2 && + EntityInRange(p1, e2, range) && + predicate(e2, parameter)) + { + [result addObject:e2]; + } + } + + return result; +} + + +- (id) findOneEntityMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter +{ + unsigned i; + Entity *candidate = nil; + + if (predicate == NULL) predicate = YESPredicate; + + for (i = 0; i < n_entities; i++) + { + candidate = sortedEntities[i]; + if (predicate(candidate, parameter)) return candidate; + } + + return nil; +} + + +- (NSMutableArray *) findShipsMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + inRange:(double)range + ofEntity:(Entity *)entity +{ + if (predicate != NULL) + { + BinaryOperationPredicateParameter param = + { + IsShipPredicate, NULL, + predicate, parameter + }; + + return [self findEntitiesMatchingPredicate:ANDPredicate + parameter:¶m + inRange:range + ofEntity:entity]; + } + else + { + return [self findEntitiesMatchingPredicate:IsShipPredicate + parameter:NULL + inRange:range + ofEntity:entity]; + } +} + + +- (id) nearestEntityMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + relativeToEntity:(Entity *)entity +{ + unsigned i; + Vector p1; + float rangeSq = INFINITY; + id result = nil; + + if (predicate == NULL) predicate = YESPredicate; + + if (entity != nil) p1 = [entity position]; + else p1 = kZeroVector; + + for (i = 0; i < n_entities; i++) + { + Entity *e2 = sortedEntities[i]; + float distanceToReferenceEntitySquared = (float)distance2(p1, [e2 position]); + + if (entity != e2 && + distanceToReferenceEntitySquared < rangeSq && + predicate(e2, parameter)) + { + result = e2; + rangeSq = distanceToReferenceEntitySquared; + } + } + + return [[result retain] autorelease]; +} + + +- (id) nearestShipMatchingPredicate:(EntityFilterPredicate)predicate + parameter:(void *)parameter + relativeToEntity:(Entity *)entity +{ + if (predicate != NULL) + { + BinaryOperationPredicateParameter param = + { + IsShipPredicate, NULL, + predicate, parameter + }; + + return [self nearestEntityMatchingPredicate:ANDPredicate + parameter:¶m + relativeToEntity:entity]; + } + else + { + return [self nearestEntityMatchingPredicate:IsShipPredicate + parameter:NULL + relativeToEntity:entity]; + } +} + + +- (OOTimeAbsolute) getTime +{ + return universal_time; +} + + +- (OOTimeDelta) getTimeDelta +{ + return time_delta; +} + + +- (void) findCollisionsAndShadows +{ + unsigned i; + + [universeRegion clearEntityList]; + + for (i = 0; i < n_entities; i++) + [universeRegion checkEntity: sortedEntities[i]]; // sorts out which region it's in + + [universeRegion findCollisions]; + + // do check for entities that can't see the sun! + [universeRegion findShadowedEntities]; + +} + + +- (NSString*) collisionDescription +{ + if (universeRegion) + return [NSString stringWithFormat:@"c%d", universeRegion->checks_this_tick]; + else + return @"-"; +} + + +- (void) dumpCollisions +{ + dumpCollisionInfo = YES; +} + + +- (void) setViewDirection:(OOViewID) vd +{ + NSString *ms = nil; + BOOL mouseDelta = YES; + + if ((viewDirection == vd)&&(vd != VIEW_CUSTOM)&&(!displayGUI)) + return; + + switch (vd) + { + case VIEW_FORWARD: + ms = DESC(@"forward-view-string"); + displayGUI = NO; // switch off any text displays + break; + + case VIEW_AFT: + ms = DESC(@"aft-view-string"); + displayGUI = NO; // switch off any text displays + break; + + case VIEW_PORT: + ms = DESC(@"port-view-string"); + displayGUI = NO; // switch off any text displays + break; + + case VIEW_STARBOARD: + ms = DESC(@"starboard-view-string"); + displayGUI = NO; // switch off any text displays + break; + + case VIEW_CUSTOM: + ms = [[PlayerEntity sharedPlayer] customViewDescription]; + displayGUI = NO; // switch off any text displays + break; + + default: + mouseDelta = NO; + break; + } +#if OOLITE_SDL + [gameView setMouseInDeltaMode: mouseDelta]; +#else + (void)mouseDelta; +#endif + + if ((viewDirection != vd)|(viewDirection = VIEW_CUSTOM)) + { + viewDirection = vd; + if (ms) + { + [self addMessage:ms forCount:3]; + } + } +} + + +- (OOViewID) viewDirection +{ + return viewDirection; +} + + +- (NSString *) soundNameForCustomSoundKey:(NSString *)key; +{ + NSString *result = nil; + NSMutableSet *seen = nil; + id object = [customsounds objectForKey:key]; + + if ([object isKindOfClass:[NSArray class]] && [object count] > 0) + { + key = [object stringAtIndex:Ranrot() % [object count]]; + } + else + { + object=nil; + } + + result = [[OOCacheManager sharedCache] objectForKey:key inCache:@"resolved custom sounds"]; + if (result == nil) + { + // Resolve sound, allowing indirection within customsounds.plist + seen = [NSMutableSet set]; + result = key; + if (object == nil || ([result hasPrefix:@"["] && [result hasSuffix:@"]"])) + { + for (;;) + { + [seen addObject:result]; + object = [customsounds objectForKey:result]; + if( [object isKindOfClass:[NSArray class]] && [object count] > 0) + { + result = [object stringAtIndex:Ranrot() % [object count]]; + if ([key hasPrefix:@"["] && [key hasSuffix:@"]"]) key=result; + } + else + { + if ([object isKindOfClass:[NSString class]]) + result = object; + else + result = nil; + } + if (result == nil || ![result hasPrefix:@"["] || ![result hasSuffix:@"]"]) break; + if ([seen containsObject:result]) + { + OOLogERR(@"sound.customSounds.recursion", @"recursion in customsounds.plist for '%@' (at '%@'), no sound will be played.", key, result); + result = nil; + break; + } + } + } + + if (result == nil) result = @"__oolite-no-sound"; + [[OOCacheManager sharedCache] setObject:result forKey:key inCache:@"resolved custom sounds"]; + } + + if ([result isEqualToString:@"__oolite-no-sound"]) + { + OOLog(@"sound.customSounds", @"Could not resolve sound name in customsounds.plist for '%@', no sound will be played.", key); + result = nil; + } + return result; +} + + +- (void) clearPreviousMessage +{ + if (currentMessage) [currentMessage release]; + currentMessage = nil; +} + + +- (void) setMessageGuiBackgroundColor:(OOColor *)some_color +{ + [message_gui setBackgroundColor:some_color]; +} + + +- (void) displayMessage:(NSString *) text forCount:(OOTimeDelta)count +{ + if (![currentMessage isEqual:text] || universal_time >= messageRepeatTime) + { + if (currentMessage) [currentMessage release]; + currentMessage = [text retain]; + messageRepeatTime=universal_time + 6.0; + [message_gui printLongText:text align:GUI_ALIGN_CENTER color:[OOColor yellowColor] fadeTime:count key:nil addToArray:nil]; + } +} + + +- (void) displayCountdownMessage:(NSString *) text forCount:(OOTimeDelta)count +{ + if (![currentMessage isEqual:text]) + { + if (currentMessage) [currentMessage release]; + currentMessage = [text retain]; + + [message_gui printLineNoScroll:text align:GUI_ALIGN_CENTER color:[OOColor yellowColor] fadeTime:count key:nil addToArray:nil]; + } +} + + +- (void) addDelayedMessage:(NSString *)text forCount:(OOTimeDelta)count afterDelay:(double)delay +{ + NSMutableDictionary *msgDict = [NSMutableDictionary dictionaryWithCapacity:2]; + [msgDict setObject:text forKey:@"message"]; + [msgDict setObject:[NSNumber numberWithDouble:count] forKey:@"duration"]; + [self performSelector:@selector(addDelayedMessage:) withObject:msgDict afterDelay:delay]; +} + + +- (void) addDelayedMessage:(NSDictionary *) textdict +{ + NSString *msg = nil; + OOTimeDelta msg_duration; + + msg = [textdict stringForKey:@"message"]; + if (msg == nil) return; + msg_duration = [textdict nonNegativeDoubleForKey:@"duration" defaultValue:3.0]; + + [self addMessage:msg forCount:msg_duration]; +} + + +- (void) addMessage:(NSString *)text forCount:(OOTimeDelta)count +{ + if (![currentMessage isEqual:text] || universal_time >= messageRepeatTime) + { +#if OOLITE_SPEECH_SYNTH + PlayerEntity* player = [PlayerEntity sharedPlayer]; + //speech synthesis + if ([player isSpeechOn]) + { + BOOL isStandard = NO; + NSString* systemSaid=nil; + NSString* h_systemSaid=nil; + + NSString* systemName = [self getSystemName:system_seed]; + isStandard = [systemName isEqualToString: [self generateSystemName:system_seed]]; + //if the name is not the standard generated one, we can't use the generated phonemes. + systemSaid = isStandard ? [self generatePhoneticSystemName:system_seed] : systemName; + + NSString* h_systemName = [self getSystemName:[player target_system_seed]]; + isStandard= [h_systemName isEqualToString: [self generateSystemName:[player target_system_seed]]]; + h_systemSaid = isStandard ? [self generatePhoneticSystemName:[player target_system_seed]]: h_systemName; + + NSString *spoken_text = text; + if(nil != speechArray) + { + NSEnumerator *speechEnumerator = nil; + NSArray *thePair = nil; + for (speechEnumerator = [speechArray objectEnumerator]; (thePair = [speechEnumerator nextObject]); ) + { + NSString *original_phrase = [thePair stringAtIndex:0]; +#if OOLITE_MAC_OS_X + NSString *replacement_phrase = [thePair stringAtIndex:1]; +#elif OOLITE_ESPEAK + NSString *replacement_phrase = [thePair stringAtIndex:([thePair count] > 2 ? 2 : 1)]; + if (![replacement_phrase isEqualToString:@"_"]) +#endif + spoken_text = [[spoken_text componentsSeparatedByString: original_phrase] componentsJoinedByString: replacement_phrase]; + } + spoken_text = [[spoken_text componentsSeparatedByString: systemName] componentsJoinedByString: systemSaid]; + spoken_text = [[spoken_text componentsSeparatedByString: h_systemName] componentsJoinedByString: h_systemSaid]; + } + [self stopSpeaking]; + [self startSpeakingString:spoken_text]; + + } +#endif // OOLITE_SPEECH_SYNTH + + [message_gui printLongText:text align:GUI_ALIGN_CENTER color:[OOColor yellowColor] fadeTime:count key:nil addToArray:nil]; + + [currentMessage release]; + currentMessage = [text retain]; + messageRepeatTime=universal_time + 6.0; + } +} + + +- (void) addCommsMessage:(NSString *)text forCount:(OOTimeDelta)count +{ + [self addCommsMessage:text forCount:count andShowComms:YES]; +} + + +- (void) addCommsMessage:(NSString *)text forCount:(OOTimeDelta)count andShowComms:(BOOL) showComms +{ + if ([[PlayerEntity sharedPlayer] showDemoShips]) return; + + if (![currentMessage isEqualToString:text] || universal_time >= messageRepeatTime) + { + PlayerEntity* player = [PlayerEntity sharedPlayer]; + + if ([player isSpeechOn]) + { + if ([self isSpeaking]) + [self stopSpeaking]; + [self startSpeakingString:@"Incoming message."]; + } + + [message_gui printLongText:text align:GUI_ALIGN_CENTER color:[OOColor greenColor] fadeTime:(float)count key:nil addToArray:nil]; + + [comm_log_gui printLongText:text align:GUI_ALIGN_LEFT color:nil fadeTime:0.0 key:nil addToArray:[player commLog]]; + if (showComms) [self showCommsLog:6.0]; + + [currentMessage release]; + currentMessage = [text retain]; + messageRepeatTime=universal_time + 6.0; + } +} + + +- (void) showCommsLog:(OOTimeDelta)how_long +{ + [comm_log_gui setAlpha:1.0]; + [comm_log_gui fadeOutFromTime:[self getTime] overDuration:how_long]; +} + + +- (void) update:(OOTimeDelta)inDeltaT +{ + volatile float delta_t = inDeltaT; + + delta_t *= [self timeAccelerationFactor]; + + if (!no_update) + { + NSString * volatile update_stage = @"initialisation"; + NS_DURING + int i; + PlayerEntity* player = [PlayerEntity sharedPlayer]; + int ent_count = n_entities; + Entity* my_entities[ent_count]; + BOOL inGUIMode = [player showDemoShips]; + + skyClearColor[0] = 0.0; + skyClearColor[1] = 0.0; + skyClearColor[2] = 0.0; + skyClearColor[3] = 0.0; + + // use a retained copy so this can't be changed under us. + for (i = 0; i < ent_count; i++) + my_entities[i] = [sortedEntities[i] retain]; // explicitly retain each one + + time_delta = delta_t; + universal_time += delta_t; + + update_stage = @"demo management"; + if (inGUIMode && [player guiScreen] == GUI_SCREEN_INTRO2){ + if (universal_time >= demo_stage_time) + { + if (ent_count > 1) + { + Vector vel; + Quaternion q2 = kIdentityQuaternion; + + quaternion_rotate_about_y(&q2,M_PI); + + #define DEMO2_VANISHING_DISTANCE 400.0 + #define DEMO2_FLY_IN_STAGE_TIME 1.5 + + switch (demo_stage) + { + case DEMO_FLY_IN: + [demo_ship setPosition:[demo_ship destination]]; // ideal position + demo_stage = DEMO_SHOW_THING; + demo_stage_time = universal_time + 6.0; + break; + case DEMO_SHOW_THING: + vel = make_vector(0, 0, DEMO2_VANISHING_DISTANCE * demo_ship->collision_radius); + [demo_ship setVelocity:vel]; + demo_stage = DEMO_FLY_OUT; + demo_stage_time = universal_time + 1.5; + break; + case DEMO_FLY_OUT: + // change the demo_ship here + [self removeEntity:demo_ship]; + demo_ship = nil; + + NSString *shipDesc = nil; + NSString *shipName = nil; + NSDictionary *shipDict = nil; + + demo_ship_index = (demo_ship_index + 1) % [demo_ships count]; + shipDesc = [demo_ships stringAtIndex:demo_ship_index]; + shipDict = [[OOShipRegistry sharedRegistry] shipInfoForKey:shipDesc]; + if (shipDict != nil) + { + // Failure means we don't change demo_stage, so we'll automatically try again. + demo_ship = [[ShipEntity alloc] initWithDictionary:shipDict]; + } + else + { + OOLog(@"demo.loadShip.failed", @"Could not load ship \"%@\" for demo screen.", shipDesc); + demo_ship = [[ShipEntity alloc] initWithDictionary:[[OOShipRegistry sharedRegistry] shipInfoForKey:@"oolite-unknown-ship"]]; + shipName=[NSString stringWithFormat:DESC(@"unknown-ship-@"),shipDesc]; + } + + if (demo_ship != nil) + { + [self addEntity:demo_ship]; + [[demo_ship getAI] setStateMachine:@"nullAI.plist"]; + [demo_ship setOrientation:q2]; + demo_start_z=DEMO2_VANISHING_DISTANCE * demo_ship->collision_radius; + [demo_ship setPositionX:0.0f y:0.0f z:demo_start_z]; + [demo_ship setDestination: make_vector(0.0f, 0.0f, demo_start_z * 0.01f)]; // ideal position + [demo_ship setVelocity:kZeroVector]; + [demo_ship setScanClass: CLASS_NO_DRAW]; + [demo_ship setRoll:M_PI/5.0]; + [demo_ship setPitch:M_PI/10.0]; + [demo_ship setStatus:STATUS_COCKPIT_DISPLAY]; + [gui setText:shipName != nil ? shipName : [demo_ship displayName] forRow:19 align:GUI_ALIGN_CENTER]; + + demo_stage = DEMO_FLY_IN; + demo_start_time=universal_time; + demo_stage_time = demo_start_time + DEMO2_FLY_IN_STAGE_TIME; + + } + break; + } + } + } + else if (demo_stage == DEMO_FLY_IN) + { + [demo_ship setPositionX:0.0f y:0.0f z:demo_start_z + ([demo_ship destination].z - demo_start_z) *(universal_time -demo_start_time) / DEMO2_FLY_IN_STAGE_TIME ]; + } + } + + update_stage = @"update:entity"; + for (i = 0; i < ent_count; i++) + { + Entity *thing = my_entities[i]; +#ifndef NDEBUG + update_stage = [NSString stringWithFormat:@"update:entity[%@]", [thing shortDescription]]; +#endif + + [thing update:delta_t]; + + // maintain distance-from-player list + double z_distance = thing->zero_distance; + + int index = thing->zero_index; + while (index > 0 && z_distance < sortedEntities[index - 1]->zero_distance) + { + sortedEntities[index] = sortedEntities[index - 1]; // bubble up the list, usually by just one position + sortedEntities[index - 1] = thing; + thing->zero_index = index - 1; + sortedEntities[index]->zero_index = index; + index--; + } + + // update deterministic AI + if ([thing isShip]) + { + AI* theShipsAI = [(ShipEntity *)thing getAI]; + if (theShipsAI) + { + double thinkTime = [theShipsAI nextThinkTime]; + if ((universal_time > thinkTime)||(thinkTime == 0.0)) + { +#ifndef NDEBUG + update_stage = [NSString stringWithFormat:@"update:think[%@]", [thing shortDescription]]; +#endif + [theShipsAI setNextThinkTime:universal_time + [theShipsAI thinkTimeInterval]]; + [theShipsAI think]; + } + } + } + } + + // Maintain x/y/z order lists + update_stage = @"updating linked lists"; + for (i = 0; i < ent_count; i++) + { + [my_entities[i] updateLinkedLists]; + } + + // detect collisions and light ships that can see the sun + + update_stage = @"collision and shadow detection"; + [self filterSortedLists]; + [self findCollisionsAndShadows]; + + // do any required check and maintenance of linked lists + + if (doLinkedListMaintenanceThisUpdate) + { + MaintainLinkedLists(self); + doLinkedListMaintenanceThisUpdate = NO; + } + + // dispose of the non-mutable copy and everything it references neatly + + update_stage = @"clean up"; + for (i = 0; i < ent_count; i++) + [my_entities[i] release]; // explicitly release each one + + NS_HANDLER + if ([[localException name] hasPrefix:@"Oolite"]) + [self handleOoliteException:localException]; + else + { + OOLog(kOOLogException, @"***** Exception during [%@] in [Universe update:] : %@ : %@ *****", update_stage, [localException name], [localException reason]); + [localException raise]; + } + NS_ENDHANDLER + } + Entity* my_ent; + while ([entitiesDeadThisUpdate count] >0) + { + my_ent=[[entitiesDeadThisUpdate objectAtIndex:0] retain]; + [my_ent autorelease]; + [entitiesDeadThisUpdate removeObjectAtIndex:0]; + } +} + + +- (float) timeAccelerationFactor +{ + return time_acceleration_factor; +} + + +- (void) setTimeAccelerationFactor:(float)newTimeAccelerationFactor +{ + if (newTimeAccelerationFactor < TIME_ACCELERATION_FACTOR_MIN || newTimeAccelerationFactor > TIME_ACCELERATION_FACTOR_MAX) + { + newTimeAccelerationFactor = TIME_ACCELERATION_FACTOR_DEFAULT; + } + time_acceleration_factor = newTimeAccelerationFactor; +} + + +- (void) filterSortedLists +{ + Entity *e0, *next; + GLfloat start, finish, next_start, next_finish; + + // using the z_list - set or clear collisionTestFilter and clear collision_chain + e0 = z_list_start; + while (e0) + { + e0->collisionTestFilter = (![e0 canCollide]); + e0->collision_chain = nil; + e0 = e0->z_next; + } + // done. + + // start with the z_list + e0 = z_list_start; + while (e0) + { + // here we are either at the start of the list or just past a gap + start = e0->position.z - e0->collision_radius; + finish = start + 2.0f * e0->collision_radius; + next = e0->z_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated from the list of possible colliders - so skip it + next = next->z_next; + if (next) + { + next_start = next->position.z - next->collision_radius; + if (next_start < finish) + { + // e0 and next overlap + while ((next)&&(next_start < finish)) + { + // skip forward to the next gap or the end of the list + next_finish = next_start + 2.0f * next->collision_radius; + if (next_finish > finish) + finish = next_finish; + e0 = next; + next = e0->z_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated - so skip it + next = next->z_next; + if (next) + next_start = next->position.z - next->collision_radius; + } + // now either (next == nil) or (next_start >= finish)-which would imply a gap! + } + else + { + // e0 is a singleton + e0->collisionTestFilter = YES; + } + } + else // (next == nil) + { + // at the end of the list so e0 is a singleton + e0->collisionTestFilter = YES; + } + e0 = next; + } + // done! list filtered + + // then with the y_list, z_list singletons now create more gaps.. + e0 = y_list_start; + while (e0) + { + // here we are either at the start of the list or just past a gap + start = e0->position.y - e0->collision_radius; + finish = start + 2.0f * e0->collision_radius; + next = e0->y_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated from the list of possible colliders - so skip it + next = next->y_next; + if (next) + { + + next_start = next->position.y - next->collision_radius; + if (next_start < finish) + { + // e0 and next overlap + while ((next)&&(next_start < finish)) + { + // skip forward to the next gap or the end of the list + next_finish = next_start + 2.0f * next->collision_radius; + if (next_finish > finish) + finish = next_finish; + e0 = next; + next = e0->y_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated - so skip it + next = next->y_next; + if (next) + next_start = next->position.y - next->collision_radius; + } + // now either (next == nil) or (next_start >= finish)-which would imply a gap! + } + else + { + // e0 is a singleton + e0->collisionTestFilter = YES; + } + } + else // (next == nil) + { + // at the end of the list so e0 is a singleton + e0->collisionTestFilter = YES; + } + e0 = next; + } + // done! list filtered + + // finish with the x_list + e0 = x_list_start; + while (e0) + { + // here we are either at the start of the list or just past a gap + start = e0->position.x - e0->collision_radius; + finish = start + 2.0f * e0->collision_radius; + next = e0->x_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated from the list of possible colliders - so skip it + next = next->x_next; + if (next) + { + next_start = next->position.x - next->collision_radius; + if (next_start < finish) + { + // e0 and next overlap + while ((next)&&(next_start < finish)) + { + // skip forward to the next gap or the end of the list + next_finish = next_start + 2.0f * next->collision_radius; + if (next_finish > finish) + finish = next_finish; + e0 = next; + next = e0->x_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated - so skip it + next = next->x_next; + if (next) + next_start = next->position.x - next->collision_radius; + } + // now either (next == nil) or (next_start >= finish)-which would imply a gap! + } + else + { + // e0 is a singleton + e0->collisionTestFilter = YES; + } + } + else // (next == nil) + { + // at the end of the list so e0 is a singleton + e0->collisionTestFilter = YES; + } + e0 = next; + } + // done! list filtered + + // repeat the y_list - so gaps from the x_list influence singletons + e0 = y_list_start; + while (e0) + { + // here we are either at the start of the list or just past a gap + start = e0->position.y - e0->collision_radius; + finish = start + 2.0f * e0->collision_radius; + next = e0->y_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated from the list of possible colliders - so skip it + next = next->y_next; + if (next) + { + + next_start = next->position.y - next->collision_radius; + if (next_start < finish) + { + // e0 and next overlap + while ((next)&&(next_start < finish)) + { + // skip forward to the next gap or the end of the list + next_finish = next_start + 2.0f * next->collision_radius; + if (next_finish > finish) + finish = next_finish; + e0 = next; + next = e0->y_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated - so skip it + next = next->y_next; + if (next) + next_start = next->position.y - next->collision_radius; + } + // now either (next == nil) or (next_start >= finish)-which would imply a gap! + } + else + { + // e0 is a singleton + e0->collisionTestFilter = YES; + } + } + else // (next == nil) + { + // at the end of the list so e0 is a singleton + e0->collisionTestFilter = YES; + } + e0 = next; + } + // done! list filtered + + // finally, repeat the z_list - this time building collision chains... + e0 = z_list_start; + while (e0) + { + // here we are either at the start of the list or just past a gap + start = e0->position.z - e0->collision_radius; + finish = start + 2.0f * e0->collision_radius; + next = e0->z_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated from the list of possible colliders - so skip it + next = next->z_next; + if (next) + { + + next_start = next->position.z - next->collision_radius; + if (next_start < finish) + { + // e0 and next overlap + while ((next)&&(next_start < finish)) + { + // chain e0 to next in collision + e0->collision_chain = next; + // skip forward to the next gap or the end of the list + next_finish = next_start + 2.0f * next->collision_radius; + if (next_finish > finish) + finish = next_finish; + e0 = next; + next = e0->z_next; + while ((next)&&(next->collisionTestFilter)) // next has been eliminated - so skip it + next = next->z_next; + if (next) + next_start = next->position.z - next->collision_radius; + } + // now either (next == nil) or (next_start >= finish)-which would imply a gap! + e0->collision_chain = nil; // end the collision chain + } + else + { + // e0 is a singleton + e0->collisionTestFilter = YES; + } + } + else // (next == nil) + { + // at the end of the list so e0 is a singleton + e0->collisionTestFilter = YES; + } + e0 = next; + } + // done! list filtered +} + + +- (void) setGalaxy_seed:(Random_Seed) gal_seed +{ + [self setGalaxy_seed:gal_seed andReinit:NO]; +} + + +- (void) setGalaxy_seed:(Random_Seed) gal_seed andReinit:(BOOL) forced +{ + int i; + Random_Seed g_seed = gal_seed; + NSAutoreleasePool *pool = nil; + + if (!equal_seeds(galaxy_seed, gal_seed) || forced) { + galaxy_seed = gal_seed; + + // systems + for (i = 0; i < 256; i++) + { + pool = [[NSAutoreleasePool alloc] init]; + + systems[i] = g_seed; + if (system_names[i]) [system_names[i] release]; + system_names[i] = [[self getSystemName:g_seed] retain]; + rotate_seed(&g_seed); + rotate_seed(&g_seed); + rotate_seed(&g_seed); + rotate_seed(&g_seed); + + [pool release]; + } + } +} + + +- (void) setSystemTo:(Random_Seed) s_seed +{ + NSDictionary *systemData; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + OOEconomyID economy; + + [self setGalaxy_seed: [player galaxy_seed]]; + + system_seed = s_seed; + target_system_seed = s_seed; + + systemData = [self generateSystemData:target_system_seed]; + economy = [systemData unsignedCharForKey:KEY_ECONOMY]; + + [self generateEconomicDataWithEconomy:economy andRandomFactor:[player random_factor] & 0xff]; +} + + +- (Random_Seed) systemSeed +{ + return system_seed; +} + + +- (Random_Seed) systemSeedForSystemNumber:(OOSystemID)n +{ + return systems[(unsigned)n & 0xFF]; +} + + +- (Random_Seed) systemSeedForSystemName:(NSString *)sysname +{ + int i; + NSString *pname = [[sysname lowercaseString] capitalizedString]; + for (i = 0; i < 256; i++) + { + if ([pname isEqualToString:[self getSystemName: systems[i]]]) + return systems[i]; + } + + return kNilRandomSeed; +} + + +- (OOSystemID) systemIDForSystemSeed:(Random_Seed)seed +{ + int i; + for (i = 0; i < 256; i++) + { + if (equal_seeds(systems[i], seed)) return i; + } + + return -1; +} + + +- (OOSystemID) currentSystemID +{ + return [self systemIDForSystemSeed:[self systemSeed]]; +} + + +- (NSDictionary *) descriptions +{ + if (descriptions == nil) + { + // Load internal descriptions.plist for use in early init, OXP verifier etc. + // It will be replaced by merged version later if running the game normally. + descriptions = [NSDictionary dictionaryWithContentsOfFile:[[[ResourceManager builtInPath] + stringByAppendingPathComponent:@"Config"] + stringByAppendingPathComponent:@"descriptions.plist"]]; + } + return descriptions; +} + + +- (NSDictionary *) characters +{ + return characters; +} + + +- (NSDictionary *) missiontext +{ + return missiontext; +} + + +- (NSString *)descriptionForKey:(NSString *)key +{ + id object = [[self descriptions] objectForKey:key]; + if ([object isKindOfClass:[NSString class]]) return object; + else if ([object isKindOfClass:[NSArray class]] && [object count] > 0) return [object stringAtIndex:Ranrot() % [object count]]; + return nil; +} + + +- (NSString *)descriptionForArrayKey:(NSString *)key index:(unsigned)index +{ + NSArray *array = [[self descriptions] arrayForKey:key]; + if ([array count] <= index) return nil; // Catches nil array + return [array objectAtIndex:index]; +} + + +- (BOOL) descriptionBooleanForKey:(NSString *)key +{ + return [[self descriptions] boolForKey:key]; +} + + +- (NSString *) keyForPlanetOverridesForSystemSeed:(Random_Seed) s_seed inGalaxySeed:(Random_Seed) g_seed +{ + Random_Seed g0 = {0x4a, 0x5a, 0x48, 0x02, 0x53, 0xb7}; + int pnum = [self findSystemNumberAtCoords:NSMakePoint(s_seed.d,s_seed.b) withGalaxySeed:g_seed]; + int gnum = 0; + while (((g_seed.a != g0.a)||(g_seed.b != g0.b)||(g_seed.c != g0.c)||(g_seed.d != g0.d)||(g_seed.e != g0.e)||(g_seed.f != g0.f))&&(gnum < 8)) + { + gnum++; + g0.a = rotate_byte_left(g0.a); + g0.b = rotate_byte_left(g0.b); + g0.c = rotate_byte_left(g0.c); + g0.d = rotate_byte_left(g0.d); + g0.e = rotate_byte_left(g0.e); + g0.f = rotate_byte_left(g0.f); + } + return [NSString stringWithFormat:@"%d %d", gnum, pnum]; +} + + +- (NSString *) keyForInterstellarOverridesForSystemSeeds:(Random_Seed) s_seed1 :(Random_Seed) s_seed2 inGalaxySeed:(Random_Seed) g_seed +{ + Random_Seed g0 = {0x4a, 0x5a, 0x48, 0x02, 0x53, 0xb7}; + int pnum1 = [self findSystemNumberAtCoords:NSMakePoint(s_seed1.d,s_seed1.b) withGalaxySeed:g_seed]; + int pnum2 = [self findSystemNumberAtCoords:NSMakePoint(s_seed2.d,s_seed2.b) withGalaxySeed:g_seed]; + if (pnum1 > pnum2) + { // swap them + int t = pnum1; pnum1 = pnum2; pnum2 = t; + } + int gnum = 0; + while (((g_seed.a != g0.a)||(g_seed.b != g0.b)||(g_seed.c != g0.c)||(g_seed.d != g0.d)||(g_seed.e != g0.e)||(g_seed.f != g0.f))&&(gnum < 8)) + { + gnum++; + g0.a = rotate_byte_left(g0.a); + g0.b = rotate_byte_left(g0.b); + g0.c = rotate_byte_left(g0.c); + g0.d = rotate_byte_left(g0.d); + g0.e = rotate_byte_left(g0.e); + g0.f = rotate_byte_left(g0.f); + } + return [NSString stringWithFormat:@"interstellar: %d %d %d", gnum, pnum1, pnum2]; +} + + +- (NSDictionary *) generateSystemData:(Random_Seed) s_seed +{ + return [self generateSystemData:s_seed useCache:YES]; +} + + +- (NSDictionary *) generateSystemData:(Random_Seed) s_seed useCache:(BOOL) useCache +{ + static NSDictionary *cachedResult = nil; + static Random_Seed cachedSeed = {0}; + + if (useCache) + { + // Cache hit ratio is over 95% during respawn, about 80% during initial set-up. + if (EXPECT(cachedResult != nil && equal_seeds(cachedSeed, s_seed))) return [[cachedResult retain] autorelease]; + } + + [cachedResult release]; + cachedResult = nil; + cachedSeed = s_seed; + + NSMutableDictionary* systemdata = [[NSMutableDictionary alloc] initWithCapacity:8]; + + OOGovernmentID government = (s_seed.c / 8) & 7; + + OOEconomyID economy = s_seed.b & 7; + if (government < 2) + economy = economy | 2; + + OOTechLevelID techlevel = (economy ^ 7) + (s_seed.d & 3) + (government / 2) + (government & 1); + + unsigned population = (techlevel * 4) + government + economy + 1; + + unsigned productivity = ((economy ^ 7) + 3) * (government + 4) * population * 8; + + unsigned radius = (((s_seed.f & 15) + 11) * 256) + s_seed.d; + + NSString *name = [self generateSystemName:s_seed]; + NSString *inhabitant = [self generateSystemInhabitants:s_seed plural:NO]; + NSString *inhabitants = [self generateSystemInhabitants:s_seed plural:YES]; + NSString *description = DescriptionForSystem(s_seed,name); //avoids parsestring recursion + + NSString *override_key = [self keyForPlanetOverridesForSystemSeed:s_seed inGalaxySeed:galaxy_seed]; + + [systemdata setUnsignedInteger:government forKey:KEY_GOVERNMENT]; + [systemdata setUnsignedInteger:economy forKey:KEY_ECONOMY]; + [systemdata setUnsignedInteger:techlevel forKey:KEY_TECHLEVEL]; + [systemdata setUnsignedInteger:population forKey:KEY_POPULATION]; + [systemdata setUnsignedInteger:productivity forKey:KEY_PRODUCTIVITY]; + [systemdata setUnsignedInteger:radius forKey:KEY_RADIUS]; + [systemdata setObject:name forKey:KEY_NAME]; + [systemdata setObject:inhabitant forKey:KEY_INHABITANT]; + [systemdata setObject:inhabitants forKey:KEY_INHABITANTS]; + [systemdata setObject:description forKey:KEY_DESCRIPTION]; + + // check at this point + // for scripted overrides for this planet + NSDictionary *overrides = nil; + + overrides = [planetInfo dictionaryForKey:PLANETINFO_UNIVERSAL_KEY]; + if (overrides != nil) [systemdata addEntriesFromDictionary:overrides]; + overrides = [planetInfo dictionaryForKey:override_key]; + if (overrides != nil) [systemdata addEntriesFromDictionary:overrides]; + overrides = [localPlanetInfoOverrides dictionaryForKey:override_key]; + if (overrides != nil) [systemdata addEntriesFromDictionary:overrides]; + + // check if the description needs to be recalculated + if ([description isEqual:[systemdata stringForKey:KEY_DESCRIPTION]] && ![name isEqual:[systemdata stringForKey:KEY_NAME]]) + { + [systemdata setObject:DescriptionForSystem(s_seed,[systemdata stringForKey:KEY_NAME]) forKey:KEY_DESCRIPTION]; + } + + cachedResult = [systemdata copy]; + [systemdata release]; + + return cachedResult; +} + + +- (NSDictionary *) currentSystemData +{ + if (![self inInterstellarSpace]) + { + return [self generateSystemData:system_seed]; + } + else + { + static NSDictionary *interstellarDict = nil; + if (interstellarDict == nil) + { + NSString *interstellarName = DESC(@"interstellar-space"); + NSString *notApplicable = DESC(@"not-applicable"); + NSNumber *minusOne = [NSNumber numberWithInt:-1]; + NSNumber *zero = [NSNumber numberWithInt:0]; + interstellarDict = [[NSDictionary alloc] initWithObjectsAndKeys: + interstellarName, KEY_NAME, + minusOne, KEY_GOVERNMENT, + minusOne, KEY_ECONOMY, + minusOne, KEY_TECHLEVEL, + zero, KEY_POPULATION, + zero, KEY_PRODUCTIVITY, + zero, KEY_RADIUS, + notApplicable, KEY_INHABITANTS, + notApplicable, KEY_DESCRIPTION, + nil]; + } + + return interstellarDict; + } +} + + +- (BOOL) inInterstellarSpace +{ + return [self sun] == nil; +} + + +- (void)setObject:(id)object forKey:(NSString *)key forPlanetKey:(NSString *)planetKey +{ + NSMutableDictionary *overrideDict = nil; + + if (key == nil || planetKey == nil) return; + + overrideDict = [localPlanetInfoOverrides objectForKey:planetKey]; + if (overrideDict != nil) + { + /* There has been trouble with localPlanetInfoOverrides containing + immutable dictionaries. Changes to -setLocalPlanetInfoOverrides + should have fixed it, but we validate just to be certain. + -- Ahruman 20070729 + */ + if (![overrideDict isKindOfClass:[NSMutableDictionary class]]) + { + if ([overrideDict isKindOfClass:[NSDictionary class]]) + { + OOLog(@"universe.bug.setSystemData", @"BUG: localPlanetInfoOverrides entry \"%@\" is %@. This is an internal programming error; please report it.", @"immutable", planetKey); + overrideDict = [[overrideDict mutableCopy] autorelease]; + } + else + { + OOLog(@"universe.bug.setSystemData", @"BUG: localPlanetInfoOverrides entry \"%@\" is %@. This is an internal programming error; please report it.", @"not a dictionary", planetKey); + overrideDict = nil; + } + } + } + + if (overrideDict == nil) overrideDict = [NSMutableDictionary dictionary]; + + if (object != nil) [overrideDict setObject:object forKey:key]; + else [overrideDict removeObjectForKey:key]; + + [localPlanetInfoOverrides setObject:overrideDict forKey:planetKey]; +} + + +- (void) setSystemDataKey:(NSString *)key value:(NSObject *)object +{ + NSString *overrideKey = [self keyForPlanetOverridesForSystemSeed:system_seed inGalaxySeed:galaxy_seed]; + [self setObject:object forKey:key forPlanetKey:overrideKey]; +} + + +- (void) setSystemDataForGalaxy:(OOGalaxyID)gnum planet:(OOSystemID)pnum key:(NSString *)key value:(id)object +{ + // trying to set unsettable properties? + if ([key isEqualToString:KEY_RADIUS]) // buggy if we allow this key to be set + { + OOLogERR(@"script.error", @"System property '%@' cannot be set.",key); + return; + } + + NSString *overrideKey = [NSString stringWithFormat:@"%u %u", gnum, pnum]; + Random_Seed s_seed = [self systemSeedForSystemNumber:pnum]; + BOOL sameGalaxy = ([overrideKey isEqualToString:[self keyForPlanetOverridesForSystemSeed:s_seed inGalaxySeed: galaxy_seed]]); + BOOL sameSystem = (sameGalaxy && equal_seeds([self systemSeed], s_seed)); + NSDictionary *sysInfo = nil; + + // long range map fixes + if ([key isEqualToString:KEY_NAME]) + { + object=(id)[[(NSString *)object lowercaseString] capitalizedString]; + if(sameGalaxy) + { + if (system_names[pnum]) [system_names[pnum] release]; + system_names[pnum] = [(NSString *)object retain]; + } + } + else if ([key isEqualToString:@"sun_radius"]) + { + if ([object doubleValue] < 1000.0 || [object doubleValue] > 1000000.0 ) + { + object = ([object doubleValue] < 1000.0 ? (id)@"1000.0" : (id)@"1000000.0"); // works! + } + } + else if ([key hasPrefix:@"corona_"]) + { + object = (id)[NSString stringWithFormat:@"%f",OOClamp_0_1_f([object floatValue])]; + } + + [self setObject:object forKey:key forPlanetKey:overrideKey]; + + if (sameGalaxy) // refresh the current systemData cache! + sysInfo=[self generateSystemData:system_seed useCache:NO]; // needed if sameSystem + + // Apply changes that can be effective immediately, issue warning if they can't be changed just now + if (sameSystem) + { + if ([key isEqualToString:KEY_ECONOMY]) + { + if([self station]) [[self station] initialiseLocalMarketWithSeed:s_seed andRandomFactor:[[PlayerEntity sharedPlayer] random_factor]]; + } + else if ([key isEqualToString:KEY_TECHLEVEL]) + { + if([self station]){ + [[self station] setEquivalentTechLevel:[object intValue]]; + [[self station] setLocalShipyard:[self shipsForSaleForSystem:system_seed + withTL:[object intValue] atTime:[[PlayerEntity sharedPlayer] clockTime]]]; + } + } + else if ([key isEqualToString:@"sun_color"] || [key isEqualToString:@"star_count_multiplier"] || + [key isEqualToString:@"nebula_count_multiplier"] || [key hasPrefix:@"sky_"]) + { + SkyEntity *the_sky = nil; + int i; + + for (i = n_entities - 1; i > 0; i--) + if ((sortedEntities[i]) && ([sortedEntities[i] isKindOfClass:[SkyEntity class]])) + the_sky = (SkyEntity*)sortedEntities[i]; + + if (the_sky != nil) + { + [the_sky changeProperty:key withDictionary:sysInfo]; + + if ([key isEqualToString:@"sun_color"]) + { + OOColor *color=[[the_sky skyColor] blendedColorWithFraction:0.5 ofColor:[OOColor whiteColor]]; + if ([self sun]) [[self sun] setSunColor:color]; + for (i = n_entities - 1; i > 0; i--) + if ((sortedEntities[i]) && ([sortedEntities[i] isKindOfClass:[DustEntity class]])) + [(DustEntity*)sortedEntities[i] setDustColor:color]; + } + } + } + else if ([self sun] && ([key hasPrefix:@"sun_"] || [key hasPrefix:@"corona_"])) + { + [[self sun] changeSunProperty:key withDictionary:sysInfo]; + } + else if ([key isEqualToString:@"texture"]) + { + BOOL procGen=NO; +#if ALLOW_PROCEDURAL_PLANETS + procGen = doProcedurallyTexturedPlanets; +#endif + if (procGen) + [[self planet] setUpPlanetFromTexture:(NSString *)object]; + } + else if ([key isEqualToString:@"texture_hsb_color"]) + { + [[self planet] setUpPlanetFromTexture: [[self planet] textureFileName]]; + } + } +} + + +- (id) getSystemDataForGalaxy:(OOGalaxyID)gnum planet:(OOSystemID)pnum key:(NSString *)key +{ + NSString *overrideKey = [NSString stringWithFormat:@"%u %u", gnum, pnum]; + Random_Seed s_seed = [self systemSeedForSystemNumber:pnum]; + BOOL sameGalaxy=([overrideKey isEqualToString:[self keyForPlanetOverridesForSystemSeed:s_seed inGalaxySeed: galaxy_seed]]); + + if (sameGalaxy) + { + return [[self generateSystemData:s_seed] objectForKey:key]; + } + else + { + // TODO: a safe way to retrieve other galaxies system data? + + // Retrieving data from other galaxies requires temporarily altering the present galaxy_seed. + // Altering the galaxy seed might affect system populators, markets etc. Since each + // galaxy is supposed to be a totally separate entity from the others, the usefulness + // of reading other galaxies data is actually pretty marginal. Kaks 20090812 + + return @"_OTHER_GALAXY_"; + } +} + +- (NSString *) getSystemName:(Random_Seed)s_seed +{ + return [[self generateSystemData:s_seed] stringForKey:KEY_NAME]; +} + + +- (NSString *) getSystemInhabitants:(Random_Seed) s_seed +{ + return [self getSystemInhabitants:s_seed plural:YES]; +} + + +- (NSString *) getSystemInhabitants:(Random_Seed) s_seed plural:(BOOL)plural +{ + NSString *ret = nil; + if (!plural) + ret = [[self generateSystemData:s_seed] stringForKey:KEY_INHABITANT]; + if (ret != nil) // the singular form might be absent. + return ret; + else + return [[self generateSystemData:s_seed] stringForKey:KEY_INHABITANTS]; +} + + +- (NSString *) generateSystemName:(Random_Seed) s_seed +{ + int i; + + NSString *digrams = [self descriptionForKey:@"digrams"]; + NSString *apostrophe = [self descriptionForKey:@"digrams-apostrophe"]; + NSMutableString *name = [NSMutableString string]; + int size = 4; + + if ((s_seed.a & 0x40) == 0) + size = 3; + + for (i = 0; i < size; i++) + { + NSString *c1, *c2; + int x = s_seed.f & 0x1f; + if (x != 0) + { + x += 12; x *= 2; + c1 = [digrams substringWithRange:NSMakeRange(x,1)]; + c2 = [digrams substringWithRange:NSMakeRange(x+1,1)]; + [name appendString:c1]; + if (![c2 isEqual:apostrophe]) [name appendString:c2]; + } + rotate_seed(&s_seed); + } + + return [name capitalizedString]; +} + + +- (NSString *) generatePhoneticSystemName:(Random_Seed) s_seed +{ + int i; +#if OOLITE_MAC_OS_X + NSString *phonograms = [self descriptionForKey:@"phonograms"]; +#else + NSString *phonograms = [self descriptionForKey:@"espkphonos"]; +#endif + NSMutableString *name = [NSMutableString string]; + int size = 4; + + if ((s_seed.a & 0x40) == 0) + size = 3; + + for (i = 0; i < size; i++) + { + NSString *c1; + int x = s_seed.f & 0x1f; + if (x != 0) + { + x += 12; x *= 4; + c1 = [phonograms substringWithRange:NSMakeRange(x,4)]; + [name appendString:c1]; + } + rotate_seed(&s_seed); + } + +#if OOLITE_MAC_OS_X + return [NSString stringWithFormat:@"[[inpt PHON]]%@[[inpt TEXT]]", name]; +#else + return [NSString stringWithFormat:@"[[%@]]", name]; +#endif +} + + +- (NSString *) generateSystemInhabitants:(Random_Seed)s_seed plural:(BOOL)plural +{ + NSMutableString *inhabitants = [NSMutableString string]; + NSArray *inhabitantStrings = nil; + //i18n: Some languages have different plural and singular forms for adjectives. + BOOL singularAdjectivesExist = NO; + + // getSystemInhabitants is now used in most cases, to enable plist overrides. + if (s_seed.e < 127) + { + [inhabitants appendString:DESC_PLURAL(@"human-colonial-description", plural ? -1 : 1)]; + } + else + { + inhabitantStrings = [[self descriptions] arrayForKey:KEY_INHABITANTS]; + // The first 5 arrays in 'inhabitants' are the standard ones, anything else below is language specific + // and will refer to the different singular forms for the particular language we are translating to. + // If this is the case, three more arrays are expected, raising the total count of subarrays to 8. + singularAdjectivesExist = [inhabitantStrings count] == 8; + + int inhab = (s_seed.f / 4) & 7; + if (inhab < 3) + [inhabitants appendString:[[inhabitantStrings arrayAtIndex:plural ? + 0 : singularAdjectivesExist ? 5 : 0] stringAtIndex:inhab]]; + + inhab = s_seed.f / 32; + if (inhab < 6) + { + [inhabitants appendString:@" "]; + [inhabitants appendString:[[inhabitantStrings arrayAtIndex:plural ? + 1 : singularAdjectivesExist ? 6 : 1] stringAtIndex:inhab]]; + } + + inhab = (s_seed.d ^ s_seed.b) & 7; + if (inhab < 6) + { + [inhabitants appendString:@" "]; + [inhabitants appendString:[[inhabitantStrings arrayAtIndex:plural ? + 2 : singularAdjectivesExist ? 7 : 2] stringAtIndex:inhab]]; + } + + inhab = (inhab + (s_seed.f & 3)) & 7; + [inhabitants appendString:@" "]; + [inhabitants appendString:[[inhabitantStrings arrayAtIndex:plural ? 4 : 3] stringAtIndex:inhab]]; + } + + return inhabitants; +} + + +- (Random_Seed) findSystemAtCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed +{ + return systems[[self findSystemNumberAtCoords:coords withGalaxySeed:gal_seed]]; +} + + +- (NSArray*) nearbyDestinationsWithinRange:(double) range +{ + Random_Seed here = [self systemSeed]; + int i; + NSMutableArray* result = [NSMutableArray arrayWithCapacity:16]; + + // make list of connected systems + for (i = 0; i < 256; i++) + { + double dist = distanceBetweenPlanetPositions(here.d, here.b, systems[i].d, systems[i].b); + if ((dist > 0) && (dist <= range) && (dist <= 7.0)) // limit to systems within 7LY + { + [result addObject: [NSDictionary dictionaryWithObjectsAndKeys: + StringFromRandomSeed(systems[i]), @"system_seed", + [NSNumber numberWithDouble:dist], @"distance", + [self getSystemName:systems[i]], @"name", + nil]]; + } + } + + return result; +} + + +- (Random_Seed) findNeighbouringSystemToCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed +{ + if (!equal_seeds(gal_seed, galaxy_seed)) + [self setGalaxy_seed:gal_seed]; + + Random_Seed system = gal_seed; + double distance; + int n,i,j; + double min_dist = 10000.0; + + // make list of connected systems + BOOL connected[256]; + for (i = 0; i < 256; i++) + connected[i] = NO; + connected[0] = YES; // system zero is always connected (true for galaxies 0..7) + for (n = 0; n < 3; n++) //repeat three times for surety + { + for (i = 0; i < 256; i++) // flood fill out from system zero + { + for (j = 0; j < 256; j++) + { + double dist = distanceBetweenPlanetPositions(systems[i].d, systems[i].b, systems[j].d, systems[j].b); + if (dist <= 7.0) + { + connected[j] |= connected[i]; + connected[i] |= connected[j]; + } + } + } + } + + for (i = 0; i < 256; i++) + { + distance = distanceBetweenPlanetPositions((int)coords.x, (int)coords.y, systems[i].d, systems[i].b); + if ((connected[i])&&(distance < min_dist)&&(distance != 0.0)) + { + min_dist = distance; + system = systems[i]; + } + } + + return system; +} + + +- (Random_Seed) findConnectedSystemAtCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed +{ + if (!equal_seeds(gal_seed, galaxy_seed)) + [self setGalaxy_seed:gal_seed]; + + Random_Seed system = gal_seed; + double distance; + int n,i,j; + double min_dist = 10000.0; + + // make list of connected systems + BOOL connected[256]; + for (i = 0; i < 256; i++) + connected[i] = NO; + connected[0] = YES; // system zero is always connected (true for galaxies 0..7) + for (n = 0; n < 3; n++) //repeat three times for surety + { + for (i = 0; i < 256; i++) // flood fill out from system zero + { + for (j = 0; j < 256; j++) + { + double dist = distanceBetweenPlanetPositions(systems[i].d, systems[i].b, systems[j].d, systems[j].b); + if (dist <= 7.0) + { + connected[j] |= connected[i]; + connected[i] |= connected[j]; + } + } + } + } + + for (i = 0; i < 256; i++) + { + distance = distanceBetweenPlanetPositions((int)coords.x, (int)coords.y, systems[i].d, systems[i].b); + if ((connected[i])&&(distance < min_dist)) + { + min_dist = distance; + system = systems[i]; + } + } + + return system; +} + + +- (int) findSystemNumberAtCoords:(NSPoint) coords withGalaxySeed:(Random_Seed) gal_seed +{ + if (!equal_seeds(gal_seed, galaxy_seed)) + [self setGalaxy_seed:gal_seed]; + + OOUInteger system = NSNotFound; + unsigned distance, dx, dy; + unsigned i; + unsigned min_dist = 10000; + + for (i = 0; i < 256; i++) + { + dx = abs(coords.x - systems[i].d); + dy = abs(coords.y - systems[i].b); + + if (dx > dy) distance = (dx + dx + dy) / 2; + else distance = (dx + dy + dy) / 2; + + if (distance < min_dist) + { + min_dist = distance; + system = i; + } + + if ((distance == min_dist)&&(coords.y > systems[i].b)) // with coincident systems choose only if ABOVE + { + system = i; + } + } + return system; +} + + +- (NSPoint) findSystemCoordinatesWithPrefix:(NSString *) p_fix withGalaxySeed:(Random_Seed) gal_seed +{ + if (!equal_seeds(gal_seed, galaxy_seed)) + [self setGalaxy_seed:gal_seed]; + + NSPoint system_coords = NSMakePoint(-1.0,-1.0); + int i; + int result = -1; + for (i = 0; i < 256; i++) + { + system_found[i] = NO; + if ([[system_names[i] lowercaseString] hasPrefix:p_fix]) + { + system_found[i] = YES; + if (result < 0) + { + system_coords.x = systems[i].d; + system_coords.y = systems[i].b; + result = i; + } + } + } + return system_coords; +} + + +- (BOOL*) systems_found +{ + return (BOOL*)system_found; +} + + +- (NSString*)systemNameIndex:(OOSystemID)index +{ + return system_names[index & 255]; +} + + +- (NSDictionary *) routeFromSystem:(OOSystemID) start toSystem:(OOSystemID) goal +{ + NSMutableArray *route = [NSMutableArray arrayWithCapacity:255]; + + // range checks + if ((start > 255)||(goal > 255)) return nil; + + // use A* algorithm to determine shortest route + + // for this we need the neighbouring (<= 7LY distant) systems + // listed for each system[] + + NSMutableArray *neighbour_systems = [NSMutableArray arrayWithCapacity:256]; + unsigned i; + for (i = 0; i < 256; i++) + [neighbour_systems addObject:[self neighboursToSystem:i]]; // each is retained as it goes in + + // each node must store these values: + // g(X) cost_from_start == distance from node to parent_node + g(parent node) + // h(X) cost_to_goal (heuristic estimate) == distance from node to goal + // f(X) total_cost_estimate == g(X) + h(X) + // parent_node + + // each node will be stored as a NSDictionary + + // two lists of nodes are required: + // open_nodes (yet to be explored) = a priority list where the next node always has the lowest f(X) + // closed_nodes (explored) + + // the open list will be stored as an NSMutableArray of indices to node_open with additions to the priority queue + // being inserted into the correct position, a list of pointers also tracks each node + + NSMutableArray *open_nodes = [NSMutableArray arrayWithCapacity:256]; + NSDictionary *node_open[256]; + + // the closed list is a simple array of flags + + BOOL node_closed[256]; + + // initialise the lists: + for (i = 0; i < 256; i++) + { + node_closed[i] = NO; + node_open[i] = nil; + } + + // initialise the start node + OOSystemID location = start; + double cost_from_start = 0.0; + double cost_to_goal = distanceBetweenPlanetPositions(systems[start].d, systems[start].b, systems[goal].d, systems[goal].b); + double total_cost_estimate = cost_from_start + cost_to_goal; + NSDictionary *parent_node = nil; + + NSDictionary *startNode = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:location], @"location", + [NSNumber numberWithDouble:cost_from_start], @"cost_from_start", + [NSNumber numberWithDouble:cost_to_goal], @"cost_to_goal", + [NSNumber numberWithDouble:total_cost_estimate], @"total_cost_estimate", + NULL]; + + // push start node on open + [open_nodes addObject:[NSNumber numberWithInt:start]]; + node_open[start] = startNode; + + // process the list until success or failure + while ([open_nodes count] > 0) + { + // pop the node from open list + location = [open_nodes unsignedCharAtIndex:0]; + + NSDictionary* node = node_open[location]; + [open_nodes removeObjectAtIndex:0]; + + cost_from_start = [node doubleForKey:@"cost_from_start"]; +#if DEAD_STORE + cost_to_goal = [node doubleForKey:@"cost_to_goal"]; +#endif + total_cost_estimate = [node doubleForKey:@"total_cost_estimate"]; + parent_node = [node dictionaryForKey:@"parent_node"]; + + // if at goal we're done! + if (location == goal) + { + // construct route backwards from this location + double total_cost = total_cost_estimate; + while (parent_node) + { + [route insertObject:[node objectForKey:@"location"] atIndex:0]; + node = parent_node; +#if DEAD_STORE + //Unused variables. -- Ahruman 2008-11-10 + location = [node intForKey:@"location"]; + cost_from_start = [node doubleForKey:@"cost_from_start"]; + cost_to_goal = [node doubleForKey:@"cost_to_goal"]; + total_cost_estimate = [node doubleForKey:@"total_cost_estimate"]; +#endif + parent_node = [node dictionaryForKey:@"parent_node"]; + } + [route insertUnsignedInteger:start atIndex:0]; + return [NSDictionary dictionaryWithObjectsAndKeys: + route, @"route", + [NSNumber numberWithDouble:total_cost], @"distance", + nil]; // we're done! + } + else + { + NSArray* neighbours = [neighbour_systems arrayAtIndex:location]; + + for (i = 0; i < [neighbours count]; i++) + { + OOSystemID newLocation = [neighbours intAtIndex:i]; + double newCostFromStart = cost_from_start + distanceBetweenPlanetPositions(systems[newLocation].d, systems[newLocation].b, systems[location].d, systems[location].b); + double newCostToGoal = distanceBetweenPlanetPositions(systems[newLocation].d, systems[newLocation].b, systems[goal].d, systems[goal].b); + double newTotalCostEstimate = newCostFromStart + newCostToGoal; + + // ignore this node if it exists and there's no improvement + BOOL ignore_node = node_closed[newLocation]; + if (node_open[newLocation]) + { + if ([node_open[newLocation] doubleForKey:@"cost_from_start"] <= newCostFromStart) + ignore_node = YES; + } + if (!ignore_node) + { + // store the new or improved information + NSDictionary* newNode = [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithInt:newLocation], @"location", + [NSNumber numberWithDouble:newCostFromStart], @"cost_from_start", + [NSNumber numberWithDouble:newCostToGoal], @"cost_to_goal", + [NSNumber numberWithDouble:newTotalCostEstimate], @"total_cost_estimate", + node, @"parent_node", + NULL]; + // remove node from closed list + node_closed[newLocation] = NO; + // add node to open list + node_open[newLocation] = newNode; + // add node to priority queue + unsigned p = 0; + while (p < [open_nodes count]) + { + NSDictionary* node_ref = node_open[[open_nodes unsignedCharAtIndex:p]]; + if ([node_ref doubleForKey:@"total_cost_estimate"] > newTotalCostEstimate) + { + [open_nodes insertObject:[NSNumber numberWithInt:newLocation] atIndex:p]; + p = 99999; + } + p++; + } + if (p < 256) // not found a place, add it on the end + [open_nodes addObject:[NSNumber numberWithInt:newLocation]]; + + } + } + } + node_closed[location] = YES; + } + + // if we get here, we've failed to find a route + + return nil; +} + + +- (NSArray *) neighboursToSystem: (OOSystemID) system_number +{ + NSMutableArray *neighbours = [NSMutableArray arrayWithCapacity:32]; + double distance; + OOSystemID i; + for (i = 0; i < 256; i++) + { + distance = distanceBetweenPlanetPositions(systems[system_number].d, systems[system_number].b, systems[i].d, systems[i].b); + if ((distance <= 7.0)&&(i != system_number)) + { + [neighbours addObject:[NSNumber numberWithInt:i]]; + } + } + return neighbours; +} + + +- (NSMutableDictionary *) localPlanetInfoOverrides; +{ + return localPlanetInfoOverrides; +} + + +- (void) setLocalPlanetInfoOverrides:(NSDictionary *)dict +{ + NSEnumerator *keyEnum = nil; + NSString *key = nil; + id value = nil; + + /* Bug: localPlanetInfoOverrides contains immutable dictionaries, rather + than mutable dictionaries. + Analysis: when loading a saved game, localPlanetInfoOverrides is + restored using setLocalPlanetInfoOverrides:. This was using + -[NSMutableDictionary dictionaryWithDictionary:] to copy the immutable + dictionary from the saved game. This is a shallow copy, however, + creating a mutable dictionary of immutable dictionaries. + Fix: explicitly make mutable copies of member dictionaries. (The + contents of those dictionaries, in turn, can be immutable.) + */ + [localPlanetInfoOverrides release]; + + localPlanetInfoOverrides = [[NSMutableDictionary alloc] initWithCapacity:[dict count]]; + + for (keyEnum = [dict keyEnumerator]; (key = [keyEnum nextObject]); ) + { + value = [dict objectForKey:key]; + if (value != nil) + { + value = [value mutableCopy]; + [localPlanetInfoOverrides setObject:value forKey:key]; + [value release]; + } + } +} + + +- (NSDictionary *) planetInfo +{ + return planetInfo; +} + + +- (NSArray *) equipmentData +{ + return equipmentData; +} + + +- (NSDictionary *) commodityLists +{ + return commodityLists; +} + + +- (NSArray *) commodityData +{ + return commodityData; +} + + +- (BOOL) generateEconomicDataWithEconomy:(OOEconomyID) economy andRandomFactor:(int) random_factor +{ + [commodityData release]; + commodityData = [[self commodityDataForEconomy:economy andStation:[self station] andRandomFactor:random_factor] retain]; + return YES; +} + + +- (NSArray *) commodityDataForEconomy:(OOEconomyID) economy andStation:(StationEntity *)some_station andRandomFactor:(int) random_factor +{ + NSString *stationRole = nil; + NSMutableArray *ourEconomy = nil; + unsigned i; + + stationRole = [[self currentSystemData] stringForKey:@"market"]; + if (stationRole == nil) stationRole = [some_station primaryRole]; + if ([commodityLists arrayForKey:stationRole] == nil) stationRole = @"default"; + + ourEconomy = [NSMutableArray arrayWithArray:[commodityLists arrayForKey:stationRole]]; + + for (i = 0; i < [ourEconomy count]; i++) + { + NSMutableArray *commodityInfo = [[ourEconomy arrayAtIndex:i] mutableCopy]; + + int base_price = [commodityInfo intAtIndex:MARKET_BASE_PRICE]; + int eco_adjust_price = [commodityInfo intAtIndex:MARKET_ECO_ADJUST_PRICE]; + int eco_adjust_quantity = [commodityInfo intAtIndex:MARKET_ECO_ADJUST_QUANTITY]; + int base_quantity = [commodityInfo intAtIndex:MARKET_BASE_QUANTITY]; + int mask_price = [commodityInfo intAtIndex:MARKET_MASK_PRICE]; + int mask_quantity = [commodityInfo intAtIndex:MARKET_MASK_QUANTITY]; + + int price = (base_price + (random_factor & mask_price) + (economy * eco_adjust_price)) & 255; + int quantity = (base_quantity + (random_factor & mask_quantity) - (economy * eco_adjust_quantity)) & 255; + + if (quantity > 127) quantity = 0; + quantity &= 63; + + [commodityInfo replaceObjectAtIndex:MARKET_PRICE withObject:[NSNumber numberWithInt:price * 4]]; + [commodityInfo replaceObjectAtIndex:MARKET_QUANTITY withObject:[NSNumber numberWithInt:quantity]]; + + [ourEconomy replaceObjectAtIndex:i withObject:[NSArray arrayWithArray:commodityInfo]]; + [commodityInfo release]; // release, done + } + + return [NSArray arrayWithArray:ourEconomy]; +} + + +double estimatedTimeForJourney(double distance, int hops) +{ + int min_hops = (hops > 1)? (hops - 1) : 1; + return 2000 * hops + 4000 * distance * distance / min_hops; +} + + +- (NSArray *) passengersForSystem:(Random_Seed) s_seed atTime:(double) current_time +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + + int player_repute = [player passengerReputation]; + + int random_factor = current_time; + random_factor = (random_factor >> 24) &0xff; + + // passenger departure time is generated by passenger_seed.a << 16 + passenger_seed.b << 8 + passenger_seed.c + // added to (long)(current_time) & 0xffffffffff000000 + // to give a time somewhen in the 97 days before and after the current_time + + int start = [self findSystemNumberAtCoords:NSMakePoint(s_seed.d, s_seed.b) withGalaxySeed:galaxy_seed]; + NSString* native_species = [self getSystemInhabitants:s_seed plural:NO]; + + // adjust basic seed by market random factor + Random_Seed passenger_seed = s_seed; + passenger_seed.a ^= random_factor; // XOR + passenger_seed.b ^= passenger_seed.a; // XOR + passenger_seed.c ^= passenger_seed.b; // XOR + passenger_seed.d ^= passenger_seed.c; // XOR + passenger_seed.e ^= passenger_seed.d; // XOR + passenger_seed.f ^= passenger_seed.e; // XOR + + NSMutableArray* resultArray = [NSMutableArray arrayWithCapacity:255]; + unsigned i = 0; + + for (i = 0; i < 256; i++) + { + long long reference_time = 0x1000000 * floor(current_time / 0x1000000); + + long long passenger_time = passenger_seed.a * 0x10000 + passenger_seed.b * 0x100 + passenger_seed.c; + double passenger_departure_time = reference_time + passenger_time; + + if (passenger_departure_time < 0) + passenger_departure_time += 0x1000000; // roll it around + + double days_until_departure = (passenger_departure_time - current_time) / 86400.0; + + + OOSystemID passenger_destination = passenger_seed.d; // system number 0..255 + Random_Seed destination_seed = systems[passenger_destination]; + NSDictionary *destinationInfo = [self generateSystemData:destination_seed]; + OOGovernmentID destination_government = [destinationInfo unsignedIntForKey:KEY_GOVERNMENT]; + + int pick_up_factor = destination_government + floor(days_until_departure) - 7; // lower for anarchies (gov 0) + + if ((days_until_departure > 0.0)&&(pick_up_factor <= player_repute)&&(passenger_seed.d != start)) + { + BOOL lowercaseIgnore = [[self descriptions] boolForKey:@"lowercase_ignore"]; // i18n. + // determine the passenger's species + int passenger_species = passenger_seed.f & 3; // 0-1 native, 2 human colonial, 3 other + NSString* passenger_species_string = [NSString stringWithString:native_species]; + if (passenger_species == 2) + passenger_species_string = DESC(@"human-colonial-description%0"); + if (passenger_species == 3) + { + passenger_species_string = [self getSystemInhabitants:passenger_seed plural:NO]; + } + if(!lowercaseIgnore) + { + passenger_species_string = [[passenger_species_string lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + else + { + passenger_species_string = [passenger_species_string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + } + + // determine the passenger's name + seed_RNG_only_for_planet_description(passenger_seed); + NSString* passenger_name = [NSString stringWithFormat:@"%@ %@", ExpandDescriptionForSeed(@"%R", passenger_seed), ExpandDescriptionForSeed(@"%R", passenger_seed)]; + // If passenger is a human, make his name more... human like. + if ([[passenger_species_string componentsSeparatedByString:@" "] containsObject:DESC(@"human-word")]) + passenger_name = [NSString stringWithFormat:@"%@ %@", ExpandDescriptionForSeed(@"%R", passenger_seed), ExpandDescriptionForSeed(@"[nom]", passenger_seed)]; + + // determine information about the route... + NSDictionary* routeInfo = [self routeFromSystem:start toSystem:passenger_destination]; + + // some routes are impossible! + if (routeInfo) + { + NSString* destination_name = [self getSystemName:destination_seed]; + + double route_length = [routeInfo doubleForKey:@"distance"]; + int route_hops = [[routeInfo arrayForKey:@"route"] count] - 1; + + // Credits increase exponentially with number of hops (more with reputation > 5) + 8..15 cr per LY + bonus for low government level of destination + OOCreditsQuantity fee = 5 * pow(route_hops, player_repute > 5 ? 2.65 : 2.5) + route_length * (8 + (passenger_seed.e & 7)) + 5 * (7 - destination_government) * (7 - destination_government); + + fee = cunningFee(fee); + + // premium = 20% of fee + int premium = fee * 20 / 100; + fee -= premium; + + // 1hr per LY*LY, + 30 mins per hop + double passenger_arrival_time = passenger_departure_time + estimatedTimeForJourney(route_length, route_hops); + + + NSString* long_description = [NSString stringWithFormat: + DESC(@"contracts-@-a-@-wishes-to-go-to-@"), + passenger_name, passenger_species_string, destination_name]; + + long_description = [NSString stringWithFormat: + DESC_PLURAL(@"contracts-@-the-route-is-f-light-years-long-a-minimum-of-d-jumps", route_hops), long_description, + route_length, route_hops]; + + long_description = [NSString stringWithFormat: + DESC(@"contracts-@-you-will-need-to-depart-within-@-in-order-to-arrive-within-@-time"), long_description, + [self shortTimeDescription:(passenger_departure_time - current_time)], [self shortTimeDescription:(passenger_arrival_time - current_time)]]; + + long_description = [NSString stringWithFormat: + DESC(@"contracts-@-will-pay-@-@-in-advance-and-@-on-arrival"), long_description, + OOIntCredits(premium + fee), OOIntCredits(premium), OOIntCredits(fee)]; + + NSDictionary* passenger_info_dictionary = [NSDictionary dictionaryWithObjectsAndKeys: + passenger_name, PASSENGER_KEY_NAME, + destination_name, PASSENGER_KEY_DESTINATION_NAME, + [NSNumber numberWithInt:start], PASSENGER_KEY_START, + [NSNumber numberWithInt:passenger_destination], PASSENGER_KEY_DESTINATION, + long_description, PASSENGER_KEY_LONG_DESCRIPTION, + [NSNumber numberWithDouble:passenger_departure_time], PASSENGER_KEY_DEPARTURE_TIME, + [NSNumber numberWithDouble:passenger_arrival_time], PASSENGER_KEY_ARRIVAL_TIME, + [NSNumber numberWithInt:fee], PASSENGER_KEY_FEE, + [NSNumber numberWithInt:premium], PASSENGER_KEY_PREMIUM, + NULL]; + + [resultArray addObject:passenger_info_dictionary]; + } + } + + // next passenger + rotate_seed(&passenger_seed); + rotate_seed(&passenger_seed); + rotate_seed(&passenger_seed); + rotate_seed(&passenger_seed); + + } + + return [NSArray arrayWithArray:resultArray]; +} + + +- (NSString *) timeDescription:(double) interval +{ + double r_time = interval; + NSString* result = @""; + + if (r_time > 86400) + { + int days = floor(r_time / 86400); + r_time -= 86400 * days; + result = [NSString stringWithFormat:@"%@ %d day%@", result, days, (days > 1) ? @"s" : @""]; + } + if (r_time > 3600) + { + int hours = floor(r_time / 3600); + r_time -= 3600 * hours; + result = [NSString stringWithFormat:@"%@ %d hour%@", result, hours, (hours > 1) ? @"s" : @""]; + } + if (r_time > 60) + { + int mins = floor(r_time / 60); + r_time -= 60 * mins; + result = [NSString stringWithFormat:@"%@ %d minute%@", result, mins, (mins > 1) ? @"s" : @""]; + } + if (r_time > 0) + { + int secs = floor(r_time); + result = [NSString stringWithFormat:@"%@ %d second%@", result, secs, (secs > 1) ? @"s" : @""]; + } + return [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + + +- (NSString *) shortTimeDescription:(double) interval +{ + double r_time = interval; + NSString* result = @""; + int parts = 0; + + if (interval <= 0.0) + return DESC(@"contracts-no-time"); + + if (r_time > 86400) + { + int days = floor(r_time / 86400); + r_time -= 86400 * days; + result = [NSString stringWithFormat:@"%@ %d %@", result, days, DESC_PLURAL(@"contracts-day-word", days)]; + parts++; + } + if (r_time > 3600) + { + int hours = floor(r_time / 3600); + r_time -= 3600 * hours; + result = [NSString stringWithFormat:@"%@ %d %@", result, hours, DESC_PLURAL(@"contracts-hour-word", hours)]; + parts++; + } + if (parts < 2 && r_time > 60) + { + int mins = floor(r_time / 60); + r_time -= 60 * mins; + result = [NSString stringWithFormat:@"%@ %d %@", result, mins, DESC_PLURAL(@"contracts-minute-word", mins)]; + parts++; + } + if (parts < 2 && r_time > 0) + { + int secs = floor(r_time); + result = [NSString stringWithFormat:@"%@ %d %@", result, secs, DESC_PLURAL(@"contracts-second-word", secs)]; + } + return [result stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; +} + + +- (NSArray *) contractsForSystem:(Random_Seed) s_seed atTime:(double) current_time +{ + PlayerEntity* player = [PlayerEntity sharedPlayer]; + + int player_repute = [player contractReputation]; + + int random_factor = current_time; + random_factor = (random_factor >> 24) &0xff; + + // contract departure time is generated by contract_seed.a << 16 + contract_seed.b << 8 + contract_seed.c + // added to (long)(current_time + 0x800000) & 0xffffffffff000000 + // to give a time somewhen in the 97 days before and after the current_time + + int start = [self findSystemNumberAtCoords:NSMakePoint(s_seed.d, s_seed.b) withGalaxySeed:galaxy_seed]; + + // adjust basic seed by market random factor + Random_Seed contract_seed = s_seed; + contract_seed.f ^= random_factor; // XOR back to front + contract_seed.e ^= contract_seed.f; // XOR + contract_seed.d ^= contract_seed.e; // XOR + contract_seed.c ^= contract_seed.d; // XOR + contract_seed.b ^= contract_seed.c; // XOR + contract_seed.a ^= contract_seed.b; // XOR + + NSMutableArray* resultArray = [NSMutableArray arrayWithCapacity:255]; + int i = 0; + + NSArray* localMarket; + if ([[self station] localMarket]) + localMarket = [[self station] localMarket]; + else + localMarket = [[self station] initialiseLocalMarketWithSeed:s_seed andRandomFactor:random_factor]; + + for (i = 0; i < 256; i++) + { + long long reference_time = 0x1000000 * floor(current_time / 0x1000000); + + long long contract_time = contract_seed.a * 0x10000 + contract_seed.b * 0x100 + contract_seed.c; + double contract_departure_time = reference_time + contract_time; + + if (contract_departure_time < 0) + contract_departure_time += 0x1000000; // wrap around + + double days_until_departure = (contract_departure_time - current_time) / 86400.0; + + // determine the destination + int contract_destination = contract_seed.d; // system number 0..255 + Random_Seed destination_seed = systems[contract_destination]; + + NSDictionary *destinationInfo = [self generateSystemData:destination_seed]; + OOGovernmentID destination_government = [destinationInfo unsignedIntForKey:KEY_GOVERNMENT]; + + int pick_up_factor = destination_government + floor(days_until_departure) - 7; // lower for anarchies (gov 0) + + if ((days_until_departure > 0.0)&&(pick_up_factor <= player_repute)&&(contract_seed.d != start)) + { + OOGovernmentID destination_economy = [destinationInfo unsignedIntForKey:KEY_ECONOMY]; + NSArray *destinationMarket = [self commodityDataForEconomy:destination_economy andStation:[self station] andRandomFactor:random_factor]; + + // now we need a commodity that's both plentiful here and scarce there... + // build list of goods allocating 0..100 for each based on how + // much of each quantity there is. Use a ratio of n x 100/64 + int quantities[[localMarket count]]; + int total_quantity = 0; + unsigned i; + for (i = 0; i < [localMarket count]; i++) + { + // -- plentiful here + int q = [[localMarket arrayAtIndex:i] intAtIndex:MARKET_QUANTITY]; + if (q < 0) q = 0; + if (q > 64) q = 64; + quantities[i] = q; + // -- and scarce there + q = 64 - [[destinationMarket arrayAtIndex:i] intAtIndex:MARKET_QUANTITY]; + if (q < 0) q = 0; + if (q > 64) q = 64; + quantities[i] *= q; // multiply plentiful factor x scarce factor + total_quantity += quantities[i]; + } + int co_type, qr, unit; + unsigned int co_amount; + // seed random number generator + int super_rand1 = contract_seed.a * 256 * 256 + contract_seed.c * 256 + contract_seed.e; + int super_rand2 = contract_seed.b * 256 * 256 + contract_seed.d * 256 + contract_seed.f; + ranrot_srand(super_rand2); + + // select a random point in the histogram + qr = total_quantity ? (super_rand2 % total_quantity) : 0; + + co_type = 0; + while (qr > 0) + { + qr -= quantities[co_type++]; + } + if (--co_type < 0) { + continue; + } + // units + unit = [self unitsForCommodity:co_type]; + + if ((unit == UNITS_TONS)||([player contractReputation] == 7)) // only the best reputation gets to carry gold/platinum/jewels + { + // how much?... + co_amount = 0; + while (co_amount < 30) { + co_amount += ((1 + (Ranrot() & 31)) * (1 + (Ranrot() & 15)) * [self getRandomAmountOfCommodity:co_type]); + } + // calculate a quantity discount + int discount = 10 + floor (0.1 * co_amount); + if (discount > 35) + discount = 35; + + int price_per_unit = [[localMarket arrayAtIndex:co_type] unsignedIntAtIndex:MARKET_PRICE] * (100 - discount) / 100 ; + + // what is that worth locally + float local_cargo_value = 0.1 * co_amount * price_per_unit; + + // and the mark-up + float destination_cargo_value = 0.1 * co_amount * [[destinationMarket arrayAtIndex:co_type] unsignedIntAtIndex:MARKET_PRICE] * (200 + discount) / 200 ; + + // total profit + float profit_for_trip = destination_cargo_value - local_cargo_value; + + if (profit_for_trip > 100.0) // overheads!! + { + // determine information about the route... + NSDictionary* routeInfo = [self routeFromSystem:start toSystem:contract_destination]; + + // some routes are impossible! + if (routeInfo) + { + NSString *destination_name = [self getSystemName:destination_seed]; + + double route_length = [routeInfo doubleForKey:@"distance"]; + int route_hops = [[routeInfo arrayForKey:@"route"] count] - 1; + + // percentage taken by contracter + int contractors_share = 90 + destination_government; + // less 5% per op to a minimum of 10% + contractors_share -= route_hops * 10; + if (contractors_share < 10) + contractors_share = 10; + int contract_share = 100 - contractors_share; + + // what the contract pays + float fee = profit_for_trip * contract_share / 100; + + fee = cunningFee(fee); + + // premium = local price + float premium = round(local_cargo_value); + + // 1hr per LY*LY, + 30 mins per hop + double contract_arrival_time = contract_departure_time + estimatedTimeForJourney(route_length, route_hops); + + NSString* long_description = [NSString stringWithFormat: + DESC(@"contracts-deliver-a-cargo-of-@-to-@"), + [self describeCommodity:co_type amount:co_amount], destination_name]; + + long_description = [NSString stringWithFormat: + DESC_PLURAL(@"contracts-@-the-route-is-f-light-years-long-a-minimum-of-d-jumps", route_hops), long_description, + route_length, route_hops]; + + long_description = [NSString stringWithFormat: + DESC(@"contracts-@-you-will-need-to-depart-within-@-in-order-to-arrive-within-@-time"), long_description, + [self shortTimeDescription:(contract_departure_time - current_time)], [self shortTimeDescription:(contract_arrival_time - current_time)]]; + + long_description = [NSString stringWithFormat: + DESC(@"contracts-@-the-contract-will-cost-you-@-and-pay-a-total-of-@"), long_description, + OOIntCredits(premium), OOIntCredits(premium + fee)]; + + NSDictionary* contract_info_dictionary = [NSDictionary dictionaryWithObjectsAndKeys: + [NSString stringWithFormat:@"%06x-%06x", super_rand1, super_rand2 ],CONTRACT_KEY_ID, + [NSNumber numberWithInt:start], CONTRACT_KEY_START, + [NSNumber numberWithInt:contract_destination], CONTRACT_KEY_DESTINATION, + destination_name, CONTRACT_KEY_DESTINATION_NAME, + [NSNumber numberWithInt:co_type], CONTRACT_KEY_CARGO_TYPE, + [NSNumber numberWithInt:co_amount], CONTRACT_KEY_CARGO_AMOUNT, + [self describeCommodity:co_type amount:co_amount], CONTRACT_KEY_CARGO_DESCRIPTION, + long_description, CONTRACT_KEY_LONG_DESCRIPTION, + [NSNumber numberWithDouble:contract_departure_time], CONTRACT_KEY_DEPARTURE_TIME, + [NSNumber numberWithDouble:contract_arrival_time], CONTRACT_KEY_ARRIVAL_TIME, + [NSNumber numberWithFloat:fee], CONTRACT_KEY_FEE, + [NSNumber numberWithFloat:premium], CONTRACT_KEY_PREMIUM, + NULL]; + + [resultArray addObject:contract_info_dictionary]; + } + } + } + } + + // next contract + rotate_seed(&contract_seed); + rotate_seed(&contract_seed); + rotate_seed(&contract_seed); + rotate_seed(&contract_seed); + + } + + return [NSArray arrayWithArray:resultArray]; +} + + +- (NSArray *) shipsForSaleForSystem:(Random_Seed) s_seed withTL:(OOTechLevelID) specialTL atTime:(OOTimeAbsolute) current_time +{ + int random_factor = current_time; + random_factor = (random_factor >> 24) &0xff; + + // ship sold time is generated by ship_seed.a << 16 + ship_seed.b << 8 + ship_seed.c + // added to (long)(current_time + 0x800000) & 0xffffffffff000000 + // to give a time somewhen in the 97 days before and after the current_time + + // adjust basic seed by market random factor + Random_Seed ship_seed = s_seed; + ship_seed.f ^= random_factor; // XOR back to front + ship_seed.e ^= ship_seed.f; // XOR + ship_seed.d ^= ship_seed.e; // XOR + ship_seed.c ^= ship_seed.d; // XOR + ship_seed.b ^= ship_seed.c; // XOR + ship_seed.a ^= ship_seed.b; // XOR + + NSMutableDictionary *resultDictionary = [NSMutableDictionary dictionary]; + + float tech_price_boost = (ship_seed.a + ship_seed.b) / 256.0; + unsigned i; + PlayerEntity *player = [PlayerEntity sharedPlayer]; + OOShipRegistry *registry = [OOShipRegistry sharedRegistry]; + + for (i = 0; i < 256; i++) + { + long long reference_time = 0x1000000 * floor(current_time / 0x1000000); + + long long c_time = ship_seed.a * 0x10000 + ship_seed.b * 0x100 + ship_seed.c; + double ship_sold_time = reference_time + c_time; + + if (ship_sold_time < 0) + ship_sold_time += 0x1000000; // wraparound + + double days_until_sale = (ship_sold_time - current_time) / 86400.0; + + NSMutableArray *keysForShips = [NSMutableArray arrayWithArray:[registry playerShipKeys]]; + unsigned si; + for (si = 0; si < [keysForShips count]; si++) + { + //eliminate any ships that fail a 'conditions test' + NSString *key = [keysForShips stringAtIndex:si]; + NSDictionary *dict = [registry shipyardInfoForKey:key]; + NSArray *conditions = [dict arrayForKey:@"conditions"]; + + if (![player scriptTestConditions:conditions]) + { + [keysForShips removeObjectAtIndex:si--]; + } + } + + NSDictionary *systemInfo = [self generateSystemData:system_seed]; + OOTechLevelID techlevel; + if (specialTL != NSNotFound) + { + //if we are passed a tech level use that + techlevel = specialTL; + } + else + { + //otherwise use default for system + techlevel = [systemInfo unsignedIntForKey:KEY_TECHLEVEL]; + } + unsigned ship_index = (ship_seed.d * 0x100 + ship_seed.e) % [keysForShips count]; + NSString *ship_key = [keysForShips stringAtIndex:ship_index]; + NSDictionary *ship_info = [registry shipyardInfoForKey:ship_key]; + OOTechLevelID ship_techlevel = [ship_info intForKey:KEY_TECHLEVEL]; + + double chance = 1.0 - pow(1.0 - [ship_info doubleForKey:KEY_CHANCE], OOMax_f(1, techlevel - ship_techlevel)); + + // seed random number generator + int super_rand1 = ship_seed.a * 0x10000 + ship_seed.c * 0x100 + ship_seed.e; + int super_rand2 = ship_seed.b * 0x10000 + ship_seed.d * 0x100 + ship_seed.f; + ranrot_srand(super_rand2); + + NSDictionary* ship_base_dict = nil; + + ship_base_dict = [[OOShipRegistry sharedRegistry] shipInfoForKey:ship_key]; + + if ((days_until_sale > 0.0) && (days_until_sale < 30.0) && (ship_techlevel <= techlevel) && (randf() < chance) && (ship_base_dict != nil)) + { + NSMutableDictionary* ship_dict = [NSMutableDictionary dictionaryWithDictionary:ship_base_dict]; + NSMutableString* description = [NSMutableString stringWithCapacity:256]; + NSMutableString* short_description = [NSMutableString stringWithCapacity:256]; + NSString *shipName = [ship_dict stringForKey:@"display_name" defaultValue:[ship_dict stringForKey:KEY_NAME]]; + OOCreditsQuantity price = [ship_info unsignedIntForKey:KEY_PRICE]; + OOCreditsQuantity base_price = price; + NSMutableArray* extras = [NSMutableArray arrayWithArray:[[ship_info dictionaryForKey:KEY_STANDARD_EQUIPMENT] arrayForKey:KEY_EQUIPMENT_EXTRAS]]; + NSString* fwd_weapon_string = [[ship_info dictionaryForKey:KEY_STANDARD_EQUIPMENT] stringForKey:KEY_EQUIPMENT_FORWARD_WEAPON]; + NSString* aft_weapon_string = [[ship_info dictionaryForKey:KEY_STANDARD_EQUIPMENT] stringForKey:KEY_EQUIPMENT_AFT_WEAPON]; + + NSMutableArray* options = [NSMutableArray arrayWithArray:[ship_info arrayForKey:KEY_OPTIONAL_EQUIPMENT]]; + OOCargoQuantity max_cargo = [ship_dict unsignedIntForKey:@"max_cargo"]; + +// // more info for potential purchasers - how to reveal this I'm not yet sure... +// NSString* brochure_desc = [self brochureDescriptionWithDictionary: ship_dict standardEquipment: extras optionalEquipment: options]; +// NSLog(@"%@ Brochure description : \"%@\"", [ship_dict objectForKey:KEY_NAME], brochure_desc); + + [description appendFormat:@"%@:", shipName]; + [short_description appendFormat:@"%@:", shipName]; + + OOWeaponType fwd_weapon = EquipmentStringToWeaponTypeSloppy(fwd_weapon_string); + OOWeaponType aft_weapon = EquipmentStringToWeaponTypeSloppy(aft_weapon_string); + //port and starboard weapons are not modified in the shipyard + + int passenger_berths = 0; + BOOL customised = NO; + BOOL weapon_customised = NO; + BOOL other_weapon_added = NO; + + NSString* fwd_weapon_desc = nil; + NSString* aft_weapon_desc = nil; + + NSString* short_extras_string = DESC(@"plus-@"); + NSString* passengerBerthLongDesc = nil; + + // customise the ship (if chance = 1, then ship will get all possible add ons) + while ((randf() < chance) && ([options count])) + { + chance *= chance; //decrease the chance of a further customisation (unless it is 1, which might be a bug) + int option_index = Ranrot() % [options count]; + NSString *equipmentKey = [options stringAtIndex:option_index]; + OOEquipmentType *item = [OOEquipmentType equipmentTypeWithIdentifier:equipmentKey]; + + if (item != nil) + { + OOTechLevelID eqTechLevel = [item techLevel]; + OOCreditsQuantity eqPrice = [item price] / 10; // all amounts are x/10 due to being represented in tenths of credits. + NSString *eqShortDesc = [item name]; + NSString *eqLongDesc = [item descriptiveText]; + + if ([item techLevel] > techlevel) + { + // Cap maximum tech level. + eqTechLevel = MIN(eqTechLevel, 15U); + + // Higher tech items are rarer! + if (randf() * (eqTechLevel - techlevel) < 1.0) + { + // All included equip has a 10% discount. + eqPrice *= (tech_price_boost + eqTechLevel - techlevel) * 90 / 100; + } + else + eqPrice = 0; // Bar this upgrade. + } + + if ([item incompatibleEquipment] != nil && extras != nil) + { + NSEnumerator *keyEnum = nil; + id key = nil; + + for (keyEnum = [[item incompatibleEquipment] objectEnumerator]; (key = [keyEnum nextObject]); ) + { + if ([extras containsObject:key]) + { + [options removeObject:equipmentKey]; + eqPrice = 0; + break; + } + } + } + + if (eqPrice > 0) + { + if ([equipmentKey hasPrefix:@"EQ_WEAPON"]) + { + OOWeaponType new_weapon = EquipmentStringToWeaponTypeSloppy(equipmentKey); + //fit best weapon forward + if (new_weapon > fwd_weapon) + { + //again remember to divide price by 10 to get credits from tenths of credit + price -= [self getPriceForWeaponSystemWithKey:fwd_weapon_string] * 90 / 1000; // 90% credits + price += eqPrice; + fwd_weapon_string = equipmentKey; + fwd_weapon = new_weapon; + [ship_dict setObject:fwd_weapon_string forKey:KEY_EQUIPMENT_FORWARD_WEAPON]; + weapon_customised = YES; + fwd_weapon_desc = eqShortDesc; + } + else + { + //if less good than current forward, try fitting is to rear + if (!aft_weapon || new_weapon > aft_weapon) + { + price -= [self getPriceForWeaponSystemWithKey:aft_weapon_string] * 90 / 1000; // 90% credits + price += eqPrice; + aft_weapon_string = equipmentKey; + aft_weapon = new_weapon; + [ship_dict setObject:aft_weapon_string forKey:KEY_EQUIPMENT_AFT_WEAPON]; + other_weapon_added = YES; + aft_weapon_desc = eqShortDesc; + } + else + { + [options removeObject:equipmentKey]; //dont try again + } + } + + } + else + { + if ([equipmentKey isEqualToString:@"EQ_PASSENGER_BERTH"]) + { + if ((max_cargo >= 5) && (randf() < chance)) + { + max_cargo -= 5; + price += eqPrice; + [extras addObject:equipmentKey]; + if (passenger_berths == 0) + { + // This will be needed to construct the description for passenger berths. + // Note: use of lowercaseString is bad from an i18n perspective, + // but the string is never actually shown anyway... + passengerBerthLongDesc = [NSString stringWithFormat:@"%@", [eqLongDesc lowercaseString]]; + } + passenger_berths++; + customised = YES; + } + else + { + // remove the option if there's no space left + [options removeObject:equipmentKey]; + } + } + else + { + price += eqPrice; + [extras addObject:equipmentKey]; + [description appendFormat:DESC(@"extra-@-@"), eqShortDesc, [eqLongDesc lowercaseString]]; + [short_description appendFormat:short_extras_string, eqShortDesc]; + short_extras_string = @" %@."; + customised = YES; + [options removeObject:equipmentKey]; //dont add twice + } + } + } + } + } + // i18n: Some languages require that no conversion to lower case string takes place. + BOOL lowercaseIgnore = [[self descriptions] boolForKey:@"lowercase_ignore"]; + + if (passenger_berths) + { + NSString* npb = (passenger_berths > 1)? [NSString stringWithFormat:@"%d ", passenger_berths] : (id)@""; + NSString* ppb = DESC_PLURAL(@"passenger-berth", passenger_berths); + [description appendFormat:@"Extra %@%@ (%@)", npb, ppb, passengerBerthLongDesc]; + [short_description appendFormat:@"Extra %@%@.", npb, ppb]; + } + + if (!customised) + { + [description appendString:DESC(@"shipyard-standard-customer-model")]; + [short_description appendString:DESC(@"shipyard-standard-customer-model")]; + } + + if (weapon_customised) + { + + [description appendFormat:DESC(@"shipyard-forward-weapon-has-been-upgraded-to-a-@"), + (lowercaseIgnore ? fwd_weapon_desc : [fwd_weapon_desc lowercaseString])]; + [short_description appendFormat:DESC(@"shipyard-forward-weapon-upgraded-to-@"), + (lowercaseIgnore ? fwd_weapon_desc : [fwd_weapon_desc lowercaseString])]; + } + if (other_weapon_added) + { + [description appendFormat:@"aft %@", (lowercaseIgnore ? aft_weapon_desc : [aft_weapon_desc lowercaseString])]; + } + if (price > base_price) + { + price = base_price + cunningFee(price - base_price); + } + + [description appendFormat:DESC(@"shipyard-selling-price-@"), OOIntCredits(price)]; + [short_description appendFormat:DESC(@"shipyard-price-@"), OOIntCredits(price)]; + + NSString* ship_id = [NSString stringWithFormat:@"%06x-%06x", super_rand1, super_rand2]; + + NSDictionary* ship_info_dictionary = [NSDictionary dictionaryWithObjectsAndKeys: + ship_id, SHIPYARD_KEY_ID, + ship_key, SHIPYARD_KEY_SHIPDATA_KEY, + ship_dict, SHIPYARD_KEY_SHIP, + description, SHIPYARD_KEY_DESCRIPTION, + short_description, KEY_SHORT_DESCRIPTION, + [NSNumber numberWithInt:price], SHIPYARD_KEY_PRICE, + extras, KEY_EQUIPMENT_EXTRAS, + NULL]; + + [resultDictionary setObject:ship_info_dictionary forKey:ship_id]; // should order them fairly randomly + } + + // next contract + rotate_seed(&ship_seed); + rotate_seed(&ship_seed); + rotate_seed(&ship_seed); + rotate_seed(&ship_seed); + } + + NSMutableArray *resultArray = [[[resultDictionary allValues] mutableCopy] autorelease]; + [resultArray sortUsingFunction:compareName context:NULL]; + + // remove identically priced ships of the same name + i = 1; + + while (i < [resultArray count]) + { + if (compareName([resultArray objectAtIndex:i - 1], [resultArray objectAtIndex:i], nil) == NSOrderedSame ) + { + [resultArray removeObjectAtIndex: i]; + } + else + { + i++; + } + } + + return [NSArray arrayWithArray:resultArray]; +} + +static OOComparisonResult compareName(id dict1, id dict2, void * context) +{ + NSDictionary *ship1 = [(NSDictionary *)dict1 dictionaryForKey:SHIPYARD_KEY_SHIP]; + NSDictionary *ship2 = [(NSDictionary *)dict2 dictionaryForKey:SHIPYARD_KEY_SHIP]; + NSString *name1 = [ship1 stringForKey:KEY_NAME]; + NSString *name2 = [ship2 stringForKey:KEY_NAME]; + + NSComparisonResult result = [name1 compare:name2]; + if (result != NSOrderedSame) + return result; + else + return comparePrice(dict1, dict2, context); +} + +static OOComparisonResult comparePrice(id dict1, id dict2, void * context) +{ + NSNumber *price1 = [(NSDictionary *)dict1 objectForKey:SHIPYARD_KEY_PRICE]; + NSNumber *price2 = [(NSDictionary *)dict2 objectForKey:SHIPYARD_KEY_PRICE]; + + return [price1 compare:price2]; +} + +- (OOCreditsQuantity) tradeInValueForCommanderDictionary:(NSDictionary*) dict; +{ + // get basic information about the craft + + NSString *ship_desc = [dict stringForKey:@"ship_desc"]; + OOWeaponType ship_fwd_weapon = [dict unsignedIntForKey:@"forward_weapon"]; + OOWeaponType ship_aft_weapon = [dict unsignedIntForKey:@"aft_weapon"]; + OOWeaponType ship_port_weapon = [dict unsignedIntForKey:@"port_weapon"]; + OOWeaponType ship_starboard_weapon = [dict unsignedIntForKey:@"starboard_weapon"]; + unsigned ship_missiles = [dict unsignedIntForKey:@"missiles"]; + unsigned ship_max_passengers = [dict unsignedIntForKey:@"max_passengers"]; + NSMutableArray *ship_extra_equipment = [NSMutableArray arrayWithArray:[[dict dictionaryForKey:@"extra_equipment"] allKeys]]; + + // given the ship model (from ship_desc) + // get the basic information about the standard customer model for that craft + NSDictionary *shipyard_info = [[OOShipRegistry sharedRegistry] shipyardInfoForKey:ship_desc]; + NSDictionary *basic_info = [shipyard_info dictionaryForKey:KEY_STANDARD_EQUIPMENT]; + OOCreditsQuantity base_price = [shipyard_info unsignedLongLongForKey:SHIPYARD_KEY_PRICE]; + // This checks a rare, but possible case. If the ship for which we are trying to calculate a trade in value + // does not have a shipyard dictionary entry, report it and set its base price to 0 -- Nikos 20090613. + if (shipyard_info == nil) + { + OOLogERR(@"universe.tradeInValueForCommanderDictionary.valueCalculationError", + @"Shipyard dictionary entry for ship %@ required for trade in value calculation, but does not exist. Setting ship value to 0.", ship_desc); + + base_price = 0ULL; + } + unsigned base_missiles = [basic_info unsignedIntForKey:KEY_EQUIPMENT_MISSILES]; + OOCreditsQuantity base_missiles_value = base_missiles * [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_MISSILE"] / 10; + NSString *base_fwd_weapon_key = [basic_info stringForKey:KEY_EQUIPMENT_FORWARD_WEAPON]; + OOCreditsQuantity base_weapon_value = [UNIVERSE getPriceForWeaponSystemWithKey:base_fwd_weapon_key] / 10; + NSArray *base_extra_equipment = [basic_info arrayForKey:KEY_EQUIPMENT_EXTRAS]; + NSString *weapon_key = nil; + + + OOCreditsQuantity ship_fwd_weapon_value = 0; + OOCreditsQuantity ship_other_weapons_value = 0; + + OOCreditsQuantity ship_missiles_value = ship_missiles * [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_MISSILE"] / 10; + + // work out weapon values + if (ship_fwd_weapon) + { + weapon_key = WeaponTypeToEquipmentString(ship_fwd_weapon); + ship_fwd_weapon_value = [UNIVERSE getPriceForWeaponSystemWithKey:weapon_key] / 10; + } + if (ship_aft_weapon) + { + weapon_key = WeaponTypeToEquipmentString(ship_aft_weapon); + ship_other_weapons_value += [UNIVERSE getPriceForWeaponSystemWithKey:weapon_key] / 10; + } + if (ship_port_weapon) + { + weapon_key = WeaponTypeToEquipmentString(ship_port_weapon); + ship_other_weapons_value += [UNIVERSE getPriceForWeaponSystemWithKey:weapon_key] / 10; + } + if (ship_starboard_weapon) + { + weapon_key = WeaponTypeToEquipmentString(ship_starboard_weapon); + ship_other_weapons_value += [UNIVERSE getPriceForWeaponSystemWithKey:weapon_key] / 10; + } + + // remove from ship_extra_equipment any items in base_extra_equipment + unsigned i; + int j; + for (i = 0; i < [base_extra_equipment count]; i++) + { + NSString *standard_option = [base_extra_equipment stringAtIndex:i]; + for (j = 0; j < (int)[ship_extra_equipment count]; j++) + { + if ([[ship_extra_equipment stringAtIndex:j] isEqual:standard_option]) + [ship_extra_equipment removeObjectAtIndex:j--]; + if ((j > 0)&&([[ship_extra_equipment stringAtIndex:j] isEqual:@"EQ_PASSENGER_BERTH"])) + [ship_extra_equipment removeObjectAtIndex:j--]; + } + } + + OOCreditsQuantity extra_equipment_value = ship_max_passengers * [UNIVERSE getPriceForWeaponSystemWithKey:@"EQ_PASSENGER_BERTH"] / 10ULL; + for (i = 0; i < [ship_extra_equipment count]; i++) + extra_equipment_value += [UNIVERSE getPriceForWeaponSystemWithKey:[ship_extra_equipment stringAtIndex:i]] / 10ULL; + + // final reckoning + OOCreditsQuantity result = base_price; + + // add on extra weapons - base weapons + extra_equipment_value += ship_fwd_weapon_value - base_weapon_value; + extra_equipment_value += ship_other_weapons_value; + + // add on missile values + extra_equipment_value += ship_missiles_value - base_missiles_value; + + // add on equipment + result += (extra_equipment_value * 0.9); //discount 10% for second hand value + + return result; +} + +- (NSString*) brochureDescriptionWithDictionary:(NSDictionary*) dict standardEquipment:(NSArray*) extras optionalEquipment:(NSArray*) options +{ + NSMutableArray *mut_extras = [NSMutableArray arrayWithArray:extras]; + NSString *allOptions = [options componentsJoinedByString:@" "]; + + NSMutableString *desc = [NSMutableString stringWithFormat:@"The %@.", [dict stringForKey: KEY_NAME]]; + + // cargo capacity and expansion + OOCargoQuantity max_cargo = [dict unsignedIntForKey:@"max_cargo"]; + if (max_cargo) + { + OOCargoQuantity extra_cargo = [dict unsignedIntForKey:@"max_cargo" defaultValue:15]; + [desc appendFormat:@" Cargo capacity %dt", max_cargo]; + BOOL canExpand = ([allOptions rangeOfString:@"EQ_CARGO_BAY"].location != NSNotFound); + if (canExpand) + [desc appendFormat:@" (expandable to %dt at most starports)", max_cargo + extra_cargo]; + [desc appendString:@"."]; + } + + // speed + float top_speed = [dict intForKey:@"max_flight_speed"]; + [desc appendFormat:@" Top speed %.3fLS.", 0.001 * top_speed]; + + // passenger berths + if ([mut_extras count]) + { + unsigned n_berths = 0; + unsigned i; + for (i = 0; i < [mut_extras count]; i++) + { + NSString* item_key = [mut_extras stringAtIndex:i]; + if ([item_key isEqual:@"EQ_PASSENGER_BERTH"]) + { + n_berths++; + [mut_extras removeObjectAtIndex:i--]; + } + } + if (n_berths) + { + if (n_berths == 1) + [desc appendString:@" Includes luxury accomodation for a single passenger."]; + else + [desc appendFormat:@" Includes luxury accomodation for %d passengers.", n_berths]; + } + } + + // standard fittings + if ([mut_extras count]) + { + [desc appendString:@"\nComes with"]; + unsigned i, j; + for (i = 0; i < [mut_extras count]; i++) + { + NSString* item_key = [mut_extras stringAtIndex:i]; + NSString* item_desc = nil; + for (j = 0; ((j < [equipmentData count])&&(!item_desc)) ; j++) + { + NSString *eq_type = [[equipmentData arrayAtIndex:j] stringAtIndex:EQUIPMENT_KEY_INDEX]; + if ([eq_type isEqual:item_key]) + item_desc = [[equipmentData arrayAtIndex:j] stringAtIndex:EQUIPMENT_SHORT_DESC_INDEX]; + } + if (item_desc) + { + int c = [mut_extras count] - i; + switch (c) + { + case 1: + [desc appendFormat:@" %@ fitted as standard.", item_desc]; + break; + case 2: + [desc appendFormat:@" %@ and", item_desc]; + break; + default: + [desc appendFormat:@" %@,", item_desc]; + break; + } + } + } + } + + // optional fittings + if ([options count]) + { + [desc appendString:@"\nCan additionally be outfitted with"]; + unsigned i, j; + for (i = 0; i < [options count]; i++) + { + NSString* item_key = [options stringAtIndex:i]; + NSString* item_desc = nil; + for (j = 0; ((j < [equipmentData count])&&(!item_desc)) ; j++) + { + NSString *eq_type = [[equipmentData arrayAtIndex:j] stringAtIndex:EQUIPMENT_KEY_INDEX]; + if ([eq_type isEqual:item_key]) + item_desc = [[equipmentData arrayAtIndex:j] stringAtIndex:EQUIPMENT_SHORT_DESC_INDEX]; + } + if (item_desc) + { + int c = [options count] - i; + switch (c) + { + case 1: + [desc appendFormat:@" %@ at suitably equipped starports.", item_desc]; + break; + case 2: + [desc appendFormat:@" %@ and/or", item_desc]; + break; + default: + [desc appendFormat:@" %@,", item_desc]; + break; + } + } + } + } + + return desc; +} + + +- (Vector) getWitchspaceExitPosition +{ + Vector result; + seed_RNG_only_for_planet_description(system_seed); + + // new system is hyper-centric : witchspace exit point is origin + result.x = 0.0; + result.y = 0.0; + result.z = 0.0; + + result.x += SCANNER_MAX_RANGE*(gen_rnd_number()/256.0 - 0.5); // offset by a set amount, up to 12.8 km + result.y += SCANNER_MAX_RANGE*(gen_rnd_number()/256.0 - 0.5); + result.z += SCANNER_MAX_RANGE*(gen_rnd_number()/256.0 - 0.5); + + return result; +} + + +- (Quaternion) getWitchspaceExitRotation +{ + // this should be fairly close to {0,0,0,1} + Quaternion q_result; + seed_RNG_only_for_planet_description(system_seed); + + + q_result.x = (gen_rnd_number() - 128)/1024.0; + q_result.y = (gen_rnd_number() - 128)/1024.0; + q_result.z = (gen_rnd_number() - 128)/1024.0; + q_result.w = 1.0; + quaternion_normalize(&q_result); + + return q_result; +} + + +- (Vector) getSunSkimStartPositionForShip:(ShipEntity*) ship +{ + if (!ship) + { + OOLog(kOOLogParameterError, @"***** No ship set in Universe getSunSkimStartPositionForShip:"); + return kZeroVector; + } + PlanetEntity* the_sun = [self sun]; + // get vector from sun position to ship + if (!the_sun) + { + OOLog(kOOLogInconsistentState, @"***** No sun set in Universe getSunSkimStartPositionForShip:"); + return kZeroVector; + } + Vector v0 = the_sun->position; + Vector v1 = ship->position; + v1.x -= v0.x; v1.y -= v0.y; v1.z -= v0.z; // vector from sun to ship + if (v1.x||v1.y||v1.z) + v1 = vector_normal(v1); + else + v1.z = 1.0; + double radius = SUN_SKIM_RADIUS_FACTOR * the_sun->collision_radius - 250.0; // 250 m inside the skim radius + v1.x *= radius; v1.y *= radius; v1.z *= radius; + v1.x += v0.x; v1.y += v0.y; v1.z += v0.z; + + return v1; +} + + +- (Vector) getSunSkimEndPositionForShip:(ShipEntity*) ship +{ + PlanetEntity* the_sun = [self sun]; + if (!ship) + { + OOLog(kOOLogParameterError, @"***** No ship set in Universe getSunSkimEndPositionForShip:"); + return kZeroVector; + } + // get vector from sun position to ship + if (!the_sun) + { + OOLog(kOOLogInconsistentState, @"***** No sun set in Universe getSunSkimEndPositionForShip:"); + return kZeroVector; + } + Vector v0 = the_sun->position; + Vector v1 = ship->position; + v1.x -= v0.x; v1.y -= v0.y; v1.z -= v0.z; + if (v1.x||v1.y||v1.z) + v1 = vector_normal(v1); + else + v1.z = 1.0; + Vector v2 = make_vector(randf()-0.5, randf()-0.5, randf()-0.5); // random vector + if (v2.x||v2.y||v2.z) + v2 = vector_normal(v2); + else + v2.x = 1.0; + Vector v3 = cross_product(v1, v2); // random vector at 90 degrees to v1 and v2 (random Vector) + if (v3.x||v3.y||v3.z) + v3 = vector_normal(v3); + else + v3.y = 1.0; + double radius = the_sun->collision_radius * SUN_SKIM_RADIUS_FACTOR - 250.0; // 250 m inside the skim radius + v1.x *= radius; v1.y *= radius; v1.z *= radius; + v1.x += v0.x; v1.y += v0.y; v1.z += v0.z; + v1.x += 15000 * v3.x; v1.y += 15000 * v3.y; v1.z += 15000 * v3.z; // point 15000m at a tangent to sun from v1 + v1.x -= v0.x; v1.y -= v0.y; v1.z -= v0.z; + if (v1.x||v1.y||v1.z) + v1 = vector_normal(v1); + else + v1.z = 1.0; + v1.x *= radius; v1.y *= radius; v1.z *= radius; + v1.x += v0.x; v1.y += v0.y; v1.z += v0.z; + + return v1; +} + + +- (NSArray*) listBeaconsWithCode:(NSString*) code +{ + NSMutableArray* result = [NSMutableArray array]; + ShipEntity* beacon = [self firstBeacon]; + while (beacon) + { + NSString* beacon_code = [beacon beaconCode]; + OOLog(kOOLogFoundBeacon, @"Beacon: %@ has code %@", beacon, beacon_code); + if ([beacon_code rangeOfString:code options: NSCaseInsensitiveSearch].location != NSNotFound) + [result addObject:beacon]; + beacon = (ShipEntity*)[self entityForUniversalID:[beacon nextBeaconID]]; + } + return [result sortedArrayUsingSelector:@selector(compareBeaconCodeWith:)]; +} + + +- (void) allShipsDoScriptEvent:(NSString*) event andReactToAIMessage:(NSString*) message +{ + int i; + int ent_count = n_entities; + int ship_count = 0; + ShipEntity* my_ships[ent_count]; + for (i = 0; i < ent_count; i++) + if (sortedEntities[i]->isShip) + my_ships[ship_count++] = [sortedEntities[i] retain]; // retained + + for (i = 0; i < ship_count; i++) + { + ShipEntity* se = my_ships[i]; + [se doScriptEvent:event]; + [[se getAI] reactToMessage:message]; + [se release]; // released + } +} + +/////////////////////////////////////// + +- (GuiDisplayGen *) gui +{ + return gui; +} + + +- (GuiDisplayGen *) comm_log_gui +{ + return comm_log_gui; +} + + +- (GuiDisplayGen *) message_gui +{ + return message_gui; +} + + +- (void) clearGUIs +{ + [gui clear]; + [message_gui clear]; + [comm_log_gui clear]; + [comm_log_gui printLongText:DESC(@"communications-log-string") + align:GUI_ALIGN_CENTER color:[OOColor yellowColor] fadeTime:0 key:nil addToArray:nil]; +} + + +- (void) resetCommsLogColor +{ + [comm_log_gui setTextColor:[OOColor whiteColor]]; +} + + +- (void) setDisplayCursor:(BOOL) value +{ + displayCursor = !!value; + +#ifdef GNUSTEP + + [gameView autoShowMouse]; + +#endif +} + + +- (BOOL) displayCursor +{ + return displayCursor; +} + + +- (void) setDisplayText:(BOOL) value +{ + displayGUI = !!value; +} + + +- (BOOL) displayGUI +{ + return displayGUI; +} + + +- (void) setDisplayFPS:(BOOL) value +{ + displayFPS = !!value; +} + + +- (BOOL) displayFPS +{ + return displayFPS; +} + + +- (void) setAutoSave:(BOOL) value +{ + autoSave = !!value; + [[NSUserDefaults standardUserDefaults] setBool:autoSave forKey:@"autosave"]; +} + + +- (BOOL) autoSave +{ + return autoSave; +} + + +- (void) setAutoSaveNow:(BOOL) value +{ + autoSaveNow = !!value; +} + + +- (BOOL) autoSaveNow +{ + return autoSaveNow; +} + + +- (void) setWireframeGraphics:(BOOL) value +{ + wireframeGraphics = !!value; + [[NSUserDefaults standardUserDefaults] setBool:wireframeGraphics forKey:@"wireframe-graphics"]; +} + + +- (BOOL) wireframeGraphics +{ + return wireframeGraphics; +} + + +- (void) setReducedDetail:(BOOL) value +{ + reducedDetail = !!value; + [[NSUserDefaults standardUserDefaults] setBool:reducedDetail forKey:@"reduced-detail-graphics"]; +} + + +- (BOOL) reducedDetail +{ + return reducedDetail; +} + + +- (void) setShaderEffectsLevel:(OOShaderSetting)value +{ + if (SHADERS_MIN <= value && value <= SHADERS_MAX) + { + shaderEffectsLevel = value; + [[NSUserDefaults standardUserDefaults] setInteger:shaderEffectsLevel forKey:@"shader-effects-level"]; + } +} + + +- (OOShaderSetting) shaderEffectsLevel +{ + if (![[OOOpenGLExtensionManager sharedManager] shadersSupported]) return SHADERS_NOT_SUPPORTED; + return shaderEffectsLevel; +} + + +- (BOOL) useShaders +{ + return [self shaderEffectsLevel] > SHADERS_OFF; +} + + +- (void) handleOoliteException:(NSException*) ooliteException +{ + if (ooliteException) + { + if ([[ooliteException name] isEqual: OOLITE_EXCEPTION_FATAL]) + { + exception = [ooliteException retain]; + + PlayerEntity* player = [PlayerEntity sharedPlayer]; + [player setStatus:STATUS_HANDLING_ERROR]; + + OOLog(kOOLogException, @"***** Handling Fatal : %@ : %@ *****",[exception name], [exception reason]); + NSString* exception_msg = [NSString stringWithFormat:@"Exception : %@ : %@ Please take a screenshot and/or press esc or Q to quit.", [exception name], [exception reason]]; + [self addMessage:exception_msg forCount:30.0]; + [[self gameController] pause_game]; + } + else + { + OOLog(kOOLogException, @"***** Handling Non-fatal : %@ : %@ *****",[ooliteException name], [ooliteException reason]); + } + } +} + + +- (GLfloat)airResistanceFactor +{ + return airResistanceFactor; +} + + +// speech routines +#if OOLITE_MAC_OS_X + +- (void) startSpeakingString:(NSString *) text +{ + [speechSynthesizer startSpeakingString:[NSString stringWithFormat:@"[[volm %.3f]]%@", 0.3333333f * [OOSound masterVolume], text]]; +} + + +- (void) stopSpeaking +{ + if ([speechSynthesizer respondsToSelector:@selector(stopSpeakingAtBoundary:)]) + { + [speechSynthesizer stopSpeakingAtBoundary:NSSpeechWordBoundary]; + } + else + { + [speechSynthesizer stopSpeaking]; + } +} + + +- (BOOL) isSpeaking +{ + return [speechSynthesizer isSpeaking]; +} + +#elif OOLITE_ESPEAK + +- (void) startSpeakingString:(NSString *) text +{ + size_t length = [text length]; + char *ctext = malloc (length + 2); + sprintf (ctext, "%s%c", [text cString], 0); // extra NUL; working around an apparent misrendering bug... + espeak_Synth(ctext, length + 2 /* inc. NULs */, 0, POS_CHARACTER, length, espeakCHARS_UTF8 | espeakPHONEMES | espeakENDPAUSE, NULL, NULL); + free (ctext); +} + +- (void) stopSpeaking +{ + espeak_Cancel(); +} + +- (BOOL) isSpeaking +{ + return espeak_IsPlaying(); +} + +- (NSString *) voiceName:(unsigned int) index +{ + if (index >= espeak_voice_count) + return @"-"; + return [NSString stringWithCString: espeak_voices[index]->name]; +} + +- (unsigned int) voiceNumber:(NSString *) name +{ + if (name == nil) + return UINT_MAX; + + const char *const label = [name cString]; + if (!label) + return UINT_MAX; + + unsigned int index = -1; + while (espeak_voices[++index] && strcmp (espeak_voices[index]->name, label)) + /**/; + return (index < espeak_voice_count) ? index : UINT_MAX; +} + +- (unsigned int) nextVoice:(unsigned int) index +{ + if (++index >= espeak_voice_count) + index = 0; + return index; +} + +- (unsigned int) prevVoice:(unsigned int) index +{ + if (--index >= espeak_voice_count) + index = espeak_voice_count - 1; + return index; +} + +- (unsigned int) setVoice:(unsigned int) index withGenderM:(BOOL) isMale +{ + if (index == UINT_MAX) + index = [self voiceNumber:DESC(@"espeak-default-voice")]; + + if (index < espeak_voice_count) + { + espeak_VOICE voice = { espeak_voices[index]->name, NULL, NULL, isMale ? 1 : 2 }; + espeak_SetVoiceByProperties (&voice); + } + + return index; +} + +#else + +- (void) startSpeakingString:(NSString *) text {} + +- (void) stopSpeaking {} + +- (BOOL) isSpeaking +{ + return NO; +} +#endif + +@end + + +@implementation Universe (OOPrivate) + +- (BOOL)doRemoveEntity:(Entity *)entity +{ + // remove reference to entity in linked lists + if ([entity canCollide]) // filter only collidables disappearing + { + doLinkedListMaintenanceThisUpdate = YES; + } + + [entity removeFromLinkedLists]; + + // moved forward ^^ + // remove from the reference dictionary + int old_id = [entity universalID]; + entity_for_uid[old_id] = nil; + [entity setUniversalID:NO_TARGET]; + [entity wasRemovedFromUniverse]; + + // maintain sorted lists + int index = entity->zero_index; + + int n = 1; + if (index >= 0) + { + if (sortedEntities[index] != entity) + { + OOLog(kOOLogInconsistentState, @"DEBUG: Universe removeEntity:%@ ENTITY IS NOT IN THE RIGHT PLACE IN THE ZERO_DISTANCE SORTED LIST -- FIXING...", entity); + unsigned i; + index = -1; + for (i = 0; (i < n_entities)&&(index == -1); i++) + if (sortedEntities[i] == entity) + index = i; + if (index == -1) + OOLog(kOOLogInconsistentState, @"DEBUG: Universe removeEntity:%@ ENTITY IS NOT IN THE ZERO_DISTANCE SORTED LIST -- CONTINUING...", entity); + } + if (index != -1) + { + while ((unsigned)index < n_entities) + { + while (((unsigned)index + n < n_entities)&&(sortedEntities[index + n] == entity)) + n++; // ie there's a duplicate entry for this entity + sortedEntities[index] = sortedEntities[index + n]; // copy entity[index + n] -> entity[index] (preserves sort order) + if (sortedEntities[index]) + sortedEntities[index]->zero_index = index; // give it its correct position + index++; + } + if (n > 1) + OOLog(kOOLogInconsistentState, @"DEBUG: Universe removeEntity: REMOVED %d EXTRA COPIES OF %@ FROM THE ZERO_DISTANCE SORTED LIST", n - 1, entity); + while (n--) + { + n_entities--; + sortedEntities[n_entities] = nil; + } + } + entity->zero_index = -1; // it's GONE! + } + + // remove from the definitive list + if ([entities containsObject:entity]) + { + if (entity->isRing) breakPatternCounter--; + + if (entity->isShip) + { + int bid = firstBeacon; + ShipEntity* se = (ShipEntity*)entity; + if ([se isBeacon]) + { + if (bid == old_id) + firstBeacon = [se nextBeaconID]; + else + { + ShipEntity* beacon = [self entityForUniversalID:bid]; + while ((beacon != nil)&&([beacon nextBeaconID] != old_id)) + beacon = [self entityForUniversalID:[beacon nextBeaconID]]; + + [beacon setNextBeacon:[self entityForUniversalID:[se nextBeaconID]]]; + + while ([beacon nextBeaconID] != NO_TARGET) + beacon = [self entityForUniversalID:[beacon nextBeaconID]]; + lastBeacon = [beacon universalID]; + } + [se setBeaconCode:nil]; + } + } + + + if ([entity isWormhole]) + { + [activeWormholes removeObject:entity]; + } + else if ([entity isPlanet] && ![entity isSun]) + { + [allPlanets removeObject:entity]; + } + + [entities removeObject:entity]; + return YES; + } + + return NO; +} + + +- (void) preloadSounds +{ + NSEnumerator *soundEnum = nil; + NSEnumerator *arraySoundEnum = nil; + id object=nil; + NSString *soundName = nil; + + // Preload sounds to avoid loading stutter. + for (soundEnum = [customsounds objectEnumerator]; (object = [soundEnum nextObject]); ) + { + if([object isKindOfClass:[NSString class]]) + { + soundName=object; + if (![soundName hasPrefix:@"["] && ![soundName hasSuffix:@"]"]) + { + [ResourceManager ooSoundNamed:soundName inFolder:@"Sounds"]; + } + } + else if([object isKindOfClass:[NSArray class]] && [object count] > 0) + { + for (arraySoundEnum = [object objectEnumerator]; (soundName = [arraySoundEnum nextObject]); ) + if (![soundName hasPrefix:@"["] && ![soundName hasSuffix:@"]"]) + { + [ResourceManager ooSoundNamed:soundName inFolder:@"Sounds"]; + } + } + } +} + + +#if SUPPORT_GRAPHVIZ_OUT +- (void) dumpDebugGraphViz +{ + if ([[NSUserDefaults standardUserDefaults] boolForKey:@"universe-dump-debug-graphviz"]) + { + [self dumpSystemDescriptionGraphViz]; + } +} + + +- (void) addNumericRefsInString:(NSString *)string toGraphViz:(NSMutableString *)graphViz fromNode:(NSString *)fromNode nodeCount:(unsigned)nodeCount +{ + NSString *index = nil; + int start, end; + NSRange remaining, subRange; + unsigned i; + + remaining = NSMakeRange(0, [string length]); + + for (;;) + { + subRange = [string rangeOfString:@"[" options:NSLiteralSearch range:remaining]; + if (subRange.location == NSNotFound) break; + start = subRange.location + subRange.length; + remaining.length -= start - remaining.location; + remaining.location = start; + + subRange = [string rangeOfString:@"]" options:NSLiteralSearch range:remaining]; + if (subRange.location == NSNotFound) break; + end = subRange.location; + remaining.length -= end - remaining.location; + remaining.location = end; + + index = [string substringWithRange:NSMakeRange(start, end - start)]; + i = [index intValue]; + + // Each node gets a colour for its incoming edges. The multiplication and mod shuffle them to avoid adjacent nodes having similar colours. + [graphViz appendFormat:@"\t%@ -> n%u_0 [color=\"%f,0.75,0.8\" lhead=cluster_%u]\n", fromNode, i, ((float)(i * 511 % nodeCount)) / ((float)nodeCount), i]; + } + + if ([string rangeOfString:@"%I"].location != NSNotFound) + { + [graphViz appendFormat:@"\t%@ -> percent_I [color=\"0,0,0.25\"]\n", fromNode]; + } + if ([string rangeOfString:@"%H"].location != NSNotFound) + { + [graphViz appendFormat:@"\t%@ -> percent_H [color=\"0,0,0.45\"]\n", fromNode]; + } + if ([string rangeOfString:@"%R"].location != NSNotFound || [string rangeOfString:@"%N"].location != NSNotFound) + { + [graphViz appendFormat:@"\t%@ -> percent_RN [color=\"0,0,0.65\"]\n", fromNode]; + } + + // TODO: test graphViz output for @"%Jxxx" + if ([string rangeOfString:@"%J"].location != NSNotFound) + { + [graphViz appendFormat:@"\t%@ -> percent_J [color=\"0,0,0.75\"]\n", fromNode]; + } +} + + +- (void) dumpSystemDescriptionGraphViz +{ + NSMutableString *graphViz = nil; + NSArray *systemDescriptions = nil; + NSArray *thisDesc = nil; + unsigned i, count, j, subCount; + NSString *descLine = nil; + NSArray *curses = nil; + NSString *label = nil; + NSDictionary *keyMap = nil; + + keyMap = [ResourceManager dictionaryFromFilesNamed:@"sysdesc_key_table.plist" + inFolder:@"Config" + andMerge:NO]; + + graphViz = [NSMutableString stringWithString: + @"// System description grammar:\n\n" + "digraph system_descriptions\n" + "{\n" + "\tgraph [charset=\"UTF-8\", label=\"System description grammar\", labelloc=t, labeljust=l rankdir=LR compound=true nodesep=0.02 ranksep=1.5 concentrate=true fontname=Helvetica]\n" + "\tedge [arrowhead=dot]\n" + "\tnode [shape=none height=0.2 width=3 fontname=Helvetica]\n\t\n"]; + + systemDescriptions = [[self descriptions] arrayForKey:@"system_description"]; + count = [systemDescriptions count]; + + // Add system-description-string as special node (it's the one thing that ties [14] to everything else). + descLine = DESC(@"system-description-string"); + label = OOStringifySystemDescriptionLine(descLine, keyMap, NO); + [graphViz appendFormat:@"\tsystem_description_string [label=\"%@\" shape=ellipse]\n", EscapedGraphVizString(label)]; + [self addNumericRefsInString:descLine + toGraphViz:graphViz + fromNode:@"system_description_string" + nodeCount:count]; + [graphViz appendString:@"\t\n"]; + + // Add special nodes for formatting codes + [graphViz appendString: + @"\tpercent_I [label=\"%I\\nInhabitants\" shape=diamond]\n" + "\tpercent_H [label=\"%H\\nSystem name\" shape=diamond]\n" + "\tpercent_RN [label=\"%R/%N\\nRandom name\" shape=diamond]\n" + "\tpercent_J [label=\"%J\\nNumbered system name\" shape=diamond]\n\t\n"]; + + // Toss in the Thargoid curses, too + [graphViz appendString:@"\tsubgraph cluster_thargoid_curses\n\t{\n\t\tlabel = \"Thargoid curses\"\n"]; + curses = [[self descriptions] arrayForKey:@"thargoid_curses"]; + subCount = [curses count]; + for (j = 0; j < subCount; ++j) + { + label = OOStringifySystemDescriptionLine([curses stringAtIndex:j], keyMap, NO); + [graphViz appendFormat:@"\t\tthargoid_curse_%u [label=\"%@\"]\n", j, EscapedGraphVizString(label)]; + } + [graphViz appendString:@"\t}\n"]; + for (j = 0; j < subCount; ++j) + { + [self addNumericRefsInString:[curses stringAtIndex:j] + toGraphViz:graphViz + fromNode:[NSString stringWithFormat:@"thargoid_curse_%u", j] + nodeCount:count]; + } + [graphViz appendString:@"\t\n"]; + + // The main show: the bits of systemDescriptions itself. + // Define the nodes + for (i = 0; i < count; ++i) + { + // Build label, using sysdesc_key_table.plist if available + label = [keyMap objectForKey:[NSString stringWithFormat:@"%u", i]]; + if (label == nil) label = [NSString stringWithFormat:@"[%u]", i]; + else label = [NSString stringWithFormat:@"[%u] (%@)", i, label]; + + [graphViz appendFormat:@"\tsubgraph cluster_%u\n\t{\n\t\tlabel=\"%@\"\n", i, EscapedGraphVizString(label)]; + + thisDesc = [systemDescriptions arrayAtIndex:i]; + subCount = [thisDesc count]; + for (j = 0; j < subCount; ++j) + { + label = OOStringifySystemDescriptionLine([thisDesc stringAtIndex:j], keyMap, NO); + [graphViz appendFormat:@"\t\tn%u_%u [label=\"\\\"%@\\\"\"]\n", i, j, EscapedGraphVizString(label)]; + } + + [graphViz appendString:@"\t}\n"]; + } + [graphViz appendString:@"\t\n"]; + + // Define the edges + for (i = 0; i != count; ++i) + { + thisDesc = [systemDescriptions arrayAtIndex:i]; + subCount = [thisDesc count]; + for (j = 0; j != subCount; ++j) + { + descLine = [thisDesc stringAtIndex:j]; + [self addNumericRefsInString:descLine + toGraphViz:graphViz + fromNode:[NSString stringWithFormat:@"n%u_%u", i, j] + nodeCount:count]; + } + } + + // Write file + [graphViz appendString:@"\t}\n"]; + [ResourceManager writeDiagnosticData:[graphViz dataUsingEncoding:NSUTF8StringEncoding] toFileNamed:@"SystemDescription.dot"]; +} +#endif + + +#ifndef NDEBUG +- (void) runLocalizationTools +{ + // Handle command line options to transform system_description array for easier localization + + NSArray *arguments = nil; + NSEnumerator *argEnum = nil; + NSString *arg = nil; + BOOL compileSysDesc = NO, exportSysDesc = NO, xml = NO; + + arguments = [[NSProcessInfo processInfo] arguments]; + + for (argEnum = [arguments objectEnumerator]; (arg = [argEnum nextObject]); ) + { + if ([arg isEqual:@"--compile-sysdesc"]) compileSysDesc = YES; + else if ([arg isEqual:@"--export-sysdesc"]) exportSysDesc = YES; + else if ([arg isEqual:@"--xml"]) xml = YES; + else if ([arg isEqual:@"--openstep"]) xml = NO; + } + + if (compileSysDesc) CompileSystemDescriptions(xml); + if (exportSysDesc) ExportSystemDescriptions(xml); +} +#endif + +@end + + +@implementation OOSound (OOCustomSounds) + ++ (id) soundWithCustomSoundKey:(NSString *)key +{ + NSString *fileName = [UNIVERSE soundNameForCustomSoundKey:key]; + if (fileName == nil) return nil; + return [ResourceManager ooSoundNamed:fileName inFolder:@"Sounds"]; +} + + +- (id) initWithCustomSoundKey:(NSString *)key +{ + [self release]; + return [[OOSound soundWithCustomSoundKey:key] retain]; +} + +@end + + +@implementation OOSoundSource (OOCustomSounds) + ++ (id) sourceWithCustomSoundKey:(NSString *)key +{ + return [[[self alloc] initWithCustomSoundKey:key] autorelease]; +} + + +- (id) initWithCustomSoundKey:(NSString *)key +{ + OOSound *theSound = [OOSound soundWithCustomSoundKey:key]; + if (theSound != nil) + { + self = [self initWithSound:theSound]; + } + else + { + [self release]; + self = nil; + } + return self; +} + + +- (void) playCustomSoundWithKey:(NSString *)key +{ + OOSound *theSound = [OOSound soundWithCustomSoundKey:key]; + if (theSound != nil) [self playSound:theSound]; +} + +@end + + +NSString *DESC_(NSString *key) +{ + NSString *result = [UNIVERSE descriptionForKey:key]; + if (result == nil) result = key; + return result; +} + + +// There's a hint of gettext about this... +NSString *DESC_PLURAL_(NSString *key, int count) +{ + NSArray *conditions = [[UNIVERSE descriptions] arrayForKey:@"plural-rules"]; + + // are we using an older descriptions.plist (1.72.x) ? + NSString *tmp = [UNIVERSE descriptionForKey:key]; + if (tmp != nil) + { + static NSMutableSet *warned = nil; + + if (![warned containsObject:tmp]) + { + OOLogWARN(@"localization.plurals", @"'%@' found in descriptions.plist, should be '%@%%0'. Localization data needs updating.",key,key); + if (warned == nil) warned = [[NSMutableSet alloc] init]; + [warned addObject:tmp]; + } + } + + if (conditions == nil) + { + if (tmp == nil) // this should mean that descriptions.plist is from 1.73 or above. + return DESC_([NSString stringWithFormat:@"%@%%%d", key, count != 1]); + // still using an older descriptions.plist + return tmp; + } + int unsigned i; + long int index; + + for (index = i = 0; i < [conditions count]; ++index, ++i) + { + const char *cond = [[conditions stringAtIndex:i] cString]; + if (!cond) + break; + + long int input = count; + BOOL flag = NO; // we XOR test results with this + + while (isspace (*cond)) + ++cond; + + for (;;) + { + while (isspace (*cond)) + ++cond; + + char command = *cond++; + + switch (command) + { + case 0: + goto passed; // end of string + + case '~': + flag = !flag; + continue; + } + + long int param = strtol (cond, (char **)&cond, 10); + + switch (command) + { + case '#': + index = param; + continue; + + case '%': + if (param < 2) + break; // ouch - fail this! + input %= param; + continue; + + case '=': + if (flag ^ (input == param)) + continue; + break; + case '!': + if (flag ^ (input != param)) + continue; + break; + + case '<': + if (flag ^ (input < param)) + continue; + break; + case '>': + if (flag ^ (input > param)) + continue; + break; + } + // if we arrive here, we have an unknown test or a test has failed + break; + } + } + +passed: + return DESC_([NSString stringWithFormat:@"%@%%%d", key, index]); +} diff --git a/src/Core/legacy_random.c b/src/Core/legacy_random.c new file mode 100644 index 00000000..79d5bd27 --- /dev/null +++ b/src/Core/legacy_random.c @@ -0,0 +1,236 @@ +/* + +legacy_random.c + +Class handling interface elements, primarily text, that are not part of the 3D +game world, together with GuiDisplayGen. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#include +#include +#include +#include "legacy_random.h" + + +const Random_Seed kNilRandomSeed = {0}; + + +static RNG_Seed rnd_seed; + + +// TODO: Why is this based on a static? Should change to MungeCheckSum(&checkSum, value); +static int checksum; +void clear_checksum() +{ + checksum = 0; +} + + +int munge_checksum(int value) +{ + int mult1 = (value & 15) + 8; + checksum += value; + checksum *= mult1; + checksum += mult1; + checksum &= 0xffff; + return checksum; +} + + +// cunning price rounding routine: +// +double cunningFee(double value) +{ + double fee = value; + double superfee = 100000.0; + unsigned long long rounded_fee = superfee * floor(0.5 + fee / superfee); + if (rounded_fee == 0) rounded_fee = 1; + double ratio = fee / (double)rounded_fee; + + while ((ratio < 0.95 || ratio > 1.05) && superfee > 1) + { + rounded_fee = superfee * floor(0.5 + fee / superfee); + if (rounded_fee == 0) rounded_fee = 1; + ratio = fee / (double)rounded_fee; + superfee /= 10.0; + } + + if (ratio > 0.95 && ratio < 1.05) + fee = rounded_fee; + + return fee; +} + + +// an implementation of RANROT +// pseudo random number generator +// +static RANROTSeed sRANROT; + + +void ranrot_srand(unsigned int seed) +{ + sRANROT.low = seed; + sRANROT.high = ~seed; + Ranrot(); Ranrot(); Ranrot(); // mix it up a bit +} + + +unsigned Ranrot(void) +{ + sRANROT.high = (sRANROT.high << 16) + (sRANROT.high >> 16); + sRANROT.high += sRANROT.low; + sRANROT.low += sRANROT.high; + return sRANROT.high & 0x7FFFFFFF; +} + + +float randf (void) +{ + return (Ranrot() & 0xffff) * (1.0f / 65536.0f); +} + + +float bellf (int n) +{ + int i = n; + float total = 0; + + if (EXPECT_NOT(i <= 0)) + { + printf("***** ERROR - attempt to generate bellf(%d)\n", n); + return 0.0f; // catch possible div-by-zero problem + } + + while (i-- > 0) + total += (Ranrot() & 1023); + return total / (1024.0f * n); +} + + +RANROTSeed RANROTGetFullSeed(void) +{ + return sRANROT; +} + + +void RANROTSetFullSeed(RANROTSeed seed) +{ + sRANROT = seed; +} + + +void seed_for_planet_description (Random_Seed s_seed) +{ + rnd_seed.a = s_seed.c; + rnd_seed.b = s_seed.d; + rnd_seed.c = s_seed.e; + rnd_seed.d = s_seed.f; + + ranrot_srand(rnd_seed.a * 0x1000000 + rnd_seed.b * 0x10000 + rnd_seed.c * 0x100 + rnd_seed.d); +} + + +void seed_RNG_only_for_planet_description (Random_Seed s_seed) +{ + rnd_seed.a = s_seed.c; + rnd_seed.b = s_seed.d; + rnd_seed.c = s_seed.e; + rnd_seed.d = s_seed.f; +} + + +RNG_Seed currentRandomSeed (void) +{ + return rnd_seed; +} + + +void setRandomSeed (RNG_Seed a_seed) +{ + rnd_seed = a_seed; +} + + +int gen_rnd_number (void) +{ + int a,x; + + x = (rnd_seed.a * 2) & 0xFF; + a = x + rnd_seed.c; + if (rnd_seed.a > 127) + a++; + rnd_seed.a = a & 0xFF; + rnd_seed.c = x; + + a = a / 256; /* a = any carry left from above */ + x = rnd_seed.b; + a = (a + x + rnd_seed.d) & 0xFF; + rnd_seed.b = a; + rnd_seed.d = x; + return a; +} + + +void make_pseudo_random_seed (Random_Seed *seed_ptr) +{ + seed_ptr->a = gen_rnd_number(); + seed_ptr->b = gen_rnd_number(); + seed_ptr->c = gen_rnd_number(); + seed_ptr->d = gen_rnd_number(); + seed_ptr->e = gen_rnd_number(); + seed_ptr->f = gen_rnd_number(); +} + + +void rotate_seed (Random_Seed *seed_ptr) +{ + unsigned int x; + unsigned int y; + + x = seed_ptr->a + seed_ptr->c; + y = seed_ptr->b + seed_ptr->d; + + + if (x > 0xFF) y++; + + x &= 0xFF; + y &= 0xFF; + + seed_ptr->a = seed_ptr->c; + seed_ptr->b = seed_ptr->d; + seed_ptr->c = seed_ptr->e; + seed_ptr->d = seed_ptr->f; + + x += seed_ptr->c; + y += seed_ptr->d; + + + if (x > 0xFF) + y++; + + x &= 0xFF; + y &= 0xFF; + + seed_ptr->e = x; + seed_ptr->f = y; +} diff --git a/src/Core/legacy_random.h b/src/Core/legacy_random.h new file mode 100644 index 00000000..400fc74e --- /dev/null +++ b/src/Core/legacy_random.h @@ -0,0 +1,147 @@ +/* + +legacy_random.h + +Pseudo-random number generator designed to produce identical results to that +used in BBC Elite (for dynamic world generation), and related functions. + +Oolite +Copyright (C) 2004-2008 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#ifndef LEGACY_RANDOM_H +#define LEGACY_RANDOM_H + +#import "OOFunctionAttributes.h" +#import + + +typedef struct Random_Seed +{ + uint8_t a, /* 6c */ + b, /* 6d */ + c, /* 6e */ + d, /* 6f */ + e, /* 70 */ + f; /* 71 */ +} Random_Seed; + + +typedef struct RNG_Seed +{ + int32_t a, + b, + c, + d; +} RNG_Seed; + + +typedef struct RANROTSeed +{ + uint32_t high, + low; +} RANROTSeed; + + +extern const Random_Seed kNilRandomSeed; + + +// checksum stuff +void clear_checksum(); +int munge_checksum(int value); + +// cunning price rounding routine: +double cunningFee(double value); + +// an implementation of RANROT +// pseudo random number generator +// +void ranrot_srand(unsigned int seed); +unsigned Ranrot(void); +#define ranrot_rand() ((int)Ranrot()) // Some uses perform arithmetic that does weird things if result is unsigned -- DustEntity.m, for instance. +float randf(void); +float bellf(int n); + +RANROTSeed RANROTGetFullSeed(void); +void RANROTSetFullSeed(RANROTSeed seed); + + +OOINLINE double distanceBetweenPlanetPositions(int x1, int y1, int x2, int y2) INLINE_CONST_FUNC; +OOINLINE double accurateDistanceBetweenPlanetPositions(int x1, int y1, int x2, int y2) INLINE_CONST_FUNC; + +void seed_for_planet_description(Random_Seed s_seed); +void seed_RNG_only_for_planet_description(Random_Seed s_seed); +RNG_Seed currentRandomSeed(void); +void setRandomSeed(RNG_Seed a_seed); + +// Range: 0..255 +int gen_rnd_number (void); + +void make_pseudo_random_seed (Random_Seed *seed_ptr); + +OOINLINE int is_nil_seed(Random_Seed a_seed) INLINE_CONST_FUNC; + +void rotate_seed (Random_Seed *seed_ptr); +OOINLINE int rotate_byte_left (int x) INLINE_CONST_FUNC; + +OOINLINE int equal_seeds(Random_Seed seed1, Random_Seed seed2) INLINE_CONST_FUNC; + + + +/*** Only inline definitions beyond this point ***/ + +OOINLINE int equal_seeds(Random_Seed seed1, Random_Seed seed2) +{ + return ((seed1.a == seed2.a)&&(seed1.b == seed2.b)&&(seed1.c == seed2.c)&&(seed1.d == seed2.d)&&(seed1.e == seed2.e)&&(seed1.f == seed2.f)); +} + + +OOINLINE int is_nil_seed(Random_Seed a_seed) +{ + return equal_seeds(a_seed, kNilRandomSeed); +} + + +OOINLINE int rotate_byte_left(int x) +{ + return ((x << 1) | (x >> 7)) & 255; +} + + +// a method used to determine interplanetary distances, +// if accurate, it has to scale distance down by a factor of 7.15:7.0 +// to allow routes navigable in the original! +OOINLINE double distanceBetweenPlanetPositions ( int x1, int y1, int x2, int y2) +{ + int dx = x1 - x2; + int dy = (y1 - y2)/2; + int dist = sqrtf(dx*dx + dy*dy); // here's where the rounding errors come in! + return 0.4 * dist; +} + + +OOINLINE double accurateDistanceBetweenPlanetPositions ( int x1, int y1, int x2, int y2) +{ + double dx = x1 - x2; + double dy = (y1 - y2) / 2.0; + double dist = sqrt(dx*dx + dy*dy); // here's where the rounding errors come in! + return 0.4 * dist; +} + +#endif diff --git a/src/SDL/Comparison.h b/src/SDL/Comparison.h new file mode 100644 index 00000000..55e55aac --- /dev/null +++ b/src/SDL/Comparison.h @@ -0,0 +1,121 @@ +// +// $Id: Comparison.h,v 1.3 2004/12/12 20:17:24 will_mason Exp $ +// +// vi: set ft=objc: + +/* + * ObjectiveLib - a library of containers and algorithms for Objective-C + * + * Copyright (c) 2004 + * Will Mason + * + * Portions: + * + * Copyright (c) 1994 + * Hewlett-Packard Company + * + * Copyright (c) 1996,1997 + * Silicon Graphics Computer Systems, Inc. + * + * Copyright (c) 1997 + * Moscow Center for SPARC Technology + * + * Copyright (c) 1999 + * Boris Fomitchev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * You may contact the author at will_mason@users.sourceforge.net. + */ + +#if defined(GNUSTEP) + +#if !defined(__COMPARISON_OL_GUARD) +#define __COMPARISON_OL_GUARD + +#include + +/** + * @category NSObject(OLComparisonMethods) Comparison.h Objectivelib/Comparison.h + * + * Comparison methods used in @ref Functors "function objects". These comparison + * methods are only required to be included when GNUstep is the platform, as + * Cocoa already defines them. Under Cocoa they are declared in the + * intuitively-named file @c NSScriptWhoseTests.h. All of these methods send + * the message @c compare: to the receiving object. + * + * @pre The receiving object must implement the method @c compare:. + */ +@interface NSObject (OLComparisonMethods) + +/** + * Return whether another object is equal to this one. This message returns YES if + * and only if the message @c compare: returns @c NSOrderedSame. + * + * @param object the object to which to compare this one + * @return YES if @a object is equal to this one, NO otherwise + */ +- (BOOL) isEqualTo: (id)object; + +/** + * Return whether this object is greater than another one. This message returns + * YES if and only if @c compare: returns @c NSOrderedDescending. + * + * @param object the object to which to compare this one + * @return YES if this object is greater than @a object, NO otherwise + */ +- (BOOL) isGreaterThan: (id)object; + +/** + * Return whether this object is greater than or equal to another one. This message returns + * YES if and only if @c compare: does not return @c NSOrderedAscending. + * + * @param object the object to which to compare this one + * @return YES if this object is greater than or equal to @a object, NO otherwise + */ +- (BOOL) isGreaterThanOrEqualTo: (id)object; + +/** + * Return whether this object is less than another one. This message returns + * YES if and only if @c compare: returns @c NSOrderedAscending. + * + * @param object the object to which to compare this one + * @return YES if this object is less than @a object, NO otherwise + */ +- (BOOL) isLessThan: (id)object; + +/** + * Return whether this object is less than or equal to another one. This message returns + * YES if and only if @c compare: does not return @c NSOrderedDescending. + * + * @param object the object to which to compare this one + * @return YES if this object is less than or equal to @a object, NO otherwise + */ +- (BOOL) isLessThanOrEqualTo: (id)object; + +/** + * Return whether another object is not equal to this one. This message returns YES if + * and only if the message @c compare: does not return @c NSOrderedSame. + * + * @param object the object to which to compare this one + * @return YES if @a object is not equal to this one, NO otherwise + */ +- (BOOL) isNotEqualTo: (id)object; + +@end + +#endif + +#endif diff --git a/src/SDL/Comparison.m b/src/SDL/Comparison.m new file mode 100644 index 00000000..8bcca0c8 --- /dev/null +++ b/src/SDL/Comparison.m @@ -0,0 +1,88 @@ +// +// $Id: Comparison.m,v 1.3 2004/11/29 23:35:50 will_mason Exp $ +// +// vi: set ft=objc: + +/* + * ObjectiveLib - a library of containers and algorithms for Objective-C + * + * Copyright (c) 2004 + * Will Mason + * + * Portions: + * + * Copyright (c) 1994 + * Hewlett-Packard Company + * + * Copyright (c) 1996,1997 + * Silicon Graphics Computer Systems, Inc. + * + * Copyright (c) 1997 + * Moscow Center for SPARC Technology + * + * Copyright (c) 1999 + * Boris Fomitchev + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * You may contact the author at will_mason@users.sourceforge.net. + */ + +#if defined(GNUSTEP) + +#include "Comparison.h" +#include + +@implementation NSObject (OLComparison) + +- (BOOL) isEqualTo: (id)object +{ + return (object != nil && [self compare: object] == NSOrderedSame) ? + YES : NO; +} + +- (BOOL) isGreaterThan: (id)object +{ + return (object != nil && [self compare: object] == NSOrderedDescending) ? + YES : NO; +} + +- (BOOL) isGreaterThanOrEqualTo: (id)object +{ + return (object != nil && [self compare: object] != NSOrderedAscending) ? + YES : NO; +} + +- (BOOL) isLessThan: (id)object +{ + return (object != nil && [self compare: object] == NSOrderedAscending) ? + YES : NO; +} + +- (BOOL) isLessThanOrEqualTo: (id)object +{ + return (object != nil && [self compare: object] != NSOrderedDescending) ? + YES : NO; +} + +- (BOOL) isNotEqualTo: (id)object +{ + return (object != nil && [self compare: object] != NSOrderedSame) ? + YES : NO; +} + +@end + +#endif diff --git a/src/SDL/JoystickHandler.h b/src/SDL/JoystickHandler.h new file mode 100644 index 00000000..8e751414 --- /dev/null +++ b/src/SDL/JoystickHandler.h @@ -0,0 +1,208 @@ +/* + +JoystickHandler.h +By Dylan Smith + +JoystickHandler handles joystick events from SDL, and translates them +into the appropriate action via a lookup table. The lookup table is +stored as a simple array rather than an ObjC dictionary since this +will be examined fairly often (once per frame during gameplay). + +Conversion methods are provided to convert between the internal +representation and an NSDictionary (for loading/saving user defaults +and for use in areas where portability/ease of coding are more important +than performance such as the GUI) + +Oolite +Copyright (C) 2004-2007 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +// Enums are used here rather than a more complex ObjC object because +// these are required very frequently (once per frame) so must be light +// on CPU cycles (try and avoid too many objc sendmsgs). +// Controls that can be an axis +enum { + AXIS_ROLL, + AXIS_PITCH, + AXIS_YAW, + AXIS_PRECISION, + AXIS_THRUST, + AXIS_VIEWX, + AXIS_VIEWY, + AXIS_end +} axfn; + +// Controls that can be a button +enum { + BUTTON_INCTHRUST, + BUTTON_DECTHRUST, + BUTTON_SCANNERZOOM, + BUTTON_JETTISON, + BUTTON_COMPASSMODE, + BUTTON_COMMSLOG, + BUTTON_DOCKCPU, + BUTTON_DOCKCPUFAST, + BUTTON_DOCKCPUTARGET, + BUTTON_FUELINJECT, + BUTTON_HYPERSPEED, + BUTTON_HYPERDRIVE, + BUTTON_GALACTICDRIVE, + BUTTON_FIRE, + BUTTON_ARMMISSILE, + BUTTON_LAUNCHMISSILE, +#ifdef TARGET_INCOMING_MISSILES + BUTTON_TARGETINCOMINGMISSILE, +#endif + BUTTON_UNARM, + BUTTON_CYCLEMISSILE, + BUTTON_ENERGYBOMB, + BUTTON_ID, + BUTTON_ECM, + BUTTON_ESCAPE, + BUTTON_CLOAK, + BUTTON_PRECISION, + BUTTON_VIEWFORWARD, + BUTTON_VIEWAFT, + BUTTON_VIEWPORT, + BUTTON_VIEWSTARBOARD, + BUTTON_end +} butfn; + +// Stick constants +#define MAX_STICKS 2 +#define MAX_AXES 16 +#define MAX_REAL_BUTTONS 64 +#define MAX_HATS 4 +#define MAX_BUTTONS (MAX_REAL_BUTTONS + 4 * MAX_HATS) +#define STICK_NOFUNCTION -1 +#define STICK_AXISUNASSIGNED -10.0 +#define STICK_PRECISIONDIV 98304 // 3 times more precise +#define STICK_NORMALDIV 32768 +#define STICK_PRECISIONFAC (STICK_PRECISIONDIV/STICK_NORMALDIV) +#define STICK_DEADZONE 0.05 + +// Kind of stick device (these are bits - if any more are added, +// the next one is 4 and so on). +#define HW_AXIS 1 +#define HW_BUTTON 2 + +// The threshold at which an axis can trigger a call back. +// The max of abs(axis) is 32767. +#define AXCBTHRESH 20000 + +// Dictionary keys - used in the defaults file +#define AXIS_SETTINGS @"JoystickAxes" // NSUserDefaults +#define BUTTON_SETTINGS @"JoystickButs" // NSUserDefaults +#define STICK_ISAXIS @"isAxis" // YES=axis NO=button +#define STICK_NUMBER @"stickNum" // Stick number 0 to 4 +#define STICK_AXBUT @"stickAxBt" // Axis or button number +#define STICK_FUNCTION @"stickFunc" // Function of axis/button +// shortcut to make code more readable when using enum as key for +// an NSDictionary +#define ENUMKEY(x) [NSString stringWithFormat: @"%d", x] + +#import +#import + +@interface JoystickHandler : NSObject +{ + @protected + + // Axis/button mapping arrays + int8_t axismap[MAX_STICKS][MAX_AXES]; + int8_t buttonmap[MAX_STICKS][MAX_BUTTONS]; + double axstate[AXIS_end]; + BOOL butstate[BUTTON_end]; + Uint8 hatstate[MAX_STICKS][MAX_HATS]; + SDL_Joystick *stick[MAX_STICKS]; + BOOL precisionMode; + int numSticks; + + // Handle callbacks - the object, selector to call + // the desired function, and the hardware (axis or button etc.) + id cbObject; + SEL cbSelector; + int cbFunc; + char cbHardware; +} + +// General. +// Note: handleSDLEvent returns a BOOL (YES we handled it or NO we +// didn't) so in the future when more handler classes are written, +// the GameView event loop can just go through an NSArray of handlers +// until it finds a handler that handles the event. +- (id) init; +- (BOOL) handleSDLEvent: (SDL_Event *)evt; + +// Roll/pitch axis +- (NSPoint) getRollPitchAxis; + +// View axis +- (NSPoint) getViewAxis; + +// Setting button and axis functions +- (void) setFunctionForAxis: (int)axis + function: (int)function + stick: (int)stickNum; +- (void) setFunctionForButton: (int)button + function: (int)function + stick: (int)stickNum; +// convert a dictionary into the internal function map +- (void) setFunction: (int)function withDict: (NSDictionary *)stickFn; +- (void) unsetAxisFunction: (int)function; +- (void) unsetButtonFunction: (int)function; + +// Accessors and discovery about the hardware. +// These work directly on the internal lookup table so to be fast +// since they are likely to be called by the game loop. +- (int) getNumSticks; +- (BOOL) getButtonState: (int)function; +- (double) getAxisState: (int)function; +- (double) getSensitivity; + +// This one just returns a pointer to the entire state array to +// allow for multiple lookups with only one objc_sendMsg +- (const BOOL *) getAllButtonStates; + +// Hardware introspection. +- (NSArray *)listSticks; + +// These use NSDictionary/NSArray since they are used outside the game +// loop and are needed for loading/saving defaults. +- (NSDictionary *)getAxisFunctions; +- (NSDictionary *)getButtonFunctions; + +// Set a callback for the next moved axis/pressed button. hwflags +// is in the form HW_AXIS | HW_BUTTON (or just one of). +- (void)setCallback: (SEL)selector + object: (id)obj + hardware: (char)hwflags; +- (void)clearCallback; + +// Methods generally only used by this class. +- (void) setDefaultMapping; +- (void) clearMappings; +- (void) clearStickStates; +- (void) decodeAxisEvent: (SDL_JoyAxisEvent *)evt; +- (void) decodeButtonEvent: (SDL_JoyButtonEvent *)evt; +- (void) decodeHatEvent: (SDL_JoyHatEvent *)evt; +- (void) saveStickSettings; +- (void) loadStickSettings; + +@end diff --git a/src/SDL/JoystickHandler.m b/src/SDL/JoystickHandler.m new file mode 100644 index 00000000..85af4292 --- /dev/null +++ b/src/SDL/JoystickHandler.m @@ -0,0 +1,581 @@ +/* + +JoystickHandler.m +By Dylan Smith + +Oolite +Copyright (C) 2004-2007 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "JoystickHandler.h" +#import "OOLogging.h" + +#define kOOLogUnconvertedNSLog @"unclassified.JoystickHandler" + + +@implementation JoystickHandler + +- (id) init +{ + int i; + + // Find and open the sticks. + numSticks=SDL_NumJoysticks(); + OOLog(@"joystickHandler.init", @"Number of joysticks detected: %d", numSticks); + if(numSticks) + { + for(i = 0; i < numSticks; i++) + { + // it's doubtful MAX_STICKS will ever get exceeded, but + // we need to be defensive. + if(i > MAX_STICKS) + break; + + stick[i]=SDL_JoystickOpen(i); + if(!stick[i]) + { + NSLog(@"Failed to open joystick #%d", i); + } + } + SDL_JoystickEventState(SDL_ENABLE); + } + + // set initial values for stick buttons/axes (NO for buttons, + // STICK_AXISUNASSIGNED for axes). Caution: calling this again + // after axes have been assigned will set all the axes to + // STICK_AXISUNASSIGNED so if there is a need to do something + // like this, then do it some other way, or change this method + // so it doesn't do that. + [self clearStickStates]; + + // Make some sensible mappings. This also ensures unassigned + // axes and buttons are set to unassigned (STICK_NOFUNCTION). + [self loadStickSettings]; + + precisionMode=NO; + return self; +} + + +- (BOOL) handleSDLEvent: (SDL_Event *)evt +{ + BOOL rc=NO; + switch(evt->type) + { + case SDL_JOYAXISMOTION: + [self decodeAxisEvent: (SDL_JoyAxisEvent *)evt]; + rc=YES; + break; + case SDL_JOYBUTTONDOWN: + case SDL_JOYBUTTONUP: + [self decodeButtonEvent: (SDL_JoyButtonEvent *)evt]; + rc=YES; + break; + case SDL_JOYHATMOTION: + [self decodeHatEvent: (SDL_JoyHatEvent *)evt]; + rc=YES; + break; + default: + NSLog(@"JoystickHandler was sent an event it doesn't know"); + } + return rc; +} + + +- (NSPoint) getRollPitchAxis +{ + return NSMakePoint(axstate[AXIS_ROLL], axstate[AXIS_PITCH]); +} + + +- (NSPoint) getViewAxis +{ + return NSMakePoint(axstate[AXIS_VIEWX], axstate[AXIS_VIEWY]); +} + + +- (BOOL) getButtonState: (int)function +{ + return butstate[function]; +} + + +- (const BOOL *)getAllButtonStates +{ + return butstate; +} + + +- (double) getAxisState: (int)function +{ + return axstate[function]; +} + + +- (double) getSensitivity +{ + return precisionMode ? STICK_PRECISIONFAC : 1.0; +} + +- (NSArray *)listSticks +{ + int i; + NSMutableArray *stickList=[NSMutableArray array]; + for(i=0; i < numSticks; i++) + { + [stickList addObject: [NSString stringWithFormat: @"%s", SDL_JoystickName(i)]]; + } + return stickList; +} + + +- (NSDictionary *)getAxisFunctions +{ + int i,j; + NSMutableDictionary *fnList=[NSMutableDictionary dictionary]; + + // Add axes + for(i=0; i < MAX_AXES; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + if(axismap[j][i] >= 0) + { + NSDictionary *fnDict=[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool: YES], STICK_ISAXIS, + [NSNumber numberWithInt: j], STICK_NUMBER, + [NSNumber numberWithInt: i], STICK_AXBUT, + nil]; + [fnList setValue: fnDict + forKey: ENUMKEY(axismap[j][i])]; + } + } + } + return fnList; +} + + +- (NSDictionary *)getButtonFunctions +{ + int i, j; + NSMutableDictionary *fnList=[NSMutableDictionary dictionary]; + + // Add buttons + for(i=0; i < MAX_BUTTONS; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + if(buttonmap[j][i] >= 0) + { + NSDictionary *fnDict=[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool: NO], STICK_ISAXIS, + [NSNumber numberWithInt: j], STICK_NUMBER, + [NSNumber numberWithInt: i], STICK_AXBUT, + nil]; + [fnList setValue: fnDict + forKey: ENUMKEY(buttonmap[j][i])]; + } + } + } + return fnList; +} + + +- (void) setFunction: (int)function withDict: (NSDictionary *)stickFn +{ + BOOL isAxis=[(NSNumber *)[stickFn objectForKey: STICK_ISAXIS] boolValue]; + int stickNum=[(NSNumber *)[stickFn objectForKey: STICK_NUMBER] intValue]; + int stickAxBt=[(NSNumber *)[stickFn objectForKey: STICK_AXBUT] intValue]; + + if(isAxis) + { + [self setFunctionForAxis: stickAxBt + function: function + stick: stickNum]; + } + else + { + [self setFunctionForButton: stickAxBt + function: function + stick: stickNum]; + } +} + + +- (void) setFunctionForAxis: (int)axis + function: (int)function + stick: (int)stickNum +{ + int i, j; + Sint16 axisvalue=SDL_JoystickGetAxis(stick[stickNum], axis); + for(i=0; i < MAX_AXES; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + if(axismap[j][i] == function) + { + axismap[j][i] = STICK_NOFUNCTION; + break; + } + } + } + axismap[stickNum][axis]=function; + + // initialize the throttle to what it's set to now (or else the + // commander has to waggle the throttle to wake it up). Other axes + // set as default. + if(function == AXIS_THRUST) + { + axstate[function]=(float)(65536 - (axisvalue + 32768)) / 65536; + } + else + { + axstate[function]=(float)axisvalue / STICK_NORMALDIV; + } +} + + +- (void) setFunctionForButton: (int)button + function: (int)function + stick: (int)stickNum +{ + int i, j; + for(i=0; i < MAX_BUTTONS; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + if(buttonmap[j][i] == function) + { + buttonmap[j][i] = STICK_NOFUNCTION; + break; + } + } + } + buttonmap[stickNum][button]=function; +} + + +- (void) unsetAxisFunction: (int)function +{ + int i, j; + for(i=0; i < MAX_AXES; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + if(axismap[j][i] == function) + { + axismap[j][i]=STICK_NOFUNCTION; + axstate[function]=STICK_AXISUNASSIGNED; + break; + } + } + } +} + + +- (void) unsetButtonFunction: (int)function +{ + int i,j; + for(i=0; i < MAX_BUTTONS; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + if(buttonmap[j][i] == function) + { + buttonmap[j][i]=STICK_NOFUNCTION; + break; + } + } + } +} + + +- (void) setDefaultMapping +{ + // assign the simplest mapping: stick 0 having + // axis 0/1 being roll/pitch and button 0 being fire, 1 being missile + // All joysticks should at least have two axes and two buttons. + axismap[0][0]=AXIS_ROLL; + axismap[0][1]=AXIS_PITCH; + buttonmap[0][0]=BUTTON_FIRE; + buttonmap[0][1]=BUTTON_LAUNCHMISSILE; +} + + +- (void) clearMappings +{ + int i, j; + for(i=0; i < MAX_AXES; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + axismap[j][i]=STICK_NOFUNCTION; + } + } + for(i=0; i < MAX_BUTTONS; i++) + { + for(j=0; j < MAX_STICKS; j++) + { + buttonmap[j][i]=STICK_NOFUNCTION; + } + } +} + + +- (void) clearStickStates +{ + int i; + for(i=0; i < AXIS_end; i++) + { + axstate[i]=STICK_AXISUNASSIGNED; + } + for(i=0; i < BUTTON_end; i++) + { + butstate[i]=0; + } +} + + +- (void)setCallback: (SEL) selector + object: (id) obj + hardware: (char)hwflags + { + cbObject=obj; + cbSelector=selector; + cbHardware=hwflags; + } + + +- (void)clearCallback +{ + cbObject=nil; + cbHardware=0; +} + + +- (void)decodeAxisEvent: (SDL_JoyAxisEvent *)evt +{ + // Which axis moved? Does the value need to be made to fit a + // certain function? Convert axis value to a double. + double axisvalue=(double)evt->value; + + // Is there a callback we need to make? + if(cbObject && (cbHardware & HW_AXIS) && abs(axisvalue) > AXCBTHRESH) + { + NSLog(@"Callback..."); + NSDictionary *fnDict=[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool: YES], STICK_ISAXIS, + [NSNumber numberWithInt: evt->which], STICK_NUMBER, + [NSNumber numberWithInt: evt->axis], STICK_AXBUT, + nil]; + cbHardware=0; + [cbObject performSelector:cbSelector withObject:fnDict]; + cbObject=nil; + + // we are done. + return; + } + + // SDL seems to have some bizarre (perhaps a bug) behaviour when + // events get queued up because the game isn't ready to handle + // them (perhaps it's loading a commander and initializing the + // universe, and the main event loop is blocked). + // What happens is SDL lies about the axis that was triggered. For + // each queued event it adds 1 to the axis number!! This does + // not seem to happen with buttons. + int function; + if(evt->axis < MAX_AXES) + { + function=axismap[evt->which][evt->axis]; + } + else + { + NSLog(@"Stick axis out of range - axis was %d", evt->axis); + return; + } + switch (function) + { + case STICK_NOFUNCTION: + // do nothing + break; + case AXIS_THRUST: + // Normalize the thrust setting. + axstate[function]=(float)(65536 - (axisvalue + 32768)) / 65536; + break; + case AXIS_ROLL: + case AXIS_PITCH: + case AXIS_YAW: + if(precisionMode) + { + axstate[function]=axisvalue / STICK_PRECISIONDIV; + } + else + { + axstate[function]=axisvalue / STICK_NORMALDIV; + } + break; + case AXIS_VIEWX: + case AXIS_VIEWY: + axstate[function]=axisvalue / STICK_NORMALDIV; + default: + // set the state with no modification. + axstate[function]=axisvalue / 32768; + } +} + + +- (void)decodeButtonEvent: (SDL_JoyButtonEvent *)evt +{ + BOOL bs=NO; + + // Is there a callback we need to make? + if(cbObject && (cbHardware & HW_BUTTON)) + { + NSDictionary *fnDict=[NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithBool: NO], STICK_ISAXIS, + [NSNumber numberWithInt: evt->which], STICK_NUMBER, + [NSNumber numberWithInt: evt->button], STICK_AXBUT, + nil]; + cbHardware=0; + [cbObject performSelector:cbSelector withObject:fnDict]; + cbObject=nil; + + // we are done. + return; + } + + // Defensive measure - see comments in the axis handler for why. + int function; + if(evt->button < MAX_BUTTONS) + { + function=buttonmap[evt->which][evt->button]; + } + else + { + NSLog(@"Joystick button out of range: %d", evt->button); + return; + } + if(evt->type == SDL_JOYBUTTONDOWN) + { + bs=YES; + if(function == BUTTON_PRECISION) + { + precisionMode=!precisionMode; + + // adjust current states now + if(precisionMode) + { + if (axstate[AXIS_PITCH] != STICK_AXISUNASSIGNED) axstate[AXIS_PITCH] /= STICK_PRECISIONFAC; + if (axstate[AXIS_ROLL] != STICK_AXISUNASSIGNED) axstate[AXIS_ROLL] /= STICK_PRECISIONFAC; + if (axstate[AXIS_YAW] != STICK_AXISUNASSIGNED) axstate[AXIS_YAW] /= STICK_PRECISIONFAC; + } + else + { + if (axstate[AXIS_PITCH] != STICK_AXISUNASSIGNED) axstate[AXIS_PITCH] *= STICK_PRECISIONFAC; + if (axstate[AXIS_ROLL] != STICK_AXISUNASSIGNED) axstate[AXIS_ROLL] *= STICK_PRECISIONFAC; + if (axstate[AXIS_YAW] != STICK_AXISUNASSIGNED) axstate[AXIS_YAW] *= STICK_PRECISIONFAC; + } + } + } + + if(function >= 0) + { + butstate[function]=bs; + } + +} + + +- (void)decodeHatEvent: (SDL_JoyHatEvent *)evt +{ + // HACK: handle this as a set of buttons + int i; + SDL_JoyButtonEvent btn; + + btn.which = evt->which; + + for (i = 0; i < 4; ++i) + { + if ((evt->value ^ hatstate[evt->which][evt->hat]) & (1 << i)) + { + btn.type = (evt->value & (1 << i)) ? SDL_JOYBUTTONDOWN : SDL_JOYBUTTONUP; + btn.button = MAX_REAL_BUTTONS + i + evt->which * 4; + btn.state = (evt->value & (1 << i)) ? SDL_PRESSED : SDL_RELEASED; + [self decodeButtonEvent:&btn]; + } + } + + hatstate[evt->which][evt->hat] = evt->value; +} + + + +- (int)getNumSticks +{ + return numSticks; +} + + +- (void)saveStickSettings +{ + NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; + [defaults setObject: [self getAxisFunctions] + forKey: AXIS_SETTINGS]; + [defaults setObject: [self getButtonFunctions] + forKey: BUTTON_SETTINGS]; + + [defaults synchronize]; +} + + +- (void)loadStickSettings +{ + unsigned i; + [self clearMappings]; + NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; + NSDictionary *axisSettings=[defaults objectForKey: AXIS_SETTINGS]; + NSDictionary *buttonSettings=[defaults objectForKey: BUTTON_SETTINGS]; + if(axisSettings) + { + NSArray *keys=[axisSettings allKeys]; + for(i=0; i < [keys count]; i++) + { + NSString *key=[keys objectAtIndex: i]; + [self setFunction: [key intValue] + withDict: [axisSettings objectForKey: key]]; + } + } + if(buttonSettings) + { + NSArray *keys=[buttonSettings allKeys]; + for(i=0; i < [keys count]; i++) + { + NSString *key=[keys objectAtIndex: i]; + [self setFunction: [key intValue] + withDict: [buttonSettings objectForKey: key]]; + } + } + else + { + // Nothing to load - set useful defaults + [self setDefaultMapping]; + } +} + +@end diff --git a/src/SDL/MyOpenGLView.h b/src/SDL/MyOpenGLView.h new file mode 100644 index 00000000..11b1ba37 --- /dev/null +++ b/src/SDL/MyOpenGLView.h @@ -0,0 +1,223 @@ +/* + +MyOpenGLView.h + +Oolite +Copyright (C) 2004-2007 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "OOCocoa.h" +#import "OOOpenGL.h" + +#ifdef GNUSTEP +#include +#endif + +#define MAX_CLEAR_DEPTH 100000000.0 +// 100 000 km. + +#define NUM_KEYS 320 + +#define MOUSE_DOUBLE_CLICK_INTERVAL 0.40 +#define MOUSE_VIRTSTICKSENSITIVITY 930.0f +#define MOUSEX_MAXIMUM 0.6 +#define MOUSEY_MAXIMUM 0.6 + +@class Entity, GameController, OpenGLSprite; + +#ifdef GNUSTEP +@class JoystickHandler; +#define OpenGLViewSuperClass NSObject +#else +#define OpenGLViewSuperClass NSOpenGLView +#endif + +enum GameViewKeys +{ + gvArrowKeyUp = 255, + gvArrowKeyDown = 254, + gvArrowKeyLeft = 253, + gvArrowKeyRight = 252, + gvFunctionKey1 = 241, + gvFunctionKey2 = 242, + gvFunctionKey3 = 243, + gvFunctionKey4 = 244, + gvFunctionKey5 = 245, + gvFunctionKey6 = 246, + gvFunctionKey7 = 247, + gvFunctionKey8 = 248, + gvFunctionKey9 = 249, + gvFunctionKey10 = 250, + gvFunctionKey11 = 251, + gvMouseLeftButton = 301, + gvMouseDoubleClick = 303, + gvHomeKey = 302, + gvNumberKey0 = 48, + gvNumberKey1 = 49, + gvNumberKey2 = 50, + gvNumberKey3 = 51, + gvNumberKey4 = 52, + gvNumberKey5 = 53, + gvNumberKey6 = 54, + gvNumberKey7 = 55, + gvNumberKey8 = 56, + gvNumberKey9 = 57 +}; + +enum StringInput +{ + gvStringInputNo = 0, + gvStringInputAlpha = 1, + gvStringInputAll = 2 +}; + +enum KeyboardType +{ + gvKeyboardAuto, + gvKeyboardUS, + gvKeyboardUK +}; + +extern int debug; + +@interface MyOpenGLView : OpenGLViewSuperClass +{ + GameController *gameController; + BOOL keys[NUM_KEYS]; + BOOL supressKeys; // DJS + + BOOL opt, ctrl, command, shift; + BOOL allowingStringInput; + BOOL isAlphabetKeyDown; + + int keycodetrans[255]; + + BOOL m_glContextInitialized; + NSPoint mouseDragStartPoint; + + NSTimeInterval timeIntervalAtLastClick; + BOOL doubleClick; + + NSMutableString *typedString; + + NSPoint virtualJoystickPosition; + + NSSize viewSize; + GLfloat display_z; + + double squareX,squareY; + NSRect bounds; + + // Full screen sizes + NSMutableArray *screenSizes; + unsigned currentSize; + BOOL fullScreen; + + // Windowed mode + NSSize currentWindowSize; + SDL_Surface* surface; + JoystickHandler *stickHandler; + +#if OOLITE_WINDOWS + + BOOL wasFullScreen; + BOOL splashScreen; + BOOL saveSize; + unsigned keyboardMap; + HWND SDL_Window; + +#endif + + NSSize firstScreen; + + // Mouse mode indicator (for mouse movement model) + BOOL mouseInDeltaMode; +} + +- (void) initSplashScreen; +- (void) endSplashScreen; +- (void) autoShowMouse; + +- (void) setStringInput: (enum StringInput) value; +- (void) allowStringInput: (BOOL) value; +- (enum StringInput) allowingStringInput; +- (NSString *) typedString; +- (void) resetTypedString; +- (void) setTypedString:(NSString*) value; + +- (NSSize) viewSize; +- (GLfloat) display_z; + +- (GameController *) gameController; +- (void) setGameController:(GameController *) controller; + +- (void) initialiseGLWithSize:(NSSize) v_size; +- (void) initialiseGLWithSize:(NSSize) v_size useVideoMode:(BOOL) v_mode; + +- (void) display; +- (void) updateScreen; +- (void) drawRect:(NSRect)rect; +- (void) updateScreenWithVideoMode:(BOOL) v_mode; + +- (void) snapShot; + +- (NSRect) bounds; ++ (NSMutableDictionary *) getNativeSize; + +- (void) setFullScreenMode:(BOOL)fsm; +- (BOOL) inFullScreenMode; +- (void) toggleScreenMode; +- (void) setDisplayMode:(int)mode fullScreen:(BOOL)fsm; + +- (int) indexOfCurrentSize; +- (void) setScreenSize: (int)sizeIndex; +- (NSMutableArray *)getScreenSizeArray; +- (void) populateFullScreenModelist; +- (NSSize) modeAsSize: (int)sizeIndex; +- (void) saveWindowSize: (NSSize) windowSize; +- (NSSize) loadWindowSize; +- (int) loadFullscreenSettings; +- (int) findDisplayModeForWidth: (unsigned int) d_width Height:(unsigned int) d_height + Refresh: (unsigned int)d_refresh; +- (NSSize) currentScreenSize; + +- (void) pollControls; +- (void) handleStringInput: (SDL_KeyboardEvent *) kbd_event; // DJS +- (JoystickHandler *)getStickHandler; // DJS + +- (void) setVirtualJoystick:(double) vmx :(double) vmy; +- (NSPoint) virtualJoystickPosition; + +- (void) setKeyboardTo: (NSString *) value; +- (void) clearKeys; +- (void) clearMouse; +- (BOOL) isAlphabetKeyDown; +- (void) supressKeysUntilKeyUp; // DJS +- (BOOL) isDown: (int) key; +- (BOOL) isOptDown; +- (BOOL) isCtrlDown; +- (BOOL) isCommandDown; +- (BOOL) isShiftDown; +- (int) numKeys; + +- (void) setMouseInDeltaMode: (BOOL) inDelta; + +// Check current state of shift key rather than relying on last event. +- (BOOL)pollShiftKey; +@end diff --git a/src/SDL/MyOpenGLView.m b/src/SDL/MyOpenGLView.m new file mode 100644 index 00000000..b3fa55ad --- /dev/null +++ b/src/SDL/MyOpenGLView.m @@ -0,0 +1,1678 @@ +/* + +MyOpenGLView.m + +Oolite +Copyright (C) 2004-2009 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import "MyOpenGLView.h" + +#import "GameController.h" +#import "Universe.h" +#import "JoystickHandler.h" // TODO: Not here! +#import "SDL_syswm.h" +#import "OOSound.h" +#import "NSFileManagerOOExtensions.h" // to find savedir +#import "PlayerEntity.h" +#import "GuiDisplayGen.h" +#import "PlanetEntity.h" +#import "OOGraphicsResetManager.h" + +#define kOOLogUnconvertedNSLog @"unclassified.MyOpenGLView" + +#define SDL_SPLASH 1 + +#include + +@implementation MyOpenGLView + ++ (NSMutableDictionary *) getNativeSize +{ + NSMutableDictionary *mode=[[NSMutableDictionary alloc] init]; + int nativeDisplayWidth = 1024; + int nativeDisplayHeight = 768; + +#if OOLITE_LINUX + SDL_SysWMinfo dpyInfo; + SDL_VERSION(&dpyInfo.version); + if(SDL_GetWMInfo(&dpyInfo)) + { + nativeDisplayWidth = DisplayWidth(dpyInfo.info.x11.display, 0); + nativeDisplayHeight = DisplayHeight(dpyInfo.info.x11.display, 0); + OOLog(@"display.mode.list.native", @"X11 native resolution detected: %d x %d", nativeDisplayWidth, nativeDisplayHeight); + } + else + { + OOLog(@"display.mode.list.native.failed", @"SDL_GetWMInfo failed, defaulting to 1024x768 for native size"); + } +#elif OOLITE_WINDOWS + nativeDisplayWidth = GetSystemMetrics(SM_CXSCREEN); + nativeDisplayHeight = GetSystemMetrics(SM_CYSCREEN); + OOLog(@"display.mode.list.native", @"Windows native resolution detected: %d x %d", nativeDisplayWidth, nativeDisplayHeight); +#else + OOLog(@"display.mode.list.native.unknown", @"Unknown architecture, defaulting to 1024x768"); +#endif + [mode setValue: [NSNumber numberWithInt: nativeDisplayWidth] forKey:kOODisplayWidth]; + [mode setValue: [NSNumber numberWithInt: nativeDisplayHeight] forKey: kOODisplayHeight]; + [mode setValue: [NSNumber numberWithInt: 0] forKey: kOODisplayRefreshRate]; + + return [mode autorelease]; +} + + +- (id) init +{ + self = [super init]; + + Uint32 colorkey; + SDL_Surface *icon=NULL; + NSString *imagesDir; + + + // TODO: This code up to and including stickHandler really ought + // not to be in this class. + OOLog(@"sdl.init", @"initialising SDL"); + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_JOYSTICK) < 0) + { + OOLog(@"sdl.init.failed", @"Unable to init SDL: %s\n", SDL_GetError()); + [self dealloc]; + return nil; + } + + SDL_putenv ("SDL_VIDEO_WINDOW_POS=center"); + + stickHandler=[[JoystickHandler alloc] init]; + // end TODO + + [OOSound setUp]; + + // Generate the window caption, containing the version number and the date the executable was compiled. + static char windowCaption[128]; + NSString *versionString = [NSString stringWithFormat:@"Oolite v%@", [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"]]; + + strcpy (windowCaption, [versionString UTF8String]); + strcat (windowCaption, " - "__DATE__); + SDL_WM_SetCaption (windowCaption, "Oolite"); // Set window title. + +#if OOLITE_WINDOWS + + //capture the window handle for later + static SDL_SysWMinfo wInfo; + SDL_VERSION(&wInfo.version); + SDL_GetWMInfo(&wInfo); + SDL_Window = wInfo.window; + imagesDir = @"Resources/Images"; + +#else + + imagesDir = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Images"]; + +#endif + + icon = SDL_LoadBMP([[imagesDir stringByAppendingPathComponent:@"WMicon.bmp"] cString]); + + if (icon != NULL) + { + colorkey = SDL_MapRGB(icon->format, 128, 0, 128); + SDL_SetColorKey(icon, SDL_SRCCOLORKEY, colorkey); + SDL_WM_SetIcon(icon, NULL); + } + SDL_FreeSurface(icon); + + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + SDL_GL_SetAttribute(SDL_GL_SWAP_CONTROL, 1); // V-sync on by default. + + OOLog(@"display.mode.list", @"CREATING MODE LIST"); + [self populateFullScreenModelist]; + currentSize = 0; + + // Find what the full screen and windowed settings are. + [self loadFullscreenSettings]; + [self loadWindowSize]; + + //set up the surface dimensions + + // changing the flags can trigger texture bugs + int videoModeFlags = SDL_HWSURFACE | SDL_OPENGL | SDL_RESIZABLE; + firstScreen= (fullScreen) ? [self modeAsSize: currentSize] : currentWindowSize; + +#if !SDL_SPLASH + + // new SDL.dll decouples mouse boundaries, snapshots & planet roundness from SetVideoMode. + surface = SDL_SetVideoMode(firstScreen.width, firstScreen.height, 32, videoModeFlags); + // blank the surface / go to fullscreen + [self initialiseGLWithSize: firstScreen]; + +#else + + #if OOLITE_WINDOWS + //pre setVideoMode adjustments + NSSize tmp=currentWindowSize; + splashScreen=YES; //don't update the window! + ShowWindow(SDL_Window,SW_HIDE); + ShowWindow(SDL_Window,SW_MINIMIZE); + // new SDL.dll decouples mouse boundaries, snapshots & planet roundness from SetVideoMode. + surface = SDL_SetVideoMode(firstScreen.width, firstScreen.height, 32, videoModeFlags); + + //post setVideoMode adjustments + currentWindowSize=tmp; + #else + + // SDL_NOFRAME here triggers the same texture bug + // as multiple setVideoMode in windows. + surface = SDL_SetVideoMode(8, 8, 32, videoModeFlags ); + + #endif + +#endif + if (!surface) + { + char * errStr = SDL_GetError(); + OOLog(@"display.mode.error", @"ERROR creating display: %s", errStr); + } + assert(surface != NULL); + + bounds.size.width = surface->w; + bounds.size.height = surface->h; + + [self autoShowMouse]; + + virtualJoystickPosition = NSMakePoint(0.0,0.0); + + typedString = [[NSMutableString alloc] initWithString:@""]; + allowingStringInput = NO; + isAlphabetKeyDown = NO; + + timeIntervalAtLastClick = [NSDate timeIntervalSinceReferenceDate]; + + m_glContextInitialized = NO; + + return self; +} + +- (void) endSplashScreen +{ + +#if SDL_SPLASH + + #if OOLITE_WINDOWS + + wasFullScreen=!fullScreen; + splashScreen=NO; + ShowWindow(SDL_Window,SW_RESTORE); + [self initialiseGLWithSize: firstScreen]; + + #else + + int videoModeFlags = SDL_HWSURFACE | SDL_OPENGL; + + videoModeFlags |= (fullScreen) ? SDL_FULLSCREEN : SDL_RESIZABLE; + surface = SDL_SetVideoMode(firstScreen.width, firstScreen.height, 32, videoModeFlags); + SDL_putenv ("SDL_VIDEO_WINDOW_POS=none"); //stop linux from auto centering on resize + + #endif + + [self autoShowMouse]; + [self updateScreen]; + +#endif + +} + +- (void) dealloc +{ + if (typedString) + [typedString release]; + + if (stickHandler) + [stickHandler release]; + + if (screenSizes) + [screenSizes release]; + + if (surface != 0) + { + SDL_FreeSurface(surface); + surface = 0; + } + + SDL_Quit(); + + [super dealloc]; +} + +- (void) autoShowMouse +{ + //don't touch the 'please wait...' cursor. + if (fullScreen) + { + if (SDL_ShowCursor(SDL_QUERY) == SDL_ENABLE) + SDL_ShowCursor(SDL_DISABLE); + } + else + { + if (SDL_ShowCursor(SDL_QUERY) == SDL_DISABLE) + SDL_ShowCursor(SDL_ENABLE); + } +} + +- (void) setStringInput: (enum StringInput) value +{ + allowingStringInput = value; +} + + +- (void) allowStringInput: (BOOL) value +{ + if (value) + allowingStringInput = gvStringInputAlpha; + else + allowingStringInput = gvStringInputNo; +} + +-(enum StringInput) allowingStringInput +{ + return allowingStringInput; +} + + +- (NSString *) typedString +{ + return typedString; +} + + +- (void) resetTypedString +{ + [typedString setString:@""]; +} + + +- (void) setTypedString:(NSString*) value +{ + [typedString setString:value]; +} + + +- (NSRect) bounds +{ + return bounds; +} + + +- (NSSize) viewSize +{ + return viewSize; +} + + +- (GLfloat) display_z +{ + return display_z; +} + + +- (GameController *) gameController +{ + return gameController; +} + + +- (void) setGameController:(GameController *) controller +{ + gameController = controller; +} + + +- (BOOL) inFullScreenMode +{ + return fullScreen; +} + +#ifdef GNUSTEP +- (void) setFullScreenMode:(BOOL)fsm +{ + fullScreen = fsm; + + // Save the settings for later. + [[NSUserDefaults standardUserDefaults] + setBool: fullScreen forKey:@"fullscreen"]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + + +- (void) toggleScreenMode +{ + [self setFullScreenMode: !fullScreen]; + if(fullScreen) + [self initialiseGLWithSize:[self modeAsSize: currentSize]]; + else + [self initialiseGLWithSize: currentWindowSize]; +} + + +- (void) setDisplayMode:(int)mode fullScreen:(BOOL)fsm +{ + [self setFullScreenMode: fsm]; + currentSize=mode; + if(fullScreen) + [self initialiseGLWithSize: [self modeAsSize: mode]]; +} + + +- (int) indexOfCurrentSize +{ + return currentSize; +} + + +- (void) setScreenSize: (int)sizeIndex +{ + currentSize=sizeIndex; + if(fullScreen) + [self initialiseGLWithSize: [self modeAsSize: currentSize]]; +} + + +- (NSMutableArray *)getScreenSizeArray +{ + return screenSizes; +} + + +- (NSSize) modeAsSize:(int)sizeIndex +{ + NSDictionary *mode=[screenSizes objectAtIndex: sizeIndex]; + return NSMakeSize([[mode objectForKey: kOODisplayWidth] intValue], + [[mode objectForKey: kOODisplayHeight] intValue]); +} + +#endif + +- (void) display +{ + [self updateScreen]; +} + +- (void) updateScreen +{ + [self drawRect: NSMakeRect(0, 0, viewSize.width, viewSize.height)]; +} + +- (void) drawRect:(NSRect)rect +{ + [self updateScreenWithVideoMode:YES]; +} + +- (void) updateScreenWithVideoMode:(BOOL) v_mode +{ + if ((viewSize.width != surface->w)||(viewSize.height != surface->h)) // resized + { +#if OOLITE_LINUX + m_glContextInitialized = NO; //probably not needed +#endif + viewSize.width = surface->w; + viewSize.height = surface->h; + } + + if (m_glContextInitialized == NO) + { + [self initialiseGLWithSize:viewSize useVideoMode:v_mode]; + } + + if (surface == 0) + return; + + // do all the drawing! + // + if (UNIVERSE) [UNIVERSE drawUniverse]; + else + { + // not set up yet, draw a black screen + glClearColor( 0.0, 0.0, 0.0, 0.0); + glClear( GL_COLOR_BUFFER_BIT); + } + + SDL_GL_SwapBuffers(); +} + +- (void) initSplashScreen +{ +#if SDL_SPLASH + + //too early for OOTexture! + SDL_Surface *image=NULL; + SDL_Rect dest; + + #if OOLITE_WINDOWS + NSString *imagesDir = @"Resources/Images"; + #else + NSString *imagesDir = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"Images"]; + #endif + + image = SDL_LoadBMP([[imagesDir stringByAppendingPathComponent:@"splash.bmp"] cString]); + + if (image == NULL) + { + SDL_FreeSurface(image); + OOLogWARN(@"sdl.gameStart", @"image 'splash.bmp' not found!"); + [self endSplashScreen]; + return; + } + + dest.x = 0; + dest.y = 0; + dest.w = image->w; + dest.h = image->h; + + #if OOLITE_WINDOWS + + dest.x = (GetSystemMetrics(SM_CXSCREEN)- dest.w)/2; + dest.y = (GetSystemMetrics(SM_CYSCREEN)-dest.h)/2; + SetWindowLong(SDL_Window,GWL_STYLE,GetWindowLong(SDL_Window,GWL_STYLE) & ~WS_CAPTION & ~WS_THICKFRAME); + ShowWindow(SDL_Window,SW_RESTORE); + MoveWindow(SDL_Window,dest.x,dest.y,dest.w,dest.h,TRUE); + + #else + + surface = SDL_SetVideoMode(dest.w, dest.h, 32, SDL_HWSURFACE | SDL_OPENGL); + + #endif + + glViewport( 0, 0, dest.w, dest.h); + + glEnable( GL_TEXTURE_2D ); + glClearColor( 0.0f, 0.0f, 0.0f, 0.0f ); + glClear( GL_COLOR_BUFFER_BIT ); + + glMatrixMode( GL_PROJECTION ); + glPushMatrix(); + glLoadIdentity(); + + glOrtho(0.0f, dest.w , dest.h, 0.0f, -1.0f, 1.0f); + + glMatrixMode( GL_MODELVIEW ); + glPushMatrix(); + glLoadIdentity(); + + GLuint texture; + GLenum texture_format; + GLint nOfColors; + + // get the number of channels in the SDL image + nOfColors = image->format->BytesPerPixel; + if (nOfColors == 4) // contains an alpha channel + { + if (image->format->Rmask == 0x000000ff) + texture_format = GL_RGBA; + else + texture_format = GL_BGRA; + } + else if (nOfColors == 3) // no alpha channel + { + if (image->format->Rmask == 0x000000ff) + texture_format = GL_RGB; + else + texture_format = GL_BGR; + } else { + SDL_FreeSurface(image); + OOLog(@"Sdl.GameStart", @"----- Encoding error within image 'splash.bmp'"); + [self endSplashScreen]; + return; + } + + glGenTextures( 1, &texture ); + glBindTexture( GL_TEXTURE_2D, texture ); + + // Set the texture's stretching properties + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); + + // Set the texture image data with the information from SDL_Surface + glTexImage2D( GL_TEXTURE_2D, 0, nOfColors, image->w, image->h, 0, + texture_format, GL_UNSIGNED_BYTE, image->pixels ); + + glBindTexture( GL_TEXTURE_2D, texture ); + glBegin( GL_QUADS ); + + glTexCoord2i( 0, 0 ); + glVertex2i( 0, 0 ); + glTexCoord2i( 1, 0 ); + glVertex2i( dest.w, 0 ); + glTexCoord2i( 1, 1 ); + glVertex2i( dest.h, dest.h ); + glTexCoord2i( 0, 1 ); + glVertex2i( 0, dest.h ); + + glEnd(); + + SDL_GL_SwapBuffers(); + glLoadIdentity(); // reset matrix + + if ( image ) { + SDL_FreeSurface( image ); + } + glDeleteTextures(1, &texture); + +#endif + +} + +- (void) initialiseGLWithSize:(NSSize) v_size +{ + [self initialiseGLWithSize:v_size useVideoMode:YES]; +} + +- (void) initialiseGLWithSize:(NSSize) v_size useVideoMode:(BOOL) v_mode +{ + viewSize = v_size; + OOLog(@"display.initGL", @"Requested a new surface of %d x %d, %@.", (int)viewSize.width, (int)viewSize.height,(fullScreen ? @"fullscreen" : @"windowed")); + +#if OOLITE_WINDOWS + #if SDL_SPLASH + if (splashScreen) + { + return; + } + #endif + + DEVMODE settings; + settings.dmSize = sizeof(DEVMODE); + settings.dmDriverExtra = 0; + EnumDisplaySettings(0, ENUM_CURRENT_SETTINGS, &settings); + //ChangeDisplaySettings(NULL, 0); + RECT wDC; + + if (fullScreen) + { + + settings.dmPelsWidth = viewSize.width; + settings.dmPelsHeight = viewSize.height; + settings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT; + if(!wasFullScreen) { + SetWindowLong(SDL_Window,GWL_STYLE,GetWindowLong(SDL_Window,GWL_STYLE) & ~WS_CAPTION & ~WS_THICKFRAME); + } + SetForegroundWindow(SDL_Window); + if (ChangeDisplaySettings(&settings, CDS_FULLSCREEN)==DISP_CHANGE_SUCCESSFUL) + { + MoveWindow(SDL_Window, 0, 0, viewSize.width, viewSize.height, TRUE); + } + else + { + m_glContextInitialized = YES; + return; + } + + } + else if ( wasFullScreen ) + { + // stop saveWindowSize from reacting to caption & frame + saveSize=NO; + ChangeDisplaySettings(NULL, 0); + SetWindowLong(SDL_Window,GWL_STYLE,GetWindowLong(SDL_Window,GWL_STYLE) | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX ); + + MoveWindow(SDL_Window,(GetSystemMetrics(SM_CXSCREEN)-(int)viewSize.width)/2, + (GetSystemMetrics(SM_CYSCREEN)-(int)viewSize.height)/2 -16,(int)viewSize.width,(int)viewSize.height,TRUE); + + ShowWindow(SDL_Window,SW_SHOW); + } + + GetClientRect(SDL_Window, &wDC); + + // change width in 4 pixels steps! (see snapShot method) + if (!fullScreen && (bounds.size.width != wDC.right - wDC.left + || bounds.size.height != wDC.bottom - wDC.top)) + { + bounds.size.width = wDC.right - wDC.left; + int w=bounds.size.width; + if (w & 3) w = w + 4 - (w & 3); + GetWindowRect(SDL_Window, &wDC); + viewSize.width = wDC.right - wDC.left + w - bounds.size.width; + viewSize.height = wDC.bottom - wDC.top; + MoveWindow(SDL_Window,wDC.left,wDC.top,viewSize.width,viewSize.height,TRUE); + GetClientRect(SDL_Window, &wDC); + } + + bounds.size.width = wDC.right - wDC.left; + bounds.size.height = wDC.bottom - wDC.top; + wasFullScreen=fullScreen; + +#else //OOLITE_LINUX + + int videoModeFlags = SDL_HWSURFACE | SDL_OPENGL; + + if (v_mode == NO) + videoModeFlags |= SDL_NOFRAME; + else if (fullScreen == YES) + { + videoModeFlags |= SDL_FULLSCREEN; + } + else + { + videoModeFlags |= SDL_RESIZABLE; + } + // change width in 4 pixels steps! (see snapShot method) + int w=viewSize.width; + if (w & 3) w = w + 4 - (w & 3); + viewSize.width=w; + surface = SDL_SetVideoMode((int)viewSize.width, (int)viewSize.height, 32, videoModeFlags); + + bounds.size.width = surface->w; + bounds.size.height = surface->h; + +#endif + OOLog(@"display.initGL", @"Created a new surface of %d x %d, %@.", (int)viewSize.width, (int)viewSize.height,(fullScreen ? @"fullscreen" : @"windowed")); + + + GLfloat sun_ambient[] = {0.1, 0.1, 0.1, 1.0}; + GLfloat sun_diffuse[] = {1.0, 1.0, 1.0, 1.0}; + GLfloat sun_specular[] = {1.0, 1.0, 1.0, 1.0}; + GLfloat sun_center_position[] = {4000000.0, 0.0, 0.0, 1.0}; + GLfloat stars_ambient[] = {0.25, 0.2, 0.25, 1.0}; + + if (viewSize.width/viewSize.height > 4.0/3.0) + display_z = 480.0 * bounds.size.width/bounds.size.height; + else + display_z = 640.0; + + float ratio = 0.5; + float aspect = bounds.size.height/bounds.size.width; + + if (surface != 0) + SDL_FreeSurface(surface); + + [self autoShowMouse]; + + glShadeModel(GL_FLAT); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glClear(GL_COLOR_BUFFER_BIT); + SDL_GL_SwapBuffers(); + + glClearDepth(MAX_CLEAR_DEPTH); + glViewport( 0, 0, bounds.size.width, bounds.size.height); + + squareX = 0.0f; + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); // reset matrix + glFrustum( -ratio, ratio, -aspect*ratio, aspect*ratio, 1.0, MAX_CLEAR_DEPTH); // set projection matrix + + glMatrixMode( GL_MODELVIEW); + + glEnable( GL_DEPTH_TEST); // depth buffer + glDepthFunc( GL_LESS); // depth buffer + + glFrontFace( GL_CCW); // face culling - front faces are AntiClockwise! + glCullFace( GL_BACK); // face culling + glEnable( GL_CULL_FACE); // face culling + + glEnable( GL_BLEND); // alpha blending + glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // alpha blending + + if (UNIVERSE) + { + [UNIVERSE setLighting]; + } + else + { + // At startup only... + glLightfv(GL_LIGHT1, GL_AMBIENT, sun_ambient); + glLightfv(GL_LIGHT1, GL_SPECULAR, sun_specular); + glLightfv(GL_LIGHT1, GL_DIFFUSE, sun_diffuse); + glLightfv(GL_LIGHT1, GL_POSITION, sun_center_position); + glLightModelfv(GL_LIGHT_MODEL_AMBIENT, stars_ambient); + + glEnable(GL_LIGHT1); // lighting + } + glEnable(GL_LIGHTING); // lighting + + // world's simplest OpenGL optimisations... + //glHint(GL_TRANSFORM_HINT_APPLE, GL_FASTEST); + glDisable(GL_NORMALIZE); + glDisable(GL_RESCALE_NORMAL); + + m_glContextInitialized = YES; +} + + +- (void) snapShot +{ + SDL_Surface* tmpSurface; + int w = viewSize.width; + int h = viewSize.height; + + if (w & 3) + w = w + 4 - (w & 3); + + // save in the oolite-saves directory. + NSString* originalDirectory = [[NSFileManager defaultManager] currentDirectoryPath]; + [[NSFileManager defaultManager] chdirToSnapshotPath]; + + int imageNo = 0; + + NSString *pathToPic = nil; + do + { + imageNo++; + pathToPic = [NSString stringWithFormat:@"oolite-%03d.bmp",imageNo]; + } while ([[NSFileManager defaultManager] fileExistsAtPath:pathToPic]); + + OOLog(@"snapshot", @">>>>> Snapshot %d x %d file chosen = %@", w, h, pathToPic); + + unsigned char *pixls = malloc(surface->w * surface->h * 3); +// SDL_Surface *screen; + glReadPixels(0,0,surface->w,surface->h,GL_RGB,GL_UNSIGNED_BYTE,pixls); + + int pitch = surface->w * 3; + unsigned char *aux = malloc( pitch ); + short h2=surface->h/2; + unsigned char *p1=pixls; + unsigned char *p2=pixls+((surface->h-1)*pitch); //go to last line + int i; + for(i=0; iw,surface->h,24,surface->w*3,0xFF,0xFF00,0xFF0000,0x0); + SDL_SaveBMP(tmpSurface, [pathToPic cString]); + SDL_FreeSurface(tmpSurface); + free(pixls); + + [[NSFileManager defaultManager] changeCurrentDirectoryPath:originalDirectory]; +} + +/* Turn the Cocoa ArrowKeys into our arrow key constants. */ +- (int) translateKeyCode: (int) input +{ + int key = input; + switch ( input ) + { + case NSUpArrowFunctionKey: + key = gvArrowKeyUp; + break; + + case NSDownArrowFunctionKey: + key = gvArrowKeyDown; + break; + + case NSLeftArrowFunctionKey: + key = gvArrowKeyLeft; + break; + + case NSRightArrowFunctionKey: + key = gvArrowKeyRight; + break; + + case NSF1FunctionKey: + key = gvFunctionKey1; + break; + + case NSF2FunctionKey: + key = gvFunctionKey2; + break; + + case NSF3FunctionKey: + key = gvFunctionKey3; + break; + + case NSF4FunctionKey: + key = gvFunctionKey4; + break; + + case NSF5FunctionKey: + key = gvFunctionKey5; + break; + + case NSF6FunctionKey: + key = gvFunctionKey6; + break; + + case NSF7FunctionKey: + key = gvFunctionKey7; + break; + + case NSF8FunctionKey: + key = gvFunctionKey8; + break; + + case NSF9FunctionKey: + key = gvFunctionKey9; + break; + + case NSF10FunctionKey: + key = gvFunctionKey10; + break; + + case NSF11FunctionKey: + key = gvFunctionKey11; + break; + + case NSHomeFunctionKey: + key = gvHomeKey; + break; + + default: + break; + } + return key; +} + + +- (void) setVirtualJoystick:(double) vmx :(double) vmy +{ + virtualJoystickPosition.x = vmx; + virtualJoystickPosition.y = vmy; +} + + +- (NSPoint) virtualJoystickPosition +{ + return virtualJoystickPosition; +} + + +///////////////////////////////////////////////////////////// + +- (void) clearKeys +{ + int i; + for (i = 0; i < [self numKeys]; i++) + keys[i] = NO; +} + + +- (void) clearMouse +{ + keys[gvMouseDoubleClick] = NO; + keys[gvMouseLeftButton] = NO; + doubleClick = NO; +} + + +- (BOOL) isAlphabetKeyDown +{ + return isAlphabetKeyDown = NO;; +} + +// DJS: When entering submenus in the gui, it is not helpful if the +// key down that brought you into the submenu is still registered +// as down when we're in. This makes isDown return NO until a key up +// event has been received from SDL. +- (void) supressKeysUntilKeyUp +{ + if (keys[gvMouseDoubleClick] == NO) + { + supressKeys = YES; + [self clearKeys]; + } + else + { + [self clearMouse]; + } + +} + + +- (BOOL) isDown: (int) key +{ + if ( supressKeys ) + return NO; + if ( key < 0 ) + return NO; + if ( key >= [self numKeys] ) + return NO; + return keys[key]; +} + + +- (BOOL) isOptDown +{ + return opt; +} + + +- (BOOL) isCtrlDown +{ + return ctrl; +} + + +- (BOOL) isCommandDown +{ + return command; +} + + +- (BOOL) isShiftDown +{ + return shift; +} + + +- (int) numKeys +{ + return NUM_KEYS; +} + +- (void) setKeyboardTo: (NSString *) value +{ +#if OOLITE_WINDOWS + keyboardMap=gvKeyboardAuto; + + if ([value isEqual: @"UK"]) + { + keyboardMap=gvKeyboardUK; + } + else if ([value isEqual: @"US"]) + { + keyboardMap=gvKeyboardUS; + } +#endif +} + +- (void)pollControls +{ + SDL_Event event; + SDL_KeyboardEvent *kbd_event; + SDL_MouseButtonEvent *mbtn_event; + SDL_MouseMotionEvent *mmove_event; + int mxdelta, mydelta; + + while (SDL_PollEvent(&event)) + { + switch (event.type) { + case SDL_JOYAXISMOTION: + case SDL_JOYBUTTONUP: + case SDL_JOYBUTTONDOWN: + case SDL_JOYHATMOTION: + [stickHandler handleSDLEvent: &event]; + break; + + case SDL_MOUSEBUTTONDOWN: + mbtn_event = (SDL_MouseButtonEvent*)&event; + switch(mbtn_event->button) + { + case SDL_BUTTON_LEFT: + keys[gvMouseLeftButton] = YES; + break; + case SDL_BUTTON_RIGHT: + // Cocoa version does this in the GameController + [self setVirtualJoystick:0.0 :0.0]; + } + break; + + case SDL_MOUSEBUTTONUP: + mbtn_event = (SDL_MouseButtonEvent*)&event; + NSTimeInterval timeBetweenClicks = [NSDate timeIntervalSinceReferenceDate] - timeIntervalAtLastClick; + timeIntervalAtLastClick += timeBetweenClicks; + if (mbtn_event->button == SDL_BUTTON_LEFT) + { + if (!doubleClick) + { + doubleClick = (timeBetweenClicks < MOUSE_DOUBLE_CLICK_INTERVAL); // One fifth of a second + keys[gvMouseDoubleClick] = doubleClick; + } + keys[gvMouseLeftButton] = NO; + } + break; + + case SDL_MOUSEMOTION: + { + // Delta mode is set when the game is in 'flight' mode. + // In this mode, the mouse movement delta is used rather + // than absolute position. This is because if the user + // clicks the right button to recentre the virtual joystick, + // if we are using absolute joystick positioning, as soon + // as the player touches the mouse again, the virtual joystick + // will snap back to the absolute position (which can be + // annoyingly fatal in battle). + if(mouseInDeltaMode) + { + // possible TODO - make virtual stick sensitivity configurable + SDL_GetRelativeMouseState(&mxdelta, &mydelta); + double mxd=(double)mxdelta / MOUSE_VIRTSTICKSENSITIVITY; + double myd=(double)mydelta / MOUSE_VIRTSTICKSENSITIVITY; + virtualJoystickPosition.x += mxd; + virtualJoystickPosition.y += myd; + + // if we excceed the limits, revert changes + if(fabs(virtualJoystickPosition.x) > MOUSEX_MAXIMUM) + { + virtualJoystickPosition.x -= mxd; + } + if(fabs(virtualJoystickPosition.y) > MOUSEY_MAXIMUM) + { + virtualJoystickPosition.y -= myd; + } + } + else + { + // Windowed mode. Use the absolute position so the + // Oolite mouse pointer appears under the X Window System + // mouse pointer. + mmove_event = (SDL_MouseMotionEvent*)&event; + + int w=bounds.size.width; + int h=bounds.size.height; + + double mx = mmove_event->x - w/2.0; + double my = mmove_event->y - h/2.0; + if (display_z > 640.0) + { + mx /= w * MAIN_GUI_PIXEL_WIDTH / display_z; + my /= h; + } + else + { + mx /= MAIN_GUI_PIXEL_WIDTH * w / 640.0; + my /= MAIN_GUI_PIXEL_HEIGHT * w / 640.0; + } + + [self setVirtualJoystick:mx :my]; + } + break; + } + case SDL_KEYDOWN: + kbd_event = (SDL_KeyboardEvent*)&event; + + if(allowingStringInput) + { + [self handleStringInput: kbd_event]; + } + + // Macro KEYCODE_DOWN_EITHER. Detect the keypress state (with shift or without) and assign appropriate values to the + // keys array. This way Oolite can use more keys, since now key '3', for example is a different keypress to '#'. +#define KEYCODE_DOWN_EITHER(a,b) do { \ +if (shift) { keys[a] = YES; keys[b] = NO; } else { keys[a] = NO; keys[b] = YES; } \ +} while (0) + +#if OOLITE_WINDOWS + /* + Enable backslash in win/UK + */ + if (kbd_event->keysym.scancode==86) + { + //non-US scancode. If in autodetect, we'll assume UK :) + if (keyboardMap==gvKeyboardAuto || keyboardMap==gvKeyboardUK) + { KEYCODE_DOWN_EITHER (166, 92); } // windows UK | or \. + } + + switch (kbd_event->keysym.sym) { + + case SDLK_BACKSLASH: + if (keyboardMap==gvKeyboardUK ) + { + keys[35] = YES; // windows UK # + } + else if (keyboardMap==gvKeyboardAuto || keyboardMap==gvKeyboardUS) + { + KEYCODE_DOWN_EITHER (166, 92); // windows US | or \. + } + break; +#else + switch (kbd_event->keysym.sym) { + + case SDLK_BACKSLASH: KEYCODE_DOWN_EITHER (166, 92); break; // | or \. +#endif + case SDLK_1: KEYCODE_DOWN_EITHER (33, gvNumberKey1); break; // ! or 1 + case SDLK_2: KEYCODE_DOWN_EITHER (64, gvNumberKey2); break; // @ or 2 + case SDLK_3: KEYCODE_DOWN_EITHER (35, gvNumberKey3); break; // # or 3 + case SDLK_4: KEYCODE_DOWN_EITHER (36, gvNumberKey4); break; // $ or 4 + case SDLK_5: KEYCODE_DOWN_EITHER (37, gvNumberKey5); break; // % or 5 + case SDLK_6: KEYCODE_DOWN_EITHER (94, gvNumberKey6); break; // ^ or 6 + case SDLK_7: KEYCODE_DOWN_EITHER (38, gvNumberKey7); break; // & or 7 + case SDLK_8: KEYCODE_DOWN_EITHER (42, gvNumberKey8); break; // * or 8 + case SDLK_9: KEYCODE_DOWN_EITHER (40, gvNumberKey9); break; // ( or 9 + case SDLK_0: KEYCODE_DOWN_EITHER (41, gvNumberKey0); break; // ) or 0 + case SDLK_MINUS: KEYCODE_DOWN_EITHER (95, 45); break; // _ or - + case SDLK_COMMA: KEYCODE_DOWN_EITHER (60, 44); break; // < or , + case SDLK_EQUALS: KEYCODE_DOWN_EITHER (43, 61); break; // + or = + case SDLK_PERIOD: KEYCODE_DOWN_EITHER (62, 46); break; // > or . + case SDLK_SLASH: KEYCODE_DOWN_EITHER (63, 47); break; // ? or / + case SDLK_a: KEYCODE_DOWN_EITHER (65, 97); break; // A or a + case SDLK_b: KEYCODE_DOWN_EITHER (66, 98); break; // B or b + case SDLK_c: KEYCODE_DOWN_EITHER (67, 99); break; // C or c + case SDLK_d: KEYCODE_DOWN_EITHER (68, 100); break; // D or d + case SDLK_e: KEYCODE_DOWN_EITHER (69, 101); break; // E or e + case SDLK_f: KEYCODE_DOWN_EITHER (70, 102); break; // F or f + case SDLK_g: KEYCODE_DOWN_EITHER (71, 103); break; // G or g + case SDLK_h: KEYCODE_DOWN_EITHER (72, 104); break; // H or h + case SDLK_i: KEYCODE_DOWN_EITHER (73, 105); break; // I or i + case SDLK_j: KEYCODE_DOWN_EITHER (74, 106); break; // J or j + case SDLK_k: KEYCODE_DOWN_EITHER (75, 107); break; // K or k + case SDLK_l: KEYCODE_DOWN_EITHER (76, 108); break; // L or l + case SDLK_m: KEYCODE_DOWN_EITHER (77, 109); break; // M or m + case SDLK_n: KEYCODE_DOWN_EITHER (78, 110); break; // N or n + case SDLK_o: KEYCODE_DOWN_EITHER (79, 111); break; // O or o + case SDLK_p: KEYCODE_DOWN_EITHER (80, 112); break; // P or p + case SDLK_q: KEYCODE_DOWN_EITHER (81, 113); break; // Q or q + case SDLK_r: KEYCODE_DOWN_EITHER (82, 114); break; // R or r + case SDLK_s: KEYCODE_DOWN_EITHER (83, 115); break; // S or s + case SDLK_t: KEYCODE_DOWN_EITHER (84, 116); break; // T or t + case SDLK_u: KEYCODE_DOWN_EITHER (85, 117); break; // U or u + case SDLK_v: KEYCODE_DOWN_EITHER (86, 118); break; // V or v + case SDLK_w: KEYCODE_DOWN_EITHER (87, 119); break; // W or w + case SDLK_x: KEYCODE_DOWN_EITHER (88, 120); break; // X or x + case SDLK_y: KEYCODE_DOWN_EITHER (89, 121); break; // Y or y + case SDLK_z: KEYCODE_DOWN_EITHER (90, 122); break; // Z or z + //SDLK_BACKQUOTE is a special case. No SDLK_ with code 126 exists. + case SDLK_BACKQUOTE: if (!shift) keys[96] = YES; break; // ` + case SDLK_LEFTBRACKET: keys[91] = YES; break; + case SDLK_RIGHTBRACKET: keys[93] = YES; break; + case SDLK_HOME: keys[gvHomeKey] = YES; break; + case SDLK_SPACE: keys[32] = YES; break; + case SDLK_RETURN: keys[13] = YES; break; + case SDLK_TAB: keys[9] = YES; break; + case SDLK_KP8: + case SDLK_UP: keys[gvArrowKeyUp] = YES; break; + case SDLK_KP2: + case SDLK_DOWN: keys[gvArrowKeyDown] = YES; break; + case SDLK_KP4: + case SDLK_LEFT: keys[gvArrowKeyLeft] = YES; break; + case SDLK_KP6: + case SDLK_RIGHT: keys[gvArrowKeyRight] = YES; break; + + case SDLK_KP_MINUS: keys[45] = YES; break; // numeric keypad - key + case SDLK_KP_PLUS: keys[43] = YES; break; // numeric keypad + key + + case SDLK_KP1: keys[310] = YES; break; + case SDLK_KP3: keys[311] = YES; break; + + case SDLK_F1: keys[gvFunctionKey1] = YES; break; + case SDLK_F2: keys[gvFunctionKey2] = YES; break; + case SDLK_F3: keys[gvFunctionKey3] = YES; break; + case SDLK_F4: keys[gvFunctionKey4] = YES; break; + case SDLK_F5: keys[gvFunctionKey5] = YES; break; + case SDLK_F6: keys[gvFunctionKey6] = YES; break; + case SDLK_F7: keys[gvFunctionKey7] = YES; break; + case SDLK_F8: keys[gvFunctionKey8] = YES; break; + case SDLK_F9: keys[gvFunctionKey9] = YES; break; + case SDLK_F10: keys[gvFunctionKey10] = YES; break; + + case SDLK_LSHIFT: + case SDLK_RSHIFT: + shift = YES; + break; + + case SDLK_LCTRL: + case SDLK_RCTRL: + ctrl = YES; + break; + + case SDLK_F11: + if (fullScreen==YES && [[gameController displayModes] count] > 1) + { + int count=[[gameController displayModes] count]; + if(shift) + { + currentSize--; + if (currentSize < 0) + currentSize = count - 1; + } + else + { + currentSize++; + if (currentSize >= count) + currentSize = 0; + } + //save the new fullscreen mode + NSDictionary *mode=[[gameController displayModes] objectAtIndex: currentSize]; + NSSize newSize = NSMakeSize ([[mode objectForKey:kOODisplayWidth] intValue], [[mode objectForKey:kOODisplayHeight] intValue]); + [gameController setDisplayWidth:newSize.width + Height:newSize.height Refresh:[[mode objectForKey:kOODisplayRefreshRate] intValue]]; + [self initialiseGLWithSize: newSize]; + + if([[PlayerEntity sharedPlayer] guiScreen]==GUI_SCREEN_GAMEOPTIONS) + { + //refresh to display current fullscreen res + [[PlayerEntity sharedPlayer] setGuiToGameOptionsScreen]; + } + } + break; + + case SDLK_F12: + [self toggleScreenMode]; + if([[PlayerEntity sharedPlayer] guiScreen]==GUI_SCREEN_GAMEOPTIONS) + { + //refresh play windowed / full screen + [[PlayerEntity sharedPlayer] setGuiToGameOptionsScreen]; + } + break; + + case SDLK_ESCAPE: + if (shift) + { + SDL_FreeSurface(surface); + [gameController exitApp]; + } + else + keys[27] = YES; + break; + default: + // Numerous cases not handled. + //OOLog(@"keys.test", @"Keydown scancode: %d", kbd_event->keysym.scancode); + ; + } + break; + + case SDL_KEYUP: + supressKeys = NO; // DJS + kbd_event = (SDL_KeyboardEvent*)&event; + //printf("Keydown scancode: %d\n", kbd_event->keysym.scancode); + +#define KEYCODE_UP_BOTH(a,b) do { \ +keys[a] = NO; keys[b] = NO; \ +} while (0) + +#if OOLITE_WINDOWS + /* + Windows locale patch. + */ + if (kbd_event->keysym.scancode==86) + { + //non-US scancode. If in autodetect, we'll assume UK :) + if (keyboardMap==gvKeyboardAuto || keyboardMap==gvKeyboardUK) + { KEYCODE_UP_BOTH (166, 92); } // windows UK | or \. + } + + switch (kbd_event->keysym.sym) { + + case SDLK_BACKSLASH: + if (keyboardMap==gvKeyboardUK ) + { + keys[35] = NO; // windows UK # + } + else if (keyboardMap==gvKeyboardAuto || keyboardMap==gvKeyboardUS) + { + KEYCODE_UP_BOTH (166, 92); // windows US | or \. + } + break; +#else + switch (kbd_event->keysym.sym) { + + case SDLK_BACKSLASH: KEYCODE_UP_BOTH (166, 92); break; // | or \. +#endif + + case SDLK_1: KEYCODE_UP_BOTH (33, gvNumberKey1); break; // ! and 1 + case SDLK_2: KEYCODE_UP_BOTH (64, gvNumberKey2); break; // @ and 2 + case SDLK_3: KEYCODE_UP_BOTH (35, gvNumberKey3); break; // # and 3 + case SDLK_4: KEYCODE_UP_BOTH (36, gvNumberKey4); break; // $ and 4 + case SDLK_5: KEYCODE_UP_BOTH (37, gvNumberKey5); break; // % and 5 + case SDLK_6: KEYCODE_UP_BOTH (94, gvNumberKey6); break; // ^ and 6 + case SDLK_7: KEYCODE_UP_BOTH (38, gvNumberKey7); break; // & and 7 + case SDLK_8: KEYCODE_UP_BOTH (42, gvNumberKey8); break; // * and 8 + case SDLK_9: KEYCODE_UP_BOTH (40, gvNumberKey9);break; // ( and 9 + case SDLK_0: KEYCODE_UP_BOTH (41, gvNumberKey0); break; // ) and 0 + case SDLK_MINUS: KEYCODE_UP_BOTH (95, 45); break; // _ and - + case SDLK_COMMA: KEYCODE_UP_BOTH (60, 44); break; // < and , + case SDLK_EQUALS: KEYCODE_UP_BOTH (43, 61); break; // + and = + case SDLK_PERIOD: KEYCODE_UP_BOTH (62, 46); break; // > and . + case SDLK_SLASH: KEYCODE_UP_BOTH (63, 47); break; // ? and / + case SDLK_a: KEYCODE_UP_BOTH (65, 97); break; // A and a + case SDLK_b: KEYCODE_UP_BOTH (66, 98); break; // B and b + case SDLK_c: KEYCODE_UP_BOTH (67, 99); break; // C and c + case SDLK_d: KEYCODE_UP_BOTH (68, 100); break; // D and d + case SDLK_e: KEYCODE_UP_BOTH (69, 101); break; // E and e + case SDLK_f: KEYCODE_UP_BOTH (70, 102); break; // F and f + case SDLK_g: KEYCODE_UP_BOTH (71, 103); break; // G and g + case SDLK_h: KEYCODE_UP_BOTH (72, 104); break; // H and h + case SDLK_i: KEYCODE_UP_BOTH (73, 105); break; // I and i + case SDLK_j: KEYCODE_UP_BOTH (74, 106); break; // J and j + case SDLK_k: KEYCODE_UP_BOTH (75, 107); break; // K and k + case SDLK_l: KEYCODE_UP_BOTH (76, 108); break; // L and l + case SDLK_m: KEYCODE_UP_BOTH (77, 109); break; // M and m + case SDLK_n: KEYCODE_UP_BOTH (78, 110); break; // N and n + case SDLK_o: KEYCODE_UP_BOTH (79, 111); break; // O and o + case SDLK_p: KEYCODE_UP_BOTH (80, 112); break; // P and p + case SDLK_q: KEYCODE_UP_BOTH (81, 113); break; // Q and q + case SDLK_r: KEYCODE_UP_BOTH (82, 114); break; // R and r + case SDLK_s: KEYCODE_UP_BOTH (83, 115); break; // S and s + case SDLK_t: KEYCODE_UP_BOTH (84, 116); break; // T and t + case SDLK_u: KEYCODE_UP_BOTH (85, 117); break; // U and u + case SDLK_v: KEYCODE_UP_BOTH (86, 118); break; // V and v + case SDLK_w: KEYCODE_UP_BOTH (87, 119); break; // W and w + case SDLK_x: KEYCODE_UP_BOTH (88, 120); break; // X and x + case SDLK_y: KEYCODE_UP_BOTH (89, 121); break; // Y and y + case SDLK_z: KEYCODE_UP_BOTH (90, 122); break; // Z and z + //SDLK_BACKQUOTE is a special case. No SDLK_ with code 126 exists. + case SDLK_BACKQUOTE: keys[96] = NO; break; // ` + case SDLK_HOME: keys[gvHomeKey] = NO; break; + case SDLK_SPACE: keys[32] = NO; break; + case SDLK_RETURN: keys[13] = NO; break; + case SDLK_TAB: keys[9] = NO; break; + case SDLK_KP8: + case SDLK_UP: keys[gvArrowKeyUp] = NO; break; + case SDLK_KP2: + case SDLK_DOWN: keys[gvArrowKeyDown] = NO; break; + case SDLK_KP4: + case SDLK_LEFT: keys[gvArrowKeyLeft] = NO; break; + case SDLK_KP6: + case SDLK_RIGHT: keys[gvArrowKeyRight] = NO; break; + + case SDLK_KP_MINUS: keys[45] = NO; break; // numeric keypad - key + case SDLK_KP_PLUS: keys[43] = NO; break; // numeric keypad + key + + case SDLK_KP1: keys[310] = NO; break; + case SDLK_KP3: keys[311] = NO; break; + + case SDLK_F1: keys[gvFunctionKey1] = NO; break; + case SDLK_F2: keys[gvFunctionKey2] = NO; break; + case SDLK_F3: keys[gvFunctionKey3] = NO; break; + case SDLK_F4: keys[gvFunctionKey4] = NO; break; + case SDLK_F5: keys[gvFunctionKey5] = NO; break; + case SDLK_F6: keys[gvFunctionKey6] = NO; break; + case SDLK_F7: keys[gvFunctionKey7] = NO; break; + case SDLK_F8: keys[gvFunctionKey8] = NO; break; + case SDLK_F9: keys[gvFunctionKey9] = NO; break; + case SDLK_F10: keys[gvFunctionKey10] = NO; break; + + case SDLK_LSHIFT: + case SDLK_RSHIFT: + shift = NO; + break; + + case SDLK_LCTRL: + case SDLK_RCTRL: + ctrl = NO; + break; + + case SDLK_ESCAPE: + keys[27] = NO; + break; + + default: + // Numerous cases not handled. + ; + } + break; + + case SDL_VIDEORESIZE: + { + SDL_ResizeEvent *rsevt=(SDL_ResizeEvent *)&event; + NSSize newSize=NSMakeSize(rsevt->w, rsevt->h); + [self initialiseGLWithSize: newSize]; +#if OOLITE_WINDOWS + if (!fullScreen && !splashScreen) + { + if (saveSize == NO) + { + // event triggered by caption & frame + // next event will be a real resize. + saveSize = YES; + } + else + { + [self saveWindowSize: newSize]; + } + } +#else + [self saveWindowSize: newSize]; +#endif + break; + } + + // caused by INTR or someone hitting close + case SDL_QUIT: + { + SDL_FreeSurface(surface); + [gameController exitApp]; + } + } + } +} + + +// DJS: String input handler. Since for SDL versions we're also handling +// freeform typing this has necessarily got more complex than the non-SDL +// versions. +- (void) handleStringInput: (SDL_KeyboardEvent *) kbd_event; +{ + SDLKey key=kbd_event->keysym.sym; + + // Del, Backspace + if(key == SDLK_BACKSPACE || key == SDLK_DELETE) + { + if([typedString length] >= 1) + { + [typedString deleteCharactersInRange: + NSMakeRange([typedString length]-1, 1)]; + } + else + { + [self resetTypedString]; + } + } + + // Note: if we start using this handler for anything other + // than savegames, a more flexible mechanism is needed + // for max. string length. + if([typedString length] < 40) + { + // keys a-z + if(key >= SDLK_a && key <= SDLK_z) + { + isAlphabetKeyDown=YES; + if(shift) + { + key=toupper(key); + } + [typedString appendFormat:@"%c", key]; + } + + // keys 0-9, Space + // Left-Shift seems to produce the key code for 0 :/ + if((key >= SDLK_0 && key <= SDLK_9) || key == SDLK_SPACE) + { + [typedString appendFormat:@"%c", key]; + } + } +} + +// Full screen mode enumerator. +- (void) populateFullScreenModelist +{ + int i; + SDL_Rect **modes; + NSMutableDictionary *mode; + + screenSizes=[[NSMutableArray alloc] init]; + + // The default resolution (slot 0) is the resolution we are + // already in since this is guaranteed to work. + mode=[MyOpenGLView getNativeSize]; + [screenSizes addObject: mode]; + + modes=SDL_ListModes(NULL, SDL_FULLSCREEN|SDL_HWSURFACE); + if(modes == (SDL_Rect **)NULL) + { + OOLog(@"display.mode.list.none", @"SDL didn't return any screen modes"); + return; + } + + if(modes == (SDL_Rect **)-1) + { + OOLog(@"display.mode.list.none", @"SDL claims 'all resolutions available' which is unhelpful in the extreme"); + return; + } + + int lastw=[[mode objectForKey: kOODisplayWidth] intValue]; + int lasth=[[mode objectForKey: kOODisplayHeight] intValue]; + for(i=0; modes[i]; i++) + { + // SDL_ListModes often lists a mode several times, + // presumably because each mode has several refresh rates. + // But the modes pointer is an SDL_Rect which can't represent + // refresh rates. WHY!? + if(modes[i]->w != lastw && modes[i]->h != lasth) + { + // new resolution, save it + mode=[NSMutableDictionary dictionary]; + [mode setValue: [NSNumber numberWithInt: (int)modes[i]->w] + forKey: kOODisplayWidth]; + [mode setValue: [NSNumber numberWithInt: (int)modes[i]->h] + forKey: kOODisplayHeight]; + [mode setValue: [NSNumber numberWithInt: 0] + forKey: kOODisplayRefreshRate]; + [screenSizes addObject: mode]; + OOLog(@"display.mode.list", @"Added res %d x %d", modes[i]->w, modes[i]->h); + lastw=modes[i]->w; + lasth=modes[i]->h; + } + } +} + +// Save and restore window sizes to/from defaults. +- (void) saveWindowSize: (NSSize) windowSize +{ + NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; + [defaults setInteger: (int)windowSize.width forKey: @"window_width"]; + [defaults setInteger: (int)windowSize.height forKey: @"window_height"]; + currentWindowSize=windowSize; +} + + +- (NSSize) loadWindowSize +{ + NSSize windowSize; + NSUserDefaults *defaults=[NSUserDefaults standardUserDefaults]; + if([defaults objectForKey:@"window_width"] && [defaults objectForKey:@"window_height"]) + { + windowSize=NSMakeSize([defaults integerForKey: @"window_width"], + [defaults integerForKey: @"window_height"]); + } + else + { + windowSize=NSMakeSize(800, 600); + } + currentWindowSize=windowSize; + return windowSize; +} + + +- (int) loadFullscreenSettings +{ + currentSize=0; + int width=0, height=0, refresh=0; + unsigned i; + + NSArray* cmdline_arguments = [[NSProcessInfo processInfo] arguments]; + + NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults]; + if ([userDefaults objectForKey:@"display_width"]) + width = [userDefaults integerForKey:@"display_width"]; + if ([userDefaults objectForKey:@"display_height"]) + height = [userDefaults integerForKey:@"display_height"]; + if ([userDefaults objectForKey:@"display_refresh"]) + refresh = [userDefaults integerForKey:@"display_refresh"]; + if([userDefaults objectForKey:@"fullscreen"]) + fullScreen=[userDefaults boolForKey:@"fullscreen"]; + + // Check if -fullscreen has been passed on the command line. If yes, set it regardless of + // what is set by .GNUstepDefaults. + for (i = 0; i < [cmdline_arguments count]; i++) + { + if ([[cmdline_arguments objectAtIndex:i] isEqual:@"-fullscreen"]) fullScreen = YES; + } + + if(width && height) + { + currentSize=[self findDisplayModeForWidth: width Height: height Refresh: refresh]; + return currentSize; + } + return currentSize; +} + + +- (int) findDisplayModeForWidth:(unsigned int) d_width Height:(unsigned int) d_height Refresh:(unsigned int) d_refresh +{ + int i, modeCount; + NSDictionary *mode; + unsigned int modeWidth, modeHeight, modeRefresh; + + modeCount = [screenSizes count]; + + for (i = 0; i < modeCount; i++) + { + mode = [screenSizes objectAtIndex: i]; + modeWidth = [[mode objectForKey: kOODisplayWidth] intValue]; + modeHeight = [[mode objectForKey: kOODisplayHeight] intValue]; + modeRefresh = [[mode objectForKey: kOODisplayRefreshRate] intValue]; + if ((modeWidth == d_width)&&(modeHeight == d_height)&&(modeRefresh == d_refresh)) + { + OOLog(@"display.mode.found", @"Found mode %@", mode); + return i; + } + } + + OOLog(@"display.mode.found.failed", @"Failed to find mode: width=%d height=%d refresh=%d", d_width, d_height, d_refresh); + OOLog(@"display.mode.found.failed.list", @"Contents of list: %@", screenSizes); + return 0; +} + + +- (NSSize) currentScreenSize +{ + NSDictionary *mode=[screenSizes objectAtIndex: currentSize]; + + if(mode) + { + return NSMakeSize([[mode objectForKey: kOODisplayWidth] intValue], + [[mode objectForKey: kOODisplayHeight] intValue]); + } + OOLog(@"display.mode.unknown", @"Screen size unknown!"); + return NSMakeSize(800, 600); +} + + +- (JoystickHandler *) getStickHandler +{ + return stickHandler; +} + + +- (void) setMouseInDeltaMode: (BOOL) inDelta +{ + mouseInDeltaMode=inDelta; +} + + +- (BOOL)pollShiftKey +{ +#if OOLITE_WINDOWS + // SDL_GetModState() does not seem to do exactly what is intended under Windows. For this reason, + // the GetKeyState Windows API call is used to detect the Shift keypress. -- Nikos. + + return 0 != (GetKeyState(VK_SHIFT) & 0x100); + +#else + return 0 != (SDL_GetModState() & (KMOD_LSHIFT | KMOD_RSHIFT)); + +#endif +} + +@end diff --git a/src/SDL/OOSDLConcreteSound.h b/src/SDL/OOSDLConcreteSound.h new file mode 100644 index 00000000..cb298fb1 --- /dev/null +++ b/src/SDL/OOSDLConcreteSound.h @@ -0,0 +1,66 @@ +/* + +OOSDLConcreteSound.h + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSDLSound.h" +#import "SDL.h" +#import "SDL_mixer.h" + + +@interface OOSDLConcreteSound: OOSound +{ + Mix_Chunk *_chunk; + NSString *_name; +} +@end + + +@interface OOSound (SDL) + +- (Mix_Chunk *) chunk; + +@end diff --git a/src/SDL/OOSDLConcreteSound.m b/src/SDL/OOSDLConcreteSound.m new file mode 100644 index 00000000..1d31d6c9 --- /dev/null +++ b/src/SDL/OOSDLConcreteSound.m @@ -0,0 +1,116 @@ +/* + +OOSDLConcreteSound.m + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSDLConcreteSound.h" +#import "OOLogging.h" + + +@implementation OOSDLConcreteSound + +- (id) initWithContentsOfFile:(NSString *)path +{ + if ((self = [super init])) + { + _chunk = Mix_LoadWAV([path cString]); + + if (_chunk != NULL) + { + #ifndef NDEBUG + OOLog(@"sound.load.success", @"Loaded a sound from \"%@\".", path); + #endif + _name = [[path lastPathComponent] copy]; + } + else + { + OOLog(@"sound.load.failed", @"Could not load a sound from \"%@\".", path); + [self release]; + self = nil; + } + } + + return self; +} + + +- (void) dealloc +{ + if (_chunk != NULL) Mix_FreeChunk(_chunk); + [_name autorelease]; + + [super dealloc]; +} + + +- (NSString *) descriptionComponents +{ + return [NSString stringWithFormat:@"\"%@\"", _name]; +} + + +- (NSString *)name +{ + return _name; +} + + +- (Mix_Chunk *) chunk +{ + return _chunk; +} + +@end + + +@implementation OOSound (SDL) + +- (Mix_Chunk *) chunk +{ + return NULL; +} + +@end diff --git a/src/SDL/OOSDLSound.h b/src/SDL/OOSDLSound.h new file mode 100644 index 00000000..f6ec33b5 --- /dev/null +++ b/src/SDL/OOSDLSound.h @@ -0,0 +1,66 @@ +/* + +OOSDLSound.h + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + + +@interface OOSound: NSObject + ++ (BOOL) setUp; ++ (void) update; + ++ (void) setMasterVolume:(float) fraction; ++ (float) masterVolume; + +- (id) initWithContentsOfFile:(NSString *)path; + +- (NSString *)name; + ++ (BOOL) isSoundOK; + +@end diff --git a/src/SDL/OOSDLSound.m b/src/SDL/OOSDLSound.m new file mode 100644 index 00000000..ce39b2eb --- /dev/null +++ b/src/SDL/OOSDLSound.m @@ -0,0 +1,154 @@ +/* + +OOSDLSound.m + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSDLSoundInternal.h" +#import "OOLogging.h" +#import "OOCollectionExtractors.h" +#import "OOMaths.h" + + +#define KEY_VOLUME_CONTROL @"volume_control" + + +static BOOL sIsSetUp = NO; +static BOOL sIsSoundOK = NO; +static int sEffectiveMasterVolume = MIX_MAX_VOLUME; + + +@implementation OOSound + ++ (BOOL) setUp +{ + if (!sIsSetUp) + { + sIsSetUp = YES; + + if (Mix_OpenAudio(44100, AUDIO_S16LSB, 2, 2048) < 0) + { + OOLog(@"sdl.init.audio.failed", @"Mix_OpenAudio: %s\n", Mix_GetError()); + return NO; + } + + Mix_AllocateChannels(kMixerGeneralChannels); + sIsSoundOK = YES; + + float volume = [[NSUserDefaults standardUserDefaults] floatForKey:KEY_VOLUME_CONTROL defaultValue:1.0]; + [self setMasterVolume:volume]; + + [OOSoundMixer sharedMixer]; + } + + return sIsSoundOK; +} + + ++ (void) setMasterVolume:(float) fraction +{ + if (!sIsSetUp && ![self setUp]) + return; + + fraction = OOClamp_0_1_f(fraction); + int volume = (float)MIX_MAX_VOLUME * fraction; + + if (volume != sEffectiveMasterVolume) + { + // -1 = all channels + Mix_Volume(-1, volume); + Mix_VolumeMusic(volume); + + sEffectiveMasterVolume = volume; + [[NSUserDefaults standardUserDefaults] setFloat:[self masterVolume] forKey:KEY_VOLUME_CONTROL]; + } +} + + ++ (float) masterVolume +{ + if (!sIsSetUp && ![self setUp] ) + return 0; + + return (float)sEffectiveMasterVolume / (float)MIX_MAX_VOLUME; +} + + +- (id) init +{ + if (!sIsSetUp) [OOSound setUp]; + return [super init]; +} + + +- (id) initWithContentsOfFile:(NSString *)path +{ + [self release]; + if (!sIsSetUp && ![OOSound setUp]) return nil; + + return [[OOSDLConcreteSound alloc] initWithContentsOfFile:path]; +} + + +- (NSString *)name +{ + OOLogGenericSubclassResponsibility(); + return @""; +} + + ++ (void) update +{ + OOSoundMixer * mixer = [OOSoundMixer sharedMixer]; + if( sIsSoundOK && mixer) + [mixer update]; +} + ++ (BOOL) isSoundOK +{ + return sIsSoundOK; +} + +@end diff --git a/src/SDL/OOSDLSoundChannel.h b/src/SDL/OOSDLSoundChannel.h new file mode 100644 index 00000000..f9e1ebb3 --- /dev/null +++ b/src/SDL/OOSDLSoundChannel.h @@ -0,0 +1,91 @@ +/* + +OOSDLSoundChannel.h + +A channel for audio playback. + +This class is an implementation detail. Do not use it directly; use an +OOSoundSource to play an OOSound. + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + +@class OOSound; + + +@interface OOSoundChannel: NSObject +{ + OOSoundChannel *_next; + id _delegate; + OOSound *_sound; + uint16_t _ID; + BOOL _playing; +} + +- (id) initWithID:(uint16_t)ID; + +- (void) update; + +- (void) setDelegate:(id)delegate; +- (uint32_t) ID; + +// Unretained pointer used to maintain simple stack +- (OOSoundChannel *) next; +- (void) setNext:(OOSoundChannel *)next; + +- (BOOL) playSound:(OOSound *)sound looped:(BOOL)loop; +- (void)stop; + +- (OOSound *)sound; + +@end + + +@interface NSObject(OOSoundChannelDelegate) + +- (void)channel:(OOSoundChannel *)inChannel didFinishPlayingSound:(OOSound *)inSound; + +@end diff --git a/src/SDL/OOSDLSoundChannel.m b/src/SDL/OOSDLSoundChannel.m new file mode 100644 index 00000000..5367d4ca --- /dev/null +++ b/src/SDL/OOSDLSoundChannel.m @@ -0,0 +1,147 @@ +/* + +OOSDLSoundChannel.m + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOSDLSoundInternal.h" + + +@interface OOSoundChannel (Private) + +- (void) hasStopped; + +@end + + +@implementation OOSoundChannel + +- (id) initWithID:(uint16_t)ID +{ + if ((self = [super init])) + { + _ID = ID; + } + return self; +} + + +- (void) update +{ + // Check if we've reached the end of a sound. + if (_sound != nil && !Mix_Playing(_ID)) [self hasStopped]; +} + + +- (void) setDelegate:(id)delegate +{ + _delegate = delegate; +} + + +- (uint32_t)ID +{ + return _ID; +} + + +- (OOSoundChannel *) next +{ + return _next; +} + + +- (void) setNext:(OOSoundChannel *)next +{ + _next = next; +} + + +- (BOOL) playSound:(OOSound *)sound looped:(BOOL)loop +{ + if (sound == nil) return NO; + + if (_sound != nil) [self stop]; + + Mix_Chunk *chunk = [sound chunk]; + if (chunk != NULL) + { + Mix_PlayChannel(_ID, chunk, loop ? -1 : 0); + _sound = [sound retain]; + return YES; + } + return NO; +} + + +- (void) stop +{ + if (_sound != nil) + { + Mix_HaltChannel(_ID); + [self hasStopped]; + } +} + + +- (void) hasStopped +{ + OOSound *sound = _sound; + _sound = nil; + + if (nil != _delegate && [_delegate respondsToSelector:@selector(channel:didFinishPlayingSound:)]) + { + [_delegate channel:self didFinishPlayingSound:sound]; + } + [sound release]; +} + + +- (OOSound *)sound +{ + return _sound; +} + +@end diff --git a/src/SDL/OOSDLSoundInternal.h b/src/SDL/OOSDLSoundInternal.h new file mode 100644 index 00000000..15b97769 --- /dev/null +++ b/src/SDL/OOSDLSoundInternal.h @@ -0,0 +1,53 @@ +/* + +OOSDLSoundInternal.h + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + + +#import "OOSDLSound.h" +#import "OOSDLConcreteSound.h" +#import "OOSDLSoundMixer.h" +#import "OOSDLSoundChannel.h" diff --git a/src/SDL/OOSDLSoundMixer.h b/src/SDL/OOSDLSoundMixer.h new file mode 100644 index 00000000..18034067 --- /dev/null +++ b/src/SDL/OOSDLSoundMixer.h @@ -0,0 +1,82 @@ +/* + +OOSDLSoundMixer.h + +Class responsible for managing and mixing sound channels. This class is an +implementation detail. Do not use it directly; use an OOSoundSource to play an +OOSound. + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import + +@class OOSoundChannel; + + +enum +{ + kMixerGeneralChannels = 32 +}; + + +@interface OOSoundMixer: NSObject +{ + OOSoundChannel *_channels[kMixerGeneralChannels]; + OOSoundChannel *_freeList; + + uint32_t _maxChannels; + uint32_t _playMask; + +} + +// Singleton accessor ++ (id) sharedMixer; + +- (void) update; + +- (OOSoundChannel *) popChannel; +- (void) pushChannel:(OOSoundChannel *)channel; + +@end diff --git a/src/SDL/OOSDLSoundMixer.m b/src/SDL/OOSDLSoundMixer.m new file mode 100644 index 00000000..dabcad58 --- /dev/null +++ b/src/SDL/OOSDLSoundMixer.m @@ -0,0 +1,183 @@ +/* + +OOSDLSoundMixer.m + +OOSDLSound - SDL_mixer sound implementation for Oolite. +Copyright (C) 2006-2008 Jens Ayton + + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + + +This file may also be distributed under the MIT/X11 license: + +Copyright (C) 2006-2008 Jens Ayton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +*/ + +#import "OOCocoa.h" +#import "OOSDLSoundInternal.h" + + +static OOSoundMixer *sSingleton = nil; + + +@implementation OOSoundMixer + ++ (id) sharedMixer +{ + if (nil == sSingleton) + { + [[self alloc] init]; + } + return sSingleton; +} + + +- (id) init +{ + BOOL OK = YES; + uint32_t idx = 0, count = kMixerGeneralChannels; + OOSoundChannel *channel; + + if (!(self = [super init])) return nil; + if (![OOSound setUp]) OK = NO; + + if (OK) + { + // Allocate channels + do + { + channel = [[OOSoundChannel alloc] initWithID:count]; + if (nil != channel) + { + _channels[idx++] = channel; + [self pushChannel:channel]; + } + } while (--count); + } + + if (!OK) + { + [super release]; + self = nil; + } + else + { + sSingleton = self; + } + + return sSingleton; +} + + +- (void) update +{ + uint32_t i; + for (i = 0; i < kMixerGeneralChannels; ++i) + { + [_channels[i] update]; + } +} + + +- (OOSoundChannel *) popChannel +{ + OOSoundChannel *channel = _freeList; + _freeList = [channel next]; + [channel setNext:nil]; + + return channel; +} + + +- (void) pushChannel:(OOSoundChannel *)channel +{ + assert(channel != nil); + + [channel setNext:_freeList]; + _freeList = channel; +} + +@end + + +@implementation OOSoundMixer (Singleton) + +/* Canonical singleton boilerplate. + See Cocoa Fundamentals Guide: Creating a Singleton Instance. + See also +sharedMixer above. + + NOTE: assumes single-threaded access. +*/ + ++ (id)allocWithZone:(NSZone *)inZone +{ + if (sSingleton == nil) + { + sSingleton = [super allocWithZone:inZone]; + return sSingleton; + } + return nil; +} + + +- (id)copyWithZone:(NSZone *)inZone +{ + return self; +} + + +- (id)retain +{ + return self; +} + + +- (OOUInteger)retainCount +{ + return UINT_MAX; +} + + +- (void)release +{} + + +- (id)autorelease +{ + return self; +} + +@end diff --git a/src/SDL/SDLMusic.h b/src/SDL/SDLMusic.h new file mode 100644 index 00000000..9f150c3a --- /dev/null +++ b/src/SDL/SDLMusic.h @@ -0,0 +1,64 @@ +/* + +SDLMusic.h + +SDLSound - SDL sound implementation for Oolite. +Copyright (C) 2005 David Taylor + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#import +#include "SDL.h" +#include "SDL_mixer.h" + + +@interface OOMusic : NSObject +{ + // The SDL_mixer music structure encapsulated by an instance of OOMusic. + Mix_Music *music; + BOOL paused; + NSString *name; +} + +// Initialise the OOMusic instance from the contents of "filepath" +- (id) initWithContentsOfFile:(NSString*) filepath; + +// Pause the music if this instance is currently playing +- (void) pause; +- (BOOL) isPaused; + +// Returns YES if this instance is playing, otherwise NO. +- (BOOL) isPlaying; + +// Start playing this instance of OOMusic, stopping any other instance +// currently playing. +- (void) playLooped:(BOOL)loop; + +// Stop the music if this instance is currently playing. +- (void) stop; + +// Resume the music if this instance was paused. Has no effect if a different +// instance was paused. +- (void) resume; + +// Rewind the music if this instance is the current instance. +- (void) goToBeginning; + +- (NSString *) name; + +@end diff --git a/src/SDL/SDLMusic.m b/src/SDL/SDLMusic.m new file mode 100644 index 00000000..3e1153a1 --- /dev/null +++ b/src/SDL/SDLMusic.m @@ -0,0 +1,236 @@ +/* + +SDLSound.h + +SDLSound - SDL sound implementation for Oolite. +Copyright (C) 2005 David Taylor + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + +#include "SDLMusic.h" +#import "OOLogging.h" +#include "OOSDLSound.h" + +#define kOOLogUnconvertedNSLog @"unclassified.SDLMusic" + +/* + * This is used by instances of OOMusic to check if they are currently playing + * or not. + * + * Because SDL_mixer only plays one piece of music at a time (which is + * reasonable), the SDL implementation of OOMusic works on the basis that + * only one instance of it is "current" at any given time. + * + * If an instance is not the current instance, the only method that will + * work is play. Calling play on an instance that is not the current + * instance will make it the current instance, stopping any music that is + * already playing. + */ +OOMusic* current; + +/* + * This function is called by SDL_mixer whenever a piece of music finishes + * playing. + * + * This resets the pointer the currently playing OOMusic object to signify + * that no music is playing. + */ +void musicFinished() +{ + current = nil; +} + + +@interface OOMusic (Private) + +- (BOOL) playWithCount:(int)count; + +@end + + +@implementation OOMusic + +/* + * Initialise an OOMusic instance from the given file. + * + * The OOMusic instance is deallocated and nil is returned if the contents + * of the file cannot be loaded by SDL_mixer. + */ +- (id) initWithContentsOfFile:(NSString*) filepath +{ + if( ![OOSound isSoundOK] ) return nil; + + [super init]; + + music = Mix_LoadMUS([filepath cString]); + if (!music) + { + NSLog(@"Mix_LoadMUS(\"%@\"): %s\n", filepath, Mix_GetError()); + [super dealloc]; + return nil; + } + + name = [[filepath lastPathComponent] copy]; + + return self; +} + +/* + * Deallocate resources used by this instance of OOMusic. + */ +- (void) dealloc +{ + if (current == self) + Mix_HaltMusic(); + + if (music) + Mix_FreeMusic(music); + + [name autorelease]; + + [super dealloc]; +} + +- (void) pause +{ + // Only pause the music if this instance is the one being played. + if (current == self) + { + Mix_PauseMusic(); + paused=YES; + } +} + +- (BOOL) isPaused +{ + return paused; +} + +/* + * Returns YES is this instance of OOMusic is currently playing. + */ +- (BOOL) isPlaying +{ + // If the "current OOMusic instance" pointer points to self, then this + // instance is playing. + if (current == self) + return YES; + + return NO; +} + +- (BOOL) playWithCount:(int)count +{ + int rc; + paused=NO; + + // Self is already playing, so do nothing. + if (current == self) + return YES; + + // Another instance is playing so stop it. + if (current != 0) + [current stop]; + + // There is a potential race condition here because the + // SDL_mixer "music stopped" callback sets current to NULL, and this + // method sets current to self. + // + // If the callback is executed from a thread created by SDL_mixer then + // that might not happen before the thread executing this code has + // already made self current. + // + // One way of avoiding this is to wait for current to be equal to + // NULL. When that happens we know the callback has been called. + while (current != 0) + ; + + rc = Mix_PlayMusic(music, count); + if (rc < 0) + { + NSLog(@"Mix_PlayMusic error: %s", Mix_GetError()); + return NO; + } + + // This is done on every call to play simply because there didn't seem to + // be another way to do it without having either a class init method or + // doing it outside this class altogether. Both of those solutions seems + // messy and this should not have a big performance hit. + Mix_HookMusicFinished(musicFinished); + current = self; + + return YES; +} + +/* + * Play the music represented by this OOMusic instance. This will replace any + * music currently playing. + * + * If this instance is already playing, there is no effect. + * + * Returns YES for success, or NO if there was a problem playing the music. + */ +- (void) playLooped:(BOOL)loop +{ + [self playWithCount:loop ? -1 : 1]; +} + +/* + * Stop playing this piece of music. + * + * Returns YES if this music was being played, or NO if this music was not + * being played. + */ +- (void) stop +{ + // Only stop the music if this instance is the one being played. + if (current == self) + { + Mix_HaltMusic(); + // Flag that there is no tune currently playing + current = 0; + } +} + +- (void) resume +{ + // Only resume playing the music if this instance is the one being played. + if (current == self) + { + Mix_ResumeMusic(); + paused=NO; + } +} + +/* + * Go back to the beginning of the music. + */ +- (void) goToBeginning +{ + // Only rewind the music if this instance is the one being played. + if (current == self) + Mix_RewindMusic(); +} + + +- (NSString *) name +{ + return name; +} + +@end diff --git a/src/SDL/main.m b/src/SDL/main.m new file mode 100644 index 00000000..7be2a192 --- /dev/null +++ b/src/SDL/main.m @@ -0,0 +1,135 @@ +/* + +main.m + +Oolite +Copyright (C) 2004-2007 Giles C Williams and contributors + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation; either version 2 +of the License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, write to the Free Software +Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +MA 02110-1301, USA. + +*/ + + +#ifdef GNUSTEP +#import +#import + +#import "GameController.h" +#import "OOLoggingExtended.h" + +#if OOLITE_WINDOWS +#import +#include +#endif +GameController* controller; +#endif + + +#ifndef NDEBUG +uint32_t gDebugFlags = 0; +#endif + + +#ifdef OOLITE_SDL_MAC +#define main SDL_main +#endif + + +int main(int argc, char *argv[]) +{ +#ifdef GNUSTEP + int i; + +#if OOLITE_WINDOWS + + // Detect current working directory and set up GNUstep environment variables + #define MAX_PATH_LEN 256 + char currentWorkingDir[MAX_PATH_LEN]; + char envVarString[2 * MAX_PATH_LEN]; + GetCurrentDirectory(MAX_PATH_LEN - 1, currentWorkingDir); + + #define SETENVVAR(var, value) do {\ + sprintf(envVarString, "%s=%s", (var), (value));\ + SDL_putenv (envVarString);\ + } while (0); + + SETENVVAR("GNUSTEP_PATH_HANDLING", "windows"); + SETENVVAR("GNUSTEP_SYSTEM_ROOT", currentWorkingDir); + SETENVVAR("GNUSTEP_LOCAL_ROOT", currentWorkingDir); + SETENVVAR("GNUSTEP_NETWORK_ROOT", currentWorkingDir); + SETENVVAR("GNUSTEP_USERS_ROOT", currentWorkingDir); + SETENVVAR("HOMEPATH", currentWorkingDir); + + /* Windows amibtiously starts apps with the C library locale set to the + system locale rather than the "C" locale as per spec. Fixing here so + numbers don't behave strangely. + */ + setlocale(LC_ALL, "C"); +#endif + + // Need this because we're not using the default run loop's autorelease + // pool. + NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; + OOLoggingInit(); + + // dajt: allocate and set the NSApplication delegate manually because not + // using NIB to do this + controller = [[GameController alloc] init]; + + // Release anything allocated during the controller initialisation that + // is no longer required. + [pool release]; + + for (i = 1; i < argc; i++) + { + // The commented out lines below do not seem to do anything, at least on Windows. + // The -fullscreen argument processing has been implemented in the loadFullscreenSettings + // method, inside src/SDL/MyOpenGLView.m. + /* + -------- Begin commented out section -------- + if (strcmp("-fullscreen", argv[i]) == 0) + [controller setFullScreenMode: YES]; + --------- End commented out section -------- + */ + + if (strcmp("-load", argv[i]) == 0) + { + i++; + if (i < argc) + [controller setPlayerFileToLoad: [NSString stringWithCString: argv[i]]]; + } + } + + // Call applicationDidFinishLaunching because NSApp is not running in + // GNUstep port. + [controller applicationDidFinishLaunching: nil]; +#endif + + // never reached + return 0; +} + + +#if OOLITE_SDL_MAC + +@implementation NSWindow (SDLBugWorkaround) + +- (void)release +{} + +@end + +#endif diff --git a/src/SDL/makebinpkg.sh b/src/SDL/makebinpkg.sh new file mode 100755 index 00000000..5516689a --- /dev/null +++ b/src/SDL/makebinpkg.sh @@ -0,0 +1,20 @@ +#!/bin/sh +if [ ! $1 ] +then + echo "Usage: makebinpkg.sh " + echo + exit +fi + +rm -rf $HOME/oolite-installer +mkdir -p $HOME/oolite-installer +tar cvf ~/oolite-installer/oolite-app.tar oolite.app --exclude .svn + +cd SelfContainedInstaller +cp install oolite-update.src README.TXT PLAYING.TXT FAQ.TXT LICENSE.TXT oolite.src ~/oolite-installer +tar cvf ~/oolite-installer/oolite-deps.tar oolite-deps --exclude .svn +echo $1 >~/oolite-installer/release.txt + +cd ~/ +tar zcvf oolite-$1.x86.tar.gz oolite-installer + diff --git a/src/SDL/makedist.sh b/src/SDL/makedist.sh new file mode 100755 index 00000000..d7a29cc9 --- /dev/null +++ b/src/SDL/makedist.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# +# This shell script simply makes a tarball of the current SVN extract, +# making sure only source and required files to build are put into the +# tarball (i.e. no .svn dirs, no oolite.app dir etc.) +# +if [ ! $1 ] +then + echo "Usage: makedist.sh " + exit +fi + +rm -rf ~/oolite-src +rm -rf ~/oolite-data +mkdir ~/oolite-src +mkdir ~/oolite-data +cp *.m *.c *.h GNU* README.TXT PORTING.TXT ~/oolite-src + +# we don't cp -r because then we'd get all the .svn stuff +# so do it a dir at a time and copy specific files +SRCDIR=Resources +RESDIR=~/oolite-data/Resources +mkdir $RESDIR +mkdir $RESDIR/AIs +cp $SRCDIR/AIs/*.plist $RESDIR/AIs +mkdir $RESDIR/Config +cp $SRCDIR/Config/*.plist $RESDIR/Config +mkdir $RESDIR/Images +cp $SRCDIR/Images/*.png $RESDIR/Images +mkdir $RESDIR/Models +cp $SRCDIR/Models/*.dat $RESDIR/Models +mkdir $RESDIR/Music +cp $SRCDIR/Music/*.ogg $RESDIR/Music +mkdir $RESDIR/Sounds +cp $SRCDIR/Sounds/*.ogg $RESDIR/Sounds +mkdir $RESDIR/Textures +cp $SRCDIR/Textures/*.png $RESDIR/Textures +cp $SRCDIR/Info-Oolite.plist $RESDIR + +cd ~/ +tar zcvf oolite-src-$1.tar.gz oolite-src +tar zcvf oolite-data-$1.tar.gz oolite-data +

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